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 1002 平原1.2 55 TimeElapsed 60
1003 平原1.3 55 TimeElapsed 60 1003 平原1.3 55 TimeElapsed 60
1004 平原1.4 55 EnemiesCleared 1004 平原1.4 55 EnemiesCleared
1005 平原1.* 0 1005 平原1.* 0 None
2001 平原2.1 55 TimeElapsed 60 2001 平原2.1 55 TimeElapsed 60
2002 平原2.2 55 TimeElapsed 60 2002 平原2.2 55 TimeElapsed 60
2003 平原2.3 55 TimeElapsed 60 2003 平原2.3 55 TimeElapsed 60
2004 平原2.4 55 EnemiesCleared 2004 平原2.4 55 EnemiesCleared
2005 平原2.* 0 2005 平原2.* 0 None
3001 平原3.1 55 TimeElapsed 60 3001 平原3.1 55 TimeElapsed 60
3002 平原3.2 55 TimeElapsed 60 3002 平原3.2 55 TimeElapsed 60
3003 平原3.3 55 TimeElapsed 60 3003 平原3.3 55 TimeElapsed 60
3004 平原3.4 55 EnemiesCleared 3004 平原3.4 55 EnemiesCleared
3005 平原3.* 0 3005 平原3.* 0 None
4001 平原4.1 55 TimeElapsed 60 4001 平原4.1 55 TimeElapsed 60
4002 平原4.2 55 TimeElapsed 60 4002 平原4.2 55 TimeElapsed 60
4003 平原4.3 55 TimeElapsed 60 4003 平原4.3 55 TimeElapsed 60

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable namespace GeometryTD.DataTable
{ {
/// <summary> /// <summary>
/// 枪口组件 /// Tag
/// </summary> /// </summary>
public class DRTag : DataRowBase public class DRTag : DataRowBase
{ {
@ -22,8 +22,12 @@ namespace GeometryTD.DataTable
/// </summary> /// </summary>
public string Name { get; private set; } public string Name { get; private set; }
public TagType TagType => (TagType)m_Id;
public RarityType MinRarity { get; private set; } public RarityType MinRarity { get; private set; }
public int Weight { 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);
@ -38,8 +42,9 @@ namespace GeometryTD.DataTable
index++; index++;
Name = columnStrings[index++]; Name = columnStrings[index++];
MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]); MinRarity = EnumUtility<RarityType>.Get(columnStrings[index++]);
Weight = int.Parse(columnStrings[index++]);
return true; 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;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
using Random = System.Random;
namespace GeometryTD.Definition namespace GeometryTD.Definition
{ {
public static class InventoryTagRuleService 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( public static TagType[] ResolveComponentTags(
IReadOnlyList<TagType> possibleTags, IReadOnlyList<TagType> possibleTags,
RarityType rarity, RarityType rarity,
InventoryTagSourceType sourceType, InventoryTagSourceType sourceType,
long itemInstanceId, long itemInstanceId,
int configId, 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) if (eligibleTags.Length <= 0)
{ {
return Array.Empty<TagType>(); return Array.Empty<TagType>();
@ -45,7 +34,7 @@ namespace GeometryTD.Definition
TagType[] result = new TagType[finalCount]; TagType[] result = new TagType[finalCount];
for (int i = 0; i < finalCount; i++) for (int i = 0; i < finalCount; i++)
{ {
int index = random.Next(0, pool.Count); int index = RollWeightedIndex(pool, ruleLookup, random);
result[i] = pool[index]; result[i] = pool[index];
pool.RemoveAt(index); pool.RemoveAt(index);
} }
@ -56,7 +45,7 @@ namespace GeometryTD.Definition
public static TagType[] GetEligibleTags( public static TagType[] GetEligibleTags(
IReadOnlyList<TagType> possibleTags, IReadOnlyList<TagType> possibleTags,
RarityType rarity, RarityType rarity,
IReadOnlyDictionary<TagType, RarityType> minRarityByTag = null) IReadOnlyDictionary<TagType, TagGenerationRuleData> rulesByTag = null)
{ {
if (possibleTags == null || possibleTags.Count <= 0) if (possibleTags == null || possibleTags.Count <= 0)
{ {
@ -64,23 +53,23 @@ namespace GeometryTD.Definition
} }
RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity); RarityType normalizedRarity = InventoryRarityRuleService.NormalizeComponentRarity(rarity);
IReadOnlyDictionary<TagType, RarityType> rarityLookup = minRarityByTag ?? DefaultMinRarityByTag; IReadOnlyDictionary<TagType, TagGenerationRuleData> 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++)
{ {
TagType tagType = possibleTags[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; continue;
} }
if (!TryGetMinRarity(tagType, rarityLookup, out RarityType minRarity)) if (!TryGetRule(tagType, ruleLookup, out TagGenerationRuleData rule))
{ {
continue; continue;
} }
if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(minRarity)) if (normalizedRarity < InventoryRarityRuleService.NormalizeComponentRarity(rule.MinRarity))
{ {
continue; continue;
} }
@ -123,28 +112,55 @@ namespace GeometryTD.Definition
private static bool IsSupportedLaunchTag(TagType tagType) 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, TagType tagType,
IReadOnlyDictionary<TagType, RarityType> rarityLookup, IReadOnlyDictionary<TagType, TagGenerationRuleData> ruleLookup,
out RarityType minRarity) out TagGenerationRuleData rule)
{ {
if (rarityLookup != null && rarityLookup.TryGetValue(tagType, out minRarity)) if (ruleLookup != null && ruleLookup.TryGetValue(tagType, out rule))
{ {
return true; return true;
} }
if (DefaultMinRarityByTag.TryGetValue(tagType, out minRarity)) rule = null;
{
return true;
}
minRarity = RarityType.White;
return false; 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( private static int BuildStableSeed(
RarityType rarity, RarityType rarity,
InventoryTagSourceType sourceType, 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()); 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) 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.Event;
using GameFramework.Fsm; using GameFramework.Fsm;
using GameFramework.Procedure; using GameFramework.Procedure;
@ -9,243 +8,6 @@ using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure 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 class ProcedureMain : ProcedureBase
{ {
public override bool UseNativeDialog => false; public override bool UseNativeDialog => false;
@ -467,7 +229,9 @@ namespace GeometryTD.Procedure
case RunNodeType.BossCombat: case RunNodeType.BossCombat:
ProcedureMainCombatEntryValidationResult validationResult = ProcedureMainCombatEntryValidationResult validationResult =
ProcedureMainCombatEntryValidationService.Validate( ProcedureMainCombatEntryValidationService.Validate(
GameEntry.PlayerInventory != null ? GameEntry.PlayerInventory.GetInventorySnapshot() : null); GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
if (!validationResult.CanEnterCombat) if (!validationResult.CanEnterCombat)
{ {
LogCombatEntryBlocked(currentNode, validationResult); LogCombatEntryBlocked(currentNode, validationResult);
@ -659,4 +423,4 @@ namespace GeometryTD.Procedure
_isReturnToMenuPending = true; _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<GoodsItemRawData> _currentGoods = new List<GoodsItemRawData>(GoodsCount);
private readonly List<DRShopPrice> _shopPriceRows = new List<DRShopPrice>(); private readonly List<DRShopPrice> _shopPriceRows = new List<DRShopPrice>();
private readonly Dictionary<TagType, RarityType> _tagMinRarityByTag = new Dictionary<TagType, RarityType>();
private IDataTable<DRShopPrice> _shopPriceTable; private IDataTable<DRShopPrice> _shopPriceTable;
private IDataTable<DRMuzzleComp> _muzzleCompTable; private IDataTable<DRMuzzleComp> _muzzleCompTable;
private IDataTable<DRBearingComp> _bearingCompTable; private IDataTable<DRBearingComp> _bearingCompTable;
private IDataTable<DRBaseComp> _baseCompTable; private IDataTable<DRBaseComp> _baseCompTable;
private IDataTable<DRTag> _tagTable;
public bool PrepareForOpen() public bool PrepareForOpen()
{ {
@ -101,9 +98,8 @@ namespace GeometryTD.UI
_muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>(); _muzzleCompTable ??= GameEntry.DataTable.GetDataTable<DRMuzzleComp>();
_bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>(); _bearingCompTable ??= GameEntry.DataTable.GetDataTable<DRBearingComp>();
_baseCompTable ??= GameEntry.DataTable.GetDataTable<DRBaseComp>(); _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."); Log.Warning("ShopFormUseCase.EnsureTables() failed. Missing required data tables.");
return false; return false;
@ -127,13 +123,10 @@ namespace GeometryTD.UI
} }
} }
EnsureTagMinRarityLookup();
return _shopPriceRows.Count > 0 && return _shopPriceRows.Count > 0 &&
_muzzleCompTable.Count > 0 && _muzzleCompTable.Count > 0 &&
_bearingCompTable.Count > 0 && _bearingCompTable.Count > 0 &&
_baseCompTable.Count > 0 && _baseCompTable.Count > 0;
_tagMinRarityByTag.Count > 0;
} }
private bool TryBuildRandomGoodsItem(int goodsIndex, out GoodsItemRawData goodsItem) private bool TryBuildRandomGoodsItem(int goodsIndex, out GoodsItemRawData goodsItem)
@ -197,8 +190,7 @@ namespace GeometryTD.UI
normalizedRarity, normalizedRarity,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
instanceId, instanceId,
config.Id, config.Id),
_tagMinRarityByTag),
AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(), AttackDamage = config.AttackDamage != null ? (int[])config.AttackDamage.Clone() : Array.Empty<int>(),
DamageRandomRate = config.DamageRandomRate, DamageRandomRate = config.DamageRandomRate,
AttackMethodType = config.AttackMethodType AttackMethodType = config.AttackMethodType
@ -224,8 +216,7 @@ namespace GeometryTD.UI
normalizedRarity, normalizedRarity,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
instanceId, instanceId,
config.Id, config.Id),
_tagMinRarityByTag),
RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(), RotateSpeed = config.RotateSpeed != null ? (float[])config.RotateSpeed.Clone() : Array.Empty<float>(),
AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>() AttackRange = config.AttackRange != null ? (float[])config.AttackRange.Clone() : Array.Empty<float>()
}; };
@ -250,34 +241,12 @@ namespace GeometryTD.UI
normalizedRarity, normalizedRarity,
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
instanceId, instanceId,
config.Id, config.Id),
_tagMinRarityByTag),
AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(), AttackSpeed = config.AttackSpeed != null ? (float[])config.AttackSpeed.Clone() : Array.Empty<float>(),
AttackPropertyType = config.AttackPropertyType 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) private int ResolveRandomPrice(RarityType rarity)
{ {
for (int i = 0; i < _shopPriceRows.Count; i++) for (int i = 0; i < _shopPriceRows.Count; i++)

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using GeometryTD.CustomUtility; using GeometryTD.CustomUtility;
using GeometryTD.DataTable;
using GeometryTD.Definition; using GeometryTD.Definition;
using NUnit.Framework; using NUnit.Framework;
@ -7,16 +8,16 @@ namespace GeometryTD.Tests.EditMode
{ {
public sealed class InventoryTagRuleServiceTests public sealed class InventoryTagRuleServiceTests
{ {
private static readonly IReadOnlyDictionary<TagType, RarityType> MinRarityByTag = private static readonly IReadOnlyDictionary<TagType, TagGenerationRuleData> RulesByTag =
new Dictionary<TagType, RarityType> new Dictionary<TagType, TagGenerationRuleData>
{ {
{ TagType.Fire, RarityType.White }, { TagType.Fire, new TagGenerationRuleData { TagType = TagType.Fire, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Ice, RarityType.White }, { TagType.Ice, new TagGenerationRuleData { TagType = TagType.Ice, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Crit, RarityType.White }, { TagType.Crit, new TagGenerationRuleData { TagType = TagType.Crit, MinRarity = RarityType.White, Weight = 20 } },
{ TagType.Shatter, RarityType.Green }, { TagType.Shatter, new TagGenerationRuleData { TagType = TagType.Shatter, MinRarity = RarityType.Green, Weight = 15 } },
{ TagType.Inferno, RarityType.Purple }, { TagType.Inferno, new TagGenerationRuleData { TagType = TagType.Inferno, MinRarity = RarityType.Purple, Weight = 5 } },
{ TagType.AbsoluteZero, RarityType.Purple }, { TagType.AbsoluteZero, new TagGenerationRuleData { TagType = TagType.AbsoluteZero, MinRarity = RarityType.Purple, Weight = 5 } },
{ TagType.Execution, RarityType.Purple }, { TagType.Execution, new TagGenerationRuleData { TagType = TagType.Execution, MinRarity = RarityType.Purple, Weight = 5 } },
}; };
[Test] [Test]
@ -33,7 +34,7 @@ namespace GeometryTD.Tests.EditMode
(TagType)99 (TagType)99
}, },
RarityType.Green, RarityType.Green,
MinRarityByTag); RulesByTag);
Assert.That(result, Is.EqualTo(new[] { TagType.Fire })); Assert.That(result, Is.EqualTo(new[] { TagType.Fire }));
} }
@ -47,7 +48,7 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
12345, 12345,
7, 7,
MinRarityByTag); RulesByTag);
TagType[] second = InventoryTagRuleService.ResolveComponentTags( TagType[] second = InventoryTagRuleService.ResolveComponentTags(
new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter }, new[] { TagType.Fire, TagType.Ice, TagType.Crit, TagType.Shatter },
@ -55,7 +56,7 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Shop, InventoryTagSourceType.Shop,
12345, 12345,
7, 7,
MinRarityByTag); RulesByTag);
Assert.That(second, Is.EqualTo(first)); Assert.That(second, Is.EqualTo(first));
} }
@ -69,7 +70,7 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Drop, InventoryTagSourceType.Drop,
9001, 9001,
4, 4,
MinRarityByTag); RulesByTag);
Assert.That(result.Length, Is.EqualTo(2)); Assert.That(result.Length, Is.EqualTo(2));
Assert.That(new HashSet<TagType>(result).Count, Is.EqualTo(result.Length)); Assert.That(new HashSet<TagType>(result).Count, Is.EqualTo(result.Length));
@ -84,11 +85,82 @@ namespace GeometryTD.Tests.EditMode
InventoryTagSourceType.Seed, InventoryTagSourceType.Seed,
42, 42,
1, 1,
MinRarityByTag); RulesByTag);
Assert.That(result, Is.EqualTo(new[] { TagType.Fire })); 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] [Test]
public void CreateSampleInventory_Generates_SeedTags_Within_Launch_Set_And_Matches_Tower_Stats() 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-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` 已进入第一阶段实现。当前已新增 `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 边界结论 ### S4-01 边界结论
@ -183,7 +187,8 @@
- `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 -> 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 当前代码状态 ### S4-06 当前代码状态
@ -201,8 +206,8 @@
### S4 后续执行计划 ### S4 后续执行计划
1. 继续推进 `S4-07`:在现有 `TagConfig` 表驱动基础上,决定是否补成文档中的完整 `TagRule`,或明确`TagConfig` 作为当前阶段的等价收口方案。 1. 继续推进 `S4-07`:在现有 `Tag.txt + TagConfig.txt` 分层方案基础上,决定是否补成文档中的完整 `TagRule`,或明确当前双表就是本阶段的等价收口方案。
2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的参数和说明配置化,后续重点转向剩余规则字段是否也要配置化 2. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准;当前已完成首发 7 个 Tag 的生成规则、参数和说明配置化,后续重点转向是否还要继续配置化更深层的规则字段
3. `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 继续留在后续扩展阶段,不因为 `S4-06` 完成而提前进入当前迭代。 3. `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 继续留在后续扩展阶段,不因为 `S4-06` 完成而提前进入当前迭代。
## 阶段 S5 - 收口耐久规则 ## 阶段 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) excel_file = pd.ExcelFile(file_path)
valid_sheets = [] valid_sheets = []
for sheet_name in excel_file.sheet_names: 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): if is_valid_sheet(df):
valid_sheets.append((sheet_name, df)) valid_sheets.append((sheet_name, df))