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 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); } } /// /// 从 config 目录构建可在条件编辑器中选择的字段目录。 /// 字段按模块当前选中的职业/专精过滤;group 队伍字段暂不收录。 /// public sealed class ConditionFieldCatalog { private static readonly HashSet 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); } } /// /// 返回指定职业/专精下可用的条件字段。classId/specId 为空时只返回公共 state 字段。 /// public IReadOnlyList GetFields(int? classId, int? specId) { var fields = new List(); if (_config is null) { return fields; } var seen = new HashSet(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; } /// /// 返回指定职业/专精下 group 队伍成员的字段(生命值/职责/驱散 + 该专精光环字段), 带类型。 /// 供动态单位编辑器选择光环、以及条件编辑器构造 单位.字段 选项使用。 /// public IReadOnlyList GetGroupFields(int? classId, int? specId) { var fields = new List(); 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(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; } /// /// 返回多 ID 光环除规范 ID 外仍可解析的字段名。别名不加入下拉,避免同一逻辑光环重复显示。 /// public IReadOnlySet GetAuraAliasFieldNames(int? classId, int? specId, bool groupOnly) { var aliases = new HashSet(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 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 fields, HashSet 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 LoadSourceClassifications(int? classId, int? specId) { var result = new Dictionary(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 target, IEnumerable 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 }; } }