chore(deps): migrate to Bevy 0.17

- Event/EventReader/EventWriter renamed to Message/MessageReader/MessageWriter
- add_event → add_message for all 67 call sites
- ScrollPosition changed to tuple struct ScrollPosition(Vec2)
- CursorIcon import moved from bevy::winit::cursor to bevy::window
- WindowResolution::from((f32,f32)) removed — use (u32,u32) tuple
- World::send_event → World::write_message in test code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-28 13:04:44 -07:00
parent c8553dc8c5
commit 648cd44387
29 changed files with 1265 additions and 733 deletions
Generated
+940 -407
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -33,7 +33,7 @@ solitaire_sync = { path = "solitaire_sync" }
solitaire_data = { path = "solitaire_data" } solitaire_data = { path = "solitaire_data" }
solitaire_engine = { path = "solitaire_engine" } solitaire_engine = { path = "solitaire_engine" }
bevy = "0.16" bevy = "0.17"
kira = "0.9" kira = "0.9"
axum = "0.8" axum = "0.8"
+1 -1
View File
@@ -21,7 +21,7 @@ fn main() {
DefaultPlugins.set(WindowPlugin { DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
title: "Solitaire Quest".into(), title: "Solitaire Quest".into(),
resolution: (1280.0, 800.0).into(), resolution: (1280u32, 800u32).into(),
..default() ..default()
}), }),
..default() ..default()
+12 -12
View File
@@ -70,9 +70,9 @@ impl Plugin for AchievementPlugin {
app.insert_resource(AchievementsResource(records)) app.insert_resource(AchievementsResource(records))
.insert_resource(AchievementsStoragePath(self.storage_path.clone())) .insert_resource(AchievementsStoragePath(self.storage_path.clone()))
.add_event::<AchievementUnlockedEvent>() .add_message::<AchievementUnlockedEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
// Run after GameMutation (so GameWonEvent is available), after // Run after GameMutation (so GameWonEvent is available), after
// StatsUpdate (so stats reflect this win), and after ProgressUpdate // StatsUpdate (so stats reflect this win), and after ProgressUpdate
// (so daily_challenge_streak is up to date for daily_devotee). // (so daily_challenge_streak is up to date for daily_devotee).
@@ -89,10 +89,10 @@ impl Plugin for AchievementPlugin {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn evaluate_on_win( fn evaluate_on_win(
mut wins: EventReader<GameWonEvent>, mut wins: MessageReader<GameWonEvent>,
mut unlocks: EventWriter<AchievementUnlockedEvent>, mut unlocks: MessageWriter<AchievementUnlockedEvent>,
mut levelups: EventWriter<LevelUpEvent>, mut levelups: MessageWriter<LevelUpEvent>,
mut xp_awarded: EventWriter<XpAwardedEvent>, mut xp_awarded: MessageWriter<XpAwardedEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
stats: Res<StatsResource>, stats: Res<StatsResource>,
path: Res<AchievementsStoragePath>, path: Res<AchievementsStoragePath>,
@@ -398,7 +398,7 @@ mod tests {
// StatsPlugin runs update_stats_on_win first (after GameMutation); that // StatsPlugin runs update_stats_on_win first (after GameMutation); that
// bumps games_won to 1 before evaluate_on_win reads StatsResource. // bumps games_won to 1 before evaluate_on_win reads StatsResource.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 1000, score: 1000,
time_seconds: 300, time_seconds: 300,
}); });
@@ -425,7 +425,7 @@ mod tests {
fn repeated_win_does_not_refire_already_unlocked_achievement() { fn repeated_win_does_not_refire_already_unlocked_achievement() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 1000, score: 1000,
time_seconds: 300, time_seconds: 300,
}); });
@@ -436,7 +436,7 @@ mod tests {
.resource_mut::<Events<AchievementUnlockedEvent>>() .resource_mut::<Events<AchievementUnlockedEvent>>()
.clear(); .clear();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 1000, score: 1000,
time_seconds: 300, time_seconds: 300,
}); });
@@ -462,7 +462,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
// "no_undo" achievement awards BonusXp(25). Trigger it by sending a // "no_undo" achievement awards BonusXp(25). Trigger it by sending a
// GameWonEvent with undo_count == 0 (default) and enough stats to match. // GameWonEvent with undo_count == 0 (default) and enough stats to match.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 1000, score: 1000,
time_seconds: 300, time_seconds: 300,
}); });
@@ -487,7 +487,7 @@ mod tests {
.0 .0
.undo_count = 1; .undo_count = 1;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 1000, score: 1000,
time_seconds: 300, time_seconds: 300,
}); });
+29 -29
View File
@@ -156,18 +156,18 @@ impl Plugin for AnimationPlugin {
// Register the events this plugin consumes so tests that don't include // Register the events this plugin consumes so tests that don't include
// GamePlugin can still run AnimationPlugin in isolation. Double-registration // GamePlugin can still run AnimationPlugin in isolation. Double-registration
// is idempotent in Bevy. // is idempotent in Bevy.
app.add_event::<GameWonEvent>() app.add_message::<GameWonEvent>()
.add_event::<AchievementUnlockedEvent>() .add_message::<AchievementUnlockedEvent>()
.add_event::<LevelUpEvent>() .add_message::<LevelUpEvent>()
.add_event::<DailyChallengeCompletedEvent>() .add_message::<DailyChallengeCompletedEvent>()
.add_event::<DailyGoalAnnouncementEvent>() .add_message::<DailyGoalAnnouncementEvent>()
.add_event::<WeeklyGoalCompletedEvent>() .add_message::<WeeklyGoalCompletedEvent>()
.add_event::<TimeAttackEndedEvent>() .add_message::<TimeAttackEndedEvent>()
.add_event::<ChallengeAdvancedEvent>() .add_message::<ChallengeAdvancedEvent>()
.add_event::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_event::<NewGameConfirmEvent>() .add_message::<NewGameConfirmEvent>()
.add_event::<InfoToastEvent>() .add_message::<InfoToastEvent>()
.add_event::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.init_resource::<EffectiveSlideDuration>() .init_resource::<EffectiveSlideDuration>()
.init_resource::<ToastQueue>() .init_resource::<ToastQueue>()
.init_resource::<ActiveToast>() .init_resource::<ActiveToast>()
@@ -207,7 +207,7 @@ fn init_slide_duration(
} }
fn sync_slide_duration( fn sync_slide_duration(
mut events: EventReader<SettingsChangedEvent>, mut events: MessageReader<SettingsChangedEvent>,
mut dur: ResMut<EffectiveSlideDuration>, mut dur: ResMut<EffectiveSlideDuration>,
) { ) {
for ev in events.read() { for ev in events.read() {
@@ -245,7 +245,7 @@ fn advance_card_anims(
fn handle_win_cascade( fn handle_win_cascade(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<GameWonEvent>, mut events: MessageReader<GameWonEvent>,
cards: Query<(Entity, &Transform), With<CardEntity>>, cards: Query<(Entity, &Transform), With<CardEntity>>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
settings: Option<Res<SettingsResource>>, settings: Option<Res<SettingsResource>>,
@@ -290,7 +290,7 @@ fn handle_win_cascade(
fn handle_achievement_toast( fn handle_achievement_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<AchievementUnlockedEvent>, mut events: MessageReader<AchievementUnlockedEvent>,
) { ) {
for ev in events.read() { for ev in events.read() {
spawn_toast( spawn_toast(
@@ -301,7 +301,7 @@ fn handle_achievement_toast(
} }
} }
fn handle_levelup_toast(mut commands: Commands, mut events: EventReader<LevelUpEvent>) { fn handle_levelup_toast(mut commands: Commands, mut events: MessageReader<LevelUpEvent>) {
for ev in events.read() { for ev in events.read() {
spawn_toast( spawn_toast(
&mut commands, &mut commands,
@@ -313,7 +313,7 @@ fn handle_levelup_toast(mut commands: Commands, mut events: EventReader<LevelUpE
fn handle_daily_goal_announcement_toast( fn handle_daily_goal_announcement_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<DailyGoalAnnouncementEvent>, mut events: MessageReader<DailyGoalAnnouncementEvent>,
) { ) {
for ev in events.read() { for ev in events.read() {
spawn_toast(&mut commands, format!("Goal: {}", ev.0), DAILY_TOAST_SECS); spawn_toast(&mut commands, format!("Goal: {}", ev.0), DAILY_TOAST_SECS);
@@ -322,7 +322,7 @@ fn handle_daily_goal_announcement_toast(
fn handle_daily_toast( fn handle_daily_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<DailyChallengeCompletedEvent>, mut events: MessageReader<DailyChallengeCompletedEvent>,
) { ) {
for ev in events.read() { for ev in events.read() {
spawn_toast( spawn_toast(
@@ -335,7 +335,7 @@ fn handle_daily_toast(
fn handle_weekly_toast( fn handle_weekly_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<WeeklyGoalCompletedEvent>, mut events: MessageReader<WeeklyGoalCompletedEvent>,
) { ) {
for ev in events.read() { for ev in events.read() {
spawn_toast( spawn_toast(
@@ -348,7 +348,7 @@ fn handle_weekly_toast(
fn handle_time_attack_toast( fn handle_time_attack_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<TimeAttackEndedEvent>, mut events: MessageReader<TimeAttackEndedEvent>,
) { ) {
for ev in events.read() { for ev in events.read() {
spawn_toast( spawn_toast(
@@ -361,7 +361,7 @@ fn handle_time_attack_toast(
fn handle_challenge_toast( fn handle_challenge_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<ChallengeAdvancedEvent>, mut events: MessageReader<ChallengeAdvancedEvent>,
) { ) {
for ev in events.read() { for ev in events.read() {
spawn_toast( spawn_toast(
@@ -374,7 +374,7 @@ fn handle_challenge_toast(
fn handle_settings_toast( fn handle_settings_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<SettingsChangedEvent>, mut events: MessageReader<SettingsChangedEvent>,
mut last_sfx: Local<Option<f32>>, mut last_sfx: Local<Option<f32>>,
mut last_music: Local<Option<f32>>, mut last_music: Local<Option<f32>>,
) { ) {
@@ -417,7 +417,7 @@ fn handle_auto_complete_toast(
fn handle_new_game_confirm_toast( fn handle_new_game_confirm_toast(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<NewGameConfirmEvent>, mut events: MessageReader<NewGameConfirmEvent>,
) { ) {
for _ in events.read() { for _ in events.read() {
spawn_toast(&mut commands, "Press N again to start a new game".to_string(), 3.0); spawn_toast(&mut commands, "Press N again to start a new game".to_string(), 3.0);
@@ -430,7 +430,7 @@ fn handle_new_game_confirm_toast(
/// decouples event production from rendering so multiple simultaneous events do /// decouples event production from rendering so multiple simultaneous events do
/// not cause overlapping toast text on screen. /// not cause overlapping toast text on screen.
fn enqueue_toasts( fn enqueue_toasts(
mut events: EventReader<InfoToastEvent>, mut events: MessageReader<InfoToastEvent>,
mut queue: ResMut<ToastQueue>, mut queue: ResMut<ToastQueue>,
) { ) {
for ev in events.read() { for ev in events.read() {
@@ -509,7 +509,7 @@ fn spawn_queued_toast(commands: &mut Commands, message: String) -> Entity {
.id() .id()
} }
fn handle_xp_awarded_toast(mut commands: Commands, mut events: EventReader<XpAwardedEvent>) { fn handle_xp_awarded_toast(mut commands: Commands, mut events: MessageReader<XpAwardedEvent>) {
for ev in events.read() { for ev in events.read() {
spawn_toast(&mut commands, format!("+{} XP", ev.amount), 3.0); spawn_toast(&mut commands, format!("+{} XP", ev.amount), 3.0);
} }
@@ -709,7 +709,7 @@ mod tests {
let mut app = App::new(); let mut app = App::new();
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin); app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
app.world_mut().send_event(InfoToastEvent("hello".to_string())); app.world_mut().write_message(InfoToastEvent("hello".to_string()));
app.update(); app.update();
let count = app let count = app
@@ -745,7 +745,7 @@ mod tests {
fn toast_queue_enqueues_on_event() { fn toast_queue_enqueues_on_event() {
let mut app = queue_app(); let mut app = queue_app();
app.world_mut() app.world_mut()
.send_event(InfoToastEvent("test message".to_string())); .write_message(InfoToastEvent("test message".to_string()));
app.update(); app.update();
// After one update the message should have been consumed (shown) or is // After one update the message should have been consumed (shown) or is
// still in the queue — either way we verify the system processed it by // still in the queue — either way we verify the system processed it by
@@ -776,7 +776,7 @@ mod tests {
app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin); app.add_plugins(MinimalPlugins).add_plugins(AnimationPlugin);
let fast_settings = Settings { animation_speed: AnimSpeed::Fast, ..Default::default() }; let fast_settings = Settings { animation_speed: AnimSpeed::Fast, ..Default::default() };
app.world_mut().send_event(SettingsChangedEvent(fast_settings)); app.world_mut().write_message(SettingsChangedEvent(fast_settings));
app.update(); app.update();
let dur = app.world().resource::<EffectiveSlideDuration>().slide_secs; let dur = app.world().resource::<EffectiveSlideDuration>().slide_secs;
@@ -795,7 +795,7 @@ mod tests {
assert_eq!(before, 0, "no animations before win"); assert_eq!(before, 0, "no animations before win");
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 500, time_seconds: 60 }); .write_message(GameWonEvent { score: 500, time_seconds: 60 });
app.update(); app.update();
let after = app let after = app
+17 -17
View File
@@ -130,15 +130,15 @@ impl Plugin for AudioPlugin {
app.insert_resource(lib); app.insert_resource(lib);
} }
app.add_event::<DrawRequestEvent>() app.add_message::<DrawRequestEvent>()
.add_event::<MoveRequestEvent>() .add_message::<MoveRequestEvent>()
.add_event::<MoveRejectedEvent>() .add_message::<MoveRejectedEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<CardFlippedEvent>() .add_message::<CardFlippedEvent>()
.add_event::<CardFaceRevealedEvent>() .add_message::<CardFaceRevealedEvent>()
.add_event::<UndoRequestEvent>() .add_message::<UndoRequestEvent>()
.add_event::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_systems(Startup, apply_initial_volume) .add_systems(Startup, apply_initial_volume)
.add_systems( .add_systems(
Update, Update,
@@ -270,7 +270,7 @@ fn apply_initial_volume(
} }
fn play_on_undo( fn play_on_undo(
mut events: EventReader<UndoRequestEvent>, mut events: MessageReader<UndoRequestEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
) { ) {
@@ -281,7 +281,7 @@ fn play_on_undo(
} }
fn apply_volume_on_change( fn apply_volume_on_change(
mut events: EventReader<SettingsChangedEvent>, mut events: MessageReader<SettingsChangedEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
mute: Option<Res<MuteState>>, mute: Option<Res<MuteState>>,
) { ) {
@@ -326,7 +326,7 @@ fn handle_mute_keys(
} }
fn play_on_draw( fn play_on_draw(
mut events: EventReader<DrawRequestEvent>, mut events: MessageReader<DrawRequestEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
game: Option<Res<GameStateResource>>, game: Option<Res<GameStateResource>>,
@@ -361,7 +361,7 @@ fn play_on_draw(
} }
fn play_on_move( fn play_on_move(
mut events: EventReader<MoveRequestEvent>, mut events: MessageReader<MoveRequestEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
) { ) {
@@ -374,7 +374,7 @@ fn play_on_move(
} }
fn play_on_rejected( fn play_on_rejected(
mut events: EventReader<MoveRejectedEvent>, mut events: MessageReader<MoveRejectedEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
) { ) {
@@ -387,7 +387,7 @@ fn play_on_rejected(
} }
fn play_on_new_game( fn play_on_new_game(
mut events: EventReader<NewGameRequestEvent>, mut events: MessageReader<NewGameRequestEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
) { ) {
@@ -400,7 +400,7 @@ fn play_on_new_game(
} }
fn play_on_win( fn play_on_win(
mut events: EventReader<GameWonEvent>, mut events: MessageReader<GameWonEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
) { ) {
@@ -418,7 +418,7 @@ fn play_on_win(
/// Driven by `CardFaceRevealedEvent`, which is fired by `tick_flip_anim` at /// Driven by `CardFaceRevealedEvent`, which is fired by `tick_flip_anim` at
/// the phase transition (scale.x crosses 0), not by the move event itself. /// the phase transition (scale.x crosses 0), not by the move event itself.
fn play_on_face_revealed( fn play_on_face_revealed(
mut events: EventReader<CardFaceRevealedEvent>, mut events: MessageReader<CardFaceRevealedEvent>,
mut audio: NonSendMut<AudioState>, mut audio: NonSendMut<AudioState>,
lib: Option<Res<SoundLibrary>>, lib: Option<Res<SoundLibrary>>,
) { ) {
+5 -5
View File
@@ -57,7 +57,7 @@ impl Plugin for AutoCompletePlugin {
fn detect_auto_complete( fn detect_auto_complete(
mut state: ResMut<AutoCompleteState>, mut state: ResMut<AutoCompleteState>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut changed: EventReader<StateChangedEvent>, mut changed: MessageReader<StateChangedEvent>,
) { ) {
// Only re-evaluate on state changes to avoid per-frame allocations. // Only re-evaluate on state changes to avoid per-frame allocations.
if changed.is_empty() && !game.is_changed() { if changed.is_empty() && !game.is_changed() {
@@ -106,7 +106,7 @@ fn drive_auto_complete(
mut state: ResMut<AutoCompleteState>, mut state: ResMut<AutoCompleteState>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
time: Res<Time>, time: Res<Time>,
mut moves: EventWriter<MoveRequestEvent>, mut moves: MessageWriter<MoveRequestEvent>,
) { ) {
if !state.active { if !state.active {
return; return;
@@ -176,7 +176,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
// Install a nearly-won state and fire StateChangedEvent. // Install a nearly-won state and fire StateChangedEvent.
app.world_mut().resource_mut::<GameStateResource>().0 = nearly_won_state(); app.world_mut().resource_mut::<GameStateResource>().0 = nearly_won_state();
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); app.update();
assert!(app.world().resource::<AutoCompleteState>().active); assert!(app.world().resource::<AutoCompleteState>().active);
@@ -186,7 +186,7 @@ mod tests {
fn drive_fires_move_request_when_active() { fn drive_fires_move_request_when_active() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().resource_mut::<GameStateResource>().0 = nearly_won_state(); app.world_mut().resource_mut::<GameStateResource>().0 = nearly_won_state();
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); // detect runs, sets active app.update(); // detect runs, sets active
app.update(); // drive fires the move app.update(); // drive fires the move
@@ -206,7 +206,7 @@ mod tests {
let mut gs = nearly_won_state(); let mut gs = nearly_won_state();
gs.is_won = true; gs.is_won = true;
app.world_mut().resource_mut::<GameStateResource>().0 = gs; app.world_mut().resource_mut::<GameStateResource>().0 = gs;
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); app.update();
assert!(!app.world().resource::<AutoCompleteState>().active); assert!(!app.world().resource::<AutoCompleteState>().active);
@@ -227,9 +227,9 @@ pub(crate) fn apply_drag_visual(
pub(crate) fn drain_input_buffer( pub(crate) fn drain_input_buffer(
mut buffer: ResMut<InputBuffer>, mut buffer: ResMut<InputBuffer>,
anims: Query<&CardAnimation>, anims: Query<&CardAnimation>,
mut move_events: EventWriter<MoveRequestEvent>, mut move_events: MessageWriter<MoveRequestEvent>,
mut draw_events: EventWriter<DrawRequestEvent>, mut draw_events: MessageWriter<DrawRequestEvent>,
mut undo_events: EventWriter<UndoRequestEvent>, mut undo_events: MessageWriter<UndoRequestEvent>,
) { ) {
if !anims.is_empty() { if !anims.is_empty() {
return; return;
+5 -5
View File
@@ -111,10 +111,10 @@ impl Plugin for CardAnimationPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// Register events and resources that interaction systems depend on, // Register events and resources that interaction systems depend on,
// idempotently — double-registration is safe in Bevy. // idempotently — double-registration is safe in Bevy.
app.add_event::<MoveRequestEvent>() app.add_message::<MoveRequestEvent>()
.add_event::<DrawRequestEvent>() .add_message::<DrawRequestEvent>()
.add_event::<UndoRequestEvent>() .add_message::<UndoRequestEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.init_resource::<DragState>() .init_resource::<DragState>()
.init_resource::<HoverState>() .init_resource::<HoverState>()
.init_resource::<InputBuffer>() .init_resource::<InputBuffer>()
@@ -163,7 +163,7 @@ impl Plugin for WinCascadePlugin {
/// Cards scatter to 8 off-screen positions with per-card stagger. The z-lift /// Cards scatter to 8 off-screen positions with per-card stagger. The z-lift
/// creates a "burst" effect as cards fly outward. /// creates a "burst" effect as cards fly outward.
fn trigger_expressive_win_cascade( fn trigger_expressive_win_cascade(
mut events: EventReader<GameWonEvent>, mut events: MessageReader<GameWonEvent>,
cards: Query<(Entity, &Transform), With<CardEntity>>, cards: Query<(Entity, &Transform), With<CardEntity>>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
mut commands: Commands, mut commands: Commands,
+11 -11
View File
@@ -157,9 +157,9 @@ impl Plugin for CardPlugin {
// `MinimalPlugins` (tests) this resource is absent by default, so we // `MinimalPlugins` (tests) this resource is absent by default, so we
// ensure it exists here. Under `DefaultPlugins` the call is a no-op. // ensure it exists here. Under `DefaultPlugins` the call is a no-op.
app.init_resource::<ButtonInput<MouseButton>>() app.init_resource::<ButtonInput<MouseButton>>()
.add_event::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_event::<CardFlippedEvent>() .add_message::<CardFlippedEvent>()
.add_event::<CardFaceRevealedEvent>() .add_message::<CardFaceRevealedEvent>()
.add_systems(PostStartup, (sync_cards_startup, update_stock_empty_indicator_startup)) .add_systems(PostStartup, (sync_cards_startup, update_stock_empty_indicator_startup))
.add_systems( .add_systems(
Update, Update,
@@ -183,8 +183,8 @@ impl Plugin for CardPlugin {
/// When card-back selection changes in Settings, re-render all cards so the /// When card-back selection changes in Settings, re-render all cards so the
/// new back colour is applied immediately (without waiting for a state change). /// new back colour is applied immediately (without waiting for a state change).
fn resync_cards_on_settings_change( fn resync_cards_on_settings_change(
mut setting_events: EventReader<SettingsChangedEvent>, mut setting_events: MessageReader<SettingsChangedEvent>,
mut state_events: EventWriter<StateChangedEvent>, mut state_events: MessageWriter<StateChangedEvent>,
) { ) {
if setting_events.read().next().is_some() { if setting_events.read().next().is_some() {
state_events.write(StateChangedEvent); state_events.write(StateChangedEvent);
@@ -213,7 +213,7 @@ fn sync_cards_startup(
} }
fn sync_cards_on_change( fn sync_cards_on_change(
mut events: EventReader<StateChangedEvent>, mut events: MessageReader<StateChangedEvent>,
commands: Commands, commands: Commands,
game: Res<GameStateResource>, game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
@@ -508,7 +508,7 @@ fn label_visibility(card: &Card) -> Visibility {
/// ///
/// Skipped when `EffectiveSlideDuration::slide_secs == 0.0` (Instant speed). /// Skipped when `EffectiveSlideDuration::slide_secs == 0.0` (Instant speed).
fn start_flip_anim( fn start_flip_anim(
mut events: EventReader<CardFlippedEvent>, mut events: MessageReader<CardFlippedEvent>,
slide_dur: Option<Res<EffectiveSlideDuration>>, slide_dur: Option<Res<EffectiveSlideDuration>>,
mut commands: Commands, mut commands: Commands,
card_entities: Query<(Entity, &CardEntity)>, card_entities: Query<(Entity, &CardEntity)>,
@@ -543,7 +543,7 @@ fn tick_flip_anim(
mut commands: Commands, mut commands: Commands,
time: Res<Time>, time: Res<Time>,
mut anims: Query<(Entity, &CardEntity, &mut Transform, &mut CardFlipAnim)>, mut anims: Query<(Entity, &CardEntity, &mut Transform, &mut CardFlipAnim)>,
mut reveal_events: EventWriter<CardFaceRevealedEvent>, mut reveal_events: MessageWriter<CardFaceRevealedEvent>,
) { ) {
let dt = time.delta_secs(); let dt = time.delta_secs();
for (entity, card_entity, mut transform, mut anim) in &mut anims { for (entity, card_entity, mut transform, mut anim) in &mut anims {
@@ -742,7 +742,7 @@ fn clear_right_click_highlights(
/// ///
/// This ensures stale highlights do not linger after a card is moved. /// This ensures stale highlights do not linger after a card is moved.
fn clear_right_click_highlights_on_state_change( fn clear_right_click_highlights_on_state_change(
mut events: EventReader<StateChangedEvent>, mut events: MessageReader<StateChangedEvent>,
mut commands: Commands, mut commands: Commands,
highlighted: Query<Entity, With<RightClickHighlight>>, highlighted: Query<Entity, With<RightClickHighlight>>,
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>, mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>,
@@ -980,7 +980,7 @@ fn update_stock_empty_indicator_startup(
/// Runs each `Update` tick when a `StateChangedEvent` arrives, keeping the /// Runs each `Update` tick when a `StateChangedEvent` arrives, keeping the
/// stock pile marker dim state and "↺" label in sync with the current stock. /// stock pile marker dim state and "↺" label in sync with the current stock.
fn update_stock_empty_indicator( fn update_stock_empty_indicator(
mut events: EventReader<StateChangedEvent>, mut events: MessageReader<StateChangedEvent>,
mut commands: Commands, mut commands: Commands,
game: Res<GameStateResource>, game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
@@ -1106,7 +1106,7 @@ mod tests {
let mut app = app(); let mut app = app();
// Trigger a draw, which moves a card from stock to waste and should // Trigger a draw, which moves a card from stock to waste and should
// flip it face-up. Count visible labels after. // flip it face-up. Count visible labels after.
app.world_mut().send_event(crate::events::DrawRequestEvent); app.world_mut().write_message(crate::events::DrawRequestEvent);
app.update(); app.update();
// Now 1 card in waste (face-up), 23 in stock (face-down). So 24 // Now 1 card in waste (face-up), 23 in stock (face-down). So 24
// hidden labels total in stock, plus 21 in tableau = 44. // hidden labels total in stock, plus 21 in tableau = 44.
+14 -14
View File
@@ -18,7 +18,7 @@ pub const CHALLENGE_UNLOCK_LEVEL: u32 = 5;
/// Fired when the player has just completed a Challenge-mode game and the /// Fired when the player has just completed a Challenge-mode game and the
/// `challenge_index` cursor advances. /// `challenge_index` cursor advances.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct ChallengeAdvancedEvent { pub struct ChallengeAdvancedEvent {
pub previous_index: u32, pub previous_index: u32,
pub new_index: u32, pub new_index: u32,
@@ -28,10 +28,10 @@ pub struct ChallengePlugin;
impl Plugin for ChallengePlugin { impl Plugin for ChallengePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<ChallengeAdvancedEvent>() app.add_message::<ChallengeAdvancedEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<InfoToastEvent>() .add_message::<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));
@@ -39,12 +39,12 @@ impl Plugin for ChallengePlugin {
} }
fn advance_on_challenge_win( fn advance_on_challenge_win(
mut wins: EventReader<GameWonEvent>, mut wins: MessageReader<GameWonEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut progress: ResMut<ProgressResource>, mut progress: ResMut<ProgressResource>,
path: Res<ProgressStoragePath>, path: Res<ProgressStoragePath>,
mut advanced: EventWriter<ChallengeAdvancedEvent>, mut advanced: MessageWriter<ChallengeAdvancedEvent>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
for _ in wins.read() { for _ in wins.read() {
if game.0.mode != GameMode::Challenge { if game.0.mode != GameMode::Challenge {
@@ -70,8 +70,8 @@ fn advance_on_challenge_win(
fn handle_start_challenge_request( 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: MessageWriter<NewGameRequestEvent>,
mut info_toast: EventWriter<InfoToastEvent>, mut info_toast: MessageWriter<InfoToastEvent>,
) { ) {
if !keys.just_pressed(KeyCode::KeyX) { if !keys.just_pressed(KeyCode::KeyX) {
return; return;
@@ -124,7 +124,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge); GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 100, time_seconds: 100,
}); });
@@ -145,7 +145,7 @@ mod tests {
fn classic_win_does_not_advance_challenge_index() { fn classic_win_does_not_advance_challenge_index() {
let mut app = headless_app(); let mut app = headless_app();
// Default GameStateResource is Classic mode. // Default GameStateResource is Classic mode.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 100, time_seconds: 100,
}); });
@@ -211,7 +211,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge); GameState::new_with_mode(1, DrawMode::DrawOne, GameMode::Challenge);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 100, time_seconds: 100,
}); });
@@ -231,7 +231,7 @@ mod tests {
fn classic_win_does_not_fire_challenge_complete_toast() { fn classic_win_does_not_fire_challenge_complete_toast() {
let mut app = headless_app(); let mut app = headless_app();
// Default mode is Classic. // Default mode is Classic.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 100, time_seconds: 100,
}); });
+1 -2
View File
@@ -12,8 +12,7 @@
//! The tint is cleared to default the frame the drag ends. //! The tint is cleared to default the frame the drag ends.
use bevy::prelude::*; use bevy::prelude::*;
use bevy::window::{PrimaryWindow, SystemCursorIcon}; use bevy::window::{CursorIcon, PrimaryWindow, SystemCursorIcon};
use bevy::winit::cursor::CursorIcon;
use solitaire_core::card::Suit; use solitaire_core::card::Suit;
use solitaire_core::game_state::{DrawMode, GameState}; use solitaire_core::game_state::{DrawMode, GameState};
use solitaire_core::pile::PileType; use solitaire_core::pile::PileType;
+17 -17
View File
@@ -43,7 +43,7 @@ pub struct DailyChallengeResource {
/// Fired when the player presses C to start the daily challenge. /// Fired when the player presses C to start the daily challenge.
/// Carries the current goal description so it can be displayed as a toast. /// Carries the current goal description so it can be displayed as a toast.
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct DailyGoalAnnouncementEvent(pub String); pub struct DailyGoalAnnouncementEvent(pub String);
impl DailyChallengeResource { impl DailyChallengeResource {
@@ -60,7 +60,7 @@ impl DailyChallengeResource {
} }
/// Fired when the player has just completed today's daily challenge. /// Fired when the player has just completed today's daily challenge.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct DailyChallengeCompletedEvent { pub struct DailyChallengeCompletedEvent {
pub date: NaiveDate, pub date: NaiveDate,
pub streak: u32, pub streak: u32,
@@ -77,11 +77,11 @@ impl Plugin for DailyChallengePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.insert_resource(DailyChallengeResource::for_today()) app.insert_resource(DailyChallengeResource::for_today())
.init_resource::<DailyChallengeTask>() .init_resource::<DailyChallengeTask>()
.add_event::<DailyChallengeCompletedEvent>() .add_message::<DailyChallengeCompletedEvent>()
.add_event::<DailyGoalAnnouncementEvent>() .add_message::<DailyGoalAnnouncementEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.add_systems(Startup, fetch_server_challenge) .add_systems(Startup, fetch_server_challenge)
.add_systems(Update, poll_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
@@ -145,14 +145,14 @@ fn poll_server_challenge(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn handle_daily_completion( fn handle_daily_completion(
mut wins: EventReader<GameWonEvent>, mut wins: MessageReader<GameWonEvent>,
daily: Res<DailyChallengeResource>, daily: Res<DailyChallengeResource>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut progress: ResMut<ProgressResource>, mut progress: ResMut<ProgressResource>,
path: Res<ProgressStoragePath>, path: Res<ProgressStoragePath>,
mut completed: EventWriter<DailyChallengeCompletedEvent>, mut completed: MessageWriter<DailyChallengeCompletedEvent>,
mut xp_awarded: EventWriter<XpAwardedEvent>, mut xp_awarded: MessageWriter<XpAwardedEvent>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
for ev in wins.read() { for ev in wins.read() {
if game.0.seed != daily.seed { if game.0.seed != daily.seed {
@@ -191,8 +191,8 @@ fn handle_daily_completion(
fn handle_start_daily_request( fn handle_start_daily_request(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
daily: Res<DailyChallengeResource>, daily: Res<DailyChallengeResource>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
mut announce: EventWriter<DailyGoalAnnouncementEvent>, mut announce: MessageWriter<DailyGoalAnnouncementEvent>,
) { ) {
if keys.just_pressed(KeyCode::KeyC) { if keys.just_pressed(KeyCode::KeyC) {
new_game.write(NewGameRequestEvent { new_game.write(NewGameRequestEvent {
@@ -244,7 +244,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(daily_seed, DrawMode::DrawOne); GameState::new(daily_seed, DrawMode::DrawOne);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
@@ -270,7 +270,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(daily_seed.wrapping_add(7777), DrawMode::DrawOne); GameState::new(daily_seed.wrapping_add(7777), DrawMode::DrawOne);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
@@ -291,13 +291,13 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(daily_seed, DrawMode::DrawOne); GameState::new(daily_seed, DrawMode::DrawOne);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
app.update(); app.update();
// Re-send win. // Re-send win.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
+17 -17
View File
@@ -1,13 +1,13 @@
//! Cross-system events used by the engine's plugins. //! Cross-system events used by the engine's plugins.
use bevy::prelude::Event; use bevy::prelude::Message;
use solitaire_core::game_state::GameMode; use solitaire_core::game_state::GameMode;
use solitaire_core::pile::PileType; use solitaire_core::pile::PileType;
use solitaire_data::AchievementRecord; use solitaire_data::AchievementRecord;
/// Request to move `count` cards from `from` to `to`. Fired by input systems, /// Request to move `count` cards from `from` to `to`. Fired by input systems,
/// consumed by `GamePlugin`. /// consumed by `GamePlugin`.
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct MoveRequestEvent { pub struct MoveRequestEvent {
pub from: PileType, pub from: PileType,
pub to: PileType, pub to: PileType,
@@ -15,16 +15,16 @@ pub struct MoveRequestEvent {
} }
/// Request to draw from the stock (or recycle waste when stock is empty). /// Request to draw from the stock (or recycle waste when stock is empty).
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct DrawRequestEvent; pub struct DrawRequestEvent;
/// Request to undo the most recent state change. /// Request to undo the most recent state change.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct UndoRequestEvent; pub struct UndoRequestEvent;
/// Request to start a new game. `seed = None` uses a system-time seed. /// Request to start a new game. `seed = None` uses a system-time seed.
/// `mode = None` reuses the current game's `GameMode`. /// `mode = None` reuses the current game's `GameMode`.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct NewGameRequestEvent { pub struct NewGameRequestEvent {
pub seed: Option<u64>, pub seed: Option<u64>,
pub mode: Option<GameMode>, pub mode: Option<GameMode>,
@@ -32,13 +32,13 @@ pub struct NewGameRequestEvent {
/// Fired by `GamePlugin` after any successful state mutation. Rendering and /// Fired by `GamePlugin` after any successful state mutation. Rendering and
/// score-display systems listen for this to refresh. /// score-display systems listen for this to refresh.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct StateChangedEvent; pub struct StateChangedEvent;
/// Fired by input/UI systems when a player attempts to drop dragged cards /// Fired by input/UI systems when a player attempts to drop dragged cards
/// on a real pile but the move violates the rules. Drives the /// on a real pile but the move violates the rules. Drives the
/// `card_invalid.wav` SFX. Not fired for drops in empty space. /// `card_invalid.wav` SFX. Not fired for drops in empty space.
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct MoveRejectedEvent { pub struct MoveRejectedEvent {
pub from: PileType, pub from: PileType,
pub to: PileType, pub to: PileType,
@@ -46,14 +46,14 @@ pub struct MoveRejectedEvent {
} }
/// Fired once when the active game transitions to won. /// Fired once when the active game transitions to won.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct GameWonEvent { pub struct GameWonEvent {
pub score: i32, pub score: i32,
pub time_seconds: u64, pub time_seconds: u64,
} }
/// Fired when a card's face-up state changes during gameplay. /// Fired when a card's face-up state changes during gameplay.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct CardFlippedEvent(pub u32); pub struct CardFlippedEvent(pub u32);
/// Fired by the flip animation at its midpoint — the instant the card face /// Fired by the flip animation at its midpoint — the instant the card face
@@ -62,37 +62,37 @@ pub struct CardFlippedEvent(pub u32);
/// Audio systems should listen to this event rather than `CardFlippedEvent` /// Audio systems should listen to this event rather than `CardFlippedEvent`
/// so the flip sound is synchronised with the visual reveal, not the move /// so the flip sound is synchronised with the visual reveal, not the move
/// that triggered the animation. /// that triggered the animation.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct CardFaceRevealedEvent(pub u32); pub struct CardFaceRevealedEvent(pub u32);
/// Achievement unlocked notification carrying the full `AchievementRecord` for /// Achievement unlocked notification carrying the full `AchievementRecord` for
/// the newly unlocked achievement. Consumed by the toast renderer and any /// the newly unlocked achievement. Consumed by the toast renderer and any
/// persistence/UI systems that need unlock metadata. /// persistence/UI systems that need unlock metadata.
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct AchievementUnlockedEvent(pub AchievementRecord); pub struct AchievementUnlockedEvent(pub AchievementRecord);
/// Request to manually trigger a sync pull from the active backend. /// Request to manually trigger a sync pull from the active backend.
/// ///
/// Fired by the Settings panel "Sync Now" button. `SyncPlugin` responds by /// Fired by the Settings panel "Sync Now" button. `SyncPlugin` responds by
/// starting a new pull task if one is not already in flight. /// starting a new pull task if one is not already in flight.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct ManualSyncRequestEvent; pub struct ManualSyncRequestEvent;
/// Fired by `InputPlugin` when N is pressed while a game is in progress /// Fired by `InputPlugin` when N is pressed while a game is in progress
/// but confirmation has not yet been received. The animation plugin shows /// but confirmation has not yet been received. The animation plugin shows
/// a "Press N again to confirm" toast. A second N press within the /// a "Press N again to confirm" toast. A second N press within the
/// confirmation window sends `NewGameRequestEvent`. /// confirmation window sends `NewGameRequestEvent`.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct NewGameConfirmEvent; pub struct NewGameConfirmEvent;
/// Generic informational toast message. Any system can fire this to display /// Generic informational toast message. Any system can fire this to display
/// a short string to the player, e.g. "Locked — reach level 5". /// a short string to the player, e.g. "Locked — reach level 5".
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct InfoToastEvent(pub String); pub struct InfoToastEvent(pub String);
/// Fired by `ProgressPlugin` immediately after awarding XP for a win so the /// Fired by `ProgressPlugin` immediately after awarding XP for a win so the
/// animation layer can display a "+N XP" toast alongside the win cascade. /// animation layer can display a "+N XP" toast alongside the win cascade.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct XpAwardedEvent { pub struct XpAwardedEvent {
pub amount: u64, pub amount: u64,
} }
@@ -100,7 +100,7 @@ pub struct XpAwardedEvent {
/// Fired by `InputPlugin` when the player presses G to forfeit the current /// Fired by `InputPlugin` when the player presses G to forfeit the current
/// game. Consumed by `StatsPlugin` which records the abandoned game, /// game. Consumed by `StatsPlugin` which records the abandoned game,
/// persists stats, and starts a fresh deal. /// persists stats, and starts a fresh deal.
#[derive(Event, Debug, Clone, Copy, Default)] #[derive(Message, Debug, Clone, Copy, Default)]
pub struct ForfeitEvent; pub struct ForfeitEvent;
/// Fired when the player requests a hint (H key). Carries the source card ID /// Fired when the player requests a hint (H key). Carries the source card ID
@@ -108,7 +108,7 @@ pub struct ForfeitEvent;
/// ///
/// Consumed by `CardPlugin` (to apply `HintHighlight` on the card entity) and /// Consumed by `CardPlugin` (to apply `HintHighlight` on the card entity) and
/// `TablePlugin` (to tint the destination `PileMarker` gold for 2 s). /// `TablePlugin` (to tint the destination `PileMarker` gold for 2 s).
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct HintVisualEvent { pub struct HintVisualEvent {
/// The `Card::id` of the source card to be highlighted. /// The `Card::id` of the source card to be highlighted.
pub source_card_id: u32, pub source_card_id: u32,
+3 -3
View File
@@ -184,7 +184,7 @@ impl Plugin for FeedbackAnimPlugin {
/// Inserts `ShakeAnim` on all card entities belonging to the destination pile /// Inserts `ShakeAnim` on all card entities belonging to the destination pile
/// when a `MoveRejectedEvent` fires. /// when a `MoveRejectedEvent` fires.
fn start_shake_anim( fn start_shake_anim(
mut events: EventReader<MoveRejectedEvent>, mut events: MessageReader<MoveRejectedEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
card_entities: Query<(Entity, &CardEntity, &Transform)>, card_entities: Query<(Entity, &CardEntity, &Transform)>,
mut commands: Commands, mut commands: Commands,
@@ -243,7 +243,7 @@ fn tick_shake_anim(
/// Inserts `SettleAnim` on the top card of every non-empty pile when /// Inserts `SettleAnim` on the top card of every non-empty pile when
/// `StateChangedEvent` fires. /// `StateChangedEvent` fires.
fn start_settle_anim( fn start_settle_anim(
mut events: EventReader<StateChangedEvent>, mut events: MessageReader<StateChangedEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
card_entities: Query<(Entity, &CardEntity)>, card_entities: Query<(Entity, &CardEntity)>,
mut commands: Commands, mut commands: Commands,
@@ -304,7 +304,7 @@ fn tick_settle_anim(
/// and fires the deal animation for every card entity currently in the world. /// and fires the deal animation for every card entity currently in the world.
/// The stagger is looked up from `SettingsResource` via `deal_stagger_secs_for_speed`. /// The stagger is looked up from `SettingsResource` via `deal_stagger_secs_for_speed`.
fn start_deal_anim( fn start_deal_anim(
mut events: EventReader<NewGameRequestEvent>, mut events: MessageReader<NewGameRequestEvent>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
settings: Option<Res<SettingsResource>>, settings: Option<Res<SettingsResource>>,
+47 -47
View File
@@ -71,16 +71,16 @@ impl Plugin for GamePlugin {
.insert_resource(GameStatePath(path)) .insert_resource(GameStatePath(path))
.init_resource::<DragState>() .init_resource::<DragState>()
.init_resource::<SyncStatusResource>() .init_resource::<SyncStatusResource>()
.add_event::<MoveRequestEvent>() .add_message::<MoveRequestEvent>()
.add_event::<DrawRequestEvent>() .add_message::<DrawRequestEvent>()
.add_event::<UndoRequestEvent>() .add_message::<UndoRequestEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<StateChangedEvent>() .add_message::<StateChangedEvent>()
.add_event::<crate::events::MoveRejectedEvent>() .add_message::<crate::events::MoveRejectedEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<crate::events::CardFlippedEvent>() .add_message::<crate::events::CardFlippedEvent>()
.add_event::<crate::events::AchievementUnlockedEvent>() .add_message::<crate::events::AchievementUnlockedEvent>()
.add_event::<InfoToastEvent>() .add_message::<InfoToastEvent>()
.add_systems( .add_systems(
Update, Update,
( (
@@ -152,9 +152,9 @@ fn seed_from_system_time() -> u64 {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn handle_new_game( fn handle_new_game(
mut commands: Commands, mut commands: Commands,
mut new_game: EventReader<NewGameRequestEvent>, mut new_game: MessageReader<NewGameRequestEvent>,
mut game: ResMut<GameStateResource>, mut game: ResMut<GameStateResource>,
mut changed: EventWriter<StateChangedEvent>, mut changed: MessageWriter<StateChangedEvent>,
settings: Option<Res<crate::settings_plugin::SettingsResource>>, settings: Option<Res<crate::settings_plugin::SettingsResource>>,
path: Option<Res<GameStatePath>>, path: Option<Res<GameStatePath>>,
confirm_screens: Query<Entity, With<ConfirmNewGameScreen>>, confirm_screens: Query<Entity, With<ConfirmNewGameScreen>>,
@@ -287,7 +287,7 @@ fn handle_confirm_input(
mut commands: Commands, mut commands: Commands,
keys: Option<Res<ButtonInput<KeyCode>>>, keys: Option<Res<ButtonInput<KeyCode>>>,
screens: Query<(Entity, &OriginalNewGameRequest), With<ConfirmNewGameScreen>>, screens: Query<(Entity, &OriginalNewGameRequest), With<ConfirmNewGameScreen>>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
) { ) {
let Ok((entity, original)) = screens.single() else { let Ok((entity, original)) = screens.single() else {
return; return;
@@ -314,10 +314,10 @@ fn handle_confirm_input(
} }
fn handle_draw( fn handle_draw(
mut draws: EventReader<DrawRequestEvent>, mut draws: MessageReader<DrawRequestEvent>,
mut game: ResMut<GameStateResource>, mut game: ResMut<GameStateResource>,
mut changed: EventWriter<StateChangedEvent>, mut changed: MessageWriter<StateChangedEvent>,
mut flipped: EventWriter<CardFlippedEvent>, mut flipped: MessageWriter<CardFlippedEvent>,
) { ) {
use solitaire_core::pile::PileType; use solitaire_core::pile::PileType;
@@ -357,11 +357,11 @@ fn handle_draw(
} }
fn handle_move( fn handle_move(
mut moves: EventReader<MoveRequestEvent>, mut moves: MessageReader<MoveRequestEvent>,
mut game: ResMut<GameStateResource>, mut game: ResMut<GameStateResource>,
mut changed: EventWriter<StateChangedEvent>, mut changed: MessageWriter<StateChangedEvent>,
mut won: EventWriter<GameWonEvent>, mut won: MessageWriter<GameWonEvent>,
mut flipped: EventWriter<crate::events::CardFlippedEvent>, mut flipped: MessageWriter<crate::events::CardFlippedEvent>,
path: Option<Res<GameStatePath>>, path: Option<Res<GameStatePath>>,
) { ) {
for ev in moves.read() { for ev in moves.read() {
@@ -408,10 +408,10 @@ fn handle_move(
} }
fn handle_undo( fn handle_undo(
mut undos: EventReader<UndoRequestEvent>, mut undos: MessageReader<UndoRequestEvent>,
mut game: ResMut<GameStateResource>, mut game: ResMut<GameStateResource>,
mut changed: EventWriter<StateChangedEvent>, mut changed: MessageWriter<StateChangedEvent>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
use solitaire_core::error::MoveError; use solitaire_core::error::MoveError;
@@ -500,9 +500,9 @@ pub fn has_legal_moves(game: &GameState) -> bool {
/// game is won. /// game is won.
fn check_no_moves( fn check_no_moves(
mut commands: Commands, mut commands: Commands,
mut events: EventReader<StateChangedEvent>, mut events: MessageReader<StateChangedEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
mut already_fired: Local<bool>, mut already_fired: Local<bool>,
game_over_screens: Query<Entity, With<GameOverScreen>>, game_over_screens: Query<Entity, With<GameOverScreen>>,
) { ) {
@@ -628,8 +628,8 @@ fn handle_game_over_input(
mut commands: Commands, mut commands: Commands,
keys: Option<Res<ButtonInput<KeyCode>>>, keys: Option<Res<ButtonInput<KeyCode>>>,
screens: Query<Entity, With<GameOverScreen>>, screens: Query<Entity, With<GameOverScreen>>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
mut undo: EventWriter<UndoRequestEvent>, mut undo: MessageWriter<UndoRequestEvent>,
) { ) {
if screens.is_empty() { if screens.is_empty() {
return; return;
@@ -685,7 +685,7 @@ fn auto_save_game_state(
/// `save_game_state_to` helper skips them). Blocking on exit is acceptable /// `save_game_state_to` helper skips them). Blocking on exit is acceptable
/// because the game loop is already shutting down. /// because the game loop is already shutting down.
fn save_game_state_on_exit( fn save_game_state_on_exit(
mut exit_events: EventReader<AppExit>, mut exit_events: MessageReader<AppExit>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
path: Res<GameStatePath>, path: Res<GameStatePath>,
) { ) {
@@ -739,7 +739,7 @@ mod tests {
.cards .cards
.len(); .len();
app.world_mut().send_event(DrawRequestEvent); app.world_mut().write_message(DrawRequestEvent);
app.update(); app.update();
let stock_after = app let stock_after = app
@@ -763,7 +763,7 @@ mod tests {
#[test] #[test]
fn draw_request_fires_state_changed_event() { fn draw_request_fires_state_changed_event() {
let mut app = test_app(42); let mut app = test_app(42);
app.world_mut().send_event(DrawRequestEvent); app.world_mut().write_message(DrawRequestEvent);
app.update(); app.update();
let events = app.world().resource::<Events<StateChangedEvent>>(); let events = app.world().resource::<Events<StateChangedEvent>>();
let mut reader = events.get_cursor(); let mut reader = events.get_cursor();
@@ -773,9 +773,9 @@ mod tests {
#[test] #[test]
fn undo_after_draw_restores_state() { fn undo_after_draw_restores_state() {
let mut app = test_app(42); let mut app = test_app(42);
app.world_mut().send_event(DrawRequestEvent); app.world_mut().write_message(DrawRequestEvent);
app.update(); app.update();
app.world_mut().send_event(UndoRequestEvent); app.world_mut().write_message(UndoRequestEvent);
app.update(); app.update();
let g = &app.world().resource::<GameStateResource>().0; let g = &app.world().resource::<GameStateResource>().0;
assert_eq!(g.piles[&PileType::Stock].cards.len(), 24); assert_eq!(g.piles[&PileType::Stock].cards.len(), 24);
@@ -795,7 +795,7 @@ mod tests {
.map(|c| c.id) .map(|c| c.id)
.collect(); .collect();
app.world_mut().send_event(NewGameRequestEvent { seed: Some(999), mode: None }); app.world_mut().write_message(NewGameRequestEvent { seed: Some(999), mode: None });
app.update(); app.update();
let after: Vec<u32> = app let after: Vec<u32> = app
@@ -858,7 +858,7 @@ mod tests {
fn invalid_move_does_not_fire_state_changed() { fn invalid_move_does_not_fire_state_changed() {
let mut app = test_app(42); let mut app = test_app(42);
// Stock -> Waste is InvalidDestination; no state change expected. // Stock -> Waste is InvalidDestination; no state change expected.
app.world_mut().send_event(MoveRequestEvent { app.world_mut().write_message(MoveRequestEvent {
from: PileType::Stock, from: PileType::Stock,
to: PileType::Waste, to: PileType::Waste,
count: 1, count: 1,
@@ -892,7 +892,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new(7654, DrawMode::DrawOne); GameState::new(7654, DrawMode::DrawOne);
app.world_mut().send_event(AppExit::Success); app.world_mut().write_message(AppExit::Success);
app.update(); app.update();
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");
@@ -913,7 +913,7 @@ mod tests {
let mut app = test_app(1); let mut app = test_app(1);
app.insert_resource(GameStatePath(Some(path.clone()))); app.insert_resource(GameStatePath(Some(path.clone())));
app.world_mut().send_event(NewGameRequestEvent { seed: Some(2), mode: None }); app.world_mut().write_message(NewGameRequestEvent { seed: Some(2), mode: None });
app.update(); app.update();
assert!(!path.exists(), "saved file should be deleted after new game"); assert!(!path.exists(), "saved file should be deleted after new game");
@@ -949,7 +949,7 @@ mod tests {
t.cards.push(Card { id: 902, suit: Suit::Clubs, rank: Rank::King, face_up: true }); t.cards.push(Card { id: 902, suit: Suit::Clubs, rank: Rank::King, face_up: true });
} }
app.world_mut().send_event(MoveRequestEvent { app.world_mut().write_message(MoveRequestEvent {
from: PileType::Tableau(0), from: PileType::Tableau(0),
to: PileType::Tableau(1), to: PileType::Tableau(1),
count: 1, count: 1,
@@ -1035,7 +1035,7 @@ mod tests {
.push(Card { id: 912, suit: Suit::Spades, rank: Rank::King, face_up: true }); .push(Card { id: 912, suit: Suit::Spades, rank: Rank::King, face_up: true });
} }
app.world_mut().send_event(MoveRequestEvent { app.world_mut().write_message(MoveRequestEvent {
from: PileType::Tableau(0), from: PileType::Tableau(0),
to: PileType::Tableau(1), to: PileType::Tableau(1),
count: 1, count: 1,
@@ -1125,7 +1125,7 @@ mod tests {
// Simulate an active game with moves made. // Simulate an active game with moves made.
app.world_mut().resource_mut::<GameStateResource>().0.move_count = 5; app.world_mut().resource_mut::<GameStateResource>().0.move_count = 5;
app.world_mut() app.world_mut()
.send_event(NewGameRequestEvent { seed: None, mode: None }); .write_message(NewGameRequestEvent { seed: None, mode: None });
app.update(); app.update();
let count = app let count = app
@@ -1146,7 +1146,7 @@ mod tests {
"test assumes a fresh game with no moves" "test assumes a fresh game with no moves"
); );
app.world_mut() app.world_mut()
.send_event(NewGameRequestEvent { seed: None, mode: None }); .write_message(NewGameRequestEvent { seed: None, mode: None });
app.update(); app.update();
let count = app let count = app
@@ -1165,7 +1165,7 @@ mod tests {
fn game_over_screen_absent_when_moves_available() { fn game_over_screen_absent_when_moves_available() {
// A fresh game always has moves (stock is non-empty). // A fresh game always has moves (stock is non-empty).
let mut app = test_app_with_input(42); let mut app = test_app_with_input(42);
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); app.update();
let count = app let count = app
@@ -1201,7 +1201,7 @@ mod tests {
}); });
} }
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); app.update();
let count = app let count = app
@@ -1240,7 +1240,7 @@ mod tests {
}); });
} }
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); app.update();
// Collect all Text values that are children of the GameOverScreen entity tree. // Collect all Text values that are children of the GameOverScreen entity tree.
@@ -1295,7 +1295,7 @@ mod tests {
face_up: true, face_up: true,
}); });
} }
app.world_mut().send_event(StateChangedEvent); app.world_mut().write_message(StateChangedEvent);
app.update(); app.update();
// Confirm the overlay is present. // Confirm the overlay is present.
@@ -1338,7 +1338,7 @@ mod tests {
fn undo_on_empty_stack_fires_info_toast() { fn undo_on_empty_stack_fires_info_toast() {
let mut app = test_app(42); let mut app = test_app(42);
// Fresh game — undo stack is empty, so undo() returns UndoStackEmpty. // Fresh game — undo stack is empty, so undo() returns UndoStackEmpty.
app.world_mut().send_event(UndoRequestEvent); app.world_mut().write_message(UndoRequestEvent);
app.update(); app.update();
let events = app.world().resource::<Events<InfoToastEvent>>(); let events = app.world().resource::<Events<InfoToastEvent>>();
@@ -1357,12 +1357,12 @@ mod tests {
fn undo_after_draw_does_not_fire_info_toast() { fn undo_after_draw_does_not_fire_info_toast() {
let mut app = test_app(42); let mut app = test_app(42);
// Make a move so the undo stack is non-empty. // Make a move so the undo stack is non-empty.
app.world_mut().send_event(DrawRequestEvent); app.world_mut().write_message(DrawRequestEvent);
app.update(); app.update();
// Clear events from the draw so we start with a clean slate. // Clear events from the draw so we start with a clean slate.
app.world_mut().resource_mut::<Events<InfoToastEvent>>().clear(); app.world_mut().resource_mut::<Events<InfoToastEvent>>().clear();
app.world_mut().send_event(UndoRequestEvent); app.world_mut().write_message(UndoRequestEvent);
app.update(); app.update();
let events = app.world().resource::<Events<InfoToastEvent>>(); let events = app.world().resource::<Events<InfoToastEvent>>();
+1 -1
View File
@@ -475,7 +475,7 @@ fn update_selection_hud(
/// to debounce so the toast only appears on the leading edge. /// to debounce so the toast only appears on the leading edge.
fn announce_auto_complete( fn announce_auto_complete(
auto_complete: Option<Res<AutoCompleteState>>, auto_complete: Option<Res<AutoCompleteState>>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
mut was_active: Local<bool>, mut was_active: Local<bool>,
) { ) {
let now_active = auto_complete.as_ref().is_some_and(|ac| ac.active); let now_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
+20 -20
View File
@@ -58,10 +58,10 @@ pub struct InputPlugin;
impl Plugin for InputPlugin { impl Plugin for InputPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<HintCycleIndex>() app.init_resource::<HintCycleIndex>()
.add_event::<NewGameConfirmEvent>() .add_message::<NewGameConfirmEvent>()
.add_event::<InfoToastEvent>() .add_message::<InfoToastEvent>()
.add_event::<ForfeitEvent>() .add_message::<ForfeitEvent>()
.add_event::<HintVisualEvent>() .add_message::<HintVisualEvent>()
.add_systems( .add_systems(
Update, Update,
( (
@@ -89,13 +89,13 @@ const FORFEIT_CONFIRM_WINDOW: f32 = 3.0;
/// within Bevy's 16-parameter limit. /// within Bevy's 16-parameter limit.
#[derive(SystemParam)] #[derive(SystemParam)]
struct KeyboardEvents<'w> { struct KeyboardEvents<'w> {
undo: EventWriter<'w, UndoRequestEvent>, undo: MessageWriter<'w, UndoRequestEvent>,
new_game: EventWriter<'w, NewGameRequestEvent>, new_game: MessageWriter<'w, NewGameRequestEvent>,
confirm_event: EventWriter<'w, NewGameConfirmEvent>, confirm_event: MessageWriter<'w, NewGameConfirmEvent>,
info_toast: EventWriter<'w, InfoToastEvent>, info_toast: MessageWriter<'w, InfoToastEvent>,
draw: EventWriter<'w, DrawRequestEvent>, draw: MessageWriter<'w, DrawRequestEvent>,
forfeit: EventWriter<'w, ForfeitEvent>, forfeit: MessageWriter<'w, ForfeitEvent>,
hint_visual: EventWriter<'w, HintVisualEvent>, hint_visual: MessageWriter<'w, HintVisualEvent>,
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -308,8 +308,8 @@ fn handle_keyboard(
/// game plugin fires after dealing — preventing a stale hint from the previous /// game plugin fires after dealing — preventing a stale hint from the previous
/// game being shown when H is pressed in that gap frame. /// game being shown when H is pressed in that gap frame.
fn reset_hint_cycle_on_state_change( fn reset_hint_cycle_on_state_change(
mut state_events: EventReader<StateChangedEvent>, mut state_events: MessageReader<StateChangedEvent>,
mut new_game_events: EventReader<NewGameRequestEvent>, mut new_game_events: MessageReader<NewGameRequestEvent>,
mut hint_cycle: ResMut<HintCycleIndex>, mut hint_cycle: ResMut<HintCycleIndex>,
) { ) {
if state_events.read().next().is_some() || new_game_events.read().next().is_some() { if state_events.read().next().is_some() || new_game_events.read().next().is_some() {
@@ -322,7 +322,7 @@ fn reset_hint_cycle_on_state_change(
fn handle_fullscreen( fn handle_fullscreen(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut windows: Query<&mut Window, With<PrimaryWindow>>, mut windows: Query<&mut Window, With<PrimaryWindow>>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
if !keys.just_pressed(KeyCode::F11) { if !keys.just_pressed(KeyCode::F11) {
return; return;
@@ -347,7 +347,7 @@ fn handle_stock_click(
windows: Query<&Window, With<PrimaryWindow>>, windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>, cameras: Query<(&Camera, &GlobalTransform)>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
mut draw: EventWriter<DrawRequestEvent>, mut draw: MessageWriter<DrawRequestEvent>,
) { ) {
if paused.is_some_and(|p| p.0) { if paused.is_some_and(|p| p.0) {
return; return;
@@ -467,9 +467,9 @@ fn end_drag(
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut drag: ResMut<DragState>, mut drag: ResMut<DragState>,
mut moves: EventWriter<MoveRequestEvent>, mut moves: MessageWriter<MoveRequestEvent>,
mut rejected: EventWriter<MoveRejectedEvent>, mut rejected: MessageWriter<MoveRejectedEvent>,
mut changed: EventWriter<StateChangedEvent>, mut changed: MessageWriter<StateChangedEvent>,
mut commands: Commands, mut commands: Commands,
card_entities: Query<(Entity, &CardEntity, &Transform)>, card_entities: Query<(Entity, &CardEntity, &Transform)>,
) { ) {
@@ -809,8 +809,8 @@ fn handle_double_click(
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut last_click: Local<HashMap<u32, f32>>, mut last_click: Local<HashMap<u32, f32>>,
mut moves: EventWriter<MoveRequestEvent>, mut moves: MessageWriter<MoveRequestEvent>,
mut rejected: EventWriter<MoveRejectedEvent>, mut rejected: MessageWriter<MoveRejectedEvent>,
) { ) {
if paused.is_some_and(|p| p.0) { if paused.is_some_and(|p| p.0) {
return; return;
+2 -2
View File
@@ -218,7 +218,7 @@ fn handle_opt_in_button(
/// Polls the opt-in task; fires an `InfoToastEvent` on completion or failure. /// Polls the opt-in task; fires an `InfoToastEvent` on completion or failure.
fn poll_opt_in_task( fn poll_opt_in_task(
mut task_res: ResMut<OptInTask>, mut task_res: ResMut<OptInTask>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
let Some(task) = task_res.0.as_mut() else { return }; let Some(task) = task_res.0.as_mut() else { return };
let Some(result) = future::block_on(future::poll_once(task)) else { return }; let Some(result) = future::block_on(future::poll_once(task)) else { return };
@@ -258,7 +258,7 @@ fn handle_opt_out_button(
/// Polls the opt-out task; fires an `InfoToastEvent` on completion or failure. /// Polls the opt-out task; fires an `InfoToastEvent` on completion or failure.
fn poll_opt_out_task( fn poll_opt_out_task(
mut task_res: ResMut<OptOutTask>, mut task_res: ResMut<OptOutTask>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
let Some(task) = task_res.0.as_mut() else { return }; let Some(task) = task_res.0.as_mut() else { return };
let Some(result) = future::block_on(future::poll_once(task)) else { return }; let Some(result) = future::block_on(future::poll_once(task)) else { return };
+4 -4
View File
@@ -54,8 +54,8 @@ impl Plugin for PausePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// Both add_event calls are idempotent — other plugins may register these // Both add_event calls are idempotent — other plugins may register these
// events first, but calling add_event again is always safe. // events first, but calling add_event again is always safe.
app.add_event::<SettingsChangedEvent>() app.add_message::<SettingsChangedEvent>()
.add_event::<StateChangedEvent>() .add_message::<StateChangedEvent>()
.init_resource::<PausedResource>() .init_resource::<PausedResource>()
.add_systems(Update, (toggle_pause, handle_pause_draw_toggle)); .add_systems(Update, (toggle_pause, handle_pause_draw_toggle));
} }
@@ -74,7 +74,7 @@ fn toggle_pause(
stats: Option<Res<StatsResource>>, stats: Option<Res<StatsResource>>,
settings: Option<Res<SettingsResource>>, settings: Option<Res<SettingsResource>>,
mut drag: Option<ResMut<DragState>>, mut drag: Option<ResMut<DragState>>,
mut changed: EventWriter<StateChangedEvent>, mut changed: MessageWriter<StateChangedEvent>,
) { ) {
if !keys.just_pressed(KeyCode::Escape) { if !keys.just_pressed(KeyCode::Escape) {
return; return;
@@ -125,7 +125,7 @@ fn handle_pause_draw_toggle(
paused: Res<PausedResource>, paused: Res<PausedResource>,
settings: Option<ResMut<SettingsResource>>, settings: Option<ResMut<SettingsResource>>,
path: Option<Res<SettingsStoragePath>>, path: Option<Res<SettingsStoragePath>>,
mut changed: EventWriter<SettingsChangedEvent>, mut changed: MessageWriter<SettingsChangedEvent>,
) { ) {
if !paused.0 { if !paused.0 {
return; return;
+15 -15
View File
@@ -25,7 +25,7 @@ pub struct ProgressResource(pub PlayerProgress);
pub struct ProgressStoragePath(pub Option<PathBuf>); pub struct ProgressStoragePath(pub Option<PathBuf>);
/// Fired when a win pushes the player to a new level. /// Fired when a win pushes the player to a new level.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct LevelUpEvent { pub struct LevelUpEvent {
pub previous_level: u32, pub previous_level: u32,
pub new_level: u32, pub new_level: u32,
@@ -64,9 +64,9 @@ impl Plugin for ProgressPlugin {
}; };
app.insert_resource(ProgressResource(loaded)) app.insert_resource(ProgressResource(loaded))
.insert_resource(ProgressStoragePath(self.storage_path.clone())) .insert_resource(ProgressStoragePath(self.storage_path.clone()))
.add_event::<LevelUpEvent>() .add_message::<LevelUpEvent>()
.add_event::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_systems( .add_systems(
Update, Update,
award_xp_on_win award_xp_on_win
@@ -77,9 +77,9 @@ impl Plugin for ProgressPlugin {
} }
fn award_xp_on_win( fn award_xp_on_win(
mut wins: EventReader<GameWonEvent>, mut wins: MessageReader<GameWonEvent>,
mut levelups: EventWriter<LevelUpEvent>, mut levelups: MessageWriter<LevelUpEvent>,
mut xp_awarded: EventWriter<XpAwardedEvent>, mut xp_awarded: MessageWriter<XpAwardedEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
path: Res<ProgressStoragePath>, path: Res<ProgressStoragePath>,
mut progress: ResMut<ProgressResource>, mut progress: ResMut<ProgressResource>,
@@ -131,7 +131,7 @@ mod tests {
fn win_awards_base_xp() { fn win_awards_base_xp() {
let mut app = headless_app(); let mut app = headless_app();
// Game starts with undo_count = 0, so the no-undo bonus applies. // Game starts with undo_count = 0, so the no-undo bonus applies.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 300, // no speed bonus time_seconds: 300, // no speed bonus
}); });
@@ -150,7 +150,7 @@ mod tests {
.0 .0
.undo_count = 1; .undo_count = 1;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 300, time_seconds: 300,
}); });
@@ -164,7 +164,7 @@ mod tests {
#[test] #[test]
fn fast_win_includes_speed_bonus() { fn fast_win_includes_speed_bonus() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 0, time_seconds: 0,
}); });
@@ -181,7 +181,7 @@ mod tests {
// Pre-load 480 XP so a 75-XP win pushes us over the 500 boundary. // Pre-load 480 XP so a 75-XP win pushes us over the 500 boundary.
app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 480; app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 480;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 300, time_seconds: 300,
}); });
@@ -198,7 +198,7 @@ mod tests {
#[test] #[test]
fn win_without_level_change_does_not_fire_levelup() { fn win_without_level_change_does_not_fire_levelup() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 300, time_seconds: 300,
}); });
@@ -213,7 +213,7 @@ mod tests {
fn xp_awarded_event_fired_with_correct_amount() { fn xp_awarded_event_fired_with_correct_amount() {
let mut app = headless_app(); let mut app = headless_app();
// Slow win, no undo → base 50 + no_undo 25 = 75 // Slow win, no undo → base 50 + no_undo 25 = 75
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 300, time_seconds: 300,
}); });
@@ -231,7 +231,7 @@ mod tests {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 480; app.world_mut().resource_mut::<ProgressResource>().0.total_xp = 480;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 300, time_seconds: 300,
}); });
@@ -256,7 +256,7 @@ mod tests {
.0 .0
.mode = solitaire_core::game_state::GameMode::Zen; .mode = solitaire_core::game_state::GameMode::Zen;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 0, // Zen mode keeps score at 0 score: 0, // Zen mode keeps score at 0
time_seconds: 300, time_seconds: 300,
}); });
+2 -2
View File
@@ -162,8 +162,8 @@ fn handle_selection_keys(
paused: Option<Res<PausedResource>>, paused: Option<Res<PausedResource>>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut selection: ResMut<SelectionState>, mut selection: ResMut<SelectionState>,
mut moves: EventWriter<MoveRequestEvent>, mut moves: MessageWriter<MoveRequestEvent>,
mut info_toast: EventWriter<InfoToastEvent>, mut info_toast: MessageWriter<InfoToastEvent>,
) { ) {
if paused.is_some_and(|p| p.0) { if paused.is_some_and(|p| p.0) {
return; return;
+19 -19
View File
@@ -36,7 +36,7 @@ pub struct SettingsStoragePath(pub Option<PathBuf>);
pub struct SettingsScreen(pub bool); pub struct SettingsScreen(pub bool);
/// Fired whenever settings change so consumers (audio, UI) can react. /// Fired whenever settings change so consumers (audio, UI) can react.
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct SettingsChangedEvent(pub Settings); pub struct SettingsChangedEvent(pub Settings);
/// Marker on the root Settings panel entity. /// Marker on the root Settings panel entity.
@@ -144,9 +144,9 @@ impl Plugin for SettingsPlugin {
.insert_resource(SettingsStoragePath(self.storage_path.clone())) .insert_resource(SettingsStoragePath(self.storage_path.clone()))
.init_resource::<SettingsScreen>() .init_resource::<SettingsScreen>()
.init_resource::<SettingsScrollPos>() .init_resource::<SettingsScrollPos>()
.add_event::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_event::<ManualSyncRequestEvent>() .add_message::<ManualSyncRequestEvent>()
.add_event::<bevy::input::mouse::MouseWheel>() .add_message::<bevy::input::mouse::MouseWheel>()
.add_systems(Update, (handle_volume_keys, toggle_settings_screen, scroll_settings_panel)); .add_systems(Update, (handle_volume_keys, toggle_settings_screen, scroll_settings_panel));
if self.ui_enabled { if self.ui_enabled {
@@ -185,7 +185,7 @@ fn handle_volume_keys(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
mut settings: ResMut<SettingsResource>, mut settings: ResMut<SettingsResource>,
path: Res<SettingsStoragePath>, path: Res<SettingsStoragePath>,
mut changed: EventWriter<SettingsChangedEvent>, mut changed: MessageWriter<SettingsChangedEvent>,
) { ) {
let mut delta = 0.0_f32; let mut delta = 0.0_f32;
if keys.just_pressed(KeyCode::BracketLeft) { if keys.just_pressed(KeyCode::BracketLeft) {
@@ -257,7 +257,7 @@ fn sync_settings_panel_visibility(
} else { } else {
// Save the current scroll offset before despawning the panel. // Save the current scroll offset before despawning the panel.
if let Ok(sp) = scroll_nodes.single() { if let Ok(sp) = scroll_nodes.single() {
scroll_pos.0 = sp.offset_y; scroll_pos.0 = sp.0.y;
} }
for entity in &panels { for entity in &panels {
commands.entity(entity).despawn(); commands.entity(entity).despawn();
@@ -383,8 +383,8 @@ fn handle_settings_buttons(
mut settings: ResMut<SettingsResource>, mut settings: ResMut<SettingsResource>,
mut screen: ResMut<SettingsScreen>, mut screen: ResMut<SettingsScreen>,
path: Res<SettingsStoragePath>, path: Res<SettingsStoragePath>,
mut changed: EventWriter<SettingsChangedEvent>, mut changed: MessageWriter<SettingsChangedEvent>,
mut manual_sync: EventWriter<ManualSyncRequestEvent>, mut manual_sync: MessageWriter<ManualSyncRequestEvent>,
mut sfx_text: Query<&mut Text, (With<SfxVolumeText>, Without<MusicVolumeText>, Without<DrawModeText>, Without<ThemeText>, Without<AnimSpeedText>, Without<ColorBlindText>)>, mut sfx_text: Query<&mut Text, (With<SfxVolumeText>, Without<MusicVolumeText>, Without<DrawModeText>, Without<ThemeText>, Without<AnimSpeedText>, Without<ColorBlindText>)>,
mut music_text: Query<&mut Text, (With<MusicVolumeText>, Without<SfxVolumeText>, Without<DrawModeText>, Without<ThemeText>, Without<AnimSpeedText>, Without<ColorBlindText>)>, mut music_text: Query<&mut Text, (With<MusicVolumeText>, Without<SfxVolumeText>, Without<DrawModeText>, Without<ThemeText>, Without<AnimSpeedText>, Without<ColorBlindText>)>,
mut draw_text: Query<&mut Text, (With<DrawModeText>, Without<SfxVolumeText>, Without<MusicVolumeText>, Without<ThemeText>, Without<AnimSpeedText>, Without<ColorBlindText>)>, mut draw_text: Query<&mut Text, (With<DrawModeText>, Without<SfxVolumeText>, Without<MusicVolumeText>, Without<ThemeText>, Without<AnimSpeedText>, Without<ColorBlindText>)>,
@@ -537,7 +537,7 @@ fn color_blind_label(enabled: bool) -> String {
/// adds to the offset; scrolling up subtracts. Clamped to >= 0 so it never /// adds to the offset; scrolling up subtracts. Clamped to >= 0 so it never
/// scrolls past the top. /// scrolls past the top.
fn scroll_settings_panel( fn scroll_settings_panel(
mut scroll_evr: EventReader<MouseWheel>, mut scroll_evr: MessageReader<MouseWheel>,
screen: Res<SettingsScreen>, screen: Res<SettingsScreen>,
mut scrollables: Query<&mut ScrollPosition, With<SettingsPanelScrollable>>, mut scrollables: Query<&mut ScrollPosition, With<SettingsPanelScrollable>>,
) { ) {
@@ -556,7 +556,7 @@ fn scroll_settings_panel(
return; return;
} }
for mut sp in scrollables.iter_mut() { for mut sp in scrollables.iter_mut() {
sp.offset_y = (sp.offset_y - delta_y).max(0.0); sp.0.y = (sp.0.y - delta_y).max(0.0);
} }
} }
@@ -595,7 +595,7 @@ fn spawn_settings_panel(
root.spawn(( root.spawn((
SettingsPanelScrollable, SettingsPanelScrollable,
SettingsScrollNode, SettingsScrollNode,
ScrollPosition { offset_y: scroll_offset, ..default() }, ScrollPosition(Vec2::new(0.0, scroll_offset)),
Node { Node {
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
padding: UiRect::all(Val::Px(28.0)), padding: UiRect::all(Val::Px(28.0)),
@@ -1095,7 +1095,7 @@ mod tests {
.spawn((SettingsPanelScrollable, ScrollPosition::default())) .spawn((SettingsPanelScrollable, ScrollPosition::default()))
.id(); .id();
// Send a downward scroll event while the panel is closed. // Send a downward scroll event while the panel is closed.
app.world_mut().send_event(MouseWheel { app.world_mut().write_message(MouseWheel {
unit: MouseScrollUnit::Line, unit: MouseScrollUnit::Line,
x: 0.0, x: 0.0,
y: -3.0, y: -3.0,
@@ -1108,7 +1108,7 @@ mod tests {
.entity(entity) .entity(entity)
.get::<ScrollPosition>() .get::<ScrollPosition>()
.unwrap() .unwrap()
.offset_y; .0.y;
assert_eq!(offset, 0.0, "scroll must not move when panel is closed"); assert_eq!(offset, 0.0, "scroll must not move when panel is closed");
} }
@@ -1123,11 +1123,11 @@ mod tests {
.world_mut() .world_mut()
.spawn(( .spawn((
SettingsPanelScrollable, SettingsPanelScrollable,
ScrollPosition { offset_y: 100.0, ..default() }, ScrollPosition(Vec2::new(0.0, 100.0)),
)) ))
.id(); .id();
// Scroll down by 2 lines (50 px/line → +100 px added to offset_y). // Scroll down by 2 lines (50 px/line → +100 px added to offset_y).
app.world_mut().send_event(MouseWheel { app.world_mut().write_message(MouseWheel {
unit: MouseScrollUnit::Line, unit: MouseScrollUnit::Line,
x: 0.0, x: 0.0,
y: -2.0, y: -2.0,
@@ -1139,7 +1139,7 @@ mod tests {
.entity(entity) .entity(entity)
.get::<ScrollPosition>() .get::<ScrollPosition>()
.unwrap() .unwrap()
.offset_y; .0.y;
assert!((offset - 200.0).abs() < 1e-3, "scrolling down should increase offset_y; got {offset}"); assert!((offset - 200.0).abs() < 1e-3, "scrolling down should increase offset_y; got {offset}");
} }
@@ -1153,11 +1153,11 @@ mod tests {
.world_mut() .world_mut()
.spawn(( .spawn((
SettingsPanelScrollable, SettingsPanelScrollable,
ScrollPosition { offset_y: 10.0, ..default() }, ScrollPosition(Vec2::new(0.0, 10.0)),
)) ))
.id(); .id();
// Scroll up by 5 lines → would subtract 250 px, but must clamp to 0. // Scroll up by 5 lines → would subtract 250 px, but must clamp to 0.
app.world_mut().send_event(MouseWheel { app.world_mut().write_message(MouseWheel {
unit: MouseScrollUnit::Line, unit: MouseScrollUnit::Line,
x: 0.0, x: 0.0,
y: 5.0, y: 5.0,
@@ -1169,7 +1169,7 @@ mod tests {
.entity(entity) .entity(entity)
.get::<ScrollPosition>() .get::<ScrollPosition>()
.unwrap() .unwrap()
.offset_y; .0.y;
assert_eq!(offset, 0.0, "scrolling past top must clamp to 0, got {offset}"); assert_eq!(offset, 0.0, "scrolling past top must clamp to 0, got {offset}");
} }
} }
+16 -16
View File
@@ -77,10 +77,10 @@ impl Plugin for StatsPlugin {
}; };
app.insert_resource(StatsResource(loaded)) app.insert_resource(StatsResource(loaded))
.insert_resource(StatsStoragePath(self.storage_path.clone())) .insert_resource(StatsStoragePath(self.storage_path.clone()))
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<ForfeitEvent>() .add_message::<ForfeitEvent>()
.add_event::<InfoToastEvent>() .add_message::<InfoToastEvent>()
// record_abandoned must read `move_count` BEFORE handle_new_game // record_abandoned must read `move_count` BEFORE handle_new_game
// clobbers it with a fresh game. // clobbers it with a fresh game.
.add_systems( .add_systems(
@@ -111,7 +111,7 @@ fn persist(path: &StatsStoragePath, stats: &StatsSnapshot, context: &str) {
} }
fn update_stats_on_win( fn update_stats_on_win(
mut events: EventReader<GameWonEvent>, mut events: MessageReader<GameWonEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut stats: ResMut<StatsResource>, mut stats: ResMut<StatsResource>,
path: Res<StatsStoragePath>, path: Res<StatsStoragePath>,
@@ -125,11 +125,11 @@ fn update_stats_on_win(
} }
fn update_stats_on_new_game( fn update_stats_on_new_game(
mut events: EventReader<NewGameRequestEvent>, mut events: MessageReader<NewGameRequestEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut stats: ResMut<StatsResource>, mut stats: ResMut<StatsResource>,
path: Res<StatsStoragePath>, path: Res<StatsStoragePath>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
for _ in events.read() { for _ in events.read() {
if game.0.move_count > 0 && !game.0.is_won { if game.0.move_count > 0 && !game.0.is_won {
@@ -149,12 +149,12 @@ fn update_stats_on_new_game(
/// `AutoCompleteState` is reset here so the "AUTO" badge and chime do not bleed /// `AutoCompleteState` is reset here so the "AUTO" badge and chime do not bleed
/// into the new deal (task #41). /// into the new deal (task #41).
fn handle_forfeit( fn handle_forfeit(
mut events: EventReader<ForfeitEvent>, mut events: MessageReader<ForfeitEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut stats: ResMut<StatsResource>, mut stats: ResMut<StatsResource>,
path: Res<StatsStoragePath>, path: Res<StatsStoragePath>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
mut auto_complete: Option<ResMut<AutoCompleteState>>, mut auto_complete: Option<ResMut<AutoCompleteState>>,
) { ) {
for _ in events.read() { for _ in events.read() {
@@ -513,7 +513,7 @@ mod tests {
#[test] #[test]
fn win_event_increments_games_won() { fn win_event_increments_games_won() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 1000, score: 1000,
time_seconds: 120, time_seconds: 120,
}); });
@@ -532,7 +532,7 @@ mod tests {
.0 .0
.draw_mode = solitaire_core::game_state::DrawMode::DrawThree; .draw_mode = solitaire_core::game_state::DrawMode::DrawThree;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
@@ -553,7 +553,7 @@ mod tests {
.move_count = 3; .move_count = 3;
app.world_mut() app.world_mut()
.send_event(NewGameRequestEvent { seed: Some(999), mode: None }); .write_message(NewGameRequestEvent { seed: Some(999), mode: None });
app.update(); app.update();
let stats = &app.world().resource::<StatsResource>().0; let stats = &app.world().resource::<StatsResource>().0;
@@ -566,7 +566,7 @@ mod tests {
fn new_game_without_moves_does_not_record_abandoned() { fn new_game_without_moves_does_not_record_abandoned() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut() app.world_mut()
.send_event(NewGameRequestEvent { seed: Some(42), mode: None }); .write_message(NewGameRequestEvent { seed: Some(42), mode: None });
app.update(); app.update();
let stats = &app.world().resource::<StatsResource>().0; let stats = &app.world().resource::<StatsResource>().0;
@@ -781,7 +781,7 @@ mod tests {
.0 .0
.move_count = 1; .move_count = 1;
app.world_mut().send_event(ForfeitEvent); app.world_mut().write_message(ForfeitEvent);
app.update(); app.update();
let events = app.world().resource::<Events<InfoToastEvent>>(); let events = app.world().resource::<Events<InfoToastEvent>>();
@@ -810,7 +810,7 @@ mod tests {
.0 .0
.move_count = 1; .move_count = 1;
app.world_mut().send_event(ForfeitEvent); app.world_mut().write_message(ForfeitEvent);
app.update(); app.update();
let events = app.world().resource::<Events<InfoToastEvent>>(); let events = app.world().resource::<Events<InfoToastEvent>>();
+3 -3
View File
@@ -93,7 +93,7 @@ impl Plugin for SyncPlugin {
.init_resource::<SyncStatusResource>() .init_resource::<SyncStatusResource>()
.init_resource::<PullTaskResult>() .init_resource::<PullTaskResult>()
.init_resource::<PullTask>() .init_resource::<PullTask>()
.add_event::<ManualSyncRequestEvent>() .add_message::<ManualSyncRequestEvent>()
.add_systems(Startup, start_pull) .add_systems(Startup, start_pull)
.add_systems(Update, (poll_pull_result, handle_manual_sync_request)) .add_systems(Update, (poll_pull_result, handle_manual_sync_request))
.add_systems(Last, push_on_exit); .add_systems(Last, push_on_exit);
@@ -121,7 +121,7 @@ fn start_pull(
/// Update system: starts a new pull task when `ManualSyncRequestEvent` is /// Update system: starts a new pull task when `ManualSyncRequestEvent` is
/// received, but only if no pull is already in flight. /// received, but only if no pull is already in flight.
fn handle_manual_sync_request( fn handle_manual_sync_request(
mut events: EventReader<ManualSyncRequestEvent>, mut events: MessageReader<ManualSyncRequestEvent>,
provider: Res<SyncProviderResource>, provider: Res<SyncProviderResource>,
mut task_res: ResMut<PullTask>, mut task_res: ResMut<PullTask>,
mut status: ResMut<SyncStatusResource>, mut status: ResMut<SyncStatusResource>,
@@ -217,7 +217,7 @@ fn poll_pull_result(
/// that blocking on exit is permitted because the game loop is already /// that blocking on exit is permitted because the game loop is already
/// shutting down. /// shutting down.
fn push_on_exit( fn push_on_exit(
mut exit_events: EventReader<AppExit>, mut exit_events: MessageReader<AppExit>,
provider: Res<SyncProviderResource>, provider: Res<SyncProviderResource>,
stats: Res<StatsResource>, stats: Res<StatsResource>,
achievements: Res<AchievementsResource>, achievements: Res<AchievementsResource>,
+6 -6
View File
@@ -47,9 +47,9 @@ impl Plugin for TablePlugin {
// Register WindowResized so the plugin works under MinimalPlugins in // Register WindowResized so the plugin works under MinimalPlugins in
// tests. Under DefaultPlugins, bevy_window has already registered it // tests. Under DefaultPlugins, bevy_window has already registered it
// and this call is a no-op. // and this call is a no-op.
app.add_event::<WindowResized>() app.add_message::<WindowResized>()
.add_event::<SettingsChangedEvent>() .add_message::<SettingsChangedEvent>()
.add_event::<HintVisualEvent>() .add_message::<HintVisualEvent>()
.add_systems(Startup, setup_table) .add_systems(Startup, setup_table)
.add_systems( .add_systems(
Update, Update,
@@ -133,7 +133,7 @@ fn spawn_background(commands: &mut Commands, window_size: Vec2, color: Color) {
} }
fn apply_theme_on_settings_change( fn apply_theme_on_settings_change(
mut events: EventReader<SettingsChangedEvent>, mut events: MessageReader<SettingsChangedEvent>,
mut backgrounds: Query<&mut Sprite, With<TableBackground>>, mut backgrounds: Query<&mut Sprite, With<TableBackground>>,
) { ) {
let Some(ev) = events.read().last() else { let Some(ev) = events.read().last() else {
@@ -213,7 +213,7 @@ fn spawn_pile_markers(commands: &mut Commands, layout: &Layout) {
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn on_window_resized( fn on_window_resized(
mut events: EventReader<WindowResized>, mut events: MessageReader<WindowResized>,
mut layout_res: Option<ResMut<LayoutResource>>, mut layout_res: Option<ResMut<LayoutResource>>,
mut backgrounds: Query< mut backgrounds: Query<
(&mut Sprite, &mut Transform), (&mut Sprite, &mut Transform),
@@ -261,7 +261,7 @@ const HINT_PILE_HIGHLIGHT_COLOUR: Color = Color::srgb(1.0, 0.85, 0.1);
/// If the pile marker already has a `HintPileHighlight` from a previous hint /// If the pile marker already has a `HintPileHighlight` from a previous hint
/// press, the timer is reset to 2 s without changing `original_color`. /// press, the timer is reset to 2 s without changing `original_color`.
fn apply_hint_pile_highlight( fn apply_hint_pile_highlight(
mut events: EventReader<HintVisualEvent>, mut events: MessageReader<HintVisualEvent>,
mut commands: Commands, mut commands: Commands,
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite, Option<&HintPileHighlight>)>, mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite, Option<&HintPileHighlight>)>,
) { ) {
+13 -13
View File
@@ -26,7 +26,7 @@ pub struct TimeAttackResource {
/// Fired when the Time Attack timer expires. The summary toast in /// Fired when the Time Attack timer expires. The summary toast in
/// `AnimationPlugin` consumes this; UI/stats consumers can also subscribe. /// `AnimationPlugin` consumes this; UI/stats consumers can also subscribe.
#[derive(Event, Debug, Clone, Copy)] #[derive(Message, Debug, Clone, Copy)]
pub struct TimeAttackEndedEvent { pub struct TimeAttackEndedEvent {
pub wins: u32, pub wins: u32,
} }
@@ -36,10 +36,10 @@ pub struct TimeAttackPlugin;
impl Plugin for TimeAttackPlugin { impl Plugin for TimeAttackPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<TimeAttackResource>() app.init_resource::<TimeAttackResource>()
.add_event::<TimeAttackEndedEvent>() .add_message::<TimeAttackEndedEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<InfoToastEvent>() .add_message::<InfoToastEvent>()
.add_systems( .add_systems(
Update, Update,
handle_start_time_attack_request.before(GameMutation), handle_start_time_attack_request.before(GameMutation),
@@ -53,8 +53,8 @@ fn handle_start_time_attack_request(
keys: Res<ButtonInput<KeyCode>>, keys: Res<ButtonInput<KeyCode>>,
progress: Res<ProgressResource>, progress: Res<ProgressResource>,
mut session: ResMut<TimeAttackResource>, mut session: ResMut<TimeAttackResource>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
mut info_toast: EventWriter<InfoToastEvent>, mut info_toast: MessageWriter<InfoToastEvent>,
) { ) {
if !keys.just_pressed(KeyCode::KeyT) { if !keys.just_pressed(KeyCode::KeyT) {
return; return;
@@ -79,7 +79,7 @@ fn handle_start_time_attack_request(
fn advance_time_attack( fn advance_time_attack(
time: Res<Time>, time: Res<Time>,
mut session: ResMut<TimeAttackResource>, mut session: ResMut<TimeAttackResource>,
mut ended: EventWriter<TimeAttackEndedEvent>, mut ended: MessageWriter<TimeAttackEndedEvent>,
paused: Option<Res<crate::pause_plugin::PausedResource>>, paused: Option<Res<crate::pause_plugin::PausedResource>>,
) { ) {
if !session.active { if !session.active {
@@ -98,10 +98,10 @@ fn advance_time_attack(
} }
fn auto_deal_on_time_attack_win( fn auto_deal_on_time_attack_win(
mut wins: EventReader<GameWonEvent>, mut wins: MessageReader<GameWonEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut session: ResMut<TimeAttackResource>, mut session: ResMut<TimeAttackResource>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
) { ) {
for _ in wins.read() { for _ in wins.read() {
if !session.active || game.0.mode != GameMode::TimeAttack { if !session.active || game.0.mode != GameMode::TimeAttack {
@@ -213,7 +213,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack); GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 60, time_seconds: 60,
}); });
@@ -237,7 +237,7 @@ mod tests {
app.world_mut().resource_mut::<GameStateResource>().0 = app.world_mut().resource_mut::<GameStateResource>().0 =
GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack); GameState::new_with_mode(7, DrawMode::DrawOne, GameMode::TimeAttack);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 60, time_seconds: 60,
}); });
@@ -256,7 +256,7 @@ mod tests {
wins: 0, wins: 0,
}; };
// GameStateResource defaults to Classic mode. // GameStateResource defaults to Classic mode.
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 60, time_seconds: 60,
}); });
+13 -13
View File
@@ -15,7 +15,7 @@ use crate::progress_plugin::{LevelUpEvent, ProgressResource, ProgressStoragePath
use crate::resources::GameStateResource; use crate::resources::GameStateResource;
/// Fired when the player has just completed a weekly goal. /// Fired when the player has just completed a weekly goal.
#[derive(Event, Debug, Clone)] #[derive(Message, Debug, Clone)]
pub struct WeeklyGoalCompletedEvent { pub struct WeeklyGoalCompletedEvent {
pub goal_id: String, pub goal_id: String,
pub description: String, pub description: String,
@@ -25,9 +25,9 @@ pub struct WeeklyGoalsPlugin;
impl Plugin for WeeklyGoalsPlugin { impl Plugin for WeeklyGoalsPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<WeeklyGoalCompletedEvent>() app.add_message::<WeeklyGoalCompletedEvent>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.add_systems(Startup, roll_weekly_goals_on_startup) .add_systems(Startup, roll_weekly_goals_on_startup)
// Run after GameMutation (so GameWonEvent is available) and // Run after GameMutation (so GameWonEvent is available) and
// ProgressUpdate (so we don't fight ProgressPlugin's add_xp). // ProgressUpdate (so we don't fight ProgressPlugin's add_xp).
@@ -57,13 +57,13 @@ fn roll_weekly_goals_on_startup(
} }
fn evaluate_weekly_goals( fn evaluate_weekly_goals(
mut wins: EventReader<GameWonEvent>, mut wins: MessageReader<GameWonEvent>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
mut progress: ResMut<ProgressResource>, mut progress: ResMut<ProgressResource>,
path: Res<ProgressStoragePath>, path: Res<ProgressStoragePath>,
mut completions: EventWriter<WeeklyGoalCompletedEvent>, mut completions: MessageWriter<WeeklyGoalCompletedEvent>,
mut levelups: EventWriter<LevelUpEvent>, mut levelups: MessageWriter<LevelUpEvent>,
mut xp_awarded: EventWriter<XpAwardedEvent>, mut xp_awarded: MessageWriter<XpAwardedEvent>,
) { ) {
let mut events: Vec<&GameWonEvent> = wins.read().collect(); let mut events: Vec<&GameWonEvent> = wins.read().collect();
if events.is_empty() { if events.is_empty() {
@@ -149,7 +149,7 @@ mod tests {
#[test] #[test]
fn first_win_increments_win_game_goal() { fn first_win_increments_win_game_goal() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
@@ -164,7 +164,7 @@ mod tests {
#[test] #[test]
fn fast_win_ticks_fast_goal_too() { fn fast_win_ticks_fast_goal_too() {
let mut app = headless_app(); let mut app = headless_app();
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 60, time_seconds: 60,
}); });
@@ -181,7 +181,7 @@ mod tests {
.0 .0
.undo_count = 1; .undo_count = 1;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 200, time_seconds: 200,
}); });
@@ -214,7 +214,7 @@ mod tests {
let xp_before = app.world().resource::<ProgressResource>().0.total_xp; let xp_before = app.world().resource::<ProgressResource>().0.total_xp;
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 60, time_seconds: 60,
}); });
@@ -280,7 +280,7 @@ mod tests {
.0 .0
.weekly_goal_week_iso = Some(key); .weekly_goal_week_iso = Some(key);
app.world_mut().send_event(GameWonEvent { app.world_mut().write_message(GameWonEvent {
score: 500, score: 500,
time_seconds: 60, time_seconds: 60,
}); });
+28 -28
View File
@@ -159,11 +159,11 @@ impl Plugin for WinSummaryPlugin {
app.init_resource::<WinSummaryPending>() app.init_resource::<WinSummaryPending>()
.init_resource::<ScreenShakeResource>() .init_resource::<ScreenShakeResource>()
.init_resource::<SessionAchievements>() .init_resource::<SessionAchievements>()
.add_event::<GameWonEvent>() .add_message::<GameWonEvent>()
.add_event::<XpAwardedEvent>() .add_message::<XpAwardedEvent>()
.add_event::<NewGameRequestEvent>() .add_message::<NewGameRequestEvent>()
.add_event::<InfoToastEvent>() .add_message::<InfoToastEvent>()
.add_event::<AchievementUnlockedEvent>() .add_message::<AchievementUnlockedEvent>()
// `cache_win_data` must run BEFORE `StatsUpdate` so it can compare // `cache_win_data` must run BEFORE `StatsUpdate` so it can compare
// the player's old personal-best values before `StatsPlugin` overwrites them. // the player's old personal-best values before `StatsPlugin` overwrites them.
.add_systems( .add_systems(
@@ -221,13 +221,13 @@ pub fn format_win_time(seconds: u64) -> String {
/// This system is scheduled `.before(StatsUpdate)` so the comparison always /// This system is scheduled `.before(StatsUpdate)` so the comparison always
/// sees the old best values. /// sees the old best values.
fn cache_win_data( fn cache_win_data(
mut won: EventReader<GameWonEvent>, mut won: MessageReader<GameWonEvent>,
mut xp: EventReader<XpAwardedEvent>, mut xp: MessageReader<XpAwardedEvent>,
mut pending: ResMut<WinSummaryPending>, mut pending: ResMut<WinSummaryPending>,
stats: Res<StatsResource>, stats: Res<StatsResource>,
game: Res<GameStateResource>, game: Res<GameStateResource>,
progress: Res<ProgressResource>, progress: Res<ProgressResource>,
mut toast: EventWriter<InfoToastEvent>, mut toast: MessageWriter<InfoToastEvent>,
) { ) {
for ev in won.read() { for ev in won.read() {
// Compare against old personal bests BEFORE StatsPlugin updates them. // Compare against old personal bests BEFORE StatsPlugin updates them.
@@ -274,8 +274,8 @@ fn cache_win_data(
/// reader covers every implicit game-context reset in addition to the /// reader covers every implicit game-context reset in addition to the
/// explicit N / "Play Again" new-game requests. /// explicit N / "Play Again" new-game requests.
fn collect_session_achievements( fn collect_session_achievements(
mut unlocks: EventReader<AchievementUnlockedEvent>, mut unlocks: MessageReader<AchievementUnlockedEvent>,
mut new_games: EventReader<NewGameRequestEvent>, mut new_games: MessageReader<NewGameRequestEvent>,
mut session: ResMut<SessionAchievements>, mut session: ResMut<SessionAchievements>,
) { ) {
// Reset on any new-game request (including mode switches via Z/X/C/T) so // Reset on any new-game request (including mode switches via Z/X/C/T) so
@@ -303,8 +303,8 @@ fn collect_session_achievements(
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn spawn_win_summary_after_delay( fn spawn_win_summary_after_delay(
mut commands: Commands, mut commands: Commands,
mut won: EventReader<GameWonEvent>, mut won: MessageReader<GameWonEvent>,
mut xp_events: EventReader<XpAwardedEvent>, mut xp_events: MessageReader<XpAwardedEvent>,
mut shake: ResMut<ScreenShakeResource>, mut shake: ResMut<ScreenShakeResource>,
mut pending: ResMut<WinSummaryPending>, mut pending: ResMut<WinSummaryPending>,
session: Res<SessionAchievements>, session: Res<SessionAchievements>,
@@ -352,7 +352,7 @@ fn handle_win_summary_buttons(
interaction_query: Query<(&Interaction, &WinSummaryButton), Changed<Interaction>>, interaction_query: Query<(&Interaction, &WinSummaryButton), Changed<Interaction>>,
overlays: Query<Entity, With<WinSummaryOverlay>>, overlays: Query<Entity, With<WinSummaryOverlay>>,
mut commands: Commands, mut commands: Commands,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: MessageWriter<NewGameRequestEvent>,
) { ) {
for (interaction, button) in &interaction_query { for (interaction, button) in &interaction_query {
if *interaction != Interaction::Pressed { if *interaction != Interaction::Pressed {
@@ -677,7 +677,7 @@ mod tests {
use solitaire_data::AchievementRecord; use solitaire_data::AchievementRecord;
let record = AchievementRecord::locked("first_win"); let record = AchievementRecord::locked("first_win");
app.world_mut() app.world_mut()
.send_event(AchievementUnlockedEvent(record)); .write_message(AchievementUnlockedEvent(record));
app.update(); app.update();
let session = app.world().resource::<SessionAchievements>(); let session = app.world().resource::<SessionAchievements>();
@@ -693,7 +693,7 @@ mod tests {
use solitaire_data::AchievementRecord; use solitaire_data::AchievementRecord;
let record = AchievementRecord::locked("first_win"); let record = AchievementRecord::locked("first_win");
app.world_mut() app.world_mut()
.send_event(AchievementUnlockedEvent(record)); .write_message(AchievementUnlockedEvent(record));
app.update(); app.update();
// Confirm it was recorded. // Confirm it was recorded.
@@ -703,7 +703,7 @@ mod tests {
); );
// Fire NewGameRequestEvent — should clear the list. // Fire NewGameRequestEvent — should clear the list.
app.world_mut().send_event(NewGameRequestEvent::default()); app.world_mut().write_message(NewGameRequestEvent::default());
app.update(); app.update();
assert!( assert!(
@@ -727,7 +727,7 @@ mod tests {
// Simulate an achievement unlock during the current session. // Simulate an achievement unlock during the current session.
let record = AchievementRecord::locked("first_win"); let record = AchievementRecord::locked("first_win");
app.world_mut() app.world_mut()
.send_event(AchievementUnlockedEvent(record)); .write_message(AchievementUnlockedEvent(record));
app.update(); app.update();
assert_eq!( assert_eq!(
@@ -739,7 +739,7 @@ mod tests {
// Simulate pressing Z (Zen mode switch) — fires NewGameRequestEvent // Simulate pressing Z (Zen mode switch) — fires NewGameRequestEvent
// with mode = Some(Zen). Same event shape used by X (Challenge), // with mode = Some(Zen). Same event shape used by X (Challenge),
// C (Daily Challenge), and T (Time Attack). // C (Daily Challenge), and T (Time Attack).
app.world_mut().send_event(NewGameRequestEvent { app.world_mut().write_message(NewGameRequestEvent {
seed: None, seed: None,
mode: Some(GameMode::Zen), mode: Some(GameMode::Zen),
}); });
@@ -756,7 +756,7 @@ mod tests {
let mut app = make_app(); let mut app = make_app();
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 1234, time_seconds: 90 }); .write_message(GameWonEvent { score: 1234, time_seconds: 90 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -771,8 +771,8 @@ mod tests {
fn cache_win_data_sets_xp_from_xp_awarded_event() { fn cache_win_data_sets_xp_from_xp_awarded_event() {
let mut app = make_app(); let mut app = make_app();
app.world_mut().send_event(GameWonEvent { score: 0, time_seconds: 0 }); app.world_mut().write_message(GameWonEvent { score: 0, time_seconds: 0 });
app.world_mut().send_event(XpAwardedEvent { amount: 75 }); app.world_mut().write_message(XpAwardedEvent { amount: 75 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -784,7 +784,7 @@ mod tests {
let mut app = make_app(); let mut app = make_app();
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 0, time_seconds: 0 }); .write_message(GameWonEvent { score: 0, time_seconds: 0 });
app.update(); app.update();
let shake = app.world().resource::<ScreenShakeResource>(); let shake = app.world().resource::<ScreenShakeResource>();
@@ -802,7 +802,7 @@ mod tests {
let mut app = make_app(); let mut app = make_app();
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 500, time_seconds: 120 }); .write_message(GameWonEvent { score: 500, time_seconds: 120 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -820,7 +820,7 @@ mod tests {
// Score 500 beats previous best of 400. // Score 500 beats previous best of 400.
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 500, time_seconds: 300 }); .write_message(GameWonEvent { score: 500, time_seconds: 300 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -838,7 +838,7 @@ mod tests {
// Score 500 does not beat 800, but time 100 < 200. // Score 500 does not beat 800, but time 100 < 200.
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 500, time_seconds: 100 }); .write_message(GameWonEvent { score: 500, time_seconds: 100 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -856,7 +856,7 @@ mod tests {
// Score 500 < 800 and time 120 > 60 — neither record broken. // Score 500 < 800 and time 120 > 60 — neither record broken.
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 500, time_seconds: 120 }); .write_message(GameWonEvent { score: 500, time_seconds: 120 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -887,7 +887,7 @@ mod tests {
} }
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 0, time_seconds: 0 }); .write_message(GameWonEvent { score: 0, time_seconds: 0 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();
@@ -903,7 +903,7 @@ mod tests {
let mut app = make_app(); let mut app = make_app();
// Default game mode is Classic — challenge_level should stay None. // Default game mode is Classic — challenge_level should stay None.
app.world_mut() app.world_mut()
.send_event(GameWonEvent { score: 0, time_seconds: 0 }); .write_message(GameWonEvent { score: 0, time_seconds: 0 });
app.update(); app.update();
let pending = app.world().resource::<WinSummaryPending>(); let pending = app.world().resource::<WinSummaryPending>();