UI/UX:技能快捷选择弹窗(Speedbar)重构 — 严格按暗黑 2 原版 1.13c 技能树分类(ListRow / Tab)分行组织与向外展开 #402

Closed
opened 2026-09-23 14:13:21 +00:00 by troytt · 1 comment
Owner

1. 问题现状与交互缺陷分析

在当前 Web 端暗黑破坏神 2 客户端中,底部控制条左右技能槽点击弹出的快捷技能选择菜单(Skill Selection Speedbar,位于 src/ui/hotkeys.ts)存在严重的非原版与排版混乱问题:

1.1 机械式取模折行,缺乏语义分组

当前 src/ui/hotkeys.ts 中的 getPopupCells 方法使用硬编码的一维网格取模计算:

const cols = 5
const cellW = 48
const cellH = 48
const base = side === 'left' ? LEFT_SKILL_BOUNDS : RIGHT_SKILL_BOUNDS
return list.map((skill, idx) => {
  const col = idx % cols
  const row = Math.floor(idx / cols)
  const x = side === 'left' ? base.x + col * cellW : base.x - col * cellW
  const y = base.y - (row + 1) * cellH - 6
  return { skill, x, y, w: cellW, h: cellH }
})
  • 跨技能树严重混杂:无论技能属于哪个分类,一律平铺在单一阵列中,每 5 个技能硬性换行。
  • 例如以法师(Sorceress)为例:
    • 第 0 行挤压了基础通用技能(普通攻击、投掷、取消召唤)以及火焰系前 2 个技能(火弹、地狱之火);
    • 第 1 行包含了火焰系后半段技能与闪电系前半段技能;
    • 第 2 行混杂了闪电系与冰霜系技能。
  • 破坏原版肌肉记忆:暗黑破坏神 2 原版老玩家习惯在固定的行区域寻找对应系别的法术(例如法师冰系在一行、电系在一行、火系在一行、基础动作在一行),当前网格式排列造成选技能困难、视觉杂乱。

2. 暗黑破坏神 2 v1.13c 原版 Speedbar 规范(Ground Truth)

根据暗黑破坏神 2 v1.13c 客户端二进制(D2Client.dll 0x6fad7440..0x6fad7536、spellsel.cpp)及官方 MPQ 核心数据表 SkillDesc.txt / Skills.txt:

2.1 行分类标准(ListRow & SkillPage 规范)

SkillDesc.txt 中的 ListRow 字段决定了技能在选择弹窗中的行分类归属:

ListRow 取值 归属类别 对应技能树/系统动作 典型技能示例
0 通用与基础动作 (General Actions) 系统默认基础动作 普通攻击 (Attack 0)、投掷 (Throw 2)、取消召唤 (Unsummon 3)、左手投掷 (Left Hand Throw 4)
1 技能树第一系 (Tab 1) 角色技能树 Page 1 / Tab 0 法师火焰系 (Fire)、圣骑士作战技能 (Combat)、野蛮人战斗技能、死灵法师诅咒、亚马逊标枪
2 技能树第二系 (Tab 2) 角色技能树 Page 2 / Tab 1 法师闪电系 (Lightning)、圣骑士进攻光环 (Offensive Auras)、死灵法师毒骨、亚马逊被动魔法
3 技能树第三系 (Tab 3) 角色技能树 Page 3 / Tab 2 法师冰霜系 (Cold)、圣骑士防御光环 (Defensive Auras)、死灵法师召唤、亚马逊弓弩
4 物品与消耗品技能 (Item / Scrolls) 卷轴/魔法书/装备附带通用技能 回城之书/卷轴 (Townportal 219/220)、辨识之书/卷轴 (Identify 217/218)
-1 禁止显示 (Hidden) 不可在技能选择菜单中列出 被动技能 (Passive=1)、内部逻辑技能、怪物专有技能

2.2 左右技能槽可用性过滤(Left vs Right Slot Filter)

  • 左键弹窗(Left-Click Speedbar):
    • 必须严格遵守 Skills.txt 中 leftskill = 1 且 passive = 0。
    • 光环类技能(aura = 1)以及大部分无目标范围/自增益技能(如传送、静电力场、冰封装甲、能量护盾等)在原版中 leftskill = 0,严禁出现在左键弹窗。
  • 右键弹窗(Right-Click Speedbar):
    • 允许所有非被动的主动技能、光环、召唤、辅助法术及卷轴/物品技能。

2.3 弹窗空间排版与展开流向(Spatial Flow & Alignment)

  1. 多行独立排布(Per-Tree Dedicated Rows):
    • 每一类(通用动作、Tab 1、Tab 2、Tab 3、物品类)独占一条水平行,行与行之间垂直堆叠。
    • 空行紧凑折叠(Compaction):如果玩家未学习且装备未赋予某系别的任何技能,则该系别所对应的行不渲染,上下行紧凑拼接,不留空隙。
  2. 垂直堆叠次序(Vertical Stacking Order,自底向上):
    • 底行(Row 0,紧贴控制栏技能槽):通用基础动作(普通攻击、投掷、取消召唤等)。
    • 中下行(Row 1):技能树第一系技能。
    • 中行(Row 2):技能树第二系技能。
    • 中上行(Row 3):技能树第三系技能。
    • 顶行(Row 4,最上方):物品类/卷轴类动作(如回城、辨识等)。
  3. 水平排列方向(Horizontal Alignment):
    • 左键槽弹窗(Left Slot Speedbar):
      • 基准点:左键槽位 LEFT_SKILL_BOUNDS: (x: 117, y: 551)。
      • 水平向右展开:每行内的技能从左向右排列(x = base.x + col * cellW)。
    • 右键槽弹窗(Right Slot Speedbar):
      • 基准点:右键槽位 RIGHT_SKILL_BOUNDS: (x: 635, y: 551)。
      • 水平向左展开:每行内的技能与右键槽右边缘对齐,向左依次延伸(x = base.x - col * cellW),保证技能菜单始终向屏幕中央展开,符合 D2 原版人体工学与视觉视口防溢出要求。
  4. 单行内排序(Intra-row Ordering):
    • 同一行内部的技能,依据其在技能树中的解锁等级需求(reqlevel)及技能 ID 升序排列(从低级技能到高级技能有序展开),保持与技能树相同的学习阶段认知一致性。

3. 改造方案与实现要求

3.1 数据层(Data Association)

  • 利用 src/data/skills-catalog.ts 中的 SKILLS_BY_ID 元数据:
    • 提取 tabIndex(0, 1, 2 分别映射至 Tab 1, Tab 2, Tab 3)。
    • 通用技能(skillId 0, 2, 3 等)标记为 category: 'general'(ListRow 0)。
    • 卷轴与物品技能(skillId 217~220 等)标记为 category: 'item'(ListRow 4)。
    • 各职业专有技能依据 tabIndex 归组至 category: 'tab0' | 'tab1' | 'tab2'。

3.2 布局计算重构 (getPopupCells)

  • 重构 SkillHotkeysHud.prototype.getPopupCells(side: 'left' | 'right'):
    • 获取当前侧可用的有效技能列表 list。
    • 按分类进行聚类分组(Group by ListRow / Category)。
    • 过滤掉技能数量为 0 的空分类组。
    • 按照标准垂直层级(通用 -> Tab 1 -> Tab 2 -> Tab 3 -> 物品)确定各组的渲染垂直行索引 visualRowIndex。
    • 计算各技能格子的绝对坐标 (x, y):
      • y = base.y - (visualRowIndex + 1) * cellH - 6
      • x:左键向右 base.x + col * cellW;右键向左 base.x - col * cellW。
  • 返回具备完整 { skill, x, y, w: 48, h: 48, category, col, row } 的元组结构。

3.3 交互与命中检测(Hit Testing & Tooltip)

  • 保持鼠标悬停(handleMouseMove)、点击分配(handleMouseDown)、快捷键绑定(F1~F8 triggerFunctionKey)与新的多行网格完全无缝联动。
  • 保证 Tooltip 浮动定位(drawSkillTooltip)在新的多行不规则网格上依然自适应贴边,不遮挡技能图标且不溢出 800×600 画布。

4. 验收标准(Acceptance Criteria)

  1. 技能树分行组织:
    • 点击左键或右键技能槽打开 Speedbar 弹窗时,技能图标不再机械地 5 个一折行,而是严格按技能分类分行呈现。
    • 通用动作(普通攻击/投掷/取消召唤)位于最下行,各系技能树(如法师的火焰系、闪电系、冰霜系)各自独立占一行。
  2. 方向与对齐正确:
    • 左键弹窗自 (117, 551) 向上并向右展开;
    • 右键弹窗自 (635, 551) 向上并向左展开(右侧对齐右技能槽)。
  3. 空系紧凑折叠:
    • 若角色未点亮某系别的任何技能,弹窗不留下空白行,各有效行紧凑堆叠。
  4. 单行内部排序规范:
    • 同一行内技能按 reqlevel 与技能 ID 升序排列(低级技能在靠槽位端,高级技能向外延伸)。
  5. 完整交互与回归通过:
    • 鼠标悬停高亮、Tooltip 弹出、F1~F8 快捷键绑定、点击设为主技能槽等功能完备。
    • 自动化测试(npm run typecheck, npm test)100% 通过,无回归破坏。
## 1. 问题现状与交互缺陷分析 在当前 Web 端暗黑破坏神 2 客户端中,底部控制条左右技能槽点击弹出的快捷技能选择菜单(Skill Selection Speedbar,位于 `src/ui/hotkeys.ts`)存在严重的非原版与排版混乱问题: ### 1.1 机械式取模折行,缺乏语义分组 当前 `src/ui/hotkeys.ts` 中的 `getPopupCells` 方法使用硬编码的一维网格取模计算: ```typescript const cols = 5 const cellW = 48 const cellH = 48 const base = side === 'left' ? LEFT_SKILL_BOUNDS : RIGHT_SKILL_BOUNDS return list.map((skill, idx) => { const col = idx % cols const row = Math.floor(idx / cols) const x = side === 'left' ? base.x + col * cellW : base.x - col * cellW const y = base.y - (row + 1) * cellH - 6 return { skill, x, y, w: cellW, h: cellH } }) ``` - **跨技能树严重混杂**:无论技能属于哪个分类,一律平铺在单一阵列中,每 5 个技能硬性换行。 - 例如以法师(Sorceress)为例: - 第 0 行挤压了基础通用技能(普通攻击、投掷、取消召唤)以及火焰系前 2 个技能(火弹、地狱之火); - 第 1 行包含了火焰系后半段技能与闪电系前半段技能; - 第 2 行混杂了闪电系与冰霜系技能。 - **破坏原版肌肉记忆**:暗黑破坏神 2 原版老玩家习惯在固定的行区域寻找对应系别的法术(例如法师冰系在一行、电系在一行、火系在一行、基础动作在一行),当前网格式排列造成选技能困难、视觉杂乱。 --- ## 2. 暗黑破坏神 2 v1.13c 原版 Speedbar 规范(Ground Truth) 根据暗黑破坏神 2 v1.13c 客户端二进制(`D2Client.dll` `0x6fad7440..0x6fad7536`、`spellsel.cpp`)及官方 MPQ 核心数据表 `SkillDesc.txt` / `Skills.txt`: ### 2.1 行分类标准(ListRow & SkillPage 规范) `SkillDesc.txt` 中的 `ListRow` 字段决定了技能在选择弹窗中的行分类归属: | ListRow 取值 | 归属类别 | 对应技能树/系统动作 | 典型技能示例 | | :--- | :--- | :--- | :--- | | **0** | **通用与基础动作 (General Actions)** | 系统默认基础动作 | 普通攻击 (Attack 0)、投掷 (Throw 2)、取消召唤 (Unsummon 3)、左手投掷 (Left Hand Throw 4) | | **1** | **技能树第一系 (Tab 1)** | 角色技能树 Page 1 / Tab 0 | 法师火焰系 (Fire)、圣骑士作战技能 (Combat)、野蛮人战斗技能、死灵法师诅咒、亚马逊标枪 | | **2** | **技能树第二系 (Tab 2)** | 角色技能树 Page 2 / Tab 1 | 法师闪电系 (Lightning)、圣骑士进攻光环 (Offensive Auras)、死灵法师毒骨、亚马逊被动魔法 | | **3** | **技能树第三系 (Tab 3)** | 角色技能树 Page 3 / Tab 2 | 法师冰霜系 (Cold)、圣骑士防御光环 (Defensive Auras)、死灵法师召唤、亚马逊弓弩 | | **4** | **物品与消耗品技能 (Item / Scrolls)** | 卷轴/魔法书/装备附带通用技能 | 回城之书/卷轴 (Townportal 219/220)、辨识之书/卷轴 (Identify 217/218) | | **-1** | **禁止显示 (Hidden)** | 不可在技能选择菜单中列出 | 被动技能 (Passive=1)、内部逻辑技能、怪物专有技能 | ### 2.2 左右技能槽可用性过滤(Left vs Right Slot Filter) - **左键弹窗(Left-Click Speedbar)**: - 必须严格遵守 `Skills.txt` 中 `leftskill = 1` 且 `passive = 0`。 - 光环类技能(`aura = 1`)以及大部分无目标范围/自增益技能(如传送、静电力场、冰封装甲、能量护盾等)在原版中 `leftskill = 0`,严禁出现在左键弹窗。 - **右键弹窗(Right-Click Speedbar)**: - 允许所有非被动的主动技能、光环、召唤、辅助法术及卷轴/物品技能。 ### 2.3 弹窗空间排版与展开流向(Spatial Flow & Alignment) 1. **多行独立排布(Per-Tree Dedicated Rows)**: - 每一类(通用动作、Tab 1、Tab 2、Tab 3、物品类)独占一条水平行,行与行之间垂直堆叠。 - **空行紧凑折叠(Compaction)**:如果玩家未学习且装备未赋予某系别的任何技能,则该系别所对应的行不渲染,上下行紧凑拼接,不留空隙。 2. **垂直堆叠次序(Vertical Stacking Order,自底向上)**: - **底行(Row 0,紧贴控制栏技能槽)**:通用基础动作(普通攻击、投掷、取消召唤等)。 - **中下行(Row 1)**:技能树第一系技能。 - **中行(Row 2)**:技能树第二系技能。 - **中上行(Row 3)**:技能树第三系技能。 - **顶行(Row 4,最上方)**:物品类/卷轴类动作(如回城、辨识等)。 3. **水平排列方向(Horizontal Alignment)**: - **左键槽弹窗(Left Slot Speedbar)**: - 基准点:左键槽位 `LEFT_SKILL_BOUNDS: (x: 117, y: 551)`。 - 水平向右展开:每行内的技能从左向右排列(`x = base.x + col * cellW`)。 - **右键槽弹窗(Right Slot Speedbar)**: - 基准点:右键槽位 `RIGHT_SKILL_BOUNDS: (x: 635, y: 551)`。 - 水平向左展开:每行内的技能与右键槽右边缘对齐,向左依次延伸(`x = base.x - col * cellW`),保证技能菜单始终向屏幕中央展开,符合 D2 原版人体工学与视觉视口防溢出要求。 4. **单行内排序(Intra-row Ordering)**: - 同一行内部的技能,依据其在技能树中的解锁等级需求(`reqlevel`)及技能 ID 升序排列(从低级技能到高级技能有序展开),保持与技能树相同的学习阶段认知一致性。 --- ## 3. 改造方案与实现要求 ### 3.1 数据层(Data Association) - 利用 `src/data/skills-catalog.ts` 中的 `SKILLS_BY_ID` 元数据: - 提取 `tabIndex`(0, 1, 2 分别映射至 Tab 1, Tab 2, Tab 3)。 - 通用技能(skillId 0, 2, 3 等)标记为 `category: 'general'`(ListRow 0)。 - 卷轴与物品技能(skillId 217~220 等)标记为 `category: 'item'`(ListRow 4)。 - 各职业专有技能依据 `tabIndex` 归组至 `category: 'tab0' | 'tab1' | 'tab2'`。 ### 3.2 布局计算重构 (`getPopupCells`) - 重构 `SkillHotkeysHud.prototype.getPopupCells(side: 'left' | 'right')`: - 获取当前侧可用的有效技能列表 `list`。 - 按分类进行聚类分组(Group by ListRow / Category)。 - 过滤掉技能数量为 0 的空分类组。 - 按照标准垂直层级(通用 -> Tab 1 -> Tab 2 -> Tab 3 -> 物品)确定各组的渲染垂直行索引 `visualRowIndex`。 - 计算各技能格子的绝对坐标 `(x, y)`: - `y = base.y - (visualRowIndex + 1) * cellH - 6` - `x`:左键向右 `base.x + col * cellW`;右键向左 `base.x - col * cellW`。 - 返回具备完整 `{ skill, x, y, w: 48, h: 48, category, col, row }` 的元组结构。 ### 3.3 交互与命中检测(Hit Testing & Tooltip) - 保持鼠标悬停(`handleMouseMove`)、点击分配(`handleMouseDown`)、快捷键绑定(F1~F8 `triggerFunctionKey`)与新的多行网格完全无缝联动。 - 保证 Tooltip 浮动定位(`drawSkillTooltip`)在新的多行不规则网格上依然自适应贴边,不遮挡技能图标且不溢出 800×600 画布。 --- ## 4. 验收标准(Acceptance Criteria) 1. **技能树分行组织**: - 点击左键或右键技能槽打开 Speedbar 弹窗时,技能图标不再机械地 5 个一折行,而是严格按技能分类分行呈现。 - 通用动作(普通攻击/投掷/取消召唤)位于最下行,各系技能树(如法师的火焰系、闪电系、冰霜系)各自独立占一行。 2. **方向与对齐正确**: - 左键弹窗自 (117, 551) 向上并向右展开; - 右键弹窗自 (635, 551) 向上并向左展开(右侧对齐右技能槽)。 3. **空系紧凑折叠**: - 若角色未点亮某系别的任何技能,弹窗不留下空白行,各有效行紧凑堆叠。 4. **单行内部排序规范**: - 同一行内技能按 `reqlevel` 与技能 ID 升序排列(低级技能在靠槽位端,高级技能向外延伸)。 5. **完整交互与回归通过**: - 鼠标悬停高亮、Tooltip 弹出、F1~F8 快捷键绑定、点击设为主技能槽等功能完备。 - 自动化测试(`npm run typecheck`, `npm test`)100% 通过,无回归破坏。
Author
Owner

✅ 修复完成与原版验证报告 (Parity Verification Report)

本 Issue 所述的“快捷技能栏按技能树分行组织”已完成全职业(7 个职业)重构与全量测试验证:

1. 核心改动说明 (src/ui/hotkeys.ts)

  1. 多行独立分类排布:
    • 彻底废除一维写死 cols = 5 取模折行;
    • 严格依据 D2 1.13c SkillDesc.txt 规范划分:
      • Row 0:基础通用动作(普通攻击 Attack 0、投掷 Throw 2、取消召唤 Unsummon 3、左手投掷 4 等);
      • Row 1~3:角色技能树的三系 Tab(Tab 1、Tab 2、Tab 3);
      • Row 4:物品与卷轴技能(回城之书/卷轴、辨识之书/卷轴,以及由装备聚气赋予的充能技能);
    • 空系紧凑折叠 (Compaction):未学习或不可用的系别自动压缩折叠,不留空白占位行。
  2. 左右键槽位原版可用性与展开流向:
    • 左键过滤:严格仅允许 leftskill = 1 且非被动技能进入左键弹窗(共 83 个合法技能),全量排除 26 个被动技能与全部圣骑士光环;
    • 右键过滤:允许所有主动技能、光环(金黄色高亮与边框)、充能技能(右下角数字徽标)与物品动作;
    • 展开几何流向:左键槽自 (117, 551) 向上并向右展开;右键槽自 (635, 551) 向上并向左展开,并受 800×600 画布安全边界防溢出保护。
  3. 行内有序排列:同一行内技能严格依据解锁需求等级(reqlevel)及技能 ID 升序排列。

2. 测试与验证结果

  • 类型检查:npm run typecheck 0 错误;
  • 全职业专项测试:tests/speedbar-authentic-parity.test.ts 新建 31 项深度用例,覆盖全 7 职业分行、空系折叠、左键过滤与几何坐标验证,相关专项测试 77/77 绿灯;
  • 全仓库回归套件:npm test 423 个测试套件、4,508 个用例 100% 通过,0 失败。
  • 独立审计裁决:经 Victory Auditor 独立终审,时序、代码真实性与测试执行全量通过,裁决 VICTORY CONFIRMED。
### ✅ 修复完成与原版验证报告 (Parity Verification Report) 本 Issue 所述的“快捷技能栏按技能树分行组织”已完成全职业(7 个职业)重构与全量测试验证: #### 1. 核心改动说明 (`src/ui/hotkeys.ts`) 1. **多行独立分类排布**: - 彻底废除一维写死 `cols = 5` 取模折行; - 严格依据 D2 1.13c `SkillDesc.txt` 规范划分: - `Row 0`:基础通用动作(普通攻击 Attack 0、投掷 Throw 2、取消召唤 Unsummon 3、左手投掷 4 等); - `Row 1~3`:角色技能树的三系 Tab(Tab 1、Tab 2、Tab 3); - `Row 4`:物品与卷轴技能(回城之书/卷轴、辨识之书/卷轴,以及由装备聚气赋予的充能技能); - **空系紧凑折叠 (Compaction)**:未学习或不可用的系别自动压缩折叠,不留空白占位行。 2. **左右键槽位原版可用性与展开流向**: - **左键过滤**:严格仅允许 `leftskill = 1` 且非被动技能进入左键弹窗(共 83 个合法技能),全量排除 26 个被动技能与全部圣骑士光环; - **右键过滤**:允许所有主动技能、光环(金黄色高亮与边框)、充能技能(右下角数字徽标)与物品动作; - **展开几何流向**:左键槽自 `(117, 551)` 向上并向右展开;右键槽自 `(635, 551)` 向上并向左展开,并受 800×600 画布安全边界防溢出保护。 3. **行内有序排列**:同一行内技能严格依据解锁需求等级(`reqlevel`)及技能 ID 升序排列。 #### 2. 测试与验证结果 - **类型检查**:`npm run typecheck` 0 错误; - **全职业专项测试**:`tests/speedbar-authentic-parity.test.ts` 新建 31 项深度用例,覆盖全 7 职业分行、空系折叠、左键过滤与几何坐标验证,相关专项测试 77/77 绿灯; - **全仓库回归套件**:`npm test` 423 个测试套件、4,508 个用例 100% 通过,0 失败。 - **独立审计裁决**:经 Victory Auditor 独立终审,时序、代码真实性与测试执行全量通过,裁决 **VICTORY CONFIRMED**。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#402
No description provided.