feat(engine): InfoToastEvent — show locked-mode messages on-screen

Replaces silent info!() log calls with on-screen toasts when the player
presses Z/X/T without reaching the required unlock level. Any system
can now fire InfoToastEvent(message) to surface a brief text overlay
without depending on a specific plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-27 02:57:34 +00:00
parent 4a33cbdc22
commit 11cb53ab29
6 changed files with 36 additions and 20 deletions
+9 -1
View File
@@ -11,7 +11,7 @@ use crate::auto_complete_plugin::AutoCompleteState;
use crate::card_plugin::CardEntity; use crate::card_plugin::CardEntity;
use crate::challenge_plugin::ChallengeAdvancedEvent; use crate::challenge_plugin::ChallengeAdvancedEvent;
use crate::daily_challenge_plugin::{DailyChallengeCompletedEvent, DailyGoalAnnouncementEvent}; use crate::daily_challenge_plugin::{DailyChallengeCompletedEvent, DailyGoalAnnouncementEvent};
use crate::events::NewGameConfirmEvent; use crate::events::{InfoToastEvent, NewGameConfirmEvent};
use crate::events::{AchievementUnlockedEvent, GameWonEvent}; use crate::events::{AchievementUnlockedEvent, GameWonEvent};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::layout::LayoutResource; use crate::layout::LayoutResource;
@@ -93,6 +93,7 @@ impl Plugin for AnimationPlugin {
.add_event::<ChallengeAdvancedEvent>() .add_event::<ChallengeAdvancedEvent>()
.add_event::<SettingsChangedEvent>() .add_event::<SettingsChangedEvent>()
.add_event::<NewGameConfirmEvent>() .add_event::<NewGameConfirmEvent>()
.add_event::<InfoToastEvent>()
.init_resource::<EffectiveSlideDuration>() .init_resource::<EffectiveSlideDuration>()
.add_systems(Startup, init_slide_duration) .add_systems(Startup, init_slide_duration)
.add_systems( .add_systems(
@@ -111,6 +112,7 @@ impl Plugin for AnimationPlugin {
handle_settings_toast, handle_settings_toast,
handle_auto_complete_toast, handle_auto_complete_toast,
handle_new_game_confirm_toast, handle_new_game_confirm_toast,
handle_info_toast,
tick_toasts, tick_toasts,
) )
.after(GameMutation), .after(GameMutation),
@@ -322,6 +324,12 @@ fn handle_new_game_confirm_toast(
} }
} }
fn handle_info_toast(mut commands: Commands, mut events: EventReader<InfoToastEvent>) {
for ev in events.read() {
spawn_toast(&mut commands, ev.0.clone(), 3.0);
}
}
fn tick_toasts( fn tick_toasts(
mut commands: Commands, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
+6 -5
View File
@@ -8,7 +8,7 @@ use bevy::prelude::*;
use solitaire_core::game_state::GameMode; use solitaire_core::game_state::GameMode;
use solitaire_data::{challenge_count, challenge_seed_for, save_progress_to}; use solitaire_data::{challenge_count, challenge_seed_for, save_progress_to};
use crate::events::{GameWonEvent, NewGameRequestEvent}; use crate::events::{GameWonEvent, InfoToastEvent, NewGameRequestEvent};
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;
@@ -31,6 +31,7 @@ impl Plugin for ChallengePlugin {
app.add_event::<ChallengeAdvancedEvent>() app.add_event::<ChallengeAdvancedEvent>()
.add_event::<GameWonEvent>() .add_event::<GameWonEvent>()
.add_event::<NewGameRequestEvent>() .add_event::<NewGameRequestEvent>()
.add_event::<InfoToastEvent>()
// Run after ProgressUpdate so we don't fight ProgressPlugin's add_xp. // Run after ProgressUpdate so we don't fight ProgressPlugin's add_xp.
.add_systems(Update, advance_on_challenge_win.after(ProgressUpdate)) .add_systems(Update, advance_on_challenge_win.after(ProgressUpdate))
.add_systems(Update, handle_start_challenge_request.before(GameMutation)); .add_systems(Update, handle_start_challenge_request.before(GameMutation));
@@ -66,15 +67,15 @@ fn handle_start_challenge_request(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
progress: Res<ProgressResource>, progress: Res<ProgressResource>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: EventWriter<NewGameRequestEvent>,
mut info_toast: EventWriter<InfoToastEvent>,
) { ) {
if !keys.just_pressed(KeyCode::KeyX) { if !keys.just_pressed(KeyCode::KeyX) {
return; return;
} }
if progress.0.level < CHALLENGE_UNLOCK_LEVEL { if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
info!( info_toast.send(InfoToastEvent(format!(
"Challenge mode locked — reach level {} (currently {}).", "Challenge mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
CHALLENGE_UNLOCK_LEVEL, progress.0.level )));
);
return; return;
} }
let Some(seed) = challenge_seed_for(progress.0.challenge_index) else { let Some(seed) = challenge_seed_for(progress.0.challenge_index) else {
+5
View File
@@ -75,3 +75,8 @@ pub struct ManualSyncRequestEvent;
/// confirmation window sends `NewGameRequestEvent`. /// confirmation window sends `NewGameRequestEvent`.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Event, Debug, Clone, Copy, Default)]
pub struct NewGameConfirmEvent; pub struct NewGameConfirmEvent;
/// Generic informational toast message. Any system can fire this to display
/// a short string to the player, e.g. "Locked — reach level 5".
#[derive(Event, Debug, Clone)]
pub struct InfoToastEvent(pub String);
+7 -6
View File
@@ -25,8 +25,8 @@ use solitaire_core::rules::{can_place_on_foundation, can_place_on_tableau};
use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC}; use crate::card_plugin::{CardEntity, TABLEAU_FAN_FRAC};
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
use crate::events::{ use crate::events::{
DrawRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, DrawRequestEvent, InfoToastEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent,
StateChangedEvent, UndoRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
}; };
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::progress_plugin::ProgressResource; use crate::progress_plugin::ProgressResource;
@@ -48,6 +48,7 @@ pub struct InputPlugin;
impl Plugin for InputPlugin { impl Plugin for InputPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<NewGameConfirmEvent>() app.add_event::<NewGameConfirmEvent>()
.add_event::<InfoToastEvent>()
.add_systems( .add_systems(
Update, Update,
( (
@@ -75,6 +76,7 @@ fn handle_keyboard(
mut undo: EventWriter<UndoRequestEvent>, mut undo: EventWriter<UndoRequestEvent>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: EventWriter<NewGameRequestEvent>,
mut confirm_event: EventWriter<NewGameConfirmEvent>, mut confirm_event: EventWriter<NewGameConfirmEvent>,
mut info_toast: EventWriter<InfoToastEvent>,
mut draw: EventWriter<DrawRequestEvent>, mut draw: EventWriter<DrawRequestEvent>,
) { ) {
// Tick down any active confirmation window. // Tick down any active confirmation window.
@@ -114,10 +116,9 @@ fn handle_keyboard(
mode: Some(solitaire_core::game_state::GameMode::Zen), mode: Some(solitaire_core::game_state::GameMode::Zen),
}); });
} else { } else {
info!( info_toast.send(InfoToastEvent(format!(
"Zen mode locked — reach level {} (currently {}).", "Zen mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
CHALLENGE_UNLOCK_LEVEL, level )));
);
} }
} }
if keys.just_pressed(KeyCode::KeyD) { if keys.just_pressed(KeyCode::KeyD) {
+3 -3
View File
@@ -39,9 +39,9 @@ pub use auto_complete_plugin::AutoCompletePlugin;
pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary}; pub use audio_plugin::{AudioPlugin, AudioState, SoundLibrary};
pub use card_plugin::{CardEntity, CardLabel, CardPlugin}; pub use card_plugin::{CardEntity, CardLabel, CardPlugin};
pub use events::{ pub use events::{
AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, ManualSyncRequestEvent, AchievementUnlockedEvent, CardFlippedEvent, DrawRequestEvent, GameWonEvent, InfoToastEvent,
MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent, NewGameRequestEvent, StateChangedEvent, ManualSyncRequestEvent, MoveRejectedEvent, MoveRequestEvent, NewGameConfirmEvent,
UndoRequestEvent, NewGameRequestEvent, StateChangedEvent, UndoRequestEvent,
}; };
pub use game_plugin::{GameMutation, GamePlugin, GameStatePath}; pub use game_plugin::{GameMutation, GamePlugin, GameStatePath};
pub use help_plugin::{HelpPlugin, HelpScreen}; pub use help_plugin::{HelpPlugin, HelpScreen};
+6 -5
View File
@@ -8,7 +8,7 @@ use bevy::prelude::*;
use solitaire_core::game_state::GameMode; use solitaire_core::game_state::GameMode;
use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL; use crate::challenge_plugin::CHALLENGE_UNLOCK_LEVEL;
use crate::events::{GameWonEvent, NewGameRequestEvent}; use crate::events::{GameWonEvent, InfoToastEvent, NewGameRequestEvent};
use crate::game_plugin::GameMutation; use crate::game_plugin::GameMutation;
use crate::progress_plugin::ProgressResource; use crate::progress_plugin::ProgressResource;
use crate::resources::GameStateResource; use crate::resources::GameStateResource;
@@ -39,6 +39,7 @@ impl Plugin for TimeAttackPlugin {
.add_event::<TimeAttackEndedEvent>() .add_event::<TimeAttackEndedEvent>()
.add_event::<GameWonEvent>() .add_event::<GameWonEvent>()
.add_event::<NewGameRequestEvent>() .add_event::<NewGameRequestEvent>()
.add_event::<InfoToastEvent>()
.add_systems( .add_systems(
Update, Update,
handle_start_time_attack_request.before(GameMutation), handle_start_time_attack_request.before(GameMutation),
@@ -53,15 +54,15 @@ fn handle_start_time_attack_request(
progress: Res<ProgressResource>, progress: Res<ProgressResource>,
mut session: ResMut<TimeAttackResource>, mut session: ResMut<TimeAttackResource>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: EventWriter<NewGameRequestEvent>,
mut info_toast: EventWriter<InfoToastEvent>,
) { ) {
if !keys.just_pressed(KeyCode::KeyT) { if !keys.just_pressed(KeyCode::KeyT) {
return; return;
} }
if progress.0.level < CHALLENGE_UNLOCK_LEVEL { if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
info!( info_toast.send(InfoToastEvent(format!(
"Time Attack locked — reach level {} (currently {}).", "Time Attack unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
CHALLENGE_UNLOCK_LEVEL, progress.0.level )));
);
return; return;
} }
*session = TimeAttackResource { *session = TimeAttackResource {