Compare commits

...

9 Commits

Author SHA1 Message Date
funman300 f6e57b759e ci: pin toolchain to 1.95.0 and install Bevy native deps
Test / test (pull_request) Successful in 27m47s
Review feedback on #135: floating 'stable' + -D warnings lets every new
clippy release redden master with new lints — pin 1.95.0 like the
web-wasm-rebuild workflow. And ubuntu-latest lacks the ALSA/udev/X11/
Wayland dev packages the Bevy crates link against; install the standard
Bevy CI set (the project's own builder images have no desktop-Bevy
precedent — android-builder targets the NDK and web-e2e only builds the
server).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:28:28 -07:00
funman300 c9adcaa5e4 ci: add workspace clippy + test gate workflow
Test / test (pull_request) Failing after 1m35s
Until now no CI workflow ran the test suite or clippy at all — the
android-release, docker-build, web-e2e, web-wasm-rebuild, and
builder-image workflows cover packaging and e2e, but a direct push to
master (including the web-wasm-rebuild bot commit) never executed
cargo test or clippy. The gate discipline in CLAUDE.md §6 existed only
on developer machines.

test.yml runs the exact §6 commands (clippy --workspace --all-targets
-D warnings; cargo test --workspace) on master pushes and PRs, with
SQLX_OFFLINE against the checked-in .sqlx cache, path-filtered so
docs-only merges don't burn CI. Toolchain/cache mirror web-e2e.yml.

Found during the 2026-07-06 large-scale review (finding H1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:21:19 -07:00
funman300 15bb136c79 Merge pull request 'perf(web): size-focused wasm-release profile for the canvas build (36.2 → 23.2 MB)' (#134) from perf/wasm-size-profile into master
Build and Deploy / build-and-push (push) Successful in 7m4s
Web E2E / web-e2e (push) Successful in 4m51s
Web WASM Rebuild / rebuild (push) Successful in 9m30s
2026-07-06 22:44:33 +00:00
funman300 16a1139eab perf(web): size-focused wasm-release profile for the canvas build
canvas_bg.wasm shipped at 36.2 MB — plain release (opt-level 3, thin
LTO) piped through wasm-opt -O2. Download size, not throughput, is the
binding constraint for the browser canvas, so solitaire_web now builds
with a dedicated wasm-release profile: opt-level "s", fat LTO, one
codegen unit. Local result: 23.2 MB after the unchanged wasm-opt -O2
pass — 35.9% smaller.

The binaryen pass stays at -O2 (the -Oz grey-screen miscompile note in
build_wasm.sh still applies). profile.strip is deliberately NOT set: on
wasm it also removes the target_features custom section, which makes
wasm-opt reject the module ('all used features should be allowed' on
trunc_sat).

Verified with the new artifact: all 5 play_canvas e2e specs pass (debug
bridge, draw3 param, apply/undo, replay diagnostics, 40-seed autoplay
invariants). Headless pixel verification is inconclusive in this
environment — wgpu cannot create a usable adapter locally and panics
identically on the OLD artifact too — so visual parity should be
confirmed against production after the next deploy.

pkg/ artifacts are intentionally not committed: the web-wasm-rebuild
workflow is the single source of truth and will regenerate them with
this profile on merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:44:16 -07:00
funman300 710cabcf0d Merge pull request 'docs(architecture): bring source-of-truth docs to post-migration reality' (#133) from docs/architecture-post-migration into master 2026-07-06 22:26:18 +00:00
funman300 5acd8e4cd0 docs(architecture): bring source-of-truth docs to post-migration reality
ARCHITECTURE.md predated the card_game migration (PR #88, 2026-06-22)
and still documented the pre-migration core: local Card with face_up,
PileType, stored score/undo/recycle, and a snapshot undo stack. Rewrites
the Core Game Models section around the upstream card_game/klondike
types and the derived-stats/replay-undo model, updates the
solitaire_core crate section (deps + ownership), adds solitaire_web and
solitaire_assetgen to the workspace tree, and corrects the
solitaire_app and Settings entries. Version bumped to 1.4.

Also adds the two missing crates to the CLAUDE.md crate map (AGENTS.md,
its gitignored local twin, was regenerated in place) and records the
full device checklist passing on the Fold 7 in SESSION_HANDOFF.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:26:06 -07:00
funman300 652c9290b9 Merge pull request 'docs: add v0.41.1 changelog section and refresh session handoff' (#132) from docs/handoff-v0.41.1 into master 2026-07-06 22:19:37 +00:00
funman300 beddbcf94d docs: add v0.41.1 changelog section and refresh session handoff
CHANGELOG gains the missing [0.41.1] section (pile-marker child-resize
fix + Android relayout diagnostics). SESSION_HANDOFF is brought forward
from the stale v0.40.0 state to today's: v0.41.x releases, the July 6
review arc (PRs #121-#131), Fold 7 on-device verification results, the
remaining device-checklist items, and new architectural notes (plugin
submodule pattern, marker child-resize rule, Fold 7 inset quirks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:19:35 -07:00
Gitea CI 3c6b6e8c22 chore(web): regenerate wasm artifacts
Build and Deploy / build-and-push (push) Successful in 6m59s
Web E2E / web-e2e (push) Successful in 4m7s
2026-07-06 21:38:35 +00:00
9 changed files with 264 additions and 84 deletions
+66
View File
@@ -0,0 +1,66 @@
# Workspace gate: the same clippy + test commands CLAUDE.md §6 requires
# locally, run on every master push and pull request. Until this workflow
# existed, nothing in CI ran the test suite at all — a direct push to
# master (or the web-wasm-rebuild bot commit) was entirely unguarded.
name: Test
on:
push:
branches: [master]
paths:
- 'solitaire_app/**'
- 'solitaire_assetgen/**'
- 'solitaire_core/**'
- 'solitaire_data/**'
- 'solitaire_engine/**'
- 'solitaire_server/src/**'
- 'solitaire_server/tests/**'
- 'solitaire_server/migrations/**'
- 'solitaire_sync/**'
- 'solitaire_wasm/**'
- 'solitaire_web/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.cargo/**'
- '.sqlx/**'
- '.gitea/workflows/test.yml'
pull_request:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust 1.95.0
uses: dtolnay/rust-toolchain@master
with:
toolchain: 1.95.0
components: clippy
- name: Cache cargo build
uses: Swatinem/rust-cache@v2
# Native link deps for the Bevy crates (engine/app/web) on a bare
# ubuntu runner: ALSA + udev for input/audio, X11 + Wayland for winit.
- name: Install Bevy native dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libasound2-dev libudev-dev pkg-config libx11-dev libxcursor-dev \
libxrandr-dev libxi-dev libwayland-dev libxkbcommon-dev
# SQLX_OFFLINE uses the checked-in `.sqlx/` query cache (no live DB),
# same as the web-e2e workflow's server prebuild.
- name: Clippy (deny warnings)
env:
SQLX_OFFLINE: 'true'
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
env:
SQLX_OFFLINE: 'true'
run: cargo test --workspace
+71 -39
View File
@@ -1,9 +1,11 @@
# Ferrous Solitaire — Architecture Document
> **Version:** 1.3
> **Version:** 1.4
> **Language:** Rust (Edition 2024)
> **Engine:** Bevy (latest stable)
> **Last Updated:** 2026-05-12
> **Last Updated:** 2026-07-06 — post card_game/klondike migration (PR #88):
> core card/pile types come from the upstream `card_game` workspace and
> score/undo/recycle are derived from the upstream session, not stored.
---
@@ -82,13 +84,15 @@ ferrous_solitaire/
│ ├── win_fanfare.wav
│ └── ambient_loop.wav
├── solitaire_core/ # Pure Rust game logic — zero external deps beyond rand/serde
├── solitaire_core/ # Pure Rust game rules — wraps upstream card_game/klondike (serde + thiserror only otherwise)
├── solitaire_sync/ # Shared API types — used by client and server
├── solitaire_data/ # Persistence, sync client, settings
├── solitaire_engine/ # Bevy ECS systems, components, plugins
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite)
├── solitaire_wasm/ # WebAssembly bindings — browser-side replay player
── solitaire_app/ # Main binary entry point
├── solitaire_server/ # Self-hosted sync server (Axum + SQLite) + web frontend
├── solitaire_wasm/ # WebAssembly bindings — browser-side logic/replay + debug bridge
── solitaire_web/ # Bevy WASM canvas build for the browser /play route
├── solitaire_assetgen/ # One-shot generator for card/background PNG assets
└── solitaire_app/ # Main binary entry point (desktop + Android cdylib)
```
---
@@ -96,18 +100,30 @@ ferrous_solitaire/
## 3. Crate Responsibilities
### `solitaire_core`
**Dependencies:** `rand`, `serde`, `chrono` only.
**Dependencies:** `serde`, `thiserror`, plus the upstream `card_game` and
`klondike` crates (pinned via the Quaternions registry — never edit upstream).
The entire game rules engine. No Bevy, no network, no file I/O. Designed to be tested in isolation with `cargo test -p solitaire_core`.
The game rules layer. No Bevy, no network, no file I/O. Designed to be tested
in isolation with `cargo test -p solitaire_core`.
Since the card_game migration (2026-06-22, PR #88) the primitive types are
**upstream**: `Card`, `Deck`, `Suit`, `Rank`, `Session` come from `card_game`;
`Klondike`, `KlondikePile`, `KlondikeInstruction`, `DrawStockConfig`,
`Foundation`, `Tableau` come from `klondike`. `solitaire_core` re-exports them
so downstream crates import from one place and never depend on the upstream
crates directly.
Owns:
- All game data models (`Card`, `Suit`, `Rank`, `Pile`, `GameState`)
- Move validation logic
- Scoring engine
- Undo stack
- `GameState` — a wrapper around the upstream `Session<Klondike>`; the session
is the single source of truth for board state and stats
- `MoveError` and the `Result`-based mutation API
- `KlondikeConfig` adaptation (`klondike_adapter`) — draw mode, scoring,
take-from-foundation
- `GameMode` (Classic / Zen / Challenge / TimeAttack) and mode-aware scoring
- Solvability check API (`SolveOutcome`, delegating to `Session::solve`)
- Win / auto-complete detection
- Achievement unlock condition evaluation
- Seeded RNG for reproducible deals
- Seeded deals (same seed ⇒ same layout, via the upstream dealer)
**Rules decisions:**
- **Stock recycling is unlimited in every draw mode — by design.** Extra
@@ -189,9 +205,13 @@ Owns:
Because `ReplayPlayer` uses the same `solitaire_core::GameState` as the desktop client, the two implementations cannot drift: the same seed + move list produces identical pile state at every step on both platforms.
### `solitaire_app`
**Dependencies:** `bevy`, `solitaire_engine`.
**Dependencies:** `bevy`, `solitaire_engine`, `solitaire_data` (+ `jni` on Android).
Thin binary entry point. Registers all Bevy plugins and sets initial window properties.
Thin entry point (desktop binary + Android `cdylib`). Registers all Bevy
plugins and sets initial window properties. The one crate in the workspace
allowed `unsafe`: the Android entry point reconstructs the raw JNI handles and
hands them to the safe `solitaire_data::android_jni` bridge; everything else
is `forbid(unsafe_code)`.
---
@@ -559,26 +579,32 @@ This ensures all players worldwide get the same challenge for a given date, rega
### Core Game Models (`solitaire_core`)
Since the card_game migration, the primitives are upstream types re-exported
through `solitaire_core`:
```rust
pub enum Suit { Clubs, Diamonds, Hearts, Spades }
pub enum Rank { Ace, Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King }
// From `card_game` (upstream — never edit):
pub enum Suit { /* Clubs, Diamonds, Hearts, Spades */ }
pub enum Rank { /* Ace ..= King */ }
pub struct Card { /* deck + suit + rank; identity type, no face_up flag —
facing is positional, tracked by the Klondike board */ }
pub struct Session<G> { /* replayable instruction log + derived stats */ }
pub struct Card {
pub id: u32,
pub suit: Suit,
pub rank: Rank,
pub face_up: bool,
}
pub enum PileType {
// From `klondike` (upstream — never edit):
pub enum KlondikePile {
Stock,
Waste,
Foundation(Suit),
Tableau(usize), // 06
Foundation(Foundation), // 4 slots, any suit may claim any slot
Tableau(Tableau), // 7 columns
}
pub enum DrawStockConfig { DrawOne, DrawThree }
pub enum KlondikeInstruction { /* RotateStock, DstFoundation, ... — the
serialized move format (schema v4+) */ }
```
pub enum DrawMode { DrawOne, DrawThree }
Owned by `solitaire_core`:
```rust
/// Active game mode. Classic is the default; others unlock at level 5.
pub enum GameMode { Classic, Zen, Challenge, TimeAttack }
@@ -589,24 +615,30 @@ pub enum MoveError {
RuleViolation(String),
UndoStackEmpty,
GameAlreadyWon,
StockEmpty,
}
pub struct GameState {
pub piles: HashMap<PileType, Vec<Card>>,
pub draw_mode: DrawMode,
pub mode: GameMode,
pub score: i32,
pub move_count: u32,
pub undo_count: u32, // number of undos used in this game
pub recycle_count: u32, // number of stock recycles
pub elapsed_seconds: u64,
pub seed: u64,
pub is_won: bool,
pub is_auto_completable: bool,
undo_stack: VecDeque<StateSnapshot>, // private, max 64 (VecDeque for O(1) pop_front)
pub seed: u64, // same seed ⇒ same deal
pub take_from_foundation: bool,
session: Session<Klondike>, // private — the single source of truth
}
```
**Derived, not stored:** `score()`, `move_count()`, `undo_count()`,
`recycle_count()`, `is_won()`, `is_auto_completable()`, and all pile
accessors read through the session. Undo replays the instruction log
(no snapshot stack); the 15 undo penalty is applied by the upstream
score formula via the session config. Persistence (schema v5) saves
`saved_moves` as upstream `KlondikeInstruction`s and rebuilds the
session by replay on load — older files carrying `score`/`undo_count`/
`recycle_count` keys load fine, the extra fields are ignored.
**Rules decision:** stock recycling is unlimited in every draw mode
(see the "Rules decisions" note in §3 `solitaire_core`).
### Persistence Models (`solitaire_data`)
```rust
@@ -644,7 +676,7 @@ pub struct AchievementRecord {
}
pub struct Settings {
pub draw_mode: DrawMode,
pub draw_mode: DrawStockConfig,
pub sfx_volume: f32, // 0.01.0
pub music_volume: f32,
pub animation_speed: AnimSpeed,
+18
View File
@@ -6,6 +6,24 @@ project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [0.41.1] — 2026-07-06
### Fixed
- **Oversized pile-marker frames after fold/unfold.** `on_window_resized`
resized the marker fill sprite but never its children, so the outline frame
and the "A"/"K" watermark kept their spawn-time size after any resize —
rendering as oversized grey slabs over empty foundation slots on foldables
(found during Galaxy Fold 7 on-device verification of v0.41.0, fixed and
re-verified on the same device). Both children are now re-derived from the
new layout on every relayout.
### Added
- **Android relayout diagnostics.** Every layout recompute on Android now
logs its window dimensions and insets (`layout: resize to WxH …`) — the
evidence channel for foldable layout reports (#130).
## [0.41.0] — 2026-07-06
> Consolidates everything shipped since v0.39.0, including the v0.40.0v0.40.3
+3 -1
View File
@@ -30,7 +30,9 @@ 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
solitaire_web/ # Bevy WASM canvas build for the browser /play route
solitaire_assetgen/ # One-shot card/background PNG asset generator
solitaire_app/ # Entry binary (desktop + Android cdylib)
assets/ # Runtime assets (except audio + default theme)
```
+16
View File
@@ -156,3 +156,19 @@ opt-level = 3
[profile.release]
opt-level = 3
lto = "thin"
# Size-focused profile for the browser canvas build (solitaire_web →
# canvas_bg.wasm). Download size is the constraint on the web, not peak
# throughput: fat LTO + one codegen unit + opt-level "s" cut the Bevy wasm
# bundle substantially versus plain release. This is rustc-side sizing only —
# the binaryen pass in build_wasm.sh stays at wasm-opt -O2 because -Oz has
# miscompiled Bevy's render pipeline before (grey screen on first load).
[profile.wasm-release]
inherits = "release"
opt-level = "s"
lto = "fat"
codegen-units = 1
# No `strip`: on wasm it also removes the target_features custom section,
# which makes wasm-opt reject the module ("all used features should be
# allowed" on trunc_sat). wasm-opt drops the name section in its output
# anyway, so strip buys nothing here.
+74 -30
View File
@@ -1,16 +1,57 @@
# Ferrous Solitaire — Session Handoff
**Last updated:** 2026-06-25 — v0.40.0 released (Android APK published); physical-device gate remains.
**Last updated:** 2026-07-06 — v0.41.0 + v0.41.1 released and verified on a
physical Galaxy Fold 7; issue tracker empty except low-priority #130.
---
## Current state
- **Branch state:** `master` pushed to origin; latest commits are the Draw-Three waste fan fix, its regression tests, and the NDK doc update (PRs #105#108).
- **Latest tag:** `v0.40.0` (released — signed arm64-v8a APK published to the Gitea release for Obtainium/sideload). `v0.39.1` was the prior published release.
- **Working tree:** clean. Local `scripts/` helpers (incl. `scripts/watch_deploy.sh`) are intentionally not committed.
- **Latest verification this session:** `cargo clippy --workspace --all-targets -- -D warnings`; `cargo test --workspace`; `cargo build -p solitaire_app`; Android cross-compile + clippy for `aarch64-linux-android` (clean); full local signed arm64-v8a APK via `scripts/build_android_apk.sh`; CI `android-release` for `v0.40.0` completed/success with APK download HTTP 200.
- **Full previous gate:** card_game work pushed to origin with `cargo test` / `clippy` gates passing.
- **Branch state:** `master` pushed to origin; latest work is the 2026-07-06
arc (PRs #121#131): scripted repo review, five issues filed and fixed,
plugin module splits, two releases.
- **Latest tags:** `v0.41.1` (pile-marker child-resize fix + Android relayout
logging) on top of `v0.41.0` (consolidated release for everything since
v0.39.0). Both released via tag push → CI signed APK; both verified
installed on hardware (`versionCode 4101`).
- **Working tree:** clean. Local `scripts/*.go` helpers are intentionally
gitignored (`.gitignore:43`); `scripts/watch_deploy.sh` is now committed.
- **Latest verification:** workspace clippy `--all-targets -D warnings`,
full test suite, `cargo ndk` clippy for `aarch64-linux-android`, CI release
builds green, and an on-device pass on the Fold 7 (fold/unfold layout,
safe-area resume, marker fix).
- **Issue tracker:** #116/#117/#118/#119/#120 all closed 2026-07-06. #130
(transient tableau clip after fold) open at low priority — did not
reproduce in repeat testing; v0.41.1's relayout logging is the evidence
channel if it recurs.
---
## 2026-07-06 session summary (v0.40.3 → v0.41.1)
- **Scripted repo review** (cratemap/todoctx/cargoclip/testfail): clippy
clean, tests green, error/SQL policies compliant. Five issues filed and
all resolved same-day.
- **#116 safe-area re-poll after resume** (PR #121): `refresh_insets` now
gates on the poll counter, settles per cycle, and rewrites insets only on
change. Verified on Fold 7 — note both Fold screens report identical
insets (top=110 bottom=0), so the re-poll path is a no-op on this device.
- **#117 Draw-1 recycle** (PR #122): unlimited recycling documented as an
intentional rules decision (ARCHITECTURE.md "Rules decisions" +
`draw_one_recycling_is_unlimited_by_design` lock-in test). A hard limit
would invalidate the difficulty seed catalog and the winnable-deal solver.
- **#118 module splits** (PRs #124/#125/#127/#128/#129): card, hud,
settings, game, and input plugins are now module directories; tests in
sibling `tests.rs`, runtime code split along system boundaries
(`pub(super)` items, mod.rs glob-imports children). Largest runtime file
is now `card_plugin/sync.rs` at 748 lines (was `card_plugin.rs` at 4,129).
- **v0.41.1 pile-marker fix** (PR #131): marker outline + "A"/"K" watermark
children are re-derived from the layout on every resize — previously
spawn-time-sized, rendering as oversized grey slabs on empty foundations
after fold/unfold. Found via photo evidence, fixed, re-verified on device.
- **Obtainium note:** reported "no suitable release" for v0.41.0 even though
the anonymous releases API, `releases/latest`, and the APK download were
all verified fine — client-side issue; sideload via adb was used instead.
---
@@ -127,32 +168,17 @@ Three bugs fixed:
## Open punch list
### 1. Physical-device smoke test — THE ONLY REMAINING v0.40.0 ITEM
### 1. Physical-device smoke test — DONE (2026-07-06, Galaxy Fold 7)
This is the **single outstanding task** for the v0.40.0 Android release. Everything
else is done and verified: workspace gates, `aarch64-linux-android` cross-compile +
clippy, release manifest sanity, a full local signed APK, the published release, and
Obtainium-facing delivery (public releases API, latest non-draft release, APK
downloadable anonymously). The only thing that cannot be done without hardware is
running the app on a real phone.
v0.41.1 installed via adb and the full device checklist passed on hardware:
fold/unfold layout on both screens (incl. the #116 resume path and the
pile-marker fix), safe-area inset resolution, Draw-Three waste fan tap
accuracy (#106), modal centring on both screens, drag-and-drop across all
pile types, text rendering, kill-and-restore, and the sync token flow.
Reminder for future gates: AVD is not a substitute — `adb shell input tap`
doesn't deliver real touch events.
Install the published APK on a real Android device (not AVD) and run the checklist
in `docs/ANDROID.md §4`. This has never been gated in CI — AVD `adb shell input tap`
doesn't deliver real touch events, so physical-device smoke testing is the only gate.
The signed release APK is published (grab it from the release page, or use the local
`target/debug/apk/ferrous-solitaire.apk`). When testing, specifically exercise the
Draw-Three waste fan fixed in #106: switch to Draw-Three, draw several cards, and
confirm dragging the visible top waste card plays *that* card, not the one beneath it.
Latest AVD smoke (2026-06-08 local / 2026-06-09 UTC): built
`target/debug/apk/ferrous-solitaire.apk` for `x86_64-linux-android`, installed
it on AVD `Pixel_7`, launched `android.app.NativeActivity`, confirmed Bevy
rendered the board, safe-area insets resolved as `top=136 bottom=63 left=0
right=0` after 2 frames, onboarding could be dismissed via AVD input, and
filtered logcat showed no Ferrous panic/fatal/ANR.
### 2. Matomo analytics live validation (independent — NOT a v0.40.0 release blocker)
### 2. Matomo analytics live validation (independent — NOT a release blocker)
Separate, ongoing task unrelated to the Android release. `Settings` has
`analytics_enabled`, `matomo_url`, and `matomo_site_id`; the engine consumes them via
@@ -164,6 +190,24 @@ validation checklist and the current web/WASM decision notes.
## Architectural notes for next session
- **Plugin submodule pattern (2026-07-06 splits):** big plugins are module
directories: `mod.rs` holds types/markers/plugin-build and glob-imports the
children (`use input::*;`); children hold `pub(super)` systems and start
with `use super::*;` plus their own external imports. Tests (`tests.rs`)
may need explicit imports for names no longer used by `mod.rs` itself.
- **Marker child-resize rule:** anything spawned as a *child* of a
layout-sized entity (outline frames, watermark text) must be re-derived in
`on_window_resized` too — resizing only the parent sprite leaves children
at spawn-time size (the v0.41.1 foldable bug).
- **Fold 7 quirks:** both screens report identical safe-area insets
(top=110, bottom=0), so inset-driven relayout never fires on fold; layout
correctness across folds rides entirely on `WindowResized`. winit 0.30
logs `TODO: find a way to notify application of content rect change` on
resume — see #130 if a stale-width layout ever reproduces; v0.41.1 logs
every Android relayout (`layout: resize to WxH`) for exactly this.
- **Reduce-motion pattern:** always gate in the `start_*` / `detect_*` system
(the trigger), not the `tick_*` system. If the component is never inserted, the
tick path never runs. See `hud_plugin.rs::detect_score_change` and
+4 -2
View File
@@ -67,7 +67,9 @@ if ! command -v wasm-bindgen &> /dev/null; then
fi
echo "Building solitaire_web (Bevy WASM app)..."
cargo build --release --target wasm32-unknown-unknown -p solitaire_web
# wasm-release is the size-focused profile (fat LTO, CGU=1, opt-level "s") —
# see [profile.wasm-release] in Cargo.toml. Download size is the constraint.
cargo build --profile wasm-release --target wasm32-unknown-unknown -p solitaire_web
echo "Running wasm-bindgen for solitaire_web..."
wasm-bindgen \
@@ -75,7 +77,7 @@ wasm-bindgen \
--out-name canvas \
--target web \
--no-typescript \
"$REPO_ROOT/target/wasm32-unknown-unknown/release/solitaire_web.wasm"
"$REPO_ROOT/target/wasm32-unknown-unknown/wasm-release/solitaire_web.wasm"
# Optional size optimisation — Bevy bundles are large (~5-15 MB uncompressed).
# wasm-opt passes are skipped silently when the tool is not installed.
+12 -12
View File
@@ -1649,62 +1649,62 @@ function __wbg_get_imports() {
return ret;
},
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 114842, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 114880, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hfb2e9a2f0bbd9ecc);
return ret;
},
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a);
return ret;
},
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array<any>"), NamedExternref("ResizeObserver")], shim_idx: 9828, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array<any>"), NamedExternref("ResizeObserver")], shim_idx: 9866, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h876550298b312ff8);
return ret;
},
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array<any>")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Array<any>")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_3);
return ret;
},
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_4);
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("FocusEvent")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("FocusEvent")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_5);
return ret;
},
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_6);
return ret;
},
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PageTransitionEvent")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PageTransitionEvent")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_7);
return ret;
},
__wbindgen_cast_0000000000000009: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("PointerEvent")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_8);
return ret;
},
__wbindgen_cast_000000000000000a: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WheelEvent")], shim_idx: 9815, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("WheelEvent")], shim_idx: 9853, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h15aa57dbc666225a_9);
return ret;
},
__wbindgen_cast_000000000000000b: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Option(NamedExternref("Blob"))], shim_idx: 9825, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Option(NamedExternref("Blob"))], shim_idx: 9863, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h657f46feffff6fe4);
return ret;
},
__wbindgen_cast_000000000000000c: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 9819, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 9857, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h545edb23183e448a);
return ret;
},
Binary file not shown.