using KernelPanic.Core;
using KernelPanic.Data;
using KernelPanic.Run;
using KernelPanic.Meta;
using KernelPanic.UI;
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace KernelPanic.Combat
{
///
/// Owns the combat turn phase state machine and coordinates combat lifecycle transitions.
///
public sealed class CombatManager : MonoBehaviour
{
[SerializeField] private TurnPhase currentPhase = TurnPhase.Boot;
private readonly DeckController _deckController = new();
private readonly InterpreterQueue _interpreterQueue = new();
private readonly LazyStack _lazyStack = new();
private readonly NativeTrack _nativeTrack = new();
private readonly StatusEffectController _statusEffects = new();
private readonly DamagePipeline _damagePipeline = new();
private readonly List _enemies = new();
private readonly HashSet _loggedEffectTodos = new();
private readonly List _generatedCardPool = new();
private readonly List _delayedEffects = new();
private readonly HashSet _enemiesPendingExit = new();
private readonly Queue _enemyExitQueue = new();
private readonly Dictionary> _pendingDeathSpawns = new();
private readonly HashSet _completedDeathVisuals = new();
private RunManager _runManager;
private RunConfig _runConfig;
private HandController _handController;
private CombatantState _playerState;
private int _selectedEnemyIndex = -1;
private CardInstance _pendingTargetCard;
private bool _awaitingWaveContinue;
private bool _runLost;
private bool _skipNextAllocateDraw;
private bool _ubuntuEmptyHandRefillUsed;
private int _fedoraCardsDiscountedThisTurn;
private float _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private int _cardsPlayedThisTurn;
private int _cardsPlayedThisWave;
private int _cCardsPlayedThisTurn;
private int _turnNumberThisWave;
private bool _packageShieldBonusTriggeredThisTurn;
private bool _packageTimeshiftTriggeredThisWave;
private bool _packageInterpreterQueueShieldTriggeredThisWave;
private bool _packageNativeDamageTriggeredThisWave;
private int _javaCardsPlayedThisCombat;
private int _javaCardsDiscountThisTurn;
private int _rawhideBonusCharges;
private int _queuedRepeatCharges;
private int _nextTurnCycleBonus;
private int _nextRacePairId = 1;
private int _activeKernelRamPenalty;
private int _deathSpawnSequencesActive;
private bool _enemyExitQueueDraining;
private EnemyInstance _enemyExitAwaitingVisual;
private Coroutine _phaseCoroutine;
public TurnPhase CurrentPhase => currentPhase;
public RunConfig RunConfig => _runConfig;
public DeckController DeckController => _deckController;
public HandController HandController => _handController;
public CombatantState PlayerState => _playerState;
public InterpreterQueue InterpreterQueue => _interpreterQueue;
public LazyStack LazyStack => _lazyStack;
public NativeTrack NativeTrack => _nativeTrack;
public DamagePipeline DamagePipeline => _damagePipeline;
public float FedoraCrashChance => _fedoraCrashChance;
public IReadOnlyList Enemies => _enemies;
public int SelectedEnemyIndex => _selectedEnemyIndex;
public CardInstance PendingTargetCard => _pendingTargetCard;
public bool AwaitingWaveContinue => _awaitingWaveContinue;
public bool RunLost => _runLost;
public int CurrentWaveNumber => _runManager == null ? 1 : _runManager.CurrentWaveNumber;
public int ActiveKernelRamPenalty => _activeKernelRamPenalty;
public bool CanSurrenderRun => currentPhase == TurnPhase.Execute
&& !_awaitingWaveContinue
&& !_runLost
&& !VisualResolutionPending
&& _playerState != null
&& !_playerState.IsDefeated;
public event Action StateChanged;
public event Action CombatLog;
private void Awake()
{
_runManager = GetComponent();
_damagePipeline.ResistanceResolver = ResolveDamageResistance;
}
private void OnEnable()
{
GameEvents.DamageDealt += HandlePackageDamageDealt;
GameEvents.CombatantDeathVisualCompleted += HandleCombatantDeathVisualCompleted;
}
private void OnDisable()
{
GameEvents.DamageDealt -= HandlePackageDamageDealt;
GameEvents.CombatantDeathVisualCompleted -= HandleCombatantDeathVisualCompleted;
}
public void StartCombat()
{
if (_runConfig == null)
{
Debug.LogWarning("CombatManager.StartCombat called without a RunConfig.");
return;
}
SetPhase(TurnPhase.Boot);
}
public void StartCombat(RunConfig config)
{
_runConfig = config;
StartCombat();
}
public void AdvancePhase()
{
if (IsCombatPaused)
{
return;
}
TurnPhase nextPhase = GetNextPhase(currentPhase);
SetPhase(nextPhase);
}
public void EndPlayerTurn()
{
if (currentPhase == TurnPhase.Execute && !IsCombatPaused)
{
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
SetPhase(TurnPhase.Interpret);
}
}
public void SurrenderRun()
{
if (!CanSurrenderRun)
{
return;
}
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
_damagePipeline.DefeatCombatant(_playerState, null, Language.C);
CheckLoss();
StateChanged?.Invoke();
}
public void QueueDefeatedCombatantForRemoval(CombatantState combatant)
{
EnemyInstance enemy = EnemyForState(combatant);
if (enemy == null)
{
return;
}
QueueDefeatedEnemyForRemoval(enemy);
}
public void ContinueToNextWave()
{
if (!_awaitingWaveContinue || _runLost)
{
return;
}
_awaitingWaveContinue = false;
DecayJavaWarmupForNextWave();
StartCoroutine(ContinueToNextWaveSequenced());
}
private IEnumerator ContinueToNextWaveSequenced()
{
yield return StartWaveSequenced(preservePlayerUptime: true);
SetPhase(TurnPhase.Allocate);
}
public void SelectEnemy(int index)
{
if (IsCombatPaused)
{
return;
}
if (index < 0 || index >= _enemies.Count || _enemies[index].State.IsDefeated)
{
_selectedEnemyIndex = -1;
StateChanged?.Invoke();
return;
}
_selectedEnemyIndex = index;
if (_pendingTargetCard != null)
{
CardInstance card = _pendingTargetCard;
_pendingTargetCard = null;
PlayCard(card);
return;
}
StateChanged?.Invoke();
}
public bool PlayCard(CardInstance card)
{
if (currentPhase != TurnPhase.Execute || IsCombatPaused || card == null || _handController == null || _playerState == null)
{
return false;
}
if (card.Definition.IsToken)
{
Log($"{GetCardName(card)} is unplayable");
return false;
}
if (card.IsBroken)
{
Log($"{GetCardName(card)} is corrupted");
return false;
}
if (card.IsLocked)
{
Log($"{GetCardName(card)} is locked; pay {EnemyArchetypeCatalog.DrmUnlockCycleCost} Cycle to unlock");
return false;
}
bool fedoraBonus = CanApplyFedoraBonus(card);
int cost = GetEffectiveCardCost(card);
if (fedoraBonus)
{
cost = Mathf.Max(0, cost - 1);
}
if (_playerState.Cycles < cost)
{
Log($"not enough cycles for {GetCardName(card)}");
return false;
}
if (RequiresTarget(card) && _selectedEnemyIndex < 0)
{
_pendingTargetCard = card;
Log($"select target for {GetCardName(card)}");
StateChanged?.Invoke();
return false;
}
if (!_handController.Remove(card))
{
return false;
}
_playerState.Cycles -= cost;
ResolutionTrack track = card.Definition.ResolutionTrack;
card.MarkPlayedThisTurn(_cardsPlayedThisTurn == 0);
_cardsPlayedThisTurn++;
_cardsPlayedThisWave++;
if (IsDistro("arch"))
{
_playerState.ArchBtwStacks++;
}
_playerState.HasPreviousPlayedCardLanguage = _playerState.HasLastPlayedCardLanguage;
if (_playerState.HasLastPlayedCardLanguage)
{
_playerState.PreviousPlayedCardLanguage = _playerState.LastPlayedCardLanguage;
}
_playerState.LastPlayedCardLanguage = card.Definition.Language;
_playerState.HasLastPlayedCardLanguage = true;
if (card.Definition.Language == Language.Java)
{
_javaCardsPlayedThisCombat++;
}
GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track));
ApplyTelemetryCardPlayed();
LogPlayIntent(card);
card.SetTargetSnapshot(CaptureTargets(card));
if (fedoraBonus)
{
bool firstFedoraBonusThisTurn = _fedoraCardsDiscountedThisTurn == 0;
_fedoraCardsDiscountedThisTurn++;
bool rawhideChargeUsed = _rawhideBonusCharges > 0;
if (rawhideChargeUsed)
{
_rawhideBonusCharges--;
}
float fedoraCrashChanceBeforeRoll = _fedoraCrashChance;
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 1000, new RollContext(_playerState)) <= Mathf.RoundToInt(_fedoraCrashChance * 10f))
{
_fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
if (IsDistro("fedora") && _runConfig.DistroVersion >= 4)
{
_playerState.Cycles += 1;
}
_deckController.Discard(card);
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
Log($"{GetCardName(card)} crashed under bleeding edge");
GameEvents.RaiseFedoraBleedingEdgeTriggered(new FedoraBleedingEdgeEvent(card, true, firstFedoraBonusThisTurn, 0, fedoraCrashChanceBeforeRoll, _fedoraCrashChance));
StateChanged?.Invoke();
return true;
}
_fedoraCrashChance = Mathf.Min(
CombatTuning.FedoraBleedingEdgeMaxCrashChance,
_fedoraCrashChance + CombatTuning.FedoraBleedingEdgeCrashChanceStep);
card.MarkFedoraNonCrashBonus();
_playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed, firstFedoraBonusThisTurn);
GameEvents.RaiseFedoraBleedingEdgeTriggered(new FedoraBleedingEdgeEvent(card, false, firstFedoraBonusThisTurn, _playerState.DamageMultiplierPercent, fedoraCrashChanceBeforeRoll, _fedoraCrashChance));
}
if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect))
{
_playerState.CurrentCardDamageMultiplierPercent = Mathf.Max(100, cPackageEffect.Amount);
}
switch (track)
{
case ResolutionTrack.InterpreterQueue:
card.MarkQueued();
_interpreterQueue.Enqueue(card);
break;
case ResolutionTrack.LazyStack:
_lazyStack.Enqueue(card);
break;
default:
ExecuteCardEffects(card, ResolveTargets(card));
ResolveCardRiders(card);
_deckController.Discard(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.Native));
_selectedEnemyIndex = -1;
break;
}
_playerState.DamageMultiplierPercent = 100;
_playerState.CurrentCardDamageMultiplierPercent = 100;
if (card.Definition.Language == Language.C)
{
_cCardsPlayedThisTurn++;
}
ApplyPackageCardPlayedEffects(card);
_pendingTargetCard = null;
TryApplyUbuntuEmptyHandRefill();
StateChanged?.Invoke();
return true;
}
public static int GetCardCost(CardInstance card)
{
if (card?.Definition == null)
{
return 0;
}
return Mathf.Max(0, card.Definition.CycleCost + card.PermanentCostDelta + card.TemporaryCostDelta);
}
public int GetEffectiveCardCost(CardInstance card)
{
int cost = GetCardCost(card);
if (card?.Definition != null && card.Definition.Language == Language.Java)
{
cost -= _javaCardsPlayedThisCombat + _javaCardsDiscountThisTurn;
}
cost = ApplyPackageCostRules(card, cost);
return Mathf.Max(0, cost);
}
public bool RequiresTarget(CardInstance card)
{
return card?.Definition != null && CardEffectFactory.RequiresSingleTarget(card.Definition) && LivingEnemyStates().Count > 0;
}
public void LogEffectTodo(CardInstance card, string message)
{
string cardId = card?.Definition == null ? string.Empty : card.Definition.Id;
string key = $"{cardId}:{message}";
if (_loggedEffectTodos.Add(key))
{
Log(message);
}
}
public bool TryCorruptRandomHandCard(string sourceLabel)
{
CardInstance card = RandomHandCard(cardInstance => cardInstance != null && !cardInstance.IsBroken);
if (card == null)
{
Log($"{sourceLabel}: no card available to corrupt");
return false;
}
card.IsBroken = true;
Log($"{sourceLabel}: corrupted {GetCardName(card)}");
StateChanged?.Invoke();
return true;
}
public bool TryUnlockCard(CardInstance card)
{
if (currentPhase != TurnPhase.Execute || card == null || !card.IsLocked || _playerState == null)
{
return false;
}
int cost = EnemyArchetypeCatalog.DrmUnlockCycleCost;
if (_playerState.Cycles < cost)
{
Log($"not enough cycles to unlock {GetCardName(card)}");
return false;
}
_playerState.Cycles -= cost;
card.IsLocked = false;
Log($"unlocked {GetCardName(card)}");
StateChanged?.Invoke();
return true;
}
public void ReportEffectResult(string message)
{
if (!string.IsNullOrWhiteSpace(message))
{
Log(message);
}
}
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 void AddJavaCostDiscountThisTurn(int amount)
{
int safeAmount = Mathf.Max(0, amount);
if (safeAmount <= 0)
{
return;
}
_javaCardsDiscountThisTurn += safeAmount;
ReportEffectResult($"Java cards cost -{safeAmount} this turn");
}
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 HandleCardExhausted(CardInstance card)
{
if (card == null || !HasPackageEffect(PackageEffectKind.ExhaustShield, out PackageEffectData effect))
{
return;
}
GrantPlayerShield(Mathf.Max(0, effect.Amount), "logrotate");
}
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 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 cardPool)
{
_generatedCardPool.Clear();
if (cardPool == null)
{
return;
}
foreach (CardDefinition card in cardPool)
{
if (card != null)
{
_generatedCardPool.Add(card);
}
}
}
public bool TryCreateGeneratedCard(Language language, Rarity rarity, out CardInstance generatedCard)
{
generatedCard = null;
List pool = new();
HashSet seenIds = new();
IReadOnlyList 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 || definition.IsToken || definition.IsRunOnly || definition.Language != language || definition.Rarity != rarity)
{
continue;
}
if (seenIds.Add(definition.Id))
{
pool.Add(definition);
}
}
if (pool.Count == 0)
{
return false;
}
int index = RandomRoll.RollRange(0, pool.Count - 1, new RollContext(_playerState));
CardDefinition selected = pool[index];
generatedCard = new CardInstance(selected)
{
TemporaryCostDelta = -selected.CycleCost
};
return true;
}
public void DrawCards(int count, string label)
{
if (count <= 0)
{
return;
}
StartCoroutine(DrawCardsToHandSequenced(count, label));
}
private void SetPhase(TurnPhase nextPhase)
{
if (IsCombatPaused && nextPhase != TurnPhase.Boot)
{
return;
}
TurnPhase previousPhase = currentPhase;
currentPhase = nextPhase;
GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase));
if (_phaseCoroutine != null)
{
StopCoroutine(_phaseCoroutine);
}
_phaseCoroutine = StartCoroutine(EnterPhase(nextPhase));
}
private static TurnPhase GetNextPhase(TurnPhase phase)
{
return phase switch
{
TurnPhase.Boot => TurnPhase.Allocate,
TurnPhase.Allocate => TurnPhase.Execute,
TurnPhase.Execute => TurnPhase.Interpret,
TurnPhase.Interpret => TurnPhase.EnemyProcess,
TurnPhase.EnemyProcess => TurnPhase.GarbageCollection,
_ => TurnPhase.Allocate
};
}
private IEnumerator EnterPhase(TurnPhase phase)
{
switch (phase)
{
case TurnPhase.Boot:
yield return BootCombatSequenced();
break;
case TurnPhase.Allocate:
yield return AllocateTurnSequenced();
break;
case TurnPhase.Execute:
StateChanged?.Invoke();
yield break;
case TurnPhase.Interpret:
yield return InterpretQueueSequenced();
break;
case TurnPhase.EnemyProcess:
yield return ProcessEnemiesSequenced();
break;
case TurnPhase.GarbageCollection:
GarbageCollect();
break;
}
if (!IsCombatPaused)
{
yield return Wait(CombatTuning.PhaseTransitionDelaySeconds);
AdvancePhase();
}
}
private static IEnumerator Wait(float seconds)
{
if (!UIPreferences.ReducedMotion && seconds > 0f)
{
yield return new WaitForSeconds(seconds);
}
}
private IEnumerator BootCombatSequenced()
{
_runLost = false;
_awaitingWaveContinue = false;
_loggedEffectTodos.Clear();
_ubuntuEmptyHandRefillUsed = false;
_fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
_fedoraCardsDiscountedThisTurn = 0;
_cardsPlayedThisTurn = 0;
_cardsPlayedThisWave = 0;
_cCardsPlayedThisTurn = 0;
_turnNumberThisWave = 0;
_packageShieldBonusTriggeredThisTurn = false;
_packageTimeshiftTriggeredThisWave = false;
_packageInterpreterQueueShieldTriggeredThisWave = false;
_packageNativeDamageTriggeredThisWave = false;
_javaCardsPlayedThisCombat = 0;
_javaCardsDiscountThisTurn = 0;
_rawhideBonusCharges = 0;
_queuedRepeatCharges = 0;
_nextTurnCycleBonus = 0;
_delayedEffects.Clear();
_enemiesPendingExit.Clear();
_enemyExitQueue.Clear();
_pendingDeathSpawns.Clear();
_completedDeathVisuals.Clear();
_deathSpawnSequencesActive = 0;
_enemyExitQueueDraining = false;
_enemyExitAwaitingVisual = null;
RandomRoll.Seed(_runConfig.RunSeed);
_playerState = new CombatantState(_runManager.EffectiveMaxUptime(), _runManager.EffectiveRam(), _runManager.EffectiveMaxCycles());
ApplyVersionState(_playerState);
ApplyPackageState(_playerState);
yield return StartWaveSequenced(preservePlayerUptime: false);
Log($"booted {_runConfig.Distro.DisplayName} with {_runManager.RunDeck.Count} cards");
StateChanged?.Invoke();
}
private IEnumerator StartWaveSequenced(bool preservePlayerUptime)
{
int carriedUptime = _playerState == null ? 0 : _playerState.CurrentUptime;
if (!preservePlayerUptime || _playerState == null)
{
_playerState = new CombatantState(_runManager.EffectiveMaxUptime(), _runManager.EffectiveRam(), _runManager.EffectiveMaxCycles());
ApplyVersionState(_playerState);
ApplyPackageState(_playerState);
}
else
{
_playerState.MaxUptime = _runManager.EffectiveMaxUptime();
_playerState.CurrentUptime = Mathf.Clamp(carriedUptime, 1, _playerState.MaxUptime);
_playerState.Ram = _runManager.EffectiveRam();
_playerState.MaxCycles = _runManager.EffectiveMaxCycles();
_playerState.Cycles = 0;
_playerState.Shield = 0;
_playerState.IncomingAttackHalfCharges = 0;
_playerState.IsDefeated = false;
_playerState.MutableStatuses.Clear();
ApplyVersionState(_playerState);
ApplyPackageState(_playerState);
}
_handController = new HandController(_playerState.Ram);
_selectedEnemyIndex = -1;
_pendingTargetCard = null;
_cardsPlayedThisWave = 0;
_turnNumberThisWave = 0;
_nextRacePairId = 1;
_packageShieldBonusTriggeredThisTurn = false;
_packageTimeshiftTriggeredThisWave = false;
ResetArchWaveState();
List startingCards = new();
for (int i = 0; i < _runManager.RunDeck.Count; i++)
{
CardInstance card = _runManager.RunDeck[i];
if (card != null)
{
card.ResetCombatState();
startingCards.Add(card);
}
}
_deckController.Initialize(startingCards);
_interpreterQueue.Clear();
_enemies.Clear();
_enemiesPendingExit.Clear();
_enemyExitQueue.Clear();
_pendingDeathSpawns.Clear();
_completedDeathVisuals.Clear();
_deathSpawnSequencesActive = 0;
_enemyExitQueueDraining = false;
_enemyExitAwaitingVisual = null;
SpawnStructuralEnemies();
RecalculateKernelRamPressure();
PickEnemyIntents();
StateChanged?.Invoke();
yield return DrawOpeningHandSequenced();
ApplyPackageWaveStartEffects();
_skipNextAllocateDraw = true;
}
private IEnumerator AllocateTurnSequenced()
{
if (CheckLoss())
{
yield break;
}
RevivePendingEnemies();
UnlockHandCards("license check expired");
_playerState.Cycles = _playerState.MaxCycles;
_fedoraCardsDiscountedThisTurn = 0;
_cardsPlayedThisTurn = 0;
_cCardsPlayedThisTurn = 0;
_turnNumberThisWave++;
_packageShieldBonusTriggeredThisTurn = false;
_javaCardsDiscountThisTurn = 0;
ApplyPackageTurnStartEffects();
if (_nextTurnCycleBonus > 0)
{
_playerState.Cycles += _nextTurnCycleBonus;
Log($"deferred cycle grant: +{_nextTurnCycleBonus}");
_nextTurnCycleBonus = 0;
}
_statusEffects.Tick(_playerState, StatusTickTiming.StartOfTurn, _playerState, _damagePipeline);
StateChanged?.Invoke();
if (CheckLoss())
{
yield break;
}
TryApplyUbuntuAptUpdate();
if (_skipNextAllocateDraw)
{
_skipNextAllocateDraw = false;
}
else
{
yield return DrawForTurnSequenced();
}
StateChanged?.Invoke();
}
private IEnumerator InterpretQueueSequenced()
{
while (_interpreterQueue.TryDequeue(out CardInstance card))
{
LogPlayIntent(card);
int resolutionCount = ConsumeQueuedResolutionCount(card);
for (int repeatIndex = 0; repeatIndex < resolutionCount; repeatIndex++)
{
ExecuteCardEffects(card, ResolveQueuedTargets(card));
if (CheckWinOrLoss())
{
yield break;
}
}
ResolveCardRiders(card);
_deckController.Discard(card);
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
StateChanged?.Invoke();
if (CheckWinOrLoss())
{
yield break;
}
yield return Wait(CombatTuning.QueueCardResolveDelaySeconds);
}
CheckWinOrLoss();
StateChanged?.Invoke();
}
private IEnumerator ProcessEnemiesSequenced()
{
List actors = BuildEnemyActionOrder();
for (int i = 0; i < actors.Count; i++)
{
EnemyInstance enemy = actors[i];
if (enemy.State.IsDefeated)
{
continue;
}
_statusEffects.Tick(enemy.State, StatusTickTiming.StartOfTurn, _playerState, _damagePipeline);
RemoveDefeatedEnemies();
if (CheckWinOrLoss())
{
yield break;
}
if (enemy.State.IsDefeated)
{
continue;
}
Log($"{enemy.Name} executes {enemy.DisplayIntent.DisplayText}");
GameEvents.RaiseEnemyWouldAct(new EnemyWouldActEvent(enemy));
StateChanged?.Invoke();
yield return Wait(CombatTuning.EnemyTelegraphDelaySeconds);
ExecuteEnemyIntent(enemy, _enemies.IndexOf(enemy));
ResolvePassiveEnemyAfterAction(enemy);
if (!enemy.State.IsDefeated)
{
enemy.MarkTurnSurvived();
}
GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent));
RemoveDefeatedEnemies();
StateChanged?.Invoke();
if (CheckWinOrLoss())
{
yield break;
}
yield return Wait(CombatTuning.EnemyActionDelaySeconds);
}
PickEnemyIntents();
ResetEnemyTurnLethalMarkers();
StateChanged?.Invoke();
}
private void GarbageCollect()
{
_selectedEnemyIndex = -1;
_pendingTargetCard = null;
_statusEffects.Tick(_playerState, StatusTickTiming.EndOfTurn, _playerState, _damagePipeline);
if (CheckLoss())
{
return;
}
ResolveDelayedEndOfTurnEffects();
if (CheckWinOrLoss())
{
return;
}
int archBtwStacksBeforeReset = _playerState?.ArchBtwStacks ?? 0;
ResetArchBtwAtEndOfTurn();
if (IsDistro("arch") && archBtwStacksBeforeReset > 0)
{
GameEvents.RaiseArchBtwTurnEnded(new ArchBtwTurnEndedEvent(archBtwStacksBeforeReset, _playerState.ArchBtwStacks > 0));
}
RemoveDefeatedEnemies();
CheckWinOrLoss();
StateChanged?.Invoke();
}
private IEnumerator DrawOpeningHandSequenced()
{
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, _handController == null ? _playerState.Ram : _handController.RamCapacity);
yield return DrawCardsToHandSequenced(openingDraw, "opening hand");
}
private IEnumerator DrawForTurnSequenced()
{
int requested = CombatTuning.DrawPerTurn;
if (CombatTuning.MinimumHandFloor > 0 && _handController.Cards.Count < CombatTuning.MinimumHandFloor && _deckController.AvailableToDrawCount > 0)
{
requested = Mathf.Max(requested, CombatTuning.MinimumHandFloor - _handController.Cards.Count);
}
yield return DrawCardsToHandSequenced(requested, "turn draw");
}
private IEnumerator DrawCardsToHandSequenced(int requestedCount, string label)
{
if (_handController == null || _playerState == null || requestedCount <= 0)
{
yield break;
}
int room = _handController.RemainingRam;
if (room <= 0)
{
Log($"{label}: hand full");
yield break;
}
IReadOnlyList drawn = _deckController.Draw(requestedCount);
int added = 0;
for (int i = 0; i < drawn.Count; i++)
{
if (_handController.Add(drawn[i]))
{
added++;
StateChanged?.Invoke();
yield return Wait(CombatTuning.CardDrawDelaySeconds);
}
else
{
_deckController.AddToDrawPile(drawn[i], shuffle: false);
}
}
if (added == 0 && _deckController.AvailableToDrawCount == 0)
{
Log($"{label}: no cards to draw");
}
else if (added < requestedCount)
{
Log($"{label}: drew {added} (hand full)");
}
else
{
Log($"{label}: drew {added}");
}
}
private void TryApplyUbuntuAptUpdate()
{
if (!IsDistro("ubuntu") || _handController == null || _playerState == null || _handController.RemainingRam <= 0)
{
return;
}
int lookCount = _runConfig.DistroVersion >= 2 ? 3 : 2;
if (!_deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card, out IReadOnlyList peeked, out bool wasTie))
{
return;
}
if (_runConfig.DistroVersion >= 4)
{
card.TemporaryCostDelta -= 1;
}
if (_handController.Add(card))
{
Log($"apt update: staged {GetCardName(card)} from top {lookCount}");
GameEvents.RaiseUbuntuAptUpdatePeeked(new UbuntuAptUpdatePeekedEvent(peeked, card, wasTie, lookCount));
}
else
{
_deckController.AddToDrawPile(card, shuffle: false);
}
}
private void TryApplyUbuntuEmptyHandRefill()
{
if (!IsDistro("ubuntu") || _runConfig.DistroVersion < 5 || _ubuntuEmptyHandRefillUsed || _handController == null || _handController.Cards.Count > 0)
{
return;
}
_ubuntuEmptyHandRefillUsed = true;
StartCoroutine(DrawCardsToHandSequenced(_playerState.Ram, "ubuntu 24.04 refill"));
}
private bool CanApplyFedoraBonus(CardInstance card)
{
return CardEffectFactory.CanReceiveBleedingEdgeDamageBonus(card?.Definition)
&& CardCanBenefitFromBleedingEdgeNow(card)
&& (CanApplyFedoraPassiveBonus() || _rawhideBonusCharges > 0);
}
private bool CardCanBenefitFromBleedingEdgeNow(CardInstance card)
{
string id = card?.Definition?.Id;
if (string.IsNullOrWhiteSpace(id))
{
return false;
}
if (id == "arch_consult_wiki")
{
return _handController == null || !HasBrokenHandCard();
}
if (id == "fedora_dnf_autoremove")
{
return HasJunkHandCard(card);
}
return true;
}
private bool HasBrokenHandCard()
{
if (_handController == null)
{
return false;
}
for (int i = 0; i < _handController.Cards.Count; i++)
{
if (_handController.Cards[i]?.IsBroken == true)
{
return true;
}
}
return false;
}
private bool HasJunkHandCard(CardInstance playedCard)
{
if (_handController == null)
{
return false;
}
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance card = _handController.Cards[i];
if (card != playedCard && IsJunkForDnfAutoremove(card))
{
return true;
}
}
return false;
}
private static bool IsJunkForDnfAutoremove(CardInstance card)
{
if (card == null)
{
return false;
}
string id = card.Definition?.Id ?? string.Empty;
string name = card.Definition?.DisplayName ?? string.Empty;
return card.Definition != null && card.Definition.IsToken
|| card.IsBroken
|| id.IndexOf("junk", StringComparison.OrdinalIgnoreCase) >= 0
|| id.IndexOf("nop", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("junk", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("NOP", StringComparison.OrdinalIgnoreCase) >= 0
|| name.IndexOf("broken", StringComparison.OrdinalIgnoreCase) >= 0;
}
private void DecayJavaWarmupForNextWave()
{
if (_javaCardsPlayedThisCombat <= 0)
{
return;
}
_javaCardsPlayedThisCombat = Mathf.Max(0, _javaCardsPlayedThisCombat - 1);
Log($"Java JIT cooled: discount now -{_javaCardsPlayedThisCombat}");
}
private bool CanApplyFedoraPassiveBonus()
{
if (!IsDistro("fedora") || _playerState == null)
{
return false;
}
int limit = _runConfig.DistroVersion >= 5 || HasOnDistroPackageEffect(PackageEffectKind.DnfFedoraPassive, out PackageEffectData dnfEffect) && dnfEffect.EnableFedoraSecondCardPassive ? 2 : 1;
return _fedoraCardsDiscountedThisTurn < limit;
}
private void ApplyFedoraGrowth(CardInstance card, bool rawhideChargeUsed, bool firstFedoraBonusThisTurn)
{
if (!IsDistro("fedora") || _runConfig.DistroVersion < 3 || card == null || !firstFedoraBonusThisTurn)
{
return;
}
int growth = rawhideChargeUsed ? 2 : 1;
if (card.ApplyCombatMagnitudeBonus(growth))
{
Log($"rawhide growth: {GetCardName(card)} +{growth} effect");
}
}
private int ApplyPackageCostRules(CardInstance card, int cost)
{
if (card == null || cost <= 0)
{
return cost;
}
IReadOnlyList packages = _runConfig?.EquippedPackages;
if (packages == null)
{
return cost;
}
int nextTurnCard = _cardsPlayedThisTurn + 1;
int nextWaveCard = _cardsPlayedThisWave + 1;
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null)
{
continue;
}
PackageEffectData effect = package.EffectFor(_runConfig?.Distro?.Id);
switch (effect.Kind)
{
case PackageEffectKind.FirstCardEachWaveCostReduction:
if (nextWaveCard == 1)
{
cost -= Mathf.Max(0, effect.Amount);
}
break;
case PackageEffectKind.EveryNthCardEachWaveFree:
if (effect.Threshold > 0 && nextWaveCard % effect.Threshold == 0)
{
cost = 0;
}
break;
case PackageEffectKind.FirstCardsEachTurnCostReduction:
if (nextTurnCard <= Mathf.Max(0, effect.Threshold))
{
cost -= Mathf.Max(0, effect.Amount);
}
break;
}
}
return cost;
}
private void ApplyPackageState(CombatantState state)
{
if (state == null)
{
return;
}
state.JavaScriptFlatDamageBonus = 0;
IReadOnlyList packages = _runConfig?.EquippedPackages;
if (packages == null)
{
return;
}
for (int i = 0; i < packages.Count; i++)
{
PackageEffectData effect = packages[i] == null ? default : packages[i].EffectFor(_runConfig?.Distro?.Id);
if (effect.Kind == PackageEffectKind.JavaScriptFlatDamage)
{
state.JavaScriptFlatDamageBonus += Mathf.Max(0, effect.Amount);
}
}
}
private void ApplyPackageWaveStartEffects()
{
IReadOnlyList packages = _runConfig?.EquippedPackages;
if (packages == null)
{
return;
}
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null)
{
continue;
}
PackageEffectData effect = package.EffectFor(_runConfig?.Distro?.Id);
switch (effect.Kind)
{
case PackageEffectKind.WaveDraw:
StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), package.Definition.DisplayName));
break;
case PackageEffectKind.WaveStartShield:
GrantPlayerShield(Mathf.Max(0, effect.Amount), package.Definition.DisplayName);
break;
case PackageEffectKind.FirstTurnEachWaveShield:
if (_turnNumberThisWave == 0)
{
GrantPlayerShield(Mathf.Max(0, effect.Amount), package.Definition.DisplayName);
}
break;
case PackageEffectKind.FirstTurnFirstWaveDraw:
if (CurrentWaveNumber == 1 && _turnNumberThisWave == 0)
{
StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), package.Definition.DisplayName));
}
break;
case PackageEffectKind.WaveGenerateBasicCard:
GeneratePackageLanguageCard(package.Definition.DisplayName);
break;
}
}
}
private void ApplyPackageTurnStartEffects()
{
if (HasPackageEffect(PackageEffectKind.EveryNthTurnShield, out PackageEffectData effect)
&& effect.Threshold > 0
&& _turnNumberThisWave % effect.Threshold == 0)
{
GrantPlayerShield(Mathf.Max(0, effect.Amount), "cron");
}
if (HasPackageEffect(PackageEffectKind.EveryNthTurnDraw, out effect)
&& effect.Threshold > 0
&& _turnNumberThisWave % effect.Threshold == 0)
{
StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), "udev"));
}
if (_turnNumberThisWave == 1
&& HasPackageEffect(PackageEffectKind.FirstTurnEachWaveCycle, out effect)
&& effect.Amount > 0)
{
_playerState.Cycles += effect.Amount;
Log($"apt-fast: gained {effect.Amount} Cycle");
}
if (HasPackageEffect(PackageEffectKind.StartTurnNoDebuffShield, out effect)
&& !HasHarmfulStatus(_playerState))
{
GrantPlayerShield(Mathf.Max(0, effect.Amount), "auditd");
}
if (HasPackageEffect(PackageEffectKind.TurnStartGenerateLanguageCard, out effect))
{
GenerateAurPackageCard(effect);
}
}
private void ApplyPackageCardPlayedEffects(CardInstance card)
{
if (card == null || _playerState == null)
{
return;
}
if (HasPackageEffect(PackageEffectKind.ThirdCardEachTurnGenerate, out PackageEffectData aptEffect)
&& _cardsPlayedThisTurn == Mathf.Max(1, aptEffect.Threshold))
{
GeneratePackageLanguageCard("apt");
if (aptEffect.RefundCycle)
{
_playerState.Cycles += 1;
Log("apt refunded 1 Cycle");
}
}
if (HasPackageEffect(PackageEffectKind.DnfFedoraPassive, out PackageEffectData dnfEffect)
&& _cardsPlayedThisTurn == 1
&& dnfEffect.Amount > 0)
{
_playerState.FlatEffectBonus += dnfEffect.Amount;
Log($"DNF wave buff +{dnfEffect.Amount} damage");
}
if (!_packageInterpreterQueueShieldTriggeredThisWave
&& card.Definition.ResolutionTrack == ResolutionTrack.InterpreterQueue
&& HasPackageEffect(PackageEffectKind.FirstInterpreterQueueCardEachWaveShield, out PackageEffectData queueEffect))
{
_packageInterpreterQueueShieldTriggeredThisWave = true;
GrantPlayerShield(Mathf.Max(0, queueEffect.Amount), "dbus");
}
if (!_packageNativeDamageTriggeredThisWave
&& card.Definition.ResolutionTrack == ResolutionTrack.Native
&& HasPackageEffect(PackageEffectKind.FirstNativeCardEachWaveFlatDamage, out PackageEffectData nativeEffect)
&& nativeEffect.Amount > 0)
{
_packageNativeDamageTriggeredThisWave = true;
_playerState.FlatEffectBonus += nativeEffect.Amount;
Log($"make: first Native card +{nativeEffect.Amount} damage");
}
if (HasPackageEffect(PackageEffectKind.EveryNthCardEachWaveCycle, out PackageEffectData cycleEffect)
&& cycleEffect.Threshold > 0
&& _cardsPlayedThisWave % cycleEffect.Threshold == 0
&& cycleEffect.Amount > 0)
{
_playerState.Cycles += cycleEffect.Amount;
Log($"systemd-timer: gained {cycleEffect.Amount} Cycle");
}
}
public void GrantPlayerShield(int amount, string label)
{
if (_playerState == null || amount <= 0)
{
return;
}
int total = amount;
if (!_packageShieldBonusTriggeredThisTurn
&& HasPackageEffect(PackageEffectKind.FirstShieldEachTurnBonus, out PackageEffectData effect))
{
int bonus = Mathf.Max(0, effect.Amount);
if (bonus > 0)
{
total += bonus;
_packageShieldBonusTriggeredThisTurn = true;
Log($"SELinux shield +{bonus}");
}
}
_playerState.Shield += total;
if (!string.IsNullOrWhiteSpace(label))
{
Log($"{label}: gained {total} Shield");
}
}
private void GeneratePackageLanguageCard(string label)
{
if (_handController == null || _handController.RemainingRam <= 0)
{
return;
}
Language language = RandomRoll.RollRange(0, 1, new RollContext(_playerState)) == 0
? _runConfig.PrimaryLanguage
: _runConfig.SecondaryLanguage;
if (!TryCreateGeneratedCard(language, Rarity.Common, out CardInstance generated))
{
Log($"{label}: TODO card-generation pool unavailable");
return;
}
if (_handController.Add(generated))
{
Log($"{label}: generated {GetCardName(generated)} at 0c");
StateChanged?.Invoke();
}
}
private void GenerateAurPackageCard(PackageEffectData effect)
{
if (_handController == null || _handController.RemainingRam <= 0)
{
return;
}
int maxRarity = Mathf.Clamp(effect.Threshold, 0, (int)Rarity.Legendary);
List pool = new();
HashSet seenIds = new(StringComparer.OrdinalIgnoreCase);
IReadOnlyList source = _generatedCardPool.Count > 0 ? _generatedCardPool : _runConfig?.StartingDeck;
if (source == null)
{
Log("AUR: TODO card-generation pool unavailable");
return;
}
for (int i = 0; i < source.Count; i++)
{
CardDefinition definition = source[i];
if (definition == null
|| definition.IsToken
|| definition.IsRunOnly
|| (int)definition.Rarity > maxRarity
|| definition.Language != Language.C && definition.Language != Language.Rust)
{
continue;
}
if (seenIds.Add(definition.Id))
{
pool.Add(definition);
}
}
if (pool.Count == 0)
{
Log("AUR: TODO card-generation pool unavailable");
return;
}
int index = RandomRoll.RollRange(0, pool.Count - 1, new RollContext(_playerState));
CardDefinition selected = pool[index];
CardInstance generated = new(selected)
{
TemporaryCostDelta = -Mathf.Clamp(effect.Amount, 0, selected.CycleCost)
};
if (_handController.Add(generated))
{
string discount = effect.Amount > 0 ? " at -1c" : string.Empty;
Log($"AUR: generated {GetCardName(generated)}{discount}");
StateChanged?.Invoke();
}
}
private void HandlePackageDamageDealt(DamageDealtEvent payload)
{
HandleEnemyDamageDealt(payload);
if (payload.Target == _playerState && _playerState != null && _playerState.ArchRollingReleaseRecoveredThisHit)
{
_playerState.ArchRollingReleaseRecoveredThisHit = false;
string recovery = "rolling release: recovered at 1 Uptime";
if (_playerState.ArchRollingReleaseShieldOnSave > 0 || _playerState.ArchRollingReleaseCyclesOnSave > 0)
{
recovery += $", +{_playerState.ArchRollingReleaseShieldOnSave} Shield, +{_playerState.ArchRollingReleaseCyclesOnSave} Cycle";
}
Log(recovery);
StateChanged?.Invoke();
}
if (payload.Target != _playerState
|| payload.UptimeDamage <= 0
|| _packageTimeshiftTriggeredThisWave
|| !HasPackageEffect(PackageEffectKind.WaveThresholdRestore, out PackageEffectData effect))
{
return;
}
int thresholdPercent = Mathf.Clamp(effect.Threshold, 1, 100);
int restoreTarget = Mathf.CeilToInt(_playerState.MaxUptime * (thresholdPercent / 100f));
if (_playerState.CurrentUptime >= restoreTarget)
{
return;
}
_packageTimeshiftTriggeredThisWave = true;
_playerState.CurrentUptime = restoreTarget;
if (effect.CleanseDebuffs)
{
_statusEffects.CleanseHarmful(_playerState);
}
Log($"Timeshift restored uptime to {thresholdPercent}%");
StateChanged?.Invoke();
}
private void HandleEnemyDamageDealt(DamageDealtEvent payload)
{
EnemyInstance enemy = FindEnemy(payload.Target);
if (enemy == null)
{
return;
}
if (payload.Amount > 0)
{
enemy.MarkDamaged();
}
if (!enemy.HasBehavior(EnemyBehaviorFlags.Revive)
|| payload.UptimeDamage <= 0
|| enemy.State.CurrentUptime > 0)
{
return;
}
int uptimeBeforeHit = enemy.State.CurrentUptime + payload.UptimeDamage;
int reapThreshold = Mathf.CeilToInt(enemy.MaxUptime * (EnemyArchetypeCatalog.ZombieReapThresholdPercent / 100f));
bool reaped = enemy.HasRevived || enemy.LethalHitThisTurn || uptimeBeforeHit <= reapThreshold;
enemy.MarkLethalHit();
if (reaped)
{
enemy.MarkReaped();
Log($"{enemy.Name} reaped");
return;
}
enemy.MarkPendingRevive();
Log($"{enemy.Name} dropped to 0 uptime; reviving next exchange");
StateChanged?.Invoke();
}
private EnemyInstance FindEnemy(CombatantState state)
{
if (state == null)
{
return null;
}
for (int i = 0; i < _enemies.Count; i++)
{
if (_enemies[i].State == state)
{
return _enemies[i];
}
}
return null;
}
private void RevivePendingEnemies()
{
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (!enemy.PendingRevive)
{
continue;
}
enemy.ReviveAtHalfUptime();
Log($"{enemy.Name} revived at {enemy.CurrentUptime} uptime");
}
}
private void ResetEnemyTurnLethalMarkers()
{
for (int i = 0; i < _enemies.Count; i++)
{
_enemies[i].ResetTurnLethalMarker();
}
}
private void ResetArchBtwAtEndOfTurn()
{
if (!IsDistro("arch") || _playerState == null || _playerState.ArchBtwStacks <= 0 || ArchBtwPersistsThisWave())
{
return;
}
_playerState.ArchBtwStacks = 0;
}
private void ResetArchWaveState()
{
if (_playerState == null)
{
return;
}
_playerState.ArchBtwStacks = 0;
_playerState.ArchRollingReleaseSavesRemaining = IsDistro("arch") ? _runConfig.DistroVersion >= 5 ? 2 : 1 : 0;
_playerState.ArchRollingReleaseAvailableThisWave = _playerState.ArchRollingReleaseSavesRemaining > 0;
_playerState.ArchRollingReleaseRecoveredThisHit = false;
_playerState.HasLastPlayedCardLanguage = false;
_playerState.HasPreviousPlayedCardLanguage = false;
}
private bool ArchBtwPersistsThisWave()
{
return IsDistro("arch")
&& (_runConfig.DistroVersion >= 5
|| HasOnDistroPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData effect)
&& effect.PersistArchBtw);
}
private bool HasOnDistroPackageEffect(PackageEffectKind kind, out PackageEffectData effect)
{
effect = default;
IReadOnlyList packages = _runConfig?.EquippedPackages;
if (packages == null)
{
return false;
}
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null || !package.Definition.IsIntendedFor(_runConfig?.Distro?.Id))
{
continue;
}
effect = package.EffectFor(_runConfig?.Distro?.Id);
if (effect.Kind == kind)
{
return true;
}
}
return false;
}
private bool HasPackageEffect(PackageEffectKind kind, out PackageEffectData effect)
{
effect = default;
IReadOnlyList packages = _runConfig?.EquippedPackages;
if (packages == null)
{
return false;
}
for (int i = 0; i < packages.Count; i++)
{
PackageInstance package = packages[i];
if (package?.Definition == null)
{
continue;
}
effect = package.EffectFor(_runConfig?.Distro?.Id);
if (effect.Kind == kind)
{
return true;
}
}
return false;
}
private static bool HasHarmfulStatus(CombatantState state)
{
if (state == null)
{
return false;
}
for (int i = 0; i < state.Statuses.Count; i++)
{
if (!StatusEffectController.GetDescriptor(state.Statuses[i].Type).IsBeneficial)
{
return true;
}
}
return false;
}
private static bool IsCrashImmune(CardInstance card)
{
string id = card?.Definition?.Id;
return string.Equals(id, "fedora_selinux", StringComparison.OrdinalIgnoreCase);
}
private void ApplyVersionState(CombatantState state)
{
if (state == null)
{
return;
}
state.ForceMaxRolls = IsDistro("mint");
state.IgnoreDamageMultipliers = IsDistro("mint");
state.AllowFlatDamageBuffs = IsDistro("mint") && _runConfig.DistroVersion >= 4;
state.FlatEffectBonus = IsDistro("mint") && _runConfig.DistroVersion >= 2 ? 2 : 0;
state.DamageMultiplierPercent = 100;
state.CurrentCardDamageMultiplierPercent = 100;
state.ArchBtwDamagePerStack = IsDistro("arch") && _runConfig.DistroVersion >= 2 ? 2 : 1;
state.ArchMakepkgBtwMultiplier = IsDistro("arch") && _runConfig.DistroVersion >= 3 ? 4 : 3;
state.ArchRollingReleaseShieldOnSave = IsDistro("arch") && _runConfig.DistroVersion >= 4 ? 15 : 0;
state.ArchRollingReleaseCyclesOnSave = IsDistro("arch") && _runConfig.DistroVersion >= 4 ? 2 : 0;
}
private bool IsDistro(string id)
{
return string.Equals(_runConfig?.Distro?.Id, id, StringComparison.OrdinalIgnoreCase);
}
public bool CanPlayerReceiveCardShield(CardInstance card)
{
return CanPlayerReceiveCardBuff(card);
}
public bool CanPlayerReceiveCardHeal(CardInstance card)
{
return CanPlayerReceiveCardBuff(card);
}
public bool CanPlayerReceiveCardBuff(CardInstance card)
{
return !IsDistro("mint") || IsCurrentDistroExclusiveCard(card);
}
private bool IsCurrentDistroExclusiveCard(CardInstance card)
{
CardDefinition definition = card?.Definition;
IReadOnlyList exclusiveCards = _runConfig?.Distro?.ExclusiveCards;
if (definition == null || exclusiveCards == null)
{
return false;
}
for (int i = 0; i < exclusiveCards.Count; i++)
{
CardDefinition exclusive = exclusiveCards[i];
if (exclusive == null)
{
continue;
}
if (ReferenceEquals(exclusive, definition)
|| string.Equals(exclusive.Id, definition.Id, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private CombatContext BuildContext(CardInstance card, IReadOnlyList targets)
{
return new CombatContext(card, _playerState, targets, this, _damagePipeline, _statusEffects, _deckController, _handController, _enemies);
}
private void ExecuteCardEffects(CardInstance card, IReadOnlyList targets)
{
IReadOnlyList effects = CardEffectFactory.CreateEffects(card?.Definition);
CombatContext context = BuildContext(card, targets);
for (int i = 0; i < effects.Count; i++)
{
effects[i].Execute(context);
RemoveDefeatedEnemies();
if (CheckWinOrLoss())
{
return;
}
}
}
private void ResolveCardRiders(CardInstance card)
{
if (card == null || card.DrawRider <= 0 || _handController == null || _playerState == null)
{
return;
}
StartCoroutine(DrawCardsToHandSequenced(card.DrawRider, "upgrade rider"));
}
private IReadOnlyList CaptureTargets(CardInstance card)
{
if (card?.Definition == null)
{
return Array.Empty();
}
if (CardEffectFactory.TargetsAllEnemies(card.Definition))
{
return LivingEnemyStates();
}
if (_selectedEnemyIndex >= 0 && _selectedEnemyIndex < _enemies.Count && !_enemies[_selectedEnemyIndex].State.IsDefeated)
{
return new[] { _enemies[_selectedEnemyIndex].State };
}
if (RequiresEnemyTarget(card))
{
CombatantState firstLiving = FirstLivingEnemyState();
return firstLiving == null ? Array.Empty() : new[] { firstLiving };
}
return Array.Empty();
}
private IReadOnlyList ResolveTargets(CardInstance card)
{
List targets = card.TargetSnapshot.Where(target => target != null && !target.IsDefeated).ToList();
if (targets.Count == 0 && RequiresEnemyTarget(card))
{
CombatantState firstLiving = FirstLivingEnemyState();
if (firstLiving != null)
{
targets.Add(firstLiving);
}
}
return targets;
}
private IReadOnlyList ResolveQueuedTargets(CardInstance card)
{
List targets = ResolveTargets(card).ToList();
if (targets.Count == 0 && CardEffectFactory.TargetsAllEnemies(card?.Definition))
{
targets.AddRange(LivingEnemyStates());
}
// Queued cards snapshot targets when played. If those targets die before Interpret, retarget to living enemies.
return targets;
}
private bool RequiresEnemyTarget(CardInstance card)
{
if (card?.Definition == null)
{
return false;
}
return CardEffectFactory.RequiresSingleTarget(card.Definition)
|| CardEffectFactory.TargetsAllEnemies(card.Definition)
|| card.Definition.Id == "lang_py_print";
}
private List LivingEnemyStates()
{
return _enemies
.Where(enemy => enemy.State != null && !enemy.State.IsDefeated)
.Select(enemy => enemy.State)
.ToList();
}
private CombatantState FirstLivingEnemyState()
{
for (int i = 0; i < _enemies.Count; i++)
{
if (_enemies[i].State != null && !_enemies[i].State.IsDefeated)
{
return _enemies[i].State;
}
}
return null;
}
private void RemoveDefeatedEnemies()
{
for (int i = _enemies.Count - 1; i >= 0; i--)
{
if (_enemies[i].State.IsDefeated)
{
QueueDefeatedEnemyForRemoval(_enemies[i]);
}
}
if (_selectedEnemyIndex >= _enemies.Count)
{
_selectedEnemyIndex = -1;
}
if (_enemyExitQueue.Count > 0 && !_enemyExitQueueDraining)
{
StartCoroutine(DrainEnemyExitQueue());
}
}
private void QueueDefeatedEnemyForRemoval(EnemyInstance enemy)
{
if (enemy == null || enemy.State == null || !enemy.State.IsDefeated || enemy.PendingRevive || _enemiesPendingExit.Contains(enemy))
{
return;
}
Log($"{enemy.Name} exited");
_runManager?.AddBits(CombatTuning.BitsPerKill);
List deathSpawns = HandleEnemyDeath(enemy);
enemy.MarkDefeatedPendingRemoval();
_enemiesPendingExit.Add(enemy);
_pendingDeathSpawns[enemy] = deathSpawns;
_enemyExitQueue.Enqueue(enemy);
if (!_enemyExitQueueDraining)
{
StartCoroutine(DrainEnemyExitQueue());
}
}
private List HandleEnemyDeath(EnemyInstance enemy)
{
List deathSpawns = new();
if (enemy == null)
{
return deathSpawns;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{
ApplySegfaultDeathPayload(enemy);
}
if (enemy.HasBehavior(EnemyBehaviorFlags.RacePair))
{
BreakRacePair(enemy);
}
if (enemy.HasBehavior(EnemyBehaviorFlags.LeavesOrphan))
{
deathSpawns.Add(CreateOrphanDeathSpawn(enemy));
}
return deathSpawns;
}
private void HandleCombatantDeathVisualCompleted(CombatantDeathVisualCompletedEvent payload)
{
EnemyInstance enemy = EnemyForState(payload.Combatant);
if (enemy == null)
{
return;
}
_completedDeathVisuals.Add(enemy);
}
private EnemyInstance EnemyForState(CombatantState state)
{
if (state == null)
{
return null;
}
for (int i = 0; i < _enemies.Count; i++)
{
if (_enemies[i].State == state)
{
return _enemies[i];
}
}
return null;
}
private IEnumerator DrainEnemyExitQueue()
{
if (_enemyExitQueueDraining)
{
yield break;
}
_enemyExitQueueDraining = true;
float elapsedMs = 0f;
while (_enemyExitQueue.Count > 0)
{
bool removedAny = false;
int pendingCount = _enemyExitQueue.Count;
for (int i = 0; i < pendingCount; i++)
{
EnemyInstance enemy = _enemyExitQueue.Dequeue();
bool visualComplete = _completedDeathVisuals.Contains(enemy);
bool timedOut = elapsedMs >= CombatTuning.VisualQueueMaxDrainMs;
if (!visualComplete && !timedOut)
{
_enemyExitQueue.Enqueue(enemy);
continue;
}
_enemyExitAwaitingVisual = enemy;
_completedDeathVisuals.Remove(enemy);
yield return CompleteEnemyExit(enemy);
_enemyExitAwaitingVisual = null;
removedAny = true;
}
if (removedAny)
{
elapsedMs = 0f;
continue;
}
elapsedMs += Time.unscaledDeltaTime * 1000f;
yield return null;
}
_enemyExitQueueDraining = false;
CheckWinOrLoss();
StateChanged?.Invoke();
if (!IsCombatPaused && currentPhase != TurnPhase.Execute)
{
yield return Wait(CombatTuning.PhaseTransitionDelaySeconds);
AdvancePhase();
}
}
private IEnumerator CompleteEnemyExit(EnemyInstance enemy)
{
if (enemy == null)
{
yield break;
}
_pendingDeathSpawns.TryGetValue(enemy, out IReadOnlyList deathSpawns);
_pendingDeathSpawns.Remove(enemy);
int removedIndex = _enemies.IndexOf(enemy);
if (removedIndex >= 0)
{
enemy.ClearDefeatedPendingRemoval();
_enemies.RemoveAt(removedIndex);
if (_selectedEnemyIndex >= _enemies.Count)
{
_selectedEnemyIndex = -1;
}
RecalculateKernelRamPressure();
StateChanged?.Invoke();
}
_enemiesPendingExit.Remove(enemy);
if (deathSpawns != null)
{
_deathSpawnSequencesActive++;
for (int i = 0; i < deathSpawns.Count; i++)
{
yield return WaitSequencedMs(CombatTuning.SpawnDelayMs, CombatTuning.ReducedMotionSpawnDelayMs);
EnemyInstance spawned = SpawnEnemy(deathSpawns[i].ArchetypeId, deathSpawns[i].UptimeBonus);
spawned.MarkSpawnedFromDeath();
spawned.PickNextIntent();
Log(deathSpawns[i].LogMessage);
StateChanged?.Invoke();
GameEvents.RaiseDeathSpawnedEnemy(new DeathSpawnedEnemyEvent(deathSpawns[i].Source, spawned));
yield return WaitSequencedMs(
CombatTuning.SpawnMaterializeMs + CombatTuning.SpawnTelegraphDelayMs,
CombatTuning.ReducedMotionSpawnMaterializeMs + CombatTuning.ReducedMotionSpawnTelegraphDelayMs);
spawned.RevealDeathSpawnIntent();
StateChanged?.Invoke();
}
_deathSpawnSequencesActive = Mathf.Max(0, _deathSpawnSequencesActive - 1);
}
}
private static IEnumerator WaitSequencedMs(int normalMs, int reducedMotionMs)
{
int durationMs = UIPreferences.ReducedMotion ? reducedMotionMs : normalMs;
if (durationMs > 0)
{
yield return new WaitForSeconds(durationMs / 1000f);
}
}
private void ApplySegfaultDeathPayload(EnemyInstance enemy)
{
_statusEffects.Apply(_playerState, StatusType.Segfault, 1, -1, enemy.State, skipNextTick: true);
TryCorruptRandomHandCard("segfault");
}
private CardInstance RandomHandCard(Predicate predicate)
{
if (_handController == null || _handController.Cards.Count == 0)
{
return null;
}
List candidates = new();
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance candidate = _handController.Cards[i];
if (predicate == null || predicate(candidate))
{
candidates.Add(candidate);
}
}
if (candidates.Count == 0)
{
return null;
}
int index = RandomRoll.RollRange(0, candidates.Count - 1, new RollContext(_playerState));
return candidates[index];
}
private void UnlockHandCards(string reason)
{
if (_handController == null)
{
return;
}
int unlocked = 0;
for (int i = 0; i < _handController.Cards.Count; i++)
{
CardInstance card = _handController.Cards[i];
if (card != null && card.IsLocked)
{
card.IsLocked = false;
unlocked++;
}
}
if (unlocked > 0)
{
Log($"{reason}: unlocked {unlocked} card(s)");
}
}
private void RecalculateKernelRamPressure()
{
if (_handController == null || _playerState == null || _runManager == null)
{
return;
}
int penalty = 0;
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy.HasBehavior(EnemyBehaviorFlags.RamPressure) && !enemy.State.IsDefeated)
{
penalty += EnemyArchetypeCatalog.KernelPanicRamPenalty;
}
}
_activeKernelRamPenalty = penalty;
int baseRam = _runManager.EffectiveRam();
int effectiveRam = Mathf.Max(1, baseRam - penalty);
_playerState.Ram = effectiveRam;
_handController.SetRamCapacity(effectiveRam);
}
private void ApplyTelemetryCardPlayed()
{
int collectors = 0;
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy.HasBehavior(EnemyBehaviorFlags.TelemetryCollector) && !enemy.State.IsDefeated)
{
enemy.AddTelemetryStack();
collectors++;
}
}
if (collectors > 0)
{
Log($"telemetry collector profiled card: +{EnemyArchetypeCatalog.TelemetryDamageGrowthPerCard} damage");
StateChanged?.Invoke();
}
}
private void LockRandomHandCard(EnemyInstance source)
{
CardInstance card = RandomHandCard(cardInstance => cardInstance != null && !cardInstance.IsBroken && !cardInstance.IsLocked);
if (card == null)
{
Log("license check: no unlockable hand card");
return;
}
card.IsLocked = true;
Log($"{source.Name} locked {GetCardName(card)}");
}
private void BreakRacePair(EnemyInstance defeated)
{
EnemyInstance survivor = RacePartner(defeated);
if (survivor == null)
{
return;
}
survivor.EnrageRaceSurvivor();
Log($"{survivor.Name} race link broke; survivor enraged");
}
private static DeathSpawnRequest CreateOrphanDeathSpawn(EnemyInstance source)
{
return new DeathSpawnRequest(source?.State, "orphan_process", 0, $"{source?.Name ?? "enemy"} left an orphan process");
}
private void SpawnStructuralEnemies()
{
int wave = CurrentWaveNumber;
int countGrowth = ((wave - 1) / 2) * CombatTuning.AdditionalEnemiesPerWave;
int count = Mathf.Clamp(CombatTuning.BaseEnemiesPerWave + countGrowth, CombatTuning.BaseEnemiesPerWave, CombatTuning.MaxEnemiesPerWave);
int waveUptimeBonus = Mathf.Max(0, wave - 1) * CombatTuning.EnemyUptimeGrowthPerWave;
bool rootkitSpawned = false;
bool eliteSpawned = TrySpawnEliteEncounter(wave, waveUptimeBonus);
for (int i = eliteSpawned ? 1 : 0; i < count; i++)
{
string archetypeId = CreatePlaceholderArchetypeId(wave, i);
if (archetypeId == "rootkit")
{
if (rootkitSpawned)
{
archetypeId = CreateRootkitFallbackArchetypeId(wave, i);
}
else
{
rootkitSpawned = true;
}
}
if (archetypeId == "race_condition" && i + 1 < count)
{
SpawnRacePair(waveUptimeBonus);
i++;
continue;
}
if (archetypeId == "race_condition")
{
archetypeId = "segfault";
}
SpawnEnemy(archetypeId, waveUptimeBonus);
}
}
private bool TrySpawnEliteEncounter(int wave, int uptimeBonus)
{
if (wave < 5)
{
return false;
}
bool guaranteedIntro = wave == 5;
bool rolledElite = RandomRoll.RollRange(1, 100, RollContext.None) <= 35;
if (!guaranteedIntro && !rolledElite)
{
return false;
}
SpawnEnemy(CreateEliteArchetypeId(), uptimeBonus);
Log("elite process escorted by fodder");
// TODO: Replace this single-elite chance with authored elite encounter assets.
return true;
}
private static string CreateEliteArchetypeId()
{
string[] pool = { "daemon", "kernel_panic", "telemetry_collector", "drm_guardian" };
int index = RandomRoll.RollRange(0, pool.Length - 1, RollContext.None);
return pool[index];
}
private void SpawnRacePair(int uptimeBonus)
{
int pairId = _nextRacePairId++;
EnemyInstance first = SpawnEnemy("race_condition", uptimeBonus);
EnemyInstance second = SpawnEnemy("race_condition", uptimeBonus);
first.SetPairId(pairId);
second.SetPairId(pairId);
Log("race condition pair spawned");
}
private EnemyInstance SpawnEnemy(string archetypeId, int uptimeBonus)
{
EnemyArchetypeDescriptor archetype = EnemyArchetypeCatalog.Get(archetypeId);
int baseUptime = RandomRoll.RollRange(archetype.BaseUptimeMin, archetype.BaseUptimeMax, RollContext.None);
EnemyInstance enemy = new(archetype, baseUptime + Mathf.Max(0, uptimeBonus));
_enemies.Add(enemy);
return enemy;
}
private static string CreatePlaceholderArchetypeId(int wave, int slotIndex)
{
// TODO: Replace this structural mix with encounter/difficulty assets.
string[] pool = wave switch
{
<= 1 => new[] { "zombie_process", "memory_leak" },
2 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld" },
3 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "segfault" },
4 => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "cron_job", "segfault", "race_condition" },
_ => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "daemon", "cron_job", "segfault", "race_condition", "rootkit" }
};
if (slotIndex == 0)
{
if (wave >= 5 && wave % 2 == 1)
{
return "rootkit";
}
return wave % 2 == 0 ? "firewalld" : "zombie_process";
}
if (slotIndex == 1 && wave >= 2)
{
if (wave >= 4 && wave % 4 == 0)
{
return "race_condition";
}
return wave % 3 == 0 ? "fork_bomb" : "memory_leak";
}
int index = RandomRoll.RollRange(0, pool.Length - 1, RollContext.None);
return pool[index];
}
private static string CreateRootkitFallbackArchetypeId(int wave, int slotIndex)
{
return slotIndex % 2 == 0
? "firewalld"
: wave % 3 == 0 ? "fork_bomb" : "memory_leak";
}
private void PickEnemyIntents()
{
for (int i = 0; i < _enemies.Count; i++)
{
if (!_enemies[i].State.IsDefeated)
{
_enemies[i].PickNextIntent();
}
}
}
private List BuildEnemyActionOrder()
{
List actors = _enemies
.Where(enemy => enemy.State != null && !enemy.State.IsDefeated)
.ToList();
for (int i = 0; i < actors.Count - 1; i++)
{
EnemyInstance enemy = actors[i];
if (!IsRacePairLinked(enemy))
{
continue;
}
EnemyInstance partner = RacePartner(enemy);
int partnerIndex = partner == null ? -1 : actors.IndexOf(partner);
if (partnerIndex <= i)
{
continue;
}
if (RandomRoll.RollRange(0, 1, new RollContext(enemy.State)) == 1)
{
actors[i] = partner;
actors[partnerIndex] = enemy;
}
}
return actors;
}
private void ExecuteEnemyIntent(EnemyInstance enemy, int enemyIndex)
{
EnemyIntent intent = enemy.CurrentIntent;
int value = intent.MinValue == intent.MaxValue
? intent.MinValue
: RandomRoll.RollRange(intent.MinValue, intent.MaxValue, new RollContext(enemy.State));
switch (intent.Kind)
{
case EnemyIntentKind.Attack:
value = ApplyEnemyAttackBonuses(enemy, value);
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;
case EnemyIntentKind.Defend:
if (enemy.HasBehavior(EnemyBehaviorFlags.DefendAllies))
{
DefendSelfAndAdjacent(enemyIndex, value);
}
else
{
enemy.State.Shield += value;
}
break;
case EnemyIntentKind.StatusAttack:
value = ApplyEnemyAttackBonuses(enemy, value);
DamageResult statusResult = _damagePipeline.DealDamage(new DamageRequest(enemy.State, _playerState, value, intent.DamageType, intent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, statusResult.FinalAmount));
if (intent.StatusStacks > 0)
{
_statusEffects.Apply(_playerState, intent.StatusType, intent.StatusStacks, intent.StatusDuration, enemy.State, skipNextTick: true);
}
break;
case EnemyIntentKind.Buff:
Log("TODO: buff intent needs enemy buff/status support.");
break;
case EnemyIntentKind.Special:
ExecuteSpecialEnemyIntent(enemy, value);
break;
}
enemy.AdvanceCountdownAfterAction();
}
private void ExecuteSpecialEnemyIntent(EnemyInstance enemy, int value)
{
if (enemy.HasBehavior(EnemyBehaviorFlags.CardLocker))
{
LockRandomHandCard(enemy);
value = ApplyIncomingAttackMitigation(value);
DamageResult result = _damagePipeline.DealDamage(new DamageRequest(enemy.State, _playerState, value, enemy.CurrentIntent.DamageType, enemy.CurrentIntent.TrueDamage, false));
GameEvents.RaisePlayerDamaged(new PlayerDamagedEvent(enemy, result.FinalAmount));
return;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{
enemy.AdvanceSegfaultCountdown();
if (enemy.CountdownRemaining <= 0)
{
_damagePipeline.DealDamage(new DamageRequest(enemy.State, enemy.State, enemy.CurrentUptime, Language.C, true, false));
Log($"{enemy.Name} dereferenced null");
}
return;
}
Log("TODO: special intent needs enemy-specific behavior hooks.");
}
private void ResolvePassiveEnemyAfterAction(EnemyInstance enemy)
{
if (enemy == null || enemy.State.IsDefeated)
{
return;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.Split))
{
TrySplitForkBomb(enemy);
}
}
private int ApplyEnemyAttackBonuses(EnemyInstance enemy, int value)
{
int modified = value;
if (IsRacePairLinked(enemy))
{
modified += EnemyArchetypeCatalog.RacePairDamageBonus;
}
if (enemy != null && enemy.RaceEnrageStacks > 0)
{
modified += enemy.RaceEnrageStacks;
}
return modified;
}
private void TrySplitForkBomb(EnemyInstance source)
{
int totalForkBombs = _enemies.Count(enemy => enemy.ArchetypeId == source.ArchetypeId && !enemy.State.IsDefeated);
if (totalForkBombs >= EnemyArchetypeCatalog.ForkBombTotalCap)
{
Log("fork bomb: split capped");
return;
}
EnemyArchetypeDescriptor archetype = EnemyArchetypeCatalog.Get(source.ArchetypeId);
int uptime = Mathf.Max(1, Mathf.CeilToInt(source.MaxUptime * 0.5f));
EnemyInstance copy = new(archetype, uptime);
copy.PickNextIntent();
_enemies.Add(copy);
Log("fork bomb split");
}
private bool IsRacePairLinked(EnemyInstance enemy)
{
return enemy != null
&& enemy.HasBehavior(EnemyBehaviorFlags.RacePair)
&& RacePartner(enemy) != null;
}
private EnemyInstance RacePartner(EnemyInstance enemy)
{
if (enemy == null || enemy.PairId < 0)
{
return null;
}
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance candidate = _enemies[i];
if (candidate != enemy
&& candidate.PairId == enemy.PairId
&& !candidate.State.IsDefeated)
{
return candidate;
}
}
return null;
}
private void DefendSelfAndAdjacent(int enemyIndex, int amount)
{
int applied = 0;
for (int i = enemyIndex - 1; i <= enemyIndex + 1; i++)
{
if (i < 0 || i >= _enemies.Count || _enemies[i].State.IsDefeated)
{
continue;
}
_enemies[i].State.Shield += amount;
applied++;
}
Log($"firewalld shielded {applied} process(es) for {amount}");
}
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 int ResolveDamageResistance(DamageRequest request, int amount)
{
EnemyInstance target = FindEnemy(request.Target);
if (target == null
|| !target.HasBehavior(EnemyBehaviorFlags.RootkitMasked)
|| !HasOtherLivingEnemy(target))
{
return amount;
}
if (amount <= 0)
{
return 0;
}
return Mathf.Max(1, Mathf.CeilToInt(amount * (EnemyArchetypeCatalog.RootkitMaskedDamagePercent / 100f)));
}
private bool HasOtherLivingEnemy(EnemyInstance target)
{
for (int i = 0; i < _enemies.Count; i++)
{
EnemyInstance enemy = _enemies[i];
if (enemy != target && enemy.State != null && !enemy.State.IsDefeated)
{
return true;
}
}
return false;
}
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 targets = LivingEnemyStates();
for (int i = 0; i < targets.Count; i++)
{
_damagePipeline.DealDamage(new DamageRequest(_playerState, targets[i], amount, language, false, true));
}
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();
}
private bool CheckLoss()
{
if (_runLost || _playerState == null || !_playerState.IsDefeated)
{
return _runLost;
}
_runLost = true;
_awaitingWaveContinue = false;
_selectedEnemyIndex = -1;
_pendingTargetCard = null;
GameEvents.RaiseEncounterLost(new EncounterLostEvent(CurrentWaveNumber));
GameEvents.RaiseRunEnded(new RunEndedEvent(playerDied: true, entropyEarned: 0));
Log("kernel panic: player uptime reached 0");
StateChanged?.Invoke();
return true;
}
private bool CheckWin()
{
if (_awaitingWaveContinue || _runLost)
{
return _awaitingWaveContinue;
}
if (LivingEnemyStates().Count > 0 || VisualResolutionPending)
{
return false;
}
_awaitingWaveContinue = true;
_selectedEnemyIndex = -1;
_pendingTargetCard = null;
int clearedWave = CurrentWaveNumber;
GameEvents.RaiseEncounterWon(new EncounterWonEvent(clearedWave));
GameEvents.RaiseWaveCleared(new WaveClearedEvent(clearedWave));
Log("wave cleared -> repository available");
StateChanged?.Invoke();
return true;
}
private void LogPlayIntent(CardInstance card)
{
string notes = card?.Definition == null || string.IsNullOrWhiteSpace(card.Definition.DesignNotes)
? "no-op scaffold"
: card.Definition.DesignNotes;
Log($"{GetCardName(card)} would resolve: {notes}");
}
private void Log(string message)
{
CombatLog?.Invoke(message);
}
private static string GetCardName(CardInstance card)
{
if (card?.Definition == null)
{
return "--";
}
return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName;
}
private bool VisualResolutionPending => _enemiesPendingExit.Count > 0
|| _enemyExitQueue.Count > 0
|| _enemyExitQueueDraining
|| _enemyExitAwaitingVisual != null
|| _deathSpawnSequencesActive > 0;
private bool IsCombatPaused => _awaitingWaveContinue || _runLost || VisualResolutionPending;
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);
}
}
private readonly struct DeathSpawnRequest
{
public DeathSpawnRequest(CombatantState source, string archetypeId, int uptimeBonus, string logMessage)
{
Source = source;
ArchetypeId = archetypeId;
UptimeBonus = uptimeBonus;
LogMessage = logMessage;
}
public CombatantState Source { get; }
public string ArchetypeId { get; }
public int UptimeBonus { get; }
public string LogMessage { get; }
}
}
}