S4 收尾

This commit is contained in:
SepComet 2026-03-11 15:02:44 +08:00
parent 515fe95441
commit c019f9f527
103 changed files with 367 additions and 230 deletions

5
.gitignore vendored
View File

@ -105,4 +105,7 @@ InitTestScene*.unity*
/.dotnet /.dotnet
/.idea /.idea
~$*.xlsx ~$*.xlsx
/tools /tools
/node_modules
package.json
package-lock.json

View File

@ -1,6 +1,6 @@
# Id 列1 Title Description Options # Id 列1 Title Description Options
# int string string string # int string string string
# 事件编号 策划备注 事件题目 事件描述 事件选项 # 事件编号 策划备注 事件题目 事件描述 事件选项
1 赌马 一名商人邀请你下注。赢了就能赚一笔。 [{"optionText":"下注 100- 稳健70% 赢 150","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":150}}],"probability":0.7},{"optionText":"下注 100- 激进30% 赢 250","requirements":[{"type":"GoldAtLeast","param":{"Count":100}}],"costEffects":[{"type":"AddGold","param":{"Count":-100}}],"rewardEffects":[{"type":"AddGold","param":{"Count":250}}],"probability":0.3}] 1 赌马 一名商人邀请你下注。赢了就能赚一笔。 [{\"optionText\":\"下注 100- 稳健70% 赢 150\",\"requirements\":[{\"type\":\"GoldAtLeast\",\"param\":{\"Count\":100}}],\"costEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":-100}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":150}}],\"probability\":0.7},{\"optionText\":\"下注 100- 激进30% 赢 250\",\"requirements\":[{\"type\":\"GoldAtLeast\",\"param\":{\"Count\":100}}],\"costEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":-100}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":250}}],\"probability\":0.3}]
2 工匠的熔炉 工匠以金币交换防御塔组件。 [{"optionText":"交出 3 个白色组件,获得 50 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":3,"Rarity":"White"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":3,"Rarity":"White"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"交出 2 个绿色组件,获得 70 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":2,"Rarity":"Green"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":2,"Rarity":"Green"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":70}}]},{"optionText":"交出 1 个蓝色组件,获得 80 金币","requirements":[{"type":"CompCountAtLeast","param":{"Count":1,"Rarity":"Blue"}}],"costEffects":[{"type":"RemoveRandomComps","param":{"Count":1,"Rarity":"Blue"}}],"rewardEffects":[{"type":"AddGold","param":{"Count":80}}]},{"optionText":"拒绝","rewardEffects":[]}] 2 工匠的熔炉 工匠以金币交换防御塔组件。 [{\"optionText\":\"交出 3 个白色组件,获得 50 金币\",\"requirements\":[{\"type\":\"CompCountAtLeast\",\"param\":{\"Count\":3,\"Rarity\":\"White\"}}],\"costEffects\":[{\"type\":\"RemoveRandomComps\",\"param\":{\"Count\":3,\"Rarity\":\"White\"}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":50}}]},{\"optionText\":\"交出 2 个绿色组件,获得 70 金币\",\"requirements\":[{\"type\":\"CompCountAtLeast\",\"param\":{\"Count\":2,\"Rarity\":\"Green\"}}],\"costEffects\":[{\"type\":\"RemoveRandomComps\",\"param\":{\"Count\":2,\"Rarity\":\"Green\"}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":70}}]},{\"optionText\":\"交出 1 个蓝色组件,获得 80 金币\",\"requirements\":[{\"type\":\"CompCountAtLeast\",\"param\":{\"Count\":1,\"Rarity\":\"Blue\"}}],\"costEffects\":[{\"type\":\"RemoveRandomComps\",\"param\":{\"Count\":1,\"Rarity\":\"Blue\"}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":80}}]},{\"optionText\":\"拒绝\",\"rewardEffects\":[]}]
3 代价与回报 某种黑暗力量向你索取代价。 [{"optionText":"展示你的防御塔","requirements":[{"type":"TowerCountAtLeast","param":{"Count":2}}],"rewardEffects":[{"type":"AddGold","param":{"Count":50}}]},{"optionText":"离开","rewardEffects":[]}] 3 代价与回报 某种黑暗力量向你索取代价。 [{\"optionText\":\"展示你的防御塔\",\"requirements\":[{\"type\":\"TowerCountAtLeast\",\"param\":{\"Count\":2}}],\"rewardEffects\":[{\"type\":\"AddGold\",\"param\":{\"Count\":50}}]},{\"optionText\":\"离开\",\"rewardEffects\":[]}]

View File

@ -1,15 +1,15 @@
# Id 列1 Name MinRarity Weight # Id 列1 Name TagGroup MinRarity Weight IsImplemented
# int string RarityType int # int string string RarityType int bool
# Tag编号 策划备注 Tag名 获取最低稀有度 权重 # Tag编号 策划备注 Tag名 Tag所属 获取最低稀有度 权重 是否启用
1 元素 Fire White 20 1 元素 Fire Element White 20 True
2 元素 BurnSpread White 20 2 元素 BurnSpread Element White 20 False
3 元素 IgniteBurst Green 15 3 元素 IgniteBurst Element Green 15 False
4 元素 Inferno Purple 5 4 元素 Inferno Element Purple 5 True
5 控制 Ice White 1 5 控制 Ice Control White 1 True
6 控制 FreezeMask White 20 6 控制 FreezeMask Control White 20 False
7 控制 Shatter Green 15 7 控制 Shatter Control Green 15 True
8 控制 AbsoluteZero Purple 1 8 控制 AbsoluteZero Control Purple 1 True
9 穿透 Pierce White 20 9 穿透 Pierce Penetrate White 20 False
10 穿透 Crit White 20 10 穿透 Crit Penetrate White 20 True
11 穿透 Overpenetrate Green 15 11 穿透 Overpenetrate Penetrate Green 15 False
12 穿透 Execution Purple 5 12 穿透 Execution Penetrate Purple 5 True

View File

@ -1,15 +1,15 @@
# Id 列1 TagType TriggerPhase Description Param # Id 列1 TagType TriggerPhase Description Param
# int TagType TagTriggerPhase string string # int TagType TagTriggerPhase string string
# Tag配置编号 策划备注 所属Tag类型 触发阶段 描述 参数Json # Tag配置编号 策划备注 所属Tag类型 触发阶段 描述 参数Json
1 元素 Fire OnAfterHit 持续对敌人造成<color=red>火</color>伤害 {"BurnDurationSeconds":3,"BurnDamagePerSecondPerStack":20,"MaxEffectiveStack":5} 1 元素 Fire OnAfterHit 持续对敌人造成<color=red>火</color>伤害 {\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":5}
2 元素 BurnSpread None 燃烧向邻近敌人传播 {"SpreadRadius":2,"SpreadDamageRate":1} 2 元素 BurnSpread None 燃烧向邻近敌人传播 {\"SpreadRadius\":2,\"SpreadDamageRate\":1}
3 元素 IgniteBurst None 燃烧结束或击杀时爆炸 {"BurstRadius":1,"BurstDamageRate":0.5} 3 元素 IgniteBurst None 燃烧结束或击杀时爆炸 {\"BurstRadius\":1,\"BurstDamageRate\":0.5}
4 元素 Inferno OnAfterHit 强化燃烧伤害或持续时间 {"BonusBurnDurationSeconds":0.1,"BonusBurnDamagePerSecondPerStack":0.1} 4 元素 Inferno OnAfterHit 强化燃烧伤害或持续时间 {\"BonusBurnDurationSeconds\":0.1,\"BonusBurnDamagePerSecondPerStack\":0.1}
5 控制 Ice OnAfterHit 命中附加减速 {"SlowDurationSeconds":2,"SlowRatioPerStack":0.2,"MinMoveSpeedMultiplier":0.4} 5 控制 Ice OnAfterHit 命中附加减速 {\"SlowDurationSeconds\":2,\"SlowRatioPerStack\":0.2,\"MinMoveSpeedMultiplier\":0.4}
6 控制 FreezeMask None 冻结积累条 / 冻结面具机制 {"FreezeBuildUpPerStack":0.1} 6 控制 FreezeMask None 冻结积累条 / 冻结面具机制 {\"FreezeBuildUpPerStack\":0.1}
7 控制 Shatter OnBeforeHit 对已减速 / 已冻结目标增伤 {"RequiresSlowedTarget":true,"DamageBonusPerStack":0.1} 7 控制 Shatter OnBeforeHit 对已减速 / 已冻结目标增伤 {\"RequiresSlowedTarget\":true,\"DamageBonusPerStack\":0.1}
8 控制 AbsoluteZero OnAfterHit 强化减速,或提高冻结触发速度 {"BonusSlowDurationSeconds":0.1,"BonusSlowRatioPerStack":0.2} 8 控制 AbsoluteZero OnAfterHit 强化减速,或提高冻结触发速度 {\"BonusSlowDurationSeconds\":0.1,\"BonusSlowRatioPerStack\":0.2}
9 穿透 Pierce None 子弹贯穿多个目标 {"ExtraPierceCount":2} 9 穿透 Pierce None 子弹贯穿多个目标 {\"ExtraPierceCount\":2}
10 穿透 Crit OnBeforeHit 命中前按概率暴击 {"CritChancePerStack":0.1,"CritDamageMultiplier":1.5} 10 穿透 Crit OnBeforeHit 命中前按概率暴击 {\"CritChancePerStack\":0.1,\"CritDamageMultiplier\":1.5}
11 穿透 Overpenetrate None 贯穿后保留部分伤害继续飞行 {"ExtraPenetrationCount":0.1,"RemainingDamageRate":0.2} 11 穿透 Overpenetrate None 贯穿后保留部分伤害继续飞行 {\"ExtraPenetrationCount\":0.1,\"RemainingDamageRate\":0.2}
12 穿透 Execution OnBeforeHit 对低血量目标增伤或直接处决 {"TargetHealthThreshold":0.3,"DamageBonusPerStack":0.5} 12 穿透 Execution OnBeforeHit 对低血量目标增伤或直接处决 {\"TargetHealthThreshold\":0.3,\"DamageBonusPerStack\":0.5}

View File

@ -355,7 +355,7 @@ namespace GeometryTD.CustomComponent
Rarity = rarity, Rarity = rarity,
Endurance = 100f, Endurance = 100f,
Constraint = config.Constraint, Constraint = config.Constraint,
Tags = InventoryTagRuleService.ResolveComponentTags( Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag, config.PossibleTag,
rarity, rarity,
InventoryTagSourceType.Drop, InventoryTagSourceType.Drop,
@ -393,7 +393,7 @@ namespace GeometryTD.CustomComponent
Rarity = rarity, Rarity = rarity,
Endurance = 100f, Endurance = 100f,
Constraint = config.Constraint, Constraint = config.Constraint,
Tags = InventoryTagRuleService.ResolveComponentTags( Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag, config.PossibleTag,
rarity, rarity,
InventoryTagSourceType.Drop, InventoryTagSourceType.Drop,
@ -430,7 +430,7 @@ namespace GeometryTD.CustomComponent
Rarity = rarity, Rarity = rarity,
Endurance = 100f, Endurance = 100f,
Constraint = config.Constraint, Constraint = config.Constraint,
Tags = InventoryTagRuleService.ResolveComponentTags( Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag, config.PossibleTag,
rarity, rarity,
InventoryTagSourceType.Drop, InventoryTagSourceType.Drop,

View File

@ -24,10 +24,14 @@ namespace GeometryTD.DataTable
public TagType TagType => (TagType)m_Id; public TagType TagType => (TagType)m_Id;
public string TagGroupText { get; private set; }
public RarityType MinRarity { get; private set; } public RarityType MinRarity { get; private set; }
public int Weight { get; private set; } public int Weight { get; private set; }
public bool IsImplemented { get; private set; }
public override bool ParseDataRow(string dataRowString, object userData) public override bool ParseDataRow(string dataRowString, object userData)
{ {
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators); string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
@ -41,8 +45,22 @@ namespace GeometryTD.DataTable
m_Id = int.Parse(columnStrings[index++]); m_Id = int.Parse(columnStrings[index++]);
index++; index++;
Name = columnStrings[index++]; Name = columnStrings[index++];
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
Weight = int.Parse(columnStrings[index++]); bool hasExtendedColumns = columnStrings.Length - index >= 4;
if (hasExtendedColumns)
{
TagGroupText = columnStrings[index++];
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
Weight = int.Parse(columnStrings[index++]);
IsImplemented = bool.Parse(columnStrings[index++]);
}
else
{
TagGroupText = string.Empty;
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
Weight = int.Parse(columnStrings[index++]);
IsImplemented = true;
}
return true; return true;
} }

View File

@ -5,6 +5,8 @@ namespace GeometryTD.Definition
None = 0, None = 0,
Status = 1, Status = 1,
NumericModifier = 2, NumericModifier = 2,
AttackShape = 3 AttackShape = 3,
// Enhances another applied status on the same hit, but does not create its own enemy-held state.
StatusModifier = 4
} }
} }

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0f2ab8a7e488575498ac933a125360d1 guid: 1c2505f2ce4cfd7499cdd3bc8c0e22eb
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eae63f0557814c14a9a2b9e6c8726404
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -7,13 +7,12 @@ namespace GeometryTD.Definition
{ {
public static class EnemyStatusTagRegistry public static class EnemyStatusTagRegistry
{ {
// Only tags that create or tick enemy-held status state belong here.
private static readonly Dictionary<TagType, IEnemyStatusTagEffect> EffectsByTag = private static readonly Dictionary<TagType, IEnemyStatusTagEffect> EffectsByTag =
new Dictionary<TagType, IEnemyStatusTagEffect> new Dictionary<TagType, IEnemyStatusTagEffect>
{ {
[TagType.Fire] = new FireTagEffect(), [TagType.Fire] = new FireTagEffect(),
[TagType.Inferno] = new InfernoTagEffect(), [TagType.Ice] = new IceTagEffect()
[TagType.Ice] = new IceTagEffect(),
[TagType.AbsoluteZero] = new AbsoluteZeroTagEffect()
}; };
public static IReadOnlyDictionary<TagType, IEnemyStatusTagEffect> Effects => EffectsByTag; public static IReadOnlyDictionary<TagType, IEnemyStatusTagEffect> Effects => EffectsByTag;
@ -23,4 +22,4 @@ namespace GeometryTD.Definition
return EffectsByTag.TryGetValue(tagType, out effect); return EffectsByTag.TryGetValue(tagType, out effect);
} }
} }
} }

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: df89bbfead1892c469024466c479bef1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -17,7 +17,7 @@ namespace GeometryTD.Definition
continue; continue;
} }
if (!TagConfigRegistry.TryGetDefinition(runtime.TagType, out TagDefinitionData definition) || if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null || definition == null ||
definition.Category != TagCategory.AttackShape || definition.Category != TagCategory.AttackShape ||
definition.TriggerPhase != triggerPhase || definition.TriggerPhase != triggerPhase ||

View File

@ -30,7 +30,7 @@ namespace GeometryTD.Definition
continue; continue;
} }
if (!TagConfigRegistry.TryGetDefinition(runtime.TagType, out TagDefinitionData definition) || if (!TagDefinitionRegistry.TryGetDefinition(runtime.TagType, out TagDefinition definition) ||
definition == null || definition == null ||
definition.Category != TagCategory.NumericModifier || definition.Category != TagCategory.NumericModifier ||
definition.TriggerPhase != TagTriggerPhase.OnBeforeHit || definition.TriggerPhase != TagTriggerPhase.OnBeforeHit ||

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: efece7a307e9a2c44b7247921e8b3194
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 343c21d630e34fc47a132c7d94f72bec
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -11,7 +11,7 @@ namespace GeometryTD.Definition
public override void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime, public override void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime,
TagRuntimeData runtimeData) TagRuntimeData runtimeData)
{ {
Debug.Assert(TagConfigRegistry.TryGetDefinition(TagType, out TagDefinitionData definition)); Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition));
FireTagConfig config = definition.Config as FireTagConfig; FireTagConfig config = definition.Config as FireTagConfig;
Debug.Assert(config != null); Debug.Assert(config != null);
@ -72,7 +72,7 @@ namespace GeometryTD.Definition
private static float GetInfernoBonusDuration(int infernoStack) private static float GetInfernoBonusDuration(int infernoStack)
{ {
if (infernoStack <= 0 || if (infernoStack <= 0 ||
!TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData definition)) !TagDefinitionRegistry.TryGetDefinition(TagType.Inferno, out TagDefinition definition))
{ {
return 0f; return 0f;
} }
@ -89,7 +89,7 @@ namespace GeometryTD.Definition
private static float GetInfernoBonusDamagePerSecond(int infernoStack) private static float GetInfernoBonusDamagePerSecond(int infernoStack)
{ {
if (infernoStack <= 0 || if (infernoStack <= 0 ||
!TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData definition)) !TagDefinitionRegistry.TryGetDefinition(TagType.Inferno, out TagDefinition definition))
{ {
return 0f; return 0f;
} }

View File

@ -14,7 +14,7 @@ namespace GeometryTD.Definition
_ = attackPayload; _ = attackPayload;
Debug.Assert(runtimeData != null); Debug.Assert(runtimeData != null);
Debug.Assert(runtimeData.TotalStack > 0); Debug.Assert(runtimeData.TotalStack > 0);
Debug.Assert(TagConfigRegistry.TryGetDefinition(TagType, out TagDefinitionData definition)); Debug.Assert(TagDefinitionRegistry.TryGetDefinition(TagType, out TagDefinition definition));
IceTagConfig config = definition.Config as IceTagConfig; IceTagConfig config = definition.Config as IceTagConfig;
Debug.Assert(config != null); Debug.Assert(config != null);
@ -53,7 +53,7 @@ namespace GeometryTD.Definition
private static float GetAbsoluteZeroBonusDuration(int absoluteZeroStack) private static float GetAbsoluteZeroBonusDuration(int absoluteZeroStack)
{ {
if (absoluteZeroStack <= 0 || if (absoluteZeroStack <= 0 ||
!TagConfigRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinitionData definition)) !TagDefinitionRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinition definition))
{ {
return 0f; return 0f;
} }
@ -70,7 +70,7 @@ namespace GeometryTD.Definition
private static float GetAbsoluteZeroBonusSlowRatio(int absoluteZeroStack) private static float GetAbsoluteZeroBonusSlowRatio(int absoluteZeroStack)
{ {
if (absoluteZeroStack <= 0 || if (absoluteZeroStack <= 0 ||
!TagConfigRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinitionData definition)) !TagDefinitionRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinition definition))
{ {
return 0f; return 0f;
} }

View File

@ -45,7 +45,7 @@ namespace GeometryTD.Definition
continue; continue;
} }
if (!TagConfigRegistry.TryGetDefinition(tag.TagType, out TagDefinitionData definition) || if (!TagDefinitionRegistry.TryGetDefinition(tag.TagType, out TagDefinition definition) ||
definition == null || definition == null ||
definition.Category != TagCategory.Status || definition.Category != TagCategory.Status ||
definition.TriggerPhase != TagTriggerPhase.OnAfterHit || definition.TriggerPhase != TagTriggerPhase.OnAfterHit ||

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 1909b51822cc4368bc604674fd119123
timeCreated: 1773105715

View File

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace GeometryTD.Definition
{
public sealed class AbsoluteZeroTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.AbsoluteZero;
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: deecaa9a38c04725b5abeb5488cbe53f
timeCreated: 1773106137

View File

@ -1,7 +0,0 @@
namespace GeometryTD.Definition
{
public sealed class InfernoTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.Inferno;
}
}

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 2aa813ec42ea4dddb717ff28da764f15
timeCreated: 1773106082

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 44a85dd6308e175428074070c55e1020
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -5,7 +5,7 @@ using Random = System.Random;
namespace GeometryTD.Definition namespace GeometryTD.Definition
{ {
public static class InventoryTagRuleService public static class ComponentTagGenerationService
{ {
public static TagType[] ResolveComponentTags( public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags, IReadOnlyList<TagType> possibleTags,
@ -13,10 +13,10 @@ namespace GeometryTD.Definition
InventoryTagSourceType sourceType, InventoryTagSourceType sourceType,
long itemInstanceId, long itemInstanceId,
int configId, int configId,
IReadOnlyDictionary<TagType, TagGenerationRuleData> rulesByTag = null, IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null,
IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> rarityTagBudgetRulesByRarity = null) IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{ {
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup); TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup);
if (eligibleTags.Length <= 0) if (eligibleTags.Length <= 0)
{ {
@ -46,7 +46,7 @@ namespace GeometryTD.Definition
public static TagType[] GetEligibleTags( public static TagType[] GetEligibleTags(
IReadOnlyList<TagType> possibleTags, IReadOnlyList<TagType> possibleTags,
RarityType rarity, RarityType rarity,
IReadOnlyDictionary<TagType, TagGenerationRuleData> rulesByTag = null) IReadOnlyDictionary<TagType, TagGenerationRule> rulesByTag = null)
{ {
if (possibleTags == null || possibleTags.Count <= 0) if (possibleTags == null || possibleTags.Count <= 0)
{ {
@ -54,7 +54,7 @@ namespace GeometryTD.Definition
} }
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules; IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
HashSet<TagType> uniqueTags = new HashSet<TagType>(); HashSet<TagType> uniqueTags = new HashSet<TagType>();
for (int i = 0; i < possibleTags.Count; i++) for (int i = 0; i < possibleTags.Count; i++)
@ -65,7 +65,7 @@ namespace GeometryTD.Definition
continue; continue;
} }
if (!TryGetRule(tagType, ruleLookup, out TagGenerationRuleData rule)) if (!TryGetRule(tagType, ruleLookup, out TagGenerationRule rule))
{ {
continue; continue;
} }
@ -92,11 +92,11 @@ namespace GeometryTD.Definition
public static int ResolveRarityTagBudget( public static int ResolveRarityTagBudget(
RarityType rarity, RarityType rarity,
Random random, Random random,
IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> rarityTagBudgetRulesByRarity = null) IReadOnlyDictionary<RarityType, RarityTagBudgetRule> rarityTagBudgetRulesByRarity = null)
{ {
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> ruleLookup = rarityTagBudgetRulesByRarity ?? RarityTagBudgetRuleRegistry.Rules; IReadOnlyDictionary<RarityType, RarityTagBudgetRule> ruleLookup = rarityTagBudgetRulesByRarity ?? RarityTagBudgetRuleRegistry.Rules;
RarityTagBudgetRuleData rule = GetRarityTagBudgetRule(normalizedRarity, ruleLookup); RarityTagBudgetRule rule = GetRarityTagBudgetRule(normalizedRarity, ruleLookup);
Debug.Assert(rule.MinCount >= 0); Debug.Assert(rule.MinCount >= 0);
Debug.Assert(rule.MaxCount >= rule.MinCount); Debug.Assert(rule.MaxCount >= rule.MinCount);
@ -112,15 +112,15 @@ namespace GeometryTD.Definition
private static bool IsSupportedLaunchTag(TagType tagType) private static bool IsSupportedLaunchTag(TagType tagType)
{ {
return TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition) && return TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) &&
definition.Config != null && definition.Config != null &&
definition.Config.IsImplemented; definition.Config.IsImplemented;
} }
private static bool TryGetRule( private static bool TryGetRule(
TagType tagType, TagType tagType,
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup, IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup,
out TagGenerationRuleData rule) out TagGenerationRule rule)
{ {
if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule)) if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule))
{ {
@ -131,9 +131,9 @@ namespace GeometryTD.Definition
return false; return false;
} }
private static RarityTagBudgetRuleData GetRarityTagBudgetRule( private static RarityTagBudgetRule GetRarityTagBudgetRule(
RarityType rarity, RarityType rarity,
IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> ruleLookup) IReadOnlyDictionary<RarityType, RarityTagBudgetRule> ruleLookup)
{ {
Debug.Assert(ruleLookup != null); Debug.Assert(ruleLookup != null);
Debug.Assert(ruleLookup.TryGetValue(rarity, out _)); Debug.Assert(ruleLookup.TryGetValue(rarity, out _));
@ -142,13 +142,13 @@ namespace GeometryTD.Definition
private static int RollWeightedIndex( private static int RollWeightedIndex(
IReadOnlyList<TagType> pool, IReadOnlyList<TagType> pool,
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup, IReadOnlyDictionary<TagType, TagGenerationRule> ruleLookup,
Random random) Random random)
{ {
int totalWeight = 0; int totalWeight = 0;
for (int i = 0; i < pool.Count; i++) for (int i = 0; i < pool.Count; i++)
{ {
TagGenerationRuleData rule = ruleLookup[pool[i]]; TagGenerationRule rule = ruleLookup[pool[i]];
Debug.Assert(rule.Weight > 0); Debug.Assert(rule.Weight > 0);
totalWeight += rule.Weight; totalWeight += rule.Weight;
} }
@ -159,7 +159,7 @@ namespace GeometryTD.Definition
int cumulative = 0; int cumulative = 0;
for (int i = 0; i < pool.Count; i++) for (int i = 0; i < pool.Count; i++)
{ {
TagGenerationRuleData rule = ruleLookup[pool[i]]; TagGenerationRule rule = ruleLookup[pool[i]];
cumulative += rule.Weight; cumulative += rule.Weight;
if (roll <= cumulative) if (roll <= cumulative)
{ {

View File

@ -1,6 +1,6 @@
namespace GeometryTD.Definition namespace GeometryTD.Definition
{ {
public sealed class RarityTagBudgetRuleData public sealed class RarityTagBudgetRule
{ {
public RarityType Rarity { get; set; } public RarityType Rarity { get; set; }
public int MinCount { get; set; } public int MinCount { get; set; }

View File

@ -6,14 +6,14 @@ namespace GeometryTD.Definition
{ {
public static class RarityTagBudgetRuleRegistry public static class RarityTagBudgetRuleRegistry
{ {
private static readonly Dictionary<RarityType, RarityTagBudgetRuleData> RulesByRarity = CreateDefaultRules(); private static readonly Dictionary<RarityType, RarityTagBudgetRule> RulesByRarity = CreateDefaultRules();
public static IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> Rules => RulesByRarity; public static IReadOnlyDictionary<RarityType, RarityTagBudgetRule> Rules => RulesByRarity;
public static void ResetToDefaults() public static void ResetToDefaults()
{ {
RulesByRarity.Clear(); RulesByRarity.Clear();
foreach (KeyValuePair<RarityType, RarityTagBudgetRuleData> pair in CreateDefaultRules()) foreach (KeyValuePair<RarityType, RarityTagBudgetRule> pair in CreateDefaultRules())
{ {
RulesByRarity.Add(pair.Key, pair.Value); RulesByRarity.Add(pair.Key, pair.Value);
} }
@ -28,14 +28,14 @@ namespace GeometryTD.Definition
} }
} }
public static bool TryGetRule(RarityType rarity, out RarityTagBudgetRuleData rule) public static bool TryGetRule(RarityType rarity, out RarityTagBudgetRule rule)
{ {
return RulesByRarity.TryGetValue(rarity, out rule); return RulesByRarity.TryGetValue(rarity, out rule);
} }
private static Dictionary<RarityType, RarityTagBudgetRuleData> CreateDefaultRules() private static Dictionary<RarityType, RarityTagBudgetRule> CreateDefaultRules()
{ {
return new Dictionary<RarityType, RarityTagBudgetRuleData> return new Dictionary<RarityType, RarityTagBudgetRule>
{ {
[RarityType.White] = CreateRule(RarityType.White, 0, 1), [RarityType.White] = CreateRule(RarityType.White, 0, 1),
[RarityType.Green] = CreateRule(RarityType.Green, 1, 1), [RarityType.Green] = CreateRule(RarityType.Green, 1, 1),
@ -45,13 +45,13 @@ namespace GeometryTD.Definition
}; };
} }
private static RarityTagBudgetRuleData CreateRule(RarityType rarity, int minCount, int maxCount) private static RarityTagBudgetRule CreateRule(RarityType rarity, int minCount, int maxCount)
{ {
Debug.Assert(rarity >= RarityType.White && rarity <= RarityType.Red); Debug.Assert(rarity >= RarityType.White && rarity <= RarityType.Red);
Debug.Assert(minCount >= 0); Debug.Assert(minCount >= 0);
Debug.Assert(maxCount >= minCount); Debug.Assert(maxCount >= minCount);
return new RarityTagBudgetRuleData return new RarityTagBudgetRule
{ {
Rarity = rarity, Rarity = rarity,
MinCount = minCount, MinCount = minCount,

View File

@ -1,6 +1,6 @@
namespace GeometryTD.Definition namespace GeometryTD.Definition
{ {
public sealed class TagGenerationRuleData public sealed class TagGenerationRule
{ {
public TagType TagType { get; set; } public TagType TagType { get; set; }
public RarityType MinRarity { get; set; } public RarityType MinRarity { get; set; }

View File

@ -6,14 +6,14 @@ namespace GeometryTD.Definition
{ {
public static class TagGenerationRuleRegistry public static class TagGenerationRuleRegistry
{ {
private static readonly Dictionary<TagType, TagGenerationRuleData> RulesByTag = CreateDefaultRules(); private static readonly Dictionary<TagType, TagGenerationRule> RulesByTag = CreateDefaultRules();
public static IReadOnlyDictionary<TagType, TagGenerationRuleData> Rules => RulesByTag; public static IReadOnlyDictionary<TagType, TagGenerationRule> Rules => RulesByTag;
public static void ResetToDefaults() public static void ResetToDefaults()
{ {
RulesByTag.Clear(); RulesByTag.Clear();
foreach (KeyValuePair<TagType, TagGenerationRuleData> pair in CreateDefaultRules()) foreach (KeyValuePair<TagType, TagGenerationRule> pair in CreateDefaultRules())
{ {
RulesByTag.Add(pair.Key, pair.Value); RulesByTag.Add(pair.Key, pair.Value);
} }
@ -28,14 +28,14 @@ namespace GeometryTD.Definition
} }
} }
public static bool TryGetRule(TagType tagType, out TagGenerationRuleData rule) public static bool TryGetRule(TagType tagType, out TagGenerationRule rule)
{ {
return RulesByTag.TryGetValue(tagType, out rule); return RulesByTag.TryGetValue(tagType, out rule);
} }
private static Dictionary<TagType, TagGenerationRuleData> CreateDefaultRules() private static Dictionary<TagType, TagGenerationRule> CreateDefaultRules()
{ {
return new Dictionary<TagType, TagGenerationRuleData> return new Dictionary<TagType, TagGenerationRule>
{ {
[TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20), [TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20),
[TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20), [TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20),
@ -52,9 +52,9 @@ namespace GeometryTD.Definition
}; };
} }
private static TagGenerationRuleData CreateRule(TagType tagType, RarityType minRarity, int weight) private static TagGenerationRule CreateRule(TagType tagType, RarityType minRarity, int weight)
{ {
return new TagGenerationRuleData return new TagGenerationRule
{ {
TagType = tagType, TagType = tagType,
MinRarity = minRarity, MinRarity = minRarity,

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e6251ee2cac123b43899d056ed4ad544
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c6219bd01b665fd4cae6580656c1f127
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -3,7 +3,7 @@ using System;
namespace GeometryTD.Definition namespace GeometryTD.Definition
{ {
[Serializable] [Serializable]
public sealed class TagDefinitionData public sealed class TagDefinition
{ {
public TagType TagType { get; set; } public TagType TagType { get; set; }
public TagCategory Category { get; set; } public TagCategory Category { get; set; }

View File

@ -5,16 +5,16 @@ using UnityEngine;
namespace GeometryTD.Definition namespace GeometryTD.Definition
{ {
public static class TagConfigRegistry public static class TagDefinitionRegistry
{ {
private static readonly Dictionary<TagType, TagDefinitionData> DefinitionsByTag = CreateDefaultDefinitions(); private static readonly Dictionary<TagType, TagDefinition> DefinitionsByTag = CreateDefaultDefinitions();
public static IReadOnlyDictionary<TagType, TagDefinitionData> Definitions => DefinitionsByTag; public static IReadOnlyDictionary<TagType, TagDefinition> Definitions => DefinitionsByTag;
public static void ResetToDefaults() public static void ResetToDefaults()
{ {
DefinitionsByTag.Clear(); DefinitionsByTag.Clear();
foreach (KeyValuePair<TagType, TagDefinitionData> pair in CreateDefaultDefinitions()) foreach (KeyValuePair<TagType, TagDefinition> pair in CreateDefaultDefinitions())
{ {
DefinitionsByTag.Add(pair.Key, pair.Value); DefinitionsByTag.Add(pair.Key, pair.Value);
} }
@ -29,23 +29,36 @@ namespace GeometryTD.Definition
} }
} }
public static bool TryGetDefinition(TagType tagType, out TagDefinitionData definition) public static bool TryGetDefinition(TagType tagType, out TagDefinition definition)
{ {
return DefinitionsByTag.TryGetValue(tagType, out definition); return DefinitionsByTag.TryGetValue(tagType, out definition);
} }
private static Dictionary<TagType, TagDefinitionData> CreateDefaultDefinitions() public static void ApplyTagRows(IEnumerable<DRTag> rows)
{ {
return new Dictionary<TagType, TagDefinitionData> if (rows == null)
{
return;
}
foreach (DRTag row in rows)
{
ApplyTagRow(row);
}
}
private static Dictionary<TagType, TagDefinition> CreateDefaultDefinitions()
{
return new Dictionary<TagType, TagDefinition>
{ {
[TagType.Fire] = CreateDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit, new FireTagConfig(true)), [TagType.Fire] = CreateDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit, new FireTagConfig(true)),
[TagType.BurnSpread] = CreateDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill, new BurnSpreadTagConfig(false)), [TagType.BurnSpread] = CreateDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill, new BurnSpreadTagConfig(false)),
[TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.OnKill, new IgniteBurstTagConfig(false)), [TagType.IgniteBurst] = CreateDefinition(TagType.IgniteBurst, TagCategory.AttackShape, TagTriggerPhase.OnKill, new IgniteBurstTagConfig(false)),
[TagType.Inferno] = CreateDefinition(TagType.Inferno, TagCategory.Status, TagTriggerPhase.OnAfterHit, new InfernoTagConfig(true)), [TagType.Inferno] = CreateDefinition(TagType.Inferno, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new InfernoTagConfig(true)),
[TagType.Ice] = CreateDefinition(TagType.Ice, TagCategory.Status, TagTriggerPhase.OnAfterHit, new IceTagConfig(true)), [TagType.Ice] = CreateDefinition(TagType.Ice, TagCategory.Status, TagTriggerPhase.OnAfterHit, new IceTagConfig(true)),
[TagType.FreezeMask] = CreateDefinition(TagType.FreezeMask, TagCategory.AttackShape, TagTriggerPhase.OnAfterHit, new FreezeMaskTagConfig(false)), [TagType.FreezeMask] = CreateDefinition(TagType.FreezeMask, TagCategory.AttackShape, TagTriggerPhase.OnAfterHit, new FreezeMaskTagConfig(false)),
[TagType.Shatter] = CreateDefinition(TagType.Shatter, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ShatterTagConfig(true)), [TagType.Shatter] = CreateDefinition(TagType.Shatter, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new ShatterTagConfig(true)),
[TagType.AbsoluteZero] = CreateDefinition(TagType.AbsoluteZero, TagCategory.Status, TagTriggerPhase.OnAfterHit, new AbsoluteZeroTagConfig(true)), [TagType.AbsoluteZero] = CreateDefinition(TagType.AbsoluteZero, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit, new AbsoluteZeroTagConfig(true)),
[TagType.Pierce] = CreateDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit, new PierceTagConfig(false)), [TagType.Pierce] = CreateDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit, new PierceTagConfig(false)),
[TagType.Crit] = CreateDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new CritTagConfig(true)), [TagType.Crit] = CreateDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit, new CritTagConfig(true)),
[TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.OnHit, new OverpenetrateTagConfig(false)), [TagType.Overpenetrate] = CreateDefinition(TagType.Overpenetrate, TagCategory.AttackShape, TagTriggerPhase.OnHit, new OverpenetrateTagConfig(false)),
@ -53,13 +66,13 @@ namespace GeometryTD.Definition
}; };
} }
private static TagDefinitionData CreateDefinition( private static TagDefinition CreateDefinition(
TagType tagType, TagType tagType,
TagCategory category, TagCategory category,
TagTriggerPhase triggerPhase, TagTriggerPhase triggerPhase,
TagConfigBase config) TagConfigBase config)
{ {
return new TagDefinitionData return new TagDefinition
{ {
TagType = tagType, TagType = tagType,
Category = category, Category = category,
@ -72,7 +85,7 @@ namespace GeometryTD.Definition
private static void ApplyRow(DRTagConfig row) private static void ApplyRow(DRTagConfig row)
{ {
Debug.Assert(row != null); Debug.Assert(row != null);
Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinitionData definition)); Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition));
definition.TriggerPhase = row.TriggerPhase; definition.TriggerPhase = row.TriggerPhase;
definition.Description = row.Description ?? string.Empty; definition.Description = row.Description ?? string.Empty;
@ -109,6 +122,16 @@ namespace GeometryTD.Definition
} }
} }
private static void ApplyTagRow(DRTag row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(DefinitionsByTag.TryGetValue(row.TagType, out TagDefinition definition));
Debug.Assert(definition.Config != null);
definition.Config.IsImplemented = row.IsImplemented;
}
private static void ApplyFireConfig(FireTagConfig config, JObject param) private static void ApplyFireConfig(FireTagConfig config, JObject param)
{ {
config.BurnDurationSeconds = ReadFloat(param, nameof(FireTagConfig.BurnDurationSeconds), config.BurnDurationSeconds); config.BurnDurationSeconds = ReadFloat(param, nameof(FireTagConfig.BurnDurationSeconds), config.BurnDurationSeconds);

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9f3ef8704ccb98147a21c03330278743
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -163,7 +163,7 @@ namespace GeometryTD.CustomUtility
return string.Empty; return string.Empty;
} }
if (TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition) && if (TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition) &&
!string.IsNullOrWhiteSpace(definition.Description)) !string.IsNullOrWhiteSpace(definition.Description))
{ {
return definition.Description; return definition.Description;

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: c90264b23a9c45c0b21bc627a6109282
timeCreated: 1773105821

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 6fb08c76711246c2afa02d60769d76ef
timeCreated: 1773106291

View File

@ -202,7 +202,7 @@ namespace GeometryTD.Procedure
var tagConfigTable = GameEntry.DataTable.GetDataTable<DRTagConfig>(); var tagConfigTable = GameEntry.DataTable.GetDataTable<DRTagConfig>();
if (tagConfigTable != null) if (tagConfigTable != null)
{ {
TagConfigRegistry.LoadFromRows(tagConfigTable.GetAllDataRows()); TagDefinitionRegistry.LoadFromRows(tagConfigTable.GetAllDataRows());
} }
} }
@ -211,7 +211,9 @@ namespace GeometryTD.Procedure
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>(); var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null) if (tagTable != null)
{ {
TagGenerationRuleRegistry.LoadFromRows(tagTable.GetAllDataRows()); DRTag[] rows = tagTable.GetAllDataRows();
TagGenerationRuleRegistry.LoadFromRows(rows);
TagDefinitionRegistry.ApplyTagRows(rows);
} }
} }

View File

@ -185,7 +185,7 @@ namespace GeometryTD.UI
Rarity = normalizedRarity, Rarity = normalizedRarity,
Endurance = 100f, Endurance = 100f,
Constraint = config.Constraint, Constraint = config.Constraint,
Tags = InventoryTagRuleService.ResolveComponentTags( Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag, config.PossibleTag,
normalizedRarity, normalizedRarity,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
@ -211,7 +211,7 @@ namespace GeometryTD.UI
Rarity = normalizedRarity, Rarity = normalizedRarity,
Endurance = 100f, Endurance = 100f,
Constraint = config.Constraint, Constraint = config.Constraint,
Tags = InventoryTagRuleService.ResolveComponentTags( Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag, config.PossibleTag,
normalizedRarity, normalizedRarity,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
@ -236,7 +236,7 @@ namespace GeometryTD.UI
Rarity = normalizedRarity, Rarity = normalizedRarity,
Endurance = 100f, Endurance = 100f,
Constraint = config.Constraint, Constraint = config.Constraint,
Tags = InventoryTagRuleService.ResolveComponentTags( Tags = ComponentTagGenerationService.ResolveComponentTags(
config.PossibleTag, config.PossibleTag,
normalizedRarity, normalizedRarity,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,

View File

@ -189,7 +189,7 @@ namespace GeometryTD.CustomUtility
private static TagType[] ResolveSeedTags(TagType[] possibleTags, RarityType rarity, long instanceId, int configId) private static TagType[] ResolveSeedTags(TagType[] possibleTags, RarityType rarity, long instanceId, int configId)
{ {
return InventoryTagRuleService.ResolveComponentTags( return ComponentTagGenerationService.ResolveComponentTags(
possibleTags, possibleTags,
rarity, rarity,
InventoryTagSourceType.Seed, InventoryTagSourceType.Seed,

View File

@ -6,34 +6,34 @@ using NUnit.Framework;
namespace GeometryTD.Tests.EditMode namespace GeometryTD.Tests.EditMode
{ {
public sealed class InventoryTagRuleServiceTests public sealed class ComponentTagGenerationServiceTests
{ {
private static readonly IReadOnlyDictionary<TagType, TagGenerationRuleData> RulesByTag = private static readonly IReadOnlyDictionary<TagType, TagGenerationRule> RulesByTag =
new Dictionary<TagType, TagGenerationRuleData> new Dictionary<TagType, TagGenerationRule>
{ {
{ TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 20 } }, { TagType.Fire, new TagGenerationRule { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 20 } }, { TagType.Ice, new TagGenerationRule { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 20 } }, { TagType.Crit, new TagGenerationRule { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.Green, Weight = 15 } }, { TagType.Shatter, new TagGenerationRule { TagType = TagType.Shatter, MinRarity = RarityType.Green, Weight = 15 } },
{ TagType.Inferno, new TagGenerationRuleData { TagType = TagType.Inferno, MinRarity = RarityType.Purple, Weight = 5 } }, { TagType.Inferno, new TagGenerationRule { TagType = TagType.Inferno, MinRarity = RarityType.Purple, Weight = 5 } },
{ TagType.AbsoluteZero, new TagGenerationRuleData { TagType = TagType.AbsoluteZero, MinRarity = RarityType.Purple, Weight = 5 } }, { TagType.AbsoluteZero, new TagGenerationRule { TagType = TagType.AbsoluteZero, MinRarity = RarityType.Purple, Weight = 5 } },
{ TagType.Execution, new TagGenerationRuleData { TagType = TagType.Execution, MinRarity = RarityType.Purple, Weight = 5 } }, { TagType.Execution, new TagGenerationRule { TagType = TagType.Execution, MinRarity = RarityType.Purple, Weight = 5 } },
}; };
private static readonly IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> RarityTagBudgetRulesByRarity = private static readonly IReadOnlyDictionary<RarityType, RarityTagBudgetRule> RarityTagBudgetRulesByRarity =
new Dictionary<RarityType, RarityTagBudgetRuleData> new Dictionary<RarityType, RarityTagBudgetRule>
{ {
{ RarityType.White, new RarityTagBudgetRuleData { Rarity = RarityType.White, MinCount = 0, MaxCount = 1 } }, { RarityType.White, new RarityTagBudgetRule { Rarity = RarityType.White, MinCount = 0, MaxCount = 1 } },
{ RarityType.Green, new RarityTagBudgetRuleData { Rarity = RarityType.Green, MinCount = 1, MaxCount = 1 } }, { RarityType.Green, new RarityTagBudgetRule { Rarity = RarityType.Green, MinCount = 1, MaxCount = 1 } },
{ RarityType.Blue, new RarityTagBudgetRuleData { Rarity = RarityType.Blue, MinCount = 1, MaxCount = 2 } }, { RarityType.Blue, new RarityTagBudgetRule { Rarity = RarityType.Blue, MinCount = 1, MaxCount = 2 } },
{ RarityType.Purple, new RarityTagBudgetRuleData { Rarity = RarityType.Purple, MinCount = 2, MaxCount = 2 } }, { RarityType.Purple, new RarityTagBudgetRule { Rarity = RarityType.Purple, MinCount = 2, MaxCount = 2 } },
{ RarityType.Red, new RarityTagBudgetRuleData { Rarity = RarityType.Red, MinCount = 2, MaxCount = 3 } }, { RarityType.Red, new RarityTagBudgetRule { Rarity = RarityType.Red, MinCount = 2, MaxCount = 3 } },
}; };
[Test] [Test]
public void GetEligibleTags_Filters_Invalid_Unsupported_And_HighRarity_Tags() public void GetEligibleTags_Filters_Invalid_Unsupported_And_HighRarity_Tags()
{ {
TagType[] result = InventoryTagRuleService.GetEligibleTags( TagType[] result = ComponentTagGenerationService.GetEligibleTags(
new[] new[]
{ {
TagType.None, TagType.None,
@ -52,7 +52,7 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveComponentTags_Is_Deterministic_For_Same_Context() public void ResolveComponentTags_Is_Deterministic_For_Same_Context()
{ {
TagType[] first = InventoryTagRuleService.ResolveComponentTags( TagType[] first = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter }, new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Blue, RarityType.Blue,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
@ -60,7 +60,7 @@ namespace GeometryTD.Tests.EditMode
7, 7,
RulesByTag); RulesByTag);
TagType[] second = InventoryTagRuleService.ResolveComponentTags( TagType[] second = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter }, new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Blue, RarityType.Blue,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
@ -74,7 +74,7 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveComponentTags_Uses_Purple_Budget_And_Does_Not_Repeat() public void ResolveComponentTags_Uses_Purple_Budget_And_Does_Not_Repeat()
{ {
TagType[] result = InventoryTagRuleService.ResolveComponentTags( TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution }, new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution },
RarityType.Purple, RarityType.Purple,
InventoryTagSourceType.Drop, InventoryTagSourceType.Drop,
@ -90,7 +90,7 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveComponentTags_Falls_Back_When_Eligible_Count_Is_Lower_Than_Budget() public void ResolveComponentTags_Falls_Back_When_Eligible_Count_Is_Lower_Than_Budget()
{ {
TagType[] result = InventoryTagRuleService.ResolveComponentTags( TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.BurnSpread }, new[] { TagType.Fire, TagType.BurnSpread },
RarityType.Red, RarityType.Red,
InventoryTagSourceType.Seed, InventoryTagSourceType.Seed,
@ -105,15 +105,15 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveComponentTags_Uses_Weight_Within_EligiblePool() public void ResolveComponentTags_Uses_Weight_Within_EligiblePool()
{ {
IReadOnlyDictionary<TagType, TagGenerationRuleData> weightedRules = IReadOnlyDictionary<TagType, TagGenerationRule> weightedRules =
new Dictionary<TagType, TagGenerationRuleData> new Dictionary<TagType, TagGenerationRule>
{ {
{ TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 100 } }, { TagType.Fire, new TagGenerationRule { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 100 } },
{ TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } }, { TagType.Ice, new TagGenerationRule { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } },
{ TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } }, { TagType.Crit, new TagGenerationRule { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } },
}; };
TagType[] result = InventoryTagRuleService.ResolveComponentTags( TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit }, new[] { TagType.Fire, TagType.Ice, TagType.Crit },
RarityType.Green, RarityType.Green,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
@ -129,19 +129,19 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveComponentTags_Prefers_HighWeight_Tag_In_ExpandedPool() public void ResolveComponentTags_Prefers_HighWeight_Tag_In_ExpandedPool()
{ {
IReadOnlyDictionary<TagType, TagGenerationRuleData> weightedRules = IReadOnlyDictionary<TagType, TagGenerationRule> weightedRules =
new Dictionary<TagType, TagGenerationRuleData> new Dictionary<TagType, TagGenerationRule>
{ {
{ TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 500 } }, { TagType.Fire, new TagGenerationRule { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 500 } },
{ TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } }, { TagType.Ice, new TagGenerationRule { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } },
{ TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } }, { TagType.Crit, new TagGenerationRule { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } },
{ TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.White, Weight = 1 } }, { TagType.Shatter, new TagGenerationRule { TagType = TagType.Shatter, MinRarity = RarityType.White, Weight = 1 } },
}; };
int fireCount = 0; int fireCount = 0;
for (int i = 0; i < 32; i++) for (int i = 0; i < 32; i++)
{ {
TagType[] result = InventoryTagRuleService.ResolveComponentTags( TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter }, new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Green, RarityType.Green,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
@ -168,7 +168,7 @@ namespace GeometryTD.Tests.EditMode
TagGenerationRuleRegistry.LoadFromRows(new[] { fireRow }); TagGenerationRuleRegistry.LoadFromRows(new[] { fireRow });
Assert.That(TagGenerationRuleRegistry.TryGetRule(TagType.Fire, out TagGenerationRuleData rule), Is.True); Assert.That(TagGenerationRuleRegistry.TryGetRule(TagType.Fire, out TagGenerationRule rule), Is.True);
Assert.That(rule.MinRarity, Is.EqualTo(RarityType.Green)); Assert.That(rule.MinRarity, Is.EqualTo(RarityType.Green));
Assert.That(rule.Weight, Is.EqualTo(33)); Assert.That(rule.Weight, Is.EqualTo(33));
@ -178,15 +178,15 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveRarityTagBudget_Uses_TableDriven_Range_Rules() public void ResolveRarityTagBudget_Uses_TableDriven_Range_Rules()
{ {
int whiteBudget = InventoryTagRuleService.ResolveRarityTagBudget( int whiteBudget = ComponentTagGenerationService.ResolveRarityTagBudget(
RarityType.White, RarityType.White,
new System.Random(0), new System.Random(0),
RarityTagBudgetRulesByRarity); RarityTagBudgetRulesByRarity);
int greenBudget = InventoryTagRuleService.ResolveRarityTagBudget( int greenBudget = ComponentTagGenerationService.ResolveRarityTagBudget(
RarityType.Green, RarityType.Green,
new System.Random(0), new System.Random(0),
RarityTagBudgetRulesByRarity); RarityTagBudgetRulesByRarity);
int redBudget = InventoryTagRuleService.ResolveRarityTagBudget( int redBudget = ComponentTagGenerationService.ResolveRarityTagBudget(
RarityType.Red, RarityType.Red,
new System.Random(0), new System.Random(0),
RarityTagBudgetRulesByRarity); RarityTagBudgetRulesByRarity);
@ -199,10 +199,10 @@ namespace GeometryTD.Tests.EditMode
[Test] [Test]
public void ResolveComponentTags_Uses_Custom_Purple_Budget_From_Rules() public void ResolveComponentTags_Uses_Custom_Purple_Budget_From_Rules()
{ {
IReadOnlyDictionary<RarityType, RarityTagBudgetRuleData> customBudgetRules = IReadOnlyDictionary<RarityType, RarityTagBudgetRule> customBudgetRules =
new Dictionary<RarityType, RarityTagBudgetRuleData>(RarityTagBudgetRulesByRarity) new Dictionary<RarityType, RarityTagBudgetRule>(RarityTagBudgetRulesByRarity)
{ {
[RarityType.Purple] = new RarityTagBudgetRuleData [RarityType.Purple] = new RarityTagBudgetRule
{ {
Rarity = RarityType.Purple, Rarity = RarityType.Purple,
MinCount = 3, MinCount = 3,
@ -210,7 +210,7 @@ namespace GeometryTD.Tests.EditMode
} }
}; };
TagType[] result = InventoryTagRuleService.ResolveComponentTags( TagType[] result = ComponentTagGenerationService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution }, new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter, TagType.Inferno, TagType.Execution },
RarityType.Purple, RarityType.Purple,
InventoryTagSourceType.Drop, InventoryTagSourceType.Drop,
@ -231,7 +231,7 @@ namespace GeometryTD.Tests.EditMode
RarityTagBudgetRuleRegistry.LoadFromRows(new[] { purpleRow }); RarityTagBudgetRuleRegistry.LoadFromRows(new[] { purpleRow });
Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Purple, out RarityTagBudgetRuleData rule), Is.True); Assert.That(RarityTagBudgetRuleRegistry.TryGetRule(RarityType.Purple, out RarityTagBudgetRule rule), Is.True);
Assert.That(rule.MinCount, Is.EqualTo(3)); Assert.That(rule.MinCount, Is.EqualTo(3));
Assert.That(rule.MaxCount, Is.EqualTo(3)); Assert.That(rule.MaxCount, Is.EqualTo(3));

View File

@ -278,7 +278,7 @@ namespace GeometryTD.Tests.EditMode
public sealed class EnemyStatusTagRegistryTests public sealed class EnemyStatusTagRegistryTests
{ {
[Test] [Test]
public void Registry_Registers_All_Status_Tags() public void Registry_Registers_Only_Independent_Status_Tags()
{ {
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Fire, out IEnemyStatusTagEffect fireEffect), Is.True); Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Fire, out IEnemyStatusTagEffect fireEffect), Is.True);
Assert.That(fireEffect, Is.TypeOf<FireTagEffect>()); Assert.That(fireEffect, Is.TypeOf<FireTagEffect>());
@ -286,50 +286,49 @@ namespace GeometryTD.Tests.EditMode
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Ice, out IEnemyStatusTagEffect iceEffect), Is.True); Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Ice, out IEnemyStatusTagEffect iceEffect), Is.True);
Assert.That(iceEffect, Is.TypeOf<IceTagEffect>()); Assert.That(iceEffect, Is.TypeOf<IceTagEffect>());
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Inferno, out IEnemyStatusTagEffect infernoEffect), Is.True); Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Inferno, out _), Is.False);
Assert.That(infernoEffect, Is.TypeOf<InfernoTagEffect>()); Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.AbsoluteZero, out _), Is.False);
Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.AbsoluteZero, out IEnemyStatusTagEffect absoluteZeroEffect), Is.True);
Assert.That(absoluteZeroEffect, Is.TypeOf<AbsoluteZeroTagEffect>());
} }
} }
public sealed class TagConfigRegistryTests public sealed class TagDefinitionRegistryTests
{ {
[Test] [Test]
public void Definitions_Register_All_Twelve_Tags() public void Definitions_Register_All_Twelve_Tags()
{ {
Assert.That(TagConfigRegistry.Definitions.Count, Is.EqualTo(12)); Assert.That(TagDefinitionRegistry.Definitions.Count, Is.EqualTo(12));
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Fire, out _), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Fire, out _), Is.True);
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Execution, out _), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Execution, out _), Is.True);
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Overpenetrate, out _), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Overpenetrate, out _), Is.True);
} }
[Test] [Test]
public void Definitions_Assign_Documented_Categories_And_Phases() public void Definitions_Assign_Documented_Categories_And_Phases()
{ {
AssertDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit); AssertDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit);
AssertDefinition(TagType.Inferno, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit);
AssertDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit); AssertDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit);
AssertDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit); AssertDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit);
AssertDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill); AssertDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill);
AssertDefinition(TagType.AbsoluteZero, TagCategory.StatusModifier, TagTriggerPhase.OnAfterHit);
} }
[Test] [Test]
public void Definitions_Mark_FirstBatch_SevenTags_AsImplemented() public void Definitions_Mark_FirstBatch_SevenTags_AsImplemented()
{ {
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Shatter, out TagDefinitionData shatter), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Shatter, out TagDefinition shatter), Is.True);
Assert.That(shatter.Config, Is.TypeOf<ShatterTagConfig>()); Assert.That(shatter.Config, Is.TypeOf<ShatterTagConfig>());
Assert.That(((ShatterTagConfig)shatter.Config).IsImplemented, Is.True); Assert.That(((ShatterTagConfig)shatter.Config).IsImplemented, Is.True);
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData inferno), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Inferno, out TagDefinition inferno), Is.True);
Assert.That(inferno.Config, Is.TypeOf<InfernoTagConfig>()); Assert.That(inferno.Config, Is.TypeOf<InfernoTagConfig>());
Assert.That(((InfernoTagConfig)inferno.Config).IsImplemented, Is.True); Assert.That(((InfernoTagConfig)inferno.Config).IsImplemented, Is.True);
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinitionData absoluteZero), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.AbsoluteZero, out TagDefinition absoluteZero), Is.True);
Assert.That(absoluteZero.Config, Is.TypeOf<AbsoluteZeroTagConfig>()); Assert.That(absoluteZero.Config, Is.TypeOf<AbsoluteZeroTagConfig>());
Assert.That(((AbsoluteZeroTagConfig)absoluteZero.Config).IsImplemented, Is.True); Assert.That(((AbsoluteZeroTagConfig)absoluteZero.Config).IsImplemented, Is.True);
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Pierce, out TagDefinitionData pierce), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Pierce, out TagDefinition pierce), Is.True);
Assert.That(pierce.Config, Is.TypeOf<PierceTagConfig>()); Assert.That(pierce.Config, Is.TypeOf<PierceTagConfig>());
Assert.That(((PierceTagConfig)pierce.Config).IsImplemented, Is.False); Assert.That(((PierceTagConfig)pierce.Config).IsImplemented, Is.False);
} }
@ -341,9 +340,9 @@ namespace GeometryTD.Tests.EditMode
bool parsed = fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":4,\"BurnDamagePerSecondPerStack\":25,\"MaxEffectiveStack\":3}", null); bool parsed = fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t火焰测试描述\t{\"BurnDurationSeconds\":4,\"BurnDamagePerSecondPerStack\":25,\"MaxEffectiveStack\":3}", null);
Assert.That(parsed, Is.True); Assert.That(parsed, Is.True);
TagConfigRegistry.LoadFromRows(new[] { fireRow }); TagDefinitionRegistry.LoadFromRows(new[] { fireRow });
Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Fire, out TagDefinitionData fire), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Fire, out TagDefinition fire), Is.True);
Assert.That(fire.TriggerPhase, Is.EqualTo(TagTriggerPhase.OnAfterHit)); Assert.That(fire.TriggerPhase, Is.EqualTo(TagTriggerPhase.OnAfterHit));
Assert.That(fire.Description, Is.EqualTo("火焰测试描述")); Assert.That(fire.Description, Is.EqualTo("火焰测试描述"));
Assert.That(fire.Config, Is.TypeOf<FireTagConfig>()); Assert.That(fire.Config, Is.TypeOf<FireTagConfig>());
@ -353,7 +352,23 @@ namespace GeometryTD.Tests.EditMode
Assert.That(config.BurnDamagePerSecondPerStack, Is.EqualTo(25f).Within(0.001f)); Assert.That(config.BurnDamagePerSecondPerStack, Is.EqualTo(25f).Within(0.001f));
Assert.That(config.MaxEffectiveStack, Is.EqualTo(3)); Assert.That(config.MaxEffectiveStack, Is.EqualTo(3));
TagConfigRegistry.ResetToDefaults(); TagDefinitionRegistry.ResetToDefaults();
}
[Test]
public void ApplyTagRows_Overrides_IsImplemented_From_TagTable()
{
DRTag pierceRow = new DRTag();
Assert.That(pierceRow.ParseDataRow("\t9\t穿透\tPierce\tPenetrate\tWhite\t20\tTrue", null), Is.True);
TagDefinitionRegistry.ResetToDefaults();
TagDefinitionRegistry.ApplyTagRows(new[] { pierceRow });
Assert.That(TagDefinitionRegistry.TryGetDefinition(TagType.Pierce, out TagDefinition pierce), Is.True);
Assert.That(pierce.Config, Is.TypeOf<PierceTagConfig>());
Assert.That(((PierceTagConfig)pierce.Config).IsImplemented, Is.True);
TagDefinitionRegistry.ResetToDefaults();
} }
[Test] [Test]
@ -364,14 +379,14 @@ namespace GeometryTD.Tests.EditMode
Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t持续燃烧\t{\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":5}", null), Is.True); Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tOnAfterHit\t持续燃烧\t{\"BurnDurationSeconds\":3,\"BurnDamagePerSecondPerStack\":20,\"MaxEffectiveStack\":5}", null), Is.True);
Assert.That(shatterRow.ParseDataRow("\t7\t控制\tShatter\tOnBeforeHit\t对减速目标增伤\t{\"RequiresSlowedTarget\":true,\"DamageBonusPerStack\":0.25}", null), Is.True); Assert.That(shatterRow.ParseDataRow("\t7\t控制\tShatter\tOnBeforeHit\t对减速目标增伤\t{\"RequiresSlowedTarget\":true,\"DamageBonusPerStack\":0.25}", null), Is.True);
TagConfigRegistry.LoadFromRows(new[] { fireRow, shatterRow }); TagDefinitionRegistry.LoadFromRows(new[] { fireRow, shatterRow });
string description = TagDisplayUtility.BuildTagDescriptionText(new[] { TagType.Fire, TagType.Shatter }); string description = TagDisplayUtility.BuildTagDescriptionText(new[] { TagType.Fire, TagType.Shatter });
Assert.That(description, Does.Contain("持续燃烧")); Assert.That(description, Does.Contain("持续燃烧"));
Assert.That(description, Does.Contain("对减速目标增伤")); Assert.That(description, Does.Contain("对减速目标增伤"));
TagConfigRegistry.ResetToDefaults(); TagDefinitionRegistry.ResetToDefaults();
} }
[Test] [Test]
@ -380,7 +395,7 @@ namespace GeometryTD.Tests.EditMode
DRTagConfig iceRow = new DRTagConfig(); DRTagConfig iceRow = new DRTagConfig();
Assert.That(iceRow.ParseDataRow("\t5\t控制\tIce\tOnAfterHit\t命中附加减速\t{\"SlowDurationSeconds\":2,\"SlowRatioPerStack\":0.2,\"MinMoveSpeedMultiplier\":0.4}", null), Is.True); Assert.That(iceRow.ParseDataRow("\t5\t控制\tIce\tOnAfterHit\t命中附加减速\t{\"SlowDurationSeconds\":2,\"SlowRatioPerStack\":0.2,\"MinMoveSpeedMultiplier\":0.4}", null), Is.True);
TagConfigRegistry.LoadFromRows(new[] { iceRow }); TagDefinitionRegistry.LoadFromRows(new[] { iceRow });
string description = TagDisplayUtility.BuildTagDescriptionText(new[] string description = TagDisplayUtility.BuildTagDescriptionText(new[]
{ {
@ -390,7 +405,7 @@ namespace GeometryTD.Tests.EditMode
Assert.That(description, Does.Contain("Ice x2")); Assert.That(description, Does.Contain("Ice x2"));
Assert.That(description, Does.Contain("命中附加减速")); Assert.That(description, Does.Contain("命中附加减速"));
TagConfigRegistry.ResetToDefaults(); TagDefinitionRegistry.ResetToDefaults();
} }
[Test] [Test]
@ -418,7 +433,7 @@ namespace GeometryTD.Tests.EditMode
private static void AssertDefinition(TagType tagType, TagCategory expectedCategory, TagTriggerPhase expectedPhase) private static void AssertDefinition(TagType tagType, TagCategory expectedCategory, TagTriggerPhase expectedPhase)
{ {
Assert.That(TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition), Is.True); Assert.That(TagDefinitionRegistry.TryGetDefinition(tagType, out TagDefinition definition), Is.True);
Assert.That(definition, Is.Not.Null); Assert.That(definition, Is.Not.Null);
Assert.That(definition.Category, Is.EqualTo(expectedCategory)); Assert.That(definition.Category, Is.EqualTo(expectedCategory));
Assert.That(definition.TriggerPhase, Is.EqualTo(expectedPhase)); Assert.That(definition.TriggerPhase, Is.EqualTo(expectedPhase));

View File

@ -137,7 +137,7 @@
> >
> 2026-03-09 更新:`S4-03` 文档口径已冻结;`TagSystemDesign.md` 已明确组件实例生成、塔级 `Stack` 汇总、四段触发阶段、`Tag.txt + TagRule` 双表方向,以及 MVP 正式首发 7 个 Tag`Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`。`BurnSpread` 已明确后移。 > 2026-03-09 更新:`S4-03` 文档口径已冻结;`TagSystemDesign.md` 已明确组件实例生成、塔级 `Stack` 汇总、四段触发阶段、`Tag.txt + TagRule` 双表方向,以及 MVP 正式首发 7 个 Tag`Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`。`BurnSpread` 已明确后移。
> >
> 2026-03-09 更新:`S4-04` 已落地 `InventoryTagRuleService` 与 `InventoryTagSourceType`;组件实例 Tag 现在统一按 `PossibleTag + Tag.txt.MinRarity + 品质预算` 生成,并只保留当前正式首发 7 个 Tag。`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 已接入该入口,样例库存的组件与塔展示 Tag 已同步为统一结果;同时新增 `InventoryTagRuleServiceTests`。当前 CLI 下 `dotnet build GeometryTD.sln` 仍因本机缺少 Unity 引用和 `Unity.SourceGenerators*.dll` 失败,未能替代 Unity Test Runner 完成最终验证。 > 2026-03-09 更新:`S4-04` 已落地 `ComponentTagGenerationService` 与 `InventoryTagSourceType`;组件实例 Tag 现在统一按 `PossibleTag + Tag.txt.MinRarity + 品质预算` 生成,并只保留当前正式首发 7 个 Tag。`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 已接入该入口,样例库存的组件与塔展示 Tag 已同步为统一结果;同时新增 `ComponentTagGenerationServiceTests`。当前 CLI 下 `dotnet build GeometryTD.sln` 仍因本机缺少 Unity 引用和 `Unity.SourceGenerators*.dll` 失败,未能替代 Unity Test Runner 完成最终验证。
> >
> 2026-03-09 更新:`S4-05` 已新增 `TagRuntimeData``TowerTagAggregationService`;组塔与样例塔现在统一生成塔级 `TagRuntimes`,并保留兼容 `Tags` 投影。`RepoForm`、`CombatFinishForm`、`ItemDescForm` 的塔展示已切到聚合结果,重复 Tag 以 `xN` 文本显示;组件展示仍沿用组件实例 `Tags`。同时新增 `TowerTagAggregationServiceTests`;本轮改动后的最终验证仍以 Unity Test Runner 实跑结果为准。 > 2026-03-09 更新:`S4-05` 已新增 `TagRuntimeData``TowerTagAggregationService`;组塔与样例塔现在统一生成塔级 `TagRuntimes`,并保留兼容 `Tags` 投影。`RepoForm`、`CombatFinishForm`、`ItemDescForm` 的塔展示已切到聚合结果,重复 Tag 以 `xN` 文本显示;组件展示仍沿用组件实例 `Tags`。同时新增 `TowerTagAggregationServiceTests`;本轮改动后的最终验证仍以 Unity Test Runner 实跑结果为准。
> >
@ -145,11 +145,11 @@
> >
> 2026-03-10 更新:`S4-06` 已补齐首发剩余 3 个 Tag。`Shatter` 已接入命中前数值修正链,并按“目标已减速”口径增伤;`Inferno` 与 `AbsoluteZero` 已按首发方案作为 `Fire` / `Ice` 的强化 Tag 落地,分别增强 DOT 时长/伤害与减速时长/强度;对应 EditMode 测试已同步补齐。`BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 仍保持分类与占位路由,没有实际战斗效果,因此仍属于后续扩展,不计入 `S4-06` 当前完成标准。 > 2026-03-10 更新:`S4-06` 已补齐首发剩余 3 个 Tag。`Shatter` 已接入命中前数值修正链,并按“目标已减速”口径增伤;`Inferno` 与 `AbsoluteZero` 已按首发方案作为 `Fire` / `Ice` 的强化 Tag 落地,分别增强 DOT 时长/伤害与减速时长/强度;对应 EditMode 测试已同步补齐。`BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 仍保持分类与占位路由,没有实际战斗效果,因此仍属于后续扩展,不计入 `S4-06` 当前完成标准。
> >
> 2026-03-10 更新:`S4-07` 仍未开始正式收口。当前 Tag 参数仍主要承载在代码侧 `TagConfigRegistry` 与各 Tag 配置类中,`Tag.txt` / DataTable 仍只提供基础字典与 `MinRarity` 输入;文档中约定的 `TagRule` 表、触发阶段、权重、效果参数等尚未形成 DataRow 与运行时消费闭环。因此 `S4-07` 继续保持未完成状态。 > 2026-03-10 更新:`S4-07` 仍未开始正式收口。当前 Tag 参数仍主要承载在代码侧 `TagDefinitionRegistry` 与各 Tag 配置类中,`Tag.txt` / DataTable 仍只提供基础字典与 `MinRarity` 输入;文档中约定的 `TagRule` 表、触发阶段、权重、效果参数等尚未形成 DataRow 与运行时消费闭环。因此 `S4-07` 继续保持未完成状态。
> >
> 2026-03-11 更新:`S4-07` 已进入第一阶段实现。当前已新增 `TagConfig.txt``DRTagConfig``ProcedurePreload` 会在加载 `TagConfig` 表后驱动 `TagConfigRegistry.LoadFromRows(...)`,把 `TriggerPhase`、`Description` 以及首发 7 个 Tag 的配置参数从表覆盖到运行时强类型 `TagConfig`。`ItemDescForm` 也已开始消费该配置说明;塔详情会优先使用 `TagRuntimes` 构建 `Ice x2` 这类叠层展示。因此 `S4-07` 不再是“完全未开始”,但当前仍只完成了 `TagConfig` 级别的参数映射与 UI 消费,尚未把文档中的完整 `TagRule`(如权重、生成规则等)全部收口。 > 2026-03-11 更新:`S4-07` 已进入第一阶段实现。当前已新增 `TagConfig.txt``DRTagConfig``ProcedurePreload` 会在加载 `TagConfig` 表后驱动 `TagDefinitionRegistry.LoadFromRows(...)`,把 `TriggerPhase`、`Description` 以及首发 7 个 Tag 的配置参数从表覆盖到运行时强类型 `TagConfig`。`ItemDescForm` 也已开始消费该配置说明;塔详情会优先使用 `TagRuntimes` 构建 `Ice x2` 这类叠层展示。因此 `S4-07` 不再是“完全未开始”,但当前仍只完成了 `TagConfig` 级别的参数映射与 UI 消费,尚未把文档中的完整 `TagRule`(如权重、生成规则等)全部收口。
> >
> 2026-03-11 更新:`S4-07` 已继续推进到第二阶段。当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环,`InventoryTagRuleService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight``ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。 > 2026-03-11 更新:`S4-07` 已继续推进到第二阶段。当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环,`ComponentTagGenerationService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight``ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。
> >
> 2026-03-11 补充验证:已通过手工扩充组件 `PossibleTag` 候选池并调整 `Weight` 验证生成权重实际生效;同时把所有 Tag 的 `MinRarity` 提升到 `Red` 后,低品质组件不再生成任何 Tag说明 `Tag.txt` 中的 `MinRarity``Weight` 均已进入统一生成链。 > 2026-03-11 补充验证:已通过手工扩充组件 `PossibleTag` 候选池并调整 `Weight` 验证生成权重实际生效;同时把所有 Tag 的 `MinRarity` 提升到 `Red` 后,低品质组件不再生成任何 Tag说明 `Tag.txt` 中的 `MinRarity``Weight` 均已进入统一生成链。
@ -186,19 +186,19 @@
- `S4-06` 已完成:战斗链已支持 Tag 透传与统一结算,正式首发 7 个 Tag `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero` 均已有可验证效果。 - `S4-06` 已完成:战斗链已支持 Tag 透传与统一结算,正式首发 7 个 Tag `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero` 均已有可验证效果。
- `Shatter` 已接入命中前数值修正链;`Inferno`、`AbsoluteZero` 已按“强化现有 `Fire` / `Ice` 状态”的首发口径落地,状态类 Tag 的内部结构继续保持注册式运行时。 - `Shatter` 已接入命中前数值修正链;`Inferno`、`AbsoluteZero` 已按“强化现有 `Fire` / `Ice` 状态”的首发口径落地,状态类 Tag 的内部结构继续保持注册式运行时。
- `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。 - `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。
- `S4-07` 已进入第一阶段实现。当前仓库已具备 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 的消费闭环,首发 7 个 Tag 的触发阶段、描述与核心参数已可由表覆盖。 - `S4-07` 已进入第一阶段实现。当前仓库已具备 `TagConfig.txt -> DRTagConfig -> TagDefinitionRegistry -> ItemDescForm` 的消费闭环,首发 7 个 Tag 的触发阶段、描述与核心参数已可由表覆盖。
- `S4-07` 已推进到第二阶段。当前仓库同时具备 `Tag.txt -> DRTag -> TagGenerationRuleRegistry -> InventoryTagRuleService` 与 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 两条消费闭环,首发 7 个 Tag 的生成规则、触发阶段、描述与核心参数都已开始由表驱动。 - `S4-07` 已推进到第二阶段。当前仓库同时具备 `Tag.txt -> DRTag -> TagGenerationRuleRegistry -> ComponentTagGenerationService` 与 `TagConfig.txt -> DRTagConfig -> TagDefinitionRegistry -> ItemDescForm` 两条消费闭环,首发 7 个 Tag 的生成规则、触发阶段、描述与核心参数都已开始由表驱动。
- `S4-07` 已推进到第三阶段。当前仓库新增 `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry -> InventoryTagRuleService`,组件 Tag 数量预算不再写死在 `ResolveRarityTagBudget(...)``switch` 中,而是按品质从表驱动;至此生成链的候选过滤、权重抽取、数量预算三段都已进入 DataTable 消费闭环。 - `S4-07` 已推进到第三阶段。当前仓库新增 `RarityTagBudget.txt -> DRRarityTagBudget -> RarityTagBudgetRuleRegistry -> ComponentTagGenerationService`,组件 Tag 数量预算不再写死在 `ResolveRarityTagBudget(...)``switch` 中,而是按品质从表驱动;至此生成链的候选过滤、权重抽取、数量预算三段都已进入 DataTable 消费闭环。
- `S4-07` 仍未完成最终收口。当前采用的是 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 的分层方案,而不是文档原方案里的单独 `TagRule`;更深的元数据字段与完整命名口径仍待后续决定是否继续统一。 - `S4-07` 仍未完成最终收口。当前采用的是 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 的分层方案,而不是文档原方案里的单独 `TagRule`;更深的元数据字段与完整命名口径仍待后续决定是否继续统一。
### S4-06 当前代码状态 ### S4-06 当前代码状态
- 命中链路已统一为 `AttackPayload -> HitContext -> TagEffectResolver`,子弹命中时不再只传裸 `damage` - 命中链路已统一为 `AttackPayload -> HitContext -> TagEffectResolver`,子弹命中时不再只传裸 `damage`
- `Tower` 侧的 `TagRuntimes` 已能透传到战斗,且保留了旧 `Tags -> TagRuntimes` 的兼容入口,避免旧塔或旧展示数据在战斗中完全失效。 - `Tower` 侧的 `TagRuntimes` 已能透传到战斗,且保留了旧 `Tags -> TagRuntimes` 的兼容入口,避免旧塔或旧展示数据在战斗中完全失效。
- 状态类 Tag 现在按单 Tag 文件拆分: - 状态类 Tag 现在按职责目录拆分:
- 配置:每个 Tag 一个 `TagConfig` - `Metadata/Config`:每个 Tag 一个配置类
- 运行时状态:每个状态类 Tag 一个 `TagState` - `Combat/States`:每个状态类 Tag 一个运行时状态类
- 效果:每个状态类 Tag 一个 `TagEffect` - `Combat/StatusEffects`:每个状态类 Tag 一个效果类
- `EnemyTagStatusRuntime` 已不再硬编码 `burn/slow` 字段,而是按激活的状态类 Tag 动态调度 Tick。 - `EnemyTagStatusRuntime` 已不再硬编码 `burn/slow` 字段,而是按激活的状态类 Tag 动态调度 Tick。
- `Fire` 已改为独立 DOT 公式:当前使用独立 `BurnDamagePerSecondPerStack`,并按敌人实体每帧提供的 `deltaTime` 连续结算,不再依赖命中伤害或内部 `TickInterval` - `Fire` 已改为独立 DOT 公式:当前使用独立 `BurnDamagePerSecondPerStack`,并按敌人实体每帧提供的 `deltaTime` 连续结算,不再依赖命中伤害或内部 `TickInterval`
- `Ice` 仍是独立减速状态,移速倍率通过状态运行时聚合得到。 - `Ice` 仍是独立减速状态,移速倍率通过状态运行时聚合得到。
@ -220,6 +220,47 @@
| [ ] | S5-03 | 实现耐久扣减后的实际生效 | `Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`<br>`Assets/GameMain/Scripts/Entity/` | 耐久不再只是展示字段 | | [ ] | S5-03 | 实现耐久扣减后的实际生效 | `Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`<br>`Assets/GameMain/Scripts/Entity/` | 耐久不再只是展示字段 |
| [ ] | S5-04 | 实现 `0` 耐久销毁或失效闭环 | `Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`<br>`Assets/GameMain/Scripts/UI/` | 归零后行为符合最终口径 | | [ ] | S5-04 | 实现 `0` 耐久销毁或失效闭环 | `Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`<br>`Assets/GameMain/Scripts/UI/` | 归零后行为符合最终口径 |
### S5 规划结论
- `S5` 当前按“最小耐久闭环”推进,不展开维修、折价、复杂恢复、耐久与 Tag 联动等后续深度系统。
- M1 保留耐久规则,但当前目标只收口到“战斗后真实扣减 + 归零后真实失效 + UI 可解释”,不要求一并实现完整维修玩法。
- 本阶段优先保证耐久从展示字段变成实际规则入口,而不是继续增加额外经济系统或复杂数值衰减。
### S5-01 范围结论
- `S5-01` 建议明确保留耐久闭环,但只做组件实例级耐久,不新开修理站、维修货币、商店修复等额外系统。
- 当前阶段只处理战斗节点的耐久消耗;事件、商店、其他特殊节点暂不引入额外耐久变更。
- `0` 耐久当前建议先按“失效但不立即销毁实例”收口,避免在本阶段同时引入背包移除、组装解绑、自动清空参战区等高耦合逻辑。
### S5-02 推荐规则口径
- 耐久数据仍挂在组件实例上,塔本身不额外维护独立耐久池。
- 一座塔只要已装配的枪口 / 轴承 / 底座中任意一个组件耐久 `<= 0`,就视为损坏塔。
- 损坏塔不能进入参战区,也不能通过战斗入口最终校验。
- 当前阶段不实现“耐久越低属性越差”的连续衰减;在 `> 0` 耐久时,组件与塔仍按当前属性正常工作。
- 当前阶段不把耐久纳入品质、Tag、掉落或商店价格公式避免把 `S5` 扩成新的综合规则层。
### S5-03 推荐实现口径
- 每次战斗节点结算后,对本场实际参战塔所装配的三个组件各扣 `1` 点耐久。
- 扣减结果必须真实回写到库存快照 / 当前 Run 库存,而不是只改战斗内临时对象。
- 参战合法性统一复用现有校验入口扩展,不额外再造一套“耐久专用判断链”。
- 战斗入口仍需要保留最终二次校验,避免旧快照或外部改动绕过组装区 / 参战区限制。
### S5-04 推荐闭环口径
- 组件耐久归零后,当前版本按“失效不可参战”收口,不立即自动销毁实例。
- 装配了 `0` 耐久组件的塔在 UI 上需要给出明确提示,例如“组件已损坏”或“耐久为 0无法参战”。
- 若塔已在参战区且组件在上一场战斗后归零,需要在后续刷新中把该塔视为非法参战塔,并通过统一校验入口阻止再次进入战斗。
- “自动销毁组件”与“自动拆塔”保留到后续阶段再决定,当前不作为 `S5` 通过标准。
### S5 推荐执行顺序
1. 先完成 `S5-01`,把 M1 的耐久范围冻结为“最小闭环”,文档先对齐。
2. 再完成 `S5-02`,把“归零失效、非归零不衰减、不可参战”的规则写成统一口径。
3. 然后做 `S5-03`,把战斗结算后的耐久扣减与库存回写接进主链路。
4. 最后做 `S5-04`补齐参战拦截、UI 提示与 `0` 耐久失效闭环。
## 阶段 S6 - 回归与文档收尾 ## 阶段 S6 - 回归与文档收尾
| 状态 | ID | 任务 | 交付物路径 | 验收标准 | | 状态 | ID | 任务 | 交付物路径 | 验收标准 |
@ -235,15 +276,16 @@
2. `S2` 已完成口径对齐,`NodeMapForm` 不再作为 M1 阻塞项。 2. `S2` 已完成口径对齐,`NodeMapForm` 不再作为 M1 阻塞项。
3. 接下来优先做 `S3`,补齐出战合法性,解决 `P0-10` 3. 接下来优先做 `S3`,补齐出战合法性,解决 `P0-10`
4. 再做 `S4`,统一品质 / Tag 的规则口径,解决 `P0-11`。其中当前优先级已经收口为:`S4-06` 已完成,下一步转入 `S4-07` 的数据表映射。 4. 再做 `S4`,统一品质 / Tag 的规则口径,解决 `P0-11`。其中当前优先级已经收口为:`S4-06` 已完成,下一步转入 `S4-07` 的数据表映射。
5. 最后做 `S5`决定耐久是完整收口还是同步缩范围,解决 `P0-12` 5. 最后做 `S5`按“最小耐久闭环”收口真实扣减、归零失效与参战拦截,解决 `P0-12`
6. 全部完成后做 `S6`,补测试并同步文档状态。 6. 全部完成后做 `S6`,补测试并同步文档状态。
## 本周建议开工顺序 ## 本周建议开工顺序
1. `S1``S2` 已完成口径收口,可直接进入规则侧收尾 1. `S1``S2` 已完成口径收口,可直接进入规则侧收尾
2. `S3-01 ~ S3-04` 已完成,当前可转入 `S4` 2. `S3-01 ~ S3-04` 已完成,当前可转入 `S4`
3. 当前转入 `S4-07`,把 `TagRule` / DataTable 映射真正接进运行时 3. 当前先完成 `S4-07` 的三表口径收口,并把 `Tag.txt` / `TagConfig.txt` / `RarityTagBudget.txt` 的实际消费链稳定下来
4. 然后补 `S6-01 ~ S6-04` 4. 然后转入 `S5-01 ~ S5-04`,按“最小耐久闭环”推进耐久真实生效
5. 最后补 `S6-01 ~ S6-04`
## 备注 ## 备注

View File

@ -9,13 +9,13 @@
2. 每项任务必须同时满足“交付物路径”和“验收标准”才可打勾。 2. 每项任务必须同时满足“交付物路径”和“验收标准”才可打勾。
3. 数据驱动优先:数值、掉落、商店、事件都优先落到 `DataTables` 3. 数据驱动优先:数值、掉落、商店、事件都优先落到 `DataTables`
## M1 当前口径2026-03-09 ## M1 当前口径2026-03-11
- 当前仓库已经具备 `ProcedureMain + NodeMapForm + CombatNode + EventNode + ShopNode` 的主流程 Run 闭环,可以从主菜单进入游戏,完成固定 10 节点流程,并在 Boss 结算后进入正式结束态并回到主菜单。 - 当前仓库已经具备 `ProcedureMain + NodeMapForm + CombatNode + EventNode + ShopNode` 的主流程 Run 闭环,可以从主菜单进入游戏,完成固定 10 节点流程,并在 Boss 结算后进入正式结束态并回到主菜单。
- `NodeMapForm` 已满足 MVP 所需的节点流程界面M1 现在的真实缺口是合法出战 / 品质 / Tag / 耐久规则是否真正统一收口。 - `NodeMapForm` 已满足 MVP 所需的节点流程界面M1 现在的真实缺口是合法出战 / 品质 / Tag / 耐久规则是否真正统一收口。
- `P0-10 ~ P0-12` 在代码里都已有局部实现,因此文档里统一按“部分完成但未收口”处理;只有满足最终验收标准后才可改成完成 - `P0-10` 的“三组件完整合法参战”主链已落地:当前参战分配、战斗入口最终校验、失败原因与拦截提示都已接入统一合法性判断入口,但文档仍保留 `[~]`,直到与 `docs/CodeX-TODO.md` 的验收口径完全同步
- `P0-11` 在 M1 中按“规则最小闭环”收口:只要求品质计算与 Tag 来源形成单一、可复现、跨流程共用的规则入口配件槽位、Tag 数量 / 等级随机、Tag 战斗效果暂不作为当前 M1 阻塞项 - `P0-11` 已不再只是“局部展示字段”当前品质计算、Tag 生成、塔级 Tag 汇总、首发 7 个 Tag 的战斗效果、以及 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 三表驱动链路都已存在;剩余缺口主要是 `S4-07` 的最终文档口径与少量配置收口,而不是主功能缺失
- `P0-11` 的 Tag 规则拆分已记录到 `docs/TagSystemDesign.md`:先收口实例生成、塔级汇总与首发 Tag 集合,再决定哪些效果进入战斗链路 - `P0-12` 仍是当前 M1 最明确的规则缺口:已有耐久字段、展示与部分扣减入口,但还没有形成“战斗后真实扣减 -> 归零失效 -> 参战拦截”的最小闭环
## 里程碑 M1P0- 最小可玩闭环 ## 里程碑 M1P0- 最小可玩闭环
@ -30,9 +30,9 @@
| [x] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 | | [x] | P0-07 | 战斗节点基础玩法:放置塔、出怪、基地扣血、胜负判定 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/Scene/` | 可完整打一场并得到胜利/失败结果 |
| [x] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 | | [x] | P0-08 | 胜利波次与结算规则100/80/50/<50 | `Assets/GameMain/Scripts/Procedure/` | 结算奖励与惩罚严格匹配设计文档 |
| [x] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 | | [x] | P0-09 | 敌人掉落与关卡奖励(组件/配件/金币) | `Assets/GameMain/Scripts/Entity/` | 战斗结束能发放掉落并写入库存 |
| [~] | P0-10 | 节点后组装:枪口/轴承/底座三组件约束 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/UI/Templates/GameScene/` | 当前只校验“参与区至少有 1 座塔”,尚未收口为“三组件完整合法参战” | | [~] | P0-10 | 节点后组装:枪口/轴承/底座三组件约束 | `Assets/GameMain/Scripts/Entity/`<br>`Assets/GameMain/Scripts/UI/Templates/GameScene/` | 当前已形成“三组件完整合法参战”的统一校验链与战斗入口拦截;剩余工作主要是同步文档口径与收尾验收 |
| [~] | P0-11 | 品质 / Tag 规则统一入口(白绿蓝紫红) | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/Entity/` | 品质计算与 Tag 来源在组装、掉落、商店、展示链路形成单一、可复现、跨流程共用的规则入口;配件槽位与 Tag 深度规则暂不作为当前 M1 阻塞项 | | [~] | P0-11 | 品质 / Tag 规则统一入口(白绿蓝紫红) | `Assets/GameMain/Scripts/Definition/`<br>`Assets/GameMain/Scripts/Entity/` | 当前已完成品质统一、Tag 生成/汇总/展示与首发 7 个 Tag 的战斗生效;剩余工作主要是三表方案的最终收口与文档同步 |
| [~] | P0-12 | 组件/配件耐久生效与 0 耐久销毁 | `Assets/GameMain/Scripts/Entity/` | 已有耐久字段、展示与扣减入口,但尚未影响属性 / 出战资格,也未形成 `0` 耐久移除或失效闭环 | | [~] | P0-12 | 组件/配件耐久生效与 0 耐久销毁 | `Assets/GameMain/Scripts/Entity/` | 当前按“最小耐久闭环”推进:目标是形成战斗后真实扣减、`0` 耐久失效与参战拦截;维修系统、自动销毁等后续深度规则暂不作为本阶段阻塞项 |
## 里程碑 M2P1- 核心深度 ## 里程碑 M2P1- 核心深度
@ -63,9 +63,9 @@
## 本周建议开工顺序 ## 本周建议开工顺序
1. 先完成 `P0-10`(把“至少有参战塔”提升为“满足完整合法参战条件才能出战”) 1. 先完成 `P0-11` 的三表口径收口,把 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 的实际消费链与文档彻底对齐
2. 再处理 `P0-11` ~ `P0-12`(先统一 M1 范围;`P0-11` 先收口品质 / Tag 的最小规则闭环,再决定是否扩展到完整深度设计) 2. 再推进 `P0-12`,按“最小耐久闭环”实现战斗后真实扣减、`0` 耐久失效与参战拦截
3. 最后补关键流程 / 规则回归测试,并同步文档状态 3. 最后补关键流程 / 规则回归测试,并同步 `docs/TODO.md``docs/CodeX-TODO.md` 的真实状态
## 设计优化 Backlog新增 ## 设计优化 Backlog新增

View File

@ -448,7 +448,7 @@ Tag 触发阶段固定拆成四段:
1. 先基于本设计回写 `docs/CodeX-TODO.md``S4-03` 边界 1. 先基于本设计回写 `docs/CodeX-TODO.md``S4-03` 边界
2. 在 `S4-07` 中补齐并消费 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 三层表结构 2. 在 `S4-07` 中补齐并消费 `Tag.txt + RarityTagBudget.txt + TagConfig.txt` 三层表结构
3. 新增 `TagGenerationService`,先收口实例生成 3. 新增 `ComponentTagGenerationService`,先收口实例生成
4. 新增 `TowerTagAggregationService`,替换当前简单并集逻辑 4. 新增 `TowerTagAggregationService`,替换当前简单并集逻辑
5. 评估 `AttackPayload` 是否并入现有 `BulletData`,还是单独引入 5. 评估 `AttackPayload` 是否并入现有 `BulletData`,还是单独引入
6. 第一批只落固定 7 个基础 Tag避免超出 `MVP-Scope` 6. 第一批只落固定 7 个基础 Tag避免超出 `MVP-Scope`

Some files were not shown because too many files have changed in this diff Show More