diff --git a/Infrastructure/ClassBlocksStore.cs b/Infrastructure/ClassBlocksStore.cs
index fe830f61..47d30110 100644
--- a/Infrastructure/ClassBlocksStore.cs
+++ b/Infrastructure/ClassBlocksStore.cs
@@ -6,11 +6,12 @@ namespace Shigure;
///
/// 读写 Fuyutsui class/*.lua 中的 ClassBlocks(states/auras/spells/group),
-/// 保存时只替换 ClassBlocks 表字面量,保留文件其余内容。
+/// 同时读取 spellsList 供界面展示;保存时只替换 ClassBlocks 表字面量,保留文件其余内容。
///
internal static class ClassBlocksStore
{
public const string AssignmentName = "Fuyutsui.ClassBlocks";
+ public const string SpellsListAssignmentName = "Fuyutsui.spellsList";
private static readonly string[] StateCategories =
[
ClassStateCatalog.CategoryState,
@@ -28,9 +29,17 @@ internal static class ClassBlocksStore
public int TableStart { get; set; }
public int TableEndExclusive { get; set; }
public Dictionary Specs { get; set; } = new();
+ public List SpellsList { get; set; } = new();
public bool IsModernFormat { get; set; }
}
+ public sealed class SpellsListEntry
+ {
+ public long SpellId { get; set; }
+ public int Index { get; set; }
+ public string Name { get; set; } = string.Empty;
+ }
+
public sealed class SpecBlocks
{
public bool NestedStates { get; set; } = true;
@@ -112,6 +121,7 @@ internal static class ClassBlocksStore
specs[(int)specId] = spec;
}
+ var spellsList = ParseSpellsList(ExtractAssignedTable(source, SpellsListAssignmentName));
return new ClassFileDocument
{
FilePath = filePath,
@@ -119,10 +129,48 @@ internal static class ClassBlocksStore
TableStart = start,
TableEndExclusive = end,
Specs = specs,
+ SpellsList = spellsList,
IsModernFormat = modern
};
}
+ private static List ParseSpellsList(TableValue? table)
+ {
+ var result = new List();
+ if (table is null)
+ {
+ return result;
+ }
+
+ foreach (var (key, value) in table.Entries)
+ {
+ if (key is not long spellId || value is not TableValue spell)
+ {
+ continue;
+ }
+
+ var indexValue = spell.GetNumber("index");
+ var name = spell.GetString("name")?.Trim();
+ if (indexValue is null
+ || indexValue.Value <= 0
+ || indexValue.Value > int.MaxValue
+ || indexValue.Value != Math.Truncate(indexValue.Value)
+ || string.IsNullOrWhiteSpace(name))
+ {
+ continue;
+ }
+
+ result.Add(new SpellsListEntry
+ {
+ SpellId = spellId,
+ Index = (int)indexValue.Value,
+ Name = name
+ });
+ }
+
+ return result;
+ }
+
public static void Save(ClassFileDocument document)
{
if (!document.IsModernFormat)
diff --git a/Infrastructure/ClassStateCatalog.cs b/Infrastructure/ClassStateCatalog.cs
index d113b0b9..28bd32ab 100644
--- a/Infrastructure/ClassStateCatalog.cs
+++ b/Infrastructure/ClassStateCatalog.cs
@@ -31,7 +31,7 @@ internal static class ClassStateCatalog
"生命值", "一键辅助", "插入法术", "队伍类型", "队伍人数",
"首领战", "难度", "英雄天赋", "施法目标", "施法技能",
"敌人人数", "施法", "引导", "蓄力", "蓄力层数",
- "酒池", "符文", "姿态"
+ "酒池", "符文", "姿态", "救赎之魂1", "救赎之魂2",
]),
(CategoryConfig,
[
diff --git a/README.md b/README.md
index 887a762b..361f1bc1 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# Shigure
-Shigure 是 荒坂公司(Arasaka Corporation)开发的冲锋枪,有时人们只想把子弹全打出去,在硝烟过后品味眼前的一片狼藉。
+Shigure 是荒坂公司(Arasaka Corporation)开发的冲锋枪。有时人们只想把子弹全打出去,在硝烟过后品味眼前的一片狼藉。
-### 它需要配合 冬月电子(Fuyutsuki Electronics)的 [Fuyutsui](https://github.com/waynebian01/Fuyutsui)使用。
+### 它需要配合冬月电子(Fuyutsuki Electronics)的 [Fuyutsui](https://github.com/waynebian01/Fuyutsui) 使用。
# 免责声明 (Disclaimer)
@@ -26,58 +26,69 @@ Shigure 是 荒坂公司(Arasaka Corporation)开发的冲锋枪,有时人
一旦开始使用本软件,即视为用户已阅读、理解并同意本免责声明及 MIT License 的全部内容。若不同意,请立即停止使用并删除本软件及其副本。
-## 真实作用
+## 项目简介
-Shigure 是一个 Windows WinForms 桌面程序,用于扫描目标窗口状态、根据职业 keymap 选择按键,并把实时状态、队伍信息、逻辑输出和运行日志集中显示在 UI 中。
+Shigure 是一个 Windows WinForms 桌面程序。它从目标窗口读取 Fuyutsui 绘制的像素状态,按职业 keymap 和模块规则选择按键,并在界面中展示状态、光环、技能、队伍、动态单位、逻辑结果和运行日志。
-当前版本:`1.2.0.0`
+当前版本:`1.2.1`
-## 当前界面
+## 主要功能
-- 置顶浮动条:无边框小窗口,显示 Shigure 标题和逻辑状态,右侧有 `开关`(文字在 `开启`/`关闭` 间切换)、`设置`、`✕` 三个按钮;窗口可拖动、可从边缘缩放,启动后会自动启动运行循环。
-- 设置窗口:点击 `设置` 打开,集中包含 `通用`、`模块`、`状态`、`队伍`、`逻辑`、`日志`、`关于` 七个页签。
-- 通用页:可设置触发键、发送模式和当前使用的模块。模块选择器会根据实时识别到的职业 / 专精 / 队伍类型 / 英雄天赋筛选可用模块,可保持自动选择最匹配模块,也可从符合条件的模块中手动指定一个。点击触发键按钮后按下目标键即可录入,支持键盘按键、`XBUTTON1`、`XBUTTON2`,不支持 `ALT`。
-- 模块页:左侧列出全部模块,右侧可新建、编辑、删除模块,并编辑模块名称、匹配条件(职业 / 专精 / 队伍类型 / 英雄天赋)和判断规则。这个页面只负责逻辑编辑;具体启用或选择哪个模块在“通用”页完成。规则表的“技能”“目标”列为下拉菜单(选项来自当前选中职业的 keymap),“条件”列为只读,点击后弹出可视化条件编辑器。页面内嵌“动态单位 / 数量字段”面板,可定义如“最低血量单位”这类按队伍状态实时解析的单位,供“目标”和“条件”引用。
-- 状态页:显示当前匹配的模块名以及扫描后构建出的状态字段,右侧并列显示 `spells` 技能状态。
-- 队伍页:显示 `group` 队伍成员摘要。
-- 逻辑页:显示模块或职业逻辑返回的诊断信息(命中条件、动作技能、动作按键等)。
-- 日志页:记录启动、停止、职业识别、模块匹配、逻辑状态变化、步骤变化和异常信息。
-- 关于页:显示产品名、版本、运行目录、模块目录和配置文件路径。
+- 扫描 510 格顶部状态行、左侧计数条和队伍治疗吸收数据。
+- 按职业、专精、队伍类型和英雄天赋自动匹配模块,也可手动锁定符合条件的模块。
+- 使用可视化编辑器维护规则、主/子条件、动态单位、数量字段、条件动态数值和公式动态数值。
+- 从已运行的游戏进程定位 `Interface\AddOns\Fuyutsui`,同步或直接编辑职业配置与宏。
+- 支持 `switch`、`click`、`hold` 三种触发模式,以及除 `ALT` 外的键盘按键、`XBUTTON1`、`XBUTTON2`。
-触发键、发送模式的修改会立即重启运行循环。模块会保存到 `module` 文件夹,修改并保存后同样会重启运行循环以加载最新模块。窗口位置/大小、UI 状态(如规则表的列宽)会被缓存到 `cache/window-state.json`,下次启动时自动恢复。
+## 界面
-## 运行环境
+- 置顶浮动条:显示程序名、当前职业图标颜色和逻辑状态,提供 `开启/关闭`、`设置`、`✕` 按钮。窗口可拖动和缩放,显示后自动启动运行循环。
+- `通用`:设置触发键和发送模式;更新 Fuyutsui 配置;按实时环境筛选、自动选择或手动指定模块。
+- `配置`:直接编辑 Fuyutsui `class/*.lua` 中的 `ClassBlocks`,包括状态、光环、法术和队伍字段。旧版稀疏索引格式只读,需先迁移到 `states/auras/spells/group` 格式。
+- `宏`:编辑 `core/classmacros.lua` 中各职业的动态宏、静态宏和特殊宏。动态宏每项占用 30 个团队点名槽位。
+- `模块`:新建、编辑、删除本地模块,维护作者、推荐天赋、匹配条件、动态字段和有序规则。规则支持拖拽、上移、下移、复制与插入。
+- `状态`:分栏显示基础状态、`auras`、`spells` 和模块计算出的动态单位/数值。
+- `队伍`:显示 `group` 中当前队伍成员及扫描字段摘要。
+- `逻辑`:显示命中模块、目标、按键和调试值。
+- `日志`:记录启动、停止、职业识别、模块匹配、逻辑状态、施放步骤、配置同步和异常。
+- `关于`:显示产品、公司、版本、模块目录和配置目录。
+
+触发键、发送模式、模块选择、模块保存以及配置同步都会按需重启运行循环。窗口位置、大小、模块选择和表格列宽等 UI 状态保存在 `cache/window-state.json`。`module/` 与 `cache/` 都是本地数据,默认不提交到 Git。
+
+## 环境要求
- Windows
- .NET 10 SDK
+- 已安装并启用 [Fuyutsui](https://github.com/waynebian01/Fuyutsui)
+- 使用“更新配置”“配置”或“宏”页时,需要先打开目标游戏窗口,以便 Shigure 定位插件目录
## 运行
-在仓库根目录运行:
+在仓库根目录执行:
```powershell
dotnet run --project .\Shigure.csproj
```
-也可以传入启动参数:
+可选启动参数:
```powershell
dotnet run --project .\Shigure.csproj -- --window 魔兽世界 --toggle XBUTTON2 --mode switch --logic-ms 100 --render-ms 100
```
-参数说明:
-
- `--window`:目标窗口标题,默认 `魔兽世界`。
- `--toggle`:触发键,默认 `XBUTTON2`。
- `--mode`:发送模式,支持 `switch`、`click`、`hold`。
-- `--logic-ms`:逻辑循环间隔,默认 `100`,最小 `50`。
-- `--render-ms`:UI 刷新间隔,默认 `100`,最小 `100`。
+- `--logic-ms`:逻辑循环间隔,默认 `100` ms,最小 `50` ms。
+- `--render-ms`:UI 刷新间隔,默认 `100` ms,最小 `100` ms。
发送模式:
-- `switch`:按一次触发键开启逻辑,再按一次关闭。
-- `click`:按一次触发键只执行一轮逻辑。
-- `hold`:按住触发键时运行,松开后停止。
+- `switch`:按一次开启,再按一次关闭。
+- `click`:每次按下只执行一轮逻辑。
+- `hold`:按住时运行,松开后停止。
+
+启动时,程序会在系统临时目录创建随机名称的运行副本,并通过原始输出目录读取 `config/`、`keymap/` 和 `module/`。旧的临时副本会在下次启动时尝试清理。
## 构建
@@ -85,74 +96,88 @@ dotnet run --project .\Shigure.csproj -- --window 魔兽世界 --toggle XBUTTON2
dotnet build .\Shigure.csproj
```
-应用图标来自:
-
-```text
-Assets\shigure-emblem-arasaka.ico
-```
+应用图标为 `Assets\arasaka-icon.ico`。项目会把 `config/*.json`、`keymap/*.json` 和本地 `module/**/*.json` 复制到输出目录;图标、职业图和专精图作为嵌入资源打包。
## 项目结构
```text
-Shigure.csproj 项目文件,复制 Assets/config/keymap/module 到输出目录
-App\ 程序入口与启动参数解析
-UI\ WinForms 界面、编辑器、主题和 UI 缓存
-Runtime\ 扫描、状态构建、逻辑运行和运行时快照
-Modules\ 模块模型、存储、匹配、规则执行和字段目录
-Input\ keymap 读取、按键发送和 Win32 API 互操作
-Infrastructure\ 配置读取与 JSON 辅助方法
-Assets\ 图标与品牌资源
-keymap\ 各职业按键映射
-module\ UI 可编辑模块
+Shigure.csproj 项目文件与构建资源配置
+App\ 程序入口、启动参数和随机运行副本
+UI\ WinForms 主界面、配置/宏/模块编辑器和主题
+Runtime\ 像素扫描、状态构建、运行循环和快照
+Modules\ 模块模型、匹配、条件、公式和动态字段
+Input\ keymap 读取、按键发送和 Win32 API
+Infrastructure\ 配置服务、Lua 读写、Fuyutsui 转换和路径定位
+Assets\ 应用图标、品牌资源、职业图和专精图
+config\ 由 Fuyutsui 职业配置生成的扫描映射
+keymap\ 由 Fuyutsui 职业宏生成的按键映射
+module\ 本地模块(运行后或在 UI 中保存时创建)
+Tools\ 辅助脚本
```
-## 配置文件
+## Fuyutsui 配置同步
-- `config/*.json`:状态扫描配置和职业 keymap 引用,`common.json` 为公共配置,其余文件按英文职业名命名(如 `Warrior.json`)。
-- `keymap/*.json`:职业按键映射,运行时按职业 ID 选择。
-- `Assets/*.ico|*.png|*.svg`:程序图标和品牌资源,构建时会复制到输出目录。
-- `cache/window-state.json`:UI 缓存,保存置顶浮动条位置、设置窗口位置/大小和上次触发键。
+`通用` 页的“更新配置”会从目标窗口对应的游戏目录读取:
-### config 结构
+- `Interface\AddOns\Fuyutsui\class\*.lua` → `config/*.json`
+- `Interface\AddOns\Fuyutsui\core\classmacros.lua` → `keymap/*.json`
-`config` 目录描述了如何把扫描到的像素数据翻译成状态字段。每个字段是一个对象:
+同步完成后会刷新模块编辑器的字段/keymap 目录并重启运行循环。`配置` 页和 `宏` 页保存时会先写回相应 Lua 文件,再自动执行同一同步流程。写回前请自行备份需要保留的 Fuyutsui 配置。
-- `step`:在顶部行数据中的列索引,支持 `1`~`510`;特殊值 `"bar"` 表示从左侧标记行数据读取,并配合 `bar` 字段指定段索引。
+### `config` 结构
+
+`config` 描述如何把像素数据翻译成运行时状态。字段对象包含:
+
+- `step`:顶部状态行的列索引,支持 `1`~`510`;特殊值 `"bar"` 表示读取左侧计数条,并由 `bar` 指定段索引。
- `type`:字段类型,支持 `int`、`bool`、`string`。
-顶部状态行由 Fuyutsui 绘制为 510 个色块。Shigure 按两段颜色协议解码:`1`~`255` 使用 `(0, i / 255, b, 1)`,`256`~`510` 使用 `(1 / 255, (i - 255) / 255, b, 1)`;其中 `b` 会作为该 `step` 的原始值写入状态。左侧 `"bar"` 标记行仍按独立的计数条协议读取,不受顶部 510 色块数量影响。
+顶部状态行由 Fuyutsui 绘制为 510 个色块。Shigure 按两段颜色协议解码:`1`~`255` 使用 `(0, i / 255, b, 1)`,`256`~`510` 使用 `(1 / 255, (i - 255) / 255, b, 1)`,蓝色通道 `b` 是该 `step` 的原始值。左侧 `"bar"` 标记行使用独立计数条协议。
-`common.json` 包含 `锚点`、`职业`、`专精` 等基础字段和一个 `state` 公共状态块;其余职业文件如 `Warrior.json`、`Paladin.json` 按英文职业名命名,对应职业 ID,每个职业文件下:
+`common.json` 保存固定启动字段 `锚点`、`职业`、`专精`。其余文件按英文职业名命名,例如 `Warrior.json`、`Priest.json`,每个文件包含:
- `keymap`:该职业使用的 keymap 文件名。
-- 以专精 ID 为键(如 `"1"`、`"2"`、`"3"`)的对象,包含该专精特有的字段、`spells` 技能块和可选的 `group` 队伍块。
+- `一键法术`:状态编号到技能名的映射。
+- 以专精 ID 为键的对象,包含普通状态字段,以及可选的 `auras`、`spells`、`group`。
-运行时 `ConfigService` 会把 `锚点/职业/专精/state` 与当前 `职业/专精` 对应的配置合并,再由 `StateBuilder` 构建出 `GameState`。`group` 块用 `start`(起始列)和 `num`(每个成员占用的列数)描述队伍成员数据的排布。
+运行时先读取 `common.json` 确定职业和专精,再合并对应专精配置。`group.start` 是队伍数据起始列,`group.num` 是每名成员占用的列数;组内字段的 `step` 是相对偏移。
+
+### `keymap` 与单位编号
+
+每条 keymap 记录包含 `unit`、`技能`、`热键`。模块和 keymap 共用以下单位编号:
+
+- `0`:无目标。
+- `1`~`30`:队伍/团队槽位。
+- `31`:玩家。
+- `32`:当前目标。
+- `33`:焦点。
+- `34`:地面。
+- `35`:鼠标指向。
+
+当前映射版本为 `UnitMappingVersion = 2`。旧模块会在加载时迁移此前 `31` 与 `34` 的旧含义。
## 模块系统
-模块用于把“识别到的角色环境”和“要执行的判断逻辑”拆成可编辑 JSON。运行时每轮识别到 `职业`、`专精`、`队伍类型`、`英雄天赋` 后,会在 `module` 文件夹中寻找匹配模块。
+模块以 `模块名.json` 保存在 `module/`。名称不能重复;加载时会递归扫描子目录,以兼容旧版布局。模块页保存时会写入当前 Shigure 版本。
-匹配字段:
+模块匹配字段:
-- `ClassId`:职业 ID,对应状态字段 `职业`。
-- `SpecId`:专精 ID,对应状态字段 `专精`。
-- `PartyType`:队伍类型,对应状态字段 `队伍类型`。模块页使用下拉菜单:`0` 表示单人,`1-40` 表示团队,`46` 表示队伍。
-- `HeroTalent`:英雄天赋,对应状态字段 `英雄天赋`。
-- `RecommendedTalent`:模块作者填写的推荐天赋说明,仅用于展示和保存,不参与模块匹配。
+- `ClassId`:职业 ID。
+- `SpecId`:专精 ID。
+- `PartyType`:`0` 为单人、`1-40` 为团队、`46` 为队伍。
+- `HeroTalent`:英雄天赋 ID。
-字段留空(界面上选择 `任意 (*)`)表示任意值。多个模块同时匹配时,程序会优先使用匹配字段更多的模块(`Specificity` 越大优先级越高),相同时按名称排序。
-旧模块中 `PartyType` 如果保存为数字仍可读取;保存团队模块时会归一化写入 `"1-40"`。
-
-模块以 `模块名.json` 的形式平铺保存在 `module` 文件夹(文件名取自模块名称,因此模块名称不能重复)。加载时会递归扫描 `module` 下的所有子目录,旧版本保存在嵌套子文件夹中的模块文件仍可正常读取。
+任一字段留空(界面显示 `任意 (*)`)即为通配。多个模块同时匹配时,优先选择具体匹配字段更多的模块,相同则按名称排序。`RecommendedTalent` 仅用于说明,不参与匹配。
模块文件示例:
```json
{
- "Id": "戒律-20260612100954681",
+ "Id": "戒律-20260730120000000",
"Name": "戒律",
+ "Author": "模块作者",
"RecommendedTalent": "推荐天赋代码或说明",
+ "Version": "1.2.1",
+ "UnitMappingVersion": 2,
"Enabled": true,
"Match": {
"ClassId": 5,
@@ -160,75 +185,72 @@ module\ UI 可编辑模块
"PartyType": "46",
"HeroTalent": 1
},
+ "Units": [],
+ "Counts": [],
+ "ValueAdjustments": [],
"Rules": [
{
"Enabled": true,
- "Condition": "一键辅助 == 10",
- "Unit": 0,
- "Spell": "一键辅助",
+ "Condition": "生命值 < 50 && spells.真言术:盾 == 0",
+ "Unit": 31,
+ "Spell": "真言术:盾",
"Hotkey": "",
- "Step": ""
+ "Step": "自保"
}
]
}
```
-`Id` 由界面在新建时自动生成(`名称-时间戳`),用于唯一标识模块。
+规则按顺序判断,第一条命中的规则会执行。`Spell` 通过当前职业 keymap 和目标单位查找热键;填写 `Hotkey` 时会直接发送该键。技能下拉还包含以下特殊动作:
-规则按顺序判断,第一条命中的规则会执行。`Spell` 会通过当前职业 keymap 查找按键;如果填写了 `Hotkey`,则直接发送该按键(优先于 `Spell`)。`Unit` 表示技能对应的单位(默认为 `0`,即自身/当前单位)。
+- `暂停`:命中后不发送按键,并停止继续匹配本轮后续规则。
+- `自动插入法术`:读取状态字段 `插入法术`,按职业配置映射到当前可用技能。
+- `一键法术`:读取状态字段 `一键辅助`,按职业配置映射到技能。
-模块页规则表中“技能”和“目标”列为下拉菜单,选项取自当前选中职业的 keymap:技能按名称去重(keymap 中同名技能的多个 `unit` 条目只显示一次)。“目标”下拉跟随该行所选技能联动——只列出该技能在 keymap 中实际配置过的 `unit`(例如治疗技能可能有 `1-30`,而形态/自身技能往往只有 `0`),切换技能后非法的旧目标会被清空。职业选“任意 (*)”时回退读取 `keymap/keymap.json`。旧模块中不在 keymap 里的技能,或自定义技能下的目标值,会保留为额外下拉选项,不会丢失。
+### 条件
-判断条件支持:
+支持的常见表达式:
-- 状态字段:`生命值 < 50`、`战斗时间 > 0`。
-- 技能字段:`spells.圣疗术 == 0`。
-- 队伍字段:`group.1.生命值 < 60`。
-- 组合条件:`生命值 < 50 && spells.圣疗术 == 0`、`目标类型 == 1 || 首领战 > 0`。
-- 布尔简写:直接写字段名表示“为真”,前面加 `!` 表示“为假”,例如 `战斗`、`!移动`。
-- 条件留空表示始终命中。
+- 状态:`生命值 < 50`、`战斗时间 > 0`。
+- 光环:`auras.圣光涌动 > 0`。
+- 技能:`spells.圣疗术 == 0`。
+- 队伍:`group.1.生命值 < 60`。
+- 列表:`目标类型 in (1, 2)`、`职业 not in (4, 9)`。
+- 组合:`生命值 < 50 && spells.圣疗术 == 0`、`目标类型 == 1 || 首领战 > 0`。
+- 布尔简写:`移动` 表示为真,`!移动` 表示为假。
+- 空条件:始终命中。
-字符串字面量可用单引号或双引号包裹;`true/yes/是`、`false/no/否`、`null/nil/空` 会被识别为对应的布尔值或空值。
+字符串可以使用单引号或双引号。`true/yes/是`、`false/no/否`、`null/nil/空` 会转换为对应字面量。`&&` 的优先级高于 `||`,主表达式不支持括号嵌套。
-### 可视化条件编辑器
+规则还可添加多个“子条件”:主条件与子条件组是“且”关系,子条件之间是“或”关系,即 `主条件 && (子条件 1 || 子条件 2)`。这用于表达主编辑器无法用括号表示的逻辑。
-模块页规则表的“条件”列为只读,单击单元格会弹出条件编辑窗口(在新行占位符上点击并保存后会自动追加一条规则)。窗口中每行是一个比较项:
+可视化条件编辑器会按当前职业/专精提供状态、光环、技能和动态单位字段,并支持:
-- 连接:第二行起可选 `且`(`&&`)或 `或`(`||`),决定与上一行的组合方式。
-- 类型/字段:下拉菜单,状态内容来自 `config` 目录并按模块当前选中的职业/专精过滤(内部定位用的 `锚点` 不显示);技能字段以 `技能: 名称` 显示(保存为 `spells.名称`)。原条件中不在目录里的字段(如 `group.1.生命值` 或手写字段)会保留为“自定义”项,不会丢失。
-- Shigure 字段:选择 `Shigure → 延迟 (ms)` 可设置当前规则两次实际发送之间的最小间隔。例如填写 `500`,该规则持续命中时最多每 `0.5` 秒发送一次。延迟按规则独立计时,不参与真假条件判断;填 `0` 或删除该行表示不限制。
-- Shigure 字段:选择 `Shigure → 逻辑延迟 (ms)` 后,规则命中时会先立即发送按键,再暂停整个逻辑循环指定时间;暂停期间不扫描或判断规则,但触发键检测与界面刷新仍然运行。例如填写 `700`,发送后约 `0.7` 秒才恢复下一次逻辑判断。
-- 判断:数字字段提供 `==`、`!=`、`>`、`>=`、`<`、`<=`;布尔和字符串字段只提供 `==`、`!=`。
-- 值:按字段类型自适应——数字字段用数字输入框,布尔字段用 `是/否` 下拉,字符串和自定义字段用文本框。
-- 每行末尾有删除按钮,底部有“添加条件”按钮和生成表达式的实时预览。
+- 数字比较 `==`、`!=`、`>`、`>=`、`<`、`<=`。
+- 文本/数字列表比较 `in`、`not in`。
+- `延迟 (ms)`:限制同一规则两次实际发送的最小间隔。
+- `逻辑延迟 (ms)`:发送后暂停逻辑扫描指定时间;触发键检测和界面刷新仍继续。
-注意:表达式中 `&&` 的优先级高于 `||`(先算“且”再算“或”),编辑器按此语义把行序列分组,不支持括号嵌套。布尔简写(如 `战斗`、`!移动`)在编辑器中会归一化为显式比较(`战斗时间 > 0`、`移动 == false`),含义不变。关系比较(`>`、`<` 等)遇到缺失或非数字的值(如未解析的动态单位字段)时视为“不命中”,不会报错中断。
+缺失或非数字字段参与大小比较时视为不命中,不会中断运行。
-### 动态单位与数量字段
+### 动态单位、数量与动态数值
-模块页规则表上方有“动态单位 / 数量字段”面板。这些是按 `group` 队伍状态**每帧实时解析**的命名对象,逻辑移植自旧 Python 项目 `utils.py`,统一只统计 `职责 != 0` 的单位、`生命值 0` 视为死亡跳过、阈值表示只考虑 `0 < 生命值 < 阈值`。
+这些字段按 `group` 状态每帧重新计算。队伍统计会跳过 `职责 == 0` 或 `生命值 == 0` 的单位。
-- **动态单位**(可作目标,也可在条件引用):解析为一个队伍槽位(`1`~`30`)。选择器类型包括——`生命值最低`、`治疗吸收最高`、`按职责取首/末`、`按职责且不带某光环`、`带某光环(持续最久)`、`带某驱散类型`。`生命值最低` 与 `治疗吸收最高` 都支持固定/动态阈值,以及 `不筛选光环`、`带任一光环`、`不带某光环`、`带某光环`、`某光环值等于` 五种光环筛选;前者筛选 `0 < 生命值 < 阈值`,后者筛选 `治疗吸收 > 阈值`。`治疗吸收最高` 会在满足光环及阈值条件的单位中选择治疗吸收最高者,没有符合条件的单位时该动态单位不存在;其固定阈值默认值为 `0`。参数(阈值、职责、光环、光环值、驱散类型等)按所选类型自适应显示,光环候选来自当前职业/专精的 `group` 字段,并排除 `生命值`、`职责`、`驱散` 这类基础字段。编辑弹窗在第二行显示“名称”,选择 `生命值最低` 时还会显示“值名称”:填了之后,该单位解析槽位的“生命值”会暴露成一个同名数值条件字段——如取名 `最低血量` 后条件可直接写 `最低血量 < 50`(等价于 `单位名.生命值 < 50`);单位未解析时该字段视为缺失(关系比较不命中)。
-- **数量字段**(仅条件可用):解析为一个整数。选择器按数据类型分类显示:`血量 - 低于阈值`、`血量 - 低于阈值不带某光环`、`血量 - 低于阈值带某光环`、`治疗吸收 - 大于阈值`、`治疗吸收 - 大于阈值不带某光环`、`治疗吸收 - 大于阈值带某光环`、`光环 - 带某光环`。治疗吸收类型支持固定/动态阈值,固定阈值默认值为 `0`。
+- 动态单位:可按最低生命值、最高治疗吸收、职责、光环、光环值或驱散类型选择 `1`~`30` 的队伍槽位。可在规则目标中直接使用,也可用裸名称判断是否成功解析。
+- 数量字段:统计低于生命值阈值、高于治疗吸收阈值或带/不带指定光环的单位数,只能用于条件。
+- 条件动态数值:条件成立时对已有状态、光环、技能或动态字段加减一个整数。
+- 公式动态数值:使用字段和算术表达式生成命名数值,支持 `+ - * /`、括号、一元正负号和 `int`、`round`、`floor`、`ceil`、`min`、`max`。
-定义后:
+动态单位和数量阈值既可使用固定值,也可引用其它动态数值。规则引用未解析出的动态目标时会跳过该规则,继续判断下一条。
-- 在规则表“目标”列的下拉里会出现动态单位名,选它即表示“对运行时解析出的那个槽位施法”。运行时若该选择器没选中任何单位(如没人掉血),该规则会被**跳过**,继续判断下一条。
-- 在“条件”里可引用:裸 `单位名`(解析到槽位即为真,可用于存在性判断)、单位的“值名称”(数值,等于该单位的生命值)、以及数量名(如 `低血量人数 >= 3`)。这些会出现在可视化条件编辑器的字段下拉中;`单位名.字段`(如 `最低血量单位.生命值 < 50`)仍可手写,但不会展开进下拉列表。
+模块 JSON 使用 `Units`、`Counts`、`ValueAdjustments` 保存这些定义;规则使用 `UnitName` 引用动态单位。名称不能是纯数字,不能包含 `.` 或 `$`,也不能与已有状态字段或其它动态名称冲突。
-模块 JSON 中以 `Units`、`Counts` 数组保存(单位的值名称存为 `HealthName`,未填则省略),规则用 `UnitName` 字段引用动态单位(与原有数字 `Unit` 互斥),可选 `DelayMs` 保存该行的发送间隔,`LogicDelayMs` 保存发送后的逻辑暂停时长。旧模块没有这些键也能正常加载。名称与值名称不能为纯数字、不能含 `.` 或 `$`,且不能与现有状态/队伍字段或其它单位/数量名重复。
+## 默认逻辑与 C# 扩展
-## 默认逻辑
+如果没有匹配模块,`LogicRegistry` 会尝试使用按职业注册的 `IClassLogic`。当前没有注册项时会进入 `DefaultClassLogic`:仅当 `一键辅助 == 10` 且 keymap 中存在 `一键辅助` 时发送按键,否则显示“C# 职业逻辑尚未迁移”。
-如果没有任何匹配的模块,程序会回退到 `LogicRegistry.cs` 中按职业 ID 注册的 C# 逻辑;若该职业也没有注册逻辑,则使用 `DefaultClassLogic`:当状态字段 `一键辅助 == 10` 且 keymap 中存在 `一键辅助` 时发送对应按键,否则提示“C# 职业逻辑尚未迁移”。
-
-你可以在 `module` 文件夹中放置一个匹配字段全部留空的模块作为通配模块,让它在任何职业下都能命中,从而替代上述默认逻辑。
-
-## 迁移职业逻辑
-
-如果逻辑可以用简单条件表达式描述,优先在 UI 的 `模块` 页维护;如果需要复杂算法,再迁移为 C# 职业逻辑。
-
-新增 C# 职业逻辑时,建议新建一个实现 `IClassLogic` 的类:
+通常应优先在 `模块` 页维护逻辑。确实需要复杂算法时,可实现 `IClassLogic` 并在 `LogicRegistry` 中按职业 ID 注册:
```csharp
public sealed class PriestLogic : IClassLogic
@@ -236,20 +258,12 @@ public sealed class PriestLogic : IClassLogic
public LogicDecision Run(GameState state, string? specName)
{
var health = state.GetInt("生命值");
- return new LogicDecision(null, $"当前生命值: {health}", new Dictionary());
+ return new LogicDecision(
+ Hotkey: null,
+ Step: $"当前生命值: {health}",
+ UnitInfo: new Dictionary());
}
}
```
-然后在 `LogicRegistry` 中按职业 ID 注册:
-
-```csharp
-_logicByClass[5] = new PriestLogic();
-```
-
-逻辑内可以通过 `GameState.GetInt(...)`、`GameState.GetBool(...)`、`state.Spells`、`state.Group` 读取状态,并返回 `LogicDecision`:
-
-- `Hotkey`:需要发送的按键;为空则不发送。
-- `Step`:当前逻辑步骤,会显示到 UI 和日志中。
-- `UnitInfo`:诊断信息,会显示在 `逻辑` 页。
-- `ModuleName`:命中的模块名(C# 逻辑通常留空,由模块逻辑填写)。
+职业逻辑可通过 `GameState.GetInt(...)`、`GameState.GetBool(...)`、`state.Auras`、`state.Spells`、`state.Group` 读取数据,并返回 `LogicDecision`。也可以创建匹配字段全部留空的通配模块,覆盖默认逻辑。
diff --git a/UI/ClassConfigEditorControl.cs b/UI/ClassConfigEditorControl.cs
index 3cb55358..39445041 100644
--- a/UI/ClassConfigEditorControl.cs
+++ b/UI/ClassConfigEditorControl.cs
@@ -4,7 +4,8 @@ using System.Globalization;
namespace Shigure;
///
-/// 图形化编辑 Fuyutsui class/*.lua 的 ClassBlocks(states / auras / spells / group)。
+/// 图形化编辑 Fuyutsui class/*.lua 的 ClassBlocks(states / auras / spells / group),
+/// 并展示同文件中的 spellsList。
///
public sealed class ClassConfigEditorControl : UserControl
{
@@ -23,6 +24,7 @@ public sealed class ClassConfigEditorControl : UserControl
private readonly DataGridView _aurasGrid = new();
private readonly ComboBox _auraBucketBox = new();
private readonly DataGridView _spellsGrid = new();
+ private readonly DataGridView _spellsListGrid = new();
private readonly NumericUpDown _groupNumBox = new();
private readonly NumericUpDown _groupHealthBox = new();
private readonly NumericUpDown _groupRoleBox = new();
@@ -270,14 +272,14 @@ public sealed class ClassConfigEditorControl : UserControl
{
Dock = DockStyle.Fill,
BackColor = UiTheme.Border,
- ColumnCount = 4,
+ ColumnCount = 5,
RowCount = 1,
Margin = new Padding(0),
Padding = new Padding(0)
};
- for (var i = 0; i < 4; i++)
+ for (var i = 0; i < 5; i++)
{
- tabBar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 25));
+ tabBar.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
}
var contentHost = new Panel
@@ -293,7 +295,8 @@ public sealed class ClassConfigEditorControl : UserControl
BuildStatesPage(),
BuildAurasPage(),
BuildSpellsPage(),
- BuildGroupPage()
+ BuildGroupPage(),
+ BuildSpellsListPage()
};
foreach (var page in pages)
{
@@ -302,7 +305,7 @@ public sealed class ClassConfigEditorControl : UserControl
contentHost.Controls.Add(page);
}
- var labels = new Label[4];
+ var labels = new Label[5];
var selectedIndex = -1;
void SelectTab(int index)
{
@@ -326,7 +329,7 @@ public sealed class ClassConfigEditorControl : UserControl
}
}
- var titles = new[] { "状态", "光环", "法术", "队伍" };
+ var titles = new[] { "状态", "光环", "法术", "队伍", "技能列表" };
for (var i = 0; i < titles.Length; i++)
{
var index = i;
@@ -602,6 +605,51 @@ public sealed class ClassConfigEditorControl : UserControl
return panel;
}
+ private Control BuildSpellsListPage()
+ {
+ var panel = new TableLayoutPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 1,
+ RowCount = 2,
+ BackColor = UiTheme.SurfaceRaised
+ };
+ panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
+ panel.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
+
+ var hint = CreateFieldCaption("来自当前职业 Lua 的 Fuyutsui.spellsList,仅显示索引 1–100(只读)。");
+ hint.Padding = new Padding(8, 0, 0, 0);
+ panel.Controls.Add(hint, 0, 0);
+
+ ConfigureGrid(_spellsListGrid);
+ _spellsListGrid.AllowUserToAddRows = false;
+ _spellsListGrid.ReadOnly = true;
+ _spellsListGrid.EditMode = DataGridViewEditMode.EditProgrammatically;
+ _spellsListGrid.Columns.Add(new DataGridViewTextBoxColumn
+ {
+ Name = "SpellId",
+ HeaderText = "法术 ID",
+ Width = 150,
+ SortMode = DataGridViewColumnSortMode.NotSortable
+ });
+ _spellsListGrid.Columns.Add(new DataGridViewTextBoxColumn
+ {
+ Name = "Index",
+ HeaderText = "索引",
+ Width = 120,
+ SortMode = DataGridViewColumnSortMode.NotSortable
+ });
+ _spellsListGrid.Columns.Add(new DataGridViewTextBoxColumn
+ {
+ Name = "Name",
+ HeaderText = "名称",
+ AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill,
+ SortMode = DataGridViewColumnSortMode.NotSortable
+ });
+ panel.Controls.Add(_spellsListGrid, 0, 1);
+ return panel;
+ }
+
private Control BuildGroupPage()
{
var panel = new TableLayoutPanel
@@ -1048,6 +1096,7 @@ public sealed class ClassConfigEditorControl : UserControl
FillAurasGrid();
FillSpellsGrid();
FillGroupEditors();
+ FillSpellsListGrid();
}
finally
{
@@ -1248,6 +1297,23 @@ public sealed class ClassConfigEditorControl : UserControl
}
}
+ private void FillSpellsListGrid()
+ {
+ _spellsListGrid.Rows.Clear();
+ if (_currentDocument is null)
+ {
+ return;
+ }
+
+ foreach (var spell in _currentDocument.SpellsList.Where(spell => spell.Index is >= 1 and <= 100))
+ {
+ _spellsListGrid.Rows.Add(
+ spell.SpellId.ToString(CultureInfo.InvariantCulture),
+ spell.Index.ToString(CultureInfo.InvariantCulture),
+ spell.Name);
+ }
+ }
+
private void FillGroupEditors()
{
_groupAurasGrid.Rows.Clear();
@@ -1592,6 +1658,7 @@ public sealed class ClassConfigEditorControl : UserControl
_statesGrid.Rows.Clear();
_aurasGrid.Rows.Clear();
_spellsGrid.Rows.Clear();
+ _spellsListGrid.Rows.Clear();
_groupAurasGrid.Rows.Clear();
}
diff --git a/UI/ModuleEditorControl.cs b/UI/ModuleEditorControl.cs
index 89c0e3f7..30eb51c5 100644
--- a/UI/ModuleEditorControl.cs
+++ b/UI/ModuleEditorControl.cs
@@ -1,9 +1,13 @@
+using System.Diagnostics;
using System.Drawing;
+using System.Drawing.Drawing2D;
namespace Shigure;
public sealed class ModuleEditorControl : UserControl
{
+ private const string ModuleWebsiteUrl = "https://www.shigure.club";
+
private readonly ModuleStore _moduleStore;
private readonly Action _runtimeRestartRequested;
private readonly string _baseDirectory;
@@ -129,7 +133,7 @@ public sealed class ModuleEditorControl : UserControl
RowCount = 2
};
sidebar.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
- sidebar.RowStyles.Add(new RowStyle(SizeType.Absolute, 42));
+ sidebar.RowStyles.Add(new RowStyle(SizeType.Absolute, 50));
_moduleList.Dock = DockStyle.Fill;
UiTheme.StyleListBox(
@@ -142,32 +146,124 @@ public sealed class ModuleEditorControl : UserControl
_moduleList.SelectedIndexChanged += (_, _) => SelectModule(_moduleList.SelectedIndex);
sidebar.Controls.Add(_moduleList, 0, 0);
- var buttons = new FlowLayoutPanel
+ var buttons = new TableLayoutPanel
{
Dock = DockStyle.Fill,
- FlowDirection = FlowDirection.LeftToRight,
- WrapContents = false,
BackColor = UiTheme.Background,
- Margin = new Padding(0)
+ Margin = new Padding(0),
+ Padding = new Padding(14, 3, 14, 3),
+ ColumnCount = 3,
+ RowCount = 1
};
-
- var addButton = UiTheme.CreateButton("新建", UiTheme.Field, UiTheme.Text);
- addButton.Width = 72;
- addButton.Height = 34;
- addButton.Click += (_, _) => AddModule();
+ buttons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
+ buttons.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 6));
+ buttons.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
+ buttons.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
var reloadButton = UiTheme.CreateButton("刷新", UiTheme.Field, UiTheme.Text);
- reloadButton.Width = 72;
- reloadButton.Height = 34;
+ reloadButton.AutoSize = false;
+ reloadButton.Dock = DockStyle.Fill;
+ reloadButton.Margin = new Padding(0);
reloadButton.Click += (_, _) => LoadModules();
- buttons.Controls.Add(addButton);
- buttons.Controls.Add(reloadButton);
+ var getModulesButton = UiTheme.CreateButton(
+ "获取模块",
+ Color.FromArgb(252, 238, 10),
+ Color.Black);
+ getModulesButton.AutoSize = false;
+ getModulesButton.Dock = DockStyle.Fill;
+ getModulesButton.Margin = new Padding(0);
+ getModulesButton.Padding = new Padding(0, 2, 24, 2);
+ getModulesButton.TextAlign = ContentAlignment.MiddleCenter;
+ getModulesButton.FlatAppearance.BorderColor = Color.FromArgb(252, 238, 10);
+ getModulesButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(255, 244, 64);
+ getModulesButton.FlatAppearance.MouseDownBackColor = Color.FromArgb(220, 207, 8);
+ getModulesButton.Paint += (_, e) => DrawExternalLinkIcon(
+ e.Graphics,
+ getModulesButton.ClientRectangle,
+ getModulesButton.Text,
+ getModulesButton.Font,
+ getModulesButton.ForeColor,
+ getModulesButton.DeviceDpi / 96F);
+ getModulesButton.Click += (_, _) => OpenModuleWebsite();
+
+ buttons.Controls.Add(reloadButton, 0, 0);
+ buttons.Controls.Add(getModulesButton, 2, 0);
sidebar.Controls.Add(buttons, 0, 1);
return sidebar;
}
+ private static void DrawExternalLinkIcon(
+ Graphics graphics,
+ Rectangle clientBounds,
+ string text,
+ Font font,
+ Color color,
+ float scale)
+ {
+ var iconSize = 17F * scale;
+ var iconGap = 6F * scale;
+ var textSize = TextRenderer.MeasureText(
+ graphics,
+ text,
+ font,
+ Size.Empty,
+ TextFormatFlags.NoPadding | TextFormatFlags.SingleLine);
+ var groupWidth = textSize.Width + iconGap + iconSize;
+ var left = clientBounds.Left
+ + ((clientBounds.Width - groupWidth) / 2F)
+ + textSize.Width
+ + iconGap;
+ var top = clientBounds.Top + (clientBounds.Height - iconSize) / 2F;
+
+ var previousSmoothingMode = graphics.SmoothingMode;
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+ using var pen = new Pen(color, 1.8F * scale)
+ {
+ StartCap = LineCap.Round,
+ EndCap = LineCap.Round,
+ LineJoin = LineJoin.Round
+ };
+
+ PointF Point(float x, float y) => new(left + (x * scale), top + (y * scale));
+
+ graphics.DrawLines(
+ pen,
+ [
+ Point(7, 2),
+ Point(4, 2),
+ Point(2, 4),
+ Point(2, 13),
+ Point(4, 15),
+ Point(13, 15),
+ Point(15, 13),
+ Point(15, 9)
+ ]);
+ graphics.DrawLine(pen, Point(8, 9), Point(15, 2));
+ graphics.DrawLines(pen, [Point(10, 2), Point(15, 2), Point(15, 7)]);
+ graphics.SmoothingMode = previousSmoothingMode;
+ }
+
+ private static void OpenModuleWebsite()
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo(ModuleWebsiteUrl)
+ {
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(
+ $"无法打开模块网站: {ex.Message}",
+ "Shigure",
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ }
+ }
+
private Control BuildEditor()
{
var editor = new TableLayoutPanel
@@ -2467,8 +2563,13 @@ public sealed class ModuleEditorControl : UserControl
_deleteButton.Margin = new Padding(8, 0, 0, 0);
_deleteButton.Click += (_, _) => DeleteSelectedModule();
+ var addButton = UiTheme.CreateButton("新建", UiTheme.Field, UiTheme.Text);
+ addButton.Margin = new Padding(8, 0, 0, 0);
+ addButton.Click += (_, _) => AddModule();
+
buttons.Controls.Add(_saveButton);
buttons.Controls.Add(_deleteButton);
+ buttons.Controls.Add(addButton);
row.Controls.Add(hint);
row.Controls.Add(buttons);
return row;
diff --git a/UI/StatusForm.cs b/UI/StatusForm.cs
index cd145ad6..48e59232 100644
--- a/UI/StatusForm.cs
+++ b/UI/StatusForm.cs
@@ -1,10 +1,16 @@
using System.Drawing;
+using System.Drawing.Imaging;
using System.Reflection;
namespace Shigure;
public sealed class StatusForm : Form
{
+ private const string AboutWatermarkResourcePath = "Assets.arasaka-icon-transparent.png";
+ private const int AboutWatermarkSize = 800;
+ private const int AboutWatermarkBottomMargin = 80;
+ private const float AboutWatermarkOpacity = 0.08F;
+
private readonly List<(Button Button, Control View)> _navItems = new();
private RenderSnapshot? _lastSnapshot;
private bool _hasKnownBounds;
@@ -314,26 +320,61 @@ public sealed class StatusForm : Form
private Control BuildAboutPanel()
{
- var panel = new TableLayoutPanel
+ var scrollHost = new WatermarkPanel(
+ GetEmbeddedResourceName(AboutWatermarkResourcePath),
+ AboutWatermarkSize,
+ AboutWatermarkBottomMargin,
+ AboutWatermarkOpacity)
{
Dock = DockStyle.Fill,
BackColor = UiTheme.SurfaceRaised,
+ AutoScroll = true,
+ Margin = new Padding(0)
+ };
+
+ var panel = new TableLayoutPanel
+ {
+ Dock = DockStyle.Top,
+ AutoSize = true,
+ AutoSizeMode = AutoSizeMode.GrowAndShrink,
+ BackColor = Color.Transparent,
ColumnCount = 1,
- RowCount = 2,
+ RowCount = 4,
Padding = new Padding(18)
};
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
- var title = new Label
+ var heading = new TableLayoutPanel
+ {
+ AutoSize = true,
+ Dock = DockStyle.Top,
+ BackColor = Color.Transparent,
+ ColumnCount = 1,
+ RowCount = 2,
+ Margin = new Padding(0, 0, 0, 22)
+ };
+ heading.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ heading.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ heading.Controls.Add(new Label
{
Text = "Shigure",
AutoSize = true,
ForeColor = UiTheme.Text,
Font = new Font(Font.FontFamily, 18F, FontStyle.Bold),
- Margin = new Padding(0, 0, 0, 22)
- };
- panel.Controls.Add(title, 0, 0);
+ Margin = new Padding(0, 0, 0, 4)
+ }, 0, 0);
+ heading.Controls.Add(new Label
+ {
+ Text = "应用信息与 ClassBlocks 可用状态字段参考",
+ AutoSize = true,
+ ForeColor = UiTheme.Muted,
+ Font = new Font(Font.FontFamily, 9.5F, FontStyle.Regular),
+ Margin = new Padding(0)
+ }, 0, 1);
+ panel.Controls.Add(heading, 0, 0);
var assembly = Assembly.GetExecutingAssembly();
var version = AppInfo.Version;
@@ -343,7 +384,7 @@ public sealed class StatusForm : Form
{
AutoSize = true,
Dock = DockStyle.Top,
- BackColor = UiTheme.SurfaceRaised,
+ BackColor = Color.Transparent,
ColumnCount = 2,
RowCount = 0,
Padding = new Padding(0)
@@ -361,7 +402,107 @@ public sealed class StatusForm : Form
AddAboutRow(details, "配置目录", FormatAboutPath(ConfigService.ResolveConfigPath(AppPaths.BaseDirectory)));
panel.Controls.Add(details, 0, 1);
- return panel;
+
+ panel.Controls.Add(new Label
+ {
+ Text = "可用状态字段",
+ AutoSize = true,
+ ForeColor = UiTheme.Text,
+ Font = new Font(Font.FontFamily, 12F, FontStyle.Bold),
+ Margin = new Padding(0, 18, 0, 12)
+ }, 0, 2);
+
+ var fields = new TableLayoutPanel
+ {
+ AutoSize = true,
+ Dock = DockStyle.Top,
+ BackColor = Color.Transparent,
+ ColumnCount = 2,
+ RowCount = 3,
+ Margin = new Padding(0)
+ };
+ fields.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
+ fields.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 50));
+ fields.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ fields.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+ fields.RowStyles.Add(new RowStyle(SizeType.AutoSize));
+
+ fields.Controls.Add(CreateAboutFieldCard(
+ "状态",
+ [
+ "有效性", "战斗时间", "移动", "生命值", "一键辅助", "插入法术",
+ "队伍类型", "队伍人数", "首领战", "难度", "英雄天赋", "施法目标",
+ "施法技能", "敌人人数", "施法", "引导", "蓄力", "蓄力层数",
+ "酒池", "符文", "姿态", "救赎之魂1", "救赎之魂2",
+ ],
+ 150), 0, 0);
+ fields.Controls.Add(CreateAboutFieldCard(
+ "能量",
+ [
+ "法力值", "怒气值", "集中值", "能量值", "符文", "符文能量",
+ "星界能量", "漩涡值", "狂乱值", "恶魔之怒", "痛苦值",
+ "连击点", "神圣能量", "精华能量", "灵魂碎片", "真气"
+ ],
+ 150), 1, 0);
+ fields.Controls.Add(CreateAboutFieldCard(
+ "配置开关",
+ ["爆发开关", "AOE开关", "输出模式", "爆发药水开关", "延迟"],
+ 92), 0, 1);
+ fields.Controls.Add(CreateAboutFieldCard(
+ "物品",
+ ["治疗药水", "魔法药水", "治疗石", "鲁莽药水", "圣光潜力"],
+ 92), 1, 1);
+ fields.Controls.Add(CreateAboutFieldCard(
+ "目标",
+ ["类型", "生命值", "距离", "施法", "施法可打断", "引导", "引导可打断"],
+ 104), 0, 2);
+ fields.Controls.Add(CreateAboutFieldCard(
+ "焦点",
+ ["类型", "生命值", "距离", "施法", "施法可打断", "引导", "引导可打断"],
+ 104), 1, 2);
+
+ panel.Controls.Add(fields, 0, 3);
+ scrollHost.Controls.Add(panel);
+ return scrollHost;
+ }
+
+ private static string GetEmbeddedResourceName(string resourcePath)
+ => $"{typeof(StatusForm).Namespace}.{resourcePath}";
+
+ private Control CreateAboutFieldCard(string title, IReadOnlyList items, int minimumHeight)
+ {
+ var card = new AboutFieldCardPanel
+ {
+ Dock = DockStyle.Fill,
+ ColumnCount = 1,
+ RowCount = 2,
+ Padding = new Padding(16, 14, 16, 14),
+ Margin = new Padding(0, 0, 12, 12),
+ MinimumSize = new Size(0, minimumHeight)
+ };
+ card.RowStyles.Add(new RowStyle(SizeType.Absolute, 30));
+ card.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
+
+ card.Controls.Add(new Label
+ {
+ Text = title,
+ Dock = DockStyle.Fill,
+ ForeColor = UiTheme.Accent,
+ Font = new Font(Font.FontFamily, 10.5F, FontStyle.Bold),
+ TextAlign = ContentAlignment.MiddleLeft,
+ Margin = new Padding(0)
+ }, 0, 0);
+ card.Controls.Add(new Label
+ {
+ Text = string.Join(" · ", items),
+ Dock = DockStyle.Fill,
+ AutoSize = false,
+ ForeColor = UiTheme.Text,
+ Font = new Font(Font.FontFamily, 9.5F, FontStyle.Regular),
+ TextAlign = ContentAlignment.TopLeft,
+ Margin = new Padding(0, 6, 0, 0)
+ }, 0, 1);
+ return card;
}
private static string FormatAboutPath(string path)
@@ -409,6 +550,96 @@ public sealed class StatusForm : Form
}, 1, row);
}
+ private sealed class WatermarkPanel : Panel
+ {
+ private readonly Bitmap? _watermark;
+ private readonly int _watermarkSize;
+ private readonly int _bottomMargin;
+ private readonly float _opacity;
+
+ public WatermarkPanel(string resourceName, int watermarkSize, int bottomMargin, float opacity)
+ {
+ _watermarkSize = watermarkSize;
+ _bottomMargin = bottomMargin;
+ _opacity = Math.Clamp(opacity, 0F, 1F);
+ DoubleBuffered = true;
+ ResizeRedraw = true;
+
+ using var stream = typeof(StatusForm).Assembly.GetManifestResourceStream(resourceName);
+ if (stream is null)
+ {
+ return;
+ }
+
+ using var image = Image.FromStream(stream);
+ _watermark = new Bitmap(image);
+ }
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ base.OnPaint(e);
+ if (_watermark is null)
+ {
+ return;
+ }
+
+ var bounds = new Rectangle(
+ (ClientSize.Width - _watermarkSize) / 2,
+ ClientSize.Height - _watermarkSize - _bottomMargin,
+ _watermarkSize,
+ _watermarkSize);
+
+ using var attributes = new ImageAttributes();
+ var colorMatrix = new ColorMatrix
+ {
+ Matrix33 = _opacity
+ };
+ attributes.SetColorMatrix(
+ colorMatrix,
+ ColorMatrixFlag.Default,
+ ColorAdjustType.Bitmap);
+
+ e.Graphics.DrawImage(
+ _watermark,
+ bounds,
+ 0,
+ 0,
+ _watermark.Width,
+ _watermark.Height,
+ GraphicsUnit.Pixel,
+ attributes);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _watermark?.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+
+ private sealed class AboutFieldCardPanel : TableLayoutPanel
+ {
+ private const int BackgroundOpacity = 166;
+
+ public AboutFieldCardPanel()
+ {
+ SetStyle(ControlStyles.SupportsTransparentBackColor, true);
+ BackColor = Color.Transparent;
+ DoubleBuffered = true;
+ }
+
+ protected override void OnPaintBackground(PaintEventArgs e)
+ {
+ base.OnPaintBackground(e);
+ using var background = new SolidBrush(Color.FromArgb(BackgroundOpacity, UiTheme.Field));
+ e.Graphics.FillRectangle(background, e.ClipRectangle);
+ }
+ }
+
public void ShowOrActivate(RenderSnapshot? snapshot)
{
if (snapshot is not null)