Add battle action shortcuts and clarify selection UI
This commit is contained in:
@@ -8,8 +8,10 @@ import { createFinishCutscene } from './finish-cutscene.js';
|
||||
import { matchIntroductionMessage } from '../shared/match-introduction.js';
|
||||
import { EMOTE_COOLDOWN_MS, EMOTE_IDS, EMOTE_LABELS, EMOTE_LINES, type EmoteId } from '../../constants/emotes.js';
|
||||
import { sound } from './sound.js';
|
||||
import { ACTION_SHORTCUTS } from '../../constants/match-controls.js';
|
||||
|
||||
const fighter = (i: number, appearance?: FighterAppearance) => `<div class="fighter" data-fighter="${i}">
|
||||
<div class="fighter-position"></div>
|
||||
<div class="avatar"><div class="emote-bubble" role="status" hidden></div><div class="aura"></div><div class="motion-trails"><i></i><i></i><i></i></div><div class="character-direction">${fighterArt(i, false, appearance)}</div>${ashPileArt()}<div class="projectile"></div><div class="shield"></div><div class="impact"></div><button type="button" class="emote-trigger" data-emote-trigger="${i}" aria-label="내 캐릭터 감정표현 선택" aria-expanded="false" aria-controls="emote-choices" hidden></button></div>
|
||||
<div class="fighter-caption"><span class="choice-status"></span><span class="action-label"></span><span class="energy-change" aria-hidden="true"></span></div>
|
||||
</div>`;
|
||||
@@ -62,6 +64,23 @@ export function createMatchView(container: HTMLElement, onAction: (action: Actio
|
||||
const el = <T extends HTMLElement = HTMLElement>(selector: string) => container.querySelector<T>(selector)!;
|
||||
el('.result-banner h2').tabIndex = -1;
|
||||
const buttons = Array.from(container.querySelectorAll<HTMLButtonElement>('[data-action]'));
|
||||
buttons.forEach(button => {
|
||||
const shortcut = ACTION_SHORTCUTS[button.dataset.action as ActionId];
|
||||
button.setAttribute('aria-keyshortcuts', shortcut);
|
||||
button.insertAdjacentHTML('beforeend', `<kbd class="action-shortcut" aria-hidden="true">${shortcut}</kbd>`);
|
||||
});
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.defaultPrevented || event.repeat || event.ctrlKey || event.altKey || event.metaKey || emoteOpen
|
||||
|| container.hidden || document.querySelector('dialog[open]')) return;
|
||||
if (event.target instanceof HTMLElement && (event.target.isContentEditable || event.target.closest('input, textarea, select'))) return;
|
||||
const button = buttons.find(button => event.code === `Key${ACTION_SHORTCUTS[button.dataset.action as ActionId]}`);
|
||||
if (!button || button.disabled) return;
|
||||
tick();
|
||||
if (button.disabled) return;
|
||||
event.preventDefault();
|
||||
button.click();
|
||||
};
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
buttons.forEach(button => button.addEventListener('click', async () => {
|
||||
if (busy || !current || current.phase !== 'selecting' || current.ownAction || Date.now() + offset >= current.deadline) return;
|
||||
const action = button.dataset.action as ActionId;
|
||||
@@ -191,6 +210,7 @@ export function createMatchView(container: HTMLElement, onAction: (action: Actio
|
||||
appearanceSignatures[i] = signature;
|
||||
}
|
||||
node.classList.toggle('self', i === state.selfIndex);
|
||||
node.querySelector('.fighter-position')!.textContent = i === state.selfIndex ? '▼ 나' : '▼ 상대';
|
||||
node.setAttribute('role', 'group');
|
||||
node.setAttribute('aria-label', i === state.selfIndex ? '내 캐릭터' : state.mode === 'bot' ? '봇 캐릭터' : '상대 캐릭터');
|
||||
});
|
||||
@@ -201,6 +221,6 @@ export function createMatchView(container: HTMLElement, onAction: (action: Actio
|
||||
}
|
||||
cutscene.update(state);
|
||||
tick();
|
||||
}, destroy() { submission++; clearInterval(interval); cutscene.destroy(); renderer.destroy(); sound.stop(); container.onpointerdown = null; container.onkeydown = null; container.innerHTML = ''; current = undefined; },
|
||||
}, destroy() { submission++; clearInterval(interval); document.removeEventListener('keydown', onKeyDown); cutscene.destroy(); renderer.destroy(); sound.stop(); container.onpointerdown = null; container.onkeydown = null; container.innerHTML = ''; current = undefined; },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ body:is([data-screen=match], [data-screen=result]) main { padding: 0; min-height
|
||||
#match {
|
||||
--stage-top: calc(120px + env(safe-area-inset-top));
|
||||
--stage-bottom: 24px;
|
||||
--fighter-size: min(32vw, calc((100cqh - 38px) * .866667), 460px);
|
||||
--fighter-size: min(32vw, calc((100cqh - 62px) * .866667), 460px);
|
||||
--edge: clamp(20px, 3vw, 56px);
|
||||
--self-position: 26.63%;
|
||||
position: relative; isolation: isolate; flex: 1; width: 100%; max-width: none;
|
||||
@@ -47,6 +47,8 @@ body:is([data-screen=match], [data-screen=result]) main { padding: 0; min-height
|
||||
}
|
||||
/* Fighter slots stay fixed so effects and the controls share the same coordinates. */
|
||||
#match .fighter { position: static; width: 43%; }
|
||||
#match .fighter-position { margin-bottom: 8px; color: #59665d; font-size: 13px; line-height: 16px; text-align: center; }
|
||||
#match .fighter.self .fighter-position { color: #242526; font-weight: 700; }
|
||||
#match .avatar, #match[data-phase=finished] .avatar {
|
||||
width: var(--fighter-size);
|
||||
max-width: none; margin: 0; left: 50%; translate: -50% 0;
|
||||
@@ -101,6 +103,10 @@ body:is([data-screen=match], [data-screen=result]) main { padding: 0; min-height
|
||||
grid-row: span 2; align-self: center; width: 28px; height: 28px;
|
||||
}
|
||||
#match .action-buttons strong { font-size: 13px; white-space: nowrap; }
|
||||
#match .action-shortcut { display: none; font: 600 10px/1.2 var(--ui-body, sans-serif); border: 1px solid currentColor; padding: 1px 4px; }
|
||||
@media (orientation: landscape) {
|
||||
#match .action-shortcut { display: inline-block; }
|
||||
}
|
||||
#match .action-buttons small { color: #58604f; font-size: 10px; }
|
||||
#match .action-buttons button:hover:not(:disabled) { background: #fff9e9; border-color: var(--action-color); transform: translateY(-2px); }
|
||||
#match .action-buttons button:active:not(:disabled) { transform: translateY(1px); }
|
||||
@@ -147,7 +153,7 @@ body:is([data-screen=match], [data-screen=result]) #reconnect {
|
||||
--edge: 16px; --stage-bottom: 16px;
|
||||
--self-position: 25.09%;
|
||||
--stage-top: calc(100px + env(safe-area-inset-top));
|
||||
--fighter-size: min(48vw, calc((100cqh - 38px) * .866667), 288px);
|
||||
--fighter-size: min(48vw, calc((100cqh - 62px) * .866667), 288px);
|
||||
}
|
||||
#match[data-self="1"] { --self-position: 74.91%; }
|
||||
#match[data-phase=finished] { --stage-top: calc(210px + env(safe-area-inset-top)); }
|
||||
@@ -187,7 +193,7 @@ body:is([data-screen=match], [data-screen=result]) #reconnect {
|
||||
--edge: max(16px, env(safe-area-inset-left)); --stage-bottom: 8px;
|
||||
--stage-top: calc(78px + env(safe-area-inset-top));
|
||||
--self-position: 28.34%;
|
||||
--fighter-size: min(26vw, calc((100cqh - 38px) * .866667), 260px);
|
||||
--fighter-size: min(26vw, calc((100cqh - 62px) * .866667), 260px);
|
||||
}
|
||||
#match[data-self="1"] { --self-position: 71.66%; }
|
||||
#match[data-phase=finished] { --stage-top: calc(96px + env(safe-area-inset-top)); }
|
||||
|
||||
@@ -55,8 +55,7 @@ body.player-app #match button {
|
||||
}
|
||||
|
||||
body.player-app button.primary,
|
||||
body.player-app #match button.primary,
|
||||
body.player-app #match .action-buttons button[data-action=charge] {
|
||||
body.player-app #match button.primary {
|
||||
border-radius: 0;
|
||||
background: var(--ui-orange);
|
||||
color: var(--ink);
|
||||
@@ -114,9 +113,8 @@ body.player-app #waiting-leave { margin-top: 12px; }
|
||||
|
||||
body.player-app #match .action-buttons { background: transparent; border: 0; border-radius: 0; box-shadow: none; }
|
||||
body.player-app #match .action-buttons button { box-shadow: 3px 3px 0 #24252640; }
|
||||
body.player-app #match .action-buttons :is(button[data-action=charge], button.chosen) { box-shadow: 3px 3px 0 var(--ink); }
|
||||
body.player-app #match .action-buttons button.chosen { box-shadow: 3px 3px 0 var(--ink); }
|
||||
body.player-app #match .action-buttons button:disabled:not(.chosen) { box-shadow: none; }
|
||||
body.player-app #match .action-buttons button[data-action=charge]:disabled:not(.chosen) { background: #e4e4e4; color: #666; }
|
||||
body.player-app #match .action-buttons small { color: inherit; font-family: var(--ui-body); font-weight: 400; }
|
||||
body.player-app #match .action-buttons .action-symbol { color: inherit; opacity: 1; filter: none; }
|
||||
body.player-app #match .energy > svg { color: var(--ink); }
|
||||
|
||||
Reference in New Issue
Block a user