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
Generated
+917 -508
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -20,27 +20,27 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
thiserror = "1" thiserror = "2"
rand = "0.8" rand = "0.8"
async-trait = "0.1" async-trait = "0.1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
dirs = "5" dirs = "6"
keyring = "2" keyring = "2"
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false } reqwest = { version = "0.13", features = ["json", "rustls", "rustls-native-certs"], default-features = false }
solitaire_core = { path = "solitaire_core" } solitaire_core = { path = "solitaire_core" }
solitaire_sync = { path = "solitaire_sync" } 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.15" bevy = "0.16"
kira = "0.9" kira = "0.9"
axum = "0.7" axum = "0.8"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
jsonwebtoken = "9" jsonwebtoken = { version = "10", default-features = false, features = ["rust_crypto"] }
bcrypt = "0.15" bcrypt = "0.19"
tower_governor = "0.4" tower_governor = "0.8"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
dotenvy = "0.15" dotenvy = "0.15"
+5 -5
View File
@@ -156,10 +156,10 @@ fn evaluate_on_win(
} }
} }
Reward::BonusXp(amount) => { Reward::BonusXp(amount) => {
xp_awarded.send(XpAwardedEvent { amount }); xp_awarded.write(XpAwardedEvent { amount });
let prev_level = progress.0.add_xp(amount); let prev_level = progress.0.add_xp(amount);
if progress.0.leveled_up_from(prev_level) { if progress.0.leveled_up_from(prev_level) {
levelups.send(LevelUpEvent { levelups.write(LevelUpEvent {
previous_level: prev_level, previous_level: prev_level,
new_level: progress.0.level, new_level: progress.0.level,
total_xp: progress.0.total_xp, total_xp: progress.0.total_xp,
@@ -173,7 +173,7 @@ fn evaluate_on_win(
record.reward_granted = true; record.reward_granted = true;
} }
unlocks.send(AchievementUnlockedEvent(record.clone())); unlocks.write(AchievementUnlockedEvent(record.clone()));
} }
if achievements_changed { if achievements_changed {
@@ -211,8 +211,8 @@ fn toggle_achievements_screen(
if !keys.just_pressed(KeyCode::KeyA) { if !keys.just_pressed(KeyCode::KeyA) {
return; return;
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} else { } else {
spawn_achievements_screen(&mut commands, &achievements.0); spawn_achievements_screen(&mut commands, &achievements.0);
} }
+2 -2
View File
@@ -465,7 +465,7 @@ fn drive_toast_display(
active.timer -= dt; active.timer -= dt;
if active.timer <= 0.0 { if active.timer <= 0.0 {
// Despawn the toast entity and clear the active slot. // Despawn the toast entity and clear the active slot.
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
active.entity = None; active.entity = None;
active.timer = 0.0; active.timer = 0.0;
} }
@@ -532,7 +532,7 @@ fn tick_toasts(
for (entity, mut timer) in &mut toasts { for (entity, mut timer) in &mut toasts {
timer.0 -= dt; timer.0 -= dt;
if timer.0 <= 0.0 { 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; return;
}; };
moves.send(MoveRequestEvent { from, to, count: 1 }); moves.write(MoveRequestEvent { from, to, count: 1 });
state.cooldown = STEP_INTERVAL; state.cooldown = STEP_INTERVAL;
} }
@@ -236,13 +236,13 @@ pub(crate) fn drain_input_buffer(
} }
match buffer.queue.pop_front() { match buffer.queue.pop_front() {
Some(BufferedInput::Move { from }) => { Some(BufferedInput::Move { from }) => {
move_events.send(from); move_events.write(from);
} }
Some(BufferedInput::Draw) => { Some(BufferedInput::Draw) => {
draw_events.send(DrawRequestEvent); draw_events.write(DrawRequestEvent);
} }
Some(BufferedInput::Undo) => { Some(BufferedInput::Undo) => {
undo_events.send(UndoRequestEvent); undo_events.write(UndoRequestEvent);
} }
None => {} None => {}
} }
@@ -259,9 +259,9 @@ fn cursor_world(
windows: &Query<&Window, With<PrimaryWindow>>, windows: &Query<&Window, With<PrimaryWindow>>,
cameras: &Query<(&Camera, &GlobalTransform)>, cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> { ) -> Option<Vec2> {
let window = windows.get_single().ok()?; let window = windows.single().ok()?;
let cursor = window.cursor_position()?; 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() 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>, mut state_events: EventWriter<StateChangedEvent>,
) { ) {
if setting_events.read().next().is_some() { 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. // Despawn any entity whose card is no longer tracked.
for (card_id, (entity, _)) in &existing { for (card_id, (entity, _)) in &existing {
if !live_ids.contains(card_id) { 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/ // 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. // 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| { commands.entity(entity).with_children(|b| {
b.spawn(( b.spawn((
CardLabel, CardLabel,
@@ -558,7 +558,7 @@ fn tick_flip_anim(
transform.scale.x = 0.0; transform.scale.x = 0.0;
// Fire the reveal event exactly once, at the phase transition, // Fire the reveal event exactly once, at the phase transition,
// so the flip sound is synchronised with the visual face reveal. // 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 => { FlipPhase::ScalingUp => {
@@ -592,7 +592,7 @@ fn update_drag_shadow(
if drag.is_idle() { if drag.is_idle() {
// No drag in progress — remove shadow if it exists. // No drag in progress — remove shadow if it exists.
if let Some(e) = shadow.take() { if let Some(e) = shadow.take() {
commands.entity(e).despawn_recursive(); commands.entity(e).despawn();
} }
return; return;
} }
@@ -847,9 +847,9 @@ fn cursor_world_pos(
windows: &Query<&Window, With<bevy::window::PrimaryWindow>>, windows: &Query<&Window, With<bevy::window::PrimaryWindow>>,
cameras: &Query<(&Camera, &GlobalTransform)>, cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> { ) -> Option<Vec2> {
let window = windows.get_single().ok()?; let window = windows.single().ok()?;
let cursor = window.cursor_position()?; 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() camera.viewport_to_world_2d(camera_transform, cursor).ok()
} }
@@ -911,7 +911,7 @@ fn apply_stock_empty_indicator(
commands: &mut Commands, commands: &mut Commands,
game: &GameState, game: &GameState,
pile_markers: &mut Query<(Entity, &PileMarker, &mut Sprite)>, pile_markers: &mut Query<(Entity, &PileMarker, &mut Sprite)>,
label_children: &Query<(Entity, &Parent), With<StockEmptyLabel>>, label_children: &Query<(Entity, &ChildOf), With<StockEmptyLabel>>,
layout: &Layout, layout: &Layout,
) { ) {
let stock_empty = game let stock_empty = game
@@ -931,7 +931,7 @@ fn apply_stock_empty_indicator(
// Spawn the "↺" label only if one does not already exist. // Spawn the "↺" label only if one does not already exist.
let already_has_label = label_children let already_has_label = label_children
.iter() .iter()
.any(|(_, parent)| parent.get() == entity); .any(|(_, parent)| parent.parent() == entity);
if !already_has_label { if !already_has_label {
let font_size = layout.card_size.x * 0.4; let font_size = layout.card_size.x * 0.4;
commands.entity(entity).with_children(|b| { commands.entity(entity).with_children(|b| {
@@ -950,8 +950,8 @@ fn apply_stock_empty_indicator(
// Despawn any existing "↺" label children. // Despawn any existing "↺" label children.
for (label_entity, parent) in label_children.iter() { for (label_entity, parent) in label_children.iter() {
if parent.get() == entity { if parent.parent() == entity {
commands.entity(label_entity).despawn_recursive(); commands.entity(label_entity).despawn();
} }
} }
} }
@@ -965,7 +965,7 @@ fn update_stock_empty_indicator_startup(
game: Res<GameStateResource>, game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>, 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 }; let Some(layout) = layout else { return };
apply_stock_empty_indicator( apply_stock_empty_indicator(
@@ -985,7 +985,7 @@ fn update_stock_empty_indicator(
game: Res<GameStateResource>, game: Res<GameStateResource>,
layout: Option<Res<LayoutResource>>, layout: Option<Res<LayoutResource>>,
mut pile_markers: Query<(Entity, &PileMarker, &mut Sprite)>, 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() { if events.read().next().is_none() {
return; return;
+4 -4
View File
@@ -59,8 +59,8 @@ fn advance_on_challenge_win(
} }
// Human-readable level is 1-based (index 0 → "Challenge 1"). // Human-readable level is 1-based (index 0 → "Challenge 1").
let level_number = prev.saturating_add(1); let level_number = prev.saturating_add(1);
toast.send(InfoToastEvent(format!("Challenge {level_number} complete!"))); toast.write(InfoToastEvent(format!("Challenge {level_number} complete!")));
advanced.send(ChallengeAdvancedEvent { advanced.write(ChallengeAdvancedEvent {
previous_index: prev, previous_index: prev,
new_index: progress.0.challenge_index, new_index: progress.0.challenge_index,
}); });
@@ -77,7 +77,7 @@ fn handle_start_challenge_request(
return; return;
} }
if progress.0.level < CHALLENGE_UNLOCK_LEVEL { 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}" "Challenge mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
))); )));
return; return;
@@ -86,7 +86,7 @@ fn handle_start_challenge_request(
warn!("challenge seed list is empty"); warn!("challenge seed list is empty");
return; return;
}; };
new_game.send(NewGameRequestEvent { new_game.write(NewGameRequestEvent {
seed: Some(seed), seed: Some(seed),
mode: Some(GameMode::Challenge), mode: Some(GameMode::Challenge),
}); });
+2 -2
View File
@@ -52,7 +52,7 @@ fn update_cursor_icon(
game: Option<Res<GameStateResource>>, game: Option<Res<GameStateResource>>,
mut commands: Commands, 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() { if !drag.is_idle() {
commands commands
@@ -63,7 +63,7 @@ fn update_cursor_icon(
let hovering = (|| { let hovering = (|| {
let cursor = window.cursor_position()?; 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 world = camera.viewport_to_world_2d(cam_xf, cursor).ok()?;
let layout = layout.as_ref()?.0.clone(); let layout = layout.as_ref()?.0.clone();
let game = game.as_ref()?; let game = game.as_ref()?;
@@ -174,17 +174,17 @@ fn handle_daily_completion(
continue; continue;
} }
progress.0.add_xp(DAILY_BONUS_XP); 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 Some(target) = &path.0 {
if let Err(e) = save_progress_to(target, &progress.0) { if let Err(e) = save_progress_to(target, &progress.0) {
warn!("failed to save progress after daily completion: {e}"); warn!("failed to save progress after daily completion: {e}");
} }
} }
completed.send(DailyChallengeCompletedEvent { completed.write(DailyChallengeCompletedEvent {
date: daily.date, date: daily.date,
streak: progress.0.daily_challenge_streak, 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>, mut announce: EventWriter<DailyGoalAnnouncementEvent>,
) { ) {
if keys.just_pressed(KeyCode::KeyC) { if keys.just_pressed(KeyCode::KeyC) {
new_game.send(NewGameRequestEvent { new_game.write(NewGameRequestEvent {
seed: Some(daily.seed), seed: Some(daily.seed),
mode: None, mode: None,
}); });
@@ -203,7 +203,7 @@ fn handle_start_daily_request(
.goal_description .goal_description
.clone() .clone()
.unwrap_or_else(|| "Daily Challenge".to_string()); .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 { if needs_confirm && !confirm_already_open {
// Despawn any stale game-over overlay before showing confirm dialog. // Despawn any stale game-over overlay before showing confirm dialog.
for entity in &game_over_screens { for entity in &game_over_screens {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
spawn_confirm_dialog(&mut commands, *ev); spawn_confirm_dialog(&mut commands, *ev);
continue; continue;
@@ -177,10 +177,10 @@ fn handle_new_game(
// Despawn confirm and game-over overlays before starting the new game. // Despawn confirm and game-over overlays before starting the new game.
for entity in &confirm_screens { for entity in &confirm_screens {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
for entity in &game_over_screens { 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); 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}"); 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>>, screens: Query<(Entity, &OriginalNewGameRequest), With<ConfirmNewGameScreen>>,
mut new_game: EventWriter<NewGameRequestEvent>, mut new_game: EventWriter<NewGameRequestEvent>,
) { ) {
let Ok((entity, original)) = screens.get_single() else { let Ok((entity, original)) = screens.single() else {
return; return;
}; };
let Some(keys) = keys else { 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); let cancelled = keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape);
if confirmed { if confirmed {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
// Re-send with move_count already 0 would bypass the dialog next time. // 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 // We fire the event — handle_new_game will skip the dialog because
// the screen is despawned before the next read. // the screen is despawned before the next read.
new_game.send(NewGameRequestEvent { new_game.write(NewGameRequestEvent {
seed: original.0.seed, seed: original.0.seed,
mode: original.0.mode, mode: original.0.mode,
}); });
} else if cancelled { } else if cancelled {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
} }
@@ -347,9 +347,9 @@ fn handle_draw(
Ok(()) => { Ok(()) => {
// Fire a flip event for each card that moved from stock to waste. // Fire a flip event for each card that moved from stock to waste.
for id in drawn_ids { 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}"), Err(e) => warn!("draw rejected: {e}"),
} }
@@ -385,12 +385,12 @@ fn handle_move(
.and_then(|p| p.cards.last()) .and_then(|p| p.cards.last())
.is_some_and(|c| c.id == fid && c.face_up) .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 { if !was_won && game.0.is_won {
won.send(GameWonEvent { won.write(GameWonEvent {
score: game.0.score, score: game.0.score,
time_seconds: game.0.elapsed_seconds, time_seconds: game.0.elapsed_seconds,
}); });
@@ -418,10 +418,10 @@ fn handle_undo(
for _ in undos.read() { for _ in undos.read() {
match game.0.undo() { match game.0.undo() {
Ok(()) => { Ok(()) => {
changed.send(StateChangedEvent); changed.write(StateChangedEvent);
} }
Err(MoveError::UndoStackEmpty) => { 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}"), Err(e) => warn!("undo rejected: {e}"),
} }
@@ -523,7 +523,7 @@ fn check_no_moves(
let moves_ok = has_legal_moves(&game.0); let moves_ok = has_legal_moves(&game.0);
if moves_ok || game.0.is_won { if moves_ok || game.0.is_won {
for entity in &game_over_screens { 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 { 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(), "No moves available \u{2014} press D to draw or N for a new game".to_string(),
)); ));
*already_fired = true; *already_fired = true;
@@ -639,12 +639,12 @@ fn handle_game_over_input(
}; };
if keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape) { 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) { } else if keys.just_pressed(KeyCode::KeyU) {
for entity in &screens { 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) { if !keys.just_pressed(KeyCode::F1) {
return; return;
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} else { } else {
spawn_help_screen(&mut commands); spawn_help_screen(&mut commands);
} }
+3 -3
View File
@@ -31,8 +31,8 @@ fn toggle_home_screen(
if !keys.just_pressed(KeyCode::KeyM) { if !keys.just_pressed(KeyCode::KeyM) {
return; return;
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} else { } else {
spawn_home_screen(&mut commands, &game); 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 parent
.spawn(Node { .spawn(Node {
flex_direction: FlexDirection::Row, flex_direction: FlexDirection::Row,
+12 -12
View File
@@ -325,7 +325,7 @@ fn update_hud(
if game.is_changed() { if game.is_changed() {
let g = &game.0; let g = &game.0;
let is_zen = g.mode == GameMode::Zen; 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"). // Zen mode suppresses score display per spec ("No score display").
**t = if is_zen { **t = if is_zen {
String::new() String::new()
@@ -333,10 +333,10 @@ fn update_hud(
format!("Score: {}", g.score) 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); **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 { **t = match g.mode {
GameMode::Classic => match g.draw_mode { GameMode::Classic => match g.draw_mode {
DrawMode::DrawOne => String::new(), DrawMode::DrawOne => String::new(),
@@ -349,7 +349,7 @@ fn update_hud(
} }
// --- Daily challenge constraint (with time-low colour warning) --- // --- 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 { if g.is_won {
**t = String::new(); **t = String::new();
} else if let Some(dc) = daily.as_deref() { } else if let Some(dc) = daily.as_deref() {
@@ -364,7 +364,7 @@ fn update_hud(
} }
// --- Undo count --- // --- 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; let count = g.undo_count;
if count == 0 { if count == 0 {
**t = String::new(); **t = String::new();
@@ -377,7 +377,7 @@ fn update_hud(
} }
// --- Recycle counter (both modes, hidden until first recycle) --- // --- 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 { **t = if g.recycle_count > 0 {
format!("Recycles: {}", g.recycle_count) format!("Recycles: {}", g.recycle_count)
} else { } else {
@@ -386,7 +386,7 @@ fn update_hud(
} }
// --- Draw-cycle indicator (Draw-Three mode only) --- // --- 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 { **t = if g.is_won || g.draw_mode != DrawMode::DrawThree {
// Hide when not in Draw-Three or after the game is won. // Hide when not in Draw-Three or after the game is won.
String::new() String::new()
@@ -405,7 +405,7 @@ fn update_hud(
let is_zen = game.0.mode == GameMode::Zen; let is_zen = game.0.mode == GameMode::Zen;
let update_time = (ta_active || game.is_changed()) && !is_zen; let update_time = (ta_active || game.is_changed()) && !is_zen;
if update_time { 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) { if let Some(ta) = time_attack.as_ref().filter(|ta| ta.active) {
let remaining = ta.remaining_secs.max(0.0) as u64; let remaining = ta.remaining_secs.max(0.0) as u64;
let m = remaining / 60; let m = remaining / 60;
@@ -422,7 +422,7 @@ fn update_hud(
// Clear the time display immediately whenever Zen mode is active — // Clear the time display immediately whenever Zen mode is active —
// do not guard on game.is_changed() so it clears on the same frame // do not guard on game.is_changed() so it clears on the same frame
// the player presses Z, before any move is made. // 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(); **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_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()); let ac_changed = auto_complete.as_ref().is_some_and(|ac| ac.is_changed());
if ac_changed || game.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 { **t = if ac_active {
"AUTO".to_string() "AUTO".to_string()
} else { } else {
@@ -451,7 +451,7 @@ fn update_selection_hud(
selection: Option<Res<SelectionState>>, selection: Option<Res<SelectionState>>,
mut q: Query<&mut Text, With<HudSelection>>, 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()) { let label = match selection.as_deref().and_then(|s| s.selected_pile.as_ref()) {
None => String::new(), None => String::new(),
Some(PileType::Waste) => "▶ Waste".to_string(), 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); let now_active = auto_complete.as_ref().is_some_and(|ac| ac.active);
if now_active && !*was_active { if now_active && !*was_active {
toast.send(InfoToastEvent("Auto-completing...".to_string())); toast.write(InfoToastEvent("Auto-completing...".to_string()));
} }
*was_active = now_active; *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. // Countdown expired without a second N press — notify the player.
if *confirm_pending { if *confirm_pending {
*confirm_pending = false; *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 keys.just_pressed(KeyCode::KeyU) {
if *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; } if *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; }
ev.undo.send(UndoRequestEvent); ev.undo.write(UndoRequestEvent);
} }
if keys.just_pressed(KeyCode::KeyN) { if keys.just_pressed(KeyCode::KeyN) {
// If a Time Attack session is running, cancel it and start a Classic game. // If a Time Attack session is running, cancel it and start a Classic game.
@@ -148,8 +148,8 @@ fn handle_keyboard(
if session.active { if session.active {
session.active = false; session.active = false;
session.remaining_secs = 0.0; session.remaining_secs = 0.0;
ev.info_toast.send(InfoToastEvent("Time Attack ended".to_string())); ev.info_toast.write(InfoToastEvent("Time Attack ended".to_string()));
ev.new_game.send(NewGameRequestEvent { ev.new_game.write(NewGameRequestEvent {
seed: None, seed: None,
mode: Some(solitaire_core::game_state::GameMode::Classic), 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); let shift_held = keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight);
if shift_held || !active_game { if shift_held || !active_game {
// Shift+N or no active game — start immediately, no confirmation. // 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_countdown = 0.0;
*confirm_pending = false; *confirm_pending = false;
} else if *confirm_countdown > 0.0 { } else if *confirm_countdown > 0.0 {
// Second press within the window — confirmed. // Second press within the window — confirmed.
ev.new_game.send(NewGameRequestEvent::default()); ev.new_game.write(NewGameRequestEvent::default());
*confirm_countdown = 0.0; *confirm_countdown = 0.0;
*confirm_pending = false; *confirm_pending = false;
} else { } else {
// First press on an active game — require confirmation. // First press on an active game — require confirmation.
*confirm_countdown = NEW_GAME_CONFIRM_WINDOW; *confirm_countdown = NEW_GAME_CONFIRM_WINDOW;
*confirm_pending = true; *confirm_pending = true;
ev.confirm_event.send(NewGameConfirmEvent); ev.confirm_event.write(NewGameConfirmEvent);
} }
} }
if keys.just_pressed(KeyCode::KeyZ) { if keys.just_pressed(KeyCode::KeyZ) {
@@ -183,19 +183,19 @@ fn handle_keyboard(
// X is gated separately by ChallengePlugin. // X is gated separately by ChallengePlugin.
let level = progress.as_ref().map_or(0, |p| p.0.level); let level = progress.as_ref().map_or(0, |p| p.0.level);
if level >= CHALLENGE_UNLOCK_LEVEL { if level >= CHALLENGE_UNLOCK_LEVEL {
ev.new_game.send(NewGameRequestEvent { ev.new_game.write(NewGameRequestEvent {
seed: None, seed: None,
mode: Some(solitaire_core::game_state::GameMode::Zen), mode: Some(solitaire_core::game_state::GameMode::Zen),
}); });
} else { } else {
ev.info_toast.send(InfoToastEvent(format!( ev.info_toast.write(InfoToastEvent(format!(
"Zen mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}" "Zen mode unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
))); )));
} }
} }
if keys.just_pressed(KeyCode::KeyD) || keys.just_pressed(KeyCode::Space) { if keys.just_pressed(KeyCode::KeyD) || keys.just_pressed(KeyCode::Space) {
if *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; } 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 // 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 // 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 *forfeit_countdown > 0.0 { *forfeit_countdown = 0.0; }
if let Some(ref g) = game { if let Some(ref g) = game {
if g.0.is_won { 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(), "Game won! Press N for a new game".to_string(),
)); ));
} else if let Some(ref layout_res) = layout { } else if let Some(ref layout_res) = layout {
let hints = all_hints(&g.0); let hints = all_hints(&g.0);
if hints.is_empty() { 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 { } else {
// Pick the hint at the current cycle index (wrapping) and advance. // Pick the hint at the current cycle index (wrapping) and advance.
let idx = hint_cycle.0 % hints.len(); let idx = hint_cycle.0 % hints.len();
@@ -229,7 +229,7 @@ fn handle_keyboard(
} else { } else {
"Hint: draw from stock (D)".to_string() "Hint: draw from stock (D)".to_string()
}; };
ev.info_toast.send(InfoToastEvent(msg)); ev.info_toast.write(InfoToastEvent(msg));
} else { } else {
// Find the top face-up card in the source pile and highlight it. // Find the top face-up card in the source pile and highlight it.
let top_card_id = g.0.piles.get(from) let top_card_id = g.0.piles.get(from)
@@ -251,7 +251,7 @@ fn handle_keyboard(
} }
// Emit HintVisualEvent so the destination pile // Emit HintVisualEvent so the destination pile
// marker is also tinted gold for 2 s. // marker is also tinted gold for 2 s.
ev.hint_visual.send(HintVisualEvent { ev.hint_visual.write(HintVisualEvent {
source_card_id: card_id, source_card_id: card_id,
dest_pile: to.clone(), dest_pile: to.clone(),
}); });
@@ -273,7 +273,7 @@ fn handle_keyboard(
} }
_ => "Hint: move card".to_string(), _ => "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 active_game {
if *forfeit_countdown > 0.0 { if *forfeit_countdown > 0.0 {
// Second press within the confirmation window — confirmed. // Second press within the confirmation window — confirmed.
ev.forfeit.send(ForfeitEvent); ev.forfeit.write(ForfeitEvent);
*forfeit_countdown = 0.0; *forfeit_countdown = 0.0;
} else { } else {
// First press — start the countdown and warn the player. // First press — start the countdown and warn the player.
*forfeit_countdown = FORFEIT_CONFIRM_WINDOW; *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) { if !keys.just_pressed(KeyCode::F11) {
return; 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 { let new_mode = match window.mode {
WindowMode::Windowed => WindowMode::BorderlessFullscreen(MonitorSelection::Current), WindowMode::Windowed => WindowMode::BorderlessFullscreen(MonitorSelection::Current),
_ => WindowMode::Windowed, _ => WindowMode::Windowed,
@@ -337,7 +337,7 @@ fn handle_fullscreen(
WindowMode::Windowed => "Fullscreen: off", WindowMode::Windowed => "Fullscreen: off",
_ => "Fullscreen: on", _ => "Fullscreen: on",
}; };
toast.send(InfoToastEvent(label.to_string())); toast.write(InfoToastEvent(label.to_string()));
} }
fn handle_stock_click( fn handle_stock_click(
@@ -366,7 +366,7 @@ fn handle_stock_click(
return; return;
}; };
if point_in_rect(world, stock_pos, layout.0.card_size) { 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, _ => false,
}; };
if ok { if ok {
moves.send(MoveRequestEvent { moves.write(MoveRequestEvent {
from: origin.clone(), from: origin.clone(),
to: target.clone(), to: target.clone(),
count, count,
}); });
fired = true; fired = true;
} else { } else {
rejected.send(MoveRejectedEvent { rejected.write(MoveRejectedEvent {
from: origin.clone(), from: origin.clone(),
to: target.clone(), to: target.clone(),
count, count,
@@ -552,7 +552,7 @@ fn end_drag(
// Either the move succeeded (GamePlugin will also fire StateChangedEvent) // 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 // or it didn't — in both cases we emit one so cards resync to the current
// game state. Duplicate events are harmless. // game state. Duplicate events are harmless.
changed.send(StateChangedEvent); changed.write(StateChangedEvent);
let _ = fired; let _ = fired;
} }
@@ -564,9 +564,9 @@ fn cursor_world(
windows: &Query<&Window, With<PrimaryWindow>>, windows: &Query<&Window, With<PrimaryWindow>>,
cameras: &Query<(&Camera, &GlobalTransform)>, cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> { ) -> Option<Vec2> {
let window = windows.get_single().ok()?; let window = windows.single().ok()?;
let cursor = window.cursor_position()?; 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() 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). // Priority 1: move the single top card (foundation preferred, then tableau).
if let Some(dest) = best_destination(top_card, &game.0) { if let Some(dest) = best_destination(top_card, &game.0) {
moves.send(MoveRequestEvent { moves.write(MoveRequestEvent {
from: pile, from: pile,
to: dest, to: dest,
count: 1, count: 1,
@@ -864,7 +864,7 @@ fn handle_double_click(
&game.0, &game.0,
card_ids.len(), card_ids.len(),
) { ) {
moves.send(MoveRequestEvent { moves.write(MoveRequestEvent {
from: pile, from: pile,
to: dest, to: dest,
count, count,
@@ -874,7 +874,7 @@ fn handle_double_click(
// sound and shake the source pile cards as feedback. // sound and shake the source pile cards as feedback.
// `MoveRejectedEvent` with `from == to` routes the shake to // `MoveRejectedEvent` with `from == to` routes the shake to
// the source pile (which `start_shake_anim` reads from `ev.to`). // the source pile (which `start_shake_anim` reads from `ev.to`).
rejected.send(MoveRejectedEvent { rejected.write(MoveRejectedEvent {
from: pile.clone(), from: pile.clone(),
to: pile, to: pile,
count: card_ids.len(), count: card_ids.len(),
+9 -9
View File
@@ -112,8 +112,8 @@ fn toggle_leaderboard_screen(
if !keys.just_pressed(KeyCode::KeyL) { if !keys.just_pressed(KeyCode::KeyL) {
return; return;
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
closed_flag.0 = true; closed_flag.0 = true;
return; return;
} }
@@ -174,7 +174,7 @@ fn update_leaderboard_panel(
return; return;
} }
for entity in &screens { for entity in &screens {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
spawn_leaderboard_screen(&mut commands, data.0.as_deref()); spawn_leaderboard_screen(&mut commands, data.0.as_deref());
} }
} }
@@ -225,11 +225,11 @@ fn poll_opt_in_task(
task_res.0 = None; task_res.0 = None;
match result { match result {
Ok(()) => { Ok(()) => {
toast.send(InfoToastEvent("Opted in to leaderboard".to_string())); toast.write(InfoToastEvent("Opted in to leaderboard".to_string()));
} }
Err(e) => { Err(e) => {
warn!("leaderboard opt-in failed: {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; task_res.0 = None;
match result { match result {
Ok(()) => { Ok(()) => {
toast.send(InfoToastEvent("Opted out of leaderboard".to_string())); toast.write(InfoToastEvent("Opted out of leaderboard".to_string()));
} }
Err(e) => { Err(e) => {
warn!("leaderboard opt-out failed: {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(( parent.spawn((
Text::new(text.to_string()), Text::new(text.to_string()),
TextFont { font_size: 13.0, ..default() }, 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(( parent.spawn((
Text::new(text.to_string()), Text::new(text.to_string()),
TextFont { font_size: 15.0, ..default() }, TextFont { font_size: 15.0, ..default() },
+2 -2
View File
@@ -59,7 +59,7 @@ fn dismiss_on_any_input(
path: Option<Res<SettingsStoragePath>>, path: Option<Res<SettingsStoragePath>>,
screens: Query<Entity, With<OnboardingScreen>>, screens: Query<Entity, With<OnboardingScreen>>,
) { ) {
let Ok(entity) = screens.get_single() else { let Ok(entity) = screens.single() else {
return; return;
}; };
let pressed = keys.get_just_pressed().next().is_some() let pressed = keys.get_just_pressed().next().is_some()
@@ -67,7 +67,7 @@ fn dismiss_on_any_input(
if !pressed { if !pressed {
return; return;
} }
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
settings.0.first_run_complete = true; settings.0.first_run_complete = true;
persist(path.as_deref().map(|p| &p.0), &settings.0); 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 let Some(ref mut d) = drag {
if !d.is_idle() { if !d.is_idle() {
d.clear(); d.clear();
changed.send(StateChangedEvent); changed.write(StateChangedEvent);
return; return;
} }
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
paused.0 = false; paused.0 = false;
} else { } else {
// Snapshot current level and streak at pause time. // 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) { if !keys.just_pressed(KeyCode::KeyP) {
return; return;
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} else { } else {
spawn_profile_screen( spawn_profile_screen(
&mut commands, &mut commands,
@@ -246,7 +246,7 @@ fn spawn_profile_screen(
} }
/// Spawn a fixed-height vertical spacer node. /// 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 { parent.spawn(Node {
height: Val::Px(height_px), height: Val::Px(height_px),
..default() ..default()
+2 -2
View File
@@ -88,9 +88,9 @@ fn award_xp_on_win(
let used_undo = game.0.undo_count > 0; let used_undo = game.0.undo_count > 0;
let amount = xp_for_win(ev.time_seconds, used_undo); let amount = xp_for_win(ev.time_seconds, used_undo);
let prev_level = progress.0.add_xp(amount); 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) { if progress.0.leveled_up_from(prev_level) {
levelups.send(LevelUpEvent { levelups.write(LevelUpEvent {
previous_level: prev_level, previous_level: prev_level,
new_level: progress.0.level, new_level: progress.0.level,
total_xp: progress.0.total_xp, total_xp: progress.0.total_xp,
+6 -6
View File
@@ -200,11 +200,11 @@ fn handle_selection_keys(
if keys.just_pressed(KeyCode::Tab) { if keys.just_pressed(KeyCode::Tab) {
let next = cycle_next_pile(&available, selection.selected_pile.as_ref()); let next = cycle_next_pile(&available, selection.selected_pile.as_ref());
if next.is_none() { 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() } else if selection.selected_pile.is_some()
&& did_wrap(&available, selection.selected_pile.as_ref(), next.as_ref()) && 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; selection.selected_pile = next;
return; return;
@@ -236,7 +236,7 @@ fn handle_selection_keys(
// --- Priority 1: foundation move (single card) --- // --- Priority 1: foundation move (single card) ---
let foundation_dest = try_foundation_dest(card, &game.0); let foundation_dest = try_foundation_dest(card, &game.0);
if let Some(dest) = foundation_dest { if let Some(dest) = foundation_dest {
moves.send(MoveRequestEvent { moves.write(MoveRequestEvent {
from: pile.clone(), from: pile.clone(),
to: dest, to: dest,
count: 1, count: 1,
@@ -260,7 +260,7 @@ fn handle_selection_keys(
if let Some((dest, count)) = if let Some((dest, count)) =
best_tableau_destination_for_stack(bottom, pile, &game.0, run_len) best_tableau_destination_for_stack(bottom, pile, &game.0, run_len)
{ {
moves.send(MoveRequestEvent { moves.write(MoveRequestEvent {
from: pile.clone(), from: pile.clone(),
to: dest, to: dest,
count, count,
@@ -274,7 +274,7 @@ fn handle_selection_keys(
// Covers non-tableau sources (Waste, Foundation) that have no // Covers non-tableau sources (Waste, Foundation) that have no
// stack-move logic. // stack-move logic.
if let Some(dest) = best_destination(card, &game.0) { if let Some(dest) = best_destination(card, &game.0) {
moves.send(MoveRequestEvent { moves.write(MoveRequestEvent {
from: pile.clone(), from: pile.clone(),
to: dest, to: dest,
count: 1, count: 1,
@@ -343,7 +343,7 @@ fn update_selection_highlight(
) { ) {
// Always despawn any existing highlight first. // Always despawn any existing highlight first.
for entity in &highlights { for entity in &highlights {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
let Some(ref pile) = selection.selected_pile else { let Some(ref pile) = selection.selected_pile else {
+25 -25
View File
@@ -203,7 +203,7 @@ fn handle_volume_keys(
return; return;
} }
persist(&path, &settings.0); 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. /// Opens or closes the Settings panel when `O` is pressed.
@@ -256,11 +256,11 @@ 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.get_single() { if let Ok(sp) = scroll_nodes.single() {
scroll_pos.0 = sp.offset_y; scroll_pos.0 = sp.offset_y;
} }
for entity in &panels { 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); let after = settings.0.adjust_sfx_volume(-SFX_STEP);
if (before - after).abs() > f32::EPSILON { if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = sfx_text.get_single_mut() { if let Ok(mut t) = sfx_text.single_mut() {
**t = format!("{:.2}", after); **t = format!("{:.2}", after);
} }
} }
@@ -413,8 +413,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_sfx_volume(SFX_STEP); let after = settings.0.adjust_sfx_volume(SFX_STEP);
if (before - after).abs() > f32::EPSILON { if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = sfx_text.get_single_mut() { if let Ok(mut t) = sfx_text.single_mut() {
**t = format!("{:.2}", after); **t = format!("{:.2}", after);
} }
} }
@@ -424,8 +424,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_music_volume(-SFX_STEP); let after = settings.0.adjust_music_volume(-SFX_STEP);
if (before - after).abs() > f32::EPSILON { if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = music_text.get_single_mut() { if let Ok(mut t) = music_text.single_mut() {
**t = format!("{:.2}", after); **t = format!("{:.2}", after);
} }
} }
@@ -435,8 +435,8 @@ fn handle_settings_buttons(
let after = settings.0.adjust_music_volume(SFX_STEP); let after = settings.0.adjust_music_volume(SFX_STEP);
if (before - after).abs() > f32::EPSILON { if (before - after).abs() > f32::EPSILON {
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = music_text.get_single_mut() { if let Ok(mut t) = music_text.single_mut() {
**t = format!("{:.2}", after); **t = format!("{:.2}", after);
} }
} }
@@ -447,8 +447,8 @@ fn handle_settings_buttons(
DrawMode::DrawThree => DrawMode::DrawOne, DrawMode::DrawThree => DrawMode::DrawOne,
}; };
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = draw_text.get_single_mut() { if let Ok(mut t) = draw_text.single_mut() {
**t = draw_mode_label(&settings.0.draw_mode); **t = draw_mode_label(&settings.0.draw_mode);
} }
} }
@@ -459,8 +459,8 @@ fn handle_settings_buttons(
AnimSpeed::Instant => AnimSpeed::Normal, AnimSpeed::Instant => AnimSpeed::Normal,
}; };
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = anim_speed_text.get_single_mut() { if let Ok(mut t) = anim_speed_text.single_mut() {
**t = anim_speed_label(&settings.0.animation_speed); **t = anim_speed_label(&settings.0.animation_speed);
} }
} }
@@ -471,31 +471,31 @@ fn handle_settings_buttons(
Theme::Dark => Theme::Green, Theme::Dark => Theme::Green,
}; };
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = theme_text.get_single_mut() { if let Ok(mut t) = theme_text.single_mut() {
**t = theme_label(&settings.0.theme); **t = theme_label(&settings.0.theme);
} }
} }
SettingsButton::ToggleColorBlind => { SettingsButton::ToggleColorBlind => {
settings.0.color_blind_mode = !settings.0.color_blind_mode; settings.0.color_blind_mode = !settings.0.color_blind_mode;
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
if let Ok(mut t) = color_blind_text.get_single_mut() { if let Ok(mut t) = color_blind_text.single_mut() {
**t = color_blind_label(settings.0.color_blind_mode); **t = color_blind_label(settings.0.color_blind_mode);
} }
} }
SettingsButton::SelectCardBack(idx) => { SettingsButton::SelectCardBack(idx) => {
settings.0.selected_card_back = *idx; settings.0.selected_card_back = *idx;
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
} }
SettingsButton::SelectBackground(idx) => { SettingsButton::SelectBackground(idx) => {
settings.0.selected_background = *idx; settings.0.selected_background = *idx;
persist(&path, &settings.0); persist(&path, &settings.0);
changed.send(SettingsChangedEvent(settings.0.clone())); changed.write(SettingsChangedEvent(settings.0.clone()));
} }
SettingsButton::SyncNow => { SettingsButton::SyncNow => {
manual_sync.send(ManualSyncRequestEvent); manual_sync.write(ManualSyncRequestEvent);
} }
SettingsButton::Done => { SettingsButton::Done => {
screen.0 = false; 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(( parent.spawn((
Text::new(title), Text::new(title),
TextFont { TextFont {
@@ -893,7 +893,7 @@ fn section_label(parent: &mut ChildBuilder, title: &str) {
/// Generic volume row: `Label 0.80 [] [+]` /// Generic volume row: `Label 0.80 [] [+]`
fn volume_row<Marker: Component>( fn volume_row<Marker: Component>(
parent: &mut ChildBuilder, parent: &mut ChildSpawnerCommands,
label: &str, label: &str,
value: f32, value: f32,
marker: Marker, 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 parent
.spawn(( .spawn((
action, action,
+7 -7
View File
@@ -137,7 +137,7 @@ fn update_stats_on_new_game(
stats.0.record_abandoned(); stats.0.record_abandoned();
persist(&path, &stats.0, "abandoned game"); persist(&path, &stats.0, "abandoned game");
if streak > 1 { 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(); stats.0.record_abandoned();
persist(&path, &stats.0, "forfeit"); persist(&path, &stats.0, "forfeit");
if streak > 1 { 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 // 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 { if let Some(ref mut ac) = auto_complete {
**ac = AutoCompleteState::default(); **ac = AutoCompleteState::default();
} }
toast.send(InfoToastEvent("Game forfeited".to_string())); toast.write(InfoToastEvent("Game forfeited".to_string()));
new_game.send(NewGameRequestEvent::default()); new_game.write(NewGameRequestEvent::default());
} }
} }
@@ -187,8 +187,8 @@ fn toggle_stats_screen(
if !keys.just_pressed(KeyCode::KeyS) { if !keys.just_pressed(KeyCode::KeyS) {
return; return;
} }
if let Ok(entity) = screens.get_single() { if let Ok(entity) = screens.single() {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} else { } else {
spawn_stats_screen( spawn_stats_screen(
&mut commands, &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 /// 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. /// 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 parent
.spawn(( .spawn((
StatsCell, StatsCell,
+4 -4
View File
@@ -60,7 +60,7 @@ fn handle_start_time_attack_request(
return; return;
} }
if progress.0.level < CHALLENGE_UNLOCK_LEVEL { 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}" "Time Attack unlocks at level {CHALLENGE_UNLOCK_LEVEL}"
))); )));
return; return;
@@ -70,7 +70,7 @@ fn handle_start_time_attack_request(
remaining_secs: TIME_ATTACK_DURATION_SECS, remaining_secs: TIME_ATTACK_DURATION_SECS,
wins: 0, wins: 0,
}; };
new_game.send(NewGameRequestEvent { new_game.write(NewGameRequestEvent {
seed: None, seed: None,
mode: Some(GameMode::TimeAttack), mode: Some(GameMode::TimeAttack),
}); });
@@ -93,7 +93,7 @@ fn advance_time_attack(
let wins = session.wins; let wins = session.wins;
session.active = false; session.active = false;
session.remaining_secs = 0.0; 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; continue;
} }
session.wins = session.wins.saturating_add(1); session.wins = session.wins.saturating_add(1);
new_game.send(NewGameRequestEvent { new_game.write(NewGameRequestEvent {
seed: None, seed: None,
mode: Some(GameMode::TimeAttack), mode: Some(GameMode::TimeAttack),
}); });
+3 -3
View File
@@ -92,7 +92,7 @@ fn evaluate_weekly_goals(
any_change = true; any_change = true;
if just_completed { if just_completed {
bonus_xp = bonus_xp.saturating_add(WEEKLY_GOAL_XP); bonus_xp = bonus_xp.saturating_add(WEEKLY_GOAL_XP);
completions.send(WeeklyGoalCompletedEvent { completions.write(WeeklyGoalCompletedEvent {
goal_id: def.id.to_string(), goal_id: def.id.to_string(),
description: def.description.to_string(), description: def.description.to_string(),
}); });
@@ -101,10 +101,10 @@ fn evaluate_weekly_goals(
} }
if bonus_xp > 0 { 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); let prev_level = progress.0.add_xp(bonus_xp);
if progress.0.leveled_up_from(prev_level) { if progress.0.leveled_up_from(prev_level) {
levelups.send(LevelUpEvent { levelups.write(LevelUpEvent {
previous_level: prev_level, previous_level: prev_level,
new_level: progress.0.level, new_level: progress.0.level,
total_xp: progress.0.total_xp, total_xp: progress.0.total_xp,
+5 -5
View File
@@ -255,7 +255,7 @@ fn cache_win_data(
pending.challenge_level = challenge_level; pending.challenge_level = challenge_level;
if is_new_record { if is_new_record {
toast.send(InfoToastEvent("New Record!".to_string())); toast.write(InfoToastEvent("New Record!".to_string()));
} }
} }
for ev in xp.read() { for ev in xp.read() {
@@ -321,7 +321,7 @@ fn spawn_win_summary_after_delay(
*delay = Some(WIN_SUMMARY_DELAY_SECS); *delay = Some(WIN_SUMMARY_DELAY_SECS);
// Clear any stale overlay from a previous win. // Clear any stale overlay from a previous win.
for entity in &overlays { for entity in &overlays {
commands.entity(entity).despawn_recursive(); commands.entity(entity).despawn();
} }
} }
@@ -362,9 +362,9 @@ fn handle_win_summary_buttons(
WinSummaryButton::PlayAgain => { WinSummaryButton::PlayAgain => {
// Despawn the modal. // Despawn the modal.
for entity in &overlays { 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 /// 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 /// unlocked than the cap, appends a "...and N more" line so the player knows
/// there are additional unlocks visible on the achievements screen. /// 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(( card.spawn((
Text::new("Achievements Unlocked"), Text::new("Achievements Unlocked"),
TextFont { font_size: 18.0, ..default() }, TextFont { font_size: 18.0, ..default() },
+1 -3
View File
@@ -64,9 +64,7 @@ fn build_router_inner(pool: SqlitePool, rate_limit: bool) -> Router {
.finish() .finish()
.expect("invalid governor config"), .expect("invalid governor config"),
); );
auth_routes.layer(GovernorLayer { auth_routes.layer(GovernorLayer::new(governor_conf))
config: governor_conf,
})
} else { } else {
auth_routes auth_routes
}; };
-1
View File
@@ -100,7 +100,6 @@ pub fn validate_refresh_token(token: &str, secret: &str) -> Result<Claims, AppEr
// Axum extractor — allows handlers to receive AuthenticatedUser directly // Axum extractor — allows handlers to receive AuthenticatedUser directly
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthenticatedUser impl<S> FromRequestParts<S> for AuthenticatedUser
where where
S: Send + Sync, S: Send + Sync,