mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-03 07:15:24 +08:00
整合文件夹
This commit is contained in:
197
Modules/ClassNames.cs
Normal file
197
Modules/ClassNames.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
156
Modules/ConditionFieldCatalog.cs
Normal file
156
Modules/ConditionFieldCatalog.cs
Normal 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
307
Modules/FormulaEvaluator.cs
Normal 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
94
Modules/LogicRegistry.cs
Normal 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?>();
|
||||
}
|
||||
78
Modules/ModuleSpecialActions.cs
Normal file
78
Modules/ModuleSpecialActions.cs
Normal 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
1292
Modules/ModuleStore.cs
Normal file
File diff suppressed because it is too large
Load Diff
114
Modules/ModuleUnit.cs
Normal file
114
Modules/ModuleUnit.cs
Normal 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>
|
||||
/// 可选的"生命值名": 非空时把该单位解析出槽位的 生命值 暴露成一个同名数值条件字段,
|
||||
/// 例如取名 最低血量 后条件里可直接写 最低血量 < 50, 等价于 单位名.生命值 < 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>
|
||||
/// 模块内定义的命名数量字段。仅用于条件(如 低血量人数 >= 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user