仓库组件 + 防御塔升级

- `PlayerInventoryComponent`:玩家库存组件,收集金币/组件/防御塔等道具
- 补全防御塔的升级逻辑,最高 5 级
This commit is contained in:
SepComet
2026-03-02 19:50:12 +08:00
parent 5ba94828a8
commit daba9cbdf9
20 changed files with 753 additions and 138 deletions
@@ -1,26 +1,44 @@
using System;
using System.Collections.Generic;
using GeometryTD.Definition;
using GeometryTD.Entity;
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 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, DefenseTowerStatsData> _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;
}
DefenseTowerStatsData towerStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
? cachedStats
: null;
int currentLevel = GetTowerLevel(towerEntityId);
int maxLevel = ResolveMaxTowerLevel(towerStats);
return currentLevel >= maxLevel;
}
public bool TryBuildTower(Vector3Int foundationCell, Func<Vector3Int, bool> isFoundationCell, int buildIndex,
int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func<int, bool> tryConsumeCoin,
Action<int> addCoin,
@@ -53,6 +71,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats);
_towerLevelByEntityId[newTowerEntityId] = MinTowerLevel;
towerEntityId = newTowerEntityId;
return true;
}
@@ -68,42 +87,36 @@ namespace GeometryTD.Map
return false;
}
int requiredUpgradeCost = Mathf.Max(0, upgradeCost);
if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
{
return false;
}
DefenseTowerStatsData oldStats =
DefenseTowerStatsData towerStats =
_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))
int currentTowerLevel = GetTowerLevel(towerEntityId);
int maxTowerLevel = ResolveMaxTowerLevel(towerStats);
if (currentTowerLevel >= maxTowerLevel)
{
if (TryShowTowerEntity(foundationCell, oldStats, towerTypeId, tilemap, out int fallbackTowerEntityId))
{
_towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId;
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
resultTowerEntityId = fallbackTowerEntityId;
}
addCoin?.Invoke(requiredUpgradeCost);
resultTowerEntityId = towerEntityId;
return false;
}
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats);
resultTowerEntityId = newTowerEntityId;
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;
}
@@ -120,6 +133,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell.Remove(foundationCell);
_foundationCellByTowerEntityId.Remove(towerEntityId);
_towerStatsByEntityId.Remove(towerEntityId);
_towerLevelByEntityId.Remove(towerEntityId);
addCoin?.Invoke(Mathf.Max(0, destroyGain));
return true;
}
@@ -140,6 +154,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear();
_towerLevelByEntityId.Clear();
_towerEntityIdBuffer.Clear();
}
@@ -148,6 +163,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear();
_towerLevelByEntityId.Clear();
_towerEntityIdBuffer.Clear();
}
@@ -175,7 +191,8 @@ namespace GeometryTD.Map
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);
var towerData = new DefenseTowerData(entityId, typeId, towerPosition, Quaternion.identity, towerStats,
MinTowerLevel);
GameEntry.Entity.ShowDefenseTower(towerData);
towerEntityId = entityId;
@@ -203,11 +220,11 @@ namespace GeometryTD.Map
case 0:
return new DefenseTowerStatsData
{
AttackDamage = 200,
DamageRandomRate = 0f,
RotateSpeed = 200f,
AttackRange = 4.5f,
AttackSpeed = 1.5f,
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>()
@@ -215,11 +232,11 @@ namespace GeometryTD.Map
case 1:
return new DefenseTowerStatsData
{
AttackDamage = 260,
DamageRandomRate = 0f,
RotateSpeed = 160f,
AttackRange = 5f,
AttackSpeed = 1.2f,
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Fire,
Tags = Array.Empty<TagType>()
@@ -227,11 +244,11 @@ namespace GeometryTD.Map
case 2:
return new DefenseTowerStatsData
{
AttackDamage = 340,
DamageRandomRate = 0f,
RotateSpeed = 140f,
AttackRange = 5.5f,
AttackSpeed = 0.95f,
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Ice,
Tags = Array.Empty<TagType>()
@@ -239,11 +256,11 @@ namespace GeometryTD.Map
case 3:
return new DefenseTowerStatsData
{
AttackDamage = 440,
DamageRandomRate = 0f,
RotateSpeed = 120f,
AttackRange = 6f,
AttackSpeed = 0.75f,
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Poison,
Tags = Array.Empty<TagType>()
@@ -251,11 +268,11 @@ namespace GeometryTD.Map
default:
return new DefenseTowerStatsData
{
AttackDamage = 200,
DamageRandomRate = 0f,
RotateSpeed = 180f,
AttackRange = 5f,
AttackSpeed = 1f,
AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>()
@@ -273,28 +290,63 @@ namespace GeometryTD.Map
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,
AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = source.DamageRandomRate != null
? (float[])source.DamageRandomRate.Clone()
: Array.Empty<float>(),
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,
Tags = copiedTags
};
}
private static void ApplyUpgradeToStats(DefenseTowerStatsData stats)
private int GetTowerLevel(int towerEntityId)
{
if (stats == null)
if (towerEntityId == 0 || !_towerLevelByEntityId.TryGetValue(towerEntityId, out int towerLevel))
{
return;
return MinTowerLevel;
}
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);
return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
}
private static int ResolveMaxTowerLevel(DefenseTowerStatsData stats)
{
int maxCount = Mathf.Max(
GetLength(stats?.AttackDamage),
GetLength(stats?.DamageRandomRate),
GetLength(stats?.RotateSpeed),
GetLength(stats?.AttackRange),
GetLength(stats?.AttackSpeed));
if (maxCount <= 0)
{
return MinTowerLevel;
}
return Mathf.Clamp(maxCount - 1, MinTowerLevel, MaxTowerLevel);
}
private static bool TryApplyTowerStats(int towerEntityId, DefenseTowerStatsData towerStats, int towerLevel)
{
if (towerEntityId == 0 || towerStats == null || GameEntry.Entity == null)
{
return false;
}
if (GameEntry.Entity.GetGameEntity(towerEntityId) is not DefenseTowerEntity towerEntity)
{
return false;
}
return towerEntity.TryApplyStats(towerStats, towerLevel);
}
private static int GetLength<T>(T[] values)
{
return values != null ? values.Length : 0;
}
}
}