Combatant.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. using UnityEngine;
  2. namespace MazeGen
  3. {
  4. /// <summary>
  5. /// One participant in a battle - either an agent or a monster instance - behind a single
  6. /// interface so <see cref="CombatResolver"/> doesn't need to branch on which it is.
  7. ///
  8. /// Wraps the *live* <see cref="CharacterSheet"/>, not a copy: resolving a fight mutates real
  9. /// agent and monster health, which is exactly what "only the result of the fight has changed
  10. /// for that room" means. <see cref="StartHealth"/> is snapshotted before resolution purely so
  11. /// the arena playback can animate health draining down from where it actually started.
  12. /// </summary>
  13. public class Combatant
  14. {
  15. public const int TeamAgents = 0;
  16. public const int TeamMonsters = 1;
  17. /// <summary>Index of this combatant within its own battle. Combat events refer to these.</summary>
  18. public readonly int Id;
  19. public readonly int Team;
  20. public readonly string Name;
  21. public readonly Color Color;
  22. /// <summary>Set for agent combatants, null for monsters.</summary>
  23. public readonly MazeAgent Agent;
  24. /// <summary>Set for monster combatants, null for agents.</summary>
  25. public readonly MonsterEncounter.MonsterInstance Monster;
  26. public readonly CharacterSheet Sheet;
  27. /// <summary>Health at the moment the battle was resolved, for playback purposes.</summary>
  28. public readonly float StartHealth;
  29. /// <summary>Grid position during combat. Set by CombatGrid during Initialize.</summary>
  30. private Vector2Int _gridPosition = Vector2Int.zero;
  31. /// <summary>Size category for grid footprint (determines how many squares this combatant occupies).</summary>
  32. private MonsterSize _size = MonsterSize.Medium;
  33. public bool IsAgent => Agent != null;
  34. public bool IsAlive => Sheet.Stats.IsAlive;
  35. /// <summary>
  36. /// Base max health, not GetEffectiveMaxHealth(): CharacterStats.CurrentHealth is clamped
  37. /// to the base value, so using the effective one here would give health bars that can
  38. /// never fill up whenever equipment grants a health bonus.
  39. /// </summary>
  40. public float MaxHealth => Mathf.Max(1f, Sheet.Stats.MaxHealth);
  41. /// <summary>Current position on the combat grid.</summary>
  42. public Vector2Int GridPosition
  43. {
  44. get => _gridPosition;
  45. set => _gridPosition = value;
  46. }
  47. /// <summary>Size category of this combatant (affects grid footprint).</summary>
  48. public MonsterSize Size
  49. {
  50. get => _size;
  51. set => _size = value;
  52. }
  53. /// <summary>
  54. /// Grid position at the moment combatants were first placed on the grid, before any
  55. /// movement happened. CombatResolver.Resolve() runs the whole fight synchronously and
  56. /// mutates GridPosition throughout, so by the time playback starts, GridPosition already
  57. /// holds the *final* position - this snapshot is what BattleArenaView uses to place the
  58. /// combatant's sprite where it actually started, exactly like StartHealth does for health.
  59. /// </summary>
  60. public Vector2Int StartGridPosition { get; set; } = Vector2Int.zero;
  61. private Combatant(int id, int team, string name, Color color, MazeAgent agent, MonsterEncounter.MonsterInstance monster, CharacterSheet sheet)
  62. {
  63. Id = id;
  64. Team = team;
  65. Name = name;
  66. Color = color;
  67. Agent = agent;
  68. Monster = monster;
  69. Sheet = sheet;
  70. StartHealth = sheet.Stats.CurrentHealth;
  71. }
  72. public static Combatant ForAgent(int id, MazeAgent agent, Color color)
  73. => new Combatant(id, TeamAgents, agent.CharacterSheet.Name, color, agent, null, agent.CharacterSheet);
  74. public static Combatant ForMonster(int id, MonsterEncounter.MonsterInstance monster)
  75. {
  76. var combatant = new Combatant(id, TeamMonsters, monster.Template.MonsterName, monster.Template.Color, null, monster, monster.CharacterSheet);
  77. // Set size from monster template
  78. combatant.Size = monster.Template.Size;
  79. return combatant;
  80. }
  81. /// <summary>
  82. /// Calculate grid distance to another combatant using Manhattan distance.
  83. /// This represents how many squares apart they are.
  84. /// </summary>
  85. public int DistanceTo(Combatant other)
  86. {
  87. return Mathf.Abs(GridPosition.x - other.GridPosition.x) +
  88. Mathf.Abs(GridPosition.y - other.GridPosition.y);
  89. }
  90. /// <summary>
  91. /// Get the footprint size (in grid squares) for this combatant based on its Size category.
  92. /// </summary>
  93. public Vector2Int GetFootprintSize()
  94. {
  95. return Size switch
  96. {
  97. MonsterSize.Small => Vector2Int.one, // Small: 1x1, but multiple fit per square
  98. MonsterSize.Medium => Vector2Int.one, // Medium: 1x1
  99. MonsterSize.Large => new Vector2Int(4, 4), // Large: 4x4
  100. _ => Vector2Int.one
  101. };
  102. }
  103. /// <summary>
  104. /// Get the maximum number of this-sized creatures that can occupy a single grid square.
  105. /// </summary>
  106. public int GetCreaturesPerSquare()
  107. {
  108. return Size switch
  109. {
  110. MonsterSize.Small => 4, // Small: 4 per square
  111. MonsterSize.Medium => 1, // Medium: 1 per square
  112. MonsterSize.Large => 0, // Large: occupies multiple squares, not stackable
  113. _ => 1
  114. };
  115. }
  116. /// <summary>
  117. /// Check if this combatant is in melee range (adjacent) to a target.
  118. /// </summary>
  119. public bool IsAdjacentTo(Combatant target)
  120. {
  121. int distance = DistanceTo(target);
  122. return distance == 1;
  123. }
  124. /// <summary>
  125. /// Check if this combatant can reach a target with the given weapon.
  126. /// Returns true if target is within the weapon's range.
  127. /// </summary>
  128. public bool CanReachTarget(Combatant target, Weapon weapon)
  129. {
  130. if (weapon == null) return false;
  131. int distance = DistanceTo(target);
  132. // For melee weapons, must be adjacent
  133. if (weapon.WeaponType == WeaponType.Melee)
  134. {
  135. return IsAdjacentTo(target);
  136. }
  137. // For ranged/magic weapons, check against range attribute
  138. return distance <= weapon.Range;
  139. }
  140. }
  141. }