feedback5 WAC: players rebind the in-match shortcuts in 设置 → 快捷键 (community request 「快捷键可不可以自己设置」, the owner's decision of 2026-10-07)

- The settings (sp.pref.settings) hold a key map, action → KeyboardEvent.code, for refresh / freeze / level-up /
  retreat / sell / ready (the ready key also pauses a solo battle); the defaults are the 0.1.4 keys R F D Q X Space,
  so nothing changes for a player who never opens the section. sanitizeSettings keeps each saved key that may be a
  shortcut and gives any other its default; not a map, or two actions on one key, gives the defaults.
- One lookup, actionForKey (the character typed first, then the physical key: the 0.1.4 code-or-key match, identical
  for the defaults), serves shortcutFor(e, keys) in game.js and the facing wheel; the modifier, text-field, repeat and
  overlay guards are unchanged. The shop bar, ready / pause and underframe key hints read the map.
- 设置 → 快捷键 (the one SettingsModal, in a match and on the title screen): each action with its key; click or Enter
  waits, the next key binds; Esc cancels (the dialog stays open), Tab leaves; Ctrl / Alt / ⌘ combinations and the
  interface keys (Esc, Tab, Enter, arrows, F1–F12) are refused; a key in use is swapped and the line under the list
  names both; 恢复默认. Esc stays fixed. Touch-only devices: a keyboard note and a folded list; one column under 768 px.
- Tests: test/ui/feedback5-hotkeys.test.js (map, sanitising, conflicts, lookup, capture, handlers and hints with a
  rebound key), test/ui/gameLogic.test.js (sanitizeSettings now returns keys), test/ui/hotkeys.e2e.test.js (SP_E2E=1:
  撤退 rebound to W with the keyboard only in the mock match; phone width).

Golden unchanged (client UI only).
This commit is contained in:
sganggs committed 2026-10-07 01:56:19 +08:00
1 parent 30d9a41c51
commit f2bec76176
14 files changed
+772 -61

No files matched your search

+4 -1
View File
@@ -25,6 +25,8 @@ import { useEffect, useLayoutEffect, useRef, useState } from '../../vendor/hooks
import { html } from './components.js';
import { LocalSprite } from './gameComponents.js';
import { DIRS, DIR_LABEL, DEAD_ZONE_TILES, dirFromDelta, dirFromKey, rangeTiles, normDir, boardDir, viewMirrored } from './facing.js';
import { facingSwallows } from './gameLogic.js';
import { settingsStore } from './settings.js';
import { t } from '../../../shared/i18n.js';
const cx = (...p) => p.flat().filter(Boolean).join(' ');
@@ -242,7 +244,8 @@ export function FacingWheel({ view, row, col, grid, name = '', onPreview, onComm
const k = dirFromKey(e.key);
if (k) { e.preventDefault(); e.stopImmediatePropagation(); setDir(k); return; }
if (e.key === 'Enter' && L.dir) { e.preventDefault(); e.stopImmediatePropagation(); L.onCommit(L.bdir); return; }
if (e.key === ' ' || /^Key[RFD]$/.test(e.code || '')) { e.preventDefault(); e.stopImmediatePropagation(); } // no ready / shop while choosing
// no ready / shop while choosing: Space and every key of the player's map (设置 → 快捷键)
if (facingSwallows(e, settingsStore.get().keys)) { e.preventDefault(); e.stopImmediatePropagation(); }
};
window.addEventListener('keydown', onKey, true);
return () => window.removeEventListener('keydown', onKey, true);