整合文件夹

This commit is contained in:
waynebian01
2026-06-14 01:13:58 +08:00
parent cd07b2dc8c
commit 82333279a8
50 changed files with 1668 additions and 112 deletions

197
Modules/ClassNames.cs Normal file
View File

@@ -0,0 +1,197 @@
namespace Shigure;
public static class ClassNames
{
private static readonly Dictionary<int, string> Classes = new()
{
[1] = "战士",
[2] = "圣骑士",
[3] = "猎人",
[4] = "盗贼",
[5] = "牧师",
[6] = "死亡骑士",
[7] = "萨满",
[8] = "法师",
[9] = "术士",
[10] = "武僧",
[11] = "德鲁伊",
[12] = "恶魔猎手",
[13] = "唤魔师"
};
private static readonly Dictionary<(int ClassId, int SpecId), string> Specs = new()
{
[(1, 1)] = "武器",
[(1, 2)] = "狂怒",
[(1, 3)] = "防护",
[(2, 1)] = "神圣",
[(2, 2)] = "防护",
[(2, 3)] = "惩戒",
[(3, 1)] = "兽王",
[(3, 2)] = "射击",
[(3, 3)] = "生存",
[(4, 1)] = "刺杀",
[(4, 2)] = "狂徒",
[(4, 3)] = "敏锐",
[(5, 1)] = "戒律",
[(5, 2)] = "神牧",
[(5, 3)] = "暗影",
[(6, 1)] = "鲜血",
[(6, 2)] = "冰霜",
[(6, 3)] = "邪恶",
[(7, 1)] = "元素",
[(7, 2)] = "增强",
[(7, 3)] = "奶萨",
[(8, 1)] = "奥术",
[(8, 2)] = "火焰",
[(8, 3)] = "冰霜",
[(9, 1)] = "痛苦",
[(9, 2)] = "恶魔",
[(9, 3)] = "毁灭",
[(10, 1)] = "酒仙",
[(10, 2)] = "织雾",
[(10, 3)] = "踏风",
[(11, 1)] = "平衡",
[(11, 2)] = "野性",
[(11, 3)] = "守护",
[(11, 4)] = "奶德",
[(12, 1)] = "浩劫",
[(12, 2)] = "复仇",
[(12, 3)] = "噬灭",
[(13, 1)] = "湮灭",
[(13, 2)] = "恩护",
[(13, 3)] = "增辉"
};
private static readonly Dictionary<(int ClassId, int SpecId, int HeroTalentId), string> HeroTalents = new()
{
[(1, 1, 1)] = "巨神兵",
[(1, 1, 2)] = "屠戮者",
[(1, 2, 2)] = "屠戮者",
[(1, 2, 3)] = "山丘领主",
[(1, 3, 1)] = "巨神兵",
[(1, 3, 3)] = "山丘领主",
[(2, 1, 1)] = "烈日先驱",
[(2, 1, 2)] = "铸光者",
[(2, 2, 1)] = "铸光者",
[(2, 2, 2)] = "圣殿骑士",
[(2, 3, 1)] = "烈日先驱",
[(2, 3, 2)] = "圣殿骑士",
[(3, 1, 1)] = "黑暗游侠",
[(3, 1, 2)] = "猎群领袖",
[(3, 2, 1)] = "黑暗游侠",
[(3, 2, 2)] = "哨兵",
[(3, 3, 1)] = "猎群领袖",
[(3, 3, 2)] = "哨兵",
[(4, 1, 1)] = "死亡猎手",
[(4, 1, 2)] = "命缚者",
[(4, 2, 1)] = "命缚者",
[(4, 2, 2)] = "欺诈者",
[(4, 3, 1)] = "死亡猎手",
[(4, 3, 2)] = "欺诈者",
[(5, 1, 1)] = "神谕者",
[(5, 1, 2)] = "虚空编织者",
[(5, 2, 1)] = "神谕者",
[(5, 2, 2)] = "执政官",
[(5, 3, 1)] = "执政官",
[(5, 3, 2)] = "虚空编织者",
[(6, 1, 1)] = "死亡使者",
[(6, 1, 2)] = "萨莱因",
[(6, 2, 1)] = "死亡使者",
[(6, 2, 2)] = "天启骑士",
[(6, 3, 1)] = "天启骑士",
[(6, 3, 2)] = "萨莱因",
[(7, 1, 1)] = "先知",
[(7, 1, 2)] = "风暴使者",
[(7, 2, 1)] = "风暴使者",
[(7, 2, 2)] = "图腾祭祀",
[(7, 3, 1)] = "先知",
[(7, 3, 2)] = "图腾祭祀",
[(8, 1, 1)] = "疾咒师",
[(8, 1, 2)] = "日怒",
[(8, 2, 1)] = "霜火",
[(8, 2, 2)] = "日怒",
[(8, 3, 1)] = "霜火",
[(8, 3, 2)] = "疾咒师",
[(9, 1, 1)] = "地狱召唤者",
[(9, 1, 2)] = "灵魂收割者",
[(9, 2, 1)] = "恶魔使徒",
[(9, 2, 2)] = "灵魂收割者",
[(9, 3, 1)] = "恶魔使徒",
[(9, 3, 2)] = "地狱召唤者",
[(10, 1, 1)] = "祥和宗师",
[(10, 1, 2)] = "影踪派",
[(10, 2, 1)] = "天神御师",
[(10, 2, 2)] = "祥和宗师",
[(10, 3, 1)] = "天神御师",
[(10, 3, 2)] = "影踪派",
[(11, 1, 1)] = "艾露恩钦选者",
[(11, 1, 2)] = "丛林守护者",
[(11, 2, 1)] = "利爪德鲁伊",
[(11, 2, 2)] = "荒野追猎者",
[(11, 3, 1)] = "利爪德鲁伊",
[(11, 3, 2)] = "艾露恩钦选者",
[(11, 4, 1)] = "丛林守护者",
[(11, 4, 2)] = "荒野追猎者",
[(12, 1, 1)] = "奥达奇收割者",
[(12, 1, 2)] = "邪痕枭雄",
[(12, 2, 1)] = "歼灭者",
[(12, 2, 2)] = "复仇英雄天赋2",
[(12, 3, 1)] = "噬灭英雄天赋1",
[(12, 3, 2)] = "噬灭英雄天赋2",
[(13, 1, 1)] = "塑焰者",
[(13, 1, 2)] = "鳞长",
[(13, 2, 1)] = "时空守卫",
[(13, 2, 2)] = "塑焰者",
[(13, 3, 1)] = "时空守卫",
[(13, 3, 2)] = "鳞长"
};
public static (string? ClassName, string? SpecName) GetClassAndSpecName(int? classId, int? specId)
{
var className = classId is null or 0 ? null : Classes.GetValueOrDefault(classId.Value, $"职业{classId}");
var specName = classId is null or 0 || specId is null or 0
? null
: Specs.GetValueOrDefault((classId.Value, specId.Value), $"专精{specId}");
return (className, specName);
}
public static IReadOnlyList<(int Id, string Name)> GetClasses()
{
return Classes
.OrderBy(item => item.Key)
.Select(item => (item.Key, item.Value))
.ToList();
}
public static IReadOnlyList<(int Id, string Name)> GetSpecs(int classId)
{
return Specs
.Where(item => item.Key.ClassId == classId)
.OrderBy(item => item.Key.SpecId)
.Select(item => (item.Key.SpecId, item.Value))
.ToList();
}
public static IReadOnlyList<(int Id, string Name)> GetHeroTalents(int classId, int specId)
{
return HeroTalents
.Where(item => item.Key.ClassId == classId && item.Key.SpecId == specId)
.OrderBy(item => item.Key.HeroTalentId)
.Select(item => (item.Key.HeroTalentId, item.Value))
.ToList();
}
}

View File

@@ -0,0 +1,156 @@
using System.Text.Json.Nodes;
namespace Shigure;
public enum ConditionFieldType
{
Int,
Bool,
String
}
public enum ConditionFieldCategory
{
State,
Spell,
DynamicUnit
}
public sealed record ConditionField(
string Name,
string DisplayName,
ConditionFieldType Type,
ConditionFieldCategory Category = ConditionFieldCategory.State)
{
public override string ToString() => DisplayName;
}
/// <summary>
/// 从 config.json 构建可在条件编辑器中选择的字段目录。
/// 字段按模块当前选中的职业/专精过滤group 队伍字段暂不收录。
/// </summary>
public sealed class ConditionFieldCatalog
{
private readonly ConfigService? _config;
private ConditionFieldCatalog(ConfigService? config)
{
_config = config;
}
public static ConditionFieldCatalog Load(string baseDirectory)
{
try
{
var configPath = Path.Combine(baseDirectory, "config.json");
return new ConditionFieldCatalog(new ConfigService(configPath));
}
catch
{
// config 缺失或损坏时返回空目录,编辑器降级为手动输入。
return new ConditionFieldCatalog(null);
}
}
/// <summary>
/// 返回指定职业/专精下可用的条件字段。classId/specId 为空时只返回公共 state 字段。
/// </summary>
public IReadOnlyList<ConditionField> GetFields(int? classId, int? specId)
{
var fields = new List<ConditionField>();
if (_config is null)
{
return fields;
}
var seen = new HashSet<string>(StringComparer.Ordinal);
var stateConfig = _config.BuildStateConfig(classId, specId);
foreach (var (key, node) in stateConfig)
{
if (key is "group" or "spells")
{
continue;
}
if (node is JsonObject field && field.ContainsKey("step"))
{
AddField(fields, seen, key, key, ReadType(field), ConditionFieldCategory.State);
}
}
if (JsonHelpers.Get(stateConfig, "spells") is JsonObject spells)
{
foreach (var (spellName, node) in spells)
{
if (node is JsonObject field && field.ContainsKey("step"))
{
AddField(fields, seen, $"spells.{spellName}", $"技能: {spellName}", ReadType(field), ConditionFieldCategory.Spell);
}
}
}
AddField(fields, seen, ModuleSpecialActions.FailedSpell, ModuleSpecialActions.FailedSpell, ConditionFieldType.String, ConditionFieldCategory.State);
return fields;
}
/// <summary>
/// 返回指定职业/专精下 group 队伍成员的字段(生命值/职责/驱散 + 该专精光环字段), 带类型。
/// 供动态单位编辑器选择光环、以及条件编辑器构造 单位.字段 选项使用。
/// </summary>
public IReadOnlyList<ConditionField> GetGroupFields(int? classId, int? specId)
{
var fields = new List<ConditionField>();
if (_config is null)
{
return fields;
}
var stateConfig = _config.BuildStateConfig(classId, specId);
if (JsonHelpers.Get(stateConfig, "group") is not JsonObject group)
{
return fields;
}
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var (key, node) in group)
{
if (key is "start" or "num")
{
continue;
}
if (node is JsonObject field && field.ContainsKey("step") && seen.Add(key))
{
fields.Add(new ConditionField(key, key, ReadType(field)));
}
}
return fields;
}
private static void AddField(
List<ConditionField> fields,
HashSet<string> seen,
string name,
string displayName,
ConditionFieldType type,
ConditionFieldCategory category = ConditionFieldCategory.State)
{
if (seen.Add(name))
{
fields.Add(new ConditionField(name, displayName, type, category));
}
}
private static ConditionFieldType ReadType(JsonObject field)
{
return JsonHelpers.GetString(JsonHelpers.Get(field, "type")) switch
{
"bool" => ConditionFieldType.Bool,
"string" => ConditionFieldType.String,
_ => ConditionFieldType.Int
};
}
}

307
Modules/FormulaEvaluator.cs Normal file
View File

@@ -0,0 +1,307 @@
using System.Globalization;
namespace Shigure;
public static class FormulaEvaluator
{
public static bool TryEvaluateInt(string? expression, GameState state, out int value, out string? error)
{
value = 0;
error = null;
var normalized = NormalizeExpression(expression);
if (string.IsNullOrWhiteSpace(normalized))
{
error = "公式为空。";
return false;
}
try
{
var parser = new Parser(normalized, state);
var result = parser.Parse();
if (double.IsNaN(result) || double.IsInfinity(result))
{
error = "公式结果不是有效数字。";
return false;
}
value = (int)result;
return true;
}
catch (FormulaParseException ex)
{
error = ex.Message;
return false;
}
}
public static string NormalizeExpression(string? expression)
{
var text = StripComment(expression).Trim();
var equals = text.IndexOf('=');
if (equals > 0 && equals < text.Length - 1)
{
text = text[(equals + 1)..].Trim();
}
return text;
}
public static bool TrySplitAssignment(string? expression, out string field, out string formula)
{
field = string.Empty;
formula = NormalizeExpression(expression);
var text = StripComment(expression).Trim();
var equals = text.IndexOf('=');
if (equals <= 0 || equals >= text.Length - 1)
{
return false;
}
field = text[..equals].Trim();
return field.Length > 0 && formula.Length > 0;
}
private static string StripComment(string? expression)
{
var text = expression ?? string.Empty;
var comment = text.IndexOf('#');
return comment < 0 ? text : text[..comment];
}
private sealed class Parser
{
private readonly string _text;
private readonly GameState _state;
private int _position;
public Parser(string text, GameState state)
{
_text = text;
_state = state;
}
public double Parse()
{
var value = ParseExpression();
SkipWhiteSpace();
if (!IsEnd)
{
throw Error($"无法识别“{Current}”。");
}
return value;
}
private double ParseExpression()
{
var value = ParseTerm();
while (true)
{
SkipWhiteSpace();
if (Match('+'))
{
value += ParseTerm();
}
else if (Match('-'))
{
value -= ParseTerm();
}
else
{
return value;
}
}
}
private double ParseTerm()
{
var value = ParseFactor();
while (true)
{
SkipWhiteSpace();
if (Match('*'))
{
value *= ParseFactor();
}
else if (Match('/'))
{
var divisor = ParseFactor();
if (Math.Abs(divisor) < double.Epsilon)
{
throw Error("公式中出现除以 0。");
}
value /= divisor;
}
else
{
return value;
}
}
}
private double ParseFactor()
{
SkipWhiteSpace();
if (Match('+'))
{
return ParseFactor();
}
if (Match('-'))
{
return -ParseFactor();
}
return ParsePrimary();
}
private double ParsePrimary()
{
SkipWhiteSpace();
if (Match('('))
{
var value = ParseExpression();
Require(')');
return value;
}
if (char.IsDigit(Current) || Current == '.')
{
return ParseNumber();
}
if (IsIdentifierStart(Current))
{
var name = ParseIdentifier();
SkipWhiteSpace();
return Match('(') ? ParseFunction(name) : ResolveField(name);
}
throw Error("公式不完整。");
}
private double ParseFunction(string name)
{
var args = new List<double>();
SkipWhiteSpace();
if (!Match(')'))
{
while (true)
{
args.Add(ParseExpression());
SkipWhiteSpace();
if (Match(')'))
{
break;
}
Require(',');
}
}
return name.ToLowerInvariant() switch
{
"int" when args.Count == 1 => (int)args[0],
"round" when args.Count == 1 => Math.Round(args[0]),
"floor" when args.Count == 1 => Math.Floor(args[0]),
"ceil" when args.Count == 1 => Math.Ceiling(args[0]),
"min" when args.Count > 0 => args.Min(),
"max" when args.Count > 0 => args.Max(),
_ => throw Error($"不支持函数“{name}”。")
};
}
private double ParseNumber()
{
var start = _position;
while (char.IsDigit(Current) || Current == '.')
{
_position++;
}
var text = _text[start.._position];
if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)
|| double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out number))
{
return number;
}
throw Error($"数字“{text}”无效。");
}
private string ParseIdentifier()
{
var start = _position;
while (IsIdentifierPart(Current))
{
_position++;
}
return _text[start.._position].Trim();
}
private double ResolveField(string name)
{
if (ModuleConditionEvaluator.TryResolveDouble(_state, name, out var value))
{
return value;
}
throw Error($"无法读取数值“{name}”。");
}
private void Require(char expected)
{
SkipWhiteSpace();
if (!Match(expected))
{
throw Error($"缺少“{expected}”。");
}
}
private bool Match(char expected)
{
if (Current != expected)
{
return false;
}
_position++;
return true;
}
private void SkipWhiteSpace()
{
while (char.IsWhiteSpace(Current))
{
_position++;
}
}
private bool IsEnd => _position >= _text.Length;
private char Current => IsEnd ? '\0' : _text[_position];
private FormulaParseException Error(string message)
=> new($"{message} 位置: {_position + 1}");
private static bool IsIdentifierStart(char value)
=> value == '_' || value == '$' || char.IsLetter(value) || IsCjk(value);
private static bool IsIdentifierPart(char value)
=> IsIdentifierStart(value) || char.IsDigit(value) || value == '.';
private static bool IsCjk(char value)
=> value is >= '\u3400' and <= '\u9fff';
}
private sealed class FormulaParseException : Exception
{
public FormulaParseException(string message) : base(message)
{
}
}
}

94
Modules/LogicRegistry.cs Normal file
View File

@@ -0,0 +1,94 @@
namespace Shigure;
public sealed record LogicDecision(
string? Hotkey,
string Step,
IReadOnlyDictionary<string, object?> UnitInfo,
string? ModuleName = null);
public interface IClassLogic
{
LogicDecision Run(GameState state, string? specName);
}
public sealed class LogicRegistry
{
private readonly Dictionary<int, IClassLogic> _logicByClass = new();
private readonly IClassLogic _defaultLogic;
private readonly KeymapService _keymap;
private readonly ModuleStore _moduleStore;
private readonly string? _selectedModuleId;
public LogicRegistry(KeymapService keymap, ModuleStore moduleStore, string? selectedModuleId)
{
_keymap = keymap;
_moduleStore = moduleStore;
_selectedModuleId = string.IsNullOrWhiteSpace(selectedModuleId) ? null : selectedModuleId.Trim();
_defaultLogic = new DefaultClassLogic(keymap);
}
public LogicDecision Run(int? classId, int? specId, string? specName, GameState state)
{
var module = FindModule(classId, specId, state);
if (module is not null)
{
return ModuleLogic.Run(module, state, _keymap);
}
if (classId is not null && _logicByClass.TryGetValue(classId.Value, out var logic))
{
return logic.Run(state, specName);
}
return _defaultLogic.Run(state, specName);
}
public string? ResolveDynamicState(int? classId, int? specId, GameState state)
{
var module = FindModule(classId, specId, state);
if (module is null)
{
return null;
}
ModuleLogic.ResolveDynamicFields(module, state);
return module.Name;
}
private ModuleDefinition? FindModule(int? classId, int? specId, GameState state)
{
return _moduleStore.FindSelectedOrBestMatch(
_selectedModuleId,
classId,
specId,
state.GetInt("队伍类型"),
state.GetInt("英雄天赋"));
}
}
public sealed class DefaultClassLogic : IClassLogic
{
private readonly KeymapService _keymap;
public DefaultClassLogic(KeymapService keymap)
{
_keymap = keymap;
}
public LogicDecision Run(GameState state, string? specName)
{
var oneKeyAssist = state.GetInt("一键辅助");
if (oneKeyAssist == 10)
{
var hotkey = _keymap.GetHotkey(0, "一键辅助");
if (!string.IsNullOrWhiteSpace(hotkey))
{
return new LogicDecision(hotkey, "施放 一键辅助", EmptyInfo);
}
}
return new LogicDecision(null, "C# 职业逻辑尚未迁移", EmptyInfo);
}
private static readonly IReadOnlyDictionary<string, object?> EmptyInfo = new Dictionary<string, object?>();
}

View File

@@ -0,0 +1,78 @@
using System.Globalization;
namespace Shigure;
internal static class ModuleSpecialActions
{
public const string PauseSpell = "暂停";
public const string FailedSpell = "失败法术";
private static readonly Dictionary<int, string> FailedSpellMap = new()
{
[1] = "心灵尖啸",
[2] = "群体驱散",
[3] = "真言术:障",
[4] = "终极苦修",
[5] = "神圣化身",
[6] = "光晕",
[7] = "神圣赞美诗",
[8] = "虚空形态",
[9] = "吸血鬼的拥抱",
[30] = "真言术:耀",
[35] = "福音"
};
public static IReadOnlyCollection<string> FailedSpellNames => FailedSpellMap.Values;
public static bool IsPauseSpell(string? spell)
{
return string.Equals(spell?.Trim(), PauseSpell, StringComparison.Ordinal);
}
public static bool IsFailedSpell(string? spell)
{
return string.Equals(spell?.Trim(), FailedSpell, StringComparison.Ordinal);
}
public static string? GetFailedSpell(GameState state)
{
var failedSpellId = state.GetInt("法术失败");
if (!FailedSpellMap.TryGetValue(failedSpellId, out var spellName))
{
return null;
}
return state.Spells.TryGetValue(spellName, out var cooldown)
&& IsZero(cooldown)
? spellName
: null;
}
private static bool IsZero(object? value)
{
return value switch
{
int i => i == 0,
long l => l == 0,
float f => Math.Abs(f) < float.Epsilon,
double d => Math.Abs(d) < double.Epsilon,
decimal m => m == 0,
bool b => !b,
string s => TryParseZero(s, out var isZero) && isZero,
_ => false
};
}
private static bool TryParseZero(string text, out bool isZero)
{
isZero = false;
if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed)
&& !double.TryParse(text, NumberStyles.Float, CultureInfo.CurrentCulture, out parsed))
{
return false;
}
isZero = Math.Abs(parsed) < double.Epsilon;
return true;
}
}

1292
Modules/ModuleStore.cs Normal file

File diff suppressed because it is too large Load Diff

114
Modules/ModuleUnit.cs Normal file
View File

@@ -0,0 +1,114 @@
namespace Shigure;
/// <summary>
/// 动态单位选择器类型, 对应 utils.py 中返回单位槽位的函数。
/// </summary>
public enum UnitSelectorKind
{
/// <summary>生命值最低的单位。get_lowest_health_unit</summary>
LowestHealth,
/// <summary>拥有任一光环且生命值最低。get_lowest_health_unit_with_any_aura</summary>
LowestHealthWithAnyAura,
/// <summary>不带某光环且生命值最低。get_lowest_health_unit_without_aura</summary>
LowestHealthWithoutAura,
/// <summary>带某光环且生命值最低。get_lowest_health_unit_with_aura</summary>
LowestHealthWithAura,
/// <summary>某光环值等于指定值且生命值最低。get_lowest_health_unit_with_aura_count</summary>
LowestHealthWithAuraCount,
/// <summary>按职责取首个/逆序首个。get_unit_with_role</summary>
UnitWithRole,
/// <summary>按职责且不带某光环取首个/逆序首个。get_unit_with_role_and_without_aura_name</summary>
UnitWithRoleWithoutAura,
/// <summary>带某光环(取持续最久)。get_unit_with_aura</summary>
UnitWithAura,
/// <summary>带某驱散类型的首个单位。get_unit_with_dispel_type</summary>
UnitWithDispelType
}
/// <summary>
/// 模块内定义的命名动态单位。运行时由 <see cref="UnitSelector"/> 解析为 group 槽位("1".."30")。
/// 单光环类用 AuraNames[0]; WithAnyAura 用整个列表。
/// </summary>
public sealed class ModuleUnit
{
public string Name { get; set; } = string.Empty;
/// <summary>
/// 可选的"生命值名": 非空时把该单位解析出槽位的 生命值 暴露成一个同名数值条件字段,
/// 例如取名 最低血量 后条件里可直接写 最低血量 &lt; 50, 等价于 单位名.生命值 &lt; 50。
/// </summary>
public string? HealthName { get; set; }
public UnitSelectorKind Kind { get; set; } = UnitSelectorKind.LowestHealth;
public int? HealthThreshold { get; set; }
public string? HealthThresholdField { get; set; }
public int? Role { get; set; }
public bool Reverse { get; set; }
public List<string>? AuraNames { get; set; }
public int? AuraCount { get; set; }
public int? DispelType { get; set; }
public ModuleUnit Clone()
{
return new ModuleUnit
{
Name = Name,
HealthName = HealthName,
Kind = Kind,
HealthThreshold = HealthThreshold,
HealthThresholdField = HealthThresholdField,
Role = Role,
Reverse = Reverse,
AuraNames = AuraNames is null ? null : new List<string>(AuraNames),
AuraCount = AuraCount,
DispelType = DispelType
};
}
}
/// <summary>
/// 数量统计类型, 对应 utils.py 中返回整数的统计函数。
/// </summary>
public enum CountKind
{
/// <summary>生命值低于阈值的人数。count_units_below_health</summary>
UnitsBelowHealth,
/// <summary>不带某光环且生命值低于阈值的人数。count_units_without_aura_below_health</summary>
UnitsWithoutAuraBelowHealth,
/// <summary>拥有某光环的人数。count_units_with_aura</summary>
UnitsWithAura
}
/// <summary>
/// 模块内定义的命名数量字段。仅用于条件(如 低血量人数 &gt;= 3), 不能作为目标。
/// </summary>
public sealed class ModuleCountField
{
public string Name { get; set; } = string.Empty;
public CountKind Kind { get; set; } = CountKind.UnitsBelowHealth;
public int? HealthThreshold { get; set; }
public string? HealthThresholdField { get; set; }
public string? AuraName { get; set; }
public ModuleCountField Clone()
{
return new ModuleCountField
{
Name = Name,
Kind = Kind,
HealthThreshold = HealthThreshold,
HealthThresholdField = HealthThresholdField,
AuraName = AuraName
};
}
}