// Serves a build over HTTP for testing in a browser. // // npm run serve # build/web-mobile on http://localhost:8123 // npm run serve -- 9000 # a different port // // Also serves one route of its own, `/_aspects`, which loads the game in fixed // size iframes at the portrait extremes docs/UI.md asks for (4:3 to 21:9). The // engine resizes itself to fill whatever window it is in, so an iframe is the // only straightforward way to see the layout at a shape the desktop window is // not — and the harness lives here rather than in the build folder because a // rebuild wipes everything inside it. import { createServer } from 'node:http'; import { readFile } from 'node:fs/promises'; import { extname, join, normalize, resolve } from 'node:path'; const ROOT = resolve(process.argv[3] ?? 'build/web-mobile'); const PORT = Number(process.argv[2] ?? 8123); const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp', '.wasm': 'application/wasm', '.bin': 'application/octet-stream', }; /** width:height as a portrait phone reports it, from the widest to the tallest. */ const RATIOS = [ ['4:3 — widest', 3 / 4], ['16:9', 9 / 16], ['21:9 — tallest', 9 / 21], ]; function aspectsPage(height, only) { // Booting three engines at once is slow enough to be painful; `?only=16:9` // renders a single viewport when that is all you need. const wanted = only ? RATIOS.filter(([label]) => label.toLowerCase().startsWith(only.toLowerCase())) : RATIOS; const cells = (wanted.length > 0 ? wanted : RATIOS).map(([label, ratio]) => { const width = Math.round(height * ratio); return `

${label} — ${width}×${height}

`; }).join('\n'); return ` Fool — portrait layout at 4:3, 16:9 and 21:9
${cells}
`; } createServer(async (request, response) => { const path = decodeURIComponent((request.url ?? '/').split('?')[0]); if (path === '/_aspects' || path === '/_aspects.html') { const query = new URL(request.url ?? '/', 'http://x').searchParams; const height = Number(query.get('h')) || 760; response.writeHead(200, { 'Content-Type': TYPES['.html'], 'Cache-Control': 'no-store' }); response.end(aspectsPage(height, query.get('only'))); return; } const file = join(ROOT, normalize(path === '/' ? '/index.html' : path)); if (!file.startsWith(ROOT)) { response.writeHead(403); response.end('forbidden'); return; } try { const body = await readFile(file); response.writeHead(200, { 'Content-Type': TYPES[extname(file)] ?? 'application/octet-stream', 'Cache-Control': 'no-store', }); response.end(body); } catch { response.writeHead(404); response.end('not found'); } }).listen(PORT, () => { console.log(`serving ${ROOT}`); console.log(` game http://localhost:${PORT}/`); console.log(` aspects http://localhost:${PORT}/_aspects`); console.log(` one http://localhost:${PORT}/_aspects?only=16:9&h=900`); });