diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs index 99ca303..52cfe57 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatNodeComponent.cs @@ -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)) diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs index 0eb542e..736b165 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatScheduler.cs @@ -70,6 +70,7 @@ namespace GeometryTD.CustomComponent IReadOnlyList phases, IReadOnlyDictionary> 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++) { diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs index d3d4845..33e13f0 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerCoordinator.cs @@ -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; diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs index 3f09009..0e3f16c 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/CombatSchedulerRuntime.cs @@ -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; } diff --git a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs index bf59402..96bde56 100644 --- a/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs +++ b/Assets/GameMain/Scripts/CustomComponent/CombatNode/CombatScheduler/EnemyDrop/EnemyDropResolver.cs @@ -22,12 +22,28 @@ namespace GeometryTD.CustomComponent private IDataTable _drBearingComp; private IDataTable _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(); @@ -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(), 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(); @@ -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(), AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty() }; 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(); @@ -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(), 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++; + } } } diff --git a/Assets/GameMain/Scripts/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs b/Assets/GameMain/Scripts/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs index 4c5db05..bdff309 100644 --- a/Assets/GameMain/Scripts/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/PlayerInventory/PlayerInventoryComponent.cs @@ -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( diff --git a/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs b/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs index ccda831..f5e8345 100644 --- a/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs +++ b/Assets/GameMain/Scripts/CustomComponent/ShopNodeComponent.cs @@ -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; diff --git a/Assets/GameMain/Scripts/Definition/Tag/Generation/ComponentTagGenerationService.cs b/Assets/GameMain/Scripts/Definition/Tag/Generation/ComponentTagGenerationService.cs index 5ad7df0..4a8fa8f 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/Generation/ComponentTagGenerationService.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/Generation/ComponentTagGenerationService.cs @@ -10,9 +10,7 @@ namespace GeometryTD.Definition public static TagType[] ResolveComponentTags( IReadOnlyList possibleTags, RarityType rarity, - InventoryTagSourceType sourceType, - long itemInstanceId, - int configId, + InventoryTagRandomContext randomContext, IReadOnlyDictionary rulesByTag = null, IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) { @@ -23,7 +21,7 @@ namespace GeometryTD.Definition return Array.Empty(); } - 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 possibleTags, + RarityType rarity, + InventoryTagSourceType sourceType, + long itemInstanceId, + int configId, + IReadOnlyDictionary rulesByTag = null, + IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) + { + return ResolveComponentTags( + possibleTags, + rarity, + new InventoryTagRandomContext(0, sourceType, itemInstanceId, configId), + rulesByTag, + rarityTagBudgetRulesByRarity); + } + public static TagType[] GetEligibleTags( IReadOnlyList 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; } } diff --git a/Assets/GameMain/Scripts/Definition/Tag/Generation/InventoryTagRandomContext.cs b/Assets/GameMain/Scripts/Definition/Tag/Generation/InventoryTagRandomContext.cs new file mode 100644 index 0000000..0dac7c8 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/Generation/InventoryTagRandomContext.cs @@ -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; + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/Generation/InventoryTagRandomContext.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/Generation/InventoryTagRandomContext.cs.meta new file mode 100644 index 0000000..886feb5 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/Generation/InventoryTagRandomContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d4f8e95f2f94e4ca876ff4c871f5b41 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/Metadata/TagDefinitionRegistry.cs b/Assets/GameMain/Scripts/Definition/Tag/Metadata/TagDefinitionRegistry.cs index 3f1c5c5..188fb13 100644 --- a/Assets/GameMain/Scripts/Definition/Tag/Metadata/TagDefinitionRegistry.cs +++ b/Assets/GameMain/Scripts/Definition/Tag/Metadata/TagDefinitionRegistry.cs @@ -22,11 +22,7 @@ namespace GeometryTD.Definition public static void LoadFromRows(IEnumerable 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 tagConfigRows, IEnumerable 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 CreateDefaultDefinitions() { return new Dictionary diff --git a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs index 24ad0d3..7efa98b 100644 --- a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs +++ b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs @@ -199,22 +199,12 @@ namespace GeometryTD.Procedure if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("TagConfig", false)) { - var tagConfigTable = GameEntry.DataTable.GetDataTable(); - if (tagConfigTable != null) - { - TagDefinitionRegistry.LoadFromRows(tagConfigTable.GetAllDataRows()); - } + ReloadTagRegistriesFromLoadedTables(); } if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("Tag", false)) { - var tagTable = GameEntry.DataTable.GetDataTable(); - 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(); + if (tagConfigTable != null) + { + tagConfigRows = tagConfigTable.GetAllDataRows(); + } + + DRTag[] tagRows = null; + var tagTable = GameEntry.DataTable.GetDataTable(); + if (tagTable != null) + { + tagRows = tagTable.GetAllDataRows(); + TagGenerationRuleRegistry.LoadFromRows(tagRows); + } + + TagDefinitionRegistry.ReloadFromRows(tagConfigRows, tagRows); + } + private void OnLoadDataTableFailure(object sender, GameEventArgs e) { LoadDataTableFailureEventArgs ne = (LoadDataTableFailureEventArgs)e; diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs index 3b1361e..17656b4 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/ProcedureMain.cs @@ -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); diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs index 99f0c35..7faa522 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunModel.cs @@ -71,11 +71,13 @@ namespace GeometryTD.Procedure internal RunState( string runId, + int runSeed, LevelThemeType themeType, List nodes, BackpackInventoryData runInventorySnapshot) { RunId = string.IsNullOrWhiteSpace(runId) ? Guid.NewGuid().ToString("N") : runId; + RunSeed = runSeed == 0 ? RunStateFactory.CreateRunSeed() : runSeed; ThemeType = themeType; _nodes = nodes ?? new List(); 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; } diff --git a/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs index c6e048f..8e49820 100644 --- a/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs +++ b/Assets/GameMain/Scripts/Procedure/ProcedureMain/RunStateFactory.cs @@ -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 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 nodeSeeds, - string runId = null) + string runId = null, + int runSeed = 0) { List nodes = new List(); 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; } } } diff --git a/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs b/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs index 48a4c7d..659da7d 100644 --- a/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs +++ b/Assets/GameMain/Scripts/UI/Shop/UseCase/ShopFormUseCase.cs @@ -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 _currentGoods = new List(GoodsCount); private readonly List _shopPriceRows = new List(); @@ -21,13 +23,15 @@ namespace GeometryTD.UI private IDataTable _bearingCompTable; private IDataTable _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(), 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(), AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty() }; } - 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(), AttackPropertyType = config.AttackPropertyType }; diff --git a/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs b/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs index 0b96bfd..25fa744 100644 --- a/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs +++ b/Assets/GameMain/Scripts/Utility/InventorySeedUtility.cs @@ -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)); } } } diff --git a/Assets/Tests/EditMode/ComponentTagGenerationServiceTests.cs b/Assets/Tests/EditMode/ComponentTagGenerationServiceTests.cs index c93a706..a81ad00 100644 --- a/Assets/Tests/EditMode/ComponentTagGenerationServiceTests.cs +++ b/Assets/Tests/EditMode/ComponentTagGenerationServiceTests.cs @@ -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; + } } } diff --git a/Assets/Tests/EditMode/RunStateTests.cs b/Assets/Tests/EditMode/RunStateTests.cs index 2ce4502..b3dc8ad 100644 --- a/Assets/Tests/EditMode/RunStateTests.cs +++ b/Assets/Tests/EditMode/RunStateTests.cs @@ -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)); diff --git a/Assets/Tests/EditMode/TagCombatRuntimeTests.cs b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs index b095e61..8f276ae 100644 --- a/Assets/Tests/EditMode/TagCombatRuntimeTests.cs +++ b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs @@ -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 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