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
+99
View File
@@ -0,0 +1,99 @@
// LifeTown Online - headless verification: one short browser session per screen
import { chromium } from 'playwright-core';
import fs from 'node:fs';
const EXE = '/root/.cache/ms-playwright/chromium_headless_shell-1148/chrome-linux/headless_shell';
const URL = 'http://127.0.0.1:7788';
const OUT = '/tmp/lifetown-shots';
fs.mkdirSync(OUT, { recursive: true });
const ARGS = ['--use-gl=swiftshader', '--enable-unsafe-swiftshader', '--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu-sandbox'];
const allErrors = [];
async function withPage(fn, label) {
let browser;
try {
browser = await chromium.launch({ executablePath: EXE, args: ARGS, timeout: 30000 });
const page = await browser.newPage({ viewport: { width: 960, height: 600 } });
page.on('pageerror', (e) => allErrors.push(`[${label}] ${String(e).slice(0, 250)}`));
page.on('console', (m) => { if (m.type() === 'error') allErrors.push(`[${label}] CONSOLE ${m.text().slice(0, 200)}`); });
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 20000 });
await fn(page);
console.log(`[shot] OK ${label}`);
} catch (e) {
console.log(`[shot] FAIL ${label}: ${String(e).slice(0, 140)}`);
} finally {
try { await browser?.close(); } catch {}
}
}
async function enterGame(page) {
const uname = 'Sunny' + Math.floor(Math.random() * 900000 + 100000);
await page.waitForTimeout(700);
await page.click('#btn-newchar', { force: true });
await page.fill('#cr-name', uname);
await page.click('#btn-startlife', { force: true });
await page.waitForTimeout(4200);
}
(async () => {
await withPage(async (page) => {
await page.waitForTimeout(1100);
await page.screenshot({ path: `${OUT}/1-title.png` });
}, 'title');
await withPage(async (page) => {
await page.waitForTimeout(700);
await page.click('#btn-newchar', { force: true });
await page.fill('#cr-name', 'Sunny' + Math.floor(Math.random() * 900000 + 100000));
await page.waitForTimeout(1200);
await page.screenshot({ path: `${OUT}/2-creator.png` });
}, 'creator');
await withPage(async (page) => {
await enterGame(page);
await page.screenshot({ path: `${OUT}/3-town.png` });
await page.keyboard.down('KeyW');
await page.waitForTimeout(1200);
await page.keyboard.up('KeyW');
await page.waitForTimeout(400);
await page.screenshot({ path: `${OUT}/4-walked.png` });
}, 'town');
await withPage(async (page) => {
await enterGame(page);
await page.click('[data-act="phone"]', { force: true });
await page.waitForTimeout(900);
await page.screenshot({ path: `${OUT}/5-phone.png` });
}, 'phone');
await withPage(async (page) => {
await enterGame(page);
await page.click('[data-act="home"]', { force: true });
await page.waitForTimeout(600);
await page.screenshot({ path: `${OUT}/6-homemenu.png` });
const btns = await page.$$('#dialog-body button');
if (btns[1]) await btns[1].click({ force: true });
await page.waitForTimeout(3200);
await page.screenshot({ path: `${OUT}/7-home.png` });
}, 'home');
await withPage(async (page) => {
await enterGame(page);
await page.click('[data-act="home"]', { force: true });
await page.waitForTimeout(500);
const btns = await page.$$('#dialog-body button');
if (btns[1]) await btns[1].click({ force: true });
await page.waitForTimeout(3000);
await page.click('[data-act="build"]', { force: true });
await page.waitForTimeout(700);
await page.click('#btn-shop', { force: true });
await page.waitForTimeout(900);
await page.screenshot({ path: `${OUT}/8-catalog.png` });
await page.click('#catalog-close', { force: true });
await page.waitForTimeout(400);
await page.screenshot({ path: `${OUT}/9-buildmode.png` });
}, 'build');
console.log(allErrors.length ? `PAGE ERRORS (${allErrors.length}):\n` + allErrors.slice(0, 10).join('\n') : 'NO PAGE ERRORS');
})();