poe2-bot/src/Roboto.Memory/Snapshots/Entity.cs
2026-03-04 16:49:23 -05:00

132 lines
3.9 KiB
C#

namespace Roboto.Memory;
public enum EntityType
{
Unknown,
Player,
Monster,
Npc,
Effect,
WorldItem,
MiscellaneousObject,
Terrain,
Critter,
Chest,
Shrine,
Portal,
TownPortal,
Waypoint,
AreaTransition,
Door,
Doodad,
}
/// <summary>
/// Raw entity data read from process memory. No business logic or classification —
/// type classification lives in EntityReader (Memory-internal) and EntityClassifier (Data layer).
/// </summary>
public class Entity
{
public nint Address { get; }
public uint Id { get; }
public string? Path { get; }
public string? Metadata { get; }
// Position (from Render component)
public bool HasPosition { get; internal set; }
public float X { get; internal set; }
public float Y { get; internal set; }
public float Z { get; internal set; }
// Vitals (from Life component — only populated when explicitly read)
public bool HasVitals { get; internal set; }
public int LifeCurrent { get; internal set; }
public int LifeTotal { get; internal set; }
public int ManaCurrent { get; internal set; }
public int ManaTotal { get; internal set; }
public int EsCurrent { get; internal set; }
public int EsTotal { get; internal set; }
// Component info
public int ComponentCount { get; internal set; }
public HashSet<string>? Components { get; internal set; }
// Component-based properties (populated by GameMemoryReader)
public bool IsTargetable { get; internal set; }
public bool IsOpened { get; internal set; }
public bool IsAvailable { get; internal set; }
public int Rarity { get; internal set; }
// Mods (from Mods component)
public List<string>? ModNames { get; internal set; }
// AreaTransition destination (raw area ID, e.g. "G1_4")
public string? TransitionName { get; internal set; }
public int TransitionState { get; internal set; } = -1;
// Action state (from Actor component)
public short ActionId { get; internal set; }
public bool IsAttacking { get; internal set; }
public bool IsMoving { get; internal set; }
// Classification (set by EntityReader)
public EntityType Type { get; internal set; }
// Derived properties
public bool IsAlive => HasVitals && LifeCurrent > 0;
public bool IsDead => HasVitals && LifeCurrent <= 0;
public bool HasComponent(string name) => Components?.Contains(name) == true;
/// <summary>
/// Grid-plane distance to another point (ignores Z).
/// </summary>
public float DistanceTo(float px, float py)
{
var dx = X - px;
var dy = Y - py;
return MathF.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Grid-plane distance to another entity.
/// </summary>
public float DistanceTo(Entity other) => DistanceTo(other.X, other.Y);
/// <summary>
/// Short category string derived from path (e.g. "Monsters", "Effects", "NPC").
/// </summary>
public string PathCategory
{
get
{
if (Path is null) return "?";
var parts = Path.Split('/');
return parts.Length >= 2 ? parts[1] : "?";
}
}
internal Entity(nint address, uint id, string? path)
{
Address = address;
Id = id;
Path = path;
Metadata = ExtractMetadata(path);
}
/// <summary>
/// Strips the "@N" instance suffix from the path.
/// "Metadata/Monsters/Wolves/RottenWolf1_@2" → "Metadata/Monsters/Wolves/RottenWolf1_"
/// </summary>
private static string? ExtractMetadata(string? path)
{
if (path is null) return null;
var atIndex = path.LastIndexOf('@');
return atIndex > 0 ? path[..atIndex] : path;
}
public override string ToString()
{
var pos = HasPosition ? $"({X:F0},{Y:F0})" : "no pos";
return $"[{Id}] {Type} {Path ?? "?"} {pos}";
}
}