From be8f97eca863b53b915bd7fb949ce5f2bdb898a6 Mon Sep 17 00:00:00 2001 From: waynebian01 Date: Sat, 15 Aug 2026 16:14:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=92=8C=E7=BC=93=E5=AD=98=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E8=81=8C=E8=B4=A3=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 3 +- Fuyutsui/Fuyutsui.toc | 2 +- Fuyutsui/core/block.lua | 5 ++- Infrastructure/AppPaths.cs | 4 ++ Infrastructure/UiCacheStore.cs | 2 +- Modules/ModuleStore.cs | 5 +-- Modules/ModuleUnit.cs | 12 ++++++ Modules/UnitSelector.cs | 46 ++++++++++++++++++++--- README.md | 6 +-- Shigure.csproj | 6 +-- UI/UnitEditorForm.cs | 69 +++++++++++++++++++++++++++++++++- UI/UnitSummary.cs | 24 +++++++++++- 12 files changed, 160 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8285936e..ef18138d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic - **没有测试项目**:验证 = 能编译 + 实际运行点开「设置」走查。`dotnet build` 干净通过(0 警告 0 错误)是基线要求。 - 启动参数见 [README.md](README.md#运行)(`--toggle/--mode/--logic-ms/--render-ms`),解析在 [App/AppOptions.cs](App/AppOptions.cs)。目标进程名来自 `wow_process.txt`。 -程序直接从当前 EXE 所在目录运行;`AppPaths.BaseDirectory` 即 `AppContext.BaseDirectory`,配置、按键映射和插件源码均从该目录读取;模块从我的文档目录读取(`ModuleStore.ResolveModuleDirectory()`,见下)。 +程序直接从当前 EXE 所在目录运行;`AppPaths.BaseDirectory` 即 `AppContext.BaseDirectory`,配置、按键映射和插件源码均从该目录读取;模块与 UI 缓存位于 `AppPaths.UserDataDirectory`(`{MyDocuments}/{程序名}`)下。 ## 架构与数据流 @@ -55,6 +55,7 @@ UI/ WinForms 界面、编辑器、主题 Fuyutsui/ 内置插件权威源;构建/发布时完整复制,运行时部署到游戏 AddOns config/ keymap/ 运行时 JSON 数据(构建时复制到输出, 见 .csproj 的 None+CopyToOutputDirectory) module 运行时模块数据位于我的文档目录 {MyDocuments}/Shigure/module(启动时自动创建, 不随构建复制) +cache UI 缓存位于我的文档目录 {MyDocuments}/Shigure/cache(首次写入时自动创建) wow_process.txt 目标游戏进程名列表;构建时复制,运行期间每次定位都会重新读取 ``` diff --git a/Fuyutsui/Fuyutsui.toc b/Fuyutsui/Fuyutsui.toc index 19f4e2cb..1a4744a8 100644 --- a/Fuyutsui/Fuyutsui.toc +++ b/Fuyutsui/Fuyutsui.toc @@ -1,7 +1,7 @@ ## Interface: 120100 ## Title: Fuyutsui -## Version: 1.2.1.2 +## Version: 1.2.1.3 ## Author: Wayne Bian ## IconTexture: Interface\AddOns\Fuyutsui\core\Spell_Misc_EmotionHappy.blp ## OptionalDeps: LibRangeCheck-3.0 diff --git a/Fuyutsui/core/block.lua b/Fuyutsui/core/block.lua index e4355f50..c2dce76d 100644 --- a/Fuyutsui/core/block.lua +++ b/Fuyutsui/core/block.lua @@ -583,12 +583,13 @@ local function AnchorAuraPixelButton(button, index) button:SetPoint("TOPLEFT", UIParent, "TOPLEFT", AuraBlockXOffset(index), 0) end ---- 对齐 CreateTexture(i, b):绿通道编码索引,蓝通道随剩余秒数 0..255 从 0→1 +--- 对齐 CreateTexture(i, b):绿通道编码索引,蓝通道将不足 1 秒钳为 1,其余编码到 255 local function MakeDurationColorCurve(index) local curve = C_CurveUtil.CreateColorCurve() curve:SetType(Enum.LuaCurveType.Linear) local r, g = EncodeBlockChannels(index) - curve:AddPoint(0, CreateColor(r, g, 0, 1)) + curve:AddPoint(0, CreateColor(r, g, 1 / 255, 1)) + curve:AddPoint(1, CreateColor(r, g, 1 / 255, 1)) curve:AddPoint(255, CreateColor(r, g, 1, 1)) return curve end diff --git a/Infrastructure/AppPaths.cs b/Infrastructure/AppPaths.cs index 4f4ed16c..ecd4dea9 100644 --- a/Infrastructure/AppPaths.cs +++ b/Infrastructure/AppPaths.cs @@ -3,4 +3,8 @@ namespace Shigure; internal static class AppPaths { public static string BaseDirectory => AppContext.BaseDirectory; + + public static string UserDataDirectory => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), + AppInfo.AppName); } diff --git a/Infrastructure/UiCacheStore.cs b/Infrastructure/UiCacheStore.cs index 8ef10a8b..9731fe46 100644 --- a/Infrastructure/UiCacheStore.cs +++ b/Infrastructure/UiCacheStore.cs @@ -8,7 +8,7 @@ internal static class UiCacheStore private const string CacheFileName = "window-state.json"; private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - private static string CacheDirectory => Path.Combine(AppPaths.BaseDirectory, CacheFolderName); + private static string CacheDirectory => Path.Combine(AppPaths.UserDataDirectory, CacheFolderName); private static string CacheFilePath => Path.Combine(CacheDirectory, CacheFileName); public static UiCacheState Load() diff --git a/Modules/ModuleStore.cs b/Modules/ModuleStore.cs index c2e01cbe..ad2d05c5 100644 --- a/Modules/ModuleStore.cs +++ b/Modules/ModuleStore.cs @@ -291,10 +291,7 @@ public sealed class ModuleStore public static string ResolveModuleDirectory() { - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), - AppInfo.AppName, - "module"); + return Path.Combine(AppPaths.UserDataDirectory, "module"); } public IReadOnlyList GetModules() diff --git a/Modules/ModuleUnit.cs b/Modules/ModuleUnit.cs index 085350b3..d2b58c86 100644 --- a/Modules/ModuleUnit.cs +++ b/Modules/ModuleUnit.cs @@ -48,6 +48,16 @@ public enum UnitSelectorKind HighestHealingAbsorbWithAuraCount } +/// 最低生命值选择器使用的职责筛选方式。 +public enum UnitRoleFilterKind +{ + /// 只包含指定职责。 + Include, + + /// 排除指定职责。 + Exclude +} + /// /// 模块内定义的命名动态单位。运行时由 解析为 group 槽位("1".."30")。 /// 单光环类用 AuraNames[0]; WithAnyAura 用整个列表。 @@ -65,6 +75,7 @@ public sealed class ModuleUnit public UnitSelectorKind Kind { get; set; } = UnitSelectorKind.LowestHealth; public int? HealthThreshold { get; set; } public string? HealthThresholdField { get; set; } + public UnitRoleFilterKind? RoleFilter { get; set; } public int? Role { get; set; } public bool Reverse { get; set; } public List? AuraNames { get; set; } @@ -80,6 +91,7 @@ public sealed class ModuleUnit Kind = Kind, HealthThreshold = HealthThreshold, HealthThresholdField = HealthThresholdField, + RoleFilter = RoleFilter, Role = Role, Reverse = Reverse, AuraNames = AuraNames is null ? null : new List(AuraNames), diff --git a/Modules/UnitSelector.cs b/Modules/UnitSelector.cs index cae44fbb..8a2e1483 100644 --- a/Modules/UnitSelector.cs +++ b/Modules/UnitSelector.cs @@ -25,19 +25,35 @@ public static class UnitSelector return unit.Kind switch { - UnitSelectorKind.LowestHealth => LowestHealth(group, threshold, _ => true), + UnitSelectorKind.LowestHealth => LowestHealth( + group, + threshold, + data => MatchesRoleFilter(data, unit.RoleFilter, unit.Role)), UnitSelectorKind.LowestHealthWithAnyAura => unit.AuraNames is { Count: > 0 } names - ? LowestHealth(group, threshold, data => HasAnyAura(data, names)) + ? LowestHealth( + group, + threshold, + data => MatchesRoleFilter(data, unit.RoleFilter, unit.Role) && HasAnyAura(data, names)) : null, UnitSelectorKind.LowestHealthWithoutAura => aura is null ? null - : LowestHealth(group, threshold, data => !HasAura(data, aura)), + : LowestHealth( + group, + threshold, + data => MatchesRoleFilter(data, unit.RoleFilter, unit.Role) && !HasAura(data, aura)), UnitSelectorKind.LowestHealthWithAura => aura is null ? null - : LowestHealth(group, threshold, data => HasAura(data, aura)), + : LowestHealth( + group, + threshold, + data => MatchesRoleFilter(data, unit.RoleFilter, unit.Role) && HasAura(data, aura)), UnitSelectorKind.LowestHealthWithAuraCount => aura is null || unit.AuraCount is null ? null - : LowestHealth(group, threshold, data => AuraEquals(data, aura, unit.AuraCount.Value)), + : LowestHealth( + group, + threshold, + data => MatchesRoleFilter(data, unit.RoleFilter, unit.Role) + && AuraEquals(data, aura, unit.AuraCount.Value)), UnitSelectorKind.UnitWithRole => unit.Role is null ? null : UnitWithRole(group, unit.Role.Value, unit.Reverse, _ => true), @@ -314,6 +330,26 @@ public static class UnitSelector return TryInt(GetField(data, auraName), out var val) && val == target; } + private static bool MatchesRoleFilter( + IReadOnlyDictionary data, + UnitRoleFilterKind? filter, + int? role) + { + if (filter is null) + { + return true; + } + + if (role is null || !TryInt(GetField(data, "职责"), out var actualRole)) + { + return false; + } + + return filter == UnitRoleFilterKind.Include + ? actualRole == role.Value + : actualRole != role.Value; + } + // 职责为 None/无法解析时视为不跳过(返回 true), 与 utils.py 的 _role_not_zero 一致。 private static bool RoleNotZero(IReadOnlyDictionary data) { diff --git a/README.md b/README.md index 18ab65a8..9be79259 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Shigure 是荒坂公司(Arasaka Corporation)开发的冲锋枪。有时人们只想把子弹全打出去,在硝烟过后品味眼前的一片狼藉。 -### 仓库已内置冬月电子(Fuyutsuki Electronics)的 [Fuyutsui](https://github.com/waynebian01/Fuyutsui) 插件源码,无需另行下载或手动安装。 +### 仓库已内置冬月电子(Fuyutsuki Electronics)的 Fuyutsui 插件源码,无需另行下载或手动安装。 # 免责声明 (Disclaimer) @@ -53,7 +53,7 @@ Shigure 是一个 Windows WinForms 桌面程序。它从目标窗口读取 Fuyut - `日志`:记录启动、停止、职业识别、模块匹配、逻辑状态、施放步骤、配置同步和异常。 - `关于`:显示产品、公司、版本、模块目录和配置目录。 -触发键、发送模式、模块选择、模块保存以及配置同步都会按需重启运行循环。窗口位置、大小、模块选择和表格列宽等 UI 状态保存在 `cache/window-state.json`;模块保存在我的文档目录 `{MyDocuments}/Shigure/module`。`cache/` 与模块数据都是本地数据,默认不提交到 Git。 +触发键、发送模式、模块选择、模块保存以及配置同步都会按需重启运行循环。窗口位置、大小、模块选择和表格列宽等 UI 状态保存在我的文档目录 `{MyDocuments}/Shigure/cache/window-state.json`;模块保存在 `{MyDocuments}/Shigure/module`。`cache/` 与模块数据都是本地数据,默认不提交到 Git。 ## 环境要求 @@ -87,7 +87,7 @@ dotnet run --project .\Shigure.csproj -- --toggle XBUTTON2 --mode switch --logic - `click`:每次按下只执行一轮逻辑。 - `hold`:按住时运行,松开后停止。 -程序会直接从当前 EXE 所在目录运行,并从该目录读取 `Fuyutsui/`、`config/`、`keymap/` 和 `wow_process.txt`;模块从我的文档目录 `{MyDocuments}/Shigure/module` 读取。 +程序会直接从当前 EXE 所在目录运行,并从该目录读取 `Fuyutsui/`、`config/`、`keymap/` 和 `wow_process.txt`;模块从我的文档目录 `{MyDocuments}/Shigure/module` 读取,UI 缓存写入 `{MyDocuments}/Shigure/cache`。 ## 构建 diff --git a/Shigure.csproj b/Shigure.csproj index 03f16b4e..57ba1071 100644 --- a/Shigure.csproj +++ b/Shigure.csproj @@ -9,9 +9,9 @@ Shigure Shigure Arasaka Corporation - 1.2.1.2 - 1.2.1.2 - 1.2.1.2 + 1.2.1.3 + 1.2.1.3 + 1.2.1.3 Assets\arasaka-icon.ico PerMonitorV2 diff --git a/UI/UnitEditorForm.cs b/UI/UnitEditorForm.cs index f81ea944..0b821a56 100644 --- a/UI/UnitEditorForm.cs +++ b/UI/UnitEditorForm.cs @@ -36,6 +36,13 @@ public sealed class UnitEditorForm : Form new("某光环值等于", LowestHealthAuraFilterKind.WithAuraCount) ]; + private static readonly LowestHealthRoleFilterItem[] LowestHealthRoleFilterOptions = + [ + new("不筛选职责", null), + new("包含某职责", UnitRoleFilterKind.Include), + new("不含某职责", UnitRoleFilterKind.Exclude) + ]; + private static readonly SelectorItem[] UnitSelectors = [ new("生命值最低", UnitSelectorKind.LowestHealth), @@ -73,6 +80,7 @@ public sealed class UnitEditorForm : Form private readonly ComboBox _categoryBox = new(); private readonly ComboBox _selectorBox = new(); private readonly ComboBox _lowestHealthAuraFilterBox = new(); + private readonly ComboBox _lowestHealthRoleFilterBox = new(); private readonly FlowLayoutPanel _paramPanel = new(); private readonly Label _previewLabel = new(); private readonly ToolTip _toolTip = new(); @@ -92,6 +100,7 @@ public sealed class UnitEditorForm : Form private Label _thresholdLabel = null!; private Panel _thresholdFieldRow = null!; private Panel _lowestHealthAuraFilterRow = null!; + private Panel _lowestHealthRoleFilterRow = null!; private Panel _roleRow = null!; private Panel _reverseRow = null!; private Panel _auraRow = null!; @@ -238,6 +247,12 @@ public sealed class UnitEditorForm : Form _lowestHealthAuraFilterBox.SelectedIndex = 0; _lowestHealthAuraFilterBox.SelectedIndexChanged += (_, _) => UpdateParamVisibility(); + UiTheme.StyleComboBox(_lowestHealthRoleFilterBox); + _lowestHealthRoleFilterBox.DropDownWidth = 220; + _lowestHealthRoleFilterBox.Items.AddRange(LowestHealthRoleFilterOptions.Cast().ToArray()); + _lowestHealthRoleFilterBox.SelectedIndex = 0; + _lowestHealthRoleFilterBox.SelectedIndexChanged += (_, _) => UpdateParamVisibility(); + UiTheme.StyleComboBox(_roleBox); _roleBox.DropDownWidth = 160; _roleBox.Items.AddRange(RoleOptions.Cast().ToArray()); @@ -300,6 +315,7 @@ public sealed class UnitEditorForm : Form _thresholdLabel = _thresholdRow.Controls.OfType