feat(input): #24 Phase K.2 - auto-enter player mode at login + MMB mouse-look + DebugPanel free-fly + Tab to chat-input focus

Five changes:

1. PlayerModeAutoEntry — testable guard class that fires once after
   EnterWorld + WorldSession.State.InWorld + player entity present +
   PlayerController.State == InWorld. GameWindow arms the entry
   after EnterWorld; per-frame Tick checks all four guards and
   invokes the same fly-to-player transition the Tab handler runs.
   User-initiated fly toggle (DebugPanel button) Cancel()s pending
   entry. Skip in offline mode (no ACDREAM_LIVE) — Holtburg orbit
   stays default for testing.

2. MouseLookState + KeyBindings.RetailDefaults() binds MMB Hold to
   InputAction.CameraInstantMouseLook. GameWindow subscribes:
   - Press: hide cursor, capture position, _mouseLookActive = true.
   - Release: restore cursor, deactivate.
   - WantCaptureMouse=true while held → suspend (release cursor).
   - MouseMove while active: combined drive — chase camera yaw +
     character heading move together (retail's signature mouse-look
     behavior). Camera Y still pitches camera-only.

3. DebugPanel "Toggle Free-Fly Mode" button via DebugVM.ToggleFlyMode
   action delegate — replaces the F-key as the primary discovery
   path for free-fly. Gated on DevToolsEnabled.

4. ChatPanel.FocusInput() one-shot + IPanelRenderer.SetKeyboardFocusHere
   primitive. GameWindow's ToggleChatEntry (Tab) subscriber calls
   _chatPanel.FocusInput() so Tab moves focus to the chat input
   field. Replaces the K.1c TODO stub.

5. WantCaptureMouse gating reinforcement on surviving mouse handlers
   (no new code; verified intact from K.1b).

21 new tests (8 PlayerModeAutoEntry, 10 MouseLookState, 3 ChatPanel
focus). 1183 total green. 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 09:20:17 +02:00
parent da189103b8
commit af74eac0c2
12 changed files with 972 additions and 66 deletions

View file

@ -0,0 +1,124 @@
using System;
namespace AcDream.UI.Abstractions.Input;
/// <summary>
/// Phase K.2 — state machine for MMB-hold "instant mouse-look" mode
/// (retail's <c>CameraInstantMouseLook</c>). While active, mouse-X
/// delta drives the character's heading AND the chase camera yaw
/// together (combined drive — the camera "instantly" follows the
/// character because mouse-X moves the character, and the chase
/// camera always tracks the character). Mouse-Y is left to the
/// caller — typically pitches the chase camera only.
///
/// <para>
/// The class owns three transitions:
/// <list type="bullet">
/// <item><see cref="Press"/> — MMB pressed AND ImGui isn't hovering a
/// panel; activate, capture initial cursor position for restore.</item>
/// <item><see cref="Release"/> — MMB released; deactivate, signal
/// cursor restore.</item>
/// <item><see cref="OnWantCaptureMouseChanged"/> — ImGui took mouse
/// focus while we were active (e.g. a panel pop-up); deactivate AS IF
/// the user released the button so the cursor is restored.</item>
/// </list>
/// </para>
///
/// <para>
/// <see cref="ApplyDelta"/> is the per-frame mouse-move hook: when
/// active, scales <paramref name="dx"/> by sensitivity and feeds the
/// caller-supplied yaw mutator. The mutator is the only side-channel —
/// the class doesn't know about <c>PlayerMovementController</c> or
/// <c>ChaseCamera</c>.
/// </para>
/// </summary>
public sealed class MouseLookState
{
private readonly Action<float> _applyYawDelta;
/// <summary>True while MMB is held AND ImGui isn't capturing the
/// mouse. Mouse-X deltas drive yaw only when this is true.</summary>
public bool Active { get; private set; }
/// <summary>Cursor X at the moment <see cref="Press"/> activated.
/// Restore on release.</summary>
public float CapturedCursorX { get; private set; }
/// <summary>Cursor Y at the moment <see cref="Press"/> activated.</summary>
public float CapturedCursorY { get; private set; }
/// <summary>
/// Per-radian yaw multiplier applied to mouse-X delta. The same
/// chase-camera sensitivity factor used elsewhere in GameWindow is
/// folded in by the caller; this class only owns the constant
/// scale that converts pixels to radians. Default 0.004 matches
/// the K.1b RMB-orbit factor.
/// </summary>
public float SensitivityRadiansPerPixel { get; set; } = 0.004f;
public MouseLookState(Action<float> applyYawDelta)
{
_applyYawDelta = applyYawDelta ?? throw new ArgumentNullException(nameof(applyYawDelta));
}
/// <summary>
/// MMB press transition. Activates only when ImGui isn't capturing
/// the mouse — the dispatcher should already gate this, but the
/// guard adds defense in depth in case a binding fires through.
/// </summary>
/// <param name="cursorX">Current cursor X (captured for restore on release).</param>
/// <param name="cursorY">Current cursor Y.</param>
/// <param name="wantCaptureMouse">Mirror of
/// <c>ImGui.GetIO().WantCaptureMouse</c>. When true, the press is
/// ignored so a hover-over-panel MMB doesn't grab the cursor.</param>
public void Press(float cursorX, float cursorY, bool wantCaptureMouse)
{
if (wantCaptureMouse) return;
if (Active) return;
Active = true;
CapturedCursorX = cursorX;
CapturedCursorY = cursorY;
}
/// <summary>MMB release transition. Always deactivates if active.</summary>
public void Release()
{
if (!Active) return;
Active = false;
}
/// <summary>
/// Reactive deactivation when ImGui takes mouse focus mid-hold.
/// E.g. a tooltip pops open over the cursor while MMB is held —
/// we yield the cursor to the panel instead of staying captured.
/// </summary>
public void OnWantCaptureMouseChanged(bool wantCaptureMouse)
{
if (Active && wantCaptureMouse) Active = false;
}
/// <summary>
/// Apply a per-frame mouse-X delta. When active, scales by
/// <see cref="SensitivityRadiansPerPixel"/> times the
/// caller-supplied <paramref name="extraSensitivity"/> (typically
/// the chase-camera sens) and feeds it to the yaw mutator. Sign
/// matches retail: dragging the mouse RIGHT yaws the character to
/// the right (positive yaw delta).
/// </summary>
/// <param name="dx">Pixels of horizontal mouse motion since the
/// last frame.</param>
/// <param name="extraSensitivity">Multiplier (e.g. chase-camera
/// sensitivity) applied on top of the radians-per-pixel scale.</param>
public void ApplyDelta(float dx, float extraSensitivity)
{
if (!Active) return;
// Sign: dragging the mouse RIGHT (dx > 0) should yaw the
// character to the right. With the acdream Yaw convention
// (where Yaw 0 = +X, increasing to +Y), positive yaw is
// counter-clockwise viewed top-down — so dragging right means
// yaw goes DOWN (more clockwise). The dispatcher convention
// for chase YawOffset is `YawOffset -= dx * factor`; we keep
// the same sign here so character + camera rotate identically.
_applyYawDelta(-dx * SensitivityRadiansPerPixel * extraSensitivity);
}
}