From 5ba94828a82d5a58b2675903422c8236bcfed310 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Mon, 2 Mar 2026 17:23:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=A8=20`CombatNode`=20=E9=80=BB?= =?UTF-8?q?=E8=BE=91=20+=20=E9=87=8D=E6=9E=84=20`EnemyManager`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加了关卡内的难度系数与掉落 - 难度系数:每循环一轮波次敌人血量翻倍 - 道具掉落:按照掉落概率曲线根据波次计算当前爆率,并从新增的道具池里选择道具 - `CombatResourceManager`:统一维护关卡内资源状态(硬币/金币/道具掉落) - 重构 `EnemyManager`: - `EnemyManager`:编排子服务,不承载具体业务细节 - `EnemySpawnDirector`:管理刷怪时序 - `SpawnerResolver`:管理出生点与路径 - `EnemyLifecycleTracker`:追踪敌人生命周期 - `EnemyConfigService`:管理敌人配置与倍率 --- Assets/GameMain/DataTables/BaseComp.txt | 12 +- Assets/GameMain/DataTables/BearingComp.txt | 12 +- Assets/GameMain/DataTables/Level.txt | 6 +- Assets/GameMain/DataTables/MuzzleComp.txt | 12 +- .../GameMain/DataTables/OutGameDropPool.txt | 21 + .../DataTables/OutGameDropPool.txt.meta | 7 + .../CombatNode/CombatScheduler.meta | 3 + .../CombatEventBridge.cs | 0 .../CombatEventBridge.cs.meta | 0 .../CombatLoadSession.cs | 0 .../CombatLoadSession.cs.meta | 0 .../CombatScheduler/CombatResourceManager.cs | 398 ++++++++++++ .../CombatResourceManager.cs.meta | 11 + .../{ => CombatScheduler}/CombatScheduler.cs | 70 ++- .../CombatScheduler.cs.meta | 0 .../{ => CombatScheduler}/PhaseLoopRuntime.cs | 0 .../PhaseLoopRuntime.cs.meta | 0 .../CombatNode/EnemyManager.cs | 566 ------------------ .../CombatNode/EnemyManager.meta | 3 + .../EnemyManager/EnemyConfigService.cs | 104 ++++ .../EnemyManager/EnemyConfigService.cs.meta | 3 + .../EnemyManager/EnemyLifecycleTracker.cs | 75 +++ .../EnemyLifecycleTracker.cs.meta | 11 + .../CombatNode/EnemyManager/EnemyManager.cs | 241 ++++++++ .../{ => EnemyManager}/EnemyManager.cs.meta | 0 .../EnemyManager/EnemySpawnDirector.cs | 203 +++++++ .../EnemyManager/EnemySpawnDirector.cs.meta | 11 + .../EnemyManager/SpawnerResolver.cs | 110 ++++ .../EnemyManager/SpawnerResolver.cs.meta | 11 + .../GameMain/Scripts/DataTable/DRBaseComp.cs | 8 +- .../Scripts/DataTable/DRBearingComp.cs | 12 + .../Scripts/DataTable/DRMuzzleComp.cs | 6 + .../Scripts/DataTable/DROutGameDropPool.cs | 72 +++ .../DataTable/DROutGameDropPool.cs.meta | 11 + .../CombatEnemyHpRateChangedEventArgs.cs | 31 + .../CombatEnemyHpRateChangedEventArgs.cs.meta | 11 + .../Scripts/Procedure/ProcedurePreload.cs | 3 +- .../Combat/Context/CombatInfoFormContext.cs | 1 + .../Controller/CombatInfoFormController.cs | 19 + .../Combat/RawData/CombatInfoFormRawData.cs | 1 + .../Combat/UseCase/CombatInfoFormUseCase.cs | 20 + .../Scripts/UI/Combat/View/CombatInfoForm.cs | 12 + .../Scripts/UI/General/View/DialogForm.cs | 27 +- .../UI/UIForms/CombatFinishForm.prefab | 2 +- .../GameMain/UI/UIForms/CombatInfoForm.prefab | 137 +++++ Assets/GameMain/UI/UIForms/RepoForm.prefab | 48 +- .../UI/UIItems/TowerSelectItem.prefab | 2 +- Assets/Launcher.unity | 205 +------ docs/CombatNodeArchitecture.md | 292 +++------ 49 files changed, 1763 insertions(+), 1047 deletions(-) create mode 100644 Assets/GameMain/DataTables/OutGameDropPool.txt create mode 100644 Assets/GameMain/DataTables/OutGameDropPool.txt.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.meta rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/CombatEventBridge.cs (100%) rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/CombatEventBridge.cs.meta (100%) rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/CombatLoadSession.cs (100%) rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/CombatLoadSession.cs.meta (100%) create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs.meta rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/CombatScheduler.cs (87%) rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/CombatScheduler.cs.meta (100%) rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/PhaseLoopRuntime.cs (100%) rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => CombatScheduler}/PhaseLoopRuntime.cs.meta (100%) delete mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs rename Assets/GameMain/Scripts/CustomComponent/CombatNode/{ => EnemyManager}/EnemyManager.cs.meta (100%) create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs.meta create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs create mode 100644 Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs.meta create mode 100644 Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs create mode 100644 Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs.meta create mode 100644 Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs create mode 100644 Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs.meta diff --git a/Assets/GameMain/DataTables/BaseComp.txt b/Assets/GameMain/DataTables/BaseComp.txt index d4522ad..400c172 100644 --- a/Assets/GameMain/DataTables/BaseComp.txt +++ b/Assets/GameMain/DataTables/BaseComp.txt @@ -1,6 +1,6 @@ -# Id 策划备注 EntityId Name AttackSpeed PropertyType Constraint PossibleTag -# int int string float[] AttackPropertyType string TagType[] -# 底座编号 实体Id 底座名 攻击速度(秒/次) 攻击属性 属性约束 可能出现的Tag - 1 301 元素底座 [2,1.5,1,0.8,0.7] Fire [Fire,BurnSpread,IgniteBurst,Inferno] - 2 301 控制底座 [4,3.8,3.6,3.4,3.2] Ice [Ice,FreezeMask,Shatter,AbsoluteZero] - 3 301 穿透底座 [1,0,8,0,6,0.4,0.2] Physics [Pierce,Crit,Overpenetrate,Execution] +# Id 策划备注 EntityId Name AttackSpeed AttackSpeedPerLevel PropertyType Constraint PossibleTag +# int int string float[] float AttackPropertyType string TagType[] +# 底座编号 实体Id 底座名 各品质攻击速度(秒/次) 每级提升攻击速度 攻击属性 属性约束 可能出现的Tag + 1 301 元素底座 [2,1.5,1,0.8,0.7] -0.2 Fire [Fire,BurnSpread,IgniteBurst,Inferno] + 2 301 控制底座 [4,3.8,3.6,3.4,3.2] -0.1 Ice [Ice,FreezeMask,Shatter,AbsoluteZero] + 3 301 穿透底座 [1,0,8,0,6,0.4,0.2] 0 Physics [Pierce,Crit,Overpenetrate,Execution] diff --git a/Assets/GameMain/DataTables/BearingComp.txt b/Assets/GameMain/DataTables/BearingComp.txt index 981351b..2e1871c 100644 --- a/Assets/GameMain/DataTables/BearingComp.txt +++ b/Assets/GameMain/DataTables/BearingComp.txt @@ -1,6 +1,6 @@ -# Id 策划备注 EntityId Name RotateSpeed AttackRange Constraint PossibleTag -# int int string float[] float[] string TagType[] -# 轴承编号 实体Id 轴承名 各品质旋转速度 各品质攻击范围 属性约束 可能出现的Tag - 1 201 元素轴承 [10,12,13,14,15] [2,2,2,2,2] [Fire,BurnSpread,IgniteBurst,Inferno] - 2 201 控制轴承 [20,25,30,32,35] [6,6.5,7,8,8] [Ice,FreezeMask,Shatter,AbsoluteZero] - 3 201 穿透轴承 [60,70,80,90,100] [4,4.5,5,5.5,6] [Pierce,Crit,Overpenetrate,Execution] +# Id 策划备注 EntityId Name RotateSpeed RotateSpeedPerLevel AttackRange AttackRangePerLevel Constraint PossibleTag +# int int string float[] float float[] float string TagType[] +# 轴承编号 实体Id 轴承名 各品质旋转速度 每级提升旋转速度 各品质攻击范围 每级提升攻击范围 属性约束 可能出现的Tag + 1 201 元素轴承 [10,12,13,14,15] 15 [2,2,2,2,2] 0.3 [Fire,BurnSpread,IgniteBurst,Inferno] + 2 201 控制轴承 [20,25,30,32,35] 0 [6,6.5,7,8,8] 0.2 [Ice,FreezeMask,Shatter,AbsoluteZero] + 3 201 穿透轴承 [60,70,80,90,100] 20 [4,4.5,5,5.5,6] 0 [Pierce,Crit,Overpenetrate,Execution] diff --git a/Assets/GameMain/DataTables/Level.txt b/Assets/GameMain/DataTables/Level.txt index ab6d063..75e60cb 100644 --- a/Assets/GameMain/DataTables/Level.txt +++ b/Assets/GameMain/DataTables/Level.txt @@ -2,6 +2,6 @@ # int LevelThemeType int int VictoryType string int # 关卡号 策划备注 关卡所属主题类型 基地初始生命 初始硬币 胜利条件 胜利参数 奖励金币 1 平原1 Plain 100 100 PhasesCleared 30 - 2 平原2 Plain 100 100 PhasesCleared 30 - 3 平原3 Plain 100 100 PhasesCleared 40 - 4 平原4 Plain 100 100 BossDead 100 + 2 平原2 Plain 100 120 PhasesCleared 30 + 3 平原3 Plain 100 110 PhasesCleared 40 + 4 平原4 Plain 100 150 BossDead 100 diff --git a/Assets/GameMain/DataTables/MuzzleComp.txt b/Assets/GameMain/DataTables/MuzzleComp.txt index 00a1460..a693061 100644 --- a/Assets/GameMain/DataTables/MuzzleComp.txt +++ b/Assets/GameMain/DataTables/MuzzleComp.txt @@ -1,6 +1,6 @@ -# Id 策划备注 EntityId Name AttackDamage DamageRandomRate Method Constraint PossibleTag -# int int string int[] float AttackMethodType Constraint TagType[] -# 枪口编号 策划备注 实体Id 枪口名 各品质伤害 伤害浮动 攻击方式 Constraint 可能出现的Tag - 1 101 元素枪口 [20,30,40,50,80] 0.05 NormalBullet [Fire,BurnSpread,IgniteBurst,Inferno] - 2 101 控制枪口 [30,50,70,90,100] 0.01 NormalBullet [Ice,FreezeMask,Shatter,AbsoluteZero] - 3 101 穿透枪口 [50,55,60,80,90] 0.02 NormalBullet [Pierce,Crit,Overpenetrate,Execution] +# Id 策划备注 EntityId Name AttackDamage AttackDamagePerLevel DamageRandomRate Method Constraint PossibleTag +# int int string int[] int float AttackMethodType Constraint TagType[] +# 枪口编号 策划备注 实体Id 枪口名 各品质伤害 每级伤害提升 伤害浮动 攻击方式 Constraint 可能出现的Tag + 1 101 元素枪口 [20,30,40,50,80] 10 0.05 NormalBullet [Fire,BurnSpread,IgniteBurst,Inferno] + 2 101 控制枪口 [30,50,70,90,100] 20 0.01 NormalBullet [Ice,FreezeMask,Shatter,AbsoluteZero] + 3 101 穿透枪口 [50,55,60,80,90] 30 0.02 NormalBullet [Pierce,Crit,Overpenetrate,Execution] diff --git a/Assets/GameMain/DataTables/OutGameDropPool.txt b/Assets/GameMain/DataTables/OutGameDropPool.txt new file mode 100644 index 0000000..c6876d6 --- /dev/null +++ b/Assets/GameMain/DataTables/OutGameDropPool.txt @@ -0,0 +1,21 @@ +# Id 列1 LevelThemeType Rarity ItemType ItemId Weight MinPhase MaxPhase +# int LevelThemeType RarityType ItemType int int int int +# 道具池号 策划备注 关卡所属主题类型 道具稀有度 道具类型 道具Id 权重 出现的最低波次 出现的最高波次 + 1 平原战斗池 Plain White MuzzleComp 1 10 1 10 + 2 Plain White MuzzleComp 2 10 1 10 + 3 Plain White MuzzleComp 3 10 1 10 + 4 Plain White BearingComp 1 10 1 10 + 5 Plain White BearingComp 2 10 1 10 + 6 Plain White BearingComp 3 10 1 10 + 7 Plain White BaseComp 1 10 1 10 + 8 Plain White BaseComp 2 10 1 10 + 9 Plain White BaseComp 3 10 1 10 + 10 Plain Green MuzzleComp 1 5 1 10 + 11 Plain Green MuzzleComp 2 5 1 10 + 12 Plain Green MuzzleComp 3 5 1 10 + 13 Plain Green BearingComp 1 5 1 10 + 14 Plain Green BearingComp 2 5 1 10 + 15 Plain Green BearingComp 3 5 1 10 + 16 Plain Green BaseComp 1 5 1 10 + 17 Plain Green BaseComp 2 5 1 10 + 18 Plain Green BaseComp 3 5 1 10 diff --git a/Assets/GameMain/DataTables/OutGameDropPool.txt.meta b/Assets/GameMain/DataTables/OutGameDropPool.txt.meta new file mode 100644 index 0000000..71afedf --- /dev/null +++ b/Assets/GameMain/DataTables/OutGameDropPool.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 934d71d10a1e14141a275b57b1825dfd +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.meta new file mode 100644 index 0000000..5a9df07 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c52befd7a6c741b9bcd8f4effd7879b8 +timeCreated: 1772440848 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatEventBridge.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatEventBridge.cs rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatEventBridge.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatEventBridge.cs.meta rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatEventBridge.cs.meta diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatLoadSession.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatLoadSession.cs rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatLoadSession.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatLoadSession.cs.meta rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs.meta diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs new file mode 100644 index 0000000..00ca3c9 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs @@ -0,0 +1,398 @@ +using System; +using System.Collections.Generic; +using GameFramework.DataTable; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; +using Random = UnityEngine.Random; + +namespace GeometryTD.CustomComponent +{ + internal sealed class CombatResourceManager + { + private const float DropChanceBase = 0.05f; + private const float DropChancePerPhase = 0.0125f; + private const float DropChanceCap = 0.45f; + private const float RarityCurveScalePhase = 30f; + + private readonly List _eligibleDropPoolBuffer = new(); + private readonly Dictionary _rarityRollWeightBuffer = new(); + private readonly BackpackInventoryData _rewardInventory = new(); + + private IDataTable _drOutGameDropPool; + private IDataTable _drMuzzleComp; + private IDataTable _drBearingComp; + private IDataTable _drBaseComp; + private long _nextDropItemInstanceId = 1; + + public int GainedCoin { get; private set; } + public int GainedGold { get; private set; } + + public void Reset() + { + GainedCoin = 0; + GainedGold = 0; + _rewardInventory.Gold = 0; + _rewardInventory.MuzzleComponents.Clear(); + _rewardInventory.BearingComponents.Clear(); + _rewardInventory.BaseComponents.Clear(); + _rewardInventory.Towers.Clear(); + _nextDropItemInstanceId = 1; + } + + public BackpackInventoryData GetRewardInventorySnapshot() + { + return new BackpackInventoryData + { + Gold = _rewardInventory.Gold, + MuzzleComponents = new List(_rewardInventory.MuzzleComponents), + BearingComponents = new List(_rewardInventory.BearingComponents), + BaseComponents = new List(_rewardInventory.BaseComponents), + Towers = new List(_rewardInventory.Towers) + }; + } + + public void AddEnemyDefeatedReward(int gainedCoin, int gainedGold) + { + int coin = Mathf.Max(0, gainedCoin); + int gold = Mathf.Max(0, gainedGold); + GainedCoin += coin; + GainedGold += gold; + if (gold > 0) + { + _rewardInventory.Gold += gold; + } + + GameEntry.CombatNode?.ApplyEnemyDropReward(coin, gold); + } + + public bool TryRollOutGameItemDrop(int displayPhaseIndex, LevelThemeType themeType) + { + int phaseIndex = Mathf.Max(1, displayPhaseIndex); + float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap); + if (Random.value > dropChance) + { + return false; + } + + if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow)) + { + return false; + } + + if (!TryBuildDropItem(selectedRow, out TowerCompItemData droppedItem)) + { + return false; + } + + if (droppedItem is MuzzleCompItemData muzzleCompItemData) + { + _rewardInventory.MuzzleComponents.Add(muzzleCompItemData); + return true; + } + + if (droppedItem is BearingCompItemData bearingCompItemData) + { + _rewardInventory.BearingComponents.Add(bearingCompItemData); + return true; + } + + if (droppedItem is BaseCompItemData baseCompItemData) + { + _rewardInventory.BaseComponents.Add(baseCompItemData); + return true; + } + + return false; + } + + private bool TryPickDropPoolRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow) + { + selectedRow = null; + IDataTable dropTable = EnsureOutGameDropPoolTable(); + if (dropTable == null) + { + return false; + } + + _eligibleDropPoolBuffer.Clear(); + DROutGameDropPool[] allRows = dropTable.GetAllDataRows(); + for (int i = 0; i < allRows.Length; i++) + { + DROutGameDropPool row = allRows[i]; + if (row == null) + { + continue; + } + + if (row.LevelThemeType != themeType) + { + continue; + } + + if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase) + { + continue; + } + + _eligibleDropPoolBuffer.Add(row); + } + + if (_eligibleDropPoolBuffer.Count <= 0) + { + return false; + } + + RarityType selectedRarity = RollRarity(displayPhaseIndex, _eligibleDropPoolBuffer); + if (selectedRarity == RarityType.None) + { + return false; + } + + int totalWeight = 0; + for (int i = 0; i < _eligibleDropPoolBuffer.Count; i++) + { + DROutGameDropPool row = _eligibleDropPoolBuffer[i]; + if (row.Rarity != selectedRarity) + { + continue; + } + + totalWeight += Mathf.Max(1, row.Weight); + } + + if (totalWeight <= 0) + { + return false; + } + + int randomWeight = Random.Range(1, totalWeight + 1); + int cumulativeWeight = 0; + for (int i = 0; i < _eligibleDropPoolBuffer.Count; i++) + { + DROutGameDropPool row = _eligibleDropPoolBuffer[i]; + if (row.Rarity != selectedRarity) + { + continue; + } + + cumulativeWeight += Mathf.Max(1, row.Weight); + if (randomWeight <= cumulativeWeight) + { + selectedRow = row; + return true; + } + } + + selectedRow = _eligibleDropPoolBuffer[_eligibleDropPoolBuffer.Count - 1]; + return selectedRow != null; + } + + private RarityType RollRarity(int displayPhaseIndex, List candidates) + { + _rarityRollWeightBuffer.Clear(); + float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase); + for (int i = 0; i < candidates.Count; i++) + { + DROutGameDropPool row = candidates[i]; + if (row == null) + { + continue; + } + + float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT); + if (curveWeight <= 0f) + { + continue; + } + + if (_rarityRollWeightBuffer.TryGetValue(row.Rarity, out float existingWeight)) + { + _rarityRollWeightBuffer[row.Rarity] = existingWeight + Mathf.Max(1, row.Weight) * curveWeight; + } + else + { + _rarityRollWeightBuffer[row.Rarity] = Mathf.Max(1, row.Weight) * curveWeight; + } + } + + float totalWeight = 0f; + foreach (var pair in _rarityRollWeightBuffer) + { + totalWeight += Mathf.Max(0f, pair.Value); + } + + if (totalWeight <= 0f) + { + return RarityType.None; + } + + float randomWeight = Random.value * totalWeight; + float cumulativeWeight = 0f; + foreach (var pair in _rarityRollWeightBuffer) + { + cumulativeWeight += Mathf.Max(0f, pair.Value); + if (randomWeight <= cumulativeWeight) + { + return pair.Key; + } + } + + foreach (var pair in _rarityRollWeightBuffer) + { + return pair.Key; + } + + return RarityType.None; + } + + private static float GetRarityCurveWeight(RarityType rarityType, float phaseT) + { + float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f)); + switch (rarityType) + { + case RarityType.White: + return Mathf.Max(0.05f, 0.18f + 1.25f * hump); + case RarityType.Green: + return Mathf.Max(0.05f, 0.35f + 0.55f * hump); + case RarityType.Blue: + return 0.18f + 0.55f * phaseT; + case RarityType.Purple: + return 0.05f + 0.22f * phaseT; + case RarityType.Red: + return 0.01f + 0.08f * phaseT * phaseT; + default: + return 0f; + } + } + + private IDataTable EnsureOutGameDropPoolTable() + { + if (_drOutGameDropPool != null) + { + return _drOutGameDropPool; + } + + _drOutGameDropPool = GameEntry.DataTable.GetDataTable(); + return _drOutGameDropPool; + } + + private bool TryBuildDropItem(DROutGameDropPool row, out TowerCompItemData droppedItem) + { + droppedItem = null; + if (row == null || row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType)) + { + return false; + } + + string itemType = row.ItemType.Trim(); + if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase)) + { + return TryBuildMuzzleCompItem(row, out droppedItem); + } + + if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase)) + { + return TryBuildBearingCompItem(row, out droppedItem); + } + + if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase)) + { + return TryBuildBaseCompItem(row, out droppedItem); + } + + return false; + } + + private bool TryBuildMuzzleCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem) + { + droppedItem = null; + _drMuzzleComp ??= GameEntry.DataTable.GetDataTable(); + if (_drMuzzleComp == null) + { + return false; + } + + DRMuzzleComp config = _drMuzzleComp.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = new MuzzleCompItemData + { + InstanceId = _nextDropItemInstanceId++, + ConfigId = config.Id, + Name = config.Name, + Rarity = row.Rarity, + Endurance = 100f, + Constraint = config.Constraint, + Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty(), + AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty(), + DamageRandomRate = config.DamageRandomRate, + AttackMethodType = config.AttackMethodType + }; + return true; + } + + private bool TryBuildBearingCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem) + { + droppedItem = null; + _drBearingComp ??= GameEntry.DataTable.GetDataTable(); + if (_drBearingComp == null) + { + return false; + } + + DRBearingComp config = _drBearingComp.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = new BearingCompItemData + { + InstanceId = _nextDropItemInstanceId++, + ConfigId = config.Id, + Name = config.Name, + Rarity = row.Rarity, + Endurance = 100f, + Constraint = config.Constraint, + Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty(), + RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty(), + AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty() + }; + return true; + } + + private bool TryBuildBaseCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem) + { + droppedItem = null; + _drBaseComp ??= GameEntry.DataTable.GetDataTable(); + if (_drBaseComp == null) + { + return false; + } + + DRBaseComp config = _drBaseComp.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = new BaseCompItemData + { + InstanceId = _nextDropItemInstanceId++, + ConfigId = config.Id, + Name = config.Name, + Rarity = row.Rarity, + Endurance = 100f, + Constraint = config.Constraint, + Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty(), + AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty(), + AttackPropertyType = config.AttackPropertyType + }; + return true; + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs.meta new file mode 100644 index 0000000..ff52d33 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 650463d0d65211e4e969ea0d469516d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs similarity index 87% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index e4a258a..17d0d03 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -27,14 +27,13 @@ namespace GeometryTD.CustomComponent private readonly PhaseLoopRuntime _phaseLoopRuntime = new(); private readonly CombatLoadSession _loadSession = new(); private readonly CombatEventBridge _eventBridge = new(); + private readonly CombatResourceManager _combatResourceManager = new(); private EntityComponent _entity; private DRLevel _currentLevel; private CombatFinishFormUseCase _combatFinishFormUseCase; private SchedulerState _state = SchedulerState.Idle; private bool _initialized; - private int _gainedCoin; - private int _gainedGold; private bool _isFinishAsVictory = true; public bool IsRunning => _state == SchedulerState.WaitingForLoading || _state == SchedulerState.RunningPhase; @@ -43,10 +42,11 @@ namespace GeometryTD.CustomComponent public DRLevelPhase CurrentPhase => _phaseLoopRuntime.CurrentPhase; public MapEntity CurrentMap => _loadSession.CurrentMap; public int DisplayPhaseIndex => _phaseLoopRuntime.DisplayPhaseIndex; + public int PhaseCount => _phaseLoopRuntime.PhaseCount; public bool CanEndCombat => _phaseLoopRuntime.CanEndCombat; public int DefeatedEnemyCount => _enemyManager.DefeatedEnemyCount; - public int GainedCoin => _gainedCoin; - public int GainedGold => _gainedGold; + public int GainedCoin => _combatResourceManager.GainedCoin; + public int GainedGold => _combatResourceManager.GainedGold; public void OnInit() { @@ -89,8 +89,7 @@ namespace GeometryTD.CustomComponent _enemyManager.EndPhase(); _enemyManager.ResetCombatStats(); ResetRuntime(); - _gainedCoin = 0; - _gainedGold = 0; + _combatResourceManager.Reset(); _isFinishAsVictory = true; _currentLevel = level; @@ -229,6 +228,10 @@ namespace GeometryTD.CustomComponent GameEntry.Event.Fire( this, CombatProcessEventArgs.Create(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount)); + GameEntry.Event.Fire( + this, + CombatEnemyHpRateChangedEventArgs.Create( + ResolveEnemyHpRateMultiplier(_phaseLoopRuntime.DisplayPhaseIndex, _phaseLoopRuntime.PhaseCount))); Log.Info( "CombatScheduler phase started. Level={0}, Phase={1}, EndType={2}, Entries={3}.", @@ -241,6 +244,7 @@ namespace GeometryTD.CustomComponent private void EnterFinishFlow(string reason, bool isVictory) { int defeatedEnemyCount = _enemyManager.DefeatedEnemyCount; + BackpackInventoryData rewardInventory = _combatResourceManager.GetRewardInventorySnapshot(); // Step 1: stop runtime and clear enemy entities only. _enemyManager.EndPhase(); @@ -253,7 +257,7 @@ namespace GeometryTD.CustomComponent _currentLevel != null ? _currentLevel.Id : 0, reason); - OpenCombatFinishForm(defeatedEnemyCount, _gainedGold); + OpenCombatFinishForm(defeatedEnemyCount, _combatResourceManager.GainedGold, rewardInventory); } public void OnEnemyReachedBase(int baseDamage) @@ -290,6 +294,7 @@ namespace GeometryTD.CustomComponent _spawnEntriesByPhaseId.Clear(); _phaseLoopRuntime.Reset(); _loadSession.Reset(); + _combatResourceManager.Reset(); _currentLevel = null; _isFinishAsVictory = true; _state = SchedulerState.Idle; @@ -308,22 +313,43 @@ namespace GeometryTD.CustomComponent GameEntry.UIRouter.BindUIUseCase(UIFormType.CombatFinishForm, _combatFinishFormUseCase); } - private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold) + private void OpenCombatFinishForm(int defeatedEnemyCount, int gainedGold, BackpackInventoryData rewardInventory) { EnsureCombatFinishFormUseCaseBound(); _combatFinishFormUseCase.SetSummary( defeatedEnemyCount, - gainedGold); + gainedGold, + rewardInventory); GameEntry.UIRouter.OpenUI(UIFormType.CombatFinishForm); } public void OnEnemyDefeatedRewardResolved(int gainedCoin, int gainedGold) { - int coin = Mathf.Max(0, gainedCoin); - int gold = Mathf.Max(0, gainedGold); - _gainedCoin += coin; - _gainedGold += gold; - GameEntry.CombatNode?.ApplyEnemyDropReward(coin, gold); + _combatResourceManager.AddEnemyDefeatedReward(gainedCoin, gainedGold); + + if (_state != SchedulerState.RunningPhase) + { + return; + } + + _combatResourceManager.TryRollOutGameItemDrop( + _phaseLoopRuntime.DisplayPhaseIndex, + ResolveCurrentThemeType()); + } + + private LevelThemeType ResolveCurrentThemeType() + { + if (_currentLevel != null) + { + return _currentLevel.LevelThemeType; + } + + if (GameEntry.CombatNode != null) + { + return GameEntry.CombatNode.CurrentThemeType; + } + + return LevelThemeType.None; } private void CloseCombatFinishForm() @@ -373,6 +399,22 @@ namespace GeometryTD.CustomComponent GameEntry.CombatNode?.OnCombatEndedByScheduler(false); } + private static int ResolveEnemyHpRateMultiplier(int displayPhaseIndex, int phaseCount) + { + if (displayPhaseIndex <= 0 || phaseCount <= 0) + { + return 1; + } + + int completedLoopCount = Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount); + if (completedLoopCount >= 30) + { + return int.MaxValue; + } + + return 1 << completedLoopCount; + } + #region Event Handlers private void OnShowEntitySuccess(ShowEntitySuccessEventArgs args) diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs.meta rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs.meta diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/PhaseLoopRuntime.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/PhaseLoopRuntime.cs rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/PhaseLoopRuntime.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/PhaseLoopRuntime.cs.meta rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/PhaseLoopRuntime.cs.meta diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs deleted file mode 100644 index 4f47ea0..0000000 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs +++ /dev/null @@ -1,566 +0,0 @@ -using System.Collections.Generic; -using GameFramework.DataTable; -using GameFramework.Event; -using GeometryTD.DataTable; -using GeometryTD.Definition; -using GeometryTD.Entity; -using GeometryTD.Entity.EntityData; -using GeometryTD.Map; -using UnityEngine; -using UnityGameFramework.Runtime; - -namespace GeometryTD.CustomComponent -{ - public class EnemyManager - { - private sealed class SpawnEntryRuntime - { - public DRLevelSpawnEntry Entry; - public bool Completed; - public float NextTriggerTime; - public float EndTime; - public int RemainingCount; - } - - private const int DefaultEnemyConfigId = 1; - private const float MinStreamInterval = 0.05f; - private const float MinBurstGap = 0.01f; - - private readonly List _spawners = new List(); - private readonly Dictionary _spawnerByOrder = new Dictionary(); - private readonly List _pathBuffer = new List(); - private readonly List _spawnRuntimes = new List(); - private readonly HashSet _trackedEnemyEntityIds = new HashSet(); - private readonly List _trackedEnemyIdBuffer = new List(); - private readonly Dictionary _trackedEnemyConfigByEntityId = new Dictionary(); - - private CombatScheduler _combatScheduler; - private EntityComponent _entity; - private IDataTable _drEnemy; - - private int _spawnEnemyMaxCount = 5000; - private int _currentEnemyCount; - private int _defeatedEnemyCount; - private int _nextSpawnerIndex; - private int _currentMapEntityId; - private bool _initialized; - private bool _enemyConfigMissingLogged; - - private float _phaseElapsed; - private bool _isPhaseRunning; - - public int AliveEnemyCount => _currentEnemyCount; - public int DefeatedEnemyCount => _defeatedEnemyCount; - public bool IsPhaseSpawnCompleted { get; private set; } = true; - public bool IsPhaseRunning => _isPhaseRunning; - - public void OnInit(CombatScheduler combatScheduler) - { - _combatScheduler = combatScheduler; - if (_initialized) - { - return; - } - - _entity = GameEntry.Entity; - _drEnemy = GameEntry.DataTable.GetDataTable(); - _currentEnemyCount = 0; - _defeatedEnemyCount = 0; - _nextSpawnerIndex = 0; - _currentMapEntityId = 0; - _enemyConfigMissingLogged = false; - _spawners.Clear(); - _spawnerByOrder.Clear(); - _pathBuffer.Clear(); - _spawnRuntimes.Clear(); - _trackedEnemyEntityIds.Clear(); - _trackedEnemyIdBuffer.Clear(); - _trackedEnemyConfigByEntityId.Clear(); - _phaseElapsed = 0f; - _isPhaseRunning = false; - IsPhaseSpawnCompleted = true; - - GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); - GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); - GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); - _initialized = true; - } - - public void BeginPhase(DRLevelPhase phase, IReadOnlyList spawnEntries) - { - if (!_initialized || _combatScheduler == null) - { - return; - } - - _ = phase; - EndPhase(); - - _phaseElapsed = 0f; - _isPhaseRunning = true; - IsPhaseSpawnCompleted = false; - RefreshSpawnerCache(true); - - if (spawnEntries != null) - { - for (int i = 0; i < spawnEntries.Count; i++) - { - SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]); - if (runtime != null) - { - _spawnRuntimes.Add(runtime); - } - } - } - - IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0; - } - - public void OnUpdate(float elapseSeconds, float realElapseSeconds) - { - if (!_initialized || _combatScheduler == null || !_isPhaseRunning) - { - return; - } - - RefreshSpawnerCache(false); - _phaseElapsed += elapseSeconds; - UpdateSpawnRuntimes(); - } - - public void EndPhase() - { - _isPhaseRunning = false; - _phaseElapsed = 0f; - _spawnRuntimes.Clear(); - IsPhaseSpawnCompleted = true; - } - - public void OnDestroy() - { - if (!_initialized) - { - _combatScheduler = null; - return; - } - - CleanupTrackedEnemies(); - EndPhase(); - GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); - GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); - GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); - - _spawners.Clear(); - _spawnerByOrder.Clear(); - _pathBuffer.Clear(); - _trackedEnemyEntityIds.Clear(); - _trackedEnemyIdBuffer.Clear(); - _trackedEnemyConfigByEntityId.Clear(); - _currentEnemyCount = 0; - _defeatedEnemyCount = 0; - _currentMapEntityId = 0; - _nextSpawnerIndex = 0; - _combatScheduler = null; - _initialized = false; - } - - public void ResetCombatStats() - { - _defeatedEnemyCount = 0; - } - - public void CleanupTrackedEnemies() - { - if (_trackedEnemyEntityIds.Count <= 0) - { - _currentEnemyCount = 0; - return; - } - - _trackedEnemyIdBuffer.Clear(); - foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds) - { - _trackedEnemyIdBuffer.Add(trackedEnemyEntityId); - } - - _trackedEnemyEntityIds.Clear(); - _trackedEnemyConfigByEntityId.Clear(); - _currentEnemyCount = 0; - - if (_entity == null) - { - return; - } - - for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++) - { - int trackedEnemyEntityId = _trackedEnemyIdBuffer[i]; - if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId)) - { - _entity.HideEntity(trackedEnemyEntityId); - } - } - } - - private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry) - { - if (entry == null || entry.EntryType == EntryType.None) - { - return null; - } - - SpawnEntryRuntime runtime = new SpawnEntryRuntime - { - Entry = entry, - Completed = false, - NextTriggerTime = Mathf.Max(0f, entry.StartTime), - EndTime = Mathf.Max(0f, entry.StartTime), - RemainingCount = Mathf.Max(0, entry.Count) - }; - - switch (entry.EntryType) - { - case EntryType.Stream: - { - float duration = Mathf.Max(0f, entry.Duration); - runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime; - runtime.Completed = entry.Count <= 0; - return runtime; - } - case EntryType.Burst: - case EntryType.Boss: - runtime.Completed = runtime.RemainingCount <= 0; - return runtime; - default: - return null; - } - } - - private void UpdateSpawnRuntimes() - { - bool allCompleted = true; - for (int i = 0; i < _spawnRuntimes.Count; i++) - { - SpawnEntryRuntime runtime = _spawnRuntimes[i]; - if (runtime.Completed) - { - continue; - } - - switch (runtime.Entry.EntryType) - { - case EntryType.Stream: - ProcessStreamRuntime(runtime); - break; - case EntryType.Burst: - case EntryType.Boss: - ProcessBurstRuntime(runtime); - break; - default: - runtime.Completed = true; - break; - } - - if (!runtime.Completed) - { - allCompleted = false; - } - } - - IsPhaseSpawnCompleted = allCompleted; - } - - private void ProcessStreamRuntime(SpawnEntryRuntime runtime) - { - if (_phaseElapsed < runtime.NextTriggerTime) - { - return; - } - - int countPerWave = Mathf.Max(0, runtime.Entry.Count); - if (countPerWave <= 0) - { - runtime.Completed = true; - return; - } - - float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval; - while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime) - { - SpawnEnemies(runtime.Entry, countPerWave); - runtime.NextTriggerTime += interval; - } - - if (runtime.NextTriggerTime > runtime.EndTime) - { - runtime.Completed = true; - } - } - - private void ProcessBurstRuntime(SpawnEntryRuntime runtime) - { - if (_phaseElapsed < runtime.NextTriggerTime) - { - return; - } - - if (runtime.RemainingCount <= 0) - { - runtime.Completed = true; - return; - } - - float gap = runtime.Entry.Gap; - if (gap <= 0f) - { - SpawnEnemies(runtime.Entry, runtime.RemainingCount); - runtime.RemainingCount = 0; - runtime.Completed = true; - return; - } - - gap = Mathf.Max(gap, MinBurstGap); - while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0) - { - SpawnEnemies(runtime.Entry, 1); - runtime.RemainingCount--; - runtime.NextTriggerTime += gap; - } - - runtime.Completed = runtime.RemainingCount <= 0; - } - - private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount) - { - if (spawnCount <= 0) - { - return; - } - - Spawner spawner = ResolveSpawner(entry.SpawnPointId); - if (spawner == null) - { - return; - } - - MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null; - if (currentMap == null || - !currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || - _pathBuffer.Count <= 0) - { - return; - } - - DREnemy enemyConfig = GetEnemyConfig(entry.EnemyId); - if (enemyConfig == null) - { - return; - } - - for (int i = 0; i < spawnCount; i++) - { - if (_currentEnemyCount >= _spawnEnemyMaxCount) - { - break; - } - - int enemyEntityId = _entity.GenerateSerialId(); - _trackedEnemyEntityIds.Add(enemyEntityId); - _trackedEnemyConfigByEntityId[enemyEntityId] = enemyConfig; - EnemyData enemyData = new EnemyData( - enemyEntityId, - enemyConfig.EntityId, - _pathBuffer[0], - enemyConfig.BaseHp, - enemyConfig.Speed, - _pathBuffer); - _entity.ShowEnemy(enemyData); - } - } - - private DREnemy GetEnemyConfig(int enemyId) - { - if (_drEnemy == null) - { - _drEnemy = GameEntry.DataTable.GetDataTable(); - if (_drEnemy == null) - { - if (!_enemyConfigMissingLogged) - { - Log.Warning("EnemyManagerComponent can not find DREnemy data table."); - _enemyConfigMissingLogged = true; - } - - return null; - } - } - - if (enemyId > 0) - { - DREnemy targetConfig = _drEnemy.GetDataRow(enemyId); - if (targetConfig != null) - { - return targetConfig; - } - } - - DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId); - if (defaultConfig != null) - { - return defaultConfig; - } - - DREnemy[] allConfigs = _drEnemy.GetAllDataRows(); - if (allConfigs.Length > 0) - { - return allConfigs[0]; - } - - if (!_enemyConfigMissingLogged) - { - Log.Warning("EnemyManagerComponent found no enemy configs."); - _enemyConfigMissingLogged = true; - } - - return null; - } - - private Spawner ResolveSpawner(int spawnPointId) - { - if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner)) - { - return mappedSpawner; - } - - if (_spawners.Count <= 0) - { - return null; - } - - Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count]; - _nextSpawnerIndex++; - return fallbackSpawner; - } - - private void RefreshSpawnerCache(bool force) - { - MapEntity currentMap = _combatScheduler != null ? _combatScheduler.CurrentMap : null; - if (currentMap == null) - { - _spawners.Clear(); - _spawnerByOrder.Clear(); - _currentMapEntityId = 0; - _nextSpawnerIndex = 0; - return; - } - - if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0) - { - return; - } - - _spawners.Clear(); - _spawnerByOrder.Clear(); - _nextSpawnerIndex = 0; - _currentMapEntityId = currentMap.Id; - - Spawner[] mapSpawners = currentMap.Spawners; - for (int i = 0; i < mapSpawners.Length; i++) - { - Spawner spawner = mapSpawners[i]; - if (spawner == null) - { - continue; - } - - if (!currentMap.TryGetDefaultPathCells(spawner, out _)) - { - continue; - } - - _spawners.Add(spawner); - if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder)) - { - _spawnerByOrder[spawner.SpawnOrder] = spawner; - } - } - - _spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder)); - } - - private void OnShowEntitySuccess(object sender, GameEventArgs e) - { - if (!(e is ShowEntitySuccessEventArgs ne)) return; - - if (ne.EntityLogicType == typeof(EnemyEntity) && - _trackedEnemyEntityIds.Contains(ne.Entity.Id)) - { - _currentEnemyCount++; - } - } - - private void OnShowEntityFailure(object sender, GameEventArgs e) - { - if (!(e is ShowEntityFailureEventArgs ne)) - { - return; - } - - if (ne.EntityLogicType != typeof(EnemyEntity)) - { - return; - } - - _trackedEnemyEntityIds.Remove(ne.EntityId); - _trackedEnemyConfigByEntityId.Remove(ne.EntityId); - } - - private void OnHideEntityComplete(object sender, GameEventArgs e) - { - if (!(e is HideEntityCompleteEventArgs ne)) - { - return; - } - - if (!_trackedEnemyEntityIds.Remove(ne.EntityId)) - { - _trackedEnemyConfigByEntityId.Remove(ne.EntityId); - return; - } - - _currentEnemyCount = Mathf.Max(0, _currentEnemyCount - 1); - bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId); - bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning; - int baseDamage = 0; - int droppedCoin = 0; - int droppedGold = 0; - if (_trackedEnemyConfigByEntityId.TryGetValue(ne.EntityId, out DREnemy enemyConfig) && enemyConfig != null) - { - baseDamage = Mathf.Max(0, enemyConfig.BaseDamage); - if (wasKilled) - { - droppedCoin = Mathf.Max(0, enemyConfig.DropCoin); - float dropRate = enemyConfig.DropPercent > 1f - ? Mathf.Clamp01(enemyConfig.DropPercent * 0.01f) - : Mathf.Clamp01(enemyConfig.DropPercent); - if (enemyConfig.DropGold > 0 && dropRate > 0f && Random.value <= dropRate) - { - droppedGold = Mathf.Max(0, enemyConfig.DropGold); - } - } - } - - if (isCombatRunning && wasKilled) - { - _defeatedEnemyCount++; - _combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold); - } - else if (isCombatRunning && baseDamage > 0) - { - _combatScheduler.OnEnemyReachedBase(baseDamage); - } - - _trackedEnemyConfigByEntityId.Remove(ne.EntityId); - } - } -} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.meta new file mode 100644 index 0000000..e19e047 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d8e23ddc10c742359bfeb8c6bd765ee2 +timeCreated: 1772440818 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs new file mode 100644 index 0000000..8fdd461 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs @@ -0,0 +1,104 @@ +using System; +using GameFramework.DataTable; +using GeometryTD.DataTable; +using UnityEngine; +using UnityGameFramework.Runtime; + +namespace GeometryTD.CustomComponent +{ + public sealed class EnemyConfigService + { + private const int DefaultEnemyConfigId = 1; + + private IDataTable _drEnemy; + private bool _enemyConfigMissingLogged; + + public void Reset() + { + _drEnemy = null; + _enemyConfigMissingLogged = false; + } + + public DREnemy GetEnemyConfig(int enemyId) + { + if (_drEnemy == null) + { + _drEnemy = GameEntry.DataTable.GetDataTable(); + if (_drEnemy == null) + { + if (!_enemyConfigMissingLogged) + { + Log.Warning("EnemyManagerComponent can not find DREnemy data table."); + _enemyConfigMissingLogged = true; + } + + return null; + } + } + + if (enemyId > 0) + { + DREnemy targetConfig = _drEnemy.GetDataRow(enemyId); + if (targetConfig != null) + { + return targetConfig; + } + } + + DREnemy defaultConfig = _drEnemy.GetDataRow(DefaultEnemyConfigId); + if (defaultConfig != null) + { + return defaultConfig; + } + + DREnemy[] allConfigs = _drEnemy.GetAllDataRows(); + if (allConfigs.Length > 0) + { + return allConfigs[0]; + } + + if (!_enemyConfigMissingLogged) + { + Log.Warning("EnemyManagerComponent found no enemy configs."); + _enemyConfigMissingLogged = true; + } + + return null; + } + + public int ResolveScaledEnemyBaseHp(int baseHp, CombatScheduler combatScheduler) + { + int resolvedBaseHp = Mathf.Max(1, baseHp); + int completedLoopCount = ResolveCompletedLoopCount(combatScheduler); + if (completedLoopCount <= 0) + { + return resolvedBaseHp; + } + + double scaled = resolvedBaseHp * Math.Pow(2d, completedLoopCount); + if (scaled >= int.MaxValue) + { + return int.MaxValue; + } + + return Math.Max(1, (int)Math.Round(scaled)); + } + + private static int ResolveCompletedLoopCount(CombatScheduler combatScheduler) + { + if (combatScheduler == null) + { + return 0; + } + + int phaseCount = combatScheduler.PhaseCount; + int displayPhaseIndex = combatScheduler.DisplayPhaseIndex; + if (phaseCount <= 0 || displayPhaseIndex <= 0) + { + return 0; + } + + return Mathf.Max(0, (displayPhaseIndex - 1) / phaseCount); + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs.meta new file mode 100644 index 0000000..1cd465a --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: be15abd89ef8487eabfe09816f202a98 +timeCreated: 1772441787 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs new file mode 100644 index 0000000..5546d32 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using UnityEngine; + +namespace GeometryTD.CustomComponent +{ + internal sealed class EnemyLifecycleTracker + { + private readonly HashSet _trackedEnemyEntityIds = new(); + private readonly Dictionary _trackedEnemyConfigByEntityId = new(); + + public int AliveEnemyCount { get; private set; } + + public void Reset() + { + _trackedEnemyEntityIds.Clear(); + _trackedEnemyConfigByEntityId.Clear(); + AliveEnemyCount = 0; + } + + public void TrackEnemy(int entityId, DREnemy enemyConfig) + { + _trackedEnemyEntityIds.Add(entityId); + _trackedEnemyConfigByEntityId[entityId] = enemyConfig; + } + + public bool Contains(int entityId) + { + return _trackedEnemyEntityIds.Contains(entityId); + } + + public void HandleShowSuccess(int entityId) + { + if (_trackedEnemyEntityIds.Contains(entityId)) + { + AliveEnemyCount++; + } + } + + public void HandleShowFailure(int entityId) + { + _trackedEnemyEntityIds.Remove(entityId); + _trackedEnemyConfigByEntityId.Remove(entityId); + } + + public bool TryHandleHideComplete(int entityId, out DREnemy enemyConfig) + { + enemyConfig = null; + if (!_trackedEnemyEntityIds.Remove(entityId)) + { + _trackedEnemyConfigByEntityId.Remove(entityId); + return false; + } + + _trackedEnemyConfigByEntityId.TryGetValue(entityId, out enemyConfig); + _trackedEnemyConfigByEntityId.Remove(entityId); + AliveEnemyCount = Mathf.Max(0, AliveEnemyCount - 1); + return true; + } + + public void CopyTrackedEntityIdsTo(List buffer) + { + if (buffer == null) + { + return; + } + + buffer.Clear(); + foreach (int trackedEnemyEntityId in _trackedEnemyEntityIds) + { + buffer.Add(trackedEnemyEntityId); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs.meta new file mode 100644 index 0000000..5c5817f --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0437a4bd33e6a83429ef65cb81a2da0b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs new file mode 100644 index 0000000..e5efc83 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs @@ -0,0 +1,241 @@ +using System.Collections.Generic; +using GameFramework.Event; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using GeometryTD.Entity; +using GeometryTD.Entity.EntityData; +using UnityEngine; +using UnityGameFramework.Runtime; +using Random = UnityEngine.Random; + +namespace GeometryTD.CustomComponent +{ + public class EnemyManager + { + private readonly List _trackedEnemyIdBuffer = new(); + private readonly EnemySpawnDirector _enemySpawnDirector = new(); + private readonly EnemyConfigService _enemyConfigService = new(); + private readonly SpawnerResolver _spawnerResolver = new(); + private readonly EnemyLifecycleTracker _enemyLifecycleTracker = new(); + + private CombatScheduler _combatScheduler; + private EntityComponent _entity; + + private int _defeatedEnemyCount; + private bool _initialized; + + public int AliveEnemyCount => _enemyLifecycleTracker.AliveEnemyCount; + public int DefeatedEnemyCount => _defeatedEnemyCount; + public bool IsPhaseSpawnCompleted => _enemySpawnDirector.IsPhaseSpawnCompleted; + public bool IsPhaseRunning => _enemySpawnDirector.IsPhaseRunning; + + public void OnInit(CombatScheduler combatScheduler) + { + _combatScheduler = combatScheduler; + if (_initialized) + { + return; + } + + _entity = GameEntry.Entity; + _defeatedEnemyCount = 0; + _enemySpawnDirector.Reset(); + _enemyConfigService.Reset(); + _spawnerResolver.Reset(); + _trackedEnemyIdBuffer.Clear(); + _enemyLifecycleTracker.Reset(); + + GameEntry.Event.Subscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); + GameEntry.Event.Subscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); + GameEntry.Event.Subscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + _initialized = true; + } + + public void BeginPhase(DRLevelPhase phase, IReadOnlyList spawnEntries) + { + if (!_initialized || _combatScheduler == null) + { + return; + } + + _ = phase; + EndPhase(); + _spawnerResolver.RefreshCache(_combatScheduler, true); + _enemySpawnDirector.BeginPhase(spawnEntries); + } + + public void OnUpdate(float elapseSeconds, float realElapseSeconds) + { + if (!_initialized || _combatScheduler == null || !_enemySpawnDirector.IsPhaseRunning) + { + return; + } + + _spawnerResolver.RefreshCache(_combatScheduler, false); + _enemySpawnDirector.OnUpdate(elapseSeconds, SpawnEnemies); + } + + public void EndPhase() + { + _enemySpawnDirector.EndPhase(); + } + + public void OnDestroy() + { + if (!_initialized) + { + _combatScheduler = null; + return; + } + + CleanupTrackedEnemies(); + EndPhase(); + GameEntry.Event.Unsubscribe(ShowEntitySuccessEventArgs.EventId, OnShowEntitySuccess); + GameEntry.Event.Unsubscribe(ShowEntityFailureEventArgs.EventId, OnShowEntityFailure); + GameEntry.Event.Unsubscribe(HideEntityCompleteEventArgs.EventId, OnHideEntityComplete); + + _spawnerResolver.Reset(); + _trackedEnemyIdBuffer.Clear(); + _enemyLifecycleTracker.Reset(); + _defeatedEnemyCount = 0; + _enemyConfigService.Reset(); + _combatScheduler = null; + _initialized = false; + } + + public void ResetCombatStats() + { + _defeatedEnemyCount = 0; + } + + public void CleanupTrackedEnemies() + { + _enemyLifecycleTracker.CopyTrackedEntityIdsTo(_trackedEnemyIdBuffer); + if (_trackedEnemyIdBuffer.Count <= 0) + { + return; + } + + _enemyLifecycleTracker.Reset(); + + if (_entity == null) + { + return; + } + + for (int i = 0; i < _trackedEnemyIdBuffer.Count; i++) + { + int trackedEnemyEntityId = _trackedEnemyIdBuffer[i]; + if (_entity.HasEntity(trackedEnemyEntityId) || _entity.IsLoadingEntity(trackedEnemyEntityId)) + { + _entity.HideEntity(trackedEnemyEntityId); + } + } + } + + private void SpawnEnemies(DRLevelSpawnEntry entry, int spawnCount) + { + if (spawnCount <= 0) + { + return; + } + + if (!_spawnerResolver.TryResolveSpawnPath(_combatScheduler, entry.SpawnPointId, out IReadOnlyList pathPoints)) + { + return; + } + + DREnemy enemyConfig = _enemyConfigService.GetEnemyConfig(entry.EnemyId); + if (enemyConfig == null) + { + return; + } + + int scaledBaseHp = _enemyConfigService.ResolveScaledEnemyBaseHp(enemyConfig.BaseHp, _combatScheduler); + + for (int i = 0; i < spawnCount; i++) + { + int enemyEntityId = _entity.GenerateSerialId(); + _enemyLifecycleTracker.TrackEnemy(enemyEntityId, enemyConfig); + EnemyData enemyData = new EnemyData( + enemyEntityId, + enemyConfig.EntityId, + pathPoints[0], + scaledBaseHp, + enemyConfig.Speed, + pathPoints); + _entity.ShowEnemy(enemyData); + } + } + + private void OnShowEntitySuccess(object sender, GameEventArgs e) + { + if (!(e is ShowEntitySuccessEventArgs ne)) return; + + if (ne.EntityLogicType == typeof(EnemyEntity) && + _enemyLifecycleTracker.Contains(ne.Entity.Id)) + { + _enemyLifecycleTracker.HandleShowSuccess(ne.Entity.Id); + } + } + + private void OnShowEntityFailure(object sender, GameEventArgs e) + { + if (!(e is ShowEntityFailureEventArgs ne)) + { + return; + } + + if (ne.EntityLogicType != typeof(EnemyEntity)) + { + return; + } + + _enemyLifecycleTracker.HandleShowFailure(ne.EntityId); + } + + private void OnHideEntityComplete(object sender, GameEventArgs e) + { + if (!(e is HideEntityCompleteEventArgs ne)) + { + return; + } + + if (!_enemyLifecycleTracker.TryHandleHideComplete(ne.EntityId, out DREnemy enemyConfig)) + { + return; + } + + bool wasKilled = EnemyEntity.TryConsumeKilledFlag(ne.EntityId); + bool isCombatRunning = _combatScheduler != null && _combatScheduler.IsRunning; + int baseDamage = 0; + int droppedCoin = 0; + int droppedGold = 0; + if (enemyConfig != null) + { + baseDamage = Mathf.Max(0, enemyConfig.BaseDamage); + if (wasKilled) + { + droppedCoin = Mathf.Max(0, enemyConfig.DropCoin); + float dropRate = enemyConfig.DropPercent > 1f + ? Mathf.Clamp01(enemyConfig.DropPercent * 0.01f) + : Mathf.Clamp01(enemyConfig.DropPercent); + if (enemyConfig.DropGold > 0 && dropRate > 0f && Random.value <= dropRate) + { + droppedGold = Mathf.Max(0, enemyConfig.DropGold); + } + } + } + + if (isCombatRunning && wasKilled) + { + _defeatedEnemyCount++; + _combatScheduler.OnEnemyDefeatedRewardResolved(droppedCoin, droppedGold); + } + else if (isCombatRunning && baseDamage > 0) + { + _combatScheduler.OnEnemyReachedBase(baseDamage); + } + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs.meta rename to Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs.meta diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs new file mode 100644 index 0000000..6df3c02 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomComponent +{ + internal sealed class EnemySpawnDirector + { + private sealed class SpawnEntryRuntime + { + public DRLevelSpawnEntry Entry; + public bool Completed; + public float NextTriggerTime; + public float EndTime; + public int RemainingCount; + } + + private const float MinStreamInterval = 0.05f; + private const float MinBurstGap = 0.01f; + + private readonly List _spawnRuntimes = new(); + private float _phaseElapsed; + + public bool IsPhaseSpawnCompleted { get; private set; } = true; + public bool IsPhaseRunning { get; private set; } + + public void Reset() + { + EndPhase(); + } + + public void BeginPhase(IReadOnlyList spawnEntries) + { + EndPhase(); + + _phaseElapsed = 0f; + IsPhaseRunning = true; + IsPhaseSpawnCompleted = false; + if (spawnEntries != null) + { + for (int i = 0; i < spawnEntries.Count; i++) + { + SpawnEntryRuntime runtime = BuildSpawnRuntime(spawnEntries[i]); + if (runtime != null) + { + _spawnRuntimes.Add(runtime); + } + } + } + + IsPhaseSpawnCompleted = _spawnRuntimes.Count <= 0; + } + + public void OnUpdate(float elapseSeconds, Action spawnAction) + { + if (!IsPhaseRunning || spawnAction == null) + { + return; + } + + _phaseElapsed += elapseSeconds; + UpdateSpawnRuntimes(spawnAction); + } + + public void EndPhase() + { + IsPhaseRunning = false; + _phaseElapsed = 0f; + _spawnRuntimes.Clear(); + IsPhaseSpawnCompleted = true; + } + + private SpawnEntryRuntime BuildSpawnRuntime(DRLevelSpawnEntry entry) + { + if (entry == null || entry.EntryType == EntryType.None) + { + return null; + } + + SpawnEntryRuntime runtime = new SpawnEntryRuntime + { + Entry = entry, + Completed = false, + NextTriggerTime = Mathf.Max(0f, entry.StartTime), + EndTime = Mathf.Max(0f, entry.StartTime), + RemainingCount = Mathf.Max(0, entry.Count) + }; + + switch (entry.EntryType) + { + case EntryType.Stream: + { + float duration = Mathf.Max(0f, entry.Duration); + runtime.EndTime = duration > 0f ? runtime.NextTriggerTime + duration : runtime.NextTriggerTime; + runtime.Completed = entry.Count <= 0; + return runtime; + } + case EntryType.Burst: + case EntryType.Boss: + runtime.Completed = runtime.RemainingCount <= 0; + return runtime; + default: + return null; + } + } + + private void UpdateSpawnRuntimes(Action spawnAction) + { + bool allCompleted = true; + for (int i = 0; i < _spawnRuntimes.Count; i++) + { + SpawnEntryRuntime runtime = _spawnRuntimes[i]; + if (runtime.Completed) + { + continue; + } + + switch (runtime.Entry.EntryType) + { + case EntryType.Stream: + ProcessStreamRuntime(runtime, spawnAction); + break; + case EntryType.Burst: + case EntryType.Boss: + ProcessBurstRuntime(runtime, spawnAction); + break; + default: + runtime.Completed = true; + break; + } + + if (!runtime.Completed) + { + allCompleted = false; + } + } + + IsPhaseSpawnCompleted = allCompleted; + } + + private void ProcessStreamRuntime(SpawnEntryRuntime runtime, Action spawnAction) + { + if (_phaseElapsed < runtime.NextTriggerTime) + { + return; + } + + int countPerWave = Mathf.Max(0, runtime.Entry.Count); + if (countPerWave <= 0) + { + runtime.Completed = true; + return; + } + + float interval = runtime.Entry.Interval > 0f ? runtime.Entry.Interval : MinStreamInterval; + while (_phaseElapsed >= runtime.NextTriggerTime && runtime.NextTriggerTime <= runtime.EndTime) + { + spawnAction(runtime.Entry, countPerWave); + runtime.NextTriggerTime += interval; + } + + if (runtime.NextTriggerTime > runtime.EndTime) + { + runtime.Completed = true; + } + } + + private void ProcessBurstRuntime(SpawnEntryRuntime runtime, Action spawnAction) + { + if (_phaseElapsed < runtime.NextTriggerTime) + { + return; + } + + if (runtime.RemainingCount <= 0) + { + runtime.Completed = true; + return; + } + + float gap = runtime.Entry.Gap; + if (gap <= 0f) + { + spawnAction(runtime.Entry, runtime.RemainingCount); + runtime.RemainingCount = 0; + runtime.Completed = true; + return; + } + + gap = Mathf.Max(gap, MinBurstGap); + while (_phaseElapsed >= runtime.NextTriggerTime && runtime.RemainingCount > 0) + { + spawnAction(runtime.Entry, 1); + runtime.RemainingCount--; + runtime.NextTriggerTime += gap; + } + + runtime.Completed = runtime.RemainingCount <= 0; + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs.meta new file mode 100644 index 0000000..8e29fdf --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2f7a94ed9eec61c4ea165b6904613ab6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs new file mode 100644 index 0000000..c3c3c57 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using GeometryTD.Entity; +using GeometryTD.Map; +using UnityEngine; + +namespace GeometryTD.CustomComponent +{ + internal sealed class SpawnerResolver + { + private readonly List _spawners = new(); + private readonly Dictionary _spawnerByOrder = new(); + private readonly List _pathBuffer = new(); + + private int _nextSpawnerIndex; + private int _currentMapEntityId; + + public void Reset() + { + _spawners.Clear(); + _spawnerByOrder.Clear(); + _pathBuffer.Clear(); + _nextSpawnerIndex = 0; + _currentMapEntityId = 0; + } + + public void RefreshCache(CombatScheduler combatScheduler, bool force) + { + MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null; + if (currentMap == null) + { + Reset(); + return; + } + + if (!force && _currentMapEntityId == currentMap.Id && _spawners.Count > 0) + { + return; + } + + _spawners.Clear(); + _spawnerByOrder.Clear(); + _nextSpawnerIndex = 0; + _currentMapEntityId = currentMap.Id; + + Spawner[] mapSpawners = currentMap.Spawners; + for (int i = 0; i < mapSpawners.Length; i++) + { + Spawner spawner = mapSpawners[i]; + if (spawner == null) + { + continue; + } + + if (!currentMap.TryGetDefaultPathCells(spawner, out _)) + { + continue; + } + + _spawners.Add(spawner); + if (spawner.SpawnOrder > 0 && !_spawnerByOrder.ContainsKey(spawner.SpawnOrder)) + { + _spawnerByOrder[spawner.SpawnOrder] = spawner; + } + } + + _spawners.Sort((left, right) => left.SpawnOrder.CompareTo(right.SpawnOrder)); + } + + public bool TryResolveSpawnPath(CombatScheduler combatScheduler, int spawnPointId, out IReadOnlyList pathPoints) + { + pathPoints = null; + MapEntity currentMap = combatScheduler != null ? combatScheduler.CurrentMap : null; + if (currentMap == null) + { + return false; + } + + Spawner spawner = ResolveSpawner(spawnPointId); + if (spawner == null) + { + return false; + } + + if (!currentMap.TryFindPathWorldPoints(spawner, null, _pathBuffer) || _pathBuffer.Count <= 0) + { + return false; + } + + pathPoints = _pathBuffer; + return true; + } + + private Spawner ResolveSpawner(int spawnPointId) + { + if (spawnPointId > 0 && _spawnerByOrder.TryGetValue(spawnPointId, out Spawner mappedSpawner)) + { + return mappedSpawner; + } + + if (_spawners.Count <= 0) + { + return null; + } + + Spawner fallbackSpawner = _spawners[_nextSpawnerIndex % _spawners.Count]; + _nextSpawnerIndex++; + return fallbackSpawner; + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs.meta b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs.meta new file mode 100644 index 0000000..2fbdda0 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e3978ec852f64184e8393143a1c5484d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/DataTable/DRBaseComp.cs b/Assets/GameMain/Scripts/DataTable/DRBaseComp.cs index 93b78b3..f9fe3cd 100644 --- a/Assets/GameMain/Scripts/DataTable/DRBaseComp.cs +++ b/Assets/GameMain/Scripts/DataTable/DRBaseComp.cs @@ -28,10 +28,15 @@ namespace GeometryTD.DataTable public string Name { get; private set; } /// - /// 获取底座组件攻击速度数组 + /// 获取底座组件攻击速度数组(秒/次) /// public float[] AttackSpeed { get; private set; } + /// + /// 获取底座组件每级提升攻击速度(值为负数) + /// + public float AttackSpeedPerLevel { get; private set; } + /// /// 获取攻击属性 /// @@ -62,6 +67,7 @@ namespace GeometryTD.DataTable EntityId = int.Parse(columnStrings[index++]); Name = columnStrings[index++]; AttackSpeed = GenerateAttackSpeed(columnStrings[index++]); + AttackSpeedPerLevel = float.Parse(columnStrings[index++]); AttackPropertyType = EnumUtility.Get(columnStrings[index++]); Constraint = columnStrings[index++]; PossibleTag = GeneratePossibleTag(columnStrings[index++]); diff --git a/Assets/GameMain/Scripts/DataTable/DRBearingComp.cs b/Assets/GameMain/Scripts/DataTable/DRBearingComp.cs index 8e03496..072254e 100644 --- a/Assets/GameMain/Scripts/DataTable/DRBearingComp.cs +++ b/Assets/GameMain/Scripts/DataTable/DRBearingComp.cs @@ -32,11 +32,21 @@ namespace GeometryTD.DataTable /// public float[] RotateSpeed { get; private set; } + /// + /// 获取轴承组件每级提升旋转速度 + /// + public float RotateSpeedPerLevel { get; private set; } + /// /// 获取攻击范围 /// public float[] AttackRange { get; private set; } + /// + /// 获取每级提升攻击范围 + /// + public float AttackRangePerLevel { get; private set; } + /// /// 获取属性约束 /// @@ -62,7 +72,9 @@ namespace GeometryTD.DataTable EntityId = int.Parse(columnStrings[index++]); Name = columnStrings[index++]; RotateSpeed = GenerateFloatArray(columnStrings[index++]); + RotateSpeedPerLevel = float.Parse(columnStrings[index++]); AttackRange = GenerateFloatArray(columnStrings[index++]); + AttackRangePerLevel = float.Parse(columnStrings[index++]); Constraint = columnStrings[index++]; PossibleTag = GeneratePossibleTag(columnStrings[index++]); diff --git a/Assets/GameMain/Scripts/DataTable/DRMuzzleComp.cs b/Assets/GameMain/Scripts/DataTable/DRMuzzleComp.cs index f1d5efb..c36b938 100644 --- a/Assets/GameMain/Scripts/DataTable/DRMuzzleComp.cs +++ b/Assets/GameMain/Scripts/DataTable/DRMuzzleComp.cs @@ -32,6 +32,11 @@ namespace GeometryTD.DataTable /// public int[] AttackDamage { get; private set; } + /// + /// 获取枪口组件每级提升攻击伤害值 + /// + public int AttackDamagePerLevel { get; private set; } + /// /// 获取攻击伤害浮动 /// @@ -67,6 +72,7 @@ namespace GeometryTD.DataTable EntityId = int.Parse(columnStrings[index++]); Name = columnStrings[index++]; AttackDamage = GenerateAttackDamage(columnStrings[index++]); + AttackDamagePerLevel = int.Parse(columnStrings[index++]); DamageRandomRate = float.Parse(columnStrings[index++]); AttackMethodType = EnumUtility.Get(columnStrings[index++]); Constraint = columnStrings[index++]; diff --git a/Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs b/Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs new file mode 100644 index 0000000..c4a0e6a --- /dev/null +++ b/Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs @@ -0,0 +1,72 @@ +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + /// + /// 局外道具掉落池配置表。 + /// + public class DROutGameDropPool : DataRowBase + { + private int m_Id = 0; + + public override int Id => m_Id; + + public LevelThemeType LevelThemeType { get; private set; } + public RarityType Rarity { get; private set; } + public string ItemType { get; private set; } + public int ItemId { get; private set; } + public int Weight { get; private set; } + public int MinPhase { get; private set; } + public int MaxPhase { get; private set; } + + public override bool ParseDataRow(string dataRowString, object userData) + { + string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators); + for (int i = 0; i < columnStrings.Length; i++) + { + columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators); + } + + int index = 0; + index++; + m_Id = int.Parse(columnStrings[index++]); + index++; + LevelThemeType = EnumUtility.Get(columnStrings[index++]); + Rarity = EnumUtility.Get(columnStrings[index++]); + ItemType = columnStrings[index++]; + ItemId = ParseIntOrDefault(columnStrings[index++], 0); + Weight = ParseIntOrDefault(columnStrings[index++], 1); + MinPhase = ParseIntOrDefault(columnStrings[index++], 1); + MaxPhase = ParseIntOrDefault(columnStrings[index++], int.MaxValue); + + if (Weight <= 0) + { + Weight = 1; + } + + if (MinPhase <= 0) + { + MinPhase = 1; + } + + if (MaxPhase < MinPhase) + { + MaxPhase = MinPhase; + } + + return true; + } + + private static int ParseIntOrDefault(string raw, int fallbackValue) + { + if (int.TryParse(raw, out int parsed)) + { + return parsed; + } + + return fallbackValue; + } + } +} diff --git a/Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs.meta b/Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs.meta new file mode 100644 index 0000000..3601504 --- /dev/null +++ b/Assets/GameMain/Scripts/DataTable/DROutGameDropPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4a06af49028f1aa47acede2e0651f2c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs b/Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs new file mode 100644 index 0000000..b21d2fc --- /dev/null +++ b/Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs @@ -0,0 +1,31 @@ +using GameFramework; +using GameFramework.Event; + +namespace GeometryTD.CustomEvent +{ + public class CombatEnemyHpRateChangedEventArgs : GameEventArgs + { + public static int EventId => typeof(CombatEnemyHpRateChangedEventArgs).GetHashCode(); + + public override int Id => EventId; + + public int EnemyHpRateMultiplier { get; private set; } + + public CombatEnemyHpRateChangedEventArgs() + { + EnemyHpRateMultiplier = 1; + } + + public static CombatEnemyHpRateChangedEventArgs Create(int enemyHpRateMultiplier) + { + var args = ReferencePool.Acquire(); + args.EnemyHpRateMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1; + return args; + } + + public override void Clear() + { + EnemyHpRateMultiplier = 1; + } + } +} diff --git a/Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs.meta b/Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs.meta new file mode 100644 index 0000000..f0fb71f --- /dev/null +++ b/Assets/GameMain/Scripts/Event/Combat/CombatEnemyHpRateChangedEventArgs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c8ba0b112fc25848bf0fbc4b5919a67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Procedure/ProcedurePreload.cs b/Assets/GameMain/Scripts/Procedure/ProcedurePreload.cs index e253304..c0ea2c4 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedurePreload.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedurePreload.cs @@ -31,6 +31,7 @@ namespace GeometryTD.Procedure "ShopPrice", "Sound", "Tag", + "OutGameDropPool", "UIForm", "UISound", }; @@ -231,4 +232,4 @@ namespace GeometryTD.Procedure ne.DictionaryAssetName, ne.ErrorMessage); } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/UI/Combat/Context/CombatInfoFormContext.cs b/Assets/GameMain/Scripts/UI/Combat/Context/CombatInfoFormContext.cs index 0f4022d..7d071a4 100644 --- a/Assets/GameMain/Scripts/UI/Combat/Context/CombatInfoFormContext.cs +++ b/Assets/GameMain/Scripts/UI/Combat/Context/CombatInfoFormContext.cs @@ -6,6 +6,7 @@ namespace GeometryTD.UI public string LevelPhaseText; public string CoinText; public string BaseHpText; + public string EnemyHpRateText; public bool CanPause; public bool CanEnd; } diff --git a/Assets/GameMain/Scripts/UI/Combat/Controller/CombatInfoFormController.cs b/Assets/GameMain/Scripts/UI/Combat/Controller/CombatInfoFormController.cs index 00eb8a2..e9bb284 100644 --- a/Assets/GameMain/Scripts/UI/Combat/Controller/CombatInfoFormController.cs +++ b/Assets/GameMain/Scripts/UI/Combat/Controller/CombatInfoFormController.cs @@ -23,6 +23,7 @@ namespace GeometryTD.UI GameEntry.Event.Subscribe(CombatProcessEventArgs.EventId, OnCombatProcessChanged); GameEntry.Event.Subscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); GameEntry.Event.Subscribe(CombatBaseHpChangedEventArgs.EventId, OnCombatBaseHpChanged); + GameEntry.Event.Subscribe(CombatEnemyHpRateChangedEventArgs.EventId, OnCombatEnemyHpRateChanged); } protected override void UnsubscribeCustomEvents() @@ -32,6 +33,7 @@ namespace GeometryTD.UI GameEntry.Event.Unsubscribe(CombatProcessEventArgs.EventId, OnCombatProcessChanged); GameEntry.Event.Unsubscribe(CombatCoinChangedEventArgs.EventId, OnCombatCoinChanged); GameEntry.Event.Unsubscribe(CombatBaseHpChangedEventArgs.EventId, OnCombatBaseHpChanged); + GameEntry.Event.Unsubscribe(CombatEnemyHpRateChangedEventArgs.EventId, OnCombatEnemyHpRateChanged); } public int? OpenUI(CombatInfoFormRawData rawData) @@ -92,6 +94,7 @@ namespace GeometryTD.UI LevelPhaseText = BuildPhaseText(rawData.CurrentPhaseIndex, rawData.TotalPhaseCount), CoinText = BuildCoinText(rawData.Coin), BaseHpText = BuildBaseHpText(rawData.BaseHp, rawData.BaseHpMax), + EnemyHpRateText = BuildEnemyHpRateText(rawData.EnemyHpRateMultiplier), CanPause = rawData.CanPause, CanEnd = rawData.CanEnd }; @@ -140,6 +143,12 @@ namespace GeometryTD.UI return $"\u57FA\u5730\uFF1A{percent}%"; } + private static string BuildEnemyHpRateText(int enemyHpRateMultiplier) + { + int resolvedMultiplier = enemyHpRateMultiplier > 0 ? enemyHpRateMultiplier : 1; + return $"\u96BE\u5EA6\uFF1A{resolvedMultiplier}x"; + } + private void OnCombatPauseButtonClicked(object sender, GameEventArgs e) { if (!(sender is CombatInfoForm) || !(e is CombatPauseEventArgs)) @@ -197,6 +206,16 @@ namespace GeometryTD.UI RefreshFromUseCase(); } + private void OnCombatEnemyHpRateChanged(object sender, GameEventArgs e) + { + if (!(e is CombatEnemyHpRateChangedEventArgs)) + { + return; + } + + RefreshFromUseCase(); + } + private void RefreshFromUseCase() { if (_useCase == null) diff --git a/Assets/GameMain/Scripts/UI/Combat/RawData/CombatInfoFormRawData.cs b/Assets/GameMain/Scripts/UI/Combat/RawData/CombatInfoFormRawData.cs index 2b5aba6..d23d8f2 100644 --- a/Assets/GameMain/Scripts/UI/Combat/RawData/CombatInfoFormRawData.cs +++ b/Assets/GameMain/Scripts/UI/Combat/RawData/CombatInfoFormRawData.cs @@ -11,6 +11,7 @@ namespace GeometryTD.UI public int Coin; public int BaseHp; public int BaseHpMax; + public int EnemyHpRateMultiplier; public bool CanPause; public bool CanEnd; } diff --git a/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs b/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs index 780fe86..3fdb665 100644 --- a/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Combat/UseCase/CombatInfoFormUseCase.cs @@ -35,6 +35,9 @@ namespace GeometryTD.UI LevelThemeType themeType = level != null ? level.LevelThemeType : GameEntry.CombatNode.CurrentThemeType; int levelId = level != null ? level.Id : 0; int baseHpMax = level != null ? level.BaseHp : 0; + int enemyHpRateMultiplier = ResolveEnemyHpRateMultiplier( + GameEntry.CombatNode.CurrentPhaseIndex, + GameEntry.CombatNode.CurrentLevelPhaseCount); return new CombatInfoFormRawData { @@ -45,9 +48,26 @@ namespace GeometryTD.UI Coin = GameEntry.CombatNode.CurrentCoin, BaseHp = GameEntry.CombatNode.CurrentBaseHp, BaseHpMax = baseHpMax, + EnemyHpRateMultiplier = enemyHpRateMultiplier, CanPause = true, CanEnd = GameEntry.CombatNode.CanEndCombat }; } + + private static int ResolveEnemyHpRateMultiplier(int currentPhaseIndex, int totalPhaseCount) + { + if (currentPhaseIndex <= 0 || totalPhaseCount <= 0) + { + return 1; + } + + int completedLoopCount = UnityEngine.Mathf.Max(0, (currentPhaseIndex - 1) / totalPhaseCount); + if (completedLoopCount >= 30) + { + return int.MaxValue; + } + + return 1 << completedLoopCount; + } } } diff --git a/Assets/GameMain/Scripts/UI/Combat/View/CombatInfoForm.cs b/Assets/GameMain/Scripts/UI/Combat/View/CombatInfoForm.cs index 39866ef..4a190eb 100644 --- a/Assets/GameMain/Scripts/UI/Combat/View/CombatInfoForm.cs +++ b/Assets/GameMain/Scripts/UI/Combat/View/CombatInfoForm.cs @@ -15,6 +15,8 @@ namespace GeometryTD.UI [SerializeField] private TMP_Text _baseHpText; + [SerializeField] private TMP_Text _enemyHpRateText; + [SerializeField] private CommonButton _pauseButton; [SerializeField] private CommonButton _endButton; @@ -44,6 +46,11 @@ namespace GeometryTD.UI { _baseHpText.text = context?.BaseHpText ?? string.Empty; } + + if (_enemyHpRateText != null) + { + _enemyHpRateText.text = context?.EnemyHpRateText ?? string.Empty; + } if (_pauseButton != null) { @@ -112,6 +119,11 @@ namespace GeometryTD.UI { _baseHpText.text = string.Empty; } + + if (_enemyHpRateText != null) + { + _enemyHpRateText.text = string.Empty; + } base.OnClose(isShutdown, userData); } diff --git a/Assets/GameMain/Scripts/UI/General/View/DialogForm.cs b/Assets/GameMain/Scripts/UI/General/View/DialogForm.cs index 751fe30..88ba9b2 100644 --- a/Assets/GameMain/Scripts/UI/General/View/DialogForm.cs +++ b/Assets/GameMain/Scripts/UI/General/View/DialogForm.cs @@ -183,40 +183,25 @@ namespace GeometryTD.UI private void RefreshConfirmText(string confirmText) { - if (string.IsNullOrEmpty(confirmText)) + foreach (var text in _confirmTexts) { - confirmText = GameEntry.Localization.GetString("Dialog.ConfirmButton"); - } - - for (int i = 0; i < _confirmTexts.Length; i++) - { - _confirmTexts[i].text = confirmText; + text.text = confirmText; } } private void RefreshCancelText(string cancelText) { - if (string.IsNullOrEmpty(cancelText)) + foreach (var text in _cancelTexts) { - cancelText = GameEntry.Localization.GetString("Dialog.CancelButton"); - } - - for (int i = 0; i < _cancelTexts.Length; i++) - { - _cancelTexts[i].text = cancelText; + text.text = cancelText; } } private void RefreshOtherText(string otherText) { - if (string.IsNullOrEmpty(otherText)) + foreach (var text in _otherTexts) { - otherText = GameEntry.Localization.GetString("Dialog.OtherButton"); - } - - for (int i = 0; i < _otherTexts.Length; i++) - { - _otherTexts[i].text = otherText; + text.text = otherText; } } } diff --git a/Assets/GameMain/UI/UIForms/CombatFinishForm.prefab b/Assets/GameMain/UI/UIForms/CombatFinishForm.prefab index 1442241..d3352dd 100644 --- a/Assets/GameMain/UI/UIForms/CombatFinishForm.prefab +++ b/Assets/GameMain/UI/UIForms/CombatFinishForm.prefab @@ -1505,7 +1505,7 @@ MonoBehaviour: m_Horizontal: 0 m_Vertical: 1 m_MovementType: 1 - m_Elasticity: 0.1 + m_Elasticity: 0 m_Inertia: 1 m_DecelerationRate: 0.135 m_ScrollSensitivity: 1 diff --git a/Assets/GameMain/UI/UIForms/CombatInfoForm.prefab b/Assets/GameMain/UI/UIForms/CombatInfoForm.prefab index 34dce8c..a8ba4b0 100644 --- a/Assets/GameMain/UI/UIForms/CombatInfoForm.prefab +++ b/Assets/GameMain/UI/UIForms/CombatInfoForm.prefab @@ -459,6 +459,7 @@ MonoBehaviour: _levelPhaseText: {fileID: 8225082572338647983} _coinText: {fileID: 8462322658551354788} _baseHpText: {fileID: 1807917018366299772} + _enemyHpRateText: {fileID: 205084553981986984} _pauseButton: {fileID: 7428757802831321808} _endButton: {fileID: 5538219017489576212} --- !u!1 &7271289036404717568 @@ -629,6 +630,7 @@ RectTransform: - {fileID: 1726761505281868741} - {fileID: 4169170613063708458} - {fileID: 1569478724942140920} + - {fileID: 5154404242222297291} m_Father: {fileID: 8136671500363545760} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 1} @@ -662,6 +664,141 @@ MonoBehaviour: m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 +--- !u!1 &7780165276013755249 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5154404242222297291} + - component: {fileID: 8116375402155163259} + - component: {fileID: 205084553981986984} + m_Layer: 5 + m_Name: EnemyHpRateText + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5154404242222297291 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780165276013755249} + 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: 5356006041861078243} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &8116375402155163259 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780165276013755249} + m_CullTransparentMesh: 1 +--- !u!114 &205084553981986984 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780165276013755249} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "\u96BE\u5EA6\uFF1A2x" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 99d811b0183246646a2ce8df996f4bca, type: 2} + m_sharedMaterial: {fileID: -1106088975554028259, guid: 99d811b0183246646a2ce8df996f4bca, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 50 + m_fontSizeBase: 50 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 1 + m_VerticalAlignment: 256 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_enableWordWrapping: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 1 + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &8284139370021084558 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/GameMain/UI/UIForms/RepoForm.prefab b/Assets/GameMain/UI/UIForms/RepoForm.prefab index 5dde547..a71e7bc 100644 --- a/Assets/GameMain/UI/UIForms/RepoForm.prefab +++ b/Assets/GameMain/UI/UIForms/RepoForm.prefab @@ -842,12 +842,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -892,7 +892,7 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_AnchoredPosition.y - value: -140 + value: -100 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -978,12 +978,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -1154,12 +1154,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -1204,7 +1204,7 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_AnchoredPosition.y - value: -335 + value: -300 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -1417,22 +1417,22 @@ PrefabInstance: - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchorMax.x - value: 0.5 + value: 1 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchorMax.y - value: 0.5 + value: 1 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchorMin.x - value: 0.5 + value: 1 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchorMin.y - value: 0.5 + value: 1 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} @@ -1482,12 +1482,12 @@ PrefabInstance: - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchoredPosition.x - value: 1030 + value: -400 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchoredPosition.y - value: 700 + value: -100 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} @@ -1601,12 +1601,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -1819,7 +1819,7 @@ PrefabInstance: - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} propertyPath: m_AnchoredPosition.y - value: -600 + value: -500 objectReference: {fileID: 0} - target: {fileID: 4491355866364659447, guid: 2307f223279813546a43b221ddd496cc, type: 3} @@ -1933,12 +1933,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -1983,7 +1983,7 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_AnchoredPosition.y - value: -140 + value: -100 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -2109,12 +2109,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} @@ -2285,12 +2285,12 @@ PrefabInstance: - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.x - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} propertyPath: m_SizeDelta.y - value: 250 + value: 200 objectReference: {fileID: 0} - target: {fileID: 7862994607866941546, guid: e40294c47302bf140a51489f95814532, type: 3} diff --git a/Assets/GameMain/UI/UIItems/TowerSelectItem.prefab b/Assets/GameMain/UI/UIItems/TowerSelectItem.prefab index b1b287d..1dee9a6 100644 --- a/Assets/GameMain/UI/UIItems/TowerSelectItem.prefab +++ b/Assets/GameMain/UI/UIItems/TowerSelectItem.prefab @@ -134,7 +134,7 @@ MonoBehaviour: m_EditorClassIdentifier: m_Material: {fileID: 0} m_Color: {r: 1, g: 1, b: 1, a: 1} - m_RaycastTarget: 1 + m_RaycastTarget: 0 m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} m_Maskable: 1 m_OnCullStateChanged: diff --git a/Assets/Launcher.unity b/Assets/Launcher.unity index b448c1f..fb1889a 100644 --- a/Assets/Launcher.unity +++ b/Assets/Launcher.unity @@ -819,79 +819,6 @@ MonoBehaviour: _buildInfoTextAsset: {fileID: 4900000, guid: 0f949a7f73d128547b1314a7e471f19f, type: 3} _updateResourceFormTemplate: {fileID: 11487720, guid: a6c731de80e9d824d8f657301a357269, type: 3} ---- !u!1001 &571687048 -PrefabInstance: - m_ObjectHideFlags: 0 - serializedVersion: 2 - m_Modification: - serializedVersion: 3 - m_TransformParent: {fileID: 0} - m_Modifications: - - target: {fileID: 7091080056390424735, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_Name - value: Level1 - objectReference: {fileID: 0} - - target: {fileID: 7091080056390424735, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_IsActive - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalPosition.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalPosition.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalPosition.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalRotation.w - value: 1 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalRotation.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalRotation.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalRotation.z - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalEulerAnglesHint.x - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalEulerAnglesHint.y - value: 0 - objectReference: {fileID: 0} - - target: {fileID: 7948599801963745741, guid: 0cc71b1087c7dfd42a8233a7101fc27e, - type: 3} - propertyPath: m_LocalEulerAnglesHint.z - value: 0 - objectReference: {fileID: 0} - m_RemovedComponents: [] - m_RemovedGameObjects: [] - m_AddedGameObjects: [] - m_AddedComponents: [] - m_SourcePrefab: {fileID: 100100000, guid: 0cc71b1087c7dfd42a8233a7101fc27e, type: 3} --- !u!850595691 &819759088 LightingSettings: m_ObjectHideFlags: 0 @@ -956,134 +883,6 @@ LightingSettings: m_PVRTiledBaking: 0 m_NumRaysToShootPerTexel: -1 m_RespectSceneVisibilityWhenBakingGI: 0 ---- !u!1 &1376238055 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1376238059} - - component: {fileID: 1376238058} - - component: {fileID: 1376238056} - m_Layer: 0 - m_Name: Camera - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 0 ---- !u!114 &1376238056 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1376238055} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} - m_Name: - m_EditorClassIdentifier: - m_RenderShadows: 1 - m_RequiresDepthTextureOption: 2 - m_RequiresOpaqueTextureOption: 2 - m_CameraType: 0 - m_Cameras: [] - m_RendererIndex: -1 - m_VolumeLayerMask: - serializedVersion: 2 - m_Bits: 1 - m_VolumeTrigger: {fileID: 0} - m_VolumeFrameworkUpdateModeOption: 2 - m_RenderPostProcessing: 0 - m_Antialiasing: 0 - m_AntialiasingQuality: 2 - m_StopNaN: 0 - m_Dithering: 0 - m_ClearDepth: 1 - m_AllowXRRendering: 1 - m_AllowHDROutput: 1 - m_UseScreenCoordOverride: 0 - m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} - m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} - m_RequiresDepthTexture: 0 - m_RequiresColorTexture: 0 - m_Version: 2 - m_TaaSettings: - m_Quality: 3 - m_FrameInfluence: 0.1 - m_JitterScale: 1 - m_MipBias: 0 - m_VarianceClampScale: 0.9 - m_ContrastAdaptiveSharpening: 0 ---- !u!20 &1376238058 -Camera: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1376238055} - m_Enabled: 1 - serializedVersion: 2 - m_ClearFlags: 1 - m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} - m_projectionMatrixMode: 1 - m_GateFitMode: 2 - m_FOVAxisMode: 0 - m_Iso: 200 - m_ShutterSpeed: 0.005 - m_Aperture: 16 - m_FocusDistance: 10 - m_FocalLength: 50 - m_BladeCount: 5 - m_Curvature: {x: 2, y: 11} - m_BarrelClipping: 0.25 - m_Anamorphism: 0 - m_SensorSize: {x: 36, y: 24} - m_LensShift: {x: 0, y: 0} - m_NormalizedViewPortRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 1 - height: 1 - near clip plane: 0.3 - far clip plane: 1000 - field of view: 60 - orthographic: 1 - orthographic size: 7.5 - m_Depth: 0 - m_CullingMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RenderingPath: -1 - m_TargetTexture: {fileID: 0} - m_TargetDisplay: 0 - m_TargetEye: 3 - m_HDR: 1 - m_AllowMSAA: 1 - m_AllowDynamicResolution: 0 - m_ForceIntoRT: 0 - m_OcclusionCulling: 1 - m_StereoConvergence: 10 - m_StereoSeparation: 0.022 ---- !u!4 &1376238059 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1376238055} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: -10} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1454214586 GameObject: m_ObjectHideFlags: 0 @@ -1299,7 +1098,7 @@ Canvas: m_Enabled: 1 serializedVersion: 3 m_RenderMode: 1 - m_Camera: {fileID: 1376238058} + m_Camera: {fileID: 0} m_PlaneDistance: 100 m_PixelPerfect: 0 m_ReceivesEvents: 1 @@ -1500,5 +1299,3 @@ SceneRoots: m_Roots: - {fileID: 1852670053} - {fileID: 120093242} - - {fileID: 1376238059} - - {fileID: 571687048} diff --git a/docs/CombatNodeArchitecture.md b/docs/CombatNodeArchitecture.md index 5333881..a39a143 100644 --- a/docs/CombatNodeArchitecture.md +++ b/docs/CombatNodeArchitecture.md @@ -1,221 +1,127 @@ -# CombatNode 架构摘要 +# CombatNode Architecture -最后更新:2026-02-28 +最后更新:2026-03-02 -## 1. 目标与边界 +## 1. 总览 +CombatNode 当前分为三层: +- `CombatNodeComponent`:入口与关卡数据装配。 +- `CombatScheduler`:战斗状态机与阶段推进,以及关卡内资源结算。 +- `EnemyManager`:敌人系统 Facade(对外接口保持稳定,内部由多个子服务协作)。 -CombatNode 子系统的目标是把“战斗节点”拆成三个稳定层: +本文重点记录 `EnemyManager` 与 `CombatScheduler` 的职责边界(尤其资源管理收口后的模型)。 -- `CombatNodeComponent`:节点入口与配置缓存(门面层) -- `CombatScheduler`:单局战斗状态机(编排层) -- `EnemyManager`:刷怪与敌人生命周期(执行层) +## 2. EnemyManager 相关类 -边界约束: - -- 只负责战斗节点,不负责菜单流程切换,不负责 UI 细节。 -- 只依赖数据表和 Entity 系统,不直接持有场景流程 FSM。 -- 地图寻路由 `MapEntity` 提供能力,敌人移动由 `EnemyEntity` 自治。 - ---- - -## 2. 模块职责划分 - -## 2.1 CombatNodeComponent(门面层) - -文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs` +### 2.1 EnemyManager(Facade) +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyManager.cs` 职责: +- 对 `CombatScheduler` 提供统一接口:`OnInit / BeginPhase / OnUpdate / EndPhase / OnDestroy`。 +- 编排敌人域子服务,不承载具体业务细节。 +- 转发状态:`AliveEnemyCount`、`IsPhaseRunning`、`IsPhaseSpawnCompleted`。 +- 处理敌人实体事件(Show/Hide)后的结果上报: + - 击杀:上报 `coin/gold` 到 `CombatScheduler.OnEnemyDefeatedRewardResolved(...)` + - 到家:上报 `baseDamage` 到 `CombatScheduler.OnEnemyReachedBase(...)` -- 读取并缓存 `DRLevel / DRLevelPhase / DRLevelSpawnEntry`。 -- 按主题筛选关卡,维护 `Level -> Phase -> SpawnEntry` 映射。 -- 提供 `OnInit / StartCombat / OnUpdate / OnShutdown` 外部接口。 -- 触发节点事件: - - `NodeEnterEventArgs` - - `NodeCompleteEventArgs` +不负责: +- 不直接维护刷怪时间轴。 +- 不直接维护出生点缓存与路径查询。 +- 不直接维护敌人追踪结构。 +- 不直接维护掉落池抽样与背包库存。 +- 不直接维护敌人配置兜底和血量倍率算法。 -不做的事: - -- 不做阶段推进。 -- 不做刷怪时序。 -- 不做实体显示/隐藏细节。 - -## 2.2 CombatScheduler(编排层) - -文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler.cs` +### 2.2 EnemySpawnDirector(刷怪时序) +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemySpawnDirector.cs` 职责: +- 管理 `DRLevelSpawnEntry` 的运行时实例(`SpawnEntryRuntime`)。 +- 按时间推进 `Stream / Burst / Boss` 生成逻辑。 +- 输出“当前应生成多少敌人”,通过回调交给 `EnemyManager.SpawnEnemies` 执行。 +- 维护阶段刷怪状态:`IsPhaseRunning`、`IsPhaseSpawnCompleted`。 -- 管理战斗状态机:`Idle -> WaitingForMap -> RunningPhase -> Completed/Failed`。 -- 加载地图实体,接收地图 show/hide 成功失败事件。 -- 按 phase 结束条件推进到下一阶段。 -- 在开始新战斗或销毁时做统一清场(地图 + 本局敌人)。 - -不做的事: - -- 不直接计算每条刷怪规则的触发细节(交给 `EnemyManager`)。 - -## 2.3 EnemyManager(执行层) - -文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager.cs` +### 2.3 SpawnerResolver(出生点与路径) +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/SpawnerResolver.cs` 职责: +- 缓存当前地图可用 `Spawner`。 +- 支持 `SpawnOrder` 映射与 fallback 轮询。 +- 对外提供 `TryResolveSpawnPath(...)`,返回世界坐标路径点。 +- 在地图切换时刷新缓存,避免每次刷怪全量扫描。 -- 将 `DRLevelSpawnEntry` 转为运行时任务(stream/burst/boss)。 -- 按时间推进刷怪任务并生成 `EnemyData`。 -- 维护本局敌人数量 `AliveEnemyCount`。 -- 维护“本局生成敌人 ID 集合”,用于准确计数与清场。 - -关键点: - -- 生命周期归属按 `entityId` 跟踪,不再按实体组名粗粒度统计。 -- 清场可覆盖“已加载 + 加载中”的本局敌人。 - ---- - -## 3. 关键实体职责 - -## 3.1 MapEntity - -文件:`Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs` +### 2.4 EnemyLifecycleTracker(敌人生命周期追踪) +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyLifecycleTracker.cs` 职责: +- 追踪本局敌人 `entityId -> DREnemy`。 +- 维护 `AliveEnemyCount`。 +- 处理 `ShowSuccess / ShowFailure / HideComplete` 对追踪状态的变更。 +- 提供批量导出 tracked ids,用于 `CleanupTrackedEnemies` 清场。 -- 读取 Tilemap,识别 Path/Foundation 格子。 -- 为每个 `Spawner` 缓存默认可达路径。 -- 提供: - - `TryGetDefaultPathCells` - - `TryFindPathCells` - - `TryFindPathWorldPoints` - -## 3.2 EnemyEntity - -文件:`Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs` +### 2.5 EnemyConfigService(敌人配置与倍率) +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/EnemyManager/EnemyConfigService.cs` 职责: +- 读取 `DREnemy`,处理默认配置兜底。 +- 计算循环周目下的基础血量倍率(按 `displayPhaseIndex / phaseCount` 推导 loop)。 +- 缓存数据表引用并在 `Reset` 时清理。 -- 按路径点移动。 -- 到达终点后调用 `HideEntity` 自行退出。 +## 3. CombatScheduler 资源收口 ---- +### 3.1 CombatResourceManager(关卡内资源统一管理) +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatResourceManager.cs` -## 4. 运行时时序(简版) +职责: +- 统一维护关卡内资源状态: + - `GainedCoin`、`GainedGold` + - `BackpackInventoryData`(结算背包快照) +- 处理击杀奖励入账:`AddEnemyDefeatedReward(...)` +- 处理局外掉落抽样与物品构建:`TryRollOutGameItemDrop(...)` +- 对结算 UI 提供只读快照:`GetRewardInventorySnapshot()` -1. 菜单触发战斗:`TestMenuFormController -> GameEntry.CombatNode.StartCombat()` -2. `CombatNodeComponent.StartCombat()` 选关并把关卡配置交给 `CombatScheduler.Start()` -3. `CombatScheduler.Start()` 先清理上局残留,再 `ShowMap(...)`,状态进入 `WaitingForMap` -4. 地图加载成功事件到达,`_currentMap` 就绪 -5. `BeginNextPhase()` 进入 `RunningPhase`,`EnemyManager.BeginPhase(...)` -6. 每帧 `OnUpdate`: - - `CombatScheduler` 更新时间与结束条件 - - `EnemyManager` 推进刷怪任务并创建敌人 -7. 当前 phase 满足结束条件,`CompleteCurrentPhase()`,然后进入下一 phase -8. 全 phase 完成后隐藏地图,`GameEntry.CombatNode.EndCombat()`,抛 `NodeComplete` +### 3.2 CombatScheduler 与资源类关系 +文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` ---- +当前模型: +- `CombatScheduler` 持有 `_combatResourceManager`。 +- `GainedCoin/GainedGold` 属性透传资源类。 +- `OnEnemyDefeatedRewardResolved(...)` 统一触发: + - 货币入账 + - 关卡内掉落判定 +- `EnterFinishFlow(...)` 从资源类取 `BackpackInventoryData` 作为结算数据。 -## 5. 数据契约与 ID 规则 +## 4. 协作流程(简版) +1. `CombatScheduler.BeginNextPhase()` 调 `EnemyManager.BeginPhase(...)`。 +2. `EnemyManager` 刷新 `SpawnerResolver` 缓存,并通知 `EnemySpawnDirector.BeginPhase(...)`。 +3. 每帧 `EnemyManager.OnUpdate(...)`: + - `SpawnerResolver.RefreshCache(...)` + - `EnemySpawnDirector.OnUpdate(..., SpawnEnemies)` +4. `SpawnEnemies(...)` 执行实际生成: + - `SpawnerResolver.TryResolveSpawnPath(...)` + - `EnemyConfigService.GetEnemyConfig(...)` + - `EnemyConfigService.ResolveScaledEnemyBaseHp(...)` + - `EnemyLifecycleTracker.TrackEnemy(...)` + - `GameEntry.Entity.ShowEnemy(...)` +5. 敌人回收时 `EnemyManager.OnHideEntityComplete(...)`: + - `EnemyLifecycleTracker.TryHandleHideComplete(...)` + - 若击杀:上报 `coin/gold` 给 `CombatScheduler.OnEnemyDefeatedRewardResolved(...)` + - 若到家:上报 `baseDamage` 给 `CombatScheduler.OnEnemyReachedBase(...)` +6. `CombatScheduler.OnEnemyDefeatedRewardResolved(...)`: + - `CombatResourceManager.AddEnemyDefeatedReward(...)` + - `CombatResourceManager.TryRollOutGameItemDrop(...)` +7. `CombatScheduler.EnterFinishFlow(...)`: + - 读取 `CombatResourceManager.GetRewardInventorySnapshot()` + - 打开结算 UI -数据来源: +## 5. 关键不变量 +- 存活敌人数以 `EnemyLifecycleTracker.AliveEnemyCount` 为唯一真值来源。 +- 刷怪阶段状态以 `EnemySpawnDirector` 为唯一真值来源。 +- 关卡内资源以 `CombatResourceManager` 为唯一真值来源。 +- `EnemyManager` 只做敌人域编排与结果上报,不再持有奖励库存。 +- 清场必须只作用于本局 tracked 敌人,避免误伤其他实体。 -- `DRLevel` -- `DRLevelPhase` -- `DRLevelSpawnEntry` - -当前约定(依赖 ID 编码): - -- `levelId = phaseId / 1000` -- `phaseId = spawnEntryId / 1000` - -影响: - -- 新增配置时必须遵守该编码,否则 phase 和 entry 无法被正确归属。 - -建议: - -- 若后续改数据结构,优先改成显式外键字段,减少 ID 推导耦合。 - ---- - -## 6. 结束条件模型 - -枚举:`PhaseEndType` - -- `TimeElapsed`:按 `EndParam`(可解析 float)或 `DurationSeconds` -- `EnemiesCleared`:`IsPhaseSpawnCompleted && AliveEnemyCount <= 0` -- `BossDead`:当前与 `EnemiesCleared` 同判定 -- `None`:有 duration 走 duration,否则退化为清怪判定 - -实现点:`CombatScheduler.ShouldEndCurrentPhase()` - ---- - -## 7. 生命周期与清场策略(当前实现) - -## 7.1 地图生命周期 - -- 通过 `ShowMap` 异步加载。 -- 通过 `ShowEntitySuccess/Failure` 绑定运行态地图引用。 -- 完成关卡时隐藏地图。 -- 新战斗开始和调度器销毁前都会尝试清理地图实体。 - -## 7.2 敌人生命周期 - -- 创建时登记 `entityId` 到 `_trackedEnemyEntityIds`。 -- `ShowEntitySuccess` 仅对 tracked id 计数。 -- `HideEntityComplete` 仅对 tracked id 减计数并移除跟踪。 -- 清场时按 tracked id 隐藏(覆盖加载中和已加载实体)。 - -## 7.3 设计目标 - -- 防止跨局残留实体污染新局状态。 -- 防止其他系统 `Enemy` 组实体干扰战斗计数。 - ---- - -## 8. 扩展指南(按需求) - -## 8.1 新增阶段结束条件 - -- 改 `PhaseEndType` 枚举。 -- 在 `ShouldEndCurrentPhase()` 增加分支。 - -## 8.2 新增刷怪条目类型 - -- 改 `EntryType` 枚举。 -- 在 `EnemyManager.BuildSpawnRuntime()` 与 `UpdateSpawnRuntimes()` 增加处理。 - -## 8.3 改选关策略 - -- 当前是随机选关:`TrySelectRandomLevel()`。 -- 可改为按难度、解锁进度、权重池。 - -## 8.4 动态阻挡/重算路径 - -- 当前 `SpawnEnemies` 调 `TryFindPathWorldPoints(spawner, null, ...)`。 -- 若支持塔阻挡,传入 `blockedCells` 并处理“无路可走”策略。 - -## 8.5 结算与奖励 - -- 推荐挂在 `CombatScheduler` 完成分支(所有 phase 完成处),不要塞进 `EnemyManager`。 - ---- - -## 9. 维护时的硬性不变量 - -- `CombatNodeComponent` 只做缓存和入口,不写战斗细节。 -- `CombatScheduler` 是唯一 phase 状态机,不在别处推进 phase。 -- `AliveEnemyCount` 必须由 tracked id 驱动,不回退到组名统计。 -- 新战斗开始前必须清场。 -- `ResetRuntime()` 必须清空地图相关运行态引用和 id。 - ---- - -## 10. 快速阅读路径(新人 10 分钟) - -1. `CombatNodeComponent.StartCombat()` -2. `CombatScheduler.Start() / OnUpdate() / BeginNextPhase()` -3. `EnemyManager.BeginPhase() / OnUpdate() / SpawnEnemies()` -4. `MapEntity.TryFindPathWorldPoints()` -5. `EnemyEntity.DespawnOnReachHouse()` - -这 5 个点读完,基本能覆盖 90% 战斗节点行为。 +## 6. 维护建议 +- 新增刷怪类型:优先改 `EnemySpawnDirector`。 +- 新增路径/出生规则:优先改 `SpawnerResolver`。 +- 新增敌人追踪策略:优先改 `EnemyLifecycleTracker`。 +- 新增敌人配置兜底或倍率策略:优先改 `EnemyConfigService`。 +- 新增货币/掉落/结算背包规则:优先改 `CombatResourceManager`。