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
115
src/AcDream.App/Input/PlayerModeAutoEntry.cs
Normal file
115
src/AcDream.App/Input/PlayerModeAutoEntry.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
using System;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Phase K.2 — one-shot guard that auto-enters player mode after a
|
||||
/// successful login once every prerequisite is satisfied. Owned by
|
||||
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// Why is this its own class? The auto-entry has three independent
|
||||
/// preconditions (live session reaches <c>InWorld</c>, the player
|
||||
/// entity has been streamed into the world dictionary, and the player
|
||||
/// movement controller is constructible) plus a manual-override path
|
||||
/// (the user can flip into fly mode before the auto-entry fires —
|
||||
/// their choice wins). All four interact with each other in a way
|
||||
/// that's painful to test through GameWindow but trivial here against
|
||||
/// fakes.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The public surface is:
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="Arm"/> — call after <c>EnterWorld</c> succeeds to
|
||||
/// arm the entry trigger.</item>
|
||||
/// <item><see cref="Cancel"/> — call when the user manually enters
|
||||
/// fly mode (or any other code path that pre-empts the auto-entry).</item>
|
||||
/// <item><see cref="TryEnter"/> — call once per frame; runs the
|
||||
/// guard and fires the entry callback when armed AND every
|
||||
/// precondition is satisfied; returns true on the firing tick.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// All preconditions are passed in as predicates so the class doesn't
|
||||
/// pull in <c>WorldSession</c>, <c>PlayerMovementController</c>, or
|
||||
/// any GameWindow-internal types — the unit test wires them to plain
|
||||
/// boolean fields.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class PlayerModeAutoEntry
|
||||
{
|
||||
private readonly Func<bool> _isLiveInWorld;
|
||||
private readonly Func<bool> _isPlayerEntityPresent;
|
||||
private readonly Func<bool> _isPlayerControllerReady;
|
||||
private readonly Action _enterPlayerMode;
|
||||
|
||||
private bool _armed;
|
||||
|
||||
/// <summary>
|
||||
/// Build an auto-entry guard.
|
||||
/// </summary>
|
||||
/// <param name="isLiveInWorld">True iff the live session is in the
|
||||
/// <c>InWorld</c> state. Skip auto-entry when the session is null
|
||||
/// or hasn't reached InWorld yet.</param>
|
||||
/// <param name="isPlayerEntityPresent">True iff the player's
|
||||
/// server-guid is already in the local entity dictionary (server
|
||||
/// has streamed at least one CreateObject for the character).</param>
|
||||
/// <param name="isPlayerControllerReady">True iff the per-frame
|
||||
/// PlayerMovementController is set up. Stays true once player mode
|
||||
/// is established; the auto-entry's job is to flip it from false
|
||||
/// to true exactly once.</param>
|
||||
/// <param name="enterPlayerMode">Action invoked on the firing
|
||||
/// tick. The same routine the manual Tab handler invokes (fly →
|
||||
/// player transition). Must construct the controller + chase
|
||||
/// camera and switch the active camera; the auto-entry doesn't
|
||||
/// reach inside.</param>
|
||||
public PlayerModeAutoEntry(
|
||||
Func<bool> isLiveInWorld,
|
||||
Func<bool> isPlayerEntityPresent,
|
||||
Func<bool> isPlayerControllerReady,
|
||||
Action enterPlayerMode)
|
||||
{
|
||||
_isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
|
||||
_isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
|
||||
_isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
|
||||
_enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
|
||||
}
|
||||
|
||||
/// <summary>True iff <see cref="TryEnter"/> would still fire if the
|
||||
/// preconditions become true. Flips false on a successful entry
|
||||
/// (one-shot) or when <see cref="Cancel"/> is invoked.</summary>
|
||||
public bool IsArmed => _armed;
|
||||
|
||||
/// <summary>
|
||||
/// Arm the trigger. Call after <c>WorldSession.EnterWorld</c>
|
||||
/// returns successfully. Calling again while already armed is a
|
||||
/// no-op.
|
||||
/// </summary>
|
||||
public void Arm() => _armed = true;
|
||||
|
||||
/// <summary>
|
||||
/// Disarm the trigger without firing the callback. Call when the
|
||||
/// user has manually entered fly mode (or any other code path
|
||||
/// that pre-empts the auto-entry) — the user's choice wins.
|
||||
/// </summary>
|
||||
public void Cancel() => _armed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Guard tick. If the trigger is armed AND every precondition is
|
||||
/// satisfied, invokes <c>enterPlayerMode</c>, disarms, and
|
||||
/// returns true. Returns false otherwise (no side effects).
|
||||
/// </summary>
|
||||
public bool TryEnter()
|
||||
{
|
||||
if (!_armed) return false;
|
||||
if (!_isLiveInWorld()) return false;
|
||||
if (!_isPlayerEntityPresent()) return false;
|
||||
if (!_isPlayerControllerReady()) return false;
|
||||
|
||||
_armed = false;
|
||||
_enterPlayerMode();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue