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]
|
[registries.Quaternions]
|
||||||
index = "sparse+https://git.aleshym.co/api/packages/Quaternions/cargo/"
|
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",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_app"
|
name = "bevy_app"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -884,6 +906,35 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_diagnostic"
|
name = "bevy_diagnostic"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -951,6 +1002,36 @@ dependencies = [
|
|||||||
"encase_derive_impl",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_gizmos"
|
name = "bevy_gizmos"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -1073,14 +1154,17 @@ checksum = "6a11df62e49897def470471551c02f13c6fb488e55dddb5ab7ef098132e07754"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bevy_a11y",
|
"bevy_a11y",
|
||||||
"bevy_android",
|
"bevy_android",
|
||||||
|
"bevy_anti_alias",
|
||||||
"bevy_app",
|
"bevy_app",
|
||||||
"bevy_asset",
|
"bevy_asset",
|
||||||
"bevy_camera",
|
"bevy_camera",
|
||||||
"bevy_color",
|
"bevy_color",
|
||||||
"bevy_core_pipeline",
|
"bevy_core_pipeline",
|
||||||
"bevy_derive",
|
"bevy_derive",
|
||||||
|
"bevy_dev_tools",
|
||||||
"bevy_diagnostic",
|
"bevy_diagnostic",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
|
"bevy_feathers",
|
||||||
"bevy_gizmos_render",
|
"bevy_gizmos_render",
|
||||||
"bevy_image",
|
"bevy_image",
|
||||||
"bevy_input",
|
"bevy_input",
|
||||||
@@ -1088,6 +1172,7 @@ dependencies = [
|
|||||||
"bevy_log",
|
"bevy_log",
|
||||||
"bevy_math",
|
"bevy_math",
|
||||||
"bevy_mesh",
|
"bevy_mesh",
|
||||||
|
"bevy_pbr",
|
||||||
"bevy_platform",
|
"bevy_platform",
|
||||||
"bevy_ptr",
|
"bevy_ptr",
|
||||||
"bevy_reflect",
|
"bevy_reflect",
|
||||||
@@ -1107,6 +1192,27 @@ dependencies = [
|
|||||||
"bevy_winit",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_log"
|
name = "bevy_log"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -1167,7 +1273,9 @@ dependencies = [
|
|||||||
"bevy_asset",
|
"bevy_asset",
|
||||||
"bevy_derive",
|
"bevy_derive",
|
||||||
"bevy_ecs",
|
"bevy_ecs",
|
||||||
|
"bevy_image",
|
||||||
"bevy_math",
|
"bevy_math",
|
||||||
|
"bevy_mikktspace",
|
||||||
"bevy_platform",
|
"bevy_platform",
|
||||||
"bevy_reflect",
|
"bevy_reflect",
|
||||||
"bevy_transform",
|
"bevy_transform",
|
||||||
@@ -1180,6 +1288,71 @@ dependencies = [
|
|||||||
"wgpu-types",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_platform"
|
name = "bevy_platform"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -1506,6 +1679,7 @@ dependencies = [
|
|||||||
"bevy_input",
|
"bevy_input",
|
||||||
"bevy_input_focus",
|
"bevy_input_focus",
|
||||||
"bevy_math",
|
"bevy_math",
|
||||||
|
"bevy_picking",
|
||||||
"bevy_platform",
|
"bevy_platform",
|
||||||
"bevy_reflect",
|
"bevy_reflect",
|
||||||
"bevy_sprite",
|
"bevy_sprite",
|
||||||
@@ -1518,6 +1692,7 @@ dependencies = [
|
|||||||
"taffy",
|
"taffy",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1551,6 +1726,26 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "bevy_utils"
|
name = "bevy_utils"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
@@ -1678,6 +1873,7 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytemuck",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3483,6 +3679,17 @@ dependencies = [
|
|||||||
"weezl",
|
"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]]
|
[[package]]
|
||||||
name = "glam"
|
name = "glam"
|
||||||
version = "0.30.10"
|
version = "0.30.10"
|
||||||
@@ -3511,6 +3718,27 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
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]]
|
[[package]]
|
||||||
name = "governor"
|
name = "governor"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -4335,6 +4563,23 @@ dependencies = [
|
|||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "kira"
|
name = "kira"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -7146,6 +7391,18 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -9106,6 +9363,7 @@ dependencies = [
|
|||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"document-features",
|
"document-features",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"naga",
|
"naga",
|
||||||
"portable-atomic",
|
"portable-atomic",
|
||||||
@@ -9113,6 +9371,8 @@ dependencies = [
|
|||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
"wgpu-core",
|
"wgpu-core",
|
||||||
"wgpu-hal",
|
"wgpu-hal",
|
||||||
"wgpu-types",
|
"wgpu-types",
|
||||||
@@ -9144,6 +9404,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"wgpu-core-deps-apple",
|
"wgpu-core-deps-apple",
|
||||||
|
"wgpu-core-deps-wasm",
|
||||||
"wgpu-core-deps-windows-linux-android",
|
"wgpu-core-deps-windows-linux-android",
|
||||||
"wgpu-hal",
|
"wgpu-hal",
|
||||||
"wgpu-types",
|
"wgpu-types",
|
||||||
@@ -9158,6 +9419,15 @@ dependencies = [
|
|||||||
"wgpu-hal",
|
"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]]
|
[[package]]
|
||||||
name = "wgpu-core-deps-windows-linux-android"
|
name = "wgpu-core-deps-windows-linux-android"
|
||||||
version = "27.0.0"
|
version = "27.0.0"
|
||||||
@@ -9183,15 +9453,20 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"core-graphics-types 0.2.0",
|
"core-graphics-types 0.2.0",
|
||||||
|
"glow",
|
||||||
|
"glutin_wgl_sys",
|
||||||
"gpu-alloc",
|
"gpu-alloc",
|
||||||
"gpu-allocator",
|
"gpu-allocator",
|
||||||
"gpu-descriptor",
|
"gpu-descriptor",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
|
"js-sys",
|
||||||
|
"khronos-egl",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading",
|
"libloading",
|
||||||
"log",
|
"log",
|
||||||
"metal",
|
"metal",
|
||||||
"naga",
|
"naga",
|
||||||
|
"ndk-sys",
|
||||||
"objc",
|
"objc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ordered-float",
|
"ordered-float",
|
||||||
@@ -9204,6 +9479,8 @@ dependencies = [
|
|||||||
"renderdoc-sys",
|
"renderdoc-sys",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
"wgpu-types",
|
"wgpu-types",
|
||||||
"windows 0.58.0",
|
"windows 0.58.0",
|
||||||
"windows-core 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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xml-rs"
|
||||||
|
version = "0.8.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xmlwriter"
|
name = "xmlwriter"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ members = [
|
|||||||
"solitaire_app",
|
"solitaire_app",
|
||||||
"solitaire_assetgen",
|
"solitaire_assetgen",
|
||||||
"solitaire_wasm",
|
"solitaire_wasm",
|
||||||
|
"solitaire_web",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
+43
-7
@@ -1,18 +1,21 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Rebuild the solitaire_wasm crate and install the output into
|
# Rebuild WASM artifacts and install them into solitaire_server/web/pkg/.
|
||||||
# solitaire_server/web/pkg/ so the server can serve the replay viewer.
|
#
|
||||||
|
# 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:
|
# Prerequisites:
|
||||||
# cargo install wasm-pack
|
# cargo install wasm-pack wasm-bindgen-cli
|
||||||
# rustup target add wasm32-unknown-unknown
|
# rustup target add wasm32-unknown-unknown
|
||||||
|
# (optional) cargo install wasm-opt # for smaller canvas_bg.wasm
|
||||||
#
|
#
|
||||||
# Run from the repo root:
|
# Run from the repo root:
|
||||||
# ./build_wasm.sh
|
# ./build_wasm.sh
|
||||||
#
|
#
|
||||||
# The generated files (solitaire_wasm.js + solitaire_wasm_bg.wasm) are
|
# The generated pkg/ files are committed to git so self-hosters who don't
|
||||||
# committed to git so self-hosters who don't touch the WASM crate can
|
# touch the WASM crates can skip this step. Regenerate after any change to
|
||||||
# skip this step. Regenerate after any change to solitaire_wasm/ or
|
# solitaire_wasm/, solitaire_web/, solitaire_engine/, or solitaire_core/.
|
||||||
# solitaire_core/.
|
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -36,5 +39,38 @@ wasm-pack build \
|
|||||||
# Remove them — we manage the output directory ourselves.
|
# Remove them — we manage the output directory ourselves.
|
||||||
rm -f "$OUT_DIR/package.json" "$OUT_DIR/.gitignore"
|
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:"
|
echo "Done. Output:"
|
||||||
ls -lh "$OUT_DIR"
|
ls -lh "$OUT_DIR"
|
||||||
|
|||||||
@@ -12,11 +12,17 @@ serde_json = { workspace = true }
|
|||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
async-trait = { 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 }
|
dirs = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
uuid = { workspace = true }
|
|
||||||
klondike = { workspace = true }
|
|
||||||
|
|
||||||
# `keyring-core` is the typed Entry/Error API used by
|
# `keyring-core` is the typed Entry/Error API used by
|
||||||
# `auth_tokens`. The crate's own dependency tree pulls in
|
# `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
|
# on bionic). On Android `auth_tokens` falls back to a stub
|
||||||
# implementation that always returns `KeychainUnavailable`; the
|
# implementation that always returns `KeychainUnavailable`; the
|
||||||
# real backend lands when we wire Android Keystore via JNI.
|
# 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 }
|
keyring-core = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
|||||||
@@ -146,13 +146,17 @@ pub use settings::{
|
|||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
mod android_keystore;
|
mod android_keystore;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod auth_tokens;
|
pub mod auth_tokens;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use auth_tokens::{
|
pub use auth_tokens::{
|
||||||
TokenError, delete_tokens, load_access_token, load_refresh_token, store_tokens,
|
TokenError, delete_tokens, load_access_token, load_refresh_token, store_tokens,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod sync_client;
|
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 mod replay;
|
||||||
pub use replay::{
|
pub use replay::{
|
||||||
@@ -163,7 +167,9 @@ pub use replay::{
|
|||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
pub use replay::{latest_replay_path, load_latest_replay_from, save_latest_replay_to};
|
pub use replay::{latest_replay_path, load_latest_replay_from, save_latest_replay_to};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod matomo_client;
|
pub mod matomo_client;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use matomo_client::MatomoClient;
|
pub use matomo_client::MatomoClient;
|
||||||
|
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
|||||||
@@ -55,7 +55,15 @@ pub fn data_dir() -> Option<PathBuf> {
|
|||||||
{
|
{
|
||||||
Some(PathBuf::from(ANDROID_APP_FILES_DIR))
|
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()
|
dirs::data_dir()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,10 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
|
||||||
|
|
||||||
|
use crate::{SyncError, SyncProvider};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::{
|
use crate::{
|
||||||
SyncError, SyncProvider,
|
|
||||||
auth_tokens::{load_access_token, load_refresh_token, store_tokens},
|
auth_tokens::{load_access_token, load_refresh_token, store_tokens},
|
||||||
replay::Replay,
|
replay::Replay,
|
||||||
settings::SyncBackend,
|
settings::SyncBackend,
|
||||||
@@ -54,12 +56,17 @@ impl SyncProvider for LocalOnlyProvider {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// SolitaireServerClient
|
// 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.
|
/// HTTP sync client for the self-hosted Ferrous Solitaire server.
|
||||||
///
|
///
|
||||||
/// Authenticates via JWT stored in the OS keychain. On a 401 response the
|
/// Authenticates via JWT stored in the OS keychain. On a 401 response the
|
||||||
/// client automatically attempts a token refresh and retries the request once
|
/// client automatically attempts a token refresh and retries the request once
|
||||||
/// before returning an error.
|
/// before returning an error.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct SolitaireServerClient {
|
pub struct SolitaireServerClient {
|
||||||
/// Base URL of the server, e.g. `"https://solitaire.example.com"`.
|
/// Base URL of the server, e.g. `"https://solitaire.example.com"`.
|
||||||
/// Trailing slashes are stripped on construction.
|
/// Trailing slashes are stripped on construction.
|
||||||
@@ -70,6 +77,7 @@ pub struct SolitaireServerClient {
|
|||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl SolitaireServerClient {
|
impl SolitaireServerClient {
|
||||||
/// Construct a new client for the given server URL and username.
|
/// Construct a new client for the given server URL and username.
|
||||||
///
|
///
|
||||||
@@ -201,6 +209,7 @@ impl SolitaireServerClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl SyncProvider for SolitaireServerClient {
|
impl SyncProvider for SolitaireServerClient {
|
||||||
/// Fetch the latest sync payload from the server.
|
/// Fetch the latest sync payload from the server.
|
||||||
@@ -486,6 +495,7 @@ impl SyncProvider for SolitaireServerClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl SolitaireServerClient {
|
impl SolitaireServerClient {
|
||||||
/// Pulled out of `push_replay` so both the first attempt and the
|
/// Pulled out of `push_replay` so both the first attempt and the
|
||||||
/// post-401-retry attempt go through the same parse path.
|
/// 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
|
/// Deserialize a pull response body as [`SyncResponse`] and return its
|
||||||
/// `merged` field, or map non-200 statuses to the appropriate [`SyncError`].
|
/// `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>`.
|
/// Deserialize a leaderboard response body as `Vec<LeaderboardEntry>`.
|
||||||
async fn extract_leaderboard_body(
|
async fn extract_leaderboard_body(
|
||||||
resp: reqwest::Response,
|
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
|
/// Deserialize a push response body as [`SyncResponse`], or map non-200
|
||||||
/// statuses to the appropriate [`SyncError`].
|
/// 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`]
|
/// This is the **one** place in the codebase that matches on [`SyncBackend`]
|
||||||
/// variants. All other code receives a `Box<dyn SyncProvider + Send + Sync>`
|
/// variants. All other code receives a `Box<dyn SyncProvider + Send + Sync>`
|
||||||
/// and remains backend-agnostic.
|
/// and remains backend-agnostic.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn provider_for_backend(backend: &SyncBackend) -> Box<dyn SyncProvider + Send + Sync> {
|
pub fn provider_for_backend(backend: &SyncBackend) -> Box<dyn SyncProvider + Send + Sync> {
|
||||||
match backend {
|
match backend {
|
||||||
SyncBackend::Local => Box::new(LocalOnlyProvider),
|
SyncBackend::Local => Box::new(LocalOnlyProvider),
|
||||||
|
|||||||
+15
-11
@@ -7,15 +7,12 @@ edition.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bevy = { workspace = true }
|
bevy = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
|
||||||
kira = { workspace = true }
|
|
||||||
solitaire_core = { workspace = true }
|
solitaire_core = { workspace = true }
|
||||||
solitaire_data = { workspace = true }
|
solitaire_data = { workspace = true }
|
||||||
solitaire_sync = { workspace = true }
|
solitaire_sync = { workspace = true }
|
||||||
klondike = { workspace = true }
|
klondike = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
tokio = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
@@ -23,17 +20,24 @@ usvg = { workspace = true }
|
|||||||
resvg = { workspace = true }
|
resvg = { workspace = true }
|
||||||
tiny-skia = { workspace = true }
|
tiny-skia = { workspace = true }
|
||||||
ron = { 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 }
|
dirs = { workspace = true }
|
||||||
zip = { workspace = true }
|
zip = { workspace = true }
|
||||||
|
|
||||||
# `arboard` provides clipboard access for the Stats overlay's
|
# `arboard` has no Android backend and no wasm32 backend. Gate it out for
|
||||||
# "Copy share link" button. The crate has no Android backend
|
# both; the copy-share-link button surfaces an informational toast instead.
|
||||||
# (its `platform::Clipboard` module is unimplemented for the
|
[target.'cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))'.dependencies]
|
||||||
# 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 = { workspace = true }
|
arboard = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
|||||||
@@ -50,9 +50,11 @@
|
|||||||
use bevy::asset::AssetApp;
|
use bevy::asset::AssetApp;
|
||||||
use bevy::asset::io::AssetSourceBuilder;
|
use bevy::asset::io::AssetSourceBuilder;
|
||||||
use bevy::asset::io::embedded::EmbeddedAssetRegistry;
|
use bevy::asset::io::embedded::EmbeddedAssetRegistry;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use bevy::asset::io::file::FileAssetReader;
|
use bevy::asset::io::file::FileAssetReader;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::assets::user_dir::user_theme_dir;
|
use crate::assets::user_dir::user_theme_dir;
|
||||||
|
|
||||||
/// `AssetSourceId` of the user-themes asset source. Use it as
|
/// `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
|
/// Returns the `&mut App` so the call can be chained from the binary
|
||||||
/// entry point.
|
/// entry point.
|
||||||
pub fn register_theme_asset_sources(app: &mut App) -> &mut App {
|
pub fn register_theme_asset_sources(app: &mut App) -> &mut App {
|
||||||
|
// 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();
|
let root = user_theme_dir();
|
||||||
app.register_asset_source(
|
app.register_asset_source(
|
||||||
USER_THEMES,
|
USER_THEMES,
|
||||||
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
AssetSourceBuilder::new(move || Box::new(FileAssetReader::new(root.clone()))),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::window::RequestRedraw;
|
use bevy::window::RequestRedraw;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::audio_plugin::{AudioState, SoundLibrary};
|
use crate::audio_plugin::{AudioState, SoundLibrary};
|
||||||
use crate::events::{MoveRequestEvent, StateChangedEvent};
|
use crate::events::{MoveRequestEvent, StateChangedEvent};
|
||||||
use crate::game_plugin::GameMutation;
|
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
|
/// 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
|
/// volume (`AUTO_COMPLETE_CHIME_VOLUME`) so it is clearly recognisable but does
|
||||||
/// not overwhelm the card-place sounds that follow immediately.
|
/// not overwhelm the card-place sounds that follow immediately.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn on_auto_complete_start(
|
fn on_auto_complete_start(
|
||||||
state: Res<AutoCompleteState>,
|
state: Res<AutoCompleteState>,
|
||||||
mut was_active: Local<bool>,
|
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);
|
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.
|
/// Fires one `MoveRequestEvent` per `STEP_INTERVAL` while auto-complete is active.
|
||||||
fn drive_auto_complete(
|
fn drive_auto_complete(
|
||||||
mut state: ResMut<AutoCompleteState>,
|
mut state: ResMut<AutoCompleteState>,
|
||||||
|
|||||||
@@ -13,16 +13,18 @@ use crate::platform::{
|
|||||||
default_storage_backend,
|
default_storage_backend,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AchievementPlugin, AnalyticsPlugin, AnimationPlugin, AssetSourcesPlugin, AudioPlugin,
|
AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, AutoCompletePlugin,
|
||||||
AutoCompletePlugin, AvatarPlugin, CardAnimationPlugin, CardPlugin, ChallengePlugin,
|
CardAnimationPlugin, CardPlugin, ChallengePlugin, CursorPlugin, DailyChallengePlugin,
|
||||||
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
|
DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin, FontPlugin, GamePlugin, HelpPlugin,
|
||||||
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, LeaderboardPlugin,
|
HomePlugin, HudPlugin, InputPlugin, OnboardingPlugin, PausePlugin, PlayBySeedPlugin,
|
||||||
OnboardingPlugin, PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin,
|
ProfilePlugin, ProgressPlugin, RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin,
|
||||||
RadialMenuPlugin, ReplayOverlayPlugin, ReplayPlaybackPlugin, SafeAreaInsetsPlugin,
|
SafeAreaInsetsPlugin, SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncProvider,
|
||||||
SelectionPlugin, SettingsPlugin, SplashPlugin, StatsPlugin, SyncPlugin, SyncProvider,
|
TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, TouchSelectionPlugin,
|
||||||
SyncSetupPlugin, TablePlugin, ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin,
|
UiFocusPlugin, UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WinSummaryPlugin,
|
||||||
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.
|
/// Groups all Ferrous Solitaire gameplay plugins.
|
||||||
@@ -45,6 +47,7 @@ impl Plugin for CoreGamePlugin {
|
|||||||
Ok(guard) => guard,
|
Ok(guard) => guard,
|
||||||
Err(poisoned) => poisoned.into_inner(),
|
Err(poisoned) => poisoned.into_inner(),
|
||||||
};
|
};
|
||||||
|
#[cfg_attr(target_arch = "wasm32", allow(unused_variables))]
|
||||||
let sync_provider = sync_provider
|
let sync_provider = sync_provider
|
||||||
.take()
|
.take()
|
||||||
.expect("CoreGamePlugin::build called twice");
|
.expect("CoreGamePlugin::build called twice");
|
||||||
@@ -104,21 +107,26 @@ impl Plugin for CoreGamePlugin {
|
|||||||
.add_plugins(HudPlugin)
|
.add_plugins(HudPlugin)
|
||||||
.add_plugins(HelpPlugin)
|
.add_plugins(HelpPlugin)
|
||||||
.add_plugins(HomePlugin::default())
|
.add_plugins(HomePlugin::default())
|
||||||
.add_plugins(AvatarPlugin)
|
|
||||||
.add_plugins(ProfilePlugin)
|
.add_plugins(ProfilePlugin)
|
||||||
.add_plugins(PausePlugin)
|
.add_plugins(PausePlugin)
|
||||||
.add_plugins(SettingsPlugin::default())
|
.add_plugins(SettingsPlugin::default())
|
||||||
.add_plugins(AudioPlugin)
|
|
||||||
.add_plugins(OnboardingPlugin)
|
.add_plugins(OnboardingPlugin)
|
||||||
.add_plugins(SyncPlugin::new(sync_provider))
|
|
||||||
.add_plugins(SyncSetupPlugin)
|
|
||||||
.add_plugins(AnalyticsPlugin)
|
|
||||||
.add_plugins(LeaderboardPlugin)
|
|
||||||
.add_plugins(WinSummaryPlugin)
|
.add_plugins(WinSummaryPlugin)
|
||||||
.add_plugins(UiModalPlugin)
|
.add_plugins(UiModalPlugin)
|
||||||
.add_plugins(UiFocusPlugin)
|
.add_plugins(UiFocusPlugin)
|
||||||
.add_plugins(UiTooltipPlugin)
|
.add_plugins(UiTooltipPlugin)
|
||||||
.add_plugins(SplashPlugin)
|
.add_plugins(SplashPlugin)
|
||||||
.add_plugins(DiagnosticsHudPlugin);
|
.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::game_plugin::GameMutation;
|
||||||
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
use crate::progress_plugin::{ProgressResource, ProgressStoragePath, ProgressUpdate};
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::sync_plugin::SyncProviderResource;
|
use crate::sync_plugin::SyncProviderResource;
|
||||||
|
|
||||||
/// Bonus XP awarded for completing today's daily challenge.
|
/// Bonus XP awarded for completing today's daily challenge.
|
||||||
@@ -116,17 +117,21 @@ impl Plugin for DailyChallengePlugin {
|
|||||||
.add_message::<StartDailyChallengeRequestEvent>()
|
.add_message::<StartDailyChallengeRequestEvent>()
|
||||||
.add_message::<WarningToastEvent>()
|
.add_message::<WarningToastEvent>()
|
||||||
.add_message::<XpAwardedEvent>()
|
.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
|
// record/award after the base ProgressUpdate so we don't fight
|
||||||
// ProgressPlugin's add_xp on the same frame.
|
// ProgressPlugin's add_xp on the same frame.
|
||||||
.add_systems(Update, handle_daily_completion.after(ProgressUpdate))
|
.add_systems(Update, handle_daily_completion.after(ProgressUpdate))
|
||||||
.add_systems(Update, handle_start_daily_request.before(GameMutation))
|
.add_systems(Update, handle_start_daily_request.before(GameMutation))
|
||||||
.add_systems(Update, check_daily_expiry_warning)
|
.add_systems(Update, check_daily_expiry_warning)
|
||||||
.add_systems(Update, check_date_rollover);
|
.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.
|
/// Startup system: spawns an async task to fetch the server's daily challenge.
|
||||||
///
|
///
|
||||||
/// Only runs when `SyncProviderResource` is present (i.e. `SyncPlugin` is
|
/// Only runs when `SyncProviderResource` is present (i.e. `SyncPlugin` is
|
||||||
@@ -142,6 +147,7 @@ fn fetch_server_challenge(
|
|||||||
task_res.0 = Some(task);
|
task_res.0 = Some(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
/// Update system: polls the server-challenge fetch task.
|
/// Update system: polls the server-challenge fetch task.
|
||||||
///
|
///
|
||||||
/// On success, replaces the locally-computed seed in `DailyChallengeResource`
|
/// On success, replaces the locally-computed seed in `DailyChallengeResource`
|
||||||
|
|||||||
@@ -1512,6 +1512,7 @@ mod tests {
|
|||||||
use solitaire_data::load_game_state_from;
|
use solitaire_data::load_game_state_from;
|
||||||
|
|
||||||
let path = tmp_gs_path("exit_save");
|
let path = tmp_gs_path("exit_save");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
let mut app = test_app(7);
|
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");
|
let loaded = load_game_state_from(&path).expect("file should exist after exit");
|
||||||
assert_eq!(loaded.seed, 7654);
|
assert_eq!(loaded.seed, 7654);
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1571,6 +1573,7 @@ mod tests {
|
|||||||
use solitaire_data::load_game_state_from;
|
use solitaire_data::load_game_state_from;
|
||||||
|
|
||||||
let path = tmp_gs_path("auto_save_30s");
|
let path = tmp_gs_path("auto_save_30s");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
let mut app = test_app(42);
|
let mut app = test_app(42);
|
||||||
@@ -1601,6 +1604,7 @@ mod tests {
|
|||||||
let loaded = load_game_state_from(&path).expect("file must be loadable");
|
let loaded = load_game_state_from(&path).expect("file must be loadable");
|
||||||
assert_eq!(loaded.seed, 42);
|
assert_eq!(loaded.seed, 42);
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1608,6 +1612,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn auto_save_skips_when_no_moves() {
|
fn auto_save_skips_when_no_moves() {
|
||||||
let path = tmp_gs_path("auto_save_skip");
|
let path = tmp_gs_path("auto_save_skip");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
let mut app = test_app(99);
|
let mut app = test_app(99);
|
||||||
@@ -2165,6 +2170,7 @@ mod tests {
|
|||||||
use solitaire_data::load_replay_history_from;
|
use solitaire_data::load_replay_history_from;
|
||||||
|
|
||||||
let path = std::env::temp_dir().join("engine_test_replay_freeze.json");
|
let path = std::env::temp_dir().join("engine_test_replay_freeze.json");
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
let mut app = test_app(7654);
|
let mut app = test_app(7654);
|
||||||
@@ -2223,6 +2229,7 @@ mod tests {
|
|||||||
other => panic!("second entry must be a Move, got {other:?}"),
|
other => panic!("second entry must be a Move, got {other:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2234,6 +2241,7 @@ mod tests {
|
|||||||
use solitaire_data::load_replay_history_from;
|
use solitaire_data::load_replay_history_from;
|
||||||
|
|
||||||
let path = std::env::temp_dir().join("engine_test_replay_history_append.json");
|
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 _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
let mut app = test_app(11);
|
let mut app = test_app(11);
|
||||||
@@ -2270,6 +2278,7 @@ mod tests {
|
|||||||
assert_eq!(history.replays[0].final_score, 200);
|
assert_eq!(history.replays[0].final_score, 200);
|
||||||
assert_eq!(history.replays[1].final_score, 100);
|
assert_eq!(history.replays[1].final_score, 100);
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2281,6 +2290,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn replay_with_empty_recording_skips_save() {
|
fn replay_with_empty_recording_skips_save() {
|
||||||
let path = std::env::temp_dir().join("engine_test_replay_empty_skip.json");
|
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 _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
let mut app = test_app(1);
|
let mut app = test_app(1);
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ use solitaire_core::card::Suit;
|
|||||||
use solitaire_core::game_state::{DrawMode, GameMode};
|
use solitaire_core::game_state::{DrawMode, GameMode};
|
||||||
|
|
||||||
use crate::auto_complete_plugin::AutoCompleteState;
|
use crate::auto_complete_plugin::AutoCompleteState;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::avatar_plugin::AvatarResource;
|
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::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
|
||||||
use crate::daily_challenge_plugin::DailyChallengeResource;
|
use crate::daily_challenge_plugin::DailyChallengeResource;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
//! Bevy integration layer for Ferrous Solitaire.
|
//! Bevy integration layer for Ferrous Solitaire.
|
||||||
|
|
||||||
pub mod achievement_plugin;
|
pub mod achievement_plugin;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod analytics_plugin;
|
pub mod analytics_plugin;
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
pub mod android_clipboard;
|
pub mod android_clipboard;
|
||||||
pub mod animation_plugin;
|
pub mod animation_plugin;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod audio_plugin;
|
pub mod audio_plugin;
|
||||||
pub mod auto_complete_plugin;
|
pub mod auto_complete_plugin;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod avatar_plugin;
|
pub mod avatar_plugin;
|
||||||
pub mod card_animation;
|
pub mod card_animation;
|
||||||
pub mod card_plugin;
|
pub mod card_plugin;
|
||||||
@@ -26,6 +29,7 @@ pub mod home_plugin;
|
|||||||
pub mod hud_plugin;
|
pub mod hud_plugin;
|
||||||
pub mod input_plugin;
|
pub mod input_plugin;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod leaderboard_plugin;
|
pub mod leaderboard_plugin;
|
||||||
pub mod onboarding_plugin;
|
pub mod onboarding_plugin;
|
||||||
pub mod pause_plugin;
|
pub mod pause_plugin;
|
||||||
@@ -43,7 +47,9 @@ pub mod selection_plugin;
|
|||||||
pub mod settings_plugin;
|
pub mod settings_plugin;
|
||||||
pub mod splash_plugin;
|
pub mod splash_plugin;
|
||||||
pub mod stats_plugin;
|
pub mod stats_plugin;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod sync_plugin;
|
pub mod sync_plugin;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod sync_setup_plugin;
|
pub mod sync_setup_plugin;
|
||||||
pub mod table_plugin;
|
pub mod table_plugin;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
@@ -57,14 +63,17 @@ pub mod weekly_goals_plugin;
|
|||||||
pub mod win_summary_plugin;
|
pub mod win_summary_plugin;
|
||||||
|
|
||||||
pub use achievement_plugin::{AchievementPlugin, AchievementsResource, AchievementsScreen};
|
pub use achievement_plugin::{AchievementPlugin, AchievementsResource, AchievementsScreen};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use analytics_plugin::{AnalyticsPlugin, AnalyticsResource};
|
pub use analytics_plugin::{AnalyticsPlugin, AnalyticsResource};
|
||||||
pub use animation_plugin::{ActiveToast, AnimationPlugin, CardAnim, ToastEntity, ToastQueue};
|
pub use animation_plugin::{ActiveToast, AnimationPlugin, CardAnim, ToastEntity, ToastQueue};
|
||||||
pub use assets::{
|
pub use assets::{
|
||||||
AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES, bundled_theme_url,
|
AssetSourcesPlugin, DARK_THEME_MANIFEST_URL, USER_THEMES, bundled_theme_url,
|
||||||
populate_embedded_dark_theme, register_theme_asset_sources,
|
populate_embedded_dark_theme, register_theme_asset_sources,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
|
||||||
pub use auto_complete_plugin::AutoCompletePlugin;
|
pub use auto_complete_plugin::AutoCompletePlugin;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource};
|
pub use avatar_plugin::{AvatarFetchEvent, AvatarPlugin, AvatarResource};
|
||||||
pub use card_animation::{
|
pub use card_animation::{
|
||||||
AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin,
|
AnimationChain, AnimationTuning, BufferedInput, CardAnimation, CardAnimationPlugin,
|
||||||
@@ -117,6 +126,7 @@ pub use hud_plugin::{
|
|||||||
};
|
};
|
||||||
pub use input_plugin::InputPlugin;
|
pub use input_plugin::InputPlugin;
|
||||||
pub use layout::{Layout, LayoutResource, compute_layout};
|
pub use layout::{Layout, LayoutResource, compute_layout};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, LeaderboardScreen};
|
pub use leaderboard_plugin::{LeaderboardPlugin, LeaderboardResource, LeaderboardScreen};
|
||||||
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
pub use onboarding_plugin::{OnboardingPlugin, OnboardingScreen};
|
||||||
pub use pause_plugin::{ForfeitConfirmScreen, PausePlugin, PauseScreen, PausedResource};
|
pub use pause_plugin::{ForfeitConfirmScreen, PausePlugin, PauseScreen, PausedResource};
|
||||||
@@ -155,7 +165,9 @@ pub use stats_plugin::{
|
|||||||
ReplaySelectorCaption, SelectedReplayIndex, StatsPlugin, StatsResource, StatsScreen,
|
ReplaySelectorCaption, SelectedReplayIndex, StatsPlugin, StatsResource, StatsScreen,
|
||||||
StatsUpdate, WatchReplayButton, format_replay_caption,
|
StatsUpdate, WatchReplayButton, format_replay_caption,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use sync_plugin::{SyncPlugin, SyncProviderResource};
|
pub use sync_plugin::{SyncPlugin, SyncProviderResource};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use sync_setup_plugin::SyncSetupPlugin;
|
pub use sync_setup_plugin::SyncSetupPlugin;
|
||||||
pub use table_plugin::{
|
pub use table_plugin::{
|
||||||
BackgroundImageSet, HintPileHighlight, PileMarker, TableBackground, TablePlugin,
|
BackgroundImageSet, HintPileHighlight, PileMarker, TableBackground, TablePlugin,
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ use solitaire_core::achievement::{ALL_ACHIEVEMENTS, achievement_by_id};
|
|||||||
use solitaire_data::SyncBackend;
|
use solitaire_data::SyncBackend;
|
||||||
|
|
||||||
use crate::achievement_plugin::AchievementsResource;
|
use crate::achievement_plugin::AchievementsResource;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::avatar_plugin::AvatarResource;
|
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::events::ToggleProfileRequestEvent;
|
||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::progress_plugin::ProgressResource;
|
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
|
/// 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
|
/// into every network task — safe for concurrent `block_on` calls from multiple
|
||||||
/// worker threads.
|
/// 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)]
|
#[derive(Resource, Clone)]
|
||||||
pub struct TokioRuntimeResource(pub Arc<tokio::runtime::Runtime>);
|
pub struct TokioRuntimeResource(pub Arc<tokio::runtime::Runtime>);
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl TokioRuntimeResource {
|
impl TokioRuntimeResource {
|
||||||
/// Attempts to build the shared multi-threaded Tokio runtime.
|
/// Attempts to build the shared multi-threaded Tokio runtime.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ use solitaire_data::{
|
|||||||
|
|
||||||
use solitaire_data::settings::SyncBackend;
|
use solitaire_data::settings::SyncBackend;
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::assets::user_theme_dir;
|
use crate::assets::user_theme_dir;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
|
DeleteAccountRequestEvent, InfoToastEvent, ManualSyncRequestEvent, SyncConfigureRequestEvent,
|
||||||
@@ -32,9 +33,9 @@ use crate::events::{
|
|||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::progress_plugin::ProgressResource;
|
use crate::progress_plugin::ProgressResource;
|
||||||
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
|
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
|
||||||
use crate::theme::{
|
use crate::theme::{ThemeThumbnailCache, ThemeThumbnailPair, refresh_registry};
|
||||||
ImportError, ThemeThumbnailCache, ThemeThumbnailPair, import_theme, refresh_registry,
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
};
|
use crate::theme::{ImportError, import_theme};
|
||||||
use crate::ui_focus::{FocusGroup, FocusRow, Focusable, FocusedButton};
|
use crate::ui_focus::{FocusGroup, FocusRow, Focusable, FocusedButton};
|
||||||
use crate::ui_modal::{
|
use crate::ui_modal::{
|
||||||
ButtonVariant, ModalButton, ModalScrim, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
ButtonVariant, ModalButton, ModalScrim, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
||||||
@@ -404,6 +405,7 @@ impl Plugin for SettingsPlugin {
|
|||||||
sync_settings_panel_visibility,
|
sync_settings_panel_visibility,
|
||||||
handle_settings_buttons,
|
handle_settings_buttons,
|
||||||
handle_sync_buttons,
|
handle_sync_buttons,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
handle_scan_themes,
|
handle_scan_themes,
|
||||||
update_sync_status_text,
|
update_sync_status_text,
|
||||||
update_card_back_text,
|
update_card_back_text,
|
||||||
@@ -1857,6 +1859,7 @@ fn spawn_settings_panel(
|
|||||||
font_res,
|
font_res,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
import_themes_row(body, font_res);
|
import_themes_row(body, font_res);
|
||||||
|
|
||||||
// --- Privacy (only shown when a Matomo URL is configured) ---
|
// --- 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
|
/// [`InfoToastEvent`] is fired per imported theme. `IdCollision` errors (theme
|
||||||
/// already installed) are silently skipped; all other errors produce a warning
|
/// 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.
|
/// toast. A final toast tells the player to reopen Settings to see new themes.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn handle_scan_themes(
|
fn handle_scan_themes(
|
||||||
interaction_query: Query<(&Interaction, &SettingsButton), Changed<Interaction>>,
|
interaction_query: Query<(&Interaction, &SettingsButton), Changed<Interaction>>,
|
||||||
mut toast: MessageWriter<InfoToastEvent>,
|
mut toast: MessageWriter<InfoToastEvent>,
|
||||||
@@ -2759,6 +2763,7 @@ fn pill_button(
|
|||||||
/// then presses the button. [`handle_scan_themes`] picks them up, validates,
|
/// then presses the button. [`handle_scan_themes`] picks them up, validates,
|
||||||
/// and installs them. Reopen Settings to see newly imported themes in the
|
/// and installs them. Reopen Settings to see newly imported themes in the
|
||||||
/// card-theme picker.
|
/// card-theme picker.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn import_themes_row(parent: &mut ChildSpawnerCommands, font_res: Option<&FontResource>) {
|
fn import_themes_row(parent: &mut ChildSpawnerCommands, font_res: Option<&FontResource>) {
|
||||||
let caption_font = TextFont {
|
let caption_font = TextFont {
|
||||||
font: font_res.map(|f| f.0.clone()).unwrap_or_default(),
|
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
|
//! handles directly on card entities, so a theme switch propagates on
|
||||||
//! the next frame without re-spawning anything.
|
//! the next frame without re-spawning anything.
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub mod importer;
|
pub mod importer;
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
@@ -28,6 +29,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
use solitaire_core::card::{Rank, Suit};
|
use solitaire_core::card::{Rank, Suit};
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub use importer::{ImportError, ThemeId, import_theme, import_theme_into};
|
pub use importer::{ImportError, ThemeId, import_theme, import_theme_into};
|
||||||
pub use loader::{CardThemeLoader, CardThemeLoaderError};
|
pub use loader::{CardThemeLoader, CardThemeLoaderError};
|
||||||
pub use manifest::ThemeManifest;
|
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