feat(D.2b): write-mode movement gate that preserves autorun

In chat write mode the keyboard belongs to the input — typing "swd" must not
walk the character — but AUTORUN must keep going (the user can chat while
running).

- InputDispatcher.IsActionHeld now returns false while WantCaptureKeyboard is
  set (a focused chat input), the polling-path twin of the existing gate on
  Fired actions. This SUPERSEDES the old per-frame OnUpdate early-return, which
  also killed autorun. Gating here instead lets the movement block keep running,
  so autorun — a separate latched bool ORed into Forward at the call site, not a
  polled key — survives. Test updated to encode the new contract.
- GameWindow: the movement suppress-guard reverts to ImGui-devtools-only (the
  retail write mode no longer early-returns); wires DefaultTextInput = the chat
  input (Tab/Enter activation) and Input.Keyboard for clipboard. Drops the
  one-shot UI-scale diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-16 15:24:19 +02:00
parent 367a752078
commit 2284a376ae
3 changed files with 37 additions and 17 deletions

View file

@ -148,27 +148,30 @@ public class InputDispatcherIsActionHeldTests
}
[Fact]
public void IsActionHeld_does_not_check_WantCaptureMouse()
public void IsActionHeld_gated_off_while_keyboard_captured()
{
// Per-frame held-state lookup is independent of UI capture: even
// with WantCaptureMouse=true a movement key already held when
// ImGui took focus continues to read as held until KeyUp. Press
// events ARE gated (the Press wouldn't fire while UI captures),
// but IsActionHeld answers the keyboard's underlying "is the
// physical key down right now" — which the legacy IsKeyPressed
// also did. The per-frame OnUpdate guard on
// ImGui.GetIO().WantCaptureKeyboard is what suppresses movement
// when chat is focused.
// Write-mode gate (2026-06-16): a focused chat input sets
// WantCaptureKeyboard, and held-key polling then reads RELEASED so typing
// "swd" doesn't move the character. This SUPERSEDES the old design (where the
// per-frame OnUpdate guard early-returned out of the whole movement block) —
// that approach also killed AUTORUN. By gating here instead, the movement block
// keeps running, so autorun (a separate latched bool ORed into Forward at the
// call site, NOT a polled key) survives write mode. WantCaptureMouse alone does
// NOT gate held-key polling — only keyboard capture does.
var (dispatcher, kb, mouse, bindings) = Build();
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
kb.EmitKeyDown(Key.W, ModifierMask.None);
mouse.WantCaptureMouse = true;
mouse.WantCaptureKeyboard = true;
// Held, no capture → reads held.
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
// Even with both capture flags set, IsActionHeld remains true
// because W is physically held. The dispatcher only suppresses
// press transitions.
// Keyboard captured (write mode) → held-key polling reads released.
mouse.WantCaptureKeyboard = true;
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
// Mouse capture alone must NOT gate movement polling (only keyboard does).
mouse.WantCaptureKeyboard = false;
mouse.WantCaptureMouse = true;
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
}
}