InventoryGenerationComponent + TagRegistryComponent 收口

This commit is contained in:
SepComet 2026-03-13 11:06:14 +08:00
parent 0c92fd9886
commit aa44170d56
23 changed files with 967 additions and 251 deletions

View File

@ -26,6 +26,8 @@ public partial class GameEntry
public static InventoryGenerationComponent InventoryGeneration { get; private set; } public static InventoryGenerationComponent InventoryGeneration { get; private set; }
public static TagRegistryComponent TagRegistry { get; private set; }
private static void InitCustomComponents() private static void InitCustomComponents()
{ {
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>(); BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
@ -38,5 +40,6 @@ public partial class GameEntry
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>(); ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>(); SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>();
InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>(); InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>();
TagRegistry = UnityGameFramework.Runtime.GameEntry.GetComponent<TagRegistryComponent>();
} }
} }

View File

@ -3,6 +3,7 @@ using GameFramework.DataTable;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
@ -22,6 +23,7 @@ namespace GeometryTD.CustomComponent
public bool TryRollRow( public bool TryRollRow(
int displayPhaseIndex, int displayPhaseIndex,
LevelThemeType themeType, LevelThemeType themeType,
Random random,
out DROutGameDropPool selectedRow, out DROutGameDropPool selectedRow,
out RarityType selectedRarity) out RarityType selectedRarity)
{ {
@ -40,7 +42,7 @@ namespace GeometryTD.CustomComponent
return false; return false;
} }
selectedRarity = RollRarity(displayPhaseIndex); selectedRarity = RollRarity(displayPhaseIndex, random);
if (selectedRarity == RarityType.None) if (selectedRarity == RarityType.None)
{ {
return false; return false;
@ -71,7 +73,7 @@ namespace GeometryTD.CustomComponent
return false; return false;
} }
int randomWeight = Random.Range(1, totalWeight + 1); int randomWeight = random.Next(1, totalWeight + 1);
int cumulativeWeight = 0; int cumulativeWeight = 0;
foreach (var row in _eligibleRowBuffer) foreach (var row in _eligibleRowBuffer)
{ {
@ -127,7 +129,7 @@ namespace GeometryTD.CustomComponent
} }
} }
private RarityType RollRarity(int displayPhaseIndex) private RarityType RollRarity(int displayPhaseIndex, Random random)
{ {
_rarityWeightBuffer.Clear(); _rarityWeightBuffer.Clear();
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase); float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
@ -178,7 +180,7 @@ namespace GeometryTD.CustomComponent
return RarityType.None; return RarityType.None;
} }
float randomWeight = Random.value * totalWeight; float randomWeight = (float)(random.NextDouble() * totalWeight);
float cumulativeWeight = 0f; float cumulativeWeight = 0f;
foreach (var pair in _rarityWeightBuffer) foreach (var pair in _rarityWeightBuffer)
{ {

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using GameFramework.DataTable; using GameFramework.DataTable;
using GeometryTD.DataTable; using GeometryTD.DataTable;
@ -7,21 +6,16 @@ using GeometryTD.Factory;
using GeometryTD.UI; using GeometryTD.UI;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
using Random = UnityEngine.Random; using Random = System.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 int _runSeed; private int _runSeed;
private int _nodeSequenceIndex = -1; private int _nodeSequenceIndex = -1;
private int _nextDropTagOrdinal; private int _nextDropOrdinal;
private int _nextRewardTagOrdinal; private int _nextRewardOrdinal;
private readonly List<DRShopPrice> _shopPriceRows = new(); private readonly List<DRShopPrice> _shopPriceRows = new();
private IDataTable<DRShopPrice> _shopPriceTable; private IDataTable<DRShopPrice> _shopPriceTable;
@ -32,19 +26,20 @@ namespace GeometryTD.CustomComponent
private ShopGoodsBuilder _shopGoodsBuilder; private ShopGoodsBuilder _shopGoodsBuilder;
private DropPoolRoller _dropPoolRoller; private DropPoolRoller _dropPoolRoller;
private RewardCandidateBuilder _rewardCandidateBuilder; private RewardCandidateBuilder _rewardCandidateBuilder;
private OutGameDropItemBuilder _outGameDropItemBuilder;
public void ConfigureRunContext(int runSeed, int sequenceIndex) public void ConfigureRunContext(int runSeed, int sequenceIndex)
{ {
_runSeed = runSeed; _runSeed = runSeed;
_nodeSequenceIndex = sequenceIndex; _nodeSequenceIndex = sequenceIndex;
_nextDropTagOrdinal = 0; _nextDropOrdinal = 0;
_nextRewardTagOrdinal = 0; _nextRewardOrdinal = 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)
{ {
EnsureShopBuilder(); EnsureShopBuilder();
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex, AllocateTempInstanceId); return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
} }
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context) public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
@ -61,14 +56,24 @@ namespace GeometryTD.CustomComponent
? Mathf.Clamp01(enemy.DropPercent * 0.01f) ? Mathf.Clamp01(enemy.DropPercent * 0.01f)
: Mathf.Clamp01(enemy.DropPercent); : Mathf.Clamp01(enemy.DropPercent);
if (enemy.DropGold > 0 && dropRate > 0f && Random.value <= dropRate) int dropOrdinal = AllocateDropOrdinal();
InventoryGenerationRandomContext randomContext =
new(_runSeed, _nodeSequenceIndex, InventoryTagSourceType.Drop, dropOrdinal);
Random random = randomContext.CreateRandom();
if (enemy.DropGold > 0 && dropRate > 0f && random.NextDouble() <= dropRate)
{ {
gold = Mathf.Max(0, enemy.DropGold); gold = Mathf.Max(0, enemy.DropGold);
} }
TowerCompItemData lootItem = null; TowerCompItemData lootItem = null;
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) && if (OutGameDropRuleService.ShouldRollOutGameItem(context.DisplayPhaseIndex, random) &&
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem)) TryRollOutGameItem(
context.DisplayPhaseIndex,
context.ThemeType,
randomContext,
random,
out TowerCompItemData droppedItem))
{ {
lootItem = droppedItem; lootItem = droppedItem;
} }
@ -86,6 +91,7 @@ namespace GeometryTD.CustomComponent
displayPhaseIndex, displayPhaseIndex,
themeType, themeType,
candidateCount, candidateCount,
CreateNextRewardRandomContext,
BuildRewardCandidateItem); BuildRewardCandidateItem);
} }
@ -99,7 +105,7 @@ namespace GeometryTD.CustomComponent
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null || if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null ||
_baseCompTable == null) _baseCompTable == null)
{ {
throw new InvalidOperationException( throw new System.InvalidOperationException(
"InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables."); "InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables.");
} }
@ -111,7 +117,7 @@ namespace GeometryTD.CustomComponent
DRShopPrice[] rows = _shopPriceTable.GetAllDataRows(); DRShopPrice[] rows = _shopPriceTable.GetAllDataRows();
if (rows == null || rows.Length <= 0) if (rows == null || rows.Length <= 0)
{ {
throw new InvalidOperationException( throw new System.InvalidOperationException(
"InventoryGenerationComponent requires at least one shop price row."); "InventoryGenerationComponent requires at least one shop price row.");
} }
@ -125,7 +131,7 @@ namespace GeometryTD.CustomComponent
if (_shopPriceRows.Count <= 0) if (_shopPriceRows.Count <= 0)
{ {
throw new InvalidOperationException("InventoryGenerationComponent requires non-null shop price rows."); throw new System.InvalidOperationException("InventoryGenerationComponent requires non-null shop price rows.");
} }
} }
@ -139,11 +145,6 @@ namespace GeometryTD.CustomComponent
_baseCompTable); _baseCompTable);
} }
private long AllocateTempInstanceId()
{
return _nextTempInstanceId++;
}
private void EnsureDropTables() private void EnsureDropTables()
{ {
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>(); _dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
@ -153,7 +154,7 @@ namespace GeometryTD.CustomComponent
if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null) if (_dropPoolTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
{ {
throw new InvalidOperationException( throw new System.InvalidOperationException(
"InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables."); "InventoryGenerationComponent requires OutGameDropPool, MuzzleComp, BearingComp, and BaseComp data tables.");
} }
} }
@ -171,14 +172,22 @@ namespace GeometryTD.CustomComponent
return _rewardCandidateBuilder; return _rewardCandidateBuilder;
} }
private static bool ShouldRollOutGameItem(int displayPhaseIndex) private OutGameDropItemBuilder EnsureOutGameDropItemBuilder()
{ {
int phaseIndex = Mathf.Max(1, displayPhaseIndex); EnsureDropTables();
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap); _outGameDropItemBuilder ??= new OutGameDropItemBuilder(
return Random.value <= dropChance; _muzzleCompTable,
_bearingCompTable,
_baseCompTable);
return _outGameDropItemBuilder;
} }
private bool TryRollOutGameItem(int displayPhaseIndex, LevelThemeType themeType, out TowerCompItemData droppedItem) private bool TryRollOutGameItem(
int displayPhaseIndex,
LevelThemeType themeType,
InventoryGenerationRandomContext randomContext,
Random random,
out TowerCompItemData droppedItem)
{ {
droppedItem = null; droppedItem = null;
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller(); DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
@ -186,120 +195,46 @@ namespace GeometryTD.CustomComponent
if (!dropPoolRoller.TryRollRow( if (!dropPoolRoller.TryRollRow(
phaseIndex, phaseIndex,
themeType, themeType,
random,
out DROutGameDropPool selectedRow, out DROutGameDropPool selectedRow,
out RarityType selectedRarity) || selectedRow == null) out RarityType selectedRarity) || selectedRow == null)
{ {
return false; return false;
} }
return TryBuildDropItem( return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, out droppedItem);
selectedRow,
selectedRarity,
InventoryTagSourceType.Drop,
AllocateDropTagOrdinal(),
out droppedItem);
} }
private bool TryBuildDropItem( private int AllocateDropOrdinal()
{
return _nextDropOrdinal++;
}
private int AllocateRewardOrdinal()
{
return _nextRewardOrdinal++;
}
private InventoryGenerationRandomContext CreateNextRewardRandomContext()
{
return new InventoryGenerationRandomContext(
_runSeed,
_nodeSequenceIndex,
InventoryTagSourceType.Reward,
AllocateRewardOrdinal());
}
private TowerCompItemData BuildRewardCandidateItem(
DROutGameDropPool row, DROutGameDropPool row,
RarityType rarity, RarityType rarity,
InventoryTagSourceType sourceType, InventoryGenerationRandomContext randomContext)
int localOrdinal,
out TowerCompItemData droppedItem)
{ {
droppedItem = null; if (!EnsureOutGameDropItemBuilder().TryBuildItem(row, rarity, randomContext, out TowerCompItemData droppedItem))
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(),
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(),
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(),
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, RarityType rarity)
{
if (!TryBuildDropItem(
row,
rarity,
InventoryTagSourceType.Reward,
AllocateRewardTagOrdinal(),
out TowerCompItemData droppedItem))
{ {
return null; return null;
} }
return droppedItem; 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,64 @@
using System;
using Random = System.Random;
namespace GeometryTD.Definition
{
public readonly struct InventoryGenerationRandomContext
{
public InventoryGenerationRandomContext(
int runSeed,
int nodeSequenceIndex,
InventoryTagSourceType sourceType,
int localOrdinal)
{
RunSeed = runSeed;
NodeSequenceIndex = nodeSequenceIndex;
SourceType = sourceType;
LocalOrdinal = localOrdinal;
}
public int RunSeed { get; }
public int NodeSequenceIndex { get; }
public InventoryTagSourceType SourceType { get; }
public int LocalOrdinal { get; }
public Random CreateRandom()
{
return new Random(BuildSeed());
}
public long CreateStableItemInstanceId()
{
long normalizedSource = ((long)Math.Max(0, (int)SourceType) + 1L) << 48;
long normalizedSequence = ((long)Math.Max(0, NodeSequenceIndex) + 1L) << 24;
long normalizedOrdinal = (uint)(Math.Max(0, LocalOrdinal) + 1);
return normalizedSource | normalizedSequence | normalizedOrdinal;
}
public InventoryTagRandomContext CreateTagRandomContext(int configId)
{
return SourceType switch
{
InventoryTagSourceType.Shop => InventoryTagRandomContext.CreateShop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(RunSeed, NodeSequenceIndex, LocalOrdinal, configId),
_ => InventoryTagRandomContext.CreateDrop(RunSeed, NodeSequenceIndex, LocalOrdinal, configId)
};
}
private int BuildSeed()
{
unchecked
{
int seed = 17;
seed = seed * 31 + RunSeed;
seed = seed * 31 + NodeSequenceIndex;
seed = seed * 31 + (int)SourceType;
seed = seed * 31 + LocalOrdinal;
return seed;
}
}
}
}

View File

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

View File

@ -0,0 +1,19 @@
using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent
{
public static class OutGameDropRuleService
{
private const float DropChanceBase = 0.05f;
private const float DropChancePerPhase = 0.2f;
private const float DropChanceCap = 0.2f;
public static bool ShouldRollOutGameItem(int displayPhaseIndex, Random random)
{
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
return random.NextDouble() <= dropChance;
}
}
}

View File

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

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using GeometryTD.DataTable; using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
@ -19,7 +20,8 @@ namespace GeometryTD.CustomComponent
int displayPhaseIndex, int displayPhaseIndex,
LevelThemeType themeType, LevelThemeType themeType,
int candidateCount, int candidateCount,
Func<DROutGameDropPool, RarityType, TowerCompItemData> buildRewardItem) Func<InventoryGenerationRandomContext> createRandomContext,
Func<DROutGameDropPool, RarityType, InventoryGenerationRandomContext, TowerCompItemData> buildRewardItem)
{ {
int resolvedCount = Mathf.Max(0, candidateCount); int resolvedCount = Mathf.Max(0, candidateCount);
if (resolvedCount <= 0) if (resolvedCount <= 0)
@ -36,9 +38,12 @@ namespace GeometryTD.CustomComponent
while (candidates.Count < resolvedCount && attempts < maxAttempts) while (candidates.Count < resolvedCount && attempts < maxAttempts)
{ {
attempts++; attempts++;
InventoryGenerationRandomContext randomContext = createRandomContext();
Random random = randomContext.CreateRandom();
if (!_dropPoolRoller.TryRollRow( if (!_dropPoolRoller.TryRollRow(
phaseIndex, phaseIndex,
themeType, themeType,
random,
out DROutGameDropPool selectedRow, out DROutGameDropPool selectedRow,
out RarityType selectedRarity) || selectedRow == null) out RarityType selectedRarity) || selectedRow == null)
{ {
@ -50,7 +55,7 @@ namespace GeometryTD.CustomComponent
continue; continue;
} }
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity); TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
if (candidate == null) if (candidate == null)
{ {
continue; continue;
@ -63,16 +68,19 @@ namespace GeometryTD.CustomComponent
while (candidates.Count < resolvedCount && attempts < maxAttempts) while (candidates.Count < resolvedCount && attempts < maxAttempts)
{ {
attempts++; attempts++;
InventoryGenerationRandomContext randomContext = createRandomContext();
Random random = randomContext.CreateRandom();
if (!_dropPoolRoller.TryRollRow( if (!_dropPoolRoller.TryRollRow(
phaseIndex, phaseIndex,
themeType, themeType,
random,
out DROutGameDropPool selectedRow, out DROutGameDropPool selectedRow,
out RarityType selectedRarity) || selectedRow == null) out RarityType selectedRarity) || selectedRow == null)
{ {
break; break;
} }
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity); TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
if (candidate == null) if (candidate == null)
{ {
continue; continue;

View File

@ -7,6 +7,7 @@ using GeometryTD.Definition;
using GeometryTD.Factory; using GeometryTD.Factory;
using GeometryTD.UI; using GeometryTD.UI;
using UnityEngine; using UnityEngine;
using Random = System.Random;
namespace GeometryTD.CustomComponent namespace GeometryTD.CustomComponent
{ {
@ -32,8 +33,7 @@ namespace GeometryTD.CustomComponent
public List<GoodsItemRawData> BuildGoods( public List<GoodsItemRawData> BuildGoods(
int goodsCount, int goodsCount,
int runSeed, int runSeed,
int sequenceIndex, int sequenceIndex)
Func<long> allocateInstanceId)
{ {
if (goodsCount <= 0) if (goodsCount <= 0)
{ {
@ -43,7 +43,9 @@ namespace GeometryTD.CustomComponent
List<GoodsItemRawData> goodsItems = new(goodsCount); List<GoodsItemRawData> goodsItems = new(goodsCount);
for (int i = 0; i < goodsCount; i++) for (int i = 0; i < goodsCount; i++)
{ {
goodsItems.Add(BuildGoodsItem(i, runSeed, sequenceIndex, allocateInstanceId)); InventoryGenerationRandomContext randomContext =
new(runSeed, sequenceIndex, InventoryTagSourceType.Shop, i);
goodsItems.Add(BuildGoodsItem(i, randomContext));
} }
return goodsItems; return goodsItems;
@ -51,18 +53,17 @@ namespace GeometryTD.CustomComponent
private GoodsItemRawData BuildGoodsItem( private GoodsItemRawData BuildGoodsItem(
int goodsIndex, int goodsIndex,
int runSeed, InventoryGenerationRandomContext randomContext)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
TowerCompItemData sourceItem = BuildRandomComponentItem(goodsIndex, runSeed, sequenceIndex, allocateInstanceId); Random random = randomContext.CreateRandom();
TowerCompItemData sourceItem = BuildRandomComponentItem(randomContext, random);
return new GoodsItemRawData return new GoodsItemRawData
{ {
GoodsIndex = goodsIndex, GoodsIndex = goodsIndex,
Title = sourceItem.Name, Title = sourceItem.Name,
TypeText = BuildTypeText(sourceItem.SlotType), TypeText = BuildTypeText(sourceItem.SlotType),
Description = BuildDescription(sourceItem), Description = BuildDescription(sourceItem),
Price = ResolveRandomPrice(sourceItem.Rarity), Price = ResolveRandomPrice(sourceItem.Rarity, random),
Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(), Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(),
IconAreaContext = BuildIconAreaContext(sourceItem), IconAreaContext = BuildIconAreaContext(sourceItem),
SourceItem = sourceItem, SourceItem = sourceItem,
@ -71,70 +72,56 @@ namespace GeometryTD.CustomComponent
} }
private TowerCompItemData BuildRandomComponentItem( private TowerCompItemData BuildRandomComponentItem(
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
int slotRoll = UnityEngine.Random.Range(0, 3); int slotRoll = random.Next(0, 3);
DRShopPrice priceRow = _shopPriceRows[UnityEngine.Random.Range(0, _shopPriceRows.Count)]; DRShopPrice priceRow = _shopPriceRows[random.Next(0, _shopPriceRows.Count)];
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity( RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
priceRow != null ? priceRow.Rarity : RarityType.White); priceRow != null ? priceRow.Rarity : RarityType.White);
return slotRoll switch return slotRoll switch
{ {
0 => BuildRandomMuzzleItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId), 0 => BuildRandomMuzzleItem(rarity, randomContext, random),
1 => BuildRandomBearingItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId), 1 => BuildRandomBearingItem(rarity, randomContext, random),
_ => BuildRandomBaseItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId) _ => BuildRandomBaseItem(rarity, randomContext, random)
}; };
} }
private MuzzleCompItemData BuildRandomMuzzleItem( private MuzzleCompItemData BuildRandomMuzzleItem(
RarityType rarity, RarityType rarity,
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows(); DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
DRMuzzleComp config = rows[UnityEngine.Random.Range(0, rows.Length)]; DRMuzzleComp config = rows[random.Next(0, rows.Length)];
long instanceId = allocateInstanceId(); long instanceId = randomContext.CreateStableItemInstanceId();
InventoryTagRandomContext randomContext = return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext);
} }
private BearingCompItemData BuildRandomBearingItem( private BearingCompItemData BuildRandomBearingItem(
RarityType rarity, RarityType rarity,
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows(); DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
DRBearingComp config = rows[UnityEngine.Random.Range(0, rows.Length)]; DRBearingComp config = rows[random.Next(0, rows.Length)];
long instanceId = allocateInstanceId(); long instanceId = randomContext.CreateStableItemInstanceId();
InventoryTagRandomContext randomContext = return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext);
} }
private BaseCompItemData BuildRandomBaseItem( private BaseCompItemData BuildRandomBaseItem(
RarityType rarity, RarityType rarity,
int goodsIndex, InventoryGenerationRandomContext randomContext,
int runSeed, Random random)
int sequenceIndex,
Func<long> allocateInstanceId)
{ {
DRBaseComp[] rows = _baseCompTable.GetAllDataRows(); DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
DRBaseComp config = rows[UnityEngine.Random.Range(0, rows.Length)]; DRBaseComp config = rows[random.Next(0, rows.Length)];
long instanceId = allocateInstanceId(); long instanceId = randomContext.CreateStableItemInstanceId();
InventoryTagRandomContext randomContext = return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext);
} }
private int ResolveRandomPrice(RarityType rarity) private int ResolveRandomPrice(RarityType rarity, Random random)
{ {
for (int i = 0; i < _shopPriceRows.Count; i++) for (int i = 0; i < _shopPriceRows.Count; i++)
{ {
@ -143,7 +130,7 @@ namespace GeometryTD.CustomComponent
{ {
int min = Mathf.Max(0, row.MinPrice); int min = Mathf.Max(0, row.MinPrice);
int max = Mathf.Max(min, row.MaxPrice); int max = Mathf.Max(min, row.MaxPrice);
return UnityEngine.Random.Range(min, max + 1); return random.Next(min, max + 1);
} }
} }

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 523d63397a9c41fdaaf238bfb0e95357
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,64 @@
using System;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using UnityGameFramework.Runtime;
namespace GeometryTD.CustomComponent
{
public sealed class TagRegistryComponent : GameFrameworkComponent
{
private IDataTable<DRTagConfig> _tagConfigTable;
private IDataTable<DRTag> _tagTable;
private IDataTable<DRRarityTagBudget> _rarityTagBudgetTable;
public void OnInit()
{
ReloadAllFromLoadedTables();
}
public void ReloadAllFromLoadedTables()
{
ReloadTagDefinitionsAndGenerationFromLoadedTables();
ReloadRarityTagBudgetFromLoadedTable();
}
public void ReloadTagDefinitionsAndGenerationFromLoadedTables()
{
EnsureTagDefinitionTables();
DRTagConfig[] tagConfigRows = _tagConfigTable.GetAllDataRows();
DRTag[] tagRows = _tagTable.GetAllDataRows();
TagGenerationRuleRegistry.LoadFromRows(tagRows);
TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows);
}
public void ReloadRarityTagBudgetFromLoadedTable()
{
EnsureRarityTagBudgetTable();
RarityTagBudgetRuleRegistry.LoadFromRows(_rarityTagBudgetTable.GetAllDataRows());
}
private void EnsureTagDefinitionTables()
{
_tagConfigTable ??= GameEntry.DataTable.GetDataTable<DRTagConfig>();
_tagTable ??= GameEntry.DataTable.GetDataTable<DRTag>();
if (_tagConfigTable == null || _tagTable == null)
{
throw new InvalidOperationException(
"TagRegistryComponent requires TagConfig and Tag data tables to be loaded before initialization.");
}
}
private void EnsureRarityTagBudgetTable()
{
_rarityTagBudgetTable ??= GameEntry.DataTable.GetDataTable<DRRarityTagBudget>();
if (_rarityTagBudgetTable == null)
{
throw new InvalidOperationException(
"TagRegistryComponent requires RarityTagBudget data table to be loaded before initialization.");
}
}
}
}

View File

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

View File

@ -0,0 +1,88 @@
using System;
using GameFramework.DataTable;
using GeometryTD.DataTable;
using GeometryTD.Definition;
namespace GeometryTD.Factory
{
public sealed class OutGameDropItemBuilder
{
private readonly IDataTable<DRMuzzleComp> _muzzleCompTable;
private readonly IDataTable<DRBearingComp> _bearingCompTable;
private readonly IDataTable<DRBaseComp> _baseCompTable;
public OutGameDropItemBuilder(
IDataTable<DRMuzzleComp> muzzleCompTable,
IDataTable<DRBearingComp> bearingCompTable,
IDataTable<DRBaseComp> baseCompTable)
{
_muzzleCompTable = muzzleCompTable;
_bearingCompTable = bearingCompTable;
_baseCompTable = baseCompTable;
}
public bool TryBuildItem(
DROutGameDropPool row,
RarityType rarity,
InventoryGenerationRandomContext randomContext,
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,
randomContext.CreateStableItemInstanceId(),
rarity,
randomContext.CreateTagRandomContext(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,
randomContext.CreateStableItemInstanceId(),
rarity,
randomContext.CreateTagRandomContext(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,
randomContext.CreateStableItemInstanceId(),
rarity,
randomContext.CreateTagRandomContext(config.Id));
return true;
}
return false;
}
}
}

View File

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

View File

@ -196,45 +196,6 @@ namespace GeometryTD.Procedure
_loadedFlag[ne.DataTableAssetName] = true; _loadedFlag[ne.DataTableAssetName] = true;
Log.Info("Load data table '{0}' OK.", ne.DataTableAssetName); Log.Info("Load data table '{0}' OK.", ne.DataTableAssetName);
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("TagConfig", false))
{
ReloadTagRegistriesFromLoadedTables();
}
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("Tag", false))
{
ReloadTagRegistriesFromLoadedTables();
}
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("RarityTagBudget", false))
{
var tagBudgetTable = GameEntry.DataTable.GetDataTable<DRRarityTagBudget>();
if (tagBudgetTable != null)
{
RarityTagBudgetRuleRegistry.LoadFromRows(tagBudgetTable.GetAllDataRows());
}
}
}
private static void ReloadTagRegistriesFromLoadedTables()
{
DRTagConfig[] tagConfigRows = null;
var tagConfigTable = GameEntry.DataTable.GetDataTable<DRTagConfig>();
if (tagConfigTable != null)
{
tagConfigRows = tagConfigTable.GetAllDataRows();
}
DRTag[] tagRows = null;
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null)
{
tagRows = tagTable.GetAllDataRows();
TagGenerationRuleRegistry.LoadFromRows(tagRows);
}
TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows);
} }
private void OnLoadDataTableFailure(object sender, GameEventArgs e) private void OnLoadDataTableFailure(object sender, GameEventArgs e)

View File

@ -34,6 +34,7 @@ namespace GeometryTD.Procedure
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter); GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested); GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.TagRegistry.OnInit();
GameEntry.EventNode.OnInit(); GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain); GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit(); GameEntry.ShopNode.OnInit();

View File

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.Definition; using GeometryTD.Definition;
using UnityEngine; using UnityEngine;
using Random = UnityEngine.Random;
namespace GeometryTD.UI namespace GeometryTD.UI
{ {
@ -19,6 +18,7 @@ namespace GeometryTD.UI
private bool _allowRefreshOnce = true; private bool _allowRefreshOnce = true;
private bool _allowGiveUp = true; private bool _allowGiveUp = true;
private bool _hasRefreshed; private bool _hasRefreshed;
private int _selectionOffset;
private string _tipText = "Select one reward"; private string _tipText = "Select one reward";
public void ConfigureRewardPool( public void ConfigureRewardPool(
@ -49,6 +49,7 @@ namespace GeometryTD.UI
_allowGiveUp = allowGiveUp; _allowGiveUp = allowGiveUp;
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText; _tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
_hasRefreshed = false; _hasRefreshed = false;
_selectionOffset = 0;
_currentModel = null; _currentModel = null;
} }
@ -80,6 +81,7 @@ namespace GeometryTD.UI
_allowGiveUp = allowGiveUp; _allowGiveUp = allowGiveUp;
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText; _tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
_hasRefreshed = false; _hasRefreshed = false;
_selectionOffset = 0;
_currentModel = null; _currentModel = null;
} }
@ -115,6 +117,11 @@ namespace GeometryTD.UI
} }
_hasRefreshed = true; _hasRefreshed = true;
if (_rewardPool.Count > 0)
{
_selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count;
}
_currentModel = BuildModel(); _currentModel = BuildModel();
return _currentModel; return _currentModel;
} }
@ -172,22 +179,11 @@ namespace GeometryTD.UI
} }
int finalCount = Mathf.Clamp(_displayCount, 1, _rewardPool.Count); int finalCount = Mathf.Clamp(_displayCount, 1, _rewardPool.Count);
int[] indexes = new int[_rewardPool.Count];
for (int i = 0; i < indexes.Length; i++)
{
indexes[i] = i;
}
for (int i = 0; i < finalCount; i++)
{
int randomIndex = Random.Range(i, indexes.Length);
(indexes[i], indexes[randomIndex]) = (indexes[randomIndex], indexes[i]);
}
RewardSelectItemRawData[] results = new RewardSelectItemRawData[finalCount]; RewardSelectItemRawData[] results = new RewardSelectItemRawData[finalCount];
for (int i = 0; i < finalCount; i++) for (int i = 0; i < finalCount; i++)
{ {
RewardSelectItemRawData source = _rewardPool[indexes[i]]; int sourceIndex = (_selectionOffset + i) % _rewardPool.Count;
RewardSelectItemRawData source = _rewardPool[sourceIndex];
results[i] = source; results[i] = source;
} }

View File

@ -158,6 +158,7 @@ Transform:
- {fileID: 1322505022} - {fileID: 1322505022}
- {fileID: 1062564689} - {fileID: 1062564689}
- {fileID: 1554147129} - {fileID: 1554147129}
- {fileID: 336799288}
m_Father: {fileID: 1852670053} m_Father: {fileID: 1852670053}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &120093239 --- !u!1 &120093239
@ -294,6 +295,50 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0a0c1c547ca24c95819f5f62f0bd3ea3, type: 3} m_Script: {fileID: 11500000, guid: 0a0c1c547ca24c95819f5f62f0bd3ea3, type: 3}
m_Name: m_Name:
m_EditorClassIdentifier: m_EditorClassIdentifier:
--- !u!1 &336799287
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 336799288}
- component: {fileID: 336799289}
m_Layer: 0
m_Name: TagRegistery
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &336799288
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 336799287}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 119167776}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &336799289
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 336799287}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a268c57d4373494ab9b4d7167099f170, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1001 &343730742 --- !u!1001 &343730742
PrefabInstance: PrefabInstance:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@ -0,0 +1,437 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using GameFramework.DataTable;
using GeometryTD.CustomComponent;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using GeometryTD.UI;
using NUnit.Framework;
using UnityEngine;
using Object = UnityEngine.Object;
namespace GeometryTD.Tests.EditMode
{
public sealed class InventoryGenerationStabilityTests
{
private GameObject _gameObject;
private InventoryGenerationComponent _component;
[SetUp]
public void SetUp()
{
TagDefinitionRegistry.ResetToDefaults();
TagGenerationRuleRegistry.ResetToDefaults();
RarityTagBudgetRuleRegistry.ResetToDefaults();
_gameObject = new GameObject("InventoryGenerationStabilityTests");
_component = _gameObject.AddComponent<InventoryGenerationComponent>();
BindTables(_component);
}
[TearDown]
public void TearDown()
{
TagDefinitionRegistry.ResetToDefaults();
TagGenerationRuleRegistry.ResetToDefaults();
RarityTagBudgetRuleRegistry.ResetToDefaults();
if (_gameObject != null)
{
Object.DestroyImmediate(_gameObject);
_gameObject = null;
_component = null;
}
}
[Test]
public void BuildShopGoods_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex()
{
string first = BuildShopSignature(runSeed: 1001, sequenceIndex: 3);
string second = BuildShopSignature(runSeed: 1001, sequenceIndex: 3);
Assert.That(second, Is.EqualTo(first));
}
[Test]
public void BuildShopGoods_Distinguishes_Different_RunSeed()
{
HashSet<string> signatures = new HashSet<string>();
for (int runSeed = 1001; runSeed < 1017; runSeed++)
{
signatures.Add(BuildShopSignature(runSeed, sequenceIndex: 3));
}
Assert.That(signatures.Count, Is.GreaterThan(1));
}
[Test]
public void ResolveEnemyDrop_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex()
{
string first = BuildEnemyDropSignature(runSeed: 2001, sequenceIndex: 5);
string second = BuildEnemyDropSignature(runSeed: 2001, sequenceIndex: 5);
Assert.That(second, Is.EqualTo(first));
}
[Test]
public void ResolveEnemyDrop_Distinguishes_Different_RunSeed()
{
HashSet<string> signatures = new HashSet<string>();
for (int runSeed = 2001; runSeed < 2033; runSeed++)
{
signatures.Add(BuildEnemyDropSignature(runSeed, sequenceIndex: 5));
}
Assert.That(signatures.Count, Is.GreaterThan(1));
}
[Test]
public void BuildRewardCandidates_Is_Reproducible_For_Same_RunSeed_And_SequenceIndex()
{
string first = BuildRewardCandidateSignature(runSeed: 3001, sequenceIndex: 7);
string second = BuildRewardCandidateSignature(runSeed: 3001, sequenceIndex: 7);
Assert.That(second, Is.EqualTo(first));
}
[Test]
public void BuildRewardCandidates_Distinguishes_Different_RunSeed()
{
HashSet<string> signatures = new HashSet<string>();
for (int runSeed = 3001; runSeed < 3017; runSeed++)
{
signatures.Add(BuildRewardCandidateSignature(runSeed, sequenceIndex: 7));
}
Assert.That(signatures.Count, Is.GreaterThan(1));
}
[Test]
public void RewardSelectFormUseCase_Uses_Stable_Order_And_Deterministic_Refresh_Rotation()
{
RewardSelectFormUseCase useCase = new RewardSelectFormUseCase();
useCase.ConfigureRewardPool(
new[]
{
CreateRewardRawData(1, "一号"),
CreateRewardRawData(2, "二号"),
CreateRewardRawData(3, "三号"),
CreateRewardRawData(4, "四号")
},
displayCount: 3,
refreshCost: 0,
allowRefreshOnce: true,
allowGiveUp: false);
RewardSelectFormRawData initialModel = useCase.CreateInitialModel();
RewardSelectFormRawData refreshedModel = useCase.TryRefresh();
Assert.That(initialModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "一号", "二号", "三号" }));
Assert.That(refreshedModel.RewardItems.Select(item => item.Title).ToArray(), Is.EqualTo(new[] { "四号", "一号", "二号" }));
}
private string BuildShopSignature(int runSeed, int sequenceIndex)
{
List<GoodsItemRawData> goods = _component.BuildShopGoods(4, runSeed, sequenceIndex);
return string.Join("|", goods.Select(BuildGoodsSignaturePart));
}
private string BuildEnemyDropSignature(int runSeed, int sequenceIndex)
{
_component.ConfigureRunContext(runSeed, sequenceIndex);
EnemyDropResult result = _component.ResolveEnemyDrop(new EnemyDropContext(CreateEnemyRow(), 8, LevelThemeType.Plain));
return BuildDropSignaturePart(result);
}
private string BuildRewardCandidateSignature(int runSeed, int sequenceIndex)
{
_component.ConfigureRunContext(runSeed, sequenceIndex);
IReadOnlyList<TowerCompItemData> candidates = _component.BuildRewardCandidates(8, LevelThemeType.Plain, 3);
return string.Join("|", candidates.Select(BuildItemSignaturePart));
}
private static string BuildGoodsSignaturePart(GoodsItemRawData goods)
{
return $"{goods.GoodsIndex}:{goods.Price}:{BuildItemSignaturePart(goods.SourceItem)}";
}
private static string BuildDropSignaturePart(EnemyDropResult result)
{
return $"{result.Coin}:{result.Gold}:{BuildItemSignaturePart(result.LootItem)}";
}
private static string BuildItemSignaturePart(TowerCompItemData item)
{
if (item == null)
{
return "null";
}
string tags = item.Tags == null || item.Tags.Length <= 0
? string.Empty
: string.Join(",", item.Tags.Select(tag => tag.ToString()));
return $"{item.InstanceId}:{item.SlotType}:{item.ConfigId}:{item.Name}:{item.Rarity}:{tags}";
}
private static RewardSelectItemRawData CreateRewardRawData(long instanceId, string title)
{
return new RewardSelectItemRawData
{
Title = title,
SlotType = TowerCompSlotType.Muzzle,
SourceItem = new MuzzleCompItemData
{
InstanceId = instanceId,
Name = title,
}
};
}
private static void BindTables(InventoryGenerationComponent component)
{
SetPrivateField(component, "_shopPriceTable", new FakeDataTable<DRShopPrice>(
CreateShopPriceRow(1, RarityType.Blue, 30, 35),
CreateShopPriceRow(2, RarityType.Purple, 60, 70)));
SetPrivateField(component, "_dropPoolTable", new FakeDataTable<DROutGameDropPool>(
CreateDropPoolRow(1, "MuzzleComp", 1, "[50,40,30,10,0]"),
CreateDropPoolRow(2, "MuzzleComp", 2, "[10,30,50,20,0]"),
CreateDropPoolRow(3, "BearingComp", 1, "[20,40,20,10,0]"),
CreateDropPoolRow(4, "BaseComp", 1, "[20,20,40,10,0]")));
SetPrivateField(component, "_muzzleCompTable", new FakeDataTable<DRMuzzleComp>(
CreateMuzzleRow(1, "火焰枪口", "[Fire,Ice,Crit]"),
CreateMuzzleRow(2, "暴击枪口", "[Crit,Shatter,Execution]")));
SetPrivateField(component, "_bearingCompTable", new FakeDataTable<DRBearingComp>(
CreateBearingRow(1, "寒冰轴承", "[Ice,Shatter]"),
CreateBearingRow(2, "穿透轴承", "[Pierce,Crit]")));
SetPrivateField(component, "_baseCompTable", new FakeDataTable<DRBaseComp>(
CreateBaseRow(1, "迅捷底座", "[Fire,Crit]"),
CreateBaseRow(2, "处决底座", "[Execution,Ice]")));
}
private static void SetPrivateField(object instance, string fieldName, object value)
{
FieldInfo field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, fieldName);
field.SetValue(instance, value);
}
private static DRShopPrice CreateShopPriceRow(int id, RarityType rarity, int minPrice, int maxPrice)
{
DRShopPrice row = new DRShopPrice();
Assert.That(row.ParseDataRow($"\t{id}\t\t{rarity}\t{minPrice}\t{maxPrice}", null), Is.True);
return row;
}
private static DROutGameDropPool CreateDropPoolRow(int id, string itemType, int itemId, string weights)
{
DROutGameDropPool row = new DROutGameDropPool();
Assert.That(
row.ParseDataRow($"\t{id}\t\tPlain\t{itemType}\t{itemId}\t{weights}\t[1,1,1,1,1]\t[99,99,99,99,99]", null),
Is.True);
return row;
}
private static DRMuzzleComp CreateMuzzleRow(int id, string name, string possibleTags)
{
DRMuzzleComp row = new DRMuzzleComp();
Assert.That(
row.ParseDataRow($"\t{id}\t\t{name}\t[10,20,30,40,50]\t3\t0.15\tNormalBullet\t\t{possibleTags}", null),
Is.True);
return row;
}
private static DRBearingComp CreateBearingRow(int id, string name, string possibleTags)
{
DRBearingComp row = new DRBearingComp();
Assert.That(
row.ParseDataRow($"\t{id}\t\t{name}\t[1,2,3,4,5]\t0.5\t[10,20,30,40,50]\t1\t\t{possibleTags}", null),
Is.True);
return row;
}
private static DRBaseComp CreateBaseRow(int id, string name, string possibleTags)
{
DRBaseComp row = new DRBaseComp();
Assert.That(
row.ParseDataRow($"\t{id}\t\t{name}\t[2,4,6,8,10]\t-0.25\tFire\t\t{possibleTags}", null),
Is.True);
return row;
}
private static DREnemy CreateEnemyRow()
{
DREnemy row = new DREnemy();
Assert.That(row.ParseDataRow("\t1\t\t1\t100\t1\t1\t3\t5\t1", null), Is.True);
return row;
}
private sealed class FakeDataTable<TRow> : IDataTable<TRow> where TRow : class, IDataRow
{
private readonly Dictionary<int, TRow> _rowsById = new();
public FakeDataTable(params TRow[] rows)
{
if (rows == null)
{
return;
}
for (int i = 0; i < rows.Length; i++)
{
TRow row = rows[i];
if (row != null)
{
_rowsById[row.Id] = row;
}
}
}
public string Name => typeof(TRow).Name;
public string FullName => typeof(TRow).FullName;
public Type Type => typeof(TRow);
public int Count => _rowsById.Count;
public TRow this[int id] => GetDataRow(id);
public TRow MinIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[0]);
public TRow MaxIdDataRow => _rowsById.Count <= 0 ? null : GetDataRow(GetOrderedIds()[^1]);
public bool HasDataRow(int id) => _rowsById.ContainsKey(id);
public bool HasDataRow(Predicate<TRow> condition) => GetDataRow(condition) != null;
public TRow GetDataRow(int id) => _rowsById.TryGetValue(id, out TRow row) ? row : null;
public TRow GetDataRow(Predicate<TRow> condition)
{
if (condition == null)
{
return null;
}
foreach (TRow row in _rowsById.Values)
{
if (row != null && condition(row))
{
return row;
}
}
return null;
}
public TRow[] GetDataRows(Predicate<TRow> condition)
{
List<TRow> results = new();
GetDataRows(condition, results);
return results.ToArray();
}
public void GetDataRows(Predicate<TRow> condition, List<TRow> results)
{
results?.Clear();
if (condition == null || results == null)
{
return;
}
foreach (TRow row in _rowsById.Values)
{
if (row != null && condition(row))
{
results.Add(row);
}
}
}
public TRow[] GetDataRows(Comparison<TRow> comparison)
{
List<TRow> results = new();
GetDataRows(comparison, results);
return results.ToArray();
}
public void GetDataRows(Comparison<TRow> comparison, List<TRow> results)
{
results?.Clear();
if (results == null)
{
return;
}
results.AddRange(_rowsById.Values);
if (comparison != null)
{
results.Sort(comparison);
}
}
public TRow[] GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison)
{
List<TRow> results = new();
GetDataRows(condition, comparison, results);
return results.ToArray();
}
public void GetDataRows(Predicate<TRow> condition, Comparison<TRow> comparison, List<TRow> results)
{
GetDataRows(condition, results);
if (results != null && comparison != null)
{
results.Sort(comparison);
}
}
public TRow[] GetAllDataRows()
{
List<TRow> results = new();
GetAllDataRows(results);
return results.ToArray();
}
public void GetAllDataRows(List<TRow> results)
{
results?.Clear();
if (results == null)
{
return;
}
foreach (int id in GetOrderedIds())
{
results.Add(_rowsById[id]);
}
}
public bool AddDataRow(string dataRowString, object userData) => throw new NotSupportedException();
public bool AddDataRow(byte[] dataRowBytes, int startIndex, int length, object userData) => throw new NotSupportedException();
public bool RemoveDataRow(int id) => _rowsById.Remove(id);
public void RemoveAllDataRows()
{
_rowsById.Clear();
}
public IEnumerator<TRow> GetEnumerator()
{
foreach (int id in GetOrderedIds())
{
yield return _rowsById[id];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private int[] GetOrderedIds()
{
int[] ids = _rowsById.Keys.ToArray();
Array.Sort(ids);
return ids;
}
}
}
}

View File

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

View File

@ -9,7 +9,7 @@
- 商店、敌人掉落、满血 3 选 1 候选都在各自生成组件实例,逻辑分散,后续改规则要改多处。 - 商店、敌人掉落、满血 3 选 1 候选都在各自生成组件实例,逻辑分散,后续改规则要改多处。
- `ShopFormUseCase`、旧 `EnemyDropResolver`、`CombatSettlementService` 都曾同时承担“流程 + 规则 + 数据组装”的混合职责。 - `ShopFormUseCase`、旧 `EnemyDropResolver`、`CombatSettlementService` 都曾同时承担“流程 + 规则 + 数据组装”的混合职责。
- `runSeed` 目前只稳定了 Tag 生成,没有稳定整个组件产出流程。 - 重构前 `runSeed` 只稳定了 Tag 生成,没有稳定整个组件产出流程;当前已补齐到商店、掉落、奖励候选与奖励展示顺序
- Tag 模块本身已经分成 `Generation / Aggregation / Combat / Metadata / Presentation`,但初始化入口还散在流程代码里。 - Tag 模块本身已经分成 `Generation / Aggregation / Combat / Metadata / Presentation`,但初始化入口还散在流程代码里。
## 重构边界 ## 重构边界
@ -30,7 +30,7 @@
- `TagDefinitionRegistry` - `TagDefinitionRegistry`
- `TagGenerationRuleRegistry` - `TagGenerationRuleRegistry`
- `RarityTagBudgetRuleRegistry` - `RarityTagBudgetRuleRegistry`
- 替代当前散在 `ProcedurePreload` 里的 Tag 注册表装载逻辑 - 替代旧的 `ProcedurePreload` 直连刷新逻辑,并收口当前主流程初始化入口
### 不适合抽成 `GameFrameworkComponent` 的模块 ### 不适合抽成 `GameFrameworkComponent` 的模块
@ -54,6 +54,8 @@
- `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs` - `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs`
- 唯一负责“从配置行构造 `TowerCompItemData`”。 - 唯一负责“从配置行构造 `TowerCompItemData`”。
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
- 统一承载商店 / 掉落 / 奖励候选的随机合同。
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs` - `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs`
- 只负责商店货物生成。 - 只负责商店货物生成。
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs` - `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs`
@ -101,7 +103,9 @@
- `ProcedurePreload` - `ProcedurePreload`
- 移除 Tag 注册表刷新细节。 - 移除 Tag 注册表刷新细节。
- 只保留“数据表加载完成后通知组件刷新”。 - 只保留资源与数据表预加载职责。
- `ProcedureMain`
- 进入主流程时主动调用 `GameEntry.TagRegistry.OnInit()`
- `TagRegistryComponent` - `TagRegistryComponent`
- 成为 Tag 相关表与注册表的唯一运行时入口。 - 成为 Tag 相关表与注册表的唯一运行时入口。
@ -157,9 +161,9 @@
| 状态 | ID | 任务 | 目标 | | 状态 | ID | 任务 | 目标 |
|-----|-------|----------------------------------|--------------------| |-----|-------|----------------------------------|--------------------|
| [ ] | G4-01 | 新增 `TagRegistryComponent` | 建立 Tag 运行时入口 | | [x] | G4-01 | 新增 `TagRegistryComponent` | 建立 Tag 运行时入口 |
| [ ] | G4-02 | 挪出 `ProcedurePreload` 中 Tag 注册逻辑 | 流程代码不再直接维护 Tag 注册表 | | [x] | G4-02 | 挪出 `ProcedurePreload` 中 Tag 注册逻辑 | 流程代码不再直接维护 Tag 注册表 |
| [ ] | G4-03 | 对齐 Tag 加载时机与重载入口 | 初始化路径明确 | | [x] | G4-03 | 对齐 Tag 加载时机与重载入口 | 初始化路径明确 |
### G4 验收标准 ### G4 验收标准
@ -171,9 +175,9 @@
| 状态 | ID | 任务 | 目标 | | 状态 | ID | 任务 | 目标 |
|-----|-------|------------------------|---------------------| |-----|-------|------------------------|---------------------|
| [ ] | G5-01 | 统一商店 / 掉落 / 奖励候选的随机上下文 | 全部产出链路使用一致合同 | | [x] | G5-01 | 统一商店 / 掉落 / 奖励候选的随机上下文 | 全部产出链路使用一致合同 |
| [ ] | G5-02 | 补齐整体产出的 `runSeed` 稳定性 | 不只稳定 Tag也稳定物品产出 | | [x] | G5-02 | 补齐整体产出的 `runSeed` 稳定性 | 不只稳定 Tag也稳定物品产出 |
| [ ] | G5-03 | 增加对应 EditMode 测试 | 同 Run 可复现,跨 Run 可区分 | | [x] | G5-03 | 增加对应 EditMode 测试 | 同 Run 可复现,跨 Run 可区分 |
### G5 验收标准 ### G5 验收标准
@ -188,6 +192,7 @@
- `Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs` - `Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs` - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.cs`
- `Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs` - `Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs`
- `Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs`
- `Assets/GameMain/Scripts/Base/GameEntry.Custom.cs` - `Assets/GameMain/Scripts/Base/GameEntry.Custom.cs`
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs` - `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs`
@ -196,6 +201,7 @@
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs` - `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
- `Assets/GameMain/Scripts/CustomComponent/TagRegistry/TagRegistryComponent.cs` - `Assets/GameMain/Scripts/CustomComponent/TagRegistry/TagRegistryComponent.cs`
- `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs` - `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs`
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs` - `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs`
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs` - `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs`
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs` - `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs`
@ -215,7 +221,7 @@
2. 先替换商店货物生成。 2. 先替换商店货物生成。
3. 再替换战斗掉落与奖励候选生成。 3. 再替换战斗掉落与奖励候选生成。
4. 再拆战斗结算计算与提交。 4. 再拆战斗结算计算与提交。
5. 最后收 `TagRegistryComponent` `ProcedurePreload` 的 Tag 初始化入口。 5. 最后收 `TagRegistryComponent`主流程中的 Tag 初始化入口。
## 通过标准 ## 通过标准
@ -223,3 +229,12 @@
- 掉落、商店、奖励候选不再各自维护相似逻辑。 - 掉落、商店、奖励候选不再各自维护相似逻辑。
- Tag 模块保留现有分层,不再继续和流程代码缠在一起。 - Tag 模块保留现有分层,不再继续和流程代码缠在一起。
- `GameFrameworkComponent` 只承接运行时入口,不承接纯规则实现。 - `GameFrameworkComponent` 只承接运行时入口,不承接纯规则实现。
## 当前结果
- `InventoryGenerationComponent` 已成为商店、战斗掉落、奖励候选的统一运行时入口。
- 掉落概率规则与掉落物品构造已继续下沉到 `OutGameDropRuleService``OutGameDropItemBuilder``InventoryGenerationComponent` 保持入口编排职责。
- `TagRegistryComponent` 已成为 Tag 运行时入口,并由 `ProcedureMain` 主动初始化。
- `TagRegistryComponent` 已改为 fail-fast 初始化,缺少必要数据表时会直接暴露错误。
- `InventoryGenerationRandomContext` 已统一商店、掉落、奖励候选的随机合同,并补齐稳定临时实例 Id。
- `Assets/Tests/EditMode/InventoryGenerationStabilityTests.cs` 已覆盖 G5 的可复现性验收点。

View File

@ -310,12 +310,30 @@
- 在内部编排: - 在内部编排:
- `DropPoolRoller` - `DropPoolRoller`
- `RewardCandidateBuilder` - `RewardCandidateBuilder`
- `ComponentItemFactory` - `OutGameDropRuleService`
- `OutGameDropItemBuilder`
- `InventoryGenerationRandomContext`
约束: 约束:
- `CombatNode` 域不直接持有或复制组件产出规则。 - `CombatNode` 域不直接持有或复制组件产出规则。
- `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。 - `CombatScheduler` 与结算状态链只调用统一入口,不直接访问掉落池滚动或组件实例构造细节。
- `InventoryGenerationComponent` 负责组件实例生成、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。 - `InventoryGenerationComponent` 负责运行时入口、稳定临时实例 Id、Tag 随机上下文以及 `runSeed/sequenceIndex` 相关上下文。
- 掉落是否产出组件由 `OutGameDropRuleService` 决定;掉落池行到组件实例的构造由 `OutGameDropItemBuilder` 决定。
### 4.5.x InventoryGenerationRandomContext
文件:`Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
目标职责:
- 统一承载组件产出链路的随机合同。
- 统一派生:
- 商店 / 掉落 / 奖励候选的稳定随机流
- 稳定临时组件 `InstanceId`
- `InventoryTagRandomContext`
约束:
- `ShopGoodsBuilder`、`DropPoolRoller`、`RewardCandidateBuilder` 不再直接使用全局 `UnityEngine.Random`
- 同一 `runSeed + sequenceIndex + sourceType + localOrdinal` 下,应得到一致的物品本体与 Tag 结果。
### 4.6 CombatSettlementCalculator ### 4.6 CombatSettlementCalculator

View File

@ -1,6 +1,6 @@
# Tag System Design # Tag System Design
最后更新2026-03-12 最后更新2026-03-13
> 目标:这是 GeometryTD 当前 Tag 系统的唯一正式口径。 > 目标:这是 GeometryTD 当前 Tag 系统的唯一正式口径。
> 本文档只记录当前真实实现、当前边界与正式规则。 > 本文档只记录当前真实实现、当前边界与正式规则。
@ -76,7 +76,9 @@ M1 已完成 Tag 最小闭环:
- 当前负责 `MinRarity`、`Weight` - 当前负责 `MinRarity`、`Weight`
- `Tag.txt + TagConfig.txt -> TagDefinitionRegistry.ReloadFromRows(...)` - `Tag.txt + TagConfig.txt -> TagDefinitionRegistry.ReloadFromRows(...)`
- 当前负责 `IsImplemented`、`TriggerPhase`、`Description` 与正式运行时参数 - 当前负责 `IsImplemented`、`TriggerPhase`、`Description` 与正式运行时参数
- `ProcedurePreload``Tag``TagConfig` 任一表加载完成后,都会基于当前已加载的两张表组合重建定义层 - `ProcedureMain -> GameEntry.TagRegistry.OnInit() -> TagRegistryComponent`
- 当前在主流程进入时统一基于已加载数据表重建 Tag 定义层与生成规则层
- `TagRegistryComponent``TagConfig`、`Tag`、`RarityTagBudget` 三张表采用 fail-fast 依赖约束;缺表时直接暴露初始化错误,不再静默跳过
- `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry` - `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry`
- 当前负责按品质读取 `MinCount / MaxCount` - 当前负责按品质读取 `MinCount / MaxCount`
@ -100,9 +102,9 @@ public sealed class TagRuntimeData
当前统一入口是 `ComponentTagGenerationService`,以下链路共用同一套规则: 当前统一入口是 `ComponentTagGenerationService`,以下链路共用同一套规则:
- `InventorySeedUtility` - `InventorySeedUtility`
- `ShopFormUseCase` - `InventoryGenerationComponent.BuildShopGoods(...)`
- `EnemyDropResolver` - `InventoryGenerationComponent.ResolveEnemyDrop(...)`
- 结算奖励候选组件 - `InventoryGenerationComponent.BuildRewardCandidates(...)`
当前流程: 当前流程:
@ -124,7 +126,15 @@ Tag 随机结果的正式上下文为:
- `ItemInstanceId` - `ItemInstanceId`
- `ConfigId` - `ConfigId`
当前运行时通过 `InventoryTagRandomContext` 统一承载上述字段。 当前运行时通过 `InventoryGenerationRandomContext + InventoryTagRandomContext` 统一承载上述字段:
- `InventoryGenerationRandomContext`
- 统一承载 `runSeed + nodeSequenceIndex + sourceType + localOrdinal`
- 统一派生产出链路自己的稳定随机流
- 统一派生稳定的临时组件 `InstanceId`
- `InventoryTagRandomContext`
- 承载 Tag 生成所需的 `RunSeed / SourceType / ItemInstanceId / ConfigId`
- 由 `InventoryGenerationRandomContext` 进一步派生
各来源构造口径: 各来源构造口径: