namespace Roboto.Memory; public enum EntityType { Unknown, Player, Monster, Npc, Effect, WorldItem, MiscellaneousObject, Terrain, Critter, Chest, Shrine, Portal, TownPortal, Waypoint, AreaTransition, Door, Doodad, } /// /// Raw entity data read from process memory. No business logic or classification — /// type classification lives in EntityReader (Memory-internal) and EntityClassifier (Data layer). /// 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? 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? 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; /// /// Grid-plane distance to another point (ignores Z). /// public float DistanceTo(float px, float py) { var dx = X - px; var dy = Y - py; return MathF.Sqrt(dx * dx + dy * dy); } /// /// Grid-plane distance to another entity. /// public float DistanceTo(Entity other) => DistanceTo(other.X, other.Y); /// /// Short category string derived from path (e.g. "Monsters", "Effects", "NPC"). /// 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); } /// /// Strips the "@N" instance suffix from the path. /// "Metadata/Monsters/Wolves/RottenWolf1_@2" → "Metadata/Monsters/Wolves/RottenWolf1_" /// 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}"; } }