using System; using System.Collections.Generic; using GameFramework.DataTable; using GeometryTD.DataTable; using GeometryTD.Definition; using UnityEngine; using Random = UnityEngine.Random; namespace GeometryTD.CustomComponent { internal sealed class 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 readonly Dictionary _tagMinRarityByTag = new(); private IDataTable _drOutGameDropPool; private IDataTable _drMuzzleComp; private IDataTable _drBearingComp; private IDataTable _drBaseComp; private IDataTable _drTag; private long _nextDropItemInstanceId = 1; public void Reset() { _eligibleDropPoolBuffer.Clear(); _rarityRollWeightBuffer.Clear(); _tagMinRarityByTag.Clear(); _nextDropItemInstanceId = 1; } 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, 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 EnsureTagMinRarityLookup() { if (_tagMinRarityByTag.Count > 0) { return true; } _drTag ??= GameEntry.DataTable.GetDataTable(); if (_drTag == null) { return false; } DRTag[] rows = _drTag.GetAllDataRows(); for (int i = 0; i < rows.Length; i++) { DRTag row = rows[i]; if (row == null || row.Id <= 0) { continue; } TagType tagType = (TagType)row.Id; _tagMinRarityByTag[tagType] = row.MinRarity; } return _tagMinRarityByTag.Count > 0; } 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; } if (!EnsureTagMinRarityLookup()) { 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 = InventoryTagRuleService.ResolveComponentTags( config.PossibleTag, rarity, InventoryTagSourceType.Drop, instanceId, config.Id, _tagMinRarityByTag), 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; } if (!EnsureTagMinRarityLookup()) { 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 = InventoryTagRuleService.ResolveComponentTags( config.PossibleTag, rarity, InventoryTagSourceType.Drop, instanceId, config.Id, _tagMinRarityByTag), 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; } if (!EnsureTagMinRarityLookup()) { 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 = InventoryTagRuleService.ResolveComponentTags( config.PossibleTag, rarity, InventoryTagSourceType.Drop, instanceId, config.Id, _tagMinRarityByTag), AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty(), AttackPropertyType = config.AttackPropertyType }; return true; } } }