using System; using System.Collections.Generic; using UnityEngine; namespace Heroes.Battle { /// Display/behaviour state of a stack, driven as a finite state machine. public enum UnitStackState { /// Waiting for its turn. Idle, /// It is this stack's turn: the player is choosing an action. Active, /// Gliding along towards a hex. Moving, /// Playing an attack against . Attacking, /// Stack was wiped out; no longer participates. Dead, } /// /// Runtime state of one deployed stack of units, modelled as a small finite /// state machine so the view can react to state transitions instead of being /// driven imperatively. Pure C#: holds grid position, live health and current /// action payloads (move path, attack target); the controller advances the /// machine and the view animates each state. /// /// State flow for a turn: /// Idle --Activate--> Active /// Active --StartMove--> Moving --CompleteMove--> Active /// Active --StartAttack--> Attacking --CompleteAttack--> Active /// Active --Deactivate--> Idle /// (any) --Kill--> Dead /// public sealed class UnitStack { /// Raised after changes. Argument is this stack. public event Action StateChanged; /// Raised after the stack's health/count changes (e.g. took damage). public event Action HealthChanged; private readonly List _movePath = new List(); public UnitStack(UnitDefinition definition, int count, BattleSide side, int slotIndex, int x, int y) { Definition = definition; Side = side; SlotIndex = slotIndex; X = x; Y = y; MaxUnitHealth = definition != null ? Mathf.Max(1, definition.health) : 1; TotalHealth = Mathf.Max(0, count) * MaxUnitHealth; State = UnitStackState.Idle; } public UnitDefinition Definition { get; } public BattleSide Side { get; } /// Index of the army slot (0..6) this stack came from. public int SlotIndex { get; } /// Anchor column. Wide units extend towards the battlefield center. public int X { get; private set; } /// Row on the battlefield grid. public int Y { get; private set; } public UnitStackState State { get; private set; } /// Health of a single unit in the stack. public int MaxUnitHealth { get; } /// Current total health of the whole stack. public int TotalHealth { get; private set; } /// Live unit count, derived from total health (a partially wounded top unit still counts). public int Count => TotalHealth <= 0 ? 0 : Mathf.CeilToInt((float)TotalHealth / MaxUnitHealth); public bool IsAlive => State != UnitStackState.Dead && TotalHealth > 0; /// Movement range in hexes (equal to the unit's speed). public int Speed => Definition != null ? Mathf.Max(1, Definition.speed) : 1; public int Attack => Definition != null ? Mathf.Max(0, Definition.attack) : 0; public int Defense => Definition != null ? Mathf.Max(0, Definition.defense) : 0; public int HexWidth => Definition != null && Definition.hexWidth > 1 ? 2 : 1; /// Hex anchors to walk through during the current state. public IReadOnlyList MovePath => _movePath; /// Target of the current state. public UnitStack AttackTarget { get; private set; } /// /// Columns occupied on row . The anchor sits at the army /// edge; the second hex of a wide unit extends towards the center. /// public IEnumerable OccupiedColumns() => ColumnsFor(Side, HexWidth, X); /// Columns a stack of the given side/width would occupy at an arbitrary anchor. public static IEnumerable ColumnsFor(BattleSide side, int width, int anchorX) { int w = Mathf.Max(1, width); for (int i = 0; i < w; i++) { yield return side == BattleSide.Attacker ? anchorX + i : anchorX - i; } } // --- State transitions ------------------------------------------------- public void Activate() { if (State == UnitStackState.Dead) return; SetState(UnitStackState.Active); } public void Deactivate() { if (State == UnitStackState.Dead) return; SetState(UnitStackState.Idle); } /// Enters the Moving state with the given path of hex anchors (excluding the start hex). public void StartMove(IReadOnlyList path) { if (State == UnitStackState.Dead) return; _movePath.Clear(); if (path != null) _movePath.AddRange(path); SetState(UnitStackState.Moving); } /// Called by the view once the move animation finishes; commits the final position. public void CompleteMove() { if (_movePath.Count > 0) { var end = _movePath[_movePath.Count - 1]; X = end.x; Y = end.y; } _movePath.Clear(); if (State == UnitStackState.Dead) return; SetState(UnitStackState.Active); } public void StartAttack(UnitStack target) { if (State == UnitStackState.Dead) return; AttackTarget = target; SetState(UnitStackState.Attacking); } /// Called by the view once the attack animation finishes. public void CompleteAttack() { AttackTarget = null; if (State == UnitStackState.Dead) return; SetState(UnitStackState.Active); } /// Applies damage to the stack's total health; kills it if it drops to zero. public void ApplyDamage(int amount) { if (amount <= 0 || State == UnitStackState.Dead) return; TotalHealth = Mathf.Max(0, TotalHealth - amount); HealthChanged?.Invoke(this); if (TotalHealth <= 0) SetState(UnitStackState.Dead); } private void SetState(UnitStackState next) { if (State == next) return; State = next; StateChanged?.Invoke(this); } } }