diff --git a/Assets/Scripts/UnitBrains/BaseUnitBrain.cs b/Assets/Scripts/UnitBrains/BaseUnitBrain.cs index 513532dd0..03fb981f6 100644 --- a/Assets/Scripts/UnitBrains/BaseUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/BaseUnitBrain.cs @@ -12,156 +12,273 @@ namespace UnitBrains { public abstract class BaseUnitBrain { - public virtual string TargetUnitName => string.Empty; - public virtual bool IsPlayerUnitBrain => true; - public virtual BaseUnitPath ActivePath => _activePath; - - protected Unit unit { get; private set; } - protected IReadOnlyRuntimeModel runtimeModel => ServiceLocator.Get(); - private BaseUnitPath _activePath = null; - + public virtual string TargetUnitName => string.Empty; // Имя целевого юнита для идентификации + public virtual bool IsPlayerUnitBrain => true; // Флаг, указывающий принадлежность к игроку + + // Свойство ActivePath с публичным геттером и защищенным сеттером + // Позволяет наследникам устанавливать активный путь + public virtual BaseUnitPath ActivePath + { + get => _activePath; // Публичный геттер для чтения пути + protected set => _activePath = value; // Защищенный сеттер для установки из наследников + } + + protected Unit unit { get; private set; } // Ссылка на управляемый юнит + protected IReadOnlyRuntimeModel runtimeModel => ServiceLocator.Get(); // Доступ к модели игры + private BaseUnitPath _activePath = null; // Приватное поле для хранения активного пути + + // Массив смещений для визуального разброса снарядов при атаке private readonly Vector2[] _projectileShifts = new Vector2[] { - new (0f, 0f), - new (0.15f, 0f), - new (0f, 0.15f), - new (0.15f, 0.15f), - new (0.15f, -0.15f), - new (-0.15f, 0.15f), - new (-0.15f, -0.15f), + new (0f, 0f), // Центральный снаряд + new (0.15f, 0f), // Смещение вправо + new (0f, 0.15f), // Смещение вверх + new (0.15f, 0.15f), // Смещение вправо-вверх + new (0.15f, -0.15f), // Смещение вправо-вниз + new (-0.15f, 0.15f), // Смещение влево-вверх + new (-0.15f, -0.15f), // Смещение влево-вниз }; + // Виртуальный метод получения следующего шага движения public virtual Vector2Int GetNextStep() { + // Если есть цели в зоне атаки, стоим на месте и атакуем if (HasTargetsInRange()) - return unit.Pos; + return unit.Pos; // Возвращаем текущую позицию (не двигаемся) + // Получаем позицию вражеской базы как цель по умолчанию var target = runtimeModel.RoMap.Bases[ IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]; - _activePath = new DummyUnitPath(runtimeModel, unit.Pos, target); - return _activePath.GetNextStepFrom(unit.Pos); + // Создаем A* путь от текущей позиции до базы врага + _activePath = new AStarUnitPath(runtimeModel, unit.Pos, target); + + // Получаем следующий шаг по вычисленному пути + Vector2Int nextStep = _activePath.GetNextStepFrom(unit.Pos); + + // Проверяем валидность шага (не больше 1 клетки в любом направлении) + Vector2Int delta = nextStep - unit.Pos; + + // Если шаг невалидный (слишком большой), используем прямое движение к цели + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + Debug.LogWarning($"A* returned invalid step {nextStep} from {unit.Pos}, using fallback movement"); + nextStep = unit.Pos.CalcNextStepTowards(target); // Вычисляем прямой шаг к цели + } + + // Дополнительная проверка: можно ли пройти в выбранную клетку + if (!IsCellWalkable(nextStep)) + { + Debug.LogWarning($"Cannot walk to {nextStep} from {unit.Pos}, finding alternative"); + nextStep = FindNearbyWalkableCell(unit.Pos); // Ищем альтернативную проходимую клетку + } + + // Финальная проверка валидности шага + delta = nextStep - unit.Pos; + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + Debug.LogError($"Unable to find valid step from {unit.Pos}, staying in place"); + return unit.Pos; // Остаемся на месте если ничего не помогло + } + + return nextStep; // Возвращаем валидный следующий шаг } + // Метод получения снарядов для атаки public List GetProjectiles() { - List result = new (); + List result = new(); // Создаем список для снарядов + + // Для каждой выбранной цели генерируем снаряды foreach (var target in SelectTargets()) { - GenerateProjectiles(target, result); + GenerateProjectiles(target, result); // Генерируем снаряды для цели } + // Добавляем визуальные смещения снарядам для реалистичности стрельбы for (int i = 0; i < result.Count; i++) { - var proj = result[i]; - proj.AddStartShift(_projectileShifts[i % _projectileShifts.Length]); + var proj = result[i]; // Получаем снаряд + proj.AddStartShift(_projectileShifts[i % _projectileShifts.Length]); // Добавляем смещение } - return result; + return result; // Возвращаем список готовых снарядов } + // Метод установки ссылки на управляемый юнит public void SetUnit(Unit unit) { - this.unit = unit; + this.unit = unit; // Сохраняем ссылку на юнит } + // Виртуальный метод обновления (вызывается каждый кадр) public virtual void Update(float deltaTime, float time) { + // Базовая реализация пустая, переопределяется в наследниках при необходимости } + // Виртуальный метод генерации снарядов для атаки protected virtual void GenerateProjectiles(Vector2Int forTarget, List intoList) { + // Создаем один снаряд и добавляем его в список AddProjectileToList(CreateProjectile(forTarget), intoList); } + // Виртуальный метод выбора целей для атаки protected virtual List SelectTargets() { - var result = GetReachableTargets(); + var result = GetReachableTargets(); // Получаем все цели в зоне досягаемости + + // Оставляем только одну цель (первую в списке) по умолчанию while (result.Count > 1) - result.RemoveAt(result.Count - 1); - return result; + result.RemoveAt(result.Count - 1); // Удаляем лишние цели + + return result; // Возвращаем список с одной целью } - + + // Метод создания снаряда указанного типа protected BaseProjectile CreateProjectile(Vector2Int target) => BaseProjectile.Create(unit.Config.ProjectileType, unit, unit.Pos, target, unit.Config.Damage); - + + // Метод добавления снаряда в список protected void AddProjectileToList(BaseProjectile projectile, List list) => - list.Add(projectile); + list.Add(projectile); // Добавляем снаряд в переданный список + // Метод получения юнита на указанной позиции protected IReadOnlyUnit GetUnitAt(Vector2Int pos) => - runtimeModel.RoUnits.FirstOrDefault(u => u.Pos == pos); + runtimeModel.RoUnits.FirstOrDefault(u => u.Pos == pos); // Ищем юнита по позиции + // Метод получения юнитов в указанном радиусе protected List GetUnitsInRadius(float radius, bool enemies) { - var units = new List(); - var pos = unit.Pos; - var distanceSqr = radius * radius; - + var units = new List(); // Создаем пустой список для результатов + var pos = unit.Pos; // Текущая позиция нашего юнита + var distanceSqr = radius * radius; // Квадрат радиуса для оптимизации вычислений + + // Проверяем всех юнитов в игре foreach (var otherUnit in runtimeModel.RoUnits) { - if (otherUnit == unit) + if (otherUnit == unit) // Пропускаем самого себя continue; + // Проверяем соответствие фильтру (враги или союзники) if (enemies != (otherUnit.Config.IsPlayerUnit == unit.Config.IsPlayerUnit)) continue; - var otherPos = otherUnit.Pos; - var diff = otherPos - pos; - var distance = diff.sqrMagnitude; - if (distance <= distanceSqr) - units.Add(otherUnit); + var otherPos = otherUnit.Pos; // Позиция проверяемого юнита + var diff = otherPos - pos; // Разница позиций + var distance = diff.sqrMagnitude; // Квадрат расстояния между юнитами + + if (distance <= distanceSqr) // Если юнит в заданном радиусе + units.Add(otherUnit); // Добавляем его в список } - return units; + return units; // Возвращаем список найденных юнитов } + // Метод проверки наличия целей в зоне атаки protected bool HasTargetsInRange() { - var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; + var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; // Квадрат радиуса атаки + + // Проверяем все возможные цели foreach (var possibleTarget in GetAllTargets()) { - var diff = possibleTarget - unit.Pos; - if (diff.sqrMagnitude < attackRangeSqr) - return true; + var diff = possibleTarget - unit.Pos; // Разница позиций с целью + if (diff.sqrMagnitude < attackRangeSqr) // Если цель в радиусе атаки + return true; // Цель найдена в зоне досягаемости } - return false; + return false; // Целей в зоне атаки нет } + // Метод получения всех вражеских юнитов protected IEnumerable GetAllEnemyUnits() { return runtimeModel.RoUnits - .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain); + .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain); // Фильтруем юниты по принадлежности } + // Метод получения всех возможных целей (вражеские юниты + база) protected IEnumerable GetAllTargets() { return runtimeModel.RoUnits - .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain) - .Select(u => u.Pos) - .Append(runtimeModel.RoMap.Bases[IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]); + .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain) // Все вражеские юниты + .Select(u => u.Pos) // Получаем их позиции + .Append(runtimeModel.RoMap.Bases[IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]); // Добавляем вражескую базу } + // Метод проверки, находится ли цель в зоне атаки protected bool IsTargetInRange(Vector2Int targetPos) { - var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; - var diff = targetPos - unit.Pos; - return diff.sqrMagnitude <= attackRangeSqr; + var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; // Квадрат радиуса атаки + var diff = targetPos - unit.Pos; // Разница позиций + return diff.sqrMagnitude <= attackRangeSqr; // Возвращаем true если цель в радиусе } + // Метод получения всех целей в зоне досягаемости атаки protected List GetReachableTargets() { - var result = new List(); - var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; + var result = new List(); // Создаем пустой список результатов + + // Проверяем все возможные цели foreach (var possibleTarget in GetAllTargets()) { - if (!IsTargetInRange(possibleTarget)) - continue; - - result.Add(possibleTarget); + if (!IsTargetInRange(possibleTarget)) // Если цель не в зоне атаки + continue; // Пропускаем её + + result.Add(possibleTarget); // Добавляем доступную цель в список + } + + return result; // Возвращаем список доступных целей + } + + // Вспомогательный метод проверки проходимости клетки + private bool IsCellWalkable(Vector2Int pos) + { + // Проверяем, что клетка находится в пределах игровой карты + if (pos.x < 0 || pos.x >= runtimeModel.RoMap.Width || + pos.y < 0 || pos.y >= runtimeModel.RoMap.Height) + return false; // Клетка за пределами карты + + // Проверяем, что клетка не занята препятствием (стеной) + if (runtimeModel.RoMap[pos]) + return false; // Клетка заблокирована + + // Проверяем, что на клетке нет другого юнита + if (runtimeModel.RoUnits.Any(u => u.Pos == pos)) + return false; // Клетка занята юнитом + + return true; // Клетка проходима + } + + // Поиск ближайшей проходимой клетки рядом с текущей позицией + private Vector2Int FindNearbyWalkableCell(Vector2Int currentPos) + { + // Определяем приоритетные направления для поиска + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Вверх + Vector2Int.right, // Вправо + Vector2Int.down, // Вниз + Vector2Int.left // Влево + }; + + // Проверяем каждое направление + foreach (Vector2Int direction in directions) + { + Vector2Int neighborPos = currentPos + direction; // Вычисляем позицию соседа + + // Если соседняя клетка проходима, возвращаем её + if (IsCellWalkable(neighborPos)) + { + return neighborPos; // Найдена проходимая клетка + } } - return result; + // Если все соседние клетки заняты, остаемся на месте + Debug.LogWarning($"No walkable cells near {currentPos}, staying in place"); + return currentPos; // Возвращаем текущую позицию } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs new file mode 100644 index 000000000..4b58f7e83 --- /dev/null +++ b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs @@ -0,0 +1,354 @@ +using System.Collections.Generic; +using System.Linq; +using Model; +using UnityEngine; + +namespace UnitBrains.Pathfinding +{ + public class AStarUnitPath : BaseUnitPath + { + // Внутренний класс для хранения информации об узле в алгоритме A* + private class PathNode + { + public Vector2Int Position { get; set; } // Позиция узла на карте + public float GCost { get; set; } // Стоимость пути от начала до этого узла + public float HCost { get; set; } // Эвристическая оценка расстояния до цели + public float FCost => GCost + HCost; // Общая стоимость узла (F = G + H) + public PathNode Parent { get; set; } // Родительский узел для восстановления пути + + // Конструктор узла - инициализирует начальные значения + public PathNode(Vector2Int position) + { + Position = position; // Устанавливаем позицию узла + GCost = float.MaxValue; // Начальная стоимость пути - максимальная + HCost = 0; // Начальная эвристическая оценка - 0 + Parent = null; // Изначально родитель отсутствует + } + } + + // Конструктор класса - вызывает конструктор базового класса BaseUnitPath + public AStarUnitPath(IReadOnlyRuntimeModel runtimeModel, Vector2Int startPoint, Vector2Int endPoint) + : base(runtimeModel, startPoint, endPoint) + { + // Все параметры передаются в базовый конструктор + } + + // Основной метод расчета пути с использованием алгоритма A* + protected override void Calculate() + { + // Открытый список - содержит узлы, которые предстоит проверить + List openList = new List(); + // Закрытый список - содержит уже проверенные узлы (хэш-сет для быстрого поиска) + HashSet closedList = new HashSet(); + + // Создаем начальный узел (позиция юнита) + PathNode startNode = new PathNode(startPoint); + startNode.GCost = 0; // Стоимость пути от начала до начала равна 0 + startNode.HCost = CalculateHeuristic(startPoint, endPoint); // Вычисляем эвристику до цели + + // Добавляем начальный узел в открытый список для обработки + openList.Add(startNode); + + // Основной цикл алгоритма A* - продолжается пока есть узлы для проверки + while (openList.Count > 0) + { + // Находим узел с наименьшей общей стоимостью F = G + H + PathNode currentNode = openList.OrderBy(node => node.FCost).First(); + + // Проверяем, достигли ли мы конечной точки + if (currentNode.Position == endPoint) + { + // Восстанавливаем полный путь от конечного узла к начальному + path = ReconstructPath(currentNode); + return; // Завершаем вычисление пути + } + + // Перемещаем текущий узел из открытого списка в закрытый + openList.Remove(currentNode); + closedList.Add(currentNode.Position); + + // Проверяем всех соседей текущего узла (4 направления) + foreach (Vector2Int neighborPos in GetNeighbors(currentNode.Position)) + { + // Пропускаем узлы, которые уже были проверены + if (closedList.Contains(neighborPos)) + continue; + + // Пропускаем непроходимые клетки (стены или занятые юнитами) + if (!IsWalkable(neighborPos)) + continue; + + // Вычисляем предварительную стоимость пути до соседа + // Текущая стоимость + 1 (стоимость одного шага) + float tentativeGCost = currentNode.GCost + 1; + + // Ищем соседний узел в открытом списке + PathNode neighborNode = openList.FirstOrDefault(node => node.Position == neighborPos); + + if (neighborNode == null) // Если сосед еще не в открытом списке + { + // Создаем новый узел для соседа + neighborNode = new PathNode(neighborPos); + neighborNode.GCost = tentativeGCost; // Устанавливаем стоимость пути + neighborNode.HCost = CalculateHeuristic(neighborPos, endPoint); // Вычисляем эвристику + neighborNode.Parent = currentNode; // Устанавливаем родителя для восстановления пути + openList.Add(neighborNode); // Добавляем соседа в открытый список + } + else if (tentativeGCost < neighborNode.GCost) // Если нашли более короткий путь к соседу + { + // Обновляем стоимость и родителя (более оптимальный маршрут) + neighborNode.GCost = tentativeGCost; + neighborNode.Parent = currentNode; + } + } + } + + // Если цикл завершился, а путь не найден - выводим предупреждение + Debug.LogWarning($"A* path not found from {startPoint} to {endPoint}, using direct path as fallback"); + // Создаем прямой путь как запасной вариант (от начала к концу) + path = new Vector2Int[] { startPoint, endPoint }; + } + + // Переопределенный метод получения следующего шага с проверкой валидности + public new Vector2Int GetNextStepFrom(Vector2Int unitPos) + { + // Получаем все точки вычисленного пути в виде списка + List pathPoints = GetPath().ToList(); + + // Если путь пустой, возвращаем текущую позицию (некуда идти) + if (pathPoints.Count == 0) + { + Debug.LogWarning($"A* path is empty for unit at {unitPos}"); + return unitPos; + } + + // Если путь состоит из одной точки, значит мы уже на месте + if (pathPoints.Count == 1) + { + return pathPoints[0]; + } + + // Ищем индекс текущей позиции юнита в вычисленном пути + int currentIndex = pathPoints.IndexOf(unitPos); + + // Если текущая позиция не найдена в пути (юнит отклонился от маршрута) + if (currentIndex == -1) + { + // Ищем ближайшую точку пути к текущей позиции юнита + int closestIndex = 0; // Индекс ближайшей найденной точки + float closestDistance = float.MaxValue; // Минимальное найденное расстояние + + // Проходим по всем точкам пути для поиска ближайшей + for (int i = 0; i < pathPoints.Count; i++) + { + // Вычисляем евклидово расстояние от текущей позиции до точки пути + float distance = Vector2.Distance( + new Vector2(unitPos.x, unitPos.y), + new Vector2(pathPoints[i].x, pathPoints[i].y) + ); + + // Если нашли более близкую точку, обновляем минимум + if (distance < closestDistance) + { + closestDistance = distance; // Сохраняем новое минимальное расстояние + closestIndex = i; // Запоминаем индекс ближайшей точки + } + } + + // Если мы не в конце пути, возвращаем следующую точку после ближайшей + if (closestIndex < pathPoints.Count - 1) + { + Vector2Int nextPoint = pathPoints[closestIndex + 1]; + // Проверяем и возвращаем валидный шаг к следующей точке + return ValidateAndGetStep(unitPos, nextPoint); + } + else + { + // Если мы в конце пути, возвращаем последнюю точку (финальная позиция) + return ValidateAndGetStep(unitPos, pathPoints[pathPoints.Count - 1]); + } + } + + // Если мы уже в конце пути, возвращаем конечную точку + if (currentIndex >= pathPoints.Count - 1) + { + return pathPoints[pathPoints.Count - 1]; + } + + // Получаем следующую точку пути после текущей позиции + Vector2Int nextPathPoint = pathPoints[currentIndex + 1]; + + // Проверяем валидность шага и возвращаем его + return ValidateAndGetStep(unitPos, nextPathPoint); + } + + // Вспомогательный метод для проверки и получения валидного шага + private Vector2Int ValidateAndGetStep(Vector2Int currentPos, Vector2Int targetPos) + { + // Вычисляем разницу между текущей позицией и желаемой целевой позицией + Vector2Int delta = targetPos - currentPos; + + // Проверяем, что шаг не больше 1 клетки по любой оси + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + Debug.LogWarning($"A* next step {targetPos} is too far from {currentPos}, calculating step towards"); + + // Вычисляем направление к цели с ограничением в 1 шаг + Vector2Int step = new Vector2Int( + Mathf.Clamp(delta.x, -1, 1), // Ограничиваем движение по X одним шагом + Mathf.Clamp(delta.y, -1, 1) // Ограничиваем движение по Y одним шагом + ); + + // Вычисляем следующую позицию как текущая + ограниченный шаг + Vector2Int nextStep = currentPos + step; + + // Проверяем, что вычисленная позиция проходима + if (IsWalkable(nextStep)) + { + return nextStep; // Возвращаем валидный шаг + } + else + { + // Если прямой путь заблокирован, ищем альтернативный обходной шаг + return FindAlternativeStep(currentPos, targetPos); + } + } + + // Проверяем, что целевая позиция проходима (нет стены или юнита) + if (!IsWalkable(targetPos)) + { + Debug.LogWarning($"A* target step {targetPos} is not walkable, finding alternative"); + // Ищем альтернативный путь + return FindAlternativeStep(currentPos, targetPos); + } + + // Возвращаем валидный шаг к целевой позиции + return targetPos; + } + + // Поиск альтернативного шага, если прямой путь заблокирован + private Vector2Int FindAlternativeStep(Vector2Int currentPos, Vector2Int targetPos) + { + // Определяем все возможные направления движения (включая диагональные) + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Вверх + Vector2Int.right, // Вправо + Vector2Int.down, // Вниз + Vector2Int.left, // Влево + new Vector2Int(1, 1), // Диагональ право-вверх + new Vector2Int(-1, 1), // Диагональ лево-вверх + new Vector2Int(1, -1), // Диагональ право-вниз + new Vector2Int(-1, -1) // Диагональ лево-вниз + }; + + // Сортируем направления по близости к целевой точке направление, которое максимально приближает к цели + var sortedDirections = directions + .OrderBy(d => Vector2.Distance( + new Vector2(currentPos.x + d.x, currentPos.y + d.y), + new Vector2(targetPos.x, targetPos.y) + )) + .ToArray(); + + // Проверяем каждое направление в порядке приоритета + foreach (Vector2Int direction in sortedDirections) + { + Vector2Int alternativeStep = currentPos + direction; + + // Проверяем, что клетка находится в пределах игрового поля + if (alternativeStep.x >= 0 && alternativeStep.x < runtimeModel.RoMap.Width && + alternativeStep.y >= 0 && alternativeStep.y < runtimeModel.RoMap.Height) + { + // Проверяем, что клетка проходима (нет стены и юнита) + if (IsWalkable(alternativeStep)) + { + Debug.Log($"Found alternative step: {alternativeStep} from {currentPos}"); + return alternativeStep; // Возвращаем найденный альтернативный шаг + } + } + } + + // Если ни одно направление не подходит, остаемся на текущей позиции + Debug.LogWarning($"No alternative step found from {currentPos}, staying in place"); + return currentPos; + } + + // Получение списка соседних клеток для указанной позиции + private List GetNeighbors(Vector2Int position) + { + // Создаем пустой список для хранения соседей + List neighbors = new List(); + + // Определяем 4 основных направления движения (без диагоналей для простоты) + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Вверх (0, 1) + Vector2Int.down, // Вниз (0, -1) + Vector2Int.left, // Влево (-1, 0) + Vector2Int.right // Вправо (1, 0) + }; + + // Проверяем каждое направление + foreach (Vector2Int direction in directions) + { + Vector2Int neighborPos = position + direction; // Вычисляем позицию соседа + + // Проверяем, что сосед находится в пределах игровой карты + if (neighborPos.x >= 0 && neighborPos.x < runtimeModel.RoMap.Width && + neighborPos.y >= 0 && neighborPos.y < runtimeModel.RoMap.Height) + { + neighbors.Add(neighborPos); // Добавляем валидного соседа в список + } + } + + return neighbors; // Возвращаем список соседних клеток + } + + // Проверка, можно ли пройти через указанную клетку + private bool IsWalkable(Vector2Int position) + { + // Проверяем, что клетка не занята препятствием (стеной) + if (runtimeModel.RoMap[position]) + return false; // Клетка заблокирована стеной + + // Проверяем, что на клетке нет другого юнита + // Исключаем начальную и конечную точки пути + if (position != endPoint && position != startPoint) + { + // Проверяем всех юнитов на карте + if (runtimeModel.RoUnits.Any(u => u.Pos == position)) + return false; // Клетка занята другим юнитом + } + + return true; // Клетка проходима + } + + // Эвристическая функция для оценки расстояния до цели + private float CalculateHeuristic(Vector2Int from, Vector2Int to) + { + + return Mathf.Abs(from.x - to.x) + Mathf.Abs(from.y - to.y); + } + + // Восстановление полного пути от конечного узла к начальному + private Vector2Int[] ReconstructPath(PathNode endNode) + { + // Создаем список для хранения точек пути + List pathList = new List(); + PathNode currentNode = endNode; // Начинаем с конечного узла + + // Движемся от конечного узла к начальному по родительским ссылкам + while (currentNode != null) + { + pathList.Add(currentNode.Position); // Добавляем позицию текущего узла + currentNode = currentNode.Parent; // Переходим к родительскому узлу + } + + // Переворачиваем список, чтобы путь шел от начала к концу + pathList.Reverse(); + + // Преобразуем список в массив и возвращаем + return pathList.ToArray(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs b/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs index 80b18be07..d01c69893 100644 --- a/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs +++ b/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs @@ -6,44 +6,130 @@ namespace UnitBrains.Pathfinding { public abstract class BaseUnitPath { + // Свойство для получения начальной точки пути (только для чтения) public Vector2Int StartPoint => startPoint; + // Свойство для получения конечной точки пути (только для чтения) public Vector2Int EndPoint => endPoint; - - protected readonly IReadOnlyRuntimeModel runtimeModel; - protected readonly Vector2Int startPoint; - protected readonly Vector2Int endPoint; - protected Vector2Int[] path = null; + // Защищенные поля для хранения данных + protected readonly IReadOnlyRuntimeModel runtimeModel; // Модель времени выполнения для доступа к карте и юнитам + protected readonly Vector2Int startPoint; // Начальная точка пути + protected readonly Vector2Int endPoint; // Конечная точка пути + protected Vector2Int[] path = null; // Массив точек пути (null, если путь еще не вычислен) + + // Абстрактный метод для вычисления пути + // Должен быть реализован в наследниках (AStarUnitPath, DummyUnitPath и т.д.) protected abstract void Calculate(); - + + // Метод для получения всех точек пути + // Если путь еще не вычислен, вызывает Calculate() public IEnumerable GetPath() { + // Проверяем, был ли уже вычислен путь if (path == null) + { + // Если нет, вычисляем его Calculate(); - + } + + // Возвращаем вычисленный путь return path; } + // Метод для получения следующего шага от указанной позиции юнита + // Используется для пошагового движения по пути public Vector2Int GetNextStepFrom(Vector2Int unitPos) { - var found = false; - foreach (var cell in GetPath()) + // Флаг для отслеживания, нашли ли мы текущую позицию юнита в пути + bool found = false; + + // Проходим по всем точкам пути + foreach (Vector2Int cell in GetPath()) { + // Если мы уже нашли текущую позицию, возвращаем следующую точку if (found) + { return cell; + } - found = cell == unitPos; + // Проверяем, является ли текущая точка пути позицией юнита + if (cell == unitPos) + { + found = true; + } } - Debug.LogError($"Unit {unitPos} is not on the path"); + // Если позиция юнита не найдена в пути (ошибка) + Debug.LogError($"Unit at position {unitPos} is not on the calculated path from {startPoint} to {endPoint}"); + + // Возвращаем текущую позицию как запасной вариант return unitPos; } + // Конструктор базового класса + // Принимает модель времени выполнения, начальную и конечную точки protected BaseUnitPath(IReadOnlyRuntimeModel runtimeModel, Vector2Int startPoint, Vector2Int endPoint) { + // Проверяем, что модель не null + if (runtimeModel == null) + { + Debug.LogError("RuntimeModel cannot be null in BaseUnitPath constructor"); + } + + // Сохраняем переданные параметры this.runtimeModel = runtimeModel; this.startPoint = startPoint; this.endPoint = endPoint; + + // Путь изначально не вычислен + this.path = null; + } + + // Вспомогательный метод для проверки, вычислен ли уже путь + protected bool IsPathCalculated() + { + return path != null; + } + + // Вспомогательный метод для получения длины пути + public int GetPathLength() + { + // Получаем путь (вычисляем, если нужно) + Vector2Int[] calculatedPath = GetPath() as Vector2Int[]; + + // Возвращаем длину пути или 0, если путь не существует + return calculatedPath?.Length ?? 0; + } + + // Вспомогательный метод для проверки, содержит ли путь указанную точку + public bool ContainsPoint(Vector2Int point) + { + // Получаем путь + IEnumerable pathPoints = GetPath(); + + // Проверяем каждую точку пути + foreach (Vector2Int pathPoint in pathPoints) + { + if (pathPoint == point) + { + return true; + } + } + + return false; + } + + // Переопределение ToString() для удобной отладки + public override string ToString() + { + if (path == null) + { + return $"Path from {startPoint} to {endPoint} [NOT CALCULATED]"; + } + else + { + return $"Path from {startPoint} to {endPoint} [{path.Length} steps]"; + } } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs b/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs index 3ffc5123b..db096bbcc 100644 --- a/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs +++ b/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs @@ -7,47 +7,374 @@ namespace UnitBrains.Pathfinding { public class DebugPathOutput : MonoBehaviour { - [SerializeField] private GameObject cellHighlightPrefab; - [SerializeField] private int maxHighlights = 5; + [SerializeField] private GameObject cellHighlightPrefab; // Префаб для подсветки клетки + [SerializeField] private int maxHighlights = 5; // Максимальное количество одновременно подсвеченных клеток + [SerializeField] private float highlightDelay = 0.1f; // Задержка между подсветкой клеток (в секундах) + [SerializeField] private float fadeOutDelay = 1f; // Задержка перед удалением всех подсветок (в секундах) + [SerializeField] private Color startColor = Color.green; // Цвет начальной точки пути + [SerializeField] private Color endColor = Color.red; // Цвет конечной точки пути + [SerializeField] private Color pathColor = Color.yellow; // Цвет промежуточных точек пути - public BaseUnitPath Path { get; private set; } - private readonly List allHighlights = new(); - private Coroutine highlightCoroutine; + public BaseUnitPath Path { get; private set; } // Текущий отображаемый путь + private readonly List allHighlights = new List(); // Список всех активных подсветок + private Coroutine highlightCoroutine; // Ссылка на текущую корутину подсветки + private bool isShuttingDown = false; // Флаг завершения работы для предотвращения ошибок + // Метод для запуска подсветки пути public void HighlightPath(BaseUnitPath path) { - Path = path; - while (allHighlights.Count > 0) + // Проверяем, что объект активен и не завершает работу + if (!this.isActiveAndEnabled || isShuttingDown) + return; + + Path = path; // Сохраняем ссылку на путь + + // Очищаем все предыдущие подсветки перед созданием новых + StopAllCoroutines(); // Останавливаем все текущие корутины + ClearAllHighlightsImmediate(); // Мгновенно очищаем все подсветки + + // Запускаем новую корутину для визуализации пути + if (path != null && cellHighlightPrefab != null) + { + highlightCoroutine = StartCoroutine(HighlightCoroutine(path)); + } + } + + // Корутина для пошаговой подсветки пути + private IEnumerator HighlightCoroutine(BaseUnitPath path) + { + // Проверяем, что объект все еще активен + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // Получаем все точки пути в виде списка для удобной работы + List pathPoints = new List(path.GetPath()); + + // Проверяем, что путь содержит хотя бы одну точку + if (pathPoints.Count == 0) + { + Debug.LogWarning("DebugPathOutput: Path is empty, nothing to highlight"); + yield break; // Выходим из корутины + } + + int totalPoints = pathPoints.Count; // Общее количество точек в пути + int highlightedCount = 0; // Счетчик подсвеченных клеток + + // Проходим по каждой точке пути последовательно + for (int i = 0; i < totalPoints; i++) + { + // Проверяем, что объект все еще активен + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + Vector2Int currentPoint = pathPoints[i]; // Текущая точка пути + + // Проверяем, не превышен ли лимит одновременно подсвеченных клеток + if (highlightedCount >= maxHighlights) + { + // Удаляем самую старую подсветку (первую в списке) + if (allHighlights.Count > 0 && allHighlights[0] != null) + { + // Запускаем корутину плавного исчезновения для старой подсветки + StartCoroutine(FadeOutHighlight(allHighlights[0], 0.3f)); + + // Ждем небольшую задержку + yield return new WaitForSeconds(0.05f); + + // Удаляем объект подсветки если он еще существует + if (allHighlights.Count > 0) + { + DestroyHighlightSafe(0); + } + } + } + + // Проверяем снова перед созданием новой подсветки + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // Создаем новую подсветку для текущей точки + GameObject highlight = CreateHighlight(currentPoint); + + if (highlight != null) + { + // Устанавливаем цвет в зависимости от позиции в пути + SetHighlightColor(highlight, i, totalPoints); + + // Добавляем эффект появления (плавное увеличение) + StartCoroutine(ScaleUpHighlight(highlight, 0.3f)); + } + + highlightedCount++; // Увеличиваем счетчик подсвеченных клеток + + // Ждем указанную задержку перед подсветкой следующей клетки + yield return new WaitForSeconds(highlightDelay); + } + + // Проверяем перед финальной задержкой + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // После подсветки всего пути, ждем дополнительное время + yield return new WaitForSeconds(fadeOutDelay); + + // Проверяем перед удалением + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // Плавно убираем все оставшиеся подсветки + yield return StartCoroutine(FadeOutAllHighlights(1f)); + + // Очищаем все подсветки после завершения + ClearAllHighlightsImmediate(); + + highlightCoroutine = null; // Очищаем ссылку на корутину + } + + // Создание подсветки для указанной клетки + private GameObject CreateHighlight(Vector2Int atCell) + { + // Проверяем, что объект активен и префаб существует + if (!this.isActiveAndEnabled || isShuttingDown || cellHighlightPrefab == null) + return null; + + try + { + // Преобразуем координаты сетки в мировые координаты + Vector3 pos = Gameplay3dView.ToWorldPosition(atCell, 1f); + + // Создаем объект подсветки из префаба + GameObject highlight = Instantiate(cellHighlightPrefab, pos, Quaternion.identity); + + if (highlight != null) + { + // Устанавливаем родительский объект для организации иерархии + highlight.transform.SetParent(transform); + + // Добавляем созданную подсветку в список для отслеживания + allHighlights.Add(highlight); + } + + return highlight; // Возвращаем созданный объект + } + catch (System.Exception e) + { + Debug.LogError($"Error creating highlight at {atCell}: {e.Message}"); + return null; + } + } + + // Установка цвета подсветки в зависимости от позиции в пути + private void SetHighlightColor(GameObject highlight, int index, int totalPoints) + { + // Проверяем существование объекта + if (highlight == null) + return; + + // Получаем компонент Renderer для изменения цвета + Renderer renderer = highlight.GetComponent(); + + if (renderer != null && renderer.material != null) + { + Color targetColor; // Цвет для текущей точки + + if (index == 0) // Если это начальная точка + { + targetColor = startColor; // Зеленый цвет + } + else if (index == totalPoints - 1) // Если это конечная точка + { + targetColor = endColor; // Красный цвет + } + else // Промежуточные точки + { + // Градиент от зеленого к красному + float t = (float)index / (totalPoints - 1); // Прогресс от 0 до 1 + targetColor = Color.Lerp(startColor, endColor, t); // Интерполяция цвета + } + + // Устанавливаем цвет материала + renderer.material.color = targetColor; + } + } + + // Корутина для плавного появления подсветки (увеличение масштаба) + private IEnumerator ScaleUpHighlight(GameObject highlight, float duration) + { + // Проверяем существование объекта в самом начале + if (highlight == null) + yield break; + + // Сохраняем исходный масштаб + Vector3 originalScale = highlight.transform.localScale; + + // Устанавливаем начальный нулевой размер + highlight.transform.localScale = Vector3.zero; + + float elapsed = 0f; // Прошедшее время + + // Плавно увеличиваем масштаб от 0 до исходного + while (elapsed < duration) + { + // ИСПРАВЛЕНИЕ: Проверяем объект на null в каждой итерации + if (highlight == null || !highlight.activeInHierarchy) + yield break; // Объект уничтожен или деактивирован + + elapsed += Time.deltaTime; // Увеличиваем прошедшее время + float t = elapsed / duration; // Прогресс анимации (0-1) + + // Используем easing функцию для более красивого эффекта + float smoothT = Mathf.SmoothStep(0f, 1f, t); + + // Устанавливаем промежуточный масштаб + highlight.transform.localScale = Vector3.Lerp(Vector3.zero, originalScale, smoothT); + + yield return null; // Ждем следующий кадр + } + + // Финальная проверка и установка точного масштаба + if (highlight != null && highlight.activeInHierarchy) + { + highlight.transform.localScale = originalScale; + } + } + + // Корутина для плавного исчезновения одной подсветки + private IEnumerator FadeOutHighlight(GameObject highlight, float duration) + { + // Проверяем существование объекта + if (highlight == null) + yield break; + + Renderer renderer = highlight.GetComponent(); + + // Проверяем наличие рендерера и материала + if (renderer == null || renderer.material == null) + yield break; + + Color originalColor = renderer.material.color; // Сохраняем исходный цвет + float elapsed = 0f; // Прошедшее время + + // Плавно уменьшаем прозрачность + while (elapsed < duration) + { + // ИСПРАВЛЕНИЕ: Проверяем объект и рендерер в каждой итерации + if (highlight == null || !highlight.activeInHierarchy || renderer == null || renderer.material == null) + yield break; // Объект или компоненты уничтожены + + elapsed += Time.deltaTime; // Увеличиваем время + float t = elapsed / duration; // Прогресс (0-1) + + // Уменьшаем альфа-канал для эффекта исчезновения + Color fadedColor = originalColor; + fadedColor.a = Mathf.Lerp(1f, 0f, t); // Плавное уменьшение прозрачности + renderer.material.color = fadedColor; + + yield return null; // Ждем следующий кадр + } + } + + // Корутина для плавного исчезновения всех подсветок + private IEnumerator FadeOutAllHighlights(float duration) + { + // Проверяем, есть ли подсветки + if (allHighlights.Count == 0) + yield break; + + // Создаем копию списка для безопасной итерации + List highlightsToFade = new List(); + foreach (GameObject highlight in allHighlights) + { + if (highlight != null) + { + highlightsToFade.Add(highlight); + } + } + + // Запускаем исчезновение для всех подсветок одновременно + foreach (GameObject highlight in highlightsToFade) { - DestroyHighlight(0); + if (highlight != null && this.isActiveAndEnabled) + { + StartCoroutine(FadeOutHighlight(highlight, duration)); + } } - + + // Ждем указанную длительность + yield return new WaitForSeconds(duration); + } + + // Безопасное удаление подсветки по индексу + private void DestroyHighlightSafe(int index) + { + // Проверяем, что индекс в допустимых пределах + if (index >= 0 && index < allHighlights.Count) + { + GameObject highlight = allHighlights[index]; // Получаем объект + + if (highlight != null) + { + // Проверяем, что объект все еще существует перед уничтожением + if (highlight.activeInHierarchy || highlight != null) + { + Destroy(highlight); // Уничтожаем игровой объект + } + } + + allHighlights.RemoveAt(index); // Удаляем из списка в любом случае + } + } + + // Мгновенная очистка всех подсветок без анимации + private void ClearAllHighlightsImmediate() + { + // Удаляем все подсветки с конца списка для безопасности + for (int i = allHighlights.Count - 1; i >= 0; i--) + { + if (allHighlights[i] != null) + { + Destroy(allHighlights[i]); // Уничтожаем объект + } + } + + allHighlights.Clear(); // Очищаем список + } + + // Метод для принудительной очистки всех подсветок + public void ClearAllHighlights() + { + // Останавливаем текущую корутину, если она запущена if (highlightCoroutine != null) { StopCoroutine(highlightCoroutine); + highlightCoroutine = null; } - highlightCoroutine = StartCoroutine(HighlightCoroutine(path)); + // Останавливаем все остальные корутины + StopAllCoroutines(); + + // Очищаем подсветки + ClearAllHighlightsImmediate(); } - private IEnumerator HighlightCoroutine(BaseUnitPath path) + // Вызывается при деактивации объекта + private void OnDisable() { - // TODO Implement me - yield break; + isShuttingDown = true; // Устанавливаем флаг завершения + ClearAllHighlights(); // Очищаем все подсветки } - private void CreateHighlight(Vector2Int atCell) + // Вызывается при уничтожении объекта + private void OnDestroy() { - var pos = Gameplay3dView.ToWorldPosition(atCell, 1f); - var highlight = Instantiate(cellHighlightPrefab, pos, Quaternion.identity); - highlight.transform.SetParent(transform); - allHighlights.Add(highlight); + isShuttingDown = true; // Устанавливаем флаг завершения + ClearAllHighlights(); // Очищаем все подсветки } - private void DestroyHighlight(int index) + // Вызывается когда объект становится неактивным + private void OnApplicationQuit() { - Destroy(allHighlights[index]); - allHighlights.RemoveAt(index); + isShuttingDown = true; // Устанавливаем флаг при выходе из приложения } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index c2c80e989..bb350b7cd 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -1,73 +1,269 @@ -using System.Collections.Generic; +using Model; using Model.Runtime.Projectiles; +using System.Collections.Generic; using UnityEngine; +using Utilities; +using UnitBrains.Pathfinding; // Импорт для использования AStarUnitPath namespace UnitBrains.Player { public class SecondUnitBrain : DefaultPlayerUnitBrain { - public override string TargetUnitName => "Cobra Commando"; - private const float OverheatTemperature = 3f; - private const float OverheatCooldown = 2f; - private float _temperature = 0f; - private float _cooldownTime = 0f; - private bool _overheated; - + public override string TargetUnitName => "Cobra Commando"; // Имя юнита для идентификации + private const float OverheatTemperature = 3f; // Пороговая температура перегрева оружия + private const float OverheatCooldown = 2f; // Время остывания после перегрева (в секундах) + private float _temperature = 0f; // Текущая температура оружия + private float _cooldownTime = 0f; // Время, прошедшее с начала остывания + private bool _overheated; // Флаг, указывающий на перегрев оружия + + // Список целей, находящихся вне зоны досягаемости атаки + // Юнит будет двигаться к этим целям, используя A* pathfinding + List _outOfRangeTargets = new List(); + + // Статический счетчик для присвоения уникальных номеров юнитам этого типа + private static int _unitCounter = 0; + // Уникальный номер текущего экземпляра юнита + private int _unitNumber; + // Максимальное количество целей для "умного" распределения между юнитами + private const int MaxTargetsForSmartSelection = 3; + + // Конструктор класса - вызывается при создании каждого нового юнита + public SecondUnitBrain() + { + _unitNumber = _unitCounter; // Присваиваем текущий номер из статического счетчика + _unitCounter++; // Увеличиваем счетчик для следующего юнита + } + + // Метод генерации снарядов при атаке + // forTarget - цель для выстрела + // intoList - список, куда добавляются созданные снаряды protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) { - float overheatTemperature = OverheatTemperature; - /////////////////////////////////////// - // Homework 1.3 (1st block, 3rd module) - /////////////////////////////////////// - var projectile = CreateProjectile(forTarget); - AddProjectileToList(projectile, intoList); - /////////////////////////////////////// + float overheatTemperature = OverheatTemperature; // Локальная переменная порога перегрева + + // Если оружие перегрето, не создаем снаряды + if (GetTemperature() >= overheatTemperature) + { + return; // Выходим из метода без создания снарядов + } + + // Количество снарядов зависит от текущей температуры: температура + 1 + int currentTemp = GetTemperature(); // Получаем текущую температуру как целое число + int projectileCount = currentTemp + 1; // Вычисляем количество снарядов для выстрела + + // Создаем указанное количество снарядов + for (int i = 0; i < projectileCount; i++) + { + var projectile = CreateProjectile(forTarget); // Создаем снаряд, направленный в цель + AddProjectileToList(projectile, intoList); // Добавляем снаряд в выходной список + } + + IncreaseTemperature(); // Увеличиваем температуру оружия после выстрела } + // Метод определения следующего шага движения юнита с использованием A* public override Vector2Int GetNextStep() { - return base.GetNextStep(); + // Если юнит может атаковать цель в зоне досягаемости, остаемся на месте + if (HasTargetsInRange()) + return unit.Pos; + + Vector2Int target; // Цель для движения + + // Определяем цель для движения + if (_outOfRangeTargets.Count > 0) + { + target = _outOfRangeTargets[0]; // Берем первую цель из списка целей вне зоны атаки + } + else + { + // Если нет конкретных целей, идем к базе противника + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + target = runtimeModel.RoMap.Bases[enemyId]; // Позиция вражеской базы + } + + // Если мы уже на цели, остаемся на месте + if (unit.Pos == target) + return unit.Pos; + + // Создаем A* путь от текущей позиции до цели + ActivePath = new AStarUnitPath(runtimeModel, unit.Pos, target); + + // Получаем следующий шаг по вычисленному пути + Vector2Int nextStep = ActivePath.GetNextStepFrom(unit.Pos); + + // Проверяем валидность шага (разница не должна превышать 1 клетку) + Vector2Int delta = nextStep - unit.Pos; + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + // Если шаг невалидный, используем прямое движение к цели + Debug.LogWarning($"SecondUnitBrain: invalid A* step from {unit.Pos} to {nextStep}, using fallback"); + nextStep = unit.Pos.CalcNextStepTowards(target); + } + + // Проверяем проходимость выбранной клетки + if (!IsCellWalkable(nextStep)) + { + Debug.LogWarning($"SecondUnitBrain: cell {nextStep} is not walkable, finding alternative"); + + // Ищем альтернативный шаг среди соседних клеток + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Проверяем клетку сверху + Vector2Int.right, // Проверяем клетку справа + Vector2Int.down, // Проверяем клетку снизу + Vector2Int.left // Проверяем клетку слева + }; + + bool foundAlternative = false; // Флаг нахождения альтернативы + foreach (Vector2Int direction in directions) + { + Vector2Int alternativeStep = unit.Pos + direction; + if (IsCellWalkable(alternativeStep)) // Если клетка проходима + { + nextStep = alternativeStep; // Используем альтернативный шаг + foundAlternative = true; // Устанавливаем флаг + break; // Выходим из цикла + } + } + + if (!foundAlternative) // Если альтернатива не найдена + { + Debug.LogWarning($"SecondUnitBrain: no alternative step from {unit.Pos}, staying in place"); + return unit.Pos; // Остаемся на месте + } + } + + return nextStep; // Возвращаем валидный следующий шаг } + // Метод выбора целей для атаки protected override List SelectTargets() { - /////////////////////////////////////// - // Homework 1.4 (1st block, 4rd module) - /////////////////////////////////////// - List result = GetReachableTargets(); - while (result.Count > 1) + // Очищаем список целей для движения перед новым выбором + _outOfRangeTargets.Clear(); + + // Получаем все возможные цели (юниты противника + база) + List allTargets = new List(GetAllTargets()); + List result = new List(); // Результирующий список целей для атаки + + // Если целей на поле нет, нацеливаемся на базу противника + if (allTargets.Count == 0) + { + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; + + // Проверяем, находится ли база в зоне досягаемости атаки + if (IsTargetInRange(enemyBasePos)) + { + result.Add(enemyBasePos); // Добавляем базу как цель для атаки + return result; + } + else + { + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу в список для движения + return result; + } + } + + // Сортируем цели по расстоянию до нашей базы (приоритет - ближайшие) + SortByDistanceToOwnBase(allTargets); + + // Распределяем цели между юнитами для избежания скучивания на одной цели + int targetIndex; + if (allTargets.Count <= MaxTargetsForSmartSelection) // Если целей мало (≤ 3) + { + targetIndex = _unitNumber % allTargets.Count; // Распределяем по номеру юнита + } + else // Если целей много (> 3) + { + targetIndex = _unitNumber % MaxTargetsForSmartSelection; // Берем остаток от деления на 3 + if (targetIndex >= allTargets.Count) // Если индекс выходит за границы + { + targetIndex = allTargets.Count - 1; // Корректируем на последний валидный индекс + } + } + + // Получаем выбранную цель по вычисленному индексу + Vector2Int selectedTarget = allTargets[targetIndex]; + + // Проверяем, находится ли выбранная цель в радиусе атаки + if (IsTargetInRange(selectedTarget)) { - result.RemoveAt(result.Count - 1); + result.Add(selectedTarget); // Добавляем цель для атаки + } + else + { + _outOfRangeTargets.Add(selectedTarget); // Добавляем цель для движения к ней + + // Ищем первую цель в списке, которая находится в зоне досягаемости + foreach (Vector2Int target in allTargets) + { + if (IsTargetInRange(target)) // Если цель доступна для атаки + { + result.Add(target); // Добавляем как цель для атаки + break; // Выходим из цикла + } + } + + // Если ни одна цель не доступна для атаки + if (result.Count == 0) + { + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели для движения + } } - return result; - /////////////////////////////////////// + + return result; // Возвращаем список целей для атаки } + // Метод обновления состояния юнита (вызывается каждый кадр) public override void Update(float deltaTime, float time) { - if (_overheated) - { - _cooldownTime += Time.deltaTime; - float t = _cooldownTime / (OverheatCooldown/10); - _temperature = Mathf.Lerp(OverheatTemperature, 0, t); - if (t >= 1) + if (_overheated) // Если оружие перегрето + { + _cooldownTime += Time.deltaTime; // Увеличиваем время остывания + float t = _cooldownTime / (OverheatCooldown / 10); // Вычисляем прогресс остывания (от 0 до 1) + _temperature = Mathf.Lerp(OverheatTemperature, 0, t); // Плавно уменьшаем температуру + + if (t >= 1) // Если остывание завершено { - _cooldownTime = 0; - _overheated = false; + _cooldownTime = 0; // Сбрасываем таймер остывания + _overheated = false; // Снимаем флаг перегрева } } } + // Вспомогательный метод получения текущей температуры как целого числа private int GetTemperature() { - if(_overheated) return (int) OverheatTemperature; - else return (int)_temperature; + if (_overheated) // Если оружие перегрето + return (int)OverheatTemperature; // Возвращаем максимальную температуру (3) + else // Если не перегрето + return (int)_temperature; // Возвращаем текущую температуру } + // Вспомогательный метод увеличения температуры после выстрела private void IncreaseTemperature() { - _temperature += 1f; - if (_temperature >= OverheatTemperature) _overheated = true; + _temperature += 1f; // Увеличиваем температуру на 1 градус + if (_temperature >= OverheatTemperature) // Если достигнут порог перегрева + _overheated = true; // Устанавливаем флаг перегрева + } + + // Вспомогательный метод проверки проходимости клетки + private bool IsCellWalkable(Vector2Int pos) + { + // Проверяем, что клетка в пределах карты + if (pos.x < 0 || pos.x >= runtimeModel.RoMap.Width || + pos.y < 0 || pos.y >= runtimeModel.RoMap.Height) + return false; + + // Проверяем, что клетка не занята стеной + if (runtimeModel.RoMap[pos]) + return false; + + // Клетка проходима (не проверяем занятость юнитами для конечной точки) + return true; } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs new file mode 100644 index 000000000..aa9507cab --- /dev/null +++ b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs @@ -0,0 +1,256 @@ +using Model; +using Model.Runtime.Projectiles; +using System.Collections.Generic; +using UnityEngine; +using Utilities; +using UnitBrains.Pathfinding; // Импорт для использования AStarUnitPath + +namespace UnitBrains.Player +{ + public class ThirdUnitBrain : DefaultPlayerUnitBrain + { + public override string TargetUnitName => "Ironclad Behemoth"; // Имя юнита для идентификации + + // Параметры для реализации задержки атаки после движения + private float _attackDelay = 1f; // Задержка перед атакой после остановки (1 секунда) + private float _timeSinceLastMovement = 0f; // Время, прошедшее с момента последнего движения + private bool _isMoving = false; // Флаг, указывающий движется ли юнит в данный момент + + // Список целей вне зоны досягаемости - юнит будет двигаться к ним используя A* + List _outOfRangeTargets = new List(); + + // Статический счетчик для присвоения уникальных номеров юнитам этого типа + private static int _unitCounter = 0; + // Уникальный номер текущего экземпляра юнита + private int _unitNumber; + // Максимальное количество целей для "умного" распределения между юнитами + private const int MaxTargetsForSmartSelection = 3; + + // Конструктор класса - вызывается при создании каждого юнита + public ThirdUnitBrain() + { + _unitNumber = _unitCounter; // Присваиваем текущий номер из счетчика + _unitCounter++; // Увеличиваем счетчик для следующего юнита + } + + // Метод генерации снарядов при атаке + // forTarget - цель для выстрела + // intoList - список для добавления созданных снарядов + protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) + { + // Всегда создаем 3 снаряда (максимальная огневая мощь Behemoth) + int projectileCount = 3; + + // Цикл создания указанного количества снарядов + for (int i = 0; i < projectileCount; i++) + { + var projectile = CreateProjectile(forTarget); // Создаем снаряд, направленный в цель + AddProjectileToList(projectile, intoList); // Добавляем снаряд в выходной список + } + } + + // Метод определения следующего шага движения юнита с использованием A* + public override Vector2Int GetNextStep() + { + // Проверяем, есть ли цели для движения (вне зоны атаки) + if (_outOfRangeTargets.Count > 0) + { + _isMoving = true; // Устанавливаем флаг движения + Vector2Int target = _outOfRangeTargets[0]; // Берем первую цель из списка + + // Если мы уже на цели, останавливаемся + if (unit.Pos == target) + { + _isMoving = false; // Сбрасываем флаг движения + return unit.Pos; // Остаемся на месте + } + + // Создаем A* путь от текущей позиции до цели + ActivePath = new AStarUnitPath(runtimeModel, unit.Pos, target); + + // Получаем следующий шаг по вычисленному пути + Vector2Int nextStep = ActivePath.GetNextStepFrom(unit.Pos); + + // Проверяем валидность шага (не больше 1 клетки) + Vector2Int delta = nextStep - unit.Pos; + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + // Если шаг невалидный, используем прямое движение + Debug.LogWarning($"ThirdUnitBrain: invalid A* step from {unit.Pos} to {nextStep}, using fallback"); + nextStep = unit.Pos.CalcNextStepTowards(target); + } + + // Проверяем проходимость выбранной клетки + if (!IsCellWalkable(nextStep)) + { + Debug.LogWarning($"ThirdUnitBrain: cell {nextStep} not walkable, finding alternative"); + + // Ищем альтернативный шаг среди соседних клеток + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Проверяем вверх + Vector2Int.right, // Проверяем вправо + Vector2Int.down, // Проверяем вниз + Vector2Int.left // Проверяем влево + }; + + bool foundAlternative = false; // Флаг нахождения альтернативы + foreach (Vector2Int direction in directions) + { + Vector2Int alternativeStep = unit.Pos + direction; + if (IsCellWalkable(alternativeStep)) // Если клетка проходима + { + nextStep = alternativeStep; // Используем альтернативу + foundAlternative = true; // Устанавливаем флаг + break; // Выходим из цикла + } + } + + if (!foundAlternative) // Если альтернатива не найдена + { + Debug.LogWarning($"ThirdUnitBrain: no alternative step from {unit.Pos}, staying in place"); + _isMoving = false; // Сбрасываем флаг движения + return unit.Pos; // Остаемся на месте + } + } + + return nextStep; // Возвращаем валидный следующий шаг + } + + // Если целей для движения нет, стоим на месте + _isMoving = false; // Сбрасываем флаг движения + return unit.Pos; // Возвращаем текущую позицию (юнит не двигается) + } + + // Метод выбора целей для атаки с учетом задержки после движения + protected override List SelectTargets() + { + // Очищаем список целей для движения + _outOfRangeTargets.Clear(); + + // Получаем все доступные цели + List allTargets = new List(GetAllTargets()); + List result = new List(); // Результирующий список целей для атаки + + // БЛОКИРОВКА АТАКИ ВО ВРЕМЯ ДВИЖЕНИЯ + // Если юнит движется или прошло меньше секунды после остановки + if (_isMoving || _timeSinceLastMovement < _attackDelay) + { + // Если есть цели на поле + if (allTargets.Count > 0) + { + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели для движения к ним + } + else + { + // Если целей нет, движемся к базе противника + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу как цель для движения + } + + return result; // Возвращаем пустой список - атака заблокирована + } + // === КОНЕЦ БЛОКИРОВКИ АТАКИ === + + // Если целей на поле нет + if (allTargets.Count == 0) + { + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; + + if (IsTargetInRange(enemyBasePos)) // Если база в зоне досягаемости + { + result.Add(enemyBasePos); // Добавляем базу как цель для атаки + return result; + } + else // Если база вне зоны досягаемости + { + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу для движения к ней + return result; + } + } + + // Сортируем цели по расстоянию до нашей базы (приоритет - ближайшие) + SortByDistanceToOwnBase(allTargets); + + // Распределяем цели между юнитами для равномерной атаки + int targetIndex; + if (allTargets.Count <= MaxTargetsForSmartSelection) // Если целей мало (≤ 3) + { + targetIndex = _unitNumber % allTargets.Count; // Распределяем по номеру юнита + } + else // Если целей много (> 3) + { + targetIndex = _unitNumber % MaxTargetsForSmartSelection; // Берем остаток от деления на 3 + if (targetIndex >= allTargets.Count) // Если индекс за пределами списка + { + targetIndex = allTargets.Count - 1; // Корректируем на последний индекс + } + } + + // Получаем выбранную цель по индексу + Vector2Int selectedTarget = allTargets[targetIndex]; + + // Проверяем, находится ли цель в зоне досягаемости + if (IsTargetInRange(selectedTarget)) + { + result.Add(selectedTarget); // Добавляем для атаки + } + else + { + _outOfRangeTargets.Add(selectedTarget); // Добавляем для движения + + // Ищем первую цель в зоне досягаемости из всего списка + foreach (Vector2Int target in allTargets) + { + if (IsTargetInRange(target)) // Если цель доступна + { + result.Add(target); // Добавляем для атаки + break; // Выходим из цикла + } + } + + // Если ни одна цель не доступна для атаки + if (result.Count == 0) + { + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели для движения + } + } + + return result; // Возвращаем список целей для атаки + } + + // Метод обновления состояния юнита (вызывается каждый кадр) + public override void Update(float deltaTime, float time) + { + base.Update(deltaTime, time); // Вызываем базовый метод обновления + + // Обновляем таймер после движения + if (!_isMoving) // Если юнит не движется + { + _timeSinceLastMovement += Time.deltaTime; // Увеличиваем время с момента остановки + } + else // Если юнит движется + { + _timeSinceLastMovement = 0f; // Сбрасываем таймер при движении + } + } + + // Вспомогательный метод проверки проходимости клетки + private bool IsCellWalkable(Vector2Int pos) + { + // Проверяем, что клетка в пределах карты + if (pos.x < 0 || pos.x >= runtimeModel.RoMap.Width || + pos.y < 0 || pos.y >= runtimeModel.RoMap.Height) + return false; + + // Проверяем, что клетка не занята стеной + if (runtimeModel.RoMap[pos]) + return false; + + // Клетка проходима + return true; + } + } +} \ No newline at end of file