fix(combat): persist death and retarget on kill

Keep the velocity-only NPC adaptation inside the locomotion family so authoritative Dead motion remains persistent. Route selection clears through a dedicated combat target controller that reacquires the closest eligible creature when retail Auto Target conditions apply.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-12 09:19:38 +02:00
parent 5276a83087
commit 9b97102c67
10 changed files with 281 additions and 19 deletions

View file

@ -761,6 +761,7 @@ public sealed class GameWindow : IDisposable
private AcDream.App.UI.UiHost? _uiHost;
private AcDream.App.UI.RetailUiRuntime? _retailUiRuntime;
private AcDream.App.Combat.CombatAttackController? _combatAttackController;
private AcDream.App.Combat.CombatTargetController? _combatTargetController;
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
private readonly AcDream.Core.Items.StackSplitQuantityState _stackSplitQuantity = new();
// Phase D.2b Sub-phase C Slice 2 — the 3-D doll viewport: an off-screen RTT renderer, the UiViewport
@ -1991,6 +1992,11 @@ public sealed class GameWindow : IDisposable
== AcDream.Core.Combat.CombatInputPlanner.DualWieldCombatStyle,
playerReadyForAttack: IsPlayerReadyForCombatAttack,
autoRepeatAttack: () => _persistedGameplay.AutoRepeatAttack);
_combatTargetController = new AcDream.App.Combat.CombatTargetController(
Combat,
_selection,
autoTarget: () => _persistedGameplay.AutoTarget,
selectClosestTarget: () => SelectClosestCombatTarget(showToast: false));
// Phase D.2b retail-look retained UI (ACDREAM_RETAIL_UI=1).
if (_options.RetailUi)
@ -5125,6 +5131,12 @@ public sealed class GameWindow : IDisposable
}
}
// Authoritative Dead motion invalidates a selected combat target.
// The controller clears shared selection, whose SelectionChanged
// consumer ports retail's post-clear AutoTarget behavior.
_combatTargetController?.OnMotionApplied(
update.Guid, ae.Sequencer.CurrentMotion);
// CRITICAL: when we enter a locomotion cycle (Walk/Run/etc),
// stamp the _remoteLastMove timestamp to "now". Without this,
// the stop-detection loop in TickAnimations sees the previous
@ -5286,12 +5298,6 @@ public sealed class GameWindow : IDisposable
$"[setstate] guid=0x{parsed.Guid:X8} entityId=0x{registryKey:X8} state=0x{parsed.PhysicsState:X8} instSeq={parsed.InstanceSequence} stateSeq={parsed.StateSequence}"));
}
private static bool IsRemoteLocomotion(uint motion)
{
uint low = motion & 0xFFu;
return low is 0x05 or 0x06 or 0x07 or 0x0F or 0x10;
}
private void ApplyServerControlledVelocityCycle(
uint serverGuid,
AnimatedEntity ae,
@ -5328,11 +5334,16 @@ public sealed class GameWindow : IDisposable
return;
}
uint currentMotion = ae.Sequencer.CurrentMotion;
// AP-80 is a position-only compatibility adaptation, not an
// animation authority. Restrict it to Ready/Walk/Run so late death
// position deltas cannot replace the authoritative Dead substate.
if (!AcDream.Core.Physics.ServerControlledLocomotion
.CanApplyVelocityCycle(currentMotion))
return;
var plan = AcDream.Core.Physics.ServerControlledLocomotion
.PlanFromVelocity(velocity);
uint currentMotion = ae.Sequencer.CurrentMotion;
if (!plan.IsMoving && !IsRemoteLocomotion(currentMotion))
return;
uint style = ae.Sequencer.CurrentStyle != 0
? ae.Sequencer.CurrentStyle
@ -9949,11 +9960,10 @@ public sealed class GameWindow : IDisposable
// stop signal flows the same way as any other motion change:
// alt releases W → MoveToState(Ready) → ACE broadcasts
// UpdateMotion(Ready) + UpdatePosition with zero velocity.
// On our side, both are handled: UpdateMotion routes through
// MotionInterpreter.DoInterpretedMotion which zeroes the
// interpreter's state, and UpdatePosition's HasVelocity~0 hits
// MotionInterpreter.StopCompletely. Anything else is server
// buggery (packet loss, ACE bug) — don't guess client-side.
// On our side, only UpdateMotion changes interpreted state;
// UpdatePosition updates physics pose/velocity and never selects an
// animation. Anything else is server buggery (packet loss, ACE bug)
// — don't guess client-side.
var now = System.DateTime.UtcNow;
foreach (var kv in _animatedEntities)
@ -12575,6 +12585,11 @@ public sealed class GameWindow : IDisposable
if (!_entitiesByServerGuid.ContainsKey(guid))
return false;
if (_entitiesByServerGuid.TryGetValue(guid, out var entity)
&& _animatedEntities.TryGetValue(entity.Id, out var animated)
&& animated.Sequencer?.CurrentMotion == AcDream.Core.Physics.MotionCommand.Dead)
return false;
return (LiveItemType(guid) & AcDream.Core.Items.ItemType.Creature) != 0;
}
@ -13616,6 +13631,8 @@ public sealed class GameWindow : IDisposable
// UI resource is still alive. UiHost disposes controllers before its renderer.
_retailUiRuntime?.Dispose();
_retailUiRuntime = null;
_combatTargetController?.Dispose();
_combatTargetController = null;
_combatAttackController?.Dispose();
_combatAttackController = null;
_uiHost = null;