S4-07 process 2

当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环:
- `InventoryTagRuleService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight`
- `ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。
- 现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。
This commit is contained in:
SepComet 2026-03-11 11:20:16 +08:00
parent 9de2e50262
commit b1b68ebde5
46 changed files with 615 additions and 452 deletions

View File

@ -5,17 +5,17 @@
1002 平原1.2 55 TimeElapsed 60
1003 平原1.3 55 TimeElapsed 60
1004 平原1.4 55 EnemiesCleared
1005 平原1.* 0
1005 平原1.* 0 None
2001 平原2.1 55 TimeElapsed 60
2002 平原2.2 55 TimeElapsed 60
2003 平原2.3 55 TimeElapsed 60
2004 平原2.4 55 EnemiesCleared
2005 平原2.* 0
2005 平原2.* 0 None
3001 平原3.1 55 TimeElapsed 60
3002 平原3.2 55 TimeElapsed 60
3003 平原3.3 55 TimeElapsed 60
3004 平原3.4 55 EnemiesCleared
3005 平原3.* 0
3005 平原3.* 0 None
4001 平原4.1 55 TimeElapsed 60
4002 平原4.2 55 TimeElapsed 60
4003 平原4.3 55 TimeElapsed 60

View File

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

View File

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

View File

@ -17,20 +17,16 @@ namespace GeometryTD.CustomComponent
private readonly List<DROutGameDropPool> _eligibleDropPoolBuffer = new();
private readonly Dictionary<RarityType, float> _rarityRollWeightBuffer = new();
private readonly Dictionary<TagType, RarityType> _tagMinRarityByTag = new();
private IDataTable<DROutGameDropPool> _drOutGameDropPool;
private IDataTable<DRMuzzleComp> _drMuzzleComp;
private IDataTable<DRBearingComp> _drBearingComp;
private IDataTable<DRBaseComp> _drBaseComp;
private IDataTable<DRTag> _drTag;
private long _nextDropItemInstanceId = 1;
public void Reset()
{
_eligibleDropPoolBuffer.Clear();
_rarityRollWeightBuffer.Clear();
_tagMinRarityByTag.Clear();
_nextDropItemInstanceId = 1;
}
@ -307,35 +303,6 @@ namespace GeometryTD.CustomComponent
return _drOutGameDropPool;
}
private bool EnsureTagMinRarityLookup()
{
if (_tagMinRarityByTag.Count > 0)
{
return true;
}
_drTag ??= GameEntry.DataTable.GetDataTable<DRTag>();
if (_drTag == null)
{
return false;
}
DRTag[] rows = _drTag.GetAllDataRows();
for (int i = 0; i < rows.Length; i++)
{
DRTag row = rows[i];
if (row == null || row.Id <= 0)
{
continue;
}
TagType tagType = (TagType)row.Id;
_tagMinRarityByTag[tagType] = row.MinRarity;
}
return _tagMinRarityByTag.Count > 0;
}
private bool TryBuildDropItem(DROutGameDropPool row, out TowerCompItemData droppedItem)
{
droppedItem = null;
@ -372,11 +339,6 @@ namespace GeometryTD.CustomComponent
return false;
}
if (!EnsureTagMinRarityLookup())
{
return false;
}
DRMuzzleComp config = _drMuzzleComp.GetDataRow(row.ItemId);
if (config == null)
{
@ -398,8 +360,7 @@ namespace GeometryTD.CustomComponent
rarity,
InventoryTagSourceType.Drop,
instanceId,
config.Id,
_tagMinRarityByTag),
config.Id),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
@ -416,11 +377,6 @@ namespace GeometryTD.CustomComponent
return false;
}
if (!EnsureTagMinRarityLookup())
{
return false;
}
DRBearingComp config = _drBearingComp.GetDataRow(row.ItemId);
if (config == null)
{
@ -442,8 +398,7 @@ namespace GeometryTD.CustomComponent
rarity,
InventoryTagSourceType.Drop,
instanceId,
config.Id,
_tagMinRarityByTag),
config.Id),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
@ -459,11 +414,6 @@ namespace GeometryTD.CustomComponent
return false;
}
if (!EnsureTagMinRarityLookup())
{
return false;
}
DRBaseComp config = _drBaseComp.GetDataRow(row.ItemId);
if (config == null)
{
@ -485,8 +435,7 @@ namespace GeometryTD.CustomComponent
rarity,
InventoryTagSourceType.Drop,
instanceId,
config.Id,
_tagMinRarityByTag),
config.Id),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};

View File

@ -6,7 +6,7 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable
{
/// <summary>
/// 枪口组件
/// Tag
/// </summary>
public class DRTag : DataRowBase
{
@ -22,8 +22,12 @@ namespace GeometryTD.DataTable
/// </summary>
public string Name { get; private set; }
public TagType TagType => (TagType)m_Id;
public RarityType MinRarity { get; private set; }
public int Weight { get; private set; }
public override bool ParseDataRow(string dataRowString, object userData)
{
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
@ -38,8 +42,9 @@ namespace GeometryTD.DataTable
index++;
Name = columnStrings[index++];
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
Weight = int.Parse(columnStrings[index++]);
return true;
}
}
}
}

View File

@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainCombatEntryBlockReason
{
None = 0,
InventoryUnavailable = 1,
NoValidParticipantTower = 2
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f6d8af9f1d7d4041bfcee904724303e9
timeCreated: 1773198097

View File

@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainFlowPhase
{
Hub = 0,
NodeActive = 1,
RunCompletedPendingFinish = 2
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01450486bd824f47bcd74dda8a160ba4
timeCreated: 1773198057

View File

@ -0,0 +1,10 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainRunAdvanceResult
{
NoChange = 0,
NodeException = 1,
AdvancedToNextNode = 2,
RunCompleted = 3
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e9e0fa56d7e84874ac0f9d4a400fda13
timeCreated: 1773198071

View File

@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainRunCompletionResult
{
NoChange = 0,
ShowCompletionDialog = 1,
ReturnToMenu = 2
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e3d0df05f77043a99c463a790b5ac35f
timeCreated: 1773198084

View File

@ -1,33 +1,22 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = System.Random;
namespace GeometryTD.Definition
{
public static class InventoryTagRuleService
{
private static readonly IReadOnlyDictionary<TagType, RarityType> DefaultMinRarityByTag =
new Dictionary<TagType, RarityType>
{
{ TagType.Fire, RarityType.White },
{ TagType.Ice, RarityType.White },
{ TagType.Crit, RarityType.White },
{ TagType.Shatter, RarityType.Green },
{ TagType.Inferno, RarityType.Purple },
{ TagType.AbsoluteZero, RarityType.Purple },
{ TagType.Execution, RarityType.Purple },
};
private static readonly HashSet<TagType> SupportedLaunchTags = new HashSet<TagType>(DefaultMinRarityByTag.Keys);
public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
InventoryTagSourceType sourceType,
long itemInstanceId,
int configId,
IReadOnlyDictionary<TagType, RarityType> minRarityByTag = null)
IReadOnlyDictionary<TagType, TagGenerationRuleData> rulesByTag = null)
{
TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, minRarityByTag);
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
TagType[] eligibleTags = GetEligibleTags(possibleTags, rarity, ruleLookup);
if (eligibleTags.Length <= 0)
{
return Array.Empty<TagType>();
@ -45,7 +34,7 @@ namespace GeometryTD.Definition
TagType[] result = new TagType[finalCount];
for (int i = 0; i < finalCount; i++)
{
int index = random.Next(0, pool.Count);
int index = RollWeightedIndex(pool, ruleLookup, random);
result[i] = pool[index];
pool.RemoveAt(index);
}
@ -56,7 +45,7 @@ namespace GeometryTD.Definition
public static TagType[] GetEligibleTags(
IReadOnlyList<TagType> possibleTags,
RarityType rarity,
IReadOnlyDictionary<TagType, RarityType> minRarityByTag = null)
IReadOnlyDictionary<TagType, TagGenerationRuleData> rulesByTag = null)
{
if (possibleTags == null || possibleTags.Count <= 0)
{
@ -64,23 +53,23 @@ namespace GeometryTD.Definition
}
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<TagType, RarityType> rarityLookup = minRarityByTag ?? DefaultMinRarityByTag;
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup = rulesByTag ?? TagGenerationRuleRegistry.Rules;
HashSet<TagType> uniqueTags = new HashSet<TagType>();
for (int i = 0; i < possibleTags.Count; i++)
{
TagType tagType = possibleTags[i];
if (!IsSupportedLaunchTag(tagType) || tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType))
if (tagType == TagType.None || !Enum.IsDefined(typeof(TagType), tagType) || !IsSupportedLaunchTag(tagType))
{
continue;
}
if (!TryGetMinRarity(tagType, rarityLookup, out RarityType minRarity))
if (!TryGetRule(tagType, ruleLookup, out TagGenerationRuleData rule))
{
continue;
}
if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(minRarity))
if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(rule.MinRarity))
{
continue;
}
@ -123,28 +112,55 @@ namespace GeometryTD.Definition
private static bool IsSupportedLaunchTag(TagType tagType)
{
return SupportedLaunchTags.Contains(tagType);
return TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition) &&
definition.Config != null &&
definition.Config.IsImplemented;
}
private static bool TryGetMinRarity(
private static bool TryGetRule(
TagType tagType,
IReadOnlyDictionary<TagType, RarityType> rarityLookup,
out RarityType minRarity)
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup,
out TagGenerationRuleData rule)
{
if (rarityLookup != null && rarityLookup.TryGetValue(tagType, out minRarity))
if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule))
{
return true;
}
if (DefaultMinRarityByTag.TryGetValue(tagType, out minRarity))
{
return true;
}
minRarity = RarityType.White;
rule = null;
return false;
}
private static int RollWeightedIndex(
IReadOnlyList<TagType> pool,
IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup,
Random random)
{
int totalWeight = 0;
for (int i = 0; i < pool.Count; i++)
{
TagGenerationRuleData rule = ruleLookup[pool[i]];
Debug.Assert(rule.Weight > 0);
totalWeight += rule.Weight;
}
Debug.Assert(totalWeight > 0);
int roll = random.Next(1, totalWeight + 1);
int cumulative = 0;
for (int i = 0; i < pool.Count; i++)
{
TagGenerationRuleData rule = ruleLookup[pool[i]];
cumulative += rule.Weight;
if (roll <= cumulative)
{
return i;
}
}
return pool.Count - 1;
}
private static int BuildStableSeed(
RarityType rarity,
InventoryTagSourceType sourceType,

View File

@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public sealed class TagGenerationRuleData
{
public TagType TagType { get; set; }
public RarityType MinRarity { get; set; }
public int Weight { get; set; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfe508e818ce47649a112ecdf86cf26a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
using GeometryTD.DataTable;
using UnityEngine;
namespace GeometryTD.Definition
{
public static class TagGenerationRuleRegistry
{
private static readonly Dictionary<TagType, TagGenerationRuleData> RulesByTag = CreateDefaultRules();
public static IReadOnlyDictionary<TagType, TagGenerationRuleData> Rules => RulesByTag;
public static void ResetToDefaults()
{
RulesByTag.Clear();
foreach (KeyValuePair<TagType, TagGenerationRuleData> pair in CreateDefaultRules())
{
RulesByTag.Add(pair.Key, pair.Value);
}
}
public static void LoadFromRows(IEnumerable<DRTag> rows)
{
ResetToDefaults();
foreach (DRTag row in rows)
{
ApplyRow(row);
}
}
public static bool TryGetRule(TagType tagType, out TagGenerationRuleData rule)
{
return RulesByTag.TryGetValue(tagType, out rule);
}
private static Dictionary<TagType, TagGenerationRuleData> CreateDefaultRules()
{
return new Dictionary<TagType, TagGenerationRuleData>
{
[TagType.Fire] = CreateRule(TagType.Fire, RarityType.White, 20),
[TagType.BurnSpread] = CreateRule(TagType.BurnSpread, RarityType.White, 20),
[TagType.IgniteBurst] = CreateRule(TagType.IgniteBurst, RarityType.Green, 15),
[TagType.Inferno] = CreateRule(TagType.Inferno, RarityType.Purple, 5),
[TagType.Ice] = CreateRule(TagType.Ice, RarityType.White, 20),
[TagType.FreezeMask] = CreateRule(TagType.FreezeMask, RarityType.White, 20),
[TagType.Shatter] = CreateRule(TagType.Shatter, RarityType.Green, 15),
[TagType.AbsoluteZero] = CreateRule(TagType.AbsoluteZero, RarityType.Purple, 5),
[TagType.Pierce] = CreateRule(TagType.Pierce, RarityType.White, 20),
[TagType.Crit] = CreateRule(TagType.Crit, RarityType.White, 20),
[TagType.Overpenetrate] = CreateRule(TagType.Overpenetrate, RarityType.Green, 15),
[TagType.Execution] = CreateRule(TagType.Execution, RarityType.Purple, 5)
};
}
private static TagGenerationRuleData CreateRule(TagType tagType, RarityType minRarity, int weight)
{
return new TagGenerationRuleData
{
TagType = tagType,
MinRarity = minRarity,
Weight = weight
};
}
private static void ApplyRow(DRTag row)
{
Debug.Assert(row != null);
Debug.Assert(row.Id > 0);
Debug.Assert(row.Weight > 0);
RulesByTag[row.TagType] = CreateRule(row.TagType, row.MinRarity, row.Weight);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e72e26721274985ba2d44ca825d7f75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -204,6 +204,15 @@ namespace GeometryTD.Procedure
TagConfigRegistry.LoadFromRows(tagConfigTable.GetAllDataRows());
}
}
if (ne.DataTableAssetName == AssetUtility.GetDataTableAsset("Tag", false))
{
var tagTable = GameEntry.DataTable.GetDataTable<DRTag>();
if (tagTable != null)
{
TagGenerationRuleRegistry.LoadFromRows(tagTable.GetAllDataRows());
}
}
}
private void OnLoadDataTableFailure(object sender, GameEventArgs e)

View File

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

View File

@ -1,4 +1,3 @@
using System.Text;
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
@ -9,243 +8,6 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure
{
public enum ProcedureMainFlowPhase
{
Hub = 0,
NodeActive = 1,
RunCompletedPendingFinish = 2
}
public enum ProcedureMainRunAdvanceResult
{
NoChange = 0,
NodeException = 1,
AdvancedToNextNode = 2,
RunCompleted = 3
}
public enum ProcedureMainRunCompletionResult
{
NoChange = 0,
ShowCompletionDialog = 1,
ReturnToMenu = 2
}
public enum ProcedureMainCombatEntryBlockReason
{
None = 0,
InventoryUnavailable = 1,
NoValidParticipantTower = 2
}
public sealed class ProcedureMainCombatEntryValidationResult
{
public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None;
public ProcedureMainCombatEntryBlockReason BlockReason { get; set; }
public CombatParticipantTowerValidationSummary ValidationSummary { get; set; }
}
public static class ProcedureMainRunFlowService
{
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
RunState runState,
RunNodeCompletionStatus completionStatus,
BackpackInventoryData snapshot)
{
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot))
{
return ProcedureMainRunAdvanceResult.NoChange;
}
if (runState != null && runState.IsCompleted)
{
return ProcedureMainRunAdvanceResult.RunCompleted;
}
return completionStatus == RunNodeCompletionStatus.Exception
? ProcedureMainRunAdvanceResult.NodeException
: ProcedureMainRunAdvanceResult.AdvancedToNextNode;
}
}
public static class ProcedureMainRunCompletionService
{
public static ProcedureMainRunCompletionResult TryEnterCompletedPendingFinish(bool isCompletionDialogShown)
{
return isCompletionDialogShown
? ProcedureMainRunCompletionResult.NoChange
: ProcedureMainRunCompletionResult.ShowCompletionDialog;
}
public static ProcedureMainRunCompletionResult TryConfirmReturnToMenu(
ProcedureMainFlowPhase flowPhase,
bool isReturnToMenuPending)
{
if (flowPhase != ProcedureMainFlowPhase.RunCompletedPendingFinish || isReturnToMenuPending)
{
return ProcedureMainRunCompletionResult.NoChange;
}
return ProcedureMainRunCompletionResult.ReturnToMenu;
}
}
public static class ProcedureMainNodeEventGuardService
{
public static bool MatchesCurrentNode(
RunState runState,
int nodeId,
RunNodeType nodeType,
int sequenceIndex)
{
RunNodeState currentNode = runState?.CurrentNode;
if (currentNode == null)
{
return false;
}
return (nodeId <= 0 || nodeId == currentNode.NodeId) &&
(nodeType == RunNodeType.None || nodeType == currentNode.NodeType) &&
(sequenceIndex < 0 || sequenceIndex == currentNode.SequenceIndex);
}
}
public static class ProcedureMainCombatEntryValidationService
{
public static ProcedureMainCombatEntryValidationResult Validate(BackpackInventoryData inventory)
{
if (inventory == null)
{
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable,
ValidationSummary = new CombatParticipantTowerValidationSummary()
};
}
CombatParticipantTowerValidationSummary summary =
CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory);
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = summary.HasAnyValidParticipantTower
? ProcedureMainCombatEntryBlockReason.None
: ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ValidationSummary = summary
};
}
public static string BuildInvalidParticipantTowerLog(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "none";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
if (builder.Length > 0)
{
builder.Append(", ");
}
builder.Append('#');
builder.Append(result.TowerInstanceId);
builder.Append(':');
builder.Append(result.FailureReason);
}
return builder.Length > 0 ? builder.ToString() : "none";
}
public static DialogFormRawData BuildBlockedCombatDialogRawData(
ProcedureMainCombatEntryValidationResult validationResult)
{
return new DialogFormRawData
{
Mode = 1,
Title = "无法进入战斗",
Message = BuildBlockedCombatDialogMessage(validationResult),
PauseGame = false,
ConfirmText = "知道了"
};
}
private static string BuildBlockedCombatDialogMessage(
ProcedureMainCombatEntryValidationResult validationResult)
{
if (validationResult == null)
{
return "当前无法确认出战信息,请稍后重试。";
}
switch (validationResult.BlockReason)
{
case ProcedureMainCombatEntryBlockReason.InventoryUnavailable:
return "当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。";
case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower:
return BuildNoValidParticipantTowerMessage(validationResult.ValidationSummary);
default:
return "当前出战校验未通过,暂时不能进入战斗。";
}
}
private static string BuildNoValidParticipantTowerMessage(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。";
}
StringBuilder builder = new StringBuilder();
builder.Append("参战区没有可出战的完整塔。");
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
builder.Append('\n');
builder.Append("塔 #");
builder.Append(result.TowerInstanceId);
builder.Append(' ');
builder.Append(GetFailureReasonMessage(result.FailureReason));
}
return builder.ToString();
}
private static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason)
{
switch (failureReason)
{
case CombatParticipantTowerValidationFailureReason.TowerMissing:
return "已不存在,无法参战。";
case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent:
return "缺少枪口组件。";
case CombatParticipantTowerValidationFailureReason.MissingBearingComponent:
return "缺少轴承组件。";
case CombatParticipantTowerValidationFailureReason.MissingBaseComponent:
return "缺少底座组件。";
default:
return "不满足当前参战条件。";
}
}
}
public class ProcedureMain : ProcedureBase
{
public override bool UseNativeDialog => false;
@ -467,7 +229,9 @@ namespace GeometryTD.Procedure
case RunNodeType.BossCombat:
ProcedureMainCombatEntryValidationResult validationResult =
ProcedureMainCombatEntryValidationService.Validate(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null);
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
if (!validationResult.CanEnterCombat)
{
LogCombatEntryBlocked(currentNode, validationResult);
@ -659,4 +423,4 @@ namespace GeometryTD.Procedure
_isReturnToMenuPending = true;
}
}
}
}

View File

@ -0,0 +1,13 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public sealed class ProcedureMainCombatEntryValidationResult
{
public bool CanEnterCombat => BlockReason == ProcedureMainCombatEntryBlockReason.None;
public ProcedureMainCombatEntryBlockReason BlockReason { get; set; }
public CombatParticipantTowerValidationSummary ValidationSummary { get; set; }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5699aa9933294974875bfce1d34d1d4b
timeCreated: 1773198121

View File

@ -0,0 +1,140 @@
using System.Text;
using GeometryTD.Definition;
using GeometryTD.UI;
namespace GeometryTD.Procedure
{
public static class ProcedureMainCombatEntryValidationService
{
public static ProcedureMainCombatEntryValidationResult Validate(BackpackInventoryData inventory)
{
if (inventory == null)
{
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = ProcedureMainCombatEntryBlockReason.InventoryUnavailable,
ValidationSummary = new CombatParticipantTowerValidationSummary()
};
}
CombatParticipantTowerValidationSummary summary =
CombatParticipantTowerValidationService.ValidateParticipantTowers(inventory);
return new ProcedureMainCombatEntryValidationResult
{
BlockReason = summary.HasAnyValidParticipantTower
? ProcedureMainCombatEntryBlockReason.None
: ProcedureMainCombatEntryBlockReason.NoValidParticipantTower,
ValidationSummary = summary
};
}
public static string BuildInvalidParticipantTowerLog(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "none";
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
if (builder.Length > 0)
{
builder.Append(", ");
}
builder.Append('#');
builder.Append(result.TowerInstanceId);
builder.Append(':');
builder.Append(result.FailureReason);
}
return builder.Length > 0 ? builder.ToString() : "none";
}
public static DialogFormRawData BuildBlockedCombatDialogRawData(
ProcedureMainCombatEntryValidationResult validationResult)
{
return new DialogFormRawData
{
Mode = 1,
Title = "无法进入战斗",
Message = BuildBlockedCombatDialogMessage(validationResult),
PauseGame = false,
ConfirmText = "知道了"
};
}
private static string BuildBlockedCombatDialogMessage(
ProcedureMainCombatEntryValidationResult validationResult)
{
if (validationResult == null)
{
return "当前无法确认出战信息,请稍后重试。";
}
switch (validationResult.BlockReason)
{
case ProcedureMainCombatEntryBlockReason.InventoryUnavailable:
return "当前无法读取库存快照,暂时不能进入战斗。请重新进入本轮流程后再试。";
case ProcedureMainCombatEntryBlockReason.NoValidParticipantTower:
return BuildNoValidParticipantTowerMessage(validationResult.ValidationSummary);
default:
return "当前出战校验未通过,暂时不能进入战斗。";
}
}
private static string BuildNoValidParticipantTowerMessage(
CombatParticipantTowerValidationSummary summary)
{
if (summary?.InvalidResults == null || summary.InvalidResults.Count <= 0)
{
return "参战区至少需要 1 座完整装配了枪口、轴承、底座的塔,才能进入战斗。";
}
StringBuilder builder = new StringBuilder();
builder.Append("参战区没有可出战的完整塔。");
for (int i = 0; i < summary.InvalidResults.Count; i++)
{
CombatParticipantTowerValidationResult result = summary.InvalidResults[i];
if (result == null)
{
continue;
}
builder.Append('\n');
builder.Append("塔 #");
builder.Append(result.TowerInstanceId);
builder.Append(' ');
builder.Append(GetFailureReasonMessage(result.FailureReason));
}
return builder.ToString();
}
private static string GetFailureReasonMessage(CombatParticipantTowerValidationFailureReason failureReason)
{
switch (failureReason)
{
case CombatParticipantTowerValidationFailureReason.TowerMissing:
return "已不存在,无法参战。";
case CombatParticipantTowerValidationFailureReason.MissingMuzzleComponent:
return "缺少枪口组件。";
case CombatParticipantTowerValidationFailureReason.MissingBearingComponent:
return "缺少轴承组件。";
case CombatParticipantTowerValidationFailureReason.MissingBaseComponent:
return "缺少底座组件。";
default:
return "不满足当前参战条件。";
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be3e90d1a1874e26af6d1555c301aa26
timeCreated: 1773198334

View File

@ -0,0 +1,22 @@
namespace GeometryTD.Procedure
{
public static class ProcedureMainNodeEventGuardService
{
public static bool MatchesCurrentNode(
RunState runState,
int nodeId,
RunNodeType nodeType,
int sequenceIndex)
{
RunNodeState currentNode = runState?.CurrentNode;
if (currentNode == null)
{
return false;
}
return (nodeId <= 0 || nodeId == currentNode.NodeId) &&
(nodeType == RunNodeType.None || nodeType == currentNode.NodeType) &&
(sequenceIndex < 0 || sequenceIndex == currentNode.SequenceIndex);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 212a0e12be2248bf9415a779414312a1
timeCreated: 1773198194

View File

@ -0,0 +1,26 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class ProcedureMainRunCompletionService
{
public static ProcedureMainRunCompletionResult TryEnterCompletedPendingFinish(bool isCompletionDialogShown)
{
return isCompletionDialogShown
? ProcedureMainRunCompletionResult.NoChange
: ProcedureMainRunCompletionResult.ShowCompletionDialog;
}
public static ProcedureMainRunCompletionResult TryConfirmReturnToMenu(
ProcedureMainFlowPhase flowPhase,
bool isReturnToMenuPending)
{
if (flowPhase != ProcedureMainFlowPhase.RunCompletedPendingFinish || isReturnToMenuPending)
{
return ProcedureMainRunCompletionResult.NoChange;
}
return ProcedureMainRunCompletionResult.ReturnToMenu;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8d565626cc594843989da76a623a1698
timeCreated: 1773198165

View File

@ -0,0 +1,27 @@
using GeometryTD.Definition;
namespace GeometryTD.Procedure
{
public static class ProcedureMainRunFlowService
{
public static ProcedureMainRunAdvanceResult TryAdvanceRun(
RunState runState,
RunNodeCompletionStatus completionStatus,
BackpackInventoryData snapshot)
{
if (!RunStateAdvanceService.TryCompleteCurrentNode(runState, completionStatus, snapshot))
{
return ProcedureMainRunAdvanceResult.NoChange;
}
if (runState != null && runState.IsCompleted)
{
return ProcedureMainRunAdvanceResult.RunCompleted;
}
return completionStatus == RunNodeCompletionStatus.Exception
? ProcedureMainRunAdvanceResult.NodeException
: ProcedureMainRunAdvanceResult.AdvancedToNextNode;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f62dcb2d97fe4f0c884e6eab3d500edf
timeCreated: 1773198145

View File

@ -16,13 +16,10 @@ namespace GeometryTD.UI
private readonly List<GoodsItemRawData> _currentGoods = new List<GoodsItemRawData>(GoodsCount);
private readonly List<DRShopPrice> _shopPriceRows = new List<DRShopPrice>();
private readonly Dictionary<TagType, RarityType> _tagMinRarityByTag = new Dictionary<TagType, RarityType>();
private IDataTable<DRShopPrice> _shopPriceTable;
private IDataTable<DRMuzzleComp> _muzzleCompTable;
private IDataTable<DRBearingComp> _bearingCompTable;
private IDataTable<DRBaseComp> _baseCompTable;
private IDataTable<DRTag> _tagTable;
public bool PrepareForOpen()
{
@ -101,9 +98,8 @@ namespace GeometryTD.UI
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>();
_tagTable ??= GameEntry.DataTable.GetDataTable<DRTag>();
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null || _tagTable == null)
if (_shopPriceTable == null || _muzzleCompTable == null || _bearingCompTable == null || _baseCompTable == null)
{
Log.Warning("ShopFormUseCase.EnsureTables() failed. Missing required data tables.");
return false;
@ -127,13 +123,10 @@ namespace GeometryTD.UI
}
}
EnsureTagMinRarityLookup();
return _shopPriceRows.Count > 0 &&
_muzzleCompTable.Count > 0 &&
_bearingCompTable.Count > 0 &&
_baseCompTable.Count > 0 &&
_tagMinRarityByTag.Count > 0;
_baseCompTable.Count > 0;
}
private bool TryBuildRandomGoodsItem(int goodsIndex, out GoodsItemRawData goodsItem)
@ -197,8 +190,7 @@ namespace GeometryTD.UI
normalizedRarity,
InventoryTagSourceType.Shop,
instanceId,
config.Id,
_tagMinRarityByTag),
config.Id),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType
@ -224,8 +216,7 @@ namespace GeometryTD.UI
normalizedRarity,
InventoryTagSourceType.Shop,
instanceId,
config.Id,
_tagMinRarityByTag),
config.Id),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
};
@ -250,34 +241,12 @@ namespace GeometryTD.UI
normalizedRarity,
InventoryTagSourceType.Shop,
instanceId,
config.Id,
_tagMinRarityByTag),
config.Id),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType
};
}
private void EnsureTagMinRarityLookup()
{
if (_tagMinRarityByTag.Count > 0 || _tagTable == null)
{
return;
}
DRTag[] rows = _tagTable.GetAllDataRows();
for (int i = 0; i < rows.Length; i++)
{
DRTag row = rows[i];
if (row == null || row.Id <= 0)
{
continue;
}
TagType tagType = (TagType)row.Id;
_tagMinRarityByTag[tagType] = row.MinRarity;
}
}
private int ResolveRandomPrice(RarityType rarity)
{
for (int i = 0; i < _shopPriceRows.Count; i++)

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition;
using NUnit.Framework;
@ -7,16 +8,16 @@ namespace GeometryTD.Tests.EditMode
{
public sealed class InventoryTagRuleServiceTests
{
private static readonly IReadOnlyDictionary<TagType, RarityType> MinRarityByTag =
new Dictionary<TagType, RarityType>
private static readonly IReadOnlyDictionary<TagType, TagGenerationRuleData> RulesByTag =
new Dictionary<TagType, TagGenerationRuleData>
{
{ TagType.Fire, RarityType.White },
{ TagType.Ice, RarityType.White },
{ TagType.Crit, RarityType.White },
{ TagType.Shatter, RarityType.Green },
{ TagType.Inferno, RarityType.Purple },
{ TagType.AbsoluteZero, RarityType.Purple },
{ TagType.Execution, RarityType.Purple },
{ TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.Green, Weight = 15 } },
{ TagType.Inferno, new TagGenerationRuleData { TagType = TagType.Inferno, MinRarity = RarityType.Purple, Weight = 5 } },
{ TagType.AbsoluteZero, new TagGenerationRuleData { TagType = TagType.AbsoluteZero, MinRarity = RarityType.Purple, Weight = 5 } },
{ TagType.Execution, new TagGenerationRuleData { TagType = TagType.Execution, MinRarity = RarityType.Purple, Weight = 5 } },
};
[Test]
@ -33,7 +34,7 @@ namespace GeometryTD.Tests.EditMode
(TagType)99
},
RarityType.Green,
MinRarityByTag);
RulesByTag);
Assert.That(result, Is.EqualTo(new[] { TagType.Fire }));
}
@ -47,7 +48,7 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Shop,
12345,
7,
MinRarityByTag);
RulesByTag);
TagType[] second = InventoryTagRuleService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
@ -55,7 +56,7 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Shop,
12345,
7,
MinRarityByTag);
RulesByTag);
Assert.That(second, Is.EqualTo(first));
}
@ -69,7 +70,7 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Drop,
9001,
4,
MinRarityByTag);
RulesByTag);
Assert.That(result.Length, Is.EqualTo(2));
Assert.That(new HashSet<TagType>(result).Count, Is.EqualTo(result.Length));
@ -84,11 +85,82 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Seed,
42,
1,
MinRarityByTag);
RulesByTag);
Assert.That(result, Is.EqualTo(new[] { TagType.Fire }));
}
[Test]
public void ResolveComponentTags_Uses_Weight_Within_EligiblePool()
{
IReadOnlyDictionary<TagType, TagGenerationRuleData> weightedRules =
new Dictionary<TagType, TagGenerationRuleData>
{
{ TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 100 } },
{ TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } },
{ TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } },
};
TagType[] result = InventoryTagRuleService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit },
RarityType.Green,
InventoryTagSourceType.Shop,
1,
1,
weightedRules);
Assert.That(result, Has.Length.EqualTo(1));
Assert.That(result[0], Is.EqualTo(TagType.Fire));
}
[Test]
public void ResolveComponentTags_Prefers_HighWeight_Tag_In_ExpandedPool()
{
IReadOnlyDictionary<TagType, TagGenerationRuleData> weightedRules =
new Dictionary<TagType, TagGenerationRuleData>
{
{ TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 500 } },
{ TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 1 } },
{ TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 1 } },
{ TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.White, Weight = 1 } },
};
int fireCount = 0;
for (int i = 0; i < 32; i++)
{
TagType[] result = InventoryTagRuleService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
RarityType.Green,
InventoryTagSourceType.Shop,
1000 + i,
1,
weightedRules);
Assert.That(result, Has.Length.EqualTo(1));
if (result[0] == TagType.Fire)
{
fireCount++;
}
}
Assert.That(fireCount, Is.GreaterThanOrEqualTo(28));
}
[Test]
public void TagGenerationRuleRegistry_LoadFromRows_Overrides_MinRarity_And_Weight()
{
DRTag fireRow = new DRTag();
Assert.That(fireRow.ParseDataRow("\t1\t元素\tFire\tGreen\t33", null), Is.True);
TagGenerationRuleRegistry.LoadFromRows(new[] { fireRow });
Assert.That(TagGenerationRuleRegistry.TryGetRule(TagType.Fire, out TagGenerationRuleData rule), Is.True);
Assert.That(rule.MinRarity, Is.EqualTo(RarityType.Green));
Assert.That(rule.Weight, Is.EqualTo(33));
TagGenerationRuleRegistry.ResetToDefaults();
}
[Test]
public void CreateSampleInventory_Generates_SeedTags_Within_Launch_Set_And_Matches_Tower_Stats()
{

View File

@ -148,6 +148,10 @@
> 2026-03-10 更新:`S4-07` 仍未开始正式收口。当前 Tag 参数仍主要承载在代码侧 `TagConfigRegistry` 与各 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` 已继续推进到第二阶段。当前 `Tag.txt -> DRTag -> TagGenerationRuleRegistry` 已形成组件 Tag 生成规则闭环,`InventoryTagRuleService` 不再主要依赖内部 `MinRarity` 硬编码,而是统一消费 `DRTag` 提供的 `MinRarity + Weight``ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 也已切回这一统一入口。现阶段的职责边界明确为:`Tag.txt` 负责基础字典与生成规则,`TagConfig.txt` 负责触发阶段、描述与效果参数。
>
> 2026-03-11 补充验证:已通过手工扩充组件 `PossibleTag` 候选池并调整 `Weight` 验证生成权重实际生效;同时把所有 Tag 的 `MinRarity` 提升到 `Red` 后,低品质组件不再生成任何 Tag说明 `Tag.txt` 中的 `MinRarity``Weight` 均已进入统一生成链。
### S4-01 边界结论
@ -183,7 +187,8 @@
- `Shatter` 已接入命中前数值修正链;`Inferno`、`AbsoluteZero` 已按“强化现有 `Fire` / `Ice` 状态”的首发口径落地,状态类 Tag 的内部结构继续保持注册式运行时。
- `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。
- `S4-07` 已进入第一阶段实现。当前仓库已具备 `TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 的消费闭环,首发 7 个 Tag 的触发阶段、描述与核心参数已可由表覆盖。
- `S4-07` 仍未完成最终收口。当前采用的是 `TagConfig` 表驱动,而不是文档原方案里的完整 `TagRule`;权重、生成规则等字段仍未全部映射回 DataTable / DataRow / 运行时消费链。
- `S4-07` 已推进到第二阶段。当前仓库同时具备 `Tag.txt -> DRTag -> TagGenerationRuleRegistry -> InventoryTagRuleService``TagConfig.txt -> DRTagConfig -> TagConfigRegistry -> ItemDescForm` 两条消费闭环,首发 7 个 Tag 的生成规则、触发阶段、描述与核心参数都已开始由表驱动。
- `S4-07` 仍未完成最终收口。当前采用的是 `Tag.txt + TagConfig.txt` 的分层方案,而不是文档原方案里的单独 `TagRule`;更深的规则字段与完整命名口径仍待后续决定是否继续统一。
### S4-06 当前代码状态
@ -201,8 +206,8 @@
### S4 后续执行计划
1. 继续推进 `S4-07`:在现有 `TagConfig` 表驱动基础上,决定是否补成文档中的完整 `TagRule`,或明确`TagConfig` 作为当前阶段的等价收口方案。
2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的参数和说明配置化,后续重点转向剩余规则字段是否也要配置化
1. 继续推进 `S4-07`:在现有 `Tag.txt + TagConfig.txt` 分层方案基础上,决定是否补成文档中的完整 `TagRule`,或明确当前双表就是本阶段的等价收口方案。
2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的生成规则、参数和说明配置化,后续重点转向是否还要继续配置化更深层的规则字段
3. `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 继续留在后续扩展阶段,不因为 `S4-06` 完成而提前进入当前迭代。
## 阶段 S5 - 收口耐久规则

Binary file not shown.

Binary file not shown.

View File

@ -36,7 +36,12 @@ def convert_excel_to_txt(folder_path='.'):
excel_file = pd.ExcelFile(file_path)
valid_sheets = []
for sheet_name in excel_file.sheet_names:
df = pd.read_excel(excel_file, sheet_name=sheet_name, header=None)
df = pd.read_excel(
excel_file,
sheet_name=sheet_name,
header=None,
keep_default_na=False,
)
if is_valid_sheet(df):
valid_sheets.append((sheet_name, df))