f23df3b8053094f406d6e7f843e66c7ee29964b0
280 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
a292a7ead0 |
feat(engine): swap ACCENT_PRIMARY from cyan #6fc2ef to brick red #a54242
Project-wide palette shift at user request. Replaces the cyan primary accent everywhere it surfaces — splash boot screen, home menu glyphs, action chevrons, replay overlay banner + scrub fill + chip border, achievement checkmarks, leaderboard #1 indicator, radial menu fill, focus ring, card-back canonical badge, etc. — with `#a54242` from the same base16-eighties family as the existing pink suit colour. Knock-on changes that all land in this commit per the lockstep rule: - ui_theme.rs: ACCENT_PRIMARY (#a54242), ACCENT_PRIMARY_HOVER (#c25e5e brightened companion), FOCUS_RING (same hue, 0.85 alpha). Module-level palette comment + STOCK_BADGE_FG + CARD_SHADOW_ALPHA_DRAG doc strings updated to match. - card_plugin.rs: card_back_colour(0) now returns the brick-red ACCENT_PRIMARY (was cyan). RED_SUIT_COLOUR_CBM swapped from cyan to lime #acc267 — the CBM alternative needs to stay hue-distinct from the new red-family primary, lime is the next-best non-red base16-eighties accent. text_colour doc + CBM tests renamed cyan→lime in lockstep (text_colour_color_blind_mode_swaps_red_suits_to_lime). - card_face_svg.rs: BACK_ACCENTS[0] now "#a54242" (canonical Terminal back). - splash_plugin.rs / ui_modal.rs / replay_overlay.rs / selection_plugin.rs: descriptive "cyan" comments swapped to "accent" / "primary-accent" wording so the doc strings stay decoupled from any specific hue. Future palette tweaks won't require comment churn. - design-system.md: YAML token frontmatter updated (primary, surface-tint, suit-red-cb, primary-container, on-primary-container, inverse-primary). Palette table gains a project-specific `base08` slot for the new red. CTA / Selection / Card-back badge / Primary button / Bottom-bar active-icon / glow / CBM swap text all retuned. Historical references preserved (e.g. "Was cyan #6fc2ef before the 2026-05-08 swap") so the audit trail stays in the spec. - card_face_svg_pin.rs: rebaselined. Exactly one hash drift (back_0 — the canonical Terminal back's badge changed colour). Other 56 hashes identical (face SVGs don't reference the accent; back_1..4 use unchanged accents). The one-hash-drift signal confirms the change scope was surgical. Workspace clippy + cargo test --workspace clean, 1184 passing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
dd101b3d54 |
fix(engine): render bottom-right card glyph upright (no 180° rotation)
The user noticed the bottom-right large suit glyphs were rendering upside-down — point-up hearts, stem-up spades — because the SVG transform pipeline applied a `rotate(180)` to match the traditional playing-card inverted-corner convention. That convention exists so a card reads correctly when flipped or read from the opposite side of the table. Single-orientation digital play doesn't benefit from it; most modern digital decks have abandoned it. User preference is upright. Drops the rotate from face_svg's bottom-right `<g transform>` and adjusts the translate so the visible glyph still lands at (178, 286)–(242, 350) — same screen footprint, same scale, just no flip. design-system.md § Game Cards updated in lockstep — line 220 no longer says "rotated 180°", instead documents the deliberate deviation from the traditional convention. Knock-on lockstep changes in this commit: - EXPECTED in tests/card_face_svg_pin.rs rebaselined: 52 face hashes shift, 5 back hashes unchanged. - assets/cards/faces/*.png regenerated (52 face PNGs). - solitaire_engine/assets/themes/default/*_*.svg regenerated (52 theme face SVGs that production rasterises at startup). Workspace clippy + cargo test --workspace clean. Pin test passes against the new hashes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
af414b6aed |
fix(engine): render card suit glyphs as SVG paths instead of text
The user's first post-migration screenshot showed near-invisible suit glyphs on every card — the rank rendered at correct size but the ♠ ♥ ♦ ♣ marks were tiny dots regardless of the requested 20px / 64px font-size. Root cause: the bundled FiraMono in svg_loader::shared_fontdb doesn't carry usable Unicode suit glyphs (U+2660-2666). usvg silently fell back to a substitute rendering at default size, producing the "tofu" effect. Fixes by replacing the `<text>` glyph rendering with inline SVG paths. `suit_path_d(suit)` returns a single closed-perimeter path authored in a 32 × 32 logical box, then face_svg wraps it in two `<g transform>` blocks (top-left small + bottom-right rotated large). Path-based rendering bypasses the font system entirely — same bytes on every machine, no fontdb dependency, no substitution risk. Same path data renders correctly whether filled (♥ ♠) or outlined (♦ ♣ — the always-on color-blind glyph differentiation from the design system). Knock-on changes that must land in this commit per the migration plan's lockstep rule: - `EXPECTED` in tests/card_face_svg_pin.rs rebaselined: 52 face hashes change (text → path), 5 back hashes unchanged (back_svg untouched). The bootstrap pattern in the test handled the rebaseline cleanly — empty EXPECTED, re-run, paste, re-run. - assets/cards/faces/*.png regenerated (the 52 face PNGs). - solitaire_engine/assets/themes/default/*_*.svg regenerated (the 52 theme face SVGs that production rasterises at startup). Both rendering paths must agree. Workspace clippy + cargo test --workspace clean. Pin test passes against the new hashes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ae84dc1504 |
fix(engine): clear top-bar overlap by aligning action buttons to TYPE_BODY
The post-Option-D screenshot showed the left-anchored HUD column
("Score: 0 Moves: 0 0:00") and the right-anchored action button
row colliding mid-screen at portrait/narrow window widths. Both
were absolute-positioned siblings without a shared flex parent,
so Bevy 0.15's UI couldn't auto-arrange them when their natural
widths exceeded the available horizontal space.
The action button text was a hardcoded `font_size: 16.0` literal
— a miss from the typography migration audit, since every other
text element in `hud_plugin.rs` already routes through the
`TYPE_*` tokens. Switching to `TYPE_BODY` (14.0) brings the
button row in line with the design system *and* trims roughly
12% off label widths.
Pairs with a horizontal-padding cut from VAL_SPACE_3 to
VAL_SPACE_2: 8px less on each side, six buttons, ~96px total
reclaimed across the row. Vertical padding stays at VAL_SPACE_2
so button height tracks the rest of the chrome band.
Combined effect: the action button row narrows by ~150-200px,
which is enough margin to clear typical portrait window widths
without requiring a structural refactor (a shared SpaceBetween
flex parent for HUD+actions would be more robust but touches
many query sites and was out of scope for the visual-polish
pass).
cargo clippy + cargo test --workspace clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
8719f77ec2 |
fix(engine): regenerate table backgrounds to flat Terminal palette
The post-Option-D screenshot showed Terminal cards correctly but a green felt play surface — the chrome migration only retuned in-engine constants, leaving the on-disk PNGs at assets/backgrounds/bg_*.png as the legacy felt textures. Adds solitaire_engine/examples/background_generator.rs following the same regeneratable pattern as card_face_generator. Five solid near-black variants from the base16-eighties palette: - bg_0: #151515 (Terminal canonical, BG_PRIMARY) - bg_1: #0a0a0a (BG_DEEPEST) - bg_2: #1a1a1a (BG_ELEVATED — same as card face) - bg_3: #121820 (slight cool tint) - bg_4: #201812 (slight warm tint) Per design-system.md the Terminal play surface is *flat* — no felt, no gradient — so all 5 slots are pure solid colours. Each PNG is 120 × 168 (matches the legacy tile size; spawn_background stretches to window_size * 2.0 at runtime so source resolution is immaterial). On-disk weight drops from ~16KB average to ~100 bytes per tile. Run with: cargo run --example background_generator --release Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a14200ac2f |
fix(engine): regenerate default theme SVGs to Terminal aesthetic
Step 4's PNG regeneration left the cards looking unchanged at
runtime because the PNGs at assets/cards/ are only the *fallback*
art — production renders the bundled-default theme's SVGs, which
get include_bytes!()-embedded into the binary by
solitaire_engine::assets::sources and applied to CardImageSet at
startup by theme::plugin::apply_theme_to_card_image_set. Those
SVGs were still the legacy vector-playing-cards art.
Extends card_face_generator to write SVGs into both runtime
paths in lockstep:
1. assets/cards/{faces,backs}/*.png — fallback art (unchanged
from step 4).
2. solitaire_engine/assets/themes/default/*.svg — what production
actually renders. 52 face SVGs + 1 back SVG, generated from
the same face_svg / back_svg builders as the PNGs so the two
paths can never visually diverge.
Adds two helper functions to card_face_svg:
- theme_suit_token (clubs/diamonds/hearts/spades — lowercase
full word, matching CardKey::manifest_name)
- theme_rank_token (ace/2..10/jack/queen/king — same)
The theme back uses BACK_ACCENTS[0] (canonical Terminal cyan).
The other four accents only live as PNG fallbacks because the
theme system carries one back per theme.
Net SVG diff: -14884 / +940 lines — the legacy vector-playing-
cards SVGs were ~300 lines each of Inkscape-authored paths;
the Terminal SVGs are ~10 lines of programmatic output.
Workspace clippy + cargo test --workspace clean. Pin test
unaffected (the SVG builders themselves did not change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e8bf9d79da |
feat(engine): migrate cards to Terminal aesthetic — artwork + constants
Step 4+5 lockstep commit closing Option D from SESSION_HANDOFF. The 52 face PNGs + 5 back PNGs in assets/cards/ are regenerated to the Terminal-aesthetic artwork emitted by the card_face_generator example (#1a1a1a face, #fb9fb1 / #d0d0d0 suit glyphs, scanline-pattern backs with palette-rotated badge accents). Resolution drops from 512×768 to 256×384 — sufficient for ~250 px-wide desktop sprites and ~⅓ the on-disk weight. Constant fallback path migrated in lockstep so the constant-fallback tests (under MinimalPlugins) and the PNG path (production) agree at every commit boundary: - CARD_FACE_COLOUR → #1a1a1a (was off-white #fafaf2) - RED_SUIT_COLOUR → #fb9fb1 (was #c71f26) - BLACK_SUIT_COLOUR → #d0d0d0 (was #141414) - CARD_FACE_COLOUR_RED_CBM → renamed to RED_SUIT_COLOUR_CBM, value #6fc2ef (was #d9ebff). Semantic shift: pre-Terminal this was a face-background tint, now it's a suit-glyph colour swap. The Terminal face is uniformly CARD_FACE_COLOUR regardless of CBM; CBM only swaps red suits to cyan in the glyph itself. - card_back_colour() → returns the 5 base16-eighties accents matching card_face_svg::BACK_ACCENTS in lockstep, so the test-fallback back is the same hue family as the on-disk PNG art for that index. Function signatures shift to follow the semantic move: - text_colour gains a color_blind: bool parameter (returns RED_SUIT_COLOUR_CBM for red+CBM). - face_colour deleted entirely. The face is uniform CARD_FACE_COLOUR; card_sprite inlines the constant. CBM parameter dropped from card_sprite as a knock-on. Test updates land in this commit per the migration plan: - text_colour_is_red_for_hearts_and_diamonds + sibling: pass `, false` to text_colour calls now that the signature has the CBM bool. - 4 face_colour CBM tests replaced with 2 text_colour CBM tests asserting (a) red-suit cards swap to cyan in CBM and (b) black-suit cards do not change. Engine test count: 747 → 745 (net -2 from the test consolidation — 4 face_colour tests collapsed into 2 text_colour CBM tests). Sign-off criteria: a human still needs to `cargo run -p solitaire_app` and confirm Terminal cards render. clippy + cargo test --workspace clean as of this commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
48b28d29f8 |
test(engine): pin card-face SVG output against rasteriser drift
Step 3 of the migration plan in docs/ui-mockups/card-face-migration.md. Extracts face_svg / back_svg + palette constants from the card_face_generator example into a new solitaire_engine::assets::card_face_svg module so an integration test can call them. The example becomes a thin wrapper. The new tests/card_face_svg_pin.rs hashes the raw RGBA8 pixel bytes from rasterising every face × suit + every back accent and compares each FNV-1a fingerprint against an embedded constant. Catches silent rendering drift if usvg / resvg / tiny_skia / the bundled FiraMono ever change in a way that perturbs pixels. Hashing is FNV-1a inline (~5 lines) rather than adding sha2 or blake3 — cryptographic strength isn't load-bearing here, just stable byte fingerprints. When the SVG builders intentionally change, empty EXPECTED to `&[]` and re-run the test once; it panics with the new hashes formatted as Rust source ready to paste back in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
babe5cc9c8 |
feat(engine): add full card-face SVG generator example
Generates 52 face PNGs (4 suits × 13 ranks) + 5 back PNGs into assets/cards/. Implements step 2 of the migration plan in docs/ui-mockups/card-face-migration.md — the bytes this emits are what step 4 commits alongside the card_plugin constant migration. Filled vs outlined glyphs (♥♠ filled; ♦♣ outlined) implement the always-on color-blind glyph differentiation from the design system. The 5 back themes share the canonical Terminal scanline pattern but rotate the badge accent through the base16-eighties palette so all 5 slots stay distinguishable without leaving the palette. Run with: cargo run --example card_face_generator --release Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3a4bb63a6f |
feat(engine): add card-face SVG generator PoC example
Rasterises one Ace of Spades to /tmp/ace_spades_terminal.png via the existing usvg + resvg + tiny_skia stack already used by svg_loader. Proves the per-card grain works before looping over all 52 faces + 5 backs in step 2 of the migration plan. Run with: cargo run --example card_face_poc --release Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a27cf5a020 |
feat(engine): add tiled scanline overlay to splash
Closes the second half of the splash polish arc deferred in
|
||
|
|
29136d815d |
feat(engine): add pulsing trailing cursor to splash "▌ ready_" line
Closes the cursor-pulse half of the splash polish arc deferred in
|
||
|
|
e080b49914 |
feat(engine): restyle replay progress text as Terminal MOVE chip
Closes the centre-text half of the replay-overlay enrichments arc. The plain "Move N of M" text becomes a 1px ACCENT_PRIMARY-bordered chip containing "MOVE N/M" — uppercase + slash separator reads as a Terminal output line and matches the floating-chip motif in docs/ui-mockups/replay-overlay-mobile.html. The chip lives in-banner rather than floating above the focused card; the screen-takeover treatment that requires plumbing cursor → card identity remains deferred per SESSION_HANDOFF. Implementation: the centre Text spawn is now wrapped in a Node with 1px border + axes(VAL_SPACE_2, VAL_SPACE_1) padding and no background fill (Terminal aesthetic gets depth from borders + tonal layering, not shadows). The ReplayOverlayProgressText marker stays on the inner Text so update_progress_text continues to repaint contents unchanged. format_progress now returns "MOVE N/M" for Playing and "REPLAY COMPLETE" for Completed (uppercase to match the chip's typographic treatment); Inactive still returns "" since the overlay shouldn't be spawned in that state. Used BorderColor::all(ACCENT_PRIMARY) — Bevy's BorderColor is per-side in 0.18, no longer the tuple struct it was earlier. Module-level docstring + ReplayOverlayScrubFill doc comment both updated to quote the new "MOVE N/M" string. Test overlay_progress_text_reflects_cursor swapped its assertion to match. 1182 tests still pass; clippy clean. This closes Option C from the SESSION_HANDOFF Resume prompt's banner- local enrichments. The full screen-takeover redesign (mini-tableau, playback controls, move-log scroll, WIN MOVE marker requiring a win_move_index field on Replay) remains the multi-session item. |
||
|
|
54005d5494 |
feat(engine): add GAME #YYYY-DDD caption beneath the replay headline
Adds the right-anchored game-identifier piece of the replay-overlay
mockup (docs/ui-mockups/replay-overlay-mobile.html), adapted to live
under the existing "▌ replay" headline rather than as a separate
top-bar surface — the screen-takeover redesign is intentionally
deferred per the SESSION_HANDOFF punch list.
The caption reads `GAME #{year}-{ordinal:03}` (e.g. `GAME #2026-122`
for a replay recorded 2026-05-02), matching the mockup's
`GAME #2024-127` motif. Year + chrono ordinal gives a compact,
monotonically-increasing identifier that's grep-friendly across
replay files. TYPE_CAPTION (11 px) / TEXT_SECONDARY paint so the
caption reads as subordinate metadata, not a callout.
Implementation: new ReplayOverlayGameCaption marker, new pure
helper `format_game_caption(state) -> Option<String>` (None for
Inactive / Completed since the replay is consumed in those branches),
left-side label spawn restructured into a column container holding
the headline + caption with a 2 px row gap. BANNER_HEIGHT bumped
48 → 60 px so the column fits without overflow (16 px vertical
padding + 1 px scrub + ~39 px content; +12 px banner mass is the
deliberate cost of the new content).
Two new tests (1180 → 1182): format_game_caption_covers_state_corners
pins the three branches (Inactive / Completed / Playing) plus the
zero-pad-to-3-digits invariant for early-January ordinals; and
overlay_game_caption_shows_replay_date drives ReplayPlaybackState
end-to-end and asserts the caption text on spawn and that the
overlay stays spawned through Playing → Completed.
MOVE chip restyle from the same mockup is the next commit.
|
||
|
|
6204db8bb1 |
feat(engine): port replay banner label to ▌ cursor-block treatment
Aligns the replay overlay's headline with the splash boot-screen idiom
landed in
|
||
|
|
c84d9f445c |
feat(engine): scrub fill bar + per-frame updater for replay overlay
Closes the spawn-time half of the replay-overlay redesign open in SESSION_HANDOFF.md by adding the 1px cyan scrub bar called for in docs/ui-mockups/replay-overlay-mobile.html. A track in BORDER_SUBTLE spans the bottom edge of the banner and the cyan ACCENT_PRIMARY fill mirrors cursor / total via a new ReplayOverlayScrubFill component + update_scrub_fill system. The pure scrub_pct helper is shared between the spawn path (initial fill width) and the per-frame updater so the first paint already reflects state instead of popping 0 → cursor on the first tick — same shape as the existing format_progress / update_progress_text split. Two new tests (1176 → 1178): scrub_pct_covers_state_corners pins the helper's four corners (Inactive / cursor=0 / midpoint / Completed) and overlay_scrub_fill_tracks_cursor drives ReplayPlaybackState end-to-end and asserts Node.width on the unique scrub-fill entity. Same change- detection guard as the text updaters, so an idle replay leaves the node untouched. Header text treatment, move-log scroll, MOVE chip, and WIN MOVE callout from the same mockup are still open — separate commits. |
||
|
|
cacb19c03f |
feat(engine): port the splash to the Terminal boot-screen treatment
Implements the full mockup-spec splash from
docs/ui-mockups/splash-mobile.html plus the desktop adaptation rules
from docs/ui-mockups/desktop-adaptation.md. The header (cursor block,
wordmark, divider, "TERMINAL EDITION" subtitle), boot log (three
✓ check rows + "▌ ready_"), progress bar (1px track with full-width
cyan fill + "DONE · 247 ASSETS" caption), and footer
(BASE16-EIGHTIES label, eight palette swatches, version) all land
together. Rules-driven sizing: boot-log column capped at 480 px on
desktop (otherwise 70 % viewport), progress bar capped at 720 px
(otherwise 80 %), per the desktop-adaptation spec.
Refactored the alpha-fade scaffold from per-marker queries
(SplashTitle / SplashSubtitle / SplashCursor) to a single
SplashFadable { base_color: Color } + SplashFadableBg variant.
~15 fadable elements now share one global query each; adding more
elements is one component-attach, not three new query types.
Skipped (each its own potential follow-up):
- Scanline overlay — needs a tiled-pattern asset or a custom
shader; both are out of scope for a UI-Node port.
- Pulsing cursor on the "ready_" line — would fight the global
fade timeline; stays static.
- "RUSTY SOLITAIRE" wordmark from the mockup — actual product is
"Solitaire Quest"; the mockup leaked the repo name.
Tests: 8 carried + 2 new (Terminal boot-screen content present;
fadables start transparent and reach full alpha).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9891ae4ba3 |
refactor(engine): final hint-highlight + replay-overlay token cleanup
- input_plugin's hint-source card tint moves from raw bright-yellow `srgba(1.0, 1.0, 0.4, 1.0)` to the design-system STATE_WARNING token, so the source card and the destination pile (which already uses STATE_WARNING via HINT_PILE_HIGHLIGHT_COLOUR) wear the same attention colour as a coherent pair. - replay_overlay had two stale doc comments referencing the old "loud yellow accent" — Primary is now cyan (ACCENT_PRIMARY). Comments updated; no behaviour change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
cdcaddaabe |
feat(engine): add Terminal cursor block to splash overlay
Splash now renders the design system's signature `▌` cyan terminal- cursor glyph (96px) above the wordmark, matching docs/ui-mockups/ splash-mobile.html. The cursor uses ACCENT_PRIMARY and fades on the same per-frame alpha schedule as the title and subtitle so the brand beat still dissolves as a single layer. Did NOT pull in the mockup's full boot-loader treatment (scanline overlay, ✓ check log lines, progress bar, ROOT@SOLITAIRE prompt) — those are aesthetic features that warrant their own commit, not this token-port pass. The splash already consumed every relevant ui_theme token; the cursor glyph is the single highest-signal visual element the spec called for. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d752870007 |
refactor(engine): migrate card_plugin chrome to Terminal tokens
- Drag-elevation shadow now sources its colour from CARD_SHADOW_COLOR + CARD_SHADOW_ALPHA_DRAG, so the Terminal "no box-shadow" policy disables the stack shadow in lockstep with the per-card shadows. Re-enabling shadows for a future palette swap is now a one-line edit in ui_theme, not a hunt across plugins. - RIGHT_CLICK_HIGHLIGHT_COLOUR retuned from raw `srgba(0.2, 0.8, 0.2, 0.6)` to STATE_SUCCESS's RGB at 60% alpha. Spelled as a literal because Alpha::with_alpha isn't const on stable; a new test pins the RGB to STATE_SUCCESS so a palette swap can't drift the two apart. - Drop the duplicated PILE_MARKER_DEFAULT_COLOUR const — import the promoted const from table_plugin instead. STOCK_NORMAL_COLOUR is now an alias of that const so all idle pile-marker tints track a single source of truth. - Stock recycle "↺" text changed from raw `srgba(1.0, 1.0, 1.0, 0.7)` to TEXT_PRIMARY at 0.7 alpha, picking up the off-white foreground used elsewhere in the Terminal UI. Card-face / suit / card-back palette constants are intentionally NOT migrated: the runtime path renders PNG artwork that's still on the previous "white card" palette, so swapping the fallback constants ahead of artwork regeneration would mix two visual systems for any code path where image loading fails. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1d1543e4bc |
test(engine): align card-shadow drag-vs-idle assertion with Terminal "no shadow" intent
Commit
|
||
|
|
651f4060e6 |
refactor(engine): migrate table_plugin chrome to Terminal tokens
- Promote `marker_colour` to module-level const PILE_MARKER_DEFAULT_COLOUR and re-export it. cursor_plugin::MARKER_DEFAULT now imports the const directly, replacing the prior duplicated literal kept in sync only by doc comment. Drift becomes a compile error instead of a stale claim. - Empty-tableau "K" placeholder text now uses TEXT_PRIMARY at 0.35 alpha (was raw `Color::srgba(1.0, 1.0, 1.0, 0.35)`) so it picks up the Terminal off-white foreground. - HINT_PILE_HIGHLIGHT_COLOUR retuned from bright `srgb(1.0, 0.85, 0.1)` to the design-system STATE_WARNING token (`#ddb26f`). Spelled as a literal because Alpha::with_alpha is not yet const on stable; a new test pins the RGB to STATE_WARNING so a palette swap can't drift the two apart silently. - The existing "is gold" character test was hardcoded to the old bright palette (red ≥ 0.9). Loosened to "warmer than cool" + ranges that the Terminal muted gold satisfies, with exact-RGB tracking handled by the new STATE_WARNING test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a1376075bd |
feat(engine): port toasts to the Terminal design-system spec
Toasts now follow `docs/ui-mockups/design-system.md`: - Bottom-anchored absolute position (was top / mid-screen) - Opaque BG_ELEVATED fill (was translucent black-at-alpha) - 1px accent border keyed off a new ToastVariant enum - TYPE_BODY_LG caption (was 22 / 32 px literals) - RADIUS_MD corners ToastVariant exposes Info / Warning / Error / Celebration, each mapped to its design-system token via border_color(). Variants are threaded through every spawn_toast call site: - Achievement / Level-up / XP / Daily / Weekly / Challenge → Celebration - Goal-announcement / Time-attack / Settings volume / Auto-complete → Info Queued banner and fire-and-forget toasts use slightly different bottom anchors (6% vs. 14%) so a celebration toast spawned in the same frame as a queued info banner layers above it instead of overlapping. Two new tests pin variant→border mapping to the design tokens and require all four borders to be visually distinct. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ceec4fc486 |
refactor(engine): route gameplay-feedback colours through Terminal tokens
Selection-highlight tints in selection_plugin and the valid-drop
marker tint in cursor_plugin were hand-tuned RGB literals from the
prior Premium-Solitaire palette. Migrate them to the semantic
state tokens introduced in ui_theme:
- keyboard-drag source highlight (picking) → ACCENT_PRIMARY
- keyboard-drag source highlight (lifted) → STATE_WARNING
- keyboard-drag destination highlight → STATE_SUCCESS
- cursor_plugin::MARKER_VALID → STATE_SUCCESS @ 0.55α
`MARKER_VALID` stays a Color literal (Alpha::with_alpha is not yet
const on stable); a new tracking test pins its RGB to STATE_SUCCESS
so a future palette swap can't drift the two apart silently.
Also fix three stale doc comments in ui_modal that still described
the previous yellow / magenta palette ("Loud yellow CTA",
"Primary swaps to the magenta secondary accent"). Cyan and lavender
now, matching the actual token values.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0d477ac9fd |
feat(engine): Terminal design-token system in ui_theme
Replaces the prior Premium-Solitaire palette and ad-hoc constants with the full Terminal (base16-eighties) token set: near-black surface ramp, cyan primary CTA, lime/lavender/gold/teal/pink semantic accents, 5-rung type scale, 7-rung 4-multiple spacing scale, 3-step radius, 14-rung z-index hierarchy, and a complete motion budget. Card drop-shadow alphas pinned to 0 — Terminal depth is 1px borders + tonal layering, not box-shadow. Tokens stay as `pub const` so static contexts (default Sprite colours etc.) keep compiling; a future UiTheme resource can layer runtime switching on top without breaking the constant API. Four unit tests pin the spacing/type/z-index invariants so a careless edit can't silently break the scale. Plugin-by-plugin migration to consume these tokens follows in subsequent commits. Spec: docs/ui-mockups/design-system.md Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4b51e50203 |
fix(data): route data_dir() through a per-platform shim so Android persists
dirs::data_dir() returns None on Android, which silently disabled every persistence path (settings, stats, achievements, replays, game-state, time-attack sessions, user themes). New solitaire_data::platform::data_dir() shim falls through to dirs::data_dir() on desktop and returns the per-app sandbox at /data/data/com.solitairequest.app/files on Android — no JNI needed, since the package id is pinned in [package.metadata.android]. CLAUDE.md §10 already flagged this as a known pitfall; the shim pays it down at the one chokepoint instead of per feature. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
fb8b2ac684 |
feat(app): Android build target — first working APK at 54 MB
Wires the workspace through `cargo apk build`. After this commit `cargo apk build -p solitaire_app --target x86_64-linux-android` produces a debug-signed APK at `target/debug/apk/solitaire-quest.apk` containing all assets and `lib/x86_64/libsolitaire_app.so` — runnable on the AVD or a physical x86_64 device. The five gating points discovered by iterating compile cycles: 1. solitaire_app split into bin + lib. cargo-apk needs a `cdylib` to bundle as `libmain.so`; pure-bin crates panic with "Bin is not compatible with Cdylib". `src/lib.rs` carries the ECS bootstrap as `pub fn run`; `src/main.rs` is a 3-line shim that delegates for the desktop `cargo run` path. 2. `[package.metadata.android]` pins target SDK 34 / min SDK 26 so cargo-apk doesn't probe for whatever default it ships (which on this machine was an uninstalled API 30). `assets = "../assets"` lets the same asset directory feed both desktop and APK. 3. Workspace `bevy` features add `android-native-activity` (the Bevy-side glue that pairs with cargo-apk's NativeActivity wrapper). The feature is target-gated inside bevy_internal so desktop builds compile it out. 4. `arboard` (clipboard, used by Stats's "Copy share link") has no Android backend — `cargo apk build` fails with E0433 on `platform::Clipboard` if unconditional. Target-gated to `cfg(not(target_os = "android"))`; the system surfaces an informational toast on Android until JNI ClipboardManager is wired in the Phase-Android round. 5. `keyring` + `keyring-core` cannot compile for android — the transitive `rpassword` uses `libc::__errno_location` which bionic doesn't expose. Both crates target-gated; `auth_tokens` ships a stub on Android that returns `KeychainUnavailable` for every call, matching how callers already handle a Linux box without Secret Service. Cosmetic post-pass panic: cargo-apk panics AFTER the APK is signed when it tries to also wrap the bin target. The APK on disk is unaffected. Working around this with `cargo apk build --lib` is the next small step. What's verified: - Desktop `cargo build`, `cargo clippy --workspace --all-targets`, and `cargo test --workspace` all clean. - `cargo apk build -p solitaire_app --target x86_64-linux-android` produces 54 MB debug APK with libsolitaire_app.so + assets. What's NOT yet verified: - Whether the APK actually launches on the AVD / a phone (next step: `adb install` + `adb logcat` against the bevy_test AVD). - Whether `dirs::data_dir()` on Android returns a usable path (sync / persistence will surface this if not). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
690e1d2ad6 |
feat(engine): F3-toggleable FPS / frame-time overlay
Performance work for the upcoming Android port needs a numeric
baseline we can quote across desktop and mobile, instead of "feels
slow". DiagnosticsHudPlugin wraps Bevy's FrameTimeDiagnosticsPlugin
and renders a tiny corner readout the developer can toggle with F3.
- Hidden by default — production builds ship the plugin but the
overlay starts invisible.
- F3 reads ButtonInput<KeyCode> directly (not gated by pause /
modal state); diagnostics should always be reachable.
- Reads `smoothed()` FPS + frame_time so the cell isn't a jittery
per-frame scoreboard. Format: "FPS NN \u{2022} M.MM ms".
- Anchored top-right at z = Z_SPLASH + 100 so the readout sits
above every modal / toast / splash layer.
- Update system bails when hidden so we don't pay the
diagnostic-store lookup or text mutation when nobody's looking.
Next up on the perf track: get the Android build target wired so we
can put real numbers in this readout from a phone or emulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
35516d31f6 |
docs(help): add M / P / Win-Summary-Enter to the Overlays section
The Help (F1) modal's Overlays section listed S/A/L/O but skipped two post-v0.18 entries — M (Home / Mode launcher) and P (Profile) — and never mentioned the recently-shipped Enter accelerator that dismisses the Win Summary. Help is the canonical keyboard-discovery surface. Three new rows cover the gap so a player who opens F1 sees every overlay-toggle key, plus the contextual Enter shortcut. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9b065e5ac6 |
feat(stats): append "Shareable" badge to the Latest-win caption
The Copy share link button on the Stats overlay only produces a URL
when the displayed replay has a `share_url` populated; otherwise it
surfaces a toast explaining the upload prerequisite. Players had no
way to know the button would work without clicking it.
Adds a "\u{2022} Shareable" suffix to the Latest-win caption when
the displayed replay carries a share_url, matching the format the
v0.19.0 handoff sketched ("Replay 3 / 8 \u{2022} Shareable") for
the future Prev/Next selector. The Prev/Next markers exist in
stats_plugin but no spawn site renders them today, so the live
fix is on the existing single-replay caption.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e1b8766e15 |
feat(settings): "Smart window size" toggle to opt out of monitor-relative
launch sizing Players who specifically prefer the literal 1280×800 baseline on every fresh-install launch had no way to opt out of the v0.19.0 smart-default sizer. Adds a Gameplay-section toggle (mirrors the "Winnable deals only" pattern) so they can flip it off. - New `Settings::disable_smart_default_size: bool` field with `#[serde(default)]` so legacy `settings.json` files load to the shipped behaviour (smart sizer enabled). - Settings panel gains a "Smart window size" row with ON/OFF label inverting the negative flag, and a tooltip clarifying that saved window geometry always wins over both branches. - `solitaire_app::main` reads the flag once at startup and skips the `apply_smart_default_window_size` registration when it's set. Mid-session changes apply on next launch (documented on the field). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
67c150bd7b |
test(engine): wall-clock-bounded loop for pull_failure flake
The fixed 5-update budget in `pull_failure_sets_error_status` was the last test still subject to the AsyncComputeTaskPool starvation mode that v0.19.0's auto-save fix already cleared. Under heavy parallel cargo-test load, 5 updates wasn't always enough for the failing pull task to surface its Err and flip SyncStatusResource to Error. Pumps updates in a loop bounded by a 5-second deadline (with std::thread::yield_now between iterations to give the task pool a chance to run), exiting as soon as the status flips. Mirrors the auto-save flake fix shape — a healthy run hits the assertion in a handful of frames, while a starved run gets the budget it needs without hanging the suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6037596cc0 |
fix(engine): double-click move animation no longer plays twice
A successful double-click was rendering the slide-to-destination
animation twice — once from the first press's MoveRequestEvent
landing, and again from the release's StateChangedEvent racing the
in-flight CardAnim and replacing it from the mid-animation
position.
The frame trace:
Frame N (second press):
handle_double_click → MoveRequestEvent (queued)
start_drag → DragState set, drag.committed = false
(start_drag never mutates Transform; the
card is still visually in place)
handle_move → applies the move, fires StateChangedEvent
sync_cards_on_change → cur ≠ target, inserts CardAnim slide
(animation #1 starts)
Frames N+1, N+2, …:
follow_drag idles (drag uncommitted, cursor not moving)
CardAnim animates the card from old to new pile
Frame N+K (release):
end_drag → drag.committed = false branch:
drag.clear() + StateChangedEvent ← CULPRIT
sync_cards_on_change → sees the card mid-CardAnim
(cur ≠ target), replaces CardAnim
with a fresh one starting at the
current mid-position (animation #2
visibly restarts the slide)
The fix is one line: drop the StateChangedEvent write in the
uncommitted-drag branch of end_drag. The defensive resync was
never needed there — start_drag only mutates the DragState
resource on press, never card transforms, so an uncommitted drag
has no visual side effect to undo. The committed-drag branch (line
762) keeps its StateChangedEvent write since snap-back from a
real drag does need a resync.
Existing tests pass unchanged. The bug only manifested in the
specific timing of double-click → quick-release before
animation-complete; an integration test would require driving
mouse press/release across several frames with a dispatched
GameMutation pass between, which is heavier than the fix
warrants.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
d7ffb16df5 |
fix(engine): single-card double-click with no destination now plays the reject animation
handle_double_click had a coverage gap. The flow was:
- Priority 1: try moving the single top card to its best
destination (foundation, then tableau).
- Priority 2: if Priority 1 failed AND the player clicked the
base of a multi-card stack, try moving the whole stack.
`MoveRejectedEvent` was only fired inside the Priority 2 else-branch
— so a double-click on a single card with no legal destination
fell through both priorities silently: no card_invalid.wav, no
shake animation on the source pile, the player got zero feedback
that the click was acknowledged.
The fix collapses both priorities' failure paths into one
unconditional `MoveRejectedEvent` write at the end of the
double-click branch. Single-card miss now plays the same feedback
as multi-card-stack miss. The early `return` on each successful
move keeps the rejection branch from firing on the success path.
Pre-fix, a player double-clicking the 7♠ buried under a 6♥ on
column 5 (no foundation slot for 7s; no tableau column accepting
black 7) saw nothing happen. Post-fix, the source pile shakes
and the invalid-move sound plays, exactly like a drag-and-drop
rejection.
Workspace: 1170 passing tests / 0 failing. cargo clippy
--workspace --all-targets -- -D warnings clean.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
0b3140ad6d |
Revert "feat(engine): theme thumbnails accept PNG faces alongside SVG"
This reverts commit
|
||
|
|
e41def8c89 |
Revert "feat(engine): per-theme nearest-sampling opt-in for pixel-art themes"
This reverts commit
|
||
|
|
aad8bb9c83 |
Revert "feat(engine): bundle Rusty Pixel as a built-in theme"
This reverts commit
|