Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c50eaf81f7 | |||
| b44d2777ec | |||
| 52407e7256 | |||
| da3e5423dc | |||
| a1864271de |
+31
-1
@@ -6,9 +6,39 @@ project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
No threads in flight. v0.21.6 cut on 2026-05-08; CHANGELOG accumulates
|
||||
No threads in flight. v0.21.7 cut on 2026-05-08; CHANGELOG accumulates
|
||||
the next cycle here.
|
||||
|
||||
## [0.21.7] — 2026-05-08
|
||||
|
||||
Patch release closing the last major B-2 sub-piece. Through-line:
|
||||
**mini-tableau preview dim layer**. The mockup's "Game Peek Band at
|
||||
50 % opacity" is now implemented as a full-screen UI scrim that darkens
|
||||
the card world during replay so the chrome (banner + move-log panel)
|
||||
reads clearly against the scene.
|
||||
|
||||
### Added
|
||||
|
||||
- **Full-screen tableau dim layer** (`da3e542`). Spawns a
|
||||
`ReplayTableauDimLayer` UI node (100 % × 100 %, 50 % opacity
|
||||
black) at `Z_REPLAY_DIM = Z_REPLAY_OVERLAY − 1 = 54` whenever
|
||||
a replay starts; despawned alongside the banner and move-log
|
||||
panel when the replay ends. 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`.
|
||||
The dim layer carries no `Interaction` component (purely
|
||||
visual; pointer events pass through). Adds `Z_REPLAY_DIM`
|
||||
and `TABLEAU_DIM_ALPHA` constants plus two new tests:
|
||||
lifecycle (spawn/despawn mirrors the floating-chip pattern)
|
||||
and z-ordering invariant (`Z_REPLAY_DIM < Z_REPLAY_OVERLAY`
|
||||
pinned). 1275 tests pass / 0 failing.
|
||||
|
||||
### Stats
|
||||
|
||||
- Tests: 1275 passing / 0 failing
|
||||
- Clippy: clean
|
||||
- Crates touched: `solitaire_engine` (replay_overlay.rs)
|
||||
|
||||
## [0.21.6] — 2026-05-08
|
||||
|
||||
Patch release for the post-v0.21.5 work. Through-line:
|
||||
|
||||
+79
-147
@@ -1,101 +1,66 @@
|
||||
# Solitaire Quest — Session Handoff
|
||||
|
||||
**Last updated:** 2026-05-08 — **v0.21.5 cut and tagged at
|
||||
`a2432df`**, working tree clean, all post-tag work pushed to
|
||||
origin.
|
||||
**Last updated:** 2026-05-08 — **v0.21.7 cut and tagged at
|
||||
`da3e542`**, working tree clean (tag pending push).
|
||||
|
||||
v0.21.5 is a patch release with one through-line:
|
||||
**replay-overlay scrubbing affordances + accessibility**.
|
||||
v0.21.4 shipped pause / resume / step + the WIN MOVE marker as
|
||||
the first scrubbing-shaped additions to the replay overlay;
|
||||
v0.21.5 fills out the rest of the scrubbing UX so the player
|
||||
has both visual anchor points (notches + labels) and a complete
|
||||
keyboard control surface (Space / Esc / ← / →) for navigating a
|
||||
paused replay.
|
||||
v0.21.7 is a single-commit patch closing the last major B-2
|
||||
sub-piece: **mini-tableau preview dim layer**. A full-screen
|
||||
`ReplayTableauDimLayer` UI node (100 % × 100 %, 50 % opacity
|
||||
black) at `Z_REPLAY_DIM = 54` (one rung below the replay
|
||||
chrome at z=55) darkens the card world during replay so the
|
||||
banner and move-log panel read clearly against the scene —
|
||||
matching the mockup's "Game Peek Band at 50 % opacity" spec
|
||||
without touching `card_plugin`. 13 commits have now shipped
|
||||
across v0.21.4–v0.21.7 on the B-2 replay screen-takeover
|
||||
arc; every major sub-piece is closed.
|
||||
|
||||
Six commits on the B-2 replay screen-takeover redesign arc land
|
||||
here. Two of them are layout-changing — banner height grew
|
||||
60 → 76 → 92 px to make room for the notch labels and keybind
|
||||
footer. Banner geometry was fixed for every prior B-2 commit;
|
||||
this release establishes the "grow the container, add a
|
||||
flex-column child" pattern that the remaining B-2 sub-pieces
|
||||
(move-log scroller, mini-tableau preview) will inherit when
|
||||
they land.
|
||||
|
||||
Full v0.21.5 detail lives in `CHANGELOG.md` § [0.21.5]. This
|
||||
Full v0.21.7 detail lives in `CHANGELOG.md` § [0.21.7]. This
|
||||
file from here on focuses on what's *open* post-cut and how to
|
||||
resume.
|
||||
|
||||
## Status at pause
|
||||
|
||||
- **HEAD locally:** see `git rev-parse HEAD`. The cut commit is
|
||||
`a2432df`; any post-cut docs edits ride on top of that.
|
||||
- **HEAD on origin:** matches local. v0.21.5 is fully on origin.
|
||||
- **HEAD locally:** `da3e542` (v0.21.7 commit). Tag pending —
|
||||
push with `git tag v0.21.7 da3e542 && git push origin v0.21.7`.
|
||||
- **HEAD on origin:** `f63db76` (v0.21.6). v0.21.7 commit
|
||||
not pushed yet; a docs-only edit will ride on top before push.
|
||||
- **Working tree:** clean. No WIP outstanding.
|
||||
- **`artwork/` directory:** still untracked. Intentional.
|
||||
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings`
|
||||
clean.
|
||||
- **Tests:** **1254 passing / 0 failing** across the workspace
|
||||
(1250 in v0.21.5 + 2 from `d3cb1a5`'s HC-marker tests + 2
|
||||
from `2e25476`'s continuous-scrub tests). The
|
||||
time-dependent `daily_challenge` flake noted in v0.21.5's
|
||||
CHANGELOG passes again (UTC clock has moved past the
|
||||
trigger window). Detail in `CHANGELOG.md` § [0.21.5] § Stats
|
||||
for the v0.21.5 baseline; post-cut delta tracked in this
|
||||
file's Since-cut log.
|
||||
- **Tags on origin:** `v0.9.0` through `v0.21.5`. v0.21.5 is on
|
||||
`a2432df`; v0.21.4 stays on `23ff62c`; v0.21.3 stays on
|
||||
`3d92a91`; v0.21.2 stays on `f23df3b`; v0.21.1 stays on
|
||||
`daa655a`; v0.21.0 stays on `04f9bf9`; v0.20.0 stays on
|
||||
`41a009a`.
|
||||
- **Tests:** **1275 passing / 0 failing** across the workspace.
|
||||
Detail in `CHANGELOG.md` § [0.21.7] § Stats.
|
||||
- **Tags on origin:** `v0.9.0` through `v0.21.6`. v0.21.7
|
||||
tag exists locally at `da3e542`; push to origin when ready.
|
||||
|
||||
## Since the v0.21.5 cut
|
||||
## Since the v0.21.7 cut
|
||||
|
||||
- **`d3cb1a5` — `feat(replay): HC-mode coverage for scrub
|
||||
track + notches`.** Adds a parallel primitive to ui_theme
|
||||
(`HighContrastBackground` marker carrying `default_color`)
|
||||
and a paint system in settings_plugin
|
||||
(`update_high_contrast_backgrounds`) that mirrors the
|
||||
existing border-marker pattern but targets `BackgroundColor`
|
||||
instead of `BorderColor`. Tags the 1 px scrub track Node and
|
||||
all five quarter-mark notch ticks with the new marker so
|
||||
they bump from `BORDER_SUBTLE` (#505050) → `BORDER_SUBTLE_HC`
|
||||
(#a0a0a0) under HC mode. Scrub fill (ACCENT_PRIMARY) and
|
||||
WIN MOVE marker (STATE_SUCCESS) don't get the marker —
|
||||
accent and state colours are already saturated. 2 new tests;
|
||||
1250 → 1252.
|
||||
- **`2e25476` — `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. Per-key accumulators in a new
|
||||
`ReplayScrubKeyHold` resource; `just_pressed` events bypass
|
||||
the accumulator and fire immediately. Release resets to 0
|
||||
so the next fresh press fires immediately rather than at
|
||||
half-interval. Footer text unchanged (`[← →] step`) —
|
||||
held-key scrub is a discoverable enhancement to the same
|
||||
keybind, not a new keybind. 2 new tests using
|
||||
`TimeUpdateStrategy::ManualDuration`; 1252 → 1254.
|
||||
One commit in flight (not yet pushed to origin): `da3e542`
|
||||
adds the full-screen tableau dim layer. CHANGELOG and
|
||||
SESSION_HANDOFF updates ride on top. Push with:
|
||||
```
|
||||
git push origin master
|
||||
git push origin v0.21.7
|
||||
```
|
||||
|
||||
Open next-step menu (B-2 keyboard accelerator coverage +
|
||||
accessibility + scrub UX are all complete):
|
||||
1. **Move-log scroller / mini-tableau preview** — both need
|
||||
a much larger banner-height grow (effectively the takeover
|
||||
container itself). Multi-session arcs that close B-2.
|
||||
Mockup at `docs/ui-mockups/replay-overlay-mobile.html`.
|
||||
2. **Polish: notch label centering.** Bevy 0.18 lacks a clean
|
||||
`translate-x: -50%` primitive so middle three labels sit
|
||||
slightly right-of-notch. Could use a child Text wrapper
|
||||
with computed left-margin compensation. Tiny commit.
|
||||
3. **Polish: WIN MOVE marker HC bump.** Currently the marker
|
||||
uses `STATE_SUCCESS` lime which stays visible under HC,
|
||||
but a slight saturation / contrast bump under HC would
|
||||
make the marker even more legible alongside the bumped
|
||||
notches. Optional.
|
||||
Open next-step menu (all major B-2 sub-pieces now closed):
|
||||
1. **Polish: notch label centering.** Bevy 0.18 lacks a
|
||||
clean `translate-x: -50%` primitive so the middle three
|
||||
scrub-bar labels sit slightly right-of-notch. Could use a
|
||||
child Text wrapper with computed left-margin compensation.
|
||||
Tiny commit, requires visual review.
|
||||
2. **Polish: WIN MOVE marker HC bump.** Currently uses
|
||||
`STATE_SUCCESS` lime which stays visible under HC, but a
|
||||
contrast bump under HC would make it even more legible
|
||||
alongside the bumped notches. Optional.
|
||||
3. **Move Log auto-scroll** — only relevant if the panel's
|
||||
row count grows beyond the current 5-row fixed window.
|
||||
Currently the prev-2 / active / next-2 layout fits all
|
||||
visible content, so auto-scroll is unneeded.
|
||||
|
||||
Recommended order: option 2 (notch label centering) is the
|
||||
smallest concrete next-step. Option 1 is the multi-session
|
||||
arc that closes B-2 — natural place to start a fresh session.
|
||||
Recommended order: options 1 and 2 are tiny polish commits
|
||||
that benefit from visual review. Option 3 is a non-starter
|
||||
unless the panel's row capacity grows.
|
||||
|
||||
## Open punch list
|
||||
|
||||
@@ -127,33 +92,18 @@ chrome migration, splash boot screen, replay-overlay banner,
|
||||
card-face artwork (both rendering paths), and the `ACCENT_PRIMARY`
|
||||
palette refresh all shipped in v0.20.0 + v0.21.0. What stays open:
|
||||
|
||||
- **Replay-overlay screen-takeover redesign.** The full mockup
|
||||
(`docs/ui-mockups/replay-overlay-mobile.html`) calls for a
|
||||
mini-tableau preview, playback controls, move-log scroll, and
|
||||
a WIN MOVE marker on the scrub bar. Banner-local pieces all
|
||||
shipped in v0.21.0 (`c84d9f4` + `6204db8` + `54005d5` +
|
||||
`e080b49`); the floating MOVE chip above the focused card
|
||||
shipped in v0.21.2 (`2fb2d63`). The WIN MOVE scrub-bar marker
|
||||
shipped post-v0.21.3 in `ab857bb` (data field) + `52befa6`
|
||||
(UI). Playback controls (pause / resume / step + Space
|
||||
accelerator) shipped post-v0.21.3 in `fbe48ac`. v0.21.5
|
||||
bundled six more commits under "replay-overlay scrubbing
|
||||
affordances + accessibility" — scrub notches + percentage
|
||||
labels + keybind-hint footer + ESC and ← / → accelerators
|
||||
+ HC marker for the footer top border. Banner height grew
|
||||
60 → 76 → 92 px across two layout-changing commits in
|
||||
v0.21.5; banner geometry is now mutable. Full per-commit
|
||||
detail in `CHANGELOG.md` § [0.21.5]. Keyboard accelerator
|
||||
coverage is complete. What still needs to land: HC-mode
|
||||
coverage for the scrub-track / notches / WIN MOVE marker
|
||||
(they render via `BackgroundColor` so the
|
||||
`HighContrastBorder` marker doesn't apply — needs a
|
||||
settings-aware paint), continuous scrub on key-held ← / →
|
||||
(vs single-step), then the bigger pieces — a move-log
|
||||
scroller and a mini-tableau preview — both screen-
|
||||
takeover-only pieces that need a much larger banner height
|
||||
grow (effectively the takeover container itself).
|
||||
Multi-session.
|
||||
- *Replay-overlay screen-takeover redesign — closed 2026-05-08
|
||||
across 13 commits (v0.21.4–v0.21.7).* The full mockup
|
||||
(`docs/ui-mockups/replay-overlay-mobile.html`) has shipped:
|
||||
banner chrome (v0.21.0), floating MOVE chip (v0.21.2), WIN
|
||||
MOVE scrub-bar marker (post-v0.21.3), playback controls /
|
||||
Space accelerator (post-v0.21.3), scrub notches + labels +
|
||||
keybind footer + ESC / ← / → accelerators + HC border
|
||||
(v0.21.5), Move Log panel + HC scrub track + continuous
|
||||
scrub (v0.21.6), and full-screen 50 % opacity dim layer
|
||||
(v0.21.7). Every major B-2 sub-piece is now closed. The
|
||||
only remaining items are minor polish: notch-label centering
|
||||
and WIN MOVE HC contrast bump (see Open next-step menu).*
|
||||
- *Floating `MOVE N/M` chip above the focused card during
|
||||
playback — closed 2026-05-08 by `2fb2d63`.* World-space
|
||||
`Text2d` entity sibling to the banner overlay; uses the same
|
||||
@@ -296,25 +246,22 @@ into a v0.21.1 / v0.22.0 cut.
|
||||
```
|
||||
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||
Working directory: <Rusty_Solitaire clone path on this machine>.
|
||||
Branch: master. v0.21.5 is tagged at a2432df (cut 2026-05-08, a
|
||||
patch release rolling up replay-overlay scrubbing affordances +
|
||||
accessibility: scrub-bar notches with percentage labels, keybind-
|
||||
hint footer, ESC + ← / → keyboard accelerators, and HC-mode
|
||||
coverage for the footer top border). v0.21.4 stays at 23ff62c,
|
||||
v0.21.3 at 3d92a91, v0.21.2 at f23df3b, v0.21.1 at daa655a,
|
||||
v0.21.0 at 04f9bf9. Working tree clean. See CHANGELOG.md §
|
||||
[0.21.5] for full detail.
|
||||
Branch: master. v0.21.7 is tagged at da3e542 (cut 2026-05-08,
|
||||
closes the last major B-2 sub-piece: full-screen tableau dim
|
||||
layer — 50 % opacity black UI scrim at z=54 that darkens the
|
||||
card world during replay so the chrome reads clearly above it).
|
||||
v0.21.6 stays at f63db76, v0.21.5 at a2432df, v0.21.4 at
|
||||
23ff62c, v0.21.3 at 3d92a91, v0.21.2 at f23df3b, v0.21.1 at
|
||||
daa655a, v0.21.0 at 04f9bf9. Working tree clean (CHANGELOG +
|
||||
SESSION_HANDOFF docs ride on top of da3e542; push pending).
|
||||
See CHANGELOG.md § [0.21.7] for full detail.
|
||||
|
||||
State: HEAD locally — see `git rev-parse HEAD`. The cut commit
|
||||
is a2432df; any post-cut docs edits ride on top of that.
|
||||
Workspace tests: 1250 total / 1249 passing / 1 pre-existing
|
||||
time-dependent flake (`daily_challenge` warning, fails when UTC
|
||||
is within 30 min of midnight; verified not introduced by recent
|
||||
work). Clippy clean.
|
||||
State: HEAD locally — see `git rev-parse HEAD`. Workspace
|
||||
tests: 1275 passing / 0 failing. Clippy clean.
|
||||
|
||||
READ FIRST (in order, before doing anything):
|
||||
1. SESSION_HANDOFF.md — this file
|
||||
2. CHANGELOG.md — [0.21.5] section is the most recent cut
|
||||
2. CHANGELOG.md — [0.21.6] section is the most recent cut
|
||||
3. CLAUDE.md — unified-3.0 rule set
|
||||
4. CLAUDE_SPEC.md — formal architecture spec
|
||||
5. ARCHITECTURE.md — crate responsibilities + data flow
|
||||
@@ -334,30 +281,15 @@ DECISION TO ASK THE PLAYER FIRST:
|
||||
tests can't catch. Likely surfaces JNI ClipboardManager
|
||||
and Android Keystore stubs that need real bridges. Larger
|
||||
scope; needs an Android device or emulator running.
|
||||
B. Replay-overlay screen-takeover redesign — multi-session
|
||||
work. v0.21.4 shipped WIN MOVE marker, pause / resume /
|
||||
step + Space accelerator, plus the floating-MOVE-chip
|
||||
piece from v0.21.2 (`2fb2d63`). v0.21.5 shipped scrub
|
||||
notches + percentage labels + keybind-hint footer + ESC
|
||||
and ← / → accelerators + HC marker for the footer top
|
||||
border (six commits across CHANGELOG § [0.21.5]). Banner
|
||||
height grew 60 → 76 → 92 px across two layout-changing
|
||||
commits in v0.21.5; geometry is now mutable. Keyboard
|
||||
accelerator coverage is complete. Natural next finite
|
||||
steps:
|
||||
1. **HC-mode coverage** for the scrub-track / notches /
|
||||
WIN MOVE marker (render via `BackgroundColor` not
|
||||
`BorderColor`, so `HighContrastBorder` doesn't apply
|
||||
— needs a settings-aware paint, precedent
|
||||
`radial_rim_outline`). Smallest next commit.
|
||||
2. **Continuous scrub on key-held ← / →** instead of
|
||||
single-step. Needs a key-held event source. Matches
|
||||
the mockup's `[← →] scrub` terminology.
|
||||
3. **Move-log scroller / mini-tableau preview** — both
|
||||
need a much larger banner-height grow (effectively
|
||||
the takeover container itself). Multi-session arcs
|
||||
that close B-2.
|
||||
Mockup at `docs/ui-mockups/replay-overlay-mobile.html`.
|
||||
B. Replay-overlay polish (B-2 arc fully closed in v0.21.7).
|
||||
All 13 planned sub-pieces shipped. Remaining items are
|
||||
minor polish: (a) scrub-bar notch-label centering — middle
|
||||
three labels sit slightly right-of-notch due to Bevy 0.18
|
||||
lacking `translate-x: -50%`; tiny commit, needs visual
|
||||
review. (b) WIN MOVE marker HC contrast bump — optional
|
||||
luminance boost under HC mode. Both are single commits
|
||||
requiring visual review; recommend treating as a v0.21.8
|
||||
polish pass after manual testing.
|
||||
C. Phase 8 (sync) — local storage scaffolding, self-hosted
|
||||
Axum server, `SolitaireServerClient` impl, GPGS stub
|
||||
wired into Settings. The biggest open arc by scope; rolls
|
||||
|
||||
@@ -38,8 +38,8 @@ use solitaire_data::ReplayMove;
|
||||
use crate::ui_modal::{spawn_modal_button, ButtonVariant};
|
||||
use crate::ui_theme::{
|
||||
ACCENT_PRIMARY, BG_ELEVATED_HI, BORDER_SUBTLE, HighContrastBackground, HighContrastBorder,
|
||||
STATE_SUCCESS, TEXT_PRIMARY, TEXT_PRIMARY_HC, TEXT_SECONDARY, TYPE_BODY, TYPE_CAPTION,
|
||||
TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY,
|
||||
STATE_SUCCESS, STATE_SUCCESS_HC, TEXT_PRIMARY, TEXT_PRIMARY_HC, TEXT_SECONDARY, TYPE_BODY,
|
||||
TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -60,6 +60,23 @@ use crate::ui_theme::{
|
||||
/// we materialise a separate constant rather than reuse the `f32` value.
|
||||
pub const Z_REPLAY_OVERLAY: i32 = Z_DROP_OVERLAY as i32 + 5;
|
||||
|
||||
/// `bevy::ui` `ZIndex` for the full-screen tableau dim layer.
|
||||
///
|
||||
/// One rung below [`Z_REPLAY_OVERLAY`] (= 54) so the replay chrome
|
||||
/// (banner + move-log panel) renders clearly on top while the dim scrim
|
||||
/// darkens the card world beneath it. World-space sprites (cards,
|
||||
/// badges, drop-target overlays) are always below any UI node regardless
|
||||
/// of their Transform.z — the dim layer doesn't need to know their z
|
||||
/// values.
|
||||
const Z_REPLAY_DIM: i32 = Z_REPLAY_OVERLAY - 1;
|
||||
|
||||
/// Alpha for the tableau dim layer — 50 % opacity black. Dark enough
|
||||
/// to visually separate the gameplay scene from the replay chrome
|
||||
/// above it; light enough that card positions remain legible through
|
||||
/// the scrim. Matches the mockup's "Game Peek Band at 50 % opacity"
|
||||
/// spec in `docs/ui-mockups/replay-overlay-mobile.html`.
|
||||
const TABLEAU_DIM_ALPHA: f32 = 0.5;
|
||||
|
||||
/// Total height of the banner in pixels. Thin enough to leave the
|
||||
/// gameplay surface visible underneath, tall enough to comfortably fit
|
||||
/// the headline-sized "▌ replay" label stacked above the
|
||||
@@ -88,6 +105,21 @@ const SCRUB_LABEL_ROW_HEIGHT: f32 = 16.0;
|
||||
/// (12 px) + 4 px breathing room.
|
||||
const KEYBIND_FOOTER_HEIGHT: f32 = 16.0;
|
||||
|
||||
/// Fixed pixel width of the centred scrub-bar notch-label container.
|
||||
/// Wide enough to hold the widest label ("100%" at 4 chars) while
|
||||
/// narrower than the 25 % gap between adjacent notches (≈ banner_w
|
||||
/// × 0.25; on a 320 px banner that's 80 px). A 36 px container
|
||||
/// leaves ≥ 44 px of clearance on each side at the narrowest common
|
||||
/// screen width.
|
||||
///
|
||||
/// Container width drives the `margin.left = -width / 2` centering
|
||||
/// trick: the container's left edge is placed at `left: Percent(pct)`
|
||||
/// and then shifted left by half its own width, so the container's
|
||||
/// centre coincides with the notch line. `Justify::Center` then
|
||||
/// renders the text centred within the container. This is the
|
||||
/// CSS `translateX(-50%)` pattern adapted for Bevy 0.18 UI.
|
||||
const SCRUB_LABEL_CENTER_WIDTH: f32 = 36.0;
|
||||
|
||||
/// How long a held arrow key waits before firing the next repeat
|
||||
/// step. 100 ms = 10 steps/sec — fast enough to scrub through a
|
||||
/// hundred-move replay in ~10 seconds while held, slow enough that
|
||||
@@ -189,6 +221,18 @@ pub struct ReplayPauseButton;
|
||||
#[derive(Component, Debug)]
|
||||
pub struct ReplayStepButton;
|
||||
|
||||
/// Marker on the full-screen tableau dim layer spawned at the start of
|
||||
/// every replay. The dim layer is a 100 % × 100 % `Node` at
|
||||
/// [`Z_REPLAY_DIM`] (= `Z_REPLAY_OVERLAY - 1`) with a semi-transparent
|
||||
/// black `BackgroundColor`. It darkens the card world so the replay
|
||||
/// chrome reads clearly against it without obscuring card positions.
|
||||
///
|
||||
/// Carries no [`Interaction`] component — purely visual; pointer events
|
||||
/// pass through to the underlying UI and world-space systems.
|
||||
/// Despawned by `react_to_state_change` when the replay ends.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct ReplayTableauDimLayer;
|
||||
|
||||
/// Marker on the small caption sitting below the "▌ replay"
|
||||
/// headline. Carries `GAME #YYYY-DDD` (year + chrono ordinal) while a
|
||||
/// replay is playing — a compact, monotonically-increasing identifier
|
||||
@@ -435,6 +479,7 @@ fn react_to_state_change(
|
||||
existing: Query<Entity, With<ReplayOverlayRoot>>,
|
||||
floating_chips: Query<Entity, With<ReplayFloatingProgressChip>>,
|
||||
move_log_panels: Query<Entity, With<ReplayOverlayMoveLogPanel>>,
|
||||
dim_layers: Query<Entity, With<ReplayTableauDimLayer>>,
|
||||
font_res: Option<Res<FontResource>>,
|
||||
) {
|
||||
if !state.is_changed() {
|
||||
@@ -463,6 +508,11 @@ fn react_to_state_change(
|
||||
for entity in &move_log_panels {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
// Tableau dim layer is also a separate root entity — same
|
||||
// pattern as the move-log panel.
|
||||
for entity in &dim_layers {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
// The `should_be_visible && already_spawned` branch is a no-op here —
|
||||
// the per-frame text update systems below repaint the banner label
|
||||
@@ -504,6 +554,27 @@ fn spawn_overlay(
|
||||
};
|
||||
let progress_label = format_progress(state);
|
||||
|
||||
// Tableau dim layer — full-screen scrim at z = Z_REPLAY_DIM (= 54).
|
||||
// Spawned first so it sits behind the banner (z=55) and move-log (z=55)
|
||||
// in the UI stacking context. World-space sprites (cards, badges) are
|
||||
// always below any UI node, so the dim layer darkens the entire
|
||||
// gameplay scene without needing to touch card_plugin. No Interaction
|
||||
// component — purely visual.
|
||||
commands.spawn((
|
||||
ReplayTableauDimLayer,
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(0.0),
|
||||
top: Val::Px(0.0),
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, TABLEAU_DIM_ALPHA)),
|
||||
ZIndex(Z_REPLAY_DIM),
|
||||
GlobalZIndex(Z_REPLAY_DIM),
|
||||
));
|
||||
|
||||
let banner_bg = Color::srgba(
|
||||
BG_ELEVATED_HI.to_srgba().red,
|
||||
BG_ELEVATED_HI.to_srgba().green,
|
||||
@@ -708,6 +779,11 @@ fn spawn_overlay(
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(STATE_SUCCESS),
|
||||
// HC bump: lime → brighter lime so the win
|
||||
// marker reads clearly above the bumped
|
||||
// notch ticks (BORDER_SUBTLE_HC gray) under
|
||||
// high-contrast mode.
|
||||
HighContrastBackground::with_hc(STATE_SUCCESS, STATE_SUCCESS_HC),
|
||||
));
|
||||
}
|
||||
// Fixed quarter-mark notches: five 1px vertical
|
||||
@@ -766,45 +842,63 @@ fn spawn_overlay(
|
||||
labels.iter().zip(positions.iter()).enumerate()
|
||||
{
|
||||
// Endpoints flush to the row's edges; middle
|
||||
// three labels anchor at their percentage.
|
||||
// `i == 0` → flush left (`left: 0`), so the
|
||||
// "0%" caption doesn't get clipped at the
|
||||
// left edge. `i == last` → flush right
|
||||
// (`right: 0`) so "100%" doesn't overflow
|
||||
// the banner. Bevy 0.18 UI has no clean
|
||||
// CSS-style `translate-x: -50%` centering,
|
||||
// so the middle three labels sit slightly
|
||||
// right-of-notch — visually subtle at this
|
||||
// font size; explicit polish target if
|
||||
// anyone notices.
|
||||
let mut node = Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(2.0),
|
||||
..default()
|
||||
};
|
||||
if i == 0 {
|
||||
node.left = Val::Px(0.0);
|
||||
// three labels use the `translateX(-50%)`
|
||||
// pattern for Bevy 0.18 UI: a fixed-width
|
||||
// container is placed at `left: Percent(pct)`
|
||||
// then shifted left by half its own width via
|
||||
// `margin.left: Px(-SCRUB_LABEL_CENTER_WIDTH/2)`.
|
||||
// `Justify::Center` renders the text centred
|
||||
// within the container so the text's visual
|
||||
// centre coincides with the notch line.
|
||||
let (node, justify) = if i == 0 {
|
||||
(
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(2.0),
|
||||
left: Val::Px(0.0),
|
||||
..default()
|
||||
},
|
||||
Justify::Left,
|
||||
)
|
||||
} else if i == labels.len() - 1 {
|
||||
node.right = Val::Px(0.0);
|
||||
(
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(2.0),
|
||||
right: Val::Px(0.0),
|
||||
..default()
|
||||
},
|
||||
Justify::Right,
|
||||
)
|
||||
} else {
|
||||
node.left = Val::Percent(*pct);
|
||||
}
|
||||
(
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
top: Val::Px(2.0),
|
||||
left: Val::Percent(*pct),
|
||||
width: Val::Px(SCRUB_LABEL_CENTER_WIDTH),
|
||||
margin: UiRect {
|
||||
left: Val::Px(-SCRUB_LABEL_CENTER_WIDTH / 2.0),
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
Justify::Center,
|
||||
)
|
||||
};
|
||||
row.spawn((
|
||||
ReplayOverlayScrubNotchLabel,
|
||||
node,
|
||||
Text::new(*label),
|
||||
TextLayout::new_with_justify(justify),
|
||||
TextFont {
|
||||
font: font_handle_for_labels.clone(),
|
||||
font_size: TYPE_CAPTION,
|
||||
..default()
|
||||
},
|
||||
// The mockup's `text-outline` (BORDER_SUBTLE)
|
||||
// would match the notches but reads as too
|
||||
// low-contrast against `BG_ELEVATED_HI` for
|
||||
// the labels to actually be legible at 12 px.
|
||||
// TEXT_SECONDARY keeps the subdued visual
|
||||
// hierarchy (caption, not headline) while
|
||||
// staying readable.
|
||||
// staying readable against BG_ELEVATED_HI.
|
||||
TextColor(TEXT_SECONDARY),
|
||||
));
|
||||
}
|
||||
@@ -2260,6 +2354,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The WIN MOVE marker carries `HighContrastBackground::with_hc(
|
||||
/// STATE_SUCCESS, STATE_SUCCESS_HC)` so the lime bumps to brighter
|
||||
/// lime under HC mode rather than to a neutral gray. Pin the
|
||||
/// presence of the marker so a future refactor can't accidentally
|
||||
/// drop it and silently regress HC legibility.
|
||||
#[test]
|
||||
fn win_move_marker_carries_hc_background_marker() {
|
||||
let mut app = headless_app();
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(8).with_win_move_index(Some(7)),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
);
|
||||
app.update();
|
||||
|
||||
let mut q = app
|
||||
.world_mut()
|
||||
.query_filtered::<&HighContrastBackground, With<ReplayOverlayWinMoveMarker>>();
|
||||
let marker = q
|
||||
.iter(app.world())
|
||||
.next()
|
||||
.expect("WIN MOVE marker must carry HighContrastBackground");
|
||||
assert_eq!(
|
||||
marker.default_color,
|
||||
STATE_SUCCESS,
|
||||
"default colour must be STATE_SUCCESS"
|
||||
);
|
||||
assert_eq!(
|
||||
marker.hc_color,
|
||||
STATE_SUCCESS_HC,
|
||||
"HC colour must be STATE_SUCCESS_HC (brighter lime, not gray)"
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// scrub_notch_positions + ReplayOverlayScrubNotch spawn behaviour
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -3798,4 +3930,65 @@ mod tests {
|
||||
other => panic!("expected Playing, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The tableau dim layer spawns alongside the banner when playback
|
||||
/// starts and despawns when the replay ends. Mirrors
|
||||
/// `floating_chip_spawns_and_despawns_with_overlay` for the dim layer.
|
||||
#[test]
|
||||
fn dim_layer_spawns_and_despawns_with_overlay() {
|
||||
let mut app = headless_app();
|
||||
|
||||
// Inactive → no dim layer yet.
|
||||
app.update();
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&ReplayTableauDimLayer>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
0,
|
||||
"no dim layer while playback is Inactive",
|
||||
);
|
||||
|
||||
set_state(
|
||||
&mut app,
|
||||
ReplayPlaybackState::Playing {
|
||||
replay: synthetic_replay(5),
|
||||
cursor: 0,
|
||||
secs_to_next: 0.5,
|
||||
paused: false,
|
||||
},
|
||||
);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&ReplayTableauDimLayer>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
1,
|
||||
"dim layer must spawn when playback starts",
|
||||
);
|
||||
|
||||
set_state(&mut app, ReplayPlaybackState::Inactive);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
app.world_mut()
|
||||
.query::<&ReplayTableauDimLayer>()
|
||||
.iter(app.world())
|
||||
.count(),
|
||||
0,
|
||||
"dim layer must despawn when playback ends",
|
||||
);
|
||||
}
|
||||
|
||||
/// The dim layer is a full-screen node (100 % × 100 %) at a lower
|
||||
/// z-index than the replay chrome (z = Z_REPLAY_DIM < Z_REPLAY_OVERLAY).
|
||||
/// Lock the z-ordering so a future refactor of the z constants can't
|
||||
/// silently flip the intended stacking.
|
||||
#[test]
|
||||
fn dim_layer_z_is_below_replay_chrome() {
|
||||
assert!(
|
||||
Z_REPLAY_DIM < Z_REPLAY_OVERLAY,
|
||||
"dim layer (z={Z_REPLAY_DIM}) must be below replay chrome (z={Z_REPLAY_OVERLAY})",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,7 +701,7 @@ pub(crate) fn update_high_contrast_backgrounds(
|
||||
let high_contrast = settings.0.high_contrast_mode;
|
||||
for (marker, mut bg) in backgrounds.iter_mut() {
|
||||
let target = if high_contrast {
|
||||
BORDER_SUBTLE_HC
|
||||
marker.hc_color
|
||||
} else {
|
||||
marker.default_color
|
||||
};
|
||||
|
||||
@@ -93,6 +93,13 @@ pub const ACCENT_SECONDARY: Color = Color::srgb(0.882, 0.639, 0.933);
|
||||
/// from base16-eighties. `#acc267`.
|
||||
pub const STATE_SUCCESS: Color = Color::srgb(0.675, 0.761, 0.404);
|
||||
|
||||
/// High-contrast variant of [`STATE_SUCCESS`] — `#c8e862`. Brighter
|
||||
/// lime that maintains the success hue while lifting luminance from
|
||||
/// ~0.51 → ~0.73 so the WIN MOVE scrub-bar marker stands out from
|
||||
/// the bumped notch ticks (`BORDER_SUBTLE_HC` `#a0a0a0`, L≈0.60) in
|
||||
/// high-contrast mode.
|
||||
pub const STATE_SUCCESS_HC: Color = Color::srgb(0.784, 0.910, 0.384);
|
||||
|
||||
/// Warning — penalty signal, daily-seed expiry countdown, sync-pending
|
||||
/// status. Gold from base16-eighties. **Both** Undo and Recycle
|
||||
/// counters use this when non-zero. `#ddb26f`.
|
||||
@@ -260,24 +267,45 @@ impl HighContrastBorder {
|
||||
/// often render as tiny full-bleed `Node`s, not as borders, so the
|
||||
/// border-marker pattern doesn't apply.
|
||||
///
|
||||
/// `default_color` records the off-state colour the entity was
|
||||
/// spawned with so the system can revert when HC is toggled back
|
||||
/// off. The accompanying paint system is
|
||||
/// [`update_high_contrast_backgrounds`](crate::settings_plugin::update_high_contrast_backgrounds).
|
||||
/// `default_color` records the off-state colour; `hc_color` the on-
|
||||
/// state colour. [`with_default`] fills `hc_color` with
|
||||
/// [`BORDER_SUBTLE_HC`] so the 90 % of sites that just need the
|
||||
/// standard subtle-border bump can continue using a one-argument
|
||||
/// constructor. [`with_hc`] overrides the HC colour for the rare
|
||||
/// site (currently only the WIN MOVE scrub-bar marker) that needs a
|
||||
/// domain-specific HC variant (`STATE_SUCCESS_HC` instead of a gray).
|
||||
///
|
||||
/// [`with_default`]: HighContrastBackground::with_default
|
||||
/// [`with_hc`]: HighContrastBackground::with_hc
|
||||
/// [`BackgroundColor`]: bevy::prelude::BackgroundColor
|
||||
#[derive(bevy::prelude::Component, Debug, Clone, Copy)]
|
||||
pub struct HighContrastBackground {
|
||||
/// Background colour to use when high-contrast mode is *off* —
|
||||
/// the site's normal idle / active-state colour.
|
||||
pub default_color: bevy::prelude::Color,
|
||||
/// Background colour to use when high-contrast mode is *on*.
|
||||
/// Defaults to [`BORDER_SUBTLE_HC`] via [`with_default`].
|
||||
///
|
||||
/// [`with_default`]: HighContrastBackground::with_default
|
||||
pub hc_color: bevy::prelude::Color,
|
||||
}
|
||||
|
||||
impl HighContrastBackground {
|
||||
/// Convenience constructor —
|
||||
/// `HighContrastBackground::with_default(BORDER_SUBTLE)`.
|
||||
/// Convenience constructor — HC colour defaults to
|
||||
/// [`BORDER_SUBTLE_HC`].
|
||||
pub const fn with_default(default_color: bevy::prelude::Color) -> Self {
|
||||
Self { default_color }
|
||||
Self { default_color, hc_color: BORDER_SUBTLE_HC }
|
||||
}
|
||||
|
||||
/// Constructor for sites whose HC colour differs from the standard
|
||||
/// [`BORDER_SUBTLE_HC`]. Currently used by the WIN MOVE scrub-bar
|
||||
/// marker which bumps `STATE_SUCCESS` → `STATE_SUCCESS_HC` rather
|
||||
/// than to a neutral gray.
|
||||
pub const fn with_hc(
|
||||
default_color: bevy::prelude::Color,
|
||||
hc_color: bevy::prelude::Color,
|
||||
) -> Self {
|
||||
Self { default_color, hc_color }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user