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
@@ -30,6 +30,7 @@ import { Img, BondGlyph, CoinGlyph, GIcon, RichText } from './gameComponents.js'
|
||||
import { priceTone, mergeProgress, mergeTarget, shopBlockReason, chessLoadout, offerHeader, briefingBondTip, ownStandIn, standInLoadout, standInLabel, standInTip, standInForText, ownDiyRecord, diyGetter } from './gameLogic.js';
|
||||
import { chessPortraitUrl, itemIconUrl, profIconUrl, uiUrl, skillIconUrl, skillRecordIconUrl, moduleTypeIconUrl } from './assetUrls.js';
|
||||
import { data } from '../data.js';
|
||||
import { hotkeyLabelOf } from './settings.js';
|
||||
import { t } from '../../../shared/i18n.js';
|
||||
|
||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||
@@ -179,7 +180,7 @@ function LevelCard({ shop, reason, armed = false, onTap }) {
|
||||
const max = lv >= (shop?.maxLevel ?? 6);
|
||||
const price = shop?.upgradePrice ?? 0;
|
||||
return html`<button type="button" class=${cx('lvcard', max && 'is-max', reason && 'is-disabled', armed && 'is-armed')} onClick=${() => !reason && onTap()}
|
||||
title=${reason || (armed ? t('再次点击确认升级({price} 资金)', { price }) : t('升级调度中心({price} 资金) · D', { price }))} aria-disabled=${reason ? 'true' : 'false'}
|
||||
title=${reason || (armed ? t('再次点击确认升级({price} 资金)', { price }) : t('升级调度中心({price} 资金) · {key}', { price, key: hotkeyLabelOf('levelUp') }))} aria-disabled=${reason ? 'true' : 'false'}
|
||||
aria-pressed=${String(!!armed)}>
|
||||
${!max ? html`<${HexBadge} value=${price} tone=${reason && reason !== t('调度中心已达最高等级') ? 'dark' : 'gold'} size="md" class="lvcard__price" />` : null}
|
||||
<span class="lvcard__frame">
|
||||
@@ -187,7 +188,7 @@ function LevelCard({ shop, reason, armed = false, onTap }) {
|
||||
<b class="lvcard__num num">${lv}</b>
|
||||
</span>
|
||||
<span class="lvcard__label">${max ? t('已满级') : armed ? t('确认升级') : t('升级')}</span>
|
||||
<kbd class="lvcard__key">D</kbd>
|
||||
<kbd class="lvcard__key">${hotkeyLabelOf('levelUp')}</kbd>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
@@ -323,19 +324,20 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel,
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const hk = { refresh: hotkeyLabelOf('refresh'), freeze: hotkeyLabelOf('freeze') }; // the player's keys (设置 → 快捷键)
|
||||
return html`<section class=${cx('shopbar', frozen && 'is-frozen', !editable && 'is-locked', showReward && 'has-reward', armed && 'has-armed')} ref=${barRef} aria-label=${t('调度中心')}>
|
||||
<div class="shopbar__tools">
|
||||
<span class="shopbar__remain">${t('剩余可放置角色:')}<b class=${cx('num', remaining === 0 && 't-orange')}>${remaining}</b></span>
|
||||
<button type="button" class=${cx('toolbtn', 'toolbtn--ice', frozen && 'is-on')} disabled=${!!frzReason} onClick=${onFreeze}
|
||||
title=${frzReason || (frozen ? t('解冻商店 · F') : t('冻结商店(下回合保留) · F'))}>
|
||||
title=${frzReason || (frozen ? t('解冻商店 · {key}', { key: hk.freeze }) : t('冻结商店(下回合保留) · {key}', { key: hk.freeze }))}>
|
||||
<${Img} src=${uiUrl(data.get('assets'), frozen ? 'shopPanel/frozen_icon2' : 'shopPanel/frozen_icon')} class="toolbtn__img" fallback=${html`<${Icon} name="snow" />`} />
|
||||
<span>${frozen ? t('解冻') : t('冻结')}</span><kbd>F</kbd>
|
||||
<span>${frozen ? t('解冻') : t('冻结')}</span><kbd>${hk.freeze}</kbd>
|
||||
</button>
|
||||
<button type="button" class="toolbtn toolbtn--amber" disabled=${!!refReason} onClick=${onRefresh} title=${refReason || t('刷新商店 · R')}>
|
||||
<button type="button" class="toolbtn toolbtn--amber" disabled=${!!refReason} onClick=${onRefresh} title=${refReason || t('刷新商店 · {key}', { key: hk.refresh })}>
|
||||
<${Img} src=${uiUrl(data.get('assets'), 'shopPanel/refresh_icon')} class="toolbtn__img" fallback=${html`<${Icon} name="refresh" />`} />
|
||||
<span>${t('刷新')}</span>
|
||||
${free > 0 ? html`<span class="toolbtn__free">${t('免费 ×{free}', { free })}</span>` : html`<${HexBadge} value=${shop.refreshPrice ?? 1} tone=${refReason ? 'dark' : 'gold'} size="sm" />`}
|
||||
<kbd>R</kbd>
|
||||
<kbd>${hk.refresh}</kbd>
|
||||
</button>
|
||||
</div>
|
||||
<div class="shopbar__row">
|
||||
|
||||
Reference in new issue
Block a user