implement more enemies, balance some programming languages and enemies to be less annoying

This commit is contained in:
2026-07-10 10:21:47 +02:00
parent e3ea42c93e
commit 18297fac9a
33 changed files with 1025 additions and 136 deletions
+119 -2
View File
@@ -12,7 +12,8 @@ namespace KernelPanic.UI
{
/// <summary>
/// Runtime-built GameScene combat UI. Intent icons use a compact terminal vocabulary:
/// ! attack/status attack, # defend, + buff, * special, ? hidden, ~ reviving, : split, @ countdown.
/// ! attack/status attack/segfault, # defend, + buff, * special, ? hidden/rootkit,
/// ~ reviving/race link, : split, @ countdown, . orphan.
/// </summary>
[RequireComponent(typeof(UIDocument))]
[RequireComponent(typeof(RunManager))]
@@ -276,9 +277,12 @@ namespace KernelPanic.UI
VisualElement enemiesPanel = CreatePanel("enemy processes", "intent telemetry", 438);
main.Add(enemiesPanel);
ScrollView enemyScroll = new(ScrollViewMode.Vertical);
enemyScroll.AddToClassList("enemy-scroll");
enemiesPanel.Add(enemyScroll);
_enemyRow = new();
_enemyRow.AddToClassList("enemy-row");
enemiesPanel.Add(_enemyRow);
enemyScroll.Add(_enemyRow);
VisualElement bottom = new();
bottom.AddToClassList("bottom-layout");
@@ -445,6 +449,9 @@ namespace KernelPanic.UI
{
_enemyRow.Clear();
_combatantElements.Clear();
int enemyCount = _combatManager.Enemies.Count;
bool compactEnemies = enemyCount >= 5;
_enemyRow.EnableInClassList("enemy-row-compact", compactEnemies);
if (_combatManager.PlayerState != null)
{
_combatantElements[_combatManager.PlayerState] = _playerPanel;
@@ -457,6 +464,7 @@ namespace KernelPanic.UI
Button card = new(() => _combatManager.SelectEnemy(index));
card.text = string.Empty;
card.AddToClassList("enemy-card");
card.EnableInClassList("enemy-card-compact", compactEnemies);
bool highlighted = _combatManager.PendingTargetCard != null || _combatManager.SelectedEnemyIndex == index;
if (highlighted)
{
@@ -473,6 +481,11 @@ namespace KernelPanic.UI
card.Add(marker);
}
if (enemy.HasSpecialSignal)
{
card.Add(EnemySignalPanel(enemy, _combatManager.Enemies));
}
_combatantElements[enemy.State] = card;
DetectBeneficialResourceFeedback(enemy.State, card);
ApplyStatusStateClasses(card, enemy.State);
@@ -823,6 +836,12 @@ namespace KernelPanic.UI
button.AddToClassList("hand-card-unaffordable");
}
if (card.IsBroken || card.IsLocked)
{
button.AddToClassList("hand-card-unaffordable");
button.SetEnabled(false);
}
PopulateCardFace(button, card);
return button;
}
@@ -853,6 +872,13 @@ namespace KernelPanic.UI
top.Add(name);
target.Add(top);
if (card.IsBroken || card.IsLocked)
{
Label state = new(card.IsBroken ? "segv corrupt" : "locked");
state.AddToClassList("status-label");
target.Add(state);
}
VisualElement tags = new();
tags.AddToClassList("tag-row");
tags.Add(Tag(card.Definition.Language.ToString(), LanguageClass(card.Definition.Language)));
@@ -885,6 +911,97 @@ namespace KernelPanic.UI
return panel;
}
private static VisualElement EnemySignalPanel(EnemyInstance enemy, IReadOnlyList<EnemyInstance> enemies)
{
VisualElement row = new();
row.AddToClassList("status-row");
if (enemy.SpawnedFromDeath)
{
row.Add(Tag(". orphaned", "track-tag"));
}
if (enemy.HasEliteSignal)
{
row.Add(Tag("! elite", "intent-attack"));
}
if (enemy.HasSplitSignal)
{
string text = CountLivingArchetype(enemies, enemy.ArchetypeId) >= EnemyArchetypeCatalog.ForkBombTotalCap
? ": split capped"
: ": split next";
row.Add(Tag(text, "track-tag"));
}
if (enemy.HasSegfaultSignal)
{
row.Add(Tag($"! corrupt in {enemy.CountdownRemaining}", "track-tag"));
}
if (enemy.HasRaceSignal)
{
string text = HasLivingPair(enemies, enemy) ? "~ linked random" : "~ unlinked";
row.Add(Tag(text, "track-tag"));
}
if (enemy.HasRootkitSignal)
{
string text = HasOtherLivingEnemy(enemies, enemy) ? "? masked reduced" : "? exposed";
row.Add(Tag(text, "track-tag"));
}
return row;
}
private static int CountLivingArchetype(IReadOnlyList<EnemyInstance> enemies, string archetypeId)
{
int count = 0;
for (int i = 0; i < enemies.Count; i++)
{
EnemyInstance enemy = enemies[i];
if (enemy.ArchetypeId == archetypeId && !enemy.State.IsDefeated)
{
count++;
}
}
return count;
}
private static bool HasLivingPair(IReadOnlyList<EnemyInstance> enemies, EnemyInstance target)
{
if (target == null || target.PairId < 0)
{
return false;
}
for (int i = 0; i < enemies.Count; i++)
{
EnemyInstance enemy = enemies[i];
if (enemy != target && enemy.PairId == target.PairId && !enemy.State.IsDefeated)
{
return true;
}
}
return false;
}
private static bool HasOtherLivingEnemy(IReadOnlyList<EnemyInstance> enemies, EnemyInstance target)
{
for (int i = 0; i < enemies.Count; i++)
{
EnemyInstance enemy = enemies[i];
if (enemy != target && !enemy.State.IsDefeated)
{
return true;
}
}
return false;
}
private static VisualElement MeterBlock(string name, int current, int max, MeterTone tone)
{
float ratio = max <= 0 ? 0f : Mathf.Clamp01((float)current / max);