Fix various issues related to styling and animations

This commit is contained in:
2026-07-10 21:28:10 +02:00
parent c973c2d51a
commit 2ba43fb2d3
10 changed files with 2126 additions and 205 deletions
+12
View File
@@ -72,6 +72,11 @@ namespace KernelPanic.Combat
GrantRustOverkillShield(context, result, survivable);
}
if (result.TargetDefeated)
{
context.CombatManager.QueueDefeatedCombatantForRemoval(target);
}
if (overkillSpills)
{
dealtDamage |= SpillOverkillDamage(context, target, result, survivable);
@@ -118,8 +123,15 @@ namespace KernelPanic.Combat
}
int survivable = GetSurvivableDamage(spillTarget, _trueDamage);
GameEvents.RaiseOverflowDamageTravel(new OverflowDamageTravelEvent(context.Source, firstTarget, spillTarget, remaining, _language));
DamageResult spillResult = context.DamagePipeline.DealDamage(new DamageRequest(context.Source, spillTarget, remaining, _language, _trueDamage, false, applySourceModifiers: false));
dealtDamage |= spillResult.IncomingAmount > 0;
if (spillResult.TargetDefeated)
{
context.CombatManager.QueueDefeatedCombatantForRemoval(spillTarget);
}
firstTarget = spillTarget;
remaining = UnityEngine.Mathf.Max(0, spillResult.IncomingAmount - survivable);
}
+272 -24
View File
@@ -28,6 +28,10 @@ namespace KernelPanic.Combat
private readonly HashSet<string> _loggedEffectTodos = new();
private readonly List<CardDefinition> _generatedCardPool = new();
private readonly List<DelayedCombatEffect> _delayedEffects = new();
private readonly HashSet<EnemyInstance> _enemiesPendingExit = new();
private readonly Queue<EnemyInstance> _enemyExitQueue = new();
private readonly Dictionary<EnemyInstance, IReadOnlyList<DeathSpawnRequest>> _pendingDeathSpawns = new();
private readonly HashSet<EnemyInstance> _completedDeathVisuals = new();
private RunManager _runManager;
private RunConfig _runConfig;
private HandController _handController;
@@ -39,7 +43,7 @@ namespace KernelPanic.Combat
private bool _skipNextAllocateDraw;
private bool _ubuntuEmptyHandRefillUsed;
private int _fedoraCardsDiscountedThisTurn;
private int _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private float _fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
private int _cardsPlayedThisTurn;
private int _cardsPlayedThisWave;
private int _cCardsPlayedThisTurn;
@@ -55,6 +59,9 @@ namespace KernelPanic.Combat
private int _nextTurnCycleBonus;
private int _nextRacePairId = 1;
private int _activeKernelRamPenalty;
private int _deathSpawnSequencesActive;
private bool _enemyExitQueueDraining;
private EnemyInstance _enemyExitAwaitingVisual;
private Coroutine _phaseCoroutine;
public TurnPhase CurrentPhase => currentPhase;
@@ -66,6 +73,7 @@ namespace KernelPanic.Combat
public LazyStack LazyStack => _lazyStack;
public NativeTrack NativeTrack => _nativeTrack;
public DamagePipeline DamagePipeline => _damagePipeline;
public float FedoraCrashChance => _fedoraCrashChance;
public IReadOnlyList<EnemyInstance> Enemies => _enemies;
public int SelectedEnemyIndex => _selectedEnemyIndex;
public CardInstance PendingTargetCard => _pendingTargetCard;
@@ -73,6 +81,12 @@ namespace KernelPanic.Combat
public bool RunLost => _runLost;
public int CurrentWaveNumber => _runManager == null ? 1 : _runManager.CurrentWaveNumber;
public int ActiveKernelRamPenalty => _activeKernelRamPenalty;
public bool CanSurrenderRun => currentPhase == TurnPhase.Execute
&& !_awaitingWaveContinue
&& !_runLost
&& !VisualResolutionPending
&& _playerState != null
&& !_playerState.IsDefeated;
public event Action StateChanged;
public event Action<string> CombatLog;
@@ -86,11 +100,13 @@ namespace KernelPanic.Combat
private void OnEnable()
{
GameEvents.DamageDealt += HandlePackageDamageDealt;
GameEvents.CombatantDeathVisualCompleted += HandleCombatantDeathVisualCompleted;
}
private void OnDisable()
{
GameEvents.DamageDealt -= HandlePackageDamageDealt;
GameEvents.CombatantDeathVisualCompleted -= HandleCombatantDeathVisualCompleted;
}
public void StartCombat()
@@ -131,6 +147,31 @@ namespace KernelPanic.Combat
}
}
public void SurrenderRun()
{
if (!CanSurrenderRun)
{
return;
}
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
_damagePipeline.DefeatCombatant(_playerState, null, Language.C);
CheckLoss();
StateChanged?.Invoke();
}
public void QueueDefeatedCombatantForRemoval(CombatantState combatant)
{
EnemyInstance enemy = EnemyForState(combatant);
if (enemy == null)
{
return;
}
QueueDefeatedEnemyForRemoval(enemy);
}
public void ContinueToNextWave()
{
if (!_awaitingWaveContinue || _runLost)
@@ -156,7 +197,7 @@ namespace KernelPanic.Combat
return;
}
if (index < 0 || index >= _enemies.Count)
if (index < 0 || index >= _enemies.Count || _enemies[index].State.IsDefeated)
{
_selectedEnemyIndex = -1;
StateChanged?.Invoke();
@@ -264,7 +305,8 @@ namespace KernelPanic.Combat
_rawhideBonusCharges--;
}
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 100, new RollContext(_playerState)) <= _fedoraCrashChance)
float fedoraCrashChanceBeforeRoll = _fedoraCrashChance;
if (!IsCrashImmune(card) && RandomRoll.RollRange(1, 1000, new RollContext(_playerState)) <= Mathf.RoundToInt(_fedoraCrashChance * 10f))
{
_fedoraCrashChance = CombatTuning.FedoraBleedingEdgeBaseCrashChance;
if (IsDistro("fedora") && _runConfig.DistroVersion >= 4)
@@ -276,6 +318,7 @@ namespace KernelPanic.Combat
_pendingTargetCard = null;
_selectedEnemyIndex = -1;
Log($"{GetCardName(card)} crashed under bleeding edge");
GameEvents.RaiseFedoraBleedingEdgeTriggered(new FedoraBleedingEdgeEvent(card, true, firstFedoraBonusThisTurn, 0, fedoraCrashChanceBeforeRoll, _fedoraCrashChance));
StateChanged?.Invoke();
return true;
}
@@ -286,6 +329,7 @@ namespace KernelPanic.Combat
card.MarkFedoraNonCrashBonus();
_playerState.DamageMultiplierPercent = _runConfig.DistroVersion >= 2 ? 175 : 150;
ApplyFedoraGrowth(card, rawhideChargeUsed, firstFedoraBonusThisTurn);
GameEvents.RaiseFedoraBleedingEdgeTriggered(new FedoraBleedingEdgeEvent(card, false, firstFedoraBonusThisTurn, _playerState.DamageMultiplierPercent, fedoraCrashChanceBeforeRoll, _fedoraCrashChance));
}
if (card.Definition.Language == Language.C && _cCardsPlayedThisTurn == 0 && HasPackageEffect(PackageEffectKind.FirstCThisTurnDamageMultiplier, out PackageEffectData cPackageEffect))
@@ -669,6 +713,13 @@ namespace KernelPanic.Combat
_queuedRepeatCharges = 0;
_nextTurnCycleBonus = 0;
_delayedEffects.Clear();
_enemiesPendingExit.Clear();
_enemyExitQueue.Clear();
_pendingDeathSpawns.Clear();
_completedDeathVisuals.Clear();
_deathSpawnSequencesActive = 0;
_enemyExitQueueDraining = false;
_enemyExitAwaitingVisual = null;
RandomRoll.Seed(_runConfig.RunSeed);
_playerState = new CombatantState(_runManager.EffectiveMaxUptime(), _runManager.EffectiveRam(), _runManager.EffectiveMaxCycles());
ApplyVersionState(_playerState);
@@ -726,6 +777,13 @@ namespace KernelPanic.Combat
_deckController.Initialize(startingCards);
_interpreterQueue.Clear();
_enemies.Clear();
_enemiesPendingExit.Clear();
_enemyExitQueue.Clear();
_pendingDeathSpawns.Clear();
_completedDeathVisuals.Clear();
_deathSpawnSequencesActive = 0;
_enemyExitQueueDraining = false;
_enemyExitAwaitingVisual = null;
SpawnStructuralEnemies();
RecalculateKernelRamPressure();
PickEnemyIntents();
@@ -877,7 +935,13 @@ namespace KernelPanic.Combat
return;
}
int archBtwStacksBeforeReset = _playerState?.ArchBtwStacks ?? 0;
ResetArchBtwAtEndOfTurn();
if (IsDistro("arch") && archBtwStacksBeforeReset > 0)
{
GameEvents.RaiseArchBtwTurnEnded(new ArchBtwTurnEndedEvent(archBtwStacksBeforeReset, _playerState.ArchBtwStacks > 0));
}
RemoveDefeatedEnemies();
CheckWinOrLoss();
StateChanged?.Invoke();
@@ -952,7 +1016,7 @@ namespace KernelPanic.Combat
}
int lookCount = _runConfig.DistroVersion >= 2 ? 3 : 2;
if (!_deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card))
if (!_deckController.TryDrawCheapestFromTop(lookCount, out CardInstance card, out IReadOnlyList<PeekedCardInfo> peeked, out bool wasTie))
{
return;
}
@@ -965,6 +1029,7 @@ namespace KernelPanic.Combat
if (_handController.Add(card))
{
Log($"apt update: staged {GetCardName(card)} from top {lookCount}");
GameEvents.RaiseUbuntuAptUpdatePeeked(new UbuntuAptUpdatePeekedEvent(peeked, card, wasTie, lookCount));
}
else
{
@@ -1836,17 +1901,7 @@ namespace KernelPanic.Combat
{
if (_enemies[i].State.IsDefeated)
{
EnemyInstance enemy = _enemies[i];
if (enemy.PendingRevive)
{
continue;
}
Log($"{enemy.Name} exited");
_runManager?.AddBits(CombatTuning.BitsPerKill);
HandleEnemyDeath(enemy);
_enemies.RemoveAt(i);
RecalculateKernelRamPressure();
QueueDefeatedEnemyForRemoval(_enemies[i]);
}
}
@@ -1854,15 +1909,42 @@ namespace KernelPanic.Combat
{
_selectedEnemyIndex = -1;
}
if (_enemyExitQueue.Count > 0 && !_enemyExitQueueDraining)
{
StartCoroutine(DrainEnemyExitQueue());
}
}
private void HandleEnemyDeath(EnemyInstance enemy)
private void QueueDefeatedEnemyForRemoval(EnemyInstance enemy)
{
if (enemy == null)
if (enemy == null || enemy.State == null || !enemy.State.IsDefeated || enemy.PendingRevive || _enemiesPendingExit.Contains(enemy))
{
return;
}
Log($"{enemy.Name} exited");
_runManager?.AddBits(CombatTuning.BitsPerKill);
List<DeathSpawnRequest> deathSpawns = HandleEnemyDeath(enemy);
enemy.MarkDefeatedPendingRemoval();
_enemiesPendingExit.Add(enemy);
_pendingDeathSpawns[enemy] = deathSpawns;
_enemyExitQueue.Enqueue(enemy);
if (!_enemyExitQueueDraining)
{
StartCoroutine(DrainEnemyExitQueue());
}
}
private List<DeathSpawnRequest> HandleEnemyDeath(EnemyInstance enemy)
{
List<DeathSpawnRequest> deathSpawns = new();
if (enemy == null)
{
return deathSpawns;
}
if (enemy.HasBehavior(EnemyBehaviorFlags.SegfaultOnDeath))
{
ApplySegfaultDeathPayload(enemy);
@@ -1875,7 +1957,148 @@ namespace KernelPanic.Combat
if (enemy.HasBehavior(EnemyBehaviorFlags.LeavesOrphan))
{
SpawnOrphanFromDeath(enemy);
deathSpawns.Add(CreateOrphanDeathSpawn(enemy));
}
return deathSpawns;
}
private void HandleCombatantDeathVisualCompleted(CombatantDeathVisualCompletedEvent payload)
{
EnemyInstance enemy = EnemyForState(payload.Combatant);
if (enemy == null)
{
return;
}
_completedDeathVisuals.Add(enemy);
}
private EnemyInstance EnemyForState(CombatantState state)
{
if (state == null)
{
return null;
}
for (int i = 0; i < _enemies.Count; i++)
{
if (_enemies[i].State == state)
{
return _enemies[i];
}
}
return null;
}
private IEnumerator DrainEnemyExitQueue()
{
if (_enemyExitQueueDraining)
{
yield break;
}
_enemyExitQueueDraining = true;
float elapsedMs = 0f;
while (_enemyExitQueue.Count > 0)
{
bool removedAny = false;
int pendingCount = _enemyExitQueue.Count;
for (int i = 0; i < pendingCount; i++)
{
EnemyInstance enemy = _enemyExitQueue.Dequeue();
bool visualComplete = _completedDeathVisuals.Contains(enemy);
bool timedOut = elapsedMs >= CombatTuning.VisualQueueMaxDrainMs;
if (!visualComplete && !timedOut)
{
_enemyExitQueue.Enqueue(enemy);
continue;
}
_enemyExitAwaitingVisual = enemy;
_completedDeathVisuals.Remove(enemy);
yield return CompleteEnemyExit(enemy);
_enemyExitAwaitingVisual = null;
removedAny = true;
}
if (removedAny)
{
elapsedMs = 0f;
continue;
}
elapsedMs += Time.unscaledDeltaTime * 1000f;
yield return null;
}
_enemyExitQueueDraining = false;
CheckWinOrLoss();
StateChanged?.Invoke();
if (!IsCombatPaused && currentPhase != TurnPhase.Execute)
{
yield return Wait(CombatTuning.PhaseTransitionDelaySeconds);
AdvancePhase();
}
}
private IEnumerator CompleteEnemyExit(EnemyInstance enemy)
{
if (enemy == null)
{
yield break;
}
_pendingDeathSpawns.TryGetValue(enemy, out IReadOnlyList<DeathSpawnRequest> deathSpawns);
_pendingDeathSpawns.Remove(enemy);
int removedIndex = _enemies.IndexOf(enemy);
if (removedIndex >= 0)
{
enemy.ClearDefeatedPendingRemoval();
_enemies.RemoveAt(removedIndex);
if (_selectedEnemyIndex >= _enemies.Count)
{
_selectedEnemyIndex = -1;
}
RecalculateKernelRamPressure();
StateChanged?.Invoke();
}
_enemiesPendingExit.Remove(enemy);
if (deathSpawns != null)
{
_deathSpawnSequencesActive++;
for (int i = 0; i < deathSpawns.Count; i++)
{
yield return WaitSequencedMs(CombatTuning.SpawnDelayMs, CombatTuning.ReducedMotionSpawnDelayMs);
EnemyInstance spawned = SpawnEnemy(deathSpawns[i].ArchetypeId, deathSpawns[i].UptimeBonus);
spawned.MarkSpawnedFromDeath();
spawned.PickNextIntent();
Log(deathSpawns[i].LogMessage);
StateChanged?.Invoke();
GameEvents.RaiseDeathSpawnedEnemy(new DeathSpawnedEnemyEvent(deathSpawns[i].Source, spawned));
yield return WaitSequencedMs(
CombatTuning.SpawnMaterializeMs + CombatTuning.SpawnTelegraphDelayMs,
CombatTuning.ReducedMotionSpawnMaterializeMs + CombatTuning.ReducedMotionSpawnTelegraphDelayMs);
spawned.RevealDeathSpawnIntent();
StateChanged?.Invoke();
}
_deathSpawnSequencesActive = Mathf.Max(0, _deathSpawnSequencesActive - 1);
}
}
private static IEnumerator WaitSequencedMs(int normalMs, int reducedMotionMs)
{
int durationMs = UIPreferences.ReducedMotion ? reducedMotionMs : normalMs;
if (durationMs > 0)
{
yield return new WaitForSeconds(durationMs / 1000f);
}
}
@@ -2004,11 +2227,9 @@ namespace KernelPanic.Combat
Log($"{survivor.Name} race link broke; survivor enraged");
}
private void SpawnOrphanFromDeath(EnemyInstance source)
private static DeathSpawnRequest CreateOrphanDeathSpawn(EnemyInstance source)
{
EnemyInstance orphan = SpawnEnemy("orphan_process", 0);
orphan.MarkSpawnedFromDeath();
Log($"{source.Name} left an orphan process");
return new DeathSpawnRequest(source?.State, "orphan_process", 0, $"{source?.Name ?? "enemy"} left an orphan process");
}
private void SpawnStructuralEnemies()
@@ -2473,11 +2694,16 @@ namespace KernelPanic.Combat
private bool CheckWin()
{
if (_awaitingWaveContinue || _runLost || _enemies.Count > 0)
if (_awaitingWaveContinue || _runLost)
{
return _awaitingWaveContinue;
}
if (LivingEnemyStates().Count > 0 || VisualResolutionPending)
{
return false;
}
_awaitingWaveContinue = true;
_selectedEnemyIndex = -1;
_pendingTargetCard = null;
@@ -2512,7 +2738,13 @@ namespace KernelPanic.Combat
return string.IsNullOrWhiteSpace(card.Definition.DisplayName) ? card.Definition.Id : card.Definition.DisplayName;
}
private bool IsCombatPaused => _awaitingWaveContinue || _runLost;
private bool VisualResolutionPending => _enemiesPendingExit.Count > 0
|| _enemyExitQueue.Count > 0
|| _enemyExitQueueDraining
|| _enemyExitAwaitingVisual != null
|| _deathSpawnSequencesActive > 0;
private bool IsCombatPaused => _awaitingWaveContinue || _runLost || VisualResolutionPending;
private enum DelayedCombatEffectKind
{
@@ -2543,5 +2775,21 @@ namespace KernelPanic.Combat
return new DelayedCombatEffect(DelayedCombatEffectKind.TimeshiftRestore, uptime, garbageCollectionsRemaining);
}
}
private readonly struct DeathSpawnRequest
{
public DeathSpawnRequest(CombatantState source, string archetypeId, int uptimeBonus, string logMessage)
{
Source = source;
ArchetypeId = archetypeId;
UptimeBonus = uptimeBonus;
LogMessage = logMessage;
}
public CombatantState Source { get; }
public string ArchetypeId { get; }
public int UptimeBonus { get; }
public string LogMessage { get; }
}
}
}
+22 -3
View File
@@ -41,9 +41,9 @@ namespace KernelPanic.Combat
public const int StatUpgradeHealPercent = 30;
public const int StatUpgradeRam = 1;
public const int UpgradeMagnitudeBonus = 2;
public const int FedoraBleedingEdgeBaseCrashChance = 5;
public const int FedoraBleedingEdgeCrashChanceStep = 5;
public const int FedoraBleedingEdgeMaxCrashChance = 90;
public const float FedoraBleedingEdgeBaseCrashChance = 5f;
public const float FedoraBleedingEdgeCrashChanceStep = 2.5f;
public const float FedoraBleedingEdgeMaxCrashChance = 90f;
public const int HitMagnitudeModeratePercent = 10;
public const int HitMagnitudeMajorPercent = 25;
public const int HitMagnitudeMassivePercent = 45;
@@ -66,6 +66,25 @@ namespace KernelPanic.Combat
public const float QueueCardResolveDelaySeconds = 0.45f;
public const float EnemyTelegraphDelaySeconds = 0.35f;
public const float EnemyActionDelaySeconds = 0.45f;
public const int DeathBeatMs = 420;
public const int PostDeathPauseMs = 180;
public const int OverflowTravelMs = 180;
public const int SpawnDelayMs = 220;
public const int SpawnMaterializeMs = 220;
public const int SpawnTelegraphDelayMs = 120;
public const int CombatBeatDefaultMs = 120;
public const int CombatBeatSettleMs = 60;
public const int VisualQueueMaxDrainMs = 2400;
public const int ReducedMotionDeathBeatMs = 90;
public const int ReducedMotionPostDeathPauseMs = 45;
public const int ReducedMotionOverflowTravelMs = 55;
public const int ReducedMotionSpawnDelayMs = 60;
public const int ReducedMotionSpawnMaterializeMs = 70;
public const int ReducedMotionSpawnTelegraphDelayMs = 35;
public const int ReducedMotionCombatBeatDefaultMs = 45;
public const int ReducedMotionCombatBeatMinMs = 35;
public const int ReducedMotionCombatBeatMaxMs = 90;
}
public enum HitMagnitudeTier
+23 -4
View File
@@ -61,6 +61,21 @@ namespace KernelPanic.Combat
{
public System.Func<DamageRequest, int, int> ResistanceResolver { get; set; }
public DamageResult DefeatCombatant(CombatantState target, CombatantState source, Language language)
{
if (target == null || target.IsDefeated)
{
return new DamageResult(0, false);
}
int uptimeDamage = Mathf.Max(0, target.CurrentUptime);
target.CurrentUptime = 0;
GameEvents.RaiseDamageDealt(new DamageDealtEvent(source, target, uptimeDamage, language, uptimeDamage, 0, false, 0, uptimeDamage, true));
target.IsDefeated = true;
GameEvents.RaiseCombatantDefeated(new CombatantDefeatedEvent(target));
return new DamageResult(uptimeDamage, true, false, 0, 0, uptimeDamage, uptimeDamage);
}
public DamageResult DealDamage(DamageRequest request)
{
if (request.Target == null || request.Target.IsDefeated)
@@ -70,9 +85,10 @@ namespace KernelPanic.Combat
int amount = Mathf.Max(0, request.Amount);
bool wasCritical = false;
int archBtwBonus = 0;
if (request.ApplySourceModifiers)
{
amount = ApplyFlatAdditions(amount, request);
amount = ApplyFlatAdditions(amount, request, out archBtwBonus);
amount = ApplyMultipliers(amount, request);
amount = ApplyCrit(amount, request, out wasCritical);
}
@@ -81,7 +97,8 @@ namespace KernelPanic.Combat
int finalAmount = ApplyShieldAndUptime(amount, request, out int absorbedAmount, out int shieldDamage, out int uptimeDamage);
ApplyArchRollingReleaseSave(request);
GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language, amount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage, request.TrueDamage));
bool rollingReleaseSaveTriggered = request.Target != null && request.Target.ArchRollingReleaseRecoveredThisHit;
GameEvents.RaiseDamageDealt(new DamageDealtEvent(request.Source, request.Target, finalAmount, request.Language, amount, absorbedAmount, wasCritical, shieldDamage, uptimeDamage, request.TrueDamage, archBtwBonus, rollingReleaseSaveTriggered));
bool defeated = false;
if (request.Target.CurrentUptime <= 0 && !request.Target.IsDefeated)
@@ -113,8 +130,9 @@ namespace KernelPanic.Combat
return Mathf.RoundToInt(amount * (multiplier / 100f) * (currentCardMultiplier / 100f));
}
private static int ApplyFlatAdditions(int amount, DamageRequest request)
private static int ApplyFlatAdditions(int amount, DamageRequest request, out int archBtwBonus)
{
archBtwBonus = 0;
if (amount == int.MaxValue)
{
return amount;
@@ -127,7 +145,8 @@ namespace KernelPanic.Combat
if (request.Source != null && (request.Language == Language.C || request.Language == Language.Rust))
{
amount += Mathf.Max(0, request.Source.ArchBtwStacks) * Mathf.Max(1, request.Source.ArchBtwDamagePerStack);
archBtwBonus = Mathf.Max(0, request.Source.ArchBtwStacks) * Mathf.Max(1, request.Source.ArchBtwDamagePerStack);
amount += archBtwBonus;
}
return amount;
+28 -1
View File
@@ -3,6 +3,21 @@ using System.Linq;
namespace KernelPanic.Combat
{
/// <summary>
/// Describes one card inspected by a top-of-deck peek (e.g. Ubuntu's apt update), for UI display.
/// </summary>
public readonly struct PeekedCardInfo
{
public PeekedCardInfo(CardInstance card, int cost)
{
Card = card;
Cost = cost;
}
public CardInstance Card { get; }
public int Cost { get; }
}
/// <summary>
/// Manages draw, discard, and exhaust piles for a combat deck.
/// </summary>
@@ -55,9 +70,11 @@ namespace KernelPanic.Combat
return drawn;
}
public bool TryDrawCheapestFromTop(int lookCount, out CardInstance card)
public bool TryDrawCheapestFromTop(int lookCount, out CardInstance card, out IReadOnlyList<PeekedCardInfo> peeked, out bool wasTie)
{
card = null;
peeked = System.Array.Empty<PeekedCardInfo>();
wasTie = false;
if (lookCount <= 0)
{
return false;
@@ -76,17 +93,27 @@ namespace KernelPanic.Combat
int inspectCount = System.Math.Min(lookCount, _drawPile.Count);
int selectedIndex = _drawPile.Count - 1;
int selectedCost = CombatManager.GetCardCost(_drawPile[selectedIndex]);
List<PeekedCardInfo> inspected = new(inspectCount) { new PeekedCardInfo(_drawPile[selectedIndex], selectedCost) };
int tieCount = 1;
for (int offset = 1; offset < inspectCount; offset++)
{
int index = _drawPile.Count - 1 - offset;
int cost = CombatManager.GetCardCost(_drawPile[index]);
inspected.Add(new PeekedCardInfo(_drawPile[index], cost));
if (cost < selectedCost)
{
selectedCost = cost;
selectedIndex = index;
tieCount = 1;
}
else if (cost == selectedCost)
{
tieCount++;
}
}
peeked = inspected;
wasTie = tieCount > 1;
card = _drawPile[selectedIndex];
_drawPile.RemoveAt(selectedIndex);
return card != null;
+19
View File
@@ -44,10 +44,12 @@ namespace KernelPanic.Combat
public bool PendingRevive { get; private set; }
public bool Reaped { get; private set; }
public bool LethalHitThisTurn { get; private set; }
public bool DefeatedPendingRemoval { get; private set; }
public int PairId { get; private set; } = -1;
public int RaceEnrageStacks { get; private set; }
public int TelemetryStacks { get; private set; }
public bool SpawnedFromDeath { get; private set; }
public bool DeathSpawnIntentRevealed { get; private set; } = true;
public bool HasPendingMarker => PendingRevive || HasRevived;
public bool HasSpecialSignal => HasSplitSignal || HasSegfaultSignal || HasRaceSignal || HasRootkitSignal || HasEliteSignal || HasRamPressureSignal || HasTelemetrySignal || HasCardLockerSignal || SpawnedFromDeath;
public bool HasSplitSignal => HasBehavior(EnemyBehaviorFlags.Split);
@@ -133,6 +135,12 @@ namespace KernelPanic.Combat
public void MarkSpawnedFromDeath()
{
SpawnedFromDeath = true;
DeathSpawnIntentRevealed = false;
}
public void RevealDeathSpawnIntent()
{
DeathSpawnIntentRevealed = true;
}
public void EnrageRaceSurvivor()
@@ -162,8 +170,19 @@ namespace KernelPanic.Combat
LethalHitThisTurn = true;
}
public void MarkDefeatedPendingRemoval()
{
DefeatedPendingRemoval = true;
}
public void ClearDefeatedPendingRemoval()
{
DefeatedPendingRemoval = false;
}
public void MarkPendingRevive()
{
DefeatedPendingRemoval = false;
PendingRevive = true;
State.IsDefeated = false;
State.CurrentUptime = 0;