132 lines
5.6 KiB
TypeScript
132 lines
5.6 KiB
TypeScript
import './style.css';
|
|
import './lobby.css';
|
|
import { APP_NAME, LOBBY, type LobbyData } from './constants/lobby.ts';
|
|
|
|
const lobby = document.getElementById('lobby')!;
|
|
const title = document.getElementById('lobby-title')!;
|
|
const status = document.getElementById('lobby-status')!;
|
|
const options = document.getElementById('lobby-options')!;
|
|
const back = document.getElementById('lobby-back') as HTMLButtonElement;
|
|
const retry = document.getElementById('lobby-retry') as HTMLButtonElement;
|
|
const hud = document.querySelector<HTMLElement>('.hud')!;
|
|
hud.inert = true;
|
|
document.title = APP_NAME;
|
|
let channel: LobbyData['channel'];
|
|
let channelRequest: AbortController | undefined;
|
|
let background: { dispose(): void } | undefined;
|
|
let backgroundCancelled = false;
|
|
|
|
function stopBackground() {
|
|
backgroundCancelled = true;
|
|
background?.dispose();
|
|
background = undefined;
|
|
}
|
|
|
|
function message(text: string) { status.textContent = text; status.hidden = !text; }
|
|
function screen(heading = '', text = '') {
|
|
title.textContent = heading;
|
|
title.hidden = !heading;
|
|
message(text);
|
|
options.replaceChildren();
|
|
back.hidden = retry.hidden = true;
|
|
if (heading) title.focus({ preventScroll: true });
|
|
}
|
|
function choice(name: string, detail: string, disabled: boolean, action: () => void) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button'; button.className = 'lobby-choice'; button.disabled = disabled;
|
|
const label = document.createElement('span'); label.textContent = name; button.append(label);
|
|
if (detail) { const description = document.createElement('small'); description.textContent = detail; button.append(description); }
|
|
button.addEventListener('click', action); options.append(button);
|
|
}
|
|
function home(focus = false) {
|
|
channelRequest?.abort();
|
|
screen();
|
|
choice('혼자보기', '', false, () => { void enter(false); });
|
|
choice('같이 보기', '', false, () => { void showChannels(); });
|
|
if (focus) options.querySelector('button')!.focus();
|
|
}
|
|
async function showChannels() {
|
|
channelRequest?.abort();
|
|
const request = new AbortController(); channelRequest = request;
|
|
screen('채널 선택', '불러오는 중…');
|
|
back.hidden = false; back.onclick = () => home(true);
|
|
try {
|
|
const response = await fetch(LOBBY.endpoint, { cache: 'no-store', signal: AbortSignal.any([request.signal, AbortSignal.timeout(LOBBY.timeoutMs)]) });
|
|
if (!response.ok) throw new Error('채널 조회 실패');
|
|
const data = await response.json() as LobbyData;
|
|
if (request.signal.aborted) return;
|
|
channel = data.channel;
|
|
message(channel.available ? '' : '지금은 입장할 수 없습니다.');
|
|
choice(channel.name, `${channel.players} / ${channel.capacity}명`, !channel.available, () => { void enter(true); });
|
|
if (!channel.available) { retry.hidden = false; retry.onclick = () => { void showChannels(); }; }
|
|
} catch {
|
|
if (request.signal.aborted) return;
|
|
message('채널에 연결하지 못했습니다.');
|
|
retry.hidden = false; retry.onclick = () => { void showChannels(); };
|
|
}
|
|
}
|
|
function reveal() {
|
|
lobby.hidden = true;
|
|
document.body.classList.remove('in-lobby');
|
|
hud.inert = false;
|
|
document.querySelector<HTMLButtonElement>('.scene-toolbar button')?.focus();
|
|
}
|
|
async function enter(shared: boolean) {
|
|
stopBackground();
|
|
screen('', '불러오는 중…');
|
|
const url = new URL(location.href);
|
|
url.searchParams.set('site', 'gwanaksan');
|
|
url.searchParams.set('mode', shared ? 'shared' : 'solo');
|
|
url.searchParams.delete('terrain'); url.searchParams.delete('profile');
|
|
history.replaceState(null, '', url);
|
|
let ready = false, connected = !shared, entered = false;
|
|
const finish = () => {
|
|
if (!ready || !connected || entered) return;
|
|
entered = true;
|
|
requestAnimationFrame(() => requestAnimationFrame(reveal));
|
|
};
|
|
const fail = () => {
|
|
message('입장하지 못했습니다.');
|
|
retry.textContent = '다시 시도'; retry.hidden = false;
|
|
retry.onclick = () => location.reload();
|
|
};
|
|
try {
|
|
const scene = await import('./main.ts');
|
|
await scene.start({
|
|
roomId: shared ? channel.id : undefined,
|
|
connection(text, joined) {
|
|
connected = !shared || joined;
|
|
if (entered || !shared) return;
|
|
message(joined ? '불러오는 중…' : text);
|
|
retry.hidden = joined;
|
|
retry.textContent = '다시 입장';
|
|
retry.onclick = () => document.getElementById('retry-connection')!.click();
|
|
finish();
|
|
},
|
|
ready() { ready = true; finish(); },
|
|
error: fail,
|
|
});
|
|
} catch { fail(); }
|
|
}
|
|
|
|
// Explicit solo/comparison and developer links retain direct entry.
|
|
const query = new URLSearchParams(location.search);
|
|
if (query.get('mode') === 'solo' || (import.meta.env.DEV && query.has('profile'))) {
|
|
void import('./main.ts').then(module => module.start({ ready: reveal, error() {
|
|
message('장면을 불러오지 못했습니다.'); retry.hidden = false; retry.onclick = () => location.reload();
|
|
} })).catch(() => { message('장면을 불러오지 못했습니다.'); retry.hidden = false; retry.onclick = () => location.reload(); });
|
|
} else {
|
|
if (query.get('mode') === 'shared') void showChannels();
|
|
else home();
|
|
void import('./scene/lobbySunrise.ts').then(module => {
|
|
if (backgroundCancelled) return;
|
|
const scene = module.createLobbySunrise(document.querySelector<HTMLCanvasElement>('#lobby-sunrise')!);
|
|
background = scene;
|
|
return scene.ready;
|
|
}).catch(error => {
|
|
background?.dispose();
|
|
console.error('시작 화면 지형을 불러오지 못했습니다.', error);
|
|
});
|
|
}
|
|
window.addEventListener('pagehide', event => { if (!event.persisted) stopBackground(); });
|