chore(deps): migrate to Bevy 0.16, axum 0.8, and other package updates

- Bump bevy 0.15 → 0.16; fixes all breaking API changes:
  ChildBuilder → ChildSpawnerCommands, Parent → ChildOf,
  despawn_descendants → despawn_related::<Children>(),
  despawn_recursive → despawn (now recursive by default),
  EventWriter::send → write, Query::{get_single,get_single_mut}
  → {single,single_mut}, ChildOf::get → parent()
- Bump axum 0.7 → 0.8; remove axum::async_trait from FromRequestParts
- Bump tower_governor 0.4 → 0.8; fix GovernorLayer::new() API
- Bump jsonwebtoken 9 → 10 with rust_crypto feature only
- Bump thiserror 1 → 2, dirs 5 → 6, bcrypt 0.15 → 0.19,
  reqwest 0.12 → 0.13 (rustls feature rename)
- Regenerate .sqlx offline cache for sqlx compile-time query checks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-28 12:31:12 -07:00
parent eedddb979e
commit c8553dc8c5
28 changed files with 1098 additions and 692 deletions
+5 -5
View File
@@ -156,10 +156,10 @@ fn evaluate_on_win(
}
}
Reward::BonusXp(amount) => {
xp_awarded.send(XpAwardedEvent { amount });
xp_awarded.write(XpAwardedEvent { amount });
let prev_level = progress.0.add_xp(amount);
if progress.0.leveled_up_from(prev_level) {
levelups.send(LevelUpEvent {
levelups.write(LevelUpEvent {
previous_level: prev_level,
new_level: progress.0.level,
total_xp: progress.0.total_xp,
@@ -173,7 +173,7 @@ fn evaluate_on_win(
record.reward_granted = true;
}
unlocks.send(AchievementUnlockedEvent(record.clone()));
unlocks.write(AchievementUnlockedEvent(record.clone()));
}
if achievements_changed {
@@ -211,8 +211,8 @@ fn toggle_achievements_screen(
if !keys.just_pressed(KeyCode::KeyA) {
return;
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} else {
spawn_achievements_screen(&mut commands, &achievements.0);
}
+2 -2
View File
@@ -465,7 +465,7 @@ fn drive_toast_display(
active.timer -= dt;
if active.timer <= 0.0 {
// Despawn the toast entity and clear the active slot.
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
active.entity = None;
active.timer = 0.0;
}
@@ -532,7 +532,7 @@ fn tick_toasts(
for (entity, mut timer) in &mut toasts {
timer.0 -= dt;
if timer.0 <= 0.0 {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
}
}
+1 -1
View File
@@ -122,7 +122,7 @@ fn drive_auto_complete(
return;
};
moves.send(MoveRequestEvent { from, to, count: 1 });
moves.write(MoveRequestEvent { from, to, count: 1 });
state.cooldown = STEP_INTERVAL;
}
@@ -236,13 +236,13 @@ pub(crate) fn drain_input_buffer(
}
match buffer.queue.pop_front() {
Some(BufferedInput::Move { from }) => {
move_events.send(from);
move_events.write(from);
}
Some(BufferedInput::Draw) => {
draw_events.send(DrawRequestEvent);
draw_events.write(DrawRequestEvent);
}
Some(BufferedInput::Undo) => {
undo_events.send(UndoRequestEvent);
undo_events.write(UndoRequestEvent);
}
None => {}
}
@@ -259,9 +259,9 @@ fn cursor_world(
windows: &Query<&Window, With<PrimaryWindow>>,
cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> {
let window = windows.get_single().ok()?;
let window = windows.single().ok()?;
let cursor = window.cursor_position()?;
let (camera, camera_transform) = cameras.get_single().ok()?;
let (camera, camera_transform) = cameras.single().ok()?;
camera.viewport_to_world_2d(camera_transform, cursor).ok()
}
+13 -13
View File
@@ -187,7 +187,7 @@ fn resync_cards_on_settings_change(
mut state_events: EventWriter<StateChangedEvent>,
) {
if setting_events.read().next().is_some() {
state_events.send(StateChangedEvent);
state_events.write(StateChangedEvent);
}
}
@@ -256,7 +256,7 @@ fn sync_cards(
// Despawn any entity whose card is no longer tracked.
for (card_id, (entity, _)) in &existing {
if !live_ids.contains(card_id) {
commands.entity(*entity).despawn_recursive();
commands.entity(*entity).despawn();
}
}
@@ -443,7 +443,7 @@ fn update_card_entity(
// Despawn the old label child and respawn a fresh one, so rank/suit/
// colour/visibility all stay in sync with the card's current state.
commands.entity(entity).despawn_descendants();
commands.entity(entity).despawn_related::<Children>();
commands.entity(entity).with_children(|b| {
b.spawn((
CardLabel,
@@ -558,7 +558,7 @@ fn tick_flip_anim(
transform.scale.x = 0.0;
// Fire the reveal event exactly once, at the phase transition,
// so the flip sound is synchronised with the visual face reveal.
reveal_events.send(CardFaceRevealedEvent(card_entity.card_id));
reveal_events.write(CardFaceRevealedEvent(card_entity.card_id));
}
}
FlipPhase::ScalingUp => {
@@ -592,7 +592,7 @@ fn update_drag_shadow(
if drag.is_idle() {
// No drag in progress — remove shadow if it exists.
if let Some(e) = shadow.take() {
commands.entity(e).despawn_recursive();
commands.entity(e).despawn();
}
return;
}
@@ -847,9 +847,9 @@ fn cursor_world_pos(
windows: &Query<&Window, With<bevy::window::PrimaryWindow>>,
cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> {
let window = windows.get_single().ok()?;
let window = windows.single().ok()?;
let cursor = window.cursor_position()?;
let (camera, camera_transform) = cameras.get_single().ok()?;
let (camera, camera_transform) = cameras.single().ok()?;
camera.viewport_to_world_2d(camera_transform, cursor).ok()
}
@@ -911,7 +911,7 @@ fn apply_stock_empty_indicator(
commands: &mut Commands,
game: &GameState,
pile_markers: &mut Query<(Entity, &PileMarker, &mut Sprite)>,
label_children: &Query<(Entity, &Parent), With<StockEmptyLabel>>,
label_children: &Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
layout: &Layout,
) {
let stock_empty = game
@@ -931,7 +931,7 @@ fn apply_stock_empty_indicator(
// Spawn the "↺" label only if one does not already exist.
let already_has_label = label_children
.iter()
.any(|(_, parent)| parent.get() == entity);
.any(|(_, parent)| parent.parent() == entity);
if !already_has_label {
let font_size = layout.card_size.x * 0.4;
commands.entity(entity).with_children(|b| {
@@ -950,8 +950,8 @@ fn apply_stock_empty_indicator(
// Despawn any existing "↺" label children.
for (label_entity, parent) in label_children.iter() {
if parent.get() == entity {
commands.entity(label_entity).despawn_recursive();
if parent.parent() == entity {
commands.entity(label_entity).despawn();
}
}
}
@@ -965,7 +965,7 @@ fn update_stock_empty_indicator_startup(
game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>,
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>,
label_children: Query<(Entity, &Parent), With<StockEmptyLabel>>,
label_children: Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
) {
let Some(layout) = layout else { return };
apply_stock_empty_indicator(
@@ -985,7 +985,7 @@ fn update_stock_empty_indicator(
game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>,
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>,
label_children: Query<(Entity, &Parent), With<StockEmptyLabel>>,
label_children: Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
) {
if events.read().next().is_none() {
return;
+4 -4
View File
@@ -59,8 +59,8 @@ fn advance_on_challenge_win(
}
// Human-readable level is 1-based (index 0 → "Challenge 1").
let level_number = prev.saturating_add(1);
toast.send(InfoToastEvent(format!("Challenge {level_number} complete!")));
advanced.send(ChallengeAdvancedEvent {
toast.write(InfoToastEvent(format!("Challenge {level_number} complete!")));
advanced.write(ChallengeAdvancedEvent {
previous_index: prev,
new_index: progress.0.challenge_index,
});
@@ -77,7 +77,7 @@ fn handle_start_challenge_request(
return;
}
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
info_toast.send(InfoToastEvent(format!(
info_toast.write(InfoToastEvent(format!(
"Challenge mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
)));
return;
@@ -86,7 +86,7 @@ fn handle_start_challenge_request(
warn!("challenge seed list is empty");
return;
};
new_game.send(NewGameRequestEvent {
new_game.write(NewGameRequestEvent {
seed: Some(seed),
mode: Some(GameMode::Challenge),
});
+2 -2
View File
@@ -52,7 +52,7 @@ fn update_cursor_icon(
game: Option<Res<GameStateResource>>,
mut commands: Commands,
) {
let Ok((win_entity, window)) = windows.get_single() else { return };
let Ok((win_entity, window)) = windows.single() else { return };
if !drag.is_idle() {
commands
@@ -63,7 +63,7 @@ fn update_cursor_icon(
let hovering = (|| {
let cursor = window.cursor_position()?;
let (camera, cam_xf) = cameras.get_single().ok()?;
let (camera, cam_xf) = cameras.single().ok()?;
let world = camera.viewport_to_world_2d(cam_xf, cursor).ok()?;
let layout = layout.as_ref()?.0.clone();
let game = game.as_ref()?;
@@ -174,17 +174,17 @@ fn handle_daily_completion(
continue;
}
progress.0.add_xp(DAILY_BONUS_XP);
xp_awarded.send(XpAwardedEvent { amount: DAILY_BONUS_XP });
xp_awarded.write(XpAwardedEvent { amount: DAILY_BONUS_XP });
if let Some(target) = &path.0 {
if let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after daily completion: {e}");
}
}
completed.send(DailyChallengeCompletedEvent {
completed.write(DailyChallengeCompletedEvent {
date: daily.date,
streak: progress.0.daily_challenge_streak,
});
toast.send(InfoToastEvent("Daily challenge complete! +100 XP".to_string()));
toast.write(InfoToastEvent("Daily challenge complete! +100 XP".to_string()));
}
}
@@ -195,7 +195,7 @@ fn handle_start_daily_request(
mut announce: EventWriter<DailyGoalAnnouncementEvent>,
) {
if keys.just_pressed(KeyCode::KeyC) {
new_game.send(NewGameRequestEvent {
new_game.write(NewGameRequestEvent {
seed: Some(daily.seed),
mode: None,
});
@@ -203,7 +203,7 @@ fn handle_start_daily_request(
.goal_description
.clone()
.unwrap_or_else(|| "Daily Challenge".to_string());
announce.send(DailyGoalAnnouncementEvent(desc));
announce.write(DailyGoalAnnouncementEvent(desc));
}
}
+20 -20
View File
@@ -169,7 +169,7 @@ fn handle_new_game(
if needs_confirm && !confirm_already_open {
// Despawn any stale game-over overlay before showing confirm dialog.
for entity in &game_over_screens {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
spawn_confirm_dialog(&mut commands, *ev);
continue;
@@ -177,10 +177,10 @@ fn handle_new_game(
// Despawn confirm and game-over overlays before starting the new game.
for entity in &confirm_screens {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
for entity in &game_over_screens {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
let seed = ev.seed.unwrap_or_else(seed_from_system_time);
@@ -199,7 +199,7 @@ fn handle_new_game(
warn!("game_state: failed to delete saved game: {e}");
}
}
changed.send(StateChangedEvent);
changed.write(StateChangedEvent);
}
}
@@ -289,7 +289,7 @@ fn handle_confirm_input(
screens: Query<(Entity, &OriginalNewGameRequest), With<ConfirmNewGameScreen>>,
mut new_game: EventWriter<NewGameRequestEvent>,
) {
let Ok((entity, original)) = screens.get_single() else {
let Ok((entity, original)) = screens.single() else {
return;
};
let Some(keys) = keys else {
@@ -300,16 +300,16 @@ fn handle_confirm_input(
let cancelled = keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape);
if confirmed {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
// Re-send with move_count already 0 would bypass the dialog next time.
// We fire the event — handle_new_game will skip the dialog because
// the screen is despawned before the next read.
new_game.send(NewGameRequestEvent {
new_game.write(NewGameRequestEvent {
seed: original.0.seed,
mode: original.0.mode,
});
} else if cancelled {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
}
@@ -347,9 +347,9 @@ fn handle_draw(
Ok(()) => {
// Fire a flip event for each card that moved from stock to waste.
for id in drawn_ids {
flipped.send(CardFlippedEvent(id));
flipped.write(CardFlippedEvent(id));
}
changed.send(StateChangedEvent);
changed.write(StateChangedEvent);
}
Err(e) => warn!("draw rejected: {e}"),
}
@@ -385,12 +385,12 @@ fn handle_move(
.and_then(|p| p.cards.last())
.is_some_and(|c| c.id == fid && c.face_up)
{
flipped.send(crate::events::CardFlippedEvent(fid));
flipped.write(crate::events::CardFlippedEvent(fid));
}
}
changed.send(StateChangedEvent);
changed.write(StateChangedEvent);
if !was_won && game.0.is_won {
won.send(GameWonEvent {
won.write(GameWonEvent {
score: game.0.score,
time_seconds: game.0.elapsed_seconds,
});
@@ -418,10 +418,10 @@ fn handle_undo(
for _ in undos.read() {
match game.0.undo() {
Ok(()) => {
changed.send(StateChangedEvent);
changed.write(StateChangedEvent);
}
Err(MoveError::UndoStackEmpty) => {
toast.send(InfoToastEvent("Nothing to undo".to_string()));
toast.write(InfoToastEvent("Nothing to undo".to_string()));
}
Err(e) => warn!("undo rejected: {e}"),
}
@@ -523,7 +523,7 @@ fn check_no_moves(
let moves_ok = has_legal_moves(&game.0);
if moves_ok || game.0.is_won {
for entity in &game_over_screens {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
}
@@ -532,7 +532,7 @@ fn check_no_moves(
}
if !moves_ok && !*already_fired {
toast.send(InfoToastEvent(
toast.write(InfoToastEvent(
"No moves available \u{2014} press D to draw or N for a new game".to_string(),
));
*already_fired = true;
@@ -639,12 +639,12 @@ fn handle_game_over_input(
};
if keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape) {
new_game.send(NewGameRequestEvent::default());
new_game.write(NewGameRequestEvent::default());
} else if keys.just_pressed(KeyCode::KeyU) {
for entity in &screens {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
undo.send(UndoRequestEvent);
undo.write(UndoRequestEvent);
}
}
+2 -2
View File
@@ -25,8 +25,8 @@ fn toggle_help_screen(
if !keys.just_pressed(KeyCode::F1) {
return;
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} else {
spawn_help_screen(&mut commands);
}
+3 -3
View File
@@ -31,8 +31,8 @@ fn toggle_home_screen(
if !keys.just_pressed(KeyCode::KeyM) {
return;
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} else {
spawn_home_screen(&mut commands, &game);
}
@@ -139,7 +139,7 @@ fn spawn_home_screen(commands: &mut Commands, game: &GameStateResource) {
});
}
fn spawn_shortcut_row(parent: &mut ChildBuilder, key: &str, action: &str) {
fn spawn_shortcut_row(parent: &mut ChildSpawnerCommands, key: &str, action: &str) {
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
+12 -12
View File
@@ -325,7 +325,7 @@ fn update_hud(
if game.is_changed() {
let g = &game.0;
let is_zen = g.mode == GameMode::Zen;
if let Ok(mut t) = score_q.get_single_mut() {
if let Ok(mut t) = score_q.single_mut() {
// Zen mode suppresses score display per spec ("No score display").
**t = if is_zen {
String::new()
@@ -333,10 +333,10 @@ fn update_hud(
format!("Score: {}", g.score)
};
}
if let Ok(mut t) = moves_q.get_single_mut() {
if let Ok(mut t) = moves_q.single_mut() {
**t = format!("Moves: {}", g.move_count);
}
if let Ok(mut t) = mode_q.get_single_mut() {
if let Ok(mut t) = mode_q.single_mut() {
**t = match g.mode {
GameMode::Classic => match g.draw_mode {
DrawMode::DrawOne => String::new(),
@@ -349,7 +349,7 @@ fn update_hud(
}
// --- Daily challenge constraint (with time-low colour warning) ---
if let Ok((mut t, mut color)) = challenge_q.get_single_mut() {
if let Ok((mut t, mut color)) = challenge_q.single_mut() {
if g.is_won {
**t = String::new();
} else if let Some(dc) = daily.as_deref() {
@@ -364,7 +364,7 @@ fn update_hud(
}
// --- Undo count ---
if let Ok((mut t, mut color)) = undos_q.get_single_mut() {
if let Ok((mut t, mut color)) = undos_q.single_mut() {
let count = g.undo_count;
if count == 0 {
**t = String::new();
@@ -377,7 +377,7 @@ fn update_hud(
}
// --- Recycle counter (both modes, hidden until first recycle) ---
if let Ok(mut t) = recycles_q.get_single_mut() {
if let Ok(mut t) = recycles_q.single_mut() {
**t = if g.recycle_count > 0 {
format!("Recycles: {}", g.recycle_count)
} else {
@@ -386,7 +386,7 @@ fn update_hud(
}
// --- Draw-cycle indicator (Draw-Three mode only) ---
if let Ok(mut t) = draw_cycle_q.get_single_mut() {
if let Ok(mut t) = draw_cycle_q.single_mut() {
**t = if g.is_won || g.draw_mode != DrawMode::DrawThree {
// Hide when not in Draw-Three or after the game is won.
String::new()
@@ -405,7 +405,7 @@ fn update_hud(
let is_zen = game.0.mode == GameMode::Zen;
let update_time = (ta_active || game.is_changed()) && !is_zen;
if update_time {
if let Ok(mut t) = time_q.get_single_mut() {
if let Ok(mut t) = time_q.single_mut() {
if let Some(ta) = time_attack.as_ref().filter(|ta| ta.active) {
let remaining = ta.remaining_secs.max(0.0) as u64;
let m = remaining / 60;
@@ -422,7 +422,7 @@ fn update_hud(
// Clear the time display immediately whenever Zen mode is active —
// do not guard on game.is_changed() so it clears on the same frame
// the player presses Z, before any move is made.
if let Ok(mut t) = time_q.get_single_mut() {
if let Ok(mut t) = time_q.single_mut() {
**t = String::new();
}
}
@@ -432,7 +432,7 @@ fn update_hud(
let ac_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
let ac_changed = auto_complete.as_ref().is_some_and(|ac| ac.is_changed());
if ac_changed || game.is_changed() {
if let Ok(mut t) = auto_q.get_single_mut() {
if let Ok(mut t) = auto_q.single_mut() {
**t = if ac_active {
"AUTO".to_string()
} else {
@@ -451,7 +451,7 @@ fn update_selection_hud(
selection: Option<Res<SelectionState>>,
mut q: Query<&mut Text, With<HudSelection>>,
) {
let Ok(mut t) = q.get_single_mut() else { return };
let Ok(mut t) = q.single_mut() else { return };
let label = match selection.as_deref().and_then(|s| s.selected_pile.as_ref()) {
None => String::new(),
Some(PileType::Waste) => "▶ Waste".to_string(),
@@ -480,7 +480,7 @@ fn announce_auto_complete(
) {
let now_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
if now_active && !*was_active {
toast.send(InfoToastEvent("Auto-completing...".to_string()));
toast.write(InfoToastEvent("Auto-completing...".to_string()));
}
*was_active = now_active;
}
+28 -28
View File
@@ -126,7 +126,7 @@ fn handle_keyboard(
// Countdown expired without a second N press — notify the player.
if *confirm_pending {
*confirm_pending = false;
ev.info_toast.send(InfoToastEvent("New game cancelled".to_string()));
ev.info_toast.write(InfoToastEvent("New game cancelled".to_string()));
}
}
}
@@ -140,7 +140,7 @@ fn handle_keyboard(
if keys.just_pressed(KeyCode::KeyU) {
if *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; }
ev.undo.send(UndoRequestEvent);
ev.undo.write(UndoRequestEvent);
}
if keys.just_pressed(KeyCode::KeyN) {
// If a Time Attack session is running, cancel it and start a Classic game.
@@ -148,8 +148,8 @@ fn handle_keyboard(
if session.active {
session.active = false;
session.remaining_secs = 0.0;
ev.info_toast.send(InfoToastEvent("Time Attack ended".to_string()));
ev.new_game.send(NewGameRequestEvent {
ev.info_toast.write(InfoToastEvent("Time Attack ended".to_string()));
ev.new_game.write(NewGameRequestEvent {
seed: None,
mode: Some(solitaire_core::game_state::GameMode::Classic),
});
@@ -162,19 +162,19 @@ fn handle_keyboard(
let shift_held = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight);
if shift_held || !active_game {
// Shift+N or no active game — start immediately, no confirmation.
ev.new_game.send(NewGameRequestEvent::default());
ev.new_game.write(NewGameRequestEvent::default());
*confirm_countdown = 0.0;
*confirm_pending = false;
} else if *confirm_countdown > 0.0 {
// Second press within the window — confirmed.
ev.new_game.send(NewGameRequestEvent::default());
ev.new_game.write(NewGameRequestEvent::default());
*confirm_countdown = 0.0;
*confirm_pending = false;
} else {
// First press on an active game — require confirmation.
*confirm_countdown = NEW_GAME_CONFIRM_WINDOW;
*confirm_pending = true;
ev.confirm_event.send(NewGameConfirmEvent);
ev.confirm_event.write(NewGameConfirmEvent);
}
}
if keys.just_pressed(KeyCode::KeyZ) {
@@ -183,19 +183,19 @@ fn handle_keyboard(
// X is gated separately by ChallengePlugin.
let level = progress.as_ref().map_or(0, |p| p.0.level);
if level >= CHALLENGE_UNLOCK_LEVEL {
ev.new_game.send(NewGameRequestEvent {
ev.new_game.write(NewGameRequestEvent {
seed: None,
mode: Some(solitaire_core::game_state::GameMode::Zen),
});
} else {
ev.info_toast.send(InfoToastEvent(format!(
ev.info_toast.write(InfoToastEvent(format!(
"Zen mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
)));
}
}
if keys.just_pressed(KeyCode::KeyD) || keys.just_pressed(KeyCode::Space) {
if *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; }
ev.draw.send(DrawRequestEvent);
ev.draw.write(DrawRequestEvent);
}
// H — cycle through all available hints on each press, highlighting the
// source card yellow for 1.5 s. The index wraps around once all hints have
@@ -204,13 +204,13 @@ fn handle_keyboard(
if *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; }
if let Some(ref g) = game {
if g.0.is_won {
ev.info_toast.send(InfoToastEvent(
ev.info_toast.write(InfoToastEvent(
"Game won! Press N for a new game".to_string(),
));
} else if let Some(ref layout_res) = layout {
let hints = all_hints(&g.0);
if hints.is_empty() {
ev.info_toast.send(InfoToastEvent("No hints available".to_string()));
ev.info_toast.write(InfoToastEvent("No hints available".to_string()));
} else {
// Pick the hint at the current cycle index (wrapping) and advance.
let idx = hint_cycle.0 % hints.len();
@@ -229,7 +229,7 @@ fn handle_keyboard(
} else {
"Hint: draw from stock (D)".to_string()
};
ev.info_toast.send(InfoToastEvent(msg));
ev.info_toast.write(InfoToastEvent(msg));
} else {
// Find the top face-up card in the source pile and highlight it.
let top_card_id = g.0.piles.get(from)
@@ -251,7 +251,7 @@ fn handle_keyboard(
}
// Emit HintVisualEvent so the destination pile
// marker is also tinted gold for 2 s.
ev.hint_visual.send(HintVisualEvent {
ev.hint_visual.write(HintVisualEvent {
source_card_id: card_id,
dest_pile: to.clone(),
});
@@ -273,7 +273,7 @@ fn handle_keyboard(
}
_ => "Hint: move card".to_string(),
};
ev.info_toast.send(InfoToastEvent(msg));
ev.info_toast.write(InfoToastEvent(msg));
}
}
}
@@ -287,12 +287,12 @@ fn handle_keyboard(
if active_game {
if *forfeit_countdown > 0.0 {
// Second press within the confirmation window — confirmed.
ev.forfeit.send(ForfeitEvent);
ev.forfeit.write(ForfeitEvent);
*forfeit_countdown = 0.0;
} else {
// First press — start the countdown and warn the player.
*forfeit_countdown = FORFEIT_CONFIRM_WINDOW;
ev.info_toast.send(InfoToastEvent("Press G again to forfeit".to_string()));
ev.info_toast.write(InfoToastEvent("Press G again to forfeit".to_string()));
}
}
}
@@ -327,7 +327,7 @@ fn handle_fullscreen(
if !keys.just_pressed(KeyCode::F11) {
return;
}
let Ok(mut window) = windows.get_single_mut() else { return };
let Ok(mut window) = windows.single_mut() else { return };
let new_mode = match window.mode {
WindowMode::Windowed => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
_ => WindowMode::Windowed,
@@ -337,7 +337,7 @@ fn handle_fullscreen(
WindowMode::Windowed => "Fullscreen: off",
_ => "Fullscreen: on",
};
toast.send(InfoToastEvent(label.to_string()));
toast.write(InfoToastEvent(label.to_string()));
}
fn handle_stock_click(
@@ -366,7 +366,7 @@ fn handle_stock_click(
return;
};
if point_in_rect(world, stock_pos, layout.0.card_size) {
draw.send(DrawRequestEvent);
draw.write(DrawRequestEvent);
}
}
@@ -517,14 +517,14 @@ fn end_drag(
_ => false,
};
if ok {
moves.send(MoveRequestEvent {
moves.write(MoveRequestEvent {
from: origin.clone(),
to: target.clone(),
count,
});
fired = true;
} else {
rejected.send(MoveRejectedEvent {
rejected.write(MoveRejectedEvent {
from: origin.clone(),
to: target.clone(),
count,
@@ -552,7 +552,7 @@ fn end_drag(
// Either the move succeeded (GamePlugin will also fire StateChangedEvent)
// or it didn't — in both cases we emit one so cards resync to the current
// game state. Duplicate events are harmless.
changed.send(StateChangedEvent);
changed.write(StateChangedEvent);
let _ = fired;
}
@@ -564,9 +564,9 @@ fn cursor_world(
windows: &Query<&Window, With<PrimaryWindow>>,
cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> {
let window = windows.get_single().ok()?;
let window = windows.single().ok()?;
let cursor = window.cursor_position()?;
let (camera, camera_transform) = cameras.get_single().ok()?;
let (camera, camera_transform) = cameras.single().ok()?;
camera.viewport_to_world_2d(camera_transform, cursor).ok()
}
@@ -844,7 +844,7 @@ fn handle_double_click(
// Priority 1: move the single top card (foundation preferred, then tableau).
if let Some(dest) = best_destination(top_card, &game.0) {
moves.send(MoveRequestEvent {
moves.write(MoveRequestEvent {
from: pile,
to: dest,
count: 1,
@@ -864,7 +864,7 @@ fn handle_double_click(
&game.0,
card_ids.len(),
) {
moves.send(MoveRequestEvent {
moves.write(MoveRequestEvent {
from: pile,
to: dest,
count,
@@ -874,7 +874,7 @@ fn handle_double_click(
// sound and shake the source pile cards as feedback.
// `MoveRejectedEvent` with `from == to` routes the shake to
// the source pile (which `start_shake_anim` reads from `ev.to`).
rejected.send(MoveRejectedEvent {
rejected.write(MoveRejectedEvent {
from: pile.clone(),
to: pile,
count: card_ids.len(),
+9 -9
View File
@@ -112,8 +112,8 @@ fn toggle_leaderboard_screen(
if !keys.just_pressed(KeyCode::KeyL) {
return;
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
closed_flag.0 = true;
return;
}
@@ -174,7 +174,7 @@ fn update_leaderboard_panel(
return;
}
for entity in &screens {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
spawn_leaderboard_screen(&mut commands, data.0.as_deref());
}
}
@@ -225,11 +225,11 @@ fn poll_opt_in_task(
task_res.0 = None;
match result {
Ok(()) => {
toast.send(InfoToastEvent("Opted in to leaderboard".to_string()));
toast.write(InfoToastEvent("Opted in to leaderboard".to_string()));
}
Err(e) => {
warn!("leaderboard opt-in failed: {e}");
toast.send(InfoToastEvent("Leaderboard update failed".to_string()));
toast.write(InfoToastEvent("Leaderboard update failed".to_string()));
}
}
}
@@ -265,11 +265,11 @@ fn poll_opt_out_task(
task_res.0 = None;
match result {
Ok(()) => {
toast.send(InfoToastEvent("Opted out of leaderboard".to_string()));
toast.write(InfoToastEvent("Opted out of leaderboard".to_string()));
}
Err(e) => {
warn!("leaderboard opt-out failed: {e}");
toast.send(InfoToastEvent("Leaderboard update failed".to_string()));
toast.write(InfoToastEvent("Leaderboard update failed".to_string()));
}
}
}
@@ -454,7 +454,7 @@ fn spawn_leaderboard_screen(commands: &mut Commands, entries: Option<&[Leaderboa
});
}
fn header_cell(parent: &mut ChildBuilder, text: &str, width: f32) {
fn header_cell(parent: &mut ChildSpawnerCommands, text: &str, width: f32) {
parent.spawn((
Text::new(text.to_string()),
TextFont { font_size: 13.0, ..default() },
@@ -463,7 +463,7 @@ fn header_cell(parent: &mut ChildBuilder, text: &str, width: f32) {
));
}
fn data_cell(parent: &mut ChildBuilder, text: &str, width: f32, color: Color) {
fn data_cell(parent: &mut ChildSpawnerCommands, text: &str, width: f32, color: Color) {
parent.spawn((
Text::new(text.to_string()),
TextFont { font_size: 15.0, ..default() },
+2 -2
View File
@@ -59,7 +59,7 @@ fn dismiss_on_any_input(
path: Option<Res<SettingsStoragePath>>,
screens: Query<Entity, With<OnboardingScreen>>,
) {
let Ok(entity) = screens.get_single() else {
let Ok(entity) = screens.single() else {
return;
};
let pressed = keys.get_just_pressed().next().is_some()
@@ -67,7 +67,7 @@ fn dismiss_on_any_input(
if !pressed {
return;
}
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
settings.0.first_run_complete = true;
persist(path.as_deref().map(|p| &p.0), &settings.0);
}
+4 -4
View File
@@ -90,12 +90,12 @@ fn toggle_pause(
if let Some(ref mut d) = drag {
if !d.is_idle() {
d.clear();
changed.send(StateChangedEvent);
changed.write(StateChangedEvent);
return;
}
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
paused.0 = false;
} else {
// Snapshot current level and streak at pause time.
@@ -146,7 +146,7 @@ fn handle_pause_draw_toggle(
}
}
}
changed.send(SettingsChangedEvent(settings.0.clone()));
changed.write(SettingsChangedEvent(settings.0.clone()));
}
}
+3 -3
View File
@@ -42,8 +42,8 @@ fn toggle_profile_screen(
if !keys.just_pressed(KeyCode::KeyP) {
return;
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} else {
spawn_profile_screen(
&mut commands,
@@ -246,7 +246,7 @@ fn spawn_profile_screen(
}
/// Spawn a fixed-height vertical spacer node.
fn spawn_spacer(parent: &mut ChildBuilder, height_px: f32) {
fn spawn_spacer(parent: &mut ChildSpawnerCommands, height_px: f32) {
parent.spawn(Node {
height: Val::Px(height_px),
..default()
+2 -2
View File
@@ -88,9 +88,9 @@ fn award_xp_on_win(
let used_undo = game.0.undo_count > 0;
let amount = xp_for_win(ev.time_seconds, used_undo);
let prev_level = progress.0.add_xp(amount);
xp_awarded.send(XpAwardedEvent { amount });
xp_awarded.write(XpAwardedEvent { amount });
if progress.0.leveled_up_from(prev_level) {
levelups.send(LevelUpEvent {
levelups.write(LevelUpEvent {
previous_level: prev_level,
new_level: progress.0.level,
total_xp: progress.0.total_xp,
+6 -6
View File
@@ -200,11 +200,11 @@ fn handle_selection_keys(
if keys.just_pressed(KeyCode::Tab) {
let next = cycle_next_pile(&available, selection.selected_pile.as_ref());
if next.is_none() {
info_toast.send(InfoToastEvent("No cards to select".to_string()));
info_toast.write(InfoToastEvent("No cards to select".to_string()));
} else if selection.selected_pile.is_some()
&& did_wrap(&available, selection.selected_pile.as_ref(), next.as_ref())
{
info_toast.send(InfoToastEvent("Back to first card".to_string()));
info_toast.write(InfoToastEvent("Back to first card".to_string()));
}
selection.selected_pile = next;
return;
@@ -236,7 +236,7 @@ fn handle_selection_keys(
// --- Priority 1: foundation move (single card) ---
let foundation_dest = try_foundation_dest(card, &game.0);
if let Some(dest) = foundation_dest {
moves.send(MoveRequestEvent {
moves.write(MoveRequestEvent {
from: pile.clone(),
to: dest,
count: 1,
@@ -260,7 +260,7 @@ fn handle_selection_keys(
if let Some((dest, count)) =
best_tableau_destination_for_stack(bottom, pile, &game.0, run_len)
{
moves.send(MoveRequestEvent {
moves.write(MoveRequestEvent {
from: pile.clone(),
to: dest,
count,
@@ -274,7 +274,7 @@ fn handle_selection_keys(
// Covers non-tableau sources (Waste, Foundation) that have no
// stack-move logic.
if let Some(dest) = best_destination(card, &game.0) {
moves.send(MoveRequestEvent {
moves.write(MoveRequestEvent {
from: pile.clone(),
to: dest,
count: 1,
@@ -343,7 +343,7 @@ fn update_selection_highlight(
) {
// Always despawn any existing highlight first.
for entity in &highlights {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
let Some(ref pile) = selection.selected_pile else {
+25 -25
View File
@@ -203,7 +203,7 @@ fn handle_volume_keys(
return;
}
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
changed.write(SettingsChangedEvent(settings.0.clone()));
}
/// Opens or closes the Settings panel when `O` is pressed.
@@ -256,11 +256,11 @@ fn sync_settings_panel_visibility(
}
} else {
// Save the current scroll offset before despawning the panel.
if let Ok(sp) = scroll_nodes.get_single() {
if let Ok(sp) = scroll_nodes.single() {
scroll_pos.0 = sp.offset_y;
}
for entity in &panels {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
}
}
@@ -402,8 +402,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_sfx_volume(-SFX_STEP);
if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = sfx_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = sfx_text.single_mut() {
**t = format!("{:.2}", after);
}
}
@@ -413,8 +413,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_sfx_volume(SFX_STEP);
if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = sfx_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = sfx_text.single_mut() {
**t = format!("{:.2}", after);
}
}
@@ -424,8 +424,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_music_volume(-SFX_STEP);
if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = music_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = music_text.single_mut() {
**t = format!("{:.2}", after);
}
}
@@ -435,8 +435,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_music_volume(SFX_STEP);
if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = music_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = music_text.single_mut() {
**t = format!("{:.2}", after);
}
}
@@ -447,8 +447,8 @@ fn handle_settings_buttons(
DrawMode::DrawThree => DrawMode::DrawOne,
};
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = draw_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = draw_text.single_mut() {
**t = draw_mode_label(&settings.0.draw_mode);
}
}
@@ -459,8 +459,8 @@ fn handle_settings_buttons(
AnimSpeed::Instant => AnimSpeed::Normal,
};
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = anim_speed_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = anim_speed_text.single_mut() {
**t = anim_speed_label(&settings.0.animation_speed);
}
}
@@ -471,31 +471,31 @@ fn handle_settings_buttons(
Theme::Dark => Theme::Green,
};
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = theme_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = theme_text.single_mut() {
**t = theme_label(&settings.0.theme);
}
}
SettingsButton::ToggleColorBlind => {
settings.0.color_blind_mode = !settings.0.color_blind_mode;
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = color_blind_text.get_single_mut() {
changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = color_blind_text.single_mut() {
**t = color_blind_label(settings.0.color_blind_mode);
}
}
SettingsButton::SelectCardBack(idx) => {
settings.0.selected_card_back = *idx;
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
changed.write(SettingsChangedEvent(settings.0.clone()));
}
SettingsButton::SelectBackground(idx) => {
settings.0.selected_background = *idx;
persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone()));
changed.write(SettingsChangedEvent(settings.0.clone()));
}
SettingsButton::SyncNow => {
manual_sync.send(ManualSyncRequestEvent);
manual_sync.write(ManualSyncRequestEvent);
}
SettingsButton::Done => {
screen.0 = false;
@@ -880,7 +880,7 @@ fn spawn_settings_panel(
});
}
fn section_label(parent: &mut ChildBuilder, title: &str) {
fn section_label(parent: &mut ChildSpawnerCommands, title: &str) {
parent.spawn((
Text::new(title),
TextFont {
@@ -893,7 +893,7 @@ fn section_label(parent: &mut ChildBuilder, title: &str) {
/// Generic volume row: `Label 0.80 [] [+]`
fn volume_row<Marker: Component>(
parent: &mut ChildBuilder,
parent: &mut ChildSpawnerCommands,
label: &str,
value: f32,
marker: Marker,
@@ -924,7 +924,7 @@ fn volume_row<Marker: Component>(
});
}
fn icon_button(parent: &mut ChildBuilder, label: &str, action: SettingsButton) {
fn icon_button(parent: &mut ChildSpawnerCommands, label: &str, action: SettingsButton) {
parent
.spawn((
action,
+7 -7
View File
@@ -137,7 +137,7 @@ fn update_stats_on_new_game(
stats.0.record_abandoned();
persist(&path, &stats.0, "abandoned game");
if streak > 1 {
toast.send(InfoToastEvent(format!("Streak of {streak} broken!")));
toast.write(InfoToastEvent(format!("Streak of {streak} broken!")));
}
}
}
@@ -163,7 +163,7 @@ fn handle_forfeit(
stats.0.record_abandoned();
persist(&path, &stats.0, "forfeit");
if streak > 1 {
toast.send(InfoToastEvent(format!("Streak of {streak} broken!")));
toast.write(InfoToastEvent(format!("Streak of {streak} broken!")));
}
}
// Reset auto-complete so the badge and chime don't carry over to the
@@ -171,8 +171,8 @@ fn handle_forfeit(
if let Some(ref mut ac) = auto_complete {
**ac = AutoCompleteState::default();
}
toast.send(InfoToastEvent("Game forfeited".to_string()));
new_game.send(NewGameRequestEvent::default());
toast.write(InfoToastEvent("Game forfeited".to_string()));
new_game.write(NewGameRequestEvent::default());
}
}
@@ -187,8 +187,8 @@ fn toggle_stats_screen(
if !keys.just_pressed(KeyCode::KeyS) {
return;
}
if let Ok(entity) = screens.get_single() {
commands.entity(entity).despawn_recursive();
if let Ok(entity) = screens.single() {
commands.entity(entity).despawn();
} else {
spawn_stats_screen(
&mut commands,
@@ -349,7 +349,7 @@ fn spawn_stats_screen(
/// Spawn a single stat cell: a large value label on top and a small grey
/// descriptor below, inside a fixed-width column node with a [`StatsCell`] marker.
fn spawn_stat_cell(parent: &mut ChildBuilder, value: &str, label: &str) {
fn spawn_stat_cell(parent: &mut ChildSpawnerCommands, value: &str, label: &str) {
parent
.spawn((
StatsCell,
+4 -4
View File
@@ -60,7 +60,7 @@ fn handle_start_time_attack_request(
return;
}
if progress.0.level < CHALLENGE_UNLOCK_LEVEL {
info_toast.send(InfoToastEvent(format!(
info_toast.write(InfoToastEvent(format!(
"Time Attack unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
)));
return;
@@ -70,7 +70,7 @@ fn handle_start_time_attack_request(
remaining_secs: TIME_ATTACK_DURATION_SECS,
wins: 0,
};
new_game.send(NewGameRequestEvent {
new_game.write(NewGameRequestEvent {
seed: None,
mode: Some(GameMode::TimeAttack),
});
@@ -93,7 +93,7 @@ fn advance_time_attack(
let wins = session.wins;
session.active = false;
session.remaining_secs = 0.0;
ended.send(TimeAttackEndedEvent { wins });
ended.write(TimeAttackEndedEvent { wins });
}
}
@@ -108,7 +108,7 @@ fn auto_deal_on_time_attack_win(
continue;
}
session.wins = session.wins.saturating_add(1);
new_game.send(NewGameRequestEvent {
new_game.write(NewGameRequestEvent {
seed: None,
mode: Some(GameMode::TimeAttack),
});
+3 -3
View File
@@ -92,7 +92,7 @@ fn evaluate_weekly_goals(
any_change = true;
if just_completed {
bonus_xp = bonus_xp.saturating_add(WEEKLY_GOAL_XP);
completions.send(WeeklyGoalCompletedEvent {
completions.write(WeeklyGoalCompletedEvent {
goal_id: def.id.to_string(),
description: def.description.to_string(),
});
@@ -101,10 +101,10 @@ fn evaluate_weekly_goals(
}
if bonus_xp > 0 {
xp_awarded.send(XpAwardedEvent { amount: bonus_xp });
xp_awarded.write(XpAwardedEvent { amount: bonus_xp });
let prev_level = progress.0.add_xp(bonus_xp);
if progress.0.leveled_up_from(prev_level) {
levelups.send(LevelUpEvent {
levelups.write(LevelUpEvent {
previous_level: prev_level,
new_level: progress.0.level,
total_xp: progress.0.total_xp,
+5 -5
View File
@@ -255,7 +255,7 @@ fn cache_win_data(
pending.challenge_level = challenge_level;
if is_new_record {
toast.send(InfoToastEvent("New Record!".to_string()));
toast.write(InfoToastEvent("New Record!".to_string()));
}
}
for ev in xp.read() {
@@ -321,7 +321,7 @@ fn spawn_win_summary_after_delay(
*delay = Some(WIN_SUMMARY_DELAY_SECS);
// Clear any stale overlay from a previous win.
for entity in &overlays {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
}
@@ -362,9 +362,9 @@ fn handle_win_summary_buttons(
WinSummaryButton::PlayAgain => {
// Despawn the modal.
for entity in &overlays {
commands.entity(entity).despawn_recursive();
commands.entity(entity).despawn();
}
new_game.send(NewGameRequestEvent::default());
new_game.write(NewGameRequestEvent::default());
}
}
}
@@ -543,7 +543,7 @@ const MAX_ACHIEVEMENTS_SHOWN: usize = 3;
/// Shows at most [`MAX_ACHIEVEMENTS_SHOWN`] names. When more achievements were
/// unlocked than the cap, appends a "...and N more" line so the player knows
/// there are additional unlocks visible on the achievements screen.
fn spawn_achievements_section(card: &mut ChildBuilder, names: &[String]) {
fn spawn_achievements_section(card: &mut ChildSpawnerCommands, names: &[String]) {
card.spawn((
Text::new("Achievements Unlocked"),
TextFont { font_size: 18.0, ..default() },