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:
parent
9de2e50262
commit
b1b68ebde5
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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\
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -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());
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<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++)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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 - 收口耐久规则
|
||||||
|
|
|
||||||
BIN
数据表/Tag.xlsx
BIN
数据表/Tag.xlsx
Binary file not shown.
Binary file not shown.
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue