From 89d82e1b76c53f7e89c7b72ce86bb0361cd7f809 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 13 May 2026 17:59:08 +0200 Subject: [PATCH] feat(B.4b): GameWindow wires Select/Use handlers via WorldPicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Rendering/GameWindow.cs | 74 +++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 6d2da3f..6124b01 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -783,6 +783,7 @@ public sealed class GameWindow : IDisposable /// fields when a 0xF625 ObjDescEvent arrives carrying only updated visuals. /// private readonly Dictionary _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))