Give somebody the turn, and let them spend it #39

Merged
icub3d merged 1 commit from 8-mission-phases into main 2026-09-13 22:08:22 +00:00
Owner

Declares the mission loop rather than leaving it implied, and closes the last gap between
"an action can be resolved" and "a player can take one".

The phases

MissionPhase is a sub-state of GameState::Mission — PlayerTurn, EnemyTurn,
Resolving, Animating — following MenuScreen, so leaving a mission tears it down for
free and systems are gated on a phase rather than asking "is it my turn?" in their bodies.

Turn names the acting unit and is absent between turns, which is the signal
begin_turn waits for: it advances the scheduler exactly once per turn and refills what
that unit has to spend. Advancing merely to look at who is next would spend a turn
nobody took.

Nothing is driven by a timer. A turn ends when its allowance is gone; Animating passes
straight through until playback (#9) gives it something to wait for.

The ordering block earned its keep immediately

CombatSet declares the whole order in one place: Reconcile, then Turn, then Act.

Worth recording: adding CombatSet::Turn to the enum without adding it to
configure_sets gave it no run condition at all, so combat systems ran during Loading
and panicked looking for a Scheduler that does not exist yet. The single block is what
makes that kind of mistake visible — which is the argument this issue was making.

The third stage of the two-stage commit

Pressing a chosen destination again raises Confirmed from the map. That says only that
the player asked; whether it is that unit's turn is combat's question and is answered
there. The dependency stays one-way — combat already reads the map, and the map still
knows nothing about turns.

Hostiles give up their turn immediately. A placeholder, and the loop does not turn without
it: the opening turn belongs to the fastest unit, which in the shipped deployment is a
hostile, so without this a mission stalls before the player has moved once. #11 replaces
the body rather than the shape.

The two-representation fix you asked for

ADR 0008 flagged the snapshot and the components as two representations that could drift.
The write-back now destructures Combatant exhaustively, so adding a field stops it
compiling until somebody decides whether the world needs to hear about it — a silent bug
turned into a build error. The ignored fields (unit, team, movement, attack) are
ignored on purpose and say why.

Only changed units are written back, because writing every one marks every GridPos
changed and the movement preview rebuilds on exactly that signal — an attack across the
map would have quietly redrawn an overlay that never moved.

Acceptance criteria

  • MissionPhase does not exist outside GameState::Mission — asserted in the
    style of menu_screen_exists_only_inside_main_menu, including that it is absent in
    the menu and torn down on leaving.
  • Transitions driven by the scheduler and by playback, not timers — begin_turn
    pulls from the scheduler, finish_animating ends or continues the turn. No Timer
    anywhere.
  • Full ordering in a single readable block — and the bug above is why that matters.

Verification

cargo fmt --check, cargo clippy --all-targets -- -D warnings (and --features perf),
cargo test — 171 passing, up from 163.

Ran on a Pixel Tablet: a unit selects, a destination holds, confirming walks it — but
only for the unit whose turn it is, and nothing on screen says which that is. I spent
several attempts tapping the wrong unit before testing the path headlessly. The loop is
sound; the game is hard to play by eye until the HUD lands. That makes #10 blocking in
practice
, not just desirable.

Closes #8

🤖 Generated with Claude Code

https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh

Declares the mission loop rather than leaving it implied, and closes the last gap between "an action can be resolved" and "a player can take one". ## The phases `MissionPhase` is a sub-state of `GameState::Mission` — `PlayerTurn`, `EnemyTurn`, `Resolving`, `Animating` — following `MenuScreen`, so leaving a mission tears it down for free and systems are gated on a phase rather than asking "is it my turn?" in their bodies. `Turn` names the acting unit and is **absent between turns**, which is the signal `begin_turn` waits for: it advances the scheduler exactly once per turn and refills what that unit has to spend. Advancing merely to *look* at who is next would spend a turn nobody took. Nothing is driven by a timer. A turn ends when its allowance is gone; `Animating` passes straight through until playback (#9) gives it something to wait for. ## The ordering block earned its keep immediately `CombatSet` declares the whole order in one place: `Reconcile`, then `Turn`, then `Act`. Worth recording: adding `CombatSet::Turn` to the enum *without* adding it to `configure_sets` gave it no run condition at all, so combat systems ran during `Loading` and panicked looking for a `Scheduler` that does not exist yet. The single block is what makes that kind of mistake visible — which is the argument this issue was making. ## The third stage of the two-stage commit Pressing a chosen destination again raises `Confirmed` from the map. That says only that the player asked; whether it is that unit's turn is combat's question and is answered there. The dependency stays one-way — combat already reads the map, and the map still knows nothing about turns. Hostiles give up their turn immediately. A placeholder, and the loop does not turn without it: the opening turn belongs to the fastest unit, which in the shipped deployment is a hostile, so without this a mission stalls before the player has moved once. **#11 replaces the body rather than the shape.** ## The two-representation fix you asked for ADR 0008 flagged the snapshot and the components as two representations that could drift. The write-back now **destructures `Combatant` exhaustively**, so adding a field stops it compiling until somebody decides whether the world needs to hear about it — a silent bug turned into a build error. The ignored fields (`unit`, `team`, `movement`, `attack`) are ignored on purpose and say why. Only changed units are written back, because writing every one marks every `GridPos` changed and the movement preview rebuilds on exactly that signal — an attack across the map would have quietly redrawn an overlay that never moved. ## Acceptance criteria - [x] **`MissionPhase` does not exist outside `GameState::Mission`** — asserted in the style of `menu_screen_exists_only_inside_main_menu`, including that it is absent in the menu and torn down on leaving. - [x] **Transitions driven by the scheduler and by playback, not timers** — `begin_turn` pulls from the scheduler, `finish_animating` ends or continues the turn. No `Timer` anywhere. - [x] **Full ordering in a single readable block** — and the bug above is why that matters. ## Verification `cargo fmt --check`, `cargo clippy --all-targets -- -D warnings` (and `--features perf`), `cargo test` — **171 passing**, up from 163. Ran on a Pixel Tablet: a unit selects, a destination holds, confirming walks it — but **only for the unit whose turn it is, and nothing on screen says which that is.** I spent several attempts tapping the wrong unit before testing the path headlessly. The loop is sound; the game is hard to play by eye until the HUD lands. That makes **#10 blocking in practice**, not just desirable. Closes #8 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
Declares the mission loop rather than leaving it implied, and closes the last gap
between "an action can be resolved" and "a player can take one".

`MissionPhase` is a sub-state of `GameState::Mission` — `PlayerTurn`,
`EnemyTurn`, `Resolving`, `Animating` — following `MenuScreen`, so leaving a
mission tears it down for free and systems are gated on a phase rather than
asking "is it my turn?" in their own bodies.

`CombatSet` now declares the whole order in one block: `Reconcile`, then `Turn`,
then `Act`. That block is load-bearing in a way worth recording — adding
`CombatSet::Turn` to the enum without adding it to `configure_sets` gave it no
run condition at all, and combat systems duly ran during `Loading` and panicked
looking for a `Scheduler` that does not exist yet. The single block is the thing
that makes that visible.

`Turn` names the acting unit and is absent between turns, which is the signal
`begin_turn` waits for: it advances the scheduler exactly once per turn and
refills what that unit has to spend. Advancing merely to *look* at who is next
would spend a turn nobody took. Nothing is driven by a timer — a turn ends when
its allowance is gone, and `Animating` passes straight through until playback
(#9) gives it something to wait for.

The two-stage commit gets its third stage. Pressing a chosen destination again
raises `Confirmed` from the map, which says only that the player asked; whether
it is that unit's turn is combat's question and is answered there. That keeps
the dependency one way — combat already reads the map, and now the map still
knows nothing about turns.

Hostiles give up their turn immediately. A placeholder, and the loop does not
turn without it: the opening turn belongs to the fastest unit, which in the
shipped deployment is a hostile, so without this a mission stalls before the
player has moved once. #11 replaces the body rather than the shape.

ADR 0008 flagged the snapshot and the components as two representations that
could drift. The write-back now destructures `Combatant` exhaustively, so adding
a field stops it compiling until somebody decides whether the world needs to
hear about it — a silent bug turned into a build error. The ignored fields are
ignored on purpose and say why. Only changed units are written back, because
writing every one marks every `GridPos` changed and the movement preview rebuilds
on exactly that signal.

Verified on a Pixel Tablet: a unit selects, a destination holds, and confirming
walks it — but only for the unit whose turn it is, and nothing on screen says
which that is. The loop is sound and the game is hard to play by eye until the
HUD lands (#10).

Closes #8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TA4hJHkRSU3XBxZYtMKXdh
icub3d merged commit fd59f4b46c into main 2026-09-13 22:08:22 +00:00
icub3d deleted branch 8-mission-phases 2026-09-13 22:08:23 +00:00
Sign in to join this conversation.
No description provided.