Pace combat resolution across phases instead of resolving instantly
Convert CombatManager's phase state machine to coroutines so drawing, interpreter-queue cards, and enemy actions each resolve one at a time with visible delays, instead of an entire phase mutating state in a single synchronous frame. Delays are skipped when UIPreferences.ReducedMotion is set. Also drops the UI's old artificial cascade stagger for queued-card animations since real per-card timing now provides the spacing.
This commit is contained in:
@@ -2,8 +2,10 @@ using KernelPanic.Core;
|
|||||||
using KernelPanic.Data;
|
using KernelPanic.Data;
|
||||||
using KernelPanic.Run;
|
using KernelPanic.Run;
|
||||||
using KernelPanic.Meta;
|
using KernelPanic.Meta;
|
||||||
|
using KernelPanic.UI;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ namespace KernelPanic.Combat
|
|||||||
private int rawhideBonusCharges;
|
private int rawhideBonusCharges;
|
||||||
private int queuedRepeatCharges;
|
private int queuedRepeatCharges;
|
||||||
private int nextTurnCycleBonus;
|
private int nextTurnCycleBonus;
|
||||||
|
private Coroutine phaseCoroutine;
|
||||||
|
|
||||||
public TurnPhase CurrentPhase => currentPhase;
|
public TurnPhase CurrentPhase => currentPhase;
|
||||||
public RunConfig RunConfig => runConfig;
|
public RunConfig RunConfig => runConfig;
|
||||||
@@ -116,7 +119,12 @@ namespace KernelPanic.Combat
|
|||||||
|
|
||||||
awaitingWaveContinue = false;
|
awaitingWaveContinue = false;
|
||||||
DecayJavaWarmupForNextWave();
|
DecayJavaWarmupForNextWave();
|
||||||
StartWave(preservePlayerUptime: true);
|
StartCoroutine(ContinueToNextWaveSequenced());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ContinueToNextWaveSequenced()
|
||||||
|
{
|
||||||
|
yield return StartWaveSequenced(preservePlayerUptime: true);
|
||||||
SetPhase(TurnPhase.Allocate);
|
SetPhase(TurnPhase.Allocate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +466,13 @@ namespace KernelPanic.Combat
|
|||||||
TurnPhase previousPhase = currentPhase;
|
TurnPhase previousPhase = currentPhase;
|
||||||
currentPhase = nextPhase;
|
currentPhase = nextPhase;
|
||||||
GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase));
|
GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase));
|
||||||
EnterPhase(nextPhase);
|
|
||||||
|
if (phaseCoroutine != null)
|
||||||
|
{
|
||||||
|
StopCoroutine(phaseCoroutine);
|
||||||
|
}
|
||||||
|
|
||||||
|
phaseCoroutine = StartCoroutine(EnterPhase(nextPhase));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TurnPhase GetNextPhase(TurnPhase phase)
|
private static TurnPhase GetNextPhase(TurnPhase phase)
|
||||||
@@ -474,52 +488,46 @@ namespace KernelPanic.Combat
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnterPhase(TurnPhase phase)
|
private IEnumerator EnterPhase(TurnPhase phase)
|
||||||
{
|
{
|
||||||
switch (phase)
|
switch (phase)
|
||||||
{
|
{
|
||||||
case TurnPhase.Boot:
|
case TurnPhase.Boot:
|
||||||
BootCombat();
|
yield return BootCombatSequenced();
|
||||||
if (!IsCombatPaused)
|
|
||||||
{
|
|
||||||
AdvancePhase();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case TurnPhase.Allocate:
|
case TurnPhase.Allocate:
|
||||||
AllocateTurn();
|
yield return AllocateTurnSequenced();
|
||||||
if (!IsCombatPaused)
|
|
||||||
{
|
|
||||||
AdvancePhase();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case TurnPhase.Execute:
|
case TurnPhase.Execute:
|
||||||
StateChanged?.Invoke();
|
StateChanged?.Invoke();
|
||||||
break;
|
yield break;
|
||||||
case TurnPhase.Interpret:
|
case TurnPhase.Interpret:
|
||||||
InterpretQueue();
|
yield return InterpretQueueSequenced();
|
||||||
if (!IsCombatPaused)
|
|
||||||
{
|
|
||||||
AdvancePhase();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case TurnPhase.EnemyProcess:
|
case TurnPhase.EnemyProcess:
|
||||||
ProcessEnemies();
|
yield return ProcessEnemiesSequenced();
|
||||||
if (!IsCombatPaused)
|
|
||||||
{
|
|
||||||
AdvancePhase();
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case TurnPhase.GarbageCollection:
|
case TurnPhase.GarbageCollection:
|
||||||
GarbageCollect();
|
GarbageCollect();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsCombatPaused)
|
if (!IsCombatPaused)
|
||||||
{
|
{
|
||||||
|
yield return Wait(CombatTuning.PhaseTransitionDelaySeconds);
|
||||||
AdvancePhase();
|
AdvancePhase();
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BootCombat()
|
private static IEnumerator Wait(float seconds)
|
||||||
|
{
|
||||||
|
if (!UIPreferences.ReducedMotion && seconds > 0f)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(seconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator BootCombatSequenced()
|
||||||
{
|
{
|
||||||
runLost = false;
|
runLost = false;
|
||||||
awaitingWaveContinue = false;
|
awaitingWaveContinue = false;
|
||||||
@@ -537,12 +545,12 @@ namespace KernelPanic.Combat
|
|||||||
RandomRoll.Seed(runConfig.RunSeed);
|
RandomRoll.Seed(runConfig.RunSeed);
|
||||||
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
|
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
|
||||||
ApplyVersionState(playerState);
|
ApplyVersionState(playerState);
|
||||||
StartWave(preservePlayerUptime: false);
|
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();
|
StateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartWave(bool preservePlayerUptime)
|
private IEnumerator StartWaveSequenced(bool preservePlayerUptime)
|
||||||
{
|
{
|
||||||
int carriedUptime = playerState == null ? 0 : playerState.CurrentUptime;
|
int carriedUptime = playerState == null ? 0 : playerState.CurrentUptime;
|
||||||
if (!preservePlayerUptime || playerState == null)
|
if (!preservePlayerUptime || playerState == null)
|
||||||
@@ -584,15 +592,16 @@ namespace KernelPanic.Combat
|
|||||||
enemies.Clear();
|
enemies.Clear();
|
||||||
SpawnStructuralEnemies();
|
SpawnStructuralEnemies();
|
||||||
PickEnemyIntents();
|
PickEnemyIntents();
|
||||||
DrawOpeningHand();
|
StateChanged?.Invoke();
|
||||||
|
yield return DrawOpeningHandSequenced();
|
||||||
skipNextAllocateDraw = true;
|
skipNextAllocateDraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AllocateTurn()
|
private IEnumerator AllocateTurnSequenced()
|
||||||
{
|
{
|
||||||
if (CheckLoss())
|
if (CheckLoss())
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerState.Cycles = playerState.MaxCycles;
|
playerState.Cycles = playerState.MaxCycles;
|
||||||
@@ -606,9 +615,10 @@ namespace KernelPanic.Combat
|
|||||||
nextTurnCycleBonus = 0;
|
nextTurnCycleBonus = 0;
|
||||||
}
|
}
|
||||||
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
|
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
|
||||||
|
StateChanged?.Invoke();
|
||||||
if (CheckLoss())
|
if (CheckLoss())
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryApplyUbuntuAptUpdate();
|
TryApplyUbuntuAptUpdate();
|
||||||
@@ -619,13 +629,13 @@ namespace KernelPanic.Combat
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DrawForTurn();
|
yield return DrawForTurnSequenced();
|
||||||
}
|
}
|
||||||
|
|
||||||
StateChanged?.Invoke();
|
StateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InterpretQueue()
|
private IEnumerator InterpretQueueSequenced()
|
||||||
{
|
{
|
||||||
while (interpreterQueue.TryDequeue(out CardInstance card))
|
while (interpreterQueue.TryDequeue(out CardInstance card))
|
||||||
{
|
{
|
||||||
@@ -636,24 +646,27 @@ namespace KernelPanic.Combat
|
|||||||
ExecuteCardEffects(card, ResolveQueuedTargets(card));
|
ExecuteCardEffects(card, ResolveQueuedTargets(card));
|
||||||
if (CheckWinOrLoss())
|
if (CheckWinOrLoss())
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolveCardRiders(card);
|
ResolveCardRiders(card);
|
||||||
deckController.Discard(card);
|
deckController.Discard(card);
|
||||||
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
|
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
|
||||||
|
StateChanged?.Invoke();
|
||||||
if (CheckWinOrLoss())
|
if (CheckWinOrLoss())
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield return Wait(CombatTuning.QueueCardResolveDelaySeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckWinOrLoss();
|
CheckWinOrLoss();
|
||||||
StateChanged?.Invoke();
|
StateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ProcessEnemies()
|
private IEnumerator ProcessEnemiesSequenced()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < enemies.Count; i++)
|
for (int i = 0; i < enemies.Count; i++)
|
||||||
{
|
{
|
||||||
@@ -667,7 +680,7 @@ namespace KernelPanic.Combat
|
|||||||
RemoveDefeatedEnemies();
|
RemoveDefeatedEnemies();
|
||||||
if (CheckWinOrLoss())
|
if (CheckWinOrLoss())
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enemy.State.IsDefeated)
|
if (enemy.State.IsDefeated)
|
||||||
@@ -677,12 +690,18 @@ namespace KernelPanic.Combat
|
|||||||
|
|
||||||
Log($"{enemy.Name} executes {enemy.CurrentIntent.DisplayText}");
|
Log($"{enemy.Name} executes {enemy.CurrentIntent.DisplayText}");
|
||||||
GameEvents.RaiseEnemyWouldAct(new EnemyWouldActEvent(enemy));
|
GameEvents.RaiseEnemyWouldAct(new EnemyWouldActEvent(enemy));
|
||||||
|
StateChanged?.Invoke();
|
||||||
|
yield return Wait(CombatTuning.EnemyTelegraphDelaySeconds);
|
||||||
|
|
||||||
ExecuteEnemyIntent(enemy);
|
ExecuteEnemyIntent(enemy);
|
||||||
GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent));
|
GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent));
|
||||||
|
StateChanged?.Invoke();
|
||||||
if (CheckLoss())
|
if (CheckLoss())
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
yield return Wait(CombatTuning.EnemyActionDelaySeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
PickEnemyIntents();
|
PickEnemyIntents();
|
||||||
@@ -710,13 +729,13 @@ namespace KernelPanic.Combat
|
|||||||
StateChanged?.Invoke();
|
StateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawOpeningHand()
|
private IEnumerator DrawOpeningHandSequenced()
|
||||||
{
|
{
|
||||||
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, playerState.Ram);
|
int openingDraw = Mathf.Min(CombatTuning.OpeningHandSize, playerState.Ram);
|
||||||
DrawCardsToHand(openingDraw, "opening hand");
|
yield return DrawCardsToHandSequenced(openingDraw, "opening hand");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawForTurn()
|
private IEnumerator DrawForTurnSequenced()
|
||||||
{
|
{
|
||||||
int requested = CombatTuning.DrawPerTurn;
|
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)
|
||||||
@@ -724,31 +743,32 @@ namespace KernelPanic.Combat
|
|||||||
requested = Mathf.Max(requested, CombatTuning.MinimumHandFloor - handController.Cards.Count);
|
requested = Mathf.Max(requested, CombatTuning.MinimumHandFloor - handController.Cards.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawCardsToHand(requested, "turn draw");
|
yield return DrawCardsToHandSequenced(requested, "turn draw");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawCardsToHand(int requestedCount, string label)
|
private IEnumerator DrawCardsToHandSequenced(int requestedCount, string label)
|
||||||
{
|
{
|
||||||
if (handController == null || playerState == null || requestedCount <= 0)
|
if (handController == null || playerState == null || requestedCount <= 0)
|
||||||
{
|
{
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
int room = handController.RemainingRam;
|
int room = handController.RemainingRam;
|
||||||
if (room <= 0)
|
if (room <= 0)
|
||||||
{
|
{
|
||||||
Log($"{label}: hand full");
|
Log($"{label}: hand full");
|
||||||
return;
|
yield break;
|
||||||
}
|
}
|
||||||
|
|
||||||
int drawCount = requestedCount;
|
IReadOnlyList<CardInstance> drawn = deckController.Draw(requestedCount);
|
||||||
IReadOnlyList<CardInstance> drawn = deckController.Draw(drawCount);
|
|
||||||
int added = 0;
|
int added = 0;
|
||||||
for (int i = 0; i < drawn.Count; i++)
|
for (int i = 0; i < drawn.Count; i++)
|
||||||
{
|
{
|
||||||
if (handController.Add(drawn[i]))
|
if (handController.Add(drawn[i]))
|
||||||
{
|
{
|
||||||
added++;
|
added++;
|
||||||
|
StateChanged?.Invoke();
|
||||||
|
yield return Wait(CombatTuning.CardDrawDelaySeconds);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -806,7 +826,7 @@ namespace KernelPanic.Combat
|
|||||||
}
|
}
|
||||||
|
|
||||||
ubuntuEmptyHandRefillUsed = true;
|
ubuntuEmptyHandRefillUsed = true;
|
||||||
DrawCardsToHand(playerState.Ram, "ubuntu 24.04 refill");
|
StartCoroutine(DrawCardsToHandSequenced(playerState.Ram, "ubuntu 24.04 refill"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanApplyFedoraBonus()
|
private bool CanApplyFedoraBonus()
|
||||||
@@ -902,7 +922,7 @@ namespace KernelPanic.Combat
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawCardsToHand(card.DrawRider, "upgrade rider");
|
StartCoroutine(DrawCardsToHandSequenced(card.DrawRider, "upgrade rider"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<CombatantState> CaptureTargets(CardInstance card)
|
private IReadOnlyList<CombatantState> CaptureTargets(CardInstance card)
|
||||||
|
|||||||
@@ -41,5 +41,14 @@ namespace KernelPanic.Combat
|
|||||||
public const int StatUpgradeHeal = 5;
|
public const int StatUpgradeHeal = 5;
|
||||||
public const int StatUpgradeRam = 1;
|
public const int StatUpgradeRam = 1;
|
||||||
public const int UpgradeMagnitudeBonus = 2;
|
public const int UpgradeMagnitudeBonus = 2;
|
||||||
|
|
||||||
|
// Combat pacing: gaps between phase transitions and per-item resolution steps so
|
||||||
|
// players can follow drawing/queue/enemy resolution instead of seeing it all at once.
|
||||||
|
// Skipped entirely when UIPreferences.ReducedMotion is set.
|
||||||
|
public const float PhaseTransitionDelaySeconds = 0.35f;
|
||||||
|
public const float CardDrawDelaySeconds = 0.12f;
|
||||||
|
public const float QueueCardResolveDelaySeconds = 0.45f;
|
||||||
|
public const float EnemyTelegraphDelaySeconds = 0.35f;
|
||||||
|
public const float EnemyActionDelaySeconds = 0.45f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ namespace KernelPanic.UI
|
|||||||
private readonly Dictionary<CombatantState, int> previousUptime = new();
|
private readonly Dictionary<CombatantState, int> previousUptime = new();
|
||||||
private readonly Dictionary<CombatantState, int> previousShield = new();
|
private readonly Dictionary<CombatantState, int> previousShield = new();
|
||||||
private readonly HashSet<CardInstance> knownHandCards = new();
|
private readonly HashSet<CardInstance> knownHandCards = new();
|
||||||
private int queueCascadeIndex;
|
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -1047,15 +1046,10 @@ namespace KernelPanic.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (payload.Track == ResolutionTrack.InterpreterQueue)
|
if (payload.Track == ResolutionTrack.InterpreterQueue)
|
||||||
{
|
|
||||||
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);
|
PlayElementBeat(chip, "queue-chip-resolve", 260);
|
||||||
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
|
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
|
||||||
}, delay);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,11 +1063,6 @@ namespace KernelPanic.UI
|
|||||||
|
|
||||||
private void HandlePhaseChanged(PhaseChangedEvent payload)
|
private void HandlePhaseChanged(PhaseChangedEvent payload)
|
||||||
{
|
{
|
||||||
if (payload.NextPhase == TurnPhase.Interpret)
|
|
||||||
{
|
|
||||||
queueCascadeIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScheduleFeedback(() => PlayElementBeat(phaseLabel, "phase-pulse", 260), 0);
|
ScheduleFeedback(() => PlayElementBeat(phaseLabel, "phase-pulse", 260), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user