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;
}
_combatScheduler.OnInit();
_combatScheduler.OnInit(OnCombatEndedByScheduler);
_runtimeInitialized = true;
return true;
}

View File

@ -1,38 +1,19 @@
using System;
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.CustomEvent;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent
{
internal sealed class CombatInRunResourceManager
{
// 每次击杀敌人后,进入“掉组件判定”的概率:
// chance = clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0, DropChanceCap)
private const float DropChanceBase = 0.05f;
private const float DropChancePerPhase = 0.2f;
private const float DropChanceCap = 0.2f;
// 用于把阶段映射到 [0,1] 的稀有度曲线输入:
// phaseT = clamp01((phaseIndex - 1) / RarityCurveScalePhase)
// 该值越大,稀有度随阶段提升的节奏越慢。
private const float RarityCurveScalePhase = 30f;
private readonly List<DROutGameDropPool> _eligibleDropPoolBuffer = 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 IDataTable<DROutGameDropPool> _drOutGameDropPool;
private IDataTable<DRMuzzleComp> _drMuzzleComp;
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
private long _nextDropItemInstanceId = 1;
private BackpackInventoryData _combatInventorySnapshot = new();
private bool _isCombatActive;
public int CurrentCoin { get; private set; }
@ -54,7 +35,7 @@ namespace GeometryTD.CustomComponent
MaxBaseHp = Mathf.Max(0, level.BaseHp);
CurrentBaseHp = MaxBaseHp;
CurrentCoin = Mathf.Max(0, level.StartCoin);
CacheBuildTowerStatsSnapshot();
CacheCombatSnapshots();
_isCombatActive = true;
}
@ -72,13 +53,14 @@ namespace GeometryTD.CustomComponent
GainedCoin = 0;
GainedGold = 0;
_buildTowerStatsSnapshot.Clear();
_participantTowerSnapshot.Clear();
_combatInventorySnapshot = new BackpackInventoryData();
_rewardInventory.Gold = 0;
_rewardInventory.MuzzleComponents.Clear();
_rewardInventory.BearingComponents.Clear();
_rewardInventory.BaseComponents.Clear();
_rewardInventory.Towers.Clear();
_rewardInventory.ParticipantTowerInstanceIds.Clear();
_nextDropItemInstanceId = 1;
}
public BackpackInventoryData GetRewardInventorySnapshot()
@ -86,6 +68,26 @@ namespace GeometryTD.CustomComponent
return InventoryCloneUtility.CloneInventory(_rewardInventory);
}
public BackpackInventoryData GetCombatInventorySnapshot()
{
return InventoryCloneUtility.CloneInventory(_combatInventorySnapshot);
}
public IReadOnlyList<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)
{
int requiredCoin = Mathf.Max(0, coin);
@ -188,9 +190,23 @@ namespace GeometryTD.CustomComponent
_rewardInventory.Gold += gold;
}
private void CacheBuildTowerStatsSnapshot()
public void AddEnemyDefeatedLoot(TowerCompItemData droppedItem)
{
if (!_isCombatActive || droppedItem == null)
{
return;
}
AppendRewardItem(droppedItem);
}
private void CacheCombatSnapshots()
{
_buildTowerStatsSnapshot.Clear();
_participantTowerSnapshot.Clear();
_combatInventorySnapshot = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: new BackpackInventoryData();
if (GameEntry.PlayerInventory == null)
{
return;
@ -205,14 +221,18 @@ namespace GeometryTD.CustomComponent
for (int i = 0; i < towers.Count; i++)
{
TowerItemData tower = towers[i];
if (tower?.Stats == null)
if (tower == null)
{
continue;
}
_participantTowerSnapshot.Add(InventoryCloneUtility.CloneTower(tower));
if (tower.Stats != null)
{
_buildTowerStatsSnapshot.Add(InventoryCloneUtility.CloneTowerStats(tower.Stats));
}
}
}
private void FireCoinChangedEvent(int deltaCoin)
{
@ -234,427 +254,20 @@ namespace GeometryTD.CustomComponent
GameEntry.Event.Fire(this, CombatBaseHpChangedEventArgs.Create(CurrentBaseHp, deltaBaseHp));
}
/// <summary>
/// 击杀敌人时的掉落入口。
/// displayPhaseIndex 会影响:
/// 1) 总掉落概率(由 DropChanceBase / DropChancePerPhase / DropChanceCap 决定)
/// 2) 稀有度倾向(通过 phaseT 进入 RollRarity
/// themeType 会按关卡主题过滤掉落池LevelThemeType 不匹配直接不参与)。
/// </summary>
public bool TryRollOutGameItemDrop(int displayPhaseIndex, LevelThemeType themeType)
private void AppendRewardItem(TowerCompItemData droppedItem)
{
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
if (Random.value > dropChance)
{
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)
switch (droppedItem)
{
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;
}
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;
}
if (mapLoadContext?.InitialMapData == null)
{
errorMessage = "CombatLoadSession map load failed. MapEntityLoadContext is missing initial map data.";
return false;
}
string mapAssetName = level.Id.ToString();
string mapAssetPath = AssetUtility.GetLevelMapAsset(mapAssetName);
if (GameEntry.Resource.HasAsset(mapAssetPath) == HasAssetResult.NotExist)
@ -242,13 +248,11 @@ namespace GeometryTD.CustomComponent
}
_loadingMapEntityId = _entity.GenerateSerialId();
MapData resolvedMapData = mapLoadContext?.InitialMapData != null
? mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero)
: new MapData(_loadingMapEntityId, level.Id, Vector3.zero);
MapData resolvedMapData = mapLoadContext.InitialMapData.CloneForEntity(_loadingMapEntityId, Vector3.zero);
MapEntityLoadContext resolvedLoadContext = new MapEntityLoadContext(
resolvedMapData,
mapLoadContext?.TryConsumeCoin,
mapLoadContext?.AddCoin);
mapLoadContext.TryConsumeCoin,
mapLoadContext.AddCoin);
_entity.ShowMap(resolvedLoadContext, mapAssetName);
return true;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,22 @@
using GeometryTD.Definition;
namespace GeometryTD.CustomComponent
{
internal readonly struct EnemyDropResolveResult
{
public static EnemyDropResolveResult Empty => new(0, 0, false);
public static EnemyDropResolveResult Empty => new(0, 0, null);
public EnemyDropResolveResult(int coin, int gold, bool shouldRollOutGameItem)
public EnemyDropResolveResult(int coin, int gold, TowerCompItemData lootItem)
{
Coin = coin;
Gold = gold;
ShouldRollOutGameItem = shouldRollOutGameItem;
LootItem = lootItem;
}
public int Coin { get; }
public int Gold { get; }
public bool ShouldRollOutGameItem { get; }
public TowerCompItemData LootItem { get; }
}
}

View File

@ -1,4 +1,8 @@
using System;
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
using Random = UnityEngine.Random;
@ -6,6 +10,27 @@ namespace GeometryTD.CustomComponent
{
internal sealed class EnemyDropResolver
{
private const float DropChanceBase = 0.05f;
private const float DropChancePerPhase = 0.2f;
private const float DropChanceCap = 0.2f;
private const float RarityCurveScalePhase = 30f;
private readonly List<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)
{
DREnemy enemy = context.Enemy;
@ -26,7 +51,375 @@ namespace GeometryTD.CustomComponent
gold = Mathf.Max(0, enemy.DropGold);
}
return new EnemyDropResolveResult(coin, gold, shouldRollOutGameItem: true);
TowerCompItemData lootItem = null;
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) &&
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem))
{
lootItem = droppedItem;
}
return new EnemyDropResolveResult(coin, gold, lootItem);
}
public IReadOnlyList<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; }
void ChangeState(CombatStateBase nextState);
bool TryConsumeCoin(int coin);
void AddCoin(int coin);
bool TryEndCombatByPlayer();
bool TryDebugFail(string errorMessage);
bool OnCombatFinishReturnRequested();

View File

@ -49,16 +49,6 @@ namespace GeometryTD
entityComponent.ShowEntity(typeof(BulletEntity), "Bullet", Constant.AssetPriority.BulletAsset, data);
}
public static void ShowMap(this EntityComponent entityComponent, MapData data)
{
ShowMap(entityComponent, data, null);
}
public static void ShowMap(this EntityComponent entityComponent, MapData data, string mapAssetName)
{
ShowMap(entityComponent, new MapEntityLoadContext(data, null, null), mapAssetName);
}
public static void ShowMap(this EntityComponent entityComponent, MapEntityLoadContext loadContext)
{
ShowMap(entityComponent, loadContext, null);

View File

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