- Server-authoritative Node/WS server: sessions, zones (town + instanced homes + public venue interiors), NPC routines, economy, relationships, persistence - TypeScript + Three.js client: character creator, Maple Court district, enterable venues (café, city hall, gym, store, arcade), build mode with 58-item furniture catalog, needs/mood systems, phone UI, day/night + weather, procedural audio - Verified by two-client e2e protocol suite and headless-browser walkthrough
56 lines
2.1 KiB
JavaScript
56 lines
2.1 KiB
JavaScript
// LifeTown Online — entry point: static file server + WebSocket upgrade.
|
|
import http from 'node:http';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { WebSocketServer } from 'ws';
|
|
import { GameServer } from './game.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const DIST = path.join(__dirname, '..', 'client', 'dist');
|
|
const PORT = Number(process.env.GAME_PORT || 7788);
|
|
|
|
const MIME = {
|
|
'.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript',
|
|
'.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg',
|
|
'.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.wasm': 'application/wasm',
|
|
'.woff2': 'font/woff2', '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg', '.webp': 'image/webp',
|
|
};
|
|
|
|
function serveStatic(req, res) {
|
|
let urlPath = decodeURIComponent(new URL(req.url, 'http://x').pathname);
|
|
if (urlPath === '/') urlPath = '/index.html';
|
|
let filePath = path.normalize(path.join(DIST, urlPath));
|
|
if (!filePath.startsWith(DIST)) { res.writeHead(403); res.end('Forbidden'); return; }
|
|
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
// SPA fallback
|
|
filePath = path.join(DIST, 'index.html');
|
|
if (!fs.existsSync(filePath)) { res.writeHead(503); res.end('LifeTown client is not built yet. Run: npm run build'); return; }
|
|
}
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
res.writeHead(200, {
|
|
'Content-Type': MIME[ext] || 'application/octet-stream',
|
|
'Cache-Control': ext === '.html' ? 'no-cache' : 'public, max-age=3600',
|
|
});
|
|
fs.createReadStream(filePath).pipe(res);
|
|
}
|
|
|
|
const server = http.createServer(serveStatic);
|
|
|
|
// Single WebSocket endpoint: /ws
|
|
const wss = new WebSocketServer({ noServer: true });
|
|
server.on('upgrade', (req, socket, head) => {
|
|
const { pathname } = new URL(req.url, 'http://x');
|
|
if (pathname === '/ws') {
|
|
wss.handleUpgrade(req, socket, head, (ws) => game.onConnection(ws));
|
|
} else {
|
|
socket.destroy();
|
|
}
|
|
});
|
|
|
|
const game = new GameServer(wss);
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`[LifeTown] server listening on http://127.0.0.1:${PORT}`);
|
|
});
|