diff --git a/Assets/Resources/PlayerUnits/PlayerUnit3.prefab b/Assets/Resources/PlayerUnits/PlayerUnit3.prefab index 234d5b6a1..034b86c0d 100644 --- a/Assets/Resources/PlayerUnits/PlayerUnit3.prefab +++ b/Assets/Resources/PlayerUnits/PlayerUnit3.prefab @@ -67,7 +67,7 @@ MonoBehaviour: _maxHealth: 400 _brainUpdateDelay: 0.25 _moveDelay: 0.25 - _attackDelay: 0.75 + _attackDelay: 0.15 _attackRange: 3.5 _shotsPerTarget: 1 _targetsInVolley: 1 diff --git a/Assets/Scripts/Controller/LevelController.cs b/Assets/Scripts/Controller/LevelController.cs index d98cfa5d6..48c62ccfa 100644 --- a/Assets/Scripts/Controller/LevelController.cs +++ b/Assets/Scripts/Controller/LevelController.cs @@ -43,6 +43,7 @@ public void StartLevel(int level) var density = Random.Range(_settings.MapMinDensity, _settings.MapMaxDensity); var map = MapGenerator.Generate(_settings.MapWidth, _settings.MapHeight, density, level); _runtimeModel.Clear(); + UnitBrains.ArmyBrain.Reset(); _runtimeModel.Map = new Map(map, Settings.PlayersCount); _runtimeModel.Stage = RuntimeModel.GameStage.ChooseUnit; _runtimeModel.Bases[RuntimeModel.PlayerId] = new MainBase(_settings.MainBaseMaxHp); diff --git a/Assets/Scripts/EnterPoint.cs b/Assets/Scripts/EnterPoint.cs index 6f474a574..1f0eca643 100644 --- a/Assets/Scripts/EnterPoint.cs +++ b/Assets/Scripts/EnterPoint.cs @@ -10,7 +10,7 @@ public class EnterPoint : MonoBehaviour { [SerializeField] private Settings _settings; [SerializeField] private Canvas _targetCanvas; - private float _timeScale = 1; + private float _timeScale = 5; void Start() { diff --git a/Assets/Scripts/Model/Runtime/Projectiles/ArchToTileProjectile.cs b/Assets/Scripts/Model/Runtime/Projectiles/ArchToTileProjectile.cs index c37ed0218..84e4d2d21 100644 --- a/Assets/Scripts/Model/Runtime/Projectiles/ArchToTileProjectile.cs +++ b/Assets/Scripts/Model/Runtime/Projectiles/ArchToTileProjectile.cs @@ -30,11 +30,13 @@ protected override void UpdateImpl(float deltaTime, float time) // Insert you code here /////////////////////////////////////// + float maxHeight = totalDistance * 0.6f; + localHeight = maxHeight * (((t * 2 - 1) * (t * 2 - 1) * -1) + 1); /////////////////////////////////////// // End of the code to insert /////////////////////////////////////// - + Height = localHeight; if (time > StartTime + _timeToTarget) Hit(_target); diff --git a/Assets/Scripts/UnitBrains/ArmyBrain.cs b/Assets/Scripts/UnitBrains/ArmyBrain.cs new file mode 100644 index 000000000..c55ea941c --- /dev/null +++ b/Assets/Scripts/UnitBrains/ArmyBrain.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Model; +using Model.Runtime.ReadOnly; +using UnityEngine; +using Utilities; + +namespace UnitBrains +{ + public class ArmyBrain + { + private readonly TimeUtil _timeUtil; + private readonly IReadOnlyRuntimeModel _runtimeModel; + + private static ArmyBrain _instance; + + private ArmyBrain() + { + _timeUtil = ServiceLocator.Get(); + _runtimeModel = ServiceLocator.Get(); + } + + public static ArmyBrain GetInstance() + { + if (_instance == null) + _instance = new ArmyBrain(); + return _instance; + } + + public static void Reset() => _instance = null; + + // Ближайший к нашей базе враг, если враги на нашей половине; + // иначе — враг с наименьшим HP. + public IReadOnlyUnit GetRecommendedTarget() + { + var enemies = _runtimeModel.RoBotUnits.ToList(); + if (!enemies.Any()) return null; + + var onOurHalf = EnemiesOnOurHalf(enemies); + if (onOurHalf.Any()) + return onOurHalf.OrderBy(e => DistanceToPlayerBase(e.Pos)).First(); + + return enemies.OrderBy(e => e.Health).First(); + } + + // Перед нашей базой, если враги на нашей половине; + // иначе — на расстоянии выстрела от ближайшего к базе врага. + public Vector2Int GetRecommendedPoint() + { + var enemies = _runtimeModel.RoBotUnits.ToList(); + var playerBase = _runtimeModel.RoMap.Bases[RuntimeModel.PlayerId]; + + if (!enemies.Any()) + return playerBase; + + var onOurHalf = EnemiesOnOurHalf(enemies); + if (onOurHalf.Any()) + { + var enemyBase = _runtimeModel.RoMap.Bases[RuntimeModel.BotPlayerId]; + var step = DirectionStep(playerBase, enemyBase); + return playerBase + step * 3; + } + + var nearest = enemies.OrderBy(e => DistanceToPlayerBase(e.Pos)).First(); + return PointAtRangeFrom(nearest.Pos, playerBase, attackRange: 3); + } + + private List EnemiesOnOurHalf(List enemies) + { + var playerBase = _runtimeModel.RoMap.Bases[RuntimeModel.PlayerId]; + int half = _runtimeModel.RoMap.Width / 2; + bool baseOnLeft = playerBase.x <= half; + return baseOnLeft + ? enemies.Where(e => e.Pos.x <= half).ToList() + : enemies.Where(e => e.Pos.x > half).ToList(); + } + + private float DistanceToPlayerBase(Vector2Int pos) + { + return Vector2Int.Distance(pos, _runtimeModel.RoMap.Bases[RuntimeModel.PlayerId]); + } + + private static Vector2Int DirectionStep(Vector2Int from, Vector2Int to) + { + var d = to - from; + return new Vector2Int( + d.x == 0 ? 0 : (d.x > 0 ? 1 : -1), + d.y == 0 ? 0 : (d.y > 0 ? 1 : -1) + ); + } + + private static Vector2Int PointAtRangeFrom(Vector2Int origin, Vector2Int toward, int attackRange) + { + var delta = toward - origin; + float dist = delta.magnitude; + if (dist < 1f) return origin; + return origin + new Vector2Int( + Mathf.RoundToInt(delta.x / dist * attackRange), + Mathf.RoundToInt(delta.y / dist * attackRange) + ); + } + } +} diff --git a/Assets/Scripts/UnitBrains/ArmyBrain.cs.meta b/Assets/Scripts/UnitBrains/ArmyBrain.cs.meta new file mode 100644 index 000000000..542032e20 --- /dev/null +++ b/Assets/Scripts/UnitBrains/ArmyBrain.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1b03cbef97454f2a8045ba8119a0eab2 +timeCreated: 1776883177 \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/BaseUnitBrain.cs b/Assets/Scripts/UnitBrains/BaseUnitBrain.cs index 513532dd0..532f09b2f 100644 --- a/Assets/Scripts/UnitBrains/BaseUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/BaseUnitBrain.cs @@ -39,7 +39,7 @@ public virtual Vector2Int GetNextStep() var target = runtimeModel.RoMap.Bases[ IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]; - _activePath = new DummyUnitPath(runtimeModel, unit.Pos, target); + _activePath = new AStarUnitPath(runtimeModel, unit.Pos, target); return _activePath.GetNextStepFrom(unit.Pos); } diff --git a/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs new file mode 100644 index 000000000..e410d8111 --- /dev/null +++ b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Model; +using UnityEngine; + +namespace UnitBrains.Pathfinding +{ + public class AStarUnitPath : BaseUnitPath + { + private static readonly Vector2Int[] Directions = + { + new Vector2Int( 1, 0), + new Vector2Int(-1, 0), + new Vector2Int( 0, 1), + new Vector2Int( 0,-1), + }; + + public AStarUnitPath(IReadOnlyRuntimeModel runtimeModel, Vector2Int startPoint, Vector2Int endPoint) + : base(runtimeModel, startPoint, endPoint) + { + } + + protected override void Calculate() + { + var goal = ResolveGoal(endPoint); + + if (startPoint == goal) + { + path = new[] { startPoint }; + return; + } + + if (startPoint == goal) + { + path = new[] { startPoint }; + return; + } + + var open = new List { startPoint }; + var openSet = new HashSet { startPoint }; + var closed = new HashSet(); + + var cameFrom = new Dictionary(); + + var gScore = new Dictionary { [startPoint] = 0 }; + var fScore = new Dictionary { [startPoint] = Heuristic(startPoint, goal) }; + + const int MAX_ITERS = 20000; + var iters = 0; + var bestSoFar = startPoint; + var bestSoFarH = Heuristic(startPoint, goal); + + while (open.Count > 0) + { + if (++iters > MAX_ITERS) + { + Debug.LogWarning($"AStar exceeded MAX_ITERS={MAX_ITERS}. Returning fallback path."); + path = new[] { startPoint }; + return; + } + + var current = SelectBest(open, fScore, goal); + var currentH = Heuristic(current, goal); + if (currentH < bestSoFarH) + { + bestSoFarH = currentH; + bestSoFar = current; + } + + if (current == goal) + { + path = ReconstructPath(cameFrom, current).ToArray(); + return; + } + + open.Remove(current); + openSet.Remove(current); + closed.Add(current); + + foreach (var dir in Directions) + { + var neighbor = current + dir; + + if (closed.Contains(neighbor)) + continue; + + if (!IsWalkableOrGoal(neighbor, goal)) + continue; + + var tentativeG = GetScore(gScore, current) + 1; + + if (!openSet.Contains(neighbor)) + { + open.Add(neighbor); + openSet.Add(neighbor); + } + else + { + if (tentativeG >= GetScore(gScore, neighbor)) + continue; + } + + cameFrom[neighbor] = current; + gScore[neighbor] = tentativeG; + fScore[neighbor] = tentativeG + Heuristic(neighbor, goal); + } + } + + if (bestSoFar != startPoint) + path = ReconstructPath(cameFrom, bestSoFar).ToArray(); + else + path = new[] { startPoint }; + } + + private bool IsOccupiedByUnit(Vector2Int cell) + { + foreach (var u in runtimeModel.RoUnits) + { + if (u.Pos == cell) + return true; + } + return false; + } + + private Vector2Int ResolveGoal(Vector2Int desired) + { + if (runtimeModel.IsTileWalkable(desired)) + return desired; + + var visited = new HashSet(); + var q = new Queue(); + + // стартуем не с desired (он непроходим), а с его соседей + foreach (var dir in Directions) + { + var n = desired + dir; + if (visited.Add(n)) + q.Enqueue(n); + } + + const int MAX_NODES = 5000; // защита + var processed = 0; + + while (q.Count > 0 && processed++ < MAX_NODES) + { + var cur = q.Dequeue(); + if (runtimeModel.IsTileWalkable(cur)) + return cur; + + foreach (var dir in Directions) + { + var n = cur + dir; + if (visited.Add(n)) + q.Enqueue(n); + } + } + + // если совсем нет проходимых — возвращаем старт (пути нет) + return startPoint; + } + + private bool IsWalkableOrGoal(Vector2Int cell, Vector2Int goal) + { + if (cell == startPoint) return true; + if (cell == goal) return true; + + if (!runtimeModel.IsTileWalkable(cell)) + return false; + + if (IsOccupiedByUnit(cell)) + return false; + + return true; + } + + private static int Heuristic(Vector2Int a, Vector2Int b) + => Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y); // Manhattan + + private static int GetScore(Dictionary scores, Vector2Int key) + => scores.TryGetValue(key, out var v) ? v : int.MaxValue / 4; + + private static Vector2Int SelectBest(List open, Dictionary fScore, Vector2Int goal) + { + var best = open[0]; + var bestF = GetScore(fScore, best); + var bestH = Heuristic(best, goal); + + for (int i = 1; i < open.Count; i++) + { + var v = open[i]; + var f = GetScore(fScore, v); + if (f > bestF) continue; + + var h = Heuristic(v, goal); + if (f < bestF || h < bestH) + { + best = v; + bestF = f; + bestH = h; + } + } + + return best; + } + + private static IEnumerable ReconstructPath(Dictionary cameFrom, Vector2Int current) + { + var stack = new Stack(); + stack.Push(current); + + while (cameFrom.TryGetValue(current, out var prev)) + { + current = prev; + stack.Push(current); + } + + while (stack.Count > 0) + yield return stack.Pop(); + } + } +} diff --git a/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs.meta b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs.meta new file mode 100644 index 000000000..0e7fd86b8 --- /dev/null +++ b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2320d6821a0a1ede9c9166f7191faa6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs b/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs index 3ffc5123b..c91b9ee88 100644 --- a/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs +++ b/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs @@ -32,8 +32,22 @@ public void HighlightPath(BaseUnitPath path) private IEnumerator HighlightCoroutine(BaseUnitPath path) { - // TODO Implement me - yield break; + // небольшая задержка, чтобы визуально было видно "прокладку" + var delay = new WaitForSeconds(0.03f); + + foreach (var cell in path.GetPath()) + { + CreateHighlight(cell); + + // ограничиваем количество подсветок + while (allHighlights.Count > maxHighlights) + { + DestroyHighlight(0); + } + + // подсветка "по кадрам" с небольшой задержкой + yield return delay; + } } private void CreateHighlight(Vector2Int atCell) diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index c2c80e989..d6f42855e 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -1,6 +1,10 @@ using System.Collections.Generic; using Model.Runtime.Projectiles; using UnityEngine; +using System.Linq; +using Model; // чтобы видеть RuntimeModel.PlayerId / BotPlayerId +using UnitBrains.Pathfinding; +using System.Collections.Generic; namespace UnitBrains.Player { @@ -12,21 +16,48 @@ public class SecondUnitBrain : DefaultPlayerUnitBrain private float _temperature = 0f; private float _cooldownTime = 0f; private bool _overheated; + private static int s_unitCounter = 0; + private int _unitNumber; + private const int MAX_SMART_TARGETS = 3; + + //private readonly System.Collections.Generic.List _pendingTargets = new System.Collections.Generic.List(); + private readonly List _pendingTargets = new List(); + private Vector2Int? _currentObjective; + + public SecondUnitBrain() { _unitNumber = s_unitCounter++; } + private void SortByDistanceToOwnBase(List list) + { + var myBase = runtimeModel.RoMap.Bases[IsPlayerUnitBrain ? RuntimeModel.PlayerId : RuntimeModel.BotPlayerId]; + list.Sort((a, b) => ((a - myBase).sqrMagnitude).CompareTo((b - myBase).sqrMagnitude)); + } + 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 currentTemperature = GetTemperature(); + if (currentTemperature >= overheatTemperature) { return; } + + for (float i = -1; i < currentTemperature; i++) + { + /////////////////////////////////////// + // Homework 1.3 (1st block, 3rd module) + /////////////////////////////////////// + var projectile = CreateProjectile(forTarget); + AddProjectileToList(projectile, intoList); + /////////////////////////////////////// + } + IncreaseTemperature(); } public override Vector2Int GetNextStep() { - return base.GetNextStep(); + if (_currentObjective == null && _pendingTargets.Count > 0) _currentObjective = _pendingTargets[0]; + if (_currentObjective == null) return unit.Pos; + if (IsTargetInRange(_currentObjective.Value)) return unit.Pos; + var path = new DummyUnitPath(runtimeModel, unit.Pos, _currentObjective.Value); + return path.GetNextStepFrom(unit.Pos); } protected override List SelectTargets() @@ -34,11 +65,21 @@ protected override List SelectTargets() /////////////////////////////////////// // Homework 1.4 (1st block, 4rd module) /////////////////////////////////////// - List result = GetReachableTargets(); - while (result.Count > 1) + _pendingTargets.Clear(); + var goals = new List(); + foreach (var t in GetAllTargets()) goals.Add(t); + if (goals.Count == 0) { - result.RemoveAt(result.Count - 1); + var enemyBase = runtimeModel.RoMap.Bases[IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]; + goals.Add(enemyBase); } + SortByDistanceToOwnBase(goals); + int idx = _unitNumber % MAX_SMART_TARGETS; + if (idx >= goals.Count) idx = 0; + var chosen = goals[idx]; + _currentObjective = chosen; + var result = new List(); + if (IsTargetInRange(chosen)) result.Add(chosen); else _pendingTargets.Add(chosen); return result; /////////////////////////////////////// } diff --git a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs new file mode 100644 index 000000000..1967f49ef --- /dev/null +++ b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using UnitBrains.Pathfinding; +using UnityEngine; + +namespace UnitBrains.Player +{ + public class ThirdUnitBrain : DefaultPlayerUnitBrain + { + public override string TargetUnitName => "Ironclad Behemoth"; + + private enum BrainMode { Move, Attack, Switching } + + private const float SwitchDuration = 1f; + private BrainMode _mode = BrainMode.Move; + private BrainMode _pendingMode = BrainMode.Move; + private float _switchTimer = 0f; + private bool hasTargets = false; + + private void BeginSwitch(BrainMode to) + { + _pendingMode = to; + _mode = BrainMode.Switching; + _switchTimer = SwitchDuration; + } + + public override void Update(float deltaTime, float time) + { + base.Update(deltaTime, time); + + if (_mode == BrainMode.Switching) + { + _switchTimer -= deltaTime; + if (_switchTimer <= 0f) + { + _mode = _pendingMode; + _switchTimer = 0f; + } + return; + } + + var desired = hasTargets ? BrainMode.Attack : BrainMode.Move; + + if (_mode != desired) + { + BeginSwitch(desired); + } + } + + protected override List SelectTargets() + { + var coordinator = ArmyBrain.GetInstance(); + var recommended = coordinator.GetRecommendedTarget(); + + if (recommended != null) + { + float twoRanges = unit.Config.AttackRange * 2f; + float dist = Vector2Int.Distance(unit.Pos, recommended.Pos); + if (dist <= twoRanges && IsTargetInRange(recommended.Pos)) + { + hasTargets = true; + return new List { recommended.Pos }; + } + } + + var result = base.SelectTargets(); + hasTargets = result.Count > 0; + if (_mode != BrainMode.Attack) + result.Clear(); + + return result; + } + + public override Vector2Int GetNextStep() + { + if (_mode != BrainMode.Move) + return unit.Pos; + + var target = ArmyBrain.GetInstance().GetRecommendedPoint(); + if (target == unit.Pos) + return unit.Pos; + + var path = new DummyUnitPath(runtimeModel, unit.Pos, target); + return path.GetNextStepFrom(unit.Pos); + } + } +} diff --git a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs.meta b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs.meta new file mode 100644 index 000000000..53320a370 --- /dev/null +++ b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7cb6cb646d154fa4b72229800b12347c +timeCreated: 1760474320 \ No newline at end of file diff --git a/Assets/Scripts/View/ChooseUnitView.cs b/Assets/Scripts/View/ChooseUnitView.cs index 8f6d741f5..53ae0da4c 100644 --- a/Assets/Scripts/View/ChooseUnitView.cs +++ b/Assets/Scripts/View/ChooseUnitView.cs @@ -26,6 +26,7 @@ private void Start() private void Update() { + if (_model == null) return; var visible = _model.Stage == RuntimeModel.GameStage.ChooseUnit; if (visible != _root.gameObject.activeSelf) _root.gameObject.SetActive(visible);