From e7cdbe6f2afa653da66f8fe8fbced7637d989d21 Mon Sep 17 00:00:00 2001 From: waynebian01 Date: Thu, 30 Jul 2026 01:32:16 +0800 Subject: [PATCH] feat: update warrior keymap and add priest keymap with new skills and hotkeys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modified warrior keymap to change the unit for "拳击" and ensure proper formatting. - Introduced a new ReservedUnit class to manage reserved units and their display names. - Added a comprehensive priest keymap with multiple skills and corresponding hotkeys, enhancing gameplay functionality. --- Infrastructure/FuyutsuiConfigConverter.cs | 2 +- Infrastructure/FuyutsuiKeymapConverter.cs | 78 +- Modules/ConditionFieldCatalog.cs | 12 +- Modules/ModuleSpecialActions.cs | 14 +- Modules/ModuleStore.cs | 22 + Modules/ModuleUnit.cs | 31 +- Modules/ReservedUnit.cs | 47 + README.md | 4 +- UI/ClassMacrosEditorControl.cs | 112 +- UI/ModuleDisplay.cs | 2 +- UI/ModuleEditorControl.cs | 28 +- UI/UiTheme.cs | 40 +- UI/UnitEditorForm.cs | 164 ++- UI/UnitSelector.cs | 108 +- UI/UnitSummary.cs | 35 +- keymap/deathknight.json | 20 +- keymap/demonhunter.json | 54 +- keymap/druid.json | 86 +- keymap/evoker.json | 4 +- keymap/hunter.json | 4 +- keymap/mage.json | 70 +- keymap/monk.json | 8 +- keymap/paladin.json | 88 +- keymap/priest.json | 1367 +++++++++++++++++++++ keymap/rogue.json | 2 +- keymap/shaman.json | 76 +- keymap/warlock.json | 6 +- keymap/warrior.json | 8 +- 28 files changed, 2198 insertions(+), 294 deletions(-) create mode 100644 Modules/ReservedUnit.cs create mode 100644 keymap/priest.json diff --git a/Infrastructure/FuyutsuiConfigConverter.cs b/Infrastructure/FuyutsuiConfigConverter.cs index 738bd5dc..1d916723 100644 --- a/Infrastructure/FuyutsuiConfigConverter.cs +++ b/Infrastructure/FuyutsuiConfigConverter.cs @@ -479,6 +479,6 @@ internal static class FuyutsuiConfigConverter private static string NormalizeStateName(string name) => string.Equals(name, "法术失败", StringComparison.Ordinal) - ? ModuleSpecialActions.FailedSpell + ? ModuleSpecialActions.InsertSpellState : name; } diff --git a/Infrastructure/FuyutsuiKeymapConverter.cs b/Infrastructure/FuyutsuiKeymapConverter.cs index 0bf76133..a0275020 100644 --- a/Infrastructure/FuyutsuiKeymapConverter.cs +++ b/Infrastructure/FuyutsuiKeymapConverter.cs @@ -134,9 +134,11 @@ internal static partial class FuyutsuiKeymapConverter { var relativeIndex = i - dynamicSlots - 1; MacroEntry? entry = null; + var isStaticEntry = false; if (relativeIndex < staticSpells.Count) { entry = staticSpells[relativeIndex]; + isStaticEntry = true; } else { @@ -149,7 +151,18 @@ internal static partial class FuyutsuiKeymapConverter if (entry is { Body.Length: > 0 } macroEntry) { - spell = ResolveSpellName(macroEntry); + if (isStaticEntry) + { + var parsed = ParseStaticMacro(macroEntry.Body, macroEntry.Comment); + unit = parsed.Unit; + spell = parsed.Spell; + } + else + { + var parsed = ParseSpecialMacro(macroEntry.Body, macroEntry.Comment); + unit = parsed.Unit; + spell = parsed.Spell; + } } if (entry is { Body.Length: > 0 } @@ -184,6 +197,56 @@ internal static partial class FuyutsuiKeymapConverter return spell.StartsWith("item:", StringComparison.OrdinalIgnoreCase); } + internal readonly record struct ParsedMacro(int Unit, string Spell); + + /// + /// 解析静态宏供 keymap 与宏列表共用。玩家、当前目标、焦点、地面、鼠标指向分别使用保留单位 31-35。 + /// + internal static ParsedMacro ParseStaticMacro(string raw, string? comment = null) + { + var target = StaticTargetRegex().Match(raw); + var unit = target.Success + ? ResolveUnitName(target.Groups["unit"].Value) + : ReservedUnit.None; + + return new ParsedMacro(unit, ResolveSpellName(new MacroEntry(raw, comment))); + } + + /// + /// 解析特殊宏:方括号中的首个单位作为 unit,技能沿用宏技能推导(castsequence 只取逗号前首项)。 + /// + internal static ParsedMacro ParseSpecialMacro(string raw, string? comment = null) + { + // 标准 WoW 条件允许单位出现在其它条件之后,例如 [known:123,@cursor]。 + var target = StaticTargetRegex().Match(raw); + var unit = target.Success + ? ResolveUnitName(target.Groups["unit"].Value) + : ReservedUnit.None; + + // 特殊宏也接受 [player]/[玩家]/[31] 这种“方括号内直接写单位”的简写。 + if (unit == ReservedUnit.None && SpecialUnitRegex().Match(raw) is { Success: true } specialUnit) + { + unit = ResolveUnitName(specialUnit.Groups["unit"].Value); + } + + var spell = ResolveSpellName(new MacroEntry(raw, comment)); + spell = spell.Split(',', 2, StringSplitOptions.TrimEntries)[0]; + return new ParsedMacro(unit, spell); + } + + private static int ResolveUnitName(string raw) + { + return raw.Trim().TrimStart('@').ToLowerInvariant() switch + { + "player" or "玩家" or "31" => ReservedUnit.Player, + "target" or "目标" or "32" => ReservedUnit.Target, + "focus" or "焦点" or "33" => ReservedUnit.Focus, + "cursor" or "地面" or "34" => ReservedUnit.Cursor, + "mouseover" or "鼠标" or "35" => ReservedUnit.Mouseover, + _ => ReservedUnit.None + }; + } + /// 同行 `--` 注释优先作为技能名;否则从宏文本推导。 private static string ResolveSpellName(MacroEntry entry) { @@ -257,7 +320,6 @@ internal static partial class FuyutsuiKeymapConverter // 取 ; 分支中第一段(专精/条件分支) var firstBranch = text.Split(';', 2, StringSplitOptions.TrimEntries)[0]; - var hasFocus = FocusTargetRegex().IsMatch(firstBranch) || FocusTargetRegex().IsMatch(text); var spell = StripConditions(firstBranch); if (string.IsNullOrWhiteSpace(spell)) @@ -265,11 +327,6 @@ internal static partial class FuyutsuiKeymapConverter return string.Empty; } - if (hasFocus && !spell.EndsWith("(焦点)", StringComparison.Ordinal)) - { - return spell + "(焦点)"; - } - return spell; } @@ -390,6 +447,9 @@ internal static partial class FuyutsuiKeymapConverter [GeneratedRegex(@"\[[^\]]*\]", RegexOptions.CultureInvariant)] private static partial Regex ConditionRegex(); - [GeneratedRegex(@"@focus\b|target\s*=\s*focus\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex FocusTargetRegex(); + [GeneratedRegex(@"\[[^\]]*@(?cursor|target|focus|player|mouseover)\b[^\]]*\]", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex StaticTargetRegex(); + + [GeneratedRegex(@"\[\s*@?(?player|target|focus|cursor|mouseover|玩家|目标|焦点|地面|鼠标|无目标|0|31|32|33|34|35)\s*(?:,|\])", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex SpecialUnitRegex(); } diff --git a/Modules/ConditionFieldCatalog.cs b/Modules/ConditionFieldCatalog.cs index 0a118457..82e89bdb 100644 --- a/Modules/ConditionFieldCatalog.cs +++ b/Modules/ConditionFieldCatalog.cs @@ -77,8 +77,7 @@ public sealed class ConditionFieldCatalog foreach (var (key, node) in stateConfig) { if (key is "group" or "spells" or "auras" - || key == "锚点" - || ModuleSpecialActions.IsFailedSpell(key)) + || key == "锚点") { continue; } @@ -111,7 +110,14 @@ public sealed class ConditionFieldCatalog } } - AddField(fields, seen, ModuleSpecialActions.FailedSpell, ModuleSpecialActions.FailedSpell, ConditionFieldType.String, ConditionFieldCategory.State); + // 原始“插入法术”按 config 中的 int 状态保留;转换为技能名的特殊字段单独置底。 + AddField( + fields, + seen, + ModuleSpecialActions.FailedSpell, + ModuleSpecialActions.FailedSpell, + ConditionFieldType.String, + ConditionFieldCategory.State); return fields; } diff --git a/Modules/ModuleSpecialActions.cs b/Modules/ModuleSpecialActions.cs index 929a05f3..99c458f2 100644 --- a/Modules/ModuleSpecialActions.cs +++ b/Modules/ModuleSpecialActions.cs @@ -5,7 +5,8 @@ namespace Shigure; internal static class ModuleSpecialActions { public const string PauseSpell = "暂停"; - public const string FailedSpell = "插入法术"; + public const string InsertSpellState = "插入法术"; + public const string FailedSpell = "自动插入法术"; public const string OneKeySpell = "一键法术"; public static bool IsPauseSpell(string? spell) @@ -18,6 +19,15 @@ internal static class ModuleSpecialActions return string.Equals(spell?.Trim(), FailedSpell, StringComparison.Ordinal); } + /// 旧模块动作中的“插入法术”迁移为新的特殊动作名,条件字段不在此处改写。 + public static string NormalizeSpellAction(string? spell) + { + var normalized = spell?.Trim() ?? string.Empty; + return string.Equals(normalized, InsertSpellState, StringComparison.Ordinal) + ? FailedSpell + : normalized; + } + public static bool IsOneKeySpell(string? spell) { return string.Equals(spell?.Trim(), OneKeySpell, StringComparison.Ordinal); @@ -25,7 +35,7 @@ internal static class ModuleSpecialActions public static string? GetFailedSpell(GameState state, IReadOnlyDictionary? failedSpellMap) { - var failedSpellId = state.GetInt(FailedSpell); + var failedSpellId = state.GetInt(InsertSpellState); if (failedSpellMap is null || !failedSpellMap.TryGetValue(failedSpellId, out var spellName)) { return null; diff --git a/Modules/ModuleStore.cs b/Modules/ModuleStore.cs index ba0bcbd9..68b12415 100644 --- a/Modules/ModuleStore.cs +++ b/Modules/ModuleStore.cs @@ -8,12 +8,16 @@ namespace Shigure; public sealed class ModuleDefinition { + internal const int CurrentUnitMappingVersion = 2; + public string Id { get; set; } = string.Empty; public string Name { get; set; } = "新模块"; public string Author { get; set; } = string.Empty; public string RecommendedTalent { get; set; } = string.Empty; // 保存时写入当时的 Shigure 版本(AppInfo.Version)。 public string Version { get; set; } = string.Empty; + // v2: 31=玩家、32=目标、33=焦点、34=地面、35=鼠标;旧模块 v1 的 31/34 含义相反。 + public int? UnitMappingVersion { get; set; } public bool Enabled { get; set; } = true; public ModuleMatch Match { get; set; } = new(); public List Units { get; set; } = new(); @@ -33,6 +37,7 @@ public sealed class ModuleDefinition Author = Author, RecommendedTalent = RecommendedTalent, Version = Version, + UnitMappingVersion = UnitMappingVersion, Enabled = Enabled, FilePath = FilePath, Match = Match.Clone(), @@ -49,6 +54,7 @@ public sealed class ModuleDefinition { Id = ModuleStore.CreateModuleId(name), Name = name, + UnitMappingVersion = CurrentUnitMappingVersion, Enabled = true, Rules = [ @@ -511,8 +517,24 @@ public sealed class ModuleStore } module.Rules ??= new List(); + if (module.UnitMappingVersion.GetValueOrDefault() < ModuleDefinition.CurrentUnitMappingVersion) + { + foreach (var rule in module.Rules) + { + rule.Unit = rule.Unit switch + { + 31 => ReservedUnit.Cursor, + 34 => ReservedUnit.Player, + _ => rule.Unit + }; + } + + module.UnitMappingVersion = ModuleDefinition.CurrentUnitMappingVersion; + } + foreach (var rule in module.Rules) { + rule.Spell = ModuleSpecialActions.NormalizeSpellAction(rule.Spell); rule.DelayMs = rule.DelayMs is > 0 ? rule.DelayMs : null; rule.LogicDelayMs = rule.LogicDelayMs is > 0 ? rule.LogicDelayMs : null; if (rule.SubConditions is null) diff --git a/Modules/ModuleUnit.cs b/Modules/ModuleUnit.cs index 3d6b6f79..085350b3 100644 --- a/Modules/ModuleUnit.cs +++ b/Modules/ModuleUnit.cs @@ -30,7 +30,22 @@ public enum UnitSelectorKind UnitWithAura, /// 带某驱散类型的首个单位。get_unit_with_dispel_type - UnitWithDispelType + UnitWithDispelType, + + /// 治疗吸收高于阈值且治疗吸收最高的单位。 + HighestHealingAbsorb, + + /// 拥有任一光环、治疗吸收高于阈值且治疗吸收最高的单位。 + HighestHealingAbsorbWithAnyAura, + + /// 不带某光环、治疗吸收高于阈值且治疗吸收最高的单位。 + HighestHealingAbsorbWithoutAura, + + /// 带某光环、治疗吸收高于阈值且治疗吸收最高的单位。 + HighestHealingAbsorbWithAura, + + /// 某光环值等于指定值、治疗吸收高于阈值且治疗吸收最高的单位。 + HighestHealingAbsorbWithAuraCount } /// @@ -86,7 +101,19 @@ public enum CountKind UnitsWithoutAuraBelowHealth, /// 拥有某光环的人数。count_units_with_aura - UnitsWithAura + UnitsWithAura, + + /// 拥有某光环且生命值低于阈值的人数。 + UnitsWithAuraBelowHealth, + + /// 治疗吸收大于阈值的人数。 + UnitsAboveHealingAbsorb, + + /// 不带某光环且治疗吸收大于阈值的人数。 + UnitsWithoutAuraAboveHealingAbsorb, + + /// 拥有某光环且治疗吸收大于阈值的人数。 + UnitsWithAuraAboveHealingAbsorb } /// diff --git a/Modules/ReservedUnit.cs b/Modules/ReservedUnit.cs new file mode 100644 index 00000000..4edc058b --- /dev/null +++ b/Modules/ReservedUnit.cs @@ -0,0 +1,47 @@ +using System.Globalization; + +namespace Shigure; + +/// +/// keymap 中团队槽位 1-30 之外的保留单位,以及模块编辑器使用的中文显示名称。 +/// +internal static class ReservedUnit +{ + public const int None = 0; + public const int Player = 31; + public const int Target = 32; + public const int Focus = 33; + public const int Cursor = 34; + public const int Mouseover = 35; + + public static string ToDisplayText(int unit) + { + return unit switch + { + None => "无目标", + Player => "玩家", + Target => "目标", + Focus => "焦点", + Cursor => "地面", + Mouseover => "鼠标", + _ => unit.ToString(CultureInfo.InvariantCulture) + }; + } + + public static int? ParseDisplayText(string? text) + { + var value = text?.Trim() ?? string.Empty; + return value switch + { + "无目标" => None, + "玩家" => Player, + "目标" => Target, + "焦点" => Focus, + "地面" => Cursor, + "鼠标" => Mouseover, + _ => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var unit) + ? unit + : null + }; + } +} diff --git a/README.md b/README.md index b5e197e1..887a762b 100644 --- a/README.md +++ b/README.md @@ -208,8 +208,8 @@ module\ UI 可编辑模块 模块页规则表上方有“动态单位 / 数量字段”面板。这些是按 `group` 队伍状态**每帧实时解析**的命名对象,逻辑移植自旧 Python 项目 `utils.py`,统一只统计 `职责 != 0` 的单位、`生命值 0` 视为死亡跳过、阈值表示只考虑 `0 < 生命值 < 阈值`。 -- **动态单位**(可作目标,也可在条件引用):解析为一个队伍槽位(`1`~`30`)。选择器类型包括——`生命值最低`、`按职责取首/末`、`按职责且不带某光环`、`带某光环(持续最久)`、`带某驱散类型`。选择 `生命值最低` 后,可在新的“光环筛选”下拉里继续选择 `不筛选光环`、`带任一光环`、`不带某光环`、`带某光环` 或 `某光环值等于`。参数(血量阈值、职责、光环、光环值、驱散类型等)按所选类型自适应显示,光环候选来自当前职业/专精的 `group` 字段,并排除 `生命值`、`职责`、`驱散` 这类基础字段。编辑弹窗在第二行显示“名称”,选择 `生命值最低` 时还会显示“值名称”:填了之后,该单位解析槽位的“生命值”会暴露成一个同名数值条件字段——如取名 `最低血量` 后条件可直接写 `最低血量 < 50`(等价于 `单位名.生命值 < 50`);单位未解析时该字段视为缺失(关系比较不命中)。 -- **数量字段**(仅条件可用):解析为一个整数。类型包括 `低于阈值的人数`、`不带某光环且低血的人数`、`拥有某光环的人数`。 +- **动态单位**(可作目标,也可在条件引用):解析为一个队伍槽位(`1`~`30`)。选择器类型包括——`生命值最低`、`治疗吸收最高`、`按职责取首/末`、`按职责且不带某光环`、`带某光环(持续最久)`、`带某驱散类型`。`生命值最低` 与 `治疗吸收最高` 都支持固定/动态阈值,以及 `不筛选光环`、`带任一光环`、`不带某光环`、`带某光环`、`某光环值等于` 五种光环筛选;前者筛选 `0 < 生命值 < 阈值`,后者筛选 `治疗吸收 > 阈值`。`治疗吸收最高` 会在满足光环及阈值条件的单位中选择治疗吸收最高者,没有符合条件的单位时该动态单位不存在;其固定阈值默认值为 `0`。参数(阈值、职责、光环、光环值、驱散类型等)按所选类型自适应显示,光环候选来自当前职业/专精的 `group` 字段,并排除 `生命值`、`职责`、`驱散` 这类基础字段。编辑弹窗在第二行显示“名称”,选择 `生命值最低` 时还会显示“值名称”:填了之后,该单位解析槽位的“生命值”会暴露成一个同名数值条件字段——如取名 `最低血量` 后条件可直接写 `最低血量 < 50`(等价于 `单位名.生命值 < 50`);单位未解析时该字段视为缺失(关系比较不命中)。 +- **数量字段**(仅条件可用):解析为一个整数。选择器按数据类型分类显示:`血量 - 低于阈值`、`血量 - 低于阈值不带某光环`、`血量 - 低于阈值带某光环`、`治疗吸收 - 大于阈值`、`治疗吸收 - 大于阈值不带某光环`、`治疗吸收 - 大于阈值带某光环`、`光环 - 带某光环`。治疗吸收类型支持固定/动态阈值,固定阈值默认值为 `0`。 定义后: diff --git a/UI/ClassMacrosEditorControl.cs b/UI/ClassMacrosEditorControl.cs index 50c98c10..111d355d 100644 --- a/UI/ClassMacrosEditorControl.cs +++ b/UI/ClassMacrosEditorControl.cs @@ -27,6 +27,7 @@ public sealed class ClassMacrosEditorControl : UserControl private ClassMacrosStore.ClassMacros? _currentMacros; private string? _currentClassFile; private bool _suppressUi; + private bool _updatingDerivedColumns; private bool _dirty; public ClassMacrosEditorControl(Func resolveClassMacrosPath, Func updateConfigAsync) @@ -231,8 +232,8 @@ public sealed class ClassMacrosEditorControl : UserControl var pages = new Control[] { BuildDynamicPage(), - BuildArrayPage(_staticGrid, "法术/条件或完整宏(空字符串 = 占位)", "注释"), - BuildArrayPage(_specialGrid, "追加宏内容(空字符串 = 占位)", "注释") + BuildArrayPage(_staticGrid, "法术/条件或完整宏(空字符串 = 占位)", "注释", showParsedMacro: true), + BuildArrayPage(_specialGrid, "追加宏内容(空字符串 = 占位)", "注释", showParsedMacro: true) }; foreach (var page in pages) { @@ -342,7 +343,11 @@ public sealed class ClassMacrosEditorControl : UserControl return panel; } - private Control BuildArrayPage(DataGridView grid, string textHeader, string commentHeader) + private Control BuildArrayPage( + DataGridView grid, + string textHeader, + string commentHeader, + bool showParsedMacro) { var panel = new TableLayoutPanel { @@ -362,6 +367,24 @@ public sealed class ClassMacrosEditorControl : UserControl Width = 72, ReadOnly = true }); + if (showParsedMacro) + { + grid.Columns.Add(new DataGridViewTextBoxColumn + { + Name = "Unit", + HeaderText = "单位", + Width = 72, + ReadOnly = true + }); + grid.Columns.Add(new DataGridViewTextBoxColumn + { + Name = "Spell", + HeaderText = "技能", + Width = 180, + ReadOnly = true + }); + } + grid.Columns.Add(new DataGridViewTextBoxColumn { Name = "Text", @@ -379,8 +402,21 @@ public sealed class ClassMacrosEditorControl : UserControl private void WireGrid(DataGridView grid) { grid.CellContentClick += HandleDeleteClick; - grid.CellValueChanged += (_, _) => + grid.CellValueChanged += (_, e) => { + if (_updatingDerivedColumns) + { + return; + } + + if ((grid == _staticGrid || grid == _specialGrid) + && e.RowIndex >= 0 + && e.ColumnIndex >= 0 + && grid.Columns[e.ColumnIndex].Name is "Text" or "Comment") + { + UpdateMacroDisplay(grid, grid.Rows[e.RowIndex]); + } + MarkDirty(); UpdateOffsetHint(); }; @@ -640,25 +676,8 @@ public sealed class ClassMacrosEditorControl : UserControl _dynamicGrid.Rows.Add(name, "×"); } - for (var i = 0; i < _currentMacros.StaticSpells.Count; i++) - { - var entry = _currentMacros.StaticSpells[i]; - _staticGrid.Rows.Add( - (i + 1).ToString(CultureInfo.InvariantCulture), - entry.Text, - entry.Comment ?? "", - "×"); - } - - for (var i = 0; i < _currentMacros.SpecialSpells.Count; i++) - { - var entry = _currentMacros.SpecialSpells[i]; - _specialGrid.Rows.Add( - (i + 1).ToString(CultureInfo.InvariantCulture), - entry.Text, - entry.Comment ?? "", - "×"); - } + AddArrayRows(_staticGrid, _currentMacros.StaticSpells); + AddArrayRows(_specialGrid, _currentMacros.SpecialSpells); } finally { @@ -853,6 +872,53 @@ public sealed class ClassMacrosEditorControl : UserControl UpdateOffsetHint(); } + private void AddArrayRows(DataGridView grid, List entries) + { + for (var i = 0; i < entries.Count; i++) + { + var entry = entries[i]; + var rowIndex = grid.Rows.Add(); + var row = grid.Rows[rowIndex]; + row.Cells["Index"].Value = (i + 1).ToString(CultureInfo.InvariantCulture); + row.Cells["Text"].Value = entry.Text; + row.Cells["Comment"].Value = entry.Comment ?? ""; + row.Cells["Delete"].Value = "×"; + if (grid == _staticGrid || grid == _specialGrid) + { + UpdateMacroDisplay(grid, row); + } + } + } + + private void UpdateMacroDisplay(DataGridView grid, DataGridViewRow row) + { + if (!grid.Columns.Contains("Unit") + || !grid.Columns.Contains("Spell") + || row.IsNewRow) + { + return; + } + + var text = row.Cells["Text"].Value?.ToString() ?? ""; + var comment = row.Cells["Comment"].Value?.ToString(); + var parsed = grid == _specialGrid + ? FuyutsuiKeymapConverter.ParseSpecialMacro(text, comment) + : FuyutsuiKeymapConverter.ParseStaticMacro(text, comment); + + _updatingDerivedColumns = true; + try + { + row.Cells["Unit"].Value = grid == _specialGrid + ? ReservedUnit.ToDisplayText(parsed.Unit) + : parsed.Unit.ToString(CultureInfo.InvariantCulture); + row.Cells["Spell"].Value = parsed.Spell; + } + finally + { + _updatingDerivedColumns = false; + } + } + private static void RenumberArrayRows(DataGridView grid) { if (!grid.Columns.Contains("Index")) diff --git a/UI/ModuleDisplay.cs b/UI/ModuleDisplay.cs index 256718cf..31ab799d 100644 --- a/UI/ModuleDisplay.cs +++ b/UI/ModuleDisplay.cs @@ -4,7 +4,7 @@ internal static class ModuleDisplay { public static string FormatListItem(ModuleDefinition module) { - return $"{module.Name} [{FormatMatch(module.Match)}]"; + return module.Name; } public static string FormatMatch(ModuleMatch match) diff --git a/UI/ModuleEditorControl.cs b/UI/ModuleEditorControl.cs index b7c49b4e..89c0e3f7 100644 --- a/UI/ModuleEditorControl.cs +++ b/UI/ModuleEditorControl.cs @@ -132,7 +132,12 @@ public sealed class ModuleEditorControl : UserControl sidebar.RowStyles.Add(new RowStyle(SizeType.Absolute, 42)); _moduleList.Dock = DockStyle.Fill; - UiTheme.StyleListBox(_moduleList, Font); + UiTheme.StyleListBox( + _moduleList, + Font, + index => index >= 0 && index < _modules.Count + ? (_modules[index].Match.ClassId, _modules[index].Match.SpecId) + : (null, null)); _moduleList.BackColor = UiTheme.Background; _moduleList.SelectedIndexChanged += (_, _) => SelectModule(_moduleList.SelectedIndex); sidebar.Controls.Add(_moduleList, 0, 0); @@ -853,7 +858,7 @@ public sealed class ModuleEditorControl : UserControl _unitColumn.Items.Add(string.Empty); foreach (var unit in _keymapCatalog.GetUnits(classId)) { - _unitColumn.Items.Add(unit.ToString()); + _unitColumn.Items.Add(ReservedUnit.ToDisplayText(unit)); } foreach (DataGridViewRow row in _rulesGrid.Rows) @@ -905,8 +910,9 @@ public sealed class ModuleEditorControl : UserControl if (ModuleSpecialActions.IsOneKeySpell(spell)) { - cell.Items.Add("0"); - cell.Value = "0"; + var noTarget = ReservedUnit.ToDisplayText(ReservedUnit.None); + cell.Items.Add(noTarget); + cell.Value = noTarget; return; } @@ -917,7 +923,7 @@ public sealed class ModuleEditorControl : UserControl foreach (var unit in allowed) { - cell.Items.Add(unit.ToString()); + cell.Items.Add(ReservedUnit.ToDisplayText(unit)); } // 动态单位与技能无关, 始终可选; 放在 keymap 编号之后。 @@ -946,7 +952,7 @@ public sealed class ModuleEditorControl : UserControl } else { - // 技能切换导致旧的数字目标非法, 清空。 + // 技能切换导致旧目标非法, 清空。 cell.Value = string.Empty; } } @@ -2433,7 +2439,7 @@ public sealed class ModuleEditorControl : UserControl var hint = new Label { - Text = "目标可选 keymap 编号或上方定义的动态单位;点击“条件”列打开可视化编辑器", + Text = "目标可选技能支持的单位或上方定义的动态单位;点击“条件”列打开可视化编辑器", Dock = DockStyle.Fill, ForeColor = UiTheme.Muted, TextAlign = ContentAlignment.MiddleLeft, @@ -2549,10 +2555,10 @@ public sealed class ModuleEditorControl : UserControl foreach (var rule in module.Rules) { - // 动态目标优先显示单位名, 否则显示数字单位。 + // 动态目标优先显示单位名;保留单位显示中文,其余团队槽位显示数字。 var unitText = !string.IsNullOrWhiteSpace(rule.UnitName) ? rule.UnitName! - : rule.Unit?.ToString() ?? string.Empty; + : rule.Unit is { } unit ? ReservedUnit.ToDisplayText(unit) : string.Empty; EnsureComboItem(_spellColumn, rule.Spell); // 先加行(目标先留空), 再按技能重建目标选项并写回目标值, 避免值不在选项内被吞掉。 var index = _rulesGrid.Rows.Add(rule.Enabled, rule.Spell, string.Empty, rule.Condition); @@ -2817,7 +2823,7 @@ public sealed class ModuleEditorControl : UserControl continue; } - // 目标文本命中已定义动态单位名 → UnitName; 否则按数字 → Unit; 都不是则留空。 + // 目标文本命中已定义动态单位名 → UnitName;否则把中文保留单位或数字槽位还原为 Unit。 var isDynamic = unitNames.Contains(unitText); var subs = metadata.SubConditions .Select(sub => sub?.Trim() ?? string.Empty) @@ -2827,7 +2833,7 @@ public sealed class ModuleEditorControl : UserControl { Enabled = CellBool(row, "Enabled", defaultValue: true), Condition = condition, - Unit = isDynamic ? null : ParseNullableInt(unitText), + Unit = isDynamic ? null : ReservedUnit.ParseDisplayText(unitText), UnitName = isDynamic ? unitText : null, Spell = spell, Hotkey = string.Empty, diff --git a/UI/UiTheme.cs b/UI/UiTheme.cs index 18a6ea93..9750b3ef 100644 --- a/UI/UiTheme.cs +++ b/UI/UiTheme.cs @@ -268,7 +268,10 @@ internal static class UiTheme }; } - public static void StyleListBox(ListBox listBox, Font font) + public static void StyleListBox( + ListBox listBox, + Font font, + Func? moduleMatchSelector = null) { listBox.BackColor = Surface; listBox.ForeColor = Text; @@ -301,7 +304,40 @@ internal static class UiTheme e.Graphics.FillRectangle(accent, e.Bounds.Left, e.Bounds.Top + 5, 3, e.Bounds.Height - 10); } - var textBounds = new Rectangle(e.Bounds.Left + 10, e.Bounds.Top, e.Bounds.Width - 14, e.Bounds.Height); + var textLeft = e.Bounds.Left + 10; + if (moduleMatchSelector is not null) + { + var (classId, specId) = moduleMatchSelector(e.Index); + var icons = new[] + { + classId is { } matchedClassId ? GetClassIcon(matchedClassId) : null, + classId is { } matchedSpecClassId && specId is { } matchedSpecId + ? GetSpecIcon(matchedSpecClassId, matchedSpecId) + : null + }; + var iconSize = Math.Min(font.Height, e.Bounds.Height - 8); + foreach (var icon in icons) + { + if (icon is null) + { + continue; + } + + var iconBounds = new Rectangle( + textLeft, + e.Bounds.Top + (e.Bounds.Height - iconSize) / 2, + iconSize, + iconSize); + e.Graphics.DrawImage(icon, iconBounds); + textLeft = iconBounds.Right + 4; + } + } + + var textBounds = new Rectangle( + textLeft, + e.Bounds.Top, + Math.Max(0, e.Bounds.Right - textLeft - 4), + e.Bounds.Height); TextRenderer.DrawText( e.Graphics, listBox.Items[e.Index]?.ToString() ?? string.Empty, diff --git a/UI/UnitEditorForm.cs b/UI/UnitEditorForm.cs index 3c034cdc..f81ea944 100644 --- a/UI/UnitEditorForm.cs +++ b/UI/UnitEditorForm.cs @@ -39,6 +39,7 @@ public sealed class UnitEditorForm : Form private static readonly SelectorItem[] UnitSelectors = [ new("生命值最低", UnitSelectorKind.LowestHealth), + new("治疗吸收最高", UnitSelectorKind.HighestHealingAbsorb), new("按职责", UnitSelectorKind.UnitWithRole), new("按职责且不带某光环", UnitSelectorKind.UnitWithRoleWithoutAura), new("带某光环(持续最久)", UnitSelectorKind.UnitWithAura), @@ -47,9 +48,13 @@ public sealed class UnitEditorForm : Form private static readonly CountItem[] CountSelectors = [ - new("低于阈值的人数", CountKind.UnitsBelowHealth), - new("不带某光环且低血的人数", CountKind.UnitsWithoutAuraBelowHealth), - new("拥有某光环的人数", CountKind.UnitsWithAura) + new("血量 - 低于阈值", CountKind.UnitsBelowHealth), + new("血量 - 低于阈值不带某光环", CountKind.UnitsWithoutAuraBelowHealth), + new("血量 - 低于阈值带某光环", CountKind.UnitsWithAuraBelowHealth), + new("治疗吸收 - 大于阈值", CountKind.UnitsAboveHealingAbsorb), + new("治疗吸收 - 大于阈值不带某光环", CountKind.UnitsWithoutAuraAboveHealingAbsorb), + new("治疗吸收 - 大于阈值带某光环", CountKind.UnitsWithAuraAboveHealingAbsorb), + new("光环 - 带某光环", CountKind.UnitsWithAura) ]; private static readonly ThresholdModeItem[] ThresholdModeOptions = @@ -84,6 +89,7 @@ public sealed class UnitEditorForm : Form private Panel _thresholdModeRow = null!; private Panel _thresholdRow = null!; + private Label _thresholdLabel = null!; private Panel _thresholdFieldRow = null!; private Panel _lowestHealthAuraFilterRow = null!; private Panel _roleRow = null!; @@ -92,6 +98,7 @@ public sealed class UnitEditorForm : Form private Panel _aurasRow = null!; private Panel _auraCountRow = null!; private Panel _dispelRow = null!; + private bool _usesHealingAbsorbThreshold; public ModuleUnit? ResultUnit { get; private set; } public ModuleCountField? ResultCount { get; private set; } @@ -290,6 +297,7 @@ public sealed class UnitEditorForm : Form _thresholdModeRow = BuildLabeledRow("阈值类型", _thresholdModeBox); _thresholdRow = BuildLabeledRow("血量阈值 (<)", _thresholdBox); + _thresholdLabel = _thresholdRow.Controls.OfType