diff --git a/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml b/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml index 889bf0a..315ec41 100644 --- a/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml +++ b/.idea/.idea.linuxdistrosbuttheyarebreedable/.idea/workspace.xml @@ -11,37 +11,30 @@ - - - - - - - - - - - - - + - - - - + + + + + + + + - - - + + + + + + - - + - - @@ -118,23 +111,23 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -145,34 +138,34 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + { @@ -264,7 +257,8 @@ - + + - @@ -498,7 +500,6 @@ diff --git a/Assets/Data/Cards/Languages/C/Malloc.asset b/Assets/Data/Cards/Languages/C/Malloc.asset index c674d58..383fbf5 100644 --- a/Assets/Data/Cards/Languages/C/Malloc.asset +++ b/Assets/Data/Cards/Languages/C/Malloc.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: id: lang_c_malloc displayName: malloc() - description: deal 8. 20% segfault: take 3. + description: deal 8. 20% segfault: uptime -3. flavorText: memory acquired. ownership emotionally unresolved. language: 0 rarity: 0 diff --git a/Assets/Data/Cards/Languages/C/Pointer.asset b/Assets/Data/Cards/Languages/C/Pointer.asset index 60782f6..3d68b5c 100644 --- a/Assets/Data/Cards/Languages/C/Pointer.asset +++ b/Assets/Data/Cards/Languages/C/Pointer.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: id: lang_c_pointer displayName: pointer - description: deal 6. if the previous card was C, deal 10 instead. + description: deal 6. if the previous card was C, deal 10 instead. 20% segfault: uptime -3. flavorText: points at the bug with suspicious confidence. language: 0 rarity: 0 diff --git a/Assets/Data/Cards/Languages/C/Printf.asset b/Assets/Data/Cards/Languages/C/Printf.asset index a959fc3..1503090 100644 --- a/Assets/Data/Cards/Languages/C/Printf.asset +++ b/Assets/Data/Cards/Languages/C/Printf.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: id: lang_c_printf displayName: printf() - description: deal 4. + description: deal 4. 20% segfault: uptime -3. flavorText: debugging by yelling into the void. language: 0 rarity: 0 diff --git a/Assets/Data/Cards/Shop/Free.asset b/Assets/Data/Cards/Shop/Free.asset index 6eda7de..0e92e60 100644 --- a/Assets/Data/Cards/Shop/Free.asset +++ b/Assets/Data/Cards/Shop/Free.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: id: shop_c_free displayName: free() - description: deal 12. if segfault recoil happens, draw 2. + description: deal 12. 20% segfault: uptime -3 and draw 2. flavorText: double-free your schedule, not your heap. language: 0 rarity: 0 diff --git a/Assets/Data/Cards/Shop/KernelExploit.asset b/Assets/Data/Cards/Shop/KernelExploit.asset index 373691d..04ef461 100644 --- a/Assets/Data/Cards/Shop/KernelExploit.asset +++ b/Assets/Data/Cards/Shop/KernelExploit.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: id: shop_c_kernel_exploit displayName: kernel_exploit - description: deal 25 ignoring Shield. 50% segfault: take 8. + description: deal 25 ignoring Shield. 50% segfault: uptime -8. flavorText: privilege escalated. heartbeat also escalated. language: 0 rarity: 1 diff --git a/Assets/Data/Cards/Shop/Sizeof.asset b/Assets/Data/Cards/Shop/Sizeof.asset index e56ced4..e24862f 100644 --- a/Assets/Data/Cards/Shop/Sizeof.asset +++ b/Assets/Data/Cards/Shop/Sizeof.asset @@ -14,7 +14,7 @@ MonoBehaviour: m_EditorClassIdentifier: id: shop_c_sizeof displayName: sizeof() - description: deal 3 x C cards in hand. + description: deal 3 x C cards in hand. 20% segfault: uptime -3. flavorText: compile-time confidence, runtime consequences. language: 0 rarity: 1 diff --git a/Assets/Scripts/Combat/CardEffects.cs b/Assets/Scripts/Combat/CardEffects.cs index 4c67810..c6f982e 100644 --- a/Assets/Scripts/Combat/CardEffects.cs +++ b/Assets/Scripts/Combat/CardEffects.cs @@ -35,6 +35,7 @@ namespace KernelPanic.Combat targets = ShuffleTargets(targets, context.Source); } + bool dealtDamage = false; for (int i = 0; i < targets.Count; i++) { CombatantState target = targets[i]; @@ -54,11 +55,14 @@ namespace KernelPanic.Combat ? _trueDamage ? target.CurrentUptime : target.CurrentUptime + target.Shield : 0; DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, _trueDamage, _canCrit)); + dealtDamage |= result.IncomingAmount > 0; if (rustOverkillToShield) { GrantRustOverkillShield(context, result, survivable); } } + + SegfaultRecoil.TryApply(context, _language, dealtDamage); } private static void GrantRustOverkillShield(CombatContext context, DamageResult result, int survivable) @@ -164,7 +168,8 @@ namespace KernelPanic.Combat int roll = RandomRoll.RollRange(1, 100, new RollContext(context.Source)); int damageAmount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_damageAmount, context), context.Card); int amount = roll <= _successPercent ? damageAmount : 0; - context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, true)); + DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, true)); + SegfaultRecoil.TryApply(context, _language, result.IncomingAmount > 0); context.CombatManager.ReportEffectResult(amount == 0 ? "typeof -> NaN (0)" : $"typeof -> {amount}"); } } @@ -189,41 +194,6 @@ namespace KernelPanic.Combat } } - public sealed class SegfaultDamageEffect : ICardEffect - { - private readonly int _amount; - private readonly int _selfDamage; - private readonly int _segfaultPercent; - private readonly bool _trueDamage; - private readonly bool _drawOnSegfault; - - public SegfaultDamageEffect(int amount, int selfDamage, int segfaultPercent, bool trueDamage = false, bool drawOnSegfault = false) - { - _amount = amount; - _selfDamage = selfDamage; - _segfaultPercent = segfaultPercent; - _trueDamage = trueDamage; - _drawOnSegfault = drawOnSegfault; - } - - public void Execute(CombatContext context) - { - new DealDamageEffect(_amount, _amount, Language.C, _trueDamage).Execute(context); - bool segfault = RandomRoll.RollRange(1, 100, new RollContext(context.Source)) <= _segfaultPercent; - if (!segfault) - { - return; - } - - context.DamagePipeline.DealDamage(new DamageRequest(context.Source, context.Source, _selfDamage, Language.C, false, false)); - context.CombatManager.ReportEffectResult($"segfault: took {_selfDamage}"); - if (_drawOnSegfault) - { - context.CombatManager.DrawCards(2, "free() recoil"); - } - } - } - public sealed class PreviousLanguageDamageEffect : ICardEffect { private readonly int _baseAmount; @@ -687,6 +657,7 @@ namespace KernelPanic.Combat int amount = UpgradeMath.ScaleAmount(UpgradeMath.ApplySourceFlatBonus(_amount, context), context.Card); int survivable = target.CurrentUptime + target.Shield; DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, target, amount, _language, false, true)); + SegfaultRecoil.TryApply(context, _language, result.IncomingAmount > 0); int overkill = UnityEngine.Mathf.Max(0, result.IncomingAmount - survivable); if (overkill > 0) { @@ -875,6 +846,58 @@ namespace KernelPanic.Combat } } + internal static class SegfaultRecoil + { + private const int DefaultPercent = 20; + private const int DefaultUptimeDamage = 3; + + public static void TryApply(CombatContext context, Language damageLanguage, bool dealtDamage) + { + if (!dealtDamage + || context == null + || context.SegfaultRecoilRolled + || context.Source == null + || context.Source.IsDefeated + || context.Card?.Definition?.Language != Language.C + || damageLanguage != Language.C) + { + return; + } + + context.SegfaultRecoilRolled = true; + GetProfile(context.Card, out int percent, out int uptimeDamage, out bool drawOnSegfault); + if (RandomRoll.RollRange(1, 100, new RollContext(context.Source)) > percent) + { + return; + } + + DamageResult result = context.DamagePipeline.DealDamage(new DamageRequest(null, context.Source, uptimeDamage, Language.C, true, false)); + context.CombatManager.ReportEffectResult($"segfault: uptime -{result.FinalAmount}"); + if (drawOnSegfault) + { + context.CombatManager.DrawCards(2, "free() recoil"); + } + } + + private static void GetProfile(CardInstance card, out int percent, out int uptimeDamage, out bool drawOnSegfault) + { + percent = DefaultPercent; + uptimeDamage = DefaultUptimeDamage; + drawOnSegfault = false; + + switch (card?.Definition?.Id) + { + case "shop_c_free": + drawOnSegfault = true; + break; + case "shop_c_kernel_exploit": + percent = 50; + uptimeDamage = 8; + break; + } + } + } + public static class CardEffectFactory { public static IReadOnlyList CreateEffects(CardDefinition definition) @@ -899,7 +922,7 @@ namespace KernelPanic.Combat new DrawEffect(1) }, "lang_rs_borrow" => One(new ShieldEffect(5)), - "lang_c_malloc" => One(new SegfaultDamageEffect(8, 3, 20)), + "lang_c_malloc" => One(new DealDamageEffect(8, 8, Language.C)), "lang_c_printf" => One(new DealDamageEffect(4, 4, Language.C)), "lang_c_pointer" => One(new PreviousLanguageDamageEffect(6, 10, Language.C)), "lang_java_public_static_main" => One(new DealDamageEffect(7, 7, Language.Java)), @@ -958,9 +981,9 @@ namespace KernelPanic.Combat "arch_makepkg" => One(new BtwScaledDamageEffect(4, 2, Language.C)), "arch_aur_helper" => One(new RandomCommonLanguageCardEffect()), "arch_minimal_install" => One(new MinimalInstallEffect()), - "shop_c_free" => One(new SegfaultDamageEffect(12, 3, 20, drawOnSegfault: true)), + "shop_c_free" => One(new DealDamageEffect(12, 12, Language.C)), "shop_c_sizeof" => One(new SizeofDamageEffect()), - "shop_c_kernel_exploit" => One(new SegfaultDamageEffect(25, 8, 50, true)), + "shop_c_kernel_exploit" => One(new DealDamageEffect(25, 25, Language.C, true)), _ => Todo("card effect is not authored") }; } @@ -1030,9 +1053,9 @@ namespace KernelPanic.Combat "lang_rs_fn_main" => $"deal {UpgradeMath.ScaleAmount(6, card)}. overkill becomes shield.{marker}", "lang_rs_unwrap" => $"deal {UpgradeMath.ScaleAmount(9, card)}. overkill becomes shield. draw {UpgradeMath.ScaleAmount(1, card)}.{marker}", "lang_rs_borrow" => $"gain {UpgradeMath.ScaleShield(5, card)} shield.{marker}", - "lang_c_malloc" => $"deal {UpgradeMath.ScaleAmount(8, card)}. 20%: segfault, take 3.{marker}", - "lang_c_printf" => $"deal {UpgradeMath.ScaleAmount(4, card)}.{marker}", - "lang_c_pointer" => $"deal {UpgradeMath.ScaleAmount(6, card)}; {UpgradeMath.ScaleAmount(10, card)} if the previous card was C.{marker}", + "lang_c_malloc" => $"deal {UpgradeMath.ScaleAmount(8, card)}. 20%: segfault, uptime -3.{marker}", + "lang_c_printf" => $"deal {UpgradeMath.ScaleAmount(4, card)}. 20%: segfault, uptime -3.{marker}", + "lang_c_pointer" => $"deal {UpgradeMath.ScaleAmount(6, card)}; {UpgradeMath.ScaleAmount(10, card)} if the previous card was C. 20%: segfault, uptime -3.{marker}", "lang_java_public_static_main" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(7, card)}. JIT: Java cards cost 1 less this combat each time you play a Java card; discount decays by 1 after each wave.{marker}", "lang_java_system_out_println" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(5, card)}. JIT applies; discount decays by 1 after each wave.{marker}", "lang_java_new_object" => $"uses 2 RAM. deal {UpgradeMath.ScaleAmount(3, card)}. JIT applies; discount decays by 1 after each wave.{marker}", @@ -1057,14 +1080,14 @@ namespace KernelPanic.Combat "fedora_dnf_update" => $"deal {UpgradeMath.ScaleAmount(9, card)}. costs 1 less per Java card played this combat.{marker}", "fedora_rawhide" => $"draw {UpgradeMath.ScaleAmount(2, card)}. next card this turn gains bleeding edge.{marker}", "fedora_selinux" => $"next {UpgradeMath.ScaleAmount(2, card)} enemy attacks deal half damage.{marker}", - "arch_pacman_syu" => $"deal {UpgradeMath.ScaleAmount(8, card)}. TODO: 20% break a random hand card.{marker}", + "arch_pacman_syu" => $"deal {UpgradeMath.ScaleAmount(8, card)}. 20%: segfault, uptime -3. TODO: 20% break a random hand card.{marker}", "arch_consult_wiki" => $"fix a broken card and draw {UpgradeMath.ScaleAmount(1, card)}; if none, deal {UpgradeMath.ScaleAmount(6, card)}.{marker}", - "arch_makepkg" => $"deal {UpgradeMath.ScaleAmount(4, card)} + btw x2; x3 at Arch v3+.{marker}", + "arch_makepkg" => $"deal {UpgradeMath.ScaleAmount(4, card)} + btw x2; x3 at Arch v3+. 20%: segfault, uptime -3.{marker}", "arch_aur_helper" => $"add a random C/Rust common card to hand; it costs 0 this turn.{marker}", "arch_minimal_install" => $"exhaust up to 2 hand cards. per card: gain 3 Shield and 1 Cycle.{marker}", - "shop_c_free" => $"deal {UpgradeMath.ScaleAmount(12, card)}. segfault recoil draws 2.{marker}", - "shop_c_sizeof" => $"deal 3 x C cards in hand.{marker}", - "shop_c_kernel_exploit" => $"deal {UpgradeMath.ScaleAmount(25, card)} ignoring Shield. 50%: take 8.{marker}", + "shop_c_free" => $"deal {UpgradeMath.ScaleAmount(12, card)}. 20%: segfault, uptime -3 and draw 2.{marker}", + "shop_c_sizeof" => $"deal 3 x C cards in hand. 20%: segfault, uptime -3.{marker}", + "shop_c_kernel_exploit" => $"deal {UpgradeMath.ScaleAmount(25, card)} ignoring Shield. 50%: segfault, uptime -8.{marker}", _ => string.IsNullOrWhiteSpace(card.Definition.Description) ? "effect not authored." : $"{card.Definition.Description}{marker}" }; } diff --git a/Assets/Scripts/Combat/CombatContext.cs b/Assets/Scripts/Combat/CombatContext.cs index 327d5cd..2331e61 100644 --- a/Assets/Scripts/Combat/CombatContext.cs +++ b/Assets/Scripts/Combat/CombatContext.cs @@ -38,5 +38,7 @@ namespace KernelPanic.Combat public DeckController DeckController { get; } public HandController HandController { get; } public IReadOnlyList Enemies { get; } + + internal bool SegfaultRecoilRolled { get; set; } } } diff --git a/Assets/Scripts/Combat/CombatManager.cs b/Assets/Scripts/Combat/CombatManager.cs index 0bae309..25bf43c 100644 --- a/Assets/Scripts/Combat/CombatManager.cs +++ b/Assets/Scripts/Combat/CombatManager.cs @@ -18,65 +18,65 @@ namespace KernelPanic.Combat { [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 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 int fedoraCrashChance = 10; - 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 Coroutine phaseCoroutine; + 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 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 int _fedoraCrashChance = 10; + 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 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 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 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 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 event Action StateChanged; public event Action CombatLog; private void Awake() { - runManager = GetComponent(); + _runManager = GetComponent(); } private void OnEnable() @@ -91,7 +91,7 @@ namespace KernelPanic.Combat public void StartCombat() { - if (runConfig == null) + if (_runConfig == null) { Debug.LogWarning("CombatManager.StartCombat called without a RunConfig."); return; @@ -102,7 +102,7 @@ namespace KernelPanic.Combat public void StartCombat(RunConfig config) { - runConfig = config; + _runConfig = config; StartCombat(); } @@ -121,20 +121,20 @@ namespace KernelPanic.Combat { if (currentPhase == TurnPhase.Execute && !IsCombatPaused) { - pendingTargetCard = null; - selectedEnemyIndex = -1; + _pendingTargetCard = null; + _selectedEnemyIndex = -1; SetPhase(TurnPhase.Interpret); } } public void ContinueToNextWave() { - if (!awaitingWaveContinue || runLost) + if (!_awaitingWaveContinue || _runLost) { return; } - awaitingWaveContinue = false; + _awaitingWaveContinue = false; DecayJavaWarmupForNextWave(); StartCoroutine(ContinueToNextWaveSequenced()); } @@ -152,18 +152,18 @@ namespace KernelPanic.Combat return; } - if (index < 0 || index >= enemies.Count) + if (index < 0 || index >= _enemies.Count) { - selectedEnemyIndex = -1; + _selectedEnemyIndex = -1; StateChanged?.Invoke(); return; } - selectedEnemyIndex = index; - if (pendingTargetCard != null) + _selectedEnemyIndex = index; + if (_pendingTargetCard != null) { - CardInstance card = pendingTargetCard; - pendingTargetCard = null; + CardInstance card = _pendingTargetCard; + _pendingTargetCard = null; PlayCard(card); return; } @@ -173,7 +173,7 @@ namespace KernelPanic.Combat public bool PlayCard(CardInstance card) { - if (currentPhase != TurnPhase.Execute || IsCombatPaused || card == null || handController == null || playerState == null) + if (currentPhase != TurnPhase.Execute || IsCombatPaused || card == null || _handController == null || _playerState == null) { return false; } @@ -191,46 +191,46 @@ namespace KernelPanic.Combat cost = Mathf.Max(0, cost - 1); } - if (playerState.Cycles < cost) + if (_playerState.Cycles < cost) { Log($"not enough cycles for {GetCardName(card)}"); return false; } - if (RequiresTarget(card) && selectedEnemyIndex < 0) + if (RequiresTarget(card) && _selectedEnemyIndex < 0) { - pendingTargetCard = card; + _pendingTargetCard = card; Log($"select target for {GetCardName(card)}"); StateChanged?.Invoke(); return false; } - if (!handController.Remove(card)) + if (!_handController.Remove(card)) { return false; } - playerState.Cycles -= cost; + _playerState.Cycles -= cost; ResolutionTrack track = card.Definition.ResolutionTrack; - card.MarkPlayedThisTurn(cardsPlayedThisTurn == 0); - cardsPlayedThisTurn++; - cardsPlayedThisWave++; + card.MarkPlayedThisTurn(_cardsPlayedThisTurn == 0); + _cardsPlayedThisTurn++; + _cardsPlayedThisWave++; if (IsDistro("arch")) { - playerState.ArchBtwStacks++; + _playerState.ArchBtwStacks++; } - playerState.HasPreviousPlayedCardLanguage = playerState.HasLastPlayedCardLanguage; - if (playerState.HasLastPlayedCardLanguage) + _playerState.HasPreviousPlayedCardLanguage = _playerState.HasLastPlayedCardLanguage; + if (_playerState.HasLastPlayedCardLanguage) { - playerState.PreviousPlayedCardLanguage = playerState.LastPlayedCardLanguage; + _playerState.PreviousPlayedCardLanguage = _playerState.LastPlayedCardLanguage; } - playerState.LastPlayedCardLanguage = card.Definition.Language; - playerState.HasLastPlayedCardLanguage = true; + _playerState.LastPlayedCardLanguage = card.Definition.Language; + _playerState.HasLastPlayedCardLanguage = true; if (card.Definition.Language == Language.Java) { - javaCardsPlayedThisCombat++; + _javaCardsPlayedThisCombat++; } GameEvents.RaiseCardPlayed(new CardPlayedEvent(card, track)); @@ -239,68 +239,68 @@ namespace KernelPanic.Combat if (fedoraBonus) { - fedoraCardsDiscountedThisTurn++; - bool rawhideChargeUsed = rawhideBonusCharges > 0; + _fedoraCardsDiscountedThisTurn++; + bool rawhideChargeUsed = _rawhideBonusCharges > 0; if (rawhideChargeUsed) { - rawhideBonusCharges--; + _rawhideBonusCharges--; } - if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(playerState)) <= fedoraCrashChance) + if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(_playerState)) <= _fedoraCrashChance) { - fedoraCrashChance = 10; - if (IsDistro("fedora") && runConfig.DistroVersion >= 4) + _fedoraCrashChance = 10; + if (IsDistro("fedora") && _runConfig.DistroVersion >= 4) { - playerState.Cycles += 1; + _playerState.Cycles += 1; } - deckController.Discard(card); - pendingTargetCard = null; - selectedEnemyIndex = -1; + _deckController.Discard(card); + _pendingTargetCard = null; + _selectedEnemyIndex = -1; Log($"{GetCardName(card)} crashed under bleeding edge"); StateChanged?.Invoke(); return true; } - fedoraCrashChance = Mathf.Min(90, fedoraCrashChance + 10); + _fedoraCrashChance = Mathf.Min(90, _fedoraCrashChance + 10); card.MarkFedoraNonCrashBonus(); - playerState.DamageMultiplierPercent = runConfig.DistroVersion >= 2 ? 175 : 150; + _playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150; ApplyFedoraGrowth(card, rawhideChargeUsed); } - if (card.Definition.Language == Language.C && cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect)) + if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect)) { - playerState.CurrentCardDamageMultiplierPercent = Mathf.Max(100, cPackageEffect.Amount); + _playerState.CurrentCardDamageMultiplierPercent = Mathf.Max(100, cPackageEffect.Amount); } switch (track) { case ResolutionTrack.InterpreterQueue: card.MarkQueued(); - interpreterQueue.Enqueue(card); + _interpreterQueue.Enqueue(card); break; case ResolutionTrack.LazyStack: - lazyStack.Enqueue(card); + _lazyStack.Enqueue(card); break; default: ExecuteCardEffects(card, ResolveTargets(card)); ResolveCardRiders(card); - deckController.Discard(card); + _deckController.Discard(card); GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.Native)); - selectedEnemyIndex = -1; + _selectedEnemyIndex = -1; break; } - playerState.DamageMultiplierPercent = 100; - playerState.CurrentCardDamageMultiplierPercent = 100; + _playerState.DamageMultiplierPercent = 100; + _playerState.CurrentCardDamageMultiplierPercent = 100; if (card.Definition.Language == Language.C) { - cCardsPlayedThisTurn++; + _cCardsPlayedThisTurn++; } ApplyPackageCardPlayedEffects(card); - pendingTargetCard = null; + _pendingTargetCard = null; TryApplyUbuntuEmptyHandRefill(); StateChanged?.Invoke(); return true; @@ -321,7 +321,7 @@ namespace KernelPanic.Combat int cost = GetCardCost(card); if (card?.Definition != null && card.Definition.Language == Language.Java) { - cost -= javaCardsPlayedThisCombat + javaCardsDiscountThisTurn; + cost -= _javaCardsPlayedThisCombat + _javaCardsDiscountThisTurn; } cost = ApplyPackageCostRules(card, cost); @@ -337,7 +337,7 @@ namespace KernelPanic.Combat { string cardId = card?.Definition == null ? string.Empty : card.Definition.Id; string key = $"{cardId}:{message}"; - if (loggedEffectTodos.Add(key)) + if (_loggedEffectTodos.Add(key)) { Log(message); } @@ -353,12 +353,12 @@ namespace KernelPanic.Combat public void AddNextTurnCycleBonus(int amount) { - nextTurnCycleBonus += Mathf.Max(0, amount); + _nextTurnCycleBonus += Mathf.Max(0, amount); } public void AddQueuedRepeatCharges(int amount) { - queuedRepeatCharges += Mathf.Max(0, amount); + _queuedRepeatCharges += Mathf.Max(0, amount); if (amount > 0) { ReportEffectResult($"next {amount} queued card(s) resolve twice"); @@ -373,34 +373,34 @@ namespace KernelPanic.Combat return; } - javaCardsDiscountThisTurn += safeAmount; + _javaCardsDiscountThisTurn += safeAmount; ReportEffectResult($"Java cards cost -{safeAmount} this turn"); } public int ConsumeQueuedResolutionCount(CardInstance card) { - if (queuedRepeatCharges <= 0 || card == null) + if (_queuedRepeatCharges <= 0 || card == null) { return 1; } - queuedRepeatCharges--; + _queuedRepeatCharges--; return 2; } public void GrantRawhideBonus(int charges) { - rawhideBonusCharges += Mathf.Max(0, charges); + _rawhideBonusCharges += Mathf.Max(0, charges); } public void AddIncomingAttackHalfCharges(int charges) { - if (playerState == null) + if (_playerState == null) { return; } - playerState.IncomingAttackHalfCharges += Mathf.Max(0, charges); + _playerState.IncomingAttackHalfCharges += Mathf.Max(0, charges); ReportEffectResult($"next {charges} enemy attack(s) halved"); } @@ -416,13 +416,13 @@ namespace KernelPanic.Combat public void ScheduleUpdateManagerRepeat(int damageAmount) { - delayedEffects.Add(DelayedCombatEffect.UpdateManagerRepeat(damageAmount, 2)); + _delayedEffects.Add(DelayedCombatEffect.UpdateManagerRepeat(damageAmount, 2)); ReportEffectResult("update manager scheduled repeat"); } public void ScheduleTimeshiftRestore(int uptime) { - delayedEffects.Add(DelayedCombatEffect.TimeshiftRestore(uptime, 2)); + _delayedEffects.Add(DelayedCombatEffect.TimeshiftRestore(uptime, 2)); ReportEffectResult($"timeshift snapshot recorded {uptime} uptime"); } @@ -434,7 +434,7 @@ namespace KernelPanic.Combat return false; } - IReadOnlyList source = generatedCardPool.Count > 0 ? generatedCardPool : runConfig?.StartingDeck; + IReadOnlyList source = _generatedCardPool.Count > 0 ? _generatedCardPool : _runConfig?.StartingDeck; if (source == null) { return false; @@ -455,7 +455,7 @@ namespace KernelPanic.Combat public void SetGeneratedCardPool(IEnumerable cardPool) { - generatedCardPool.Clear(); + _generatedCardPool.Clear(); if (cardPool == null) { return; @@ -465,7 +465,7 @@ namespace KernelPanic.Combat { if (card != null) { - generatedCardPool.Add(card); + _generatedCardPool.Add(card); } } } @@ -476,7 +476,7 @@ namespace KernelPanic.Combat List pool = new(); HashSet seenIds = new(); - IReadOnlyList source = generatedCardPool.Count > 0 ? generatedCardPool : runConfig?.StartingDeck; + IReadOnlyList source = _generatedCardPool.Count > 0 ? _generatedCardPool : _runConfig?.StartingDeck; if (source == null) { return false; @@ -501,7 +501,7 @@ namespace KernelPanic.Combat return false; } - int index = RandomRoll.RollRange(0, pool.Count - 1, new RollContext(playerState)); + int index = RandomRoll.RollRange(0, pool.Count - 1, new RollContext(_playerState)); CardDefinition selected = pool[index]; generatedCard = new CardInstance(selected) { @@ -531,12 +531,12 @@ namespace KernelPanic.Combat currentPhase = nextPhase; GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase)); - if (phaseCoroutine != null) + if (_phaseCoroutine != null) { - StopCoroutine(phaseCoroutine); + StopCoroutine(_phaseCoroutine); } - phaseCoroutine = StartCoroutine(EnterPhase(nextPhase)); + _phaseCoroutine = StartCoroutine(EnterPhase(nextPhase)); } private static TurnPhase GetNextPhase(TurnPhase phase) @@ -593,72 +593,72 @@ namespace KernelPanic.Combat private IEnumerator BootCombatSequenced() { - runLost = false; - awaitingWaveContinue = false; - loggedEffectTodos.Clear(); - ubuntuEmptyHandRefillUsed = false; - fedoraCrashChance = 10; - 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(); - RandomRoll.Seed(runConfig.RunSeed); - playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles()); - ApplyVersionState(playerState); - ApplyPackageState(playerState); + _runLost = false; + _awaitingWaveContinue = false; + _loggedEffectTodos.Clear(); + _ubuntuEmptyHandRefillUsed = false; + _fedoraCrashChance = 10; + _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(); + 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"); + 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) + int carriedUptime = _playerState == null ? 0 : _playerState.CurrentUptime; + if (!preservePlayerUptime || _playerState == null) { - playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles()); - ApplyVersionState(playerState); - ApplyPackageState(playerState); + _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); + _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; - packageShieldBonusTriggeredThisTurn = false; - packageTimeshiftTriggeredThisWave = false; + _handController = new HandController(_playerState.Ram); + _selectedEnemyIndex = -1; + _pendingTargetCard = null; + _cardsPlayedThisWave = 0; + _turnNumberThisWave = 0; + _packageShieldBonusTriggeredThisTurn = false; + _packageTimeshiftTriggeredThisWave = false; ResetArchWaveState(); List startingCards = new(); - for (int i = 0; i < runManager.RunDeck.Count; i++) + for (int i = 0; i < _runManager.RunDeck.Count; i++) { - CardInstance card = runManager.RunDeck[i]; + CardInstance card = _runManager.RunDeck[i]; if (card != null) { card.ResetCombatState(); @@ -666,15 +666,15 @@ namespace KernelPanic.Combat } } - deckController.Initialize(startingCards); - interpreterQueue.Clear(); - enemies.Clear(); + _deckController.Initialize(startingCards); + _interpreterQueue.Clear(); + _enemies.Clear(); SpawnStructuralEnemies(); PickEnemyIntents(); StateChanged?.Invoke(); yield return DrawOpeningHandSequenced(); ApplyPackageWaveStartEffects(); - skipNextAllocateDraw = true; + _skipNextAllocateDraw = true; } private IEnumerator AllocateTurnSequenced() @@ -684,21 +684,22 @@ namespace KernelPanic.Combat yield break; } - playerState.Cycles = playerState.MaxCycles; - fedoraCardsDiscountedThisTurn = 0; - cardsPlayedThisTurn = 0; - cCardsPlayedThisTurn = 0; - turnNumberThisWave++; - packageShieldBonusTriggeredThisTurn = false; - javaCardsDiscountThisTurn = 0; + RevivePendingEnemies(); + _playerState.Cycles = _playerState.MaxCycles; + _fedoraCardsDiscountedThisTurn = 0; + _cardsPlayedThisTurn = 0; + _cCardsPlayedThisTurn = 0; + _turnNumberThisWave++; + _packageShieldBonusTriggeredThisTurn = false; + _javaCardsDiscountThisTurn = 0; ApplyPackageTurnStartEffects(); - if (nextTurnCycleBonus > 0) + if (_nextTurnCycleBonus > 0) { - playerState.Cycles += nextTurnCycleBonus; - Log($"deferred cycle grant: +{nextTurnCycleBonus}"); - nextTurnCycleBonus = 0; + _playerState.Cycles += _nextTurnCycleBonus; + Log($"deferred cycle grant: +{_nextTurnCycleBonus}"); + _nextTurnCycleBonus = 0; } - statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline); + _statusEffects.Tick(_playerState, StatusTickTiming.StartOfTurn, _playerState, _damagePipeline); StateChanged?.Invoke(); if (CheckLoss()) { @@ -707,9 +708,9 @@ namespace KernelPanic.Combat TryApplyUbuntuAptUpdate(); - if (skipNextAllocateDraw) + if (_skipNextAllocateDraw) { - skipNextAllocateDraw = false; + _skipNextAllocateDraw = false; } else { @@ -721,7 +722,7 @@ namespace KernelPanic.Combat private IEnumerator InterpretQueueSequenced() { - while (interpreterQueue.TryDequeue(out CardInstance card)) + while (_interpreterQueue.TryDequeue(out CardInstance card)) { LogPlayIntent(card); int resolutionCount = ConsumeQueuedResolutionCount(card); @@ -735,7 +736,7 @@ namespace KernelPanic.Combat } ResolveCardRiders(card); - deckController.Discard(card); + _deckController.Discard(card); GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue)); StateChanged?.Invoke(); if (CheckWinOrLoss()) @@ -752,15 +753,16 @@ namespace KernelPanic.Combat private IEnumerator ProcessEnemiesSequenced() { - for (int i = 0; i < enemies.Count; i++) + int enemyCountAtPhaseStart = _enemies.Count; + for (int i = 0; i < enemyCountAtPhaseStart && i < _enemies.Count; i++) { - EnemyInstance enemy = enemies[i]; + EnemyInstance enemy = _enemies[i]; if (enemy.State.IsDefeated) { continue; } - statusEffects.Tick(enemy.State, StatusTickTiming.StartOfTurn, playerState, damagePipeline); + _statusEffects.Tick(enemy.State, StatusTickTiming.StartOfTurn, _playerState, _damagePipeline); RemoveDefeatedEnemies(); if (CheckWinOrLoss()) { @@ -772,12 +774,13 @@ namespace KernelPanic.Combat continue; } - Log($"{enemy.Name} executes {enemy.CurrentIntent.DisplayText}"); + Log($"{enemy.Name} executes {enemy.DisplayIntent.DisplayText}"); GameEvents.RaiseEnemyWouldAct(new EnemyWouldActEvent(enemy)); StateChanged?.Invoke(); yield return Wait(CombatTuning.EnemyTelegraphDelaySeconds); - ExecuteEnemyIntent(enemy); + ExecuteEnemyIntent(enemy, i); + enemy.MarkTurnSurvived(); GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent)); StateChanged?.Invoke(); if (CheckLoss()) @@ -789,14 +792,15 @@ namespace KernelPanic.Combat } PickEnemyIntents(); + ResetEnemyTurnLethalMarkers(); StateChanged?.Invoke(); } private void GarbageCollect() { - selectedEnemyIndex = -1; - pendingTargetCard = null; - statusEffects.Tick(playerState, StatusTickTiming.EndOfTurn, playerState, damagePipeline); + _selectedEnemyIndex = -1; + _pendingTargetCard = null; + _statusEffects.Tick(_playerState, StatusTickTiming.EndOfTurn, _playerState, _damagePipeline); if (CheckLoss()) { return; @@ -816,16 +820,16 @@ namespace KernelPanic.Combat private IEnumerator DrawOpeningHandSequenced() { - int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, playerState.Ram); + int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, _playerState.Ram); 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) + if (CombatTuning.MinimumHandFloor > 0 && _handController.Cards.Count < CombatTuning.MinimumHandFloor && _deckController.AvailableToDrawCount > 0) { - requested = Mathf.Max(requested, CombatTuning.MinimumHandFloor - handController.Cards.Count); + requested = Mathf.Max(requested, CombatTuning.MinimumHandFloor - _handController.Cards.Count); } yield return DrawCardsToHandSequenced(requested, "turn draw"); @@ -833,23 +837,23 @@ namespace KernelPanic.Combat private IEnumerator DrawCardsToHandSequenced(int requestedCount, string label) { - if (handController == null || playerState == null || requestedCount <= 0) + if (_handController == null || _playerState == null || requestedCount <= 0) { yield break; } - int room = handController.RemainingRam; + int room = _handController.RemainingRam; if (room <= 0) { Log($"{label}: hand full"); yield break; } - IReadOnlyList drawn = deckController.Draw(requestedCount); + IReadOnlyList drawn = _deckController.Draw(requestedCount); int added = 0; for (int i = 0; i < drawn.Count; i++) { - if (handController.Add(drawn[i])) + if (_handController.Add(drawn[i])) { added++; StateChanged?.Invoke(); @@ -857,11 +861,11 @@ namespace KernelPanic.Combat } else { - deckController.AddToDrawPile(drawn[i], shuffle: false); + _deckController.AddToDrawPile(drawn[i], shuffle: false); } } - if (added == 0 && deckController.AvailableToDrawCount == 0) + if (added == 0 && _deckController.AvailableToDrawCount == 0) { Log($"{label}: no cards to draw"); } @@ -877,73 +881,73 @@ namespace KernelPanic.Combat private void TryApplyUbuntuAptUpdate() { - if (!IsDistro("ubuntu") || handController == null || playerState == null || handController.RemainingRam <= 0) + 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)) + int lookCount = _runConfig.DistroVersion >= 2 ? 3 : 2; + if (!_deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card)) { return; } - if (runConfig.DistroVersion >= 4) + if (_runConfig.DistroVersion >= 4) { card.TemporaryCostDelta -= 1; } - if (handController.Add(card)) + if (_handController.Add(card)) { Log($"apt update: staged {GetCardName(card)} from top {lookCount}"); } else { - deckController.AddToDrawPile(card, shuffle: false); + _deckController.AddToDrawPile(card, shuffle: false); } } private void TryApplyUbuntuEmptyHandRefill() { - if (!IsDistro("ubuntu") || runConfig.DistroVersion < 5 || ubuntuEmptyHandRefillUsed || handController == null || handController.Cards.Count > 0) + if (!IsDistro("ubuntu") || _runConfig.DistroVersion < 5 || _ubuntuEmptyHandRefillUsed || _handController == null || _handController.Cards.Count > 0) { return; } - ubuntuEmptyHandRefillUsed = true; - StartCoroutine(DrawCardsToHandSequenced(playerState.Ram, "ubuntu 24.04 refill")); + _ubuntuEmptyHandRefillUsed = true; + StartCoroutine(DrawCardsToHandSequenced(_playerState.Ram, "ubuntu 24.04 refill")); } private bool CanApplyFedoraBonus() { - return CanApplyFedoraPassiveBonus() || rawhideBonusCharges > 0; + return CanApplyFedoraPassiveBonus() || _rawhideBonusCharges > 0; } private void DecayJavaWarmupForNextWave() { - if (javaCardsPlayedThisCombat <= 0) + if (_javaCardsPlayedThisCombat <= 0) { return; } - javaCardsPlayedThisCombat = Mathf.Max(0, javaCardsPlayedThisCombat - 1); - Log($"Java JIT cooled: discount now -{javaCardsPlayedThisCombat}"); + _javaCardsPlayedThisCombat = Mathf.Max(0, _javaCardsPlayedThisCombat - 1); + Log($"Java JIT cooled: discount now -{_javaCardsPlayedThisCombat}"); } private bool CanApplyFedoraPassiveBonus() { - if (!IsDistro("fedora") || playerState == null) + 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; + 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) { - if (!IsDistro("fedora") || runConfig.DistroVersion < 3 || card == null || !card.WasFirstCardThisTurn) + if (!IsDistro("fedora") || _runConfig.DistroVersion < 3 || card == null || !card.WasFirstCardThisTurn) { return; } @@ -962,14 +966,14 @@ namespace KernelPanic.Combat return cost; } - IReadOnlyList packages = runConfig?.EquippedPackages; + IReadOnlyList packages = _runConfig?.EquippedPackages; if (packages == null) { return cost; } - int nextTurnCard = cardsPlayedThisTurn + 1; - int nextWaveCard = cardsPlayedThisWave + 1; + int nextTurnCard = _cardsPlayedThisTurn + 1; + int nextWaveCard = _cardsPlayedThisWave + 1; for (int i = 0; i < packages.Count; i++) { PackageInstance package = packages[i]; @@ -978,7 +982,7 @@ namespace KernelPanic.Combat continue; } - PackageEffectData effect = package.EffectFor(runConfig?.Distro?.Id); + PackageEffectData effect = package.EffectFor(_runConfig?.Distro?.Id); switch (effect.Kind) { case PackageEffectKind.FirstCardEachWaveCostReduction: @@ -1013,7 +1017,7 @@ namespace KernelPanic.Combat } state.JavaScriptFlatDamageBonus = 0; - IReadOnlyList packages = runConfig?.EquippedPackages; + IReadOnlyList packages = _runConfig?.EquippedPackages; if (packages == null) { return; @@ -1021,7 +1025,7 @@ namespace KernelPanic.Combat for (int i = 0; i < packages.Count; i++) { - PackageEffectData effect = packages[i] == null ? default : packages[i].EffectFor(runConfig?.Distro?.Id); + PackageEffectData effect = packages[i] == null ? default : packages[i].EffectFor(_runConfig?.Distro?.Id); if (effect.Kind == PackageEffectKind.JavaScriptFlatDamage) { state.JavaScriptFlatDamageBonus += Mathf.Max(0, effect.Amount); @@ -1031,7 +1035,7 @@ namespace KernelPanic.Combat private void ApplyPackageWaveStartEffects() { - IReadOnlyList packages = runConfig?.EquippedPackages; + IReadOnlyList packages = _runConfig?.EquippedPackages; if (packages == null) { return; @@ -1045,7 +1049,7 @@ namespace KernelPanic.Combat continue; } - PackageEffectData effect = package.EffectFor(runConfig?.Distro?.Id); + PackageEffectData effect = package.EffectFor(_runConfig?.Distro?.Id); switch (effect.Kind) { case PackageEffectKind.WaveDraw: @@ -1055,13 +1059,13 @@ namespace KernelPanic.Combat GrantPlayerShield(Mathf.Max(0, effect.Amount), package.Definition.DisplayName); break; case PackageEffectKind.FirstTurnEachWaveShield: - if (turnNumberThisWave == 0) + if (_turnNumberThisWave == 0) { GrantPlayerShield(Mathf.Max(0, effect.Amount), package.Definition.DisplayName); } break; case PackageEffectKind.FirstTurnFirstWaveDraw: - if (CurrentWaveNumber == 1 && turnNumberThisWave == 0) + if (CurrentWaveNumber == 1 && _turnNumberThisWave == 0) { StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), package.Definition.DisplayName)); } @@ -1077,28 +1081,28 @@ namespace KernelPanic.Combat { if (HasPackageEffect(PackageEffectKind.EveryNthTurnShield, out PackageEffectData effect) && effect.Threshold > 0 - && turnNumberThisWave % 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) + && _turnNumberThisWave % effect.Threshold == 0) { StartCoroutine(DrawCardsToHandSequenced(Mathf.Max(0, effect.Amount), "udev")); } - if (turnNumberThisWave == 1 + if (_turnNumberThisWave == 1 && HasPackageEffect(PackageEffectKind.FirstTurnEachWaveCycle, out effect) && effect.Amount > 0) { - playerState.Cycles += effect.Amount; + _playerState.Cycles += effect.Amount; Log($"apt-fast: gained {effect.Amount} Cycle"); } if (HasPackageEffect(PackageEffectKind.StartTurnNoDebuffShield, out effect) - && !HasHarmfulStatus(playerState)) + && !HasHarmfulStatus(_playerState)) { GrantPlayerShield(Mathf.Max(0, effect.Amount), "auditd"); } @@ -1111,79 +1115,79 @@ namespace KernelPanic.Combat private void ApplyPackageCardPlayedEffects(CardInstance card) { - if (card == null || playerState == null) + if (card == null || _playerState == null) { return; } if (HasPackageEffect(PackageEffectKind.ThirdCardEachTurnGenerate, out PackageEffectData aptEffect) - && cardsPlayedThisTurn == Mathf.Max(1, aptEffect.Threshold)) + && _cardsPlayedThisTurn == Mathf.Max(1, aptEffect.Threshold)) { GeneratePackageLanguageCard("apt"); if (aptEffect.RefundCycle) { - playerState.Cycles += 1; + _playerState.Cycles += 1; Log("apt refunded 1 Cycle"); } } if (HasPackageEffect(PackageEffectKind.DnfFedoraPassive, out PackageEffectData dnfEffect) - && cardsPlayedThisTurn == 1 + && _cardsPlayedThisTurn == 1 && dnfEffect.Amount > 0) { - playerState.FlatEffectBonus += dnfEffect.Amount; + _playerState.FlatEffectBonus += dnfEffect.Amount; Log($"DNF wave buff +{dnfEffect.Amount} damage"); } - if (!packageInterpreterQueueShieldTriggeredThisWave + if (!_packageInterpreterQueueShieldTriggeredThisWave && card.Definition.ResolutionTrack == ResolutionTrack.InterpreterQueue && HasPackageEffect(PackageEffectKind.FirstInterpreterQueueCardEachWaveShield, out PackageEffectData queueEffect)) { - packageInterpreterQueueShieldTriggeredThisWave = true; + _packageInterpreterQueueShieldTriggeredThisWave = true; GrantPlayerShield(Mathf.Max(0, queueEffect.Amount), "dbus"); } - if (!packageNativeDamageTriggeredThisWave + if (!_packageNativeDamageTriggeredThisWave && card.Definition.ResolutionTrack == ResolutionTrack.Native && HasPackageEffect(PackageEffectKind.FirstNativeCardEachWaveFlatDamage, out PackageEffectData nativeEffect) && nativeEffect.Amount > 0) { - packageNativeDamageTriggeredThisWave = true; - playerState.FlatEffectBonus += nativeEffect.Amount; + _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 + && _cardsPlayedThisWave % cycleEffect.Threshold == 0 && cycleEffect.Amount > 0) { - playerState.Cycles += cycleEffect.Amount; + _playerState.Cycles += cycleEffect.Amount; Log($"systemd-timer: gained {cycleEffect.Amount} Cycle"); } } public void GrantPlayerShield(int amount, string label) { - if (playerState == null || amount <= 0) + if (_playerState == null || amount <= 0) { return; } int total = amount; - if (!packageShieldBonusTriggeredThisTurn + if (!_packageShieldBonusTriggeredThisTurn && HasPackageEffect(PackageEffectKind.FirstShieldEachTurnBonus, out PackageEffectData effect)) { int bonus = Mathf.Max(0, effect.Amount); if (bonus > 0) { total += bonus; - packageShieldBonusTriggeredThisTurn = true; + _packageShieldBonusTriggeredThisTurn = true; Log($"SELinux shield +{bonus}"); } } - playerState.Shield += total; + _playerState.Shield += total; if (!string.IsNullOrWhiteSpace(label)) { Log($"{label}: gained {total} Shield"); @@ -1192,14 +1196,14 @@ namespace KernelPanic.Combat private void GeneratePackageLanguageCard(string label) { - if (handController == null || handController.RemainingRam <= 0) + if (_handController == null || _handController.RemainingRam <= 0) { return; } - Language language = RandomRoll.RollRange(0, 1, new RollContext(playerState)) == 0 - ? runConfig.PrimaryLanguage - : runConfig.SecondaryLanguage; + Language language = RandomRoll.RollRange(0, 1, new RollContext(_playerState)) == 0 + ? _runConfig.PrimaryLanguage + : _runConfig.SecondaryLanguage; if (!TryCreateGeneratedCard(language, Rarity.Common, out CardInstance generated)) { @@ -1207,7 +1211,7 @@ namespace KernelPanic.Combat return; } - if (handController.Add(generated)) + if (_handController.Add(generated)) { Log($"{label}: generated {GetCardName(generated)} at 0c"); StateChanged?.Invoke(); @@ -1216,7 +1220,7 @@ namespace KernelPanic.Combat private void GenerateAurPackageCard(PackageEffectData effect) { - if (handController == null || handController.RemainingRam <= 0) + if (_handController == null || _handController.RemainingRam <= 0) { return; } @@ -1224,7 +1228,7 @@ namespace KernelPanic.Combat 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; + IReadOnlyList source = _generatedCardPool.Count > 0 ? _generatedCardPool : _runConfig?.StartingDeck; if (source == null) { Log("AUR: TODO card-generation pool unavailable"); @@ -1255,14 +1259,14 @@ namespace KernelPanic.Combat return; } - int index = RandomRoll.RollRange(0, pool.Count - 1, new RollContext(playerState)); + 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)) + if (_handController.Add(generated)) { string discount = effect.Amount > 0 ? " at -1c" : string.Empty; Log($"AUR: generated {GetCardName(generated)}{discount}"); @@ -1272,74 +1276,153 @@ namespace KernelPanic.Combat private void HandlePackageDamageDealt(DamageDealtEvent payload) { - if (payload.Target == playerState && playerState != null && playerState.ArchRollingReleaseRecoveredThisHit) + HandleEnemyDamageDealt(payload); + + if (payload.Target == _playerState && _playerState != null && _playerState.ArchRollingReleaseRecoveredThisHit) { - playerState.ArchRollingReleaseRecoveredThisHit = false; + _playerState.ArchRollingReleaseRecoveredThisHit = false; string recovery = "rolling release: recovered at 1 Uptime"; - if (playerState.ArchRollingReleaseShieldOnSave > 0 || playerState.ArchRollingReleaseCyclesOnSave > 0) + if (_playerState.ArchRollingReleaseShieldOnSave > 0 || _playerState.ArchRollingReleaseCyclesOnSave > 0) { - recovery += $", +{playerState.ArchRollingReleaseShieldOnSave} Shield, +{playerState.ArchRollingReleaseCyclesOnSave} Cycle"; + recovery += $", +{_playerState.ArchRollingReleaseShieldOnSave} Shield, +{_playerState.ArchRollingReleaseCyclesOnSave} Cycle"; } Log(recovery); StateChanged?.Invoke(); } - if (payload.Target != playerState + if (payload.Target != _playerState || payload.UptimeDamage <= 0 - || packageTimeshiftTriggeredThisWave + || _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) + int restoreTarget = Mathf.CeilToInt(_playerState.MaxUptime * (thresholdPercent / 100f)); + if (_playerState.CurrentUptime >= restoreTarget) { return; } - packageTimeshiftTriggeredThisWave = true; - playerState.CurrentUptime = restoreTarget; + _packageTimeshiftTriggeredThisWave = true; + _playerState.CurrentUptime = restoreTarget; if (effect.CleanseDebuffs) { - statusEffects.CleanseHarmful(playerState); + _statusEffects.CleanseHarmful(_playerState); } Log($"Timeshift restored uptime to {thresholdPercent}%"); StateChanged?.Invoke(); } - private void ResetArchBtwAtEndOfTurn() + private void HandleEnemyDamageDealt(DamageDealtEvent payload) { - if (!IsDistro("arch") || playerState == null || playerState.ArchBtwStacks <= 0 || ArchBtwPersistsThisWave()) + EnemyInstance enemy = FindEnemy(payload.Target); + if (enemy == null) { return; } - playerState.ArchBtwStacks = 0; + 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) + 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; + _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 + && (_runConfig.DistroVersion >= 5 || HasOnDistroPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData effect) && effect.PersistArchBtw); } @@ -1347,7 +1430,7 @@ namespace KernelPanic.Combat private bool HasOnDistroPackageEffect(PackageEffectKind kind, out PackageEffectData effect) { effect = default; - IReadOnlyList packages = runConfig?.EquippedPackages; + IReadOnlyList packages = _runConfig?.EquippedPackages; if (packages == null) { return false; @@ -1356,12 +1439,12 @@ namespace KernelPanic.Combat for (int i = 0; i < packages.Count; i++) { PackageInstance package = packages[i]; - if (package?.Definition == null || !package.Definition.IsIntendedFor(runConfig?.Distro?.Id)) + if (package?.Definition == null || !package.Definition.IsIntendedFor(_runConfig?.Distro?.Id)) { continue; } - effect = package.EffectFor(runConfig?.Distro?.Id); + effect = package.EffectFor(_runConfig?.Distro?.Id); if (effect.Kind == kind) { return true; @@ -1374,7 +1457,7 @@ namespace KernelPanic.Combat private bool HasPackageEffect(PackageEffectKind kind, out PackageEffectData effect) { effect = default; - IReadOnlyList packages = runConfig?.EquippedPackages; + IReadOnlyList packages = _runConfig?.EquippedPackages; if (packages == null) { return false; @@ -1388,7 +1471,7 @@ namespace KernelPanic.Combat continue; } - effect = package.EffectFor(runConfig?.Distro?.Id); + effect = package.EffectFor(_runConfig?.Distro?.Id); if (effect.Kind == kind) { return true; @@ -1429,21 +1512,21 @@ namespace KernelPanic.Combat return; } - state.forceMaxRolls = IsDistro("mint"); + 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.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 ? 3 : 2; - state.ArchRollingReleaseShieldOnSave = IsDistro("arch") && runConfig.DistroVersion >= 4 ? 15 : 0; - state.ArchRollingReleaseCyclesOnSave = IsDistro("arch") && runConfig.DistroVersion >= 4 ? 2 : 0; + state.ArchBtwDamagePerStack = IsDistro("arch") && _runConfig.DistroVersion >= 2 ? 2 : 1; + state.ArchMakepkgBtwMultiplier = IsDistro("arch") && _runConfig.DistroVersion >= 3 ? 3 : 2; + 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); + return string.Equals(_runConfig?.Distro?.Id, id, StringComparison.OrdinalIgnoreCase); } public bool CanPlayerReceiveCardShield(CardInstance card) @@ -1464,7 +1547,7 @@ namespace KernelPanic.Combat private bool IsCurrentDistroExclusiveCard(CardInstance card) { CardDefinition definition = card?.Definition; - IReadOnlyList exclusiveCards = runConfig?.Distro?.ExclusiveCards; + IReadOnlyList exclusiveCards = _runConfig?.Distro?.ExclusiveCards; if (definition == null || exclusiveCards == null) { return false; @@ -1490,7 +1573,7 @@ namespace KernelPanic.Combat private CombatContext BuildContext(CardInstance card, IReadOnlyList targets) { - return new CombatContext(card, playerState, targets, this, damagePipeline, statusEffects, deckController, handController, enemies); + return new CombatContext(card, _playerState, targets, this, _damagePipeline, _statusEffects, _deckController, _handController, _enemies); } private void ExecuteCardEffects(CardInstance card, IReadOnlyList targets) @@ -1510,7 +1593,7 @@ namespace KernelPanic.Combat private void ResolveCardRiders(CardInstance card) { - if (card == null || card.DrawRider <= 0 || handController == null || playerState == null) + if (card == null || card.DrawRider <= 0 || _handController == null || _playerState == null) { return; } @@ -1530,9 +1613,9 @@ namespace KernelPanic.Combat return LivingEnemyStates(); } - if (selectedEnemyIndex >= 0 && selectedEnemyIndex < enemies.Count && !enemies[selectedEnemyIndex].State.IsDefeated) + if (_selectedEnemyIndex >= 0 && _selectedEnemyIndex < _enemies.Count && !_enemies[_selectedEnemyIndex].State.IsDefeated) { - return new[] { enemies[selectedEnemyIndex].State }; + return new[] { _enemies[_selectedEnemyIndex].State }; } if (RequiresEnemyTarget(card)) @@ -1585,7 +1668,7 @@ namespace KernelPanic.Combat private List LivingEnemyStates() { - return enemies + return _enemies .Where(enemy => enemy.State != null && !enemy.State.IsDefeated) .Select(enemy => enemy.State) .ToList(); @@ -1593,11 +1676,11 @@ namespace KernelPanic.Combat private CombatantState FirstLivingEnemyState() { - for (int i = 0; i < enemies.Count; i++) + for (int i = 0; i < _enemies.Count; i++) { - if (enemies[i].State != null && !enemies[i].State.IsDefeated) + if (_enemies[i].State != null && !_enemies[i].State.IsDefeated) { - return enemies[i].State; + return _enemies[i].State; } } @@ -1606,68 +1689,83 @@ namespace KernelPanic.Combat private void RemoveDefeatedEnemies() { - for (int i = enemies.Count - 1; i >= 0; i--) + for (int i = _enemies.Count - 1; i >= 0; i--) { - if (enemies[i].State.IsDefeated) + if (_enemies[i].State.IsDefeated) { - Log($"{enemies[i].Name} exited"); - runManager?.AddBits(CombatTuning.BitsPerKill); - enemies.RemoveAt(i); + if (_enemies[i].PendingRevive) + { + continue; + } + + Log($"{_enemies[i].Name} exited"); + _runManager?.AddBits(CombatTuning.BitsPerKill); + _enemies.RemoveAt(i); } } - if (selectedEnemyIndex >= enemies.Count) + if (_selectedEnemyIndex >= _enemies.Count) { - selectedEnemyIndex = -1; + _selectedEnemyIndex = -1; } } private void SpawnStructuralEnemies() { int wave = CurrentWaveNumber; - int count = Mathf.Clamp(CombatTuning.BaseEnemiesPerWave + ((wave - 1) * CombatTuning.AdditionalEnemiesPerWave), CombatTuning.BaseEnemiesPerWave, CombatTuning.MaxEnemiesPerWave); + 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; for (int i = 0; i < count; i++) { - // TODO: Replace this low-HP additive wave stub with the real encounter/difficulty system. - int baseUptime = RandomRoll.RollRange(CombatTuning.BaseEnemyUptimeMin, CombatTuning.BaseEnemyUptimeMax, RollContext.None); - int uptime = baseUptime + waveUptimeBonus; - enemies.Add(new EnemyInstance("zombie_process", uptime, CreateZombieProcessIntentPool(wave, i))); + SpawnEnemy(CreatePlaceholderArchetypeId(wave, i), waveUptimeBonus); } } - private static IReadOnlyList CreateZombieProcessIntentPool(int wave, int slotIndex) + private void SpawnEnemy(string archetypeId, int uptimeBonus) { - // TODO: Replace this code-shaped pool with EnemyDefinition-provided intent data. - int attack = GetZombieProcessAttack(wave, slotIndex); - return new[] - { - new EnemyIntent(EnemyIntentKind.Attack, attack, attack, Language.C, "attack", ">"), - new EnemyIntent(EnemyIntentKind.Defend, 3 + wave, 3 + wave, Language.C, "defend", "#"), - new EnemyIntent(EnemyIntentKind.StatusAttack, CombatTuning.EnemyStatusAttackDamage, CombatTuning.EnemyStatusAttackDamage, Language.C, "leak", "!", false, StatusType.MemoryLeak, 1, CombatTuning.EnemyStatusDuration) - }; + EnemyArchetypeDescriptor archetype = EnemyArchetypeCatalog.Get(archetypeId); + int baseUptime = RandomRoll.RollRange(archetype.BaseUptimeMin, archetype.BaseUptimeMax, RollContext.None); + _enemies.Add(new EnemyInstance(archetype, baseUptime + Mathf.Max(0, uptimeBonus))); } - private static int GetZombieProcessAttack(int wave, int slotIndex) + private static string CreatePlaceholderArchetypeId(int wave, int slotIndex) { - int safeWave = Mathf.Max(1, wave); - int waveGrowth = (safeWave - 1) / Mathf.Max(1, CombatTuning.EnemyAttackGrowthEveryWaves); - int slotVariance = CombatTuning.EnemySlotAttackVariance <= 0 ? 0 : slotIndex % (CombatTuning.EnemySlotAttackVariance + 1); - return CombatTuning.BaseEnemyAttack + waveGrowth + slotVariance; + // 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", "daemon" }, + _ => new[] { "zombie_process", "memory_leak", "fork_bomb", "firewalld", "daemon", "cron_job" } + }; + + if (slotIndex == 0) + { + return wave % 2 == 0 ? "firewalld" : "zombie_process"; + } + + if (slotIndex == 1 && wave >= 2) + { + return wave % 3 == 0 ? "fork_bomb" : "memory_leak"; + } + + int index = RandomRoll.RollRange(0, pool.Length - 1, RollContext.None); + return pool[index]; } private void PickEnemyIntents() { - for (int i = 0; i < enemies.Count; i++) + for (int i = 0; i < _enemies.Count; i++) { - if (!enemies[i].State.IsDefeated) + if (!_enemies[i].State.IsDefeated) { - enemies[i].PickNextIntent(); + _enemies[i].PickNextIntent(); } } } - private void ExecuteEnemyIntent(EnemyInstance enemy) + private void ExecuteEnemyIntent(EnemyInstance enemy, int enemyIndex) { EnemyIntent intent = enemy.CurrentIntent; int value = intent.MinValue == intent.MaxValue @@ -1678,37 +1776,91 @@ namespace KernelPanic.Combat { case EnemyIntentKind.Attack: value = ApplyIncomingAttackMitigation(value); - DamageResult attackResult = damagePipeline.DealDamage(new DamageRequest(enemy.State, playerState, value, intent.DamageType, intent.TrueDamage, false)); + 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: - enemy.State.Shield += value; + if (enemy.HasBehavior(EnemyBehaviorFlags.DefendAllies)) + { + DefendSelfAndAdjacent(enemyIndex, value); + } + else + { + enemy.State.Shield += value; + } break; case EnemyIntentKind.StatusAttack: - DamageResult statusResult = damagePipeline.DealDamage(new DamageRequest(enemy.State, playerState, value, intent.DamageType, intent.TrueDamage, false)); + 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); + _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: - Log("TODO: special intent needs enemy-specific behavior hooks."); + ExecuteSpecialEnemyIntent(enemy); break; } + + enemy.AdvanceCountdownAfterAction(); + } + + private void ExecuteSpecialEnemyIntent(EnemyInstance enemy) + { + if (enemy.HasBehavior(EnemyBehaviorFlags.Split)) + { + TrySplitForkBomb(enemy); + return; + } + + Log("TODO: special intent needs enemy-specific behavior hooks."); + } + + 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 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) + if (_playerState == null || _playerState.IncomingAttackHalfCharges <= 0) { return value; } - playerState.IncomingAttackHalfCharges--; + _playerState.IncomingAttackHalfCharges--; int mitigated = Mathf.CeilToInt(Mathf.Max(0, value) * 0.5f); Log($"SELinux enforcing halved attack {value}->{mitigated}"); return mitigated; @@ -1716,17 +1868,17 @@ namespace KernelPanic.Combat private void ResolveDelayedEndOfTurnEffects() { - for (int i = delayedEffects.Count - 1; i >= 0; i--) + for (int i = _delayedEffects.Count - 1; i >= 0; i--) { - DelayedCombatEffect effect = delayedEffects[i]; + DelayedCombatEffect effect = _delayedEffects[i]; effect.GarbageCollectionsRemaining--; if (effect.GarbageCollectionsRemaining > 0) { - delayedEffects[i] = effect; + _delayedEffects[i] = effect; continue; } - delayedEffects.RemoveAt(i); + _delayedEffects.RemoveAt(i); switch (effect.Kind) { case DelayedCombatEffectKind.UpdateManagerRepeat: @@ -1744,7 +1896,7 @@ namespace KernelPanic.Combat List targets = LivingEnemyStates(); for (int i = 0; i < targets.Count; i++) { - damagePipeline.DealDamage(new DamageRequest(playerState, targets[i], amount, language, false, true)); + _damagePipeline.DealDamage(new DamageRequest(_playerState, targets[i], amount, language, false, true)); } Log($"{label}: dealt {amount} to all enemies"); @@ -1753,24 +1905,24 @@ namespace KernelPanic.Combat private void RestoreTimeshift(int snapshot) { - if (playerState == null || playerState.IsDefeated || playerState.CurrentUptime >= snapshot) + if (_playerState == null || _playerState.IsDefeated || _playerState.CurrentUptime >= snapshot) { return; } int target = snapshot; - if (IsDistro("mint") && runConfig.DistroVersion >= 3) + if (IsDistro("mint") && _runConfig.DistroVersion >= 3) { - target = Mathf.Min(playerState.MaxUptime, snapshot + 2); + target = Mathf.Min(_playerState.MaxUptime, snapshot + 2); } - if (IsDistro("mint") && runConfig.DistroVersion >= 5) + if (IsDistro("mint") && _runConfig.DistroVersion >= 5) { - target = playerState.MaxUptime; + target = _playerState.MaxUptime; } - int restored = target - playerState.CurrentUptime; - playerState.CurrentUptime = target; + int restored = target - _playerState.CurrentUptime; + _playerState.CurrentUptime = target; Log($"timeshift restored {restored} uptime"); } @@ -1781,15 +1933,15 @@ namespace KernelPanic.Combat private bool CheckLoss() { - if (runLost || playerState == null || !playerState.IsDefeated) + if (_runLost || _playerState == null || !_playerState.IsDefeated) { - return runLost; + return _runLost; } - runLost = true; - awaitingWaveContinue = false; - selectedEnemyIndex = -1; - pendingTargetCard = null; + _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"); @@ -1799,14 +1951,14 @@ namespace KernelPanic.Combat private bool CheckWin() { - if (awaitingWaveContinue || runLost || enemies.Count > 0) + if (_awaitingWaveContinue || _runLost || _enemies.Count > 0) { - return awaitingWaveContinue; + return _awaitingWaveContinue; } - awaitingWaveContinue = true; - selectedEnemyIndex = -1; - pendingTargetCard = null; + _awaitingWaveContinue = true; + _selectedEnemyIndex = -1; + _pendingTargetCard = null; int clearedWave = CurrentWaveNumber; GameEvents.RaiseEncounterWon(new EncounterWonEvent(clearedWave)); GameEvents.RaiseWaveCleared(new WaveClearedEvent(clearedWave)); @@ -1838,7 +1990,7 @@ namespace KernelPanic.Combat return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName; } - private bool IsCombatPaused => awaitingWaveContinue || runLost; + private bool IsCombatPaused => _awaitingWaveContinue || _runLost; private enum DelayedCombatEffectKind { diff --git a/Assets/Scripts/Combat/CombatTuning.cs b/Assets/Scripts/Combat/CombatTuning.cs index 0d617f5..c5eb8c7 100644 --- a/Assets/Scripts/Combat/CombatTuning.cs +++ b/Assets/Scripts/Combat/CombatTuning.cs @@ -12,7 +12,7 @@ namespace KernelPanic.Combat public const int BaseEnemyUptimeMin = 5; public const int BaseEnemyUptimeMax = 10; - public const int EnemyUptimeGrowthPerWave = 2; + public const int EnemyUptimeGrowthPerWave = 1; public const int BaseEnemyAttack = 3; public const int EnemyAttackGrowthEveryWaves = 2; diff --git a/Assets/Scripts/Combat/CombatantState.cs b/Assets/Scripts/Combat/CombatantState.cs index 3f4c95d..2e48d3b 100644 --- a/Assets/Scripts/Combat/CombatantState.cs +++ b/Assets/Scripts/Combat/CombatantState.cs @@ -26,7 +26,7 @@ namespace KernelPanic.Combat public int Cycles { get; set; } public int MaxCycles { get; set; } public bool IsDefeated { get; set; } - public bool forceMaxRolls { get; set; } + public bool ForceMaxRolls { get; set; } public bool IgnoreDamageMultipliers { get; set; } public bool AllowFlatDamageBuffs { get; set; } public int FlatEffectBonus { get; set; } diff --git a/Assets/Scripts/Combat/DamagePipeline.cs b/Assets/Scripts/Combat/DamagePipeline.cs index b44f21b..307a93b 100644 --- a/Assets/Scripts/Combat/DamagePipeline.cs +++ b/Assets/Scripts/Combat/DamagePipeline.cs @@ -116,7 +116,7 @@ namespace KernelPanic.Combat private static int ApplyCrit(int amount, DamageRequest request, out bool wasCritical) { wasCritical = false; - if (!request.CanCrit || amount <= 0 || request.Source != null && request.Source.forceMaxRolls) + if (!request.CanCrit || amount <= 0 || request.Source != null && request.Source.ForceMaxRolls) { return amount; } diff --git a/Assets/Scripts/Combat/EnemyArchetype.cs b/Assets/Scripts/Combat/EnemyArchetype.cs new file mode 100644 index 0000000..e099803 --- /dev/null +++ b/Assets/Scripts/Combat/EnemyArchetype.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using KernelPanic.Core; +using UnityEngine; + +namespace KernelPanic.Combat +{ + [Flags] + public enum EnemyBehaviorFlags + { + None = 0, + Revive = 1 << 0, + Split = 1 << 1, + Grow = 1 << 2, + ObfuscateIntent = 1 << 3, + DefendAllies = 1 << 4, + Countdown = 1 << 5 + } + + public sealed class EnemyArchetypeDescriptor + { + public EnemyArchetypeDescriptor( + string id, + string displayName, + int baseUptimeMin, + int baseUptimeMax, + EnemyBehaviorFlags behaviorFlags, + IReadOnlyList intentPool) + { + Id = string.IsNullOrWhiteSpace(id) ? "unknown_process" : id; + DisplayName = string.IsNullOrWhiteSpace(displayName) ? Id : displayName; + BaseUptimeMin = Mathf.Max(1, baseUptimeMin); + BaseUptimeMax = Mathf.Max(BaseUptimeMin, baseUptimeMax); + BehaviorFlags = behaviorFlags; + IntentPool = intentPool ?? Array.Empty(); + } + + public string Id { get; } + public string DisplayName { get; } + public int BaseUptimeMin { get; } + public int BaseUptimeMax { get; } + public EnemyBehaviorFlags BehaviorFlags { get; } + public IReadOnlyList IntentPool { get; } + } + + /// + /// Runtime archetype construction site. Future EnemyDefinition assets should replace this table + /// by providing the same descriptor data: id, display name, base Uptime range, intents, and flags. + /// + public static class EnemyArchetypeCatalog + { + public const int ZombieReapThresholdPercent = 30; + public const int MemoryLeakAttackGrowthPerTurn = 1; + public const int ForkBombTotalCap = 5; + public const int CronInitialCountdown = 2; + + private static readonly EnemyArchetypeDescriptor ZombieProcess = new( + "zombie_process", + "Zombie Process", + 6, + 10, + EnemyBehaviorFlags.Revive, + new[] + { + new EnemyIntent(EnemyIntentKind.Attack, 3, 4, Language.C, "attack", "!") + }); + + private static readonly EnemyArchetypeDescriptor MemoryLeak = new( + "memory_leak", + "Memory Leak", + 4, + 7, + EnemyBehaviorFlags.Grow, + new[] + { + new EnemyIntent(EnemyIntentKind.StatusAttack, 1, 1, Language.C, "leak", "!", false, StatusType.MemoryLeak, 1, CombatTuning.EnemyStatusDuration) + }); + + private static readonly EnemyArchetypeDescriptor ForkBomb = new( + "fork_bomb", + "Fork Bomb", + 3, + 5, + EnemyBehaviorFlags.Split, + new[] + { + new EnemyIntent(EnemyIntentKind.Special, 0, 0, Language.C, "fork", ":", false, StatusType.MemoryLeak, 0, -1, "split") + }); + + private static readonly EnemyArchetypeDescriptor Daemon = new( + "daemon", + "Daemon", + 10, + 14, + EnemyBehaviorFlags.ObfuscateIntent, + new[] + { + new EnemyIntent(EnemyIntentKind.Attack, 6, 8, Language.C, "daemon hit", "!") + }); + + private static readonly EnemyArchetypeDescriptor Defender = new( + "firewalld", + "firewalld", + 5, + 8, + EnemyBehaviorFlags.DefendAllies, + new[] + { + new EnemyIntent(EnemyIntentKind.Defend, 3, 5, Language.C, "shield adjacent", "#"), + new EnemyIntent(EnemyIntentKind.Defend, 3, 5, Language.C, "shield adjacent", "#"), + new EnemyIntent(EnemyIntentKind.Attack, 1, 2, Language.C, "poke", "!") + }); + + private static readonly EnemyArchetypeDescriptor CronJob = new( + "cron_job", + "Cron Job", + 6, + 9, + EnemyBehaviorFlags.Countdown, + new[] + { + new EnemyIntent(EnemyIntentKind.Attack, 10, 12, Language.C, "cron fire", "@") + }); + + public static EnemyArchetypeDescriptor Get(string id) + { + return id switch + { + "memory_leak" => MemoryLeak, + "fork_bomb" => ForkBomb, + "daemon" => Daemon, + "firewalld" => Defender, + "cron_job" => CronJob, + _ => ZombieProcess + }; + } + } +} diff --git a/Assets/Scripts/Combat/EnemyInstance.cs b/Assets/Scripts/Combat/EnemyInstance.cs index e16d047..b5bba6e 100644 --- a/Assets/Scripts/Combat/EnemyInstance.cs +++ b/Assets/Scripts/Combat/EnemyInstance.cs @@ -10,24 +10,57 @@ namespace KernelPanic.Combat private readonly List _intentPool = new(); public EnemyInstance(string name, int maxUptime, IEnumerable intentPool) + : this(new EnemyArchetypeDescriptor(name, name, maxUptime, maxUptime, EnemyBehaviorFlags.None, new List(intentPool ?? System.Array.Empty())), maxUptime) { - Name = string.IsNullOrWhiteSpace(name) ? "unknown_process" : name; + } + + public EnemyInstance(EnemyArchetypeDescriptor archetype, int maxUptime) + { + Archetype = archetype ?? EnemyArchetypeCatalog.Get("zombie_process"); + Name = Archetype.DisplayName; State = new CombatantState(maxUptime, 0, 0); - if (intentPool != null) + if (Archetype.IntentPool != null) { - _intentPool.AddRange(intentPool); + _intentPool.AddRange(Archetype.IntentPool); } + + CountdownRemaining = HasBehavior(EnemyBehaviorFlags.Countdown) ? EnemyArchetypeCatalog.CronInitialCountdown : 0; } public string Name { get; } + public EnemyArchetypeDescriptor Archetype { get; } + public string ArchetypeId => Archetype.Id; public int CurrentUptime => State.CurrentUptime; public int MaxUptime => State.MaxUptime; public CombatantState State { get; } public IReadOnlyList IntentPool => _intentPool; public EnemyIntent CurrentIntent { get; private set; } + public int TurnsAlive { get; private set; } + public int CountdownRemaining { get; private set; } + public bool IntentRevealed { get; private set; } + public bool HasRevived { get; private set; } + public bool PendingRevive { get; private set; } + public bool Reaped { get; private set; } + public bool LethalHitThisTurn { get; private set; } + public bool HasPendingMarker => PendingRevive || HasRevived; + public EnemyIntent DisplayIntent => BuildDisplayIntent(); public void PickNextIntent() { + if (PendingRevive) + { + CurrentIntent = new EnemyIntent(EnemyIntentKind.Special, 0, 0, KernelPanic.Core.Language.C, "reviving", "~", false, KernelPanic.Core.StatusType.MemoryLeak, 0, -1, "next"); + return; + } + + if (HasBehavior(EnemyBehaviorFlags.Countdown)) + { + CurrentIntent = CountdownRemaining > 0 + ? new EnemyIntent(EnemyIntentKind.Special, CountdownRemaining, CountdownRemaining, KernelPanic.Core.Language.C, "countdown", "@") + : _intentPool.Count > 0 ? _intentPool[0] : default; + return; + } + if (_intentPool.Count == 0) { CurrentIntent = default; @@ -36,6 +69,92 @@ namespace KernelPanic.Combat int index = RandomRoll.RollRange(0, _intentPool.Count - 1, new RollContext(State)); CurrentIntent = _intentPool[index]; + if (HasBehavior(EnemyBehaviorFlags.Grow)) + { + int growth = TurnsAlive * EnemyArchetypeCatalog.MemoryLeakAttackGrowthPerTurn; + CurrentIntent = CurrentIntent.WithValues(CurrentIntent.MinValue + growth, CurrentIntent.MaxValue + growth, "growing leak"); + } + } + + public bool HasBehavior(EnemyBehaviorFlags behavior) + { + return (Archetype.BehaviorFlags & behavior) != 0; + } + + public void MarkDamaged() + { + if (HasBehavior(EnemyBehaviorFlags.ObfuscateIntent)) + { + IntentRevealed = true; + } + } + + public void MarkTurnSurvived() + { + TurnsAlive++; + } + + public void ResetTurnLethalMarker() + { + LethalHitThisTurn = false; + } + + public void MarkLethalHit() + { + LethalHitThisTurn = true; + } + + public void MarkPendingRevive() + { + PendingRevive = true; + State.IsDefeated = false; + State.CurrentUptime = 0; + State.Shield = 0; + CurrentIntent = new EnemyIntent(EnemyIntentKind.Special, 0, 0, KernelPanic.Core.Language.C, "reviving", "~", false, KernelPanic.Core.StatusType.MemoryLeak, 0, -1, "next"); + } + + public void ReviveAtHalfUptime() + { + PendingRevive = false; + HasRevived = true; + State.IsDefeated = false; + State.CurrentUptime = UnityEngine.Mathf.Max(1, UnityEngine.Mathf.CeilToInt(State.MaxUptime * 0.5f)); + PickNextIntent(); + } + + public void MarkReaped() + { + PendingRevive = false; + Reaped = true; + State.IsDefeated = true; + State.CurrentUptime = 0; + } + + public void AdvanceCountdownAfterAction() + { + if (!HasBehavior(EnemyBehaviorFlags.Countdown)) + { + return; + } + + CountdownRemaining = CurrentIntent.Kind == EnemyIntentKind.Attack + ? EnemyArchetypeCatalog.CronInitialCountdown + : UnityEngine.Mathf.Max(0, CountdownRemaining - 1); + } + + private EnemyIntent BuildDisplayIntent() + { + if (PendingRevive) + { + return new EnemyIntent(EnemyIntentKind.Special, 0, 0, KernelPanic.Core.Language.C, "reviving", "~", false, KernelPanic.Core.StatusType.MemoryLeak, 0, -1, "next"); + } + + if (HasBehavior(EnemyBehaviorFlags.ObfuscateIntent) && !IntentRevealed) + { + return new EnemyIntent(EnemyIntentKind.Special, 0, 0, KernelPanic.Core.Language.C, "unknown", "?", false, KernelPanic.Core.StatusType.MemoryLeak, 0, -1, "?"); + } + + return CurrentIntent; } } } diff --git a/Assets/Scripts/Combat/EnemyIntent.cs b/Assets/Scripts/Combat/EnemyIntent.cs index 4ae2a30..bcf51f9 100644 --- a/Assets/Scripts/Combat/EnemyIntent.cs +++ b/Assets/Scripts/Combat/EnemyIntent.cs @@ -26,7 +26,8 @@ namespace KernelPanic.Combat bool trueDamage = false, StatusType statusType = StatusType.MemoryLeak, int statusStacks = 0, - int statusDuration = -1) + int statusDuration = -1, + string valueTextOverride = null) { Kind = kind; MinValue = minValue; @@ -38,6 +39,7 @@ namespace KernelPanic.Combat StatusType = statusType; StatusStacks = statusStacks; StatusDuration = statusDuration; + ValueTextOverride = valueTextOverride; } public EnemyIntentKind Kind { get; } @@ -50,8 +52,27 @@ namespace KernelPanic.Combat public StatusType StatusType { get; } public int StatusStacks { get; } public int StatusDuration { get; } + public string ValueTextOverride { get; } - public string ValueText => MinValue == MaxValue ? MinValue.ToString() : $"{MinValue}-{MaxValue}"; + public string ValueText => string.IsNullOrWhiteSpace(ValueTextOverride) + ? MinValue == MaxValue ? MinValue.ToString() : $"{MinValue}-{MaxValue}" + : ValueTextOverride; public string DisplayText => $"{IconKey} {DisplayLabel} {ValueText}"; + + public EnemyIntent WithValues(int minValue, int maxValue, string displayLabel = null, string iconKey = null, string valueTextOverride = null) + { + return new EnemyIntent( + Kind, + minValue, + maxValue, + DamageType, + displayLabel ?? DisplayLabel, + iconKey ?? IconKey, + TrueDamage, + StatusType, + StatusStacks, + StatusDuration, + valueTextOverride); + } } } diff --git a/Assets/Scripts/Combat/RandomRoll.cs b/Assets/Scripts/Combat/RandomRoll.cs index 3d1da91..9a17b4b 100644 --- a/Assets/Scripts/Combat/RandomRoll.cs +++ b/Assets/Scripts/Combat/RandomRoll.cs @@ -36,7 +36,7 @@ namespace KernelPanic.Combat } // Mint's passive will flip this bool; no passive sets it in this scaffold. - if (context.Source != null && context.Source.forceMaxRolls) + if (context.Source != null && context.Source.ForceMaxRolls) { return max; } diff --git a/Assets/Scripts/Meta/GachaPullContext.cs b/Assets/Scripts/Meta/GachaPullContext.cs index 614f234..6c93b9c 100644 --- a/Assets/Scripts/Meta/GachaPullContext.cs +++ b/Assets/Scripts/Meta/GachaPullContext.cs @@ -10,34 +10,34 @@ namespace KernelPanic.Meta /// public static class GachaPullContext { - private static PendingGachaPull pendingPull; - private static CompletedGachaPull completedPull; + private static PendingGachaPull _pendingPull; + private static CompletedGachaPull _completedPull; - public static bool HasPendingPull => pendingPull != null; - public static bool HasCompletedPull => completedPull != null; + public static bool HasPendingPull => _pendingPull != null; + public static bool HasCompletedPull => _completedPull != null; public static void SetPending(string bannerId, int pullCount, int entropyTokenCount, DistroDatabase distroDatabase, PackageDatabase packageDatabase, string focusUnitId) { - pendingPull = new PendingGachaPull(bannerId, pullCount, entropyTokenCount, distroDatabase, packageDatabase, focusUnitId); - completedPull = null; + _pendingPull = new PendingGachaPull(bannerId, pullCount, entropyTokenCount, distroDatabase, packageDatabase, focusUnitId); + _completedPull = null; } public static bool TryConsumePending(out PendingGachaPull pull) { - pull = pendingPull; - pendingPull = null; + pull = _pendingPull; + _pendingPull = null; return pull != null; } public static void SetCompleted(CompletedGachaPull pull) { - completedPull = pull; + _completedPull = pull; } public static bool TryConsumeCompleted(out CompletedGachaPull pull) { - pull = completedPull; - completedPull = null; + pull = _completedPull; + _completedPull = null; return pull != null; } } diff --git a/Assets/Scripts/Meta/SaveService.cs b/Assets/Scripts/Meta/SaveService.cs index 1c2b0f1..7e02e13 100644 --- a/Assets/Scripts/Meta/SaveService.cs +++ b/Assets/Scripts/Meta/SaveService.cs @@ -90,7 +90,7 @@ namespace KernelPanic.Meta // with access to PackageDatabase) consumes this once per load to grant Cache for the extras, // then clears it. See MainMenuController.LoadMetaState. [NonSerialized] - public List collapsedPackageDuplicates = new(); + public List CollapsedPackageDuplicates = new(); public static SaveData CreateDefault() { @@ -236,7 +236,7 @@ namespace KernelPanic.Meta private void NormalizeOwnedPackages() { - collapsedPackageDuplicates.Clear(); + CollapsedPackageDuplicates.Clear(); Dictionary highestById = new(StringComparer.OrdinalIgnoreCase); Dictionary extraCountById = new(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < ownedPackages.Count; i++) @@ -265,7 +265,7 @@ namespace KernelPanic.Meta ownedPackages = new List(highestById.Values); foreach (KeyValuePair extra in extraCountById) { - collapsedPackageDuplicates.Add(new CollapsedPackageDuplicate(extra.Key, extra.Value)); + CollapsedPackageDuplicates.Add(new CollapsedPackageDuplicate(extra.Key, extra.Value)); } ownedPackageIds.Clear(); diff --git a/Assets/Scripts/Run/RunContext.cs b/Assets/Scripts/Run/RunContext.cs index 6ca03b5..538050a 100644 --- a/Assets/Scripts/Run/RunContext.cs +++ b/Assets/Scripts/Run/RunContext.cs @@ -12,9 +12,9 @@ namespace KernelPanic.Run /// public static class RunContext { - private static PendingRun pendingRun; + private static PendingRun _pendingRun; - public static bool HasPendingRun => pendingRun != null; + public static bool HasPendingRun => _pendingRun != null; public static void Set(DistroDefinition distro, IReadOnlyList equippedCards, Language primaryLanguage, Language secondaryLanguage) { @@ -28,44 +28,44 @@ namespace KernelPanic.Run public static void Set(DistroDefinition distro, IReadOnlyList equippedCards, IReadOnlyList equippedPackages, Language primaryLanguage, Language secondaryLanguage, int distroVersion) { - pendingRun = new PendingRun(distro, equippedCards, ToInstances(equippedPackages), primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion); + _pendingRun = new PendingRun(distro, equippedCards, ToInstances(equippedPackages), primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion); } public static void Set(DistroDefinition distro, IReadOnlyList equippedCards, IReadOnlyList equippedPackages, Language primaryLanguage, Language secondaryLanguage, int distroVersion) { - pendingRun = new PendingRun(distro, equippedCards, equippedPackages, primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion); + _pendingRun = new PendingRun(distro, equippedCards, equippedPackages, primaryLanguage, secondaryLanguage, Environment.TickCount, distroVersion); } public static bool TryCreateRunConfig(LanguageDeckDatabase languageDeckDatabase, out RunConfig config) { - if (pendingRun == null || pendingRun.Distro == null) + if (_pendingRun == null || _pendingRun.Distro == null) { config = null; return false; } - config = pendingRun.CreateRunConfig(languageDeckDatabase); - pendingRun = null; + config = _pendingRun.CreateRunConfig(languageDeckDatabase); + _pendingRun = null; return true; } private sealed class PendingRun { - private readonly DistroDefinition distro; - private readonly List equippedCards = new(); - private readonly List equippedPackages = new(); - private readonly Language primaryLanguage; - private readonly Language secondaryLanguage; - private readonly int runSeed; - private readonly int distroVersion; + private readonly DistroDefinition _distro; + private readonly List _equippedCards = new(); + private readonly List _equippedPackages = new(); + private readonly Language _primaryLanguage; + private readonly Language _secondaryLanguage; + private readonly int _runSeed; + private readonly int _distroVersion; public PendingRun(DistroDefinition distro, IReadOnlyList cards, IReadOnlyList packages, Language primaryLanguage, Language secondaryLanguage, int runSeed, int distroVersion) { - this.distro = distro; - this.primaryLanguage = primaryLanguage; - this.secondaryLanguage = secondaryLanguage; - this.runSeed = runSeed; - this.distroVersion = distroVersion; + this._distro = distro; + this._primaryLanguage = primaryLanguage; + this._secondaryLanguage = secondaryLanguage; + this._runSeed = runSeed; + this._distroVersion = distroVersion; if (cards == null) { @@ -76,7 +76,7 @@ namespace KernelPanic.Run { if (cards[i] != null && !cards[i].IsRunOnly) { - equippedCards.Add(cards[i]); + _equippedCards.Add(cards[i]); } } @@ -89,19 +89,19 @@ namespace KernelPanic.Run { if (packages[i]?.Definition != null) { - equippedPackages.Add(packages[i]); + _equippedPackages.Add(packages[i]); } } } - public DistroDefinition Distro => distro; + public DistroDefinition Distro => _distro; public RunConfig CreateRunConfig(LanguageDeckDatabase languageDeckDatabase) { - List startingDeck = new(equippedCards); - AddLanguageDeck(languageDeckDatabase, primaryLanguage, startingDeck); - AddLanguageDeck(languageDeckDatabase, secondaryLanguage, startingDeck); - return new RunConfig(distro, primaryLanguage, secondaryLanguage, startingDeck, equippedPackages, runSeed, distroVersion); + List startingDeck = new(_equippedCards); + AddLanguageDeck(languageDeckDatabase, _primaryLanguage, startingDeck); + AddLanguageDeck(languageDeckDatabase, _secondaryLanguage, startingDeck); + return new RunConfig(_distro, _primaryLanguage, _secondaryLanguage, startingDeck, _equippedPackages, _runSeed, _distroVersion); } private static void AddLanguageDeck(LanguageDeckDatabase languageDeckDatabase, Language language, List target) diff --git a/Assets/Scripts/Run/RunManager.cs b/Assets/Scripts/Run/RunManager.cs index aa3649a..b33593f 100644 --- a/Assets/Scripts/Run/RunManager.cs +++ b/Assets/Scripts/Run/RunManager.cs @@ -18,39 +18,39 @@ namespace KernelPanic.Run [SerializeField] private bool isRunActive; [SerializeField] private bool isPlayerDead; - private readonly List runDeck = new(); - private readonly List repositoryOffers = new(); - private readonly HashSet soldOfferKeys = new(StringComparer.OrdinalIgnoreCase); - private int bits; - private int rerollsThisVisit; - private int maxUptimeBonus; - private int maxCyclesBonus; - private int ramBonus; - private int packageMaxUptimeBonus; - private int packageMaxCyclesBonus; - private int packageRamBonus; - private int wavesCleared; - private int accruedBandwidth; - private int accruedEntropy; - private bool repositoryVisitActive; - private bool rewardsSettled; + private readonly List _runDeck = new(); + private readonly List _repositoryOffers = new(); + private readonly HashSet _soldOfferKeys = new(StringComparer.OrdinalIgnoreCase); + private int _bits; + private int _rerollsThisVisit; + private int _maxUptimeBonus; + private int _maxCyclesBonus; + private int _ramBonus; + private int _packageMaxUptimeBonus; + private int _packageMaxCyclesBonus; + private int _packageRamBonus; + private int _wavesCleared; + private int _accruedBandwidth; + private int _accruedEntropy; + private bool _repositoryVisitActive; + private bool _rewardsSettled; public int CurrentWaveNumber => currentWaveNumber; public bool IsRunActive => isRunActive; public bool IsPlayerDead => isPlayerDead; public RunConfig CurrentConfig { get; private set; } - public IReadOnlyList RunDeck => runDeck; - public IReadOnlyList RepositoryOffers => repositoryOffers; - public bool RepositoryVisitActive => repositoryVisitActive; - public int Bits => bits; - public int RerollCost => CombatTuning.RerollBaseCost + (rerollsThisVisit * CombatTuning.RerollCostStep); - public int MaxUptimeBonus => maxUptimeBonus; - public int MaxCyclesBonus => maxCyclesBonus; - public int RamBonus => ramBonus; - public int WavesCleared => wavesCleared; - public int AccruedBandwidth => accruedBandwidth; - public int AccruedEntropy => accruedEntropy; - public bool RewardsSettled => rewardsSettled; + public IReadOnlyList RunDeck => _runDeck; + public IReadOnlyList RepositoryOffers => _repositoryOffers; + public bool RepositoryVisitActive => _repositoryVisitActive; + public int Bits => _bits; + public int RerollCost => CombatTuning.RerollBaseCost + (_rerollsThisVisit * CombatTuning.RerollCostStep); + public int MaxUptimeBonus => _maxUptimeBonus; + public int MaxCyclesBonus => _maxCyclesBonus; + public int RamBonus => _ramBonus; + public int WavesCleared => _wavesCleared; + public int AccruedBandwidth => _accruedBandwidth; + public int AccruedEntropy => _accruedEntropy; + public bool RewardsSettled => _rewardsSettled; public event Action RepositoryChanged; @@ -72,18 +72,18 @@ namespace KernelPanic.Run isRunActive = true; isPlayerDead = false; currentWaveNumber = 1; - bits = 0; - rerollsThisVisit = 0; - maxUptimeBonus = 0; - maxCyclesBonus = 0; - ramBonus = 0; - packageMaxUptimeBonus = 0; - packageMaxCyclesBonus = 0; - packageRamBonus = 0; - wavesCleared = 0; - accruedBandwidth = 0; - accruedEntropy = 0; - rewardsSettled = false; + _bits = 0; + _rerollsThisVisit = 0; + _maxUptimeBonus = 0; + _maxCyclesBonus = 0; + _ramBonus = 0; + _packageMaxUptimeBonus = 0; + _packageMaxCyclesBonus = 0; + _packageRamBonus = 0; + _wavesCleared = 0; + _accruedBandwidth = 0; + _accruedEntropy = 0; + _rewardsSettled = false; ApplyKernelPackageBonuses(config); InitializeRunDeck(config); StartCombat(); @@ -101,40 +101,40 @@ namespace KernelPanic.Run return; } - bits += amount; + _bits += amount; RepositoryChanged?.Invoke(); } public void GenerateRepositoryOffers(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase) { - repositoryOffers.Clear(); - rerollsThisVisit = 0; - soldOfferKeys.Clear(); + _repositoryOffers.Clear(); + _rerollsThisVisit = 0; + _soldOfferKeys.Clear(); FillRepositoryOffers(cardDatabase, languageDeckDatabase); - repositoryVisitActive = true; + _repositoryVisitActive = true; RepositoryChanged?.Invoke(); } public bool RerollRepositoryOffers(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase) { int cost = RerollCost; - if (bits < cost) + if (_bits < cost) { return false; } - bits -= cost; - rerollsThisVisit++; - repositoryOffers.Clear(); + _bits -= cost; + _rerollsThisVisit++; + _repositoryOffers.Clear(); FillRepositoryOffers(cardDatabase, languageDeckDatabase); - repositoryVisitActive = true; + _repositoryVisitActive = true; RepositoryChanged?.Invoke(); return true; } public bool BuyOffer(RepositoryOffer offer, CombatantState playerState) { - if (offer == null || offer.Sold || bits < offer.Price) + if (offer == null || offer.Sold || _bits < offer.Price) { return false; } @@ -152,43 +152,43 @@ namespace KernelPanic.Run return false; } - bits -= offer.Price; + _bits -= offer.Price; offer.MarkSold(); - soldOfferKeys.Add(OfferKey(offer)); + _soldOfferKeys.Add(OfferKey(offer)); RepositoryChanged?.Invoke(); return true; } public bool RemoveCard(CardInstance card) { - if (card == null || bits < CombatTuning.RemoveCardCost || runDeck.Count <= 1) + if (card == null || _bits < CombatTuning.RemoveCardCost || _runDeck.Count <= 1) { return false; } - if (!runDeck.Remove(card)) + if (!_runDeck.Remove(card)) { return false; } - bits -= CombatTuning.RemoveCardCost; + _bits -= CombatTuning.RemoveCardCost; RepositoryChanged?.Invoke(); return true; } public int EffectiveMaxUptime() { - return Mathf.Max(1, (CurrentConfig?.Distro?.BaseUptime ?? 1) + maxUptimeBonus + packageMaxUptimeBonus); + return Mathf.Max(1, (CurrentConfig?.Distro?.BaseUptime ?? 1) + _maxUptimeBonus + _packageMaxUptimeBonus); } public int EffectiveRam() { - return Mathf.Max(1, (CurrentConfig?.Distro?.BaseRam ?? 1) + ramBonus + packageRamBonus); + return Mathf.Max(1, (CurrentConfig?.Distro?.BaseRam ?? 1) + _ramBonus + _packageRamBonus); } public int EffectiveMaxCycles() { - return Mathf.Max(1, (CurrentConfig?.Distro?.BaseCyclesPerTurn ?? 1) + maxCyclesBonus + packageMaxCyclesBonus); + return Mathf.Max(1, (CurrentConfig?.Distro?.BaseCyclesPerTurn ?? 1) + _maxCyclesBonus + _packageMaxCyclesBonus); } public static PackageStatImpact CalculatePackageStatImpact(DistroDefinition distro, IReadOnlyList packages) @@ -232,35 +232,35 @@ namespace KernelPanic.Run private void ApplyKernelPackageBonuses(RunConfig config) { PackageStatImpact impact = CalculatePackageStatImpact(config?.Distro, config?.EquippedPackages); - packageMaxUptimeBonus = impact.UptimeBonus; - packageMaxCyclesBonus = impact.CyclesBonus; - packageRamBonus = impact.RamBonus; + _packageMaxUptimeBonus = impact.UptimeBonus; + _packageMaxCyclesBonus = impact.CyclesBonus; + _packageRamBonus = impact.RamBonus; } public bool TrySettleRunRewards(out int bandwidth, out int entropy) { - bandwidth = accruedBandwidth; - entropy = accruedEntropy; - if (rewardsSettled) + bandwidth = _accruedBandwidth; + entropy = _accruedEntropy; + if (_rewardsSettled) { return false; } - rewardsSettled = true; + _rewardsSettled = true; return bandwidth > 0 || entropy > 0; } private void HandleWaveCleared(WaveClearedEvent payload) { int clearedWave = Mathf.Max(1, payload.WaveNumber); - wavesCleared = Mathf.Max(wavesCleared, clearedWave); - accruedBandwidth += CalculateBandwidthReward(clearedWave); - accruedEntropy += CalculateEntropyReward(clearedWave); + _wavesCleared = Mathf.Max(_wavesCleared, clearedWave); + _accruedBandwidth += CalculateBandwidthReward(clearedWave); + _accruedEntropy += CalculateEntropyReward(clearedWave); currentWaveNumber = Mathf.Max(currentWaveNumber + 1, payload.WaveNumber + 1); - repositoryOffers.Clear(); - rerollsThisVisit = 0; - soldOfferKeys.Clear(); - repositoryVisitActive = false; + _repositoryOffers.Clear(); + _rerollsThisVisit = 0; + _soldOfferKeys.Clear(); + _repositoryVisitActive = false; GameEvents.RaiseWaveAdvanced(new WaveAdvancedEvent(currentWaveNumber)); } @@ -268,13 +268,13 @@ namespace KernelPanic.Run { isRunActive = false; isPlayerDead = payload.PlayerDied; - bits = 0; - repositoryOffers.Clear(); - repositoryVisitActive = false; - runDeck.Clear(); - maxUptimeBonus = 0; - maxCyclesBonus = 0; - ramBonus = 0; + _bits = 0; + _repositoryOffers.Clear(); + _repositoryVisitActive = false; + _runDeck.Clear(); + _maxUptimeBonus = 0; + _maxCyclesBonus = 0; + _ramBonus = 0; RepositoryChanged?.Invoke(); } @@ -291,7 +291,7 @@ namespace KernelPanic.Run private void InitializeRunDeck(RunConfig config) { - runDeck.Clear(); + _runDeck.Clear(); if (config?.StartingDeck == null) { return; @@ -302,7 +302,7 @@ namespace KernelPanic.Run CardDefinition definition = config.StartingDeck[i]; if (definition != null && !definition.IsRunOnly) { - runDeck.Add(new CardInstance(definition)); + _runDeck.Add(new CardInstance(definition)); } } } @@ -310,16 +310,16 @@ namespace KernelPanic.Run private void FillRepositoryOffers(CardDatabase cardDatabase, LanguageDeckDatabase languageDeckDatabase) { int guard = 0; - while (repositoryOffers.Count < CombatTuning.ShopSize && guard < 80) + while (_repositoryOffers.Count < CombatTuning.ShopSize && guard < 80) { guard++; RepositoryOffer offer = CreateRandomOffer(cardDatabase, languageDeckDatabase); - if (offer == null || soldOfferKeys.Contains(OfferKey(offer)) || repositoryOffers.Any(existing => IsDuplicateOffer(existing, offer))) + if (offer == null || _soldOfferKeys.Contains(OfferKey(offer)) || _repositoryOffers.Any(existing => IsDuplicateOffer(existing, offer))) { continue; } - repositoryOffers.Add(offer); + _repositoryOffers.Add(offer); } } @@ -353,7 +353,7 @@ namespace KernelPanic.Run private RepositoryOffer CreateUpgradeOffer() { - List candidates = runDeck.Where(card => card != null && card.CanReceiveRepositoryUpgrade).ToList(); + List candidates = _runDeck.Where(card => card != null && card.CanReceiveRepositoryUpgrade).ToList(); if (candidates.Count == 0) { return CreateStatOffer(); @@ -463,7 +463,7 @@ namespace KernelPanic.Run return false; } - runDeck.Add(new CardInstance(card)); + _runDeck.Add(new CardInstance(card)); return true; } @@ -488,7 +488,7 @@ namespace KernelPanic.Run switch (kind) { case RunStatUpgradeKind.MaxCycles: - maxCyclesBonus += CombatTuning.StatUpgradeMaxCycles; + _maxCyclesBonus += CombatTuning.StatUpgradeMaxCycles; if (playerState != null) { playerState.MaxCycles += CombatTuning.StatUpgradeMaxCycles; @@ -496,7 +496,7 @@ namespace KernelPanic.Run } return true; case RunStatUpgradeKind.MaxUptime: - maxUptimeBonus += CombatTuning.StatUpgradeMaxUptime; + _maxUptimeBonus += CombatTuning.StatUpgradeMaxUptime; if (playerState != null) { playerState.MaxUptime += CombatTuning.StatUpgradeMaxUptime; @@ -511,7 +511,7 @@ namespace KernelPanic.Run playerState.CurrentUptime = Mathf.Min(playerState.MaxUptime, playerState.CurrentUptime + CombatTuning.StatUpgradeHeal); return true; case RunStatUpgradeKind.Ram: - ramBonus += CombatTuning.StatUpgradeRam; + _ramBonus += CombatTuning.StatUpgradeRam; if (playerState != null) { playerState.Ram += CombatTuning.StatUpgradeRam; @@ -525,9 +525,9 @@ namespace KernelPanic.Run public struct PackageStatImpact { - private readonly List uptimeSources; - private readonly List ramSources; - private readonly List cyclesSources; + private readonly List _uptimeSources; + private readonly List _ramSources; + private readonly List _cyclesSources; public PackageStatImpact(int baseUptime, int baseRam, int baseCycles) { @@ -537,9 +537,9 @@ namespace KernelPanic.Run UptimeBonus = 0; RamBonus = 0; CyclesBonus = 0; - uptimeSources = new List(); - ramSources = new List(); - cyclesSources = new List(); + _uptimeSources = new List(); + _ramSources = new List(); + _cyclesSources = new List(); } public int BaseUptime { get; } @@ -551,9 +551,9 @@ namespace KernelPanic.Run public int FinalUptime => Mathf.Max(1, BaseUptime + UptimeBonus); public int FinalRam => Mathf.Max(1, BaseRam + RamBonus); public int FinalCycles => Mathf.Max(1, BaseCycles + CyclesBonus); - public IReadOnlyList UptimeSources => uptimeSources; - public IReadOnlyList RamSources => ramSources; - public IReadOnlyList CyclesSources => cyclesSources; + public IReadOnlyList UptimeSources => _uptimeSources; + public IReadOnlyList RamSources => _ramSources; + public IReadOnlyList CyclesSources => _cyclesSources; public void AddUptime(int amount, PackageDefinition source) { @@ -563,7 +563,7 @@ namespace KernelPanic.Run } UptimeBonus += amount; - AddSource(source, uptimeSources); + AddSource(source, _uptimeSources); } public void AddRam(int amount, PackageDefinition source) @@ -574,7 +574,7 @@ namespace KernelPanic.Run } RamBonus += amount; - AddSource(source, ramSources); + AddSource(source, _ramSources); } public void AddCycles(int amount, PackageDefinition source) @@ -585,7 +585,7 @@ namespace KernelPanic.Run } CyclesBonus += amount; - AddSource(source, cyclesSources); + AddSource(source, _cyclesSources); } private static void AddSource(PackageDefinition source, List sources) diff --git a/Assets/Scripts/UI/CombatSceneController.cs b/Assets/Scripts/UI/CombatSceneController.cs index ee947d2..4a9eec5 100644 --- a/Assets/Scripts/UI/CombatSceneController.cs +++ b/Assets/Scripts/UI/CombatSceneController.cs @@ -12,7 +12,7 @@ namespace KernelPanic.UI { /// /// Runtime-built GameScene combat UI. Intent icons use a compact terminal vocabulary: - /// ! attack/status attack, # defend, + buff, * special. + /// ! attack/status attack, # defend, + buff, * special, ? hidden, ~ reviving, : split, @ countdown. /// [RequireComponent(typeof(UIDocument))] [RequireComponent(typeof(RunManager))] @@ -29,56 +29,56 @@ namespace KernelPanic.UI [SerializeField] private CardDatabase cardDatabase; [SerializeField] private LanguageDeckDatabase languageDeckDatabase; - private UIDocument document; - private RunManager runManager; - private CombatManager combatManager; - private SaveService saveService; - private VisualElement root; - private VisualElement statusBar; - private Label waveLabel; - private Label phaseLabel; - private Label entropyLabel; - private Label bitsLabel; - private Label bandwidthLabel; - private Label rootCreditsLabel; - private Label seedLabel; - private VisualElement playerPanel; - private VisualElement interpreterStrip; - private VisualElement lazyStackPile; - private VisualElement tokenArea; - private VisualElement enemyRow; - private VisualElement handRow; - private VisualElement turnResourceGrid; - private Label logLabel; - private VisualElement feedbackLayer; - private VisualElement damageVignette; - private VisualElement overlay; - private Color distroAccent; - private readonly Dictionary combatantElements = new(); - private readonly Dictionary handCardElements = new(); - private readonly Dictionary queueChipElements = new(); - private readonly Dictionary stackChipElements = new(); - private readonly Dictionary previousUptime = new(); - private readonly Dictionary previousShield = new(); - private readonly Dictionary feedbackBeatVersions = new(); - private readonly HashSet knownHandCards = new(); - private int queueCascadeIndex; - private int feedbackCascadeIndex; - private int feedbackBeatVersion; + private UIDocument _document; + private RunManager _runManager; + private CombatManager _combatManager; + private SaveService _saveService; + private VisualElement _root; + private VisualElement _statusBar; + private Label _waveLabel; + private Label _phaseLabel; + private Label _entropyLabel; + private Label _bitsLabel; + private Label _bandwidthLabel; + private Label _rootCreditsLabel; + private Label _seedLabel; + private VisualElement _playerPanel; + private VisualElement _interpreterStrip; + private VisualElement _lazyStackPile; + private VisualElement _tokenArea; + private VisualElement _enemyRow; + private VisualElement _handRow; + private VisualElement _turnResourceGrid; + private Label _logLabel; + private VisualElement _feedbackLayer; + private VisualElement _damageVignette; + private VisualElement _overlay; + private Color _distroAccent; + private readonly Dictionary _combatantElements = new(); + private readonly Dictionary _handCardElements = new(); + private readonly Dictionary _queueChipElements = new(); + private readonly Dictionary _stackChipElements = new(); + private readonly Dictionary _previousUptime = new(); + private readonly Dictionary _previousShield = new(); + private readonly Dictionary _feedbackBeatVersions = new(); + private readonly HashSet _knownHandCards = new(); + private int _queueCascadeIndex; + private int _feedbackCascadeIndex; + private int _feedbackBeatVersion; private void Awake() { - document = GetComponent(); - runManager = GetComponent(); - combatManager = GetComponent(); - saveService = new SaveService(); - root = document.rootVisualElement; + _document = GetComponent(); + _runManager = GetComponent(); + _combatManager = GetComponent(); + _saveService = new SaveService(); + _root = _document.rootVisualElement; LoadStyles(); ApplyTerminalFont(); BuildLayout(); - root.AddToClassList("scene-fade-hidden"); - root.schedule.Execute(() => root.RemoveFromClassList("scene-fade-hidden")).StartingIn(0); + _root.AddToClassList("scene-fade-hidden"); + _root.schedule.Execute(() => _root.RemoveFromClassList("scene-fade-hidden")).StartingIn(0); } private void ApplyTerminalFont() @@ -86,16 +86,16 @@ namespace KernelPanic.UI var font = TerminalFontResolver.Resolve(null); if (font != null) { - root.style.unityFontDefinition = new StyleFontDefinition(font); + _root.style.unityFontDefinition = new StyleFontDefinition(font); } } private void OnEnable() { - root?.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion); - combatManager.StateChanged += Refresh; - combatManager.CombatLog += HandleCombatLog; - runManager.RepositoryChanged += Refresh; + _root?.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion); + _combatManager.StateChanged += Refresh; + _combatManager.CombatLog += HandleCombatLog; + _runManager.RepositoryChanged += Refresh; GameEvents.CardPlayed += HandleCardPlayed; GameEvents.CardResolved += HandleCardResolved; GameEvents.PhaseChanged += HandlePhaseChanged; @@ -116,9 +116,9 @@ namespace KernelPanic.UI return; } - distroAccent = config.Distro == null ? Color.white : config.Distro.AccentColor; - combatManager.SetGeneratedCardPool(BuildGeneratedCardPool()); - runManager.StartRun(config); + _distroAccent = config.Distro == null ? Color.white : config.Distro.AccentColor; + _combatManager.SetGeneratedCardPool(BuildGeneratedCardPool()); + _runManager.StartRun(config); Refresh(); } @@ -143,9 +143,9 @@ namespace KernelPanic.UI private void OnDisable() { SettleRunRewardsIfNeeded(); - combatManager.StateChanged -= Refresh; - combatManager.CombatLog -= HandleCombatLog; - runManager.RepositoryChanged -= Refresh; + _combatManager.StateChanged -= Refresh; + _combatManager.CombatLog -= HandleCombatLog; + _runManager.RepositoryChanged -= Refresh; GameEvents.CardPlayed -= HandleCardPlayed; GameEvents.CardResolved -= HandleCardResolved; GameEvents.PhaseChanged -= HandlePhaseChanged; @@ -222,11 +222,11 @@ namespace KernelPanic.UI private void HandleMissingRunConfig() { - root.Clear(); - root.AddToClassList("combat-root"); + _root.Clear(); + _root.AddToClassList("combat-root"); Label error = new("combat bootstrap failed: no RunContext and no editor fallback data"); error.AddToClassList("overlay-line"); - root.Add(error); + _root.Add(error); Debug.LogError("GameScene could not start: no RunContext and no Ubuntu editor fallback data."); } @@ -235,91 +235,91 @@ namespace KernelPanic.UI StyleSheet rarityStyleSheet = Resources.Load(SharedRarityStyleResourcePath); if (rarityStyleSheet != null) { - root.styleSheets.Add(rarityStyleSheet); + _root.styleSheets.Add(rarityStyleSheet); } StyleSheet styleSheet = Resources.Load(StyleResourcePath); if (styleSheet != null) { - root.styleSheets.Add(styleSheet); + _root.styleSheets.Add(styleSheet); } StyleSheet scrollbarStyleSheet = Resources.Load(SharedScrollbarStyleResourcePath); if (scrollbarStyleSheet != null) { - root.styleSheets.Add(scrollbarStyleSheet); + _root.styleSheets.Add(scrollbarStyleSheet); } } private void BuildLayout() { - root.Clear(); - root.AddToClassList("combat-root"); - root.style.flexGrow = 1; + _root.Clear(); + _root.AddToClassList("combat-root"); + _root.style.flexGrow = 1; - statusBar = BuildStatusBar(); - root.Add(statusBar); + _statusBar = BuildStatusBar(); + _root.Add(_statusBar); VisualElement main = new(); main.AddToClassList("main-layout"); - root.Add(main); + _root.Add(main); - playerPanel = CreatePanel("player", "htop process monitor", 242); - main.Add(playerPanel); + _playerPanel = CreatePanel("player", "htop process monitor", 242); + main.Add(_playerPanel); VisualElement fieldPanel = CreatePanel("field", "resolution tracks", 0); fieldPanel.AddToClassList("field-panel"); main.Add(fieldPanel); - interpreterStrip = AddTrackZone(fieldPanel, "interpreter queue", "FIFO queued scripts", "track-zone-queue"); - lazyStackPile = AddTrackZone(fieldPanel, "lazy stack", "LIFO delayed work", "track-zone-stack"); - tokenArea = AddTrackZone(fieldPanel, "goroutines", "token lane", "track-zone-token"); + _interpreterStrip = AddTrackZone(fieldPanel, "interpreter queue", "FIFO queued scripts", "track-zone-queue"); + _lazyStackPile = AddTrackZone(fieldPanel, "lazy stack", "LIFO delayed work", "track-zone-stack"); + _tokenArea = AddTrackZone(fieldPanel, "goroutines", "token lane", "track-zone-token"); VisualElement enemiesPanel = CreatePanel("enemy processes", "intent telemetry", 438); main.Add(enemiesPanel); - enemyRow = new(); - enemyRow.AddToClassList("enemy-row"); - enemiesPanel.Add(enemyRow); + _enemyRow = new(); + _enemyRow.AddToClassList("enemy-row"); + enemiesPanel.Add(_enemyRow); VisualElement bottom = new(); bottom.AddToClassList("bottom-layout"); - root.Add(bottom); + _root.Add(bottom); VisualElement handPanel = CreatePanel("hand", "executable cards", 0); handPanel.AddToClassList("hand-panel"); bottom.Add(handPanel); - handRow = new(); - handRow.AddToClassList("hand-row"); - handPanel.Add(handRow); + _handRow = new(); + _handRow.AddToClassList("hand-row"); + handPanel.Add(_handRow); VisualElement commandPanel = CreatePanel("turn", "command", 240); commandPanel.AddToClassList("turn-panel"); bottom.Add(commandPanel); - turnResourceGrid = new(); - turnResourceGrid.AddToClassList("turn-resource-grid"); - commandPanel.Add(turnResourceGrid); + _turnResourceGrid = new(); + _turnResourceGrid.AddToClassList("turn-resource-grid"); + commandPanel.Add(_turnResourceGrid); - Button endTurn = new(() => combatManager.EndPlayerTurn()) { text = "> end-turn" }; + Button endTurn = new(() => _combatManager.EndPlayerTurn()) { text = "> end-turn" }; endTurn.AddToClassList("primary-action"); commandPanel.Add(endTurn); - logLabel = new(); - logLabel.AddToClassList("log-line"); - commandPanel.Add(logLabel); + _logLabel = new(); + _logLabel.AddToClassList("log-line"); + commandPanel.Add(_logLabel); - feedbackLayer = new(); - feedbackLayer.AddToClassList("feedback-layer"); - feedbackLayer.pickingMode = PickingMode.Ignore; - root.Add(feedbackLayer); + _feedbackLayer = new(); + _feedbackLayer.AddToClassList("feedback-layer"); + _feedbackLayer.pickingMode = PickingMode.Ignore; + _root.Add(_feedbackLayer); - damageVignette = new(); - damageVignette.AddToClassList("damage-vignette"); - damageVignette.pickingMode = PickingMode.Ignore; - root.Add(damageVignette); + _damageVignette = new(); + _damageVignette.AddToClassList("damage-vignette"); + _damageVignette.pickingMode = PickingMode.Ignore; + _root.Add(_damageVignette); - overlay = new(); - overlay.AddToClassList("overlay"); - overlay.style.display = DisplayStyle.None; - root.Add(overlay); + _overlay = new(); + _overlay.AddToClassList("overlay"); + _overlay.style.display = DisplayStyle.None; + _root.Add(_overlay); } private VisualElement BuildStatusBar() @@ -327,20 +327,20 @@ namespace KernelPanic.UI VisualElement bar = new(); bar.AddToClassList("combat-status-bar"); - waveLabel = AddStatusReadout(bar, "wave", "--", false); - phaseLabel = AddStatusReadout(bar, "phase", "--", true); - bitsLabel = AddStatusReadout(bar, "Bits", "0", false); - entropyLabel = AddStatusReadout(bar, "Entropy", "0", false); - bandwidthLabel = AddStatusReadout(bar, "Bandwidth", "0", false); - rootCreditsLabel = AddStatusReadout(bar, "Root Credits", "0", false); + _waveLabel = AddStatusReadout(bar, "wave", "--", false); + _phaseLabel = AddStatusReadout(bar, "phase", "--", true); + _bitsLabel = AddStatusReadout(bar, "Bits", "0", false); + _entropyLabel = AddStatusReadout(bar, "Entropy", "0", false); + _bandwidthLabel = AddStatusReadout(bar, "Bandwidth", "0", false); + _rootCreditsLabel = AddStatusReadout(bar, "Root Credits", "0", false); VisualElement spacer = new(); spacer.AddToClassList("status-spacer"); bar.Add(spacer); - seedLabel = new("--"); - seedLabel.AddToClassList("seed-value"); - bar.Add(seedLabel); + _seedLabel = new("--"); + _seedLabel.AddToClassList("seed-value"); + bar.Add(_seedLabel); return bar; } @@ -383,7 +383,7 @@ namespace KernelPanic.UI private void Refresh() { - if (combatManager.PlayerState == null) + if (_combatManager.PlayerState == null) { return; } @@ -399,65 +399,65 @@ namespace KernelPanic.UI private void RefreshStatus() { - SaveData data = saveService.Load(); - RunConfig config = runManager.CurrentConfig; - waveLabel.text = runManager.CurrentWaveNumber.ToString(); - phaseLabel.text = PhaseText(combatManager.CurrentPhase); - bitsLabel.text = runManager.Bits.ToString(); - entropyLabel.text = FormatWalletWithAccrual(data.entropyBalance, runManager.AccruedEntropy); - bandwidthLabel.text = FormatWalletWithAccrual(data.bandwidthBalance, runManager.AccruedBandwidth); - rootCreditsLabel.text = data.rootCredits.ToString(); - seedLabel.text = $"seed {config?.RunSeed ?? 0}"; + SaveData data = _saveService.Load(); + RunConfig config = _runManager.CurrentConfig; + _waveLabel.text = _runManager.CurrentWaveNumber.ToString(); + _phaseLabel.text = PhaseText(_combatManager.CurrentPhase); + _bitsLabel.text = _runManager.Bits.ToString(); + _entropyLabel.text = FormatWalletWithAccrual(data.entropyBalance, _runManager.AccruedEntropy); + _bandwidthLabel.text = FormatWalletWithAccrual(data.bandwidthBalance, _runManager.AccruedBandwidth); + _rootCreditsLabel.text = data.rootCredits.ToString(); + _seedLabel.text = $"seed {config?.RunSeed ?? 0}"; } private void RefreshPlayer() { - playerPanel.Clear(); - combatantElements.Remove(combatManager.PlayerState); - combatantElements[combatManager.PlayerState] = playerPanel; - playerPanel.Add(PanelHeader(DisplayName(runManager.CurrentConfig.Distro), "htop")); - ApplyAccent(playerPanel); + _playerPanel.Clear(); + _combatantElements.Remove(_combatManager.PlayerState); + _combatantElements[_combatManager.PlayerState] = _playerPanel; + _playerPanel.Add(PanelHeader(DisplayName(_runManager.CurrentConfig.Distro), "htop")); + ApplyAccent(_playerPanel); - CombatantState state = combatManager.PlayerState; - DetectBeneficialResourceFeedback(state, playerPanel); - ApplyStatusStateClasses(playerPanel, state); - playerPanel.Add(MeterBlock("uptime", state.CurrentUptime, state.MaxUptime, MeterTone.Uptime)); - playerPanel.Add(MeterBlock("shield", state.Shield, Mathf.Max(1, state.Shield), state.Shield > 0 ? MeterTone.Beneficial : MeterTone.Muted)); - playerPanel.Add(CycleBlock(state.Cycles, state.MaxCycles)); + CombatantState state = _combatManager.PlayerState; + DetectBeneficialResourceFeedback(state, _playerPanel); + ApplyStatusStateClasses(_playerPanel, state); + _playerPanel.Add(MeterBlock("uptime", state.CurrentUptime, state.MaxUptime, MeterTone.Uptime)); + _playerPanel.Add(MeterBlock("shield", state.Shield, Mathf.Max(1, state.Shield), state.Shield > 0 ? MeterTone.Beneficial : MeterTone.Muted)); + _playerPanel.Add(CycleBlock(state.Cycles, state.MaxCycles)); - Label ram = new($"ram hand cap {combatManager.HandController.UsedRam}/{combatManager.HandController.RamCapacity}"); + Label ram = new($"ram hand cap {_combatManager.HandController.UsedRam}/{_combatManager.HandController.RamCapacity}"); ram.AddToClassList("ram-note"); - playerPanel.Add(ram); - playerPanel.Add(StatusBlock(state, true)); + _playerPanel.Add(ram); + _playerPanel.Add(StatusBlock(state, true)); } private void RefreshTracks() { - queueChipElements.Clear(); - stackChipElements.Clear(); - FillCardStrip(interpreterStrip, combatManager.InterpreterQueue.Cards, "queue empty"); - FillCardStrip(lazyStackPile, combatManager.LazyStack.Cards, "stack empty"); - tokenArea.Clear(); - tokenArea.Add(EmptyState("no goroutines or tokens")); + _queueChipElements.Clear(); + _stackChipElements.Clear(); + FillCardStrip(_interpreterStrip, _combatManager.InterpreterQueue.Cards, "queue empty"); + FillCardStrip(_lazyStackPile, _combatManager.LazyStack.Cards, "stack empty"); + _tokenArea.Clear(); + _tokenArea.Add(EmptyState("no goroutines or tokens")); } private void RefreshEnemies() { - enemyRow.Clear(); - combatantElements.Clear(); - if (combatManager.PlayerState != null) + _enemyRow.Clear(); + _combatantElements.Clear(); + if (_combatManager.PlayerState != null) { - combatantElements[combatManager.PlayerState] = playerPanel; + _combatantElements[_combatManager.PlayerState] = _playerPanel; } - for (int i = 0; i < combatManager.Enemies.Count; i++) + for (int i = 0; i < _combatManager.Enemies.Count; i++) { int index = i; - EnemyInstance enemy = combatManager.Enemies[i]; - Button card = new(() => combatManager.SelectEnemy(index)); + EnemyInstance enemy = _combatManager.Enemies[i]; + Button card = new(() => _combatManager.SelectEnemy(index)); card.text = string.Empty; card.AddToClassList("enemy-card"); - bool highlighted = combatManager.PendingTargetCard != null || combatManager.SelectedEnemyIndex == index; + bool highlighted = _combatManager.PendingTargetCard != null || _combatManager.SelectedEnemyIndex == index; if (highlighted) { card.AddToClassList("enemy-card-targeted"); @@ -466,7 +466,14 @@ namespace KernelPanic.UI Label name = new(enemy.Name); name.AddToClassList("enemy-name"); card.Add(name); - combatantElements[enemy.State] = card; + if (enemy.HasPendingMarker) + { + Label marker = new(enemy.PendingRevive ? "~ reviving" : "~ revived once"); + marker.AddToClassList("status-label"); + card.Add(marker); + } + + _combatantElements[enemy.State] = card; DetectBeneficialResourceFeedback(enemy.State, card); ApplyStatusStateClasses(card, enemy.State); card.Add(MeterBlock("uptime", enemy.CurrentUptime, enemy.MaxUptime, MeterTone.Uptime)); @@ -478,107 +485,107 @@ namespace KernelPanic.UI } card.Add(StatusPipRow(enemy.State)); - card.Add(IntentPanel(enemy.CurrentIntent)); - enemyRow.Add(card); + card.Add(IntentPanel(enemy.DisplayIntent)); + _enemyRow.Add(card); } } private void RefreshHand() { - handRow.Clear(); - handCardElements.Clear(); + _handRow.Clear(); + _handCardElements.Clear(); HashSet currentHandCards = new(); - IReadOnlyList hand = combatManager.HandController.Cards; + IReadOnlyList hand = _combatManager.HandController.Cards; for (int i = 0; i < hand.Count; i++) { CardInstance card = hand[i]; currentHandCards.Add(card); VisualElement face = CardFace(card); - handCardElements[card] = face; - if (!knownHandCards.Contains(card)) + _handCardElements[card] = face; + if (!_knownHandCards.Contains(card)) { PlayElementBeat(face, "hand-card-drawn", 260); } - handRow.Add(face); + _handRow.Add(face); } - knownHandCards.Clear(); + _knownHandCards.Clear(); foreach (CardInstance card in currentHandCards) { - knownHandCards.Add(card); + _knownHandCards.Add(card); } } private void RefreshTurnPanel() { - turnResourceGrid.Clear(); - CombatantState state = combatManager.PlayerState; - IReadOnlyList hand = combatManager.HandController.Cards; - turnResourceGrid.Add(TurnStat("cycles", $"{state.Cycles}/{state.MaxCycles}")); - turnResourceGrid.Add(TurnStat("hand", $"{combatManager.HandController.UsedRam}/{combatManager.HandController.RamCapacity}")); - turnResourceGrid.Add(TurnStat("draw", combatManager.DeckController.DrawPile.Count.ToString())); - turnResourceGrid.Add(TurnStat("discard", combatManager.DeckController.DiscardPile.Count.ToString())); - turnResourceGrid.Add(TurnStat("exhaust", combatManager.DeckController.ExhaustPile.Count.ToString())); + _turnResourceGrid.Clear(); + CombatantState state = _combatManager.PlayerState; + IReadOnlyList hand = _combatManager.HandController.Cards; + _turnResourceGrid.Add(TurnStat("cycles", $"{state.Cycles}/{state.MaxCycles}")); + _turnResourceGrid.Add(TurnStat("hand", $"{_combatManager.HandController.UsedRam}/{_combatManager.HandController.RamCapacity}")); + _turnResourceGrid.Add(TurnStat("draw", _combatManager.DeckController.DrawPile.Count.ToString())); + _turnResourceGrid.Add(TurnStat("discard", _combatManager.DeckController.DiscardPile.Count.ToString())); + _turnResourceGrid.Add(TurnStat("exhaust", _combatManager.DeckController.ExhaustPile.Count.ToString())); } private void RefreshOverlay() { - if (overlay == null) + if (_overlay == null) { return; } - overlay.Clear(); - if (combatManager.RunLost) + _overlay.Clear(); + if (_combatManager.RunLost) { SettleRunRewardsIfNeeded(); - overlay.style.display = DisplayStyle.Flex; - overlay.Add(OverlayTitle("kernel panic")); - overlay.Add(OverlayLine("fatal: player uptime reached 0")); - overlay.Add(OverlayLine($"wave {runManager.CurrentWaveNumber} reached seed {runManager.CurrentConfig?.RunSeed ?? 0}")); - overlay.Add(OverlayLine($"waves cleared: {runManager.WavesCleared}")); - overlay.Add(OverlayLine($"+{runManager.AccruedBandwidth} bandwidth")); - overlay.Add(OverlayLine($"+{runManager.AccruedEntropy} entropy")); - Button returnButton = new(() => SceneLoader.LoadMainMenu(root)) { text = "> return to menu" }; + _overlay.style.display = DisplayStyle.Flex; + _overlay.Add(OverlayTitle("kernel panic")); + _overlay.Add(OverlayLine("fatal: player uptime reached 0")); + _overlay.Add(OverlayLine($"wave {_runManager.CurrentWaveNumber} reached seed {_runManager.CurrentConfig?.RunSeed ?? 0}")); + _overlay.Add(OverlayLine($"waves cleared: {_runManager.WavesCleared}")); + _overlay.Add(OverlayLine($"+{_runManager.AccruedBandwidth} bandwidth")); + _overlay.Add(OverlayLine($"+{_runManager.AccruedEntropy} entropy")); + Button returnButton = new(() => SceneLoader.LoadMainMenu(_root)) { text = "> return to menu" }; returnButton.AddToClassList("primary-action"); - overlay.Add(returnButton); + _overlay.Add(returnButton); return; } - if (combatManager.AwaitingWaveContinue) + if (_combatManager.AwaitingWaveContinue) { - if (!runManager.RepositoryVisitActive) + if (!_runManager.RepositoryVisitActive) { - runManager.GenerateRepositoryOffers(cardDatabase, languageDeckDatabase); + _runManager.GenerateRepositoryOffers(cardDatabase, languageDeckDatabase); return; } - overlay.style.display = DisplayStyle.Flex; - overlay.Add(BuildRepositoryView()); + _overlay.style.display = DisplayStyle.Flex; + _overlay.Add(BuildRepositoryView()); return; } - overlay.style.display = DisplayStyle.None; + _overlay.style.display = DisplayStyle.None; } private void SettleRunRewardsIfNeeded() { - if (runManager == null || saveService == null) + if (_runManager == null || _saveService == null) { return; } - if (!runManager.TrySettleRunRewards(out int bandwidth, out int entropy)) + if (!_runManager.TrySettleRunRewards(out int bandwidth, out int entropy)) { return; } - SaveData data = saveService.Load(); + SaveData data = _saveService.Load(); data.bandwidthBalance = Math.Max(0, data.bandwidthBalance) + bandwidth; data.entropyBalance = Math.Max(0, data.entropyBalance) + entropy; - data.RecordRunStats(runManager.CurrentConfig?.Distro?.Id, runManager.CurrentWaveNumber); - saveService.Save(data); + data.RecordRunStats(_runManager.CurrentConfig?.Distro?.Id, _runManager.CurrentWaveNumber); + _saveService.Save(data); } private VisualElement BuildRepositoryView() @@ -592,15 +599,15 @@ namespace KernelPanic.UI VisualElement offers = new(); offers.AddToClassList("repository-offers"); - if (runManager.RepositoryOffers.Count == 0) + if (_runManager.RepositoryOffers.Count == 0) { offers.Add(EmptyState("repository clean: no offers left this visit")); } else { - for (int i = 0; i < runManager.RepositoryOffers.Count; i++) + for (int i = 0; i < _runManager.RepositoryOffers.Count; i++) { - offers.Add(RepositoryOfferTile(runManager.RepositoryOffers[i])); + offers.Add(RepositoryOfferTile(_runManager.RepositoryOffers[i])); } } @@ -610,23 +617,23 @@ namespace KernelPanic.UI VisualElement footer = new(); footer.AddToClassList("repository-footer"); - Button reroll = new(() => runManager.RerollRepositoryOffers(cardDatabase, languageDeckDatabase)) + Button reroll = new(() => _runManager.RerollRepositoryOffers(cardDatabase, languageDeckDatabase)) { - text = $"apt update - reroll ({runManager.RerollCost} bits)" + text = $"apt update - reroll ({_runManager.RerollCost} bits)" }; reroll.AddToClassList("primary-action"); reroll.AddToClassList("repository-footer-action"); - reroll.EnableInClassList("repo-unaffordable", runManager.Bits < runManager.RerollCost); - reroll.SetEnabled(runManager.Bits >= runManager.RerollCost); + reroll.EnableInClassList("repo-unaffordable", _runManager.Bits < _runManager.RerollCost); + reroll.SetEnabled(_runManager.Bits >= _runManager.RerollCost); footer.Add(reroll); - Button leave = new(() => combatManager.ContinueToNextWave()) { text = "> boot next-wave" }; + Button leave = new(() => _combatManager.ContinueToNextWave()) { text = "> boot next-wave" }; leave.AddToClassList("primary-action"); leave.AddToClassList("repository-leave-action"); VisualElement leaveBlock = new(); leaveBlock.AddToClassList("repository-leave-block"); leaveBlock.Add(leave); - Label banked = new($"{runManager.Bits} bits banked"); + Label banked = new($"{_runManager.Bits} bits banked"); banked.AddToClassList("repository-banked-hint"); leaveBlock.Add(banked); footer.Add(leaveBlock); @@ -641,9 +648,9 @@ namespace KernelPanic.UI VisualElement copy = new(); copy.AddToClassList("repository-header-copy"); - Label title = new($"repository :: wave {runManager.CurrentWaveNumber - 1} cleared"); + Label title = new($"repository :: wave {_runManager.CurrentWaveNumber - 1} cleared"); title.AddToClassList("overlay-title"); - Label subtitle = new($"$ apt browse {runManager.RepositoryOffers.Count}/{CombatTuning.ShopSize} offers"); + Label subtitle = new($"$ apt browse {_runManager.RepositoryOffers.Count}/{CombatTuning.ShopSize} offers"); subtitle.AddToClassList("overlay-line"); copy.Add(title); copy.Add(subtitle); @@ -652,7 +659,7 @@ namespace KernelPanic.UI wallet.AddToClassList("repository-wallet"); Label walletLabel = new("bits wallet"); walletLabel.AddToClassList("status-label"); - Label walletValue = new($"◆ {runManager.Bits}"); + Label walletValue = new($"◆ {_runManager.Bits}"); walletValue.AddToClassList("repository-wallet-value"); wallet.Add(walletLabel); wallet.Add(walletValue); @@ -667,7 +674,7 @@ namespace KernelPanic.UI VisualElement tile = new(); tile.AddToClassList("repository-tile"); tile.AddToClassList(OfferTileClass(offer.Kind)); - bool unavailable = offer.Sold || runManager.Bits < offer.Price; + bool unavailable = offer.Sold || _runManager.Bits < offer.Price; tile.EnableInClassList("repo-unaffordable", unavailable); tile.EnableInClassList("repo-sold", offer.Sold); @@ -688,12 +695,12 @@ namespace KernelPanic.UI }; tile.Add(content); - Button buy = new(() => runManager.BuyOffer(offer, combatManager.PlayerState)) + Button buy = new(() => _runManager.BuyOffer(offer, _combatManager.PlayerState)) { text = offer.Sold ? "installed" : OfferCommandText(offer) }; buy.AddToClassList("repository-action"); - buy.SetEnabled(!offer.Sold && runManager.Bits >= offer.Price); + buy.SetEnabled(!offer.Sold && _runManager.Bits >= offer.Price); tile.Add(buy); return tile; } @@ -784,16 +791,16 @@ namespace KernelPanic.UI VisualElement rows = new(); rows.AddToClassList("repository-remove-list"); - for (int i = 0; i < runManager.RunDeck.Count; i++) + for (int i = 0; i < _runManager.RunDeck.Count; i++) { - CardInstance card = runManager.RunDeck[i]; - Button remove = new(() => runManager.RemoveCard(card)) + CardInstance card = _runManager.RunDeck[i]; + Button remove = new(() => _runManager.RemoveCard(card)) { text = $"apt remove {DisplayName(card.Definition)}" }; remove.AddToClassList("repo-remove-button"); - remove.EnableInClassList("repo-unaffordable", runManager.Bits < CombatTuning.RemoveCardCost || runManager.RunDeck.Count <= 1); - remove.SetEnabled(runManager.Bits >= CombatTuning.RemoveCardCost && runManager.RunDeck.Count > 1); + remove.EnableInClassList("repo-unaffordable", _runManager.Bits < CombatTuning.RemoveCardCost || _runManager.RunDeck.Count <= 1); + remove.SetEnabled(_runManager.Bits >= CombatTuning.RemoveCardCost && _runManager.RunDeck.Count > 1); rows.Add(remove); } @@ -806,12 +813,12 @@ namespace KernelPanic.UI Button button = new(() => PlayCardWithFeedback(card)); button.text = string.Empty; button.AddToClassList("hand-card"); - if (combatManager.PendingTargetCard == card) + if (_combatManager.PendingTargetCard == card) { button.AddToClassList("hand-card-selected"); } - if (GetDisplayCardCost(card) > combatManager.PlayerState.Cycles) + if (GetDisplayCardCost(card) > _combatManager.PlayerState.Cycles) { button.AddToClassList("hand-card-unaffordable"); } @@ -867,7 +874,7 @@ namespace KernelPanic.UI Label label = new("intent"); label.AddToClassList("intent-label"); - Label value = new($"{IntentIcon(intent.Kind)} {intent.ValueText}"); + Label value = new($"{IntentIcon(intent)} {intent.ValueText}"); value.AddToClassList("intent-value"); value.AddToClassList(className); Label detail = new(IntentText(intent)); @@ -995,13 +1002,13 @@ namespace KernelPanic.UI for (int i = 0; i < cards.Count; i++) { VisualElement chip = CardChip(cards[i]); - if (target == interpreterStrip) + if (target == _interpreterStrip) { - queueChipElements[cards[i]] = chip; + _queueChipElements[cards[i]] = chip; } - else if (target == lazyStackPile) + else if (target == _lazyStackPile) { - stackChipElements[cards[i]] = chip; + _stackChipElements[cards[i]] = chip; } target.Add(chip); @@ -1010,20 +1017,20 @@ namespace KernelPanic.UI private bool PlayCardWithFeedback(CardInstance card) { - int cyclesBefore = combatManager.PlayerState == null ? 0 : combatManager.PlayerState.Cycles; - bool played = combatManager.PlayCard(card); + int cyclesBefore = _combatManager.PlayerState == null ? 0 : _combatManager.PlayerState.Cycles; + bool played = _combatManager.PlayCard(card); if (played) { - int spent = Mathf.Max(0, cyclesBefore - (combatManager.PlayerState == null ? cyclesBefore : combatManager.PlayerState.Cycles)); + int spent = Mathf.Max(0, cyclesBefore - (_combatManager.PlayerState == null ? cyclesBefore : _combatManager.PlayerState.Cycles)); if (spent > 0) { - PlayElementBeat(turnResourceGrid, "cycles-spent-beat", 220); + PlayElementBeat(_turnResourceGrid, "cycles-spent-beat", 220); } return true; } - if (combatManager.PendingTargetCard != card && handCardElements.TryGetValue(card, out VisualElement face)) + if (_combatManager.PendingTargetCard != card && _handCardElements.TryGetValue(card, out VisualElement face)) { PlayElementBeat(face, "feedback-denied", 220); } @@ -1038,11 +1045,11 @@ namespace KernelPanic.UI return; } - VisualElement source = handCardElements.TryGetValue(payload.Card, out VisualElement cardElement) ? cardElement : null; + VisualElement source = _handCardElements.TryGetValue(payload.Card, out VisualElement cardElement) ? cardElement : null; VisualElement destination = payload.Track switch { - ResolutionTrack.InterpreterQueue => interpreterStrip, - ResolutionTrack.LazyStack => lazyStackPile, + ResolutionTrack.InterpreterQueue => _interpreterStrip, + ResolutionTrack.LazyStack => _lazyStackPile, _ => FirstTargetElement(payload.Card) }; @@ -1058,22 +1065,22 @@ namespace KernelPanic.UI if (payload.Track == ResolutionTrack.InterpreterQueue) { - int delay = UIPreferences.ReducedMotion ? 0 : Mathf.Min(queueCascadeIndex * 120, 600); - queueCascadeIndex++; + int delay = UIPreferences.ReducedMotion ? 0 : Mathf.Min(_queueCascadeIndex * 120, 600); + _queueCascadeIndex++; ScheduleFeedback(() => { - VisualElement chip = queueChipElements.TryGetValue(payload.Card, out VisualElement queuedChip) ? queuedChip : interpreterStrip; + VisualElement chip = _queueChipElements.TryGetValue(payload.Card, out VisualElement queuedChip) ? queuedChip : _interpreterStrip; PlayElementBeat(chip, "queue-chip-resolve", 260); - FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard"); + FlyCardGhost(payload.Card, chip, _turnResourceGrid, "feedback-card-discard"); }, delay); return; } if (payload.Track == ResolutionTrack.LazyStack) { - VisualElement chip = stackChipElements.TryGetValue(payload.Card, out VisualElement stackChip) ? stackChip : lazyStackPile; + VisualElement chip = _stackChipElements.TryGetValue(payload.Card, out VisualElement stackChip) ? stackChip : _lazyStackPile; PlayElementBeat(chip, "stack-chip-resolve", 320); - FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard"); + FlyCardGhost(payload.Card, chip, _turnResourceGrid, "feedback-card-discard"); } } @@ -1081,16 +1088,16 @@ namespace KernelPanic.UI { if (payload.NextPhase == TurnPhase.Interpret) { - queueCascadeIndex = 0; + _queueCascadeIndex = 0; } - feedbackCascadeIndex = 0; - ScheduleFeedback(() => PlayElementBeat(phaseLabel, "phase-pulse", 260), 0); + _feedbackCascadeIndex = 0; + ScheduleFeedback(() => PlayElementBeat(_phaseLabel, "phase-pulse", 260), 0); } private void HandleDamageDealt(DamageDealtEvent payload) { - bool isPlayer = payload.Target == combatManager.PlayerState; + bool isPlayer = payload.Target == _combatManager.PlayerState; VisualElement target = CombatantElement(payload.Target); ScheduleCombatBeat(beatIndex => { @@ -1122,7 +1129,7 @@ namespace KernelPanic.UI { SpawnFloatingText(target, "killed", "float-kill float-large", 0); SpawnDeathGhost(targetBounds); - if (target != enemyRow) + if (target != _enemyRow) { PlayElementBeat(target, "feedback-killed", 420); } @@ -1168,37 +1175,37 @@ namespace KernelPanic.UI return; } - if (previousUptime.TryGetValue(state, out int oldUptime) && state.CurrentUptime > oldUptime) + if (_previousUptime.TryGetValue(state, out int oldUptime) && state.CurrentUptime > oldUptime) { SpawnFloatingText(element, $"+{state.CurrentUptime - oldUptime}", "float-heal"); PlayElementBeat(element, "feedback-boost", 220); } - if (previousShield.TryGetValue(state, out int oldShield) && state.Shield > oldShield) + if (_previousShield.TryGetValue(state, out int oldShield) && state.Shield > oldShield) { SpawnFloatingText(element, $"+{state.Shield - oldShield} shield", "float-heal"); PlayElementBeat(element, "feedback-block", 220); } - previousUptime[state] = state.CurrentUptime; - previousShield[state] = state.Shield; + _previousUptime[state] = state.CurrentUptime; + _previousShield[state] = state.Shield; } private VisualElement CombatantElement(CombatantState state) { - if (state != null && combatantElements.TryGetValue(state, out VisualElement element)) + if (state != null && _combatantElements.TryGetValue(state, out VisualElement element)) { return element; } - return state == combatManager.PlayerState ? playerPanel : enemyRow; + return state == _combatManager.PlayerState ? _playerPanel : _enemyRow; } private VisualElement FirstTargetElement(CardInstance card) { if (card?.TargetSnapshot == null) { - return enemyRow; + return _enemyRow; } for (int i = 0; i < card.TargetSnapshot.Count; i++) @@ -1210,7 +1217,7 @@ namespace KernelPanic.UI } } - return enemyRow; + return _enemyRow; } private void PlayElementBeat(VisualElement element, string className, int durationMs) @@ -1220,12 +1227,12 @@ namespace KernelPanic.UI return; } - int version = ++feedbackBeatVersion; - feedbackBeatVersions[element] = version; + int version = ++_feedbackBeatVersion; + _feedbackBeatVersions[element] = version; RemoveFeedbackBeatClasses(element); ScheduleFeedback(() => { - if (!feedbackBeatVersions.TryGetValue(element, out int currentVersion) || currentVersion != version) + if (!_feedbackBeatVersions.TryGetValue(element, out int currentVersion) || currentVersion != version) { return; } @@ -1233,26 +1240,26 @@ namespace KernelPanic.UI element.AddToClassList(className); ScheduleFeedback(() => { - if (!feedbackBeatVersions.TryGetValue(element, out int removeVersion) || removeVersion != version) + if (!_feedbackBeatVersions.TryGetValue(element, out int removeVersion) || removeVersion != version) { return; } element.RemoveFromClassList(className); - feedbackBeatVersions.Remove(element); + _feedbackBeatVersions.Remove(element); }, UIPreferences.ReducedMotion ? 80 : durationMs); }, 0); } private void SpawnFloatingText(VisualElement anchor, string text, string className, int cascadeOffset = 0) { - if (feedbackLayer == null || anchor == null) + if (_feedbackLayer == null || anchor == null) { return; } Rect anchorRect = anchor.worldBound; - Rect rootRect = root.worldBound; + Rect rootRect = _root.worldBound; Label label = new(text); label.AddToClassList("float-number"); if (!string.IsNullOrWhiteSpace(className)) @@ -1271,7 +1278,7 @@ namespace KernelPanic.UI float offsetY = ((cascadeOffset % 3) * 5f); label.style.left = anchorRect.center.x - rootRect.x - 30f + offsetX; label.style.top = anchorRect.center.y - rootRect.y - 16f + offsetY; - feedbackLayer.Add(label); + _feedbackLayer.Add(label); if (UIPreferences.ReducedMotion) { @@ -1290,13 +1297,13 @@ namespace KernelPanic.UI private void SpawnImpactMarker(VisualElement anchor, bool critical, bool mitigated, int cascadeOffset) { - if (feedbackLayer == null || anchor == null) + if (_feedbackLayer == null || anchor == null) { return; } Rect anchorRect = anchor.worldBound; - Rect rootRect = root.worldBound; + Rect rootRect = _root.worldBound; Label marker = new(critical ? "crit" : mitigated ? "clang" : "hit"); marker.AddToClassList("impact-marker"); marker.AddToClassList(critical ? "impact-marker-crit" : mitigated ? "impact-marker-clang" : "impact-marker-hit"); @@ -1304,7 +1311,7 @@ namespace KernelPanic.UI float lane = (cascadeOffset % 4) * 18f; marker.style.left = anchorRect.xMax - rootRect.x - 54f; marker.style.top = anchorRect.y - rootRect.y + 8f + lane; - feedbackLayer.Add(marker); + _feedbackLayer.Add(marker); if (UIPreferences.ReducedMotion) { @@ -1322,12 +1329,12 @@ namespace KernelPanic.UI private void FlyCardGhost(CardInstance card, VisualElement source, VisualElement destination, string className) { - if (feedbackLayer == null || card == null) + if (_feedbackLayer == null || card == null) { return; } - Rect rootRect = root.worldBound; + Rect rootRect = _root.worldBound; Rect sourceRect = source == null ? new Rect(rootRect.center.x - 70f, rootRect.center.y - 50f, 140f, 110f) : source.worldBound; Rect destinationRect = destination == null ? sourceRect : destination.worldBound; @@ -1339,7 +1346,7 @@ namespace KernelPanic.UI ghost.style.top = sourceRect.y - rootRect.y; ghost.style.width = Mathf.Max(110f, sourceRect.width); ghost.style.height = Mathf.Max(94f, sourceRect.height); - feedbackLayer.Add(ghost); + _feedbackLayer.Add(ghost); if (UIPreferences.ReducedMotion) { @@ -1359,12 +1366,12 @@ namespace KernelPanic.UI private void SpawnDeathGhost(Rect sourceRect) { - if (feedbackLayer == null || sourceRect.width <= 0f || sourceRect.height <= 0f) + if (_feedbackLayer == null || sourceRect.width <= 0f || sourceRect.height <= 0f) { return; } - Rect rootRect = root.worldBound; + Rect rootRect = _root.worldBound; float ghostWidth = Mathf.Min(sourceRect.width, 156f); float ghostHeight = Mathf.Min(sourceRect.height, 132f); VisualElement ghost = new(); @@ -1374,7 +1381,7 @@ namespace KernelPanic.UI ghost.style.top = sourceRect.center.y - rootRect.y - (ghostHeight * 0.5f); ghost.style.width = ghostWidth; ghost.style.height = ghostHeight; - feedbackLayer.Add(ghost); + _feedbackLayer.Add(ghost); if (UIPreferences.ReducedMotion) { @@ -1392,12 +1399,12 @@ namespace KernelPanic.UI private void FlashDamageVignette(bool critical) { - if (damageVignette == null) + if (_damageVignette == null) { return; } - PlayElementBeat(damageVignette, critical ? "damage-vignette-crit" : "damage-vignette-on", UIPreferences.ReducedMotion ? 120 : critical ? 320 : 240); + PlayElementBeat(_damageVignette, critical ? "damage-vignette-crit" : "damage-vignette-on", UIPreferences.ReducedMotion ? 120 : critical ? 320 : 240); } private static void RemoveFeedbackBeatClasses(VisualElement element) @@ -1433,19 +1440,19 @@ namespace KernelPanic.UI return; } - int beatIndex = feedbackCascadeIndex++; + int beatIndex = _feedbackCascadeIndex++; int delay = UIPreferences.ReducedMotion ? 0 : Mathf.Min(beatIndex * 120, 840); ScheduleFeedback(() => action(beatIndex), delay + (UIPreferences.ReducedMotion ? 0 : holdMs)); } private void ScheduleFeedback(Action action, int delayMs) { - if (action == null || root == null) + if (action == null || _root == null) { return; } - root.schedule.Execute(action).StartingIn(Mathf.Max(0, delayMs)); + _root.schedule.Execute(action).StartingIn(Mathf.Max(0, delayMs)); } private static void ApplyStatusStateClasses(VisualElement element, CombatantState state) @@ -1491,9 +1498,9 @@ namespace KernelPanic.UI private void HandleCombatLog(string message) { - if (logLabel != null) + if (_logLabel != null) { - logLabel.text = message ?? string.Empty; + _logLabel.text = message ?? string.Empty; } } @@ -1520,7 +1527,7 @@ namespace KernelPanic.UI private int GetDisplayCardCost(CardInstance card) { - return combatManager == null ? CombatManager.GetCardCost(card) : combatManager.GetEffectiveCardCost(card); + return _combatManager == null ? CombatManager.GetCardCost(card) : _combatManager.GetEffectiveCardCost(card); } private static VisualElement CreatePanel(string title, string subtitle, float width) @@ -1552,7 +1559,7 @@ namespace KernelPanic.UI private void ApplyAccent(VisualElement element) { - element.style.borderTopColor = distroAccent; + element.style.borderTopColor = _distroAccent; } private static VisualElement TurnStat(string labelText, string valueText) @@ -1652,9 +1659,14 @@ namespace KernelPanic.UI }; } - private static string IntentIcon(EnemyIntentKind kind) + private static string IntentIcon(EnemyIntent intent) { - return kind switch + if (!string.IsNullOrWhiteSpace(intent.IconKey)) + { + return intent.IconKey; + } + + return intent.Kind switch { EnemyIntentKind.Attack => "!", EnemyIntentKind.StatusAttack => "!", @@ -1673,7 +1685,7 @@ namespace KernelPanic.UI EnemyIntentKind.StatusAttack => $"{intent.DisplayLabel} x{intent.StatusStacks}", EnemyIntentKind.Defend => "defend", EnemyIntentKind.Buff => "buff", - EnemyIntentKind.Special => "special", + EnemyIntentKind.Special => intent.DisplayLabel, _ => intent.DisplayLabel }; } diff --git a/Assets/Scripts/UI/GamblingSceneController.cs b/Assets/Scripts/UI/GamblingSceneController.cs index cb4cd0d..dedaa0c 100644 --- a/Assets/Scripts/UI/GamblingSceneController.cs +++ b/Assets/Scripts/UI/GamblingSceneController.cs @@ -33,53 +33,53 @@ namespace KernelPanic.UI private const int SummaryTickSteps = 8; private const int SummaryTickMs = 220; - private readonly List rewards = new(); - private readonly List cardViews = new(); - private UIDocument document; - private VisualElement root; - private Label commandLabel; - private Label costLabel; - private Label flourishLabel; - private Label skipHint; - private VisualElement resultGrid; - private VisualElement effectLayer; - private VisualElement goldShimmer; - private VisualElement summaryPanel; - private Label summaryTotalsLabel; - private Button continueButton; - private string completedHeader; - private bool skipRequested; - private bool revealComplete; - private bool failed; + private readonly List _rewards = new(); + private readonly List _cardViews = new(); + private UIDocument _document; + private VisualElement _root; + private Label _commandLabel; + private Label _costLabel; + private Label _flourishLabel; + private Label _skipHint; + private VisualElement _resultGrid; + private VisualElement _effectLayer; + private VisualElement _goldShimmer; + private VisualElement _summaryPanel; + private Label _summaryTotalsLabel; + private Button _continueButton; + private string _completedHeader; + private bool _skipRequested; + private bool _revealComplete; + private bool _failed; private void Awake() { - document = GetComponent(); - root = document.rootVisualElement; - root.Clear(); - root.focusable = true; - root.AddToClassList("gambling-root"); - root.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion); + _document = GetComponent(); + _root = _document.rootVisualElement; + _root.Clear(); + _root.focusable = true; + _root.AddToClassList("gambling-root"); + _root.EnableInClassList("reduced-motion", UIPreferences.ReducedMotion); LoadStyles(); BuildLayout(); - root.RegisterCallback(HandleKeyDown); - root.RegisterCallback(HandlePointerDown); + _root.RegisterCallback(HandleKeyDown); + _root.RegisterCallback(HandlePointerDown); } private void OnDestroy() { - if (root == null) + if (_root == null) { return; } - root.UnregisterCallback(HandleKeyDown); - root.UnregisterCallback(HandlePointerDown); + _root.UnregisterCallback(HandleKeyDown); + _root.UnregisterCallback(HandlePointerDown); } private void Start() { - root.Focus(); + _root.Focus(); StartCoroutine(RunCutscene()); } @@ -95,7 +95,7 @@ namespace KernelPanic.UI StyleSheet styleSheet = Resources.Load(resourcePath); if (styleSheet != null) { - root.styleSheets.Add(styleSheet); + _root.styleSheets.Add(styleSheet); } } @@ -103,7 +103,7 @@ namespace KernelPanic.UI { VisualElement shell = new(); shell.AddToClassList("gambling-shell"); - root.Add(shell); + _root.Add(shell); VisualElement frame = new(); frame.AddToClassList("gambling-frame"); @@ -117,76 +117,76 @@ namespace KernelPanic.UI titleBlock.AddToClassList("gambling-title-block"); header.Add(titleBlock); - commandLabel = new("git pull"); - commandLabel.AddToClassList("gambling-command"); - titleBlock.Add(commandLabel); + _commandLabel = new("git pull"); + _commandLabel.AddToClassList("gambling-command"); + titleBlock.Add(_commandLabel); - costLabel = new(); - costLabel.AddToClassList("gambling-cost"); - titleBlock.Add(costLabel); + _costLabel = new(); + _costLabel.AddToClassList("gambling-cost"); + titleBlock.Add(_costLabel); - skipHint = new("[tap/key] reveal all"); - skipHint.AddToClassList("gambling-skip"); - header.Add(skipHint); + _skipHint = new("[tap/key] reveal all"); + _skipHint.AddToClassList("gambling-skip"); + header.Add(_skipHint); - flourishLabel = new(); - flourishLabel.AddToClassList("gambling-flourish"); - frame.Add(flourishLabel); + _flourishLabel = new(); + _flourishLabel.AddToClassList("gambling-flourish"); + frame.Add(_flourishLabel); - resultGrid = new VisualElement(); - resultGrid.AddToClassList("gambling-results"); - frame.Add(resultGrid); + _resultGrid = new VisualElement(); + _resultGrid.AddToClassList("gambling-results"); + frame.Add(_resultGrid); - goldShimmer = new VisualElement(); - goldShimmer.AddToClassList("gold-shimmer"); - goldShimmer.pickingMode = PickingMode.Ignore; - frame.Add(goldShimmer); + _goldShimmer = new VisualElement(); + _goldShimmer.AddToClassList("gold-shimmer"); + _goldShimmer.pickingMode = PickingMode.Ignore; + frame.Add(_goldShimmer); - effectLayer = new VisualElement(); - effectLayer.AddToClassList("effect-layer"); - effectLayer.pickingMode = PickingMode.Ignore; - frame.Add(effectLayer); + _effectLayer = new VisualElement(); + _effectLayer.AddToClassList("effect-layer"); + _effectLayer.pickingMode = PickingMode.Ignore; + frame.Add(_effectLayer); - summaryPanel = new VisualElement(); - summaryPanel.AddToClassList("summary-panel"); - summaryPanel.AddToClassList("hidden"); - frame.Add(summaryPanel); + _summaryPanel = new VisualElement(); + _summaryPanel.AddToClassList("summary-panel"); + _summaryPanel.AddToClassList("hidden"); + frame.Add(_summaryPanel); - continueButton = new Button(ContinueToMenu) { text = "> continue" }; - continueButton.AddToClassList("continue-button"); - continueButton.AddToClassList("continue-button-pending"); - continueButton.SetEnabled(false); - frame.Add(continueButton); + _continueButton = new Button(ContinueToMenu) { text = "> continue" }; + _continueButton.AddToClassList("continue-button"); + _continueButton.AddToClassList("continue-button-pending"); + _continueButton.SetEnabled(false); + frame.Add(_continueButton); } private IEnumerator RunCutscene() { if (!GachaPullContext.TryConsumePending(out PendingGachaPull pending)) { - failed = true; - commandLabel.text = "git pull failed"; - costLabel.text = "no pending pull"; + _failed = true; + _commandLabel.text = "git pull failed"; + _costLabel.text = "no pending pull"; ShowContinue(); yield break; } - commandLabel.text = pending.PullCount == 10 ? "git pull --ten" : "git pull --single"; + _commandLabel.text = pending.PullCount == 10 ? "git pull --ten" : "git pull --single"; int pullCost = pending.PullCount == 10 ? GachaService.BeginnerTenPullCost : GachaService.BeginnerSinglePullCost; - costLabel.text = pending.EntropyTokenCount > 0 + _costLabel.text = pending.EntropyTokenCount > 0 ? $"cost: {pending.EntropyTokenCount * GachaService.EntropyPerCommit} Entropy" : $"cost: {pullCost} Commits / {pullCost * GachaService.EntropyPerCommit} Entropy"; - flourishLabel.text = "$ fetch-pack | verify | unpack"; + _flourishLabel.text = "$ fetch-pack | verify | unpack"; CompletedGachaPull completed = ResolvePull(pending); GachaPullContext.SetCompleted(completed); - completedHeader = completed.HeaderText; - rewards.Clear(); - rewards.AddRange(completed.Rewards); + _completedHeader = completed.HeaderText; + _rewards.Clear(); + _rewards.AddRange(completed.Rewards); - if (rewards.Count == 0) + if (_rewards.Count == 0) { - failed = true; - costLabel.text = completed.HeaderText; + _failed = true; + _costLabel.text = completed.HeaderText; ShowContinue(); yield break; } @@ -199,24 +199,24 @@ namespace KernelPanic.UI } yield return WaitOrSkip(FlourishMs); - flourishLabel.text = "$ unpacking sealed objects"; - for (int i = 0; i < rewards.Count; i++) + _flourishLabel.text = "$ unpacking sealed objects"; + for (int i = 0; i < _rewards.Count; i++) { - if (skipRequested) + if (_skipRequested) { RevealAll(); yield break; } - CompletedGachaReward reward = rewards[i]; - PullCardView card = cardViews[i]; + CompletedGachaReward reward = _rewards[i]; + PullCardView card = _cardViews[i]; int holdMs = GetAnticipationMs(reward.Stars); if (holdMs > 0) { ApplyGridFocus(i, reward.Stars); card.ShowHold(reward.Stars >= 5 ? "........ signed tag" : "... promoted object", reward.Stars); yield return WaitOrSkip(holdMs); - if (skipRequested) + if (_skipRequested) { RevealAll(); yield break; @@ -315,25 +315,25 @@ namespace KernelPanic.UI private void BuildRows() { - resultGrid.Clear(); - cardViews.Clear(); - for (int i = 0; i < rewards.Count; i++) + _resultGrid.Clear(); + _cardViews.Clear(); + for (int i = 0; i < _rewards.Count; i++) { - PullCardView card = new(rewards[i]); - cardViews.Add(card); - resultGrid.Add(card.Root); + PullCardView card = new(_rewards[i]); + _cardViews.Add(card); + _resultGrid.Add(card.Root); } } private void RevealAll() { - skipRequested = true; + _skipRequested = true; ClearGridFocus(); - goldShimmer.RemoveFromClassList("gold-shimmer-on"); - for (int i = 0; i < cardViews.Count; i++) + _goldShimmer.RemoveFromClassList("gold-shimmer-on"); + for (int i = 0; i < _cardViews.Count; i++) { - cardViews[i].HideHold(); - cardViews[i].Reveal(allowFlash: false, persistentMotion: !UIPreferences.ReducedMotion); + _cardViews[i].HideHold(); + _cardViews[i].Reveal(allowFlash: false, persistentMotion: !UIPreferences.ReducedMotion); } CompleteReveal(); @@ -341,49 +341,49 @@ namespace KernelPanic.UI private void CompleteReveal() { - if (revealComplete) + if (_revealComplete) { return; } - revealComplete = true; - skipHint.text = "complete"; - flourishLabel.text = completedHeader; - flourishLabel.AddToClassList("gambling-flourish-complete"); - BuildSummary(animate: !skipRequested && !UIPreferences.ReducedMotion); + _revealComplete = true; + _skipHint.text = "complete"; + _flourishLabel.text = _completedHeader; + _flourishLabel.AddToClassList("gambling-flourish-complete"); + BuildSummary(animate: !_skipRequested && !UIPreferences.ReducedMotion); ShowContinue(); } private void BuildSummary(bool animate) { - summaryPanel.Clear(); - summaryPanel.RemoveFromClassList("hidden"); - summaryTotalsLabel = new Label { name = "PullSummaryTotals" }; - summaryTotalsLabel.AddToClassList("summary-totals"); - summaryPanel.Add(summaryTotalsLabel); + _summaryPanel.Clear(); + _summaryPanel.RemoveFromClassList("hidden"); + _summaryTotalsLabel = new Label { name = "PullSummaryTotals" }; + _summaryTotalsLabel.AddToClassList("summary-totals"); + _summaryPanel.Add(_summaryTotalsLabel); SummaryTotals totals = CalculateTotals(); if (!animate) { - summaryTotalsLabel.text = FormatTotals(totals.Units, totals.Merges, totals.Cache, totals.LanguagesText); + _summaryTotalsLabel.text = FormatTotals(totals.Units, totals.Merges, totals.Cache, totals.LanguagesText); return; } - summaryTotalsLabel.AddToClassList("summary-tick"); + _summaryTotalsLabel.AddToClassList("summary-tick"); for (int step = 0; step <= SummaryTickSteps; step++) { int capturedStep = step; int delay = Mathf.RoundToInt((SummaryTickMs / (float)SummaryTickSteps) * capturedStep); - summaryTotalsLabel.schedule.Execute(() => + _summaryTotalsLabel.schedule.Execute(() => { float t = capturedStep / (float)SummaryTickSteps; int units = Mathf.RoundToInt(Mathf.Lerp(0, totals.Units, t)); int merges = Mathf.RoundToInt(Mathf.Lerp(0, totals.Merges, t)); int cache = Mathf.RoundToInt(Mathf.Lerp(0, totals.Cache, t)); - summaryTotalsLabel.text = FormatTotals(units, merges, cache, capturedStep == SummaryTickSteps ? totals.LanguagesText : "..."); + _summaryTotalsLabel.text = FormatTotals(units, merges, cache, capturedStep == SummaryTickSteps ? totals.LanguagesText : "..."); if (capturedStep == SummaryTickSteps) { - summaryTotalsLabel.RemoveFromClassList("summary-tick"); + _summaryTotalsLabel.RemoveFromClassList("summary-tick"); } }).StartingIn(delay); } @@ -395,25 +395,25 @@ namespace KernelPanic.UI int merges = 0; int cache = 0; HashSet languages = new(); - for (int i = 0; i < rewards.Count; i++) + for (int i = 0; i < _rewards.Count; i++) { - if (rewards[i].OutcomeKind == PullOutcomeKind.Granted && rewards[i].IsCharacter) + if (_rewards[i].OutcomeKind == PullOutcomeKind.Granted && _rewards[i].IsCharacter) { units++; } - if (rewards[i].RewardType == GachaRewardType.Package) + if (_rewards[i].RewardType == GachaRewardType.Package) { - cache += rewards[i].MergesAwarded; + cache += _rewards[i].MergesAwarded; } else { - merges += rewards[i].MergesAwarded; + merges += _rewards[i].MergesAwarded; } - for (int languageIndex = 0; languageIndex < rewards[i].LanguagesUnlocked.Count; languageIndex++) + for (int languageIndex = 0; languageIndex < _rewards[i].LanguagesUnlocked.Count; languageIndex++) { - languages.Add(rewards[i].LanguagesUnlocked[languageIndex]); + languages.Add(_rewards[i].LanguagesUnlocked[languageIndex]); } } @@ -591,7 +591,7 @@ namespace KernelPanic.UI { float elapsed = 0f; float duration = Mathf.Max(0f, milliseconds / 1000f); - while (!skipRequested && elapsed < duration) + while (!_skipRequested && elapsed < duration) { elapsed += Time.deltaTime; yield return null; @@ -615,17 +615,17 @@ namespace KernelPanic.UI return; } - for (int i = 0; i < cardViews.Count; i++) + for (int i = 0; i < _cardViews.Count; i++) { - cardViews[i].SetDimmed(i != focusedIndex); + _cardViews[i].SetDimmed(i != focusedIndex); } } private void ClearGridFocus() { - for (int i = 0; i < cardViews.Count; i++) + for (int i = 0; i < _cardViews.Count; i++) { - cardViews[i].SetDimmed(false); + _cardViews[i].SetDimmed(false); } } @@ -656,32 +656,32 @@ namespace KernelPanic.UI private void PlayGoldShimmer(VisualElement source) { - if (source == null || goldShimmer?.parent == null) + if (source == null || _goldShimmer?.parent == null) { return; } Rect sourceBounds = source.worldBound; - Rect parentBounds = goldShimmer.parent.worldBound; + Rect parentBounds = _goldShimmer.parent.worldBound; float left = Mathf.Max(0f, sourceBounds.x - parentBounds.x); float top = Mathf.Max(0f, sourceBounds.y - parentBounds.y); - goldShimmer.style.left = left; - goldShimmer.style.top = top; - goldShimmer.style.width = sourceBounds.width; - goldShimmer.style.height = sourceBounds.height; - goldShimmer.AddToClassList("gold-shimmer-on"); - goldShimmer.schedule.Execute(() => goldShimmer.RemoveFromClassList("gold-shimmer-on")).StartingIn(LegendaryShimmerMs); + _goldShimmer.style.left = left; + _goldShimmer.style.top = top; + _goldShimmer.style.width = sourceBounds.width; + _goldShimmer.style.height = sourceBounds.height; + _goldShimmer.AddToClassList("gold-shimmer-on"); + _goldShimmer.schedule.Execute(() => _goldShimmer.RemoveFromClassList("gold-shimmer-on")).StartingIn(LegendaryShimmerMs); } private void EmitSparks(VisualElement source, Color color, int count, float radius) { - if (effectLayer == null || source == null) + if (_effectLayer == null || source == null) { return; } Rect sourceBounds = source.worldBound; - Rect layerBounds = effectLayer.worldBound; + Rect layerBounds = _effectLayer.worldBound; float centerX = sourceBounds.center.x - layerBounds.x; float centerY = sourceBounds.center.y - layerBounds.y; float halfWidth = sourceBounds.width * 0.5f; @@ -728,41 +728,41 @@ namespace KernelPanic.UI VisualElement spark = new(); spark.pickingMode = PickingMode.Ignore; spark.AddToClassList("spark"); - effectLayer.Add(spark); + _effectLayer.Add(spark); return spark; } private void HandleKeyDown(KeyDownEvent evt) { - if (revealComplete) + if (_revealComplete) { ContinueToMenu(); evt.StopPropagation(); return; } - skipRequested = true; + _skipRequested = true; evt.StopPropagation(); } private void HandlePointerDown(PointerDownEvent evt) { - if (revealComplete) + if (_revealComplete) { return; } - skipRequested = true; + _skipRequested = true; evt.StopPropagation(); } private void ShowContinue() { - continueButton.SetEnabled(true); - continueButton.RemoveFromClassList("continue-button-pending"); - if (failed) + _continueButton.SetEnabled(true); + _continueButton.RemoveFromClassList("continue-button-pending"); + if (_failed) { - skipHint.text = "failed"; + _skipHint.text = "failed"; } } @@ -789,17 +789,17 @@ namespace KernelPanic.UI private sealed class PullCardView { - private readonly CompletedGachaReward reward; - private readonly Label rarityLabel; - private readonly Label nameLabel; - private readonly Label statusLabel; - private readonly Label typeIconLabel; - private IVisualElementScheduledItem glowPulse; - private bool glowOn; + private readonly CompletedGachaReward _reward; + private readonly Label _rarityLabel; + private readonly Label _nameLabel; + private readonly Label _statusLabel; + private readonly Label _typeIconLabel; + private IVisualElementScheduledItem _glowPulse; + private bool _glowOn; public PullCardView(CompletedGachaReward reward) { - this.reward = reward; + this._reward = reward; RarityStyle rarity = RarityPresentation.ForStars(reward.Stars); Root = new VisualElement(); Root.AddToClassList("pull-card"); @@ -814,27 +814,27 @@ namespace KernelPanic.UI indexLabel.AddToClassList("pull-index"); top.Add(indexLabel); - rarityLabel = new(rarity.Stars); - rarityLabel.AddToClassList("pull-rarity"); - rarityLabel.AddToClassList(rarity.ClassName); - top.Add(rarityLabel); + _rarityLabel = new(rarity.Stars); + _rarityLabel.AddToClassList("pull-rarity"); + _rarityLabel.AddToClassList(rarity.ClassName); + top.Add(_rarityLabel); - typeIconLabel = new(reward.IsCharacter ? "@" : "#"); - typeIconLabel.tooltip = reward.TypeText; - typeIconLabel.AddToClassList("pull-type-icon"); - top.Add(typeIconLabel); + _typeIconLabel = new(reward.IsCharacter ? "@" : "#"); + _typeIconLabel.tooltip = reward.TypeText; + _typeIconLabel.AddToClassList("pull-type-icon"); + top.Add(_typeIconLabel); - nameLabel = new(reward.DisplayName); - nameLabel.AddToClassList("pull-name"); - Root.Add(nameLabel); + _nameLabel = new(reward.DisplayName); + _nameLabel.AddToClassList("pull-name"); + Root.Add(_nameLabel); - statusLabel = new(reward.StatusText); - statusLabel.AddToClassList("pull-status"); - statusLabel.EnableInClassList("pull-status-new", reward.OutcomeKind == PullOutcomeKind.Granted); - statusLabel.visible = !string.IsNullOrWhiteSpace(reward.StatusText); - if (statusLabel.visible) + _statusLabel = new(reward.StatusText); + _statusLabel.AddToClassList("pull-status"); + _statusLabel.EnableInClassList("pull-status-new", reward.OutcomeKind == PullOutcomeKind.Granted); + _statusLabel.visible = !string.IsNullOrWhiteSpace(reward.StatusText); + if (_statusLabel.visible) { - Root.Add(statusLabel); + Root.Add(_statusLabel); } } @@ -846,11 +846,11 @@ namespace KernelPanic.UI Root.AddToClassList("pull-card-held"); Root.EnableInClassList("pull-card-held-rare", stars == 4); Root.EnableInClassList("pull-card-held-legendary", stars >= 5); - rarityLabel.text = "..."; - nameLabel.text = holdText; - statusLabel.text = ""; - statusLabel.visible = false; - typeIconLabel.visible = false; + _rarityLabel.text = "..."; + _nameLabel.text = holdText; + _statusLabel.text = ""; + _statusLabel.visible = false; + _typeIconLabel.visible = false; } public void HideHold() @@ -862,21 +862,21 @@ namespace KernelPanic.UI public void Reveal(bool allowFlash, bool persistentMotion) { - RarityStyle rarity = RarityPresentation.ForStars(reward.Stars); + RarityStyle rarity = RarityPresentation.ForStars(_reward.Stars); Root.RemoveFromClassList("pull-card-hidden"); Root.RemoveFromClassList("pull-card-held"); Root.RemoveFromClassList("pull-card-held-rare"); Root.RemoveFromClassList("pull-card-held-legendary"); Root.AddToClassList("pull-card-revealed"); - Root.EnableInClassList("pull-card-glow-4", persistentMotion && reward.Stars == 4); - Root.EnableInClassList("pull-card-glow-5", persistentMotion && reward.Stars >= 5); - SetPersistentGlow(persistentMotion && reward.Stars >= 5); - rarityLabel.text = reward.Stars >= 5 ? $"{rarity.Badge} {rarity.Stars}" : rarity.Stars; - nameLabel.text = reward.DisplayName; - typeIconLabel.text = reward.IsCharacter ? "@" : "#"; - typeIconLabel.visible = true; - statusLabel.text = reward.StatusText; - statusLabel.visible = !string.IsNullOrWhiteSpace(reward.StatusText); + Root.EnableInClassList("pull-card-glow-4", persistentMotion && _reward.Stars == 4); + Root.EnableInClassList("pull-card-glow-5", persistentMotion && _reward.Stars >= 5); + SetPersistentGlow(persistentMotion && _reward.Stars >= 5); + _rarityLabel.text = _reward.Stars >= 5 ? $"{rarity.Badge} {rarity.Stars}" : rarity.Stars; + _nameLabel.text = _reward.DisplayName; + _typeIconLabel.text = _reward.IsCharacter ? "@" : "#"; + _typeIconLabel.visible = true; + _statusLabel.text = _reward.StatusText; + _statusLabel.visible = !string.IsNullOrWhiteSpace(_reward.StatusText); if (allowFlash) { Root.AddToClassList("pull-card-flash"); @@ -897,34 +897,34 @@ namespace KernelPanic.UI public void PulseStatus(int durationMs) { - if (!statusLabel.visible) + if (!_statusLabel.visible) { return; } - statusLabel.AddToClassList("pull-status-pop"); - statusLabel.schedule.Execute(() => statusLabel.RemoveFromClassList("pull-status-pop")).StartingIn(durationMs); + _statusLabel.AddToClassList("pull-status-pop"); + _statusLabel.schedule.Execute(() => _statusLabel.RemoveFromClassList("pull-status-pop")).StartingIn(durationMs); } private void SetPersistentGlow(bool enabled) { if (!enabled) { - glowPulse?.Pause(); + _glowPulse?.Pause(); Root.RemoveFromClassList("pull-card-glow-on"); return; } - if (glowPulse != null) + if (_glowPulse != null) { - glowPulse.Resume(); + _glowPulse.Resume(); return; } - glowPulse = Root.schedule.Execute(() => + _glowPulse = Root.schedule.Execute(() => { - glowOn = !glowOn; - Root.EnableInClassList("pull-card-glow-on", glowOn); + _glowOn = !_glowOn; + Root.EnableInClassList("pull-card-glow-on", _glowOn); }).Every(900); } } diff --git a/Assets/Scripts/UI/MainMenuController.cs b/Assets/Scripts/UI/MainMenuController.cs index 910729c..eaf2d42 100644 --- a/Assets/Scripts/UI/MainMenuController.cs +++ b/Assets/Scripts/UI/MainMenuController.cs @@ -28,8 +28,8 @@ namespace KernelPanic.UI "Courier New" }; - private static FontAsset runtimeMonospaceFont; - private static bool warnedMissingFont; + private static FontAsset _runtimeMonospaceFont; + private static bool _warnedMissingFont; public static FontAsset Resolve(FontAsset configuredFont) { @@ -38,25 +38,25 @@ namespace KernelPanic.UI return configuredFont; } - if (runtimeMonospaceFont != null) + if (_runtimeMonospaceFont != null) { - return runtimeMonospaceFont; + return _runtimeMonospaceFont; } Font dynamicFont = Font.CreateDynamicFontFromOSFont(PreferredMonospaceFontFamilies, 16); if (dynamicFont == null) { - if (!warnedMissingFont) + if (!_warnedMissingFont) { Debug.LogWarning("No monospace OS font found; terminal UI will use Unity's default font."); - warnedMissingFont = true; + _warnedMissingFont = true; } return null; } - runtimeMonospaceFont = FontAsset.CreateFontAsset(dynamicFont); - return runtimeMonospaceFont; + _runtimeMonospaceFont = FontAsset.CreateFontAsset(dynamicFont); + return _runtimeMonospaceFont; } } @@ -667,7 +667,7 @@ namespace KernelPanic.UI _saveData = _saveService.Load(); // Snapshot before the next EnsureLists() call below, which re-runs NormalizeOwnedPackages // and would otherwise clear this (no new duplicates survive a second normalization pass). - List collapsedPackageDuplicates = new(_saveData.collapsedPackageDuplicates); + List collapsedPackageDuplicates = new(_saveData.CollapsedPackageDuplicates); _saveData.EnsureLists(); _wallet.SetBalance(_saveData.entropyBalance);