Files
maze32/web/index.html
T

131 lines
9.2 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>maze32 — лобби</title>
<style>
:root { color-scheme: dark; font-family: ui-monospace, "Cascadia Mono", monospace; --ink:#f5edff; --bg:#090711; --panel:#1d1230; --edge:#65488a; --aqua:#42ead2; --p1:#59a9ff; --p2:#ff73bf; --ok:#72e28b; --warn:#ffd75a; --bad:#ff657a; }
* { box-sizing: border-box; }
body { min-height:100vh; margin:0; padding:max(1rem,env(safe-area-inset-top)) max(1rem,env(safe-area-inset-right)) max(1rem,env(safe-area-inset-bottom)) max(1rem,env(safe-area-inset-left)); background:radial-gradient(circle at top,#24113a,var(--bg) 55%); color:var(--ink); }
main { width:min(70rem,100%); margin:auto; }
header { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem; margin-bottom:1rem; }
h1,h2 { margin:.2rem 0; text-transform:uppercase; letter-spacing:.09em; text-shadow:2px 2px #000; }
h1 { color:var(--aqua); }
.status { padding:.45rem .7rem; border:1px solid var(--edge); background:#10091d; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(17rem,1fr)); gap:1rem; }
section { min-height:13rem; padding:1rem; border:2px solid var(--edge); box-shadow:5px 5px 0 #050307; background:var(--panel); }
.player { display:grid; grid-template-columns:2.2rem 1fr auto; gap:.7rem; align-items:center; margin:.75rem 0; padding:.75rem; border:1px solid #493563; background:#130c20; }
.p1 { border-left:5px solid var(--p1); } .p2 { border-left:5px solid var(--p2); }
.lamp { color:var(--bad); } .online .lamp { color:var(--ok); }
label { display:block; margin:.8rem 0 .3rem; }
input,button { min-height:2.8rem; border:2px solid var(--aqua); border-radius:0; background:#0c1520; color:var(--ink); font:inherit; }
input { width:100%; padding:.55rem; }
button { padding:.5rem .8rem; cursor:pointer; box-shadow:3px 3px 0 #020506; }
button:hover:not(:disabled) { background:#123a40; } button:active:not(:disabled) { transform:translate(2px,2px); box-shadow:1px 1px 0 #020506; }
button:disabled { opacity:.45; cursor:not-allowed; }
.actions { display:flex; flex-wrap:wrap; gap:.6rem; margin-top:.7rem; }
#notice { min-height:2.8rem; margin-top:1rem; padding:.7rem; border-left:4px solid var(--warn); background:#17101f; }
#notice.error { border-color:var(--bad); color:#ffc0c8; }
#notice.good { border-color:var(--ok); color:#b5ffc4; }
dialog { width:min(28rem,calc(100% - 2rem)); border:3px solid var(--aqua); background:#0b0712ef; color:var(--ink); text-align:center; }
dialog::backdrop { background:#030205d9; }
#count { margin:1rem; color:var(--warn); font-size:clamp(5rem,30vw,12rem); animation:pulse .65s steps(2,end) infinite; }
.observer-warning { display:none; color:var(--warn); }
body.observer-mode .observer-warning { display:block; }
@media (min-width:1024px) and (min-height:768px) { body.observer-mode .observer-warning { display:none; } }
@keyframes pulse { 50% { transform:scale(1.08); } }
</style>
</head>
<body>
<main>
<header><h1>maze32</h1><span class="status" id="connection">Подключаем провода к воздуху…</span></header>
<div class="grid">
<section>
<h2>Игроки</h2>
<div class="player p1" id="p1"><b>P1</b><span class="name">Свободно</span><span class="lamp">●</span></div>
<div class="player p2" id="p2"><b>P2</b><span class="name">Свободно</span><span class="lamp">●</span></div>
</section>
<section>
<h2>Наблюдатели</h2>
<p><strong id="observers">0</strong> / 4 смотрят очень внимательно.</p>
<p class="observer-warning">Для режима наблюдателя нужен экран не меньше 1024 × 768.</p>
<button id="observe">Смотреть лабиринт</button>
</section>
<section>
<h2>Пульт запуска</h2>
<div id="join">
<label for="name">Позывной игрока</label>
<input id="name" maxlength="32" autocomplete="nickname" placeholder="Например, Байт-Беглец">
<div class="actions"><button id="suggest">Подкинуть имя</button><button id="play">Занять слот</button></div>
</div>
<div class="actions"><button id="start" disabled>Начать 3-2-1</button><a href="/provision"><button>Wi‑Fi</button></a></div>
</section>
</div>
<div id="notice" role="status">Сервер решает всё. Браузер только красиво моргает.</div>
</main>
<dialog id="overlay"><h2 id="overlay-title">Приготовились!</h2><div id="count">3</div><p id="overlay-text">Сервер синхронизирует старт.</p></dialog>
<script>
(() => {
const $ = id => document.getElementById(id);
const state = { socket:null, clientSeq:0, serverSeq:0, session:null, lobby:null, reconnect:null };
const tokenKey = 'maze32.session.v1';
const notice = (text, kind='') => { $('notice').textContent=text; $('notice').className=kind; };
const send = (type,payload={},requestId=type) => {
if (!state.socket || state.socket.readyState !== WebSocket.OPEN) return;
state.socket.send(JSON.stringify({v:1,type,seq:++state.clientSeq,requestId,payload}));
};
const render = () => {
if (!state.lobby) return;
state.lobby.players.forEach(player => {
const row = $('p'+player.slot);
row.querySelector('.name').textContent = player.occupied ? player.name : 'Свободно';
row.classList.toggle('online',player.connected);
});
$('observers').textContent=state.lobby.observers;
$('join').hidden=!!state.session;
$('observe').hidden=!!state.session;
const owner=state.session?.role==='player' && state.session.slot===1;
$('start').disabled=!(owner && state.lobby.ready && state.lobby.phase==='waiting');
document.body.classList.toggle('observer-mode',state.session?.role==='observer');
};
const countdown = () => {
const dialog=$('overlay'); $('overlay-title').textContent='Приготовились!'; $('overlay-text').textContent='Один сервер — один момент старта.';
if (!dialog.open) dialog.showModal();
let value=3; $('count').textContent=value;
const timer=setInterval(() => { value--; if (value>0) $('count').textContent=value; else clearInterval(timer); },1000);
};
const handle = event => {
let message; try { message=JSON.parse(event.data); } catch { notice('Сервер прислал нечитаемый пакет.','error'); return; }
if (message.v!==1 || !Number.isInteger(message.seq) || message.seq<=state.serverSeq) return;
state.serverSeq=message.seq;
if (message.type==='session') {
state.session=message.payload; localStorage.setItem(tokenKey,message.payload.token);
notice(message.payload.role==='player' ? `Слот P${message.payload.slot} закреплён. Токен спрятан в этом браузере.` : 'Вы в зрительном зале. Руками не трогаем!','good');
} else if (message.type==='lobby') { state.lobby=message.payload; render();
} else if (message.type==='name_suggestion') { $('name').value=message.payload.name;
} else if (message.type==='countdown') { countdown();
} else if (message.type==='active') { $('count').textContent='GO'; $('overlay-title').textContent='Старт!'; $('overlay-text').textContent='Лобби активировано. Лабиринт появится в следующем этапе.'; setTimeout(()=>$('overlay').close(),1200);
} else if (message.type==='error') {
if (message.payload.code==='invalid_token') { localStorage.removeItem(tokenKey); state.session=null; render(); }
notice(message.payload.message || 'Команда отклонена сервером.','error');
}
};
const connect = () => {
clearTimeout(state.reconnect); state.clientSeq=0; state.serverSeq=0;
const scheme=location.protocol==='https:'?'wss':'ws'; state.socket=new WebSocket(`${scheme}://${location.host}/ws`);
state.socket.onopen=() => { $('connection').textContent='Связь с ESP: есть'; const token=localStorage.getItem(tokenKey); if(token) send('restore',{token},'refresh'); };
state.socket.onmessage=handle;
state.socket.onclose=() => { $('connection').textContent='Связь потеряна, переподключаемся…'; state.reconnect=setTimeout(connect,1000); };
state.socket.onerror=() => notice('WebSocket споткнулся. Пробуем снова…','error');
};
$('suggest').onclick=()=>send('suggest_name',{},'suggest');
$('play').onclick=()=>send('join',{role:'player',name:$('name').value},'join-player');
$('observe').onclick=()=>send('join',{role:'observer'},'join-observer');
$('start').onclick=()=>send('start',{},'start');
connect();
})();
</script>
</body>
</html>