randomp2p/app.js

286 lines
8.9 KiB
JavaScript
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.

(function() {
'use strict';
let mesh = null;
let protocol = null;
let role = null;
let scanningHost = false;
const $ = id => document.getElementById(id);
/* ─── Screen routing ─── */
function show(name) {
['start','host','join','game','result'].forEach(s =>
$(`screen-${s}`).classList.toggle('active', s === name)
);
}
/* ─── Player list ─── */
function renderPlayers(containerId) {
const list = $(containerId);
if (!list) return;
const all = [mesh.myPeerId, ...mesh.getPeerIds()];
const seen = new Set();
list.innerHTML = '';
all.forEach(id => {
if (seen.has(id)) return;
seen.add(id);
const isMe = id === mesh.myPeerId;
const li = document.createElement('li');
li.className = 'player-chip' + (isMe ? ' self' : '');
li.innerHTML = `<span class="dot online"></span>${isMe ? 'Du' : id.slice(0, 8)}`;
list.appendChild(li);
});
}
/* ─── QR ─── */
let qrHost = null;
function showQR(containerId, data) {
const c = $(containerId);
if (!c) return;
c.innerHTML = '';
qrHost = new QRCode(c, { text: data, width: 200, height: 200, correctLevel: QRCode.CorrectLevel.L });
}
/* ─── Camera ─── */
let camStream = null;
let camRaf = null;
async function camStart(videoId) {
await camStop();
camStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 320 }, height: { ideal: 240 } }
});
const v = $(videoId);
v.srcObject = camStream;
v.setAttribute('playsinline', '');
v.classList.add('active');
await v.play();
return v;
}
function camStop() {
if (camRaf) { cancelAnimationFrame(camRaf); camRaf = null; }
if (camStream) { camStream.getTracks().forEach(t => t.stop()); camStream = null; }
}
function camScan(videoId, canvasId, callback) {
const v = $(videoId);
const c = $(canvasId);
const ctx = c.getContext('2d');
camRaf = requestAnimationFrame(function tick() {
if (!v.videoWidth) { camRaf = requestAnimationFrame(tick); return; }
c.width = v.videoWidth; c.height = v.videoHeight;
ctx.drawImage(v, 0, 0);
const img = ctx.getImageData(0, 0, c.width, c.height);
const code = jsQR(img.data, img.width, img.height);
if (code && code.data) {
try {
const p = JSON.parse(code.data);
if (p.v === 1) { callback(p, code.data); return; }
} catch(e) {}
}
camRaf = requestAnimationFrame(tick);
});
}
/* ─── Coin animation ─── */
function flipCoin(resultBit, areaId) {
const area = $(areaId);
if (!area) return;
area.hidden = false;
const coin = area.querySelector('.coin');
coin.classList.remove('flipping','result-kopf','result-zahl');
void coin.offsetWidth;
coin.classList.add('flipping');
setTimeout(() => {
coin.classList.remove('flipping');
coin.classList.add(resultBit === 0 ? 'result-kopf' : 'result-zahl');
}, 1500);
}
/* ─── Protocol phases UI ─── */
function updatePhases(state) {
const map = { IDLE:'ready', STARTING:'ready', COMMITTING:'commit',
WAITING_FOR_COMMITS:'commit', REVEALING:'reveal',
WAITING_FOR_REVEALS:'reveal', VERIFYING:'verify', COMPLETE:'done' };
const active = map[state] || 'ready';
let doneSeen = false;
['ready','commit','reveal','verify','done'].forEach(p => {
const el = $('phase-'+p);
if (!el) return;
el.classList.toggle('active', p === active && !doneSeen);
el.classList.toggle('done', doneSeen);
if (p === active) doneSeen = true;
});
}
/* ─── Game setup ─── */
function setupGame(autostart) {
show('game');
renderPlayers('game-player-list');
$('btn-start-protocol').hidden = autostart || role !== 'host';
$('coin-area').hidden = true;
$('result-area').hidden = true;
$('protocol-text').textContent = autostart ? 'Starte Protokoll...' : 'Bereit zum Münzwurf';
updatePhases('IDLE');
protocol = new CoinFlipProtocol(mesh);
protocol.onstatechange = updatePhases;
protocol.onprogress = (phase, rcvd, total) => {
$('protocol-text').textContent =
phase === 'commit' ? `Commits: ${rcvd}/${total}` : `Reveals: ${rcvd}/${total}`;
};
protocol.oncomplete = result => {
const text = result === 0 ? 'KOPF' : 'ZAHL';
$('protocol-text').textContent = `Ergebnis: ${text}!`;
flipCoin(result, 'coin-area');
$('result-area').hidden = false;
$('result-text').textContent = text;
$('btn-start-protocol').hidden = true;
};
protocol.onerror = err => {
$('protocol-text').textContent = 'Fehler: ' + err;
};
mesh.ondata = (peerId, msg) => {
if (msg.type === 'protocol_start' && protocol && protocol.state === 'IDLE') {
$('btn-start-protocol').hidden = true;
protocol.start();
return;
}
if (msg.type === 'protocol' && protocol) {
protocol.handleMessage(peerId, msg);
}
};
if (autostart) {
protocol.start();
}
}
/* ─── Start screen ─── */
$('btn-create').addEventListener('click', () => {
role = 'host';
mesh = new MeshNet();
mesh.onpeerconnect = pid => {
renderPlayers('host-player-list');
$('btn-start-game').disabled = mesh.getCount() < 1;
};
mesh.onpeerdisconnect = () => {
renderPlayers('host-player-list');
$('btn-start-game').disabled = mesh.getCount() < 1;
};
mesh.onqrupdate = d => showQR('qr-host', d);
mesh.createRoom();
show('host');
renderPlayers('host-player-list');
$('btn-start-game').disabled = true;
});
/* ─── Host: scan answer QR ─── */
$('btn-scan-answer').addEventListener('click', async () => {
if (camStream) { camStop(); return; }
try {
await camStart('host-camera');
$('host-scan-status').textContent = 'Warte auf Antwort-QR...';
scanningHost = true;
camScan('host-camera', 'host-camera-canvas', (parsed, raw) => {
if (!parsed.s || !parsed.id) return;
if (parsed.to && parsed.to !== mesh.myPeerId) return;
camStop();
scanningHost = false;
$('host-scan-status').textContent = 'Verbunden!';
mesh.feedAnswer(raw);
$('btn-scan-answer').textContent = 'Antwort-QR scannen';
});
$('btn-scan-answer').textContent = 'Scannen beenden';
} catch (e) {
$('host-scan-status').textContent = 'Kamera-Fehler';
}
});
/* ─── Join screen ─── */
$('btn-join').addEventListener('click', async () => {
role = 'peer';
mesh = new MeshNet();
show('join');
$('join-answer-section').hidden = true;
$('join-scan-status').textContent = 'QR-Code scannen...';
mesh.onanswerready = answerData => {
$('join-answer-section').hidden = false;
showQR('qr-answer', answerData);
$('join-scan-status').textContent = 'Antwort-QR dem Host zeigen';
camStop();
};
mesh.onpeerconnect = () => {
renderPlayers('join-player-list');
$('join-scan-status').textContent = 'Verbunden warte auf Start...';
};
mesh.ondata = (peerId, msg) => {
if (msg.type === 'protocol_start') {
setupGame(true);
}
};
try {
await camStart('join-camera');
let scanned = false;
camScan('join-camera', 'join-camera-canvas', (parsed, raw) => {
if (scanned) return;
if (!parsed.s || !parsed.id) return;
scanned = true;
$('join-scan-status').textContent = 'Verbinde...';
mesh.joinFromQR(raw);
});
} catch (e) {
$('join-scan-status').textContent = 'Kamera nicht verfügbar';
}
});
/* ─── Host: start game button (in host screen) ─── */
$('btn-start-game').addEventListener('click', () => {
setupGame(false);
});
/* ─── Host: start protocol button (in game screen) ─── */
$('btn-start-protocol').addEventListener('click', async () => {
if (!mesh || !protocol) return;
mesh.broadcast({ type: 'protocol_start' });
$('btn-start-protocol').hidden = true;
await protocol.start();
});
/* ─── Retry / again ─── */
$('btn-retry').addEventListener('click', () => {
if (protocol) protocol.reset();
$('coin-area').hidden = true;
$('result-area').hidden = true;
$('btn-start-protocol').hidden = role !== 'host';
$('protocol-text').textContent = 'Bereit zum Münzwurf';
updatePhases('IDLE');
});
/* ─── Result screen → play again ─── */
$('btn-play-again').addEventListener('click', () => {
if (protocol) protocol.reset();
$('coin-area').hidden = true;
$('result-area').hidden = true;
setupGame(false);
});
/* ─── Back to start ─── */
$('btn-back-start').addEventListener('click', () => {
if (mesh) { mesh.destroy(); mesh = null; }
protocol = null;
camStop();
show('start');
});
})();