diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index 63b92ac..55bb81f 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -234,12 +234,13 @@ public sealed class PlayerMovementController private float _autoWalkMinDistance; private float _autoWalkDistanceToObject; private bool _autoWalkMoveTowards; - // Wire's WalkRunThreshold — retail semantic: locomotion runs while - // remaining distance > threshold, walks once inside threshold. ACE's - // Use/PickUp path uses MoveToParameters.SetDefaults() = 15.0f, so - // most pickup targets walk the entire way. ACE's charge path sets - // it to 1.0f so combat chase runs almost the whole way. - private float _autoWalkWalkRunThreshold; + // Walk-vs-run decision is made ONCE at BeginServerAutoWalk based on + // initial distance vs the wire's WalkRunThreshold, then held for the + // duration of the auto-walk. Earlier per-frame evaluation produced + // "runs partway then walks the rest" which the user reported as + // wrong: the character should run all the way to the target then + // stop, not transition to a walk near the end. + private bool _autoWalkInitiallyRunning; /// /// True while a server-initiated auto-walk (MoveToObject inbound) is @@ -357,7 +358,18 @@ public sealed class PlayerMovementController _autoWalkMinDistance = minDistance; _autoWalkDistanceToObject = distanceToObject; _autoWalkMoveTowards = moveTowards; - _autoWalkWalkRunThreshold = walkRunThreshold; + + // Decide run vs walk ONCE based on the initial horizontal + // distance from the player to the destination. Run-all-the-way + // is the retail-faithful behaviour the user observed: pick a + // distant target → character runs the whole way, decelerates + // to a stop at use radius. Earlier per-frame evaluation made + // the body transition to a walk inside threshold and felt + // wrong (the user reported "runs partway then walks"). + float dx = destinationWorld.X - _body.Position.X; + float dy = destinationWorld.Y - _body.Position.Y; + float initialDist = MathF.Sqrt(dx * dx + dy * dy); + _autoWalkInitiallyRunning = initialDist > walkRunThreshold; } /// @@ -452,15 +464,13 @@ public sealed class PlayerMovementController while (Yaw < -MathF.PI) Yaw += 2f * MathF.PI; } - // Walk vs run per the wire's WalkRunThreshold. Retail semantics: - // dist > threshold → RUN, dist ≤ threshold → WALK. ACE's default - // for Use/PickUp is 15.0 m (so close targets walk the whole way); - // ACE's combat charge path sets it to 1.0 m (so chase runs to the - // last metre then walks the final approach). Matches the user's - // observed retail behaviour: "When at a distance X it should - // start running towards the double clicked target… When at a - // shorter distance it should walk to it." - bool shouldRun = dist > _autoWalkWalkRunThreshold; + // Walk vs run decided ONCE at BeginServerAutoWalk based on + // initial distance — held for the rest of the auto-walk so the + // character keeps running all the way to the target instead of + // transitioning to a walk inside the threshold. Matches user- + // observed retail behaviour ("if its far away it should run + // all the way to the object and then stop"). + bool shouldRun = _autoWalkInitiallyRunning; // 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 60a3dd5..b3bc3a9 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9064,10 +9064,36 @@ public sealed class GameWindow : IDisposable private void UseCurrentSelection() { - if (_selectedGuid is uint sel) - SendUse(sel); - else + if (_selectedGuid is not uint sel) + { _debugVm?.AddToast("Nothing selected"); + return; + } + + // B.7 (2026-05-15): the user requested R behave as a universal + // interact key — pickup for items, use for NPCs / doors / + // lifestones / portals / corpses. Matches retail's "use" + // behaviour where the action picked depends on the target's + // type rather than forcing the player to remember a different + // hotkey per target type. + bool isPickupableItem = true; + if (_liveEntityInfoByGuid.TryGetValue(sel, out var info) + && (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0) + { + // NPCs / monsters / players are Use targets, never PickUp. + isPickupableItem = false; + } + if (_lastSpawnByGuid.TryGetValue(sel, out var spawn) + && spawn.ObjectDescriptionFlags is { } odf) + { + // BF_DOOR | BF_LIFESTONE | BF_PORTAL | BF_CORPSE → Use, not PickUp. + // (acclient.h:6431-6463) + const uint NonPickupMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u; + if ((odf & NonPickupMask) != 0) isPickupableItem = false; + } + + if (isPickupableItem) SendPickUp(sel); + else SendUse(sel); } private void SendUse(uint guid) diff --git a/src/AcDream.App/UI/TargetIndicatorPanel.cs b/src/AcDream.App/UI/TargetIndicatorPanel.cs index 3b63148..21d86ae 100644 --- a/src/AcDream.App/UI/TargetIndicatorPanel.cs +++ b/src/AcDream.App/UI/TargetIndicatorPanel.cs @@ -54,15 +54,36 @@ public sealed class TargetIndicatorPanel public float TriangleSize { get; set; } = 10f; /// - /// World-space height of the indicator box. The panel projects feet - /// (at WorldPosition) and head (at WorldPosition.Z + - /// EntityHeight) to screen space and uses the projected pixel - /// distance as the box height. 1.8 m matches a standing humanoid; + /// World-space height of the indicator box for entities that don't + /// have a more specific type tag. Items use a smaller value (see + /// ). 1.8 m matches a standing humanoid; /// short items still get a small box because the projection /// preserves apparent size. /// public float EntityHeight { get; set; } = 1.8f; + /// + /// Resolve the world-space height to use for a given entity's + /// indicator box. Humanoids (Creature flag) use 1.8 m; doors / + /// lifestones / portals use 2.4 m (door-frame tall); ground items + /// use 0.5 m so the box hugs the item rather than ballooning out + /// to humanoid height. Falls back to + /// for entities without a recognisable type tag. + /// + public float EntityHeightFor(uint itemType, uint pwdBitfield) + { + bool isCreature = (itemType & (uint)AcDream.Core.Items.ItemType.Creature) != 0; + if (isCreature) return 1.8f; + + // BF_DOOR = 0x1000, BF_LIFESTONE = 0x4000, BF_PORTAL = 0x40000 + // (acclient.h:6431-6463) + const uint TallStructureMask = 0x1000u | 0x4000u | 0x40000u; + if ((pwdBitfield & TallStructureMask) != 0) return 2.4f; + + // Default: small ground item / object. + return 0.5f; + } + /// /// Box width = projected height × /// . Retail's Vivid Target Indicator @@ -111,10 +132,11 @@ public sealed class TargetIndicatorPanel // projection. if (!TryProjectToScreen(info.WorldPosition, viewProj, viewport, out var feetScreen)) return; + float entityHeight = EntityHeightFor(info.ItemType, info.ObjectDescriptionFlags); var headWorld = new Vector3( info.WorldPosition.X, info.WorldPosition.Y, - info.WorldPosition.Z + EntityHeight); + info.WorldPosition.Z + entityHeight); if (!TryProjectToScreen(headWorld, viewProj, viewport, out var headScreen)) return;