9d4234cdedc5a4975d54570ca6fa5fb38cce1ba1
683 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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": -v0.21.5 |
||
|
|
511550232c |
docs(handoff): record HC marker + ← / → wiring; recommend v0.21.5 cut
Two more post-v0.21.4 carve-outs land: - |
||
|
|
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> |
||
|
|
3cc8eacafa |
docs(handoff): record ESC accelerator; B's next step is HC polish
Post-v0.21.4 fourth carve-out:
|
||
|
|
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> |
||
|
|
decbe0bbd9 |
docs(handoff): record keybind footer; B's next step is ESC accelerator
Post-v0.21.4 third carve-out:
|
||
|
|
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> |
||
|
|
d11d97e677 |
docs(handoff): record notch labels; B's next step is keybind footer
Post-v0.21.4 second carve-out:
|
||
|
|
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> |
||
|
|
c9e4c0b4cd |
docs(handoff): record scrub-bar notches; B's next step is notch labels
Post-v0.21.4 carve-out:
|
||
|
|
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> |
||
|
|
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> |
||
|
|
23ff62c397 |
docs: cut v0.21.4 — replay-scrubbing accessibility
Patch release for the three post-v0.21.3 commits on the B-2 replay screen-takeover redesign arc. One through-line: the replay overlay gains scrubbing affordances. The player can see at a glance where the winning move sits (WIN MOVE marker on the scrub bar) and stop on any move to inspect the board (pause / resume / step controls plus a Space keyboard accelerator). Also adds the data foundation that makes the marker possible: `Replay::win_move_index: Option<usize>`, an additive serde-default field that doesn't bump `REPLAY_SCHEMA_VERSION` because legacy on-disk replays load with `None` and simply don't get a marker. Remaining B-2 work — screen-takeover layout, move-log scroller, mini-tableau preview — shares a layout-reflow prerequisite the banner-only overlay can't carry, so it's deferred to a future cycle that can take it as a single multi-session arc. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>v0.21.4 |
||
|
|
0b2ffca016 |
docs(handoff): record playback controls; B's next step is takeover layout
Captures `fbe48ac` (pause / resume / step + Space accelerator) under "Since the v0.21.3 cut", marks playback controls closed in the Visual-identity follow-ups list, identifies the screen-takeover layout itself (with move-log scroller + mini-tableau preview as its sub-pieces) as the next finite step on B, and bumps the test count to 1228. 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>
|
||
|
|
cd79877933 |
docs(handoff): record WIN MOVE marker ship; B's next finite step
Captures `52befa6` (WIN MOVE marker on the scrub bar) under "Since the v0.21.3 cut", marks the marker piece of B-2 closed in the Visual-identity follow-ups list, identifies playback controls (play/pause/step) as the next bounded commit on B, and bumps the test count to 1220. 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> |
||
|
|
e63046700c |
docs(handoff): record win_move_index data field; B's next finite step
Captures `ab857bb` (Replay::win_move_index data field) under "Since the v0.21.3 cut". Updates the Visual-identity follow-up entry for B-2 to flag the data-layer prerequisite as landed and identifies the WIN MOVE scrub-bar marker UI as the natural next finite commit. Bumps test count to 1212. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ab857bbb6e |
feat(data): add Replay::win_move_index for the WIN MOVE scrub marker
First finite step toward the B-2 replay screen-takeover redesign: the data foundation. Adds an additive optional `win_move_index: Option<usize>` field on `Replay`, defaulting to `None` via `#[serde(default)]` so older `latest_replay.json` / `replays.json` files load unchanged — no `REPLAY_SCHEMA_VERSION` bump needed since the field is purely additive and nullable. Populated at the live recording site (`game_plugin::handle_game_won`) via a new builder-style setter `Replay::with_win_move_index`. For fresh recordings the value is always `Some(moves.len() - 1)` because recording freezes on win, but storing the index explicitly lets the playback UI read the WIN MOVE position directly without re-deriving it on every render — and leaves room for future recording semantics that capture post-win state. UI consumption (the WIN MOVE marker on the scrub bar, plus the broader screen-takeover redesign — move-log scroller, mini- tableau preview, playback controls) lands in subsequent commits. Test coverage: default value, builder set / set-None, on-disk round-trip, and the legacy-JSON-loads-with-None backward-compat contract (the test that pins the no-schema-bump claim). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
886e0cf8a1 |
docs(handoff): refresh post-v0.21.3 — anchor to new tag, reset menu state
Anchors handoff to v0.21.3 at `3d92a91`, resets the "Since the cut" section to placeholder, updates the READ FIRST CHANGELOG pointer, and bumps the Resume-prompt summary to reflect the accessibility arc closure as the v0.21.3 through-line. Resume menu stays at A/B/C since v0.21.3 closes only post-v0.21.2 carve-outs (the remaining options were already heavy / multi-session). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3d92a91e3b |
docs: cut v0.21.3 — accessibility arc closure + Toast Warning driver
Patch release for the two post-v0.21.2 commits. One through-line: the v0.21.2 "dynamic-paint sites stay un-tagged" carve-out turned out to be over-cautious — re-reading the code showed only the radial rim was actually a border-paint cycle. v0.21.3 closes the carve-out: HUD action buttons + modal buttons take the existing `HighContrastBorder` marker pattern; the radial rim folds HC into its per-frame respawn via `radial_rim_outline`. Bonus: `ToastVariant::Warning` gets its first real consumer in this cycle (daily-challenge expiry < 30 min from UTC reset). Every `ToastVariant` now has at least one driver — the enum is fully load-bearing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>v0.21.3 |
||
|
|
9113cdb483 |
docs(handoff): record HC dynamic-paint rollout; menu drops D → 3 options
Marks the HC dynamic-paint rollout (`c153363`) closed under the High-contrast accessibility entry, captures it in "Since the v0.21.2 cut", bumps the test count to 1207, and trims the Resume prompt menu from 4 → 3 options (A Android, B replay screen-takeover, C Phase 8 sync). All three remaining options are multi-session by nature; the resume prompt now flags that explicitly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c153363626 |
feat(accessibility): finish HC rollout — HUD + modal buttons + radial rim
Closes the v0.21.2 carve-out: dynamic-paint sites that were left un-tagged because their paint cycles were assumed to race `update_high_contrast_borders`. Re-reading the code revealed only one of three sites is actually a border-paint cycle — the other two paint backgrounds, with static borders that take the marker pattern cleanly: * HUD action buttons (`spawn_action_button`): `paint_action_buttons` only mutates `BackgroundColor`. Tag the spawn with `HighContrastBorder::with_default(BORDER_SUBTLE)`. * Modal buttons (`spawn_modal_button`): `paint_modal_buttons` also only mutates `BackgroundColor`. Same marker pattern. * Radial menu rim (`radial_redraw_overlay`): full despawn-respawn every frame; sprites, not UI nodes; the marker can't apply. Folds the HC choice into the spawn site instead — under HC the *focused* rim boosts to `BORDER_SUBTLE_HC` rather than `BORDER_STRONG`. Naive marker substitution would invert the visual hierarchy because `BORDER_SUBTLE_HC` (#a0a0a0) is lighter than `BORDER_STRONG` (#505050); folding the choice in keeps the focused rim *more* visible under HC, not less. Decision logic for the rim is extracted to `radial_rim_outline` — a pure function with a 4-row truth-table test (focused × HC). After this commit, every UI surface tagged in v0.21.x's accessibility arc either carries `HighContrastBorder` or has its HC behaviour folded into its own spawn cycle. No "un-tagged because race-risk" surfaces remain. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
93b67f1d0b |
docs(handoff): record Toast Warning wiring; menu drops C → 4 options
Marks the daily-challenge-expiry Warning toast (`279e23d`) closed in the Visual-identity follow-ups list, captures it in "Since the v0.21.2 cut", bumps the test count to 1203, and trims the Resume prompt menu from 5 → 4 options (A Android, B-2 replay takeover, C Phase 8 sync, D HC dynamic-paint). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
279e23d0af |
feat(toast): wire ToastVariant::Warning for daily-challenge expiry
Adds the first in-engine consumer of `ToastVariant::Warning` — a 4s amber-bordered toast that fires once per daily-challenge date when the player is within 30 minutes of UTC midnight reset and hasn't yet completed today's challenge. Mirrors the v0.21.2 `ToastVariant::Error` wiring: a domain-event message (`WarningToastEvent(String)`) crosses the plugin boundary; `animation_plugin::handle_warning_toast` reads it and spawns the fire-and-forget toast. Suppression is decided by a pure helper (`compute_expiry_warning_minutes`) that's exhaustively covered by 7 unit tests + 1 in-Bevy idempotence test. After this lands, every `ToastVariant` (Info, Warning, Error, Celebration) has at least one real driver — closing the "is this enum scaffolding or load-bearing?" ambiguity that's been latent since the variant was introduced. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
12fba2157a |
docs(handoff): refresh post-v0.21.2 — anchor to new tag, update menu
Mirrors the post-v0.21.0 → v0.21.1 → v0.21.2 cut-then-refresh
pattern. Cut commit (
|
||
|
|
f23df3b805 |
docs: cut v0.21.2 — accessibility extensions + replay polish + first real Toast Error consumer
Promotes [Unreleased] to [0.21.2] dated 2026-05-08 and opens a fresh empty [Unreleased]. Patch release covering 6 substantive post-v0.21.1 commits (plus the v0.21.1 handoff refresh). Three through-lines: - **Accessibility extensions.** Closes the two threads v0.21.1 left explicitly open. Reduce-motion was previously gated only on card slide_secs; v0.21.2 extends it to splash scanline + cursor pulse (`ed152e2`). HC borders had `BORDER_SUBTLE_HC` defined but no consumers; v0.21.2 builds the `HighContrastBorder` marker + `update_high_contrast_borders` system (`c9af1ea`) and rolls it out across 8 surfaces (`d87761d` + `ec804d5`). - **Replay polish.** New floating MOVE chip rendered above the destination pile of the most-recently-applied move during playback (`2fb2d63`). World-space `Text2d` entity that reuses the same `LayoutResource` pile coordinates as every other piece of pile geometry — stays correctly positioned through window resizes without any UI / camera math. - **First real `ToastVariant::Error` consumer.** Wires `MoveRejectedEvent` to a 2-second pink-bordered "Invalid move" toast (`68d50b5`). Joins the existing `card_invalid.wav` audio + destination-pile shake visual as the accessibility-focused readable text channel. cargo clippy --workspace --all-targets -- -D warnings clean. 1195 passing / 0 failing (net +3 from v0.21.1's 1192). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>v0.21.2 |
||
|
|
68d50b5021 |
feat(toast): wire ToastVariant::Error for invalid-move feedback
Resume-prompt Option C — first in-engine consumer of `ToastVariant::Error`. The variant has had a slot in the enum since v0.20.0's toast system landed; this commit wires a real driver event so the slot is no longer dead code. ### Driver: MoveRejectedEvent When a player tries an illegal placement (drops dragged cards on a real pile but the move violates the rules), `MoveRejectedEvent` fires. The existing rejection-feedback chain plays `card_invalid.wav` (audio cue) and triggers the destination-pile shake (visual cue via `feedback_anim_plugin`). This commit adds a third leg — a 2-second pink-bordered Error toast reading "Invalid move" — primarily for accessibility: - **Audio cue alone** doesn't help deaf players. - **Visual shake alone** is brief and easy to miss for low-vision players or anyone with reduce-motion enabled (which gates the shake's animation timing). - **Toast text** is persistent ~2 s, readable, and unambiguous. The three legs together cover the major perception channels. ### Implementation New `handle_move_rejected_toast` system in `animation_plugin` mirrors the shape of `handle_xp_awarded_toast` — read events, fire `spawn_toast(commands, "Invalid move", 2.0, ToastVariant::Error)`. Registered in the plugin's Update set between `handle_xp_awarded_toast` and `tick_toasts` so the toast spawn pipeline picks it up the same frame the event fires. `AnimationPlugin::build` gains `.add_message::<MoveRejectedEvent>()` so the message is initialized when the plugin runs under MinimalPlugins (tests). The message is also registered by `feedback_anim_plugin` — Bevy's `add_message` is idempotent, so both registrations coexist cleanly. Also drops the `#[allow(dead_code)]` from `ToastVariant::Error` (stale now that the variant has a real consumer) and updates the variant's doc comment to point at `handle_move_rejected_toast`. ### Test New `move_rejected_event_spawns_error_toast` pins the wiring: firing a `MoveRejectedEvent` spawns exactly one `ToastOverlay` on the next tick. Matches the shape of the existing `info_toast_event_spawns_toast_overlay` test. 1195 passing (+1 from prior 1194). Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ec804d54c6 |
feat(accessibility): finish HC chrome rollout — home + settings panel borders
Continues the rollout from `c9af1ea` (modal scaffold) and `d87761d` (tooltip + 3 panels). Tags the remaining 7 static- border surfaces in the chrome so the HC chrome thread is effectively complete: - **`home_plugin.rs` × 3**: the home-screen Level/XP/Score summary row (line 842), the home-screen mode-selector buttons (line 945), the home-screen mode-hotkey chips (line 1158). - **`settings_plugin.rs` × 4**: the card-back picker swatches (line 1952), the theme picker swatches (line 2093), the Sync Now button (line 2214), and the swatch glyph buttons (line 2274). Pre-tagging audit: confirmed none of these sites have a dynamic-paint system that would race the `update_high_contrast_borders` system. `paint_action_buttons` in `hud_plugin.rs` only paints entities tagged with the `ActionButton` marker (HUD buttons only). The focus-overlay system in `ui_focus.rs` spawns *separate* overlay entities for focus indication, never mutating the original `BorderColor`. Settings panel buttons / swatches use their own `SettingsButton` enum for click routing; their `BorderColor` is set at spawn time and not touched again. After this commit, every `BorderColor::all(BORDER_SUBTLE)` site in the chrome (excluding the dynamic-paint sites that are intentionally skipped — HUD action buttons, modal buttons, radial menu rim) carries a `HighContrastBorder` marker. The HC thread for chrome borders is closed; the dynamic-paint sites remain open for a future iteration that needs a different shape (folding HC into the dynamic-paint logic, or having HC consult hover/focus state). 1194 passing / 0 failing across the workspace (unchanged — no new tests; the system-level lifecycle of `HighContrastBorder` was already covered by the modal-scaffold scaffolding in `c9af1ea`). Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d87761d451 |
feat(accessibility): roll HighContrastBorder out to tooltip + 3 panel borders
Continues the HC chrome rollout started by `c9af1ea` (which wired just the modal scaffold). Tags four more static-border surfaces so they boost to `BORDER_SUBTLE_HC` (#a0a0a0) when high-contrast mode is on: - **Tooltip** (`ui_tooltip.rs:191`). The hover-revealed caption popup. Border legibility matters because tooltips are usually brief — if the player has to squint to find the panel edge, the tooltip dismisses before they've parsed it. - **Onboarding banner key chips** (`onboarding_plugin.rs:388`). The first-run UI's "press H or ?" key chips. First-run onboarding has the highest stakes for accessibility — a low-vision player who can't see the chips can't discover the help system. - **Help panel key chips** (`help_plugin.rs:265`). Same treatment as the onboarding chips: keyboard-shortcut chips inside the F1 cheat sheet. - **Stats panel cells** (`stats_plugin.rs:1019`). The S-key overlay's individual stat cells. A dense grid of bordered numbers is exactly the kind of surface where HC's `#505050 → #a0a0a0` boost makes the layout legible. Each tagging is one line on the spawn tuple plus an import. The existing `update_high_contrast_borders` system in `settings_plugin` (added in `c9af1ea`) handles all tagged entities uniformly — no system changes needed. ### Skipped on this pass Sites with dynamic hover/focus paint systems (HUD action buttons, modal buttons, radial menu rim) intentionally not tagged because their existing paint cycles would race the HC system. Wiring HC into those needs a different shape — either fold HC into the dynamic-paint logic, or have HC consult the hover/focus state. Future scope. Other HC-tagging candidates (`home_plugin.rs:842/945/1158` home menu element borders, `settings_plugin.rs:1952/2093/2214/2274` settings panel rows) are likely fine to tag but I'm capping this commit at four to keep it reviewable. Pattern is established; future commits can extend. 1194 passing / 0 failing across the workspace (unchanged — no new tests; the system-level test in `c9af1ea`'s scaffolding covers all tagged entities uniformly). Workspace clippy clean. 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> |
||
|
|
c9af1ead22 |
feat(accessibility): wire BORDER_SUBTLE_HC into the modal scaffold
Resume-prompt Option E, part 2 of 2 — HC chrome borders. Pairs with the reduce-motion gating in `ed152e2`. v0.21.1 introduced `BORDER_SUBTLE_HC` (#a0a0a0) but never wired it: the constant existed, no consumer used it. Spec at `design-system.md` §Accessibility (#2) mandates outline boost from `#505050` (BORDER_STRONG) to `#a0a0a0` under high-contrast mode so panels and popovers stay legible on low-quality displays. ### Architecture - New `HighContrastBorder` component in `ui_theme` carrying a `default_color: Color` field that records the off-state colour the entity was spawned with. Tag any UI node where border legibility is accessibility-critical. - New `update_high_contrast_borders` system in `settings_plugin` walks all tagged entities each Update tick, sets `BorderColor` to `BORDER_SUBTLE_HC` when `Settings::high_contrast_mode` is on, otherwise to `marker.default_color`. Compares against current `BorderColor` and only mutates when different so Bevy's change-detection doesn't trigger repaints every frame. ### Tagged in this commit - The modal scaffold's card border (`ui_modal::spawn_modal`). This is the primary accessibility target — modals demand attention and a low-vision player needs to perceive the panel boundary. Default colour: `BORDER_STRONG` (#505050); HC variant: `BORDER_SUBTLE_HC` (#a0a0a0). ### Future scope Other `BORDER_SUBTLE` / `BORDER_STRONG` consumer sites (help panel, stats panel, tooltip, action buttons, settings rows, etc.) can be tagged in follow-ups by adding `HighContrastBorder::with_default(...)` to their spawn tuple. The system handles any entity carrying the marker — no further changes needed once a site is tagged. Started small here to keep the commit reviewable and prove the architecture before rolling out broadly. Workspace clippy + cargo test --workspace clean. 1193 passing (unchanged from prior — no new tests added; the system is small enough that the running-game verification is the meaningful check). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ed152e2d8f |
feat(accessibility): gate splash scanline + cursor pulse on reduce-motion
Resume-prompt Option E, part 1 of 2 (the reduce-motion piece; HC chrome borders follow in a separate commit). v0.21.1 wired `Settings::reduce_motion_mode` through `effective_slide_secs` so cards snap instead of sliding under reduce-motion. The design-system spec at §Accessibility (#3) calls out two more sources of non-essential motion that reduce-motion should suppress: the splash CRT scanline effect and the splash cursor pulse. This commit gates both. ### Splash cursor pulse (`pulse_splash_cursor`) Previously sine-pulsed every frame regardless of settings. Now reads `Settings::reduce_motion_mode` and skips the pulse multiplier when on — the cursor still fades in / out with the global splash alpha (essential timing), but doesn't blink (decorative motion). The fade is preserved on purpose: skipping it would hard-cut the splash on/off, which is jarring; the spec specifically calls out *non-essential* motion as the reduce- motion target, and a decorative blink is more clearly non-essential than a fade timeline. ### Splash scanline overlay (`spawn_splash`) Previously generated and spawned unconditionally when `Assets<Image>` was available. Now skipped entirely when reduce-motion is on — without the scanline overlay the boot screen still reads as terminal-themed (foreground content, borders, palette swatches all unchanged); the scanlines are purely decorative. ### Test New `splash_skips_scanline_overlay_under_reduce_motion` pins the gate behaviour: under `reduce_motion_mode = true`, the splash root still spawns (essential motion intact) but the `SplashScanlineOverlay` entity is absent. 1193 passing (+1 from prior 1192). Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
279a834f9d |
docs(handoff): refresh post-v0.21.1 — anchor to new tag, renumber Resume menu
Mirrors the post-v0.20.0 → v0.21.0 → v0.21.1 cut-then-refresh
pattern. Cut commit (
|
||
|
|
daa655a0af |
docs: cut v0.21.1 — icon, accessibility, card-visual iteration
Promotes the [Unreleased] section to [0.21.1] dated 2026-05-08 and opens a fresh empty [Unreleased]. Patch release covering the 10 post-v0.21.0 commits. Two Resume-prompt options closed: - A — App icon. Runtime Window::icon wired via WinitWindows on desktop (target-gated to non-Android since Android draws its launcher icon from the APK manifest); 9-size PNG hierarchy at assets/icon/ generated by a new icon_generator example from a shared icon_svg builder. The follow-up `716a025` wraps NonSend<WinitWindows> in Option<...> to satisfy Bevy 0.18's stricter system-param validation. - F — High-contrast and reduce-motion accessibility modes. Settings flags wired through the engine + Settings panel UI toggles. CBM and HC compose; reduce-motion forces card slide duration to 0 regardless of AnimSpeed. Card-visual iteration cycle moved through three states: v0.21.0 Terminal pink/gray → 4-colour-deck experiment (`62b61cc`) → traditional 2-colour reversion at player request (`ddb6540`, saturated red + near-white). Two visible bugs surfaced and were fixed: - `dd97021` dropped the suit-coloured card border to remove anti-aliasing artifacts at the rounded corners. - `4d48cad` hides pile markers when occupied — the actual visible-artifact fix for "gray L corners". Implements the documented but previously-not-enforced "remain visible only where a pile is empty" invariant in table_plugin's module doc. cargo clippy --workspace --all-targets -- -D warnings clean. 1192 passing / 0 failing (net +8 from v0.21.0's 1184). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>v0.21.1 |
||
|
|
4d48cad4e3 |
fix(engine): hide pile markers under cards — kill the gray-corner artifact
Player feedback after the border-drop fix did NOT close the
"gray corners" complaint: "I do not see anything change." The
border was a real artifact, but the *visible* gray came from a
different source.
Root cause: pile markers are 8%-alpha-white sprites sized to
the card area, sitting at `Z_PILE_MARKER = -1.0` beneath every
card. Composited against the dark play surface, the marker's
effective colour is ≈`#272727` — visibly gray. When a card
(rounded corners, opaque body) sits on top, the marker's
rectangular fill bleeds through the 4 small triangular regions
where the card's rounded corner curves cut away from the card's
bounding rectangle. That bleed-through is the "gray L" the
player saw at each card corner.
Fix: hide pile-marker sprites for any pile that has a card on
top. New `sync_pile_marker_visibility` system runs each Update
tick, guarded by `game.is_changed()` so the work skips on idle
frames. Iterates `(&PileMarker, &mut Visibility)` and sets
`Hidden` for occupied piles, `Inherited` for empty.
This implements the *documented* invariant declared in the
module-level doc comment ("Pile markers ... remain visible only
where a pile is empty") that was previously not enforced —
markers always rendered. Strictly speaking this is a
documentation-vs-implementation drift fix, not a behaviour
change.
### Why the border-drop fix didn't address this
The border drop changed the SVG stroke and removed *one* source
of corner artifacts (anti-aliased red/near-white stroke fading
through gray). It correctly drifted 52 face hashes. But the
visible gray at corners came from a *different* layer — the
pile-marker sprite *behind* the card, not the card stroke
itself. Right test target, wrong visible-artifact target.
Two layers, two fixes; this commit closes the second.
### Test
New `pile_markers_hide_when_pile_is_occupied` pins the
post-deal state: 8 markers hidden (stock + 7 tableau), 5
markers visible (waste + 4 foundations). 1192 passing
(+1 from prior 1191).
Workspace clippy clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
dd970215cc |
fix(engine): drop card-face border to remove gray-corner artifact
Player feedback after the 2-colour revert: "I do not like the
grey corners on the cards." The visible artifact was anti-
aliasing physics — the 1 px suit-coloured stroke (red for
hearts/diamonds, near-white for clubs/spades) faded through
gray pixels into the dark play surface at each rounded corner,
producing a visible "gray sliver" at the four arcs of every
card.
Fix: drop the stroke entirely. The card body fill defines the
shape against the play surface; the 5-unit brightness gap
between `#1a1a1a` body and `#151515` surface is enough to read
as a card edge without an explicit stroke. Anti-aliasing on a
fill-only rounded rect blends `#1a1a1a → #151515` over a few
pixels — barely perceptible compared to the
`stroke → transparent` gradient that produced the artifact.
### Changes
- `card_face_svg.rs`: removed `stroke="{colour}" stroke-width="2"`
from the card body rect. Reverted the 1 px stroke inset back
to `(x=0, y=0, width=256, height=384)` since there's no
longer a stroke to keep inside the pixmap. Module-level
comment updated to document the reasoning.
- `design-system.md` § Game Cards line 225 updated: "Border:
1px solid in suit color" → "Border: none." with the
artifact rationale recorded as audit trail.
- `card_face_svg_pin.rs` rebaselined: all 52 face hashes drift
(every card's perimeter pixels changed); 5 back hashes
unchanged.
Workspace clippy + cargo test --workspace clean. 1191 passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ddb65403c2 |
feat(engine): revert to traditional 2-colour deck with saturated red + near-white
Per player feedback after the brief 4-colour-deck experiment: "can we make the card suit colors the same as a regular solitaire game would." Reverts the 4-colour split (`62b61cc`) and bumps both 2-colour hues to read more like a real Microsoft-Solitaire-on-dark-mode deck. ### Constants - `RED_SUIT_COLOUR`: `#fb9fb1` (Terminal pink, then briefly hearts-only) → `#e35353` (saturated red). More chromatic, less pastel; reads as "the red suit" rather than "a Terminal- themed pink." Visually distinct from `ACCENT_PRIMARY` `#a54242` (the brick-red CTA accent) so chrome and suit don't collapse to the same hue. - `BLACK_SUIT_COLOUR`: `#d0d0d0` (matched `TEXT_PRIMARY`) → `#e8e8e8` (near-white). Bumped slightly brighter so it reads as a chromatic-neutral counterpart to the new saturated red, not as "the same gray as body text." `TEXT_PRIMARY_HC` (`#f5f5f5`) is still brighter for the high-contrast boost path. - `RED_SUIT_COLOUR_HC`: `#ff8aa0` (pinkish boost matching the v0.21.0 pink default) → `#ff6868` (brighter saturated red). Now reads as "more chromatic" than the new default red, not "less saturated." - `DIAMOND_SUIT_COLOUR` and `CLUB_SUIT_COLOUR` deleted — the 4-colour split is gone, hearts/diamonds re-pair under `RED_SUIT_COLOUR` and clubs/spades under `BLACK_SUIT_COLOUR`. ### `card_face_svg.rs` - Module-level constants collapse from four (`SUIT_HEART` / `SUIT_DIAMOND` / `SUIT_CLUB` / `SUIT_SPADE`) back to two (`SUIT_RED` / `SUIT_DARK`) at the new saturated-red / near-white values. - `suit_paint()` reverts to the 2-colour pairing: hearts filled-red, diamonds outlined-red, spades filled-near-white, clubs outlined-near-white. Filled-vs-outlined glyph differentiation stays the always-on CBM fallback. ### `card_plugin.rs` - `text_colour()` reverts to a `card.suit.is_red()` bifurcation. Comment block updated to reflect the new truth table: red suits → saturated red (or CBM lime / HC brighter red); dark suits → near-white (or HC brighter near-white). ### Tests Test block restructured back to the pre-4-colour shape: two red/black pairing tests instead of one 4-colour distinctness test. CBM/HC compose tests retuned to the 2-colour world (red suits compose, dark suits compose; no separate diamonds-immune or clubs-immune cases). 1191 passing / 0 failing — net 0 from the prior commit (3 tests removed: the 4-colour distinctness test + the diamonds/clubs-immune test; 2 tests added back: the red-pairing + dark-pairing tests; existing tests amended to new colour assumptions). ### `card_face_svg_pin` All 52 face hashes drift (every suit's colour shifted); 5 back hashes unchanged. Surgical rebaseline. ### `design-system.md` §Suit Colors retitled "Two-color traditional pairing", table updated with the new hex values, CBM section text simplified back to red→lime swap on both red suits. Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
62b61cc786 |
feat(engine): switch card fronts to 4-colour deck
Hearts pink (`#fb9fb1`), Diamonds gold (`#ddb26f`), Clubs lime (`#acc267`), Spades gray (`#d0d0d0`) — each suit picks up its own base16-eighties accent so a player scanning the table can distinguish the suit by hue alone (faster recognition than the 2-colour traditional red/black scheme; common in poker decks). All four colours already exist in the palette as semantic state-token accents, so this is a pure remapping at the suit- glyph site, not a palette extension. The outlined-glyph differentiation (♦ ♣ outlined, ♥ ♠ filled) is preserved on top of the colour split — it stays the always- on colour-blind fallback per `design-system.md` §Accessibility, and matters more than ever now that CBM hearts (lime) and default clubs (lime) share a hue. ### Changes - `card_face_svg.rs`: split `SUIT_RED` / `SUIT_DARK` into four per-suit constants (`SUIT_HEART` / `SUIT_DIAMOND` / `SUIT_CLUB` / `SUIT_SPADE`). `suit_paint()` returns each suit's own colour. Card border picks up the suit colour automatically via the existing `(colour, paint)` destructure. - `card_plugin.rs`: new `DIAMOND_SUIT_COLOUR` + `CLUB_SUIT_COLOUR` constants; `text_colour()` rewritten as a per-suit match (was red/black bifurcation). Both rendering paths (PNG production + constant fallback under MinimalPlugins) stay in lockstep. - CBM behaviour clarified: only hearts swap to lime now; diamonds + clubs + spades are already hue-distinct from the heart pink and stay unchanged. Under CBM the heart (lime) and club (lime) share a hue but stay distinguishable via the always-on filled-vs-outlined glyph differentiation. - HC behaviour: only hearts (→ HC red) and spades (→ HC white) have defined boosts. Diamonds (gold) and clubs (lime) are already mid-luminance accents and stay at their default. New test `text_colour_diamonds_and_clubs_are_immune_to_accessibility_flags` pins all four flag combinations as no-ops for the gold + lime suits. - `design-system.md` §Suit Colors retitled "Four-color deck" with the 4-colour table; CBM section text updated to describe the hearts-only swap and the hearts/clubs hue collision under CBM. - `card_face_svg_pin.rs` rebaselined: 26 hashes drift (13 clubs + 13 diamonds — the two suits whose colours changed). Hearts, spades, and the 5 backs all keep their prior hashes. Surgical scope, exactly what the pin test was designed to surface. ### Tests 1191 passing / 0 failing — net 0 from the prior baseline: two old 2-colour tests removed (`text_colour_is_red_for_hearts_and_diamonds`, `text_colour_is_black_for_clubs_and_spades`), one consolidated 4-colour test added (`text_colour_4_colour_deck_assigns_each_suit_its_own_hue`) plus a pairwise-distinct invariant guard, and one new test covering the gold/lime suits' immunity to CBM/HC flags. Six existing CBM/HC tests rewritten to use only the suits each flag actually affects under the new scheme (hearts for CBM, hearts + spades for HC). Workspace clippy clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
31139ae455 |
docs(handoff): record Options A + F closures, refresh Resume prompt menu
Two post-v0.21.0 options closed today; "Since the v0.21.0 cut" section now narrates both: - A — App icon (`3eb3a26` + `716a025`). Runtime Window::icon wired via WinitWindows on desktop, 9-size PNG hierarchy at assets/icon/. The follow-up `716a025` wraps NonSend in Option<...> to satisfy Bevy 0.18's stricter system-param validation. - F — Accessibility modes (`c5787c6` + `07e0357`). High- contrast and reduce-motion settings flags + Settings UI toggles + engine wiring. CBM and HC compose; reduce-motion forces card slide_secs to 0. Open punch list refreshed: - Visual-identity follow-ups: HC and reduce-motion entries marked closed with future-scope notes (HC chrome borders, reduce-motion splash gating). - Carried forward from v0.19.0: App icon entry marked closed with future-scope note for .ico/.icns bundle formats (need new deps + matter only at packaging time). Resume prompt menu trimmed: A and F decision options now marked closed inline (preserved for audit-trail readability). B, C, D, E remain live. No runtime / test changes — pure docs hygiene to keep the handoff orientation accurate as work flows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
07e035771c |
feat(accessibility): add Settings UI toggles for high-contrast + reduce-motion
Resume-prompt Option F, part 2 of 2 — pairs with the engine wiring in |
||
|
|
c5787c6953 |
feat(accessibility): wire high-contrast + reduce-motion modes through engine
Resume-prompt Option F, part 1 of 2. Adds two accessibility flags to Settings and threads each through the engine surfaces that react to them. Settings UI toggle rows follow in a separate commit; players who want to test today can edit `settings.json` manually. Spec at `docs/ui-mockups/design-system.md` §Accessibility (#2 and #3). ### High-contrast mode `Settings::high_contrast_mode: bool` (defaults to false; serde- default for back-compat). When on: - Red-suit text colour boosts from `RED_SUIT_COLOUR` (`#fb9fb1`) to a new `RED_SUIT_COLOUR_HC` (`#ff8aa0`). - Black-suit text colour boosts from `BLACK_SUIT_COLOUR` (`#d0d0d0`) to a new `TEXT_PRIMARY_HC` (`#f5f5f5`). - New `BORDER_SUBTLE_HC` (`#a0a0a0`) constant available for future chrome-side wiring (this commit only routes HC through card text rendering — chrome border boost is a separable follow-up). The HC and CBM flags compose. CBM red→lime wins over HC on red suits when both are on (lime is itself a high-luminance accent, so the HC boost has nothing further to do). HC still applies to black suits when both flags are on (CBM doesn't touch black). Four new `text_colour` tests pin the truth table. ### Reduce-motion mode `Settings::reduce_motion_mode: bool` (defaults to false; serde- default for back-compat). When on: - Card-slide animation duration is forced to `0.0` regardless of the player's `AnimSpeed` selection — cards snap instantly to their target position. Implemented by extracting a new `effective_slide_secs(&Settings)` helper that wraps `anim_speed_to_secs` with the reduce-motion gate. - Future scaffolding hooks (splash scanline, warning-chip pulse, card-lift z-bump animation) follow the same `if settings.reduce_motion_mode { skip }` pattern when wired — stays out of scope for this commit since each motion path needs its own per-system gate. Two new tests cover the gate behaviour and the fall-through-to- AnimSpeed pass-through path. ### Threading `text_colour` signature extended with a `high_contrast: bool` parameter; `sync_cards` / `sync_cards_startup` / `sync_cards_on_change` / `sync_cards` core / `spawn_card_entity` / `update_card_entity` all gain a parallel parameter mirroring the existing `color_blind: bool` plumbing. Verbose but matches the established pattern; a future refactor could pack both into an `AccessibilityView` struct, but bigger blast radius. ### Stats 1191 passing / 0 failing across the workspace (net +6 from v0.21.0's 1185 baseline once the icon-pin test landed): - 4 new `text_colour` HC tests in `card_plugin` (red-suit boost, black-suit boost, CBM-wins-on-red, black-suits-with-CBM+HC-still-boost). - 2 new `effective_slide_secs` tests in `animation_plugin` (zero-out under reduce-motion, fall-through to AnimSpeed when off). `cargo clippy --workspace --all-targets -- -D warnings` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
716a025352 |
fix(app): wrap WinitWindows in Option to satisfy Bevy 0.18 param validation
`NonSend<WinitWindows>` failed system-param validation on the first few frames before `WinitWindows` was populated, panicking the Update system before any logic could run. Bevy 0.18's stricter validation panics rather than skips when a non-send resource is absent, with an error message spelling out the fix: *"wrap the parameter in `Option<T>` and handle `None` when it happens."* Wraps `winit_windows` as `Option<NonSend<WinitWindows>>` and early-returns on `None`, mirroring the same lifecycle handling already applied to `winit_windows.get_window(primary_entity)` — both fail in the same window of frames before winit's `Resumed` event fires. Repro from the user's `cargo run` log: ``` thread 'Compute Task Pool (2)' panicked at .../bevy_ecs-0.18.1/src/error/handler.rs:125:1: Encountered an error in system ...: Parameter ... failed validation: Non-send resource does not exist ``` Workspace clippy + cargo test --workspace clean, 1185 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3eb3a26789 |
feat(app): wire desktop window icon — Terminal ▌RS mark at runtime
Closes Resume-prompt Option A (the post-v0.21.0 first option). Half-day desktop work, no cert dependency. Three deliverables: 1. **SVG-authored icon** (`solitaire_engine/src/assets/icon_svg.rs`) — square Terminal mark: `#151515` background, brick-red `#a54242` 1 px border, brick-red ▌ cursor block centered, "RS" monogram in `#d0d0d0` foreground gray beneath. Same shape that already lives on the splash boot screen and card-back monogram, reused as the project's signature visual mark. Authored in a 64-unit logical box so it scales cleanly at every rasterisation target. 2. **9-size PNG hierarchy** (16, 24, 32, 48, 64, 128, 256, 512, 1024 px) regenerated by `solitaire_engine/examples/icon_generator.rs` into `assets/icon/icon_<size>.png`. Sizes cover Linux hicolor (16, 24, 32, 48, 64, 128, 256, 512), Windows .ico targets (16, 32, 48, 256), and macOS .icns targets (16, 32, 64, 128, 256, 512, 1024). The runtime path uses just the 256 px slot; the smaller sizes are pre-rendered for downstream packaging. 3. **Runtime `Window::icon` wiring** (`solitaire_app/src/lib.rs`). Bevy 0.18 has no `Window::icon` field — the icon is set through the underlying `winit::window::Window` via the `WinitWindows` resource. `set_window_icon` runs each Update tick, retries silently until `WinitWindows` is populated (typically frame 1 or 2), decodes the embedded 256 px PNG via `tiny_skia`, builds a `winit::window::Icon`, and self-disables via `Local<bool>`. Same one-shot pattern as `apply_smart_default_window_size`. Desktop-only — Android draws its launcher icon from the APK manifest, so the system is target-gated to `cfg(not(target_os = "android"))`. Dep changes (CLAUDE.md §8 user-confirmed): - `winit = "0.30"` promoted from a transitive Bevy dep to a direct dep on `solitaire_app` so `winit::window::Icon` is in scope — bevy_winit 0.18 doesn't re-export it. Version pinned to whatever Bevy uses; if Bevy bumps winit, this line bumps in lockstep. - `tiny-skia` added as a direct dep on `solitaire_app` for PNG → RGBA decode. Already in workspace deps for `solitaire_engine`; no version drift risk. - Both new deps target-gated to non-Android only. Test infrastructure: `solitaire_engine/tests/icon_svg_pin.rs` hashes the rasterised RGBA bytes at all 9 sizes via FNV-1a (same shape as `card_face_svg_pin`). Bootstrap pattern (empty EXPECTED → panic with hashes formatted as Rust source → paste back in) handles future intentional builder edits cleanly. Workspace clippy + cargo test --workspace clean. 1185 passing (+1 from v0.21.0's 1184 baseline — the icon pin's `rasterised_icon_bytes_match_pinned_hashes`). Out of scope for this commit: `.icns` / `.ico` bundling for macOS / Windows app packaging. Both are packaging-time concerns (set via bundle manifests, not runtime calls) and would need new deps (`ico` and `icns` crates) — separate followup if/when the project ships as a packaged macOS / Windows app rather than just `cargo run`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0c1cc40266 |
docs(handoff): refresh post-v0.21.0 — drop historical sections, retune Resume prompt
Mirrors the v0.20.0 → post-cut refresh pattern (commit |
||
|
|
04f9bf9be3 |
docs: cut v0.21.0 — visual-identity completion + palette refresh
Promotes the [Unreleased] section to [0.21.0] dated 2026-05-08
and opens a fresh empty [Unreleased]. The cycle's three through-
lines:
- **Card-face / suit / card-back artwork migration.** Closes
the v0.20.0 thread that explicitly deferred card-face palette
migration. 10 commits across 2 days landed both rendering
paths (assets/cards/*.png fallback + the bundled-default
theme SVGs that include_bytes!()-embed into the binary) on
identical Terminal art generated by shared face_svg /
back_svg builders. The card_face_svg_pin integration test
guards rasteriser drift via FNV-1a on raw RGBA bytes.
- **Splash + replay-overlay polish.** Closes Resume-prompt
Options B (splash cursor pulse + scanline overlay) and C
(replay banner ▌ label + GAME caption + MOVE chip + scrub
bar). Splash gets the SplashFadable scaffold that lets
future overlays fade N >> 3 elements via one marker + one
global lerp query.
- **ACCENT_PRIMARY palette swap.** Late-cycle stakeholder
decision: cyan #6fc2ef → brick red #a54242. Touches every
primary-accent surface across the engine. RED_SUIT_COLOUR_CBM
swapped from cyan to lime #acc267 in lockstep so the colour-
blind alternative stays hue-distinct from the new red-family
primary.
Three sign-off follow-ups surfaced once a human booted the
running game; all matched the same shape ("fallback path the
chrome migration walked past"): the embedded default theme
overrode the new PNGs, the table backgrounds were a separate
PNG path the v0.20.0 chrome migration didn't touch, and the
action-button row's font_size: 16.0 literal slipped through the
typography migration audit. All recorded under "Fixed".
Phase 8 (sync) and Phase Android runtime gaps (JNI bridges,
APK launch verification on device) remain open and roll
forward.
cargo clippy --workspace --all-targets -- -D warnings clean.
1184 passing / 0 failing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
v0.21.0
|