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