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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() },
|
||||
|
||||
Reference in New Issue
Block a user