From cffb10ff18763ab026175a7fd01c3a22ea366314 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 15 May 2026 15:15:30 +0200 Subject: [PATCH] =?UTF-8?q?fix(B.6):=20tighter=205=C2=B0=20alignment=20+?= =?UTF-8?q?=20defer=20Use=20until=20rotation=20completes;=20file=20#69=20t?= =?UTF-8?q?urn=20anim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: 'You should be face to face with the NPC before sending use. So first is rotation, when you are facing, then using.' and 'it does not face it completely.' Two changes: 1. Split alignment thresholds in ApplyAutoWalkOverlay: walkAligned (30°) — gate for synthesised Forward+Run motion during far-range approach; body walks while finishing residual turn within 30°. aligned (5°) — gate for arrival-fire. Final facing before the auto-walk ends and the action re-sends. Matches retail's tight pre-Use rotation tolerance. Within-arrival check still requires alignment; without alignment the body holds in turn-only mode regardless of distance. 2. Defer wire Use/PickUp packet for CLOSE-range targets. SendUse and SendPickUp now check IsCloseRangeTarget(guid): if the player is already within the target's use-radius, we install the speculative overlay, set _pendingPostArrivalAction, and RETURN without sending the wire packet. AutoWalkArrived fires after the local rotation completes (alignment within 5°); the existing re-send handler then fires SendUse with isRetryAfterArrival=true, sending the wire packet at that moment. Effect: rotate first, THEN Use — the NPC/door/item only sees the action after the character has turned to face it. Far-range path unchanged: send immediately, ACE auto-walks, arrival re-sends. Filed #69: turn animation (leg/arm cycle while pivoting). The body now rotates but doesn't play the TurnLeft/TurnRight cycle the user wants to see. Separate scope — needs motion-interpreter integration. --- docs/ISSUES.md | 38 +++++++++++ .../Input/PlayerMovementController.cs | 34 ++++++---- src/AcDream.App/Rendering/GameWindow.cs | 67 ++++++++++++++++++- 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 0111505..d55d87c 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -46,6 +46,44 @@ Copy this block when adding a new issue: # Active issues +## #69 — Local player rotation isn't animated (no leg/arm cycle while pivoting) + +**Status:** OPEN +**Severity:** LOW (visual polish — rotation works, just looks stiff) +**Filed:** 2026-05-15 (B.6 close-range turn-to-face) +**Component:** motion / animation cycle + +**Description:** When the auto-walk overlay rotates the local player +(close-range Use turn-to-face, or turn-first phase of a far-range walk), +the body's Yaw rotates smoothly but no leg / arm animation plays — +the body just statue-pivots. Retail played a `TurnLeft` / `TurnRight` +motion cycle while rotating, visible to observers as the character +moving their legs / arms to turn. + +**Cause:** `ApplyAutoWalkOverlay` synthesises `Forward+Run` input +during the walking phase (so the motion interpreter emits `RunForward` +cycle commands), but synthesises nothing during the turn-only phase +— so the motion interpreter emits no command and the sequencer +holds whatever cycle was last set (typically Ready / idle). + +**Approach:** While turning (`!walkAligned`), synthesise +`TurnLeft = delta > 0` / `TurnRight = delta < 0` so the motion +interpreter emits the turn command. Care needed: the existing +`Update` body also steps Yaw on `TurnLeft`/`TurnRight` input — if +both apply, the body rotates twice as fast. Cleanest: set the input +flags AND skip the overlay's own Yaw step (let Update's existing +handling do the rotation). + +**Acceptance:** A retail observer watching `+Acdream` turn to face +an NPC sees the turning animation play (leg shuffle / arm swing) for +the duration of the rotation. + +**Estimated scope:** Small. ~30 LOC in `ApplyAutoWalkOverlay` plus +verification that retail's `TurnLeft`/`TurnRight` cycle is in the +human motion table. + +--- + ## #68 — Remote players don't stop running animation on auto-walk arrival **Status:** OPEN diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 0cf7b4b..32ba7ed 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -482,7 +482,8 @@ public sealed class PlayerMovementController // walk-while-turning threshold off, suppress Forward this frame // so the body turns IN PLACE first. Once we're within the // threshold, the synthesised Forward+Run kicks in below. - bool aligned = true; + bool aligned = true; + bool walkAligned = true; if (dist > 1e-4f) { float desiredYaw = MathF.Atan2(dy, dx); @@ -501,13 +502,21 @@ public sealed class PlayerMovementController while (Yaw > MathF.PI) Yaw -= 2f * MathF.PI; while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI; - // 30° "walk-while-turning" threshold: outside this, body - // turns in place. Inside, body walks forward while finishing - // any remaining alignment. Matches retail-feel observation; - // exact retail value is in MoveToManager but ~30° is a - // sensible heuristic for now. + // Two alignment thresholds: + // walkWhileTurning (30°): outside this, body turns in place. + // Inside, body walks forward while + // finishing residual alignment. + // fullyAligned (5°): the arrival-fire alignment. ACE + // rotates server-side via Rotate(target) + // BEFORE invoking the Use callback — + // user reported 'it does not face it + // completely', so the final-alignment + // check must be tighter than the + // walking gate. const float WalkWhileTurningRad = 30f * MathF.PI / 180f; - aligned = MathF.Abs(delta) <= WalkWhileTurningRad; + const float FullyAlignedRad = 5f * MathF.PI / 180f; + walkAligned = MathF.Abs(delta) <= WalkWhileTurningRad; + aligned = MathF.Abs(delta) <= FullyAlignedRad; } // End the auto-walk once the body is BOTH within use radius @@ -528,11 +537,12 @@ public sealed class PlayerMovementController // all the way to the object and then stop"). bool shouldRun = _autoWalkInitiallyRunning; - // Turn-first gate: if not yet aligned with the target, suppress - // forward motion so the body turns in place rather than - // walking an arc. Also suppress when already within arrival — - // we just turned to face it; no need to step forward into it. - bool moveForward = aligned && !withinArrival; + // Turn-first gate: if not yet within the 30° walking band, + // suppress forward motion so the body turns in place rather + // than walking an arc. Also suppress when already within + // arrival — we just turned to face it; no need to step forward + // into it. + bool moveForward = walkAligned && !withinArrival; // Synthesize "moving forward" input. The rest of Update reads // Yaw + input.Forward + input.Run to drive _motion + body diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d2d960d..5c562da 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9121,8 +9121,25 @@ public sealed class GameWindow : IDisposable // overlay provides the matching local rotation. Either way the // alignment-gated arrival ensures the body finishes facing // the target before stopping. + // + // User feedback (2026-05-15): 'first is rotation, when you are + // facing, then using.' For close-range we DEFER the wire packet + // until our local overlay arrives (turn-then-fire). The + // _pendingPostArrivalAction handler will re-fire SendUse with + // isRetryAfterArrival=true after the body finishes turning. + // For far range we still send immediately so ACE can start + // its MoveToChain. if (!isRetryAfterArrival) + { InstallSpeculativeTurnToTarget(guid); + _pendingPostArrivalAction = (guid, false); + if (IsCloseRangeTarget(guid)) + { + if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled) + Console.WriteLine($"[B.4b] use deferred (close-range, turn-first) guid=0x{guid:X8}"); + return; // wait for AutoWalkArrived to fire the wire send + } + } var seq = _liveSession.NextGameActionSequence(); var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid); @@ -9148,11 +9165,21 @@ public sealed class GameWindow : IDisposable _debugVm?.AddToast("Not in world"); return; } - // B.6 (2026-05-15): same speculative turn-to-target install as - // SendUse — close-range pickup still rotates locally to face - // the item before the pickup resolves server-side. + // B.6 (2026-05-15): same speculative turn-to-target + deferral as + // SendUse — close-range pickup rotates locally to face the + // item first, then the wire packet fires when the local + // overlay reports arrival. if (!isRetryAfterArrival) + { InstallSpeculativeTurnToTarget(itemGuid); + _pendingPostArrivalAction = (itemGuid, true); + if (IsCloseRangeTarget(itemGuid)) + { + if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeAutoWalkEnabled) + Console.WriteLine($"[B.5] pickup deferred (close-range, turn-first) item=0x{itemGuid:X8}"); + return; + } + } // B.5 polish (2026-05-14): silently block client-side when the // selected entity is a creature/NPC. ACE's @@ -9235,6 +9262,40 @@ public sealed class GameWindow : IDisposable /// 0.6 m for everything else (ground items). /// /// + /// + /// B.6 (2026-05-15). True if the local player is already inside the + /// target's use radius right now — i.e. ACE will NOT auto-walk us + /// when we send the action. Used to gate the close-range deferral + /// in SendUse / SendPickUp: when close, we hold the wire packet + /// until our local rotation overlay reports alignment, then fire. + /// + private bool IsCloseRangeTarget(uint targetGuid) + { + if (_playerController is null) return false; + if (!_entitiesByServerGuid.TryGetValue(targetGuid, out var entity)) + return false; + + // Mirror InstallSpeculativeTurnToTarget's per-type radius heuristic. + float useRadius = 0.6f; + if (_liveEntityInfoByGuid.TryGetValue(targetGuid, out var info) + && (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0) + { + useRadius = 3.0f; + } + else if (_lastSpawnByGuid.TryGetValue(targetGuid, out var spawn) + && spawn.ObjectDescriptionFlags is { } odf) + { + const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u; + if ((odf & LargeFlatMask) != 0) useRadius = 2.0f; + } + + var bodyPos = _playerController.Position; + float dx = entity.Position.X - bodyPos.X; + float dy = entity.Position.Y - bodyPos.Y; + float distSq = dx * dx + dy * dy; + return distSq <= useRadius * useRadius; + } + private void InstallSpeculativeTurnToTarget(uint targetGuid) { if (_playerController is null) return;