changed most TODOs to TODONEs

This commit is contained in:
2026-07-08 10:49:05 +02:00
parent ac09d67677
commit 50135e2e38
11 changed files with 666 additions and 48 deletions
+281 -4
View File
@@ -25,6 +25,7 @@ namespace KernelPanic.Combat
private readonly List<EnemyInstance> enemies = new();
private readonly HashSet<string> loggedEffectTodos = new();
private readonly List<CardDefinition> generatedCardPool = new();
private readonly List<DelayedCombatEffect> delayedEffects = new();
private RunManager runManager;
private RunConfig runConfig;
private HandController handController;
@@ -37,6 +38,11 @@ namespace KernelPanic.Combat
private bool ubuntuEmptyHandRefillUsed;
private int fedoraCardsDiscountedThisTurn;
private int fedoraCrashChance = 10;
private int cardsPlayedThisTurn;
private int javaCardsPlayedThisCombat;
private int rawhideBonusCharges;
private int queuedRepeatCharges;
private int nextTurnCycleBonus;
public TurnPhase CurrentPhase => currentPhase;
public RunConfig RunConfig => runConfig;
@@ -145,8 +151,14 @@ namespace KernelPanic.Combat
return false;
}
if (card.Definition.IsToken)
{
Log($"{GetCardName(card)} is unplayable");
return false;
}
bool fedoraBonus = CanApplyFedoraBonus();
int cost = GetCardCost(card);
int cost = GetEffectiveCardCost(card);
if (fedoraBonus)
{
cost = Mathf.Max(0, cost - 1);
@@ -173,6 +185,13 @@ namespace KernelPanic.Combat
playerState.Cycles -= cost;
ResolutionTrack track = card.Definition.ResolutionTrack;
card.MarkPlayedThisTurn(cardsPlayedThisTurn == 0);
cardsPlayedThisTurn++;
if (card.Definition.Language == Language.Java)
{
javaCardsPlayedThisCombat++;
}
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track));
LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card));
@@ -180,7 +199,13 @@ namespace KernelPanic.Combat
if (fedoraBonus)
{
fedoraCardsDiscountedThisTurn++;
if (RandomRoll.RollRange(1, 100, new RollContext(playerState)) <= fedoraCrashChance)
bool rawhideChargeUsed = rawhideBonusCharges > 0;
if (rawhideChargeUsed)
{
rawhideBonusCharges--;
}
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(playerState)) <= fedoraCrashChance)
{
fedoraCrashChance = 10;
if (IsDistro("fedora") && runConfig.DistroVersion >= 4)
@@ -198,11 +223,13 @@ namespace KernelPanic.Combat
fedoraCrashChance = Mathf.Min(90, fedoraCrashChance + 10);
playerState.DamageMultiplierPercent = runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed);
}
switch (track)
{
case ResolutionTrack.InterpreterQueue:
card.MarkQueued();
interpreterQueue.Enqueue(card);
break;
case ResolutionTrack.LazyStack:
@@ -235,6 +262,17 @@ namespace KernelPanic.Combat
return Mathf.Max(0, card.Definition.CycleCost + card.PermanentCostDelta + card.TemporaryCostDelta);
}
public int GetEffectiveCardCost(CardInstance card)
{
int cost = GetCardCost(card);
if (card?.Definition != null && string.Equals(card.Definition.Id, "fedora_dnf_update", StringComparison.OrdinalIgnoreCase))
{
cost -= javaCardsPlayedThisCombat;
}
return Mathf.Max(0, cost);
}
public bool RequiresTarget(CardInstance card)
{
return card?.Definition != null && CardEffectFactory.RequiresSingleTarget(card.Definition) && LivingEnemyStates().Count > 0;
@@ -258,6 +296,86 @@ namespace KernelPanic.Combat
}
}
public void AddNextTurnCycleBonus(int amount)
{
nextTurnCycleBonus += Mathf.Max(0, amount);
}
public void AddQueuedRepeatCharges(int amount)
{
queuedRepeatCharges += Mathf.Max(0, amount);
if (amount > 0)
{
ReportEffectResult($"next {amount} queued card(s) resolve twice");
}
}
public int ConsumeQueuedResolutionCount(CardInstance card)
{
if (queuedRepeatCharges <= 0 || card == null)
{
return 1;
}
queuedRepeatCharges--;
return 2;
}
public void GrantRawhideBonus(int charges)
{
rawhideBonusCharges += Mathf.Max(0, charges);
}
public void AddIncomingAttackHalfCharges(int charges)
{
if (playerState == null)
{
return;
}
playerState.IncomingAttackHalfCharges += Mathf.Max(0, charges);
ReportEffectResult($"next {charges} enemy attack(s) halved");
}
public void ScheduleUpdateManagerRepeat(int damageAmount)
{
delayedEffects.Add(DelayedCombatEffect.UpdateManagerRepeat(damageAmount, 2));
ReportEffectResult("update manager scheduled repeat");
}
public void ScheduleTimeshiftRestore(int uptime)
{
delayedEffects.Add(DelayedCombatEffect.TimeshiftRestore(uptime, 2));
ReportEffectResult($"timeshift snapshot recorded {uptime} uptime");
}
public bool TryCreateGeneratedCardById(string id, out CardInstance generatedCard)
{
generatedCard = null;
if (string.IsNullOrWhiteSpace(id))
{
return false;
}
IReadOnlyList<CardDefinition> source = generatedCardPool.Count > 0 ? generatedCardPool : runConfig?.StartingDeck;
if (source == null)
{
return false;
}
for (int i = 0; i < source.Count; i++)
{
CardDefinition definition = source[i];
if (definition != null && string.Equals(definition.Id, id, StringComparison.OrdinalIgnoreCase))
{
generatedCard = new CardInstance(definition);
return true;
}
}
return false;
}
public void SetGeneratedCardPool(IEnumerable<CardDefinition> cardPool)
{
generatedCardPool.Clear();
@@ -268,7 +386,7 @@ namespace KernelPanic.Combat
foreach (CardDefinition card in cardPool)
{
if (card != null && !card.IsToken && !card.IsRunOnly)
if (card != null)
{
generatedCardPool.Add(card);
}
@@ -394,6 +512,12 @@ namespace KernelPanic.Combat
ubuntuEmptyHandRefillUsed = false;
fedoraCrashChance = 10;
fedoraCardsDiscountedThisTurn = 0;
cardsPlayedThisTurn = 0;
javaCardsPlayedThisCombat = 0;
rawhideBonusCharges = 0;
queuedRepeatCharges = 0;
nextTurnCycleBonus = 0;
delayedEffects.Clear();
RandomRoll.Seed(runConfig.RunSeed);
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
ApplyVersionState(playerState);
@@ -418,6 +542,7 @@ namespace KernelPanic.Combat
playerState.MaxCycles = runManager.EffectiveMaxCycles();
playerState.Cycles = 0;
playerState.Shield = 0;
playerState.IncomingAttackHalfCharges = 0;
playerState.IsDefeated = false;
playerState.MutableStatuses.Clear();
ApplyVersionState(playerState);
@@ -456,6 +581,13 @@ namespace KernelPanic.Combat
playerState.Cycles = playerState.MaxCycles;
fedoraCardsDiscountedThisTurn = 0;
cardsPlayedThisTurn = 0;
if (nextTurnCycleBonus > 0)
{
playerState.Cycles += nextTurnCycleBonus;
Log($"deferred cycle grant: +{nextTurnCycleBonus}");
nextTurnCycleBonus = 0;
}
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
if (CheckLoss())
{
@@ -481,7 +613,16 @@ namespace KernelPanic.Combat
while (interpreterQueue.TryDequeue(out CardInstance card))
{
LogPlayIntent(card);
ExecuteCardEffects(card, ResolveQueuedTargets(card));
int resolutionCount = ConsumeQueuedResolutionCount(card);
for (int repeatIndex = 0; repeatIndex < resolutionCount; repeatIndex++)
{
ExecuteCardEffects(card, ResolveQueuedTargets(card));
if (CheckWinOrLoss())
{
return;
}
}
ResolveCardRiders(card);
deckController.Discard(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
@@ -541,6 +682,12 @@ namespace KernelPanic.Combat
return;
}
ResolveDelayedEndOfTurnEffects();
if (CheckWinOrLoss())
{
return;
}
RemoveDefeatedEnemies();
CheckWinOrLoss();
StateChanged?.Invoke();
@@ -638,6 +785,11 @@ namespace KernelPanic.Combat
}
private bool CanApplyFedoraBonus()
{
return CanApplyFedoraPassiveBonus() || rawhideBonusCharges > 0;
}
private bool CanApplyFedoraPassiveBonus()
{
if (!IsDistro("fedora") || playerState == null)
{
@@ -648,6 +800,27 @@ namespace KernelPanic.Combat
return fedoraCardsDiscountedThisTurn < limit;
}
private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed)
{
if (!IsDistro("fedora") || runConfig.DistroVersion < 3 || card == null || !card.WasFirstCardThisTurn)
{
return;
}
int growth = rawhideChargeUsed ? 2 : 1;
if (card.ApplyCombatMagnitudeBonus(growth))
{
Log($"rawhide growth: {GetCardName(card)} +{growth} effect");
}
}
private static bool IsCrashImmune(CardInstance card)
{
string id = card?.Definition?.Id;
return string.Equals(id, "fedora_cargo_build", StringComparison.OrdinalIgnoreCase)
|| string.Equals(id, "fedora_selinux", StringComparison.OrdinalIgnoreCase);
}
private void ApplyVersionState(CombatantState state)
{
if (state == null)
@@ -856,6 +1029,7 @@ namespace KernelPanic.Combat
switch (intent.Kind)
{
case EnemyIntentKind.Attack:
value = ApplyIncomingAttackMitigation(value);
DamageResult attackResult = damagePipeline.DealDamage(new DamageRequest(enemy.State, playerState, value, intent.DamageType, intent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, attackResult.FinalAmount));
break;
@@ -879,6 +1053,79 @@ namespace KernelPanic.Combat
}
}
private int ApplyIncomingAttackMitigation(int value)
{
if (playerState == null || playerState.IncomingAttackHalfCharges <= 0)
{
return value;
}
playerState.IncomingAttackHalfCharges--;
int mitigated = Mathf.CeilToInt(Mathf.Max(0, value) * 0.5f);
Log($"SELinux enforcing halved attack {value}->{mitigated}");
return mitigated;
}
private void ResolveDelayedEndOfTurnEffects()
{
for (int i = delayedEffects.Count - 1; i >= 0; i--)
{
DelayedCombatEffect effect = delayedEffects[i];
effect.GarbageCollectionsRemaining--;
if (effect.GarbageCollectionsRemaining > 0)
{
delayedEffects[i] = effect;
continue;
}
delayedEffects.RemoveAt(i);
switch (effect.Kind)
{
case DelayedCombatEffectKind.UpdateManagerRepeat:
DealToAllEnemies(effect.Amount, Language.Python, "update manager repeat");
break;
case DelayedCombatEffectKind.TimeshiftRestore:
RestoreTimeshift(effect.Amount);
break;
}
}
}
private void DealToAllEnemies(int amount, Language language, string label)
{
List<CombatantState> targets = LivingEnemyStates();
for (int i = 0; i < targets.Count; i++)
{
damagePipeline.DealDamage(new DamageRequest(playerState, targets[i], amount, language, false, false));
}
Log($"{label}: dealt {amount} to all enemies");
RemoveDefeatedEnemies();
}
private void RestoreTimeshift(int snapshot)
{
if (playerState == null || playerState.IsDefeated || playerState.CurrentUptime >= snapshot)
{
return;
}
int target = snapshot;
if (IsDistro("mint") && runConfig.DistroVersion >= 3)
{
target = Mathf.Min(playerState.MaxUptime, snapshot + 2);
}
if (IsDistro("mint") && runConfig.DistroVersion >= 5)
{
target = playerState.MaxUptime;
}
int restored = target - playerState.CurrentUptime;
playerState.CurrentUptime = target;
Log($"timeshift restored {restored} uptime");
}
private bool CheckWinOrLoss()
{
return CheckLoss() || CheckWin();
@@ -944,5 +1191,35 @@ namespace KernelPanic.Combat
}
private bool IsCombatPaused => awaitingWaveContinue || runLost;
private enum DelayedCombatEffectKind
{
UpdateManagerRepeat,
TimeshiftRestore
}
private struct DelayedCombatEffect
{
private DelayedCombatEffect(DelayedCombatEffectKind kind, int amount, int garbageCollectionsRemaining)
{
Kind = kind;
Amount = amount;
GarbageCollectionsRemaining = garbageCollectionsRemaining;
}
public DelayedCombatEffectKind Kind { get; }
public int Amount { get; }
public int GarbageCollectionsRemaining { get; set; }
public static DelayedCombatEffect UpdateManagerRepeat(int amount, int garbageCollectionsRemaining)
{
return new DelayedCombatEffect(DelayedCombatEffectKind.UpdateManagerRepeat, amount, garbageCollectionsRemaining);
}
public static DelayedCombatEffect TimeshiftRestore(int uptime, int garbageCollectionsRemaining)
{
return new DelayedCombatEffect(DelayedCombatEffectKind.TimeshiftRestore, uptime, garbageCollectionsRemaining);
}
}
}
}