S7-01 + S7-02

This commit is contained in:
SepComet 2026-03-12 10:46:11 +08:00
parent d34af661b9
commit 185ea43323
20 changed files with 468 additions and 112 deletions

View File

@ -178,6 +178,7 @@ namespace GeometryTD.CustomComponent
public void StartCombat(
int levelId = 0,
string runId = null,
int runSeed = 0,
int nodeId = 0,
RunNodeType nodeType = RunNodeType.None,
int sequenceIndex = -1)
@ -232,6 +233,7 @@ namespace GeometryTD.CustomComponent
phaseList,
_selectedSpawnEntriesByPhaseId,
runId,
runSeed,
nodeId,
nodeType,
sequenceIndex))

View File

@ -70,6 +70,7 @@ namespace GeometryTD.CustomComponent
IReadOnlyList<DRLevelPhase> phases,
IReadOnlyDictionary<int, IReadOnlyList<DRLevelSpawnEntry>> spawnEntriesByPhaseId,
string runId = null,
int runSeed = 0,
int nodeId = 0,
RunNodeType nodeType = RunNodeType.None,
int sequenceIndex = -1)
@ -95,9 +96,11 @@ namespace GeometryTD.CustomComponent
_runtime.CurrentLevel = level;
_runtime.RunId = runId;
_runtime.RunSeed = runSeed;
_runtime.NodeId = nodeId;
_runtime.NodeType = nodeType;
_runtime.SequenceIndex = sequenceIndex;
_runtime.EnemyDropResolver.ConfigureRunContext(runSeed, sequenceIndex);
_runtime.CombatRunResourceStore.InitializeForCombat(level);
for (int i = 0; i < phases.Count; i++)
{

View File

@ -57,6 +57,7 @@ namespace GeometryTD.CustomComponent
_runtime.IsCompleted = false;
_runtime.NodeEnterFired = false;
_runtime.RunId = null;
_runtime.RunSeed = 0;
_runtime.NodeId = 0;
_runtime.NodeType = RunNodeType.None;
_runtime.SequenceIndex = -1;

View File

@ -31,6 +31,7 @@ namespace GeometryTD.CustomComponent
public bool NodeEnterFired { get; set; }
public CombatSettlementContext SettlementContext { get; set; }
public string RunId { get; set; }
public int RunSeed { get; set; }
public int NodeId { get; set; }
public RunNodeType NodeType { get; set; }
public int SequenceIndex { get; set; }

View File

@ -22,12 +22,28 @@ namespace GeometryTD.CustomComponent
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
private long _nextDropItemInstanceId = 1;
private int _runSeed;
private int _nodeSequenceIndex = -1;
private int _nextDropTagOrdinal;
private int _nextRewardTagOrdinal;
public void Reset()
{
_eligibleDropPoolBuffer.Clear();
_rarityRollWeightBuffer.Clear();
_nextDropItemInstanceId = 1;
_runSeed = 0;
_nodeSequenceIndex = -1;
_nextDropTagOrdinal = 0;
_nextRewardTagOrdinal = 0;
}
public void ConfigureRunContext(int runSeed, int nodeSequenceIndex)
{
_runSeed = runSeed;
_nodeSequenceIndex = nodeSequenceIndex;
_nextDropTagOrdinal = 0;
_nextRewardTagOrdinal = 0;
}
public EnemyDropResult Resolve(in EnemyDropContext context)
@ -90,7 +106,7 @@ namespace GeometryTD.CustomComponent
continue;
}
if (!TryBuildDropItem(selectedRow, out TowerCompItemData droppedItem) || droppedItem == null)
if (!TryBuildDropItem(selectedRow, InventoryTagSourceType.Reward, AllocateRewardTagOrdinal(), out TowerCompItemData droppedItem) || droppedItem == null)
{
continue;
}
@ -129,7 +145,7 @@ namespace GeometryTD.CustomComponent
return false;
}
return TryBuildDropItem(selectedRow, out droppedItem);
return TryBuildDropItem(selectedRow, InventoryTagSourceType.Drop, AllocateDropTagOrdinal(), out droppedItem);
}
private bool TryPickDropPoolRow(int displayPhaseIndex, LevelThemeType themeType, out DROutGameDropPool selectedRow)
@ -303,7 +319,11 @@ namespace GeometryTD.CustomComponent
return _drOutGameDropPool;
}
private bool TryBuildDropItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
private bool TryBuildDropItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
if (row == null || row.ItemId <= 0 || string.IsNullOrWhiteSpace(row.ItemType))
@ -314,23 +334,27 @@ namespace GeometryTD.CustomComponent
string itemType = row.ItemType.Trim();
if (itemType.Equals("MuzzleComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildMuzzleCompItem(row, out droppedItem);
return TryBuildMuzzleCompItem(row, sourceType, localOrdinal, out droppedItem);
}
if (itemType.Equals("BearingComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildBearingCompItem(row, out droppedItem);
return TryBuildBearingCompItem(row, sourceType, localOrdinal, out droppedItem);
}
if (itemType.Equals("BaseComp", StringComparison.OrdinalIgnoreCase))
{
return TryBuildBaseCompItem(row, out droppedItem);
return TryBuildBaseCompItem(row, sourceType, localOrdinal, out droppedItem);
}
return false;
}
private bool TryBuildMuzzleCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
private bool TryBuildMuzzleCompItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
@ -358,9 +382,7 @@ namespace GeometryTD.CustomComponent
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
rarity,
InventoryTagSourceType.Drop,
instanceId,
config.Id),
CreateRandomContext(sourceType, localOrdinal, config.Id)),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
@ -368,7 +390,11 @@ namespace GeometryTD.CustomComponent
return true;
}
private bool TryBuildBearingCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
private bool TryBuildBearingCompItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBearingComp ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
@ -396,16 +422,18 @@ namespace GeometryTD.CustomComponent
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
rarity,
InventoryTagSourceType.Drop,
instanceId,
config.Id),
CreateRandomContext(sourceType, localOrdinal, config.Id)),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
return true;
}
private bool TryBuildBaseCompItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
private bool TryBuildBaseCompItem(
DROutGameDropPool row,
InventoryTagSourceType sourceType,
int localOrdinal,
out TowerCompItemData droppedItem)
{
droppedItem = null;
_drBaseComp ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
@ -433,13 +461,33 @@ namespace GeometryTD.CustomComponent
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
rarity,
InventoryTagSourceType.Drop,
instanceId,
config.Id),
CreateRandomContext(sourceType, localOrdinal, config.Id)),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};
return true;
}
private InventoryTagRandomContext CreateRandomContext(
InventoryTagSourceType sourceType,
int localOrdinal,
int configId)
{
return sourceType switch
{
InventoryTagSourceType.Reward => InventoryTagRandomContext.CreateReward(_runSeed, _nodeSequenceIndex, localOrdinal, configId),
_ => InventoryTagRandomContext.CreateDrop(_runSeed, _nodeSequenceIndex, localOrdinal, configId)
};
}
private int AllocateDropTagOrdinal()
{
return _nextDropTagOrdinal++;
}
private int AllocateRewardTagOrdinal()
{
return _nextRewardTagOrdinal++;
}
}
}

View File

@ -24,10 +24,10 @@ namespace GeometryTD.CustomComponent
}
}
public void OnInit()
public void OnInit(BackpackInventoryData initialInventory = null)
{
EnsureServices();
_commandModel.Initialize(InventorySeedUtility.CreateSampleInventory(), MaxParticipantTowerCount);
_commandModel.Initialize(initialInventory ?? InventorySeedUtility.CreateSampleInventory(), MaxParticipantTowerCount);
BackpackInventoryData inventory = _queryModel.Inventory;
Log.Info(

View File

@ -9,6 +9,7 @@ namespace GeometryTD.CustomComponent
public class ShopNodeComponent : GameFrameworkComponent
{
private string _activeRunId;
private int _activeRunSeed;
private int _activeNodeId;
private RunNodeType _activeNodeType = RunNodeType.None;
private int _activeSequenceIndex = -1;
@ -28,20 +29,21 @@ namespace GeometryTD.CustomComponent
_initialized = true;
}
public void StartShop(string runId = null, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
public void StartShop(string runId = null, int runSeed = 0, int nodeId = 0, RunNodeType nodeType = RunNodeType.None, int sequenceIndex = -1)
{
if (!_initialized)
{
OnInit();
}
if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen())
if (_shopFormUseCase == null || !_shopFormUseCase.PrepareForOpen(runSeed, sequenceIndex))
{
Log.Warning("ShopNodeComponent.StartShop() failed. Shop use case is unavailable or goods generation failed.");
return;
}
_activeRunId = runId;
_activeRunSeed = runSeed;
_activeNodeId = nodeId;
_activeNodeType = nodeType;
_activeSequenceIndex = sequenceIndex;
@ -68,6 +70,7 @@ namespace GeometryTD.CustomComponent
private void ClearActiveNodeContext()
{
_activeRunId = null;
_activeRunSeed = 0;
_activeNodeId = 0;
_activeNodeType = RunNodeType.None;
_activeSequenceIndex = -1;

View File

@ -10,9 +10,7 @@ namespace GeometryTD.Definition
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagSourceType sourceType,
long itemInstanceId,
int configId,
InventoryTagRandomContext randomContext,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
@ -23,7 +21,7 @@ namespace GeometryTD.Definition
return Array.Empty<TagType>();
}
Random random = new Random(BuildStableSeed(rarity, sourceType, itemInstanceId, configId));
Random random = new Random(BuildStableSeed(rarity, randomContext));
int tagBudget = ResolveRarityTagBudget(rarity, random, rarityTagBudgetRulesByRarity);
if (tagBudget <= 0)
{
@ -43,6 +41,23 @@ namespace GeometryTD.Definition
return result;
}
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagSourceType sourceType,
long itemInstanceId,
int configId,
IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{
return ResolveComponentTags(
possibleTags,
rarity,
new InventoryTagRandomContext(0, sourceType, itemInstanceId, configId),
rulesByTag,
rarityTagBudgetRulesByRarity);
}
public static TagType[] GetEligibleTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
@ -170,20 +185,17 @@ namespace GeometryTD.Definition
return pool.Count - 1;
}
private static int BuildStableSeed(
RarityType rarity,
InventoryTagSourceType sourceType,
long itemInstanceId,
int configId)
private static int BuildStableSeed(RarityType rarity, InventoryTagRandomContext randomContext)
{
unchecked
{
int seed = 17;
seed = seed * 31 + randomContext.RunSeed;
seed = seed * 31 + (int)InventoryRarityRuleService.NormalizeComponentRarity(rarity);
seed = seed * 31 + (int)sourceType;
seed = seed * 31 + configId;
seed = seed * 31 + (int)itemInstanceId;
seed = seed * 31 + (int)(itemInstanceId >> 32);
seed = seed * 31 + (int)randomContext.SourceType;
seed = seed * 31 + randomContext.ConfigId;
seed = seed * 31 + (int)randomContext.ItemInstanceId;
seed = seed * 31 + (int)(randomContext.ItemInstanceId >> 32);
return seed;
}
}

View File

@ -0,0 +1,63 @@
using System;
namespace GeometryTD.Definition
{
[Serializable]
public readonly struct InventoryTagRandomContext
{
public InventoryTagRandomContext(int runSeed, InventoryTagSourceType sourceType, long itemInstanceId, int configId)
{
RunSeed = runSeed;
SourceType = sourceType;
ItemInstanceId = itemInstanceId;
ConfigId = configId;
}
public int RunSeed { get; }
public InventoryTagSourceType SourceType { get; }
public long ItemInstanceId { get; }
public int ConfigId { get; }
public static InventoryTagRandomContext CreateSeed(int runSeed, long itemInstanceId, int configId)
{
return new InventoryTagRandomContext(runSeed, InventoryTagSourceType.Seed, itemInstanceId, configId);
}
public static InventoryTagRandomContext CreateShop(int runSeed, int nodeSequenceIndex, int goodsIndex, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Shop,
ComposeLocalItemInstanceId(nodeSequenceIndex, goodsIndex),
configId);
}
public static InventoryTagRandomContext CreateDrop(int runSeed, int nodeSequenceIndex, int dropOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Drop,
ComposeLocalItemInstanceId(nodeSequenceIndex, dropOrdinal),
configId);
}
public static InventoryTagRandomContext CreateReward(int runSeed, int nodeSequenceIndex, int rewardOrdinal, int configId)
{
return new InventoryTagRandomContext(
runSeed,
InventoryTagSourceType.Reward,
ComposeLocalItemInstanceId(nodeSequenceIndex, rewardOrdinal),
configId);
}
public static long ComposeLocalItemInstanceId(int nodeSequenceIndex, int localOrdinal)
{
long normalizedSequence = Math.Max(0, nodeSequenceIndex) + 1L;
long normalizedOrdinal = Math.Max(0, localOrdinal) + 1L;
return (normalizedSequence << 32) | (uint)normalizedOrdinal;
}
}
}

View File

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

View File

@ -22,11 +22,7 @@ namespace GeometryTD.Definition
public static void LoadFromRows(IEnumerable<DRTagConfig> rows)
{
ResetToDefaults();
foreach (DRTagConfig row in rows)
{
ApplyRow(row);
}
ReloadFromRows(rows, null);
}
public static bool TryGetDefinition(TagType tagType, out TagDefinition definition)
@ -47,6 +43,27 @@ namespace GeometryTD.Definition
}
}
public static void ReloadFromRows(IEnumerable<DRTagConfig> tagConfigRows, IEnumerable<DRTag> tagRows)
{
ResetToDefaults();
if (tagConfigRows != null)
{
foreach (DRTagConfig row in tagConfigRows)
{
ApplyRow(row);
}
}
if (tagRows != null)
{
foreach (DRTag row in tagRows)
{
ApplyTagRow(row);
}
}
}
private static Dictionary<TagType, TagDefinition> CreateDefaultDefinitions()
{
return new Dictionary<TagType, TagDefinition>

View File

@ -199,22 +199,12 @@ namespace GeometryTD.Procedure
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("TagConfig", false))
{
var tagConfigTable = GameEntry.DataTable.GetDataTable<DRTagConfig>();
if (tagConfigTable != null)
{
TagDefinitionRegistry.LoadFromRows(tagConfigTable.GetAllDataRows());
}
ReloadTagRegistriesFromLoadedTables();
}
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("Tag", false))
{
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null)
{
DRTag[] rows = tagTable.GetAllDataRows();
TagGenerationRuleRegistry.LoadFromRows(rows);
TagDefinitionRegistry.ApplyTagRows(rows);
}
ReloadTagRegistriesFromLoadedTables();
}
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("RarityTagBudget", false))
@ -227,6 +217,26 @@ namespace GeometryTD.Procedure
}
}
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)
{
LoadDataTableFailureEventArgs ne = (LoadDataTableFailureEventArgs)e;

View File

@ -1,7 +1,9 @@
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using System;
using GeometryTD.CustomEvent;
using GeometryTD.CustomUtility;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
@ -34,13 +36,19 @@ namespace GeometryTD.Procedure
GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit();
GameEntry.PlayerInventory?.OnInit();
string runId = Guid.NewGuid().ToString("N");
int runSeed = RunStateFactory.CreateRunSeed();
BackpackInventoryData initialInventory = InventorySeedUtility.CreateSampleInventory(runSeed);
GameEntry.PlayerInventory?.OnInit(initialInventory);
_currentRunState = RunStateFactory.CreateFixedRun(
LevelThemeType.Plain,
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
: initialInventory,
runId,
runSeed);
_repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
@ -259,6 +267,7 @@ namespace GeometryTD.Procedure
GameEntry.CombatNode.StartCombat(
currentNode.LinkedLevelId,
_currentRunState.RunId,
_currentRunState.RunSeed,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);
@ -273,6 +282,7 @@ namespace GeometryTD.Procedure
case RunNodeType.Shop:
GameEntry.ShopNode.StartShop(
_currentRunState.RunId,
_currentRunState.RunSeed,
currentNode.NodeId,
currentNode.NodeType,
currentNode.SequenceIndex);

View File

@ -71,11 +71,13 @@ namespace GeometryTD.Procedure
internal RunState(
string runId,
int runSeed,
LevelThemeType themeType,
List<RunNodeState> nodes,
BackpackInventoryData runInventorySnapshot)
{
RunId = string.IsNullOrWhiteSpace(runId) ? Guid.NewGuid().ToString("N") : runId;
RunSeed = runSeed == 0 ? RunStateFactory.CreateRunSeed() : runSeed;
ThemeType = themeType;
_nodes = nodes ?? new List<RunNodeState>();
RunInventorySnapshot = InventoryCloneUtility.CloneInventory(runInventorySnapshot);
@ -85,6 +87,8 @@ namespace GeometryTD.Procedure
public string RunId { get; internal set; }
public int RunSeed { get; internal set; }
public LevelThemeType ThemeType { get; internal set; }
public int CurrentNodeIndex { get; internal set; }

View File

@ -8,17 +8,19 @@ namespace GeometryTD.Procedure
public static RunState CreateFixedRun(
LevelThemeType themeType,
BackpackInventoryData initialInventorySnapshot,
string runId = null)
string runId = null,
int runSeed = 0)
{
IReadOnlyList<RunNodeSeed> fixedNodeSeeds = FixedRunNodeSequenceBuilder.Build(themeType);
return Create(themeType, initialInventorySnapshot, fixedNodeSeeds, runId);
return Create(themeType, initialInventorySnapshot, fixedNodeSeeds, runId, runSeed);
}
public static RunState Create(
LevelThemeType themeType,
BackpackInventoryData initialInventorySnapshot,
IEnumerable<RunNodeSeed> nodeSeeds,
string runId = null)
string runId = null,
int runSeed = 0)
{
List<RunNodeState> nodes = new List<RunNodeState>();
if (nodeSeeds != null)
@ -44,7 +46,13 @@ namespace GeometryTD.Procedure
}
}
return new RunState(runId, themeType, nodes, initialInventorySnapshot);
return new RunState(runId, runSeed, themeType, nodes, initialInventorySnapshot);
}
public static int CreateRunSeed()
{
int seed = System.Guid.NewGuid().GetHashCode();
return seed == 0 ? 1 : seed;
}
}
}

View File

@ -13,6 +13,8 @@ namespace GeometryTD.UI
{
private const int GoodsCount = 4;
private long _nextTempInstanceId = 1000000;
private int _activeRunSeed;
private int _activeSequenceIndex = -1;
private readonly List<GoodsItemRawData> _currentGoods = new List<GoodsItemRawData>(GoodsCount);
private readonly List<DRShopPrice> _shopPriceRows = new List<DRShopPrice>();
@ -21,13 +23,15 @@ namespace GeometryTD.UI
private IDataTable<DRBearingComp> _bearingCompTable;
private IDataTable<DRBaseComp> _baseCompTable;
public bool PrepareForOpen()
public bool PrepareForOpen(int runSeed = 0, int sequenceIndex = -1)
{
if (!EnsureTables())
{
return false;
}
_activeRunSeed = runSeed;
_activeSequenceIndex = sequenceIndex;
_currentGoods.Clear();
for (int i = 0; i < GoodsCount; i++)
{
@ -132,7 +136,7 @@ namespace GeometryTD.UI
private bool TryBuildRandomGoodsItem(int goodsIndex, out GoodsItemRawData goodsItem)
{
goodsItem = null;
TowerCompItemData sourceItem = BuildRandomComponentItem();
TowerCompItemData sourceItem = BuildRandomComponentItem(goodsIndex);
if (sourceItem == null)
{
return false;
@ -153,7 +157,7 @@ namespace GeometryTD.UI
return true;
}
private TowerCompItemData BuildRandomComponentItem()
private TowerCompItemData BuildRandomComponentItem(int goodsIndex)
{
int slotRoll = UnityEngine.Random.Range(0, 3);
DRShopPrice priceRow = _shopPriceRows[UnityEngine.Random.Range(0, _shopPriceRows.Count)];
@ -163,15 +167,15 @@ namespace GeometryTD.UI
switch (slotRoll)
{
case 0:
return BuildRandomMuzzleItem(rarity);
return BuildRandomMuzzleItem(rarity, goodsIndex);
case 1:
return BuildRandomBearingItem(rarity);
return BuildRandomBearingItem(rarity, goodsIndex);
default:
return BuildRandomBaseItem(rarity);
return BuildRandomBaseItem(rarity, goodsIndex);
}
}
private MuzzleCompItemData BuildRandomMuzzleItem(RarityType rarity)
private MuzzleCompItemData BuildRandomMuzzleItem(RarityType rarity, int goodsIndex)
{
DRMuzzleComp[] rows = _muzzleCompTable.GetAllDataRows();
DRMuzzleComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
@ -188,16 +192,14 @@ namespace GeometryTD.UI
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
normalizedRarity,
InventoryTagSourceType.Shop,
instanceId,
config.Id),
InventoryTagRandomContext.CreateShop(_activeRunSeed, _activeSequenceIndex, goodsIndex, config.Id)),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
};
}
private BearingCompItemData BuildRandomBearingItem(RarityType rarity)
private BearingCompItemData BuildRandomBearingItem(RarityType rarity, int goodsIndex)
{
DRBearingComp[] rows = _bearingCompTable.GetAllDataRows();
DRBearingComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
@ -214,15 +216,13 @@ namespace GeometryTD.UI
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
normalizedRarity,
InventoryTagSourceType.Shop,
instanceId,
config.Id),
InventoryTagRandomContext.CreateShop(_activeRunSeed, _activeSequenceIndex, goodsIndex, config.Id)),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
}
private BaseCompItemData BuildRandomBaseItem(RarityType rarity)
private BaseCompItemData BuildRandomBaseItem(RarityType rarity, int goodsIndex)
{
DRBaseComp[] rows = _baseCompTable.GetAllDataRows();
DRBaseComp config = rows[UnityEngine.Random.Range(0, rows.Length)];
@ -239,9 +239,7 @@ namespace GeometryTD.UI
Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag,
normalizedRarity,
InventoryTagSourceType.Shop,
instanceId,
config.Id),
InventoryTagRandomContext.CreateShop(_activeRunSeed, _activeSequenceIndex, goodsIndex, config.Id)),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};

View File

@ -12,7 +12,7 @@ namespace GeometryTD.CustomUtility
private static readonly TagType[] IceAbsoluteZeroPool = { TagType.Ice, TagType.AbsoluteZero };
private static readonly TagType[] PierceExecutionPool = { TagType.Pierce, TagType.Execution };
public static BackpackInventoryData CreateSampleInventory()
public static BackpackInventoryData CreateSampleInventory(int runSeed = 0)
{
BackpackInventoryData inventory = new BackpackInventoryData
{
@ -31,7 +31,7 @@ namespace GeometryTD.CustomUtility
DamageRandomRate = 0.05f,
AttackMethodType = AttackMethodType.NormalBullet,
Constraint = string.Empty,
Tags = ResolveSeedTags(FirePool, RarityType.Blue, 10001, 1)
Tags = ResolveSeedTags(FirePool, RarityType.Blue, runSeed, 10001, 1)
};
BearingCompItemData bearing = new BearingCompItemData
@ -45,7 +45,7 @@ namespace GeometryTD.CustomUtility
RotateSpeed = new[] { 100f, 120f, 130f, 140f, 150f },
AttackRange = new[] { 3f, 4f, 5f, 6f, 8f },
Constraint = string.Empty,
Tags = ResolveSeedTags(FirePool, RarityType.Blue, 20001, 1)
Tags = ResolveSeedTags(FirePool, RarityType.Blue, runSeed, 20001, 1)
};
BaseCompItemData baseComp = new BaseCompItemData
@ -59,7 +59,7 @@ namespace GeometryTD.CustomUtility
AttackSpeed = new[] { 1f, 2f, 3f, 3.5f, 0.7f },
AttackPropertyType = AttackPropertyType.Fire,
Constraint = string.Empty,
Tags = ResolveSeedTags(FirePool, RarityType.Blue, 30001, 1)
Tags = ResolveSeedTags(FirePool, RarityType.Blue, runSeed, 30001, 1)
};
TowerItemData tower = new TowerItemData
@ -108,7 +108,7 @@ namespace GeometryTD.CustomUtility
DamageRandomRate = 0.01f,
AttackMethodType = AttackMethodType.NormalBullet,
Constraint = string.Empty,
Tags = ResolveSeedTags(IceFreezePool, RarityType.Blue, 10002, 2)
Tags = ResolveSeedTags(IceFreezePool, RarityType.Blue, runSeed, 10002, 2)
});
inventory.MuzzleComponents.Add(new MuzzleCompItemData
@ -123,7 +123,7 @@ namespace GeometryTD.CustomUtility
DamageRandomRate = 0.02f,
AttackMethodType = AttackMethodType.NormalBullet,
Constraint = string.Empty,
Tags = ResolveSeedTags(CritPiercePool, RarityType.Purple, 10003, 3)
Tags = ResolveSeedTags(CritPiercePool, RarityType.Purple, runSeed, 10003, 3)
});
inventory.BearingComponents.Add(new BearingCompItemData
@ -137,7 +137,7 @@ namespace GeometryTD.CustomUtility
RotateSpeed = new[] { 200f, 250f, 300f, 320f, 350f },
AttackRange = new[] { 6f, 6.5f, 7f, 8f, 8f },
Constraint = string.Empty,
Tags = ResolveSeedTags(IceShatterPool, RarityType.Blue, 20002, 2)
Tags = ResolveSeedTags(IceShatterPool, RarityType.Blue, runSeed, 20002, 2)
});
inventory.BearingComponents.Add(new BearingCompItemData
@ -151,7 +151,7 @@ namespace GeometryTD.CustomUtility
RotateSpeed = new[] { 60f, 70f, 80f, 90f, 100f },
AttackRange = new[] { 4f, 4.5f, 5f, 5.5f, 6f },
Constraint = string.Empty,
Tags = ResolveSeedTags(PierceOverpenetratePool, RarityType.Purple, 20003, 3)
Tags = ResolveSeedTags(PierceOverpenetratePool, RarityType.Purple, runSeed, 20003, 3)
});
inventory.BaseComponents.Add(new BaseCompItemData
@ -165,7 +165,7 @@ namespace GeometryTD.CustomUtility
AttackSpeed = new[] { 4f, 4.2f, 4.4f, 4.6f, 4.8f },
AttackPropertyType = AttackPropertyType.Ice,
Constraint = string.Empty,
Tags = ResolveSeedTags(IceAbsoluteZeroPool, RarityType.Blue, 30002, 2)
Tags = ResolveSeedTags(IceAbsoluteZeroPool, RarityType.Blue, runSeed, 30002, 2)
});
inventory.BaseComponents.Add(new BaseCompItemData
@ -179,7 +179,7 @@ namespace GeometryTD.CustomUtility
AttackSpeed = new[] { 1f, 1f, 1f, 1f, 1f },
AttackPropertyType = AttackPropertyType.Physics,
Constraint = string.Empty,
Tags = ResolveSeedTags(PierceExecutionPool, RarityType.Purple, 30003, 3)
Tags = ResolveSeedTags(PierceExecutionPool, RarityType.Purple, runSeed, 30003, 3)
});
inventory.ParticipantTowerInstanceIds.Add(90001);
@ -187,14 +187,12 @@ namespace GeometryTD.CustomUtility
return inventory;
}
private static TagType[] ResolveSeedTags(TagType[] possibleTags, RarityType rarity, long instanceId, int configId)
private static TagType[] ResolveSeedTags(TagType[] possibleTags, RarityType rarity, int runSeed, long instanceId, int configId)
{
return ComponentTagGenerationService.ResolveComponentTags(
possibleTags,
rarity,
InventoryTagSourceType.Seed,
instanceId,
configId);
InventoryTagRandomContext.CreateSeed(runSeed, instanceId, configId));
}
}
}

View File

@ -55,31 +55,44 @@ namespace GeometryTD.Tests.EditMode
TagType[] first = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Blue,
InventoryTagSourceType.Shop,
12345,
7,
InventoryTagRandomContext.CreateShop(1001, 2, 0, 7),
RulesByTag);
TagType[] second = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Blue,
InventoryTagSourceType.Shop,
12345,
7,
InventoryTagRandomContext.CreateShop(1001, 2, 0, 7),
RulesByTag);
Assert.That(second, Is.EqualTo(first));
}
[Test]
public void ResolveComponentTags_Distinguishes_Different_RunSeed_With_Same_Other_Context()
{
TagType[] first = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution },
RarityType.Purple,
InventoryTagRandomContext.CreateDrop(1001, 3, 0, 7),
RulesByTag,
RarityTagBudgetRulesByRarity);
TagType[] second = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution },
RarityType.Purple,
InventoryTagRandomContext.CreateDrop(2002, 3, 0, 7),
RulesByTag,
RarityTagBudgetRulesByRarity);
Assert.That(second, Is.Not.EqualTo(first));
}
[Test]
public void ResolveComponentTags_Uses_Purple_Budget_And_Does_Not_Repeat()
{
TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution },
RarityType.Purple,
InventoryTagSourceType.Drop,
9001,
4,
InventoryTagRandomContext.CreateDrop(1001, 1, 0, 4),
RulesByTag,
RarityTagBudgetRulesByRarity);
@ -93,9 +106,7 @@ namespace GeometryTD.Tests.EditMode
TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.BurnSpread },
RarityType.Red,
InventoryTagSourceType.Seed,
42,
1,
InventoryTagRandomContext.CreateSeed(1001, 42, 1),
RulesByTag,
RarityTagBudgetRulesByRarity);
@ -116,9 +127,7 @@ namespace GeometryTD.Tests.EditMode
TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit },
RarityType.Green,
InventoryTagSourceType.Shop,
1,
1,
InventoryTagRandomContext.CreateShop(1001, 0, 0, 1),
weightedRules,
RarityTagBudgetRulesByRarity);
@ -144,9 +153,7 @@ namespace GeometryTD.Tests.EditMode
TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Green,
InventoryTagSourceType.Shop,
1000 + i,
1,
InventoryTagRandomContext.CreateShop(1001 + i, 0, 0, 1),
weightedRules,
RarityTagBudgetRulesByRarity);
@ -189,6 +196,46 @@ namespace GeometryTD.Tests.EditMode
Assert.That(absoluteZeroRule.Weight, Is.EqualTo(1));
}
[Test]
public void GetEligibleTags_Uses_Final_TagDefinition_State_When_TagConfig_Loads_First()
{
DRTagConfig fireConfigRow = CreateFireConfigRow();
DRTag fireTagRow = CreateFireDisabledRow();
TagDefinitionRegistry.ReloadFromRows(new[] { fireConfigRow }, null);
TagDefinitionRegistry.ReloadFromRows(new[] { fireConfigRow }, new[] { fireTagRow });
TagType[] result = ComponentTagGenerationService.GetEligibleTags(
new[] { TagType.Fire, TagType.Ice },
RarityType.White,
RulesByTag);
Assert.That(result, Is.EqualTo(new[] { TagType.Ice }));
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ResolveComponentTags_Uses_Final_TagDefinition_State_When_Tag_Loads_First()
{
DRTagConfig fireConfigRow = CreateFireConfigRow();
DRTag fireTagRow = CreateFireDisabledRow();
TagDefinitionRegistry.ReloadFromRows(null, new[] { fireTagRow });
TagDefinitionRegistry.ReloadFromRows(new[] { fireConfigRow }, new[] { fireTagRow });
TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice },
RarityType.Green,
InventoryTagRandomContext.CreateShop(1001, 0, 0, 11),
RulesByTag,
RarityTagBudgetRulesByRarity);
Assert.That(result, Is.EqualTo(new[] { TagType.Ice }));
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ResolveRarityTagBudget_Uses_TableDriven_Range_Rules()
{
@ -227,9 +274,7 @@ namespace GeometryTD.Tests.EditMode
TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution },
RarityType.Purple,
InventoryTagSourceType.Drop,
99,
7,
InventoryTagRandomContext.CreateDrop(1001, 1, 0, 7),
RulesByTag,
customBudgetRules);
@ -277,7 +322,7 @@ namespace GeometryTD.Tests.EditMode
[Test]
public void CreateSampleInventory_Generates_SeedTags_Within_Launch_Set_And_Matches_Tower_Stats()
{
BackpackInventoryData inventory = InventorySeedUtility.CreateSampleInventory();
BackpackInventoryData inventory = InventorySeedUtility.CreateSampleInventory(1001);
TowerItemData tower = inventory.Towers[0];
MuzzleCompItemData muzzle = inventory.MuzzleComponents[0];
BearingCompItemData bearing = inventory.BearingComponents[0];
@ -291,5 +336,44 @@ namespace GeometryTD.Tests.EditMode
Assert.That(tower.Stats.TagRuntimes[0].TotalStack, Is.EqualTo(3));
Assert.That(tower.Stats.Tags, Is.EqualTo(new[] { TagType.Fire }));
}
[Test]
public void CreateSampleInventory_Is_Deterministic_For_Same_RunSeed()
{
BackpackInventoryData first = InventorySeedUtility.CreateSampleInventory(1001);
BackpackInventoryData second = InventorySeedUtility.CreateSampleInventory(1001);
Assert.That(first.MuzzleComponents[1].Tags, Is.EqualTo(second.MuzzleComponents[1].Tags));
Assert.That(first.BearingComponents[1].Tags, Is.EqualTo(second.BearingComponents[1].Tags));
Assert.That(first.BaseComponents[1].Tags, Is.EqualTo(second.BaseComponents[1].Tags));
}
[Test]
public void InventoryTagRandomContext_Composes_Stable_Local_InstanceIds_Per_Source()
{
InventoryTagRandomContext shop = InventoryTagRandomContext.CreateShop(1001, 2, 1, 7);
InventoryTagRandomContext drop = InventoryTagRandomContext.CreateDrop(1001, 2, 1, 7);
InventoryTagRandomContext reward = InventoryTagRandomContext.CreateReward(1001, 2, 1, 7);
Assert.That(shop.ItemInstanceId, Is.EqualTo(InventoryTagRandomContext.ComposeLocalItemInstanceId(2, 1)));
Assert.That(drop.ItemInstanceId, Is.EqualTo(reward.ItemInstanceId));
Assert.That(shop.SourceType, Is.EqualTo(InventoryTagSourceType.Shop));
Assert.That(drop.SourceType, Is.EqualTo(InventoryTagSourceType.Drop));
Assert.That(reward.SourceType, Is.EqualTo(InventoryTagSourceType.Reward));
}
private static DRTagConfig CreateFireConfigRow()
{
DRTagConfig row = new DRTagConfig();
Assert.That(row.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":4,\"BurnDamagePerSecondPerStack\":25,\"MaxEffectiveStack\":3}", null), Is.True);
return row;
}
private static DRTag CreateFireDisabledRow()
{
DRTag row = new DRTag();
Assert.That(row.ParseDataRow("\t1\t元素\tFire\tElement\tWhite\t20\tFalse", null), Is.True);
return row;
}
}
}

View File

@ -29,6 +29,7 @@ namespace GeometryTD.Tests.EditMode
"run-test");
Assert.That(runState.RunId, Is.EqualTo("run-test"));
Assert.That(runState.RunSeed, Is.Not.EqualTo(0));
Assert.That(runState.ThemeType, Is.EqualTo(LevelThemeType.Plain));
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
Assert.That(runState.IsCompleted, Is.False);
@ -83,6 +84,31 @@ namespace GeometryTD.Tests.EditMode
Assert.That(runState.RunInventorySnapshot.Gold, Is.EqualTo(130));
}
[Test]
public void Factory_Preserves_Explicit_RunSeed_And_Generates_NonZero_Default_RunSeed()
{
RunState explicitSeedRun = RunStateFactory.Create(
LevelThemeType.Plain,
new BackpackInventoryData(),
new[]
{
new RunNodeSeed { NodeType = RunNodeType.Combat, LinkedLevelId = 1 }
},
"run-explicit",
123456);
RunState generatedSeedRun = RunStateFactory.Create(
LevelThemeType.Plain,
new BackpackInventoryData(),
new[]
{
new RunNodeSeed { NodeType = RunNodeType.Combat, LinkedLevelId = 1 }
},
"run-generated");
Assert.That(explicitSeedRun.RunSeed, Is.EqualTo(123456));
Assert.That(generatedSeedRun.RunSeed, Is.Not.EqualTo(0));
}
[Test]
public void AdvanceService_Exception_Marks_Current_Node_Exception_Without_Completing_Run()
{
@ -234,6 +260,7 @@ namespace GeometryTD.Tests.EditMode
"fixed-run");
Assert.That(runState.RunId, Is.EqualTo("fixed-run"));
Assert.That(runState.RunSeed, Is.Not.EqualTo(0));
Assert.That(runState.NodeCount, Is.EqualTo(10));
Assert.That(runState.CurrentNodeIndex, Is.EqualTo(0));
Assert.That(runState.CurrentNode.NodeType, Is.EqualTo(RunNodeType.Combat));

View File

@ -435,6 +435,34 @@ namespace GeometryTD.Tests.EditMode
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ReloadFromRows_Uses_TagTable_As_IsImplemented_Source_When_TagConfig_Loads_First()
{
DRTagConfig fireConfigRow = CreateFireConfigRow();
DRTag fireTagRow = CreateFireDisabledRow();
TagDefinitionRegistry.ReloadFromRows(new[] { fireConfigRow }, null);
TagDefinitionRegistry.ReloadFromRows(new[] { fireConfigRow }, new[] { fireTagRow });
AssertFireDefinition(false);
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ReloadFromRows_Uses_TagTable_As_IsImplemented_Source_When_Tag_Loads_First()
{
DRTagConfig fireConfigRow = CreateFireConfigRow();
DRTag fireTagRow = CreateFireDisabledRow();
TagDefinitionRegistry.ReloadFromRows(null, new[] { fireTagRow });
TagDefinitionRegistry.ReloadFromRows(new[] { fireConfigRow }, new[] { fireTagRow });
AssertFireDefinition(false);
TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void BuildTagDescriptionText_Uses_Registry_Descriptions()
{
@ -502,6 +530,34 @@ namespace GeometryTD.Tests.EditMode
Assert.That(definition.Category, Is.EqualTo(expectedCategory));
Assert.That(definition.TriggerPhase, Is.EqualTo(expectedPhase));
}
private static DRTagConfig CreateFireConfigRow()
{
DRTagConfig row = new DRTagConfig();
Assert.That(row.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":4,\"BurnDamagePerSecondPerStack\":25,\"MaxEffectiveStack\":3}", null), Is.True);
return row;
}
private static DRTag CreateFireDisabledRow()
{
DRTag row = new DRTag();
Assert.That(row.ParseDataRow("\t1\t元素\tFire\tElement\tWhite\t20\tFalse", null), Is.True);
return row;
}
private static void AssertFireDefinition(bool isImplemented)
{
Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Fire, out TagDefinition fire), Is.True);
Assert.That(fire.TriggerPhase, Is.EqualTo(TagTriggerPhase.OnAfterHit));
Assert.That(fire.Description, Is.EqualTo("火焰测试描述"));
Assert.That(fire.Config, Is.TypeOf<FireTagConfig>());
FireTagConfig config = (FireTagConfig)fire.Config;
Assert.That(config.IsImplemented, Is.EqualTo(isImplemented));
Assert.That(config.BurnDurationSeconds, Is.EqualTo(4f).Within(0.001f));
Assert.That(config.BurnDamagePerSecondPerStack, Is.EqualTo(25f).Within(0.001f));
Assert.That(config.MaxEffectiveStack, Is.EqualTo(3));
}
}
public sealed class AttackPayloadFlowTests