whackrow
Version:
JS dev kit for game+controls over WebSocket with Vite dev server.
142 lines (132 loc) • 7.94 kB
JavaScript
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
const cwd = process.cwd();
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkgRoot = path.resolve(__dirname, '..');
function ensureDir(p) { if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true }); }
function writeIfMissing(file, content) {
if (!fs.existsSync(file)) fs.writeFileSync(file, content, 'utf8');
}
const demoDir = path.join(cwd, 'demo');
ensureDir(demoDir);
// Generate root-level main.js from package template if missing
try {
const srcPath = path.join(pkgRoot, 'src', 'startGame.js');
const dstPath = path.join(cwd, 'main.js');
if (!fs.existsSync(dstPath) && fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, dstPath);
}
} catch {}
// Generate root-level ControlsClient.js from package template if missing
try {
const srcPath = path.join(pkgRoot, 'src', 'ControlsClient.js');
const dstPath = path.join(cwd, 'ControlsClient.js');
if (!fs.existsSync(dstPath) && fs.existsSync(srcPath)) {
fs.copyFileSync(srcPath, dstPath);
}
} catch {}
writeIfMissing(path.join(demoDir, 'game.html'), `<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Game</title>
<style>html,body{margin:0;height:100%}canvas{display:block;width:100vw;height:100vh;background:#0b0f1a}</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script type="module">
import { startGame } from '../main.js';
const wsUrl = (location.search.match(/ws=([^&]+)/) || [])[1] ? decodeURIComponent((location.search.match(/ws=([^&]+)/)||[])[1]) : 'ws://'+location.hostname+':3030';
const ws = new WebSocket(wsUrl);
try{ws.binaryType='blob'}catch{}
ws.addEventListener('open',()=>{try{console.log('[whackrow][game] WS open',wsUrl)}catch{}});
ws.addEventListener('error',e=>{try{console.error('[whackrow][game] WS error',e)}catch{}});
ws.addEventListener('close',e=>{try{console.warn('[whackrow][game] WS close',e.code,e.reason)}catch{}});
const c = document.getElementById('gameCanvas');
const api = await startGame({ canvas: c });
ws.addEventListener('message',async e=>{try{try{console.log('[whackrow][game] WS message raw',e.data)}catch{}const raw=e.data;const txt=(typeof raw==='string')?raw:(raw&&raw.text?await raw.text():'');const m=JSON.parse(txt);try{console.log('[whackrow][game] WS message parsed',m)}catch{}if(m&&m.type==='player_connected'){const id=m.playerId||'P1';api.ensurePlayer(id);toast('Player connected: '+id);return}if(m&&m.type==='move'&&m.vector){const pid=m.playerId||'P1';api.ensurePlayer(pid);api.setJoystickFor(pid,m);}}catch(err){try{console.error('[whackrow][game] parse error',err)}catch{}}});
function toast(t){const el=document.createElement('div');el.textContent=t;el.style.position='fixed';el.style.left='50%';el.style.top='20px';el.style.transform='translateX(-50%)';el.style.background='rgba(0,0,0,0.7)';el.style.color='#fff';el.style.padding='8px 14px';el.style.borderRadius='10px';el.style.border='1px solid rgba(255,255,255,0.2)';el.style.zIndex='10000';document.body.appendChild(el);setTimeout(()=>{try{document.body.removeChild(el)}catch{}},1800)}
// QR to Controls on LAN origin; ensure WS uses LAN host for phones
const lanOrigin = (typeof __WHACKROW_DEV_LAN_ORIGIN__ !== 'undefined' && __WHACKROW_DEV_LAN_ORIGIN__) ? __WHACKROW_DEV_LAN_ORIGIN__ : location.origin;
const lanHost = (() => { try { return new URL(lanOrigin).hostname } catch { return location.hostname } })();
const wsForQr = 'ws://' + lanHost + ':3030';
const controlsUrl = lanOrigin + '/controls.html?ws=' + encodeURIComponent(wsForQr);
const box = document.createElement('div');
box.style.position = 'fixed'; box.style.right = '10px'; box.style.bottom = '10px'; box.style.zIndex = '9999';
box.style.background = 'rgba(0,0,0,0.5)'; box.style.border = '1px solid rgba(255,255,255,0.25)'; box.style.borderRadius = '10px'; box.style.padding = '8px';
const q = document.createElement('canvas'); q.width = 180; q.height = 180; box.appendChild(q);
const label = document.createElement('div'); label.style.color = '#fff'; label.style.font = '12px/1.2 system-ui'; label.style.textAlign = 'center'; label.style.marginTop = '6px';
label.textContent = 'Scan for Controls'; box.appendChild(label);
document.body.appendChild(box);
try {
const mod = await import('qrcode')
const QRCode = mod && (mod.default || mod)
if (QRCode && QRCode.toCanvas) QRCode.toCanvas(q, controlsUrl, { margin: 1, width: 180 })
} catch {}
</script>
</body>
</html>`);
writeIfMissing(path.join(demoDir, 'controls.html'), `
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Controls</title>
<style>html,body{margin:0;height:100%;background:#0b0f1a;color:#fff}.w{display:grid;place-items:center;height:100%}#joystick{position:relative;width:220px;height:220px;border-radius:50%;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.15)}</style>
</head>
<body>
<div class="w"><div id="joystick"></div></div>
<script type="module">
import { ControlsClient } from '../ControlsClient.js';
const wsUrl = (location.search.match(/ws=([^&]+)/) || [])[1] ? decodeURIComponent((location.search.match(/ws=([^&]+)/)||[])[1]) : 'ws://'+location.hostname+':3030';
const playerId = (location.search.match(/player=([^&]+)/) || [])[1];
const c = new ControlsClient({ url: wsUrl, playerId });
c.connect();
c.attachVirtualJoystick(document.getElementById('joystick'));
</script>
</body>
</html>`);
writeIfMissing(path.join(demoDir, 'index.html'), `
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Whackrow Demo</title>
<style>html,body{margin:0;height:100%;background:#0b0f1a;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial}.w{display:grid;place-items:center;height:100%;gap:12px;text-align:center;padding:16px}.c{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.14);padding:16px;border-radius:12px;max-width:640px}.u{font-family:ui-monospace,Menlo,Monaco,Consolas,"Liberation Mono","Courier New";background:rgba(255,255,255,.08);padding:6px 10px;border-radius:8px;display:inline-block}</style>
</head>
<body>
<div class="w">
<div class="c">
<h1 style="margin:0 0 8px;">Whackrow Demo</h1>
<p>Open Game and Controls pages:</p>
<p><a href="/game.html" style="color:#38bdf8;">Game</a> · <a href="/controls.html" style="color:#38bdf8;">Controls</a></p>
<div id="lan"></div>
</div>
</div>
<script>
const o = (typeof __WHACKROW_DEV_LAN_ORIGIN__ !== 'undefined') ? __WHACKROW_DEV_LAN_ORIGIN__ : '';
if (o) {
const el = document.getElementById('lan');
if (el) el.innerHTML = '<div style="margin-top:8px;">LAN URLs: <span class="u">'+o+'/game.html</span> · <span class="u">'+o+'/controls.html</span></div>';
}
</script>
</body>
</html>`);
// Print helpful URLs
const nets = os.networkInterfaces();
const ips = [];
for (const k of Object.keys(nets)) for (const n of nets[k]||[]) if (n.family==='IPv4' && !n.internal) ips.push(n.address);
console.log('Scaffolded demo pages in ./demo');
console.log('Open with dev server:');
console.log(' npm run dev');
console.log('Then visit:');
console.log(' http://localhost:5173/game.html');
console.log(' http://localhost:5173/controls.html');
for (const ip of ips) {
console.log(` http://${ip}:5173/game.html`);
console.log(` http://${ip}:5173/controls.html`);
}