Files
Ferrous-Solitaire/solitaire_engine/src/core_game_plugin.rs
T
funman300 36605751cd
Test / fmt (pull_request) Successful in 4s
Test / test (pull_request) Successful in 4m33s
feat(engine): hold-/ hotkey cheat sheet + unified binding table (Phase J)
Phase J audit findings: the visible focus ring the doc asked for
already exists (FocusOverlay singleton, 2px accent ring, breathing
pulse, reduce-motion aware — landed after the doc was written), and
post-Phase-C every modal already opens and dismisses keyboard-only.
The real gaps were two silently diverging static hotkey tables
(onboarding's slide vs Help's list — onboarding still said 'Mode
Launcher (then 1-5)') and no way to see the bindings mid-game.

- New crate::hotkeys module owns THE binding table (21 rows, verified
  against a grep inventory of every just_pressed(KeyCode::..) handler);
  rows carry an `essential` flag — the onboarding slide teaches that
  subset, the cheat sheet shows everything.
- New cheat_sheet_plugin: hold `/` for a right-anchored reference
  overlay of every binding; release hides it. Deliberately not a
  spawn_modal modal (momentary reference, closer to a tooltip), never
  spawns while a modal owns the screen (also keeps it out of the
  seed-entry field), inert on touch builds.
- Onboarding's stale local table deleted in favour of the shared one
  (copy updated: Home naming, hold-to-repeat undo, added H).

5 new tests (table integrity, essential-subset size, uniqueness,
show/hide driver, modal suppression). Workspace + clippy green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:58:23 -07:00

141 lines
5.9 KiB
Rust

//! Central plugin that groups all gameplay plugins.
//!
//! Register [`CoreGamePlugin`] once in the app instead of the individual
//! plugins. Plugin registration lives here rather than directly in the app
//! entry point.
use std::sync::Mutex;
use bevy::prelude::*;
use crate::platform::{
ClipboardBackendResource, StorageBackendResource, default_clipboard_backend,
default_storage_backend,
};
use crate::{
AchievementPlugin, AnimationPlugin, AssetSourcesPlugin, AutoCompletePlugin,
CardAnimationPlugin, CardPlugin, ChallengePlugin, CheatSheetPlugin, ContextualTipsPlugin,
CursorPlugin, DailyChallengePlugin, DiagnosticsHudPlugin, DifficultyPlugin, FeedbackAnimPlugin,
FontPlugin, GamePlugin, HelpPlugin, HomePlugin, HudPlugin, InputPlugin, OnboardingPlugin,
PausePlugin, PlayBySeedPlugin, ProfilePlugin, ProgressPlugin, RadialMenuPlugin,
ReplayOverlayPlugin, ReplayPlaybackPlugin, SafeAreaInsetsPlugin, SelectionPlugin,
SettingsPlugin, SolutionPlaybackPlugin, SplashPlugin, StatsPlugin, SyncProvider, TablePlugin,
ThemePlugin, ThemeRegistryPlugin, TimeAttackPlugin, TouchSelectionPlugin, UiFocusPlugin,
UiModalPlugin, UiTooltipPlugin, WeeklyGoalsPlugin, WhatsNewPlugin, WinSummaryPlugin,
};
#[cfg(not(target_arch = "wasm32"))]
use crate::{
AnalyticsPlugin, AudioPlugin, AvatarPlugin, LeaderboardPlugin, SyncPlugin, SyncSetupPlugin,
ThemeStorePlugin,
};
/// Groups all Ferrous Solitaire gameplay plugins.
pub struct CoreGamePlugin {
sync_provider: Mutex<Option<Box<dyn SyncProvider + Send + Sync>>>,
}
impl CoreGamePlugin {
/// Create a new [`CoreGamePlugin`] with the sync provider used by [`SyncPlugin`].
pub fn new(sync_provider: Box<dyn SyncProvider + Send + Sync>) -> Self {
Self {
sync_provider: Mutex::new(Some(sync_provider)),
}
}
}
impl Plugin for CoreGamePlugin {
fn build(&self, app: &mut App) {
let mut sync_provider = match self.sync_provider.lock() {
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");
match default_storage_backend() {
Ok(storage) => {
app.insert_resource(StorageBackendResource(storage));
}
Err(err) => {
warn!("storage: failed to initialize platform backend: {err}");
}
}
match default_clipboard_backend() {
Ok(clipboard) => {
app.insert_resource(ClipboardBackendResource(clipboard));
}
Err(err) => {
warn!("clipboard: failed to initialize platform backend: {err}");
}
}
app.add_plugins(AssetSourcesPlugin)
.add_plugins(ThemePlugin)
.add_plugins(ThemeRegistryPlugin)
.add_plugins(FontPlugin)
.add_plugins(GamePlugin)
.add_plugins(TablePlugin)
.add_plugins(CardPlugin)
// Cursor-icon feedback is desktop-only; Android has no pointer cursor.
// The drop-target highlight systems (update_drop_highlights,
// update_drop_target_overlays) live in CursorPlugin but ARE useful
// on Android — they've been left running because their Bevy system
// params compile and function on Android; only the CursorIcon insert
// is inert. Gate the whole plugin if the cursor APIs ever cause
// Android linker issues; for now it's harmless to leave it registered.
.add_plugins(CursorPlugin)
.add_plugins(InputPlugin)
.add_plugins(RadialMenuPlugin)
.add_plugins(SelectionPlugin)
.add_plugins(TouchSelectionPlugin)
.add_plugins(AnimationPlugin)
.add_plugins(FeedbackAnimPlugin)
.add_plugins(CardAnimationPlugin)
.add_plugins(AutoCompletePlugin)
.add_plugins(SolutionPlaybackPlugin)
.add_plugins(ReplayPlaybackPlugin)
.add_plugins(ReplayOverlayPlugin)
.add_plugins(StatsPlugin::default())
.add_plugins(ProgressPlugin::default())
.add_plugins(AchievementPlugin::default())
.add_plugins(DailyChallengePlugin)
.add_plugins(WeeklyGoalsPlugin)
.add_plugins(ChallengePlugin)
.add_plugins(PlayBySeedPlugin)
.add_plugins(DifficultyPlugin)
.add_plugins(TimeAttackPlugin)
.add_plugins(SafeAreaInsetsPlugin)
.add_plugins(HudPlugin)
.add_plugins(HelpPlugin)
.add_plugins(HomePlugin::default())
.add_plugins(ProfilePlugin)
.add_plugins(crate::you_hub_plugin::YouHubPlugin)
.add_plugins(PausePlugin)
.add_plugins(SettingsPlugin::default())
.add_plugins(OnboardingPlugin)
.add_plugins(WhatsNewPlugin)
.add_plugins(ContextualTipsPlugin)
.add_plugins(CheatSheetPlugin)
.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(ThemeStorePlugin)
.add_plugins(AnalyticsPlugin)
.add_plugins(LeaderboardPlugin);
}
}