refactor 8:

- 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。
This commit is contained in:
SepComet 2026-03-07 21:01:03 +08:00
parent 703fd6f540
commit 380f901c1a
14 changed files with 485 additions and 508 deletions

View File

@ -319,7 +319,7 @@ namespace GeometryTD.CustomComponent
return true; return true;
} }
_combatScheduler.OnInit(); _combatScheduler.OnInit(OnCombatEndedByScheduler);
_runtimeInitialized = true; _runtimeInitialized = true;
return true; return true;
} }

View File

@ -1,38 +1,19 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.CustomEvent; using GeometryTD.CustomEvent;
using GeometryTD.CustomUtility; using GeometryTD.CustomUtility;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
internal sealed class CombatInRunResourceManager 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<DROutGameDropPool> _eligibleDropPoolBuffer = new();
private readonly List<TowerStatsData> _buildTowerStatsSnapshot = new(); private readonly List<TowerStatsData> _buildTowerStatsSnapshot = new();
private readonly Dictionary<RarityType, float> _rarityRollWeightBuffer = new(); private readonly List<TowerItemData> _participantTowerSnapshot = new();
private readonly BackpackInventoryData _rewardInventory = new(); private readonly BackpackInventoryData _rewardInventory = new();
private IDataTable<DROutGameDropPool> _drOutGameDropPool; private BackpackInventoryData _combatInventorySnapshot = new();
private IDataTable<DRMuzzleComp> _drMuzzleComp;
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
private long _nextDropItemInstanceId = 1;
private bool _isCombatActive; private bool _isCombatActive;
public int CurrentCoin { get; private set; } public int CurrentCoin { get; private set; }
@ -54,7 +35,7 @@ namespace GeometryTD.CustomComponent
MaxBaseHp = Mathf.Max(0, level.BaseHp); MaxBaseHp = Mathf.Max(0, level.BaseHp);
CurrentBaseHp = MaxBaseHp; CurrentBaseHp = MaxBaseHp;
CurrentCoin = Mathf.Max(0, level.StartCoin); CurrentCoin = Mathf.Max(0, level.StartCoin);
CacheBuildTowerStatsSnapshot(); CacheCombatSnapshots();
_isCombatActive = true; _isCombatActive = true;
} }
@ -72,13 +53,14 @@ namespace GeometryTD.CustomComponent
GainedCoin = 0; GainedCoin = 0;
GainedGold = 0; GainedGold = 0;
_buildTowerStatsSnapshot.Clear(); _buildTowerStatsSnapshot.Clear();
_participantTowerSnapshot.Clear();
_combatInventorySnapshot = new BackpackInventoryData();
_rewardInventory.Gold = 0; _rewardInventory.Gold = 0;
_rewardInventory.MuzzleComponents.Clear(); _rewardInventory.MuzzleComponents.Clear();
_rewardInventory.BearingComponents.Clear(); _rewardInventory.BearingComponents.Clear();
_rewardInventory.BaseComponents.Clear(); _rewardInventory.BaseComponents.Clear();
_rewardInventory.Towers.Clear(); _rewardInventory.Towers.Clear();
_rewardInventory.ParticipantTowerInstanceIds.Clear(); _rewardInventory.ParticipantTowerInstanceIds.Clear();
_nextDropItemInstanceId = 1;
} }
public BackpackInventoryData GetRewardInventorySnapshot() public BackpackInventoryData GetRewardInventorySnapshot()
@ -86,6 +68,26 @@ namespace GeometryTD.CustomComponent
return InventoryCloneUtility.CloneInventory(_rewardInventory); return InventoryCloneUtility.CloneInventory(_rewardInventory);
} }
public BackpackInventoryData GetCombatInventorySnapshot()
{
return InventoryCloneUtility.CloneInventory(_combatInventorySnapshot);
}
public IReadOnlyList<TowerItemData> GetParticipantTowerSnapshot()
{
List<TowerItemData> snapshot = new List<TowerItemData>(_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) public bool TryConsumeCoin(int coin)
{ {
int requiredCoin = Mathf.Max(0, coin); int requiredCoin = Mathf.Max(0, coin);
@ -188,9 +190,23 @@ namespace GeometryTD.CustomComponent
_rewardInventory.Gold += gold; _rewardInventory.Gold += gold;
} }
private void CacheBuildTowerStatsSnapshot() public void AddEnemyDefeatedLoot(TowerCompItemData droppedItem)
{
if (!_isCombatActive || droppedItem == null)
{
return;
}
AppendRewardItem(droppedItem);
}
private void CacheCombatSnapshots()
{ {
_buildTowerStatsSnapshot.Clear(); _buildTowerStatsSnapshot.Clear();
_participantTowerSnapshot.Clear();
_combatInventorySnapshot = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: new BackpackInventoryData();
if (GameEntry.PlayerInventory == null) if (GameEntry.PlayerInventory == null)
{ {
return; return;
@ -205,14 +221,18 @@ namespace GeometryTD.CustomComponent
for (int i = 0; i < towers.Count; i++) for (int i = 0; i < towers.Count; i++)
{ {
TowerItemData tower = towers[i]; TowerItemData tower = towers[i];
if (tower?.Stats == null) if (tower == null)
{ {
continue; continue;
} }
_participantTowerSnapshot.Add(InventoryCloneUtility.CloneTower(tower));
if (tower.Stats != null)
{
_buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats)); _buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats));
} }
} }
}
private void FireCoinChangedEvent(int deltaCoin) private void FireCoinChangedEvent(int deltaCoin)
{ {
@ -234,427 +254,20 @@ namespace GeometryTD.CustomComponent
GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp)); GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp));
} }
/// <summary> private void AppendRewardItem(TowerCompItemData droppedItem)
/// 击杀敌人时的掉落入口。
/// displayPhaseIndex 会影响:
/// 1) 总掉落概率(由 DropChanceBase / DropChancePerPhase / DropChanceCap 决定)
/// 2) 稀有度倾向(通过 phaseT 进入 RollRarity
/// themeType 会按关卡主题过滤掉落池LevelThemeType 不匹配直接不参与)。
/// </summary>
public bool TryRollOutGameItemDrop(int displayPhaseIndex, LevelThemeType themeType)
{ {
int phaseIndex = Mathf.Max(1, displayPhaseIndex); switch (droppedItem)
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;
}
/// <summary>
/// 直接抽取一个具体掉落物(不走前面的总掉率门槛)。
/// 但 displayPhaseIndex 和 themeType 仍会通过掉落池过滤、稀有度加权影响结果。
/// </summary>
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<TowerCompItemData> RollSettlementRewardCandidates(
int displayPhaseIndex,
LevelThemeType themeType,
int candidateCount)
{
int resolvedCount = Mathf.Max(0, candidateCount);
if (resolvedCount <= 0)
{
return Array.Empty<TowerCompItemData>();
}
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
HashSet<int> selectedPoolRowIds = new HashSet<int>();
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;
case BearingCompItemData bearingComp:
_rewardInventory.BearingComponents.Add(InventoryCloneUtility.CloneBearingComp(bearingComp));
break;
case BaseCompItemData baseComp:
_rewardInventory.BaseComponents.Add(InventoryCloneUtility.CloneBaseComp(baseComp));
break; 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;
}
/// <summary>
/// 两阶段加权抽样:
/// 1) 先按主题 + 阶段区间过滤候选行
/// 2) 先抽稀有度(稀有度权重 = 同稀有度所有行的 row.Weight * 稀有度曲线权重 之和)
/// 3) 在该稀有度内再按 row.Weight 抽具体行
/// </summary>
private bool TryPickDropPoolRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
{
selectedRow = null;
IDataTable<DROutGameDropPool> 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;
}
/// <summary>
/// 在过滤后的候选行里抽稀有度。
/// 实际稀有度权重公式:
/// rarityWeight = sum(max(1, row.Weight) * GetRarityCurveWeight(row.Rarity, phaseT))
/// 其中 phaseT = clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase)。
/// </summary>
private RarityType RollRarity(int displayPhaseIndex, List<DROutGameDropPool> 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;
}
/// <summary>
/// 返回各稀有度在当前阶段的曲线权重。
/// 返回值越大,该稀有度在 RollRarity 中越容易被抽到。
/// phaseT 取值范围为 [0,1]
/// - White/Green钟形趋势前中期权重更高
/// - Blue/Purple随阶段近似线性上升
/// - Red随阶段二次上升前期很低后期明显抬升
/// </summary>
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<DROutGameDropPool> EnsureOutGameDropPoolTable()
{
if (_drOutGameDropPool != null)
{
return _drOutGameDropPool;
}
_drOutGameDropPool = GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
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<DRMuzzleComp>();
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<TagType>(),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
};
return true;
}
private bool TryBuildBearingCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
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<TagType>(),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
return true;
}
private bool TryBuildBaseCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
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<TagType>(),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};
return true;
} }
} }
} }

View File

@ -233,6 +233,12 @@ namespace GeometryTD.CustomComponent
return false; 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 mapAssetName = level.Id.ToString();
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName); string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist) if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
@ -242,13 +248,11 @@ namespace GeometryTD.CustomComponent
} }
_loadingMapEntityId = _entity.GenerateSerialId(); _loadingMapEntityId = _entity.GenerateSerialId();
MapData resolvedMapData = mapLoadContext?.InitialMapData != null MapData resolvedMapData = mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero);
? mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero)
: new MapData(_loadingMapEntityId, level.Id, Vector3.zero);
MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext( MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext(
resolvedMapData, resolvedMapData,
mapLoadContext?.TryConsumeCoin, mapLoadContext.TryConsumeCoin,
mapLoadContext?.AddCoin); mapLoadContext.AddCoin);
_entity.ShowMap(resolvedLoadContext, mapAssetName); _entity.ShowMap(resolvedLoadContext, mapAssetName);
return true; return true;
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.CustomEvent; using GeometryTD.CustomEvent;
using GeometryTD.DataTable; using GeometryTD.DataTable;
@ -39,8 +40,10 @@ namespace GeometryTD.CustomComponent
public int GainedCoin => _context.CombatInRunResourceManager.GainedCoin; public int GainedCoin => _context.CombatInRunResourceManager.GainedCoin;
public int GainedGold => _context.CombatInRunResourceManager.GainedGold; public int GainedGold => _context.CombatInRunResourceManager.GainedGold;
public void OnInit() public void OnInit(Action<bool> combatEndedCallback)
{ {
_context.CombatEndedCallback = combatEndedCallback;
if (!_initialized) if (!_initialized)
{ {
_context.Entity = GameEntry.Entity; _context.Entity = GameEntry.Entity;
@ -142,6 +145,7 @@ namespace GeometryTD.CustomComponent
_context.EventBridge.Unbind(); _context.EventBridge.Unbind();
_context.CombatFinishFormUseCase = null; _context.CombatFinishFormUseCase = null;
_context.RewardSelectFormUseCase = null; _context.RewardSelectFormUseCase = null;
_context.CombatEndedCallback = null;
_context.Entity = null; _context.Entity = null;
_initialized = false; _initialized = false;
@ -187,15 +191,7 @@ namespace GeometryTD.CustomComponent
_flowCoordinator.ResolveCurrentThemeType()); _flowCoordinator.ResolveCurrentThemeType());
EnemyDropResolveResult result = _context.EnemyDropResolver.Resolve(context); EnemyDropResolveResult result = _context.EnemyDropResolver.Resolve(context);
_context.CombatInRunResourceManager.AddEnemyDefeatedReward(result.Coin, result.Gold); _context.CombatInRunResourceManager.AddEnemyDefeatedReward(result.Coin, result.Gold);
_context.CombatInRunResourceManager.AddEnemyDefeatedLoot(result.LootItem);
if (!result.ShouldRollOutGameItem)
{
return;
}
_context.CombatInRunResourceManager.TryRollOutGameItemDrop(
context.DisplayPhaseIndex,
context.ThemeType);
} }
public bool OnCombatFinishReturnRequested() public bool OnCombatFinishReturnRequested()

View File

@ -49,6 +49,7 @@ namespace GeometryTD.CustomComponent
_context.PhaseLoopRuntime.Reset(); _context.PhaseLoopRuntime.Reset();
_context.LoadSession.Reset(); _context.LoadSession.Reset();
_context.CombatInRunResourceManager.Reset(); _context.CombatInRunResourceManager.Reset();
_context.EnemyDropResolver.Reset();
_context.SettlementContext = null; _context.SettlementContext = null;
_context.CurrentLevel = null; _context.CurrentLevel = null;
_context.IsFinishAsVictory = true; _context.IsFinishAsVictory = true;
@ -164,11 +165,6 @@ namespace GeometryTD.CustomComponent
return _context.CurrentLevel.LevelThemeType; return _context.CurrentLevel.LevelThemeType;
} }
if (GameEntry.CombatNode != null)
{
return GameEntry.CombatNode.CurrentThemeType;
}
return LevelThemeType.None; return LevelThemeType.None;
} }
@ -177,16 +173,6 @@ namespace GeometryTD.CustomComponent
return _context.CombatInRunResourceManager.ApplyBaseDamage(damage); 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() public int GetCurrentBaseHp()
{ {
return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp); return Mathf.Max(0, _context.CombatInRunResourceManager.CurrentBaseHp);
@ -261,7 +247,7 @@ namespace GeometryTD.CustomComponent
_context.IsCompleted = true; _context.IsCompleted = true;
_context.CurrentState = null; _context.CurrentState = null;
_context.CombatInRunResourceManager.MarkCombatEnded(); _context.CombatInRunResourceManager.MarkCombatEnded();
GameEntry.CombatNode?.OnCombatEndedByScheduler(succeeded); _context.CombatEndedCallback?.Invoke(succeeded);
} }
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Entity; using GeometryTD.Entity;
@ -23,6 +24,7 @@ namespace GeometryTD.CustomComponent
public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; } public CombatFinishFormUseCase CombatFinishFormUseCase { get; set; }
public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; } public RewardSelectFormUseCase RewardSelectFormUseCase { get; set; }
public CombatStateBase CurrentState { get; set; } public CombatStateBase CurrentState { get; set; }
public Action<bool> CombatEndedCallback { get; set; }
public bool IsFinishAsVictory { get; set; } = true; public bool IsFinishAsVictory { get; set; } = true;
public bool IsCompleted { get; set; } public bool IsCompleted { get; set; }
public bool NodeEnterFired { get; set; } public bool NodeEnterFired { get; set; }

View File

@ -87,19 +87,19 @@ namespace GeometryTD.CustomComponent
public bool TryPrepareRewardSelection( public bool TryPrepareRewardSelection(
CombatSettlementContext settlementContext, CombatSettlementContext settlementContext,
CombatInRunResourceManager resourceManager, EnemyDropResolver enemyDropResolver,
int displayPhaseIndex, int displayPhaseIndex,
LevelThemeType themeType, LevelThemeType themeType,
RewardSelectFormUseCase rewardSelectFormUseCase, RewardSelectFormUseCase rewardSelectFormUseCase,
Action<RewardSelectItemRawData> onRewardSelected, Action<RewardSelectItemRawData> onRewardSelected,
Action onGiveUp) Action onGiveUp)
{ {
if (settlementContext == null || resourceManager == null || rewardSelectFormUseCase == null) if (settlementContext == null || enemyDropResolver == null || rewardSelectFormUseCase == null)
{ {
return false; return false;
} }
IReadOnlyList<TowerCompItemData> candidateItems = resourceManager.RollSettlementRewardCandidates( IReadOnlyList<TowerCompItemData> candidateItems = enemyDropResolver.RollSettlementRewardCandidates(
displayPhaseIndex, displayPhaseIndex,
themeType, themeType,
RewardSelectDisplayCount); RewardSelectDisplayCount);

View File

@ -60,13 +60,12 @@ namespace GeometryTD.CustomComponent
position: Vector3.zero, position: Vector3.zero,
initialCoin: Context.CombatInRunResourceManager.CurrentCoin, initialCoin: Context.CombatInRunResourceManager.CurrentCoin,
buildTowerStatsSnapshot: buildTowerStatsSnapshot, buildTowerStatsSnapshot: buildTowerStatsSnapshot,
inventorySnapshot: GameEntry.PlayerInventory != null inventorySnapshot: Context.CombatInRunResourceManager.GetCombatInventorySnapshot(),
? GameEntry.PlayerInventory.GetInventorySnapshot() participantTowerSnapshot: Context.CombatInRunResourceManager.GetParticipantTowerSnapshot());
: null, return new MapEntityLoadContext(
participantTowerSnapshot: GameEntry.PlayerInventory != null mapData,
? GameEntry.PlayerInventory.GetParticipantTowerSnapshot() Context.CombatInRunResourceManager.TryConsumeCoin,
: null); Context.CombatInRunResourceManager.AddCoin);
return new MapEntityLoadContext(mapData, Flow.TryConsumeCoin, Flow.AddCoin);
} }
} }
} }

View File

@ -18,7 +18,7 @@ namespace GeometryTD.CustomComponent
Flow.EnsureRewardSelectFormUseCaseBound(); Flow.EnsureRewardSelectFormUseCaseBound();
if (!Context.SettlementFlowService.TryPrepareRewardSelection( if (!Context.SettlementFlowService.TryPrepareRewardSelection(
Context.SettlementContext, Context.SettlementContext,
Context.CombatInRunResourceManager, Context.EnemyDropResolver,
Context.PhaseLoopRuntime.DisplayPhaseIndex, Context.PhaseLoopRuntime.DisplayPhaseIndex,
Flow.ResolveCurrentThemeType(), Flow.ResolveCurrentThemeType(),
Context.RewardSelectFormUseCase, Context.RewardSelectFormUseCase,

View File

@ -1,20 +1,22 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
internal readonly struct EnemyDropResolveResult 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; Coin = coin;
Gold = gold; Gold = gold;
ShouldRollOutGameItem = shouldRollOutGameItem; LootItem = lootItem;
} }
public int Coin { get; } public int Coin { get; }
public int Gold { get; } public int Gold { get; }
public bool ShouldRollOutGameItem { get; } public TowerCompItemData LootItem { get; }
} }
} }

View File

@ -1,4 +1,8 @@
using System;
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = UnityEngine.Random; using Random = UnityEngine.Random;
@ -6,6 +10,27 @@ namespace GeometryTD.CustomComponent
{ {
internal sealed class EnemyDropResolver 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<DROutGameDropPool> _eligibleDropPoolBuffer = new();
private readonly Dictionary<RarityType, float> _rarityRollWeightBuffer = new();
private IDataTable<DROutGameDropPool> _drOutGameDropPool;
private IDataTable<DRMuzzleComp> _drMuzzleComp;
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
private long _nextDropItemInstanceId = 1;
public void Reset()
{
_eligibleDropPoolBuffer.Clear();
_rarityRollWeightBuffer.Clear();
_nextDropItemInstanceId = 1;
}
public EnemyDropResolveResult Resolve(in EnemyDropResolveContext context) public EnemyDropResolveResult Resolve(in EnemyDropResolveContext context)
{ {
DREnemy enemy = context.Enemy; DREnemy enemy = context.Enemy;
@ -26,7 +51,375 @@ namespace GeometryTD.CustomComponent
gold = Mathf.Max(0, enemy.DropGold); 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<TowerCompItemData> RollSettlementRewardCandidates(
int displayPhaseIndex,
LevelThemeType themeType,
int candidateCount)
{
int resolvedCount = Mathf.Max(0, candidateCount);
if (resolvedCount <= 0)
{
return Array.Empty<TowerCompItemData>();
}
List<TowerCompItemData> candidates = new List<TowerCompItemData>(resolvedCount);
HashSet<int> selectedPoolRowIds = new HashSet<int>();
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<DROutGameDropPool> 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<DROutGameDropPool> 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<DROutGameDropPool> EnsureOutGameDropPoolTable()
{
if (_drOutGameDropPool != null)
{
return _drOutGameDropPool;
}
_drOutGameDropPool = GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
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<DRMuzzleComp>();
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<TagType>(),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
};
return true;
}
private bool TryBuildBearingCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
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<TagType>(),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
return true;
}
private bool TryBuildBaseCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
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<TagType>(),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};
return true;
} }
} }
} }

View File

@ -12,8 +12,6 @@ namespace GeometryTD.CustomComponent
bool CanEndCombat { get; } bool CanEndCombat { get; }
void ChangeState(CombatStateBase nextState); void ChangeState(CombatStateBase nextState);
bool TryConsumeCoin(int coin);
void AddCoin(int coin);
bool TryEndCombatByPlayer(); bool TryEndCombatByPlayer();
bool TryDebugFail(string errorMessage); bool TryDebugFail(string errorMessage);
bool OnCombatFinishReturnRequested(); bool OnCombatFinishReturnRequested();

View File

@ -49,16 +49,6 @@ namespace GeometryTD
entityComponent.ShowEntity(typeof(BulletEntity), "Bullet", Constant.AssetPriority.BulletAsset, data); 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) public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext)
{ {
ShowMap(entityComponent, loadContext, null); ShowMap(entityComponent, loadContext, null);

View File

@ -267,11 +267,6 @@ namespace GeometryTD.Entity
return loadContext; return loadContext;
} }
if (userData is MapData legacyMapData)
{
return new MapEntityLoadContext(legacyMapData, null, null);
}
return null; return null;
} }
@ -440,4 +435,3 @@ namespace GeometryTD.Entity
} }
} }
} }