Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
118 lines
3.9 KiB
C#
118 lines
3.9 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Combat;
|
|
|
|
/// <summary>
|
|
/// Focused automatic-attack target owner. It shares canonical selection and
|
|
/// live-entity state without retaining the broader interaction controller.
|
|
/// </summary>
|
|
internal sealed class CombatAttackTargetSource : ICombatAttackTargetSource
|
|
{
|
|
private readonly SelectionState _selection;
|
|
private readonly LiveEntityRuntime _liveEntities;
|
|
private readonly ClientObjectTable _objects;
|
|
private readonly ILocalPlayerIdentitySource _player;
|
|
|
|
public CombatAttackTargetSource(
|
|
SelectionState selection,
|
|
LiveEntityRuntime liveEntities,
|
|
ClientObjectTable objects,
|
|
ILocalPlayerIdentitySource player)
|
|
{
|
|
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
|
_liveEntities = liveEntities
|
|
?? throw new ArgumentNullException(nameof(liveEntities));
|
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
|
_player = player ?? throw new ArgumentNullException(nameof(player));
|
|
}
|
|
|
|
public uint? SelectedObjectId => _selection.SelectedObjectId;
|
|
|
|
public uint? GetSelectedOrClosestCombatTarget(bool autoTarget)
|
|
{
|
|
if (_selection.SelectedObjectId is { } selected
|
|
&& IsHostileMonster(selected))
|
|
{
|
|
return selected;
|
|
}
|
|
|
|
if (!autoTarget)
|
|
return null;
|
|
|
|
(uint Guid, float DistanceSquared)? closest = FindClosestHostileMonster();
|
|
if (closest is not { } best)
|
|
{
|
|
_selection.Clear(SelectionChangeSource.Keyboard);
|
|
return null;
|
|
}
|
|
|
|
_selection.Select(best.Guid, SelectionChangeSource.Keyboard);
|
|
string? name = _objects.Get(best.Guid)?.Name;
|
|
string label = string.IsNullOrWhiteSpace(name)
|
|
? $"0x{best.Guid:X8}"
|
|
: name;
|
|
Console.WriteLine(
|
|
$"combat: selected target 0x{best.Guid:X8} {label} dist={MathF.Sqrt(best.DistanceSquared):F1}");
|
|
return best.Guid;
|
|
}
|
|
|
|
private (uint Guid, float DistanceSquared)? FindClosestHostileMonster()
|
|
{
|
|
if (!_liveEntities.TryGetWorldEntity(
|
|
_player.ServerGuid,
|
|
out WorldEntity playerEntity))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
(uint Guid, float DistanceSquared)? best = null;
|
|
foreach (LiveEntityRecord record in _liveEntities.VisibleRecords)
|
|
{
|
|
uint guid = record.ServerGuid;
|
|
WorldEntity entity = record.WorldEntity!;
|
|
if (!IsHostileMonster(guid))
|
|
continue;
|
|
|
|
float distanceSquared = Vector3.DistanceSquared(
|
|
entity.Position,
|
|
playerEntity.Position);
|
|
if (best is null || distanceSquared < best.Value.DistanceSquared)
|
|
best = (guid, distanceSquared);
|
|
}
|
|
return best;
|
|
}
|
|
|
|
private bool IsHostileMonster(uint serverGuid)
|
|
{
|
|
uint playerGuid = _player.ServerGuid;
|
|
if (serverGuid == playerGuid
|
|
|| !_liveEntities.TryGetInteractionEligibleRecord(
|
|
serverGuid,
|
|
out LiveEntityRecord record)
|
|
|| record.WorldEntity is not { } entity)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_liveEntities.TryGetAnimationRuntime(entity.Id, out var animation)
|
|
&& animation.CurrentMotion == MotionCommand.Dead)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ClientObject? candidate = _objects.Get(serverGuid);
|
|
return candidate is not null
|
|
&& (candidate.Type & ItemType.Creature) != 0
|
|
&& CombatTargetPolicy.IsHostileMonster(
|
|
playerGuid,
|
|
_objects.Get(playerGuid),
|
|
candidate);
|
|
}
|
|
}
|