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 0000000..894d84f Binary files /dev/null and b/数据表/RarityTagBudget.xlsx differ