using System; using System.Collections.Generic; using GeometryTD; using GeometryTD.Definition; using GeometryTD.Entity.EntityData; using GeometryTD.Pathfinding; using GeometryTD.UI; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Tilemaps; using UnityGameFramework.Runtime; namespace GeometryTD.Entity { public class MapEntity : EntityBase { private const string PathTileName = "Path"; private const string FoundationTileName = "Foundation"; private const int DefaultTowerTypeId = 401; private static readonly Spawner[] EmptySpawners = Array.Empty(); private readonly List _pathCells = new(); private readonly List _foundationCells = new(); private readonly HashSet _pathCellSet = new(); private readonly HashSet _foundationCellSet = new(); private readonly IMapPathfinder _mapPathfinder = new GridMapPathfinder(); private readonly List _pathCellBuffer = new(); private readonly Dictionary _spawnerPathStartByRef = new(); private readonly Dictionary> _defaultPathCellsBySpawner = new(); private readonly Dictionary _towerEntityIdByFoundationCell = new(); private readonly Dictionary _foundationCellByTowerEntityId = new(); private readonly Dictionary _towerStatsByEntityId = new(); private readonly List _towerEntityIdBuffer = new(); [SerializeField] private bool _enableCombatSelectInput = true; [SerializeField] private int _towerTypeId = DefaultTowerTypeId; [SerializeField] private int[] _buildTowerCosts = { 80, 120, 160, 220 }; [SerializeField] private int _upgradeCost = 80; [SerializeField] private int _destroyGain = 40; private MapDataRefs _mapDataRefs; private MapData _mapData; private bool _hasHousePathCell; private Vector3Int _housePathCell; private CombatSelectFormUseCase _combatSelectFormUseCase; private bool _hasSelectedFoundationCell; private Vector3Int _selectedFoundationCell; private int _selectedTowerEntityId; public IReadOnlyList PathCells => _pathCells; public IReadOnlyList FoundationCells => _foundationCells; public Tilemap Tilemap => _mapDataRefs != null ? _mapDataRefs.Tilemap : null; public Spawner[] Spawners => _mapDataRefs?.Spawners ?? EmptySpawners; public House House => _mapDataRefs?.House; public bool IsPathCell(Vector3Int cellPosition) { return _pathCellSet.Contains(cellPosition); } public bool IsFoundationCell(Vector3Int cellPosition) { return _foundationCellSet.Contains(cellPosition); } public bool TryGetNearestPathCell(Vector3 worldPosition, out Vector3Int pathCell) { pathCell = default; if (_pathCells.Count <= 0 || Tilemap == null) { return false; } Vector3Int directCell = Tilemap.WorldToCell(worldPosition); if (_pathCellSet.Contains(directCell)) { pathCell = directCell; return true; } float minDistance = float.MaxValue; for (int i = 0; i < _pathCells.Count; i++) { Vector3Int candidate = _pathCells[i]; float distance = (Tilemap.GetCellCenterWorld(candidate) - worldPosition).sqrMagnitude; if (distance >= minDistance) { continue; } minDistance = distance; pathCell = candidate; } return minDistance < float.MaxValue; } public Vector3 GetPathCellCenterWorld(Vector3Int pathCell) { return Tilemap != null ? Tilemap.GetCellCenterWorld(pathCell) : Vector3.zero; } public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList pathCells) { pathCells = null; if (spawner == null) { return false; } if (!_defaultPathCellsBySpawner.TryGetValue(spawner, out List cachedPathCells)) { return false; } pathCells = cachedPathCells; return true; } public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection blockedCells, List pathResult) { if (pathResult == null) { return false; } pathResult.Clear(); if (spawner == null || !_hasHousePathCell) { return false; } if (!_spawnerPathStartByRef.TryGetValue(spawner, out Vector3Int startCell)) { return false; } return _mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, blockedCells, pathResult); } public bool TryFindPathWorldPoints(Spawner spawner, IReadOnlyCollection blockedCells, List worldPathResult) { if (worldPathResult == null) { return false; } worldPathResult.Clear(); if (Tilemap == null) { return false; } if (!TryFindPathCells(spawner, blockedCells, _pathCellBuffer)) { return false; } foreach (var pos in _pathCellBuffer) { worldPathResult.Add(GetPathCellCenterWorld(pos)); } return true; } protected override void OnInit(object userData) { base.OnInit(userData); _mapDataRefs = GetComponent(); if (_mapDataRefs == null) { Log.Error("MapDataRefs is missing on map entity '{0}'.", name); } InitializeCombatSelectUseCase(); } protected override void OnShow(object userData) { base.OnShow(userData); _mapData = userData as MapData; if (_mapData == null) { Log.Warning("MapData is invalid for map entity '{0}'.", Id); } RefreshTiles(); ConfigureCombatSelectUseCase(); HideCombatSelectForm(); } protected override void OnHide(bool isShutdown, object userData) { HideCombatSelectForm(); ClearPlacedTowers(); ClearRuntimeData(); base.OnHide(isShutdown, userData); } protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) { base.OnUpdate(elapseSeconds, realElapseSeconds); HandleCombatSelectInput(); } private void RefreshTiles() { ClearRuntimeData(); if (_mapDataRefs == null) { _mapDataRefs = GetComponent(); if (_mapDataRefs == null) { Log.Error("MapDataRefs is missing on map entity '{0}'.", name); return; } } Tilemap tilemap = _mapDataRefs.Tilemap; if (tilemap == null) { Log.Error("Tilemap reference is missing in MapDataRefs on '{0}'.", name); return; } BoundsInt bounds = tilemap.cellBounds; foreach (Vector3Int cellPosition in bounds.allPositionsWithin) { TileBase tile = tilemap.GetTile(cellPosition); if (tile == null || string.IsNullOrEmpty(tile.name)) { continue; } if (string.Equals(tile.name, PathTileName, StringComparison.Ordinal)) { _pathCells.Add(cellPosition); _pathCellSet.Add(cellPosition); continue; } if (string.Equals(tile.name, FoundationTileName, StringComparison.Ordinal)) { _foundationCells.Add(cellPosition); _foundationCellSet.Add(cellPosition); } } RefreshPathCache(); Log.Info( "Map '{0}' initialized. LevelId={1}, PathCells={2}, FoundationCells={3}, Spawners={4}, House={5}, Routes={6}.", name, _mapData != null ? _mapData.LevelId : 0, _pathCells.Count, _foundationCells.Count, Spawners.Length, House != null ? House.name : "None", _defaultPathCellsBySpawner.Count); } private void RefreshPathCache() { _hasHousePathCell = false; _housePathCell = default; _spawnerPathStartByRef.Clear(); _defaultPathCellsBySpawner.Clear(); if (House == null) { Log.Warning("Map '{0}' has no house reference, path cache skipped.", name); return; } if (!TryGetNearestPathCell(House.Position, out _housePathCell)) { Log.Warning("Map '{0}' house position can not map to a valid path cell.", name); return; } _hasHousePathCell = true; Spawner[] spawners = Spawners; foreach (var spawner in spawners) { if (spawner == null) { continue; } if (!TryGetNearestPathCell(spawner.Position, out Vector3Int startCell)) { Log.Warning("Map '{0}' spawner '{1}' can not map to a valid path cell.", name, spawner.name); continue; } _spawnerPathStartByRef[spawner] = startCell; List defaultPathCells = new List(); if (!_mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, null, defaultPathCells)) { Log.Warning( "Map '{0}' spawner '{1}' has no path to house cell {2}.", name, spawner.name, _housePathCell); continue; } _defaultPathCellsBySpawner[spawner] = defaultPathCells; } } private void ClearRuntimeData() { _pathCells.Clear(); _foundationCells.Clear(); _pathCellSet.Clear(); _foundationCellSet.Clear(); _pathCellBuffer.Clear(); _hasHousePathCell = false; _housePathCell = default; _spawnerPathStartByRef.Clear(); _defaultPathCellsBySpawner.Clear(); _towerEntityIdByFoundationCell.Clear(); _foundationCellByTowerEntityId.Clear(); _towerStatsByEntityId.Clear(); _towerEntityIdBuffer.Clear(); ClearSelectedObject(); } private void InitializeCombatSelectUseCase() { if (_combatSelectFormUseCase == null) { _combatSelectFormUseCase = new CombatSelectFormUseCase(); } GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatSelectForm, _combatSelectFormUseCase); } private void ConfigureCombatSelectUseCase() { if (_combatSelectFormUseCase == null) { return; } _combatSelectFormUseCase.SetCoinProvider(GetCurrentCoin); for (int i = 0; i < 4; i++) { int buildIndex = i; _combatSelectFormUseCase.SetBuildAction( buildIndex, () => TryBuildTower(buildIndex), GetBuildTowerCost(buildIndex)); } _combatSelectFormUseCase.SetUpgradeAction( TryUpgradeTower, Mathf.Max(0, _upgradeCost)); _combatSelectFormUseCase.SetDestroyAction( TryDestroyTower, Mathf.Max(0, _destroyGain)); } private void HandleCombatSelectInput() { if (!_enableCombatSelectInput || !Input.GetMouseButtonDown(0)) { return; } if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) { return; } if (!TryBuildCombatSelectUserData(out CombatSelectFormUserData userData)) { userData = new CombatSelectFormUserData { ClickObjectType = CombatSelectClickObjectType.None }; } ApplySelectedObject(userData); GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData); } private bool TryBuildCombatSelectUserData(out CombatSelectFormUserData userData) { userData = null; if (Tilemap == null || !TryGetPointerWorldPosition(out Vector3 worldPosition, out Vector2 contentPosition)) { return false; } Vector3Int clickedCell = Tilemap.WorldToCell(worldPosition); CombatSelectClickObjectType clickObjectType = CombatSelectClickObjectType.None; int towerEntityId = 0; if (_towerEntityIdByFoundationCell.TryGetValue(clickedCell, out int occupiedTowerEntityId)) { clickObjectType = CombatSelectClickObjectType.Tower; towerEntityId = occupiedTowerEntityId; } else if (IsFoundationCell(clickedCell)) { clickObjectType = CombatSelectClickObjectType.Foundation; } userData = new CombatSelectFormUserData { ClickObjectType = clickObjectType, ContentPosition = contentPosition, WorldPosition = worldPosition, CellPosition = clickedCell, TowerEntityId = towerEntityId, UpgradeCost = Mathf.Max(0, _upgradeCost), DestroyGain = Mathf.Max(0, _destroyGain) }; return true; } private bool TryGetPointerWorldPosition(out Vector3 worldPosition, out Vector2 contentPosition) { worldPosition = Vector3.zero; contentPosition = Vector2.zero; Camera mainCamera = GameEntry.Scene != null ? GameEntry.Scene.MainCamera : Camera.main; if (mainCamera == null) { return false; } Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); float mapPlaneZ = Tilemap != null ? Tilemap.transform.position.z : CachedTransform.position.z; Vector3 planeNormal = mainCamera.transform.forward.sqrMagnitude > Mathf.Epsilon ? -mainCamera.transform.forward : Vector3.forward; Plane mapPlane = new Plane(planeNormal, new Vector3(0f, 0f, mapPlaneZ)); if (!mapPlane.Raycast(ray, out float enterDistance)) { return false; } worldPosition = ray.GetPoint(enterDistance); contentPosition = BuildContentPosition(Input.mousePosition); return true; } private static Vector2 BuildContentPosition(Vector3 pointerScreenPosition) { return new Vector2( pointerScreenPosition.x - Screen.width * 0.5f, pointerScreenPosition.y - Screen.height * 0.5f); } private void ApplySelectedObject(CombatSelectFormUserData userData) { if (userData == null) { ClearSelectedObject(); return; } switch (userData.ClickObjectType) { case CombatSelectClickObjectType.Foundation: _hasSelectedFoundationCell = true; _selectedFoundationCell = userData.CellPosition; _selectedTowerEntityId = 0; break; case CombatSelectClickObjectType.Tower: _hasSelectedFoundationCell = true; _selectedFoundationCell = userData.CellPosition; _selectedTowerEntityId = userData.TowerEntityId; break; default: ClearSelectedObject(); break; } } private bool TryBuildTower(int buildIndex) { if (!_hasSelectedFoundationCell || !IsFoundationCell(_selectedFoundationCell)) { return false; } if (_towerEntityIdByFoundationCell.ContainsKey(_selectedFoundationCell)) { return false; } int buildCost = GetBuildTowerCost(buildIndex); if (!TryConsumeCoin(buildCost)) { return false; } DefenseTowerStatsData towerStats = BuildTowerStats(buildIndex); if (!TryShowTowerEntity(_selectedFoundationCell, towerStats, out int towerEntityId)) { GameEntry.CombatNode?.AddCoin(buildCost); return false; } _towerEntityIdByFoundationCell[_selectedFoundationCell] = towerEntityId; _foundationCellByTowerEntityId[towerEntityId] = _selectedFoundationCell; _towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats); _selectedTowerEntityId = towerEntityId; return true; } private bool TryUpgradeTower() { if (!TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell)) { return false; } int upgradeCost = Mathf.Max(0, _upgradeCost); if (!TryConsumeCoin(upgradeCost)) { 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, out int newTowerEntityId)) { if (TryShowTowerEntity(foundationCell, oldStats, out int fallbackTowerEntityId)) { _towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId; _foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell; _towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats); _selectedTowerEntityId = fallbackTowerEntityId; } GameEntry.CombatNode?.AddCoin(upgradeCost); return false; } _towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId; _foundationCellByTowerEntityId[newTowerEntityId] = foundationCell; _towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats); _hasSelectedFoundationCell = true; _selectedFoundationCell = foundationCell; _selectedTowerEntityId = newTowerEntityId; return true; } private bool TryDestroyTower() { if (!TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell)) { return false; } HideTowerEntity(towerEntityId); _towerEntityIdByFoundationCell.Remove(foundationCell); _foundationCellByTowerEntityId.Remove(towerEntityId); _towerStatsByEntityId.Remove(towerEntityId); GameEntry.CombatNode?.AddCoin(Mathf.Max(0, _destroyGain)); ClearSelectedObject(); return true; } private bool TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell) { towerEntityId = 0; foundationCell = default; if (_selectedTowerEntityId == 0) { return false; } if (!_foundationCellByTowerEntityId.TryGetValue(_selectedTowerEntityId, out foundationCell)) { return false; } towerEntityId = _selectedTowerEntityId; return true; } private bool TryShowTowerEntity(Vector3Int foundationCell, DefenseTowerStatsData towerStats, 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 void ClearPlacedTowers() { _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(); ClearSelectedObject(); } private void HideCombatSelectForm() { _combatSelectFormUseCase?.Hide(); GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm); } private void ClearSelectedObject() { _hasSelectedFoundationCell = false; _selectedFoundationCell = default; _selectedTowerEntityId = 0; } private int GetBuildTowerCost(int buildIndex) { if (_buildTowerCosts == null || buildIndex < 0 || buildIndex >= _buildTowerCosts.Length) { return 0; } return Mathf.Max(0, _buildTowerCosts[buildIndex]); } private static bool TryConsumeCoin(int cost) { int requiredCoin = Mathf.Max(0, cost); if (requiredCoin <= 0) { return true; } if (GameEntry.CombatNode == null) { return false; } return GameEntry.CombatNode.TryConsumeCoin(requiredCoin); } private static int GetCurrentCoin() { return GameEntry.CombatNode != null ? Mathf.Max(0, GameEntry.CombatNode.CurrentCoin) : 0; } private static DefenseTowerStatsData BuildTowerStats(int buildIndex) { switch (buildIndex) { case 0: return new DefenseTowerStatsData { AttackDamage = 10, 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 = 10, 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); } } }