69ce9afab9e71c927ba4f46b567b29465cc460ec
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
69ce9afab9 |
feat(engine): foundation completion flourish — King-on-foundation celebration
Now that foundations are unlocked and "completing" one is a real moment (rather than a foregone conclusion based on suit assignment), each Ace-through-King run gets its own small celebration when the King lands. Three layers fire on a single FoundationCompletedEvent emitted by game_plugin's handle_move when a successful move leaves a PileType::Foundation pile holding 13 cards: 1. King card scale-pulse via a new FoundationFlourish component. Triangular curve 1.0 → 1.15 → 1.0 over MOTION_FOUNDATION_FLOURISH _SECS (0.4s) — same shape as the existing ScorePulse so the feel matches. 2. Pile-marker tint flourish via FoundationMarkerFlourish — the foundation marker's sprite colour lerps to STATE_SUCCESS for the first half of the duration then fades back. Reuses the existing success-signal palette; no new colour token. 3. Audio cue: foundation_complete.wav, a synthesised C6→E6→G6 triad with 2nd-harmonic warmth and AR decay (~240 ms). Sits an octave above win_fanfare's root so the layered fourth-completion + win cascade reads cleanly. Generated via solitaire_assetgen's foundation_complete() function and embedded via include_bytes!(). The visual systems run .after(GameMutation) so the post-move pile state is visible when the King is identified. Both flourish components remove themselves once elapsed time exceeds duration — no animation queue or scheduler integration needed. Pure foundation_flourish_scale(elapsed, duration) helper is unit-tested for the curve, edge clamps, and zero-duration safety. Three integration tests on the firing logic verify the event fires exactly once when a King completes a foundation, doesn't fire for non-foundation moves, and doesn't fire when the foundation is at 12 cards. The fourth completion still co-occurs with the win cascade — the two layer cleanly because the flourish's scale is on the King card sprite while the cascade is a screen-shake + per-card rotation, and the foundation_complete ping is a higher octave than the win fanfare's root. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f712b89fe4 |
feat(engine): drop shadows on cards with lifted state during drag
Cards previously read as flat stickers on the felt — no separation cue, no sense the play surface had any depth. Each CardEntity now spawns a CardShadow child sprite: neutral black at 25 % alpha, sized to card_size + 4 px halo, offset (2, -3) and rendered at local z -0.05 so it sits behind its card. Cards in the active drag set switch to a lifted shadow: alpha 40 %, offset (4, -6), padding (8, 8). update_card_shadows_on_drag runs every Update and snaps each shadow to the right state based on DragState membership — no lerp, no animation cost. The pure card_shadow_params(is_dragged) helper is unit-tested for the four parameter values. resize_cards_in_place gains a third query for shadows so the in-place resize keeps shadows cheap (no Sprite regeneration); the shadow's current alpha is read to preserve idle vs lifted padding across a resize. update_card_entity's despawn_related call is followed by a fresh add_card_shadow_child so the shadow re-attaches when the card is repainted (face flip, settings change, theme swap). The pre-existing bulk drag-shadow under the whole lifted stack is untouched — per-card shadows complement it. All shadow values flow through eight new ui_theme tokens (CARD_SHADOW_COLOR, alphas, offsets, paddings, local z) so the visual is tunable in one place. Color is neutral black so the shadows don't conflict with color-blind mode's red/blue suit tints. Four new tests pin the contract: shadow params for idle and drag states, every CardEntity spawns with exactly one CardShadow child, and dragging shifts only the dragged shadow's offset while leaving unrelated shadows on the idle offset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f6c916641a |
feat(engine): visible drop-target overlay during drag
The existing update_drop_highlights system tinted PileMarker sprites green for valid drops, but the marker is a card-sized rectangle that sits behind the stack. Once a tableau column had any cards on it the marker was occluded and the highlight effectively invisible — the handoff's "drops feel guess-y because there's no preview" point. A new update_drop_target_overlays system spawns an overlay above every legal target during drag: a soft DROP_TARGET_FILL rectangle sized to the pile's actual visible footprint (full fanned column for tableaux, card-sized for foundations and empty tableaux) plus four thin DROP_TARGET_OUTLINE edges forming a 3 px border. Z_DROP_OVERLAY = 50 sits above static cards (z ~1) but below the dragged stack (DRAG_Z = 500), so the overlay never occludes the card the player is holding. The valid-target enumeration mirrors update_drop_highlights exactly so the rules can't drift, and pile geometry mirrors input_plugin's pile_drop_rect. The original marker-tint system is untouched; it still does its job for empty-pile placeholders. The overlay layer is purely additive — running alongside, not replacing. Token values reuse the existing STATE_SUCCESS hue (#4ADE80) at 10% fill / 75% outline so the overlay green matches the rest of the success-signal palette (foundation completion, sync OK, etc.). Three headless tests pin the contract: overlay spawns for valid tableau drops, doesn't spawn for invalid destinations, and despawns the moment the drag ends. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2c72e1fc87 |
feat(engine): reserve top band for HUD so it stops crowding the cards
Player report: the action button bar (Menu / Undo / Pause / Help / Modes / New Game) and Score / Moves / Timer text were sharing the same vertical band as the stock + foundation row, with no visual separation. The HUD read as part of the play surface. Two-part fix: 1. layout.rs reserves HUD_BAND_HEIGHT (64 px) at the top of the window. Card-grid math takes that off the available vertical budget so cards still fit; top_y shifts down by the same amount. New layout test pins the reservation. Existing worst_case_tableau_fits_vertically tests verify the height-budget arithmetic still holds. 2. hud_plugin.rs spawns a translucent purple band (BG_HUD_BAND, new token in ui_theme.rs at the BG_BASE hue with 0.70 alpha) filling that reserved zone. Z-index sits one rung below Z_HUD so action buttons paint on top while the band reads as their container. The band's bottom edge lines up with the top edge of the highest playable card, so the buttons feel anchored to a "tools strip" rather than floating in the play area. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5d57b67934 |
feat(engine): branded splash screen on launch
The window previously snapped straight to a card deal, which read more like a prototype than a finished game. SplashPlugin lays a fullscreen overlay (BG_BASE backdrop, ACCENT_PRIMARY title, version subtitle) on top of the gameplay layer for MOTION_SPLASH_TOTAL_SECS — the board deals behind it so the splash dissolve hands off naturally to the deal animation. Visibility curves through fade-in (300ms), hold (~1s), fade-out (300ms) using a pure splash_alpha helper that gets pinned by a unit test rather than wired to the Bevy clock — Time<Virtual>'s 250ms per-tick clamp makes float-tight alpha assertions around the fade boundary brittle. Any keystroke or mouse-button press jumps the age forward to the fade-out window so the splash dissolves immediately. The dismiss handler is read-only on ButtonInput / Touches, so the same press is still visible to gameplay handlers downstream — pressing Space on the splash both dismisses it and triggers the next-tick stock draw, as verified by dismissal_keypress_is_visible_to_other_systems. Z_SPLASH sits above every other UI rung (Z_TOAST + 100) so the splash owns the viewport for its brief lifetime. The hierarchy test was extended to enforce the new rung's monotonic position. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
54d34972d4 |
feat(engine): tooltip infrastructure with hover delay (foundation only)
A new ui_tooltip module owns a Tooltip(Cow<'static, str>) component that turns any UI node into a hover-revealing help target. Bevy 0.18's required-components attribute auto-inserts an Interaction so callers just attach Tooltip and the rest is wired. A single overlay entity is reparented above the focus ring (new Z_TOOLTIP token = Z_FOCUS_RING + 10) and tracked from the hovered target's GlobalTransform + ComputedNode. The chained Update systems start a hover timer on Interaction::Hovered, show the overlay once MOTION_TOOLTIP_DELAY_SECS (0.5s) has elapsed, hide it the moment hover ends, and refresh the text when the hover target switches without an intervening unhover. Tested headless under MinimalPlugins with a 200ms ManualDuration ticker — Bevy clamps Time<Virtual>'s max_delta to 250ms by default, so a one-shot 1s step doesn't actually advance the clock past the threshold; the tests step five times to exercise both pre- and post-delay invariants. This commit ships the infrastructure only — no entity in the engine has Tooltip attached yet. A follow-up applies tooltips to the HUD readouts and action bar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
12789529a1 |
feat(engine): keyboard focus rings on modal buttons (Phase 1)
Every button spawned via spawn_modal_button is now keyboard-navigable. Tab/Shift-Tab cycles focus within the active modal, Enter activates the focused button via the same Interaction::Pressed signal mouse clicks use, and the primary action auto-focuses on modal open. Mouse clicks transfer focus so the two input modes stay in sync. The visual indicator is a single overlay entity that's reparented above the topmost modal scrim and tracks the focused button's GlobalTransform + ComputedNode each frame. Sitting outside the modal-card subtree means the ring isn't affected by the open animation's 0.96→1.0 scale, and sitting outside any scroll container means it can't be clipped by Settings' Overflow::scroll_y. Z-order sits one rung above Z_MODAL_TOP via the new Z_FOCUS_RING token. Existing 11 modals (Help, Stats, Achievements, Settings, Profile, Leaderboard, Pause, Forfeit confirm, GameOver, Confirm new game, Onboarding, Home) get focus support without any call-site changes — attach_focusable_to_modal_buttons walks the ancestry of any ModalButton lacking Focusable to find its scrim and tags it automatically. selection_plugin's Tab handler keeps working when no modal is open; when one is, focus consumes Tab/Enter before the selection system sees them. Phase 1 scope only — HUD action bar, Home mode cards, and Settings bespoke buttons (icon, swatch, toggle) come in Phase 2/3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6723416a55 |
feat(engine): convert PauseScreen to modal + add ForfeitConfirmScreen
Pause overlay drops its bespoke full-screen layout and is rebuilt on the standard `ui_modal` scaffold: uniform scrim, centred card, real Resume (Primary, Esc) and Forfeit (Tertiary, G) action buttons. The Draw Mode row stays inline in the body so the existing toggle still fires `SettingsChangedEvent`. The G-key double-press toast countdown is replaced with a real modal: `G` (or clicking Forfeit on Pause) fires the new `ForfeitRequestEvent`, which `PausePlugin` answers by spawning `ForfeitConfirmScreen` at `Z_PAUSE_DIALOG` (above pause). The modal exposes Cancel + "Yes, forfeit" buttons plus Y/Enter/N/Esc accelerators; confirmation despawns both modals, clears `PausedResource`, and fires `ForfeitEvent` for `StatsPlugin`. `toggle_pause` now early-returns when a forfeit modal is visible (and runs `.before(handle_forfeit_keyboard)`) so an Esc that closes the forfeit modal doesn't also re-open pause in the same frame. The legacy `forfeit_countdown` field, `FORFEIT_CONFIRM_WINDOW` constant, and the six pure-function countdown tests are removed; new tests cover the modal-spawn / confirm / cancel paths and the active- game predicate that still gates the G hotkey. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8da62bd05f |
feat(engine): add ui_modal primitive (scaffold + button variants)
Phase 3 step 3 of the UX overhaul. Adds a reusable modal helper that
the next 6 commits use to convert each overlay screen. The audit found
11 overlays using 3 different visual styles with scrim alpha drift
between 0.60 and 0.92; this primitive collapses all of that into one
consistent shape.
API surface:
- spawn_modal(commands, plugin_marker, z, build_card) — full-screen
scrim (uniform SCRIM token) + centred card (BG_ELEVATED, RADIUS_LG,
BORDER_STRONG outline, max-width 720, min-width 360, padding
SPACE_5). Returns the scrim entity for one-call despawn.
- spawn_modal_header(parent, title, font_res) — TYPE_HEADLINE
+ TEXT_PRIMARY, the canonical overlay heading.
- spawn_modal_body_text(parent, text, color, font_res) — TYPE_BODY_LG
paragraph; pass TEXT_PRIMARY or TEXT_SECONDARY.
- spawn_modal_actions(parent, build_buttons) — flex-row
justify-end with margin-top.
- spawn_modal_button(parent, marker, label, hotkey,
variant, font_res) — real Button
entity with optional TYPE_CAPTION hotkey-hint chip.
ButtonVariant enum drives colour:
Primary idle ACCENT_PRIMARY hover ACCENT_PRIMARY_HOVER
pressed ACCENT_SECONDARY (yellow → pink press flash)
Secondary idle BG_ELEVATED_HI hover BG_ELEVATED_TOP
pressed BG_ELEVATED
Tertiary idle BG_ELEVATED hover BG_ELEVATED_HI
pressed BG_ELEVATED_PRESSED
A new BG_ELEVATED_TOP token plus ACCENT_PRIMARY_HOVER cover the new
hover/press combinations cleanly.
UiModalPlugin registers paint_modal_buttons so every ModalButton gets
hover and press feedback automatically — overlay plugins don't add
their own paint systems. Plugin registered in solitaire_app.
A self-test asserts each variant's idle / hover / pressed colours are
all distinct; another verifies the plugin builds under MinimalPlugins.
This commit is purely additive — no overlay calls the new helpers
yet. The next commits convert each overlay to use them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e14852c093 |
feat(engine): add ui_theme.rs design-token module
Phase 3 step 1 of the UX overhaul. Centralises every UI design token — colours, typography, spacing, border-radius, z-index, and motion durations — so subsequent overhaul commits read from one source of truth instead of scattering hex codes and magic numbers across plugin files. The audit (2026-04-30) found: - 40+ hardcoded Color::srgb literals across UI surfaces. - 12 distinct font sizes (14/15/16/17/18/22/26/28/30/32/40/48 px) with no scale. - 8+ z-index magic numbers across overlay plugins (200, 210, 220, 230, 250, 300, 400) with no documented hierarchy. - Motion durations only partially honouring AnimSpeed — slide and cascade did, but toast / shake / settle / deal were hardcoded. ui_theme.rs collapses these into: - Midnight Purple base (BG_BASE / BG_ELEVATED / BG_ELEVATED_HI / BG_ELEVATED_PRESSED) + Balatro-yellow ACCENT_PRIMARY + warm magenta ACCENT_SECONDARY + state colours (success/warning/danger/ info) + text tiers (primary/secondary/disabled) + a uniform SCRIM. - 5-rung typography scale (display 40 / headline 26 / body-lg 18 / body 14 / caption 11). - 4-multiple spacing scale (4/8/12/16/24/32/48), with VAL_SPACE_* Val::Px convenience constants. - 3 border-radius rungs (sm 4 / md 8 / lg 16). - Documented monotonically-increasing z-index hierarchy enforced by a unit test. - All MOTION_* duration constants funnelled through scaled_duration() so AnimSpeed (Normal/Fast/Instant) applies to every animation, not just slide and cascade. This commit is purely additive — no call sites change yet. Subsequent commits in the overhaul migrate plugins to the tokens one region at a time (HUD restructure, modal primitive, then per- overlay conversions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |