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 BackpackInventoryData _rewardInventory = new(); private IDataTable _drOutGameDropPool; private IDataTable _drMuzzleComp; private IDataTable _drBearingComp; private IDataTable _drBaseComp; private long _nextDropItemInstanceId = 1; private bool _isCombatActive; public int CurrentCoin { get; private set; } public int CurrentGold => _isCombatActive ? GainedGold : 0; public int CurrentBaseHp { get; private set; } public int MaxBaseHp { get; private set; } public int CurrentBuildTowerCount => _isCombatActive ? _buildTowerStatsSnapshot.Count : 0; public int GainedCoin { get; private set; } public int GainedGold { get; private set; } public void InitializeForCombat(DRLevel level) { Reset(); if (level == null) { return; } MaxBaseHp = Mathf.Max(0, level.BaseHp); CurrentBaseHp = MaxBaseHp; CurrentCoin = Mathf.Max(0, level.StartCoin); CacheBuildTowerStatsSnapshot(); _isCombatActive = true; } public void MarkCombatEnded() { _isCombatActive = false; } public void Reset() { _isCombatActive = false; CurrentCoin = 0; CurrentBaseHp = 0; MaxBaseHp = 0; GainedCoin = 0; GainedGold = 0; _buildTowerStatsSnapshot.Clear(); _rewardInventory.Gold = 0; _rewardInventory.MuzzleComponents.Clear(); _rewardInventory.BearingComponents.Clear(); _rewardInventory.BaseComponents.Clear(); _rewardInventory.Towers.Clear(); _rewardInventory.ParticipantTowerInstanceIds.Clear(); _nextDropItemInstanceId = 1; } public BackpackInventoryData GetRewardInventorySnapshot() { return InventoryCloneUtility.CloneInventory(_rewardInventory); } public bool TryConsumeCoin(int coin) { int requiredCoin = Mathf.Max(0, coin); if (requiredCoin <= 0) { return true; } if (!_isCombatActive || CurrentCoin < requiredCoin) { return false; } CurrentCoin -= requiredCoin; FireCoinChangedEvent(-requiredCoin); return true; } public void AddCoin(int coin) { int gainedCoin = Mathf.Max(0, coin); if (!_isCombatActive || gainedCoin <= 0) { return; } CurrentCoin += gainedCoin; FireCoinChangedEvent(gainedCoin); } public int ApplyBaseDamage(int damage) { int resolvedDamage = Mathf.Max(0, damage); if (!_isCombatActive || resolvedDamage <= 0) { return CurrentBaseHp; } int previousBaseHp = CurrentBaseHp; CurrentBaseHp = Mathf.Max(0, CurrentBaseHp - resolvedDamage); int deltaBaseHp = CurrentBaseHp - previousBaseHp; if (deltaBaseHp != 0) { FireBaseHpChangedEvent(deltaBaseHp); } return CurrentBaseHp; } public bool TryGetBuildTowerStats(int buildIndex, out TowerStatsData stats) { stats = null; if (!_isCombatActive || buildIndex < 0 || buildIndex >= _buildTowerStatsSnapshot.Count) { return false; } TowerStatsData sourceStats = _buildTowerStatsSnapshot[buildIndex]; if (sourceStats == null) { return false; } stats = InventoryCloneUtility.CloneTowerStats(sourceStats); return stats != null; } public void AddEnemyDefeatedReward(int gainedCoin, int gainedGold) { if (!_isCombatActive) { return; } int coin = Mathf.Max(0, gainedCoin); int gold = Mathf.Max(0, gainedGold); GainedCoin += coin; GainedGold += gold; if (coin > 0) { CurrentCoin += coin; FireCoinChangedEvent(coin); } if (gold > 0) { _rewardInventory.Gold += gold; } } public void AddSettlementGold(int gainedGold) { int gold = Mathf.Max(0, gainedGold); if (gold <= 0) { return; } GainedGold += gold; _rewardInventory.Gold += gold; } private void CacheBuildTowerStatsSnapshot() { _buildTowerStatsSnapshot.Clear(); if (GameEntry.PlayerInventory == null) { return; } IReadOnlyList towers = GameEntry.PlayerInventory.GetParticipantTowerSnapshot(); if (towers == null || towers.Count <= 0) { return; } for (int i = 0; i < towers.Count; i++) { TowerItemData tower = towers[i]; if (tower?.Stats == null) { continue; } _buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats)); } } private void FireCoinChangedEvent(int deltaCoin) { if (!_isCombatActive) { return; } GameEntry.Event.Fire(this, CombatCoinChangedEventArgs.Create(CurrentCoin, deltaCoin)); } private void FireBaseHpChangedEvent(int deltaBaseHp) { if (!_isCombatActive) { return; } 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) { int phaseIndex = Mathf.Max(1, displayPhaseIndex); float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap); if (Random.value > dropChance) { 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) { 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; } /// /// 两阶段加权抽样: /// 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; } } }