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.Run;
|
||||
using KernelPanic.Meta;
|
||||
using KernelPanic.UI;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -44,6 +46,7 @@ namespace KernelPanic.Combat
|
||||
private int rawhideBonusCharges;
|
||||
private int queuedRepeatCharges;
|
||||
private int nextTurnCycleBonus;
|
||||
private Coroutine phaseCoroutine;
|
||||
|
||||
public TurnPhase CurrentPhase => currentPhase;
|
||||
public RunConfig RunConfig => runConfig;
|
||||
@@ -116,7 +119,12 @@ namespace KernelPanic.Combat
|
||||
|
||||
awaitingWaveContinue = false;
|
||||
DecayJavaWarmupForNextWave();
|
||||
StartWave(preservePlayerUptime: true);
|
||||
StartCoroutine(ContinueToNextWaveSequenced());
|
||||
}
|
||||
|
||||
private IEnumerator ContinueToNextWaveSequenced()
|
||||
{
|
||||
yield return StartWaveSequenced(preservePlayerUptime: true);
|
||||
SetPhase(TurnPhase.Allocate);
|
||||
}
|
||||
|
||||
@@ -458,7 +466,13 @@ namespace KernelPanic.Combat
|
||||
TurnPhase previousPhase = currentPhase;
|
||||
currentPhase = nextPhase;
|
||||
GameEvents.RaisePhaseChanged(new PhaseChangedEvent(previousPhase, nextPhase));
|
||||
EnterPhase(nextPhase);
|
||||
|
||||
if (phaseCoroutine != null)
|
||||
{
|
||||
StopCoroutine(phaseCoroutine);
|
||||
}
|
||||
|
||||
phaseCoroutine = StartCoroutine(EnterPhase(nextPhase));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
case TurnPhase.Boot:
|
||||
BootCombat();
|
||||
if (!IsCombatPaused)
|
||||
{
|
||||
AdvancePhase();
|
||||
}
|
||||
yield return BootCombatSequenced();
|
||||
break;
|
||||
case TurnPhase.Allocate:
|
||||
AllocateTurn();
|
||||
if (!IsCombatPaused)
|
||||
{
|
||||
AdvancePhase();
|
||||
}
|
||||
yield return AllocateTurnSequenced();
|
||||
break;
|
||||
case TurnPhase.Execute:
|
||||
StateChanged?.Invoke();
|
||||
break;
|
||||
yield break;
|
||||
case TurnPhase.Interpret:
|
||||
InterpretQueue();
|
||||
if (!IsCombatPaused)
|
||||
{
|
||||
AdvancePhase();
|
||||
}
|
||||
yield return InterpretQueueSequenced();
|
||||
break;
|
||||
case TurnPhase.EnemyProcess:
|
||||
ProcessEnemies();
|
||||
if (!IsCombatPaused)
|
||||
{
|
||||
AdvancePhase();
|
||||
}
|
||||
yield return ProcessEnemiesSequenced();
|
||||
break;
|
||||
case TurnPhase.GarbageCollection:
|
||||
GarbageCollect();
|
||||
if (!IsCombatPaused)
|
||||
{
|
||||
AdvancePhase();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!IsCombatPaused)
|
||||
{
|
||||
yield return Wait(CombatTuning.PhaseTransitionDelaySeconds);
|
||||
AdvancePhase();
|
||||
}
|
||||
}
|
||||
|
||||
private void BootCombat()
|
||||
private static IEnumerator Wait(float seconds)
|
||||
{
|
||||
if (!UIPreferences.ReducedMotion && seconds > 0f)
|
||||
{
|
||||
yield return new WaitForSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator BootCombatSequenced()
|
||||
{
|
||||
runLost = false;
|
||||
awaitingWaveContinue = false;
|
||||
@@ -537,12 +545,12 @@ namespace KernelPanic.Combat
|
||||
RandomRoll.Seed(runConfig.RunSeed);
|
||||
playerState = new CombatantState(runManager.EffectiveMaxUptime(), runManager.EffectiveRam(), runManager.EffectiveMaxCycles());
|
||||
ApplyVersionState(playerState);
|
||||
StartWave(preservePlayerUptime: false);
|
||||
yield return StartWaveSequenced(preservePlayerUptime: false);
|
||||
Log($"booted {runConfig.Distro.DisplayName} with {runManager.RunDeck.Count} cards");
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void StartWave(bool preservePlayerUptime)
|
||||
private IEnumerator StartWaveSequenced(bool preservePlayerUptime)
|
||||
{
|
||||
int carriedUptime = playerState == null ? 0 : playerState.CurrentUptime;
|
||||
if (!preservePlayerUptime || playerState == null)
|
||||
@@ -584,15 +592,16 @@ namespace KernelPanic.Combat
|
||||
enemies.Clear();
|
||||
SpawnStructuralEnemies();
|
||||
PickEnemyIntents();
|
||||
DrawOpeningHand();
|
||||
StateChanged?.Invoke();
|
||||
yield return DrawOpeningHandSequenced();
|
||||
skipNextAllocateDraw = true;
|
||||
}
|
||||
|
||||
private void AllocateTurn()
|
||||
private IEnumerator AllocateTurnSequenced()
|
||||
{
|
||||
if (CheckLoss())
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
playerState.Cycles = playerState.MaxCycles;
|
||||
@@ -606,9 +615,10 @@ namespace KernelPanic.Combat
|
||||
nextTurnCycleBonus = 0;
|
||||
}
|
||||
statusEffects.Tick(playerState, StatusTickTiming.StartOfTurn, playerState, damagePipeline);
|
||||
StateChanged?.Invoke();
|
||||
if (CheckLoss())
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
TryApplyUbuntuAptUpdate();
|
||||
@@ -619,13 +629,13 @@ namespace KernelPanic.Combat
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawForTurn();
|
||||
yield return DrawForTurnSequenced();
|
||||
}
|
||||
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void InterpretQueue()
|
||||
private IEnumerator InterpretQueueSequenced()
|
||||
{
|
||||
while (interpreterQueue.TryDequeue(out CardInstance card))
|
||||
{
|
||||
@@ -636,24 +646,27 @@ namespace KernelPanic.Combat
|
||||
ExecuteCardEffects(card, ResolveQueuedTargets(card));
|
||||
if (CheckWinOrLoss())
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
ResolveCardRiders(card);
|
||||
deckController.Discard(card);
|
||||
GameEvents.RaiseCardResolved(new CardResolvedEvent(card, ResolutionTrack.InterpreterQueue));
|
||||
StateChanged?.Invoke();
|
||||
if (CheckWinOrLoss())
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return Wait(CombatTuning.QueueCardResolveDelaySeconds);
|
||||
}
|
||||
|
||||
CheckWinOrLoss();
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void ProcessEnemies()
|
||||
private IEnumerator ProcessEnemiesSequenced()
|
||||
{
|
||||
for (int i = 0; i < enemies.Count; i++)
|
||||
{
|
||||
@@ -667,7 +680,7 @@ namespace KernelPanic.Combat
|
||||
RemoveDefeatedEnemies();
|
||||
if (CheckWinOrLoss())
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (enemy.State.IsDefeated)
|
||||
@@ -677,12 +690,18 @@ namespace KernelPanic.Combat
|
||||
|
||||
Log($"{enemy.Name} executes {enemy.CurrentIntent.DisplayText}");
|
||||
GameEvents.RaiseEnemyWouldAct(new EnemyWouldActEvent(enemy));
|
||||
StateChanged?.Invoke();
|
||||
yield return Wait(CombatTuning.EnemyTelegraphDelaySeconds);
|
||||
|
||||
ExecuteEnemyIntent(enemy);
|
||||
GameEvents.RaiseEnemyActed(new EnemyActedEvent(enemy, enemy.CurrentIntent));
|
||||
StateChanged?.Invoke();
|
||||
if (CheckLoss())
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return Wait(CombatTuning.EnemyActionDelaySeconds);
|
||||
}
|
||||
|
||||
PickEnemyIntents();
|
||||
@@ -710,13 +729,13 @@ namespace KernelPanic.Combat
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void DrawOpeningHand()
|
||||
private IEnumerator DrawOpeningHandSequenced()
|
||||
{
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
int room = handController.RemainingRam;
|
||||
if (room <= 0)
|
||||
{
|
||||
Log($"{label}: hand full");
|
||||
return;
|
||||
yield break;
|
||||
}
|
||||
|
||||
int drawCount = requestedCount;
|
||||
IReadOnlyList<CardInstance> drawn = deckController.Draw(drawCount);
|
||||
IReadOnlyList<CardInstance> drawn = deckController.Draw(requestedCount);
|
||||
int added = 0;
|
||||
for (int i = 0; i < drawn.Count; i++)
|
||||
{
|
||||
if (handController.Add(drawn[i]))
|
||||
{
|
||||
added++;
|
||||
StateChanged?.Invoke();
|
||||
yield return Wait(CombatTuning.CardDrawDelaySeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -806,7 +826,7 @@ namespace KernelPanic.Combat
|
||||
}
|
||||
|
||||
ubuntuEmptyHandRefillUsed = true;
|
||||
DrawCardsToHand(playerState.Ram, "ubuntu 24.04 refill");
|
||||
StartCoroutine(DrawCardsToHandSequenced(playerState.Ram, "ubuntu 24.04 refill"));
|
||||
}
|
||||
|
||||
private bool CanApplyFedoraBonus()
|
||||
@@ -902,7 +922,7 @@ namespace KernelPanic.Combat
|
||||
return;
|
||||
}
|
||||
|
||||
DrawCardsToHand(card.DrawRider, "upgrade rider");
|
||||
StartCoroutine(DrawCardsToHandSequenced(card.DrawRider, "upgrade rider"));
|
||||
}
|
||||
|
||||
private IReadOnlyList<CombatantState> CaptureTargets(CardInstance card)
|
||||
|
||||
@@ -41,5 +41,14 @@ namespace KernelPanic.Combat
|
||||
public const int StatUpgradeHeal = 5;
|
||||
public const int StatUpgradeRam = 1;
|
||||
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> previousShield = new();
|
||||
private readonly HashSet<CardInstance> knownHandCards = new();
|
||||
private int queueCascadeIndex;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -1048,14 +1047,9 @@ namespace KernelPanic.UI
|
||||
|
||||
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;
|
||||
PlayElementBeat(chip, "queue-chip-resolve", 260);
|
||||
FlyCardGhost(payload.Card, chip, turnResourceGrid, "feedback-card-discard");
|
||||
}, delay);
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1069,11 +1063,6 @@ namespace KernelPanic.UI
|
||||
|
||||
private void HandlePhaseChanged(PhaseChangedEvent payload)
|
||||
{
|
||||
if (payload.NextPhase == TurnPhase.Interpret)
|
||||
{
|
||||
queueCascadeIndex = 0;
|
||||
}
|
||||
|
||||
ScheduleFeedback(() => PlayElementBeat(phaseLabel, "phase-pulse", 260), 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user