using System; using System.Collections.Generic; using GeometryTD.Definition; using GeometryTD.Entity; using GeometryTD.Entity.EntityData; using UnityEngine; using UnityEngine.Tilemaps; namespace GeometryTD.Map { public sealed class TowerPlacementService { private const int DefaultTowerTypeId = 401; private const int MinTowerLevel = 0; private const int MaxTowerLevel = 4; private readonly Dictionary _towerEntityIdByFoundationCell = new(); private readonly Dictionary _foundationCellByTowerEntityId = new(); private readonly Dictionary _towerStatsByEntityId = new(); private readonly Dictionary _towerLevelByEntityId = new(); private readonly List _towerEntityIdBuffer = new(); public IReadOnlyDictionary TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell; public IReadOnlyDictionary FoundationCellByTowerEntityId => _foundationCellByTowerEntityId; public bool IsTowerAtMaxLevel(int towerEntityId) { if (towerEntityId == 0) { return false; } DefenseTowerStatsData towerStats = _towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats) ? cachedStats : null; int currentLevel = GetTowerLevel(towerEntityId); int maxLevel = ResolveMaxTowerLevel(towerStats); return currentLevel >= maxLevel; } public bool TryBuildTower(Vector3Int foundationCell, Func isFoundationCell, int buildIndex, int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func tryConsumeCoin, Action addCoin, out int towerEntityId) { towerEntityId = 0; if (isFoundationCell == null || !isFoundationCell(foundationCell)) { return false; } if (_towerEntityIdByFoundationCell.ContainsKey(foundationCell)) { return false; } int buildCost = GetBuildTowerCost(buildTowerCosts, buildIndex); if (tryConsumeCoin != null && !tryConsumeCoin(buildCost)) { return false; } DefenseTowerStatsData towerStats = BuildTowerStats(buildIndex); if (!TryShowTowerEntity(foundationCell, towerStats, towerTypeId, tilemap, out int newTowerEntityId)) { addCoin?.Invoke(buildCost); return false; } _towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId; _foundationCellByTowerEntityId[newTowerEntityId] = foundationCell; _towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats); _towerLevelByEntityId[newTowerEntityId] = MinTowerLevel; towerEntityId = newTowerEntityId; return true; } public bool TryUpgradeTower(int towerEntityId, int upgradeCost, int towerTypeId, Tilemap tilemap, Func tryConsumeCoin, Action addCoin, out int resultTowerEntityId, out Vector3Int foundationCell) { resultTowerEntityId = 0; foundationCell = default; if (towerEntityId == 0 || !_foundationCellByTowerEntityId.TryGetValue(towerEntityId, out foundationCell)) { return false; } DefenseTowerStatsData towerStats = _towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats) ? CloneTowerStats(cachedStats) : BuildTowerStats(0); int currentTowerLevel = GetTowerLevel(towerEntityId); int maxTowerLevel = ResolveMaxTowerLevel(towerStats); if (currentTowerLevel >= maxTowerLevel) { resultTowerEntityId = towerEntityId; return false; } int requiredUpgradeCost = Mathf.Max(0, upgradeCost); if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost)) { resultTowerEntityId = towerEntityId; return false; } int nextTowerLevel = Mathf.Clamp(currentTowerLevel + 1, MinTowerLevel, maxTowerLevel); if (!TryApplyTowerStats(towerEntityId, towerStats, nextTowerLevel)) { addCoin?.Invoke(requiredUpgradeCost); resultTowerEntityId = towerEntityId; return false; } _towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats); _towerLevelByEntityId[towerEntityId] = nextTowerLevel; resultTowerEntityId = towerEntityId; return true; } public bool TryDestroyTower(int towerEntityId, int destroyGain, Action addCoin, out Vector3Int foundationCell) { foundationCell = default; if (towerEntityId == 0 || !_foundationCellByTowerEntityId.TryGetValue(towerEntityId, out foundationCell)) { return false; } HideTowerEntity(towerEntityId); _towerEntityIdByFoundationCell.Remove(foundationCell); _foundationCellByTowerEntityId.Remove(towerEntityId); _towerStatsByEntityId.Remove(towerEntityId); _towerLevelByEntityId.Remove(towerEntityId); addCoin?.Invoke(Mathf.Max(0, destroyGain)); return true; } public void HideAndClearAllPlacedTowers() { _towerEntityIdBuffer.Clear(); foreach (KeyValuePair pair in _foundationCellByTowerEntityId) { _towerEntityIdBuffer.Add(pair.Key); } for (int i = 0; i < _towerEntityIdBuffer.Count; i++) { HideTowerEntity(_towerEntityIdBuffer[i]); } _towerEntityIdByFoundationCell.Clear(); _foundationCellByTowerEntityId.Clear(); _towerStatsByEntityId.Clear(); _towerLevelByEntityId.Clear(); _towerEntityIdBuffer.Clear(); } public void ClearTracking() { _towerEntityIdByFoundationCell.Clear(); _foundationCellByTowerEntityId.Clear(); _towerStatsByEntityId.Clear(); _towerLevelByEntityId.Clear(); _towerEntityIdBuffer.Clear(); } private static int GetBuildTowerCost(int[] buildTowerCosts, int buildIndex) { if (buildTowerCosts == null || buildIndex < 0 || buildIndex >= buildTowerCosts.Length) { return 0; } return Mathf.Max(0, buildTowerCosts[buildIndex]); } private static bool TryShowTowerEntity(Vector3Int foundationCell, DefenseTowerStatsData towerStats, int towerTypeId, Tilemap tilemap, out int towerEntityId) { towerEntityId = 0; if (GameEntry.Entity == null) { return false; } int entityId = GameEntry.Entity.GenerateSerialId(); int typeId = towerTypeId > 0 ? towerTypeId : DefaultTowerTypeId; Vector3 towerPosition = tilemap != null ? tilemap.GetCellCenterWorld(foundationCell) : foundationCell; towerPosition.z = 0f; var towerData = new DefenseTowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats, MinTowerLevel); GameEntry.Entity.ShowDefenseTower(towerData); towerEntityId = entityId; return true; } private static void HideTowerEntity(int towerEntityId) { if (towerEntityId == 0 || GameEntry.Entity == null) { return; } UnityGameFramework.Runtime.Entity towerEntity = GameEntry.Entity.GetEntity(towerEntityId); if (towerEntity != null) { GameEntry.Entity.HideEntity(towerEntity); } } private static DefenseTowerStatsData BuildTowerStats(int buildIndex) { switch (buildIndex) { case 0: return new DefenseTowerStatsData { AttackDamage = new[] { 200, 220, 240, 260, 300 }, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f }, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Physics, Tags = Array.Empty() }; case 1: return new DefenseTowerStatsData { AttackDamage = new[] { 200, 220, 240, 260, 300 }, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f }, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Fire, Tags = Array.Empty() }; case 2: return new DefenseTowerStatsData { AttackDamage = new[] { 200, 220, 240, 260, 300 }, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f }, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Ice, Tags = Array.Empty() }; case 3: return new DefenseTowerStatsData { AttackDamage = new[] { 200, 220, 240, 260, 300 }, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f }, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Poison, Tags = Array.Empty() }; default: return new DefenseTowerStatsData { AttackDamage = new[] { 200, 220, 240, 260, 300 }, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f }, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f }, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f }, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Physics, Tags = Array.Empty() }; } } private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source) { if (source == null) { return BuildTowerStats(0); } TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty(); return new DefenseTowerStatsData { AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty(), DamageRandomRate = source.DamageRandomRate != null ? (float[])source.DamageRandomRate.Clone() : Array.Empty(), RotateSpeed = source.RotateSpeed != null ? (float[])source.RotateSpeed.Clone() : Array.Empty(), AttackRange = source.AttackRange != null ? (float[])source.AttackRange.Clone() : Array.Empty(), AttackSpeed = source.AttackSpeed != null ? (float[])source.AttackSpeed.Clone() : Array.Empty(), AttackMethodType = source.AttackMethodType, AttackPropertyType = source.AttackPropertyType, Tags = copiedTags }; } private int GetTowerLevel(int towerEntityId) { if (towerEntityId == 0 || !_towerLevelByEntityId.TryGetValue(towerEntityId, out int towerLevel)) { return MinTowerLevel; } return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel); } private static int ResolveMaxTowerLevel(DefenseTowerStatsData stats) { int maxCount = Mathf.Max( GetLength(stats?.AttackDamage), GetLength(stats?.DamageRandomRate), GetLength(stats?.RotateSpeed), GetLength(stats?.AttackRange), GetLength(stats?.AttackSpeed)); if (maxCount <= 0) { return MinTowerLevel; } return Mathf.Clamp(maxCount - 1, MinTowerLevel, MaxTowerLevel); } private static bool TryApplyTowerStats(int towerEntityId, DefenseTowerStatsData towerStats, int towerLevel) { if (towerEntityId == 0 || towerStats == null || GameEntry.Entity == null) { return false; } if (GameEntry.Entity.GetGameEntity(towerEntityId) is not DefenseTowerEntity towerEntity) { return false; } return towerEntity.TryApplyStats(towerStats, towerLevel); } private static int GetLength(T[] values) { return values != null ? values.Length : 0; } } }