mirror of
https://github.com/waynebian01/Shigure.git
synced 2026-09-02 22:39:01 +08:00
475 lines
16 KiB
C#
475 lines
16 KiB
C#
using System.Text.Json.Nodes;
|
||
|
||
namespace Shigure;
|
||
|
||
public enum ConditionFieldType
|
||
{
|
||
Int,
|
||
Bool,
|
||
String
|
||
}
|
||
|
||
public enum ConditionFieldCategory
|
||
{
|
||
State,
|
||
Shigure,
|
||
Aura,
|
||
Spell,
|
||
DynamicUnit,
|
||
DynamicValue
|
||
}
|
||
|
||
public static class ShigureConditionFields
|
||
{
|
||
// 仅供条件编辑器承载规则级配置,不会写入条件表达式参与状态求值。
|
||
public const string Delay = "$shigure.delay";
|
||
public const string LogicDelay = "$shigure.logicDelay";
|
||
}
|
||
|
||
public sealed record ConditionField(
|
||
string Name,
|
||
string DisplayName,
|
||
ConditionFieldType Type,
|
||
ConditionFieldCategory Category = ConditionFieldCategory.State,
|
||
string? Classification = null,
|
||
long? ItemId = null)
|
||
{
|
||
public override string ToString() => DisplayName;
|
||
}
|
||
|
||
public sealed record ConditionSpell(long SpellId, int Index, string Name)
|
||
{
|
||
public string DisplayName => $"{Name} / {SpellId}";
|
||
}
|
||
|
||
public static class SpellIdConditionFields
|
||
{
|
||
public const string OneKeyAssist = "一键辅助";
|
||
public const string InsertSpell = "插入法术";
|
||
public const string CastingSpell = "施法技能";
|
||
public const string PreviousSpell = "上个技能";
|
||
|
||
private static readonly HashSet<string> Names = new(StringComparer.Ordinal)
|
||
{
|
||
OneKeyAssist,
|
||
InsertSpell,
|
||
CastingSpell,
|
||
PreviousSpell
|
||
};
|
||
|
||
public static bool Contains(string? fieldName)
|
||
{
|
||
var normalized = fieldName?.Trim() ?? string.Empty;
|
||
if (normalized.StartsWith("state.", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
normalized = normalized["state.".Length..];
|
||
}
|
||
|
||
return Names.Contains(normalized);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 config 目录构建可在条件编辑器中选择的字段目录。
|
||
/// 字段按模块当前选中的职业/专精过滤;group 队伍字段暂不收录。
|
||
/// </summary>
|
||
public sealed class ConditionFieldCatalog
|
||
{
|
||
private static readonly HashSet<string> RemovedCastFields = new(StringComparer.Ordinal)
|
||
{
|
||
"施法",
|
||
"目标施法",
|
||
"焦点施法",
|
||
"首领1施法",
|
||
"首领2施法",
|
||
"首领3施法",
|
||
"首领4施法",
|
||
"首领5施法"
|
||
};
|
||
|
||
private readonly ConfigService? _config;
|
||
private readonly string _baseDirectory;
|
||
|
||
private ConditionFieldCatalog(ConfigService? config, string baseDirectory)
|
||
{
|
||
_config = config;
|
||
_baseDirectory = baseDirectory;
|
||
}
|
||
|
||
public static ConditionFieldCatalog Load(string baseDirectory)
|
||
{
|
||
try
|
||
{
|
||
return new ConditionFieldCatalog(ConfigService.LoadFromBaseDirectory(baseDirectory), baseDirectory);
|
||
}
|
||
catch
|
||
{
|
||
// config 缺失或损坏时返回空目录,编辑器降级为手动输入。
|
||
return new ConditionFieldCatalog(null, baseDirectory);
|
||
}
|
||
}
|
||
|
||
/// <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);
|
||
var sourceClassifications = LoadSourceClassifications(classId, specId);
|
||
|
||
foreach (var (key, node) in stateConfig)
|
||
{
|
||
if (key is "group" or "spells" or "auras"
|
||
|| key == "锚点"
|
||
|| RemovedCastFields.Contains(key))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (node is JsonObject field && field.ContainsKey("step"))
|
||
{
|
||
AddField(
|
||
fields,
|
||
seen,
|
||
key,
|
||
key,
|
||
ReadType(field),
|
||
ConditionFieldCategory.State,
|
||
ReadClassification(field)
|
||
?? sourceClassifications.GetValueOrDefault(key)
|
||
?? InferStateClassification(key),
|
||
ReadItemId(field));
|
||
}
|
||
}
|
||
|
||
if (JsonHelpers.Get(stateConfig, "auras") is JsonObject auras)
|
||
{
|
||
foreach (var (auraName, node) in auras)
|
||
{
|
||
if (node is JsonObject field && field.ContainsKey("step"))
|
||
{
|
||
var displayName = ReadDisplayName(field) ?? auraName;
|
||
AddField(
|
||
fields,
|
||
seen,
|
||
$"auras.{auraName}",
|
||
$"{displayName} / {ReadSpellId(field)?.ToString() ?? "?"}",
|
||
ReadType(field),
|
||
ConditionFieldCategory.Aura,
|
||
ReadClassification(field)
|
||
?? sourceClassifications.GetValueOrDefault($"auras.{auraName}")
|
||
?? InferAuraClassification(auraName));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (JsonHelpers.Get(stateConfig, "spells") is JsonObject spells)
|
||
{
|
||
foreach (var (spellName, node) in spells)
|
||
{
|
||
if (node is JsonObject field && field.ContainsKey("step"))
|
||
{
|
||
var displayName = ReadDisplayName(field) ?? spellName;
|
||
AddField(
|
||
fields,
|
||
seen,
|
||
$"spells.{spellName}",
|
||
$"技能: {displayName} / {ReadSpellId(field)?.ToString() ?? "?"}",
|
||
ReadType(field),
|
||
ConditionFieldCategory.Spell);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 原始“插入法术”按 config 中的 int 状态保留;转换为技能名的特殊字段单独置底。
|
||
AddField(
|
||
fields,
|
||
seen,
|
||
ModuleSpecialActions.FailedSpell,
|
||
ModuleSpecialActions.FailedSpell,
|
||
ConditionFieldType.String,
|
||
ConditionFieldCategory.State,
|
||
ClassStateCatalog.CategoryState);
|
||
|
||
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))
|
||
{
|
||
var displayName = ReadDisplayName(field) ?? key;
|
||
var spellId = ReadSpellId(field);
|
||
fields.Add(new ConditionField(
|
||
key,
|
||
spellId is null ? displayName : $"{displayName} / {spellId}",
|
||
ReadType(field)));
|
||
}
|
||
}
|
||
|
||
// 治疗吸收由网格扫描注入,不在 config 的 group 字段里声明。
|
||
if (seen.Add("治疗吸收"))
|
||
{
|
||
fields.Add(new ConditionField("治疗吸收", "治疗吸收", ConditionFieldType.Int));
|
||
}
|
||
|
||
return fields;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回多 ID 光环除规范 ID 外仍可解析的字段名。别名不加入下拉,避免同一逻辑光环重复显示。
|
||
/// </summary>
|
||
public IReadOnlySet<string> GetAuraAliasFieldNames(int? classId, int? specId, bool groupOnly)
|
||
{
|
||
var aliases = new HashSet<string>(StringComparer.Ordinal);
|
||
if (_config is null)
|
||
{
|
||
return aliases;
|
||
}
|
||
|
||
var stateConfig = _config.BuildStateConfig(classId, specId);
|
||
if (!groupOnly && JsonHelpers.Get(stateConfig, "auras") is JsonObject auras)
|
||
{
|
||
foreach (var (fieldName, node) in auras)
|
||
{
|
||
if (node is JsonObject field)
|
||
{
|
||
AddAliases(aliases, $"auras.{fieldName}", field, includeScope: true);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (groupOnly && JsonHelpers.Get(stateConfig, "group") is JsonObject group)
|
||
{
|
||
foreach (var (fieldName, node) in group)
|
||
{
|
||
if (node is JsonObject field && fieldName.StartsWith("auras.", StringComparison.Ordinal))
|
||
{
|
||
AddAliases(aliases, fieldName, field, includeScope: false);
|
||
}
|
||
}
|
||
}
|
||
|
||
return aliases;
|
||
|
||
static void AddAliases(ISet<string> target, string fieldName, JsonObject field, bool includeScope)
|
||
{
|
||
var canonicalId = ReadSpellId(field);
|
||
var metric = JsonHelpers.GetString(JsonHelpers.Get(field, "metric"));
|
||
var scope = JsonHelpers.GetString(JsonHelpers.Get(field, "scope"));
|
||
if (canonicalId is null || string.IsNullOrWhiteSpace(metric)
|
||
|| JsonHelpers.Get(field, "spellIds") is not JsonArray spellIds)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var node in spellIds)
|
||
{
|
||
var alias = JsonHelpers.GetLong(node);
|
||
if (alias is null || alias <= 0 || alias == canonicalId)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
target.Add(includeScope
|
||
? $"auras.{scope}.{alias}.{metric}"
|
||
: $"auras.{alias}.{metric}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private static void AddField(
|
||
List<ConditionField> fields,
|
||
HashSet<string> seen,
|
||
string name,
|
||
string displayName,
|
||
ConditionFieldType type,
|
||
ConditionFieldCategory category = ConditionFieldCategory.State,
|
||
string? classification = null,
|
||
long? itemId = null)
|
||
{
|
||
if (seen.Add(name))
|
||
{
|
||
fields.Add(new ConditionField(name, displayName, type, category, classification, itemId));
|
||
}
|
||
}
|
||
|
||
private Dictionary<string, string> LoadSourceClassifications(int? classId, int? specId)
|
||
{
|
||
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||
if (classId is null || specId is null)
|
||
{
|
||
return result;
|
||
}
|
||
|
||
try
|
||
{
|
||
var path = Path.Combine(
|
||
_baseDirectory,
|
||
"Fuyutsui",
|
||
"class",
|
||
$"{ClassNames.GetConfigFileName(classId.Value)}.lua");
|
||
if (!File.Exists(path))
|
||
{
|
||
return result;
|
||
}
|
||
|
||
var document = ClassBlocksStore.Load(path);
|
||
if (!document.Specs.TryGetValue(specId.Value, out var spec))
|
||
{
|
||
return result;
|
||
}
|
||
|
||
if (spec.NestedStates)
|
||
{
|
||
foreach (var (classification, names) in spec.CategorizedStates)
|
||
{
|
||
foreach (var sourceName in names)
|
||
{
|
||
var name = NormalizeStateName(sourceName);
|
||
var key = IsUnitStateClassification(classification)
|
||
? classification + name
|
||
: name;
|
||
result[key] = classification;
|
||
}
|
||
}
|
||
|
||
foreach (var item in spec.Items)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(item.Name))
|
||
{
|
||
result[item.Name] = ClassStateCatalog.CategoryItem;
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
foreach (var sourceName in spec.FlatStates)
|
||
{
|
||
var name = NormalizeStateName(sourceName);
|
||
result[name] = InferStateClassification(name);
|
||
}
|
||
}
|
||
|
||
AddAuraClassifications(result, spec.PlayerAuras, string.Empty, "玩家");
|
||
AddAuraClassifications(result, spec.TargetHarmfulAuras, "目标", "目标减益");
|
||
AddAuraClassifications(result, spec.TargetHelpfulAuras, "目标", "目标增益");
|
||
AddAuraClassifications(result, spec.FocusHarmfulAuras, "焦点", "焦点减益");
|
||
AddAuraClassifications(result, spec.FocusHelpfulAuras, "焦点", "焦点增益");
|
||
}
|
||
catch
|
||
{
|
||
// Lua 配置暂不可读时继续使用生成 config 内的分类或名称推断。
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
private static void AddAuraClassifications(
|
||
Dictionary<string, string> target,
|
||
IEnumerable<ClassBlocksStore.AuraEntry> auras,
|
||
string namePrefix,
|
||
string classification)
|
||
{
|
||
foreach (var aura in auras)
|
||
{
|
||
target[$"auras.{namePrefix}{aura.Name}"] = classification;
|
||
}
|
||
}
|
||
|
||
private static string? ReadClassification(JsonObject field)
|
||
=> JsonHelpers.GetString(JsonHelpers.Get(field, "category"))?.Trim() is { Length: > 0 } value
|
||
? value
|
||
: null;
|
||
|
||
private static string? ReadDisplayName(JsonObject field)
|
||
=> JsonHelpers.GetString(JsonHelpers.Get(field, "displayName"))?.Trim() is { Length: > 0 } value
|
||
? value
|
||
: null;
|
||
|
||
private static long? ReadSpellId(JsonObject field)
|
||
=> JsonHelpers.GetLong(JsonHelpers.Get(field, "spellId"));
|
||
|
||
private static long? ReadItemId(JsonObject field)
|
||
=> JsonHelpers.GetLong(JsonHelpers.Get(field, "itemId")) is > 0 and var itemId
|
||
? itemId
|
||
: null;
|
||
|
||
private static string InferStateClassification(string name)
|
||
{
|
||
foreach (var classification in ClassStateCatalog.TopCategories.Where(IsUnitStateClassification))
|
||
{
|
||
if (name.StartsWith(classification, StringComparison.Ordinal))
|
||
{
|
||
return classification;
|
||
}
|
||
}
|
||
|
||
return ClassStateCatalog.FindCategory(name) ?? ClassStateCatalog.CategoryState;
|
||
}
|
||
|
||
private static string InferAuraClassification(string name)
|
||
=> name.StartsWith("目标", StringComparison.Ordinal)
|
||
? "目标光环"
|
||
: name.StartsWith("焦点", StringComparison.Ordinal)
|
||
? "焦点光环"
|
||
: "玩家";
|
||
|
||
private static bool IsUnitStateClassification(string classification)
|
||
=> classification is ClassStateCatalog.CategoryTarget
|
||
or ClassStateCatalog.CategoryFocus
|
||
or ClassStateCatalog.CategoryMouseover
|
||
or ClassStateCatalog.CategoryPet
|
||
or ClassStateCatalog.CategoryBoss1
|
||
or ClassStateCatalog.CategoryBoss2
|
||
or ClassStateCatalog.CategoryBoss3
|
||
or ClassStateCatalog.CategoryBoss4
|
||
or ClassStateCatalog.CategoryBoss5;
|
||
|
||
private static string NormalizeStateName(string name)
|
||
=> string.Equals(name, "法术失败", StringComparison.Ordinal)
|
||
? ModuleSpecialActions.InsertSpellState
|
||
: name;
|
||
|
||
private static ConditionFieldType ReadType(JsonObject field)
|
||
{
|
||
return JsonHelpers.GetString(JsonHelpers.Get(field, "type")) switch
|
||
{
|
||
"bool" => ConditionFieldType.Bool,
|
||
"string" => ConditionFieldType.String,
|
||
_ => ConditionFieldType.Int
|
||
};
|
||
}
|
||
}
|