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:
1 parent
30d9a41c51
commit
f2bec76176
14 files changed
+772
-61
No files matched your search
@@ -27,6 +27,7 @@
|
||||
import { html, HexBadge, Icon } from './components.js';
|
||||
import { GIcon } from './gameComponents.js';
|
||||
import { useTileScreen } from './facingWheel.js';
|
||||
import { hotkeyLabelOf } from './settings.js';
|
||||
import { localAsset } from '../data.js';
|
||||
import { GEO } from '../../../shared/constants.js';
|
||||
import { t, tParts } from '../../../shared/i18n.js';
|
||||
@@ -85,6 +86,7 @@ export function Underframe({ view, uid = null, row, col, actions, name = '', bus
|
||||
const s = g.s > 0 ? g.s : 64;
|
||||
const half = s * 1.05;
|
||||
const stop = (e) => e.stopPropagation();
|
||||
const keys = { retreat: hotkeyLabelOf('retreat'), sell: hotkeyLabelOf('sell') }; // the player's keys (设置 → 快捷键)
|
||||
return html`<div class="uframe" data-uid=${uid} style=${`left:${g.x}px;top:${g.y}px;width:${half * 2}px;height:${half * 2}px`} role="group"
|
||||
aria-label=${t('{name} 操作', { name: name || t('单位') })}>
|
||||
<svg class="uframe__dia" viewBox="-110 -110 220 220" aria-hidden="true">
|
||||
@@ -92,13 +94,13 @@ export function Underframe({ view, uid = null, row, col, actions, name = '', bus
|
||||
<path class="uframe__corner" d="M-100 0 L-86 -14 M-100 0 L-86 14 M100 0 L86 -14 M100 0 L86 14 M0 -100 L-14 -86 M0 -100 L14 -86 M0 100 L-14 86 M0 100 L14 86" />
|
||||
</svg>
|
||||
${actions.retreat ? html`<button type="button" class="uframe__btn uframe__btn--retreat" disabled=${busy} onPointerDown=${stop}
|
||||
onClick=${(e) => { stop(e); onRetreat?.(); }} title=${actions.sell != null ? t('撤退至整备区(Q)') : t('撤退至整备区')} aria-label=${t('撤退')} aria-keyshortcuts=${actions.sell != null ? 'Q' : undefined}>
|
||||
<${RetreatGlyph} /><span class="uframe__label">${actions.sell != null ? t('撤退[Q]') : t('撤退')}</span>
|
||||
onClick=${(e) => { stop(e); onRetreat?.(); }} title=${actions.sell != null ? t('撤退至整备区({key})', { key: keys.retreat }) : t('撤退至整备区')} aria-label=${t('撤退')} aria-keyshortcuts=${actions.sell != null ? keys.retreat : undefined}>
|
||||
<${RetreatGlyph} /><span class="uframe__label">${actions.sell != null ? t('撤退[{key}]', { key: keys.retreat }) : t('撤退')}</span>
|
||||
</button>` : null}
|
||||
${actions.sell != null ? html`<button type="button" class="uframe__btn uframe__btn--sell" disabled=${busy} onPointerDown=${stop}
|
||||
onClick=${(e) => { stop(e); onSell?.(); }} title=${t('出售(+{sell} 资金,X)', { sell: actions.sell })} aria-label=${t('出售,获得 {sell} 资金', { sell: actions.sell })} aria-keyshortcuts="X">
|
||||
onClick=${(e) => { stop(e); onSell?.(); }} title=${t('出售(+{sell} 资金,{key})', { sell: actions.sell, key: keys.sell })} aria-label=${t('出售,获得 {sell} 资金', { sell: actions.sell })} aria-keyshortcuts=${keys.sell}>
|
||||
<${PlateIcon} sprite="icon_sell" glyph="sell" tone="sell" />
|
||||
<span class="uframe__label">${t('出售[X]')}</span>
|
||||
<span class="uframe__label">${t('出售[{key}]', { key: keys.sell })}</span>
|
||||
<${HexBadge} value=${`+${actions.sell}`} tone="gold" size="sm" class="uframe__price" />
|
||||
</button>` : null}
|
||||
${actions.destroy ? html`<button type="button" class=${cx('uframe__btn', 'uframe__btn--destroy')} disabled=${busy} onPointerDown=${stop}
|
||||
|
||||
Reference in new issue
Block a user