feat(B.4b): GameWindow wires Select/Use handlers via WorldPicker

Closes #57. Adds three OnInputAction switch cases (SelectLeft,
SelectDblLeft, UseSelected) and three private helpers
(PickAndStoreSelection, UseCurrentSelection, SendUse). Single-click
selects but does not Use; double-click selects + Uses; R hotkey
sends Use on the existing _selectedGuid. ImGui mouse-capture
filtering already happens in InputDispatcher — no new guard needed.

Diagnostic lines emitted for log grep:
  [B.4b] pick guid=0x{guid:X8} name={label}
  [B.4b] use  guid=0x{guid:X8} seq={seq}

Also adds a one-line doc comment on _selectedGuid clarifying its
dual-purpose role (combat Q-cycle + interaction click), per the Task 3
review.

Build green; tests 1046/1054 (8 pre-existing-baseline fails
unchanged). Switch-case behavior verified at runtime via the Holtburg
inn doorway visual test (per spec §Testing → Runtime verification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-13 17:59:08 +02:00
parent 7b4aff21b6
commit 89d82e1b76

View file

@ -783,6 +783,7 @@ public sealed class GameWindow : IDisposable
/// fields when a 0xF625 ObjDescEvent arrives carrying only updated visuals.
/// </summary>
private readonly Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> _lastSpawnByGuid = new();
// Current selection: written by Q-cycle (combat) and LMB click (interact); cleared on entity despawn.
private uint? _selectedGuid;
private readonly record struct LiveEntityInfo(
string? Name,
@ -8629,6 +8630,18 @@ public sealed class GameWindow : IDisposable
SendLiveCombatAttack(AcDream.Core.Combat.CombatAttackAction.High);
break;
case AcDream.UI.Abstractions.Input.InputAction.SelectLeft:
PickAndStoreSelection(useImmediately: false);
break;
case AcDream.UI.Abstractions.Input.InputAction.SelectDblLeft:
PickAndStoreSelection(useImmediately: true);
break;
case AcDream.UI.Abstractions.Input.InputAction.UseSelected:
UseCurrentSelection();
break;
case AcDream.UI.Abstractions.Input.InputAction.EscapeKey:
if (_cameraController?.IsFlyMode == true)
_cameraController.ToggleFly(); // exit fly, release cursor
@ -8703,6 +8716,67 @@ public sealed class GameWindow : IDisposable
return SelectClosestCombatTarget(showToast: false);
}
// ============================================================
// Phase B.4b — outbound Use handler. Wires three input actions
// (LMB click select, LMB-double-click select+use, R hotkey
// use-selected) through WorldPicker into InteractRequests.BuildUse.
// The inbound reply (SetState 0xF74B) lands via L.2g slice 1.
// ============================================================
private void PickAndStoreSelection(bool useImmediately)
{
if (_cameraController is null || _window is null) return;
var camera = _cameraController.Active;
var (origin, direction) = AcDream.Core.Selection.WorldPicker.BuildRay(
mouseX: _lastMouseX, mouseY: _lastMouseY,
viewportW: _window.Size.X, viewportH: _window.Size.Y,
view: camera.View, projection: camera.Projection);
if (direction.LengthSquared() < 1e-6f) return; // degenerate ray
var picked = AcDream.Core.Selection.WorldPicker.Pick(
origin, direction,
_entitiesByServerGuid.Values,
skipServerGuid: _playerServerGuid,
maxDistance: 50f);
if (picked is uint guid)
{
_selectedGuid = guid;
string label = DescribeLiveEntity(guid);
Console.WriteLine($"[B.4b] pick guid=0x{guid:X8} name={label}");
_debugVm?.AddToast($"Selected: {label}");
if (useImmediately) SendUse(guid);
}
else
{
_debugVm?.AddToast("Nothing to select");
}
}
private void UseCurrentSelection()
{
if (_selectedGuid is uint sel)
SendUse(sel);
else
_debugVm?.AddToast("Nothing selected");
}
private void SendUse(uint guid)
{
if (_liveSession is null
|| _liveSession.CurrentState != AcDream.Core.Net.WorldSession.State.InWorld)
{
_debugVm?.AddToast("Not in world");
return;
}
var seq = _liveSession.NextGameActionSequence();
var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
_liveSession.SendGameAction(body);
Console.WriteLine($"[B.4b] use guid=0x{guid:X8} seq={seq}");
}
private uint? SelectClosestCombatTarget(bool showToast)
{
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var playerEntity))