InventoryGenerationComponent G2

This commit is contained in:
SepComet 2026-03-12 17:34:00 +08:00
parent f67888a8da
commit 53b6b261dd
14 changed files with 504 additions and 527 deletions

View File

@ -100,7 +100,7 @@ namespace GeometryTD.CustomComponent
_runtime.NodeId = nodeId; _runtime.NodeId = nodeId;
_runtime.NodeType = nodeType; _runtime.NodeType = nodeType;
_runtime.SequenceIndex = sequenceIndex; _runtime.SequenceIndex = sequenceIndex;
_runtime.EnemyDropResolver.ConfigureRunContext(runSeed, sequenceIndex); GameEntry.InventoryGeneration.ConfigureRunContext(runSeed, sequenceIndex);
_runtime.CombatRunResourceStore.InitializeForCombat(level); _runtime.CombatRunResourceStore.InitializeForCombat(level);
for (int i = 0; i < phases.Count; i++) for (int i = 0; i < phases.Count; i++)
{ {
@ -201,7 +201,7 @@ namespace GeometryTD.CustomComponent
enemy, enemy,
_runtime.PhaseLoopRuntime.DisplayPhaseIndex, _runtime.PhaseLoopRuntime.DisplayPhaseIndex,
_coordinator.ResolveCurrentThemeType()); _coordinator.ResolveCurrentThemeType());
EnemyDropResult result = _runtime.EnemyDropResolver.Resolve(context); EnemyDropResult result = GameEntry.InventoryGeneration.ResolveEnemyDrop(context);
_runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold); _runtime.CombatRunResourceStore.AddEnemyDefeatedReward(result.Coin, result.Gold);
_runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem); _runtime.CombatRunResourceStore.AddEnemyDefeatedLoot(result.LootItem);
} }

View File

@ -50,7 +50,6 @@ namespace GeometryTD.CustomComponent
_runtime.PhaseLoopRuntime.Reset(); _runtime.PhaseLoopRuntime.Reset();
_runtime.LoadSession.Reset(); _runtime.LoadSession.Reset();
_runtime.CombatRunResourceStore.Reset(); _runtime.CombatRunResourceStore.Reset();
_runtime.EnemyDropResolver.Reset();
_runtime.SettlementContext = null; _runtime.SettlementContext = null;
_runtime.CurrentLevel = null; _runtime.CurrentLevel = null;
_runtime.DidCombatWin = true; _runtime.DidCombatWin = true;

View File

@ -17,7 +17,6 @@ namespace GeometryTD.CustomComponent
public CombatLoadSession LoadSession { get; } = new(); public CombatLoadSession LoadSession { get; } = new();
public CombatEventBridge EventBridge { get; } = new(); public CombatEventBridge EventBridge { get; } = new();
public CombatRunResourceStore CombatRunResourceStore { get; } = new(); public CombatRunResourceStore CombatRunResourceStore { get; } = new();
public EnemyDropResolver EnemyDropResolver { get; } = new();
public CombatSettlementService CombatSettlementService { get; } = new(); public CombatSettlementService CombatSettlementService { get; } = new();
public EntityComponent Entity { get; set; } public EntityComponent Entity { get; set; }

View File

@ -83,19 +83,18 @@ namespace GeometryTD.CustomComponent
public bool TryPrepareRewardSelection( public bool TryPrepareRewardSelection(
CombatSettlementContext settlementContext, CombatSettlementContext settlementContext,
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 || enemyDropResolver == null || rewardSelectFormUseCase == null) if (settlementContext == null || rewardSelectFormUseCase == null)
{ {
return false; return false;
} }
IReadOnlyList<TowerCompItemData> candidateItems = enemyDropResolver.RollSettlementRewardCandidates( IReadOnlyList<TowerCompItemData> candidateItems = GameEntry.InventoryGeneration.BuildRewardCandidates(
displayPhaseIndex, displayPhaseIndex,
themeType, themeType,
RewardSelectDisplayCount); RewardSelectDisplayCount);

View File

@ -18,7 +18,6 @@ namespace GeometryTD.CustomComponent
Coordinator.EnsureRewardSelectFormUseCaseBound(); Coordinator.EnsureRewardSelectFormUseCaseBound();
if (!Runtime.CombatSettlementService.TryPrepareRewardSelection( if (!Runtime.CombatSettlementService.TryPrepareRewardSelection(
Runtime.SettlementContext, Runtime.SettlementContext,
Runtime.EnemyDropResolver,
Runtime.PhaseLoopRuntime.DisplayPhaseIndex, Runtime.PhaseLoopRuntime.DisplayPhaseIndex,
Coordinator.ResolveCurrentThemeType(), Coordinator.ResolveCurrentThemeType(),
Runtime.RewardSelectFormUseCase, Runtime.RewardSelectFormUseCase,

View File

@ -1,493 +0,0 @@
using System;
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent
{
public sealed class EnemyDropResolver
{
private const float DropChanceBase = 0.05f;
private const float DropChancePerPhase = 0.2f;
private const float DropChanceCap = 0.2f;
private const float RarityCurveScalePhase = 30f;
private readonly List<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;
private int _runSeed;
private int _nodeSequenceIndex = -1;
private int _nextDropTagOrdinal;
private int _nextRewardTagOrdinal;
public void Reset()
{
_eligibleDropPoolBuffer.Clear();
_rarityRollWeightBuffer.Clear();
_nextDropItemInstanceId = 1;
_runSeed = 0;
_nodeSequenceIndex = -1;
_nextDropTagOrdinal = 0;
_nextRewardTagOrdinal = 0;
}
public void ConfigureRunContext(int runSeed, int nodeSequenceIndex)
{
_runSeed = runSeed;
_nodeSequenceIndex = nodeSequenceIndex;
_nextDropTagOrdinal = 0;
_nextRewardTagOrdinal = 0;
}
public EnemyDropResult Resolve(in EnemyDropContext context)
{
DREnemy enemy = context.Enemy;
if (enemy == null)
{
return EnemyDropResult.Empty;
}
int coin = Mathf.Max(0, enemy.DropCoin);
int gold = 0;
float dropRate = enemy.DropPercent > 1f
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
: Mathf.Clamp01(enemy.DropPercent);
if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate)
{
gold = Mathf.Max(0, enemy.DropGold);
}
TowerCompItemData lootItem = null;
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) &&
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem))
{
lootItem = droppedItem;
}
return new EnemyDropResult(coin, gold, lootItem);
}
public IReadOnlyList<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, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem) || droppedItem == null)
{
continue;
}
candidates.Add(droppedItem);
}
attempts = 0;
while (candidates.Count < resolvedCount && attempts < maxAttempts)
{
attempts++;
if (!TryRollOutGameItem(phaseIndex, themeType, out TowerCompItemData droppedItem) || droppedItem == null)
{
break;
}
candidates.Add(droppedItem);
}
return candidates;
}
private static bool ShouldRollOutGameItem(int displayPhaseIndex)
{
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
return Random.value <= dropChance;
}
private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem)
{
droppedItem = null;
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
if (!TryPickDropPoolRow(phaseIndex, themeType, out DROutGameDropPool selectedRow))
{
return false;
}
return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem);
}
private bool TryPickDropPoolRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
{
selectedRow = null;
IDataTable<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,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
if (row == null || row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
{
return false;
}
string itemType = row.ItemType.Trim();
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildMuzzleCompItem(row, sourceType, localOrdinal, out droppedItem);
}
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildBearingCompItem(row, sourceType, localOrdinal, out droppedItem);
}
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildBaseCompItem(row, sourceType, localOrdinal, out droppedItem);
}
return false;
}
private bool TryBuildMuzzleCompItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
if (_drMuzzleComp == null)
{
return false;
}
DRMuzzleComp config = _drMuzzleComp.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
long instanceId = _nextDropItemInstanceId++;
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity);
droppedItem = new MuzzleCompItemData
{
InstanceId = instanceId,
ConfigId = config.Id,
Name = config.Name,
Rarity = rarity,
Endurance = 100f,
Constraint = config.Constraint,
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
rarity,
CreateRandomContext(sourceType, localOrdinal, config.Id)),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
};
return true;
}
private bool TryBuildBearingCompItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
if (_drBearingComp == null)
{
return false;
}
DRBearingComp config = _drBearingComp.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
long instanceId = _nextDropItemInstanceId++;
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity);
droppedItem = new BearingCompItemData
{
InstanceId = instanceId,
ConfigId = config.Id,
Name = config.Name,
Rarity = rarity,
Endurance = 100f,
Constraint = config.Constraint,
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
rarity,
CreateRandomContext(sourceType, localOrdinal, config.Id)),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
return true;
}
private bool TryBuildBaseCompItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
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;
}
long instanceId = _nextDropItemInstanceId++;
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity);
droppedItem = new BaseCompItemData
{
InstanceId = instanceId,
ConfigId = config.Id,
Name = config.Name,
Rarity = rarity,
Endurance = 100f,
Constraint = config.Constraint,
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
rarity,
CreateRandomContext(sourceType, localOrdinal, config.Id)),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};
return true;
}
private InventoryTagRandomContext CreateRandomContext(
InventoryTagSourceType sourceType,
int localOrdinal,
int configId)
{
return sourceType switch
{
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId),
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId)
};
}
private int AllocateDropTagOrdinal()
{
return _nextDropTagOrdinal++;
}
private int AllocateRewardTagOrdinal()
{
return _nextRewardTagOrdinal++;
}
}
}

View File

@ -0,0 +1,186 @@
using System.Collections.Generic;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
public sealed class DropPoolRoller
{
private const float RarityCurveScalePhase = 30f;
private readonly List<DROutGameDropPool> _eligibleRowBuffer = new();
private readonly Dictionary<RarityType, float> _rarityWeightBuffer = new();
private readonly IDataTable<DROutGameDropPool> _dropPoolTable;
public DropPoolRoller(IDataTable<DROutGameDropPool> dropPoolTable)
{
_dropPoolTable = dropPoolTable;
}
public bool TryRollRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
{
selectedRow = null;
DROutGameDropPool[] allRows = _dropPoolTable.GetAllDataRows();
if (allRows == null || allRows.Length <= 0)
{
return false;
}
CollectEligibleRows(allRows, displayPhaseIndex, themeType);
if (_eligibleRowBuffer.Count <= 0)
{
return false;
}
RarityType selectedRarity = RollRarity(displayPhaseIndex);
if (selectedRarity == RarityType.None)
{
return false;
}
int totalWeight = 0;
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
{
DROutGameDropPool row = _eligibleRowBuffer[i];
if (row.Rarity != selectedRarity)
{
continue;
}
totalWeight += Mathf.Max(1, row.Weight);
}
if (totalWeight <= 0)
{
return false;
}
int randomWeight = Random.Range(1, totalWeight + 1);
int cumulativeWeight = 0;
foreach (var row in _eligibleRowBuffer)
{
if (row.Rarity != selectedRarity)
{
continue;
}
cumulativeWeight += Mathf.Max(1, row.Weight);
if (randomWeight <= cumulativeWeight)
{
selectedRow = row;
return true;
}
}
selectedRow = _eligibleRowBuffer[_eligibleRowBuffer.Count - 1];
return selectedRow != null;
}
private void CollectEligibleRows(
DROutGameDropPool[] allRows,
int displayPhaseIndex,
LevelThemeType themeType)
{
_eligibleRowBuffer.Clear();
for (int i = 0; i < allRows.Length; i++)
{
DROutGameDropPool row = allRows[i];
if (row == null)
{
continue;
}
if (row.LevelThemeType != themeType)
{
continue;
}
if (displayPhaseIndex < row.MinPhase || displayPhaseIndex > row.MaxPhase)
{
continue;
}
_eligibleRowBuffer.Add(row);
}
}
private RarityType RollRarity(int displayPhaseIndex)
{
_rarityWeightBuffer.Clear();
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
for (int i = 0; i < _eligibleRowBuffer.Count; i++)
{
DROutGameDropPool row = _eligibleRowBuffer[i];
float curveWeight = GetRarityCurveWeight(row.Rarity, phaseT);
if (curveWeight <= 0f)
{
continue;
}
float rowWeight = Mathf.Max(1, row.Weight) * curveWeight;
if (_rarityWeightBuffer.TryGetValue(row.Rarity, out float existingWeight))
{
_rarityWeightBuffer[row.Rarity] = existingWeight + rowWeight;
}
else
{
_rarityWeightBuffer[row.Rarity] = rowWeight;
}
}
float totalWeight = 0f;
foreach (var pair in _rarityWeightBuffer)
{
totalWeight += Mathf.Max(0f, pair.Value);
}
if (totalWeight <= 0f)
{
return RarityType.None;
}
float randomWeight = Random.value * totalWeight;
float cumulativeWeight = 0f;
foreach (var pair in _rarityWeightBuffer)
{
cumulativeWeight += Mathf.Max(0f, pair.Value);
if (randomWeight <= cumulativeWeight)
{
return pair.Key;
}
}
foreach (var pair in _rarityWeightBuffer)
{
return pair.Key;
}
return RarityType.None;
}
private static float GetRarityCurveWeight(RarityType rarityType, float phaseT)
{
float hump = Mathf.Exp(-Mathf.Pow((phaseT - 0.35f) / 0.28f, 2f));
switch (rarityType)
{
case RarityType.White:
return Mathf.Max(0.05f, 0.18f + 1.25f * hump);
case RarityType.Green:
return Mathf.Max(0.05f, 0.35f + 0.55f * hump);
case RarityType.Blue:
return 0.18f + 0.55f * phaseT;
case RarityType.Purple:
return 0.05f + 0.22f * phaseT;
case RarityType.Red:
return 0.01f + 0.08f * phaseT * phaseT;
default:
return 0f;
}
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: b7dc48b97505d0242bf5846ba835f3b3 guid: 807de988c6a1431ab7b4b6d4d5a7d92f
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -3,21 +3,43 @@ using System.Collections.Generic;
using GameFramework.DataTable; using GameFramework.DataTable;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using GeometryTD.Factory;
using GeometryTD.UI; using GeometryTD.UI;
using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
using Random = UnityEngine.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
public sealed class InventoryGenerationComponent : GameFrameworkComponent public sealed class InventoryGenerationComponent : GameFrameworkComponent
{ {
private const float DropChanceBase = 0.05f;
private const float DropChancePerPhase = 0.2f;
private const float DropChanceCap = 0.2f;
private long _nextTempInstanceId = 1000000; private long _nextTempInstanceId = 1000000;
private int _runSeed;
private int _nodeSequenceIndex = -1;
private int _nextDropTagOrdinal;
private int _nextRewardTagOrdinal;
private readonly List<DRShopPrice> _shopPriceRows = new(); private readonly List<DRShopPrice> _shopPriceRows = new();
private IDataTable<DRShopPrice> _shopPriceTable; private IDataTable<DRShopPrice> _shopPriceTable;
private IDataTable<DROutGameDropPool> _dropPoolTable;
private IDataTable<DRMuzzleComp> _muzzleCompTable; private IDataTable<DRMuzzleComp> _muzzleCompTable;
private IDataTable<DRBearingComp> _bearingCompTable; private IDataTable<DRBearingComp> _bearingCompTable;
private IDataTable<DRBaseComp> _baseCompTable; private IDataTable<DRBaseComp> _baseCompTable;
private ShopGoodsBuilder _shopGoodsBuilder; private ShopGoodsBuilder _shopGoodsBuilder;
private DropPoolRoller _dropPoolRoller;
private RewardCandidateBuilder _rewardCandidateBuilder;
public void ConfigureRunContext(int runSeed, int sequenceIndex)
{
_runSeed = runSeed;
_nodeSequenceIndex = sequenceIndex;
_nextDropTagOrdinal = 0;
_nextRewardTagOrdinal = 0;
}
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1) public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
{ {
@ -27,8 +49,31 @@ namespace GeometryTD.CustomComponent
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context) public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
{ {
throw new NotImplementedException( DREnemy enemy = context.Enemy;
"InventoryGenerationComponent.ResolveEnemyDrop() will be implemented in G2."); 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<TowerCompItemData> BuildRewardCandidates( public IReadOnlyList<TowerCompItemData> BuildRewardCandidates(
@ -36,8 +81,12 @@ namespace GeometryTD.CustomComponent
LevelThemeType themeType, LevelThemeType themeType,
int candidateCount) int candidateCount)
{ {
throw new NotImplementedException( RewardCandidateBuilder rewardCandidateBuilder = EnsureRewardCandidateBuilder();
"InventoryGenerationComponent.BuildRewardCandidates() will be implemented in G2."); return rewardCandidateBuilder.BuildCandidates(
displayPhaseIndex,
themeType,
candidateCount,
BuildRewardCandidateItem);
} }
private void EnsureShopTables() private void EnsureShopTables()
@ -94,5 +143,148 @@ namespace GeometryTD.CustomComponent
{ {
return _nextTempInstanceId++; return _nextTempInstanceId++;
} }
private void EnsureDropTables()
{
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
{
throw new InvalidOperationException(
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables.");
}
}
private DropPoolRoller EnsureDropPoolRoller()
{
EnsureDropTables();
_dropPoolRoller ??= new DropPoolRoller(_dropPoolTable);
return _dropPoolRoller;
}
private RewardCandidateBuilder EnsureRewardCandidateBuilder()
{
_rewardCandidateBuilder ??= new RewardCandidateBuilder(EnsureDropPoolRoller());
return _rewardCandidateBuilder;
}
private static bool ShouldRollOutGameItem(int displayPhaseIndex)
{
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
return Random.value <= dropChance;
}
private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem)
{
droppedItem = null;
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
if (!dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
{
return false;
}
return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem);
}
private bool TryBuildDropItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
if (row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
{
return false;
}
string itemType = row.ItemType.Trim();
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
{
DRMuzzleComp config = _muzzleCompTable.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
droppedItem = ComponentItemFactory.CreateMuzzle(
config,
AllocateTempInstanceId(),
row.Rarity,
CreateRandomContext(sourceType, localOrdinal, config.Id));
return true;
}
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
{
DRBearingComp config = _bearingCompTable.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
droppedItem = ComponentItemFactory.CreateBearing(
config,
AllocateTempInstanceId(),
row.Rarity,
CreateRandomContext(sourceType, localOrdinal, config.Id));
return true;
}
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
{
DRBaseComp config = _baseCompTable.GetDataRow(row.ItemId);
if (config == null)
{
return false;
}
droppedItem = ComponentItemFactory.CreateBase(
config,
AllocateTempInstanceId(),
row.Rarity,
CreateRandomContext(sourceType, localOrdinal, config.Id));
return true;
}
return false;
}
private int AllocateDropTagOrdinal()
{
return _nextDropTagOrdinal++;
}
private int AllocateRewardTagOrdinal()
{
return _nextRewardTagOrdinal++;
}
private TowerCompItemData BuildRewardCandidateItem(DROutGameDropPool row)
{
if (!TryBuildDropItem(row, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem))
{
return null;
}
return droppedItem;
}
private InventoryTagRandomContext CreateRandomContext(
InventoryTagSourceType sourceType,
int localOrdinal,
int configId)
{
return sourceType switch
{
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId),
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId)
};
}
} }
} }

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityEngine;
namespace GeometryTD.CustomComponent
{
public sealed class RewardCandidateBuilder
{
private readonly DropPoolRoller _dropPoolRoller;
public RewardCandidateBuilder(DropPoolRoller dropPoolRoller)
{
_dropPoolRoller = dropPoolRoller;
}
public IReadOnlyList<TowerCompItemData> BuildCandidates(
int displayPhaseIndex,
LevelThemeType themeType,
int candidateCount,
Func<DROutGameDropPool, TowerCompItemData> buildRewardItem)
{
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 (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
{
break;
}
if (!selectedPoolRowIds.Add(selectedRow.Id))
{
continue;
}
TowerCompItemData candidate = buildRewardItem(selectedRow);
if (candidate == null)
{
continue;
}
candidates.Add(candidate);
}
attempts = 0;
while (candidates.Count < resolvedCount && attempts < maxAttempts)
{
attempts++;
if (!_dropPoolRoller.TryRollRow(phaseIndex, themeType, out DROutGameDropPool selectedRow) || selectedRow == null)
{
break;
}
TowerCompItemData candidate = buildRewardItem(selectedRow);
if (candidate == null)
{
continue;
}
candidates.Add(candidate);
}
return candidates;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5fe3f5eaa2194d13b4f6f2ff4b2f7de2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -186,10 +186,9 @@ namespace GeometryTD.Tests.EditMode
bool prepared = new CombatSettlementService().TryPrepareRewardSelection( bool prepared = new CombatSettlementService().TryPrepareRewardSelection(
settlementContext, settlementContext,
null,
displayPhaseIndex: 1, displayPhaseIndex: 1,
themeType: LevelThemeType.Plain, themeType: LevelThemeType.Plain,
rewardSelectFormUseCase: new RewardSelectFormUseCase(), rewardSelectFormUseCase: null,
onRewardSelected: _ => { }, onRewardSelected: _ => { },
onGiveUp: null); onGiveUp: null);

View File

@ -111,10 +111,10 @@
| 状态 | ID | 任务 | 目标 | | 状态 | ID | 任务 | 目标 |
|-----|-------|-----------------------------------|------------| |-----|-------|-----------------------------------|------------|
| [ ] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 | | [x] | G1-01 | 新增 `InventoryGenerationComponent` | 建立统一组件产出入口 |
| [ ] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 | | [x] | G1-02 | 新增 `ComponentItemFactory` | 收口组件实例构造 |
| [ ] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 | | [x] | G1-03 | 新增 `ShopGoodsBuilder` | 收口商店货物生成 |
| [ ] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 | | [x] | G1-04 | 让 `ShopFormUseCase` 改用组件入口 | 商店不再自己生成组件 |
### G1 验收标准 ### G1 验收标准

View File

@ -1,6 +1,6 @@
# CombatNode 设计规范(开发约束) # CombatNode 设计规范(开发约束)
最后更新2026-03-06 最后更新2026-03-12
## 1. 适用范围与目标 ## 1. 适用范围与目标
@ -296,22 +296,25 @@
- `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。 - `Coin / BaseHp` 变化事件同时携带“当前值”和“变化量”。
- `Gold` 只是结算累计值,不要求战斗内实时事件驱动。 - `Gold` 只是结算累计值,不要求战斗内实时事件驱动。
### 4.5 EnemyDropResolver ### 4.5 InventoryGenerationComponent
文件:`Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs` 文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
目标职责: 目标职责:
- 只负责敌人死亡后的掉落判定。 - 作为局外组件产出的统一运行时入口。
- 输入: - 对外提供:
- `DREnemy` - `BuildShopGoods(...)`
- 当前阶段索引 - `ResolveEnemyDrop(...)`
- 当前主题或关卡上下文 - `BuildRewardCandidates(...)`
- 输出: - 在内部编排:
- 掉落结果对象(`coin / gold / loot` - `DropPoolRoller`
- `RewardCandidateBuilder`
- `ComponentItemFactory`
约束: 约束:
- 不直接修改资源状态。 - `CombatNode` 域不直接持有或复制组件产出规则。
- 不直接读取 `CombatNodeComponent`、`MapEntity`、`EnemyManager` 内部状态。 - `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。
- `InventoryGenerationComponent` 负责组件实例生成、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。
### 4.6 IPhaseEndCondition ### 4.6 IPhaseEndCondition
@ -406,7 +409,7 @@ Boss 识别规则:
- `OnEnemyDefeated(DREnemy enemy)` - `OnEnemyDefeated(DREnemy enemy)`
- `OnEnemyReachedBase(DREnemy enemy)` - `OnEnemyReachedBase(DREnemy enemy)`
- `CombatScheduler` 公共层负责处理敌人事件的通用副作用: - `CombatScheduler` 公共层负责处理敌人事件的通用副作用:
- 击杀:调用 `EnemyDropResolver`,再调用局内资源管理器入账。 - 击杀:调用 `GameEntry.InventoryGeneration.ResolveEnemyDrop(...)`,再调用局内资源管理器入账。
- 到家:调用局内资源管理器扣减 `BaseHp` - 到家:调用局内资源管理器扣减 `BaseHp`
约束: 约束:
@ -467,7 +470,7 @@ Boss 识别规则:
1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。 1. `CombatScheduler` 只做状态机管理与共享运行时收口,不继续吸收具体业务细节。
2. `CombatNodeComponent` 不再持有战斗内资源真值。 2. `CombatNodeComponent` 不再持有战斗内资源真值。
3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots``CombatRunResourceStore` 为唯一真值来源。 3. 局内 `Coin / Gold / BaseHp / Loot Backpack / BuildTowerSnapshots``CombatRunResourceStore` 为唯一真值来源。
4. 敌人死亡掉落判定以 `EnemyDropResolver` 为唯一判定入口 4. 组件产出规则以 `InventoryGenerationComponent` 为统一运行时入口;战斗掉落与奖励候选都通过它生成
5. 存活敌人数与 `HasAliveBoss``EnemyLifecycleTracker` 为唯一真值来源。 5. 存活敌人数与 `HasAliveBoss``EnemyLifecycleTracker` 为唯一真值来源。
6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。 6. Phase 运行时信息与统一结束标记以 `PhaseLoopRuntime` 为唯一真值来源。
7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。 7. `PhaseEndType` 的退出条件以 `IPhaseEndCondition` 实现类为唯一判定入口。
@ -498,7 +501,11 @@ Boss 识别规则:
### 10.3 新增敌人掉落规则 ### 10.3 新增敌人掉落规则
优先改 `EnemyDropResolver`,不要在 `EnemyManager` 或状态类里直接计算掉落。 优先改 `InventoryGenerationComponent` 及其下层规则模块,不要在 `EnemyManager`、`CombatScheduler` 或状态类里直接计算掉落。
### 10.3.x 新增奖励候选规则
优先改 `InventoryGenerationComponent`、`RewardCandidateBuilder` 或 `DropPoolRoller`,不要在结算状态链里复制一套候选生成规则。
### 10.4 新增战斗内资源或建塔快照规则 ### 10.4 新增战斗内资源或建塔快照规则