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
+3 -3
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
+15 -15
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
+15 -51
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}
@@ -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
};
+7 -2
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;
}
}
}
}
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainCombatEntryBlockReason
{
None = 0,
InventoryUnavailable = 1,
NoValidParticipantTower = 2
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f6d8af9f1d7d4041bfcee904724303e9
timeCreated: 1773198097
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainFlowPhase
{
Hub = 0,
NodeActive = 1,
RunCompletedPendingFinish = 2
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01450486bd824f47bcd74dda8a160ba4
timeCreated: 1773198057
@@ -0,0 +1,10 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainRunAdvanceResult
{
NoChange = 0,
NodeException = 1,
AdvancedToNextNode = 2,
RunCompleted = 3
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e9e0fa56d7e84874ac0f9d4a400fda13
timeCreated: 1773198071
@@ -0,0 +1,9 @@
namespace GeometryTD.Definition
{
public enum ProcedureMainRunCompletionResult
{
NoChange = 0,
ShowCompletionDialog = 1,
ReturnToMenu = 2
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e3d0df05f77043a99c463a790b5ac35f
timeCreated: 1773198084
@@ -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,
@@ -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; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cfe508e818ce47649a112ecdf86cf26a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e72e26721274985ba2d44ca825d7f75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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)
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f18d07456c02b1546b198ed25ba69e18
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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;
}
}
}
}
@@ -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; }
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5699aa9933294974875bfce1d34d1d4b
timeCreated: 1773198121
@@ -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 "不满足当前参战条件。";
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be3e90d1a1874e26af6d1555c301aa26
timeCreated: 1773198334
@@ -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);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 212a0e12be2248bf9415a779414312a1
timeCreated: 1773198194
@@ -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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8d565626cc594843989da76a623a1698
timeCreated: 1773198165
@@ -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;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f62dcb2d97fe4f0c884e6eab3d500edf
timeCreated: 1773198145
@@ -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++)