From daba9cbdf944e9283969d024cc4b358a982bce37 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Mon, 2 Mar 2026 19:50:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=93=E5=BA=93=E7=BB=84=E4=BB=B6=20+=20?= =?UTF-8?q?=E9=98=B2=E5=BE=A1=E5=A1=94=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `PlayerInventoryComponent`:玩家库存组件,收集金币/组件/防御塔等道具 - 补全防御塔的升级逻辑,最高 5 级 --- .../Scripts/Base/GameEntry.Builtin.cs | 4 +- .../GameMain/Scripts/Base/GameEntry.Custom.cs | 10 +- .../Components/DefenseTowerController.cs | 59 ++- .../CombatScheduler/CombatScheduler.cs | 1 + .../PlayerInventoryComponent.cs | 408 ++++++++++++++++++ .../PlayerInventoryComponent.cs.meta | 11 + .../DataStruct/DefenseTowerItemData.cs | 10 +- .../Entity/EntityData/DefenseTowerData.cs | 11 +- .../EntityLogic/CombatSelectInputService.cs | 20 +- .../Entity/EntityLogic/DefenseTowerEntity.cs | 15 +- .../Scripts/Entity/EntityLogic/MapEntity.cs | 8 +- .../Scripts/Procedure/ProcedureMenu.cs | 1 + .../Scene/Map/TowerPlacementService.cs | 190 +++++--- .../Controller/CombatSelectFormController.cs | 4 + .../RawData/CombatSelectFormUserData.cs | 1 + .../Combat/UseCase/CombatFinishFormUseCase.cs | 4 +- .../Combat/UseCase/CombatSelectFormUseCase.cs | 5 + .../UI/Game/UseCase/RepoFormUseCase.cs | 26 +- Assets/Launcher.unity | 95 +++- docs/TODO.md | 8 +- 20 files changed, 753 insertions(+), 138 deletions(-) create mode 100644 Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs.meta diff --git a/Assets/GameMain/Scripts/Base/GameEntry.Builtin.cs b/Assets/GameMain/Scripts/Base/GameEntry.Builtin.cs index 70535a7..b9ace14 100644 --- a/Assets/GameMain/Scripts/Base/GameEntry.Builtin.cs +++ b/Assets/GameMain/Scripts/Base/GameEntry.Builtin.cs @@ -5,14 +5,12 @@ // Feedback: mailto:ellan@gameframework.cn //------------------------------------------------------------ -using UnityEngine; using UnityGameFramework.Runtime; - /// /// 游戏入口。 /// -public partial class GameEntry : MonoBehaviour +public partial class GameEntry { /// /// 获取游戏基础组件。 diff --git a/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs b/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs index c1bfed6..a2e6a8f 100644 --- a/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs +++ b/Assets/GameMain/Scripts/Base/GameEntry.Custom.cs @@ -8,6 +8,8 @@ public partial class GameEntry { public static BuiltinDataComponent BuiltinData { get; private set; } + public static PlayerInventoryComponent PlayerInventory { get; private set; } + public static HPBarComponent HPBar { get; private set; } public static UIRouterComponent UIRouter { get; private set; } @@ -23,16 +25,12 @@ public partial class GameEntry private static void InitCustomComponents() { BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent(); + PlayerInventory = UnityGameFramework.Runtime.GameEntry.GetComponent(); HPBar = UnityGameFramework.Runtime.GameEntry.GetComponent(); UIRouter = UnityGameFramework.Runtime.GameEntry.GetComponent(); EventNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); CombatNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); ShopNode = UnityGameFramework.Runtime.GameEntry.GetComponent(); ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent(); - if (ResolutionAdapter == null) - { - UnityGameFramework.Runtime.Log.Warning( - "ResolutionAdapterComponent is missing. Please add it in Launcher scene and inject UI roots."); - } } -} +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Components/DefenseTowerController.cs b/Assets/GameMain/Scripts/Components/DefenseTowerController.cs index 123bbc7..7bcc268 100644 --- a/Assets/GameMain/Scripts/Components/DefenseTowerController.cs +++ b/Assets/GameMain/Scripts/Components/DefenseTowerController.cs @@ -8,6 +8,8 @@ namespace Components public class DefenseTowerController : MonoBehaviour { private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator"; + private const int MinTowerLevel = 0; + private const int MaxTowerLevel = 4; private static Material s_AttackRangeSharedMaterial; [SerializeField] private ShooterMuzzleComp _muzzleComp; @@ -15,7 +17,6 @@ namespace Components [SerializeField] private BasicBaseComp _baseComp; [SerializeField] private Transform _scanOrigin; [SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f; - [SerializeField] private bool _autoUpdate = true; [SerializeField] private LineRenderer _attackRangeRenderer; [SerializeField] [Min(12)] private int _attackRangeSegments = 64; [SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f; @@ -25,6 +26,7 @@ namespace Components private Transform _currentTarget; private float _retargetTimer; private float _attackRange; + private int _towerLevel; public Transform CurrentTarget => _currentTarget; @@ -33,17 +35,12 @@ namespace Components ResolveComponents(); } - private void Update() + public void OnInit(DefenseTowerStatsData stats) { - if (!_autoUpdate) - { - return; - } - - OnUpdate(Time.deltaTime); + OnInit(stats, MinTowerLevel); } - public void OnInit(DefenseTowerStatsData stats) + public void OnInit(DefenseTowerStatsData stats, int towerLevel) { ResolveComponents(); if (stats == null) @@ -51,10 +48,16 @@ namespace Components return; } - _muzzleComp?.OnInit(stats.AttackDamage, stats.AttackMethodType); - _bearingComp?.OnInit(stats.RotateSpeed, stats.AttackRange); - _baseComp?.OnInit(stats.AttackSpeed, stats.AttackPropertyType); - SetAttackRange(stats.AttackRange); + _towerLevel = Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel); + int attackDamage = ResolveIntValue(stats.AttackDamage, _towerLevel, 1, 1); + float rotateSpeed = ResolveFloatValue(stats.RotateSpeed, _towerLevel, 180f, 1f); + 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); _currentTarget = null; _retargetTimer = 0f; @@ -65,6 +68,7 @@ namespace Components SetAttackRangeVisible(false); _currentTarget = null; _retargetTimer = 0f; + _towerLevel = MinTowerLevel; _muzzleComp?.OnReset(); _bearingComp?.OnReset(); _baseComp?.OnReset(); @@ -86,11 +90,6 @@ namespace Components RebuildAttackRangeGeometry(); } - public void SetAutoUpdate(bool autoUpdate) - { - _autoUpdate = autoUpdate; - } - public void SetTarget(Transform target) { _currentTarget = target; @@ -195,6 +194,30 @@ namespace Components 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() { if (_attackRangeRenderer == null) diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index 17d0d03..4e1088b 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -245,6 +245,7 @@ namespace GeometryTD.CustomComponent { int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount; BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot(); + GameEntry.PlayerInventory?.MergeInventory(rewardInventory); // Step 1: stop runtime and clear enemy entities only. _enemyManager.EndPhase(); diff --git a/Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs b/Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs new file mode 100644 index 0000000..efd7bcd --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs @@ -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(); + } + + private static float[] CloneFloatArray(float[] source) + { + return source != null ? (float[])source.Clone() : Array.Empty(); + } + + private static TagType[] CloneTags(TagType[] source) + { + return source != null ? (TagType[])source.Clone() : Array.Empty(); + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs.meta b/Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs.meta new file mode 100644 index 0000000..bf5412d --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/PlayerInventoryComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5db8d7bb243947fdbb9fe75a9d234d10 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/DefenseTowerItemData.cs b/Assets/GameMain/Scripts/Definition/DataStruct/DefenseTowerItemData.cs index fe3a52f..de9140f 100644 --- a/Assets/GameMain/Scripts/Definition/DataStruct/DefenseTowerItemData.cs +++ b/Assets/GameMain/Scripts/Definition/DataStruct/DefenseTowerItemData.cs @@ -9,11 +9,11 @@ namespace GeometryTD.Definition [Serializable] public sealed class DefenseTowerStatsData { - public int AttackDamage { get; set; } - public float DamageRandomRate { get; set; } - public float RotateSpeed { get; set; } - public float AttackRange { get; set; } - public float AttackSpeed { get; set; } + public int[] AttackDamage { get; set; } + public float[] DamageRandomRate { get; set; } + public float[] RotateSpeed { get; set; } + public float[] AttackRange { get; set; } + public float[] AttackSpeed { get; set; } public AttackMethodType AttackMethodType { get; set; } public AttackPropertyType AttackPropertyType { get; set; } public TagType[] Tags { get; set; } diff --git a/Assets/GameMain/Scripts/Entity/EntityData/DefenseTowerData.cs b/Assets/GameMain/Scripts/Entity/EntityData/DefenseTowerData.cs index c67fe36..00ac405 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/DefenseTowerData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/DefenseTowerData.cs @@ -8,13 +8,16 @@ namespace GeometryTD.Entity.EntityData public class DefenseTowerData : EntityDataBase { [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) { Position = position; Rotation = rotation; _stats = stats ?? new DefenseTowerStatsData(); + _towerLevel = Mathf.Max(0, towerLevel); } public DefenseTowerStatsData Stats @@ -22,5 +25,11 @@ namespace GeometryTD.Entity.EntityData get => _stats; set => _stats = value ?? new DefenseTowerStatsData(); } + + public int TowerLevel + { + get => _towerLevel; + set => _towerLevel = Mathf.Max(0, value); + } } } diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectInputService.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectInputService.cs index 2cbcb65..47c4fc6 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectInputService.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/CombatSelectInputService.cs @@ -8,9 +8,15 @@ namespace GeometryTD.Entity { public sealed class CombatSelectInputService { - public bool TryBuildUserData(Tilemap tilemap, Transform mapTransform, - IReadOnlyDictionary towerEntityIdByFoundationCell, Func isFoundationCell, - int upgradeCost, int destroyGain, out CombatSelectFormUserData userData) + public bool TryBuildUserData( + Tilemap tilemap, + Transform mapTransform, + IReadOnlyDictionary towerEntityIdByFoundationCell, + Func isFoundationCell, + Func isTowerAtMaxLevel, + int upgradeCost, + int destroyGain, + out CombatSelectFormUserData userData) { userData = null; if (tilemap == null || !TryGetPointerWorldPosition(tilemap, mapTransform, out Vector3 worldPosition, @@ -44,13 +50,15 @@ namespace GeometryTD.Entity WorldPosition = worldPosition, CellPosition = clickedCell, TowerEntityId = towerEntityId, + IsTowerAtMaxLevel = towerEntityId != 0 && isTowerAtMaxLevel != null && isTowerAtMaxLevel(towerEntityId), UpgradeCost = Mathf.Max(0, upgradeCost), DestroyGain = Mathf.Max(0, destroyGain) }; 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) { worldPosition = Vector3.zero; @@ -63,7 +71,9 @@ namespace GeometryTD.Entity } 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 ? -mainCamera.transform.forward : Vector3.forward; diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/DefenseTowerEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/DefenseTowerEntity.cs index 18505dd..2856455 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/DefenseTowerEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/DefenseTowerEntity.cs @@ -1,4 +1,5 @@ using Components; +using GeometryTD.Definition; using GeometryTD.Entity.EntityData; using UnityGameFramework.Runtime; @@ -13,6 +14,17 @@ namespace GeometryTD.Entity _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) { base.OnInit(userData); @@ -41,8 +53,7 @@ namespace GeometryTD.Entity return; } - _towerController.SetAutoUpdate(false); - _towerController.OnInit(towerData.Stats); + _towerController.OnInit(towerData.Stats, towerData.TowerLevel); } protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs index 3e51c15..68e94de 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs @@ -242,7 +242,8 @@ namespace GeometryTD.Entity if (_combatSelectInputService == null || !_combatSelectInputService.TryBuildUserData(Tilemap, CachedTransform, _towerPlacementService != null ? _towerPlacementService.TowerEntityIdByFoundationCell : null, - IsFoundationCell, _upgradeCost, _destroyGain, out CombatSelectFormUserData userData)) + IsFoundationCell, IsTowerAtMaxLevel, _upgradeCost, _destroyGain, + out CombatSelectFormUserData userData)) { userData = new CombatSelectFormUserData { @@ -254,6 +255,11 @@ namespace GeometryTD.Entity GameEntry.UIRouter.OpenUI(UIFormType.CombatSelectForm, userData); } + private bool IsTowerAtMaxLevel(int towerEntityId) + { + return _towerPlacementService != null && _towerPlacementService.IsTowerAtMaxLevel(towerEntityId); + } + private void ApplySelectedObject(CombatSelectFormUserData userData) { _towerSelectionPresenter?.ApplySelectedObject(userData); diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMenu.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMenu.cs index 4d29aee..5bb8a26 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMenu.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMenu.cs @@ -35,6 +35,7 @@ namespace GeometryTD.Procedure GameEntry.EventNode.OnInit(); GameEntry.CombatNode.OnInit(LevelThemeType.Plain); GameEntry.ShopNode.OnInit(); + GameEntry.PlayerInventory?.OnInit(); _repoFormUseCase = new RepoFormUseCase(); GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase); diff --git a/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs b/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs index a1f152f..7e7e493 100644 --- a/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs +++ b/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs @@ -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 _towerEntityIdByFoundationCell = new(); private readonly Dictionary _foundationCellByTowerEntityId = new(); private readonly Dictionary _towerStatsByEntityId = new(); + private readonly Dictionary _towerLevelByEntityId = new(); private readonly List _towerEntityIdBuffer = new(); public IReadOnlyDictionary TowerEntityIdByFoundationCell => _towerEntityIdByFoundationCell; public IReadOnlyDictionary 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 isFoundationCell, int buildIndex, int[] buildTowerCosts, int towerTypeId, Tilemap tilemap, Func tryConsumeCoin, Action 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() @@ -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() @@ -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() @@ -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() @@ -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() @@ -273,28 +290,63 @@ namespace GeometryTD.Map TagType[] copiedTags = source.Tags != null ? (TagType[])source.Tags.Clone() : Array.Empty(); return new DefenseTowerStatsData { - AttackDamage = source.AttackDamage, - DamageRandomRate = source.DamageRandomRate, - RotateSpeed = source.RotateSpeed, - AttackRange = source.AttackRange, - AttackSpeed = source.AttackSpeed, + AttackDamage = source.AttackDamage != null ? (int[])source.AttackDamage.Clone() : Array.Empty(), + DamageRandomRate = source.DamageRandomRate != null + ? (float[])source.DamageRandomRate.Clone() + : Array.Empty(), + RotateSpeed = source.RotateSpeed != null ? (float[])source.RotateSpeed.Clone() : Array.Empty(), + AttackRange = source.AttackRange != null ? (float[])source.AttackRange.Clone() : Array.Empty(), + AttackSpeed = source.AttackSpeed != null ? (float[])source.AttackSpeed.Clone() : Array.Empty(), 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[] values) + { + return values != null ? values.Length : 0; } } } diff --git a/Assets/GameMain/Scripts/UI/Combat/Controller/CombatSelectFormController.cs b/Assets/GameMain/Scripts/UI/Combat/Controller/CombatSelectFormController.cs index c380d18..fb67ffa 100644 --- a/Assets/GameMain/Scripts/UI/Combat/Controller/CombatSelectFormController.cs +++ b/Assets/GameMain/Scripts/UI/Combat/Controller/CombatSelectFormController.cs @@ -98,10 +98,12 @@ namespace GeometryTD.UI switch (userData.ClickObjectType) { case CombatSelectClickObjectType.Foundation: + _useCase.SetUpgradeVisible(true); _useCase.ShowForFoundation(userData.ContentPosition); break; case CombatSelectClickObjectType.Tower: _useCase.SetUpgradePrice(userData.UpgradeCost); + _useCase.SetUpgradeVisible(!userData.IsTowerAtMaxLevel); _useCase.SetDestroyGain(userData.DestroyGain); _useCase.ShowForTower(userData.ContentPosition); break; @@ -133,6 +135,7 @@ namespace GeometryTD.UI return null; } + _useCase.SetUpgradeVisible(true); _useCase.ShowForFoundation(contentPosition); return OpenUI(_useCase.TryRefresh()); } @@ -146,6 +149,7 @@ namespace GeometryTD.UI } _useCase.SetUpgradePrice(upgradeCost); + _useCase.SetUpgradeVisible(true); _useCase.SetDestroyGain(destroyGain); _useCase.ShowForTower(contentPosition); return OpenUI(_useCase.TryRefresh()); diff --git a/Assets/GameMain/Scripts/UI/Combat/RawData/CombatSelectFormUserData.cs b/Assets/GameMain/Scripts/UI/Combat/RawData/CombatSelectFormUserData.cs index 510056e..a9c0b9a 100644 --- a/Assets/GameMain/Scripts/UI/Combat/RawData/CombatSelectFormUserData.cs +++ b/Assets/GameMain/Scripts/UI/Combat/RawData/CombatSelectFormUserData.cs @@ -9,6 +9,7 @@ namespace GeometryTD.UI public Vector3 WorldPosition; public Vector3Int CellPosition; public int TowerEntityId; + public bool IsTowerAtMaxLevel; public int UpgradeCost; public int DestroyGain; } diff --git a/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs b/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs index 177b1e7..1499d84 100644 --- a/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatFinishFormUseCase.cs @@ -6,7 +6,6 @@ namespace GeometryTD.UI { public class CombatFinishFormUseCase : IUIUseCase { - private readonly RepoFormUseCase _repoFormUseCase = new RepoFormUseCase(); private CombatScheduler _combatScheduler; private int _defeatedEnemyCount; private int _gainedGold; @@ -57,8 +56,7 @@ namespace GeometryTD.UI BackpackInventoryData rewardInventory = _rewardInventory; if (rewardInventory == null) { - RepoFormRawData repoRawData = _repoFormUseCase.CreateInitialModel(); - rewardInventory = repoRawData != null ? repoRawData.Inventory : null; + rewardInventory = RepoFormUseCase.SampleInventory(); } return new CombatFinishFormRawData diff --git a/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatSelectFormUseCase.cs b/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatSelectFormUseCase.cs index 2843949..8940786 100644 --- a/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatSelectFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatSelectFormUseCase.cs @@ -134,6 +134,11 @@ namespace GeometryTD.UI _upgradeOption.Price = Mathf.Max(0, cost); } + public void SetUpgradeVisible(bool visible) + { + _upgradeOption.IsVisible = visible; + } + public void SetDestroyGain(int gain) { _destroyOption.Price = Mathf.Max(0, gain); diff --git a/Assets/GameMain/Scripts/UI/Game/UseCase/RepoFormUseCase.cs b/Assets/GameMain/Scripts/UI/Game/UseCase/RepoFormUseCase.cs index 1b2d1ee..9b3f188 100644 --- a/Assets/GameMain/Scripts/UI/Game/UseCase/RepoFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Game/UseCase/RepoFormUseCase.cs @@ -7,14 +7,16 @@ namespace GeometryTD.UI { public RepoFormRawData CreateInitialModel() { - BackpackInventoryData sample = BuildSampleInventory(); + BackpackInventoryData sample = GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : SampleInventory(); return new RepoFormRawData { Inventory = sample }; } - private static BackpackInventoryData BuildSampleInventory() + public static BackpackInventoryData SampleInventory() { BackpackInventoryData inventory = new BackpackInventoryData { @@ -75,11 +77,11 @@ namespace GeometryTD.UI BaseComponentInstanceId = baseComp.InstanceId, Stats = new DefenseTowerStatsData { - AttackDamage = 30, - DamageRandomRate = 0.05f, - RotateSpeed = 12f, - AttackRange = 2f, - 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.Fire, Tags = new[] { TagType.Fire, TagType.BurnSpread } @@ -98,11 +100,11 @@ namespace GeometryTD.UI BaseComponentInstanceId = 0, Stats = new DefenseTowerStatsData { - AttackDamage = 50, - DamageRandomRate = 0.03f, - RotateSpeed = 20f, - AttackRange = 4f, - 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 = new[] { TagType.Pierce } diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity index fb1889a..47d7be6 100644 --- a/Assets/Launcher.unity +++ b/Assets/Launcher.unity @@ -153,11 +153,9 @@ Transform: m_Children: - {fileID: 513208573} - {fileID: 1604812193} - - {fileID: 159392563} - - {fileID: 1549230541} - - {fileID: 1758164286} - {fileID: 2007255511} - {fileID: 428539048} + - {fileID: 1322505022} m_Father: {fileID: 1852670053} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &120093239 @@ -275,12 +273,12 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 159392562} 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_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 119167776} + m_Father: {fileID: 1322505022} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &159392564 MonoBehaviour: @@ -883,6 +881,85 @@ LightingSettings: m_PVRTiledBaking: 0 m_NumRaysToShootPerTexel: -1 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 GameObject: m_ObjectHideFlags: 0 @@ -991,12 +1068,12 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1549230540} 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_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 119167776} + m_Father: {fileID: 1322505022} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1549230542 MonoBehaviour: @@ -1184,12 +1261,12 @@ Transform: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1758164285} 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_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] - m_Father: {fileID: 119167776} + m_Father: {fileID: 1322505022} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!114 &1758164287 MonoBehaviour: diff --git a/docs/TODO.md b/docs/TODO.md index 12a1b83..e54f6c5 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -14,11 +14,11 @@ | 状态 | ID | 任务 | 交付物路径 | 验收标准 | |-----|-------|-------------------------------------|----------------------------------------------------------------------------------------|---------------------------| | [x] | P0-01 | 冻结 MVP 范围(只保留:战斗节点/事件节点/商店节点/节点后组装) | `docs/MVP-Scope.md` | 明确“做/不做”清单,团队评审通过 | -| [ ] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 | -| [ ] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 | -| [ ] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 进入/退出节点时状态可持续传递 | +| [x] | P0-02 | 补齐数据表:组件、配件、敌人、波次、节点、事件、商店商品 | `Assets/GameMain/DataTables/*.txt` | 游戏启动可无报错加载全部新增表 | +| [x] | P0-03 | 新增/完善 DataRow 解析类 | `Assets/GameMain/Scripts/DataTable/*.cs` | 每个新增表都有对应 DR 类并成功解析 | +| [x] | P0-04 | 建立单局 Run 状态模型(金币、生命、库存、当前节点) | `Assets/GameMain/Scripts/Procedure/` | 进入/退出节点时状态可持续传递 | | [ ] | 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/`
`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 | | [ ] | P0-08 | 胜利波次与结算规则(100/80/50/<50) | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 | | [ ] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |