UNPKG

whackrow

Version:

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

142 lines (132 loc) 7.94 kB
#!/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'), `<!doctype 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'), `<!doctype 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'), `<!doctype 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`); }