Commit graph

2 commits

Author SHA1 Message Date
Erik
785dd92378 fix(input): Phase K live-test fixes pt3 — fly→chase round-trip, Shift coexists, run-speed for backward + strafe
Four issues from the K-fix2 launch (2026-04-26 user report):

1. Can't return from free-fly to player view.
   CameraController.ToggleFly only swaps Fly↔Orbit, so a user who
   flew out of player mode landed in orbit (Holtburg) on
   toggle-back instead of the chase camera. Added
   ToggleFlyOrChase() helper that prefers Fly→Chase /
   Chase→Fly when _playerMode is true and a chase camera is
   available; falls back to the original Fly↔Orbit toggle for
   offline / pre-login flows. Wired into all three free-fly
   entry points: keyboard shortcut (Ctrl+Shift+F), Camera menu
   item, and DebugPanel button.

2. Shift while moving STOPS instead of dropping to walk.
   Root cause: InputDispatcher.IsChordHeld required
   _keyboard.CurrentModifiers to match chord.Modifiers EXACTLY.
   So with W bound as (W, None), holding W and then pressing
   Shift made CurrentModifiers=Shift mismatch chord (None) →
   IsActionHeld(MovementForward) returned false → Forward flag
   dropped → player stopped. Fixed by relaxing IsChordHeld:
   when chord.Modifiers is None, Shift is allowed to coexist
   (it's the retail walk-modifier). Other modifiers
   (Ctrl, Alt, Win) still mismatch strictly so Ctrl+W stays a
   distinct chord from W.

   +2 tests pinning the new permissive-Shift / strict-Ctrl
   semantics.

3. Backwards too slow when running.
   forwardCmdSpeed for the WalkBackward branch was hardcoded
   to 1.0; localY was hardcoded to -(WalkAnimSpeed * 0.65).
   Neither honored input.Run. With Run=true (default),
   backward now scales by runRate (~2.4×) so X = "run
   backwards" matches the forward run pace × the 0.65
   backward animation cycle ratio.

4. Strafe too slow when running.
   localX for SideStepLeft / SideStepRight was hardcoded to
   ±SidestepAnimSpeed regardless of Run. Same fix: when Run
   is held, scale by runRate so strafe at default speed
   matches the run-forward pace.

Tests: 1220 → 1222 (the two new IsChordHeld tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 14:48:45 +02:00
Erik
256e9624bd feat(input): #22 Phase K.1b - cut handlers over to dispatcher (single input path)
Removes the parallel direct keyboard/mouse polling that K.1a left in
GameWindow alongside the new dispatcher. Now every input flows
through InputDispatcher; legacy IsKeyPressed/KeyDown/MouseDown/MouseUp/
Scroll handlers in GameWindow are deleted (~220-line refactor).

Bindings remain acdream-current (W/S/A/D/Z/X movement, Shift run,
F-key debug surface). K.1c flips them to retail.

Pieces:
- InputDispatcher.IsActionHeld(InputAction): per-frame held-state
  query for movement (W/X/A/D/Z/X/Shift/Space) so PlayerMovement-
  Controller can read action state without polling raw keys.
  Internally walks all bindings for the action; chord match
  requires modifier mask exactness.
- InputAction adds AcdreamRmbOrbitHold (Hold-activation, RMB held
  drives chase-camera orbit) and AcdreamFlyDown (Ctrl held in fly
  mode for descent).
- GameWindow OnInputAction subscriber replaces the entire KeyDown
  switch + per-mouse-button handlers. Single dispatcher event drives:
    - F1  AcdreamToggleDebugPanel
    - F2  AcdreamToggleCollisionWires
    - F3  AcdreamDumpNearby
    - F7  AcdreamCycleTimeOfDay
    - F8  AcdreamSensitivityDown
    - F9  AcdreamSensitivityUp
    - F10 AcdreamCycleWeather
    - F   AcdreamToggleFlyMode
    - Tab AcdreamTogglePlayerMode (player/fly toggle - K.1c will
          reassign this to ToggleChatEntry)
    - Esc EscapeKey (cancel fly mode etc.)
    - Mouse wheel ScrollUp/ScrollDown (camera zoom)
    - RMB held (Hold) drives orbit; LMB drag still drives orbit
      camera; mouse position handled by surviving MouseMove handler
      which is gated on ImGui WantCaptureMouse.
- MovementInput per-frame: reads from _inputDispatcher.IsActionHeld.
  MouseDeltaX hardcoded to 0f (mouse never drives character yaw).
  _playerMouseDeltaX field stays defined for chase-camera RMB-orbit
  but is never consumed by movement.
- WantCaptureMouse explicit gate at the top of every surviving mouse
  handler in GameWindow (defense in depth - dispatcher already gates
  via IMouseSource.WantCaptureMouse).

Movement-input boundary preserved: PlayerMovementController.Update
still takes the same MovementInput struct. Existing
PlayerMovementControllerTests continue green - no regression in
motion-command byte production.

Two deviations:
1. Scroll lost magnitude going through the dispatcher (fixed-step
   zoom). Acceptable - discrete wheel-tick matches retail feel
   anyway.
2. Movement chords are duplicated with both ModifierMask.None and
   ModifierMask.Shift (covering "shift held to run while walking
   forward" etc.) so the dispatcher's modifier-strict matching
   preserves the modifier-blind feel of the old IsKeyPressed
   polling. Will be reshaped cleanly in K.1c when retail's
   walk-modifier semantics flip (default = run, shift held = walk).

15 new tests:
- InputDispatcherIsActionHeldTests: 7 cases covering chord-held +
  release + modifier-mismatch + multi-binding-for-action.
- InputDispatcherTests: 3 scroll-action cases.
- DispatcherToMovementIntegrationTests (Core.Tests): 5 cases
  proving FakeKeyboardSource.Press(W) -> dispatcher.IsActionHeld ->
  MovementInput.Forward -> PlayerMovementController produces the
  expected motion-command bytes. Includes the regression-prevention
  test that mouse-X delta value (zero vs nonzero) doesn't affect
  the motion bytes.

Solution total: 1133 green (243 Core.Net + 225 UI + 665 Core),
0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 23:43:11 +02:00