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

@ -42,6 +42,14 @@ public sealed class ChatPanel : IPanel
// entries without yanking the user's manual scroll.
private int _lastRenderedCount;
// Phase K.2: one-shot focus request for the chat input. Set by
// FocusInput() (driven by Tab → ToggleChatEntry); the next Render
// call emits SetKeyboardFocusHere immediately before the input
// field and clears the flag. Without the one-shot semantics, the
// panel would steal focus on every frame and the user could never
// click into another widget.
private bool _focusRequested;
public ChatPanel(ChatVM vm)
{
_vm = vm ?? throw new ArgumentNullException(nameof(vm));
@ -56,6 +64,15 @@ public sealed class ChatPanel : IPanel
/// <inheritdoc />
public bool IsVisible { get; set; } = true;
/// <summary>
/// Phase K.2: request keyboard focus for the chat input on the
/// NEXT <see cref="Render"/>. One-shot — fires once and resets,
/// so callers (e.g. <c>GameWindow</c>'s Tab handler subscribing to
/// <c>ToggleChatEntry</c>) can drive it on a single key press
/// without trapping the user permanently in the input field.
/// </summary>
public void FocusInput() => _focusRequested = true;
/// <inheritdoc />
public void Render(PanelContext ctx, IPanelRenderer renderer)
{
@ -115,6 +132,15 @@ public sealed class ChatPanel : IPanel
// Phase I.4: input field. Backend implementation clears _input
// on submit per the IPanelRenderer contract.
renderer.Separator();
// Phase K.2: honor a pending FocusInput() request — emit
// SetKeyboardFocusHere immediately before the input widget so
// ImGui (or the future custom backend) applies it to that
// field. One-shot: clear the flag after firing.
if (_focusRequested)
{
renderer.SetKeyboardFocusHere();
_focusRequested = false;
}
if (renderer.InputTextSubmit("##chatinput", ref _input, InputBufferMaxLen, out var submitted)
&& submitted is not null)
{

View file

@ -211,6 +211,12 @@ public sealed class DebugPanel : IPanel
r.SameLine();
if (r.Button("Toggle collision wires")) _vm.ToggleCollisionWires?.Invoke();
// Phase K.2 — explicit free-fly toggle button. Mirrors the
// legacy F-key alias but is discoverable to users who haven't
// memorized the Ctrl+F* debug bindings. Action handle owned
// by GameWindow; null-safe for tests / offline.
if (r.Button("Toggle Free-Fly Mode")) _vm.ToggleFlyMode?.Invoke();
r.Text(_vm.DebugWireframes ? "collision wires: ON" : "collision wires: OFF");
}

View file

@ -255,6 +255,15 @@ public sealed class DebugVM
/// </summary>
public Action? ToggleCollisionWires { get; set; }
/// <summary>
/// Phase K.2 — toggle the free-fly camera. Lets a user opt out of
/// the auto-entered chase camera (e.g. to inspect a remote part of
/// the world without the player following) without needing to find
/// the Ctrl+F* debug binding. Wired by <c>GameWindow</c> to the
/// same routine the legacy F-key fly toggle invokes.
/// </summary>
public Action? ToggleFlyMode { get; set; }
// ── Combat event ring + toast ring ─────────────────────────────────
/// <summary>