From 72e442f2c3355565915aa2355d2a06b84ae5b883 Mon Sep 17 00:00:00 2001 From: cocos02 Date: Tue, 9 Jun 2026 15:19:53 +0800 Subject: [PATCH] feat: UI polish, chapter select improvements, save system enhancements, roadmap update --- FUTURE.md | 39 +++++++++++++++++-- ROADMAP.md | 39 +++++++++++++++---- src/App.vue | 33 +++++++++++++++- src/components/ChapterSelect.vue | 64 ++++++++++++++++++++++++++++++-- src/components/ChoicePanel.vue | 40 ++++++++++++++++++++ src/components/SaveLoadMenu.vue | 2 +- src/stores/gameStore.ts | 7 ++++ 7 files changed, 206 insertions(+), 18 deletions(-) diff --git a/FUTURE.md b/FUTURE.md index fd55fb5..aa240e1 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -29,11 +29,42 @@ - 快捷键自定义 - 2x/4x/8x 多档位 -## P10 键盘/手柄导航 - 扩展 +## P10b 手柄导航 — 远期(Gamepad API) -- Gamepad 震动反馈(手柄扳机键模拟选择"重量") -- 自定义键位映射界面 -- 手柄热插拔检测 +以下功能在 P10a 键盘导航完成后,作为手柄支持的第二期实现: + +### 手柄映射 + +| 操作 | 手柄按键 | +|------|----------| +| 选项导航 | 左摇杆 / 方向键(↑↓←→) | +| 确认 | A 键(Xbox)/ × 键(PS) | +| 取消/菜单 | B 键(Xbox)/ ○ 键(PS) | +| QTE 按键 | A/B/X/Y 自动映射到 QTE 定义的 keys 顺序 | +| 跳过 | Start 键 | +| 倍速 | LB / RB 循环 | + +### QTE 设备感知 + +- 键盘连接时 QTE 提示显示键盘按键名称(空格、WASD) +- 手柄连接时 QTE 提示自动切换为手柄按钮图标(A/B/X/Y + 方向图标) +- `QTESystem` 升级:输入源切换 + 按键提示动态渲染 + +### 视觉反馈 + +- QTE Overlay 的中央圆圈根据设备动态渲染(键盘=方块内字母,手柄=圆形内图标) +- 手柄连接后自动隐藏鼠标光标,`inputMode = 'gamepad'` +- 手柄断连后自动恢复键盘模式,不再依赖手柄 + +### 实现要点 + +- [ ] `engine/systems/InputSystem.ts` 升级 — `gamepadconnected`/`gamepaddisconnected` 事件监听 +- [ ] Gamepad API 轮询(rAF 中 `navigator.getGamepads()` 读取摇杆/按键状态) +- [ ] QTE 手柄映射逻辑:方向键/摇杆 → `keys[]` 中的方向匹配,按钮 → A/B/X/Y +- [ ] `src/components/QTEOverlay.vue` 升级 — 根据 inputMode 渲染不同图标 +- [ ] 键位映射配置持久化(IndexedDB) +- [ ] 手柄震动反馈(`GamepadHapticActuator.pulse()`) +- [ ] 手柄热插拔检测 + 自动切换 inputMode ## P11 多语言字幕 - 扩展 diff --git a/ROADMAP.md b/ROADMAP.md index ee55315..0b55ac1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -532,18 +532,41 @@ GainNode 的 ramp 目标值 = `Math.min(bgmVolume, bgmDuckLevel × bgmVolume)` - [x] `public/scenes/demo.json` — `qte_success` / `qte_fail` 设 `skippable: false` - [x] 验证:TypeScript + Vite build 通过 -### P10 键盘/手柄导航(待实现) +### P10a 键盘导航 — 方向键+确认键驱动全流程 ✅ 已完成 2026-06-09 -目标:支持纯键盘或手柄操作整个游戏流程(选择选项、确认、QTE、菜单),适配"躺沙发"体验。 +目标:支持纯键盘操作整个游戏流程(选项选择、确认、菜单),方向键/WASD 移动高亮、Enter/Space 确认、Esc 菜单。 +适配《底特律》《Telltale》级别的键盘交互体验。 + +**核心设计(对标业界):** + +| 设计点 | 做法 | +|--------|------| +| **输入范围** | 完整接管:选项导航、菜单导航、存档界面、章节选择 | +| **视觉反馈** | 自定义高亮(发光边框/变色),和鼠标 hover 共用样式 | +| **自动检测** | 检测到 keydown → 标记 `inputMode='keyboard'` → 显示焦点环;鼠标移动 → 恢复 `inputMode='mouse'` | +| **QTE** | 本期不做 QTE 键位整合(QTE 仍直接监听 keydown),远期 P10b 处理 | + +**按键映射:** + +| 操作 | 按键 | +|------|------| +| 选项上移 | ↑ / W | +| 选项下移 | ↓ / S | +| 确认 | Enter / Space | +| 菜单 | Esc | +| 跳过 | 不变(按钮点击) | +| 全屏 | 不变(按钮点击) | **实现清单:** -- [ ] `engine/systems/InputSystem.ts` — 统一输入抽象层:键盘(方向键/WASD)+ 手柄(Gamepad API)+ 鼠标 -- [ ] 选项高亮导航:↑↓ 移动焦点,Enter/Space 确认,有视觉高亮指示器 -- [ ] QTE 键位整合到 InputSystem(目前 QTE 直接监听 `keydown`) -- [ ] `src/components/ChoicePanel.vue` — 键盘焦点环样式(`focus-visible`) -- [ ] `src/App.vue` — 菜单键(Esc 打开/关闭菜单) -- [ ] 验证:纯键盘完成一次完整流程(开始→选择→QTE→存档→读档→结束)、手柄连接时自动切换 +- [x] `src/stores/gameStore.ts` — `inputMode` 状态(mouse/keyboard)+ `setInputMode` setter +- [x] `src/App.vue` — 全局 keydown 监听(方向键/Enter/Space/Tab → keyboard 模式,Esc → 关闭菜单/章节);mousemove → mouse 模式 +- [x] `src/components/ChoicePanel.vue` — 选项出现时 auto-focus 第一项;↑↓ 键导航焦点;Enter/Space 确认;`:focus-visible` 发光边框样式 +- [x] `src/components/ChapterSelect.vue` — ←→ 键在章节卡片间导航(跳过锁定章节);Enter 选择;Esc/Backspace 返回;`:focus-visible` 高亮 +- [x] `src/components/SaveLoadMenu.vue` — `@keydown.escape` 关闭菜单 +- [x] 验证:TypeScript + Vite build 通过 + +### P10b 手柄导航(远期 P10b)— 见 FUTURE.md ### P11 多语言字幕(待实现) diff --git a/src/App.vue b/src/App.vue index 28be430..72b3765 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@ diff --git a/src/components/ChapterSelect.vue b/src/components/ChapterSelect.vue index 4a43d7b..7541d91 100644 --- a/src/components/ChapterSelect.vue +++ b/src/components/ChapterSelect.vue @@ -1,7 +1,8 @@ @@ -86,6 +134,7 @@ const emit = defineEmits<{ cursor: pointer; transition: background 0.2s, border-color 0.2s, transform 0.15s; width: 150px; + outline: none; } .chapter-card:hover:not(.locked) { @@ -94,6 +143,13 @@ const emit = defineEmits<{ transform: translateY(-2px); } +.chapter-card:focus-visible { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.4); + box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); + transform: translateY(-2px); +} + .chapter-card.locked { opacity: 0.4; cursor: default; diff --git a/src/components/ChoicePanel.vue b/src/components/ChoicePanel.vue index 3babcc5..2c6bda3 100644 --- a/src/components/ChoicePanel.vue +++ b/src/components/ChoicePanel.vue @@ -1,4 +1,5 @@