仓库组件 + 防御塔升级

- `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

View File

@ -5,14 +5,12 @@
// Feedback: mailto:ellan@gameframework.cn // Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------ //------------------------------------------------------------
using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
/// <summary> /// <summary>
/// 游戏入口。 /// 游戏入口。
/// </summary> /// </summary>
public partial class GameEntry : MonoBehaviour public partial class GameEntry
{ {
/// <summary> /// <summary>
/// 获取游戏基础组件。 /// 获取游戏基础组件。

View File

@ -8,6 +8,8 @@ public partial class GameEntry
{ {
public static BuiltinDataComponent BuiltinData { get; private set; } public static BuiltinDataComponent BuiltinData { get; private set; }
public static PlayerInventoryComponent PlayerInventory { get; private set; }
public static HPBarComponent HPBar { get; private set; } public static HPBarComponent HPBar { get; private set; }
public static UIRouterComponent UIRouter { get; private set; } public static UIRouterComponent UIRouter { get; private set; }
@ -23,16 +25,12 @@ public partial class GameEntry
private static void InitCustomComponents() private static void InitCustomComponents()
{ {
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>(); BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
PlayerInventory = UnityGameFramework.Runtime.GameEntry.GetComponent<PlayerInventoryComponent>();
HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent<HPBarComponent>(); HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent<HPBarComponent>();
UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>(); UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent<UIRouterComponent>();
EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent<EventNodeComponent>(); EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent<EventNodeComponent>();
CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent<CombatNodeComponent>(); CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent<CombatNodeComponent>();
ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent<ShopNodeComponent>(); ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent<ShopNodeComponent>();
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>(); ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
if (ResolutionAdapter == null)
{
UnityGameFramework.Runtime.Log.Warning(
"ResolutionAdapterComponent is missing. Please add it in Launcher scene and inject UI roots.");
}
} }
} }

View File

@ -8,6 +8,8 @@ namespace Components
public class DefenseTowerController : MonoBehaviour public class DefenseTowerController : MonoBehaviour
{ {
private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator"; private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator";
private const int MinTowerLevel = 0;
private const int MaxTowerLevel = 4;
private static Material s_AttackRangeSharedMaterial; private static Material s_AttackRangeSharedMaterial;
[SerializeField] private ShooterMuzzleComp _muzzleComp; [SerializeField] private ShooterMuzzleComp _muzzleComp;
@ -15,7 +17,6 @@ namespace Components
[SerializeField] private BasicBaseComp _baseComp; [SerializeField] private BasicBaseComp _baseComp;
[SerializeField] private Transform _scanOrigin; [SerializeField] private Transform _scanOrigin;
[SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f; [SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f;
[SerializeField] private bool _autoUpdate = true;
[SerializeField] private LineRenderer _attackRangeRenderer; [SerializeField] private LineRenderer _attackRangeRenderer;
[SerializeField] [Min(12)] private int _attackRangeSegments = 64; [SerializeField] [Min(12)] private int _attackRangeSegments = 64;
[SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f; [SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f;
@ -25,6 +26,7 @@ namespace Components
private Transform _currentTarget; private Transform _currentTarget;
private float _retargetTimer; private float _retargetTimer;
private float _attackRange; private float _attackRange;
private int _towerLevel;
public Transform CurrentTarget => _currentTarget; public Transform CurrentTarget => _currentTarget;
@ -33,17 +35,12 @@ namespace Components
ResolveComponents(); ResolveComponents();
} }
private void Update() public void OnInit(DefenseTowerStatsData stats)
{ {
if (!_autoUpdate) OnInit(stats, MinTowerLevel);
{
return;
}
OnUpdate(Time.deltaTime);
} }
public void OnInit(DefenseTowerStatsData stats) public void OnInit(DefenseTowerStatsData stats, int towerLevel)
{ {
ResolveComponents(); ResolveComponents();
if (stats == null) if (stats == null)
@ -51,10 +48,16 @@ namespace Components
return; return;
} }
_muzzleComp?.OnInit(stats.AttackDamage, stats.AttackMethodType); _towerLevel = Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
_bearingComp?.OnInit(stats.RotateSpeed, stats.AttackRange); int attackDamage = ResolveIntValue(stats.AttackDamage, _towerLevel, 1, 1);
_baseComp?.OnInit(stats.AttackSpeed, stats.AttackPropertyType); float rotateSpeed = ResolveFloatValue(stats.RotateSpeed, _towerLevel, 180f, 1f);
SetAttackRange(stats.AttackRange); float attackRange = ResolveFloatValue(stats.AttackRange, _towerLevel, 5f, 0.1f);
float attackSpeed = ResolveFloatValue(stats.AttackSpeed, _towerLevel, 1f, 0.01f);
_muzzleComp?.OnInit(attackDamage, stats.AttackMethodType);
_bearingComp?.OnInit(rotateSpeed, attackRange);
_baseComp?.OnInit(attackSpeed, stats.AttackPropertyType);
SetAttackRange(attackRange);
SetAttackRangeVisible(false); SetAttackRangeVisible(false);
_currentTarget = null; _currentTarget = null;
_retargetTimer = 0f; _retargetTimer = 0f;
@ -65,6 +68,7 @@ namespace Components
SetAttackRangeVisible(false); SetAttackRangeVisible(false);
_currentTarget = null; _currentTarget = null;
_retargetTimer = 0f; _retargetTimer = 0f;
_towerLevel = MinTowerLevel;
_muzzleComp?.OnReset(); _muzzleComp?.OnReset();
_bearingComp?.OnReset(); _bearingComp?.OnReset();
_baseComp?.OnReset(); _baseComp?.OnReset();
@ -86,11 +90,6 @@ namespace Components
RebuildAttackRangeGeometry(); RebuildAttackRangeGeometry();
} }
public void SetAutoUpdate(bool autoUpdate)
{
_autoUpdate = autoUpdate;
}
public void SetTarget(Transform target) public void SetTarget(Transform target)
{ {
_currentTarget = target; _currentTarget = target;
@ -195,6 +194,30 @@ namespace Components
return target != null && target.gameObject.activeInHierarchy; return target != null && target.gameObject.activeInHierarchy;
} }
private static int ResolveIntValue(int[] values, int level, int fallback, int minValue)
{
int resolved = Mathf.Max(minValue, fallback);
if (values == null || values.Length <= 0)
{
return resolved;
}
int index = Mathf.Clamp(level, 0, values.Length - 1);
return Mathf.Max(minValue, values[index]);
}
private static float ResolveFloatValue(float[] values, int level, float fallback, float minValue)
{
float resolved = Mathf.Max(minValue, fallback);
if (values == null || values.Length <= 0)
{
return resolved;
}
int index = Mathf.Clamp(level, 0, values.Length - 1);
return Mathf.Max(minValue, values[index]);
}
private void EnsureAttackRangeRenderer() private void EnsureAttackRangeRenderer()
{ {
if (_attackRangeRenderer == null) if (_attackRangeRenderer == null)

View File

@ -245,6 +245,7 @@ namespace GeometryTD.CustomComponent
{ {
int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount; int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount;
BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot(); BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot();
GameEntry.PlayerInventory?.MergeInventory(rewardInventory);
// Step 1: stop runtime and clear enemy entities only. // Step 1: stop runtime and clear enemy entities only.
_enemyManager.EndPhase(); _enemyManager.EndPhase();

View File

@ -0,0 +1,408 @@
using System;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public class PlayerInventoryComponent : GameFrameworkComponent
{
private BackpackInventoryData _inventory = new BackpackInventoryData();
private long _nextInstanceId = 1;
private bool _initialized;
public int Gold
{
get
{
EnsureInitialized();
return _inventory.Gold;
}
}
public void OnInit()
{
_inventory = CloneInventory(RepoFormUseCase.SampleInventory());
RebuildNextInstanceId();
_initialized = true;
Log.Info(
"PlayerInventory initialized. Gold={0}, Tower={1}, Muzzle={2}, Bearing={3}, Base={4}.",
_inventory.Gold,
_inventory.Towers.Count,
_inventory.MuzzleComponents.Count,
_inventory.BearingComponents.Count,
_inventory.BaseComponents.Count);
}
public BackpackInventoryData GetInventorySnapshot()
{
EnsureInitialized();
return CloneInventory(_inventory);
}
public void MergeInventory(BackpackInventoryData gainedInventory)
{
EnsureInitialized();
if (gainedInventory == null)
{
return;
}
int gainedGold = Mathf.Max(0, gainedInventory.Gold);
int gainedMuzzleCount = 0;
int gainedBearingCount = 0;
int gainedBaseCount = 0;
int gainedTowerCount = 0;
if (gainedGold > 0)
{
_inventory.Gold += gainedGold;
}
if (gainedInventory.MuzzleComponents != null)
{
for (int i = 0; i < gainedInventory.MuzzleComponents.Count; i++)
{
MuzzleCompItemData source = gainedInventory.MuzzleComponents[i];
if (source == null)
{
continue;
}
MuzzleCompItemData cloned = CloneMuzzleComp(source);
cloned.InstanceId = AllocateInstanceId();
_inventory.MuzzleComponents.Add(cloned);
gainedMuzzleCount++;
}
}
if (gainedInventory.BearingComponents != null)
{
for (int i = 0; i < gainedInventory.BearingComponents.Count; i++)
{
BearingCompItemData source = gainedInventory.BearingComponents[i];
if (source == null)
{
continue;
}
BearingCompItemData cloned = CloneBearingComp(source);
cloned.InstanceId = AllocateInstanceId();
_inventory.BearingComponents.Add(cloned);
gainedBearingCount++;
}
}
if (gainedInventory.BaseComponents != null)
{
for (int i = 0; i < gainedInventory.BaseComponents.Count; i++)
{
BaseCompItemData source = gainedInventory.BaseComponents[i];
if (source == null)
{
continue;
}
BaseCompItemData cloned = CloneBaseComp(source);
cloned.InstanceId = AllocateInstanceId();
_inventory.BaseComponents.Add(cloned);
gainedBaseCount++;
}
}
if (gainedInventory.Towers != null)
{
for (int i = 0; i < gainedInventory.Towers.Count; i++)
{
DefenseTowerItemData source = gainedInventory.Towers[i];
if (source == null)
{
continue;
}
DefenseTowerItemData cloned = CloneTower(source);
cloned.InstanceId = AllocateInstanceId();
_inventory.Towers.Add(cloned);
gainedTowerCount++;
}
}
if (gainedGold > 0 || gainedMuzzleCount > 0 || gainedBearingCount > 0 || gainedBaseCount > 0 ||
gainedTowerCount > 0)
{
Log.Info(
"PlayerInventory merged reward. Gold+{0}, Tower+{1}, Muzzle+{2}, Bearing+{3}, Base+{4}.",
gainedGold,
gainedTowerCount,
gainedMuzzleCount,
gainedBearingCount,
gainedBaseCount);
}
}
public bool TryConsumeGold(int costGold)
{
EnsureInitialized();
int resolvedCost = Mathf.Max(0, costGold);
if (resolvedCost <= 0)
{
return true;
}
if (_inventory.Gold < resolvedCost)
{
return false;
}
_inventory.Gold -= resolvedCost;
return true;
}
public void AddGold(int gainGold)
{
EnsureInitialized();
int resolvedGain = Mathf.Max(0, gainGold);
if (resolvedGain <= 0)
{
return;
}
_inventory.Gold += resolvedGain;
}
private void EnsureInitialized()
{
if (_initialized)
{
return;
}
OnInit();
}
private long AllocateInstanceId()
{
if (_nextInstanceId < 1)
{
_nextInstanceId = 1;
}
return _nextInstanceId++;
}
private void RebuildNextInstanceId()
{
long maxInstanceId = 0;
if (_inventory.Towers != null)
{
for (int i = 0; i < _inventory.Towers.Count; i++)
{
DefenseTowerItemData item = _inventory.Towers[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
}
}
}
if (_inventory.MuzzleComponents != null)
{
for (int i = 0; i < _inventory.MuzzleComponents.Count; i++)
{
MuzzleCompItemData item = _inventory.MuzzleComponents[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
}
}
}
if (_inventory.BearingComponents != null)
{
for (int i = 0; i < _inventory.BearingComponents.Count; i++)
{
BearingCompItemData item = _inventory.BearingComponents[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
}
}
}
if (_inventory.BaseComponents != null)
{
for (int i = 0; i < _inventory.BaseComponents.Count; i++)
{
BaseCompItemData item = _inventory.BaseComponents[i];
if (item != null)
{
maxInstanceId = Math.Max(maxInstanceId, item.InstanceId);
}
}
}
_nextInstanceId = Math.Max(1, maxInstanceId + 1);
}
private static BackpackInventoryData CloneInventory(BackpackInventoryData source)
{
BackpackInventoryData cloned = new BackpackInventoryData();
if (source == null)
{
return cloned;
}
cloned.Gold = Mathf.Max(0, source.Gold);
if (source.MuzzleComponents != null)
{
for (int i = 0; i < source.MuzzleComponents.Count; i++)
{
MuzzleCompItemData item = source.MuzzleComponents[i];
if (item != null)
{
cloned.MuzzleComponents.Add(CloneMuzzleComp(item));
}
}
}
if (source.BearingComponents != null)
{
for (int i = 0; i < source.BearingComponents.Count; i++)
{
BearingCompItemData item = source.BearingComponents[i];
if (item != null)
{
cloned.BearingComponents.Add(CloneBearingComp(item));
}
}
}
if (source.BaseComponents != null)
{
for (int i = 0; i < source.BaseComponents.Count; i++)
{
BaseCompItemData item = source.BaseComponents[i];
if (item != null)
{
cloned.BaseComponents.Add(CloneBaseComp(item));
}
}
}
if (source.Towers != null)
{
for (int i = 0; i < source.Towers.Count; i++)
{
DefenseTowerItemData item = source.Towers[i];
if (item != null)
{
cloned.Towers.Add(CloneTower(item));
}
}
}
return cloned;
}
private static MuzzleCompItemData CloneMuzzleComp(MuzzleCompItemData source)
{
return new MuzzleCompItemData
{
InstanceId = source.InstanceId,
ConfigId = source.ConfigId,
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
Constraint = source.Constraint,
Tags = CloneTags(source.Tags),
AttackDamage = CloneIntArray(source.AttackDamage),
DamageRandomRate = source.DamageRandomRate,
AttackMethodType = source.AttackMethodType
};
}
private static BearingCompItemData CloneBearingComp(BearingCompItemData source)
{
return new BearingCompItemData
{
InstanceId = source.InstanceId,
ConfigId = source.ConfigId,
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
Constraint = source.Constraint,
Tags = CloneTags(source.Tags),
RotateSpeed = CloneFloatArray(source.RotateSpeed),
AttackRange = CloneFloatArray(source.AttackRange)
};
}
private static BaseCompItemData CloneBaseComp(BaseCompItemData source)
{
return new BaseCompItemData
{
InstanceId = source.InstanceId,
ConfigId = source.ConfigId,
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
Constraint = source.Constraint,
Tags = CloneTags(source.Tags),
AttackSpeed = CloneFloatArray(source.AttackSpeed),
AttackPropertyType = source.AttackPropertyType
};
}
private static DefenseTowerItemData CloneTower(DefenseTowerItemData source)
{
return new DefenseTowerItemData
{
InstanceId = source.InstanceId,
Name = source.Name,
Rarity = source.Rarity,
Endurance = source.Endurance,
MuzzleComponentInstanceId = source.MuzzleComponentInstanceId,
BearingComponentInstanceId = source.BearingComponentInstanceId,
BaseComponentInstanceId = source.BaseComponentInstanceId,
Stats = CloneTowerStats(source.Stats)
};
}
private static DefenseTowerStatsData CloneTowerStats(DefenseTowerStatsData source)
{
if (source == null)
{
return new DefenseTowerStatsData();
}
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 = CloneTags(source.Tags)
};
}
private static int[] CloneIntArray(int[] source)
{
return source != null ? (int[])source.Clone() : Array.Empty<int>();
}
private static float[] CloneFloatArray(float[] source)
{
return source != null ? (float[])source.Clone() : Array.Empty<float>();
}
private static TagType[] CloneTags(TagType[] source)
{
return source != null ? (TagType[])source.Clone() : Array.Empty<TagType>();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5db8d7bb243947fdbb9fe75a9d234d10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -9,11 +9,11 @@ namespace GeometryTD.Definition
[Serializable] [Serializable]
public sealed class DefenseTowerStatsData public sealed class DefenseTowerStatsData
{ {
public int AttackDamage { get; set; } public int[] AttackDamage { get; set; }
public float DamageRandomRate { get; set; } public float[] DamageRandomRate { get; set; }
public float RotateSpeed { get; set; } public float[] RotateSpeed { get; set; }
public float AttackRange { get; set; } public float[] AttackRange { get; set; }
public float AttackSpeed { get; set; } public float[] AttackSpeed { get; set; }
public AttackMethodType AttackMethodType { get; set; } public AttackMethodType AttackMethodType { get; set; }
public AttackPropertyType AttackPropertyType { get; set; } public AttackPropertyType AttackPropertyType { get; set; }
public TagType[] Tags { get; set; } public TagType[] Tags { get; set; }

View File

@ -8,13 +8,16 @@ namespace GeometryTD.Entity.EntityData
public class DefenseTowerData : EntityDataBase public class DefenseTowerData : EntityDataBase
{ {
[SerializeField] private DefenseTowerStatsData _stats = new DefenseTowerStatsData(); [SerializeField] private DefenseTowerStatsData _stats = new DefenseTowerStatsData();
[SerializeField] private int _towerLevel = 0;
public DefenseTowerData(int entityId, int typeId, Vector3 position, Quaternion rotation, DefenseTowerStatsData stats) public DefenseTowerData(int entityId, int typeId, Vector3 position, Quaternion rotation,
DefenseTowerStatsData stats, int towerLevel = 0)
: base(entityId, typeId) : base(entityId, typeId)
{ {
Position = position; Position = position;
Rotation = rotation; Rotation = rotation;
_stats = stats ?? new DefenseTowerStatsData(); _stats = stats ?? new DefenseTowerStatsData();
_towerLevel = Mathf.Max(0, towerLevel);
} }
public DefenseTowerStatsData Stats public DefenseTowerStatsData Stats
@ -22,5 +25,11 @@ namespace GeometryTD.Entity.EntityData
get => _stats; get => _stats;
set => _stats = value ?? new DefenseTowerStatsData(); set => _stats = value ?? new DefenseTowerStatsData();
} }
public int TowerLevel
{
get => _towerLevel;
set => _towerLevel = Mathf.Max(0, value);
}
} }
} }

View File

@ -8,9 +8,15 @@ namespace GeometryTD.Entity
{ {
public sealed class CombatSelectInputService public sealed class CombatSelectInputService
{ {
public bool TryBuildUserData(Tilemap tilemap, Transform mapTransform, public bool TryBuildUserData(
IReadOnlyDictionary<Vector3Int, int> towerEntityIdByFoundationCell, Func<Vector3Int, bool> isFoundationCell, Tilemap tilemap,
int upgradeCost, int destroyGain, out CombatSelectFormUserData userData) Transform mapTransform,
IReadOnlyDictionary<Vector3Int, int> towerEntityIdByFoundationCell,
Func<Vector3Int, bool> isFoundationCell,
Func<int, bool> isTowerAtMaxLevel,
int upgradeCost,
int destroyGain,
out CombatSelectFormUserData userData)
{ {
userData = null; userData = null;
if (tilemap == null || !TryGetPointerWorldPosition(tilemap, mapTransform, out Vector3 worldPosition, if (tilemap == null || !TryGetPointerWorldPosition(tilemap, mapTransform, out Vector3 worldPosition,
@ -44,13 +50,15 @@ namespace GeometryTD.Entity
WorldPosition = worldPosition, WorldPosition = worldPosition,
CellPosition = clickedCell, CellPosition = clickedCell,
TowerEntityId = towerEntityId, TowerEntityId = towerEntityId,
IsTowerAtMaxLevel = towerEntityId != 0 && isTowerAtMaxLevel != null && isTowerAtMaxLevel(towerEntityId),
UpgradeCost = Mathf.Max(0, upgradeCost), UpgradeCost = Mathf.Max(0, upgradeCost),
DestroyGain = Mathf.Max(0, destroyGain) DestroyGain = Mathf.Max(0, destroyGain)
}; };
return true; return true;
} }
private static bool TryGetPointerWorldPosition(Tilemap tilemap, Transform mapTransform, out Vector3 worldPosition, private static bool TryGetPointerWorldPosition(Tilemap tilemap, Transform mapTransform,
out Vector3 worldPosition,
out Vector2 contentPosition) out Vector2 contentPosition)
{ {
worldPosition = Vector3.zero; worldPosition = Vector3.zero;
@ -63,7 +71,9 @@ namespace GeometryTD.Entity
} }
Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition); Ray ray = mainCamera.ScreenPointToRay(Input.mousePosition);
float mapPlaneZ = tilemap != null ? tilemap.transform.position.z : (mapTransform != null ? mapTransform.position.z : 0f); float mapPlaneZ = tilemap != null
? tilemap.transform.position.z
: (mapTransform != null ? mapTransform.position.z : 0f);
Vector3 planeNormal = mainCamera.transform.forward.sqrMagnitude > Mathf.Epsilon Vector3 planeNormal = mainCamera.transform.forward.sqrMagnitude > Mathf.Epsilon
? -mainCamera.transform.forward ? -mainCamera.transform.forward
: Vector3.forward; : Vector3.forward;

View File

@ -1,4 +1,5 @@
using Components; using Components;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData; using GeometryTD.Entity.EntityData;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
@ -13,6 +14,17 @@ namespace GeometryTD.Entity
_towerController?.SetAttackRangeVisible(visible); _towerController?.SetAttackRangeVisible(visible);
} }
public bool TryApplyStats(DefenseTowerStatsData stats, int towerLevel)
{
if (_towerController == null || stats == null)
{
return false;
}
_towerController.OnInit(stats, towerLevel);
return true;
}
protected override void OnInit(object userData) protected override void OnInit(object userData)
{ {
base.OnInit(userData); base.OnInit(userData);
@ -41,8 +53,7 @@ namespace GeometryTD.Entity
return; return;
} }
_towerController.SetAutoUpdate(false); _towerController.OnInit(towerData.Stats, towerData.TowerLevel);
_towerController.OnInit(towerData.Stats);
} }
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)

View File

@ -242,7 +242,8 @@ namespace GeometryTD.Entity
if (_combatSelectInputService == null || if (_combatSelectInputService == null ||
!_combatSelectInputService.TryBuildUserData(Tilemap, CachedTransform, !_combatSelectInputService.TryBuildUserData(Tilemap, CachedTransform,
_towerPlacementService != null ? _towerPlacementService.TowerEntityIdByFoundationCell : null, _towerPlacementService != null ? _towerPlacementService.TowerEntityIdByFoundationCell : null,
IsFoundationCell, _upgradeCost, _destroyGain, out CombatSelectFormUserData userData)) IsFoundationCell, IsTowerAtMaxLevel, _upgradeCost, _destroyGain,
out CombatSelectFormUserData userData))
{ {
userData = new CombatSelectFormUserData userData = new CombatSelectFormUserData
{ {
@ -254,6 +255,11 @@ namespace GeometryTD.Entity
GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData); GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData);
} }
private bool IsTowerAtMaxLevel(int towerEntityId)
{
return _towerPlacementService != null && _towerPlacementService.IsTowerAtMaxLevel(towerEntityId);
}
private void ApplySelectedObject(CombatSelectFormUserData userData) private void ApplySelectedObject(CombatSelectFormUserData userData)
{ {
_towerSelectionPresenter?.ApplySelectedObject(userData); _towerSelectionPresenter?.ApplySelectedObject(userData);

View File

@ -35,6 +35,7 @@ namespace GeometryTD.Procedure
GameEntry.EventNode.OnInit(); GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain); GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit(); GameEntry.ShopNode.OnInit();
GameEntry.PlayerInventory?.OnInit();
_repoFormUseCase = new RepoFormUseCase(); _repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase); GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);

View File

@ -1,26 +1,44 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.Definition; using GeometryTD.Definition;
using GeometryTD.Entity;
using GeometryTD.Entity.EntityData; using GeometryTD.Entity.EntityData;
using GeometryTD.Pathfinding;
using UnityEngine; using UnityEngine;
using UnityEngine.Tilemaps; using UnityEngine.Tilemaps;
using UnityGameFramework.Runtime;
namespace GeometryTD.Map namespace GeometryTD.Map
{ {
public sealed class TowerPlacementService public sealed class TowerPlacementService
{ {
private const int DefaultTowerTypeId = 401; 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<Vector3Int, int> _towerEntityIdByFoundationCell = new();
private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new(); private readonly Dictionary<int, Vector3Int> _foundationCellByTowerEntityId = new();
private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new(); private readonly Dictionary<int, DefenseTowerStatsData> _towerStatsByEntityId = new();
private readonly Dictionary<int, int> _towerLevelByEntityId = new();
private readonly List<int> _towerEntityIdBuffer = new(); private readonly List<int> _towerEntityIdBuffer = new();
public IReadOnlyDictionary<Vector3Int, int> TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell; public IReadOnlyDictionary<Vector3Int, int> TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell;
public IReadOnlyDictionary<int, Vector3Int> FoundationCellByTowerEntityId => _foundationCellByTowerEntityId; 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, public bool TryBuildTower(Vector3Int foundationCell, Func<Vector3Int, bool> isFoundationCell, int buildIndex,
int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func<int, bool> tryConsumeCoin, int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func<int, bool> tryConsumeCoin,
Action<int> addCoin, Action<int> addCoin,
@ -53,6 +71,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId; _towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId;
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell; _foundationCellByTowerEntityId[newTowerEntityId] = foundationCell;
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats); _towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(towerStats);
_towerLevelByEntityId[newTowerEntityId] = MinTowerLevel;
towerEntityId = newTowerEntityId; towerEntityId = newTowerEntityId;
return true; return true;
} }
@ -68,42 +87,36 @@ namespace GeometryTD.Map
return false; return false;
} }
int requiredUpgradeCost = Mathf.Max(0, upgradeCost); DefenseTowerStatsData towerStats =
if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
{
return false;
}
DefenseTowerStatsData oldStats =
_towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats) _towerStatsByEntityId.TryGetValue(towerEntityId, out DefenseTowerStatsData cachedStats)
? CloneTowerStats(cachedStats) ? CloneTowerStats(cachedStats)
: BuildTowerStats(0); : BuildTowerStats(0);
DefenseTowerStatsData upgradedStats = CloneTowerStats(oldStats); int currentTowerLevel = GetTowerLevel(towerEntityId);
ApplyUpgradeToStats(upgradedStats); int maxTowerLevel = ResolveMaxTowerLevel(towerStats);
if (currentTowerLevel >= maxTowerLevel)
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)) resultTowerEntityId = towerEntityId;
{
_towerEntityIdByFoundationCell[foundationCell] = fallbackTowerEntityId;
_foundationCellByTowerEntityId[fallbackTowerEntityId] = foundationCell;
_towerStatsByEntityId[fallbackTowerEntityId] = CloneTowerStats(oldStats);
resultTowerEntityId = fallbackTowerEntityId;
}
addCoin?.Invoke(requiredUpgradeCost);
return false; return false;
} }
_towerEntityIdByFoundationCell[foundationCell] = newTowerEntityId; int requiredUpgradeCost = Mathf.Max(0, upgradeCost);
_foundationCellByTowerEntityId[newTowerEntityId] = foundationCell; if (tryConsumeCoin != null && !tryConsumeCoin(requiredUpgradeCost))
_towerStatsByEntityId[newTowerEntityId] = CloneTowerStats(upgradedStats); {
resultTowerEntityId = newTowerEntityId; 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; return true;
} }
@ -120,6 +133,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell.Remove(foundationCell); _towerEntityIdByFoundationCell.Remove(foundationCell);
_foundationCellByTowerEntityId.Remove(towerEntityId); _foundationCellByTowerEntityId.Remove(towerEntityId);
_towerStatsByEntityId.Remove(towerEntityId); _towerStatsByEntityId.Remove(towerEntityId);
_towerLevelByEntityId.Remove(towerEntityId);
addCoin?.Invoke(Mathf.Max(0, destroyGain)); addCoin?.Invoke(Mathf.Max(0, destroyGain));
return true; return true;
} }
@ -140,6 +154,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell.Clear(); _towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear(); _foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear(); _towerStatsByEntityId.Clear();
_towerLevelByEntityId.Clear();
_towerEntityIdBuffer.Clear(); _towerEntityIdBuffer.Clear();
} }
@ -148,6 +163,7 @@ namespace GeometryTD.Map
_towerEntityIdByFoundationCell.Clear(); _towerEntityIdByFoundationCell.Clear();
_foundationCellByTowerEntityId.Clear(); _foundationCellByTowerEntityId.Clear();
_towerStatsByEntityId.Clear(); _towerStatsByEntityId.Clear();
_towerLevelByEntityId.Clear();
_towerEntityIdBuffer.Clear(); _towerEntityIdBuffer.Clear();
} }
@ -175,7 +191,8 @@ namespace GeometryTD.Map
int typeId = towerTypeId > 0 ? towerTypeId : DefaultTowerTypeId; int typeId = towerTypeId > 0 ? towerTypeId : DefaultTowerTypeId;
Vector3 towerPosition = tilemap != null ? tilemap.GetCellCenterWorld(foundationCell) : foundationCell; Vector3 towerPosition = tilemap != null ? tilemap.GetCellCenterWorld(foundationCell) : foundationCell;
towerPosition.z = 0f; 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); GameEntry.Entity.ShowDefenseTower(towerData);
towerEntityId = entityId; towerEntityId = entityId;
@ -203,11 +220,11 @@ namespace GeometryTD.Map
case 0: case 0:
return new DefenseTowerStatsData return new DefenseTowerStatsData
{ {
AttackDamage = 200, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 200f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 4.5f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 1.5f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics, AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>() Tags = Array.Empty<TagType>()
@ -215,11 +232,11 @@ namespace GeometryTD.Map
case 1: case 1:
return new DefenseTowerStatsData return new DefenseTowerStatsData
{ {
AttackDamage = 260, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 160f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 5f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 1.2f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Fire, AttackPropertyType = AttackPropertyType.Fire,
Tags = Array.Empty<TagType>() Tags = Array.Empty<TagType>()
@ -227,11 +244,11 @@ namespace GeometryTD.Map
case 2: case 2:
return new DefenseTowerStatsData return new DefenseTowerStatsData
{ {
AttackDamage = 340, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 140f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 5.5f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 0.95f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Ice, AttackPropertyType = AttackPropertyType.Ice,
Tags = Array.Empty<TagType>() Tags = Array.Empty<TagType>()
@ -239,11 +256,11 @@ namespace GeometryTD.Map
case 3: case 3:
return new DefenseTowerStatsData return new DefenseTowerStatsData
{ {
AttackDamage = 440, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 120f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 6f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 0.75f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Poison, AttackPropertyType = AttackPropertyType.Poison,
Tags = Array.Empty<TagType>() Tags = Array.Empty<TagType>()
@ -251,11 +268,11 @@ namespace GeometryTD.Map
default: default:
return new DefenseTowerStatsData return new DefenseTowerStatsData
{ {
AttackDamage = 200, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 180f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 5f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 1f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics, AttackPropertyType = AttackPropertyType.Physics,
Tags = Array.Empty<TagType>() Tags = Array.Empty<TagType>()
@ -273,28 +290,63 @@ namespace GeometryTD.Map
TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>(); TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty<TagType>();
return new DefenseTowerStatsData return new DefenseTowerStatsData
{ {
AttackDamage = source.AttackDamage, AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = source.DamageRandomRate, DamageRandomRate = source.DamageRandomRate != null
RotateSpeed = source.RotateSpeed, ? (float[])source.DamageRandomRate.Clone()
AttackRange = source.AttackRange, : Array.Empty<float>(),
AttackSpeed = source.AttackSpeed, 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, AttackMethodType = source.AttackMethodType,
AttackPropertyType = source.AttackPropertyType, AttackPropertyType = source.AttackPropertyType,
Tags = copiedTags 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); return Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
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); 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;
} }
} }
} }

View File

@ -98,10 +98,12 @@ namespace GeometryTD.UI
switch (userData.ClickObjectType) switch (userData.ClickObjectType)
{ {
case CombatSelectClickObjectType.Foundation: case CombatSelectClickObjectType.Foundation:
_useCase.SetUpgradeVisible(true);
_useCase.ShowForFoundation(userData.ContentPosition); _useCase.ShowForFoundation(userData.ContentPosition);
break; break;
case CombatSelectClickObjectType.Tower: case CombatSelectClickObjectType.Tower:
_useCase.SetUpgradePrice(userData.UpgradeCost); _useCase.SetUpgradePrice(userData.UpgradeCost);
_useCase.SetUpgradeVisible(!userData.IsTowerAtMaxLevel);
_useCase.SetDestroyGain(userData.DestroyGain); _useCase.SetDestroyGain(userData.DestroyGain);
_useCase.ShowForTower(userData.ContentPosition); _useCase.ShowForTower(userData.ContentPosition);
break; break;
@ -133,6 +135,7 @@ namespace GeometryTD.UI
return null; return null;
} }
_useCase.SetUpgradeVisible(true);
_useCase.ShowForFoundation(contentPosition); _useCase.ShowForFoundation(contentPosition);
return OpenUI(_useCase.TryRefresh()); return OpenUI(_useCase.TryRefresh());
} }
@ -146,6 +149,7 @@ namespace GeometryTD.UI
} }
_useCase.SetUpgradePrice(upgradeCost); _useCase.SetUpgradePrice(upgradeCost);
_useCase.SetUpgradeVisible(true);
_useCase.SetDestroyGain(destroyGain); _useCase.SetDestroyGain(destroyGain);
_useCase.ShowForTower(contentPosition); _useCase.ShowForTower(contentPosition);
return OpenUI(_useCase.TryRefresh()); return OpenUI(_useCase.TryRefresh());

View File

@ -9,6 +9,7 @@ namespace GeometryTD.UI
public Vector3 WorldPosition; public Vector3 WorldPosition;
public Vector3Int CellPosition; public Vector3Int CellPosition;
public int TowerEntityId; public int TowerEntityId;
public bool IsTowerAtMaxLevel;
public int UpgradeCost; public int UpgradeCost;
public int DestroyGain; public int DestroyGain;
} }

View File

@ -6,7 +6,6 @@ namespace GeometryTD.UI
{ {
public class CombatFinishFormUseCase : IUIUseCase public class CombatFinishFormUseCase : IUIUseCase
{ {
private readonly RepoFormUseCase _repoFormUseCase = new RepoFormUseCase();
private CombatScheduler _combatScheduler; private CombatScheduler _combatScheduler;
private int _defeatedEnemyCount; private int _defeatedEnemyCount;
private int _gainedGold; private int _gainedGold;
@ -57,8 +56,7 @@ namespace GeometryTD.UI
BackpackInventoryData rewardInventory = _rewardInventory; BackpackInventoryData rewardInventory = _rewardInventory;
if (rewardInventory == null) if (rewardInventory == null)
{ {
RepoFormRawData repoRawData = _repoFormUseCase.CreateInitialModel(); rewardInventory = RepoFormUseCase.SampleInventory();
rewardInventory = repoRawData != null ? repoRawData.Inventory : null;
} }
return new CombatFinishFormRawData return new CombatFinishFormRawData

View File

@ -134,6 +134,11 @@ namespace GeometryTD.UI
_upgradeOption.Price = Mathf.Max(0, cost); _upgradeOption.Price = Mathf.Max(0, cost);
} }
public void SetUpgradeVisible(bool visible)
{
_upgradeOption.IsVisible = visible;
}
public void SetDestroyGain(int gain) public void SetDestroyGain(int gain)
{ {
_destroyOption.Price = Mathf.Max(0, gain); _destroyOption.Price = Mathf.Max(0, gain);

View File

@ -7,14 +7,16 @@ namespace GeometryTD.UI
{ {
public RepoFormRawData CreateInitialModel() public RepoFormRawData CreateInitialModel()
{ {
BackpackInventoryData sample = BuildSampleInventory(); BackpackInventoryData sample = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: SampleInventory();
return new RepoFormRawData return new RepoFormRawData
{ {
Inventory = sample Inventory = sample
}; };
} }
private static BackpackInventoryData BuildSampleInventory() public static BackpackInventoryData SampleInventory()
{ {
BackpackInventoryData inventory = new BackpackInventoryData BackpackInventoryData inventory = new BackpackInventoryData
{ {
@ -75,11 +77,11 @@ namespace GeometryTD.UI
BaseComponentInstanceId = baseComp.InstanceId, BaseComponentInstanceId = baseComp.InstanceId,
Stats = new DefenseTowerStatsData Stats = new DefenseTowerStatsData
{ {
AttackDamage = 30, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0.05f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 12f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 2f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 1.5f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Fire, AttackPropertyType = AttackPropertyType.Fire,
Tags = new[] { TagType.Fire, TagType.BurnSpread } Tags = new[] { TagType.Fire, TagType.BurnSpread }
@ -98,11 +100,11 @@ namespace GeometryTD.UI
BaseComponentInstanceId = 0, BaseComponentInstanceId = 0,
Stats = new DefenseTowerStatsData Stats = new DefenseTowerStatsData
{ {
AttackDamage = 50, AttackDamage = new[] { 200, 220, 240, 260, 300 },
DamageRandomRate = 0.03f, DamageRandomRate = new[] { 0f, 0f, 0f, 0f, 0f },
RotateSpeed = 20f, RotateSpeed = new[] { 200f, 210f, 220f, 230f, 240f },
AttackRange = 4f, AttackRange = new[] { 4.5f, 4.7f, 4.9f, 5.1f, 5.3f },
AttackSpeed = 1f, AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f },
AttackMethodType = AttackMethodType.NormalBullet, AttackMethodType = AttackMethodType.NormalBullet,
AttackPropertyType = AttackPropertyType.Physics, AttackPropertyType = AttackPropertyType.Physics,
Tags = new[] { TagType.Pierce } Tags = new[] { TagType.Pierce }

View File

@ -153,11 +153,9 @@ Transform:
m_Children: m_Children:
- {fileID: 513208573} - {fileID: 513208573}
- {fileID: 1604812193} - {fileID: 1604812193}
- {fileID: 159392563}
- {fileID: 1549230541}
- {fileID: 1758164286}
- {fileID: 2007255511} - {fileID: 2007255511}
- {fileID: 428539048} - {fileID: 428539048}
- {fileID: 1322505022}
m_Father: {fileID: 1852670053} m_Father: {fileID: 1852670053}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &120093239 --- !u!1 &120093239
@ -275,12 +273,12 @@ Transform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 159392562} m_GameObject: {fileID: 159392562}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 119167776} m_Father: {fileID: 1322505022}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &159392564 --- !u!114 &159392564
MonoBehaviour: MonoBehaviour:
@ -883,6 +881,85 @@ LightingSettings:
m_PVRTiledBaking: 0 m_PVRTiledBaking: 0
m_NumRaysToShootPerTexel: -1 m_NumRaysToShootPerTexel: -1
m_RespectSceneVisibilityWhenBakingGI: 0 m_RespectSceneVisibilityWhenBakingGI: 0
--- !u!1 &1239157580
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1239157581}
- component: {fileID: 1239157582}
m_Layer: 0
m_Name: PlayerInventory
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1239157581
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1239157580}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1322505022}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1239157582
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1239157580}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5db8d7bb243947fdbb9fe75a9d234d10, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &1322505021
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1322505022}
m_Layer: 0
m_Name: TowerDefense
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1322505022
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1322505021}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1239157581}
- {fileID: 159392563}
- {fileID: 1549230541}
- {fileID: 1758164286}
m_Father: {fileID: 119167776}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1454214586 --- !u!1 &1454214586
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@ -991,12 +1068,12 @@ Transform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1549230540} m_GameObject: {fileID: 1549230540}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 119167776} m_Father: {fileID: 1322505022}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1549230542 --- !u!114 &1549230542
MonoBehaviour: MonoBehaviour:
@ -1184,12 +1261,12 @@ Transform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1758164285} m_GameObject: {fileID: 1758164285}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 119167776} m_Father: {fileID: 1322505022}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1758164287 --- !u!114 &1758164287
MonoBehaviour: MonoBehaviour:

View File

@ -14,11 +14,11 @@
| 状态 | ID | 任务 | 交付物路径 | 验收标准 | | 状态 | ID | 任务 | 交付物路径 | 验收标准 |
|-----|-------|-------------------------------------|----------------------------------------------------------------------------------------|---------------------------| |-----|-------|-------------------------------------|----------------------------------------------------------------------------------------|---------------------------|
| [x] | P0-01 | 冻结 MVP 范围(只保留:战斗节点/事件节点/商店节点/节点后组装) | `docs/MVP-Scope.md` | 明确“做/不做”清单,团队评审通过 | | [x] | P0-01 | 冻结 MVP 范围(只保留:战斗节点/事件节点/商店节点/节点后组装) | `docs/MVP-Scope.md` | 明确“做/不做”清单,团队评审通过 |
| [ ] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 | | [x] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 |
| [ ] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 | | [x] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 |
| [ ] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 进入/退出节点时状态可持续传递 | | [x] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 进入/退出节点时状态可持续传递 |
| [ ] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss | `Assets/GameMain/Scripts/Scene/` | 每局都生成 10 节点且第 10 节点为 Boss | | [ ] | P0-05 | 实现 10 节点地图生成(最后节点固定 Boss | `Assets/GameMain/Scripts/Scene/` | 每局都生成 10 节点且第 10 节点为 Boss |
| [ ] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | 节点完成后可返回地图并进入下个节点 | | [x] | P0-06 | 实现节点选择与跳转流程(战斗/事件/商店) | `Assets/GameMain/Scripts/Procedure/` | 节点完成后可返回地图并进入下个节点 |
| [ ] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 | | [ ] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 |
| [ ] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 | | [ ] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 |
| [ ] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 | | [ ] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |