diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 96ff23f..56ff2fb 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -43,6 +43,7 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar | macOS | Self-hosted server | Full feature set | | Windows | Self-hosted server | Full feature set | | Linux | Self-hosted server | Full feature set | +| Android | Self-hosted server | Touch input; safe-area insets via JNI; `cargo-apk` build | ### Design Principles @@ -322,6 +323,12 @@ struct FontResource(Handle); struct BackgroundImageSet { handles: Vec>, // indices 0–4 match selected_background setting } + +// OS-reserved edge insets (physical px); zero on desktop +struct SafeAreaInsets { top: f32, bottom: f32, left: f32, right: f32 } + +// Whether the HUD band is visible (auto-hide chrome feature) +enum HudVisibility { Visible, Hidden } ``` ### Key Bevy Events @@ -900,7 +907,7 @@ All sound effect WAV files are embedded at compile time via `include_bytes!()` i | macOS | Primary | Self-hosted server | x86_64 + Apple Silicon (universal binary via `cargo-lipo`) | | Windows | Primary | Self-hosted server | x86_64, MSVC toolchain | | Linux | Primary | Self-hosted server | x86_64, tested on Ubuntu 22.04+ and Fedora 39+ | -| Android | Stretch | Self-hosted server | `cargo-mobile2`, touch input | +| Android | Active | Self-hosted server | `cargo-apk`; touch + long-press + double-tap; safe-area JNI; portrait layout | | iOS | Stretch | Self-hosted server | `cargo-mobile2`, touch input | Minimum Bevy window size enforced: 800×600. Desktop windows are freely resizable; layout recomputes on `WindowResized`. diff --git a/CLAUDE.md b/CLAUDE.md index 1a4b2fe..233cade 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md -version: unified-3.0 +version: unified-4.0 --- @@ -29,8 +29,9 @@ solitaire_sync/ # Shared API + merge logic solitaire_data/ # Persistence + sync client solitaire_engine/ # Bevy ECS + UI + gameplay orchestration solitaire_server/ # Axum backend (optional sync layer) +solitaire_wasm/ # WASM bindings for browser-side replay player solitaire_app/ # Entry binary -assets/ # Runtime assets (except audio) +assets/ # Runtime assets (except audio + default theme) ``` --- @@ -72,12 +73,16 @@ These override all other instructions. * NO `unwrap()` * NO `panic!()` in runtime/game logic -* All state transitions: +* Core game state mutations MUST return: ```rust id="err_model" Result ``` +* Engine / UI state changes follow ECS patterns (Resources, Events) — + they do not return `MoveError` +* Use `thiserror`-derived types for any new error enums outside `solitaire_core` + --- ## 2.4 Threading Rules @@ -126,10 +131,15 @@ trait SyncProvider ## 3.1 ECS Design * systems = single responsibility -* communication = Events only -* shared state = Resources only +* cross-system communication = Events (fire-and-forget triggers) +* persistent shared state = Resources (polled every frame or on change) * per-entity state = Components only +Events and Resources are both valid communication paths — use Events when +the receiver needs to react once; use Resources when the receiver polls +or when multiple systems read the same value (e.g. `SafeAreaInsets`, +`HudVisibility`, `LayoutResource`). + --- ## 3.2 Game State Authority @@ -149,11 +159,22 @@ Every player action MUST: Keyboard shortcuts are: → optional accelerators only +**Exception — UI chrome gestures:** +Tap-to-toggle visibility of UI chrome (e.g. auto-hiding HUD band) is +permitted without a visible button. The gesture MUST: +* affect only chrome visibility, never game state +* restore chrome automatically when any modal opens +* be purely additive (game remains fully playable with chrome always visible) + --- ## 3.4 Layout System * recompute on `WindowResized` +* recompute on `SafeAreaInsets` changed +* recompute on `HudVisibility` changed +* `compute_layout` MUST accept `hud_visible: bool`; pass `HUD_BAND_HEIGHT` + when `true`, `0.0` when `false` * no fixed resolution assumptions --- @@ -178,11 +199,18 @@ Includes: ## 4.2 Embedded Assets -Only audio: +Embed via `include_bytes!()` only when ALL of the following are true: -```text id="audio_rule" -include_bytes!() -``` +* the asset is small (< 500 KB uncompressed) +* it changes rarely (not user-customisable) +* a missing file would be a hard crash, not a graceful degradation + +Currently embedded: +* **Audio** — all `.wav` files in `audio_plugin.rs` +* **Default card theme** — shipped via `embedded://` scheme in `ThemePlugin` + +Do NOT embed card face PNGs, background images, or user fonts — +these are loaded via `AssetServer` so art can be swapped without recompile. --- @@ -210,7 +238,9 @@ Must degrade gracefully under `MinimalPlugins`. ## 5.2 Public API Rules * prefer `Into` over concrete types -* all public items require doc comments +* publicly exported functions, traits, and non-trivial types require doc comments +* simple marker components, newtype wrappers, and internal `pub` items + used only within the same crate are exempt from doc comment requirements --- @@ -276,11 +306,13 @@ NEVER commit otherwise Claude must request confirmation before: -* adding dependencies -* modifying `solitaire_sync` -* changing DB schema +* adding dependencies to `solitaire_core` or `solitaire_sync` + (engine/server crates may add deps without confirmation) +* modifying `solitaire_sync` types or the `SyncProvider` trait +* changing DB schema (migrations are append-only) * introducing `unsafe` -* changing merge strategy +* changing the merge strategy in `solitaire_sync::merge` +* changing the `SyncPayload` wire format (breaking change for existing servers) --- @@ -304,10 +336,29 @@ Core is always the source of truth. Must always be handled explicitly: +**All platforms** * Bevy `Time` uses `f32` * `sqlx::migrate!()` path is crate-relative * `dirs::data_dir()` may return `None` -* Linux may lack keyring backend +* Linux may lack keyring backend — handle `keyring::Error` gracefully + +**Android (active target — not stretch)** +* Safe-area insets arrive in frames 1–3 via JNI polling, not at startup; + UI that depends on them must handle the zero-inset initial state +* Physical pixels ≠ logical pixels: `SafeAreaInsets` values are physical + (from `WindowInsets` API); divide by `window.scale_factor()` before + passing to Bevy `Val::Px` +* `adb shell input tap` uses physical pixel coordinates +* FiraMono (bundled font) covers: ASCII, card suits U+2660–2666, + Arrows U+2190–21FF. It does NOT cover Geometric Shapes (U+25xx) — + those render as missing-glyph rectangles on Android +* The gesture/navigation bar at the bottom (≈132px physical on common + devices) is inside the Bevy viewport; use `SafeAreaInsets.bottom` to + avoid placing interactive elements in that zone +* `HUD_BAND_HEIGHT` is 128px on Android (two-row wrap) vs 64px on desktop; + layout constants are `#[cfg(target_os = "android")]` gated +* JNI calls must use `attach_current_thread_permanently` — not + `attach_current_thread` — to avoid detach-on-drop panics --- @@ -318,6 +369,12 @@ Must always be handled explicitly: * blocking async calls in ECS * insecure credential storage * bypassing core logic layer +* hardcoded pixel coordinates in layout — always derive from `compute_layout` +* Unicode Geometric Shapes block (U+25xx) in UI text — not in FiraMono +* spawning a second `ModalScrim` while one already exists without first + dismissing the existing one (use `scrims.is_empty()` guard) +* reading `SafeAreaInsets` physical values directly into `Val::Px` without + dividing by `window.scale_factor()` --- @@ -345,9 +402,74 @@ If unclear: | Both combined | full system understanding | --- -# 14. Context Injection System (AUTOMATIC SCOPE FILTER) +# 14. Modal System Conventions -## 14.1 Purpose +All full-screen overlay panels MUST use the `spawn_modal` / `ModalScrim` pattern +from `solitaire_engine::ui_modal`. + +## 14.1 Spawn pattern + +```rust +let scrim = spawn_modal(commands, MyScreenMarker, Z_MODAL_PANEL, |card| { + spawn_modal_header(card, "Title", font_res); + // ... body nodes ... + spawn_modal_actions(card, |actions| { + spawn_modal_button(actions, MyCloseButton, "Done", None, + ButtonVariant::Primary, font_res); + }); +}); +// Optional: allow clicking the scrim outside the card to dismiss +commands.entity(scrim).insert(ScrimDismissible); +``` + +## 14.2 Guard rule + +Before spawning a new modal, check `scrims: Query<(), With>` +and return early if `!scrims.is_empty()` — unless the new modal is +explicitly replacing the current one (despawn first, then spawn). + +## 14.3 Safe area + +Every `ModalScrim` automatically receives `padding.bottom` equal to the +logical gesture-bar height via `apply_safe_area_to_modal_scrims` in +`SafeAreaInsetsPlugin`. Do not manually add bottom padding to scrim nodes. + +## 14.4 Z-ordering + +Use `Z_MODAL_PANEL` from `ui_theme` for all modal scrims. Do not use +raw `z_index` values — they drift and cause ordering bugs. + +--- + +# 15. Android Build & Verification + +## 15.1 Build command + +```bash +cargo apk build --package solitaire_app --lib +adb install -r target/debug/apk/solitaire-quest.apk +``` + +## 15.2 Coordinate system reminder + +Device physical: 1080×2400. Bevy logical: 900×2000. Scale factor: 1.20. +`adb shell input tap X Y` takes PHYSICAL coordinates. +To convert from what you see on screen (logical): multiply by 1.20. + +## 15.3 Android-specific test checklist + +Before shipping any Android build: +- [ ] Safe area insets arrive and shift HUD correctly (check after 3s) +- [ ] All modal Done buttons are above the gesture bar +- [ ] No Geometric Shapes glyphs in UI text +- [ ] HUD band does not overlap the top status bar +- [ ] Touch drag-and-drop works on all pile types + +--- + +# 16. Context Injection System (AUTOMATIC SCOPE FILTER) + +## 16.1 Purpose Before generating any response, Claude MUST construct a **minimal relevant context set**. @@ -360,7 +482,7 @@ This prevents: --- -## 14.2 Input Classification Step (MANDATORY) +## 16.2 Input Classification Step (MANDATORY) Every request MUST be classified into exactly one task type: @@ -381,13 +503,13 @@ If uncertain → ask clarification. --- -## 14.3 Context Selection Engine +## 16.3 Context Selection Engine After classification, Claude MUST include ONLY the relevant sections below. --- -## 14.4 Context Map (CORE RULESET) +## 16.4 Context Map (CORE RULESET) ### feature @@ -495,7 +617,7 @@ Include: --- -## 14.5 Context Compression Rules +## 16.5 Context Compression Rules Claude MUST obey: @@ -506,7 +628,7 @@ Claude MUST obey: --- -## 14.6 Context Priority Order +## 16.6 Context Priority Order When space is limited: @@ -517,7 +639,7 @@ When space is limited: --- -## 14.7 “No Context Pollution” Rule +## 16.7 “No Context Pollution” Rule Claude must NOT include: @@ -529,7 +651,7 @@ Claude must NOT include: --- -## 14.8 Self-Check Before Execution +## 16.8 Self-Check Before Execution Before writing code, Claude MUST verify: @@ -542,7 +664,7 @@ If any fail → revise context selection. --- -## 14.9 Injection Output Format (Internal Model) +## 16.9 Injection Output Format (Internal Model) Claude should behave as if it constructed: @@ -560,7 +682,7 @@ Claude should behave as if it constructed: --- -## 14.10 Relationship to ARCHITECTURE.md +## 16.10 Relationship to ARCHITECTURE.md * ARCHITECTURE.md = source of truth * CLAUDE.md = execution constraints diff --git a/CLAUDE_PROMPT_PACK.md b/CLAUDE_PROMPT_PACK.md index 865cae8..35a49c0 100644 --- a/CLAUDE_PROMPT_PACK.md +++ b/CLAUDE_PROMPT_PACK.md @@ -12,7 +12,7 @@ You must follow CLAUDE_SPEC.md strictly. Rules: - Do not expand scope beyond what is defined - Do not refactor unrelated code -- Do not introduce new dependencies +- Do not introduce new dependencies to solitaire_core or solitaire_sync without confirmation - Prefer minimal, surgical changes - Use existing patterns in the codebase - Return minimal diffs or changed functions only @@ -360,7 +360,7 @@ notes: target: "" -constraints:CLAUDE_WORKFLOW.md +constraints: - no behavior change - no architecture change - minimal code changes diff --git a/CLAUDE_SPEC.md b/CLAUDE_SPEC.md index ec0e559..74b0cab 100644 --- a/CLAUDE_SPEC.md +++ b/CLAUDE_SPEC.md @@ -41,6 +41,10 @@ solitaire_server: depends_on: [solitaire_sync, axum, sqlx, jsonwebtoken] role: "backend" +solitaire_wasm: +depends_on: [solitaire_core, wasm-bindgen, serde-wasm-bindgen] +role: "wasm_replay_player" + solitaire_app: depends_on: [solitaire_engine] role: "entrypoint" @@ -180,7 +184,7 @@ threading: plugins: pattern: "feature_isolation" -communication: "events" +communication: "events and resources" --- diff --git a/SESSION_HANDOFF.md b/SESSION_HANDOFF.md index d1eeb7d..d3fb503 100644 --- a/SESSION_HANDOFF.md +++ b/SESSION_HANDOFF.md @@ -89,6 +89,22 @@ Also shipped (pre-Phase 8 but post-v0.22.0, already in CHANGELOG): subcommand reads new password from stdin, bcrypt-hashes it, invalidates all active sessions for the user. +### 5b. Android UX polish (2026-05-12) + +- [x] **UX-1 — Modal Done button in gesture zone.** `apply_safe_area_to_modal_scrims` system + added to `SafeAreaInsetsPlugin` (`safe_area.rs`). Pads every `ModalScrim` bottom by + `insets.bottom / scale`. Fires on resource change + `Added`. Verified on device. +- [x] **UX-5b — Home mode glyph corruption.** Geometric Shapes (U+25xx, absent from FiraMono) + replaced with card suits U+2660–2666 in `home_plugin.rs`. Affects Zen/Challenge/Daily mode + selector buttons at level 5+. +- [x] **UX-7 — Help text wrap.** Android HUD entry shortened to + `"Open menu (Stats, Settings, Profile...)"` in `help_plugin.rs` — fits one line. +- [x] **BUG-3 — Multi-modal stacking.** `handle_menu_button` now checks + `scrims: Query<(), With>` and guards `spawn_menu_popover` with `scrims.is_empty()`. + Verified on device: ≡ tap while Stats open does nothing. + + **Note:** These 4 fixes are implemented and verified but not yet committed. + ### 6. Testing gaps - [x] **Server 401 → refresh → retry path.** Done (`198df75`): both `jwt_refresh_on_401_succeeds` (pull) and @@ -141,7 +157,7 @@ Branch: master. v0.23.0 is the current version (HEAD: 03be4fc). Fully pushed. READ FIRST (in order): 1. SESSION_HANDOFF.md — this file 2. CHANGELOG.md — [0.23.0] section has full Phase 8 detail - 3. CLAUDE.md — unified-3.0 rule set + 3. CLAUDE.md — unified-4.0 rule set 4. ARCHITECTURE.md — v1.3, fully up to date 5. docs/ui-mockups/ — design system + mockup library 6. docs/android/ — Android setup + build runbook @@ -151,5 +167,11 @@ OPEN WORK: Phase 8 punch list is fully closed. All items verified complete. Remaining nuisance: `cargo apk build --lib` noisy stderr (cosmetic, non-blocking). - Suggest starting Phase 9 planning — ask what the next arc should be. + 4 Android UX fixes are implemented and verified but NOT YET COMMITTED: + - BUG-3 (hud_plugin.rs): multi-modal stacking guard + - UX-7 (help_plugin.rs): help text wrap on Android + - UX-5b (home_plugin.rs): FiraMono glyph corruption in mode selector + - UX-1 (safe_area.rs): modal Done button in gesture zone + + Commit those first, then suggest Phase 9 planning. ``` diff --git a/docs/SESSION_HANDOFF.md b/docs/SESSION_HANDOFF.md index 1891dbd..3f148f9 100644 --- a/docs/SESSION_HANDOFF.md +++ b/docs/SESSION_HANDOFF.md @@ -1,4 +1,8 @@ -# Solitaire Quest — Session Handoff +# Solitaire Quest — Session Handoff (ARCHIVED) + +> **This file is from Phase 2 (2026-04-25, 242 tests). It is kept for historical +> reference only. The authoritative session handoff is at the repo root: +> `SESSION_HANDOFF.md`.** > Last updated: 2026-04-25 > Branch: `master` — pushed to https://github.com/funman300/Rusty_Solitaire.git diff --git a/docs/android/PLAYABILITY_TODO.md b/docs/android/PLAYABILITY_TODO.md index b8add6a..ed09ba5 100644 --- a/docs/android/PLAYABILITY_TODO.md +++ b/docs/android/PLAYABILITY_TODO.md @@ -230,6 +230,31 @@ rewrites required. --- +## P5 — UX polish (2026-05-12) + +- [x] **UX-1 — Modal Done button unreachable in gesture zone.** *Closed + 2026-05-12.* New `apply_safe_area_to_modal_scrims` system in + `safe_area.rs` pads every `ModalScrim` bottom by `insets.bottom / + window.scale_factor()` (logical pixels). Fires when `SafeAreaInsets` + changes AND when a new `ModalScrim` is spawned (`Added` + filter). Verified on device: Settings Done button reachable at physical + y ≈ 1800–2000 (was y ≈ 2232+, inside gesture zone). +- [x] **UX-5b — Home mode selector glyph corruption.** *Closed + 2026-05-12.* `home_plugin.rs` mode glyphs changed from Geometric Shapes + block (U+25xx — absent from FiraMono, renders as rectangles) to card + suits U+2660 ♠ / U+2665 ♥ / U+2666 ♦. Affects Zen, Challenge, and + Daily mode selector buttons shown at level 5+. +- [x] **UX-7 — Help screen HUD button entry wraps to two lines.** *Closed + 2026-05-12.* Android `CONTROL_SECTIONS` entry for ≡ button shortened + from `"Menu: Stats, Settings, Profile, Achievements"` to + `"Open menu (Stats, Settings, Profile...)"` in `help_plugin.rs`. + Fits on one line at 360 dp. +- [x] **BUG-3 — Multi-modal stacking (Stats + Profile simultaneously).** *Closed + 2026-05-12.* `handle_menu_button` in `hud_plugin.rs` now checks + `scrims: Query<(), With>` and only calls + `spawn_menu_popover` when `scrims.is_empty()`. Tapping ≡ while any + modal is open is a no-op. Verified on device. + ## Notes / decisions * This list is screenshot-driven; expect more items to surface once