OnboardingPlugin previously used PostStartup which fires before the
first Update tick — guaranteeing the onboarding modal and the launch
splash (MOTION_SPLASH_TOTAL_SECS = 1.6 s) overlap for the entire
splash duration. The splash sits at Z_SPLASH (the highest UI z-index),
so the two screens fought visually and the user saw a confusing frozen
composite before the splash faded out.
Fix: move spawn_if_first_run to Update and gate it on
`splashes.is_empty()` (no SplashRoot entity alive). A Local<bool>
ensures the spawn fires at most once per session. Cost: ~one frame of
latency after the splash clears, which is imperceptible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pile_positions[KlondikePile::Stock] stores the waste column position
(col_x(1)). card_plugin renders the face-down deck one column to the
left (col_x(0) = Tableau1 x) via `base.x -= tableau_col_step`.
handle_stock_click and handle_touch_stock_tap were using pile_positions
[Stock] directly, so the click hotspot was on the waste card (right
column) instead of the deck (left column). Result: clicking the
visible face-down deck did nothing, while clicking the waste pile
triggered draw.
Fix: compute deck_pos = Vec2::new(tableau1.x, waste_pos.y) and hit-test
both the deck column AND the waste slot. Accepting waste clicks matches
standard Klondike UX where either card acts as the draw trigger.
Touch tap handler receives the same fix.
Also rebuild canvas_bg.wasm with the corrected engine source and
-O2 optimisation (replacing the previous -Oz that caused grey screen).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without this setting, wgpu's naga SPIR-V→GLSL translator uses features
unsupported by ANGLE (Chromium's WebGL2 implementation): storage buffers,
tight inter-stage component limits, etc. ANGLE rejects these shaders with
a fatal "Shader translation error" and a context-lost event.
WgpuSettingsPriority::WebGL2 constrains naga to emit GLES 300es-compatible
GLSL (same limits as WebGL2 spec: no storage buffers, max 31 inter-stage
components, max 255-byte vertex stride). Firefox was already permissive
enough to work without this; Chromium required it.
Result: game renders correctly in both Chromium (ANGLE/SwiftShader) and
Firefox (native WebGL2), with zero JS errors in both environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes found while testing the Bevy WASM build in a real browser:
1. chrono wasmbind: add `wasmbind` feature to workspace chrono dep so
Local::now()/Utc::now() use js-sys::Date on wasm32 (previously
fell through to std::time::SystemTime which panics on wasm32).
2. std::time::SystemTime: replace all remaining direct SystemTime::now()
calls (4 sites across game_plugin, difficulty_plugin, time_attack_plugin,
solitaire_data/storage) with chrono::Utc::now() which is wasm32-safe.
3. user_dir: return empty PathBuf (instead of panicking) when data_dir()
is None on wasm32; there is no filesystem in the browser so user themes
are unsupported and a benign empty path is correct.
4. ThemeRegistryPlugin: gate build_registry_on_startup to non-wasm32
(the filesystem scan for user themes has nothing to scan in the browser;
only the bundled embedded themes are available).
5. AssetMetaCheck::Never: configure AssetPlugin in solitaire_web to skip
`.meta` sidecar fetches — we don't ship .meta files, so the default
AssetMetaCheck::Always produced a 404 flood on every card/background asset.
Result: `http://localhost:<port>/play` boots in Firefox with zero errors
and renders the full Bevy game — home screen, onboarding modal, HUD all
visible. Assets load correctly from /assets/. Chromium has a separate
wgpu-27/ANGLE/GLES shader translation bug (not in our code); Firefox works.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>