laatste versie
This commit is contained in:
+130
-41
@@ -10,13 +10,15 @@
|
||||
|
||||
/* ---------- state ---------- */
|
||||
let ws = null;
|
||||
const me = { id: null, host: false, name: '', color: '#ff3fa4' };
|
||||
const me = { id: null, host: false, spectator: false, name: '', color: '#ff3fa4' };
|
||||
let phase = 'lobby';
|
||||
let snap = null; // laatste snapshot
|
||||
let track = null; // {obstacles, powerups}
|
||||
const rp = {}; // render-state per speler {rx, rd, ...}
|
||||
const input = { steer: 0 };
|
||||
const input = { steer: 0, x: null };
|
||||
let visSteer = 0; // visuele stuursmoothing (alleen voor rendering)
|
||||
let followTilt = 0; // kanteling van de gevolgde koploper (alleen toeschouwer)
|
||||
let followPrevX = null;
|
||||
let fx = [];
|
||||
let shake = 0;
|
||||
let lastSent = 0;
|
||||
@@ -84,14 +86,17 @@
|
||||
|
||||
$('joinBtn').onclick = () => connect();
|
||||
$('nameInput').addEventListener('keydown', (e) => { if (e.key === 'Enter') connect(); });
|
||||
// Host-URL (?host=<token>): meteen door als toeschouwer, geen aanmeldformulier.
|
||||
if (hostToken()) connect();
|
||||
|
||||
/* ---------- message-handling ---------- */
|
||||
function handle(m) {
|
||||
switch (m.type) {
|
||||
case 'joined':
|
||||
me.id = m.id; me.host = !!m.host;
|
||||
me.id = m.id; me.host = !!m.host; me.spectator = !!m.spectator;
|
||||
me.name = m.self.name; me.color = m.self.color;
|
||||
snap = m.snapshot; track = m.track;
|
||||
document.body.classList.toggle('spectator', me.spectator);
|
||||
applySnapshot(m.snapshot);
|
||||
$('join').classList.add('hidden');
|
||||
$('room').classList.remove('hidden');
|
||||
@@ -130,6 +135,7 @@
|
||||
}
|
||||
if (hard) { rp[p.id].rx = p.x; rp[p.id].rd = p.d; }
|
||||
}
|
||||
if (hard) { input.x = null; input.steer = 0; steerPointer = null; lastTiltX = null; }
|
||||
for (const key in rp) if (!seen.has(key)) delete rp[key];
|
||||
prevPhase = newPhase;
|
||||
phase = newPhase;
|
||||
@@ -160,7 +166,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
visSteer += (input.steer - visSteer) * Math.min(1, dt * 12);
|
||||
visSteer += (tiltTarget() - visSteer) * Math.min(1, dt * 12);
|
||||
|
||||
// toeschouwer: kantel van de gevolgde koploper uit diens laterale beweging
|
||||
if (me.spectator && snap && snap.players.length) {
|
||||
const lead = snap.players.slice().sort((a, b) => a.rank - b.rank)[0];
|
||||
const lr = rp[lead.id];
|
||||
if (lr) {
|
||||
const vx = lr.rx - (followPrevX == null ? lr.rx : followPrevX);
|
||||
followPrevX = lr.rx;
|
||||
followTilt += (clamp(vx * 1.4, -1, 1) - followTilt) * Math.min(1, dt * 10);
|
||||
}
|
||||
}
|
||||
draw(ts);
|
||||
advanceFx(dt);
|
||||
shake = Math.max(0, shake - dt * 40);
|
||||
@@ -174,7 +191,31 @@
|
||||
function draw(t) {
|
||||
const meSnap = snap ? snap.players.find((p) => p.id === me.id) : null;
|
||||
if (!meSnap) {
|
||||
painter.render(ctx, { W, H, t, me: { d: 0 }, trackLength: snap ? snap.trackLength : 23000, track, cars: [], fx: [], speedFactor: 0, shake: 0 });
|
||||
// Toeschouwer (host): camera volgt de koploper (#1) live.
|
||||
const leader = snap && snap.players.length
|
||||
? snap.players.slice().sort((a, b) => a.rank - b.rank)[0] : null;
|
||||
const leadR = leader && rp[leader.id];
|
||||
const camD = leader ? (leadR ? leadR.rd : leader.d) : 0;
|
||||
const cars = [];
|
||||
let leadSpeed = 0;
|
||||
for (const key in rp) {
|
||||
const o = rp[key];
|
||||
const isFollow = !!leader && key === leader.id;
|
||||
if (isFollow) leadSpeed = o.speed;
|
||||
cars.push({
|
||||
x: o.rx, d: o.rd, name: o.name, color: o.color,
|
||||
boost: o.boost, shield: o.shield, slip: o.slip, finished: o.finished,
|
||||
isMe: isFollow,
|
||||
suffix: isFollow ? ' • koploper' : undefined,
|
||||
steer: isFollow ? followTilt : 0,
|
||||
});
|
||||
}
|
||||
painter.render(ctx, {
|
||||
W, H, t, me: { d: camD },
|
||||
trackLength: snap ? snap.trackLength : 23000, track, cars, fx,
|
||||
speedFactor: leadSpeed / 380,
|
||||
shake: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const r = rp[me.id];
|
||||
@@ -192,7 +233,7 @@
|
||||
painter.render(ctx, {
|
||||
W, H, t, me: { d: selfD },
|
||||
trackLength: snap.trackLength, track, cars, fx,
|
||||
speedFactor: (meSnap.speed || 0) / 300,
|
||||
speedFactor: (meSnap.speed || 0) / 380,
|
||||
shake,
|
||||
});
|
||||
}
|
||||
@@ -215,10 +256,11 @@
|
||||
const rem = Math.max(0, Math.ceil((snap.countdownEndsAt - now) / 1000));
|
||||
$('cdNum').textContent = rem;
|
||||
updateBoard(rem);
|
||||
updateKoplopers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (phase === 'race' && meSnap) {
|
||||
if (phase === 'race') {
|
||||
const rem = Math.max(0, snap.raceEndsAt - now);
|
||||
const tot = Math.ceil(rem / 1000);
|
||||
const min = Math.floor(tot / 60), sec = tot % 60;
|
||||
@@ -226,12 +268,15 @@
|
||||
const txt = `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||
if (tEl.textContent !== txt) tEl.textContent = txt;
|
||||
tEl.classList.toggle('danger', tot <= 10);
|
||||
$('hudSpeed').textContent = Math.round((meSnap.speed || 0) * 0.36) + ' km/u';
|
||||
$('hudPos').textContent = '#' + meSnap.rank + ' / ' + snap.players.length;
|
||||
$('hudDist').textContent = meSnap.d + ' m';
|
||||
if (meSnap) {
|
||||
$('hudSpeed').textContent = Math.round((meSnap.speed || 0) * 0.36) + ' km/u';
|
||||
$('hudPos').textContent = '#' + meSnap.rank + ' / ' + snap.players.length;
|
||||
$('hudDist').textContent = meSnap.d + ' m';
|
||||
}
|
||||
}
|
||||
|
||||
updateBoard(0);
|
||||
updateKoplopers();
|
||||
updateLead();
|
||||
}
|
||||
|
||||
@@ -268,6 +313,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- host/toeschouwer: koplopers live ---------- */
|
||||
let lastKopSig = '';
|
||||
function updateKoplopers() {
|
||||
const panel = $('spectate');
|
||||
if (!me.spectator) { panel.classList.add('hidden'); return; }
|
||||
const inRace = phase === 'race';
|
||||
panel.classList.toggle('hidden', !inRace);
|
||||
$('board').classList.toggle('hidden', inRace); // toeschouwer ziet het grote paneel i.p.v. de lijst
|
||||
if (!inRace || !snap) return;
|
||||
const sorted = snap.players.slice().sort((a, b) => a.rank - b.rank);
|
||||
const cnt = sorted.length;
|
||||
const top = sorted.slice(0, 5);
|
||||
const remain = cnt - top.length;
|
||||
const sig = cnt + '|' + top.map((p) => `${p.id}:${p.rank}:${p.d}:${p.finished}`).join(',');
|
||||
if (sig === lastKopSig) return;
|
||||
lastKopSig = sig;
|
||||
$('kopCount').textContent = cnt === 1 ? '1 rijder op de baan' : cnt + ' rijders op de baan';
|
||||
const ol = $('kopList');
|
||||
ol.innerHTML = '';
|
||||
top.forEach((p) => {
|
||||
const li = document.createElement('li');
|
||||
if (p.rank === 1) li.className = 'top';
|
||||
li.innerHTML = `<span class="rk">${p.rank}</span><span class="dot" style="background:${p.color};color:${p.color}"></span><span class="nm">${esc(p.name)}</span><span class="st">${p.finished ? '🏁 ' + fmtTime(p.finishTime) : Math.round(p.d) + ' m'}</span>`;
|
||||
ol.appendChild(li);
|
||||
});
|
||||
if (remain > 0) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'rest';
|
||||
li.textContent = '… en ' + remain + ' anderen';
|
||||
ol.appendChild(li);
|
||||
}
|
||||
$('kopFollow').textContent = sorted.length ? '📷 camera volgt ' + sorted[0].name + ' · #1' : '';
|
||||
}
|
||||
|
||||
let lastLead = '';
|
||||
function updateLead() {
|
||||
if (!snap || phase !== 'race' || snap.players.length === 0) {
|
||||
@@ -319,37 +398,47 @@
|
||||
}
|
||||
|
||||
|
||||
/* ---------- in-/uitvoer ---------- */
|
||||
const touchesLeft = new Map();
|
||||
const touchesRight = new Map();
|
||||
|
||||
canvas.addEventListener('pointerdown', (e) => {
|
||||
canvas.setPointerCapture(e.pointerId);
|
||||
const side = e.clientX < W / 2 ? 'L' : 'R';
|
||||
(side === 'L' ? touchesLeft : touchesRight).set(e.pointerId, true);
|
||||
updateSteerFromTouches();
|
||||
});
|
||||
canvas.addEventListener('pointermove', (e) => {
|
||||
if (!touchesLeft.has(e.pointerId) && !touchesRight.has(e.pointerId)) return;
|
||||
touchesLeft.delete(e.pointerId);
|
||||
touchesRight.delete(e.pointerId);
|
||||
const side = e.clientX < W / 2 ? 'L' : 'R';
|
||||
(side === 'L' ? touchesLeft : touchesRight).set(e.pointerId, true);
|
||||
updateSteerFromTouches();
|
||||
});
|
||||
const endTouch = (e) => { touchesLeft.delete(e.pointerId); touchesRight.delete(e.pointerId); updateSteerFromTouches(); };
|
||||
canvas.addEventListener('pointerup', endTouch);
|
||||
canvas.addEventListener('pointercancel', endTouch);
|
||||
|
||||
function updateSteerFromTouches() {
|
||||
if (touchesRight.size > 0 && touchesLeft.size === 0) input.steer = 1;
|
||||
else if (touchesLeft.size > 0 && touchesRight.size === 0) input.steer = -1;
|
||||
else input.steer = 0;
|
||||
/* ---------- in-/uitvoer: vinger volgen (geen vegen) ---------- */
|
||||
// De auto rijdt meteen naar de horizontale plek waar je vinger op de baan
|
||||
// staat en blijft daar staan als je loslaat. Pijltjestoetsen sturen klassiek.
|
||||
let steerPointer = null; // id van de actieve vinger
|
||||
const roadXFromClient = (clientX) => {
|
||||
const roadW = Math.min(W * 0.88, 460); // zelfde schaal als render.js
|
||||
const roadLeft = (W - roadW) / 2;
|
||||
return clamp((clientX - roadLeft) / roadW * ROAD_W, 30, ROAD_W - 30);
|
||||
};
|
||||
let lastTiltX = null; // vorige vingerpositie, voor de kanteltilt
|
||||
function tiltTarget() {
|
||||
if (input.x != null) {
|
||||
const tilt = lastTiltX == null ? 0 : clamp((input.x - lastTiltX) * 0.06, -1, 1);
|
||||
lastTiltX = input.x;
|
||||
return tilt;
|
||||
}
|
||||
lastTiltX = null;
|
||||
return input.steer;
|
||||
}
|
||||
|
||||
canvas.addEventListener('pointerdown', (e) => {
|
||||
if (steerPointer) return; // één vinger tegelijk
|
||||
steerPointer = e.pointerId;
|
||||
if (canvas.setPointerCapture) canvas.setPointerCapture(e.pointerId);
|
||||
input.x = roadXFromClient(e.clientX);
|
||||
input.steer = 0;
|
||||
});
|
||||
canvas.addEventListener('pointermove', (e) => {
|
||||
if (steerPointer !== e.pointerId) return;
|
||||
input.x = roadXFromClient(e.clientX);
|
||||
});
|
||||
const endFinger = (e) => {
|
||||
if (steerPointer !== e.pointerId) return;
|
||||
steerPointer = null; // laatste plek blijft vastgehouden (input.x)
|
||||
};
|
||||
canvas.addEventListener('pointerup', endFinger);
|
||||
canvas.addEventListener('pointercancel', endFinger);
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') input.steer = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') input.steer = 1;
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A') { input.steer = -1; input.x = null; }
|
||||
if (e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') { input.steer = 1; input.x = null; }
|
||||
if (e.key.toLowerCase() === 's' && me.host && (phase === 'lobby' || phase === 'results')
|
||||
&& document.activeElement !== $('nameInput')) {
|
||||
sendHostStart();
|
||||
@@ -358,10 +447,10 @@
|
||||
window.addEventListener('keyup', (e) => {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'a' || e.key === 'A' ||
|
||||
e.key === 'ArrowRight' || e.key === 'd' || e.key === 'D') {
|
||||
if (touchesLeft.size === 0 && touchesRight.size === 0) input.steer = 0;
|
||||
if (!steerPointer) { input.steer = 0; input.x = null; }
|
||||
}
|
||||
});
|
||||
window.addEventListener('blur', () => { input.steer = 0; });
|
||||
window.addEventListener('blur', () => { input.steer = 0; input.x = null; steerPointer = null; });
|
||||
|
||||
function sendHostStart() {
|
||||
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: 'host_start' }));
|
||||
@@ -370,7 +459,7 @@
|
||||
// input naar server
|
||||
setInterval(() => {
|
||||
if (ws && ws.readyState === 1 && me.id) {
|
||||
ws.send(JSON.stringify({ type: 'input', steer: input.steer }));
|
||||
ws.send(JSON.stringify({ type: 'input', steer: input.steer, x: input.x }));
|
||||
}
|
||||
}, 33);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user