拆分 MapEntity 职责
- TowerSelectionPresenter:选中态 + 攻击范围展示 - CombatSelectInputService:点击事件逻辑(位置计算、点击对象识别、组装 CombatSelectFormUserData) - TowerPlacementService:建造/升级/销毁与塔映射管理 - MapTopologyService:维护地图拓扑结构与寻路缓存
This commit is contained in:
parent
344191a91c
commit
5c7501d4fb
|
|
@ -1,33 +1,33 @@
|
||||||
# Id 列1 SpawnPointId StartTime EntryType EnemyId Count Interval Duration Gap
|
# Id 列1 SpawnPointId StartTime EntryType EnemyId Count Interval Duration Gap
|
||||||
# int int int EntryType int int float int float
|
# int int int EntryType int int float int float
|
||||||
# 阶段条目号 策划备注 敌人出生口Id 相对时间 条目类型 敌人Id 单次出怪数量 出怪间隔 持续时间 单怪出生时间间隔
|
# 阶段条目号 策划备注 敌人出生口Id 相对时间 条目类型 敌人Id 单次出怪数量 出怪间隔 持续时间 单怪出生时间间隔
|
||||||
1001001 1 5 Stream 1 3 5 60 0
|
1001001 1 5 Stream 1 3 5 60 0.5
|
||||||
1001002 2 5 Burst 1 10 0 0 0.5
|
1001002 2 5 Burst 1 10 0 0 0.2
|
||||||
1002001 1 3 Stream 1 3 5 60 0
|
1002001 1 3 Stream 1 3 5 60 0.5
|
||||||
1002002 2 3 Burst 1 10 0 0 0.5
|
1002002 2 3 Burst 1 10 0 0 0.2
|
||||||
1003001 1 5 Stream 1 3 5 60 0
|
1003001 1 5 Stream 1 3 5 60 0.5
|
||||||
1003002 2 5 Burst 1 10 0 0 0.5
|
1003002 2 5 Burst 1 10 0 0 0.2
|
||||||
1004001 1 3 Stream 1 3 5 60 0
|
1004001 1 3 Stream 1 3 5 60 0.5
|
||||||
1004002 2 3 Burst 1 10 0 0 0.5
|
1004002 2 3 Burst 1 10 0 0 0.2
|
||||||
1005001 1 5 Stream 1 3 5 60 0
|
1005001 1 5 Stream 1 3 5 60 0.5
|
||||||
1005002 2 5 Burst 1 10 0 0 0.5
|
1005002 2 5 Burst 1 10 0 0 0.2
|
||||||
2001001 1 5 Stream 1 3 5 60 0
|
2001001 1 5 Stream 1 3 5 60 0.5
|
||||||
2002001 1 5 Burst 1 10 0 0 0.5
|
2002001 1 5 Burst 1 10 0 0 0.2
|
||||||
2003001 1 5 Stream 1 3 5 60 0
|
2003001 1 5 Stream 1 3 5 60 0.5
|
||||||
2004001 1 5 Burst 1 10 0 0 0.5
|
2004001 1 5 Burst 1 10 0 0 0.2
|
||||||
2005001 1 5 Stream 1 3 5 60 0
|
2005001 1 5 Stream 1 3 5 60 0.5
|
||||||
3001001 1 5 Stream 1 3 5 60 0
|
3001001 1 5 Stream 1 3 5 60 0.5
|
||||||
3001002 2 5 Burst 1 10 0 0 0.5
|
3001002 2 5 Burst 1 10 0 0 0.2
|
||||||
3002001 1 3 Stream 1 3 5 60 0
|
3002001 1 3 Stream 1 3 5 60 0.5
|
||||||
3002002 2 3 Burst 1 10 0 0 0.5
|
3002002 2 3 Burst 1 10 0 0 0.2
|
||||||
3003001 1 5 Stream 1 3 5 60 0
|
3003001 1 5 Stream 1 3 5 60 0.5
|
||||||
3003002 2 5 Burst 1 10 0 0 0.5
|
3003002 2 5 Burst 1 10 0 0 0.2
|
||||||
3004001 1 3 Stream 1 3 5 60 0
|
3004001 1 3 Stream 1 3 5 60 0.5
|
||||||
3004002 2 3 Burst 1 10 0 0 0.5
|
3004002 2 3 Burst 1 10 0 0 0.2
|
||||||
3005001 1 5 Stream 1 3 5 60 0
|
3005001 1 5 Stream 1 3 5 60 0.5
|
||||||
3005002 2 5 Burst 1 10 0 0 0.5
|
3005002 2 5 Burst 1 10 0 0 0.2
|
||||||
4001001 1 5 Stream 1 3 5 60 0
|
4001001 1 5 Stream 1 3 5 60 0.5
|
||||||
4002001 1 5 Burst 1 10 0 0 0.5
|
4002001 1 5 Burst 1 10 0 0 0.2
|
||||||
4003001 1 5 Stream 1 3 5 60 0
|
4003001 1 5 Stream 1 3 5 60 0.5
|
||||||
4004001 1 5 Burst 1 10 0 0 0.5
|
4004001 1 5 Burst 1 10 0 0 0.2
|
||||||
4005001 1 5 Boss 2 1 5 60 0
|
4005001 1 5 Boss 2 1 5 60 0
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using GeometryTD.DataTable;
|
||||||
using GeometryTD.Definition;
|
using GeometryTD.Definition;
|
||||||
using GeometryTD.Entity;
|
using GeometryTD.Entity;
|
||||||
using GeometryTD.Entity.EntityData;
|
using GeometryTD.Entity.EntityData;
|
||||||
|
using GeometryTD.Map;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityGameFramework.Runtime;
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GeometryTD.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Tilemaps;
|
||||||
|
|
||||||
|
namespace GeometryTD.Entity
|
||||||
|
{
|
||||||
|
public sealed class CombatSelectInputService
|
||||||
|
{
|
||||||
|
public bool TryBuildUserData(Tilemap tilemap, Transform mapTransform,
|
||||||
|
IReadOnlyDictionary<Vector3Int, int> towerEntityIdByFoundationCell, Func<Vector3Int, bool> isFoundationCell,
|
||||||
|
int upgradeCost, int destroyGain, out CombatSelectFormUserData userData)
|
||||||
|
{
|
||||||
|
userData = null;
|
||||||
|
if (tilemap == null || !TryGetPointerWorldPosition(tilemap, mapTransform, out Vector3 worldPosition,
|
||||||
|
out Vector2 contentPosition))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3Int clickedCell = tilemap.WorldToCell(worldPosition);
|
||||||
|
CombatSelectClickObjectType clickObjectType = CombatSelectClickObjectType.None;
|
||||||
|
int towerEntityId = 0;
|
||||||
|
Vector2 resolvedContentPosition = contentPosition;
|
||||||
|
|
||||||
|
if (towerEntityIdByFoundationCell != null &&
|
||||||
|
towerEntityIdByFoundationCell.TryGetValue(clickedCell, out int occupiedTowerEntityId))
|
||||||
|
{
|
||||||
|
clickObjectType = CombatSelectClickObjectType.Tower;
|
||||||
|
towerEntityId = occupiedTowerEntityId;
|
||||||
|
resolvedContentPosition = BuildContentPositionFromCell(tilemap, clickedCell, contentPosition);
|
||||||
|
}
|
||||||
|
else if (isFoundationCell != null && isFoundationCell(clickedCell))
|
||||||
|
{
|
||||||
|
clickObjectType = CombatSelectClickObjectType.Foundation;
|
||||||
|
resolvedContentPosition = BuildContentPositionFromCell(tilemap, clickedCell, contentPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
userData = new CombatSelectFormUserData
|
||||||
|
{
|
||||||
|
ClickObjectType = clickObjectType,
|
||||||
|
ContentPosition = resolvedContentPosition,
|
||||||
|
WorldPosition = worldPosition,
|
||||||
|
CellPosition = clickedCell,
|
||||||
|
TowerEntityId = towerEntityId,
|
||||||
|
UpgradeCost = Mathf.Max(0, upgradeCost),
|
||||||
|
DestroyGain = Mathf.Max(0, destroyGain)
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetPointerWorldPosition(Tilemap tilemap, Transform mapTransform, 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 : (mapTransform != null ? mapTransform.position.z : 0f);
|
||||||
|
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);
|
||||||
|
contentPosition = new Vector2((int)contentPosition.x, (int)contentPosition.y);
|
||||||
|
contentPosition = new Vector2(contentPosition.x + 0.5f, contentPosition.y + 0.5f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 BuildContentPosition(Vector3 pointerScreenPosition)
|
||||||
|
{
|
||||||
|
return new Vector2(
|
||||||
|
pointerScreenPosition.x - Screen.width * 0.5f,
|
||||||
|
pointerScreenPosition.y - Screen.height * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 BuildContentPositionFromCell(Tilemap tilemap, Vector3Int cellPosition,
|
||||||
|
Vector2 fallbackContentPosition)
|
||||||
|
{
|
||||||
|
if (tilemap == null)
|
||||||
|
{
|
||||||
|
return fallbackContentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera mainCamera = GameEntry.Scene != null ? GameEntry.Scene.MainCamera : Camera.main;
|
||||||
|
if (mainCamera == null)
|
||||||
|
{
|
||||||
|
return fallbackContentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 cellCenterWorld = tilemap.GetCellCenterWorld(cellPosition);
|
||||||
|
Vector3 cellScreenPosition = mainCamera.WorldToScreenPoint(cellCenterWorld);
|
||||||
|
Vector2 contentPosition = BuildContentPosition(cellScreenPosition);
|
||||||
|
contentPosition = new Vector2((int)contentPosition.x, (int)contentPosition.y);
|
||||||
|
return new Vector2(contentPosition.x + 0.5f, contentPosition.y + 0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 44a338771c89a5348b00b9b0c13c1907
|
guid: 4fb7517aceac0814685c7260ac02ae6f
|
||||||
timeCreated: 1528026156
|
|
||||||
licenseType: Pro
|
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
defaultReferences: []
|
defaultReferences: []
|
||||||
executionOrder: 0
|
executionOrder: 0
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GeometryTD;
|
using GeometryTD.Map;
|
||||||
using GeometryTD.Definition;
|
using GeometryTD.Definition;
|
||||||
using GeometryTD.Entity.EntityData;
|
using GeometryTD.Entity.EntityData;
|
||||||
using GeometryTD.Pathfinding;
|
|
||||||
using GeometryTD.UI;
|
using GeometryTD.UI;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
|
|
@ -14,24 +13,9 @@ namespace GeometryTD.Entity
|
||||||
{
|
{
|
||||||
public class MapEntity : EntityBase
|
public class MapEntity : EntityBase
|
||||||
{
|
{
|
||||||
private const string PathTileName = "Path";
|
|
||||||
private const string FoundationTileName = "Foundation";
|
|
||||||
private const int DefaultTowerTypeId = 401;
|
private const int DefaultTowerTypeId = 401;
|
||||||
private static readonly Spawner[] EmptySpawners = Array.Empty<Spawner>();
|
private static readonly Spawner[] EmptySpawners = Array.Empty<Spawner>();
|
||||||
|
|
||||||
private readonly List<Vector3Int> _pathCells = new();
|
|
||||||
private readonly List<Vector3Int> _foundationCells = new();
|
|
||||||
private readonly HashSet<Vector3Int> _pathCellSet = new();
|
|
||||||
private readonly HashSet<Vector3Int> _foundationCellSet = new();
|
|
||||||
private readonly IMapPathfinder _mapPathfinder = new GridMapPathfinder();
|
|
||||||
private readonly List<Vector3Int> _pathCellBuffer = new();
|
|
||||||
private readonly Dictionary<Spawner, Vector3Int> _spawnerPathStartByRef = new();
|
|
||||||
private readonly Dictionary<Spawner, List<Vector3Int>> _defaultPathCellsBySpawner = new();
|
|
||||||
private readonly Dictionary<Vector3Int, int> _towerEntityIdByFoundationCell = new();
|
|
||||||
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
|
|
||||||
private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new();
|
|
||||||
private readonly List<int> _towerEntityIdBuffer = new();
|
|
||||||
|
|
||||||
[SerializeField] private bool _enableCombatSelectInput = true;
|
[SerializeField] private bool _enableCombatSelectInput = true;
|
||||||
[SerializeField] private int _towerTypeId = DefaultTowerTypeId;
|
[SerializeField] private int _towerTypeId = DefaultTowerTypeId;
|
||||||
[SerializeField] private int[] _buildTowerCosts = { 80, 120, 160, 220 };
|
[SerializeField] private int[] _buildTowerCosts = { 80, 120, 160, 220 };
|
||||||
|
|
@ -40,131 +24,62 @@ namespace GeometryTD.Entity
|
||||||
|
|
||||||
private MapDataRefs _mapDataRefs;
|
private MapDataRefs _mapDataRefs;
|
||||||
private MapData _mapData;
|
private MapData _mapData;
|
||||||
private bool _hasHousePathCell;
|
private MapTopologyService _mapTopologyService;
|
||||||
private Vector3Int _housePathCell;
|
|
||||||
private CombatSelectFormUseCase _combatSelectFormUseCase;
|
private CombatSelectFormUseCase _combatSelectFormUseCase;
|
||||||
private bool _hasSelectedFoundationCell;
|
private CombatSelectInputService _combatSelectInputService;
|
||||||
private Vector3Int _selectedFoundationCell;
|
private TowerPlacementService _towerPlacementService;
|
||||||
private int _selectedTowerEntityId;
|
private TowerSelectionPresenter _towerSelectionPresenter;
|
||||||
private int _attackRangeVisibleTowerEntityId;
|
|
||||||
|
|
||||||
public IReadOnlyList<Vector3Int> PathCells => _pathCells;
|
public IReadOnlyList<Vector3Int> PathCells => _mapTopologyService != null
|
||||||
public IReadOnlyList<Vector3Int> FoundationCells => _foundationCells;
|
? _mapTopologyService.PathCells
|
||||||
|
: Array.Empty<Vector3Int>();
|
||||||
|
public IReadOnlyList<Vector3Int> FoundationCells => _mapTopologyService != null
|
||||||
|
? _mapTopologyService.FoundationCells
|
||||||
|
: Array.Empty<Vector3Int>();
|
||||||
public Tilemap Tilemap => _mapDataRefs != null ? _mapDataRefs.Tilemap : null;
|
public Tilemap Tilemap => _mapDataRefs != null ? _mapDataRefs.Tilemap : null;
|
||||||
public Spawner[] Spawners => _mapDataRefs?.Spawners ?? EmptySpawners;
|
public Spawner[] Spawners => _mapDataRefs?.Spawners ?? EmptySpawners;
|
||||||
public House House => _mapDataRefs?.House;
|
public House House => _mapDataRefs?.House;
|
||||||
|
|
||||||
public bool IsPathCell(Vector3Int cellPosition)
|
public bool IsPathCell(Vector3Int cellPosition)
|
||||||
{
|
{
|
||||||
return _pathCellSet.Contains(cellPosition);
|
return _mapTopologyService != null && _mapTopologyService.IsPathCell(cellPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsFoundationCell(Vector3Int cellPosition)
|
public bool IsFoundationCell(Vector3Int cellPosition)
|
||||||
{
|
{
|
||||||
return _foundationCellSet.Contains(cellPosition);
|
return _mapTopologyService != null && _mapTopologyService.IsFoundationCell(cellPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetNearestPathCell(Vector3 worldPosition, out Vector3Int pathCell)
|
public bool TryGetNearestPathCell(Vector3 worldPosition, out Vector3Int pathCell)
|
||||||
{
|
{
|
||||||
pathCell = default;
|
pathCell = default;
|
||||||
if (_pathCells.Count <= 0 || Tilemap == null)
|
return _mapTopologyService != null && _mapTopologyService.TryGetNearestPathCell(Tilemap, worldPosition, out pathCell);
|
||||||
{
|
|
||||||
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)
|
public Vector3 GetPathCellCenterWorld(Vector3Int pathCell)
|
||||||
{
|
{
|
||||||
return Tilemap != null ? Tilemap.GetCellCenterWorld(pathCell) : Vector3.zero;
|
return _mapTopologyService != null
|
||||||
|
? _mapTopologyService.GetPathCellCenterWorld(Tilemap, pathCell)
|
||||||
|
: Vector3.zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList<Vector3Int> pathCells)
|
public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList<Vector3Int> pathCells)
|
||||||
{
|
{
|
||||||
pathCells = null;
|
pathCells = null;
|
||||||
if (spawner == null)
|
return _mapTopologyService != null && _mapTopologyService.TryGetDefaultPathCells(spawner, out pathCells);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_defaultPathCellsBySpawner.TryGetValue(spawner, out List<Vector3Int> cachedPathCells))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pathCells = cachedPathCells;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection<Vector3Int> blockedCells,
|
public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection<Vector3Int> blockedCells,
|
||||||
List<Vector3Int> pathResult)
|
List<Vector3Int> pathResult)
|
||||||
{
|
{
|
||||||
if (pathResult == null)
|
return _mapTopologyService != null && _mapTopologyService.TryFindPathCells(spawner, blockedCells, pathResult);
|
||||||
{
|
|
||||||
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<Vector3Int> blockedCells,
|
public bool TryFindPathWorldPoints(Spawner spawner, IReadOnlyCollection<Vector3Int> blockedCells,
|
||||||
List<Vector3> worldPathResult)
|
List<Vector3> worldPathResult)
|
||||||
{
|
{
|
||||||
if (worldPathResult == null)
|
return _mapTopologyService != null &&
|
||||||
{
|
_mapTopologyService.TryFindPathWorldPoints(Tilemap, spawner, blockedCells, worldPathResult);
|
||||||
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)
|
protected override void OnInit(object userData)
|
||||||
|
|
@ -178,6 +93,10 @@ namespace GeometryTD.Entity
|
||||||
}
|
}
|
||||||
|
|
||||||
InitializeCombatSelectUseCase();
|
InitializeCombatSelectUseCase();
|
||||||
|
InitializeCombatSelectInputService();
|
||||||
|
InitializeMapTopologyService();
|
||||||
|
InitializeTowerPlacementService();
|
||||||
|
InitializeTowerSelectionPresenter();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnShow(object userData)
|
protected override void OnShow(object userData)
|
||||||
|
|
@ -198,7 +117,7 @@ namespace GeometryTD.Entity
|
||||||
protected override void OnHide(bool isShutdown, object userData)
|
protected override void OnHide(bool isShutdown, object userData)
|
||||||
{
|
{
|
||||||
HideCombatSelectForm();
|
HideCombatSelectForm();
|
||||||
ClearPlacedTowers();
|
_towerPlacementService?.HideAndClearAllPlacedTowers();
|
||||||
ClearRuntimeData();
|
ClearRuntimeData();
|
||||||
base.OnHide(isShutdown, userData);
|
base.OnHide(isShutdown, userData);
|
||||||
}
|
}
|
||||||
|
|
@ -230,108 +149,14 @@ namespace GeometryTD.Entity
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BoundsInt bounds = tilemap.cellBounds;
|
_mapTopologyService?.Refresh(tilemap, Spawners, House, name, _mapData != null ? _mapData.LevelId : 0);
|
||||||
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<Vector3Int> defaultPathCells = new List<Vector3Int>();
|
|
||||||
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()
|
private void ClearRuntimeData()
|
||||||
{
|
{
|
||||||
_pathCells.Clear();
|
_mapTopologyService?.Clear();
|
||||||
_foundationCells.Clear();
|
_towerPlacementService?.ClearTracking();
|
||||||
_pathCellSet.Clear();
|
_towerSelectionPresenter?.ClearSelectedObject();
|
||||||
_foundationCellSet.Clear();
|
|
||||||
_pathCellBuffer.Clear();
|
|
||||||
_hasHousePathCell = false;
|
|
||||||
_housePathCell = default;
|
|
||||||
_spawnerPathStartByRef.Clear();
|
|
||||||
_defaultPathCellsBySpawner.Clear();
|
|
||||||
_towerEntityIdByFoundationCell.Clear();
|
|
||||||
_foundationCellByTowerEntityId.Clear();
|
|
||||||
_towerStatsByEntityId.Clear();
|
|
||||||
_towerEntityIdBuffer.Clear();
|
|
||||||
ClearSelectedObject();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeCombatSelectUseCase()
|
private void InitializeCombatSelectUseCase()
|
||||||
|
|
@ -344,6 +169,38 @@ namespace GeometryTD.Entity
|
||||||
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatSelectForm, _combatSelectFormUseCase);
|
GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatSelectForm, _combatSelectFormUseCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeTowerSelectionPresenter()
|
||||||
|
{
|
||||||
|
if (_towerSelectionPresenter == null)
|
||||||
|
{
|
||||||
|
_towerSelectionPresenter = new TowerSelectionPresenter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeTowerPlacementService()
|
||||||
|
{
|
||||||
|
if (_towerPlacementService == null)
|
||||||
|
{
|
||||||
|
_towerPlacementService = new TowerPlacementService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeCombatSelectInputService()
|
||||||
|
{
|
||||||
|
if (_combatSelectInputService == null)
|
||||||
|
{
|
||||||
|
_combatSelectInputService = new CombatSelectInputService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeMapTopologyService()
|
||||||
|
{
|
||||||
|
if (_mapTopologyService == null)
|
||||||
|
{
|
||||||
|
_mapTopologyService = new MapTopologyService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ConfigureCombatSelectUseCase()
|
private void ConfigureCombatSelectUseCase()
|
||||||
{
|
{
|
||||||
if (_combatSelectFormUseCase == null)
|
if (_combatSelectFormUseCase == null)
|
||||||
|
|
@ -382,7 +239,10 @@ namespace GeometryTD.Entity
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryBuildCombatSelectUserData(out CombatSelectFormUserData userData))
|
if (_combatSelectInputService == null ||
|
||||||
|
!_combatSelectInputService.TryBuildUserData(Tilemap, CachedTransform,
|
||||||
|
_towerPlacementService != null ? _towerPlacementService.TowerEntityIdByFoundationCell : null,
|
||||||
|
IsFoundationCell, _upgradeCost, _destroyGain, out CombatSelectFormUserData userData))
|
||||||
{
|
{
|
||||||
userData = new CombatSelectFormUserData
|
userData = new CombatSelectFormUserData
|
||||||
{
|
{
|
||||||
|
|
@ -394,334 +254,86 @@ namespace GeometryTD.Entity
|
||||||
GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, 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)
|
private void ApplySelectedObject(CombatSelectFormUserData userData)
|
||||||
{
|
{
|
||||||
if (userData == null)
|
_towerSelectionPresenter?.ApplySelectedObject(userData);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryBuildTower(int buildIndex)
|
private bool TryBuildTower(int buildIndex)
|
||||||
{
|
{
|
||||||
if (!_hasSelectedFoundationCell || !IsFoundationCell(_selectedFoundationCell))
|
if (_towerSelectionPresenter == null ||
|
||||||
|
_towerPlacementService == null ||
|
||||||
|
!_towerSelectionPresenter.TryGetSelectedFoundationCell(out Vector3Int selectedFoundationCell) ||
|
||||||
|
!IsFoundationCell(selectedFoundationCell))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_towerEntityIdByFoundationCell.ContainsKey(_selectedFoundationCell))
|
if (!_towerPlacementService.TryBuildTower(selectedFoundationCell, IsFoundationCell, buildIndex, _buildTowerCosts,
|
||||||
|
_towerTypeId, Tilemap, TryConsumeCoin, AddCoin, out int towerEntityId))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int buildCost = GetBuildTowerCost(buildIndex);
|
_towerSelectionPresenter.SelectTower(selectedFoundationCell, towerEntityId);
|
||||||
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;
|
|
||||||
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryUpgradeTower()
|
private bool TryUpgradeTower()
|
||||||
{
|
{
|
||||||
if (!TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell))
|
if (_towerSelectionPresenter == null ||
|
||||||
|
_towerPlacementService == null ||
|
||||||
|
!_towerSelectionPresenter.TryGetSelectedTower(_towerPlacementService.FoundationCellByTowerEntityId,
|
||||||
|
out int towerEntityId,
|
||||||
|
out Vector3Int foundationCell))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int upgradeCost = Mathf.Max(0, _upgradeCost);
|
if (!_towerPlacementService.TryUpgradeTower(towerEntityId, _upgradeCost, _towerTypeId, Tilemap,
|
||||||
if (!TryConsumeCoin(upgradeCost))
|
TryConsumeCoin, AddCoin, out int resultTowerEntityId, out foundationCell))
|
||||||
{
|
{
|
||||||
return false;
|
if (resultTowerEntityId != 0)
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
_towerSelectionPresenter.SelectTower(foundationCell, resultTowerEntityId);
|
||||||
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
|
|
||||||
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
|
|
||||||
_selectedTowerEntityId = fallbackTowerEntityId;
|
|
||||||
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UpdateTowerAttackRangeDisplay(0);
|
_towerSelectionPresenter.ClearSelectedObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
GameEntry.CombatNode?.AddCoin(upgradeCost);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
|
_towerSelectionPresenter.SelectTower(foundationCell, resultTowerEntityId);
|
||||||
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
|
|
||||||
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats);
|
|
||||||
_hasSelectedFoundationCell = true;
|
|
||||||
_selectedFoundationCell = foundationCell;
|
|
||||||
_selectedTowerEntityId = newTowerEntityId;
|
|
||||||
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryDestroyTower()
|
private bool TryDestroyTower()
|
||||||
{
|
{
|
||||||
if (!TryGetSelectedTower(out int towerEntityId, out Vector3Int foundationCell))
|
if (_towerSelectionPresenter == null ||
|
||||||
|
_towerPlacementService == null ||
|
||||||
|
!_towerSelectionPresenter.TryGetSelectedTower(_towerPlacementService.FoundationCellByTowerEntityId,
|
||||||
|
out int towerEntityId,
|
||||||
|
out Vector3Int foundationCell))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HideTowerEntity(towerEntityId);
|
if (!_towerPlacementService.TryDestroyTower(towerEntityId, _destroyGain, AddCoin, out foundationCell))
|
||||||
_towerEntityIdByFoundationCell.Remove(foundationCell);
|
{
|
||||||
_foundationCellByTowerEntityId.Remove(towerEntityId);
|
return false;
|
||||||
_towerStatsByEntityId.Remove(towerEntityId);
|
}
|
||||||
GameEntry.CombatNode?.AddCoin(Mathf.Max(0, _destroyGain));
|
|
||||||
|
|
||||||
ClearSelectedObject();
|
_towerSelectionPresenter.ClearSelectedObject();
|
||||||
return true;
|
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<int, Vector3Int> 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()
|
private void HideCombatSelectForm()
|
||||||
{
|
{
|
||||||
_combatSelectFormUseCase?.Hide();
|
_combatSelectFormUseCase?.Hide();
|
||||||
GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm);
|
GameEntry.UIRouter.CloseUI(UIFormType.CombatSelectForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearSelectedObject()
|
|
||||||
{
|
|
||||||
UpdateTowerAttackRangeDisplay(0);
|
|
||||||
_hasSelectedFoundationCell = false;
|
|
||||||
_selectedFoundationCell = default;
|
|
||||||
_selectedTowerEntityId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateTowerAttackRangeDisplay(int towerEntityId)
|
|
||||||
{
|
|
||||||
if (_attackRangeVisibleTowerEntityId != 0 && _attackRangeVisibleTowerEntityId != towerEntityId)
|
|
||||||
{
|
|
||||||
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
|
|
||||||
_attackRangeVisibleTowerEntityId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (towerEntityId == 0)
|
|
||||||
{
|
|
||||||
if (_attackRangeVisibleTowerEntityId != 0)
|
|
||||||
{
|
|
||||||
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
|
|
||||||
_attackRangeVisibleTowerEntityId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SetTowerAttackRangeVisible(towerEntityId, true))
|
|
||||||
{
|
|
||||||
_attackRangeVisibleTowerEntityId = towerEntityId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool SetTowerAttackRangeVisible(int towerEntityId, bool visible)
|
|
||||||
{
|
|
||||||
if (towerEntityId == 0 || GameEntry.Entity == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
EntityBase gameEntity = GameEntry.Entity.GetGameEntity(towerEntityId);
|
|
||||||
if (gameEntity is not DefenseTowerEntity towerEntity)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
towerEntity.SetAttackRangeVisible(visible);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetBuildTowerCost(int buildIndex)
|
private int GetBuildTowerCost(int buildIndex)
|
||||||
{
|
{
|
||||||
if (_buildTowerCosts == null || buildIndex < 0 || buildIndex >= _buildTowerCosts.Length)
|
if (_buildTowerCosts == null || buildIndex < 0 || buildIndex >= _buildTowerCosts.Length)
|
||||||
|
|
@ -753,105 +365,14 @@ namespace GeometryTD.Entity
|
||||||
return GameEntry.CombatNode != null ? Mathf.Max(0, GameEntry.CombatNode.CurrentCoin) : 0;
|
return GameEntry.CombatNode != null ? Mathf.Max(0, GameEntry.CombatNode.CurrentCoin) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DefenseTowerStatsData BuildTowerStats(int buildIndex)
|
private static void AddCoin(int coin)
|
||||||
{
|
{
|
||||||
switch (buildIndex)
|
int amount = Mathf.Max(0, coin);
|
||||||
{
|
if (amount <= 0)
|
||||||
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<TagType>()
|
|
||||||
};
|
|
||||||
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<TagType>()
|
|
||||||
};
|
|
||||||
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<TagType>()
|
|
||||||
};
|
|
||||||
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<TagType>()
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return new DefenseTowerStatsData
|
|
||||||
{
|
|
||||||
AttackDamage = 100,
|
|
||||||
DamageRandomRate = 0f,
|
|
||||||
RotateSpeed = 180f,
|
|
||||||
AttackRange = 5f,
|
|
||||||
AttackSpeed = 1f,
|
|
||||||
AttackMethodType = AttackMethodType.NormalBullet,
|
|
||||||
AttackPropertyType = AttackPropertyType.Physics,
|
|
||||||
Tags = Array.Empty<TagType>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
|
|
||||||
{
|
|
||||||
if (source == null)
|
|
||||||
{
|
|
||||||
return BuildTowerStats(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
GameEntry.CombatNode?.AddCoin(amount);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: e0da7117df4943347ad5ceaff9f70602
|
|
||||||
folderAsset: yes
|
|
||||||
timeCreated: 1528026155
|
|
||||||
licenseType: Pro
|
|
||||||
DefaultImporter:
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
//------------------------------------------------------------
|
|
||||||
// Game Framework
|
|
||||||
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
|
|
||||||
// Homepage: https://gameframework.cn/
|
|
||||||
// Feedback: mailto:ellan@gameframework.cn
|
|
||||||
//------------------------------------------------------------
|
|
||||||
|
|
||||||
using GameFramework.Localization;
|
|
||||||
using System;
|
|
||||||
using System.Xml;
|
|
||||||
using UnityGameFramework.Runtime;
|
|
||||||
|
|
||||||
namespace GeometryTD
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// XML 格式的本地化辅助器。
|
|
||||||
/// </summary>
|
|
||||||
public class XmlLocalizationHelper : DefaultLocalizationHelper
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 解析字典。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dictionaryString">要解析的字典字符串。</param>
|
|
||||||
/// <param name="userData">用户自定义数据。</param>
|
|
||||||
/// <returns>是否解析字典成功。</returns>
|
|
||||||
public override bool ParseData(ILocalizationManager localizationManager, string dictionaryString, object userData)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string currentLanguage = GameEntry.Localization.Language.ToString();
|
|
||||||
XmlDocument xmlDocument = new XmlDocument();
|
|
||||||
xmlDocument.LoadXml(dictionaryString);
|
|
||||||
XmlNode xmlRoot = xmlDocument.SelectSingleNode("Dictionaries");
|
|
||||||
XmlNodeList xmlNodeDictionaryList = xmlRoot.ChildNodes;
|
|
||||||
for (int i = 0; i < xmlNodeDictionaryList.Count; i++)
|
|
||||||
{
|
|
||||||
XmlNode xmlNodeDictionary = xmlNodeDictionaryList.Item(i);
|
|
||||||
if (xmlNodeDictionary.Name != "Dictionary")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string language = xmlNodeDictionary.Attributes.GetNamedItem("Language").Value;
|
|
||||||
if (language != currentLanguage)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
XmlNodeList xmlNodeStringList = xmlNodeDictionary.ChildNodes;
|
|
||||||
for (int j = 0; j < xmlNodeStringList.Count; j++)
|
|
||||||
{
|
|
||||||
XmlNode xmlNodeString = xmlNodeStringList.Item(j);
|
|
||||||
if (xmlNodeString.Name != "String")
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
string key = xmlNodeString.Attributes.GetNamedItem("Key").Value;
|
|
||||||
string value = xmlNodeString.Attributes.GetNamedItem("Value").Value;
|
|
||||||
if (!localizationManager.AddRawString(key, value))
|
|
||||||
{
|
|
||||||
Log.Warning("Can not add raw string with key '{0}' which may be invalid or duplicate.", key);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
Log.Warning("Can not parse dictionary data with exception '{0}'.", exception.ToString());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -95,7 +95,7 @@ namespace GeometryTD.Procedure
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preload dictionaries
|
// Preload dictionaries
|
||||||
LoadDictionary("Default");
|
//LoadDictionary("Default");
|
||||||
|
|
||||||
// Preload fonts
|
// Preload fonts
|
||||||
LoadFont("MainFont");
|
LoadFont("MainFont");
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9ce5dda4e04e4785ae41eb9394a401e5
|
||||||
|
timeCreated: 1772418124
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace GeometryTD
|
namespace GeometryTD.Map
|
||||||
{
|
{
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public class House : MonoBehaviour
|
public class House : MonoBehaviour
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Tilemaps;
|
using UnityEngine.Tilemaps;
|
||||||
|
|
||||||
namespace GeometryTD
|
namespace GeometryTD.Map
|
||||||
{
|
{
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public class MapDataRefs : MonoBehaviour
|
public class MapDataRefs : MonoBehaviour
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GeometryTD.Pathfinding;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Tilemaps;
|
||||||
|
using UnityGameFramework.Runtime;
|
||||||
|
|
||||||
|
namespace GeometryTD.Map
|
||||||
|
{
|
||||||
|
public sealed class MapTopologyService
|
||||||
|
{
|
||||||
|
private const string PathTileName = "Path";
|
||||||
|
private const string FoundationTileName = "Foundation";
|
||||||
|
|
||||||
|
private readonly List<Vector3Int> _pathCells = new();
|
||||||
|
private readonly List<Vector3Int> _foundationCells = new();
|
||||||
|
private readonly HashSet<Vector3Int> _pathCellSet = new();
|
||||||
|
private readonly HashSet<Vector3Int> _foundationCellSet = new();
|
||||||
|
private readonly IMapPathfinder _mapPathfinder = new GridMapPathfinder();
|
||||||
|
private readonly List<Vector3Int> _pathCellBuffer = new();
|
||||||
|
private readonly Dictionary<Spawner, Vector3Int> _spawnerPathStartByRef = new();
|
||||||
|
private readonly Dictionary<Spawner, List<Vector3Int>> _defaultPathCellsBySpawner = new();
|
||||||
|
|
||||||
|
private bool _hasHousePathCell;
|
||||||
|
private Vector3Int _housePathCell;
|
||||||
|
|
||||||
|
public IReadOnlyList<Vector3Int> PathCells => _pathCells;
|
||||||
|
public IReadOnlyList<Vector3Int> FoundationCells => _foundationCells;
|
||||||
|
|
||||||
|
public void Refresh(Tilemap tilemap, Spawner[] spawners, House house, string mapName, int levelId)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
if (tilemap == null)
|
||||||
|
{
|
||||||
|
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(tilemap, spawners, house, mapName);
|
||||||
|
Log.Info(
|
||||||
|
"Map '{0}' initialized. LevelId={1}, PathCells={2}, FoundationCells={3}, Spawners={4}, House={5}, Routes={6}.",
|
||||||
|
mapName,
|
||||||
|
levelId,
|
||||||
|
_pathCells.Count,
|
||||||
|
_foundationCells.Count,
|
||||||
|
spawners != null ? spawners.Length : 0,
|
||||||
|
house != null ? house.name : "None",
|
||||||
|
_defaultPathCellsBySpawner.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_pathCells.Clear();
|
||||||
|
_foundationCells.Clear();
|
||||||
|
_pathCellSet.Clear();
|
||||||
|
_foundationCellSet.Clear();
|
||||||
|
_pathCellBuffer.Clear();
|
||||||
|
_hasHousePathCell = false;
|
||||||
|
_housePathCell = default;
|
||||||
|
_spawnerPathStartByRef.Clear();
|
||||||
|
_defaultPathCellsBySpawner.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPathCell(Vector3Int cellPosition)
|
||||||
|
{
|
||||||
|
return _pathCellSet.Contains(cellPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsFoundationCell(Vector3Int cellPosition)
|
||||||
|
{
|
||||||
|
return _foundationCellSet.Contains(cellPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetNearestPathCell(Tilemap tilemap, 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(Tilemap tilemap, Vector3Int pathCell)
|
||||||
|
{
|
||||||
|
return tilemap != null ? tilemap.GetCellCenterWorld(pathCell) : Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetDefaultPathCells(Spawner spawner, out IReadOnlyList<Vector3Int> pathCells)
|
||||||
|
{
|
||||||
|
pathCells = null;
|
||||||
|
if (spawner == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_defaultPathCellsBySpawner.TryGetValue(spawner, out List<Vector3Int> cachedPathCells))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pathCells = cachedPathCells;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryFindPathCells(Spawner spawner, IReadOnlyCollection<Vector3Int> blockedCells,
|
||||||
|
List<Vector3Int> 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(Tilemap tilemap, Spawner spawner,
|
||||||
|
IReadOnlyCollection<Vector3Int> blockedCells,
|
||||||
|
List<Vector3> worldPathResult)
|
||||||
|
{
|
||||||
|
if (worldPathResult == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
worldPathResult.Clear();
|
||||||
|
if (tilemap == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryFindPathCells(spawner, blockedCells, _pathCellBuffer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Vector3Int pos in _pathCellBuffer)
|
||||||
|
{
|
||||||
|
worldPathResult.Add(GetPathCellCenterWorld(tilemap, pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshPathCache(Tilemap tilemap, Spawner[] spawners, House house, string mapName)
|
||||||
|
{
|
||||||
|
_hasHousePathCell = false;
|
||||||
|
_housePathCell = default;
|
||||||
|
_spawnerPathStartByRef.Clear();
|
||||||
|
_defaultPathCellsBySpawner.Clear();
|
||||||
|
|
||||||
|
if (house == null)
|
||||||
|
{
|
||||||
|
Log.Warning("Map '{0}' has no house reference, path cache skipped.", mapName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetNearestPathCell(tilemap, house.Position, out _housePathCell))
|
||||||
|
{
|
||||||
|
Log.Warning("Map '{0}' house position can not map to a valid path cell.", mapName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasHousePathCell = true;
|
||||||
|
if (spawners == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Spawner spawner in spawners)
|
||||||
|
{
|
||||||
|
if (spawner == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetNearestPathCell(tilemap, spawner.Position, out Vector3Int startCell))
|
||||||
|
{
|
||||||
|
Log.Warning("Map '{0}' spawner '{1}' can not map to a valid path cell.", mapName, spawner.name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_spawnerPathStartByRef[spawner] = startCell;
|
||||||
|
|
||||||
|
List<Vector3Int> defaultPathCells = new();
|
||||||
|
if (!_mapPathfinder.TryFindPath(_pathCells, startCell, _housePathCell, null, defaultPathCells))
|
||||||
|
{
|
||||||
|
Log.Warning(
|
||||||
|
"Map '{0}' spawner '{1}' has no path to house cell {2}.",
|
||||||
|
mapName,
|
||||||
|
spawner.name,
|
||||||
|
_housePathCell);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultPathCellsBySpawner[spawner] = defaultPathCells;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ebb51464c2374165ada752b52283f3ae
|
||||||
|
timeCreated: 1772419747
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace GeometryTD
|
namespace GeometryTD.Map
|
||||||
{
|
{
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public class Spawner : MonoBehaviour
|
public class Spawner : MonoBehaviour
|
||||||
|
|
@ -0,0 +1,300 @@
|
||||||
|
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<Vector3Int, int> _towerEntityIdByFoundationCell = new();
|
||||||
|
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
|
||||||
|
private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new();
|
||||||
|
private readonly List<int> _towerEntityIdBuffer = new();
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<Vector3Int, int> TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell;
|
||||||
|
public IReadOnlyDictionary<int, Vector3Int> FoundationCellByTowerEntityId => _foundationCellByTowerEntityId;
|
||||||
|
|
||||||
|
public bool TryBuildTower(Vector3Int foundationCell, Func<Vector3Int, bool> isFoundationCell, int buildIndex,
|
||||||
|
int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func<int, bool> tryConsumeCoin,
|
||||||
|
Action<int> 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<int, bool> tryConsumeCoin, Action<int> 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<int> 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<int, Vector3Int> 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<TagType>()
|
||||||
|
};
|
||||||
|
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<TagType>()
|
||||||
|
};
|
||||||
|
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<TagType>()
|
||||||
|
};
|
||||||
|
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<TagType>()
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return new DefenseTowerStatsData
|
||||||
|
{
|
||||||
|
AttackDamage = 100,
|
||||||
|
DamageRandomRate = 0f,
|
||||||
|
RotateSpeed = 180f,
|
||||||
|
AttackRange = 5f,
|
||||||
|
AttackSpeed = 1f,
|
||||||
|
AttackMethodType = AttackMethodType.NormalBullet,
|
||||||
|
AttackPropertyType = AttackPropertyType.Physics,
|
||||||
|
Tags = Array.Empty<TagType>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
return BuildTowerStats(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 482aab8887db4be481c207cb9e7449d0
|
||||||
|
timeCreated: 1772419375
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GeometryTD.Entity;
|
||||||
|
using GeometryTD.UI;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace GeometryTD.Map
|
||||||
|
{
|
||||||
|
public sealed class TowerSelectionPresenter
|
||||||
|
{
|
||||||
|
private bool _hasSelectedFoundationCell;
|
||||||
|
private Vector3Int _selectedFoundationCell;
|
||||||
|
private int _selectedTowerEntityId;
|
||||||
|
private int _attackRangeVisibleTowerEntityId;
|
||||||
|
|
||||||
|
public 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();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectTower(Vector3Int foundationCell, int towerEntityId)
|
||||||
|
{
|
||||||
|
_hasSelectedFoundationCell = true;
|
||||||
|
_selectedFoundationCell = foundationCell;
|
||||||
|
_selectedTowerEntityId = towerEntityId;
|
||||||
|
UpdateTowerAttackRangeDisplay(_selectedTowerEntityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetSelectedFoundationCell(out Vector3Int foundationCell)
|
||||||
|
{
|
||||||
|
foundationCell = default;
|
||||||
|
if (!_hasSelectedFoundationCell)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundationCell = _selectedFoundationCell;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetSelectedTower(IReadOnlyDictionary<int, Vector3Int> foundationCellByTowerEntityId,
|
||||||
|
out int towerEntityId, out Vector3Int foundationCell)
|
||||||
|
{
|
||||||
|
towerEntityId = 0;
|
||||||
|
foundationCell = default;
|
||||||
|
if (_selectedTowerEntityId == 0 || foundationCellByTowerEntityId == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundationCellByTowerEntityId.TryGetValue(_selectedTowerEntityId, out foundationCell))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
towerEntityId = _selectedTowerEntityId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearSelectedObject()
|
||||||
|
{
|
||||||
|
UpdateTowerAttackRangeDisplay(0);
|
||||||
|
_hasSelectedFoundationCell = false;
|
||||||
|
_selectedFoundationCell = default;
|
||||||
|
_selectedTowerEntityId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTowerAttackRangeDisplay(int towerEntityId)
|
||||||
|
{
|
||||||
|
if (_attackRangeVisibleTowerEntityId != 0 && _attackRangeVisibleTowerEntityId != towerEntityId)
|
||||||
|
{
|
||||||
|
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
|
||||||
|
_attackRangeVisibleTowerEntityId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (towerEntityId == 0)
|
||||||
|
{
|
||||||
|
if (_attackRangeVisibleTowerEntityId != 0)
|
||||||
|
{
|
||||||
|
SetTowerAttackRangeVisible(_attackRangeVisibleTowerEntityId, false);
|
||||||
|
_attackRangeVisibleTowerEntityId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SetTowerAttackRangeVisible(towerEntityId, true))
|
||||||
|
{
|
||||||
|
_attackRangeVisibleTowerEntityId = towerEntityId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool SetTowerAttackRangeVisible(int towerEntityId, bool visible)
|
||||||
|
{
|
||||||
|
if (towerEntityId == 0 || GameEntry.Entity == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityBase gameEntity = GameEntry.Entity.GetGameEntity(towerEntityId);
|
||||||
|
if (gameEntity is not DefenseTowerEntity towerEntity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
towerEntity.SetAttackRangeVisible(visible);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 872f548158cd4474ea0cca599f5ae415
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -522,7 +522,7 @@ PrefabInstance:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11461470, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11461470, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_LocalizationHelperTypeName
|
propertyPath: m_LocalizationHelperTypeName
|
||||||
value: GeometryTD.XmlLocalizationHelper
|
value:
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 11463816, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
- target: {fileID: 11463816, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
propertyPath: m_LoadAssetCountPerFrame
|
propertyPath: m_LoadAssetCountPerFrame
|
||||||
|
|
@ -701,7 +701,8 @@ PrefabInstance:
|
||||||
value: GeometryTD.CustomUtility.JsonNetUtility
|
value: GeometryTD.CustomUtility.JsonNetUtility
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
m_RemovedComponents: []
|
m_RemovedComponents: []
|
||||||
m_RemovedGameObjects: []
|
m_RemovedGameObjects:
|
||||||
|
- {fileID: 109252, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
|
||||||
m_AddedGameObjects:
|
m_AddedGameObjects:
|
||||||
- targetCorrespondingSourceObject: {fileID: 430602, guid: adb3eb1c35fcff14f89fba7b05c9d71c,
|
- targetCorrespondingSourceObject: {fileID: 430602, guid: adb3eb1c35fcff14f89fba7b05c9d71c,
|
||||||
type: 3}
|
type: 3}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
# MapEntity 设计规范(开发约束)
|
||||||
|
|
||||||
|
最后更新:2026-03-02
|
||||||
|
|
||||||
|
## 1. 目标与边界
|
||||||
|
|
||||||
|
`MapEntity` 现在是战斗地图域的编排层(orchestrator),目标是:
|
||||||
|
- 对外暴露地图能力(格子查询、路径查询、建造操作入口)。
|
||||||
|
- 在 Unity 生命周期中初始化/清理各子服务。
|
||||||
|
- 承担输入与 UI 用例的连接,不承载具体业务算法细节。
|
||||||
|
|
||||||
|
`MapEntity` 不应再直接实现以下细节:
|
||||||
|
- Tile 扫描与路径缓存算法。
|
||||||
|
- 防御塔映射字典的增删改查细节。
|
||||||
|
- 选中状态与攻击范围显示细节。
|
||||||
|
- 鼠标拾取与 `CombatSelectFormUserData` 组装细节。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 模块划分(当前标准)
|
||||||
|
|
||||||
|
### 2.1 MapEntity(编排层)
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
- 生命周期编排:`OnInit / OnShow / OnUpdate / OnHide`
|
||||||
|
- 子服务初始化与清理
|
||||||
|
- UI 用例绑定(`CombatSelectFormUseCase`)
|
||||||
|
- 将输入结果分发到选择器/建造器
|
||||||
|
|
||||||
|
### 2.2 MapTopologyService(地图拓扑层)
|
||||||
|
|
||||||
|
当前文件:`Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs`(同文件内)
|
||||||
|
|
||||||
|
职责:
|
||||||
|
- 扫描 Tilemap,构建 `PathCells` / `FoundationCells`
|
||||||
|
- 缓存 `Spawner -> 默认路径`
|
||||||
|
- 提供路径查询:
|
||||||
|
- `TryGetNearestPathCell`
|
||||||
|
- `TryGetDefaultPathCells`
|
||||||
|
- `TryFindPathCells`
|
||||||
|
- `TryFindPathWorldPoints`
|
||||||
|
|
||||||
|
约束:
|
||||||
|
- 只处理“地图拓扑与路径”,不处理经济、建塔、UI。
|
||||||
|
|
||||||
|
### 2.3 CombatSelectInputService(输入解析层)
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectInputService.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
- 鼠标屏幕坐标 -> 世界坐标
|
||||||
|
- 判定点击对象类型(`None/Foundation/Tower`)
|
||||||
|
- 组装 `CombatSelectFormUserData`
|
||||||
|
|
||||||
|
约束:
|
||||||
|
- 只读上下文,不改变游戏状态。
|
||||||
|
- Foundation/Tower 点击时,UI 定位由 Cell 中心决定(稳定定位)。
|
||||||
|
|
||||||
|
### 2.4 TowerPlacementService(塔部署层)
|
||||||
|
|
||||||
|
当前文件:`Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
- 建造 / 升级 / 销毁(含升级失败回滚)
|
||||||
|
- 维护塔映射状态:
|
||||||
|
- `foundationCell -> towerEntityId`
|
||||||
|
- `towerEntityId -> foundationCell`
|
||||||
|
- `towerEntityId -> towerStats`
|
||||||
|
- 提供整局清理接口
|
||||||
|
|
||||||
|
约束:
|
||||||
|
- 仅处理塔生命周期与映射,不处理选中态和 UI。
|
||||||
|
|
||||||
|
### 2.5 TowerSelectionPresenter(选择展示层)
|
||||||
|
|
||||||
|
文件:`Assets/GameMain/Scripts/Scene/Map/TowerSelectionPresenter.cs`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
- 维护当前选中对象
|
||||||
|
- 根据选中状态切换攻击范围显示(通过 `DefenseTowerEntity.SetAttackRangeVisible`)
|
||||||
|
|
||||||
|
约束:
|
||||||
|
- 不做建造/升级/销毁。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 运行时主流程(简版)
|
||||||
|
|
||||||
|
1. `MapEntity.OnInit` 初始化 4 个服务:
|
||||||
|
- `MapTopologyService`
|
||||||
|
- `CombatSelectInputService`
|
||||||
|
- `TowerPlacementService`
|
||||||
|
- `TowerSelectionPresenter`
|
||||||
|
2. `MapEntity.OnShow` 刷新地图拓扑,配置 UI action。
|
||||||
|
3. 每帧 `OnUpdate`:
|
||||||
|
- 采集输入(InputService)
|
||||||
|
- 更新选中对象(SelectionPresenter)
|
||||||
|
- 打开/刷新 UI
|
||||||
|
4. UI Build/Upgrade/Destroy action 回调:
|
||||||
|
- 通过 PlacementService 改变塔状态
|
||||||
|
- 通过 SelectionPresenter 同步选中和范围显示
|
||||||
|
5. `OnHide`:
|
||||||
|
- 关闭 UI
|
||||||
|
- 清理塔实体
|
||||||
|
- 清理拓扑/选择/映射运行时状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 核心不变量(必须保持)
|
||||||
|
|
||||||
|
1. `MapEntity` 只编排,不承载业务细节实现。
|
||||||
|
2. `TowerPlacementService` 是塔映射状态的唯一写入口。
|
||||||
|
3. `MapTopologyService` 是 Path/Foundation 数据的唯一来源。
|
||||||
|
4. “当前可见攻击范围”同一时刻最多一个塔。
|
||||||
|
5. 清场顺序固定:先隐藏塔,再清空映射与选择,再清理拓扑缓存。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 扩展开发规范
|
||||||
|
|
||||||
|
### 5.1 新增地图规则(地形、禁建、动态阻挡)
|
||||||
|
|
||||||
|
优先改 `MapTopologyService`,不要直接改 `MapEntity`。
|
||||||
|
|
||||||
|
### 5.2 新增建造规则(费用、限制、回滚策略)
|
||||||
|
|
||||||
|
优先改 `TowerPlacementService`,不要在 `MapEntity` 写分支。
|
||||||
|
|
||||||
|
### 5.3 新增点击交互或 UI 定位策略
|
||||||
|
|
||||||
|
优先改 `CombatSelectInputService`。
|
||||||
|
|
||||||
|
### 5.4 新增选中表现(特效、描边、信息面板联动)
|
||||||
|
|
||||||
|
优先改 `TowerSelectionPresenter`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 代码变更检查清单(PR 自检)
|
||||||
|
|
||||||
|
1. 是否把新业务放进了对应服务,而不是 `MapEntity`?
|
||||||
|
2. 是否破坏了“服务唯一写入口”不变量?
|
||||||
|
3. Build/Upgrade 失败是否有完整回滚和金币返还?
|
||||||
|
4. 清理路径是否覆盖了 `OnHide` 与重进地图场景?
|
||||||
|
5. `dotnet build GeometryTD.sln` 是否通过?
|
||||||
Loading…
Reference in New Issue