From 380f901c1ad513bdbf50d875126c96fe0e7f0a46 Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Sat, 7 Mar 2026 21:01:03 +0800 Subject: [PATCH] refactor 8: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CombatScheduler 不再反向访问 GameEntry.CombatNode。现在由 CombatNodeComponent.cs:322 在初始化时把完成回调传进 scheduler,运行时回调保存在 CombatSchedulerRuntimeContext.cs:27,完成时由 CombatSchedulerFlowCoordinator.cs:245 触发;主题解析也只看当前战斗上下文,不再回退查 facade,见 CombatSchedulerFlowCoordinator.cs:161。同时把 MapEntity 的 coin 回调改成直接连到资源真值来源 CombatLoadingState.cs:56,并把 ICombatSchedulerHost 上那两个资源转发接口删掉,见 ICombatSchedulerHost.cs:5。 - 地图加载入口也收紧了。CombatLoadSession 现在要求必须有 MapEntityLoadContext.InitialMapData,不再走隐式兜底,见 CombatLoadSession.cs:227。EntityExtension 删除了 ShowMap(MapData) 旧重载,当前只保留 ShowMap(MapEntityLoadContext),见 EntityExtension.cs:52;MapEntity 也不再接受裸 MapData 的遗留分支,见 MapEntity.cs:263。 --- .../CombatNode/CombatNodeComponent.cs | 2 +- .../CombatInRunResourceManager.cs | 497 ++---------------- .../CombatScheduler/CombatLoadSession.cs | 14 +- .../CombatScheduler/CombatScheduler.cs | 16 +- .../CombatSchedulerFlowCoordinator.cs | 18 +- .../CombatSchedulerRuntimeContext.cs | 2 + .../CombatSettlementFlowService.cs | 6 +- .../CombatStates/CombatLoadingState.cs | 13 +- .../CombatRewardSelectionState.cs | 2 +- .../EnemyDrop/EnemyDropResolveResult.cs | 10 +- .../EnemyDrop/EnemyDropResolver.cs | 395 +++++++++++++- .../CombatScheduler/ICombatSchedulerHost.cs | 2 - .../Scripts/Entity/EntityExtension.cs | 10 - .../Scripts/Entity/EntityLogic/MapEntity.cs | 6 - 14 files changed, 485 insertions(+), 508 deletions(-) diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs index ca183b8..e27de0c 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs @@ -319,7 +319,7 @@ namespace GeometryTD.CustomComponent return true; } - _combatScheduler.OnInit(); + _combatScheduler.OnInit(OnCombatEndedByScheduler); _runtimeInitialized = true; return true; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatInRunResourceManager.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatInRunResourceManager.cs index afb22e0..568d457 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatInRunResourceManager.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatInRunResourceManager.cs @@ -1,38 +1,19 @@ -using System; using System.Collections.Generic; -using GameFramework.DataTable; using GeometryTD.CustomEvent; using GeometryTD.CustomUtility; using GeometryTD.DataTable; using GeometryTD.Definition; using UnityEngine; -using Random = UnityEngine.Random; namespace GeometryTD.CustomComponent { internal sealed class CombatInRunResourceManager { - // 每次击杀敌人后,进入“掉组件判定”的概率: - // chance = clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0, DropChanceCap) - private const float DropChanceBase = 0.05f; - private const float DropChancePerPhase = 0.2f; - private const float DropChanceCap = 0.2f; - - // 用于把阶段映射到 [0,1] 的稀有度曲线输入: - // phaseT = clamp01((phaseIndex - 1) / RarityCurveScalePhase) - // 该值越大,稀有度随阶段提升的节奏越慢。 - private const float RarityCurveScalePhase = 30f; - - private readonly List _eligibleDropPoolBuffer = new(); private readonly List _buildTowerStatsSnapshot = new(); - private readonly Dictionary _rarityRollWeightBuffer = new(); + private readonly List _participantTowerSnapshot = new(); private readonly BackpackInventoryData _rewardInventory = new(); - private IDataTable _drOutGameDropPool; - private IDataTable _drMuzzleComp; - private IDataTable _drBearingComp; - private IDataTable _drBaseComp; - private long _nextDropItemInstanceId = 1; + private BackpackInventoryData _combatInventorySnapshot = new(); private bool _isCombatActive; public int CurrentCoin { get; private set; } @@ -54,7 +35,7 @@ namespace GeometryTD.CustomComponent MaxBaseHp = Mathf.Max(0, level.BaseHp); CurrentBaseHp = MaxBaseHp; CurrentCoin = Mathf.Max(0, level.StartCoin); - CacheBuildTowerStatsSnapshot(); + CacheCombatSnapshots(); _isCombatActive = true; } @@ -72,13 +53,14 @@ namespace GeometryTD.CustomComponent GainedCoin = 0; GainedGold = 0; _buildTowerStatsSnapshot.Clear(); + _participantTowerSnapshot.Clear(); + _combatInventorySnapshot = new BackpackInventoryData(); _rewardInventory.Gold = 0; _rewardInventory.MuzzleComponents.Clear(); _rewardInventory.BearingComponents.Clear(); _rewardInventory.BaseComponents.Clear(); _rewardInventory.Towers.Clear(); _rewardInventory.ParticipantTowerInstanceIds.Clear(); - _nextDropItemInstanceId = 1; } public BackpackInventoryData GetRewardInventorySnapshot() @@ -86,6 +68,26 @@ namespace GeometryTD.CustomComponent return InventoryCloneUtility.CloneInventory(_rewardInventory); } + public BackpackInventoryData GetCombatInventorySnapshot() + { + return InventoryCloneUtility.CloneInventory(_combatInventorySnapshot); + } + + public IReadOnlyList GetParticipantTowerSnapshot() + { + List snapshot = new List(_participantTowerSnapshot.Count); + for (int i = 0; i < _participantTowerSnapshot.Count; i++) + { + TowerItemData tower = _participantTowerSnapshot[i]; + if (tower != null) + { + snapshot.Add(InventoryCloneUtility.CloneTower(tower)); + } + } + + return snapshot; + } + public bool TryConsumeCoin(int coin) { int requiredCoin = Mathf.Max(0, coin); @@ -188,9 +190,23 @@ namespace GeometryTD.CustomComponent _rewardInventory.Gold += gold; } - private void CacheBuildTowerStatsSnapshot() + public void AddEnemyDefeatedLoot(TowerCompItemData droppedItem) + { + if (!_isCombatActive || droppedItem == null) + { + return; + } + + AppendRewardItem(droppedItem); + } + + private void CacheCombatSnapshots() { _buildTowerStatsSnapshot.Clear(); + _participantTowerSnapshot.Clear(); + _combatInventorySnapshot = GameEntry.PlayerInventory != null + ? GameEntry.PlayerInventory.GetInventorySnapshot() + : new BackpackInventoryData(); if (GameEntry.PlayerInventory == null) { return; @@ -205,12 +221,16 @@ namespace GeometryTD.CustomComponent for (int i = 0; i < towers.Count; i++) { TowerItemData tower = towers[i]; - if (tower?.Stats == null) + if (tower == null) { continue; } - _buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats)); + _participantTowerSnapshot.Add(InventoryCloneUtility.CloneTower(tower)); + if (tower.Stats != null) + { + _buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats)); + } } } @@ -234,427 +254,20 @@ namespace GeometryTD.CustomComponent GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp)); } - /// - /// 击杀敌人时的掉落入口。 - /// displayPhaseIndex 会影响: - /// 1) 总掉落概率(由 DropChanceBase / DropChancePerPhase / DropChanceCap 决定) - /// 2) 稀有度倾向(通过 phaseT 进入 RollRarity) - /// themeType 会按关卡主题过滤掉落池(LevelThemeType 不匹配直接不参与)。 - /// - public bool TryRollOutGameItemDrop(int displayPhaseIndex, LevelThemeType themeType) + private void AppendRewardItem(TowerCompItemData droppedItem) { - int phaseIndex = Mathf.Max(1, displayPhaseIndex); - float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap); - if (Random.value > dropChance) + switch (droppedItem) { - return false; - } - - if (!TryRollOutGameItem(phaseIndex, themeType, 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; - } - - /// - /// 直接抽取一个具体掉落物(不走前面的总掉率门槛)。 - /// 但 displayPhaseIndex 和 themeType 仍会通过掉落池过滤、稀有度加权影响结果。 - /// - public bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem) - { - droppedItem = null; - int phaseIndex = Mathf.Max(1, displayPhaseIndex); - if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow)) - { - return false; - } - - return TryBuildDropItem(selectedRow, out droppedItem); - } - - public IReadOnlyList RollSettlementRewardCandidates( - int displayPhaseIndex, - LevelThemeType themeType, - int candidateCount) - { - int resolvedCount = Mathf.Max(0, candidateCount); - if (resolvedCount <= 0) - { - return Array.Empty(); - } - - List candidates = new List(resolvedCount); - HashSet selectedPoolRowIds = new HashSet(); - int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount); - int phaseIndex = Mathf.Max(1, displayPhaseIndex); - - int attempts = 0; - while (candidates.Count < resolvedCount && attempts < maxAttempts) - { - attempts++; - if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null) - { + case MuzzleCompItemData muzzleComp: + _rewardInventory.MuzzleComponents.Add(InventoryCloneUtility.CloneMuzzleComp(muzzleComp)); break; - } - - if (!selectedPoolRowIds.Add(selectedRow.Id)) - { - continue; - } - - if (!TryBuildDropItem(selectedRow, out TowerCompItemData droppedItem) || droppedItem == null) - { - continue; - } - - candidates.Add(droppedItem); - } - - attempts = 0; - while (candidates.Count < resolvedCount && attempts < maxAttempts) - { - attempts++; - if (!TryRollOutGameItem(phaseIndex, themeType, out TowerCompItemData droppedItem) || droppedItem == null) - { + case BearingCompItemData bearingComp: + _rewardInventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp)); + break; + case BaseCompItemData baseComp: + _rewardInventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp)); break; - } - - candidates.Add(droppedItem); } - - return candidates; - } - - /// - /// 两阶段加权抽样: - /// 1) 先按主题 + 阶段区间过滤候选行 - /// 2) 先抽稀有度(稀有度权重 = 同稀有度所有行的 row.Weight * 稀有度曲线权重 之和) - /// 3) 在该稀有度内再按 row.Weight 抽具体行 - /// - 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; - } - - // 主题过滤:主题不匹配的行,概率为 0。 - if (row.LevelThemeType != themeType) - { - continue; - } - - // 阶段过滤:不在 [MinPhase, MaxPhase] 的行,概率为 0。 - 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; - } - - // 在已选稀有度内,row.Weight 是线性权重倍率。 - 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; - } - - /// - /// 在过滤后的候选行里抽稀有度。 - /// 实际稀有度权重公式: - /// rarityWeight = sum(max(1, row.Weight) * GetRarityCurveWeight(row.Rarity, phaseT)) - /// 其中 phaseT = clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase)。 - /// - 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; - } - - /// - /// 返回各稀有度在当前阶段的曲线权重。 - /// 返回值越大,该稀有度在 RollRarity 中越容易被抽到。 - /// phaseT 取值范围为 [0,1]: - /// - White/Green:钟形趋势(前中期权重更高) - /// - Blue/Purple:随阶段近似线性上升 - /// - Red:随阶段二次上升(前期很低,后期明显抬升) - /// - 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/CombatLoadSession.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs index f6d2cb6..bc3eb7d 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatLoadSession.cs @@ -233,6 +233,12 @@ namespace GeometryTD.CustomComponent return false; } + if (mapLoadContext?.InitialMapData == null) + { + errorMessage = "CombatLoadSession map load failed. MapEntityLoadContext is missing initial map data."; + return false; + } + string mapAssetName = level.Id.ToString(); string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName); if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist) @@ -242,13 +248,11 @@ namespace GeometryTD.CustomComponent } _loadingMapEntityId = _entity.GenerateSerialId(); - MapData resolvedMapData = mapLoadContext?.InitialMapData != null - ? mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero) - : new MapData(_loadingMapEntityId, level.Id, Vector3.zero); + MapData resolvedMapData = mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero); MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext( resolvedMapData, - mapLoadContext?.TryConsumeCoin, - mapLoadContext?.AddCoin); + mapLoadContext.TryConsumeCoin, + mapLoadContext.AddCoin); _entity.ShowMap(resolvedLoadContext, mapAssetName); return true; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index 988b8b9..e86c4d3 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using GeometryTD.CustomEvent; using GeometryTD.DataTable; @@ -39,8 +40,10 @@ namespace GeometryTD.CustomComponent public int GainedCoin => _context.CombatInRunResourceManager.GainedCoin; public int GainedGold => _context.CombatInRunResourceManager.GainedGold; - public void OnInit() + public void OnInit(Action combatEndedCallback) { + _context.CombatEndedCallback = combatEndedCallback; + if (!_initialized) { _context.Entity = GameEntry.Entity; @@ -142,6 +145,7 @@ namespace GeometryTD.CustomComponent _context.EventBridge.Unbind(); _context.CombatFinishFormUseCase = null; _context.RewardSelectFormUseCase = null; + _context.CombatEndedCallback = null; _context.Entity = null; _initialized = false; @@ -187,15 +191,7 @@ namespace GeometryTD.CustomComponent _flowCoordinator.ResolveCurrentThemeType()); EnemyDropResolveResult result = _context.EnemyDropResolver.Resolve(context); _context.CombatInRunResourceManager.AddEnemyDefeatedReward(result.Coin, result.Gold); - - if (!result.ShouldRollOutGameItem) - { - return; - } - - _context.CombatInRunResourceManager.TryRollOutGameItemDrop( - context.DisplayPhaseIndex, - context.ThemeType); + _context.CombatInRunResourceManager.AddEnemyDefeatedLoot(result.LootItem); } public bool OnCombatFinishReturnRequested() diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs index 3219ea8..d4216ec 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerFlowCoordinator.cs @@ -49,6 +49,7 @@ namespace GeometryTD.CustomComponent _context.PhaseLoopRuntime.Reset(); _context.LoadSession.Reset(); _context.CombatInRunResourceManager.Reset(); + _context.EnemyDropResolver.Reset(); _context.SettlementContext = null; _context.CurrentLevel = null; _context.IsFinishAsVictory = true; @@ -164,11 +165,6 @@ namespace GeometryTD.CustomComponent return _context.CurrentLevel.LevelThemeType; } - if (GameEntry.CombatNode != null) - { - return GameEntry.CombatNode.CurrentThemeType; - } - return LevelThemeType.None; } @@ -177,16 +173,6 @@ namespace GeometryTD.CustomComponent return _context.CombatInRunResourceManager.ApplyBaseDamage(damage); } - public bool TryConsumeCoin(int coin) - { - return _schedulerHost.TryConsumeCoin(coin); - } - - public void AddCoin(int coin) - { - _schedulerHost.AddCoin(coin); - } - public int GetCurrentBaseHp() { return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp); @@ -261,7 +247,7 @@ namespace GeometryTD.CustomComponent _context.IsCompleted = true; _context.CurrentState = null; _context.CombatInRunResourceManager.MarkCombatEnded(); - GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded); + _context.CombatEndedCallback?.Invoke(succeeded); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs index e7c1f29..d4e3d9f 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntimeContext.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using GeometryTD.DataTable; using GeometryTD.Entity; @@ -23,6 +24,7 @@ namespace GeometryTD.CustomComponent public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; } public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; } public CombatStateBase CurrentState { get; set; } + public Action CombatEndedCallback { get; set; } public bool IsFinishAsVictory { get; set; } = true; public bool IsCompleted { get; set; } public bool NodeEnterFired { get; set; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementFlowService.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementFlowService.cs index 1bb72bc..28010bb 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementFlowService.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementFlowService.cs @@ -87,19 +87,19 @@ namespace GeometryTD.CustomComponent public bool TryPrepareRewardSelection( CombatSettlementContext settlementContext, - CombatInRunResourceManager resourceManager, + EnemyDropResolver enemyDropResolver, int displayPhaseIndex, LevelThemeType themeType, RewardSelectFormUseCase rewardSelectFormUseCase, Action onRewardSelected, Action onGiveUp) { - if (settlementContext == null || resourceManager == null || rewardSelectFormUseCase == null) + if (settlementContext == null || enemyDropResolver == null || rewardSelectFormUseCase == null) { return false; } - IReadOnlyList candidateItems = resourceManager.RollSettlementRewardCandidates( + IReadOnlyList candidateItems = enemyDropResolver.RollSettlementRewardCandidates( displayPhaseIndex, themeType, RewardSelectDisplayCount); diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs index d4f4e88..ac23a12 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatLoadingState.cs @@ -60,13 +60,12 @@ namespace GeometryTD.CustomComponent position: Vector3.zero, initialCoin: Context.CombatInRunResourceManager.CurrentCoin, buildTowerStatsSnapshot: buildTowerStatsSnapshot, - inventorySnapshot: GameEntry.PlayerInventory != null - ? GameEntry.PlayerInventory.GetInventorySnapshot() - : null, - participantTowerSnapshot: GameEntry.PlayerInventory != null - ? GameEntry.PlayerInventory.GetParticipantTowerSnapshot() - : null); - return new MapEntityLoadContext(mapData, Flow.TryConsumeCoin, Flow.AddCoin); + inventorySnapshot: Context.CombatInRunResourceManager.GetCombatInventorySnapshot(), + participantTowerSnapshot: Context.CombatInRunResourceManager.GetParticipantTowerSnapshot()); + return new MapEntityLoadContext( + mapData, + Context.CombatInRunResourceManager.TryConsumeCoin, + Context.CombatInRunResourceManager.AddCoin); } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs index b866e78..d116e1e 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs @@ -18,7 +18,7 @@ namespace GeometryTD.CustomComponent Flow.EnsureRewardSelectFormUseCaseBound(); if (!Context.SettlementFlowService.TryPrepareRewardSelection( Context.SettlementContext, - Context.CombatInRunResourceManager, + Context.EnemyDropResolver, Context.PhaseLoopRuntime.DisplayPhaseIndex, Flow.ResolveCurrentThemeType(), Context.RewardSelectFormUseCase, diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolveResult.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolveResult.cs index 853fc84..ee7339d 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolveResult.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolveResult.cs @@ -1,20 +1,22 @@ +using GeometryTD.Definition; + namespace GeometryTD.CustomComponent { internal readonly struct EnemyDropResolveResult { - public static EnemyDropResolveResult Empty => new(0, 0, false); + public static EnemyDropResolveResult Empty => new(0, 0, null); - public EnemyDropResolveResult(int coin, int gold, bool shouldRollOutGameItem) + public EnemyDropResolveResult(int coin, int gold, TowerCompItemData lootItem) { Coin = coin; Gold = gold; - ShouldRollOutGameItem = shouldRollOutGameItem; + LootItem = lootItem; } public int Coin { get; } public int Gold { get; } - public bool ShouldRollOutGameItem { get; } + public TowerCompItemData LootItem { get; } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs index 30ffb13..2f4fbd6 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs @@ -1,4 +1,8 @@ +using System; +using System.Collections.Generic; +using GameFramework.DataTable; using GeometryTD.DataTable; +using GeometryTD.Definition; using UnityEngine; using Random = UnityEngine.Random; @@ -6,6 +10,27 @@ namespace GeometryTD.CustomComponent { internal sealed class EnemyDropResolver { + private const float DropChanceBase = 0.05f; + private const float DropChancePerPhase = 0.2f; + private const float DropChanceCap = 0.2f; + private const float RarityCurveScalePhase = 30f; + + private readonly List _eligibleDropPoolBuffer = new(); + private readonly Dictionary _rarityRollWeightBuffer = new(); + + private IDataTable _drOutGameDropPool; + private IDataTable _drMuzzleComp; + private IDataTable _drBearingComp; + private IDataTable _drBaseComp; + private long _nextDropItemInstanceId = 1; + + public void Reset() + { + _eligibleDropPoolBuffer.Clear(); + _rarityRollWeightBuffer.Clear(); + _nextDropItemInstanceId = 1; + } + public EnemyDropResolveResult Resolve(in EnemyDropResolveContext context) { DREnemy enemy = context.Enemy; @@ -26,7 +51,375 @@ namespace GeometryTD.CustomComponent gold = Mathf.Max(0, enemy.DropGold); } - return new EnemyDropResolveResult(coin, gold, shouldRollOutGameItem: true); + TowerCompItemData lootItem = null; + if (ShouldRollOutGameItem(context.DisplayPhaseIndex) && + TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem)) + { + lootItem = droppedItem; + } + + return new EnemyDropResolveResult(coin, gold, lootItem); + } + + public IReadOnlyList RollSettlementRewardCandidates( + int displayPhaseIndex, + LevelThemeType themeType, + int candidateCount) + { + int resolvedCount = Mathf.Max(0, candidateCount); + if (resolvedCount <= 0) + { + return Array.Empty(); + } + + List candidates = new List(resolvedCount); + HashSet selectedPoolRowIds = new HashSet(); + int maxAttempts = Mathf.Max(resolvedCount * 6, resolvedCount); + int phaseIndex = Mathf.Max(1, displayPhaseIndex); + + int attempts = 0; + while (candidates.Count < resolvedCount && attempts < maxAttempts) + { + attempts++; + if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null) + { + break; + } + + if (!selectedPoolRowIds.Add(selectedRow.Id)) + { + continue; + } + + if (!TryBuildDropItem(selectedRow, out TowerCompItemData droppedItem) || droppedItem == null) + { + continue; + } + + candidates.Add(droppedItem); + } + + attempts = 0; + while (candidates.Count < resolvedCount && attempts < maxAttempts) + { + attempts++; + if (!TryRollOutGameItem(phaseIndex, themeType, out TowerCompItemData droppedItem) || droppedItem == null) + { + break; + } + + candidates.Add(droppedItem); + } + + return candidates; + } + + private static bool ShouldRollOutGameItem(int displayPhaseIndex) + { + int phaseIndex = Mathf.Max(1, displayPhaseIndex); + float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap); + return Random.value <= dropChance; + } + + private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem) + { + droppedItem = null; + int phaseIndex = Mathf.Max(1, displayPhaseIndex); + if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow)) + { + return false; + } + + return TryBuildDropItem(selectedRow, out droppedItem); + } + + 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/ICombatSchedulerHost.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerHost.cs index ffefcbc..ddf80ba 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerHost.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/ICombatSchedulerHost.cs @@ -12,8 +12,6 @@ namespace GeometryTD.CustomComponent bool CanEndCombat { get; } void ChangeState(CombatStateBase nextState); - bool TryConsumeCoin(int coin); - void AddCoin(int coin); bool TryEndCombatByPlayer(); bool TryDebugFail(string errorMessage); bool OnCombatFinishReturnRequested(); diff --git a/Assets/GameMain/Scripts/Entity/EntityExtension.cs b/Assets/GameMain/Scripts/Entity/EntityExtension.cs index 52ebd0e..c2afddd 100644 --- a/Assets/GameMain/Scripts/Entity/EntityExtension.cs +++ b/Assets/GameMain/Scripts/Entity/EntityExtension.cs @@ -49,16 +49,6 @@ namespace GeometryTD entityComponent.ShowEntity(typeof(BulletEntity), "Bullet", Constant.AssetPriority.BulletAsset, data); } - public static void ShowMap(this EntityComponent entityComponent, MapData data) - { - ShowMap(entityComponent, data, null); - } - - public static void ShowMap(this EntityComponent entityComponent, MapData data, string mapAssetName) - { - ShowMap(entityComponent, new MapEntityLoadContext(data, null, null), mapAssetName); - } - public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext) { ShowMap(entityComponent, loadContext, null); diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs index fe05c11..fb04bf0 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/MapEntity.cs @@ -267,11 +267,6 @@ namespace GeometryTD.Entity return loadContext; } - if (userData is MapData legacyMapData) - { - return new MapEntityLoadContext(legacyMapData, null, null); - } - return null; } @@ -440,4 +435,3 @@ namespace GeometryTD.Entity } } } -