Fix various issues related to styling and animations
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using KernelPanic.Combat;
|
||||
|
||||
namespace KernelPanic.Core
|
||||
@@ -12,7 +13,10 @@ namespace KernelPanic.Core
|
||||
public static event Action<CardResolvedEvent> CardResolved;
|
||||
public static event Action<PhaseChangedEvent> PhaseChanged;
|
||||
public static event Action<DamageDealtEvent> DamageDealt;
|
||||
public static event Action<OverflowDamageTravelEvent> OverflowDamageTravel;
|
||||
public static event Action<CombatantDefeatedEvent> CombatantDefeated;
|
||||
public static event Action<CombatantDeathVisualCompletedEvent> CombatantDeathVisualCompleted;
|
||||
public static event Action<DeathSpawnedEnemyEvent> DeathSpawnedEnemy;
|
||||
public static event Action<EnemyWouldActEvent> EnemyWouldAct;
|
||||
public static event Action<EnemyActedEvent> EnemyActed;
|
||||
public static event Action<PlayerDamagedEvent> PlayerDamaged;
|
||||
@@ -24,6 +28,9 @@ namespace KernelPanic.Core
|
||||
public static event Action<StatusExpiredEvent> StatusExpired;
|
||||
public static event Action<StatusCleansedEvent> StatusCleansed;
|
||||
public static event Action<RunEndedEvent> RunEnded;
|
||||
public static event Action<UbuntuAptUpdatePeekedEvent> UbuntuAptUpdatePeeked;
|
||||
public static event Action<FedoraBleedingEdgeEvent> FedoraBleedingEdgeTriggered;
|
||||
public static event Action<ArchBtwTurnEndedEvent> ArchBtwTurnEnded;
|
||||
|
||||
public static void RaiseCardPlayed(CardPlayedEvent payload)
|
||||
{
|
||||
@@ -45,11 +52,26 @@ namespace KernelPanic.Core
|
||||
DamageDealt?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseOverflowDamageTravel(OverflowDamageTravelEvent payload)
|
||||
{
|
||||
OverflowDamageTravel?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseCombatantDefeated(CombatantDefeatedEvent payload)
|
||||
{
|
||||
CombatantDefeated?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseCombatantDeathVisualCompleted(CombatantDeathVisualCompletedEvent payload)
|
||||
{
|
||||
CombatantDeathVisualCompleted?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseDeathSpawnedEnemy(DeathSpawnedEnemyEvent payload)
|
||||
{
|
||||
DeathSpawnedEnemy?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseEnemyWouldAct(EnemyWouldActEvent payload)
|
||||
{
|
||||
EnemyWouldAct?.Invoke(payload);
|
||||
@@ -104,6 +126,21 @@ namespace KernelPanic.Core
|
||||
{
|
||||
RunEnded?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseUbuntuAptUpdatePeeked(UbuntuAptUpdatePeekedEvent payload)
|
||||
{
|
||||
UbuntuAptUpdatePeeked?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseFedoraBleedingEdgeTriggered(FedoraBleedingEdgeEvent payload)
|
||||
{
|
||||
FedoraBleedingEdgeTriggered?.Invoke(payload);
|
||||
}
|
||||
|
||||
public static void RaiseArchBtwTurnEnded(ArchBtwTurnEndedEvent payload)
|
||||
{
|
||||
ArchBtwTurnEnded?.Invoke(payload);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -171,7 +208,7 @@ namespace KernelPanic.Core
|
||||
{
|
||||
}
|
||||
|
||||
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage, bool trueDamage)
|
||||
public DamageDealtEvent(CombatantState source, CombatantState target, int amount, Language language, int incomingAmount, int absorbedAmount, bool wasCritical, int shieldDamage, int uptimeDamage, bool trueDamage, int archBtwBonusAmount = 0, bool archRollingReleaseSaveTriggered = false)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
@@ -183,6 +220,8 @@ namespace KernelPanic.Core
|
||||
ShieldDamage = shieldDamage;
|
||||
UptimeDamage = uptimeDamage;
|
||||
TrueDamage = trueDamage;
|
||||
ArchBtwBonusAmount = archBtwBonusAmount;
|
||||
ArchRollingReleaseSaveTriggered = archRollingReleaseSaveTriggered;
|
||||
}
|
||||
|
||||
public CombatantState Source { get; }
|
||||
@@ -195,6 +234,8 @@ namespace KernelPanic.Core
|
||||
public int ShieldDamage { get; }
|
||||
public int UptimeDamage { get; }
|
||||
public bool TrueDamage { get; }
|
||||
public int ArchBtwBonusAmount { get; }
|
||||
public bool ArchRollingReleaseSaveTriggered { get; }
|
||||
public bool WasFullyBlocked => IncomingAmount > 0 && UptimeDamage <= 0;
|
||||
public bool WasMitigated => ShieldDamage > 0 || AbsorbedAmount > 0 || WasFullyBlocked;
|
||||
}
|
||||
@@ -212,6 +253,46 @@ namespace KernelPanic.Core
|
||||
public CombatantState Combatant { get; }
|
||||
}
|
||||
|
||||
public readonly struct OverflowDamageTravelEvent
|
||||
{
|
||||
public OverflowDamageTravelEvent(CombatantState source, CombatantState from, CombatantState to, int amount, Language language)
|
||||
{
|
||||
Source = source;
|
||||
From = from;
|
||||
To = to;
|
||||
Amount = amount;
|
||||
Language = language;
|
||||
}
|
||||
|
||||
public CombatantState Source { get; }
|
||||
public CombatantState From { get; }
|
||||
public CombatantState To { get; }
|
||||
public int Amount { get; }
|
||||
public Language Language { get; }
|
||||
}
|
||||
|
||||
public readonly struct CombatantDeathVisualCompletedEvent
|
||||
{
|
||||
public CombatantDeathVisualCompletedEvent(CombatantState combatant)
|
||||
{
|
||||
Combatant = combatant;
|
||||
}
|
||||
|
||||
public CombatantState Combatant { get; }
|
||||
}
|
||||
|
||||
public readonly struct DeathSpawnedEnemyEvent
|
||||
{
|
||||
public DeathSpawnedEnemyEvent(CombatantState source, EnemyInstance enemy)
|
||||
{
|
||||
Source = source;
|
||||
Enemy = enemy;
|
||||
}
|
||||
|
||||
public CombatantState Source { get; }
|
||||
public EnemyInstance Enemy { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an enemy placeholder reaching its structural action point.
|
||||
/// </summary>
|
||||
@@ -348,4 +429,62 @@ namespace KernelPanic.Core
|
||||
public bool PlayerDied { get; }
|
||||
public int EntropyEarned { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an Ubuntu "apt update" peek-and-pick: the cards inspected at the top of the draw
|
||||
/// pile and which one was added to hand.
|
||||
/// </summary>
|
||||
public readonly struct UbuntuAptUpdatePeekedEvent
|
||||
{
|
||||
public UbuntuAptUpdatePeekedEvent(IReadOnlyList<PeekedCardInfo> peeked, CardInstance chosen, bool wasTie, int lookCount)
|
||||
{
|
||||
Peeked = peeked;
|
||||
Chosen = chosen;
|
||||
WasTie = wasTie;
|
||||
LookCount = lookCount;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PeekedCardInfo> Peeked { get; }
|
||||
public CardInstance Chosen { get; }
|
||||
public bool WasTie { get; }
|
||||
public int LookCount { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the outcome of Fedora's "Bleeding Edge" crash roll for a bonus-eligible card.
|
||||
/// </summary>
|
||||
public readonly struct FedoraBleedingEdgeEvent
|
||||
{
|
||||
public FedoraBleedingEdgeEvent(CardInstance card, bool crashed, bool firstBonusThisTurn, int damageMultiplierPercent, float crashChanceRolled, float crashChanceAfter)
|
||||
{
|
||||
Card = card;
|
||||
Crashed = crashed;
|
||||
FirstBonusThisTurn = firstBonusThisTurn;
|
||||
DamageMultiplierPercent = damageMultiplierPercent;
|
||||
CrashChanceRolled = crashChanceRolled;
|
||||
CrashChanceAfter = crashChanceAfter;
|
||||
}
|
||||
|
||||
public CardInstance Card { get; }
|
||||
public bool Crashed { get; }
|
||||
public bool FirstBonusThisTurn { get; }
|
||||
public int DamageMultiplierPercent { get; }
|
||||
public float CrashChanceRolled { get; }
|
||||
public float CrashChanceAfter { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the end-of-turn resolution of Arch's btw stacks: whether they reset or persisted.
|
||||
/// </summary>
|
||||
public readonly struct ArchBtwTurnEndedEvent
|
||||
{
|
||||
public ArchBtwTurnEndedEvent(int stacksBefore, bool persisted)
|
||||
{
|
||||
StacksBefore = stacksBefore;
|
||||
Persisted = persisted;
|
||||
}
|
||||
|
||||
public int StacksBefore { get; }
|
||||
public bool Persisted { get; }
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user