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:
parent
da189103b8
commit
af74eac0c2
12 changed files with 972 additions and 66 deletions
|
|
@ -198,4 +198,12 @@ public interface IPanelRenderer
|
|||
/// to force a scroll.
|
||||
/// </summary>
|
||||
void SetScrollHereY(float ratio);
|
||||
|
||||
/// <summary>
|
||||
/// Request keyboard focus for the NEXT widget rendered. Used by
|
||||
/// <c>ChatPanel</c> when Tab fires <c>ToggleChatEntry</c> — the
|
||||
/// chat input gets focused programmatically so the user can begin
|
||||
/// typing without clicking the field.
|
||||
/// </summary>
|
||||
void SetKeyboardFocusHere();
|
||||
}
|
||||
|
|
|
|||
124
src/AcDream.UI.Abstractions/Input/MouseLookState.cs
Normal file
124
src/AcDream.UI.Abstractions/Input/MouseLookState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue