UNPKG

whackrow

Version:

JS dev kit for game+controls over WebSocket with Vite dev server.

134 lines (115 loc) 4.24 kB
// Minimal game engine API compatible with Whackrow docs // Exports startGame({ canvas, onTimerUpdate, onPerformanceUpdate, onWinner, onPlayerEliminated }) export async function startGame({ canvas, onTimerUpdate, onPerformanceUpdate, onWinner, onPlayerEliminated } = {}) { const gameCanvas = canvas || document.getElementById('gameCanvas'); if (!gameCanvas) throw new Error('startGame: canvas not provided and #gameCanvas not found'); const ctx = gameCanvas.getContext('2d'); const playersById = new Map(); const eliminatedIds = new Set(); let isRunning = true; let frameId = 0; let last = performance.now(); let fps = 0; let framesCount = 0; let bucketMs = 0; const targetFps = 60; const targetFrameMs = 1000 / targetFps; // Load Whackrow logo PNG from images folder const logoImg = new Image(); logoImg.src = new URL('../images/whackrow_logo.PNG', import.meta.url).href; function ensurePlayer(id) { if (!id) id = 'P1'; if (eliminatedIds.has(id)) return null; if (!playersById.has(id)) { playersById.set(id, { id, pos: { x: gameCanvas.width / 2, y: gameCanvas.height / 2 }, joystick: { vector: { x: 0, y: 0 }, distance: 0 }, radius: 16, size: 140 }); } return playersById.get(id); } function setJoystickFor(id, payload) { const rec = ensurePlayer(id); if (!rec) return; const v = payload && payload.vector ? payload.vector : { x: 0, y: 0 }; const d = typeof payload?.distance === 'number' ? payload.distance : Math.max(0, Math.min(1, Math.hypot(v.x || 0, v.y || 0))); rec.joystick = { vector: { x: Number(v.x) || 0, y: Number(v.y) || 0 }, distance: d }; } function reset() { playersById.clear(); eliminatedIds.clear(); } function pause() { isRunning = false; if (frameId) { try { cancelAnimationFrame(frameId); } catch {} frameId = 0; } } function resume() { if (isRunning) return; isRunning = true; if (!frameId) frameId = requestAnimationFrame(loop); } function resize() { const dpr = window.devicePixelRatio || 1; gameCanvas.width = Math.floor(innerWidth * dpr); gameCanvas.height = Math.floor(innerHeight * dpr); gameCanvas.style.width = innerWidth + 'px'; gameCanvas.style.height = innerHeight + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } window.addEventListener('resize', resize); resize(); // Ensure a default player exists so the logo shows immediately ensurePlayer('P1'); function update(dt) { const speed = 220; // px/sec at full stick for (const rec of playersById.values()) { const vx = (rec.joystick?.vector?.x || 0) * speed; const vy = (rec.joystick?.vector?.y || 0) * speed; rec.pos.x += vx * dt; rec.pos.y += vy * dt; rec.pos.x = Math.max(rec.radius, Math.min(innerWidth - rec.radius, rec.pos.x)); rec.pos.y = Math.max(rec.radius, Math.min(innerHeight - rec.radius, rec.pos.y)); } } function render() { ctx.clearRect(0, 0, innerWidth, innerHeight); for (const rec of playersById.values()) { const size = Math.max(48, Math.min(300, rec.size || 140)); const half = size / 2; if (logoImg && logoImg.complete) { try { ctx.drawImage(logoImg, Math.floor(rec.pos.x - half), Math.floor(rec.pos.y - half), size, size); } catch { drawFallback(rec, size); } } else { drawFallback(rec, size); } } } function drawFallback(rec, size) { ctx.fillStyle = '#38bdf8'; ctx.beginPath(); ctx.arc(rec.pos.x, rec.pos.y, Math.max(16, size * 0.35), 0, Math.PI * 2); ctx.fill(); } function loop(now) { const deltaMs = now - last; const dt = Math.min(0.05, deltaMs / 1000); last = now; bucketMs += deltaMs; framesCount++; if (bucketMs >= 1000) { fps = framesCount; framesCount = 0; bucketMs = 0; try { typeof onPerformanceUpdate === 'function' && onPerformanceUpdate({ fps, targetFps }); } catch {} } update(dt); render(); if (isRunning) frameId = requestAnimationFrame(loop); } frameId = requestAnimationFrame(loop); return { ensurePlayer, setJoystickFor, reset, pause, resume }; } export default startGame;