feat(web): add solitaire_web Bevy WASM build targeting play.html canvas
Build and Deploy / build-and-push (push) Failing after 58s
Build and Deploy / build-and-push (push) Failing after 58s
Adds a new `solitaire_web` crate that compiles the full `solitaire_engine` to `wasm32-unknown-unknown` and renders to a `<canvas id="bevy-canvas">` element in `play.html` — the same ECS code path as desktop and Android. Changes to enable the WASM target: - .cargo/config.toml: add wasm32-unknown-unknown rustflags for getrandom - Workspace Cargo.toml: add solitaire_web member - solitaire_data/Cargo.toml: gate tokio/reqwest/dirs/keyring to non-wasm - solitaire_data/src: add wasm32 branch to data_dir() (returns None); cfg-gate sync_client network types, auth_tokens, matomo_client - solitaire_engine/Cargo.toml: gate tokio/reqwest/kira/arboard/dirs/zip to non-wasm (mio/cpal/arboard don't compile for wasm32-unknown-unknown) - solitaire_engine/src/lib.rs: cfg-gate module declarations and re-exports for analytics, audio, sync, sync_setup, avatar, leaderboard plugins - solitaire_engine/src/core_game_plugin.rs: cfg-gate plugin registrations that require TokioRuntime (audio, sync, analytics, leaderboard, avatar) - solitaire_engine/src/resources.rs: cfg-gate TokioRuntimeResource - solitaire_engine/src/game_plugin.rs: cfg-gate std::fs::remove_file (x10) - solitaire_engine/src/theme/mod.rs: cfg-gate importer module (uses dirs+zip) - solitaire_engine/src/settings_plugin.rs: cfg-gate theme ZIP import UI - solitaire_engine/src/assets/sources.rs: cfg-gate FileAssetReader/user_theme_dir - solitaire_engine/src/auto_complete_plugin.rs: cfg-gate audio system - solitaire_engine/src/daily_challenge_plugin.rs: cfg-gate server fetch - solitaire_engine/src/hud_plugin.rs: cfg-gate AvatarResource import - solitaire_engine/src/profile_plugin.rs: cfg-gate AvatarResource import - solitaire_server/web/play.html: minimal HTML canvas shell - solitaire_web/: new crate (Cargo.toml + src/lib.rs) - build_wasm.sh: add Bevy WASM build step (cargo + wasm-bindgen + wasm-opt) All tests pass; clippy --workspace -- -D warnings clean; native build (solitaire_engine, solitaire_app) unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
[registries.Quaternions]
|
||||
index = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
|
||||
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||
|
||||
Generated
+283
@@ -723,6 +723,28 @@ dependencies = [
|
||||
"android-activity",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_anti_alias"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "726cc494eb7d6a84ce6291c23636fd451fa4846604dc059fa93febca4e60a928"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_derive",
|
||||
"bevy_diagnostic",
|
||||
"bevy_ecs",
|
||||
"bevy_image",
|
||||
"bevy_math",
|
||||
"bevy_reflect",
|
||||
"bevy_render",
|
||||
"bevy_shader",
|
||||
"bevy_utils",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_app"
|
||||
version = "0.18.1"
|
||||
@@ -884,6 +906,35 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_dev_tools"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4f1464a3f5ef5c23d917987714ee89881f9f791e9ff97ecf6600ee846b9569e"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_color",
|
||||
"bevy_diagnostic",
|
||||
"bevy_ecs",
|
||||
"bevy_image",
|
||||
"bevy_input",
|
||||
"bevy_math",
|
||||
"bevy_picking",
|
||||
"bevy_reflect",
|
||||
"bevy_render",
|
||||
"bevy_shader",
|
||||
"bevy_state",
|
||||
"bevy_text",
|
||||
"bevy_time",
|
||||
"bevy_transform",
|
||||
"bevy_ui",
|
||||
"bevy_ui_render",
|
||||
"bevy_window",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_diagnostic"
|
||||
version = "0.18.1"
|
||||
@@ -951,6 +1002,36 @@ dependencies = [
|
||||
"encase_derive_impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_feathers"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cb29be8f8443c5cc44e1c4710bbe02877e73703c60228ca043f20529a5496c6"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"bevy_a11y",
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_color",
|
||||
"bevy_derive",
|
||||
"bevy_ecs",
|
||||
"bevy_input_focus",
|
||||
"bevy_log",
|
||||
"bevy_math",
|
||||
"bevy_picking",
|
||||
"bevy_platform",
|
||||
"bevy_reflect",
|
||||
"bevy_render",
|
||||
"bevy_shader",
|
||||
"bevy_text",
|
||||
"bevy_ui",
|
||||
"bevy_ui_render",
|
||||
"bevy_ui_widgets",
|
||||
"bevy_window",
|
||||
"smol_str",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_gizmos"
|
||||
version = "0.18.1"
|
||||
@@ -1073,14 +1154,17 @@ checksum = "6a11df62e49897def470471551c02f13c6fb488e55dddb5ab7ef098132e07754"
|
||||
dependencies = [
|
||||
"bevy_a11y",
|
||||
"bevy_android",
|
||||
"bevy_anti_alias",
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_color",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_derive",
|
||||
"bevy_dev_tools",
|
||||
"bevy_diagnostic",
|
||||
"bevy_ecs",
|
||||
"bevy_feathers",
|
||||
"bevy_gizmos_render",
|
||||
"bevy_image",
|
||||
"bevy_input",
|
||||
@@ -1088,6 +1172,7 @@ dependencies = [
|
||||
"bevy_log",
|
||||
"bevy_math",
|
||||
"bevy_mesh",
|
||||
"bevy_pbr",
|
||||
"bevy_platform",
|
||||
"bevy_ptr",
|
||||
"bevy_reflect",
|
||||
@@ -1107,6 +1192,27 @@ dependencies = [
|
||||
"bevy_winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_light"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d9d2ac64390a9baacb3c0fa0f5456ac1553959d5a387874c102a09aab8b92cc"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_color",
|
||||
"bevy_ecs",
|
||||
"bevy_image",
|
||||
"bevy_math",
|
||||
"bevy_mesh",
|
||||
"bevy_platform",
|
||||
"bevy_reflect",
|
||||
"bevy_transform",
|
||||
"bevy_utils",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_log"
|
||||
version = "0.18.1"
|
||||
@@ -1167,7 +1273,9 @@ dependencies = [
|
||||
"bevy_asset",
|
||||
"bevy_derive",
|
||||
"bevy_ecs",
|
||||
"bevy_image",
|
||||
"bevy_math",
|
||||
"bevy_mikktspace",
|
||||
"bevy_platform",
|
||||
"bevy_reflect",
|
||||
"bevy_transform",
|
||||
@@ -1180,6 +1288,71 @@ dependencies = [
|
||||
"wgpu-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_mikktspace"
|
||||
version = "0.17.0-dev"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ef8e4b7e61dfe7719bb03c884dc270cd46a82efb40f93e9933b990c5c190c59"
|
||||
|
||||
[[package]]
|
||||
name = "bevy_pbr"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5ab6944ffc6fd71604c0fbca68cc3e2a3654edfcdbfd232f9d8b88e3d20fdc0"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_color",
|
||||
"bevy_core_pipeline",
|
||||
"bevy_derive",
|
||||
"bevy_diagnostic",
|
||||
"bevy_ecs",
|
||||
"bevy_image",
|
||||
"bevy_light",
|
||||
"bevy_log",
|
||||
"bevy_math",
|
||||
"bevy_mesh",
|
||||
"bevy_platform",
|
||||
"bevy_reflect",
|
||||
"bevy_render",
|
||||
"bevy_shader",
|
||||
"bevy_transform",
|
||||
"bevy_utils",
|
||||
"bitflags 2.11.1",
|
||||
"bytemuck",
|
||||
"derive_more",
|
||||
"fixedbitset",
|
||||
"nonmax",
|
||||
"offset-allocator",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_picking"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7d524dbc8f2c9e73f7ab70c148c8f7886f3c24b8aa8c252a38ba68ed06cbf10"
|
||||
dependencies = [
|
||||
"bevy_app",
|
||||
"bevy_asset",
|
||||
"bevy_camera",
|
||||
"bevy_derive",
|
||||
"bevy_ecs",
|
||||
"bevy_input",
|
||||
"bevy_math",
|
||||
"bevy_platform",
|
||||
"bevy_reflect",
|
||||
"bevy_time",
|
||||
"bevy_transform",
|
||||
"bevy_window",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_platform"
|
||||
version = "0.18.1"
|
||||
@@ -1506,6 +1679,7 @@ dependencies = [
|
||||
"bevy_input",
|
||||
"bevy_input_focus",
|
||||
"bevy_math",
|
||||
"bevy_picking",
|
||||
"bevy_platform",
|
||||
"bevy_reflect",
|
||||
"bevy_sprite",
|
||||
@@ -1518,6 +1692,7 @@ dependencies = [
|
||||
"taffy",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1551,6 +1726,26 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_ui_widgets"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6a63cb818b0de41bdb14990e0ce1aaaa347f871750ab280f80c427e83d72712"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"bevy_a11y",
|
||||
"bevy_app",
|
||||
"bevy_camera",
|
||||
"bevy_ecs",
|
||||
"bevy_input",
|
||||
"bevy_input_focus",
|
||||
"bevy_log",
|
||||
"bevy_math",
|
||||
"bevy_picking",
|
||||
"bevy_reflect",
|
||||
"bevy_ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_utils"
|
||||
version = "0.18.1"
|
||||
@@ -1678,6 +1873,7 @@ version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
@@ -3483,6 +3679,17 @@ dependencies = [
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gl_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d"
|
||||
dependencies = [
|
||||
"khronos_api",
|
||||
"log",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glam"
|
||||
version = "0.30.10"
|
||||
@@ -3511,6 +3718,27 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"slotmap",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glutin_wgl_sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e"
|
||||
dependencies = [
|
||||
"gl_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.10.4"
|
||||
@@ -4335,6 +4563,23 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libloading",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos_api"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "kira"
|
||||
version = "0.12.0"
|
||||
@@ -7146,6 +7391,18 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solitaire_web"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"console_error_panic_hook",
|
||||
"getrandom 0.3.4",
|
||||
"solitaire_data",
|
||||
"solitaire_engine",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -9106,6 +9363,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.16.1",
|
||||
"js-sys",
|
||||
"log",
|
||||
"naga",
|
||||
"portable-atomic",
|
||||
@@ -9113,6 +9371,8 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"wgpu-core",
|
||||
"wgpu-hal",
|
||||
"wgpu-types",
|
||||
@@ -9144,6 +9404,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"wgpu-core-deps-apple",
|
||||
"wgpu-core-deps-wasm",
|
||||
"wgpu-core-deps-windows-linux-android",
|
||||
"wgpu-hal",
|
||||
"wgpu-types",
|
||||
@@ -9158,6 +9419,15 @@ dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-wasm"
|
||||
version = "27.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b1027dcf3b027a877e44819df7ceb0e2e98578830f8cd34cd6c3c7c2a7a50b7"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wgpu-core-deps-windows-linux-android"
|
||||
version = "27.0.0"
|
||||
@@ -9183,15 +9453,20 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"core-graphics-types 0.2.0",
|
||||
"glow",
|
||||
"glutin_wgl_sys",
|
||||
"gpu-alloc",
|
||||
"gpu-allocator",
|
||||
"gpu-descriptor",
|
||||
"hashbrown 0.16.1",
|
||||
"js-sys",
|
||||
"khronos-egl",
|
||||
"libc",
|
||||
"libloading",
|
||||
"log",
|
||||
"metal",
|
||||
"naga",
|
||||
"ndk-sys",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"ordered-float",
|
||||
@@ -9204,6 +9479,8 @@ dependencies = [
|
||||
"renderdoc-sys",
|
||||
"smallvec",
|
||||
"thiserror 2.0.18",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"wgpu-types",
|
||||
"windows 0.58.0",
|
||||
"windows-core 0.58.0",
|
||||
@@ -10086,6 +10363,12 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "xmlwriter"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"solitaire_app",
|
||||
"solitaire_assetgen",
|
||||
"solitaire_wasm",
|
||||
"solitaire_web",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
+43
-7
@@ -1,18 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
# Rebuild the solitaire_wasm crate and install the output into
|
||||
# solitaire_server/web/pkg/ so the server can serve the replay viewer.
|
||||
# Rebuild WASM artifacts and install them into solitaire_server/web/pkg/.
|
||||
#
|
||||
# Two artifacts are produced:
|
||||
# solitaire_wasm.* — thin replay-viewer + interactive JS API (wasm-pack)
|
||||
# canvas.* — full Bevy WASM app for play.html (cargo + wasm-bindgen)
|
||||
#
|
||||
# Prerequisites:
|
||||
# cargo install wasm-pack
|
||||
# cargo install wasm-pack wasm-bindgen-cli
|
||||
# rustup target add wasm32-unknown-unknown
|
||||
# (optional) cargo install wasm-opt # for smaller canvas_bg.wasm
|
||||
#
|
||||
# Run from the repo root:
|
||||
# ./build_wasm.sh
|
||||
#
|
||||
# The generated files (solitaire_wasm.js + solitaire_wasm_bg.wasm) are
|
||||
# committed to git so self-hosters who don't touch the WASM crate can
|
||||
# skip this step. Regenerate after any change to solitaire_wasm/ or
|
||||
# solitaire_core/.
|
||||
# The generated pkg/ files are committed to git so self-hosters who don't
|
||||
# touch the WASM crates can skip this step. Regenerate after any change to
|
||||
# solitaire_wasm/, solitaire_web/, solitaire_engine/, or solitaire_core/.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -36,5 +39,38 @@ wasm-pack build \
|
||||
# Remove them — we manage the output directory ourselves.
|
||||
rm -f "$OUT_DIR/package.json" "$OUT_DIR/.gitignore"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bevy WASM app (solitaire_web → canvas.js + canvas_bg.wasm)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if ! command -v wasm-bindgen &> /dev/null; then
|
||||
echo "error: wasm-bindgen not found." >&2
|
||||
echo " Install with: cargo install wasm-bindgen-cli" >&2
|
||||
echo " The CLI version must match the wasm-bindgen crate dep." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building solitaire_web (Bevy WASM app)..."
|
||||
cargo build --release --target wasm32-unknown-unknown -p solitaire_web
|
||||
|
||||
echo "Running wasm-bindgen for solitaire_web..."
|
||||
wasm-bindgen \
|
||||
--out-dir "$OUT_DIR" \
|
||||
--out-name canvas \
|
||||
--target web \
|
||||
"$REPO_ROOT/target/wasm32-unknown-unknown/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.
|
||||
if command -v wasm-opt &> /dev/null; then
|
||||
echo "Running wasm-opt on canvas_bg.wasm..."
|
||||
wasm-opt -Oz \
|
||||
-o "$OUT_DIR/canvas_bg.wasm" \
|
||||
"$OUT_DIR/canvas_bg.wasm"
|
||||
else
|
||||
echo "note: wasm-opt not found; skipping size optimisation."
|
||||
echo " Install with: cargo install wasm-opt (or via binaryen)"
|
||||
fi
|
||||
|
||||
echo "Done. Output:"
|
||||
ls -lh "$OUT_DIR"
|
||||
|
||||
@@ -12,11 +12,17 @@ serde_json = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
klondike = { workspace = true }
|
||||
|
||||
# These deps are not available / not needed on wasm32:
|
||||
# dirs — platform data directories (no filesystem on browser)
|
||||
# reqwest — native HTTP client (sync/analytics gated out on wasm32)
|
||||
# tokio — OS-threaded async runtime (mio doesn't compile on wasm32)
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
dirs = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
klondike = { workspace = true }
|
||||
|
||||
# `keyring-core` is the typed Entry/Error API used by
|
||||
# `auth_tokens`. The crate's own dependency tree pulls in
|
||||
@@ -25,7 +31,7 @@ klondike = { workspace = true }
|
||||
# on bionic). On Android `auth_tokens` falls back to a stub
|
||||
# implementation that always returns `KeychainUnavailable`; the
|
||||
# real backend lands when we wire Android Keystore via JNI.
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
[target.'cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies]
|
||||
keyring-core = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
|
||||
@@ -146,13 +146,17 @@ pub use settings::{
|
||||
#[cfg(target_os = "android")]
|
||||
mod android_keystore;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod auth_tokens;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use auth_tokens::{
|
||||
TokenError, delete_tokens, load_access_token, load_refresh_token, store_tokens,
|
||||
};
|
||||
|
||||
pub mod sync_client;
|
||||
pub use sync_client::{LocalOnlyProvider, SolitaireServerClient, provider_for_backend};
|
||||
pub use sync_client::LocalOnlyProvider;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use sync_client::{SolitaireServerClient, provider_for_backend};
|
||||
|
||||
pub mod replay;
|
||||
pub use replay::{
|
||||
@@ -163,7 +167,9 @@ pub use replay::{
|
||||
#[allow(deprecated)]
|
||||
pub use replay::{latest_replay_path, load_latest_replay_from, save_latest_replay_to};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod matomo_client;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use matomo_client::MatomoClient;
|
||||
|
||||
pub mod platform;
|
||||
|
||||
@@ -55,7 +55,15 @@ pub fn data_dir() -> Option<PathBuf> {
|
||||
{
|
||||
Some(PathBuf::from(ANDROID_APP_FILES_DIR))
|
||||
}
|
||||
#[cfg(not(target_os = "android"))]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
// No filesystem on the browser; all persistence goes through
|
||||
// WasmStorage (localStorage-backed). Return None so every caller
|
||||
// degrades gracefully (the same path they take on a
|
||||
// misconfigured desktop environment).
|
||||
None
|
||||
}
|
||||
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
|
||||
{
|
||||
dirs::data_dir()
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
use async_trait::async_trait;
|
||||
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
||||
|
||||
use crate::{SyncError, SyncProvider};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::{
|
||||
SyncError, SyncProvider,
|
||||
auth_tokens::{load_access_token, load_refresh_token, store_tokens},
|
||||
replay::Replay,
|
||||
settings::SyncBackend,
|
||||
@@ -54,12 +56,17 @@ impl SyncProvider for LocalOnlyProvider {
|
||||
// ---------------------------------------------------------------------------
|
||||
// SolitaireServerClient
|
||||
// ---------------------------------------------------------------------------
|
||||
// Native-only: HTTP sync client and factory function.
|
||||
// On wasm32 these are gated out because reqwest uses native OS networking
|
||||
// (mio + hyper) which does not compile for wasm32-unknown-unknown.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// HTTP sync client for the self-hosted Ferrous Solitaire server.
|
||||
///
|
||||
/// Authenticates via JWT stored in the OS keychain. On a 401 response the
|
||||
/// client automatically attempts a token refresh and retries the request once
|
||||
/// before returning an error.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct SolitaireServerClient {
|
||||
/// Base URL of the server, e.g. `"https://solitaire.example.com"`.
|
||||
/// Trailing slashes are stripped on construction.
|
||||
@@ -70,6 +77,7 @@ pub struct SolitaireServerClient {
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SolitaireServerClient {
|
||||
/// Construct a new client for the given server URL and username.
|
||||
///
|
||||
@@ -201,6 +209,7 @@ impl SolitaireServerClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl SyncProvider for SolitaireServerClient {
|
||||
/// Fetch the latest sync payload from the server.
|
||||
@@ -486,6 +495,7 @@ impl SyncProvider for SolitaireServerClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl SolitaireServerClient {
|
||||
/// Pulled out of `push_replay` so both the first attempt and the
|
||||
/// post-401-retry attempt go through the same parse path.
|
||||
@@ -581,9 +591,10 @@ impl SolitaireServerClient {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response extraction helpers
|
||||
// Response extraction helpers (native-only, use reqwest::Response)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Deserialize a pull response body as [`SyncResponse`] and return its
|
||||
/// `merged` field, or map non-200 statuses to the appropriate [`SyncError`].
|
||||
///
|
||||
@@ -607,6 +618,7 @@ async fn extract_pull_body(resp: reqwest::Response) -> Result<SyncPayload, SyncE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Deserialize a leaderboard response body as `Vec<LeaderboardEntry>`.
|
||||
async fn extract_leaderboard_body(
|
||||
resp: reqwest::Response,
|
||||
@@ -621,6 +633,7 @@ async fn extract_leaderboard_body(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Deserialize a push response body as [`SyncResponse`], or map non-200
|
||||
/// statuses to the appropriate [`SyncError`].
|
||||
///
|
||||
@@ -652,6 +665,7 @@ async fn extract_push_body(resp: reqwest::Response) -> Result<SyncResponse, Sync
|
||||
/// This is the **one** place in the codebase that matches on [`SyncBackend`]
|
||||
/// variants. All other code receives a `Box<dyn SyncProvider + Send + Sync>`
|
||||
/// and remains backend-agnostic.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn provider_for_backend(backend: &SyncBackend) -> Box<dyn SyncProvider + Send + Sync> {
|
||||
match backend {
|
||||
SyncBackend::Local => Box::new(LocalOnlyProvider),
|
||||
|
||||
+15
-11
@@ -7,15 +7,12 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
bevy = { workspace = true }
|
||||
image = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
kira = { workspace = true }
|
||||
solitaire_core = { workspace = true }
|
||||
solitaire_data = { workspace = true }
|
||||
solitaire_sync = { workspace = true }
|
||||
klondike = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -23,17 +20,24 @@ usvg = { workspace = true }
|
||||
resvg = { workspace = true }
|
||||
tiny-skia = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
|
||||
# These deps are not available / not needed on wasm32:
|
||||
# reqwest — uses mio/hyper native networking (sync plugin is gated out)
|
||||
# kira — uses cpal OS audio (audio plugin is gated out)
|
||||
# tokio — multi-threaded runtime (TokioRuntimeResource is gated out)
|
||||
# dirs — platform data directories (storage uses WasmStorage instead)
|
||||
# zip — theme ZIP importer (importer is gated out on wasm32)
|
||||
# arboard — clipboard (no wasm backend; stats copy-link uses localStorage)
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
reqwest = { workspace = true }
|
||||
kira = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
# `arboard` provides clipboard access for the Stats overlay's
|
||||
# "Copy share link" button. The crate has no Android backend
|
||||
# (its `platform::Clipboard` module is unimplemented for the
|
||||
# android target — `cargo apk build` fails with E0433 if this is
|
||||
# left unconditional). On Android the same button surfaces an
|
||||
# informational toast instead; see
|
||||
# `stats_plugin::handle_copy_share_link_button`.
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
# `arboard` has no Android backend and no wasm32 backend. Gate it out for
|
||||
# both; the copy-share-link button surfaces an informational toast instead.
|
||||
[target.'cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies]
|
||||
arboard = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
|
||||
@@ -50,9 +50,11 @@
|
||||
use bevy::asset::AssetApp;
|
||||
use bevy::asset::io::AssetSourceBuilder;
|
||||
use bevy::asset::io::embedded::EmbeddedAssetRegistry;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy::asset::io::file::FileAssetReader;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::assets::user_dir::user_theme_dir;
|
||||
|
||||
/// `AssetSourceId` of the user-themes asset source. Use it as
|
||||
@@ -235,11 +237,16 @@ const CLASSIC_THEME_SVGS: &[(&str, &[u8])] = &[
|
||||
/// Returns the `&mut App` so the call can be chained from the binary
|
||||
/// entry point.
|
||||
pub fn register_theme_asset_sources(app: &mut App) -> &mut App {
|
||||
let root = user_theme_dir();
|
||||
app.register_asset_source(
|
||||
USER_THEMES,
|
||||
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
||||
);
|
||||
// User themes are stored on the filesystem; wasm32 has no filesystem and
|
||||
// `FileAssetReader` is not available on that target.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let root = user_theme_dir();
|
||||
app.register_asset_source(
|
||||
USER_THEMES,
|
||||
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
||||
);
|
||||
}
|
||||
app
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::RequestRedraw;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::audio_plugin::{AudioState, SoundLibrary};
|
||||
use crate::events::{MoveRequestEvent, StateChangedEvent};
|
||||
use crate::game_plugin::GameMutation;
|
||||
@@ -97,6 +98,7 @@ fn detect_auto_complete(
|
||||
/// exactly once on the `false → true` edge. The win fanfare is played at half
|
||||
/// volume (`AUTO_COMPLETE_CHIME_VOLUME`) so it is clearly recognisable but does
|
||||
/// not overwhelm the card-place sounds that follow immediately.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn on_auto_complete_start(
|
||||
state: Res<AutoCompleteState>,
|
||||
mut was_active: Local<bool>,
|
||||
@@ -117,6 +119,12 @@ fn on_auto_complete_start(
|
||||
audio.play_sfx_at_volume(&lib.fanfare, AUTO_COMPLETE_CHIME_VOLUME);
|
||||
}
|
||||
|
||||
// No audio on wasm — stub keeps the system registration unconditional.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn on_auto_complete_start(state: Res<AutoCompleteState>, mut was_active: Local<bool>) {
|
||||
*was_active = state.active;
|
||||
}
|
||||
|
||||
/// Fires one `MoveRequestEvent` per `STEP_INTERVAL` while auto-complete is active.
|
||||
fn drive_auto_complete(
|
||||
mut state: ResMut<AutoCompleteState>,
|
||||
|
||||
@@ -13,16 +13,18 @@ use crate::platform::{
|
||||
default_storage_backend,
|
||||
};
|
||||
use crate::{
|
||||
AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin, AudioPlugin,
|
||||
AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
||||
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
||||
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
||||
OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin,
|
||||
RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin, SafeAreaInsetsPlugin,
|
||||
SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncPlugin, SyncProvider,
|
||||
SyncSetupPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
||||
TouchSelectionPlugin, UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin,
|
||||
WinSummaryPlugin,
|
||||
AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, AutoCompletePlugin,
|
||||
CardAnimationPlugin, CardPlugin, ChallengePlugin, CursorPlugin, DailyChallengePlugin,
|
||||
DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin,
|
||||
HomePlugin, HudPlugin, InputPlugin, OnboardingPlugin, PausePlugin, PlayBySeedPlugin,
|
||||
ProfilePlugin, ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin,
|
||||
SafeAreaInsetsPlugin, SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncProvider,
|
||||
TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, TouchSelectionPlugin,
|
||||
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::{
|
||||
AnalyticsPlugin, AudioPlugin, AvatarPlugin, LeaderboardPlugin, SyncPlugin, SyncSetupPlugin,
|
||||
};
|
||||
|
||||
/// Groups all Ferrous Solitaire gameplay plugins.
|
||||
@@ -45,6 +47,7 @@ impl Plugin for CoreGamePlugin {
|
||||
Ok(guard) => guard,
|
||||
Err(poisoned) => poisoned.into_inner(),
|
||||
};
|
||||
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
|
||||
let sync_provider = sync_provider
|
||||
.take()
|
||||
.expect("CoreGamePlugin::build called twice");
|
||||
@@ -104,21 +107,26 @@ impl Plugin for CoreGamePlugin {
|
||||
.add_plugins(HudPlugin)
|
||||
.add_plugins(HelpPlugin)
|
||||
.add_plugins(HomePlugin::default())
|
||||
.add_plugins(AvatarPlugin)
|
||||
.add_plugins(ProfilePlugin)
|
||||
.add_plugins(PausePlugin)
|
||||
.add_plugins(SettingsPlugin::default())
|
||||
.add_plugins(AudioPlugin)
|
||||
.add_plugins(OnboardingPlugin)
|
||||
.add_plugins(SyncPlugin::new(sync_provider))
|
||||
.add_plugins(SyncSetupPlugin)
|
||||
.add_plugins(AnalyticsPlugin)
|
||||
.add_plugins(LeaderboardPlugin)
|
||||
.add_plugins(WinSummaryPlugin)
|
||||
.add_plugins(UiModalPlugin)
|
||||
.add_plugins(UiFocusPlugin)
|
||||
.add_plugins(UiTooltipPlugin)
|
||||
.add_plugins(SplashPlugin)
|
||||
.add_plugins(DiagnosticsHudPlugin);
|
||||
|
||||
// Plugins that use kira/cpal audio or multi-threaded Tokio are not
|
||||
// compatible with the single-threaded wasm32 runtime. Gate them out
|
||||
// so the browser build boots silently and without a sync backend.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
app.add_plugins(AvatarPlugin)
|
||||
.add_plugins(AudioPlugin)
|
||||
.add_plugins(SyncPlugin::new(sync_provider))
|
||||
.add_plugins(SyncSetupPlugin)
|
||||
.add_plugins(AnalyticsPlugin)
|
||||
.add_plugins(LeaderboardPlugin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use crate::events::{
|
||||
use crate::game_plugin::GameMutation;
|
||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||
use crate::resources::GameStateResource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::sync_plugin::SyncProviderResource;
|
||||
|
||||
/// Bonus XP awarded for completing today's daily challenge.
|
||||
@@ -116,17 +117,21 @@ impl Plugin for DailyChallengePlugin {
|
||||
.add_message::<StartDailyChallengeRequestEvent>()
|
||||
.add_message::<WarningToastEvent>()
|
||||
.add_message::<XpAwardedEvent>()
|
||||
.add_systems(Startup, fetch_server_challenge)
|
||||
.add_systems(Update, poll_server_challenge)
|
||||
// record/award after the base ProgressUpdate so we don't fight
|
||||
// ProgressPlugin's add_xp on the same frame.
|
||||
.add_systems(Update, handle_daily_completion.after(ProgressUpdate))
|
||||
.add_systems(Update, handle_start_daily_request.before(GameMutation))
|
||||
.add_systems(Update, check_daily_expiry_warning)
|
||||
.add_systems(Update, check_date_rollover);
|
||||
|
||||
// Server-challenge fetch uses SyncProviderResource (reqwest), not available on wasm.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
app.add_systems(Startup, fetch_server_challenge)
|
||||
.add_systems(Update, poll_server_challenge);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Startup system: spawns an async task to fetch the server's daily challenge.
|
||||
///
|
||||
/// Only runs when `SyncProviderResource` is present (i.e. `SyncPlugin` is
|
||||
@@ -142,6 +147,7 @@ fn fetch_server_challenge(
|
||||
task_res.0 = Some(task);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
/// Update system: polls the server-challenge fetch task.
|
||||
///
|
||||
/// On success, replaces the locally-computed seed in `DailyChallengeResource`
|
||||
|
||||
@@ -1512,6 +1512,7 @@ mod tests {
|
||||
use solitaire_data::load_game_state_from;
|
||||
|
||||
let path = tmp_gs_path("exit_save");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(7);
|
||||
@@ -1527,6 +1528,7 @@ mod tests {
|
||||
let loaded = load_game_state_from(&path).expect("file should exist after exit");
|
||||
assert_eq!(loaded.seed, 7654);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -1571,6 +1573,7 @@ mod tests {
|
||||
use solitaire_data::load_game_state_from;
|
||||
|
||||
let path = tmp_gs_path("auto_save_30s");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(42);
|
||||
@@ -1601,6 +1604,7 @@ mod tests {
|
||||
let loaded = load_game_state_from(&path).expect("file must be loadable");
|
||||
assert_eq!(loaded.seed, 42);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -1608,6 +1612,7 @@ mod tests {
|
||||
#[test]
|
||||
fn auto_save_skips_when_no_moves() {
|
||||
let path = tmp_gs_path("auto_save_skip");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(99);
|
||||
@@ -2165,6 +2170,7 @@ mod tests {
|
||||
use solitaire_data::load_replay_history_from;
|
||||
|
||||
let path = std::env::temp_dir().join("engine_test_replay_freeze.json");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(7654);
|
||||
@@ -2223,6 +2229,7 @@ mod tests {
|
||||
other => panic!("second entry must be a Move, got {other:?}"),
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -2234,6 +2241,7 @@ mod tests {
|
||||
use solitaire_data::load_replay_history_from;
|
||||
|
||||
let path = std::env::temp_dir().join("engine_test_replay_history_append.json");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(11);
|
||||
@@ -2270,6 +2278,7 @@ mod tests {
|
||||
assert_eq!(history.replays[0].final_score, 200);
|
||||
assert_eq!(history.replays[1].final_score, 100);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
@@ -2281,6 +2290,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replay_with_empty_recording_skips_save() {
|
||||
let path = std::env::temp_dir().join("engine_test_replay_empty_skip.json");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let _ = std::fs::remove_file(&path);
|
||||
|
||||
let mut app = test_app(1);
|
||||
|
||||
@@ -13,7 +13,14 @@ use solitaire_core::card::Suit;
|
||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||
|
||||
use crate::auto_complete_plugin::AutoCompleteState;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::avatar_plugin::AvatarResource;
|
||||
// On wasm32 AvatarPlugin is gated out; define a placeholder type so the
|
||||
// Option<Res<AvatarResource>> parameters below compile without changes.
|
||||
// The resource is never inserted on wasm, so every call resolves to None.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(bevy::prelude::Resource)]
|
||||
struct AvatarResource(Option<bevy::prelude::Handle<bevy::prelude::Image>>);
|
||||
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||
use crate::daily_challenge_plugin::DailyChallengeResource;
|
||||
use crate::events::{
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
//! Bevy integration layer for Ferrous Solitaire.
|
||||
|
||||
pub mod achievement_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod analytics_plugin;
|
||||
#[cfg(target_os = "android")]
|
||||
pub mod android_clipboard;
|
||||
pub mod animation_plugin;
|
||||
pub mod assets;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod audio_plugin;
|
||||
pub mod auto_complete_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod avatar_plugin;
|
||||
pub mod card_animation;
|
||||
pub mod card_plugin;
|
||||
@@ -26,6 +29,7 @@ pub mod home_plugin;
|
||||
pub mod hud_plugin;
|
||||
pub mod input_plugin;
|
||||
pub mod layout;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod leaderboard_plugin;
|
||||
pub mod onboarding_plugin;
|
||||
pub mod pause_plugin;
|
||||
@@ -43,7 +47,9 @@ pub mod selection_plugin;
|
||||
pub mod settings_plugin;
|
||||
pub mod splash_plugin;
|
||||
pub mod stats_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod sync_plugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod sync_setup_plugin;
|
||||
pub mod table_plugin;
|
||||
pub mod theme;
|
||||
@@ -57,14 +63,17 @@ pub mod weekly_goals_plugin;
|
||||
pub mod win_summary_plugin;
|
||||
|
||||
pub use achievement_plugin::{AchievementPlugin, AchievementsResource, AchievementsScreen};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use analytics_plugin::{AnalyticsPlugin, AnalyticsResource};
|
||||
pub use animation_plugin::{ActiveToast, AnimationPlugin, CardAnim, ToastEntity, ToastQueue};
|
||||
pub use assets::{
|
||||
AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES, bundled_theme_url,
|
||||
populate_embedded_dark_theme, register_theme_asset_sources,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
||||
pub use auto_complete_plugin::AutoCompletePlugin;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource};
|
||||
pub use card_animation::{
|
||||
AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin,
|
||||
@@ -117,6 +126,7 @@ pub use hud_plugin::{
|
||||
};
|
||||
pub use input_plugin::InputPlugin;
|
||||
pub use layout::{Layout, LayoutResource, compute_layout};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, LeaderboardScreen};
|
||||
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
||||
pub use pause_plugin::{ForfeitConfirmScreen, PausePlugin, PauseScreen, PausedResource};
|
||||
@@ -155,7 +165,9 @@ pub use stats_plugin::{
|
||||
ReplaySelectorCaption, SelectedReplayIndex, StatsPlugin, StatsResource, StatsScreen,
|
||||
StatsUpdate, WatchReplayButton, format_replay_caption,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use sync_plugin::{SyncPlugin, SyncProviderResource};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use sync_setup_plugin::SyncSetupPlugin;
|
||||
pub use table_plugin::{
|
||||
BackgroundImageSet, HintPileHighlight, PileMarker, TableBackground, TablePlugin,
|
||||
|
||||
@@ -12,7 +12,11 @@ use solitaire_core::achievement::{ALL_ACHIEVEMENTS, achievement_by_id};
|
||||
use solitaire_data::SyncBackend;
|
||||
|
||||
use crate::achievement_plugin::AchievementsResource;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::avatar_plugin::AvatarResource;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[derive(bevy::prelude::Resource)]
|
||||
struct AvatarResource(Option<bevy::prelude::Handle<bevy::prelude::Image>>);
|
||||
use crate::events::ToggleProfileRequestEvent;
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
|
||||
@@ -128,9 +128,16 @@ pub struct GameInputConsumedResource(pub bool);
|
||||
/// multi-threaded runtime is built once at startup and its `Arc` cloned cheaply
|
||||
/// into every network task — safe for concurrent `block_on` calls from multiple
|
||||
/// worker threads.
|
||||
///
|
||||
/// Gated to non-wasm because `tokio::runtime::Builder::new_multi_thread()` uses
|
||||
/// `mio` for OS-level I/O polling which does not compile for wasm32. The
|
||||
/// plugins that depend on this resource (AudioPlugin, SyncPlugin,
|
||||
/// AnalyticsPlugin) are also gated out on wasm32 in `CoreGamePlugin`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct TokioRuntimeResource(pub Arc<tokio::runtime::Runtime>);
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl TokioRuntimeResource {
|
||||
/// Attempts to build the shared multi-threaded Tokio runtime.
|
||||
///
|
||||
|
||||
@@ -24,6 +24,7 @@ use solitaire_data::{
|
||||
|
||||
use solitaire_data::settings::SyncBackend;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::assets::user_theme_dir;
|
||||
use crate::events::{
|
||||
DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
|
||||
@@ -32,9 +33,9 @@ use crate::events::{
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::progress_plugin::ProgressResource;
|
||||
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
|
||||
use crate::theme::{
|
||||
ImportError, ThemeThumbnailCache, ThemeThumbnailPair, import_theme, refresh_registry,
|
||||
};
|
||||
use crate::theme::{ThemeThumbnailCache, ThemeThumbnailPair, refresh_registry};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::theme::{ImportError, import_theme};
|
||||
use crate::ui_focus::{FocusGroup, FocusRow, Focusable, FocusedButton};
|
||||
use crate::ui_modal::{
|
||||
ButtonVariant, ModalButton, ModalScrim, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
||||
@@ -404,6 +405,7 @@ impl Plugin for SettingsPlugin {
|
||||
sync_settings_panel_visibility,
|
||||
handle_settings_buttons,
|
||||
handle_sync_buttons,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
handle_scan_themes,
|
||||
update_sync_status_text,
|
||||
update_card_back_text,
|
||||
@@ -1857,6 +1859,7 @@ fn spawn_settings_panel(
|
||||
font_res,
|
||||
);
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
import_themes_row(body, font_res);
|
||||
|
||||
// --- Privacy (only shown when a Matomo URL is configured) ---
|
||||
@@ -2641,6 +2644,7 @@ fn value_text_font(font_res: Option<&FontResource>) -> TextFont {
|
||||
/// [`InfoToastEvent`] is fired per imported theme. `IdCollision` errors (theme
|
||||
/// already installed) are silently skipped; all other errors produce a warning
|
||||
/// toast. A final toast tells the player to reopen Settings to see new themes.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn handle_scan_themes(
|
||||
interaction_query: Query<(&Interaction, &SettingsButton), Changed<Interaction>>,
|
||||
mut toast: MessageWriter<InfoToastEvent>,
|
||||
@@ -2759,6 +2763,7 @@ fn pill_button(
|
||||
/// then presses the button. [`handle_scan_themes`] picks them up, validates,
|
||||
/// and installs them. Reopen Settings to see newly imported themes in the
|
||||
/// card-theme picker.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn import_themes_row(parent: &mut ChildSpawnerCommands, font_res: Option<&FontResource>) {
|
||||
let caption_font = TextFont {
|
||||
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
//! handles directly on card entities, so a theme switch propagates on
|
||||
//! the next frame without re-spawning anything.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod importer;
|
||||
pub mod loader;
|
||||
pub mod manifest;
|
||||
@@ -28,6 +29,7 @@ use thiserror::Error;
|
||||
|
||||
use solitaire_core::card::{Rank, Suit};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use importer::{ImportError, ThemeId, import_theme, import_theme_into};
|
||||
pub use loader::{CardThemeLoader, CardThemeLoaderError};
|
||||
pub use manifest::ThemeManifest;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ferrous Solitaire</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #000; overflow: hidden; }
|
||||
#bevy-canvas { display: block; width: 100vw; height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="bevy-canvas"></canvas>
|
||||
<script type="module">
|
||||
import init from "/web/pkg/canvas.js";
|
||||
await init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "solitaire_web"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
solitaire_engine = { path = "../solitaire_engine" }
|
||||
solitaire_data = { path = "../solitaire_data" }
|
||||
# Direct dep so `bevy::` resolves in lib.rs; zero extra features so this
|
||||
# contributes nothing to unification with the desktop/Android feature set.
|
||||
bevy = { workspace = true }
|
||||
wasm-bindgen = "0.2"
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
# webgl2 must only be enabled for the wasm target — it constrains the
|
||||
# renderer to WebGL2 compatibility limits, which is wrong for native builds.
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
bevy = { workspace = true, features = ["webgl2"] }
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Browser entry point for the Ferrous Solitaire Bevy WASM build.
|
||||
//!
|
||||
//! This crate compiles the full `solitaire_engine` to `wasm32-unknown-unknown`
|
||||
//! and renders to a `<canvas id="bevy-canvas">` element. It shares the same
|
||||
//! ECS code path as the desktop and Android builds; the only differences are:
|
||||
//! - Audio, sync, and analytics plugins are cfg-gated out in `CoreGamePlugin`
|
||||
//! on the wasm32 target (see `solitaire_engine/src/core_game_plugin.rs`).
|
||||
//! - `LocalOnlyProvider` is passed as the sync provider (sync is disabled).
|
||||
//! - Storage is handled automatically by `WasmStorage` (localStorage-backed),
|
||||
//! wired by `CoreGamePlugin` via `default_storage_backend()`.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::{Window, WindowPlugin};
|
||||
use solitaire_data::LocalOnlyProvider;
|
||||
use solitaire_engine::CoreGamePlugin;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn start() {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
App::new()
|
||||
.add_plugins(
|
||||
DefaultPlugins.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
// Bind to the existing <canvas id="bevy-canvas"> in play.html.
|
||||
// Without this, Bevy appends its own canvas to <body>.
|
||||
canvas: Some("#bevy-canvas".into()),
|
||||
// Let CSS size the canvas; Bevy follows the element's size.
|
||||
fit_canvas_to_parent: true,
|
||||
// Prevent the browser stealing keyboard events and scroll.
|
||||
prevent_default_event_handling: true,
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
}),
|
||||
)
|
||||
// LocalOnlyProvider disables cloud sync — correct for the web build
|
||||
// since SyncPlugin is cfg-gated out on wasm32 anyway.
|
||||
.add_plugins(CoreGamePlugin::new(Box::new(LocalOnlyProvider)))
|
||||
.run();
|
||||
}
|
||||
Reference in New Issue
Block a user