fix(engine): resolve input coordination bugs in selection/pause/keyboard

- SelectionPlugin: add clear_selection_on_state_change system so undo/move/reject
  never leave a stale selection pointing at the wrong card
- SelectionPlugin: expose SelectionKeySet system set for cross-plugin ordering
- PausePlugin: skip Escape→pause when a card is keyboard-selected; toggle_pause
  now runs before SelectionKeySet so it reads SelectionState before it is cleared
- InputPlugin: guard Space→DrawRequestEvent when SelectionState has an active pile
  so Space executes a card move instead of also drawing from stock
- window: enforce 800×600 minimum via WindowResizeConstraints
- game_state: add precondition doc to next_auto_complete_move (waste exclusion)
- card_plugin: 12 unit tests for constants, face_colour, label_visibility, label_for
- pause_plugin: add paused_resource_default and draw_mode_label exhaustiveness tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-04-28 22:13:10 +00:00
parent ffc79447d4
commit 1ec2593137
6 changed files with 207 additions and 4 deletions
+37 -1
View File
@@ -23,6 +23,7 @@ use crate::events::StateChangedEvent;
use crate::game_plugin::{GameOverScreen, GameStatePath};
use crate::progress_plugin::ProgressResource;
use crate::resources::{DragState, GameStateResource};
use crate::selection_plugin::{SelectionKeySet, SelectionState};
use crate::settings_plugin::{SettingsChangedEvent, SettingsResource, SettingsStoragePath};
use crate::stats_plugin::StatsResource;
@@ -58,7 +59,15 @@ impl Plugin for PausePlugin {
app.add_message::<SettingsChangedEvent>()
.add_message::<StateChangedEvent>()
.init_resource::<PausedResource>()
.add_systems(Update, (toggle_pause, handle_pause_draw_toggle));
.add_systems(
Update,
(
// toggle_pause must see SelectionState *before* handle_selection_keys
// clears it, so it can skip Escape when a card is selected.
toggle_pause.before(SelectionKeySet),
handle_pause_draw_toggle,
),
);
}
}
@@ -76,10 +85,16 @@ fn toggle_pause(
settings: Option<Res<SettingsResource>>,
mut drag: Option<ResMut<DragState>>,
mut changed: MessageWriter<StateChangedEvent>,
selection: Option<Res<SelectionState>>,
) {
if !keys.just_pressed(KeyCode::Escape) {
return;
}
// If a card is currently selected, let SelectionPlugin handle this Escape
// (it will clear the selection). Pause must not also open in the same frame.
if selection.is_some_and(|s| s.selected_pile.is_some()) {
return;
}
// If the game-over overlay is visible, let handle_game_over_input consume
// the Escape key (to start a new game). Do not open the pause overlay.
if !game_over_screens.is_empty() {
@@ -415,6 +430,16 @@ mod tests {
);
}
// -----------------------------------------------------------------------
// PausedResource default (pure)
// -----------------------------------------------------------------------
#[test]
fn paused_resource_default_is_unpaused() {
let p = PausedResource::default();
assert!(!p.0, "game must start unpaused");
}
// -----------------------------------------------------------------------
// draw_mode_label (pure function) — Task #64
// -----------------------------------------------------------------------
@@ -429,6 +454,17 @@ mod tests {
assert_eq!(draw_mode_label(DrawMode::DrawThree), "Draw 3");
}
/// Both variants are covered so the match is exhaustive — this test would
/// fail to compile if a new DrawMode variant were added without updating
/// `draw_mode_label`.
#[test]
fn draw_mode_label_covers_all_variants() {
for mode in [DrawMode::DrawOne, DrawMode::DrawThree] {
let label = draw_mode_label(mode);
assert!(!label.is_empty(), "draw_mode_label must never return an empty string");
}
}
// -----------------------------------------------------------------------
// pause_draw_toggle_flips_draw_mode — Task #64
// -----------------------------------------------------------------------