Compare commits

...

24 Commits

Author SHA1 Message Date
funman300 da3e5423dc feat(replay): add full-screen tableau dim layer for mini-tableau preview
Spawn a `ReplayTableauDimLayer` UI node (100% × 100%, 50% opacity black)
at z=54 (Z_REPLAY_OVERLAY − 1) whenever a replay starts. The dim layer
darkens the entire card world so the replay chrome (banner at z=55,
move-log panel at z=55) reads clearly against the scene without
obscuring card positions — matching the mockup's "Game Peek Band at
50% opacity" spec. Bevy's UI/world compositor means no changes to
card_plugin are needed: UI nodes always render above world-space sprites
regardless of Transform.z values.

The dim layer carries no Interaction component (purely visual; pointer
events pass through). Despawned alongside the banner and move-log panel
in `react_to_state_change` when the replay ends.

Adds Z_REPLAY_DIM (= 54) and TABLEAU_DIM_ALPHA (= 0.5) constants plus
two new tests: lifecycle (spawn/despawn mirrors floating chip pattern)
and z-ordering invariant (Z_REPLAY_DIM < Z_REPLAY_OVERLAY pinned).

1275 tests pass / 0 failing. Closes the last major B-2 sub-piece.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 18:01:22 -07:00
funman300 a1864271de docs(handoff): refresh post-v0.21.6 — anchor to new tag, reset menu state
Fold the six post-v0.21.5 commit narratives into CHANGELOG §
[0.21.6] (now the source of truth for that release's scope).
Reset the Since-cut log to "no threads in flight." Update
status (HEAD f63db76, tags through v0.21.6, tests 1273
passing). Resume prompt now anchors at v0.21.6.

The post-cut menu's main item is now the mini-tableau preview
— the only major B-2 sub-piece left after Move Log panel
shipped. Architectural change (touches card_plugin rendering),
best tackled in a fresh session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:48:51 -07:00
funman300 f63db769ae docs: cut v0.21.6 — Move Log panel + scrub-UX polish
Patch release rolling up six post-v0.21.5 commits under the
through-line "Move Log panel + scrub-UX polish":

- d3cb1a5: HC-mode coverage for scrub track + notches
- 2e25476: continuous scrub on key-held ← / → at 100ms cadence
- d6f32d3: Move Log panel + active row (header + format helpers)
- 140251b: 2 prev rows above active
- e7345ae: active-row highlight with ACCENT_PRIMARY background
- 4437a1a: 2 next rows below active

The Move Log panel is the first replay-overlay surface that
isn't attached to the banner — it lives at a separate screen
anchor (bottom: 0) with its own spawn/despawn lifecycle.
Establishes the multi-anchor replay UI pattern that the
remaining B-2 sub-piece (mini-tableau preview) will inherit.

Panel grows 56 → 84 → 112 px across the four move-log commits.
HighContrastBackground primitive lifted to ui_theme parallel
to HighContrastBorder; settings_plugin gains
update_high_contrast_backgrounds for the BackgroundColor
repaint cycle. Continuous scrub uses a per-key accumulator
resource (ReplayScrubKeyHold) gated on SCRUB_REPEAT_INTERVAL_SECS
(0.1s).

Tests: 1250 → 1273 (+23 net new). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:46:24 -07:00
funman300 4437a1aaf9 feat(replay): add 2 next rows below active row in Move Log panel
Symmetric to the prev-rows commit. Adds 2 about-to-apply move
rows below the active row so the panel now shows a full 5-row
window: prev offset 2 → prev offset 1 → active → next offset 1
→ next offset 2. Panel grows from 84 → 112 px to fit the
additional rows.

Format helper `format_kth_next_row(state, k)` returns the kth
about-to-apply move's text:
- k=1 → moves[cursor], displayed as "{cursor + 1} │ {body}"
- k=2 → moves[cursor + 1], displayed as "{cursor + 2} │ ..."
- Returns empty when cursor + k - 1 >= moves.len() (under-fill
  late in the replay) or k=0 (degenerate).

Symmetric implementation:
- New `ReplayOverlayMoveLogNextRow { offset: u8 }` component
- Spawn loop iterates 1..=MOVE_LOG_NEXT_ROWS in order so offset
  1 sits directly below active, offset 2 below that
- Per-frame `update_move_log_next_rows` system mirrors the
  prev-rows updater
- TEXT_SECONDARY (matching prev rows) keeps the active row's
  highlight as the focal point

For post-game replays the next rows aren't spoilers (the game
is already won). If a future use case reuses the panel during
live play, the preview-shape would need rethinking.

4 new tests:
- format_kth_next_row: k=1, 2 in-range cases + k beyond
  moves.len() out-of-range + k=0 degenerate.
- move_log_next_rows_spawn_with_panel: cardinality matches
  MOVE_LOG_NEXT_ROWS.
- move_log_next_rows_paint_helper_strings_at_spawn: text
  matches helper output per offset.
- move_log_next_rows_underfill_at_replay_end: offset 1
  populates at cursor=9/10, offset 2 stays empty.

Tests: 1269 → 1273. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:44:59 -07:00
funman300 e7345aed6c feat(replay): highlight active row in Move Log panel
Wraps the active-row Text in a Node with
BackgroundColor(ACCENT_PRIMARY) so the row reads as "current
focus" against the panel's elevated background. Inner Text
colour bumps from TEXT_PRIMARY (#d0d0d0) to TEXT_PRIMARY_HC
(#f5f5f5) for legible contrast against the brick-red highlight.

format_active_move_row now prefixes the row with `▶` (the focus
marker) so the visual hierarchy is reinforced even before the
background paints (HC mode, future palette tweaks). The empty
case still returns empty — cursor=0 doesn't paint a stray "▶ "
prefix on an otherwise-empty row.

Mirrors the mockup at docs/ui-mockups/replay-overlay-mobile.html
§ "Move Log Card" where the active row has bg-suit-red-cb
(brick-red equivalent) + dark text + the ▶ marker.

3 new tests:
- active_row_wrapper_carries_accent_primary_background: walks
  from the active-row Text to its parent Node and asserts the
  wrapper carries BackgroundColor(ACCENT_PRIMARY).
- active_row_text_uses_high_contrast_color_for_highlight: pins
  the TextColor as TEXT_PRIMARY_HC.
- active_row_format_includes_focus_prefix: pure-helper guard for
  the ▶ prefix + the cursor=0-stays-empty contract.

Plus 2 existing tests updated for the new prefixed format
(format_active_move_row_handles_cursor_zero_and_positive,
move_log_active_row_repaints_on_cursor_advance).

Tests: 1266 → 1269 (+3 net new, +2 updated). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:41:14 -07:00
funman300 140251beae feat(replay): add 2 prev rows above active row in Move Log panel
Extends the Move Log panel's single active-row to a 3-row recent-
history window: 2 prev rows showing the moves applied just before
the active one, then the active row. Display order top-to-bottom:
header → prev offset 2 (oldest) → prev offset 1 → active.

Panel grows from 56 → 84 px to fit the additional rows. Active
row keeps TEXT_PRIMARY; prev rows render in TEXT_SECONDARY so
the active row stands out from context rows even without an
explicit highlight. (Active-row highlight is a follow-up commit.)

The format helper generalises:
- New `format_kth_recent_row(state, k)` returns the text for the
  kth-most-recently-applied move (k=1 is active, k=2 is row above,
  etc.). Returns empty when k > cursor (early-replay under-fill)
  or k = 0 (degenerate).
- `format_active_move_row` becomes a thin wrapper for k=1, kept
  at module scope so call sites stay readable.

New `ReplayOverlayMoveLogPrevRow { offset: u8 }` component carries
the row's offset (1 = just-before-active, 2 = before that). Spawn
loop iterates `MOVE_LOG_PREV_ROWS..=1` in reverse so the highest-
offset (oldest) row sits topmost in the panel's flex column.

Per-frame `update_move_log_prev_rows` system reads each row's
offset, computes k = offset + 1, and repaints via
format_kth_recent_row. Empty-when-out-of-range means panels gracefully
under-fill at cursor=1 (only active populated) and cursor=2
(active + offset 1, offset 2 empty).

4 new tests:
- format_kth_recent_row: k=1, 2, 3 in-range cases + k>cursor
  out-of-range + k=0 degenerate.
- move_log_prev_rows_spawn_with_panel: cardinality matches the
  MOVE_LOG_PREV_ROWS const.
- move_log_prev_rows_paint_helper_strings_at_spawn: text matches
  helper output per offset.
- move_log_prev_rows_repaint_on_cursor_advance: drives cursor=2
  → cursor=5 and asserts offset 1 / offset 2 texts follow.

Tests: 1262 → 1266. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:35:07 -07:00
funman300 d6f32d3154 feat(replay): add Move Log panel with active-row readout
First slice of the move-log mockup at
docs/ui-mockups/replay-overlay-mobile.html § "Move Log Card".
Adds a separate root UI entity anchored to the viewport's bottom
edge (sibling-of-banner pattern, mirrors ReplayFloatingProgressChip
lifecycle) carrying a `▌ MOVE LOG · N/M` header plus a single row
showing the most-recently-applied move.

Subsequent commits in this multi-session arc add prev/next rows,
active-row highlight, and auto-scroll on cursor advance. Splitting
the work at "panel + active row only" lands the structural piece
(panel exists, lifecycle works, format helpers proven) before
tackling the harder questions about rendering un-applied future
moves and scrolling.

Position decision: bottom-of-viewport (matches mockup), separate
root entity from the 92 px top banner. Keeps the banner from
growing further into a top-heavy 170+ px strip; the
top-status + bottom-info paradigm reads as vim/IDE-style buffer
chrome that players intuitively scan.

Four pure helpers handle the formatting:
- format_pile(p) → lowercase, 1-indexed display string
  ("foundation 3" rather than enum's 0-indexed Foundation(2))
- format_move_body(m) → "{from} → {to}" or "stock cycle"
- format_move_log_header(state) → "▌ MOVE LOG · N/M",
  "▌ MOVE LOG · COMPLETE" for `Completed`, empty for `Inactive`
- format_active_move_row(state) → "{cursor} │ {body}" with
  1-based cursor for player display, empty at cursor=0

Two per-frame update systems (update_move_log_header,
update_move_log_active_row) repaint the texts on resource change
with the standard early-exit-on-no-change idiom.

Despawn handling: react_to_state_change gains a third query for
ReplayOverlayMoveLogPanel entities and despawns them on
Playing → Inactive alongside the banner root and floating chip.

Panel border carries HighContrastBorder so the 1 px top edge
bumps under HC mode — same pattern as the keybind footer.

8 new tests:
- format_pile pile-name + 1-index pinning
- format_move_body both-variant pinning
- format_move_log_header three-state coverage
- format_active_move_row cursor=0 vs cursor>0
- move_log_panel spawn cardinality (exactly one)
- move_log_panel header paints helper string at spawn
- move_log_active_row repaints on cursor advance
- move_log_panel despawn parity with overlay tree

Tests: 1254 → 1262. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:29:37 -07:00
funman300 8fdc41f36f docs(handoff): record post-v0.21.5 polish; recommend notch-label centering
Two carve-outs land on top of v0.21.5:
- d3cb1a5: HC-mode coverage for scrub track + notches via new
  HighContrastBackground primitive in ui_theme + paint system
  in settings_plugin.
- 2e25476: continuous scrub on key-held ← / → at 100ms cadence;
  matches mockup's "[← →] scrub" terminology while keeping
  single-press = single-step semantics.

Update Since-cut log, status (1250 → 1254 tests passing,
flake cleared), and next-step menu. B-2 keyboard accelerator
coverage + accessibility + scrub UX are all complete; remaining
options are notch-label centering polish (smallest), the
move-log/mini-tableau multi-session arcs that close B-2, or
WIN MOVE marker HC bump (optional).

Recommended next-step: notch label centering (small).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:20:51 -07:00
funman300 2e25476d0a feat(replay): continuous scrub on key-held arrow keys
Holding ← or → now triggers continuous step at 100 ms cadence
(10 steps/sec) — matches the mockup's `[← →] scrub`
terminology while keeping single-press = single-step semantics.

Implementation: per-key accumulators in a new
`ReplayScrubKeyHold` resource. Each frame the key is held, the
corresponding accumulator absorbs `time.delta_secs()`; when it
exceeds `SCRUB_REPEAT_INTERVAL_SECS` (0.1s) the handler fires
another step and resets the accumulator. `just_pressed` events
bypass the accumulator entirely and fire immediately —
release resets to 0 so the next fresh press also fires
immediately rather than at half-interval.

Symmetric handling for ← (backwards step via undo) and →
(forward step). Both keys remain paused-only via the same
destructure-gate pattern in the underlying step helpers.

Footer text unchanged (`[← →] step`) — the only-wired-keybinds
discipline says "list what works"; held-key continuous scrub
is a discoverable enhancement to the same keybind, not a new
keybind.

`handle_arrow_keyboard` gains `Res<Time>` and
`ResMut<ReplayScrubKeyHold>` parameters. `Time` is provided by
MinimalPlugins's TimePlugin so headless tests already have it.

2 new tests (in addition to the 4 existing arrow scenarios):
- arrow_right_keyboard_repeats_while_held: drives time at
  exactly SCRUB_REPEAT_INTERVAL_SECS per tick and asserts that
  a second step fires after the just_pressed one.
- arrow_keyboard_release_resets_accumulator: verifies the
  release branch zeros the per-key accumulator.

Tests: 1252 → 1254. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:19:46 -07:00
funman300 d3cb1a51d4 feat(replay): HC-mode coverage for scrub track + notches
The 1 px scrub track and 5 quarter-mark notch ticks paint their
shape via BackgroundColor (not BorderColor — they're tiny
full-bleed Nodes, not borders on wider containers), so the
existing HighContrastBorder marker doesn't apply to them.

Add a parallel primitive in ui_theme: HighContrastBackground
marker carrying default_color, mirroring HighContrastBorder's
shape exactly. Add update_high_contrast_backgrounds system in
settings_plugin alongside update_high_contrast_borders — same
on/off rule (off → marker.default_color, on → BORDER_SUBTLE_HC),
same change-suppression idiom (only mutate when different so
Bevy's change-detection doesn't trigger per-frame repaints).

Tag the scrub track Node and all five notch Nodes with
HighContrastBackground::with_default(BORDER_SUBTLE) so the
existing settings repaint cycle picks them up under HC mode.

The scrub fill (ACCENT_PRIMARY brick-red) and WIN MOVE marker
(STATE_SUCCESS lime-green) don't get the marker — accent and
state colours are already saturated and don't need an HC
luminance variant.

2 new tests: spawn-time marker presence on the track and
cardinality-matches-notch-count on the ticks.

Tests: 1250 → 1252. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:14:03 -07:00
funman300 c8358f4275 docs(handoff): refresh post-v0.21.5 — anchor to new tag, reset menu state
Fold the six post-v0.21.4 commit narratives into CHANGELOG §
[0.21.5] (now the source of truth for that release's scope).
Reset the Since-cut log to "no threads in flight." Update
status (HEAD `a2432df`, tags through v0.21.5, tests still
1250/1249 passing pending the time-dependent flake clearing).
Resume prompt now anchors at v0.21.5 with the smaller post-cut
menu of next-finite-steps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:08:56 -07:00
funman300 a2432dfe7a docs: cut v0.21.5 — replay-overlay scrubbing affordances + accessibility
Patch release rolling up six post-v0.21.4 commits under the
through-line "replay-overlay scrubbing affordances + accessibility":

- fe68861: quarter-mark scrub-bar notches
- d322abf: percentage labels under notches (banner 60 → 76 px)
- 1873b3f: keybind-hint footer (banner 76 → 92 px)
- 90e24d9: ESC accelerator + cross-plugin pause-modal gate
- 23902cd: HC-mode coverage for footer top border
- e5c4f51: ← / → keyboard accelerators for paused stepping

v0.21.4 shipped pause / resume / step + the WIN MOVE marker as
the first scrubbing-shaped additions; v0.21.5 fills out the rest
of the scrubbing UX so the player has both visual anchor points
(notches + labels) and a complete keyboard control surface
(Space / Esc / ← / →) for navigating a paused replay.

Two of the six commits are layout-changing — they grow the
banner from 60 → 76 → 92 px to make room for the notch labels
and keybind footer. Banner geometry was fixed for every prior
B-2 commit; this release establishes the "grow the container,
add a flex-column child" pattern that the remaining B-2
sub-pieces (move-log scroller, mini-tableau preview) will
inherit when they land.

Tests: 1228 → 1250 (+22 net new), 1249 passing, 1 pre-existing
time-dependent flake (daily_challenge warning, fails when UTC
clock is within 30 min of midnight; verified not introduced by
this release).

Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 17:05:03 -07:00
funman300 511550232c docs(handoff): record HC marker + ← / → wiring; recommend v0.21.5 cut
Two more post-v0.21.4 carve-outs land:
- 23902cd: HC-mode coverage for keybind-footer top border
  (HighContrastBorder marker so apply_high_contrast_borders
  bumps the 1 px top border under HC).
- e5c4f51: ← / → keyboard accelerators for paused stepping
  (hooks game's undo system for backwards step; footer
  extended to [SPACE] pause/resume · [ESC] stop · [← →] step).

Update Since-cut log, visual-identity bullet, B option in the
Resume menu, status (1244 → 1250 total tests / 1249 passing /
1 pre-existing flake), and HEAD hint.

Six post-v0.21.4 commits now form a coherent through-line:
replay-overlay scrubbing affordances + accessibility. Resume
menu's B option now recommends cutting v0.21.5 as the natural
next boundary.

Pre-existing flake noted: daily_challenge warning test fails
when wall-clock UTC is within 30 minutes of midnight (the
warning window the test asserts against). Verified not
introduced by recent commits via stash-and-retest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:59:08 -07:00
funman300 e5c4f51a6e feat(replay): wire ← / → keyboard accelerators for paused stepping
→ during a paused replay advances by one move (mirrors the Stop
button's existing forward-step semantics). ← decrements the
cursor and dispatches `UndoRequestEvent`, which the game's
`handle_undo` reads next frame to reverse its most-recent move
— hooking the existing undo system rather than replaying
forward from cursor 0 (every replay-applied move pushes to the
undo stack the same way a player move would, so undo is the
right reversal primitive).

Both accelerators are paused-only — backwards via a new
`step_backwards_replay_playback` in `replay_playback.rs` that
hard-gates with the same destructure pattern as
`step_replay_playback`. Pressing → during running playback or ←
at cursor 0 are silent no-ops; the player learns "pause first,
then arrow."

The mockup labels these `[← →] scrub` (continuous fast scan).
Single-move step is the closest behaviour shippable today —
continuous scrub would need either a key-held event source or
an internal speed-up loop. Footer hint reads
`[← →] step` to match what's wired rather than the aspirational
"scrub."

Footer hint extended in lockstep:
`[SPACE] pause/resume · [ESC] stop · [← →] step` — the
only-wired-keybinds discipline holds.

ReplayOverlayPlugin gains `add_message::<UndoRequestEvent>()`
defensively so the plugin can run under MinimalPlugins without
GamePlugin attached (idempotent registration; harmless when
GamePlugin is also present).

6 new tests (2 hint pins + 4 keyboard scenarios) + 1 helper-pin
update for the new hint string.

Pre-existing flake noted: `daily_challenge_plugin::tests::
check_system_fires_warning_event_only_once_per_day` is failing
because wall-clock UTC is currently within 30 minutes of
midnight, inside the daily-expiry warning window the test
asserts against. Verified pre-existing by stashing all changes
and re-running — failure persists. Same shape as the
`winnable_seed_search` flake the handoff documented earlier
this session: time-dependent, deterministically passes under
different clock conditions. Not introduced by this commit.

Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:50:59 -07:00
funman300 23902cdc44 feat(replay): HC-mode coverage for keybind-footer top border
Tag the footer's border-carrying Node with
`HighContrastBorder::with_default(BORDER_SUBTLE)` so the existing
`apply_high_contrast_borders` system bumps the 1 px top border
from `BORDER_SUBTLE` (#505050) to `BORDER_SUBTLE_HC` (#a0a0a0)
when `Settings::high_contrast_mode` is on.

Without this the footer reads as floating loose under HC because
the border that visually anchors it to the labels row above is
near-invisible at #505050 against the elevated banner background.

The footer's text colours (`TEXT_SECONDARY` on both the
mode-line and the hint) don't need an HC bump — `TEXT_SECONDARY`
is already at `#a0a0a0`, the same luminance as `BORDER_SUBTLE_HC`.
There's no `TEXT_SECONDARY_HC` constant in the palette because
secondary text is already at HC-border level by design.

The notch labels also use `TEXT_SECONDARY` and inherit the same
"already HC-bright" property — no marker needed there either.

The 1 px scrub track, notch ticks, and WIN MOVE marker render
via `BackgroundColor` (not `BorderColor`) so the
`HighContrastBorder` marker doesn't apply. HC coverage for those
decorative pieces would need a custom settings-aware paint
system (precedent: `radial_rim_outline` in `radial_menu`) and is
deferred to a follow-up commit.

1 new test pinning the marker on spawn. 1243 → 1244. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:41:49 -07:00
funman300 3cc8eacafa docs(handoff): record ESC accelerator; B's next step is HC polish
Post-v0.21.4 fourth carve-out: 90e24d9 wires ESC for replay-stop
with a cross-plugin gate in pause_plugin to defer when replay is
playing. Footer extended in lockstep to
[SPACE] pause/resume · [ESC] stop. Update Since-cut log,
visual-identity bullet, B option in the Resume menu, status
(1240 → 1243 tests), and HEAD hint.

B option's next-step menu now has three branches: HC polish
(smallest), ← / → wiring (medium, needs backwards-step path),
and the multi-session move-log/preview arcs that close B-2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:07:33 -07:00
funman300 90e24d9711 feat(replay): wire ESC accelerator for stop, gate pause modal
ESC during an active replay now stops it (mirrors the existing
Stop button click). UI-first contract from CLAUDE.md §3.3 holds
for the keyboard accelerator: every keybind the footer surfaces
points at a wired action.

Cross-plugin coordination: pause_plugin's `toggle_pause` already
listens for ESC and would otherwise open the pause modal on the
same press. Resolved by adding a fourth defer-if check to the
existing modal-stack pattern in `toggle_pause` —
`replay_state.is_some_and(|s| s.is_playing())` slots in right
after `other_modal_scrims` and before `selection`. Symmetric
shape to the existing forfeit / modal-scrim / selection /
game-over / drag gates.

Footer hint extended from `[SPACE] pause/resume` to
`[SPACE] pause/resume · [ESC] stop` in lockstep — the
"only-wired-keybinds" discipline holds.

3 new tests:
- esc_keyboard_stops_active_replay (positive: Esc → Inactive,
  overlay despawns next frame)
- esc_keyboard_is_noop_when_not_playing (negative: doesn't fire
  on Inactive state, lets global Esc listeners own those frames)
- keybind_footer_hint_lists_space_and_esc (footer text contains
  both keybinds)

Plus updated helper-pin test for the new hint string. Existing
pause_plugin tests unaffected (they don't insert a
ReplayPlaybackState resource so the new gate is a no-op for
them).

Tests: 1240 → 1243 (+3). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 16:06:02 -07:00
funman300 decbe0bbd9 docs(handoff): record keybind footer; B's next step is ESC accelerator
Post-v0.21.4 third carve-out: 1873b3f ships a keybind-hint footer
(vim-style mode line + `[SPACE] pause/resume`) at the bottom of
the banner (76 → 92 px). Update Since-cut log, visual-identity
bullet, B option in the Resume menu, status (1236 → 1240 tests),
and HEAD hint.

Footer lists only wired keybinds. Next finite step on B-2: wire
ESC for stop and extend the footer to `[SPACE] pause/resume ·
[ESC] stop` — small, single-axis, surfaces another keyboard
accelerator alongside the existing Stop button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:59:57 -07:00
funman300 1873b3f9be feat(replay): add keybind-hint footer to overlay banner
Vim-style mode line on the left (`▌ NORMAL │ replay`) plus a
keybind-hint on the right (`[SPACE] pause/resume`) gives the
existing Space accelerator a visible UI counterpart, satisfying
the UI-first contract from CLAUDE.md §3.3 for the keyboard
accelerator that v0.21.4 shipped.

The footer lists only keybinds that are *actually wired today*.
Future commits that wire ESC for stop or ← / → for prev/next
move will extend the right-hand text in lockstep — the footer
never lists aspirational keybinds (would lie to users).

Banner height grew from 76 → 92 px to make room for the 16 px
footer row. Second layout-changing commit in B-2's screen-
takeover arc; same "grow container, add flex-column child"
pattern as the notch-labels commit. 1px top border in
BORDER_SUBTLE separates the footer from the notch-label row.

Two pure helpers (`keybind_footer_mode_text`,
`keybind_footer_hint_text`) keep the static text testable
without per-text marker components on the inner Text entities.
The shared `font_handle_for_labels` clone covers both label and
footer text spawns since the labels closure only `.clone()`s
the handle (never moves it).

4 new tests: pure-helper guards, footer-spawn cardinality
(exactly one), text-set assertion (both helper strings appear as
descendants), lifecycle parity with the overlay tree.

Tests: 1236 → 1240 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:58:28 -07:00
funman300 d11d97e677 docs(handoff): record notch labels; B's next step is keybind footer
Post-v0.21.4 second carve-out: d322abf ships percentage labels
under each scrub-bar notch (banner 60 → 76 px — first real layout
change in B-2's arc). Update Since-cut log, visual-identity
bullet, B option in the Resume menu, status (1232 → 1236 tests),
and HEAD hint.

Banner geometry is now mutable; future B-2 sub-pieces follow the
same "grow container, add flex-column child" pattern. Next
finite step: keybind-hint footer (small) before the bigger
move-log / mini-tableau pieces.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:52:28 -07:00
funman300 d322abf67b feat(replay): add percentage labels under scrub-bar notches
Five `0%` / `25%` / `50%` / `75%` / `100%` labels in a new 16 px
row beneath the 1 px scrub track give the player explicit
quarter-mark readouts to pair with the notch ticks.

Pure helper `scrub_notch_labels()` returns the fixed array,
paired index-for-index with `scrub_notch_positions()`. Spawn loop
zips both helpers and applies an "endpoints flush, middle three
percent-anchored" positioning pattern: leftmost label gets
`left: 0` (no clip on `0%`), rightmost gets `right: 0` (no overflow
on `100%`), middle three anchor at `left: Val::Percent(p)` since
Bevy 0.18 UI lacks a clean CSS-style `translate-x: -50%` centering
primitive. The slight right-of-notch offset on the middle three
is visually subtle at TYPE_CAPTION; explicit polish target if
anyone notices.

Banner height grew from 60 → 76 px to make room for the label row
(76 = top row 59 flex-grow + scrub track 1 + label row 16). First
real layout change in B-2's screen-takeover arc — every prior
B-2 commit was additive at fixed banner geometry.

Label color is TEXT_SECONDARY rather than mockup's `text-outline`
(BORDER_SUBTLE) — the latter would match the notches but is too
low-contrast against BG_ELEVATED_HI to read at 12 px. TEXT_SECONDARY
keeps the subdued caption hierarchy while staying legible.

4 new tests: pure-helper guard pinning the array + helper-positions
pairing invariant, spawn cardinality, set equality between spawned
texts and helper output, lifecycle parity with the overlay tree.

Tests: 1232 → 1236 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:51:09 -07:00
funman300 c9e4c0b4cd docs(handoff): record scrub-bar notches; B's next step is notch labels
Post-v0.21.4 carve-out: fe68861 ships quarter-mark notches on the
scrub bar. Update Since-cut log, visual-identity bullet, B option
in the Resume menu, status (1228 → 1232 tests), and HEAD hint.

Next finite step on B-2: percentage labels under each notch —
forces banner height to grow from 60 px to ~76 px, making it the
first real layout change in the screen-takeover arc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:44:05 -07:00
funman300 fe68861e10 feat(replay): add quarter-mark notches to scrub bar
Five 1px vertical ticks at 0/25/50/75/100% give the player visual
anchor points for "where am I, relative to the quarter-marks of the
replay" without needing to mentally bisect the bar.

Pure helper `scrub_notch_positions()` returns the fixed array; the
spawn loop sits next to the WIN MOVE marker spawn so the two
overlays share their lifecycle with the rest of the overlay tree.
Notches paint in BORDER_SUBTLE (same as the unfilled track) and
extend vertically past the 1px track (5px tall, anchored 2px above
the track top) — same visibility trick the WIN MOVE marker uses.

Spawned after the WIN MOVE marker so a notch and the marker landing
on the same percentage paint the marker on top.

Mirrors the notch ladder in the screen-takeover mockup at
docs/ui-mockups/replay-overlay-mobile.html. First finite step toward
B-2's screen-takeover layout reflow; labels under each notch land in
a follow-up commit when the banner height grows to accommodate them.

4 new tests: pure-helper guard pinning the [0,25,50,75,100] array,
spawn-cardinality matching helper.len(), lifecycle parity with the
overlay tree, independence from win_move_index.

Tests: 1228 → 1232 (+4). Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:42:37 -07:00
funman300 c33b39cf11 docs(handoff): refresh post-v0.21.4 — anchor to new tag, reset menu state
Anchors handoff to v0.21.4 at `23ff62c`, resets the "Since the cut"
section to placeholder, updates the READ FIRST CHANGELOG pointer,
bumps the Resume-prompt summary to reflect replay-scrubbing
accessibility as the v0.21.4 through-line, and identifies the
screen-takeover layout reflow as the remaining multi-session arc
on B (with move-log scroller + mini-tableau preview as small
sub-pieces inside it).

Resume menu stays at A/B/C — A and C unchanged; B's prerequisite
sub-pieces shipped in v0.21.4 so the entry now points cleanly at
the layout reflow as the single remaining multi-session piece.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 15:28:50 -07:00
7 changed files with 2824 additions and 108 deletions
+261 -1
View File
@@ -6,9 +6,269 @@ project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
No threads in flight. v0.21.4 cut on 2026-05-08; CHANGELOG accumulates
No threads in flight. v0.21.6 cut on 2026-05-08; CHANGELOG accumulates
the next cycle here.
## [0.21.6] — 2026-05-08
Patch release for the post-v0.21.5 work. Through-line:
**Move Log panel + scrub-UX polish**. v0.21.5 closed out the
keyboard-accelerator surface (Space / Esc / ← / →) and the
keybind footer; v0.21.6 builds on that with two parallel
threads — accessibility + scrub-on-hold polish for the v0.21.5
surfaces, plus a brand-new Move Log panel anchored to the
viewport's bottom edge that gives players a 5-row recent-and-
upcoming move history alongside the existing top-edge banner.
The Move Log panel is the first replay-overlay surface that
*isn't* attached to the banner — it lives at a separate screen
anchor (bottom: 0) with its own spawn/despawn lifecycle.
Establishes the pattern for "multi-anchor replay UI" that the
remaining B-2 sub-piece (mini-tableau preview) will inherit.
### Added
- **HC-mode coverage for the scrub track + quarter-mark notch
ticks** (`d3cb1a5`). Adds parallel primitive
`HighContrastBackground` to `ui_theme` and a paint system
`update_high_contrast_backgrounds` in `settings_plugin` that
mirrors the existing border-marker pattern but targets
`BackgroundColor` instead of `BorderColor`. Tags the 1 px
scrub track Node and all five quarter-mark notch ticks so
they bump from `BORDER_SUBTLE` (`#505050`) →
`BORDER_SUBTLE_HC` (`#a0a0a0`) under HC mode. Scrub fill
(`ACCENT_PRIMARY`) and WIN MOVE marker (`STATE_SUCCESS`)
don't get the marker — accent and state colours are already
saturated and don't need an HC luminance variant.
- **Continuous scrub on key-held arrow keys** (`2e25476`).
Holding ← or → triggers continuous step at 100 ms cadence
(10 steps/sec) — matches the mockup's `[← →] scrub`
terminology while keeping single-press = single-step
semantics. Per-key accumulators in a new
`ReplayScrubKeyHold` resource; `just_pressed` events bypass
the accumulator and fire immediately. Release resets to 0
so the next fresh press fires immediately rather than at
half-interval.
- **Move Log panel** (`d6f32d3` + `140251b` + `e7345ae` +
`4437a1a`). New bottom-edge UI panel showing a 5-row window
onto recent + upcoming moves: 2 prev rows above the active
row + active row highlighted in `ACCENT_PRIMARY` + 2 next
rows below. Header reads `▌ MOVE LOG · N/M` (or
`▌ MOVE LOG · COMPLETE` when finished). Active row carries
a `▶` focus prefix and `TEXT_PRIMARY_HC` text colour for
legible contrast against the brick-red highlight. Prev /
next rows render in `TEXT_SECONDARY` so the active row
stays the focal point.
- Sibling-of-banner pattern (separate root entity anchored
at viewport bottom, not a banner child) — same
spawn/despawn lifecycle as `ReplayFloatingProgressChip`,
different screen anchor.
- Five pure helpers handle the formatting:
`format_pile`, `format_move_body`,
`format_move_log_header`, `format_kth_recent_row` (active
+ prev), `format_kth_next_row` (next). 1-indexed display
numbers throughout (`Foundation(2)` reads as "foundation
3" rather than the enum's 0-index).
- Panel grows from 56 → 84 → 112 px across the four
move-log commits. `MOVE_LOG_PREV_ROWS` and
`MOVE_LOG_NEXT_ROWS` constants (both = 2) parameterise
the row count; `format_kth_recent_row` and
`format_kth_next_row` return empty for out-of-range k so
panels gracefully under-fill at the start (cursor=1) and
end (cursor=N-1) of a replay.
- HC marker on the panel's top border so the 1 px edge
bumps under HC mode (same pattern as the keybind footer).
### Changed
- **`react_to_state_change` despawns the Move Log panel** on
`Playing → Inactive` alongside the banner root and floating
progress chip. Third query in the same defer-and-despawn
cycle.
- **Move Log panel height grew 56 → 84 → 112 px** across the
prev-rows and next-rows commits. The panel is sized to fit
the chosen row count + header + padding; tunable via the
`MOVE_LOG_PANEL_HEIGHT` const.
- **`format_active_move_row` now prefixes the `▶` focus
marker** (`e7345ae`). Wraps `format_kth_recent_row(state, 1)`
and prepends the prefix when the body is non-empty. Empty
case still returns empty — cursor=0 doesn't paint a stray
`▶` on an otherwise-empty row.
### Documentation
- `SESSION_HANDOFF.md` refreshed twice this cycle — once
recording the HC paint + continuous-scrub polish, then
again as the Move Log arc shipped commit-by-commit. The
Resume menu's B option now traces the full arc:
notches → labels → footer → ESC → HC → arrow keys →
HC paint → continuous scrub → move log.
### Stats
- **1273 passing tests / 0 failing** across the workspace
(net +23 from v0.21.5's 1250 baseline):
- 2 from `d3cb1a5` (HC marker on track + notches).
- 2 from `2e25476` (continuous-scrub repeat-while-held +
release-resets-accumulator).
- 8 from `d6f32d3` (move-log panel init + 5 helpers + 3
spawn / lifecycle scenarios).
- 4 from `140251b` (prev rows: helper k coverage + spawn
cardinality + spawn texts + repaint on cursor advance).
- 3 from `e7345ae` (active row highlight: wrapper bg +
text colour + focus prefix + cursor=0 stays empty).
- 4 from `4437a1a` (next rows: helper k coverage + spawn
cardinality + spawn texts + under-fill at replay end).
- Clippy clean across the workspace.
## [0.21.5] — 2026-05-08
Patch release for the post-v0.21.4 work. One through-line:
**replay-overlay scrubbing affordances + accessibility**. v0.21.4
shipped pause / resume / step + the WIN MOVE marker as the first
*scrubbing-shaped* additions to the replay overlay; v0.21.5
fills out the rest of the scrubbing UX so the player has both
visual anchor points (notches + labels) and a complete keyboard
control surface (Space / Esc / ← / →) for navigating a paused
replay.
Two of the six commits in this cycle are layout-changing — they
grow the banner height from 60 px → 76 px → 92 px to make room
for the notch labels and keybind footer. Banner geometry was
fixed for every prior B-2 commit; this release establishes the
"grow the container, add a flex-column child" pattern that the
remaining B-2 sub-pieces (move-log scroller, mini-tableau
preview) will inherit when they land.
### Added
- **Quarter-mark scrub-bar notches** (`fe68861`). Five 1 px
vertical ticks at 0 / 25 / 50 / 75 / 100 % give the player
visual anchor points without needing to mentally bisect the
bar. Pure helper `scrub_notch_positions()` returns the fixed
array; spawn loop sits next to the WIN MOVE marker spawn so
the lifecycles match. Notches paint in `BORDER_SUBTLE` (same
as the unfilled track) and rely on extending past the 1 px
track (5 px tall, anchored 2 px above the track top) for
visibility — same trick the WIN MOVE marker uses. Spawned
*after* the WIN MOVE marker so a notch and the marker
landing on the same percentage paint the marker on top.
- **Percentage labels under each notch** (`d322abf`). Five
`0%` / `25%` / `50%` / `75%` / `100%` labels in a new 16 px
row beneath the 1 px scrub track give the player explicit
quarter-mark readouts. Banner grew from 60 → 76 px to
accommodate the row — first **layout-changing** commit in
the B-2 arc. Pure helper `scrub_notch_labels()` returns the
fixed array, paired index-for-index with
`scrub_notch_positions()`. Spawn loop applies an "endpoints
flush, middle three percent-anchored" positioning pattern:
leftmost label gets `left: 0`, rightmost gets `right: 0`,
middle three anchor at `left: Val::Percent(p)` since Bevy
0.18 UI lacks a clean CSS-style `translate-x: -50%`
centering primitive. Label colour is `TEXT_SECONDARY`
rather than the mockup's `BORDER_SUBTLE` (the latter would
match the notches but is too low-contrast against
`BG_ELEVATED_HI` to read at 12 px).
- **Keybind-hint footer** (`1873b3f`). Vim-style mode line on
the left (`▌ NORMAL │ replay`) plus a keybind hint on the
right at the bottom edge of the banner. Banner grew from
76 → 92 px to fit the 16 px footer row. Surfaces every
wired keyboard accelerator visually so CLAUDE.md §3.3's
UI-first contract holds for keyboard accelerators too. The
footer lists *only* keybinds that are actually wired —
the only-wired-keybinds discipline means each release
cycle's hint string is a precise honest contract with the
player. Two pure helpers (`keybind_footer_mode_text`,
`keybind_footer_hint_text`) keep the static text testable.
1 px top border in `BORDER_SUBTLE` separates the footer
from the labels row.
- **ESC keyboard accelerator for replay-stop** (`90e24d9`).
New `handle_stop_keyboard` system parallels
`handle_pause_keyboard` in shape — fires only when state
is `Playing`, calls `stop_replay_playback`. Cross-plugin
coordination via `pause_plugin::toggle_pause`: added a
fourth defer-if check
(`replay_state.is_some_and(|s| s.is_playing())`) right
after the existing `other_modal_scrims` check so ESC
during active replay belongs to the replay overlay, not
the pause modal.
- **HC-mode coverage for the keybind-footer top border**
(`23902cd`).
`HighContrastBorder::with_default(BORDER_SUBTLE)` marker
on the footer's border-carrying Node so the existing
`apply_high_contrast_borders` system bumps the 1 px top
border from `#505050``#a0a0a0` when
`Settings::high_contrast_mode` is on. Without the marker
the footer reads as floating loose under HC because the
border that anchors it to the labels row is
near-invisible.
- **← / → keyboard accelerators for paused stepping**
(`e5c4f51`). New `step_backwards_replay_playback` in
`replay_playback.rs` decrements the cursor and dispatches
`UndoRequestEvent`; the game's `handle_undo` reads it
next frame to reverse its most-recent move. Hooks the
existing undo system rather than replaying-forward-from-
zero — every replay-applied move pushes to the undo stack
the same way a player move would, so undo is the right
reversal primitive. Both arrow keys are paused-only via
the same destructure-gate pattern the forward step uses.
The mockup labels these `[← →] scrub`; single-move step
is the closest behaviour shippable today, so the footer
hint reads `[← →] step` — only-wired-keybinds discipline.
### Changed
- **Banner height grew 60 → 76 → 92 px** across two
layout-changing commits (`d322abf` then `1873b3f`). Top
row's `flex_grow: 1.0` still consumes 59 px so the
existing content (label / progress chip / buttons) has
the same vertical space; the new rows (16 px labels +
16 px footer) extend the banner downward into the
gameplay area. Banner geometry is now mutable — every
prior B-2 commit fit inside fixed 60 px space.
- **Keybind-footer hint text grew alongside the wirings**:
`[SPACE] pause/resume`
`[SPACE] pause/resume · [ESC] stop`
`[SPACE] pause/resume · [ESC] stop · [← →] step`.
- **`pause_plugin::toggle_pause` now defers when a replay
is active** (`90e24d9`). Adds a fourth defer-if check to
the existing modal-stack pattern.
- **`ReplayOverlayPlugin` registers
`add_message::<UndoRequestEvent>()`** (`e5c4f51`).
Defensive registration so the plugin runs cleanly under
`MinimalPlugins` without `GamePlugin` attached.
### Documentation
- `SESSION_HANDOFF.md` refreshed five times this cycle.
The B option in the Resume menu now traces the full arc:
notches → labels → footer → ESC → HC → arrow keys.
- The pre-existing `daily_challenge` warning test that
fails when wall-clock UTC is within 30 minutes of
midnight is documented in this cycle's handoff. Same
shape as the earlier `winnable_seed_search` flake —
time-dependent, deterministically passes outside the
trigger window.
### Stats
- **1250 total tests / 1249 passing / 1 pre-existing
time-dependent flake** across the workspace (net +22 from
v0.21.4's 1228 baseline):
- 4 from `fe68861` (scrub-notch coverage)
- 4 from `d322abf` (notch-label coverage)
- 4 from `1873b3f` (keybind-footer coverage)
- 3 from `90e24d9` (ESC-accelerator coverage)
- 1 from `23902cd` (HC-marker coverage)
- 6 from `e5c4f51` (arrow-keyboard coverage)
- **Pre-existing flake**:
`daily_challenge_plugin::tests::check_system_fires_warning_event_only_once_per_day`
fails when wall-clock UTC is within 30 minutes of
midnight. Verified pre-existing by stash-and-retest
before each commit. Will pass deterministically outside
the trigger window. Not introduced by this release.
- Clippy clean across the workspace.
## [0.21.4] — 2026-05-08
Patch release for the post-v0.21.3 work. One through-line:
+109 -97
View File
@@ -1,91 +1,85 @@
# Solitaire Quest — Session Handoff
**Last updated:** 2026-05-08 — **v0.21.3 cut and tagged at
`3d92a91`**, working tree clean, all post-tag work pushed to
**Last updated:** 2026-05-08 — **v0.21.6 cut and tagged at
`f63db76`**, working tree clean, all post-tag work pushed to
origin.
v0.21.3 is a patch release with one through-line: **accessibility
arc closure**. v0.21.2 explicitly carved out "dynamic-paint sites"
(HUD action buttons, modal buttons, radial menu rim) on the
assumption that their existing paint cycles would race the
central `update_high_contrast_borders` system. v0.21.3 walks the
actual code, finds the carve-out was over-cautious, and closes
it. Bonus: the first real consumer of `ToastVariant::Warning`
also lands here, making the `ToastVariant` enum fully load-bearing
(every variant has at least one driver).
v0.21.6 is a patch release with through-line:
**Move Log panel + scrub-UX polish**. v0.21.5 closed out the
keyboard-accelerator surface (Space / Esc / ← / →) and the
keybind footer; v0.21.6 builds on that with two parallel
threads — accessibility + scrub-on-hold polish for the v0.21.5
surfaces, plus a brand-new Move Log panel anchored to the
viewport's bottom edge that gives players a 5-row recent-and-
upcoming move history alongside the existing top-edge banner.
Full v0.21.3 detail lives in `CHANGELOG.md` § [0.21.3]. This
The Move Log panel is the first replay-overlay surface that
*isn't* attached to the banner — it lives at a separate screen
anchor (`bottom: 0`) with its own spawn/despawn lifecycle.
Establishes the "multi-anchor replay UI" pattern that the
remaining B-2 sub-piece (mini-tableau preview) will inherit.
Six commits on the B-2 replay screen-takeover redesign arc land
here, bringing the post-v0.21.4 total to 12. The remaining B-2
piece — mini-tableau preview that dims the gameplay tableau
during replay — is the only major sub-piece still open.
Full v0.21.6 detail lives in `CHANGELOG.md` § [0.21.6]. This
file from here on focuses on what's *open* post-cut and how to
resume.
## Status at pause
- **HEAD locally:** see `git rev-parse HEAD`. The cut commit is
`3d92a91`; post-cut work on B-2 (`ab857bb` data field +
`52befa6` WIN MOVE marker UI + `fbe48ac` playback controls)
rides on top of that.
- **HEAD on origin:** matches local. v0.21.3 is fully on origin.
`f63db76`; any post-cut docs edits ride on top of that.
- **HEAD on origin:** matches local. v0.21.6 is fully on origin.
- **Working tree:** clean. No WIP outstanding.
- **`artwork/` directory:** still untracked. Intentional.
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
clean.
- **Tests:** **1228 passing / 0 failing** across the workspace
(1207 from v0.21.3's stats + 5 from `ab857bb`'s
`win_move_index` coverage + 8 from `52befa6`'s WIN MOVE marker
pure-helper truth-table + spawn lifecycle + 8 from `fbe48ac`'s
pause / step / keyboard accelerator coverage).
- **Tags on origin:** `v0.9.0` through `v0.21.3`. v0.21.3 is on
`3d92a91`; v0.21.2 stays on `f23df3b`; v0.21.1 stays on
`daa655a`; v0.21.0 stays on `04f9bf9`; v0.20.0 stays on
`41a009a`.
- **Tests:** **1273 passing / 0 failing** across the workspace.
Detail in `CHANGELOG.md` § [0.21.6] § Stats.
- **Tags on origin:** `v0.9.0` through `v0.21.6`. v0.21.6 is on
`f63db76`; v0.21.5 stays on `a2432df`; v0.21.4 stays on
`23ff62c`; v0.21.3 stays on `3d92a91`; v0.21.2 stays on
`f23df3b`; v0.21.1 stays on `daa655a`; v0.21.0 stays on
`04f9bf9`; v0.20.0 stays on `41a009a`.
## Since the v0.21.3 cut
## Since the v0.21.6 cut
- **`ab857bb``Replay::win_move_index` data field landed.**
First finite step toward the B-2 replay screen-takeover
redesign. Additive optional `Option<usize>` on `Replay` with
`#[serde(default)]` so older `latest_replay.json` /
`replays.json` files load unchanged (no schema bump). Populated
at the live recording site via a new `with_win_move_index`
builder; for fresh recordings the value is always
`Some(moves.len() - 1)` because recording freezes on win, but
storing it explicitly lets the playback UI read the WIN MOVE
position directly without re-deriving on every render. 5 new
tests (1207 → 1212): default, builder set / set-None, on-disk
round-trip, legacy-JSON-loads-with-None backward-compat.
- **`52befa6` — WIN MOVE marker on the scrub bar.** Second
commit on B-2 — the UI that consumes the data field. New
`ReplayOverlayWinMoveMarker` component spawned as a sibling
to `ReplayOverlayScrubFill` under the 1px scrub track,
absolute-positioned at `replay.win_move_index / total` along
the bar. Painted in `STATE_SUCCESS` (green) so the marker
reads as "this is where the win lives." Pure helper
`win_move_marker_pct` returns `None` for any state where the
marker shouldn't draw (Inactive, Completed, replay missing
the field, empty move list); percentage clamps to `[0, 100]`
defensively. Lifecycle is spawn-time only — the marker is
immutable during a single playback because the underlying
`Replay` doesn't change while `Playing`. Despawned with the
overlay tree on transition back to `Inactive`. 8 new tests
(1212 → 1220): pure-helper truth table + spawn-presence /
spawn-absence / despawn-lifecycle observables.
- **`fbe48ac` — playback controls (pause / resume / step).**
Third commit on B-2. New `paused: bool` field on
`ReplayPlaybackState::Playing`; `tick_replay_playback` skips
the `secs_to_next` decrement entirely while paused so cursor
and timer freeze together. New public API:
`toggle_pause_replay_playback` and `step_replay_playback`
(the latter hard-gated to `Playing { paused: true }` so
manual stepping can't race the tick loop). UI: Pause /
Resume button (label repaints reactively via
`update_pause_button_label` which walks `Children` from
marker to inner `Text`) + Step button + Space keyboard
accelerator. Existing 25 `Playing { ... }` construction
sites across tests gained `paused: false` mechanically.
8 new tests (1220 → 1228): label truth table, label repaint
on state change, click-toggles-paused, step advances exactly
one cursor with paused preserved, step-while-running no-op,
Space toggles paused.
No threads in flight. Working tree clean as of 2026-05-08. New
work since the cut would land here as commit narratives; for
the v0.21.6 contents themselves, see `CHANGELOG.md` § [0.21.6].
Open next-step menu (Move Log + scrub-UX + keyboard accelerator
coverage + accessibility are all complete):
1. **Mini-tableau preview** — the only remaining major B-2
sub-piece. Mockup shows a 240 px-tall band at 50 % opacity
showing the gameplay tableau peeking through the replay
chrome. Implementation needs to add a settings-aware dim
overlay or alpha modulation on the tableau cards during
replay. Architectural — touches `card_plugin` rendering.
Multi-session.
2. **Move Log auto-scroll** — only relevant if the panel's
row count grows beyond the current 5-row fixed window.
Currently the prev-2 / active / next-2 layout fits all
visible content, so auto-scroll is unneeded. Becomes
relevant if a future commit expands the panel's row
capacity (e.g. 10-row scrolling list).
3. **Polish: notch label centering.** Bevy 0.18 lacks a
clean `translate-x: -50%` primitive so middle three
labels sit slightly right-of-notch. Could use a child
Text wrapper with computed left-margin compensation.
Tiny commit, requires visual review.
4. **Polish: WIN MOVE marker HC bump.** Currently uses
`STATE_SUCCESS` lime which stays visible under HC, but a
contrast bump under HC would make it even more legible
alongside the bumped notches. Optional.
Recommended order: option 1 (mini-tableau preview) is the
big remaining piece that closes B-2 — best tackled in a
fresh session because it crosses into `card_plugin`. Options
3 and 4 are visual polish that benefit from user review.
## Open punch list
@@ -126,10 +120,21 @@ palette refresh all shipped in v0.20.0 + v0.21.0. What stays open:
shipped in v0.21.2 (`2fb2d63`). The WIN MOVE scrub-bar marker
shipped post-v0.21.3 in `ab857bb` (data field) + `52befa6`
(UI). Playback controls (pause / resume / step + Space
accelerator) shipped post-v0.21.3 in `fbe48ac`. What still
needs to land: a move-log scroller and a mini-tableau
preview — both screen-takeover-only pieces that need a
larger layout reflow than the existing banner can carry.
accelerator) shipped post-v0.21.3 in `fbe48ac`. v0.21.5
bundled six more commits under "replay-overlay scrubbing
affordances + accessibility" (scrub notches + labels +
keybind footer + ESC and ← / → accelerators + HC border).
v0.21.6 bundled six more under "Move Log panel + scrub-UX
polish" — bottom-edge Move Log panel with prev/active/next
rows + active highlight, HC-mode coverage for the scrub
track + notches, continuous scrub on key-held arrows. Banner
height grew 60 → 76 → 92 px across two layout-changing
commits in v0.21.5; Move Log panel grew 56 → 84 → 112 px
across the v0.21.6 move-log commits. Per-commit detail in
`CHANGELOG.md` § [0.21.5] and § [0.21.6]. The only major
B-2 piece left is the mini-tableau preview — the mockup's
"Game Peek Band" at 50 % opacity. Architectural; touches
`card_plugin` rendering.
Multi-session.
- *Floating `MOVE N/M` chip above the focused card during
playback — closed 2026-05-08 by `2fb2d63`.* World-space
@@ -273,20 +278,23 @@ into a v0.21.1 / v0.22.0 cut.
```
You are a senior Rust + Bevy developer working on Solitaire Quest.
Working directory: <Rusty_Solitaire clone path on this machine>.
Branch: master. v0.21.3 is tagged at 3d92a91 (cut 2026-05-08, a
patch release rolling up the accessibility-arc closure: HC reaches
the previously-carved-out dynamic-paint sites, and the first real
consumer of `ToastVariant::Warning` lands as the daily-challenge
expiry toast). v0.21.2 stays at f23df3b, v0.21.1 at daa655a,
v0.21.0 at 04f9bf9. Working tree clean. See CHANGELOG.md §
[0.21.3] for full detail.
Branch: master. v0.21.6 is tagged at f63db76 (cut 2026-05-08, a
patch release rolling up Move Log panel + scrub-UX polish:
brand-new bottom-edge Move Log panel with prev / active / next
row context + active-row highlight, plus HC-mode coverage for
scrub track + notches and continuous scrub on key-held arrow
keys). v0.21.5 stays at a2432df, v0.21.4 at 23ff62c, v0.21.3
at 3d92a91, v0.21.2 at f23df3b, v0.21.1 at daa655a, v0.21.0 at
04f9bf9. Working tree clean. See CHANGELOG.md § [0.21.6] for
full detail.
State: HEAD locally — see `git rev-parse HEAD`. All workspace tests
pass (1207+; check with `cargo test --workspace`), clippy clean.
State: HEAD locally — see `git rev-parse HEAD`. The cut commit
is f63db76; any post-cut docs edits ride on top of that.
Workspace tests: 1273 passing / 0 failing. Clippy clean.
READ FIRST (in order, before doing anything):
1. SESSION_HANDOFF.md — this file
2. CHANGELOG.md — [0.21.3] section is the most recent cut
2. CHANGELOG.md — [0.21.6] section is the most recent cut
3. CLAUDE.md — unified-3.0 rule set
4. CLAUDE_SPEC.md — formal architecture spec
5. ARCHITECTURE.md — crate responsibilities + data flow
@@ -306,18 +314,22 @@ DECISION TO ASK THE PLAYER FIRST:
tests can't catch. Likely surfaces JNI ClipboardManager
and Android Keystore stubs that need real bridges. Larger
scope; needs an Android device or emulator running.
B. Replay-overlay screen-takeover redesign — multi-session
work. Three sub-pieces shipped post-v0.21.3: WIN MOVE
marker (`ab857bb` data field + `52befa6` UI), playback
controls (`fbe48ac` pause/resume/step + Space). What
still needs to land: a move-log scroller and a
mini-tableau preview — both layout-heavy pieces that need
more vertical real estate than the current banner-only
overlay carries, so the natural next finite step is the
screen-takeover layout itself (mockup at
`docs/ui-mockups/replay-overlay-mobile.html`). The
smaller floating-MOVE-chip piece shipped in v0.21.2
(`2fb2d63`).
B. Replay-overlay screen-takeover redesign — nearly complete
after 12 commits across v0.21.4-6. Scrub bar with notches
+ labels + WIN MOVE marker, pause / resume / step / stop
buttons, Space + Esc + ← / → keyboard accelerators with
continuous scrub on hold, keybind-hint footer, full HC-mode
coverage on the banner pieces, and a brand-new bottom-edge
Move Log panel with a 5-row prev/active/next window all
ship. The only remaining major B-2 sub-piece is the
**mini-tableau preview** — the mockup's "Game Peek Band"
at 50 % opacity showing the tableau through the replay
chrome. Implementation needs a settings-aware dim overlay
or alpha modulation on the tableau cards during replay.
Architectural — touches `card_plugin` rendering. Best
tackled in a fresh session because it crosses into a
plugin the recent B-2 work hasn't touched. Mockup at
`docs/ui-mockups/replay-overlay-mobile.html`.
C. Phase 8 (sync) — local storage scaffolding, self-hosted
Axum server, `SolitaireServerClient` impl, GPGS stub
wired into Settings. The biggest open arc by scope; rolls
+11
View File
@@ -30,6 +30,7 @@ use crate::events::{
use crate::font_plugin::FontResource;
use crate::game_plugin::{GameOverScreen, GameStatePath};
use crate::progress_plugin::ProgressResource;
use crate::replay_playback::ReplayPlaybackState;
use crate::resources::{DragState, GameStateResource};
use crate::selection_plugin::{SelectionKeySet, SelectionState};
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsStoragePath};
@@ -154,6 +155,7 @@ fn toggle_pause(
mut drag: Option<ResMut<DragState>>,
mut changed: MessageWriter<StateChangedEvent>,
selection: Option<Res<SelectionState>>,
replay_state: Option<Res<ReplayPlaybackState>>,
) {
let PauseModalQueries {
pause_screens: screens,
@@ -184,6 +186,15 @@ fn toggle_pause(
if !other_modal_scrims.is_empty() {
return;
}
// If a replay is currently playing, let `replay_overlay::handle_stop_keyboard`
// own the Esc press — that handler stops the replay. Without this guard a
// single Esc both stops the replay AND opens the pause modal on top of the
// (now empty) board, leaving the player on a screen they didn't ask for.
// The HUD-button path is gated too; clicking Pause while watching a replay
// is almost always an accident.
if replay_state.is_some_and(|s| s.is_playing()) {
return;
}
// If a card is currently selected, let SelectionPlugin handle this Escape
// (it will clear the selection). Pause must not also open in the same frame.
if selection.is_some_and(|s| s.selected_pile.is_some()) {
File diff suppressed because it is too large Load Diff
+47 -1
View File
@@ -42,7 +42,7 @@
use bevy::prelude::*;
use solitaire_data::{Replay, ReplayMove};
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent};
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
use crate::game_plugin::{GameMutation, RecordingReplay};
use crate::resources::GameStateResource;
use crate::settings_plugin::SettingsResource;
@@ -284,6 +284,52 @@ pub fn step_replay_playback(
true
}
/// Steps the replay **backwards** by exactly one move while paused.
///
/// Strategy: the live game's undo system is the source of truth for
/// reversing moves. Every move the replay forward-stepped (via
/// [`step_replay_playback`] or the auto-advance loop in
/// [`tick_replay_playback`]) was dispatched as a canonical
/// [`MoveRequestEvent`] / [`DrawRequestEvent`], which the game
/// applied and pushed onto its undo stack. So a backwards step here
/// is simply: decrement the cursor (so the about-to-apply move
/// re-points at the one we're rewinding past) and fire an
/// [`UndoRequestEvent`] so the game reverses its most-recent move
/// next frame.
///
/// Hard-gated to the paused state via destructure pattern —
/// matches the existing [`step_replay_playback`] gate so the
/// player can only scrub one direction at a time and the tick
/// loop never races a manual rewind.
///
/// Returns `false` and is a no-op in three cases:
/// - State isn't `Playing` (no replay attached).
/// - State is `Playing` but not paused (the tick loop owns the cursor).
/// - Cursor is already at 0 (nothing to rewind past).
///
/// Returns `true` on a successful step; the actual game-state
/// reversal happens next frame when `handle_undo` reads the
/// `UndoRequestEvent`.
pub fn step_backwards_replay_playback(
state: &mut ResMut<ReplayPlaybackState>,
undo_writer: &mut MessageWriter<UndoRequestEvent>,
) -> bool {
let ReplayPlaybackState::Playing {
cursor,
paused: true,
..
} = state.as_mut()
else {
return false;
};
if *cursor == 0 {
return false;
}
*cursor -= 1;
undo_writer.write(UndoRequestEvent);
true
}
/// Tick system. Runs every frame; only does work when
/// [`ReplayPlaybackState::is_playing`].
///
+38 -1
View File
@@ -34,7 +34,8 @@ use crate::ui_modal::{
};
use crate::ui_tooltip::Tooltip;
use crate::ui_theme::{
BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, BORDER_SUBTLE_HC, HighContrastBorder,
BG_BASE, BG_ELEVATED, BG_ELEVATED_HI, BORDER_SUBTLE, BORDER_SUBTLE_HC, HighContrastBackground,
HighContrastBorder,
RADIUS_SM, SPACE_2, STATE_SUCCESS, TEXT_PRIMARY, TEXT_SECONDARY, TYPE_BODY, TYPE_BODY_LG,
TYPE_CAPTION, VAL_SPACE_2, VAL_SPACE_3, Z_MODAL_PANEL,
};
@@ -365,6 +366,7 @@ impl Plugin for SettingsPlugin {
update_color_blind_text,
update_high_contrast_text,
update_high_contrast_borders,
update_high_contrast_backgrounds,
update_reduce_motion_text,
update_tooltip_delay_text,
update_time_bonus_multiplier_text,
@@ -674,6 +676,41 @@ fn update_high_contrast_borders(
}
}
/// Repaints `BackgroundColor` on every entity tagged with
/// [`HighContrastBackground`] based on `Settings::high_contrast_mode`.
/// Off → the marker's `default_color`; on → `BORDER_SUBTLE_HC`
/// (`#a0a0a0`). Compares against the current background and only
/// mutates when different so Bevy's change-detection doesn't trigger
/// repaints every frame.
///
/// Parallel to [`update_high_contrast_borders`]. Same on/off rule,
/// same change-suppression idiom, different colour channel —
/// `BackgroundColor` for tick marks, decorative strips, fine
/// separators that paint their shape directly rather than via a
/// `BorderColor` on a wider Node.
///
/// Tagged sites in v0.21.x: the replay overlay's 1 px scrub track
/// + 5 quarter-mark notch ticks (`replay_overlay::spawn_overlay`).
///
/// More sites can be tagged in follow-ups by adding
/// `HighContrastBackground::with_default(...)` to their spawn tuple.
pub(crate) fn update_high_contrast_backgrounds(
settings: Res<SettingsResource>,
mut backgrounds: Query<(&HighContrastBackground, &mut BackgroundColor)>,
) {
let high_contrast = settings.0.high_contrast_mode;
for (marker, mut bg) in backgrounds.iter_mut() {
let target = if high_contrast {
BORDER_SUBTLE_HC
} else {
marker.default_color
};
if bg.0 != target {
*bg = BackgroundColor(target);
}
}
}
fn update_reduce_motion_text(
settings: Res<SettingsResource>,
mut text_nodes: Query<&mut Text, With<ReduceMotionText>>,
+29
View File
@@ -252,6 +252,35 @@ impl HighContrastBorder {
}
}
/// Marker for entities whose [`BackgroundColor`] should swap to
/// [`BORDER_SUBTLE_HC`] when `Settings::high_contrast_mode` is on.
/// Parallel to [`HighContrastBorder`] but for sites that paint their
/// shape via `BackgroundColor` rather than `BorderColor` —
/// `bevy::ui` 1 px decorative strips, tick marks, fine separators
/// often render as tiny full-bleed `Node`s, not as borders, so the
/// border-marker pattern doesn't apply.
///
/// `default_color` records the off-state colour the entity was
/// spawned with so the system can revert when HC is toggled back
/// off. The accompanying paint system is
/// [`update_high_contrast_backgrounds`](crate::settings_plugin::update_high_contrast_backgrounds).
///
/// [`BackgroundColor`]: bevy::prelude::BackgroundColor
#[derive(bevy::prelude::Component, Debug, Clone, Copy)]
pub struct HighContrastBackground {
/// Background colour to use when high-contrast mode is *off* —
/// the site's normal idle / active-state colour.
pub default_color: bevy::prelude::Color,
}
impl HighContrastBackground {
/// Convenience constructor —
/// `HighContrastBackground::with_default(BORDER_SUBTLE)`.
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
Self { default_color }
}
}
/// Strong border — hover outline, focused button, active popover.
/// `outline` from the design system. `#505050`.
pub const BORDER_STRONG: Color = Color::srgba(0.314, 0.314, 0.314, 1.0);