Initial commit: ssessesse ki battle
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import type { LocalMatchState } from '../src/shared/p2p-types.js';
|
||||
// Browser-only visual fixture. Uses the real view, rules and timings; never shipped.
|
||||
import { createMatchView } from '../src/client/match-view.js';
|
||||
import { resolveTurn } from '../src/shared/rules.js';
|
||||
import { MATCH_COUNTDOWN_MS, TURN_DURATION_MS, REVEAL_DURATION_MS } from '../constants/gameplay.js';
|
||||
import type { ActionId, Pair, PlayerStats } from '../src/shared/types.js';
|
||||
|
||||
const container = document.querySelector<HTMLElement>('#match')!;
|
||||
const zero = { current: 0, best: 0, wins: 0, losses: 0, draws: 0 };
|
||||
const stats: PlayerStats = { bot: { ...zero }, friend: { ...zero } };
|
||||
let current: LocalMatchState, timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let selfIndex: 0 | 1 = 0;
|
||||
const view = createMatchView(container, async action => {
|
||||
current = { ...current, ownAction: action, version: current.version + 1, clockNow: Date.now() };
|
||||
view.update(current);
|
||||
return true;
|
||||
}, () => prepare(), () => prepare(), () => { document.body.dataset.screen = 'result'; });
|
||||
function state(): LocalMatchState {
|
||||
const now = Date.now();
|
||||
return { matchId: crypto.randomUUID(), mode: 'friend', phase: 'selecting', version: 1, selfIndex, energy: [3, 3], turnId: 1, clockNow: now, startsAt: now, deadline: now + TURN_DURATION_MS, ownAction: null, rematch: [false, false], opponentPresent: true, opponentLeft: false, saveStatus: 'saved', aborted: false };
|
||||
}
|
||||
function show(s: LocalMatchState) { document.body.dataset.screen = 'match'; current = s; view.update(s); }
|
||||
function prepare() {
|
||||
clearTimeout(timer);
|
||||
const s = state(); s.phase = 'countdown'; s.startsAt += MATCH_COUNTDOWN_MS; s.deadline = s.startsAt + TURN_DURATION_MS;
|
||||
show(s);
|
||||
timer = setTimeout(() => show({ ...current, phase: 'selecting', clockNow: Date.now(), version: current.version + 1 }), MATCH_COUNTDOWN_MS);
|
||||
}
|
||||
function reveal(actions: Pair<ActionId>, energy: Pair<number> = [3, 3], elapsed = 0) {
|
||||
clearTimeout(timer);
|
||||
const s = state(), now = Date.now() - elapsed, outcome = resolveTurn(energy, actions);
|
||||
s.energy = outcome.energy; s.phase = outcome.winnerIndex === null ? 'revealing' : 'finished';
|
||||
s.reveal = { ...outcome, matchId: s.matchId, turnId: 1, actions, energyBefore: energy, revealedAt: now, revealEndsAt: now + REVEAL_DURATION_MS };
|
||||
if (outcome.winnerIndex !== null) s.finished = { matchId: s.matchId, winnerIndex: outcome.winnerIndex, reason: 'hit', stats, revealEndsAt: now + REVEAL_DURATION_MS, saved: true };
|
||||
show(s);
|
||||
if (!s.finished) timer = setTimeout(() => show({ ...current, phase: 'selecting', reveal: undefined, turnId: 2, ownAction: null, clockNow: Date.now(), deadline: Date.now() + TURN_DURATION_MS, version: current.version + 1 }), REVEAL_DURATION_MS);
|
||||
}
|
||||
function result(winnerIndex: 0 | 1 | null) {
|
||||
clearTimeout(timer);
|
||||
const s = state(); s.phase = 'finished';
|
||||
s.finished = { matchId: s.matchId, winnerIndex, reason: winnerIndex === null ? 'double_timeout' : 'timeout', stats, revealEndsAt: Date.now(), saved: true };
|
||||
show(s);
|
||||
}
|
||||
const scenarios: [string, () => void][] = [
|
||||
['준비', prepare], ['선택', () => { clearTimeout(timer); show(state()); }],
|
||||
['충전', () => reveal(['charge', 'charge'])], ['파 명중', () => reveal(['blast', 'charge'])],
|
||||
['방어', () => reveal(['guard', 'blast'])], ['상쇄', () => reveal(['blast', 'blast'])],
|
||||
['순간이동', () => reveal(['dodge', 'beam'])], ['에네르기파', () => reveal(['beam', 'guard'])],
|
||||
['왼쪽 피격', () => reveal(['charge', 'blast'])], ['왼쪽 에네르기파 피격', () => reveal(['guard', 'beam'])],
|
||||
['에네르기파 상쇄', () => reveal(['beam', 'beam'])], ['파 회피', () => reveal(['blast', 'dodge'])],
|
||||
['양쪽 막기', () => reveal(['guard', 'guard'])],
|
||||
['피니시 복구', () => reveal(['blast', 'charge'], [3, 3], REVEAL_DURATION_MS + 500)],
|
||||
['컷씬 중간 복구', () => reveal(['beam', 'guard'], [3, 3], 650)],
|
||||
['에네르기파 표정 복구', () => reveal(['beam', 'guard'], [3, 3], 2500)],
|
||||
['에네르기파 소멸 복구', () => reveal(['beam', 'guard'], [3, 3], 3100)],
|
||||
['에네르기파 종료 뒤 복구', () => reveal(['guard', 'beam'], [3, 3], 60_000)],
|
||||
['컷씬 종료 뒤 복구', () => reveal(['charge', 'blast'], [3, 3], 60_000)],
|
||||
['컷씬 저장 응답', () => {
|
||||
reveal(['blast', 'charge']);
|
||||
timer = setTimeout(() => {
|
||||
show({ ...current, version: current.version + 1, clockNow: Date.now() });
|
||||
timer = setTimeout(() => show({ ...current, version: current.version + 1, clockNow: Date.now() }), 4300);
|
||||
}, 700);
|
||||
}],
|
||||
['컷씬 중 새 대전', () => {
|
||||
reveal(['blast', 'charge']);
|
||||
timer = setTimeout(prepare, 900);
|
||||
}],
|
||||
['패배', () => result(selfIndex === 0 ? 1 : 0)], ['승리', () => result(selfIndex)], ['무승부', () => result(null)],
|
||||
['기 부족', () => { clearTimeout(timer); show({ ...state(), energy: [0, 0] }); }],
|
||||
['오른쪽 플레이어', () => { selfIndex = selfIndex === 0 ? 1 : 0; clearTimeout(timer); show(state()); }],
|
||||
];
|
||||
const picker = document.createElement('select'); picker.setAttribute('aria-label', '검증 상태');
|
||||
for (const [label] of scenarios) { const option = document.createElement('option'); option.textContent = label; option.value = label; picker.append(option); }
|
||||
const replay = document.createElement('button'); replay.textContent = '재생';
|
||||
const run = () => scenarios.find(([label]) => label === picker.value)![1]();
|
||||
picker.onchange = run; replay.onclick = run;
|
||||
document.querySelector('#scenarios')!.append(picker, replay);
|
||||
prepare();
|
||||
|
||||
Reference in New Issue
Block a user