using UnityEngine; namespace MazeGen { /// /// One participant in a battle - either an agent or a monster instance - behind a single /// interface so doesn't need to branch on which it is. /// /// Wraps the *live* , not a copy: resolving a fight mutates real /// agent and monster health, which is exactly what "only the result of the fight has changed /// for that room" means. is snapshotted before resolution purely so /// the arena playback can animate health draining down from where it actually started. /// public class Combatant { public const int TeamAgents = 0; public const int TeamMonsters = 1; /// Index of this combatant within its own battle. Combat events refer to these. public readonly int Id; public readonly int Team; public readonly string Name; public readonly Color Color; /// Set for agent combatants, null for monsters. public readonly MazeAgent Agent; /// Set for monster combatants, null for agents. public readonly MonsterEncounter.MonsterInstance Monster; public readonly CharacterSheet Sheet; /// Health at the moment the battle was resolved, for playback purposes. public readonly float StartHealth; /// Grid position during combat. Set by CombatGrid during Initialize. private Vector2Int _gridPosition = Vector2Int.zero; /// Size category for grid footprint (determines how many squares this combatant occupies). private MonsterSize _size = MonsterSize.Medium; public bool IsAgent => Agent != null; public bool IsAlive => Sheet.Stats.IsAlive; /// /// Base max health, not GetEffectiveMaxHealth(): CharacterStats.CurrentHealth is clamped /// to the base value, so using the effective one here would give health bars that can /// never fill up whenever equipment grants a health bonus. /// public float MaxHealth => Mathf.Max(1f, Sheet.Stats.MaxHealth); /// Current position on the combat grid. public Vector2Int GridPosition { get => _gridPosition; set => _gridPosition = value; } /// Size category of this combatant (affects grid footprint). public MonsterSize Size { get => _size; set => _size = value; } /// /// Grid position at the moment combatants were first placed on the grid, before any /// movement happened. CombatResolver.Resolve() runs the whole fight synchronously and /// mutates GridPosition throughout, so by the time playback starts, GridPosition already /// holds the *final* position - this snapshot is what BattleArenaView uses to place the /// combatant's sprite where it actually started, exactly like StartHealth does for health. /// public Vector2Int StartGridPosition { get; set; } = Vector2Int.zero; private Combatant(int id, int team, string name, Color color, MazeAgent agent, MonsterEncounter.MonsterInstance monster, CharacterSheet sheet) { Id = id; Team = team; Name = name; Color = color; Agent = agent; Monster = monster; Sheet = sheet; StartHealth = sheet.Stats.CurrentHealth; } public static Combatant ForAgent(int id, MazeAgent agent, Color color) => new Combatant(id, TeamAgents, agent.CharacterSheet.Name, color, agent, null, agent.CharacterSheet); public static Combatant ForMonster(int id, MonsterEncounter.MonsterInstance monster) { var combatant = new Combatant(id, TeamMonsters, monster.Template.MonsterName, monster.Template.Color, null, monster, monster.CharacterSheet); // Set size from monster template combatant.Size = monster.Template.Size; return combatant; } /// /// Calculate grid distance to another combatant using Manhattan distance. /// This represents how many squares apart they are. /// public int DistanceTo(Combatant other) { return Mathf.Abs(GridPosition.x - other.GridPosition.x) + Mathf.Abs(GridPosition.y - other.GridPosition.y); } /// /// Get the footprint size (in grid squares) for this combatant based on its Size category. /// public Vector2Int GetFootprintSize() { return Size switch { MonsterSize.Small => Vector2Int.one, // Small: 1x1, but multiple fit per square MonsterSize.Medium => Vector2Int.one, // Medium: 1x1 MonsterSize.Large => new Vector2Int(4, 4), // Large: 4x4 _ => Vector2Int.one }; } /// /// Get the maximum number of this-sized creatures that can occupy a single grid square. /// public int GetCreaturesPerSquare() { return Size switch { MonsterSize.Small => 4, // Small: 4 per square MonsterSize.Medium => 1, // Medium: 1 per square MonsterSize.Large => 0, // Large: occupies multiple squares, not stackable _ => 1 }; } /// /// Check if this combatant is in melee range (adjacent) to a target. /// public bool IsAdjacentTo(Combatant target) { int distance = DistanceTo(target); return distance == 1; } /// /// Check if this combatant can reach a target with the given weapon. /// Returns true if target is within the weapon's range. /// public bool CanReachTarget(Combatant target, Weapon weapon) { if (weapon == null) return false; int distance = DistanceTo(target); // For melee weapons, must be adjacent if (weapon.WeaponType == WeaponType.Melee) { return IsAdjacentTo(target); } // For ranged/magic weapons, check against range attribute return distance <= weapon.Range; } } }