- S4-01 先确定 M1 需要的品质 / Tag
- S4-02 把品质计算整理成单一入口 规则边界 - S4-03 先固化 Tag 系统设计与首发范围
This commit is contained in:
parent
2d09b01c55
commit
73b7adedb8
|
|
@ -351,7 +351,7 @@ namespace GeometryTD.CustomComponent
|
|||
InstanceId = _nextDropItemInstanceId++,
|
||||
ConfigId = config.Id,
|
||||
Name = config.Name,
|
||||
Rarity = row.Rarity,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity),
|
||||
Endurance = 100f,
|
||||
Constraint = config.Constraint,
|
||||
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
|
||||
|
|
@ -382,7 +382,7 @@ namespace GeometryTD.CustomComponent
|
|||
InstanceId = _nextDropItemInstanceId++,
|
||||
ConfigId = config.Id,
|
||||
Name = config.Name,
|
||||
Rarity = row.Rarity,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity),
|
||||
Endurance = 100f,
|
||||
Constraint = config.Constraint,
|
||||
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
|
||||
|
|
@ -412,7 +412,7 @@ namespace GeometryTD.CustomComponent
|
|||
InstanceId = _nextDropItemInstanceId++,
|
||||
ConfigId = config.Id,
|
||||
Name = config.Name,
|
||||
Rarity = row.Rarity,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(row.Rarity),
|
||||
Endurance = 100f,
|
||||
Constraint = config.Constraint,
|
||||
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
|
||||
|
|
|
|||
|
|
@ -60,7 +60,10 @@ namespace GeometryTD.CustomComponent
|
|||
{
|
||||
InstanceId = towerInstanceId,
|
||||
Name = $"组装防御塔-{towerInstanceId}",
|
||||
Rarity = ResolveAverageRarity(muzzleComp.Rarity, bearingComp.Rarity, baseComp.Rarity),
|
||||
Rarity = InventoryRarityRuleService.ResolveTowerRarity(
|
||||
muzzleComp.Rarity,
|
||||
bearingComp.Rarity,
|
||||
baseComp.Rarity),
|
||||
MuzzleComponentInstanceId = muzzleComp.InstanceId,
|
||||
BearingComponentInstanceId = bearingComp.InstanceId,
|
||||
BaseComponentInstanceId = baseComp.InstanceId,
|
||||
|
|
@ -181,14 +184,6 @@ namespace GeometryTD.CustomComponent
|
|||
return mergedTags;
|
||||
}
|
||||
|
||||
private static RarityType ResolveAverageRarity(RarityType muzzleRarity, RarityType bearingRarity, RarityType baseRarity)
|
||||
{
|
||||
float avg = ((int)muzzleRarity + (int)bearingRarity + (int)baseRarity) / 3f;
|
||||
int rounded = Mathf.RoundToInt(avg);
|
||||
int clamped = Mathf.Clamp(rounded, (int)RarityType.White, (int)RarityType.Red);
|
||||
return (RarityType)clamped;
|
||||
}
|
||||
|
||||
private IDataTable<DRMuzzleComp> EnsureMuzzleTable()
|
||||
{
|
||||
_drMuzzleComp ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace GeometryTD.Definition
|
||||
{
|
||||
public static class InventoryRarityRuleService
|
||||
{
|
||||
public static RarityType NormalizeComponentRarity(RarityType rarity)
|
||||
{
|
||||
if (rarity < RarityType.White || rarity > RarityType.Red)
|
||||
{
|
||||
return RarityType.White;
|
||||
}
|
||||
|
||||
return rarity;
|
||||
}
|
||||
|
||||
public static RarityType ResolveTowerRarity(
|
||||
RarityType muzzleRarity,
|
||||
RarityType bearingRarity,
|
||||
RarityType baseRarity)
|
||||
{
|
||||
int normalizedMuzzle = (int)NormalizeComponentRarity(muzzleRarity);
|
||||
int normalizedBearing = (int)NormalizeComponentRarity(bearingRarity);
|
||||
int normalizedBase = (int)NormalizeComponentRarity(baseRarity);
|
||||
|
||||
float average = (normalizedMuzzle + normalizedBearing + normalizedBase) / 3f;
|
||||
int rounded = Mathf.RoundToInt(average);
|
||||
int clamped = Mathf.Clamp(rounded, (int)RarityType.White, (int)RarityType.Red);
|
||||
return (RarityType)clamped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3d65ff6ea2dc49be85529a7ca4c87fc8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -158,7 +158,8 @@ namespace GeometryTD.UI
|
|||
{
|
||||
int slotRoll = UnityEngine.Random.Range(0, 3);
|
||||
DRShopPrice priceRow = _shopPriceRows[UnityEngine.Random.Range(0, _shopPriceRows.Count)];
|
||||
RarityType rarity = priceRow != null ? priceRow.Rarity : RarityType.White;
|
||||
RarityType rarity = InventoryRarityRuleService.NormalizeComponentRarity(
|
||||
priceRow != null ? priceRow.Rarity : RarityType.White);
|
||||
|
||||
switch (slotRoll)
|
||||
{
|
||||
|
|
@ -180,7 +181,7 @@ namespace GeometryTD.UI
|
|||
InstanceId = _nextTempInstanceId++,
|
||||
ConfigId = config.Id,
|
||||
Name = config.Name,
|
||||
Rarity = rarity,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity),
|
||||
Endurance = 100f,
|
||||
Constraint = config.Constraint,
|
||||
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
|
||||
|
|
@ -199,7 +200,7 @@ namespace GeometryTD.UI
|
|||
InstanceId = _nextTempInstanceId++,
|
||||
ConfigId = config.Id,
|
||||
Name = config.Name,
|
||||
Rarity = rarity,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity),
|
||||
Endurance = 100f,
|
||||
Constraint = config.Constraint,
|
||||
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
|
||||
|
|
@ -217,7 +218,7 @@ namespace GeometryTD.UI
|
|||
InstanceId = _nextTempInstanceId++,
|
||||
ConfigId = config.Id,
|
||||
Name = config.Name,
|
||||
Rarity = rarity,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity),
|
||||
Endurance = 100f,
|
||||
Constraint = config.Constraint,
|
||||
Tags = config.PossibleTag != null ? (TagType[])config.PossibleTag.Clone() : Array.Empty<TagType>(),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 10001,
|
||||
ConfigId = 1,
|
||||
Name = "元素枪口",
|
||||
Rarity = RarityType.Green,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Green),
|
||||
Endurance = 90f,
|
||||
IsAssembledIntoTower = true,
|
||||
AttackDamage = new[] { 200, 300, 400, 500, 800 },
|
||||
|
|
@ -31,7 +31,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 20001,
|
||||
ConfigId = 1,
|
||||
Name = "元素轴承",
|
||||
Rarity = RarityType.Green,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Green),
|
||||
Endurance = 1f,
|
||||
IsAssembledIntoTower = true,
|
||||
RotateSpeed = new[] { 100f, 120f, 130f, 140f, 150f },
|
||||
|
|
@ -45,7 +45,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 30001,
|
||||
ConfigId = 1,
|
||||
Name = "元素底座",
|
||||
Rarity = RarityType.Green,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Green),
|
||||
Endurance = 88f,
|
||||
IsAssembledIntoTower = true,
|
||||
AttackSpeed = new[] { 1f, 2f, 3f, 3.5f, 0.7f },
|
||||
|
|
@ -58,7 +58,10 @@ namespace GeometryTD.CustomUtility
|
|||
{
|
||||
InstanceId = 90001,
|
||||
Name = "测试防御塔-A",
|
||||
Rarity = RarityType.Green,
|
||||
Rarity = InventoryRarityRuleService.ResolveTowerRarity(
|
||||
muzzle.Rarity,
|
||||
bearing.Rarity,
|
||||
baseComp.Rarity),
|
||||
IsParticipatingInCombat = true,
|
||||
MuzzleComponentInstanceId = muzzle.InstanceId,
|
||||
BearingComponentInstanceId = bearing.InstanceId,
|
||||
|
|
@ -86,7 +89,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 10002,
|
||||
ConfigId = 2,
|
||||
Name = "控制枪口",
|
||||
Rarity = RarityType.Blue,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue),
|
||||
Endurance = 80,
|
||||
IsAssembledIntoTower = false,
|
||||
AttackDamage = new[] { 200, 300, 400, 500, 600 },
|
||||
|
|
@ -101,7 +104,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 10003,
|
||||
ConfigId = 3,
|
||||
Name = "穿透枪口",
|
||||
Rarity = RarityType.Purple,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple),
|
||||
Endurance = 97f,
|
||||
IsAssembledIntoTower = false,
|
||||
AttackDamage = new[] { 50, 55, 60, 80, 90 },
|
||||
|
|
@ -116,7 +119,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 20002,
|
||||
ConfigId = 2,
|
||||
Name = "控制轴承",
|
||||
Rarity = RarityType.Blue,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue),
|
||||
Endurance = 20,
|
||||
IsAssembledIntoTower = false,
|
||||
RotateSpeed = new[] { 200f, 250f, 300f, 320f, 350f },
|
||||
|
|
@ -130,7 +133,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 20003,
|
||||
ConfigId = 3,
|
||||
Name = "穿透轴承",
|
||||
Rarity = RarityType.Purple,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple),
|
||||
Endurance = 96f,
|
||||
IsAssembledIntoTower = false,
|
||||
RotateSpeed = new[] { 60f, 70f, 80f, 90f, 100f },
|
||||
|
|
@ -144,7 +147,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 30002,
|
||||
ConfigId = 2,
|
||||
Name = "控制底座",
|
||||
Rarity = RarityType.Blue,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Blue),
|
||||
Endurance = 50f,
|
||||
IsAssembledIntoTower = false,
|
||||
AttackSpeed = new[] { 4f, 4.2f, 4.4f, 4.6f, 4.8f },
|
||||
|
|
@ -158,7 +161,7 @@ namespace GeometryTD.CustomUtility
|
|||
InstanceId = 30003,
|
||||
ConfigId = 3,
|
||||
Name = "穿透底座",
|
||||
Rarity = RarityType.Purple,
|
||||
Rarity = InventoryRarityRuleService.NormalizeComponentRarity(RarityType.Purple),
|
||||
Endurance = 30f,
|
||||
IsAssembledIntoTower = false,
|
||||
AttackSpeed = new[] { 1f, 1f, 1f, 1f, 1f },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using GeometryTD.CustomUtility;
|
||||
using GeometryTD.Definition;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GeometryTD.Tests.EditMode
|
||||
{
|
||||
public sealed class InventoryRarityRuleServiceTests
|
||||
{
|
||||
[TestCase(RarityType.None, RarityType.White)]
|
||||
[TestCase(RarityType.White, RarityType.White)]
|
||||
[TestCase(RarityType.Green, RarityType.Green)]
|
||||
[TestCase(RarityType.Red, RarityType.Red)]
|
||||
[TestCase((RarityType)(-1), RarityType.White)]
|
||||
[TestCase((RarityType)99, RarityType.White)]
|
||||
public void NormalizeComponentRarity_Returns_Expected_Value(
|
||||
RarityType input,
|
||||
RarityType expected)
|
||||
{
|
||||
RarityType result = InventoryRarityRuleService.NormalizeComponentRarity(input);
|
||||
|
||||
Assert.That(result, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(RarityType.White, RarityType.Green, RarityType.Green, RarityType.Green)]
|
||||
[TestCase(RarityType.Green, RarityType.Blue, RarityType.Purple, RarityType.Blue)]
|
||||
[TestCase(RarityType.Red, RarityType.White, RarityType.White, RarityType.Green)]
|
||||
[TestCase(RarityType.None, RarityType.None, RarityType.None, RarityType.White)]
|
||||
[TestCase((RarityType)99, RarityType.Red, RarityType.Red, RarityType.Purple)]
|
||||
public void ResolveTowerRarity_Returns_Average_Rounded_Result(
|
||||
RarityType muzzleRarity,
|
||||
RarityType bearingRarity,
|
||||
RarityType baseRarity,
|
||||
RarityType expected)
|
||||
{
|
||||
RarityType result = InventoryRarityRuleService.ResolveTowerRarity(
|
||||
muzzleRarity,
|
||||
bearingRarity,
|
||||
baseRarity);
|
||||
|
||||
Assert.That(result, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CreateSampleInventory_Uses_Same_Tower_Rarity_Rule_As_Service()
|
||||
{
|
||||
BackpackInventoryData inventory = InventorySeedUtility.CreateSampleInventory();
|
||||
TowerItemData tower = inventory.Towers[0];
|
||||
MuzzleCompItemData muzzle = inventory.MuzzleComponents[0];
|
||||
BearingCompItemData bearing = inventory.BearingComponents[0];
|
||||
BaseCompItemData baseComp = inventory.BaseComponents[0];
|
||||
|
||||
RarityType expected = InventoryRarityRuleService.ResolveTowerRarity(
|
||||
muzzle.Rarity,
|
||||
bearing.Rarity,
|
||||
baseComp.Rarity);
|
||||
|
||||
Assert.That(tower.Rarity, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d64c323af12f4036a8c0baed83f750aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -123,12 +123,46 @@
|
|||
|
||||
## 阶段 S4 - 收口品质 / Tag 规则
|
||||
|
||||
| 状态 | ID | 任务 | 交付物路径 | 验收标准 |
|
||||
|-----|-------|-------------------------|-----------------------------------------------------------------------------------------------------|----------------|
|
||||
| [ ] | S4-01 | 先确定 M1 需要的品质 / Tag 规则边界 | `docs/CodeX-TODO.md`<br>`docs/TODO.md` | 文档先对齐,再落代码 |
|
||||
| [ ] | S4-02 | 把品质计算整理成单一入口 | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/` | 组装、掉落、商店使用一致结果 |
|
||||
| [ ] | S4-03 | 把 Tag 生成与过滤整理成单一入口 | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/` | Tag 结果可复现且可解释 |
|
||||
| [ ] | S4-04 | 补齐与数据表规则的映射关系 | `Assets/GameMain/Scripts/DataTable/`<br>`Assets/GameMain/Scripts/Definition/` | 表字段不是只存在而未被消费 |
|
||||
| 状态 | ID | 任务 | 交付物路径 | 验收标准 |
|
||||
|-----|-------|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------|-----------------------------|
|
||||
| [x] | S4-01 | 先确定 M1 需要的品质 / Tag 规则边界 | `docs/CodeX-TODO.md`<br>`docs/TODO.md` | 文档先对齐,再落代码 |
|
||||
| [x] | S4-02 | 把品质计算整理成单一入口 | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/` | 组装、掉落、商店使用一致结果 |
|
||||
| [x] | S4-03 | 先固化 Tag 系统设计与首发范围 | `docs/TagSystemDesign.md`<br>`docs/CodeX-TODO.md` | Tag 的来源、汇总、生效与首发集合口径固定 |
|
||||
| [ ] | S4-04 | 实现组件实例 Tag 的统一生成入口 | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/` | 掉落、商店、初始种子、事件奖励共用同一生成结果 |
|
||||
| [ ] | S4-05 | 实现组塔后的 Tag 汇总与展示入口 | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`<br>`Assets/GameMain/Scripts/UI/` | 组件 Tag 可汇总为塔级结果,且 UI 展示口径一致 |
|
||||
| [ ] | S4-06 | 实现首批基础 Tag 的战斗生效 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Components/`<br>`Assets/GameMain/Scripts/Definition/` | 首发 6~8 个基础 Tag 至少完成一批可验证效果 |
|
||||
| [ ] | S4-07 | 补齐 Tag 规则与数据表的映射关系 | `Assets/GameMain/Scripts/DataTable/`<br>`Assets/GameMain/Scripts/Definition/` | 表字段不是只存在而未被消费,Tag 参数可配置可解释 |
|
||||
|
||||
> 2026-03-09 更新:`InventoryRarityRuleService` 已落地;塔品质计算与组件品质归一化已统一收口,`PlayerInventoryTowerAssemblyService`、`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 已接入同一规则入口。你已确认 Unity Test Runner 中 `Assets/Tests/EditMode` 全部通过,其中包含新增的 `InventoryRarityRuleServiceTests`。
|
||||
>
|
||||
> 2026-03-09 更新:`S4-03` 文档口径已冻结;`TagSystemDesign.md` 已明确组件实例生成、塔级 `Stack` 汇总、四段触发阶段、`Tag.txt + TagRule` 双表方向,以及 MVP 正式首发 7 个 Tag:`Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`。`BurnSpread` 已明确后移。
|
||||
|
||||
### S4-01 边界结论
|
||||
|
||||
- `S4-01` 只收口 M1 所需的最小规则闭环:先统一“品质如何算、Tag 如何生成/过滤、哪些链路必须共用同一结果”,当前阶段不要求 Tag 立即产生战斗效果。
|
||||
- 品质在当前仓库中不是空白功能:组件实例已有 `Rarity` 字段,掉落、商店、初始种子都已能给组件实例赋品质;塔组装时也已有“三组件品质取平均并四舍五入”的现有实现。
|
||||
- M1 对品质的目标口径是:塔品质必须由枪口 / 轴承 / 底座三个组件品质决定,且结果在组装、掉落、商店、展示、事件条件筛选等链路中可复现、可解释、口径一致。
|
||||
- M1 当前不把“品质决定配件槽位数量”视为阻塞项;配件系统本体尚未进入主链路,这部分仍保留在后续深度阶段,而不是作为 `S4-01` 当下必须落地的规则。
|
||||
- Tag 在当前仓库中也不是空白功能:组件表已有 `PossibleTag`,实例数据与展示链路也已有 `Tags` 字段与名称展示,但现在更接近“静态拷贝 + 展示消费”,还没有统一生成 / 过滤入口。
|
||||
- M1 对 Tag 的目标口径是:Tag 结果必须有统一来源,且在组装、掉落、商店、展示链路中可复现、可解释;不能再继续分散为“哪边方便哪边直接复制 `PossibleTag`”。
|
||||
- `Tag.txt` 的 `MinRarity` 与三张组件表的 `PossibleTag` 都应视为 `S4-03 ~ S4-04` 的规则输入,而不是已经被完整消费的既成事实;当前文档必须明确这一点,避免误判为“表字段已落地”。
|
||||
- 设计稿中的深度规则暂不纳入 M1 必收口范围:不要求现在就实现按品质随机 Tag 数量、不要求实现 Tag 等级成长,也不要求实现 Tag 的战斗数值效果。
|
||||
|
||||
### S4 拆分方案
|
||||
|
||||
- `S4-02` 只处理品质入口统一,不与 Tag 生成细节耦合;目标是把“塔品质计算、组件品质输入、展示品质”都收口到统一规则服务。
|
||||
- `S4-03` 对应 `docs/TagSystemDesign.md` 的文档收口阶段:固定 Tag 的候选池、实例生成时机、组塔汇总方式、战斗触发阶段、以及 MVP 正式首发 7 个基础 Tag 的集合。
|
||||
- `S4-04` 是 Tag 系统的第一段代码落地:为组件实例引入统一的 Tag 生成入口,不再直接复制 `PossibleTag` 全量候选;掉落、商店、初始种子、事件奖励必须共用同一套生成逻辑与随机口径。
|
||||
- `S4-05` 负责把三组件 Tag 汇总为塔级结果,并统一背包、组装、商店、详情面板的展示口径;此阶段不要求完成全部战斗效果,但不能再停留在“简单并集去重”的临时实现上。
|
||||
- `S4-06` 只落首批基础 Tag 的战斗效果,不一次展开全部 12 个 Tag;优先级按 `TagSystemDesign.md` 收口为状态类和数值修正类,例如 `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`。
|
||||
- `S4-07` 负责把 Tag 设计真正映射回数据表与 DataRow,不再只保留 `MinRarity` 与 `PossibleTag` 两个弱约束字段;至少要支持权重、触发阶段、说明或效果参数中的一部分配置化消费。
|
||||
|
||||
### S4-03 首发范围结论
|
||||
|
||||
- MVP 首发 Tag 总量仍受 `docs/MVP-Scope.md` 的 `6~8` 个约束,但当前正式首发集合已固定为 7 个。
|
||||
- 首批优先做状态类与数值修正类,不优先做穿透、传播、爆炸、多命中等高侵入效果。
|
||||
- 当前正式首发基础集合为:`Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`。
|
||||
- `BurnSpread`、`Pierce`、`Overpenetrate`、`FreezeMask`、`IgniteBurst` 默认放到后续扩展阶段,不作为 `S4` 当前完成标准。
|
||||
|
||||
## 阶段 S5 - 收口耐久规则
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
- 当前仓库已经具备 `ProcedureMain + NodeMapForm + CombatNode + EventNode + ShopNode` 的主流程 Run 闭环,可以从主菜单进入游戏,完成固定 10 节点流程,并在 Boss 结算后进入正式结束态并回到主菜单。
|
||||
- `NodeMapForm` 已满足 MVP 所需的节点流程界面;M1 现在的真实缺口是合法出战 / 品质 / Tag / 耐久规则是否真正统一收口。
|
||||
- `P0-10 ~ P0-12` 在代码里都已有局部实现,因此文档里统一按“部分完成但未收口”处理;只有满足最终验收标准后才可改成完成。
|
||||
- `P0-11` 在 M1 中按“规则最小闭环”收口:只要求品质计算与 Tag 来源形成单一、可复现、跨流程共用的规则入口;配件槽位、Tag 数量 / 等级随机、Tag 战斗效果暂不作为当前 M1 阻塞项。
|
||||
- `P0-11` 的 Tag 规则拆分已记录到 `docs/TagSystemDesign.md`:先收口实例生成、塔级汇总与首发 Tag 集合,再决定哪些效果进入战斗链路。
|
||||
|
||||
## 里程碑 M1(P0)- 最小可玩闭环
|
||||
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
| [x] | P0-08 | 胜利波次与结算规则(100/80/50/<50) | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 |
|
||||
| [x] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |
|
||||
| [~] | P0-10 | 节点后组装:枪口/轴承/底座三组件约束 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/UI/Templates/GameScene/` | 当前只校验“参与区至少有 1 座塔”,尚未收口为“三组件完整合法参战” |
|
||||
| [~] | P0-11 | 品质/槽位/Tag 计算落地(白绿蓝紫红) | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/Entity/` | 组装、商店、展示链路已有局部品质 / Tag 赋值,但还没有单一、可复现、跨流程共用的规则入口 |
|
||||
| [~] | P0-11 | 品质 / Tag 规则统一入口(白绿蓝紫红) | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/Entity/` | 品质计算与 Tag 来源在组装、掉落、商店、展示链路形成单一、可复现、跨流程共用的规则入口;配件槽位与 Tag 深度规则暂不作为当前 M1 阻塞项 |
|
||||
| [~] | P0-12 | 组件/配件耐久生效与 0 耐久销毁 | `Assets/GameMain/Scripts/Entity/` | 已有耐久字段、展示与扣减入口,但尚未影响属性 / 出战资格,也未形成 `0` 耐久移除或失效闭环 |
|
||||
|
||||
## 里程碑 M2(P1)- 核心深度
|
||||
|
|
@ -62,7 +64,7 @@
|
|||
## 本周建议开工顺序
|
||||
|
||||
1. 先完成 `P0-10`(把“至少有参战塔”提升为“满足完整合法参战条件才能出战”)
|
||||
2. 再处理 `P0-11` ~ `P0-12`(先统一 M1 范围,再决定是完整收口还是同步缩范围)
|
||||
2. 再处理 `P0-11` ~ `P0-12`(先统一 M1 范围;`P0-11` 先收口品质 / Tag 的最小规则闭环,再决定是否扩展到完整深度设计)
|
||||
3. 最后补关键流程 / 规则回归测试,并同步文档状态
|
||||
|
||||
## 设计优化 Backlog(新增)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,467 @@
|
|||
# Tag System Design
|
||||
|
||||
最后更新:2026-03-09
|
||||
|
||||
> 目标:梳理 GeometryTD 的 Tag 系统设计问题,明确组件随机生成规则、战斗生效方式,以及后续实现顺序。
|
||||
|
||||
## 1. 当前现状
|
||||
|
||||
当前仓库已经有 Tag 相关的基础数据结构,但还没有形成完整系统:
|
||||
|
||||
- `TagType` 已定义 12 个 Tag,见 `Assets/GameMain/Scripts/Definition/Enum/TagType.cs`
|
||||
- `Tag.txt` 已定义 Tag 名称与 `MinRarity`
|
||||
- `MuzzleComp.txt`、`BearingComp.txt`、`BaseComp.txt` 已定义 `PossibleTag`
|
||||
- 组件实例、塔实例、UI 展示链路都已有 `Tags` 字段
|
||||
|
||||
当前问题也很明确:
|
||||
|
||||
- 组件实例现在更接近“直接复制 `PossibleTag` 全量候选”,不是真正的随机生成
|
||||
- `Tag.txt` 只有最低品质限制,不足以描述权重、互斥、数量、效果参数
|
||||
- 塔组装阶段当前只是简单做并集去重,无法表达重复 Tag 是叠层还是浪费
|
||||
- 战斗链路里,子弹命中只传 `damage + AttackPropertyType`,还没有 Tag 结算入口
|
||||
|
||||
因此,当前 Tag 更像“静态标签字段”,而不是“有来源、有汇总、有战斗行为”的系统。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
Tag 系统需要同时满足四个目标:
|
||||
|
||||
1. 组件实例上的 Tag 来源统一,不能继续由不同链路各自硬编码
|
||||
2. Tag 随机结果可复现,掉落、商店、初始种子、存档读档后口径一致
|
||||
3. 塔由三组件组装后,Tag 汇总结果可解释,重复 Tag 的处理规则清晰
|
||||
4. 战斗中的 Tag 生效有统一挂载点,不把效果分散塞进子弹、敌人、塔控制器各处
|
||||
|
||||
## 3. 核心设计问题
|
||||
|
||||
### 3.1 Tag 类型混杂
|
||||
|
||||
当前 `TagType` 同时混了三类语义:
|
||||
|
||||
- 状态类:`Fire`、`Ice`
|
||||
- 数值修正类:`Crit`、`Execution`、`Shatter`
|
||||
- 攻击形态类:`BurnSpread`、`Pierce`、`Overpenetrate`
|
||||
|
||||
这三类 Tag 的触发时机不同:
|
||||
|
||||
- 有的在命中前生效
|
||||
- 有的在命中时生效
|
||||
- 有的在命中后挂状态
|
||||
- 有的在击杀后触发
|
||||
|
||||
如果后续继续只用一个 `TagType[]` 直接驱动所有逻辑,代码会迅速失控。后续实现必须引入“Tag 元数据 + 触发阶段”的概念。
|
||||
|
||||
### 3.2 候选 Tag 不等于实例 Tag
|
||||
|
||||
组件表中的 `PossibleTag` 应理解为“这个组件可能产出的 Tag 候选池”,不是实例最终持有的 Tag 列表。
|
||||
|
||||
正确关系应为:
|
||||
|
||||
- DataTable:给候选池
|
||||
- 实例生成器:按规则从候选池里抽 Tag
|
||||
- 组件实例:保存最终抽出的 Tag 结果
|
||||
- 塔实例:组装时汇总三组件 Tag
|
||||
|
||||
### 3.3 重复 Tag 如何处理
|
||||
|
||||
三组件组塔后,同一个 Tag 很可能在多个组件上重复出现。这里必须明确规则,否则后续实现会反复返工。
|
||||
|
||||
固定规则:
|
||||
|
||||
- 同一组件内不允许重复同一个 Tag
|
||||
- 不同组件之间允许重复
|
||||
- 组塔时不直接丢弃重复 Tag,而是转成 `Stack`
|
||||
|
||||
也就是说,最终塔上的 Tag 结果不应该只是简单 `TagType[]`,而应该是“Tag + 层数”。
|
||||
|
||||
## 4. 推荐的数据模型
|
||||
|
||||
### 4.1 配置层
|
||||
|
||||
后续配置层固定采用“`Tag.txt` 基础字典 + `TagRule` 规则表”两层结构,而不是继续把所有规则硬塞进现有 `Tag.txt`。
|
||||
|
||||
- `Tag.txt`
|
||||
- 保留 `TagType`、`Name` 等基础字典信息
|
||||
- `TagRule`
|
||||
- 承载运行规则字段,至少包括以下内容:
|
||||
- `MinRarity`
|
||||
- `Weight`
|
||||
- `MaxComponentStack`
|
||||
- `TriggerPhase`
|
||||
- `Description`
|
||||
- `ParamJson` 或等价参数字段
|
||||
|
||||
用途:
|
||||
|
||||
- `MinRarity`:该 Tag 最低可出现品质
|
||||
- `Weight`:同一候选池内抽取权重
|
||||
- `MaxComponentStack`:单组件允许的最大层数
|
||||
- `TriggerPhase`:用于战斗结算路由
|
||||
- `ParamJson`:承载伤害倍率、持续时间、范围等效果参数
|
||||
|
||||
### 4.2 运行时结构
|
||||
|
||||
后续运行时模型固定从当前单纯的 `TagType[]` 演进为两层结构:
|
||||
|
||||
```csharp
|
||||
public sealed class TagEntryData
|
||||
{
|
||||
public TagType TagType { get; set; }
|
||||
public int Stack { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
public sealed class TagRuntimeData
|
||||
{
|
||||
public TagType TagType { get; set; }
|
||||
public int TotalStack { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
职责划分:
|
||||
|
||||
- 组件实例保存 `TagEntryData[]`
|
||||
- 塔实例保存汇总后的 `TagRuntimeData[]`
|
||||
- UI 展示可以继续只展示 Tag 名称,但底层不再丢失层数信息
|
||||
|
||||
## 5. 组件上如何随机生成 Tag
|
||||
|
||||
### 5.1 随机时机
|
||||
|
||||
Tag 随机应发生在“组件实例创建时”,而不是组塔时。
|
||||
|
||||
包括以下来源:
|
||||
|
||||
- 初始种子生成组件
|
||||
- 战斗掉落生成组件
|
||||
- 商店刷新生成组件
|
||||
- 事件奖励生成组件
|
||||
|
||||
这样做的好处:
|
||||
|
||||
- 玩家拿到手的组件就是确定结果
|
||||
- 背包展示、组装预览、商店预览都能直接展示真实 Tag
|
||||
- 存档不需要二次重算
|
||||
|
||||
### 5.2 候选池构建
|
||||
|
||||
单个组件实例的候选池按以下规则生成:
|
||||
|
||||
1. 读取组件配置的 `PossibleTag`
|
||||
2. 读取 Tag 表
|
||||
3. 过滤掉 `MinRarity > 组件品质` 的 Tag
|
||||
4. 只保留同时存在于 `PossibleTag` 与 Tag 表允许范围内的 Tag
|
||||
|
||||
结果:
|
||||
|
||||
- `PossibleTag` 决定这个组件系列“能出什么”
|
||||
- `MinRarity` 决定当前品质“允许出到什么档位”
|
||||
|
||||
### 5.3 Tag 数量预算
|
||||
|
||||
每个品质都保留独立的 Tag 数量预算,而不是按概率硬编码在代码里。
|
||||
|
||||
推荐默认值:
|
||||
|
||||
| 品质 | 推荐数量 |
|
||||
|------|----------|
|
||||
| White | `0~1` |
|
||||
| Green | `1` |
|
||||
| Blue | `1~2` |
|
||||
| Purple | `2` |
|
||||
| Red | `2~3` |
|
||||
|
||||
当前若严格遵循 MVP 范围,也可以先只做到:
|
||||
|
||||
- `White`: `0~1`
|
||||
- `Green`: `1`
|
||||
- `Blue`: `1~2`
|
||||
- `Purple`: `2`
|
||||
- `Red`: 暂不开放或仅内部保留
|
||||
|
||||
### 5.4 随机规则
|
||||
|
||||
固定抽取流程:
|
||||
|
||||
1. 先根据品质决定本组件本次要抽几个 Tag
|
||||
2. 从候选池中按 `Weight` 抽取
|
||||
3. 单组件内不重复抽同一个 Tag
|
||||
4. 候选池不足时允许少于目标数量,不强行补 Tag
|
||||
|
||||
### 5.5 可复现要求
|
||||
|
||||
Tag 随机必须可复现。
|
||||
|
||||
统一随机种子来源:
|
||||
|
||||
- `RunSeed`
|
||||
- `ItemInstanceId`
|
||||
- `ConfigId`
|
||||
- `SourceType`(掉落 / 商店 / 初始 / 事件)
|
||||
|
||||
原则:
|
||||
|
||||
- 同一个组件实例只生成一次 Tag
|
||||
- 存档与读档不重新随机
|
||||
- 商店刷新前后只有新实例才会有新结果
|
||||
|
||||
## 6. 组塔后的 Tag 汇总规则
|
||||
|
||||
组塔阶段不再随机,只做汇总。
|
||||
|
||||
固定流程:
|
||||
|
||||
1. 收集三组件的 `TagEntryData[]`
|
||||
2. 按 `TagType` 分组
|
||||
3. 累加 `Stack`
|
||||
4. 输出塔级别的 `TagRuntimeData[]`
|
||||
|
||||
示例:
|
||||
|
||||
- 枪口:`Fire x1`
|
||||
- 轴承:`Fire x1`, `BurnSpread x1`
|
||||
- 底座:`Inferno x1`
|
||||
|
||||
组塔后:
|
||||
|
||||
- `Fire x2`
|
||||
- `BurnSpread x1`
|
||||
- `Inferno x1`
|
||||
|
||||
这比简单去重更合理,因为后续战斗效果可以基于层数增强,而不是浪费重复结果。
|
||||
|
||||
## 7. 战斗中如何生效
|
||||
|
||||
### 7.1 现有链路限制
|
||||
|
||||
当前战斗链路是:
|
||||
|
||||
- `TowerController` 取塔属性
|
||||
- `BasicBaseComp` 发起攻击
|
||||
- `ShooterMuzzleComp` 创建子弹
|
||||
- `ShooterBullet` 命中后调用 `IDamageReceiver.TakeDamage(int damage, AttackPropertyType attackPropertyType)`
|
||||
|
||||
这条链路只够表达:
|
||||
|
||||
- 伤害数值
|
||||
- 伤害属性
|
||||
|
||||
不够表达:
|
||||
|
||||
- 暴击
|
||||
- 斩杀
|
||||
- 燃烧 DOT
|
||||
- 冻结层数
|
||||
- 穿透与连锁
|
||||
- 命中后爆炸
|
||||
|
||||
所以 Tag 不能直接继续依附在 `TakeDamage(int, AttackPropertyType)` 上。
|
||||
|
||||
### 7.2 推荐挂载点
|
||||
|
||||
后续战斗入口固定引入统一的命中载荷与结算器:
|
||||
|
||||
```csharp
|
||||
AttackPayload
|
||||
```
|
||||
|
||||
至少包含:
|
||||
|
||||
- `BaseDamage`
|
||||
- `AttackPropertyType`
|
||||
- `TagRuntimeData[]`
|
||||
- 攻击来源信息
|
||||
|
||||
命中时再构造:
|
||||
|
||||
```csharp
|
||||
HitContext
|
||||
```
|
||||
|
||||
至少包含:
|
||||
|
||||
- `Attacker`
|
||||
- `Target`
|
||||
- `AttackPayload`
|
||||
- 是否击杀
|
||||
- 命中位置
|
||||
|
||||
统一由:
|
||||
|
||||
```csharp
|
||||
TagEffectResolver
|
||||
```
|
||||
|
||||
处理 Tag 逻辑。
|
||||
|
||||
### 7.3 触发阶段
|
||||
|
||||
Tag 触发阶段固定拆成四段:
|
||||
|
||||
1. `OnBeforeHit`
|
||||
- 例:`Crit`、`Execution`
|
||||
2. `OnHit`
|
||||
- 例:直接附加伤害、穿透判定
|
||||
3. `OnAfterHit`
|
||||
- 例:施加燃烧、减速、冻结层数
|
||||
4. `OnKill`
|
||||
- 例:击杀爆炸、传播
|
||||
|
||||
这样可以避免把所有效果都堆进一个巨大 `switch` 里。
|
||||
|
||||
## 8. 当前 12 个 Tag 的效果定位
|
||||
|
||||
### 8.1 第一批优先落地
|
||||
|
||||
这些效果与当前伤害模型更兼容,适合作为第一批战斗 Tag:
|
||||
|
||||
| Tag | 首发定位 |
|
||||
|-----|----------|
|
||||
| `Fire` | 命中附加燃烧 DOT |
|
||||
| `Ice` | 命中附加减速 |
|
||||
| `Crit` | 命中前按概率暴击 |
|
||||
| `Execution` | 对低血量目标增伤或直接处决 |
|
||||
| `Shatter` | 对已减速 / 已冻结目标增伤 |
|
||||
| `Inferno` | 强化燃烧伤害或持续时间 |
|
||||
| `AbsoluteZero` | 强化减速,或提高冻结触发速度 |
|
||||
|
||||
这些效果主要需要:
|
||||
|
||||
- 命中前倍率修正
|
||||
- 敌人状态容器
|
||||
- DOT / Slow 的 runtime tick
|
||||
|
||||
对当前战斗架构侵入相对可控。
|
||||
|
||||
### 8.2 第二批再落地
|
||||
|
||||
这些效果需要更完整的弹道 / 范围 / 状态系统,固定放到后续阶段实现:
|
||||
|
||||
| Tag | 后续定位 |
|
||||
|-----|----------|
|
||||
| `BurnSpread` | 燃烧向邻近敌人传播 |
|
||||
| `IgniteBurst` | 燃烧结束或击杀时爆炸 |
|
||||
| `FreezeMask` | 冻结积累条 / 冻结面具机制 |
|
||||
| `Pierce` | 子弹贯穿多个目标 |
|
||||
| `Overpenetrate` | 贯穿后保留部分伤害继续飞行 |
|
||||
|
||||
这些效果会牵动:
|
||||
|
||||
- 子弹多命中
|
||||
- 范围查询
|
||||
- 击杀后触发
|
||||
- 敌人持续状态传播
|
||||
|
||||
不适合在最早版本一次性塞进当前命中链路。
|
||||
|
||||
## 9. 推荐的分阶段实施方案
|
||||
|
||||
### Phase 1:规则入口
|
||||
|
||||
目标:
|
||||
|
||||
- 补齐 Tag 配置模型
|
||||
- 实现组件实例随机生成 Tag
|
||||
- 统一掉落 / 商店 / 初始种子 / 事件奖励入口
|
||||
|
||||
交付重点:
|
||||
|
||||
- `TagGenerationService`
|
||||
- `TagGenerationResult`
|
||||
- 组件实例保存最终 Tag 结果
|
||||
|
||||
### Phase 2:塔级汇总
|
||||
|
||||
目标:
|
||||
|
||||
- 组塔时把三组件 Tag 汇总成塔级结果
|
||||
- 不再简单去重,而是保留层数
|
||||
|
||||
交付重点:
|
||||
|
||||
- `TowerTagAggregationService`
|
||||
- 塔实例保存 `TagRuntimeData[]`
|
||||
|
||||
### Phase 3:战斗载荷
|
||||
|
||||
目标:
|
||||
|
||||
- 把塔的 Tag 结果传到子弹 / 命中链路
|
||||
- 引入统一结算入口
|
||||
|
||||
交付重点:
|
||||
|
||||
- `AttackPayload`
|
||||
- `HitContext`
|
||||
- `TagEffectResolver`
|
||||
|
||||
### Phase 4:第一批战斗效果
|
||||
|
||||
正式首发集合固定为:
|
||||
|
||||
- `Fire`
|
||||
- `Ice`
|
||||
- `Crit`
|
||||
- `Execution`
|
||||
- `Shatter`
|
||||
- `Inferno`
|
||||
- `AbsoluteZero`
|
||||
|
||||
这批做完后,Tag 系统就从“展示字段”升级成“有实际战斗意义的最小闭环”。
|
||||
|
||||
### Phase 5:高级联动
|
||||
|
||||
后续再做:
|
||||
|
||||
- 传播
|
||||
- 爆炸
|
||||
- 冻结积累
|
||||
- 穿透与多命中
|
||||
- 更复杂的多 Tag 联动
|
||||
|
||||
## 10. 与当前 MVP 范围的关系
|
||||
|
||||
当前 `docs/TODO.md` 已把 `P0-11` 收口为“规则最小闭环”。本设计文档与该口径保持一致:
|
||||
|
||||
- M1 最低要求是统一 Tag 来源与汇总规则
|
||||
- M1 不强制要求一次做完全部高级联动
|
||||
- 若要在 M1 内让 Tag 进入战斗,只做第一批效果,不同时展开高级传播 / 贯穿体系
|
||||
|
||||
同时需要注意,`docs/MVP-Scope.md` 里写了:
|
||||
|
||||
- 基础 Tag 系统只保留 `6~8` 个
|
||||
- 不做高级 Tag 联动
|
||||
- 不做复杂触发矩阵
|
||||
|
||||
因此后续真正实施时,MVP 正式首发集合固定为以下 7 个:
|
||||
|
||||
- `Fire`
|
||||
- `Ice`
|
||||
- `Crit`
|
||||
- `Execution`
|
||||
- `Shatter`
|
||||
- `Inferno`
|
||||
- `AbsoluteZero`
|
||||
|
||||
而 `BurnSpread`、`Pierce`、`Overpenetrate`、`FreezeMask`、`IgniteBurst` 都作为后续扩展,不属于当前 `S4` 首发范围。
|
||||
|
||||
## 11. 后续文档与代码动作
|
||||
|
||||
1. 先基于本设计回写 `docs/CodeX-TODO.md` 的 `S4-03` 边界
|
||||
2. 在 `S4-07` 中新增并消费 `TagRule` 表,保留 `Tag.txt` 作为基础字典
|
||||
3. 新增 `TagGenerationService`,先收口实例生成
|
||||
4. 新增 `TowerTagAggregationService`,替换当前简单并集逻辑
|
||||
5. 评估 `AttackPayload` 是否并入现有 `BulletData`,还是单独引入
|
||||
6. 第一批只落固定 7 个基础 Tag,避免超出 `MVP-Scope`
|
||||
|
||||
## 12. 默认决策
|
||||
|
||||
若没有额外设计变更,后续默认按以下决策推进:
|
||||
|
||||
- Tag 在组件实例创建时随机,不在组塔时随机
|
||||
- `PossibleTag` 是候选池,不是最终实例值
|
||||
- 塔级 Tag 汇总保留 `Stack`
|
||||
- 配置层采用 `Tag.txt + TagRule` 双表结构
|
||||
- 第一批战斗效果优先做状态类与数值修正类,不优先做穿透 / 爆炸 / 传播
|
||||
- MVP 正式首发集合固定为 `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`
|
||||
- `BurnSpread` 明确后移,不作为当前首发集合成员
|
||||
Loading…
Reference in New Issue