InventoryGenerationComponent + TagRegistryComponent 收口
This commit is contained in:
parent
0c92fd9886
commit
aa44170d56
|
|
@ -26,6 +26,8 @@ public partial class GameEntry
|
|||
|
||||
public static InventoryGenerationComponent InventoryGeneration { get; private set; }
|
||||
|
||||
public static TagRegistryComponent TagRegistry { get; private set; }
|
||||
|
||||
private static void InitCustomComponents()
|
||||
{
|
||||
BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent<BuiltinDataComponent>();
|
||||
|
|
@ -38,5 +40,6 @@ public partial class GameEntry
|
|||
ResolutionAdapter = UnityGameFramework.Runtime.GameEntry.GetComponent<ResolutionAdapterComponent>();
|
||||
SpriteCache = UnityGameFramework.Runtime.GameEntry.GetComponent<SpriteCacheComponent>();
|
||||
InventoryGeneration = UnityGameFramework.Runtime.GameEntry.GetComponent<InventoryGenerationComponent>();
|
||||
TagRegistry = UnityGameFramework.Runtime.GameEntry.GetComponent<TagRegistryComponent>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using GameFramework.DataTable;
|
|||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
|
|
@ -22,6 +23,7 @@ namespace GeometryTD.CustomComponent
|
|||
public bool TryRollRow(
|
||||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
Random random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity)
|
||||
{
|
||||
|
|
@ -40,7 +42,7 @@ namespace GeometryTD.CustomComponent
|
|||
return false;
|
||||
}
|
||||
|
||||
selectedRarity = RollRarity(displayPhaseIndex);
|
||||
selectedRarity = RollRarity(displayPhaseIndex, random);
|
||||
if (selectedRarity == RarityType.None)
|
||||
{
|
||||
return false;
|
||||
|
|
@ -71,7 +73,7 @@ namespace GeometryTD.CustomComponent
|
|||
return false;
|
||||
}
|
||||
|
||||
int randomWeight = Random.Range(1, totalWeight + 1);
|
||||
int randomWeight = random.Next(1, totalWeight + 1);
|
||||
int cumulativeWeight = 0;
|
||||
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();
|
||||
float phaseT = Mathf.Clamp01((displayPhaseIndex - 1) / RarityCurveScalePhase);
|
||||
|
|
@ -178,7 +180,7 @@ namespace GeometryTD.CustomComponent
|
|||
return RarityType.None;
|
||||
}
|
||||
|
||||
float randomWeight = Random.value * totalWeight;
|
||||
float randomWeight = (float)(random.NextDouble() * totalWeight);
|
||||
float cumulativeWeight = 0f;
|
||||
foreach (var pair in _rarityWeightBuffer)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameFramework.DataTable;
|
||||
using GeometryTD.DataTable;
|
||||
|
|
@ -7,21 +6,16 @@ using GeometryTD.Factory;
|
|||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
using Random = UnityEngine.Random;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
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 _nodeSequenceIndex = -1;
|
||||
private int _nextDropTagOrdinal;
|
||||
private int _nextRewardTagOrdinal;
|
||||
private int _nextDropOrdinal;
|
||||
private int _nextRewardOrdinal;
|
||||
|
||||
private readonly List<DRShopPrice> _shopPriceRows = new();
|
||||
private IDataTable<DRShopPrice> _shopPriceTable;
|
||||
|
|
@ -32,19 +26,20 @@ namespace GeometryTD.CustomComponent
|
|||
private ShopGoodsBuilder _shopGoodsBuilder;
|
||||
private DropPoolRoller _dropPoolRoller;
|
||||
private RewardCandidateBuilder _rewardCandidateBuilder;
|
||||
private OutGameDropItemBuilder _outGameDropItemBuilder;
|
||||
|
||||
public void ConfigureRunContext(int runSeed, int sequenceIndex)
|
||||
{
|
||||
_runSeed = runSeed;
|
||||
_nodeSequenceIndex = sequenceIndex;
|
||||
_nextDropTagOrdinal = 0;
|
||||
_nextRewardTagOrdinal = 0;
|
||||
_nextDropOrdinal = 0;
|
||||
_nextRewardOrdinal = 0;
|
||||
}
|
||||
|
||||
public List<GoodsItemRawData> BuildShopGoods(int goodsCount, int runSeed = 0, int sequenceIndex = -1)
|
||||
{
|
||||
EnsureShopBuilder();
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex, AllocateTempInstanceId);
|
||||
return _shopGoodsBuilder.BuildGoods(goodsCount, runSeed, sequenceIndex);
|
||||
}
|
||||
|
||||
public EnemyDropResult ResolveEnemyDrop(in EnemyDropContext context)
|
||||
|
|
@ -61,14 +56,24 @@ namespace GeometryTD.CustomComponent
|
|||
? Mathf.Clamp01(enemy.DropPercent * 0.01f)
|
||||
: 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);
|
||||
}
|
||||
|
||||
TowerCompItemData lootItem = null;
|
||||
if (ShouldRollOutGameItem(context.DisplayPhaseIndex) &&
|
||||
TryRollOutGameItem(context.DisplayPhaseIndex, context.ThemeType, out TowerCompItemData droppedItem))
|
||||
if (OutGameDropRuleService.ShouldRollOutGameItem(context.DisplayPhaseIndex, random) &&
|
||||
TryRollOutGameItem(
|
||||
context.DisplayPhaseIndex,
|
||||
context.ThemeType,
|
||||
randomContext,
|
||||
random,
|
||||
out TowerCompItemData droppedItem))
|
||||
{
|
||||
lootItem = droppedItem;
|
||||
}
|
||||
|
|
@ -86,6 +91,7 @@ namespace GeometryTD.CustomComponent
|
|||
displayPhaseIndex,
|
||||
themeType,
|
||||
candidateCount,
|
||||
CreateNextRewardRandomContext,
|
||||
BuildRewardCandidateItem);
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +105,7 @@ namespace GeometryTD.CustomComponent
|
|||
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null ||
|
||||
_baseCompTable == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires ShopPrice, MuzzleComp, BearingComp, and BaseComp data tables.");
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +117,7 @@ namespace GeometryTD.CustomComponent
|
|||
DRShopPrice[] rows = _shopPriceTable.GetAllDataRows();
|
||||
if (rows == null || rows.Length <= 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
throw new System.InvalidOperationException(
|
||||
"InventoryGenerationComponent requires at least one shop price row.");
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +131,7 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
private long AllocateTempInstanceId()
|
||||
{
|
||||
return _nextTempInstanceId++;
|
||||
}
|
||||
|
||||
private void EnsureDropTables()
|
||||
{
|
||||
_dropPoolTable ??= GameEntry.DataTable.GetDataTable<DROutGameDropPool>();
|
||||
|
|
@ -153,7 +154,7 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -171,14 +172,22 @@ namespace GeometryTD.CustomComponent
|
|||
return _rewardCandidateBuilder;
|
||||
}
|
||||
|
||||
private static bool ShouldRollOutGameItem(int displayPhaseIndex)
|
||||
private OutGameDropItemBuilder EnsureOutGameDropItemBuilder()
|
||||
{
|
||||
int phaseIndex = Mathf.Max(1, displayPhaseIndex);
|
||||
float dropChance = Mathf.Clamp(DropChanceBase + (phaseIndex - 1) * DropChancePerPhase, 0f, DropChanceCap);
|
||||
return Random.value <= dropChance;
|
||||
EnsureDropTables();
|
||||
_outGameDropItemBuilder ??= new OutGameDropItemBuilder(
|
||||
_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;
|
||||
DropPoolRoller dropPoolRoller = EnsureDropPoolRoller();
|
||||
|
|
@ -186,120 +195,46 @@ namespace GeometryTD.CustomComponent
|
|||
if (!dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryBuildDropItem(
|
||||
selectedRow,
|
||||
selectedRarity,
|
||||
InventoryTagSourceType.Drop,
|
||||
AllocateDropTagOrdinal(),
|
||||
out droppedItem);
|
||||
return EnsureOutGameDropItemBuilder().TryBuildItem(selectedRow, selectedRarity, randomContext, 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,
|
||||
RarityType rarity,
|
||||
InventoryTagSourceType sourceType,
|
||||
int localOrdinal,
|
||||
out TowerCompItemData droppedItem)
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
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(),
|
||||
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))
|
||||
if (!EnsureOutGameDropItemBuilder().TryBuildItem(row, rarity, randomContext, 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 95b2ab672a7049ef9356a90d87baca50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 519549c7542f40efabf83a2b1b2dff44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using GeometryTD.DataTable;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
|
|
@ -19,7 +20,8 @@ namespace GeometryTD.CustomComponent
|
|||
int displayPhaseIndex,
|
||||
LevelThemeType themeType,
|
||||
int candidateCount,
|
||||
Func<DROutGameDropPool, RarityType, TowerCompItemData> buildRewardItem)
|
||||
Func<InventoryGenerationRandomContext> createRandomContext,
|
||||
Func<DROutGameDropPool, RarityType, InventoryGenerationRandomContext, TowerCompItemData> buildRewardItem)
|
||||
{
|
||||
int resolvedCount = Mathf.Max(0, candidateCount);
|
||||
if (resolvedCount <= 0)
|
||||
|
|
@ -36,9 +38,12 @@ namespace GeometryTD.CustomComponent
|
|||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
|
|
@ -50,7 +55,7 @@ namespace GeometryTD.CustomComponent
|
|||
continue;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity);
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
|
|
@ -63,16 +68,19 @@ namespace GeometryTD.CustomComponent
|
|||
while (candidates.Count < resolvedCount && attempts < maxAttempts)
|
||||
{
|
||||
attempts++;
|
||||
InventoryGenerationRandomContext randomContext = createRandomContext();
|
||||
Random random = randomContext.CreateRandom();
|
||||
if (!_dropPoolRoller.TryRollRow(
|
||||
phaseIndex,
|
||||
themeType,
|
||||
random,
|
||||
out DROutGameDropPool selectedRow,
|
||||
out RarityType selectedRarity) || selectedRow == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity);
|
||||
TowerCompItemData candidate = buildRewardItem(selectedRow, selectedRarity, randomContext);
|
||||
if (candidate == null)
|
||||
{
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using GeometryTD.Definition;
|
|||
using GeometryTD.Factory;
|
||||
using GeometryTD.UI;
|
||||
using UnityEngine;
|
||||
using Random = System.Random;
|
||||
|
||||
namespace GeometryTD.CustomComponent
|
||||
{
|
||||
|
|
@ -32,8 +33,7 @@ namespace GeometryTD.CustomComponent
|
|||
public List<GoodsItemRawData> BuildGoods(
|
||||
int goodsCount,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
int sequenceIndex)
|
||||
{
|
||||
if (goodsCount <= 0)
|
||||
{
|
||||
|
|
@ -43,7 +43,9 @@ namespace GeometryTD.CustomComponent
|
|||
List<GoodsItemRawData> goodsItems = new(goodsCount);
|
||||
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;
|
||||
|
|
@ -51,18 +53,17 @@ namespace GeometryTD.CustomComponent
|
|||
|
||||
private GoodsItemRawData BuildGoodsItem(
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext)
|
||||
{
|
||||
TowerCompItemData sourceItem = BuildRandomComponentItem(goodsIndex, runSeed, sequenceIndex, allocateInstanceId);
|
||||
Random random = randomContext.CreateRandom();
|
||||
TowerCompItemData sourceItem = BuildRandomComponentItem(randomContext, random);
|
||||
return new GoodsItemRawData
|
||||
{
|
||||
GoodsIndex = goodsIndex,
|
||||
Title = sourceItem.Name,
|
||||
TypeText = BuildTypeText(sourceItem.SlotType),
|
||||
Description = BuildDescription(sourceItem),
|
||||
Price = ResolveRandomPrice(sourceItem.Rarity),
|
||||
Price = ResolveRandomPrice(sourceItem.Rarity, random),
|
||||
Tags = sourceItem.Tags != null ? (TagType[])sourceItem.Tags.Clone() : Array.Empty<TagType>(),
|
||||
IconAreaContext = BuildIconAreaContext(sourceItem),
|
||||
SourceItem = sourceItem,
|
||||
|
|
@ -71,70 +72,56 @@ namespace GeometryTD.CustomComponent
|
|||
}
|
||||
|
||||
private TowerCompItemData BuildRandomComponentItem(
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
int slotRoll = UnityEngine.Random.Range(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[UnityEngine.Random.Range(0, _shopPriceRows.Count)];
|
||||
int slotRoll = random.Next(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[random.Next(0, _shopPriceRows.Count)];
|
||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
|
||||
priceRow != null ? priceRow.Rarity : RarityType.White);
|
||||
|
||||
return slotRoll switch
|
||||
{
|
||||
0 => BuildRandomMuzzleItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId),
|
||||
1 => BuildRandomBearingItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId),
|
||||
_ => BuildRandomBaseItem(rarity, goodsIndex, runSeed, sequenceIndex, allocateInstanceId)
|
||||
0 => BuildRandomMuzzleItem(rarity, randomContext, random),
|
||||
1 => BuildRandomBearingItem(rarity, randomContext, random),
|
||||
_ => BuildRandomBaseItem(rarity, randomContext, random)
|
||||
};
|
||||
}
|
||||
|
||||
private MuzzleCompItemData BuildRandomMuzzleItem(
|
||||
RarityType rarity,
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
|
||||
DRMuzzleComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
|
||||
long instanceId = allocateInstanceId();
|
||||
InventoryTagRandomContext randomContext =
|
||||
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
|
||||
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext);
|
||||
DRMuzzleComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateMuzzle(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BearingCompItemData BuildRandomBearingItem(
|
||||
RarityType rarity,
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
|
||||
DRBearingComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
|
||||
long instanceId = allocateInstanceId();
|
||||
InventoryTagRandomContext randomContext =
|
||||
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
|
||||
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext);
|
||||
DRBearingComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBearing(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private BaseCompItemData BuildRandomBaseItem(
|
||||
RarityType rarity,
|
||||
int goodsIndex,
|
||||
int runSeed,
|
||||
int sequenceIndex,
|
||||
Func<long> allocateInstanceId)
|
||||
InventoryGenerationRandomContext randomContext,
|
||||
Random random)
|
||||
{
|
||||
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
|
||||
DRBaseComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
|
||||
long instanceId = allocateInstanceId();
|
||||
InventoryTagRandomContext randomContext =
|
||||
InventoryTagRandomContext.CreateShop(runSeed, sequenceIndex, goodsIndex, config.Id);
|
||||
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext);
|
||||
DRBaseComp config = rows[random.Next(0, rows.Length)];
|
||||
long instanceId = randomContext.CreateStableItemInstanceId();
|
||||
return ComponentItemFactory.CreateBase(config, instanceId, rarity, randomContext.CreateTagRandomContext(config.Id));
|
||||
}
|
||||
|
||||
private int ResolveRandomPrice(RarityType rarity)
|
||||
private int ResolveRandomPrice(RarityType rarity, Random random)
|
||||
{
|
||||
for (int i = 0; i < _shopPriceRows.Count; i++)
|
||||
{
|
||||
|
|
@ -143,7 +130,7 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
int min = Mathf.Max(0, row.MinPrice);
|
||||
int max = Mathf.Max(min, row.MaxPrice);
|
||||
return UnityEngine.Random.Range(min, max + 1);
|
||||
return random.Next(min, max + 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 523d63397a9c41fdaaf238bfb0e95357
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a268c57d4373494ab9b4d7167099f170
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c5915901f51c430db50725908113ae1a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -196,45 +196,6 @@ namespace GeometryTD.Procedure
|
|||
|
||||
_loadedFlag[ne.DataTableAssetName] = true;
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ namespace GeometryTD.Procedure
|
|||
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
|
||||
GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
|
||||
|
||||
GameEntry.TagRegistry.OnInit();
|
||||
GameEntry.EventNode.OnInit();
|
||||
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
|
||||
GameEntry.ShopNode.OnInit();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using GeometryTD.Definition;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace GeometryTD.UI
|
||||
{
|
||||
|
|
@ -19,6 +18,7 @@ namespace GeometryTD.UI
|
|||
private bool _allowRefreshOnce = true;
|
||||
private bool _allowGiveUp = true;
|
||||
private bool _hasRefreshed;
|
||||
private int _selectionOffset;
|
||||
private string _tipText = "Select one reward";
|
||||
|
||||
public void ConfigureRewardPool(
|
||||
|
|
@ -49,6 +49,7 @@ namespace GeometryTD.UI
|
|||
_allowGiveUp = allowGiveUp;
|
||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||
_hasRefreshed = false;
|
||||
_selectionOffset = 0;
|
||||
_currentModel = null;
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ namespace GeometryTD.UI
|
|||
_allowGiveUp = allowGiveUp;
|
||||
_tipText = string.IsNullOrWhiteSpace(tipText) ? "Select one reward" : tipText;
|
||||
_hasRefreshed = false;
|
||||
_selectionOffset = 0;
|
||||
_currentModel = null;
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +117,11 @@ namespace GeometryTD.UI
|
|||
}
|
||||
|
||||
_hasRefreshed = true;
|
||||
if (_rewardPool.Count > 0)
|
||||
{
|
||||
_selectionOffset = (_selectionOffset + _displayCount) % _rewardPool.Count;
|
||||
}
|
||||
|
||||
_currentModel = BuildModel();
|
||||
return _currentModel;
|
||||
}
|
||||
|
|
@ -172,22 +179,11 @@ namespace GeometryTD.UI
|
|||
}
|
||||
|
||||
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];
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ Transform:
|
|||
- {fileID: 1322505022}
|
||||
- {fileID: 1062564689}
|
||||
- {fileID: 1554147129}
|
||||
- {fileID: 336799288}
|
||||
m_Father: {fileID: 1852670053}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &120093239
|
||||
|
|
@ -294,6 +295,50 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 0a0c1c547ca24c95819f5f62f0bd3ea3, type: 3}
|
||||
m_Name:
|
||||
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
|
||||
PrefabInstance:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3236620385724df2a9146f077f7413ae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
- 商店、敌人掉落、满血 3 选 1 候选都在各自生成组件实例,逻辑分散,后续改规则要改多处。
|
||||
- `ShopFormUseCase`、旧 `EnemyDropResolver`、`CombatSettlementService` 都曾同时承担“流程 + 规则 + 数据组装”的混合职责。
|
||||
- `runSeed` 目前只稳定了 Tag 生成,没有稳定整个组件产出流程。
|
||||
- 重构前 `runSeed` 只稳定了 Tag 生成,没有稳定整个组件产出流程;当前已补齐到商店、掉落、奖励候选与奖励展示顺序。
|
||||
- Tag 模块本身已经分成 `Generation / Aggregation / Combat / Metadata / Presentation`,但初始化入口还散在流程代码里。
|
||||
|
||||
## 重构边界
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
- `TagDefinitionRegistry`
|
||||
- `TagGenerationRuleRegistry`
|
||||
- `RarityTagBudgetRuleRegistry`
|
||||
- 替代当前散在 `ProcedurePreload` 里的 Tag 注册表装载逻辑。
|
||||
- 替代旧的 `ProcedurePreload` 直连刷新逻辑,并收口当前主流程初始化入口。
|
||||
|
||||
### 不适合抽成 `GameFrameworkComponent` 的模块
|
||||
|
||||
|
|
@ -54,6 +54,8 @@
|
|||
|
||||
- `Assets/GameMain/Scripts/Factory/ComponentItemFactory.cs`
|
||||
- 唯一负责“从配置行构造 `TowerCompItemData`”。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationRandomContext.cs`
|
||||
- 统一承载商店 / 掉落 / 奖励候选的随机合同。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/ShopGoodsBuilder.cs`
|
||||
- 只负责商店货物生成。
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/DropPoolRoller.cs`
|
||||
|
|
@ -101,7 +103,9 @@
|
|||
|
||||
- `ProcedurePreload`
|
||||
- 移除 Tag 注册表刷新细节。
|
||||
- 只保留“数据表加载完成后通知组件刷新”。
|
||||
- 只保留资源与数据表预加载职责。
|
||||
- `ProcedureMain`
|
||||
- 进入主流程时主动调用 `GameEntry.TagRegistry.OnInit()`。
|
||||
- `TagRegistryComponent`
|
||||
- 成为 Tag 相关表与注册表的唯一运行时入口。
|
||||
|
||||
|
|
@ -157,9 +161,9 @@
|
|||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|----------------------------------|--------------------|
|
||||
| [ ] | G4-01 | 新增 `TagRegistryComponent` | 建立 Tag 运行时入口 |
|
||||
| [ ] | G4-02 | 挪出 `ProcedurePreload` 中 Tag 注册逻辑 | 流程代码不再直接维护 Tag 注册表 |
|
||||
| [ ] | G4-03 | 对齐 Tag 加载时机与重载入口 | 初始化路径明确 |
|
||||
| [x] | G4-01 | 新增 `TagRegistryComponent` | 建立 Tag 运行时入口 |
|
||||
| [x] | G4-02 | 挪出 `ProcedurePreload` 中 Tag 注册逻辑 | 流程代码不再直接维护 Tag 注册表 |
|
||||
| [x] | G4-03 | 对齐 Tag 加载时机与重载入口 | 初始化路径明确 |
|
||||
|
||||
### G4 验收标准
|
||||
|
||||
|
|
@ -171,9 +175,9 @@
|
|||
|
||||
| 状态 | ID | 任务 | 目标 |
|
||||
|-----|-------|------------------------|---------------------|
|
||||
| [ ] | G5-01 | 统一商店 / 掉落 / 奖励候选的随机上下文 | 全部产出链路使用一致合同 |
|
||||
| [ ] | G5-02 | 补齐整体产出的 `runSeed` 稳定性 | 不只稳定 Tag,也稳定物品产出 |
|
||||
| [ ] | G5-03 | 增加对应 EditMode 测试 | 同 Run 可复现,跨 Run 可区分 |
|
||||
| [x] | G5-01 | 统一商店 / 掉落 / 奖励候选的随机上下文 | 全部产出链路使用一致合同 |
|
||||
| [x] | G5-02 | 补齐整体产出的 `runSeed` 稳定性 | 不只稳定 Tag,也稳定物品产出 |
|
||||
| [x] | G5-03 | 增加对应 EditMode 测试 | 同 Run 可复现,跨 Run 可区分 |
|
||||
|
||||
### G5 验收标准
|
||||
|
||||
|
|
@ -188,6 +192,7 @@
|
|||
- `Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSettlementService.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/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs`
|
||||
|
||||
|
|
@ -196,6 +201,7 @@
|
|||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/InventoryGenerationComponent.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/TagRegistry/TagRegistryComponent.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/DropPoolRoller.cs`
|
||||
- `Assets/GameMain/Scripts/CustomComponent/InventoryGeneration/RewardCandidateBuilder.cs`
|
||||
|
|
@ -215,7 +221,7 @@
|
|||
2. 先替换商店货物生成。
|
||||
3. 再替换战斗掉落与奖励候选生成。
|
||||
4. 再拆战斗结算计算与提交。
|
||||
5. 最后收 `TagRegistryComponent` 与 `ProcedurePreload` 的 Tag 初始化入口。
|
||||
5. 最后收 `TagRegistryComponent` 与主流程中的 Tag 初始化入口。
|
||||
|
||||
## 通过标准
|
||||
|
||||
|
|
@ -223,3 +229,12 @@
|
|||
- 掉落、商店、奖励候选不再各自维护相似逻辑。
|
||||
- Tag 模块保留现有分层,不再继续和流程代码缠在一起。
|
||||
- `GameFrameworkComponent` 只承接运行时入口,不承接纯规则实现。
|
||||
|
||||
## 当前结果
|
||||
|
||||
- `InventoryGenerationComponent` 已成为商店、战斗掉落、奖励候选的统一运行时入口。
|
||||
- 掉落概率规则与掉落物品构造已继续下沉到 `OutGameDropRuleService` 与 `OutGameDropItemBuilder`,`InventoryGenerationComponent` 保持入口编排职责。
|
||||
- `TagRegistryComponent` 已成为 Tag 运行时入口,并由 `ProcedureMain` 主动初始化。
|
||||
- `TagRegistryComponent` 已改为 fail-fast 初始化,缺少必要数据表时会直接暴露错误。
|
||||
- `InventoryGenerationRandomContext` 已统一商店、掉落、奖励候选的随机合同,并补齐稳定临时实例 Id。
|
||||
- `Assets/Tests/EditMode/InventoryGenerationStabilityTests.cs` 已覆盖 G5 的可复现性验收点。
|
||||
|
|
|
|||
|
|
@ -310,12 +310,30 @@
|
|||
- 在内部编排:
|
||||
- `DropPoolRoller`
|
||||
- `RewardCandidateBuilder`
|
||||
- `ComponentItemFactory`
|
||||
- `OutGameDropRuleService`
|
||||
- `OutGameDropItemBuilder`
|
||||
- `InventoryGenerationRandomContext`
|
||||
|
||||
约束:
|
||||
- `CombatNode` 域不直接持有或复制组件产出规则。
|
||||
- `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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Tag System Design
|
||||
|
||||
最后更新:2026-03-12
|
||||
最后更新:2026-03-13
|
||||
|
||||
> 目标:这是 GeometryTD 当前 Tag 系统的唯一正式口径。
|
||||
> 本文档只记录当前真实实现、当前边界与正式规则。
|
||||
|
|
@ -76,7 +76,9 @@ M1 已完成 Tag 最小闭环:
|
|||
- 当前负责 `MinRarity`、`Weight`
|
||||
- `Tag.txt + TagConfig.txt -> TagDefinitionRegistry.ReloadFromRows(...)`
|
||||
- 当前负责 `IsImplemented`、`TriggerPhase`、`Description` 与正式运行时参数
|
||||
- `ProcedurePreload` 在 `Tag` 或 `TagConfig` 任一表加载完成后,都会基于当前已加载的两张表组合重建定义层
|
||||
- `ProcedureMain -> GameEntry.TagRegistry.OnInit() -> TagRegistryComponent`
|
||||
- 当前在主流程进入时统一基于已加载数据表重建 Tag 定义层与生成规则层
|
||||
- `TagRegistryComponent` 对 `TagConfig`、`Tag`、`RarityTagBudget` 三张表采用 fail-fast 依赖约束;缺表时直接暴露初始化错误,不再静默跳过
|
||||
- `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry`
|
||||
- 当前负责按品质读取 `MinCount / MaxCount`
|
||||
|
||||
|
|
@ -100,9 +102,9 @@ public sealed class TagRuntimeData
|
|||
当前统一入口是 `ComponentTagGenerationService`,以下链路共用同一套规则:
|
||||
|
||||
- `InventorySeedUtility`
|
||||
- `ShopFormUseCase`
|
||||
- `EnemyDropResolver`
|
||||
- 结算奖励候选组件
|
||||
- `InventoryGenerationComponent.BuildShopGoods(...)`
|
||||
- `InventoryGenerationComponent.ResolveEnemyDrop(...)`
|
||||
- `InventoryGenerationComponent.BuildRewardCandidates(...)`
|
||||
|
||||
当前流程:
|
||||
|
||||
|
|
@ -124,7 +126,15 @@ Tag 随机结果的正式上下文为:
|
|||
- `ItemInstanceId`
|
||||
- `ConfigId`
|
||||
|
||||
当前运行时通过 `InventoryTagRandomContext` 统一承载上述字段。
|
||||
当前运行时通过 `InventoryGenerationRandomContext + InventoryTagRandomContext` 统一承载上述字段:
|
||||
|
||||
- `InventoryGenerationRandomContext`
|
||||
- 统一承载 `runSeed + nodeSequenceIndex + sourceType + localOrdinal`
|
||||
- 统一派生产出链路自己的稳定随机流
|
||||
- 统一派生稳定的临时组件 `InstanceId`
|
||||
- `InventoryTagRandomContext`
|
||||
- 承载 Tag 生成所需的 `RunSeed / SourceType / ItemInstanceId / ConfigId`
|
||||
- 由 `InventoryGenerationRandomContext` 进一步派生
|
||||
|
||||
各来源构造口径:
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue