using System; using System.Collections.Generic; using GeometryTD.Definition; using GeometryTD.Entity.EntityData; using GeometryTD.Pathfinding; using UnityEngine; using UnityEngine.Tilemaps; using UnityGameFramework.Runtime; namespace GeometryTD.Map { public sealed class TowerPlacementService { private const int DefaultTowerTypeId = 401; private readonly Dictionary _towerEntityIdByFoundationCell = new(); private readonly Dictionary _foundationCellByTowerEntityId = new(); private readonly Dictionary _towerStatsByEntityId = new(); private readonly List _towerEntityIdBuffer = new(); public IReadOnlyDictionary TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell; public IReadOnlyDictionary FoundationCellByTowerEntityId => _foundationCellByTowerEntityId; 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); 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; } int requiredUpgradeCost = Mathf.Max(0, upgradeCost); if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost)) { return false; } DefenseTowerStatsData oldStats = _towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats) ? CloneTowerStats(cachedStats) : BuildTowerStats(0); DefenseTowerStatsData upgradedStats = CloneTowerStats(oldStats); ApplyUpgradeToStats(upgradedStats); HideTowerEntity(towerEntityId); _towerEntityIdByFoundationCell.Remove(foundationCell); _foundationCellByTowerEntityId.Remove(towerEntityId); _towerStatsByEntityId.Remove(towerEntityId); if (!TryShowTowerEntity(foundationCell, upgradedStats, towerTypeId, tilemap, out int newTowerEntityId)) { if (TryShowTowerEntity(foundationCell, oldStats, towerTypeId, tilemap, out int fallbackTowerEntityId)) { _towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId; _foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell; _towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats); resultTowerEntityId = fallbackTowerEntityId; } addCoin?.Invoke(requiredUpgradeCost); return false; } _towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId; _foundationCellByTowerEntityId[newTowerEntityId] = foundationCell; _towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats); resultTowerEntityId = newTowerEntityId; 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); 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(); _towerEntityIdBuffer.Clear(); } public void ClearTracking() { _towerEntityIdByFoundationCell.Clear(); _foundationCellByTowerEntityId.Clear(); _towerStatsByEntityId.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); 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 = 100, DamageRandomRate = 0f, RotateSpeed = 200f, AttackRange = 4.5f, AttackSpeed = 1.5f, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Physics, Tags = Array.Empty() }; case 1: return new DefenseTowerStatsData { AttackDamage = 13, DamageRandomRate = 0f, RotateSpeed = 160f, AttackRange = 5f, AttackSpeed = 1.2f, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Fire, Tags = Array.Empty() }; case 2: return new DefenseTowerStatsData { AttackDamage = 17, DamageRandomRate = 0f, RotateSpeed = 140f, AttackRange = 5.5f, AttackSpeed = 0.95f, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Ice, Tags = Array.Empty() }; case 3: return new DefenseTowerStatsData { AttackDamage = 22, DamageRandomRate = 0f, RotateSpeed = 120f, AttackRange = 6f, AttackSpeed = 0.75f, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Poison, Tags = Array.Empty() }; default: return new DefenseTowerStatsData { AttackDamage = 100, DamageRandomRate = 0f, RotateSpeed = 180f, AttackRange = 5f, AttackSpeed = 1f, 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, DamageRandomRate = source.DamageRandomRate, RotateSpeed = source.RotateSpeed, AttackRange = source.AttackRange, AttackSpeed = source.AttackSpeed, AttackMethodType = source.AttackMethodType, AttackPropertyType = source.AttackPropertyType, Tags = copiedTags }; } private static void ApplyUpgradeToStats(DefenseTowerStatsData stats) { if (stats == null) { return; } stats.AttackDamage = Mathf.Max(1, stats.AttackDamage + 3); stats.AttackSpeed = Mathf.Max(0.01f, stats.AttackSpeed + 0.2f); stats.AttackRange = Mathf.Max(0.1f, stats.AttackRange + 0.4f); stats.RotateSpeed = Mathf.Max(1f, stats.RotateSpeed + 10f); } } }