LifeTown Online — browser multiplayer life-sim MVP

- 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
This commit is contained in:
deepseek
2026-08-23 07:01:24 +00:00
commit 93b018e42b
35 changed files with 7785 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
// 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}`);
});