From 515fe954417c9f1a265e41de821f9cd06e0a4828 Mon Sep 17 00:00:00 2001 From: SepComet <202308010230@stu.csust.edu.cn> Date: Wed, 11 Mar 2026 13:16:33 +0800 Subject: [PATCH] S4-07 process 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 组件 Tag 数量预算不再写死在 ResolveTagBudget(...) 里,而是走 RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry -> InventoryTagRuleService 这条表驱动链。 - TagBudget.txt:1 新增了按品质的 MinCount/MaxCount,预算缓存和加载入口分别在 RarityTagBudgetRuleRegistry.cs:7 和 ProcedurePreload.cs:18。 - 生成逻辑已经接到新规则,InventoryTagRuleService.cs:10 现在会先按 Tag.txt 过滤/加权,再按 RarityTagBudget 决定抽几个 Tag。 --- .../GameMain/DataTables/RarityTagBudget.txt | 8 ++ .../DataTables/RarityTagBudget.txt.meta | 7 ++ .../Scripts/DataTable/DRRarityTagBudget.cs | 38 ++++++++ .../DataTable/DRRarityTagBudget.cs.meta | 11 +++ .../Definition/InventoryTagRuleService.cs | 43 +++++---- .../Definition/RarityTagBudgetRuleData.cs | 9 ++ .../RarityTagBudgetRuleData.cs.meta | 11 +++ .../Definition/RarityTagBudgetRuleRegistry.cs | 73 +++++++++++++++ .../RarityTagBudgetRuleRegistry.cs.meta | 11 +++ .../Procedure/Base/ProcedurePreload.cs | 10 +++ .../EditMode/InventoryTagRuleServiceTests.cs | 85 +++++++++++++++++- docs/CodeX-TODO.md | 7 +- docs/TagSystemDesign.md | 29 +++--- 数据表/RarityTagBudget.xlsx | Bin 0 -> 11862 bytes 14 files changed, 303 insertions(+), 39 deletions(-) create mode 100644 Assets/GameMain/DataTables/RarityTagBudget.txt create mode 100644 Assets/GameMain/DataTables/RarityTagBudget.txt.meta create mode 100644 Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs create mode 100644 Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs create mode 100644 Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs create mode 100644 Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs.meta create mode 100644 数据表/RarityTagBudget.xlsx diff --git a/Assets/GameMain/DataTables/RarityTagBudget.txt b/Assets/GameMain/DataTables/RarityTagBudget.txt new file mode 100644 index 0000000..588fc2c --- /dev/null +++ b/Assets/GameMain/DataTables/RarityTagBudget.txt @@ -0,0 +1,8 @@ +# Id 列1 RarityType MinCount MaxCount +# int RarityType int int +# 预算编号 策划备注 稀有度 最低Tag数量 最高Tag数量 + 1 White 0 1 + 2 Green 0 2 + 3 Blue 1 3 + 4 Purple 1 3 + 5 Red 2 4 diff --git a/Assets/GameMain/DataTables/RarityTagBudget.txt.meta b/Assets/GameMain/DataTables/RarityTagBudget.txt.meta new file mode 100644 index 0000000..07ae35a --- /dev/null +++ b/Assets/GameMain/DataTables/RarityTagBudget.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6b4b5ba2b4cd50e48ae0ef4ea3e6cecf +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs b/Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs new file mode 100644 index 0000000..cb4ec0a --- /dev/null +++ b/Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs @@ -0,0 +1,38 @@ +using GeometryTD.CustomUtility; +using GeometryTD.Definition; +using UnityGameFramework.Runtime; + +namespace GeometryTD.DataTable +{ + public sealed class DRRarityTagBudget : DataRowBase + { + private int m_Id; + + public override int Id => m_Id; + + public RarityType Rarity { get; private set; } + + public int MinCount { get; private set; } + + public int MaxCount { get; private set; } + + public override bool ParseDataRow(string dataRowString, object userData) + { + string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators); + for (int i = 0; i < columnStrings.Length; i++) + { + columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators); + } + + int index = 0; + index++; + m_Id = int.Parse(columnStrings[index++]); + index++; + Rarity = EnumUtility.Get(columnStrings[index++]); + MinCount = int.Parse(columnStrings[index++]); + MaxCount = int.Parse(columnStrings[index++]); + + return true; + } + } +} diff --git a/Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs.meta b/Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs.meta new file mode 100644 index 0000000..2eb6aee --- /dev/null +++ b/Assets/GameMain/Scripts/DataTable/DRRarityTagBudget.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d5e6052cd3fe424d8b6ac53c33c063c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs b/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs index 4b27ee7..95843d2 100644 --- a/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs +++ b/Assets/GameMain/Scripts/Definition/InventoryTagRuleService.cs @@ -13,7 +13,8 @@ namespace GeometryTD.Definition InventoryTagSourceType sourceType, long itemInstanceId, int configId, - IReadOnlyDictionary rulesByTag = null) + IReadOnlyDictionary rulesByTag = null, + IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) { IReadOnlyDictionary ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup); @@ -23,7 +24,7 @@ namespace GeometryTD.Definition } Random random = new Random(BuildStableSeed(rarity, sourceType, itemInstanceId, configId)); - int tagBudget = ResolveTagBudget(rarity, random); + int tagBudget = ResolveRarityTagBudget(rarity, random, rarityTagBudgetRulesByRarity); if (tagBudget <= 0) { return Array.Empty(); @@ -88,26 +89,25 @@ namespace GeometryTD.Definition return result; } - public static int ResolveTagBudget(RarityType rarity, Random random) + public static int ResolveRarityTagBudget( + RarityType rarity, + Random random, + IReadOnlyDictionary rarityTagBudgetRulesByRarity = null) { RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); - random ??= new Random(0); + IReadOnlyDictionary ruleLookup = rarityTagBudgetRulesByRarity ?? RarityTagBudgetRuleRegistry.Rules; + RarityTagBudgetRuleData rule = GetRarityTagBudgetRule(normalizedRarity, ruleLookup); - switch (normalizedRarity) + Debug.Assert(rule.MinCount >= 0); + Debug.Assert(rule.MaxCount >= rule.MinCount); + + if (rule.MinCount == rule.MaxCount) { - case RarityType.White: - return random.Next(0, 2); - case RarityType.Green: - return 1; - case RarityType.Blue: - return random.Next(1, 3); - case RarityType.Purple: - return 2; - case RarityType.Red: - return random.Next(2, 4); - default: - return 0; + return rule.MinCount; } + + random ??= new Random(0); + return random.Next(rule.MinCount, rule.MaxCount + 1); } private static bool IsSupportedLaunchTag(TagType tagType) @@ -131,6 +131,15 @@ namespace GeometryTD.Definition return false; } + private static RarityTagBudgetRuleData GetRarityTagBudgetRule( + RarityType rarity, + IReadOnlyDictionary ruleLookup) + { + Debug.Assert(ruleLookup != null); + Debug.Assert(ruleLookup.TryGetValue(rarity, out _)); + return ruleLookup[rarity]; + } + private static int RollWeightedIndex( IReadOnlyList pool, IReadOnlyDictionary ruleLookup, diff --git a/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs new file mode 100644 index 0000000..8ab8027 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public sealed class RarityTagBudgetRuleData + { + public RarityType Rarity { get; set; } + public int MinCount { get; set; } + public int MaxCount { get; set; } + } +} diff --git a/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs.meta b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs.meta new file mode 100644 index 0000000..1ca28dd --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c0b098910eb4f8486d306ecf1812423 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs new file mode 100644 index 0000000..1f60dd8 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using GeometryTD.DataTable; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class RarityTagBudgetRuleRegistry + { + private static readonly Dictionary RulesByRarity = CreateDefaultRules(); + + public static IReadOnlyDictionary Rules => RulesByRarity; + + public static void ResetToDefaults() + { + RulesByRarity.Clear(); + foreach (KeyValuePair pair in CreateDefaultRules()) + { + RulesByRarity.Add(pair.Key, pair.Value); + } + } + + public static void LoadFromRows(IEnumerable rows) + { + ResetToDefaults(); + foreach (DRRarityTagBudget row in rows) + { + ApplyRow(row); + } + } + + public static bool TryGetRule(RarityType rarity, out RarityTagBudgetRuleData rule) + { + return RulesByRarity.TryGetValue(rarity, out rule); + } + + private static Dictionary CreateDefaultRules() + { + return new Dictionary + { + [RarityType.White] = CreateRule(RarityType.White, 0, 1), + [RarityType.Green] = CreateRule(RarityType.Green, 1, 1), + [RarityType.Blue] = CreateRule(RarityType.Blue, 1, 2), + [RarityType.Purple] = CreateRule(RarityType.Purple, 2, 2), + [RarityType.Red] = CreateRule(RarityType.Red, 2, 3) + }; + } + + private static RarityTagBudgetRuleData CreateRule(RarityType rarity, int minCount, int maxCount) + { + Debug.Assert(rarity >= RarityType.White && rarity <= RarityType.Red); + Debug.Assert(minCount >= 0); + Debug.Assert(maxCount >= minCount); + + return new RarityTagBudgetRuleData + { + Rarity = rarity, + MinCount = minCount, + MaxCount = maxCount + }; + } + + private static void ApplyRow(DRRarityTagBudget row) + { + Debug.Assert(row != null); + Debug.Assert(row.Id > 0); + Debug.Assert(row.Rarity >= RarityType.White && row.Rarity <= RarityType.Red); + Debug.Assert(row.MinCount >= 0); + Debug.Assert(row.MaxCount >= row.MinCount); + + RulesByRarity[row.Rarity] = CreateRule(row.Rarity, row.MinCount, row.MaxCount); + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs.meta b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs.meta new file mode 100644 index 0000000..b405c63 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/RarityTagBudgetRuleRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bc43423fff7c416fa0a8396e670da212 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs index 239d556..0ab9d63 100644 --- a/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs +++ b/Assets/GameMain/Scripts/Procedure/Base/ProcedurePreload.cs @@ -31,6 +31,7 @@ namespace GeometryTD.Procedure "ShopPrice", "Sound", "Tag", + "RarityTagBudget", "TagConfig", "OutGameDropPool", "UIForm", @@ -213,6 +214,15 @@ namespace GeometryTD.Procedure TagGenerationRuleRegistry.LoadFromRows(tagTable.GetAllDataRows()); } } + + if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("RarityTagBudget", false)) + { + var tagBudgetTable = GameEntry.DataTable.GetDataTable(); + if (tagBudgetTable != null) + { + RarityTagBudgetRuleRegistry.LoadFromRows(tagBudgetTable.GetAllDataRows()); + } + } } private void OnLoadDataTableFailure(object sender, GameEventArgs e) diff --git a/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs b/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs index 48a3d5c..37a5359 100644 --- a/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs +++ b/Assets/Tests/EditMode/InventoryTagRuleServiceTests.cs @@ -20,6 +20,16 @@ namespace GeometryTD.Tests.EditMode { TagType.Execution, new TagGenerationRuleData { TagType = TagType.Execution, MinRarity = RarityType.Purple, Weight = 5 } }, }; + private static readonly IReadOnlyDictionary RarityTagBudgetRulesByRarity = + new Dictionary + { + { RarityType.White, new RarityTagBudgetRuleData { Rarity = RarityType.White, MinCount = 0, MaxCount = 1 } }, + { RarityType.Green, new RarityTagBudgetRuleData { Rarity = RarityType.Green, MinCount = 1, MaxCount = 1 } }, + { RarityType.Blue, new RarityTagBudgetRuleData { Rarity = RarityType.Blue, MinCount = 1, MaxCount = 2 } }, + { RarityType.Purple, new RarityTagBudgetRuleData { Rarity = RarityType.Purple, MinCount = 2, MaxCount = 2 } }, + { RarityType.Red, new RarityTagBudgetRuleData { Rarity = RarityType.Red, MinCount = 2, MaxCount = 3 } }, + }; + [Test] public void GetEligibleTags_Filters_Invalid_Unsupported_And_HighRarity_Tags() { @@ -70,7 +80,8 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Drop, 9001, 4, - RulesByTag); + RulesByTag, + RarityTagBudgetRulesByRarity); Assert.That(result.Length, Is.EqualTo(2)); Assert.That(new HashSet(result).Count, Is.EqualTo(result.Length)); @@ -85,7 +96,8 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Seed, 42, 1, - RulesByTag); + RulesByTag, + RarityTagBudgetRulesByRarity); Assert.That(result, Is.EqualTo(new[] { TagType.Fire })); } @@ -107,7 +119,8 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Shop, 1, 1, - weightedRules); + weightedRules, + RarityTagBudgetRulesByRarity); Assert.That(result, Has.Length.EqualTo(1)); Assert.That(result[0], Is.EqualTo(TagType.Fire)); @@ -134,7 +147,8 @@ namespace GeometryTD.Tests.EditMode InventoryTagSourceType.Shop, 1000 + i, 1, - weightedRules); + weightedRules, + RarityTagBudgetRulesByRarity); Assert.That(result, Has.Length.EqualTo(1)); if (result[0] == TagType.Fire) @@ -161,6 +175,69 @@ namespace GeometryTD.Tests.EditMode TagGenerationRuleRegistry.ResetToDefaults(); } + [Test] + public void ResolveRarityTagBudget_Uses_TableDriven_Range_Rules() + { + int whiteBudget = InventoryTagRuleService.ResolveRarityTagBudget( + RarityType.White, + new System.Random(0), + RarityTagBudgetRulesByRarity); + int greenBudget = InventoryTagRuleService.ResolveRarityTagBudget( + RarityType.Green, + new System.Random(0), + RarityTagBudgetRulesByRarity); + int redBudget = InventoryTagRuleService.ResolveRarityTagBudget( + RarityType.Red, + new System.Random(0), + RarityTagBudgetRulesByRarity); + + Assert.That(whiteBudget, Is.InRange(0, 1)); + Assert.That(greenBudget, Is.EqualTo(1)); + Assert.That(redBudget, Is.InRange(2, 3)); + } + + [Test] + public void ResolveComponentTags_Uses_Custom_Purple_Budget_From_Rules() + { + IReadOnlyDictionary customBudgetRules = + new Dictionary(RarityTagBudgetRulesByRarity) + { + [RarityType.Purple] = new RarityTagBudgetRuleData + { + Rarity = RarityType.Purple, + MinCount = 3, + MaxCount = 3 + } + }; + + TagType[] result = InventoryTagRuleService.ResolveComponentTags( + new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution }, + RarityType.Purple, + InventoryTagSourceType.Drop, + 99, + 7, + RulesByTag, + customBudgetRules); + + Assert.That(result, Has.Length.EqualTo(3)); + Assert.That(new HashSet(result).Count, Is.EqualTo(3)); + } + + [Test] + public void RarityTagBudgetRuleRegistry_LoadFromRows_Overrides_Purple_Budget() + { + DRRarityTagBudget purpleRow = new DRRarityTagBudget(); + Assert.That(purpleRow.ParseDataRow("\t4\t\tPurple\t3\t3", null), Is.True); + + RarityTagBudgetRuleRegistry.LoadFromRows(new[] { purpleRow }); + + Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Purple, out RarityTagBudgetRuleData rule), Is.True); + Assert.That(rule.MinCount, Is.EqualTo(3)); + Assert.That(rule.MaxCount, Is.EqualTo(3)); + + RarityTagBudgetRuleRegistry.ResetToDefaults(); + } + [Test] public void CreateSampleInventory_Generates_SeedTags_Within_Launch_Set_And_Matches_Tower_Stats() { diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index 8170546..8c846e1 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -188,7 +188,8 @@ - `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。 - `S4-07` 已进入第一阶段实现。当前仓库已具备 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 的消费闭环,首发 7 个 Tag 的触发阶段、描述与核心参数已可由表覆盖。 - `S4-07` 已推进到第二阶段。当前仓库同时具备 `Tag.txt -> DRTag -> TagGenerationRuleRegistry -> InventoryTagRuleService` 与 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 两条消费闭环,首发 7 个 Tag 的生成规则、触发阶段、描述与核心参数都已开始由表驱动。 -- `S4-07` 仍未完成最终收口。当前采用的是 `Tag.txt + TagConfig.txt` 的分层方案,而不是文档原方案里的单独 `TagRule`;更深的规则字段与完整命名口径仍待后续决定是否继续统一。 +- `S4-07` 已推进到第三阶段。当前仓库新增 `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry -> InventoryTagRuleService`,组件 Tag 数量预算不再写死在 `ResolveRarityTagBudget(...)` 的 `switch` 中,而是按品质从表驱动;至此生成链的候选过滤、权重抽取、数量预算三段都已进入 DataTable 消费闭环。 +- `S4-07` 仍未完成最终收口。当前采用的是 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 的分层方案,而不是文档原方案里的单独 `TagRule`;更深的元数据字段与完整命名口径仍待后续决定是否继续统一。 ### S4-06 当前代码状态 @@ -206,8 +207,8 @@ ### S4 后续执行计划 -1. 继续推进 `S4-07`:在现有 `Tag.txt + TagConfig.txt` 分层方案基础上,决定是否补成文档中的完整 `TagRule`,或明确当前双表就是本阶段的等价收口方案。 -2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的生成规则、参数和说明配置化,后续重点转向是否还要继续配置化更深层的规则字段。 +1. 继续推进 `S4-07`:在现有 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 分层方案基础上,决定是否补成文档中的完整 `TagRule`,或明确当前三表就是本阶段的等价收口方案。 +2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的生成规则、数量预算、参数和说明配置化,后续重点转向是否还要继续配置化更深层的元数据字段。 3. `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 继续留在后续扩展阶段,不因为 `S4-06` 完成而提前进入当前迭代。 ## 阶段 S5 - 收口耐久规则 diff --git a/docs/TagSystemDesign.md b/docs/TagSystemDesign.md index 39c0b8d..b329aed 100644 --- a/docs/TagSystemDesign.md +++ b/docs/TagSystemDesign.md @@ -77,24 +77,23 @@ Tag 系统需要同时满足四个目标: ### 4.1 配置层 -后续配置层固定采用“`Tag.txt` 基础字典 + `TagRule` 规则表”两层结构,而不是继续把所有规则硬塞进现有 `Tag.txt`。 +当前实现固定采用“`Tag.txt + RarityTagBudget.txt + TagConfig.txt`”三层结构,而不是继续把所有规则硬塞进现有 `Tag.txt`。 - `Tag.txt` - - 保留 `TagType`、`Name` 等基础字典信息 -- `TagRule` - - 承载运行规则字段,至少包括以下内容: - - `MinRarity` - - `Weight` - - `MaxComponentStack` - - `TriggerPhase` - - `Description` - - `ParamJson` 或等价参数字段 + - 保留 `TagType`、`Name` 等基础字典与按 Tag 的生成规则 + - 当前实际承载:`MinRarity`、`Weight` +- `RarityTagBudget.txt` + - 承载按品质的数量预算 + - 当前实际承载:`Rarity`、`MinCount`、`MaxCount` +- `TagConfig.txt` + - 承载战斗与展示相关配置 + - 当前实际承载:`TriggerPhase`、`Description`、`ParamJson` 用途: - `MinRarity`:该 Tag 最低可出现品质 - `Weight`:同一候选池内抽取权重 -- `MaxComponentStack`:单组件允许的最大层数 +- `MinCount / MaxCount`:该品质组件本次可抽取的 Tag 数量预算 - `TriggerPhase`:用于战斗结算路由 - `ParamJson`:承载伤害倍率、持续时间、范围等效果参数 @@ -159,7 +158,7 @@ Tag 随机应发生在“组件实例创建时”,而不是组塔时。 ### 5.3 Tag 数量预算 -每个品质都保留独立的 Tag 数量预算,而不是按概率硬编码在代码里。 +每个品质都保留独立的 Tag 数量预算,并由 `RarityTagBudget.txt` 驱动,而不是按概率硬编码在代码里。 推荐默认值: @@ -448,7 +447,7 @@ Tag 触发阶段固定拆成四段: ## 11. 后续文档与代码动作 1. 先基于本设计回写 `docs/CodeX-TODO.md` 的 `S4-03` 边界 -2. 在 `S4-07` 中新增并消费 `TagRule` 表,保留 `Tag.txt` 作为基础字典 +2. 在 `S4-07` 中补齐并消费 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 三层表结构 3. 新增 `TagGenerationService`,先收口实例生成 4. 新增 `TowerTagAggregationService`,替换当前简单并集逻辑 5. 评估 `AttackPayload` 是否并入现有 `BulletData`,还是单独引入 @@ -461,7 +460,7 @@ Tag 触发阶段固定拆成四段: - Tag 在组件实例创建时随机,不在组塔时随机 - `PossibleTag` 是候选池,不是最终实例值 - 塔级 Tag 汇总保留 `Stack` -- 配置层采用 `Tag.txt + TagRule` 双表结构 +- 配置层采用 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 分层结构 - 第一批战斗效果优先做状态类与数值修正类,不优先做穿透 / 爆炸 / 传播 - MVP 正式首发集合固定为 `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero` - `BurnSpread` 明确后移,不作为当前首发集合成员 @@ -666,4 +665,4 @@ Execution 斩杀强化 BurnSpread 最多传播 3 次。 -否则后期怪物多时会帧率爆炸。 \ No newline at end of file +否则后期怪物多时会帧率爆炸。 diff --git a/数据表/RarityTagBudget.xlsx b/数据表/RarityTagBudget.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..894d84ffa7cd36998d603e74ebc832ba5025795d GIT binary patch literal 11862 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 fY@YiEDL=a;WF;VfMo~-