Merge pull request 'fix(engine): swallow game-mutating hotkeys during pointer drags' (#185) from fix/keyboard-during-drag into master
This commit was merged in pull request #185.
This commit is contained in:
@@ -215,6 +215,7 @@ fn handle_keyboard_core(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
progress: Option<Res<ProgressResource>>,
|
||||
drag: Res<DragState>,
|
||||
mut ev: CoreKeyboardMessages<'_>,
|
||||
mut time_attack: Option<ResMut<TimeAttackResource>>,
|
||||
selection: Option<Res<SelectionState>>,
|
||||
@@ -227,6 +228,19 @@ fn handle_keyboard_core(
|
||||
return;
|
||||
}
|
||||
|
||||
// Mutual exclusion with pointer drags — mirrors `handle_selection_keys`.
|
||||
// Undo / draw / new-game during an active mouse or touch drag would fire
|
||||
// a StateChangedEvent whose card re-sync inserts a `CardAnim` on the
|
||||
// dragged cards, fighting `follow_drag`'s per-frame Transform writes and
|
||||
// leaving `DragState` origin indices stale against the mutated state.
|
||||
// The keyboard-drag sentinel may proceed: `clear_selection_on_state_change`
|
||||
// already drops that lift cleanly when the state moves.
|
||||
if !drag.is_idle()
|
||||
&& drag.active_touch_id != Some(crate::selection_plugin::KEYBOARD_DRAG_TOUCH_ID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// During replay playback (Playing or Completed) all game-input shortcuts
|
||||
// are suppressed. The replay overlay owns Space (pause/resume) and the
|
||||
// arrow keys (step). Letting game input through would mutate
|
||||
@@ -473,11 +487,19 @@ pub fn emit_hint_visuals(
|
||||
fn handle_keyboard_forfeit(
|
||||
keys: Res<ButtonInput<KeyCode>>,
|
||||
paused: Option<Res<PausedResource>>,
|
||||
drag: Res<DragState>,
|
||||
mut requests: MessageWriter<ForfeitRequestEvent>,
|
||||
) {
|
||||
if paused.is_some_and(|p| p.0) {
|
||||
return;
|
||||
}
|
||||
// Same pointer-drag exclusion as `handle_keyboard_core` — forfeiting
|
||||
// mid-drag would mutate the game state under the dragged cards.
|
||||
if !drag.is_idle()
|
||||
&& drag.active_touch_id != Some(crate::selection_plugin::KEYBOARD_DRAG_TOUCH_ID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if !keys.just_pressed(KeyCode::KeyG) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -817,3 +817,123 @@ fn pressing_h_spawns_pending_hint_task() {
|
||||
"pressing H must spawn an async hint task",
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Pointer-drag / keyboard mutual exclusion.
|
||||
//
|
||||
// A state mutation fired while `follow_drag` is writing the dragged cards'
|
||||
// transforms every frame would make the card re-sync insert a `CardAnim`
|
||||
// on the same entities (two systems fighting over `Transform`) and leave
|
||||
// `DragState`'s origin indices stale. `handle_keyboard_core` and
|
||||
// `handle_keyboard_forfeit` therefore swallow game-mutating shortcuts
|
||||
// while a mouse or touch drag is live, mirroring `handle_selection_keys`.
|
||||
// The keyboard-lift sentinel is exempt: `clear_selection_on_state_change`
|
||||
// drops that lift cleanly when the state moves.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
fn keyboard_core_app() -> App {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.add_message::<UndoRequestEvent>();
|
||||
app.add_message::<NewGameRequestEvent>();
|
||||
app.add_message::<InfoToastEvent>();
|
||||
app.add_message::<DrawRequestEvent>();
|
||||
app.add_message::<StartZenRequestEvent>();
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
app.init_resource::<DragState>();
|
||||
app.add_systems(Update, handle_keyboard_core);
|
||||
app
|
||||
}
|
||||
|
||||
fn press_key(app: &mut App, key: KeyCode) {
|
||||
let mut input = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
|
||||
input.release(key);
|
||||
input.clear();
|
||||
input.press(key);
|
||||
}
|
||||
|
||||
fn message_count<M: Message>(app: &App) -> usize {
|
||||
let messages = app.world().resource::<Messages<M>>();
|
||||
let mut cursor = messages.get_cursor();
|
||||
cursor.read(messages).count()
|
||||
}
|
||||
|
||||
fn set_drag(app: &mut App, active_touch_id: Option<u64>) {
|
||||
let mut drag = app.world_mut().resource_mut::<DragState>();
|
||||
drag.cards = vec![Card::new(Deck::Deck1, Suit::Clubs, Rank::Two)];
|
||||
drag.committed = true;
|
||||
drag.active_touch_id = active_touch_id;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u_key_fires_undo_when_no_drag_active() {
|
||||
let mut app = keyboard_core_app();
|
||||
press_key(&mut app, KeyCode::KeyU);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
message_count::<UndoRequestEvent>(&app),
|
||||
1,
|
||||
"U with an idle DragState must fire UndoRequestEvent",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u_key_ignored_during_committed_mouse_drag() {
|
||||
let mut app = keyboard_core_app();
|
||||
set_drag(&mut app, None); // mouse drag
|
||||
press_key(&mut app, KeyCode::KeyU);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
message_count::<UndoRequestEvent>(&app),
|
||||
0,
|
||||
"U during a mouse drag must be swallowed",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_key_ignored_during_touch_drag() {
|
||||
let mut app = keyboard_core_app();
|
||||
set_drag(&mut app, Some(7)); // real touch id
|
||||
press_key(&mut app, KeyCode::KeyD);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
message_count::<DrawRequestEvent>(&app),
|
||||
0,
|
||||
"D during a touch drag must be swallowed",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u_key_allowed_during_keyboard_lift() {
|
||||
let mut app = keyboard_core_app();
|
||||
set_drag(
|
||||
&mut app,
|
||||
Some(crate::selection_plugin::KEYBOARD_DRAG_TOUCH_ID),
|
||||
);
|
||||
press_key(&mut app, KeyCode::KeyU);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
message_count::<UndoRequestEvent>(&app),
|
||||
1,
|
||||
"the keyboard-lift sentinel must not block core shortcuts",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn g_key_forfeit_ignored_during_mouse_drag() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.add_message::<ForfeitRequestEvent>();
|
||||
app.init_resource::<ButtonInput<KeyCode>>();
|
||||
app.init_resource::<DragState>();
|
||||
app.add_systems(Update, handle_keyboard_forfeit);
|
||||
|
||||
set_drag(&mut app, None);
|
||||
press_key(&mut app, KeyCode::KeyG);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
message_count::<ForfeitRequestEvent>(&app),
|
||||
0,
|
||||
"G during a mouse drag must be swallowed",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user