58f33da6bfa0f4cead860511bedeb6c58cf45793
28 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca5d8a9c55 |
fix(engine): silence Android-target dead-code and unused-import warnings
Build and Deploy / build-and-push (push) Successful in 34s
All 10 warnings were caused by hotkey/keyboard UI code behind #[cfg(not(target_os = "android"))] call sites whose definitions lacked the matching gate. Fixes: - help_plugin: gate keyboard-chip imports and font_kbd; #[allow(dead_code)] on ControlRow (keys field is data, not dead) - hud_plugin/ui_modal: replace cfg shadow pattern with cfg!() expression so the hotkey parameter is read on every platform - home_plugin: gate fn hotkey behind not(android) - onboarding_plugin: gate HotkeyRow, HOTKEYS, spawn_slide_hotkeys and their exclusive imports behind not(android) - replay_overlay: gate keybind_footer_hint_text behind not(android) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
8a3e30bd16 |
fix(android): P3 keyboard-hint sweep + clipboard JNI verified
Suppress all remaining keyboard-accelerator chips/labels on Android: - spawn_modal_button (ui_modal.rs): single cfg gate covers every modal across all 13+ callers (onboarding, pause, confirm, game-over, restore, play-by-seed, home, help, profile, stats, leaderboard, settings, achievement) - home_plugin.rs: mode-card hotkey chips (N/C/Z/X/T) gated off - replay_overlay.rs: [SPACE]/[ESC]/[←→] footer hint text gated off; mode-indicator text kept - help_plugin.rs: kbd chip containers gated off; description text kept Clipboard JNI verified on Pixel 7 AVD (Android 14): added temporary KEYCODE_C test hook, logcat confirmed "clipboard JNI OK", hook reverted. Both JNI bridges (keystore + clipboard) are now confirmed working on device. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
a449f60bc5 |
feat(stats): spawn Prev/Next replay selector in the Stats overlay
Wire the long-dormant ReplayPrevButton / ReplaySelectorCaption /
ReplayNextButton / ReplaySelectorDetail spawn site that was missing
since v0.19.0. The click handler and repaint systems already existed;
this commit adds the actual UI nodes so players can step through all
stored replays (up to REPLAY_HISTORY_CAP) instead of always watching
the most recent win.
Also fix an assertion-on-constant clippy lint in the replay_overlay
dim-layer z-order test (const { assert!() } form required).
1282 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
c50eaf81f7 |
feat(replay): add HC bump for WIN MOVE scrub-bar marker; extend HighContrastBackground
HighContrastBackground gains an optional hc_color field so sites can specify a domain-specific HC variant rather than always bumping to BORDER_SUBTLE_HC (gray). with_default() fills hc_color = BORDER_SUBTLE_HC preserving all existing behaviour; new with_hc(default, hc) lets callers specify both ends. update_high_contrast_backgrounds reads marker.hc_color instead of the hardcoded constant. STATE_SUCCESS_HC (#c8e862, L≈0.73) added to ui_theme — a brighter lime that maintains the success hue while standing out from bumped notch ticks (BORDER_SUBTLE_HC gray, L≈0.60) under HC mode. WIN MOVE marker now carries HighContrastBackground::with_hc(STATE_SUCCESS, STATE_SUCCESS_HC): lime stays lime under HC instead of turning gray. Unit test pins both the default and hc color fields on the spawned marker. 1276 tests pass / 0 failing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b44d2777ec |
fix(replay): centre scrub-bar notch labels on their notch ticks
The three middle scrub-bar labels (25%, 50%, 75%) previously had their left edge anchored at the notch percentage, making them read as "starting after" the notch. Apply the CSS translateX(-50%) pattern for Bevy 0.18 UI: give each middle label a fixed-width container (SCRUB_LABEL_CENTER_WIDTH = 36px), offset the container's left edge by -width/2 via margin.left, and add Justify::Center so the text renders centred within the container. The container's centre then coincides with the notch line at the chosen percentage. Endpoints (0%, 100%) keep their flush-left / flush-right anchoring unchanged. 1275 tests pass / 0 failing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
fbe48acef6 |
feat(replay): playback controls — pause / resume / step + Space accelerator
Third commit on the B-2 replay screen-takeover redesign. Adds the
ability to pause an in-flight replay, step through it one move at
a time while paused, and resume — both via on-screen buttons
(UI-first contract per CLAUDE.md §3.3) and the optional `Space`
keyboard accelerator.
State shape: a new `paused: bool` field on
`ReplayPlaybackState::Playing`. The `tick_replay_playback` system
skips the `secs_to_next` decrement entirely while `paused` is set
so cursor and timer freeze together — resuming starts the next
move from a full interval. Stepping fires the next move directly
via a new `step_replay_playback` API that bypasses the tick path
and is hard-gated to `Playing { paused: true }` so it can't race
the running tick loop.
Public API additions:
- `toggle_pause_replay_playback(state)` — flips the flag, returns
the new value (or None when not Playing).
- `step_replay_playback(state, moves_writer, draws_writer)` —
advances exactly one move when paused; returns true on dispatch,
false on any guard miss.
UI:
- Pause / Resume button next to Stop. Label repaints reactively
via `update_pause_button_label`, which walks `Children` from
the marked button to its inner `Text` so the spawn path doesn't
need a second marker.
- Step button next to Pause. Click fires the next move; while
unpaused the click is a no-op (guarded inside
`step_replay_playback`).
- `Space` keyboard handler reads `Option<Res<ButtonInput>>` and
no-ops when missing — keeps test-app compatibility under
`MinimalPlugins`.
Test coverage: pause-button label truth table, label repaint on
state change, click-toggles-paused, step advances cursor exactly
one with paused flag preserved, step-while-running is no-op,
Space toggles paused flag. 8 new tests (1220 → 1228).
Side-effect: 25 existing `Playing { ... }` construction sites
across `replay_overlay`, `achievement_plugin`, and
`replay_playback` tests gained `paused: false` to satisfy the new
field requirement. Mechanical edit; no behavioral change.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
52befa6199 |
feat(replay): WIN MOVE marker on the scrub bar
Second commit on the B-2 replay screen-takeover redesign — the UI that consumes the data field landed in `ab857bb`. Adds a small green tick on the scrub bar at `replay.win_move_index / total`, positioned so the playback cursor reaches the marker exactly when the move it's about to apply IS the winning move. Implementation: a new `ReplayOverlayWinMoveMarker` component spawned alongside `ReplayOverlayScrubFill` as a sibling under the 1px scrub track. Position computed by a pure helper `win_move_marker_pct` that returns `None` for any of: state not `Playing`, replay's `win_move_index` is `None` (older replay loaded from disk pre-dating the field), or empty move list. The percentage is clamped to `[0, 100]` defensively. Marker is absolute-positioned with `top: -1px` so the 3px-tall tick is centered on the 1px track line — 1px above and 1px below. Lifecycle is "spawn-time only" — the marker position never changes during a single playback because the underlying replay is immutable while `Playing`. Despawned with the rest of the overlay tree when the state returns to `Inactive`. 8 new tests cover: pure helper for Inactive / Completed / no-field / correct-position / clamp; spawn presence with field; spawn absence without field; despawn-with-overlay lifecycle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
2fb2d638bf |
feat(replay): floating MOVE chip above the focused card during playback
Resume-prompt Option B (smaller scope variant) — closes the "floating MOVE chip" piece flagged as future scope in v0.21.1's replay-overlay punch list. Leaves the multi-session screen- takeover redesign for a future B-2. The existing banner-anchored MOVE chip stays put — it provides the at-a-glance overview. The new floating chip mirrors the same text but renders above the destination pile of the most-recently- applied move, keeping progress at the player's focal point so they don't have to look up at the banner during fast-paced playback. ### Architecture - New `ReplayFloatingProgressChip` marker component on a `Text2d` entity rendered in 2D world space. World-space placement (rather than UI-space + camera projection) keeps the math trivial — the chip uses the same `LayoutResource` pile coordinates that drive every other piece of pile geometry, so it stays correctly positioned through window resizes without any extra wiring. - Lifecycle matches the banner overlay: `spawn_overlay` spawns the chip alongside the banner when a replay starts; `react_to_state_change` despawns it when the replay ends. The chip lives outside the UI tree (because it's world-space) so the despawn needs its own query — added a second `Query<Entity, With<ReplayFloatingProgressChip>>` parameter. - Z = 100 keeps the chip above every card stack (Z_DROP_OVERLAY = 50, Z_STOCK_BADGE = 30, regular tableau cards stack to the low double digits at most). ### Position + visibility logic `update_floating_progress_chip` runs each Update tick: - Resolves the destination pile of the last-applied move (`replay.moves[cursor - 1]`'s `to`). - Hides the chip when `cursor == 0` (no moves applied yet — nowhere meaningful to land) or when the last move was a `StockClick` (no destination pile, and stock-click feedback already lives at the stock pile — letting the chip jitter back to the stock every cycle would be visual noise). - Otherwise positions the chip at `pile_position + (0, card_size.y * 0.6)` — half a card lifts above the pile centre, the extra 10 % is breathing room above the card's top edge so the chip doesn't visually clip. - Updates the chip text via `format_progress(&state)` — shares the same MOVE N/M format with the banner chip. ### Test New `floating_chip_spawns_and_despawns_with_overlay` pins the lifecycle: chip absent on Inactive, exactly one chip on Playing, absent again on return to Inactive. Position correctness needs `LayoutResource` (which the headless fixture doesn't set up); covered via running-game verification rather than a unit test — the system's gate logic is small enough that pixel positioning isn't load-bearing on a test. 1194 passing (+1 from prior 1193). Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a292a7ead0 |
feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242
Project-wide palette shift at user request. Replaces the cyan primary accent everywhere it surfaces — splash boot screen, home menu glyphs, action chevrons, replay overlay banner + scrub fill + chip border, achievement checkmarks, leaderboard #1 indicator, radial menu fill, focus ring, card-back canonical badge, etc. — with `#a54242` from the same base16-eighties family as the existing pink suit colour. Knock-on changes that all land in this commit per the lockstep rule: - ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85 alpha). Module-level palette comment + STOCK_BADGE_FG + CARD_SHADOW_ALPHA_DRAG doc strings updated to match. - card_plugin.rs: card_back_colour(0) now returns the brick-red ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from cyan to lime #acc267 — the CBM alternative needs to stay hue-distinct from the new red-family primary, lime is the next-best non-red base16-eighties accent. text_colour doc + CBM tests renamed cyan→lime in lockstep (text_colour_color_blind_mode_swaps_red_suits_to_lime). - card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical Terminal back). - splash_plugin.rs / ui_modal.rs / replay_overlay.rs / selection_plugin.rs: descriptive "cyan" comments swapped to "accent" / "primary-accent" wording so the doc strings stay decoupled from any specific hue. Future palette tweaks won't require comment churn. - design-system.md: YAML token frontmatter updated (primary, surface-tint, suit-red-cb, primary-container, on-primary-container, inverse-primary). Palette table gains a project-specific `base08` slot for the new red. CTA / Selection / Card-back badge / Primary button / Bottom-bar active-icon / glow / CBM swap text all retuned. Historical references preserved (e.g. "Was cyan #6fc2ef before the 2026-05-08 swap") so the audit trail stays in the spec. - card_face_svg_pin.rs: rebaselined. Exactly one hash drift (back_0 — the canonical Terminal back's badge changed colour). Other 56 hashes identical (face SVGs don't reference the accent; back_1..4 use unchanged accents). The one-hash-drift signal confirms the change scope was surgical. Workspace clippy + cargo test --workspace clean, 1184 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
e080b49914 |
feat(engine): restyle replay progress text as Terminal MOVE chip
Closes the centre-text half of the replay-overlay enrichments arc. The plain "Move N of M" text becomes a 1px ACCENT_PRIMARY-bordered chip containing "MOVE N/M" — uppercase + slash separator reads as a Terminal output line and matches the floating-chip motif in docs/ui-mockups/replay-overlay-mobile.html. The chip lives in-banner rather than floating above the focused card; the screen-takeover treatment that requires plumbing cursor → card identity remains deferred per SESSION_HANDOFF. Implementation: the centre Text spawn is now wrapped in a Node with 1px border + axes(VAL_SPACE_2, VAL_SPACE_1) padding and no background fill (Terminal aesthetic gets depth from borders + tonal layering, not shadows). The ReplayOverlayProgressText marker stays on the inner Text so update_progress_text continues to repaint contents unchanged. format_progress now returns "MOVE N/M" for Playing and "REPLAY COMPLETE" for Completed (uppercase to match the chip's typographic treatment); Inactive still returns "" since the overlay shouldn't be spawned in that state. Used BorderColor::all(ACCENT_PRIMARY) — Bevy's BorderColor is per-side in 0.18, no longer the tuple struct it was earlier. Module-level docstring + ReplayOverlayScrubFill doc comment both updated to quote the new "MOVE N/M" string. Test overlay_progress_text_reflects_cursor swapped its assertion to match. 1182 tests still pass; clippy clean. This closes Option C from the SESSION_HANDOFF Resume prompt's banner- local enrichments. The full screen-takeover redesign (mini-tableau, playback controls, move-log scroll, WIN MOVE marker requiring a win_move_index field on Replay) remains the multi-session item. |
||
|
|
54005d5494 |
feat(engine): add GAME #YYYY-DDD caption beneath the replay headline
Adds the right-anchored game-identifier piece of the replay-overlay
mockup (docs/ui-mockups/replay-overlay-mobile.html), adapted to live
under the existing "▌ replay" headline rather than as a separate
top-bar surface — the screen-takeover redesign is intentionally
deferred per the SESSION_HANDOFF punch list.
The caption reads `GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122`
for a replay recorded 2026-05-02), matching the mockup's
`GAME #2024-127` motif. Year + chrono ordinal gives a compact,
monotonically-increasing identifier that's grep-friendly across
replay files. TYPE_CAPTION (11 px) / TEXT_SECONDARY paint so the
caption reads as subordinate metadata, not a callout.
Implementation: new ReplayOverlayGameCaption marker, new pure
helper `format_game_caption(state) -> Option<String>` (None for
Inactive / Completed since the replay is consumed in those branches),
left-side label spawn restructured into a column container holding
the headline + caption with a 2 px row gap. BANNER_HEIGHT bumped
48 → 60 px so the column fits without overflow (16 px vertical
padding + 1 px scrub + ~39 px content; +12 px banner mass is the
deliberate cost of the new content).
Two new tests (1180 → 1182): format_game_caption_covers_state_corners
pins the three branches (Inactive / Completed / Playing) plus the
zero-pad-to-3-digits invariant for early-January ordinals; and
overlay_game_caption_shows_replay_date drives ReplayPlaybackState
end-to-end and asserts the caption text on spawn and that the
overlay stays spawned through Playing → Completed.
MOVE chip restyle from the same mockup is the next commit.
|
||
|
|
6204db8bb1 |
feat(engine): port replay banner label to ▌ cursor-block treatment
Aligns the replay overlay's headline with the splash boot-screen idiom
landed in
|
||
|
|
c84d9f445c |
feat(engine): scrub fill bar + per-frame updater for replay overlay
Closes the spawn-time half of the replay-overlay redesign open in SESSION_HANDOFF.md by adding the 1px cyan scrub bar called for in docs/ui-mockups/replay-overlay-mobile.html. A track in BORDER_SUBTLE spans the bottom edge of the banner and the cyan ACCENT_PRIMARY fill mirrors cursor / total via a new ReplayOverlayScrubFill component + update_scrub_fill system. The pure scrub_pct helper is shared between the spawn path (initial fill width) and the per-frame updater so the first paint already reflects state instead of popping 0 → cursor on the first tick — same shape as the existing format_progress / update_progress_text split. Two new tests (1176 → 1178): scrub_pct_covers_state_corners pins the helper's four corners (Inactive / cursor=0 / midpoint / Completed) and overlay_scrub_fill_tracks_cursor drives ReplayPlaybackState end-to-end and asserts Node.width on the unique scrub-fill entity. Same change- detection guard as the text updaters, so an idle replay leaves the node untouched. Header text treatment, move-log scroll, MOVE chip, and WIN MOVE callout from the same mockup are still open — separate commits. |
||
|
|
9891ae4ba3 |
refactor(engine): final hint-highlight + replay-overlay token cleanup
- input_plugin's hint-source card tint moves from raw bright-yellow `srgba(1.0, 1.0, 0.4, 1.0)` to the design-system STATE_WARNING token, so the source card and the destination pile (which already uses STATE_WARNING via HINT_PILE_HIGHLIGHT_COLOUR) wear the same attention colour as a coherent pair. - replay_overlay had two stale doc comments referencing the old "loud yellow accent" — Primary is now cyan (ACCENT_PRIMARY). Comments updated; no behaviour change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9c36b49729 |
feat(engine): replay-playback overlay banner with Stop button
Visible UI for the in-engine replay playback that just landed: a thin top banner anchored to the window edge while ReplayPlaybackState is Playing or Completed, surfacing the player's current position in the move list and a way to abort. Layout: full-width banner ~48 px tall with three children — a "Replay" label in ACCENT_PRIMARY left-aligned, "Move N of M" progress text centred, and a Tertiary Stop button right-aligned via the existing spawn_modal_button helper so it gets focus rings and hover/press states for free. Z_REPLAY_OVERLAY = Z_DROP_OVERLAY + 5 (= 55) sits above HUD but well below modal scrim (≥200), so Settings, Pause, and Help still render on top of the overlay during a replay — the player can adjust audio or pause mid-playback. State-driven: the spawn system reacts to Changed<ReplayPlaybackState> transitions, swapping the banner text to "Replay complete" when state moves Playing → Completed and despawning entirely when state returns to Inactive (either via the Stop button, completion linger expiry, or external reset). Five tests cover spawn-on-Playing, progress text, stop-button clears state and despawns, despawn-on-Inactive, and Completed banner text swap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |