acdream/src/AcDream.App/Combat/CombatAttackTargetSource.cs
Erik 420e5eea70 refactor(app): key live projections by runtime identity
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.
2026-07-25 21:50:58 +02:00

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);
}
}