feat: allow three cumulative selection timeouts per match
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ACTIONS, ACTION_IDS, MATCH_COUNTDOWN_MS } from '../../constants/gameplay.js';
|
||||
import { ACTIONS, ACTION_IDS, MATCH_COUNTDOWN_MS, TIMEOUT_CHANCES } from '../../constants/gameplay.js';
|
||||
import type { ActionId } from '../shared/types.js';
|
||||
import type { LocalMatchState } from '../shared/p2p-types.js';
|
||||
import { appearanceKey, type FighterAppearance } from '../shared/cosmetics.js';
|
||||
@@ -16,7 +16,7 @@ const fighter = (i: number, appearance?: FighterAppearance) => `<div class="figh
|
||||
|
||||
export function createMatchView(container: HTMLElement, onAction: (action: ActionId) => Promise<boolean>, onRematch: () => void, onLeave: () => void, onResult: () => void = () => {}, onRules: () => void = () => {}, appearances?: [FighterAppearance, FighterAppearance], onEmote?: (id: EmoteId) => boolean | Promise<boolean>) {
|
||||
container.innerHTML = `<section class="match-introduction waiting" hidden><div class="queue-fighter" aria-hidden="true"></div><h1 role="status"></h1></section><div class="match-top"><div class="match-title"><span class="mode-label"></span><h1 class="turn-heading"></h1></div><details class="match-menu"><summary aria-label="대전 메뉴">메뉴</summary><div class="match-menu-items"><button class="match-rules quiet">규칙</button><button class="leave quiet">나가기</button></div></details></div>
|
||||
<div class="timing"><span class="selection-status" role="status"></span><span class="time-label" aria-label="남은 시간"></span><div class="time-track" role="progressbar" aria-label="남은 선택 시간" aria-valuemin="0" aria-valuemax="100"><span></span></div></div>
|
||||
<div class="timing"><span class="selection-status" role="status"></span><span class="timeout-chances" aria-label="남은 초읽기 기회"></span><span class="time-label" aria-label="남은 시간"></span><div class="time-track" role="progressbar" aria-label="남은 선택 시간" aria-valuemin="0" aria-valuemax="100"><span></span></div></div>
|
||||
<div class="arena"><div class="arena-floor" aria-hidden="true"></div><div class="result-banner" hidden><h2></h2><p class="finish-reason"></p></div>${fighter(0, appearances?.[0])}<div class="duel-cue"><span class="versus" aria-hidden="true">VS</span><strong class="countdown-number" role="status"></strong></div><span class="clash-label" role="status"></span><div class="clash-burst" aria-hidden="true"></div>${fighter(1, appearances?.[1])}</div>
|
||||
<div class="player-controls" role="group" aria-label="내 행동 선택"><div class="energy">${actionIcon('charge')}<strong class="energy-count"></strong></div><div class="action-buttons">${ACTION_IDS.map(id => `<button data-action="${id}" aria-label="${ACTIONS[id].name}, ${ACTIONS[id].effect}${ACTIONS[id].cost ? `, 기 ${ACTIONS[id].cost} 소모` : ''}"><span class="action-symbol">${actionIcon(id)}</span><strong>${ACTIONS[id].name}</strong><small>${id === 'charge' || id === 'guard' ? ACTIONS[id].effect : `기 ${ACTIONS[id].cost} 소모`}</small><span class="chosen-mark">${checkIcon}</span></button>`).join('')}</div></div>
|
||||
<div class="emote-menu" id="emote-choices" role="group" aria-label="감정표현 선택" hidden>${EMOTE_IDS.map(id => `<button type="button" data-emote="${id}">${EMOTE_LABELS[id]}</button>`).join('')}</div>
|
||||
@@ -28,7 +28,7 @@ export function createMatchView(container: HTMLElement, onAction: (action: Actio
|
||||
function closeEmotes(restoreFocus = false) {
|
||||
emoteOpen = false; emoteMenu.hidden = true;
|
||||
emoteTriggers.forEach(button => button.setAttribute('aria-expanded', 'false'));
|
||||
if (restoreFocus) emoteTriggers.find(button => Number(button.dataset.emoteTrigger) === current?.selfIndex)?.focus();
|
||||
if (restoreFocus) emoteTriggers.find(button => Number(button.dataset.emoteTrigger) === current?.selfIndex)?.focus({ preventScroll: true });
|
||||
}
|
||||
emoteTriggers.forEach(button => {
|
||||
button.onclick = () => {
|
||||
@@ -39,7 +39,7 @@ export function createMatchView(container: HTMLElement, onAction: (action: Actio
|
||||
emoteMenu.style.top = `${Math.max(72, actor.top - root.top - 104)}px`;
|
||||
emoteOpen = true; tick();
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
emoteMenu.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus();
|
||||
emoteMenu.querySelector<HTMLButtonElement>('button:not(:disabled)')?.focus({ preventScroll: true });
|
||||
};
|
||||
});
|
||||
container.onpointerdown = event => {
|
||||
@@ -116,6 +116,7 @@ export function createMatchView(container: HTMLElement, onAction: (action: Actio
|
||||
if (s.phase === 'countdown' && countdown.textContent !== count) { countdown.textContent = count; countdown.dataset.count = count; }
|
||||
el('.duel-cue').hidden = revealing || resultVisible || recovering;
|
||||
el('.selection-status').textContent = !resultVisible && !s.opponentPresent ? '연결 복구 중' : s.phase === 'countdown' ? Number.isFinite(remaining) ? '' : '상대 연결 중' : revealing ? '동시 공개' : resultVisible ? '' : busy ? '선택 중' : s.ownAction ? '선택 완료' : remaining > 0 ? '행동 선택' : '결과 대기';
|
||||
el('.timeout-chances').textContent = `초읽기 ${Math.max(0, TIMEOUT_CHANCES - (s.timeouts?.[s.selfIndex] ?? 0))}/${TIMEOUT_CHANCES}`;
|
||||
el('.time-label').textContent = s.phase === 'selecting' && !revealing && !recovering ? `${(remaining / 1000).toFixed(1)}초` : '';
|
||||
el('.time-track').hidden = recovering;
|
||||
const progress = s.phase === 'countdown' ? remaining / MATCH_COUNTDOWN_MS : s.phase === 'selecting' ? remaining / (s.deadline - s.startsAt) : 0;
|
||||
|
||||
Reference in New Issue
Block a user