fix(multi): resolve 26 bugs found in comprehensive codebase review
Build and Deploy / build-and-push (push) Successful in 3m40s
Build and Deploy / build-and-push (push) Successful in 3m40s
Core fixes (issues #12, #13, #22): - #12: undo now preserves score delta instead of restoring snapshot score - #13: take_from_foundation defaults to false (non-standard house rule) - #22: check_win validates full suit sequence, not just card count Engine fixes: - #8: replay keyboard input guard against non-replay state - #9: help modal scrims.is_empty() guard added - #10: settings modal scrims.is_empty() guard added - #11: sync_plugin builds payload at poll time (not task-spawn time) - #14: server replay mode case-sensitivity fix ("Classic") - #15: play_by_seed_plugin confirmed flag set to true on launch - #16: replay back-step debounce via Local<bool> + StateChangedEvent; register StateChangedEvent in ReplayOverlayPlugin (fixes 52 tests) - #17: time-attack timer ignores win-summary overlay - #18: HUD dropdown glyphs U+25BE → U+2193 (FiraMono-safe arrow) - #19: theme plugin applies immediate visual update on A→B→A switch - #20: SyncAuthError / SyncBusyOverlay split into separate entities so auth errors are visible after busy overlay is hidden - #21: handle_forfeit ordered before update_stats_on_new_game - #23: server merge uses correct avg_time_seconds and games_lost math - #24: win_summary migrated to ModalScrim pattern - #25: card_animation apply_deferred between animation systems - #26: cursor_plugin HashMap access uses .get() with fallback - #27: auto_complete mid-sequence deactivation guard - #28: feedback_anim SettleAnim ordered before FoundationFlourish - #29: achievement_plugin iterates all win events; adds scrims guard - #30: leaderboard modal scrims.is_empty() guard added - #31: server auth tmp file cleanup on rename failure - #32: sync_setup modal scrims.is_empty() guard added - #33: font_plugin uses match fallback; TokioRuntimeResource graceful current-thread fallback on runtime init failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ use chrono::Datelike;
|
||||
|
||||
use crate::font_plugin::FontResource;
|
||||
use crate::layout::LayoutResource;
|
||||
use crate::events::{DrawRequestEvent, MoveRequestEvent, UndoRequestEvent};
|
||||
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
|
||||
use crate::replay_playback::{
|
||||
step_backwards_replay_playback, step_replay_playback, stop_replay_playback,
|
||||
toggle_pause_replay_playback, ReplayPlaybackState,
|
||||
@@ -476,6 +476,7 @@ impl Plugin for ReplayOverlayPlugin {
|
||||
.add_message::<MoveRequestEvent>()
|
||||
.add_message::<DrawRequestEvent>()
|
||||
.add_message::<UndoRequestEvent>()
|
||||
.add_message::<StateChangedEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
@@ -1884,6 +1885,7 @@ fn handle_pause_keyboard(
|
||||
/// resets to 0 on key release so the next fresh press fires
|
||||
/// immediately. This matches the mockup's `[← →] scrub`
|
||||
/// terminology while keeping single-press = single-step semantics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_arrow_keyboard(
|
||||
keys: Option<Res<ButtonInput<KeyCode>>>,
|
||||
time: Res<Time>,
|
||||
@@ -1892,10 +1894,22 @@ fn handle_arrow_keyboard(
|
||||
mut moves_writer: MessageWriter<MoveRequestEvent>,
|
||||
mut draws_writer: MessageWriter<DrawRequestEvent>,
|
||||
mut undo_writer: MessageWriter<UndoRequestEvent>,
|
||||
mut state_changed: MessageReader<StateChangedEvent>,
|
||||
// `true` while a backward step is in-flight: cursor was decremented and
|
||||
// `UndoRequestEvent` was written, but `handle_undo` hasn't applied it yet.
|
||||
// Cleared when `StateChangedEvent` confirms the game state has caught up.
|
||||
// Prevents rapid ← presses from accumulating multiple cursor decrements
|
||||
// before any undo is applied (Bug #16).
|
||||
mut back_pending: Local<bool>,
|
||||
) {
|
||||
let Some(keys) = keys else { return };
|
||||
let dt = time.delta_secs();
|
||||
|
||||
// Clear the in-flight flag once the game confirms the undo landed.
|
||||
if state_changed.read().count() > 0 {
|
||||
*back_pending = false;
|
||||
}
|
||||
|
||||
// Right (forward step) — initial press fires immediately;
|
||||
// held repeats fire when the accumulator crosses the interval.
|
||||
if keys.just_pressed(KeyCode::ArrowRight) {
|
||||
@@ -1911,14 +1925,28 @@ fn handle_arrow_keyboard(
|
||||
hold.right_held_secs = 0.0;
|
||||
}
|
||||
|
||||
// Left (backwards step) — symmetric to the right path.
|
||||
// Left (backwards step) — gate on `back_pending` so at most one undo
|
||||
// is in-flight at a time. The cursor is only decremented inside
|
||||
// `step_backwards_replay_playback`, which also writes `UndoRequestEvent`.
|
||||
// `back_pending` is set after a successful step and cleared above when
|
||||
// `StateChangedEvent` confirms the undo was applied.
|
||||
if keys.just_pressed(KeyCode::ArrowLeft) {
|
||||
step_backwards_replay_playback(&mut state, &mut undo_writer);
|
||||
if !*back_pending {
|
||||
let fired = step_backwards_replay_playback(&mut state, &mut undo_writer);
|
||||
if fired {
|
||||
*back_pending = true;
|
||||
}
|
||||
}
|
||||
hold.left_held_secs = 0.0;
|
||||
} else if keys.pressed(KeyCode::ArrowLeft) {
|
||||
hold.left_held_secs += dt;
|
||||
if hold.left_held_secs >= SCRUB_REPEAT_INTERVAL_SECS {
|
||||
step_backwards_replay_playback(&mut state, &mut undo_writer);
|
||||
if !*back_pending {
|
||||
let fired = step_backwards_replay_playback(&mut state, &mut undo_writer);
|
||||
if fired {
|
||||
*back_pending = true;
|
||||
}
|
||||
}
|
||||
hold.left_held_secs = 0.0;
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user