//! Contextual one-time tips (Phase I of the 2026-07 UI redesign). //! //! Everything the first-run onboarding doesn't cover is invisible until //! stumbled upon: hint cycling, the radial quick-action menu. These //! tips fire **from the situation, not a tour** — each exactly once, //! recorded in `Settings` like `shown_achievement_onboarding`: //! //! - **Stall tip** — after [`STALL_TIP_SECS`] with no board change in an //! active game, an info toast points at Hint. A player staring at a //! stuck board is the one moment the tip is welcome. //! - **Radial tip** — once a game reaches [`RADIAL_TIP_MIN_MOVES`] moves //! (an engaged player) and the radial menu has never been opened, an //! info toast teaches the long-press / right-click gesture. Opening //! the radial organically marks the tip as unnecessary — it is never //! shown to someone who already knows. //! //! Tips render through the queued [`InfoToastEvent`] path, so they share //! the unified toast stack and never interrupt play. use bevy::prelude::*; use solitaire_data::save_settings_to; use crate::events::{InfoToastEvent, StateChangedEvent}; use crate::game_plugin::GameMutation; use crate::pause_plugin::PausedResource; use crate::platform::SHOW_KEYBOARD_ACCELERATORS; use crate::radial_menu::RightClickRadialState; use crate::resources::GameStateResource; use crate::settings_plugin::{SettingsResource, SettingsStoragePath}; use crate::ui_modal::ModalScrim; /// Seconds without any board change before the stall tip fires. /// Long enough that normal thinking never triggers it; short enough to /// reach a genuinely stuck player before they quit. const STALL_TIP_SECS: f32 = 45.0; /// Move count at which an engaged player earns the radial-menu teach. const RADIAL_TIP_MIN_MOVES: u32 = 15; /// Stall-tip copy per platform input vocabulary. const STALL_TIP: &str = if SHOW_KEYBOARD_ACCELERATORS { "Stuck? Press H for a hint." } else { "Stuck? Tap Hint in the bottom bar for a suggested move." }; /// Radial-tip copy per platform input vocabulary. const RADIAL_TIP: &str = if SHOW_KEYBOARD_ACCELERATORS { "Tip: right-click a card for quick actions." } else { "Tip: long-press a card for quick actions." }; /// Seconds of board inactivity, frozen while paused / a modal is open, /// reset by every [`StateChangedEvent`]. #[derive(Resource, Debug, Default)] pub struct StallClock(pub f32); /// Registers the stall clock and the two tip triggers. pub struct ContextualTipsPlugin; impl Plugin for ContextualTipsPlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_message::() .add_message::() .add_systems( Update, ( tick_stall_clock, fire_stall_hint_tip .in_set(crate::game_plugin::InfoToastWriters) .ambiguous_with(crate::game_plugin::InfoToastWriters), observe_radial_menu_use, fire_radial_menu_tip .in_set(crate::game_plugin::InfoToastWriters) .ambiguous_with(crate::game_plugin::InfoToastWriters), ) .chain() .after(GameMutation), ); } } /// Advances the stall clock; resets on any board change; freezes while /// paused, while a modal owns the screen, or when the game is over. fn tick_stall_clock( time: Res