diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index 736b165..8f4060a 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -100,7 +100,7 @@ namespace GeometryTD.CustomComponent _runtime.NodeId = nodeId; _runtime.NodeType = nodeType; _runtime.SequenceIndex = sequenceIndex; - _runtime.EnemyDropResolver.ConfigureRunContext(runSeed, sequenceIndex); + GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex); _runtime.CombatRunResourceStore.InitializeForCombat(level); for (int i = 0; i < phases.Count; i++) { @@ -201,7 +201,7 @@ namespace GeometryTD.CustomComponent enemy, _runtime.PhaseLoopRuntime.DisplayPhaseIndex, _coordinator.ResolveCurrentThemeType()); - EnemyDropResult result = _runtime.EnemyDropResolver.Resolve(context); + EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(context); _runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold); _runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem); } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs index 33e13f0..f3157b7 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs @@ -50,7 +50,6 @@ namespace GeometryTD.CustomComponent _runtime.PhaseLoopRuntime.Reset(); _runtime.LoadSession.Reset(); _runtime.CombatRunResourceStore.Reset(); - _runtime.EnemyDropResolver.Reset(); _runtime.SettlementContext = null; _runtime.CurrentLevel = null; _runtime.DidCombatWin = true; diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs index 0e3f16c..e97a315 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs @@ -17,7 +17,6 @@ namespace GeometryTD.CustomComponent public CombatLoadSession LoadSession { get; } = new(); public CombatEventBridge EventBridge { get; } = new(); public CombatRunResourceStore CombatRunResourceStore { get; } = new(); - public EnemyDropResolver EnemyDropResolver { get; } = new(); public CombatSettlementService CombatSettlementService { get; } = new(); public EntityComponent Entity { get; set; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs index f3c9d06..333ac1e 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs @@ -83,19 +83,18 @@ namespace GeometryTD.CustomComponent public bool TryPrepareRewardSelection( CombatSettlementContext settlementContext, - EnemyDropResolver enemyDropResolver, int displayPhaseIndex, LevelThemeType themeType, RewardSelectFormUseCase rewardSelectFormUseCase, Action onRewardSelected, Action onGiveUp) { - if (settlementContext == null || enemyDropResolver == null || rewardSelectFormUseCase == null) + if (settlementContext == null || rewardSelectFormUseCase == null) { return false; } - IReadOnlyList candidateItems = enemyDropResolver.RollSettlementRewardCandidates( + IReadOnlyList candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates( displayPhaseIndex, themeType, RewardSelectDisplayCount); diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs index b2ae171..c85e061 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatStates/CombatRewardSelectionState.cs @@ -18,7 +18,6 @@ namespace GeometryTD.CustomComponent Coordinator.EnsureRewardSelectFormUseCaseBound(); if (!Runtime.CombatSettlementService.TryPrepareRewardSelection( Runtime.SettlementContext, - Runtime.EnemyDropResolver, Runtime.PhaseLoopRuntime.DisplayPhaseIndex, Coordinator.ResolveCurrentThemeType(), Runtime.RewardSelectFormUseCase, diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs deleted file mode 100644 index 96bde56..0000000 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs +++ /dev/null @@ -1,493 +0,0 @@ -using System; -using System.Collections.Generic; -using GameFramework.DataTable; -using GeometryTD.DataTable; -using GeometryTD.Definition; -using UnityEngine; -using Random = UnityEngine.Random; - -namespace GeometryTD.CustomComponent -{ - public 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; - private int _runSeed; - private int _nodeSequenceIndex = -1; - private int _nextDropTagOrdinal; - private int _nextRewardTagOrdinal; - - public void Reset() - { - _eligibleDropPoolBuffer.Clear(); - _rarityRollWeightBuffer.Clear(); - _nextDropItemInstanceId = 1; - _runSeed = 0; - _nodeSequenceIndex = -1; - _nextDropTagOrdinal = 0; - _nextRewardTagOrdinal = 0; - } - - public void ConfigureRunContext(int runSeed, int nodeSequenceIndex) - { - _runSeed = runSeed; - _nodeSequenceIndex = nodeSequenceIndex; - _nextDropTagOrdinal = 0; - _nextRewardTagOrdinal = 0; - } - - public EnemyDropResult Resolve(in EnemyDropContext context) - { - DREnemy enemy = context.Enemy; - if (enemy == null) - { - return EnemyDropResult.Empty; - } - - int coin = Mathf.Max(0, enemy.DropCoin); - int gold = 0; - - float dropRate = enemy.DropPercent > 1f - ? Mathf.Clamp01(enemy.DropPercent * 0.01f) - : Mathf.Clamp01(enemy.DropPercent); - - if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate) - { - gold = Mathf.Max(0, enemy.DropGold); - } - - TowerCompItemData lootItem = null; - if (ShouldRollOutGameItem(context.DisplayPhaseIndex) && - TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem)) - { - lootItem = droppedItem; - } - - return new EnemyDropResult(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, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), 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, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), 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, - InventoryTagSourceType sourceType, - int localOrdinal, - 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, sourceType, localOrdinal, out droppedItem); - } - - if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase)) - { - return TryBuildBearingCompItem(row, sourceType, localOrdinal, out droppedItem); - } - - if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase)) - { - return TryBuildBaseCompItem(row, sourceType, localOrdinal, out droppedItem); - } - - return false; - } - - private bool TryBuildMuzzleCompItem( - DROutGameDropPool row, - InventoryTagSourceType sourceType, - int localOrdinal, - 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; - } - - long instanceId = _nextDropItemInstanceId++; - RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity); - droppedItem = new MuzzleCompItemData - { - InstanceId = instanceId, - ConfigId = config.Id, - Name = config.Name, - Rarity = rarity, - Endurance = 100f, - Constraint = config.Constraint, - Tags = ComponentTagGenerationService.ResolveComponentTags( - config.PossibleTag, - rarity, - CreateRandomContext(sourceType, localOrdinal, config.Id)), - AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty(), - DamageRandomRate = config.DamageRandomRate, - AttackMethodType = config.AttackMethodType - }; - return true; - } - - private bool TryBuildBearingCompItem( - DROutGameDropPool row, - InventoryTagSourceType sourceType, - int localOrdinal, - 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; - } - - long instanceId = _nextDropItemInstanceId++; - RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity); - droppedItem = new BearingCompItemData - { - InstanceId = instanceId, - ConfigId = config.Id, - Name = config.Name, - Rarity = rarity, - Endurance = 100f, - Constraint = config.Constraint, - Tags = ComponentTagGenerationService.ResolveComponentTags( - config.PossibleTag, - rarity, - CreateRandomContext(sourceType, localOrdinal, config.Id)), - 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, - InventoryTagSourceType sourceType, - int localOrdinal, - 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; - } - - long instanceId = _nextDropItemInstanceId++; - RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity); - droppedItem = new BaseCompItemData - { - InstanceId = instanceId, - ConfigId = config.Id, - Name = config.Name, - Rarity = rarity, - Endurance = 100f, - Constraint = config.Constraint, - Tags = ComponentTagGenerationService.ResolveComponentTags( - config.PossibleTag, - rarity, - CreateRandomContext(sourceType, localOrdinal, config.Id)), - AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty(), - AttackPropertyType = config.AttackPropertyType - }; - return true; - } - - private InventoryTagRandomContext CreateRandomContext( - InventoryTagSourceType sourceType, - int localOrdinal, - int configId) - { - return sourceType switch - { - InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId), - _ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId) - }; - } - - private int AllocateDropTagOrdinal() - { - return _nextDropTagOrdinal++; - } - - private int AllocateRewardTagOrdinal() - { - return _nextRewardTagOrdinal++; - } - } -} diff --git a/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs new file mode 100644 index 0000000..9964a04 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using GameFramework.DataTable; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomComponent +{ + public sealed class DropPoolRoller + { + private const float RarityCurveScalePhase = 30f; + + private readonly List _eligibleRowBuffer = new(); + private readonly Dictionary _rarityWeightBuffer = new(); + private readonly IDataTable _dropPoolTable; + + public DropPoolRoller(IDataTable dropPoolTable) + { + _dropPoolTable = dropPoolTable; + } + + public bool TryRollRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow) + { + selectedRow = null; + + DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows(); + if (allRows == null || allRows.Length <= 0) + { + return false; + } + + CollectEligibleRows(allRows, displayPhaseIndex, themeType); + if (_eligibleRowBuffer.Count <= 0) + { + return false; + } + + RarityType selectedRarity = RollRarity(displayPhaseIndex); + if (selectedRarity == RarityType.None) + { + return false; + } + + int totalWeight = 0; + for (int i = 0; i < _eligibleRowBuffer.Count; i++) + { + DROutGameDropPool row = _eligibleRowBuffer[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; + foreach (var row in _eligibleRowBuffer) + { + if (row.Rarity != selectedRarity) + { + continue; + } + + cumulativeWeight += Mathf.Max(1, row.Weight); + if (randomWeight <= cumulativeWeight) + { + selectedRow = row; + return true; + } + } + + selectedRow = _eligibleRowBuffer[_eligibleRowBuffer.Count - 1]; + return selectedRow != null; + } + + private void CollectEligibleRows( + DROutGameDropPool[] allRows, + int displayPhaseIndex, + LevelThemeType themeType) + { + _eligibleRowBuffer.Clear(); + + 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; + } + + _eligibleRowBuffer.Add(row); + } + } + + private RarityType RollRarity(int displayPhaseIndex) + { + _rarityWeightBuffer.Clear(); + float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase); + + for (int i = 0; i < _eligibleRowBuffer.Count; i++) + { + DROutGameDropPool row = _eligibleRowBuffer[i]; + float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT); + if (curveWeight <= 0f) + { + continue; + } + + float rowWeight = Mathf.Max(1, row.Weight) * curveWeight; + if (_rarityWeightBuffer.TryGetValue(row.Rarity, out float existingWeight)) + { + _rarityWeightBuffer[row.Rarity] = existingWeight + rowWeight; + } + else + { + _rarityWeightBuffer[row.Rarity] = rowWeight; + } + } + + float totalWeight = 0f; + foreach (var pair in _rarityWeightBuffer) + { + totalWeight += Mathf.Max(0f, pair.Value); + } + + if (totalWeight <= 0f) + { + return RarityType.None; + } + + float randomWeight = Random.value * totalWeight; + float cumulativeWeight = 0f; + foreach (var pair in _rarityWeightBuffer) + { + cumulativeWeight += Mathf.Max(0f, pair.Value); + if (randomWeight <= cumulativeWeight) + { + return pair.Key; + } + } + + foreach (var pair in _rarityWeightBuffer) + { + 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; + } + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs.meta b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs.meta similarity index 83% rename from Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs.meta rename to Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs.meta index 31c6478..98a71f9 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs.meta +++ b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: b7dc48b97505d0242bf5846ba835f3b3 +guid: 807de988c6a1431ab7b4b6d4d5a7d92f MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs index cb7416b..6799808 100644 --- a/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs @@ -3,21 +3,43 @@ using System.Collections.Generic; using GameFramework.DataTable; using GeometryTD.DataTable; using GeometryTD.Definition; +using GeometryTD.Factory; using GeometryTD.UI; +using UnityEngine; using UnityGameFramework.Runtime; +using Random = UnityEngine.Random; namespace GeometryTD.CustomComponent { public sealed class InventoryGenerationComponent : GameFrameworkComponent { + private const float DropChanceBase = 0.05f; + private const float DropChancePerPhase = 0.2f; + private const float DropChanceCap = 0.2f; + private long _nextTempInstanceId = 1000000; + private int _runSeed; + private int _nodeSequenceIndex = -1; + private int _nextDropTagOrdinal; + private int _nextRewardTagOrdinal; private readonly List _shopPriceRows = new(); private IDataTable _shopPriceTable; + private IDataTable _dropPoolTable; private IDataTable _muzzleCompTable; private IDataTable _bearingCompTable; private IDataTable _baseCompTable; private ShopGoodsBuilder _shopGoodsBuilder; + private DropPoolRoller _dropPoolRoller; + private RewardCandidateBuilder _rewardCandidateBuilder; + + public void ConfigureRunContext(int runSeed, int sequenceIndex) + { + _runSeed = runSeed; + _nodeSequenceIndex = sequenceIndex; + _nextDropTagOrdinal = 0; + _nextRewardTagOrdinal = 0; + } public List BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1) { @@ -27,8 +49,31 @@ namespace GeometryTD.CustomComponent public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context) { - throw new NotImplementedException( - "InventoryGenerationComponent.ResolveEnemyDrop() will be implemented in G2."); + DREnemy enemy = context.Enemy; + if (enemy == null) + { + return EnemyDropResult.Empty; + } + + int coin = Mathf.Max(0, enemy.DropCoin); + int gold = 0; + float dropRate = enemy.DropPercent > 1f + ? Mathf.Clamp01(enemy.DropPercent * 0.01f) + : Mathf.Clamp01(enemy.DropPercent); + + if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate) + { + gold = Mathf.Max(0, enemy.DropGold); + } + + TowerCompItemData lootItem = null; + if (ShouldRollOutGameItem(context.DisplayPhaseIndex) && + TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem)) + { + lootItem = droppedItem; + } + + return new EnemyDropResult(coin, gold, lootItem); } public IReadOnlyList BuildRewardCandidates( @@ -36,8 +81,12 @@ namespace GeometryTD.CustomComponent LevelThemeType themeType, int candidateCount) { - throw new NotImplementedException( - "InventoryGenerationComponent.BuildRewardCandidates() will be implemented in G2."); + RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder(); + return rewardCandidateBuilder.BuildCandidates( + displayPhaseIndex, + themeType, + candidateCount, + BuildRewardCandidateItem); } private void EnsureShopTables() @@ -94,5 +143,148 @@ namespace GeometryTD.CustomComponent { return _nextTempInstanceId++; } + + private void EnsureDropTables() + { + _dropPoolTable ??= GameEntry.DataTable.GetDataTable(); + _muzzleCompTable ??= GameEntry.DataTable.GetDataTable(); + _bearingCompTable ??= GameEntry.DataTable.GetDataTable(); + _baseCompTable ??= GameEntry.DataTable.GetDataTable(); + + if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null) + { + throw new InvalidOperationException( + "InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables."); + } + } + + private DropPoolRoller EnsureDropPoolRoller() + { + EnsureDropTables(); + _dropPoolRoller ??= new DropPoolRoller(_dropPoolTable); + return _dropPoolRoller; + } + + private RewardCandidateBuilder EnsureRewardCandidateBuilder() + { + _rewardCandidateBuilder ??= new RewardCandidateBuilder(EnsureDropPoolRoller()); + return _rewardCandidateBuilder; + } + + 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; + DropPoolRoller dropPoolRoller = EnsureDropPoolRoller(); + int phaseIndex = Mathf.Max(1, displayPhaseIndex); + if (!dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null) + { + return false; + } + + return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem); + } + + private bool TryBuildDropItem( + DROutGameDropPool row, + InventoryTagSourceType sourceType, + int localOrdinal, + out TowerCompItemData droppedItem) + { + droppedItem = null; + if (row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType)) + { + return false; + } + + string itemType = row.ItemType.Trim(); + if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase)) + { + DRMuzzleComp config = _muzzleCompTable.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = ComponentItemFactory.CreateMuzzle( + config, + AllocateTempInstanceId(), + row.Rarity, + CreateRandomContext(sourceType, localOrdinal, config.Id)); + return true; + } + + if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase)) + { + DRBearingComp config = _bearingCompTable.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = ComponentItemFactory.CreateBearing( + config, + AllocateTempInstanceId(), + row.Rarity, + CreateRandomContext(sourceType, localOrdinal, config.Id)); + return true; + } + + if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase)) + { + DRBaseComp config = _baseCompTable.GetDataRow(row.ItemId); + if (config == null) + { + return false; + } + + droppedItem = ComponentItemFactory.CreateBase( + config, + AllocateTempInstanceId(), + row.Rarity, + CreateRandomContext(sourceType, localOrdinal, config.Id)); + return true; + } + + return false; + } + + private int AllocateDropTagOrdinal() + { + return _nextDropTagOrdinal++; + } + + private int AllocateRewardTagOrdinal() + { + return _nextRewardTagOrdinal++; + } + + private TowerCompItemData BuildRewardCandidateItem(DROutGameDropPool row) + { + if (!TryBuildDropItem(row, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem)) + { + return null; + } + + return droppedItem; + } + + private InventoryTagRandomContext CreateRandomContext( + InventoryTagSourceType sourceType, + int localOrdinal, + int configId) + { + return sourceType switch + { + InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId), + _ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId) + }; + } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs new file mode 100644 index 0000000..e3abe08 --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using GeometryTD.DataTable; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.CustomComponent +{ + public sealed class RewardCandidateBuilder + { + private readonly DropPoolRoller _dropPoolRoller; + + public RewardCandidateBuilder(DropPoolRoller dropPoolRoller) + { + _dropPoolRoller = dropPoolRoller; + } + + public IReadOnlyList BuildCandidates( + int displayPhaseIndex, + LevelThemeType themeType, + int candidateCount, + Func buildRewardItem) + { + 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 (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null) + { + break; + } + + if (!selectedPoolRowIds.Add(selectedRow.Id)) + { + continue; + } + + TowerCompItemData candidate = buildRewardItem(selectedRow); + if (candidate == null) + { + continue; + } + + candidates.Add(candidate); + } + + attempts = 0; + while (candidates.Count < resolvedCount && attempts < maxAttempts) + { + attempts++; + if (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null) + { + break; + } + + TowerCompItemData candidate = buildRewardItem(selectedRow); + if (candidate == null) + { + continue; + } + + candidates.Add(candidate); + } + + return candidates; + } + } +} diff --git a/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs.meta b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs.meta new file mode 100644 index 0000000..9ae602a --- /dev/null +++ b/Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5fe3f5eaa2194d13b4f6f2ff4b2f7de2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/EditMode/CombatSettlementServiceTests.cs b/Assets/Tests/EditMode/CombatSettlementServiceTests.cs index be1dc70..0786d1f 100644 --- a/Assets/Tests/EditMode/CombatSettlementServiceTests.cs +++ b/Assets/Tests/EditMode/CombatSettlementServiceTests.cs @@ -186,10 +186,9 @@ namespace GeometryTD.Tests.EditMode bool prepared = new CombatSettlementService().TryPrepareRewardSelection( settlementContext, - null, displayPhaseIndex: 1, themeType: LevelThemeType.Plain, - rewardSelectFormUseCase: new RewardSelectFormUseCase(), + rewardSelectFormUseCase: null, onRewardSelected: _ => { }, onGiveUp: null); diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index a6acd59..3e4d2f0 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -111,10 +111,10 @@ | 状态 | ID | 任务 | 目标 | |-----|-------|-----------------------------------|------------| -| [ ] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 | -| [ ] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 | -| [ ] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 | -| [ ] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 | +| [x] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 | +| [x] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 | +| [x] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 | +| [x] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 | ### G1 验收标准 diff --git a/docs/CombatNodeArchitecture.md b/docs/CombatNodeArchitecture.md index 4c474ab..f2ce6f1 100644 --- a/docs/CombatNodeArchitecture.md +++ b/docs/CombatNodeArchitecture.md @@ -1,6 +1,6 @@ # CombatNode 设计规范(开发约束) -最后更新:2026-03-06 +最后更新:2026-03-12 ## 1. 适用范围与目标 @@ -296,22 +296,25 @@ - `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。 - `Gold` 只是结算累计值,不要求战斗内实时事件驱动。 -### 4.5 EnemyDropResolver +### 4.5 InventoryGenerationComponent -文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs` +文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs` 目标职责: -- 只负责敌人死亡后的掉落判定。 -- 输入: - - `DREnemy` - - 当前阶段索引 - - 当前主题或关卡上下文 -- 输出: - - 掉落结果对象(`coin / gold / loot`) +- 作为局外组件产出的统一运行时入口。 +- 对外提供: + - `BuildShopGoods(...)` + - `ResolveEnemyDrop(...)` + - `BuildRewardCandidates(...)` +- 在内部编排: + - `DropPoolRoller` + - `RewardCandidateBuilder` + - `ComponentItemFactory` 约束: -- 不直接修改资源状态。 -- 不直接读取 `CombatNodeComponent`、`MapEntity`、`EnemyManager` 内部状态。 +- `CombatNode` 域不直接持有或复制组件产出规则。 +- `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。 +- `InventoryGenerationComponent` 负责组件实例生成、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。 ### 4.6 IPhaseEndCondition @@ -406,7 +409,7 @@ Boss 识别规则: - `OnEnemyDefeated(DREnemy enemy)` - `OnEnemyReachedBase(DREnemy enemy)` - `CombatScheduler` 公共层负责处理敌人事件的通用副作用: - - 击杀:调用 `EnemyDropResolver`,再调用局内资源管理器入账。 + - 击杀:调用 `GameEntry.InventoryGeneration.ResolveEnemyDrop(...)`,再调用局内资源管理器入账。 - 到家:调用局内资源管理器扣减 `BaseHp`。 约束: @@ -467,7 +470,7 @@ Boss 识别规则: 1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。 2. `CombatNodeComponent` 不再持有战斗内资源真值。 3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots` 以 `CombatRunResourceStore` 为唯一真值来源。 -4. 敌人死亡掉落判定以 `EnemyDropResolver` 为唯一判定入口。 +4. 组件产出规则以 `InventoryGenerationComponent` 为统一运行时入口;战斗掉落与奖励候选都通过它生成。 5. 存活敌人数与 `HasAliveBoss` 以 `EnemyLifecycleTracker` 为唯一真值来源。 6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。 7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。 @@ -498,7 +501,11 @@ Boss 识别规则: ### 10.3 新增敌人掉落规则 -优先改 `EnemyDropResolver`,不要在 `EnemyManager` 或状态类里直接计算掉落。 +优先改 `InventoryGenerationComponent` 及其下层规则模块,不要在 `EnemyManager`、`CombatScheduler` 或状态类里直接计算掉落。 + +### 10.3.x 新增奖励候选规则 + +优先改 `InventoryGenerationComponent`、`RewardCandidateBuilder` 或 `DropPoolRoller`,不要在结算状态链里复制一套候选生成规则。 ### 10.4 新增战斗内资源或建塔快照规则