const http = require("http");
const { WebSocketServer } = require("ws");
const { randomUUID } = require("crypto");
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
const html = `
Portfolio World
Teleport by UI. Multiplayer shows other visitors as bodies. Esc unlocks mouse.
`;
const server = http.createServer((req, res) => {
if (req.url === "/" || req.url === "/index.html") {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
res.end(html);
return;
}
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not found");
});
const wss = new WebSocketServer({ server });
const clients = new Map();
function broadcast(obj, exceptId) {
const msg = JSON.stringify(obj);
for (const [id, c] of clients) {
if (id === exceptId) continue;
if (c.ws.readyState === 1) c.ws.send(msg);
}
}
wss.on("connection", (ws) => {
const id = randomUUID();
clients.set(id, { ws, last: Date.now(), state: null });
ws.send(JSON.stringify({
t: "welcome",
id,
peers: Array.from(clients.entries()).filter(([pid]) => pid !== id).map(([pid, v]) => ({ id: pid, s: v.state }))
}));
broadcast({ t: "join", id }, id);
ws.on("message", (buf) => {
let msg;
try { msg = JSON.parse(buf.toString()); } catch { return; }
const c = clients.get(id);
if (!c) return;
c.last = Date.now();
if (msg && msg.t === "state" && msg.s) {
c.state = msg.s;
broadcast({ t: "state", id, s: msg.s }, id);
}
});
ws.on("close", () => {
clients.delete(id);
broadcast({ t: "leave", id }, id);
});
});
setInterval(() => {
const t = Date.now();
for (const [id, c] of clients) {
if (t - c.last > 30000) {
try { c.ws.close(); } catch {}
clients.delete(id);
broadcast({ t: "leave", id }, id);
}
}
}, 2000);
server.listen(PORT, () => console.log("http://localhost:" + PORT));