init
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Map
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class House : MonoBehaviour
|
||||
{
|
||||
public Vector3 Position => transform.position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using GameFramework.Event;
|
||||
using GeometryTD.CustomEvent;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace GeometryTD.Map
|
||||
{
|
||||
public sealed class MapCombatRuntimeBridge
|
||||
{
|
||||
private Func<int, bool> _tryConsumeCoin;
|
||||
private Action<int> _addCoin;
|
||||
private bool _isCoinEventSubscribed;
|
||||
|
||||
public int CurrentCoin { get; private set; }
|
||||
|
||||
public void Initialize(MapEntityLoadContext loadContext, string mapName)
|
||||
{
|
||||
Reset();
|
||||
|
||||
MapData mapData = loadContext?.InitialMapData;
|
||||
CurrentCoin = Mathf.Max(0, mapData != null ? mapData.InitialCoin : 0);
|
||||
_tryConsumeCoin = loadContext?.TryConsumeCoin;
|
||||
_addCoin = loadContext?.AddCoin;
|
||||
|
||||
if (_tryConsumeCoin == null || _addCoin == null)
|
||||
{
|
||||
Log.Warning(
|
||||
"Map combat runtime bridge has incomplete callbacks. Map='{0}', TryConsumeCoin={1}, AddCoin={2}.",
|
||||
mapName,
|
||||
_tryConsumeCoin != null,
|
||||
_addCoin != null);
|
||||
}
|
||||
|
||||
GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged);
|
||||
_isCoinEventSubscribed = true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
if (_isCoinEventSubscribed)
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged);
|
||||
_isCoinEventSubscribed = false;
|
||||
}
|
||||
|
||||
_tryConsumeCoin = null;
|
||||
_addCoin = null;
|
||||
CurrentCoin = 0;
|
||||
}
|
||||
|
||||
public bool TryConsumeCoin(int cost)
|
||||
{
|
||||
int requiredCoin = Mathf.Max(0, cost);
|
||||
if (requiredCoin <= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _tryConsumeCoin != null && _tryConsumeCoin.Invoke(requiredCoin);
|
||||
}
|
||||
|
||||
public void AddCoin(int coin)
|
||||
{
|
||||
int amount = Mathf.Max(0, coin);
|
||||
if (amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_addCoin?.Invoke(amount);
|
||||
}
|
||||
|
||||
private void OnCombatCoinChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (e is not CombatCoinChangedEventArgs args)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentCoin = Mathf.Max(0, args.CurrentCoin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace GeometryTD.Map
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class MapDataRefs : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Tilemap _tilemap = null;
|
||||
[SerializeField] private Spawner[] _spawners = null;
|
||||
[SerializeField] private House _house = null;
|
||||
|
||||
public Tilemap Tilemap => _tilemap;
|
||||
public Spawner[] Spawners => _spawners;
|
||||
public House House => _house;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
if (_tilemap == null)
|
||||
{
|
||||
_tilemap = GetComponentInChildren<Tilemap>(true);
|
||||
}
|
||||
|
||||
if (_spawners == null || _spawners.Length == 0)
|
||||
{
|
||||
_spawners = GetComponentsInChildren<Spawner>(true);
|
||||
}
|
||||
|
||||
if (_house == null)
|
||||
{
|
||||
_house = GetComponentInChildren<House>(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
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;
|
||||
foreach (var candidate in _pathCells)
|
||||
{
|
||||
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,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Map
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
public class Spawner : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int _spawnOrder = 0;
|
||||
|
||||
public int SpawnOrder => _spawnOrder;
|
||||
|
||||
public Vector3 Position => transform.position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using GeometryTD.Entity;
|
||||
using GeometryTD.Entity.EntityData;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Tilemaps;
|
||||
|
||||
namespace GeometryTD.Map
|
||||
{
|
||||
public sealed class TowerPlacementService
|
||||
{
|
||||
private const int DefaultTowerTypeId = 401;
|
||||
private const int MinTowerLevel = 0;
|
||||
private const int MaxTowerLevel = 4;
|
||||
|
||||
private readonly Dictionary<Vector3Int, int> _towerEntityIdByFoundationCell = new();
|
||||
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
|
||||
private readonly Dictionary<int, TowerStatsData> _towerStatsByEntityId = new();
|
||||
private readonly Dictionary<int, int> _towerLevelByEntityId = new();
|
||||
private readonly List<int> _towerEntityIdBuffer = new();
|
||||
|
||||
public IReadOnlyDictionary<Vector3Int, int> TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell;
|
||||
public IReadOnlyDictionary<int, Vector3Int> FoundationCellByTowerEntityId => _foundationCellByTowerEntityId;
|
||||
|
||||
public bool IsTowerAtMaxLevel(int towerEntityId)
|
||||
{
|
||||
if (towerEntityId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TowerStatsData towerStats =
|
||||
_towerStatsByEntityId.TryGetValue(towerEntityId, out TowerStatsData cachedStats)
|
||||
? cachedStats
|
||||
: null;
|
||||
int currentLevel = GetTowerLevel(towerEntityId);
|
||||
int maxLevel = ResolveMaxTowerLevel(towerStats);
|
||||
return currentLevel >= maxLevel;
|
||||
}
|
||||
|
||||
public bool TryBuildTower(Vector3Int foundationCell, Func<Vector3Int, bool> isFoundationCell, int buildIndex,
|
||||
int[] buildTowerCosts, TowerStatsData buildTowerStats, Color muzzleColor, Color bearingColor, Color baseColor, 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;
|
||||
}
|
||||
|
||||
TowerStatsData towerStats = CloneTowerStats(buildTowerStats);
|
||||
if (towerStats == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int buildCost = GetBuildTowerCost(buildTowerCosts, buildIndex);
|
||||
if (tryConsumeCoin != null && !tryConsumeCoin(buildCost))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryShowTowerEntity(foundationCell, towerStats, muzzleColor, bearingColor, baseColor, towerTypeId, tilemap, out int newTowerEntityId))
|
||||
{
|
||||
addCoin?.Invoke(buildCost);
|
||||
return false;
|
||||
}
|
||||
|
||||
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
|
||||
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
|
||||
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats);
|
||||
_towerLevelByEntityId[newTowerEntityId] = MinTowerLevel;
|
||||
towerEntityId = newTowerEntityId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryUpgradeTower(int towerEntityId, int upgradeCost, int towerTypeId, Tilemap tilemap,
|
||||
Func<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;
|
||||
}
|
||||
|
||||
TowerStatsData towerStats =
|
||||
_towerStatsByEntityId.TryGetValue(towerEntityId, out TowerStatsData cachedStats)
|
||||
? CloneTowerStats(cachedStats)
|
||||
: BuildTowerStats(0);
|
||||
int currentTowerLevel = GetTowerLevel(towerEntityId);
|
||||
int maxTowerLevel = ResolveMaxTowerLevel(towerStats);
|
||||
if (currentTowerLevel >= maxTowerLevel)
|
||||
{
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return false;
|
||||
}
|
||||
|
||||
int requiredUpgradeCost = Mathf.Max(0, upgradeCost);
|
||||
if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
|
||||
{
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return false;
|
||||
}
|
||||
|
||||
int nextTowerLevel = Mathf.Clamp(currentTowerLevel + 1, MinTowerLevel, maxTowerLevel);
|
||||
if (!TryApplyTowerStats(towerEntityId, towerStats, nextTowerLevel))
|
||||
{
|
||||
addCoin?.Invoke(requiredUpgradeCost);
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return false;
|
||||
}
|
||||
|
||||
_towerStatsByEntityId[towerEntityId] = CloneTowerStats(towerStats);
|
||||
_towerLevelByEntityId[towerEntityId] = nextTowerLevel;
|
||||
resultTowerEntityId = towerEntityId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryDestroyTower(int towerEntityId, int destroyGain, Action<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);
|
||||
_towerLevelByEntityId.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();
|
||||
_towerLevelByEntityId.Clear();
|
||||
_towerEntityIdBuffer.Clear();
|
||||
}
|
||||
|
||||
public void ClearTracking()
|
||||
{
|
||||
_towerEntityIdByFoundationCell.Clear();
|
||||
_foundationCellByTowerEntityId.Clear();
|
||||
_towerStatsByEntityId.Clear();
|
||||
_towerLevelByEntityId.Clear();
|
||||
_towerEntityIdBuffer.Clear();
|
||||
}
|
||||
|
||||
private static int GetBuildTowerCost(int[] buildTowerCosts, int buildIndex)
|
||||
{
|
||||
if (buildTowerCosts == null || buildIndex < 0 || buildIndex >= buildTowerCosts.Length)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Mathf.Max(0, buildTowerCosts[buildIndex]);
|
||||
}
|
||||
|
||||
private static bool TryShowTowerEntity(Vector3Int foundationCell, TowerStatsData towerStats, Color muzzleColor, Color bearingColor, Color baseColor,
|
||||
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 TowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats,
|
||||
MinTowerLevel, muzzleColor, bearingColor, baseColor);
|
||||
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 TowerStatsData BuildTowerStats(int buildIndex)
|
||||
{
|
||||
switch (buildIndex)
|
||||
{
|
||||
case 0:
|
||||
return new TowerStatsData
|
||||
{
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
|
||||
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
|
||||
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
TagRuntimes = Array.Empty<TagRuntimeData>(),
|
||||
Tags = Array.Empty<TagType>()
|
||||
};
|
||||
case 1:
|
||||
return new TowerStatsData
|
||||
{
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
|
||||
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
|
||||
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Fire,
|
||||
TagRuntimes = Array.Empty<TagRuntimeData>(),
|
||||
Tags = Array.Empty<TagType>()
|
||||
};
|
||||
case 2:
|
||||
return new TowerStatsData
|
||||
{
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
|
||||
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
|
||||
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Ice,
|
||||
TagRuntimes = Array.Empty<TagRuntimeData>(),
|
||||
Tags = Array.Empty<TagType>()
|
||||
};
|
||||
case 3:
|
||||
return new TowerStatsData
|
||||
{
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
|
||||
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
|
||||
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Poison,
|
||||
TagRuntimes = Array.Empty<TagRuntimeData>(),
|
||||
Tags = Array.Empty<TagType>()
|
||||
};
|
||||
default:
|
||||
return new TowerStatsData
|
||||
{
|
||||
AttackDamage = new[] { 200, 220, 240, 260, 300 },
|
||||
DamageRandomRate = 0f,
|
||||
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
|
||||
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
|
||||
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
|
||||
AttackMethodType = AttackMethodType.NormalBullet,
|
||||
AttackPropertyType = AttackPropertyType.Physics,
|
||||
TagRuntimes = Array.Empty<TagRuntimeData>(),
|
||||
Tags = Array.Empty<TagType>()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static TowerStatsData CloneTowerStats(TowerStatsData source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return BuildTowerStats(0);
|
||||
}
|
||||
|
||||
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
|
||||
return new TowerStatsData
|
||||
{
|
||||
AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty<int>(),
|
||||
DamageRandomRate = source.DamageRandomRate,
|
||||
RotateSpeed = source.RotateSpeed != null ? (float[])source.RotateSpeed.Clone() : Array.Empty<float>(),
|
||||
AttackRange = source.AttackRange != null ? (float[])source.AttackRange.Clone() : Array.Empty<float>(),
|
||||
AttackSpeed = source.AttackSpeed != null ? (float[])source.AttackSpeed.Clone() : Array.Empty<float>(),
|
||||
AttackMethodType = source.AttackMethodType,
|
||||
AttackPropertyType = source.AttackPropertyType,
|
||||
TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(source.TagRuntimes),
|
||||
Tags = copiedTags
|
||||
};
|
||||
}
|
||||
|
||||
private int GetTowerLevel(int towerEntityId)
|
||||
{
|
||||
if (towerEntityId == 0 || !_towerLevelByEntityId.TryGetValue(towerEntityId, out int towerLevel))
|
||||
{
|
||||
return MinTowerLevel;
|
||||
}
|
||||
|
||||
return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
|
||||
}
|
||||
|
||||
private static int ResolveMaxTowerLevel(TowerStatsData stats)
|
||||
{
|
||||
int maxCount = Mathf.Max(
|
||||
GetLength(stats?.AttackDamage),
|
||||
GetLength(stats?.RotateSpeed),
|
||||
GetLength(stats?.AttackRange),
|
||||
GetLength(stats?.AttackSpeed));
|
||||
if (maxCount <= 0)
|
||||
{
|
||||
return MinTowerLevel;
|
||||
}
|
||||
|
||||
return Mathf.Clamp(maxCount - 1, MinTowerLevel, MaxTowerLevel);
|
||||
}
|
||||
|
||||
private static bool TryApplyTowerStats(int towerEntityId, TowerStatsData towerStats, int towerLevel)
|
||||
{
|
||||
if (towerEntityId == 0 || towerStats == null || GameEntry.Entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameEntry.Entity.GetGameEntity(towerEntityId) is not TowerEntity towerEntity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return towerEntity.TryApplyStats(towerStats, towerLevel);
|
||||
}
|
||||
|
||||
private static int GetLength<T>(T[] values)
|
||||
{
|
||||
return values != null ? values.Length : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 TowerEntity towerEntity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
towerEntity.SetAttackRangeVisible(visible);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user