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:
@@ -0,0 +1,61 @@
|
||||
// LifeTown Online — UI helpers shared across modules.
|
||||
import { bus } from '../state';
|
||||
import { audio } from '../audio';
|
||||
|
||||
export function $(sel: string): HTMLElement { return document.querySelector(sel) as HTMLElement; }
|
||||
export function $$(sel: string): HTMLElement[] { return [...document.querySelectorAll(sel)] as any; }
|
||||
|
||||
export function el(tag: string, cls?: string, html?: string): HTMLElement {
|
||||
const e = document.createElement(tag);
|
||||
if (cls) e.className = cls;
|
||||
if (html != null) e.innerHTML = html;
|
||||
return e;
|
||||
}
|
||||
|
||||
let toastN = 0;
|
||||
export function toast(title: string, text = '', icon = '✨', kind: '' | 'gold' | 'coral' = '', onClick?: () => void) {
|
||||
const box = $('#toasts');
|
||||
const t = el('div', `toast ${kind}`);
|
||||
t.innerHTML = `<span class="t-icon">${icon}</span><div><b>${title}</b><p>${text}</p></div>`;
|
||||
if (onClick) t.addEventListener('click', () => { onClick(); t.remove(); });
|
||||
box.appendChild(t);
|
||||
toastN++;
|
||||
setTimeout(() => { t.remove(); toastN--; }, 5200);
|
||||
while (box.children.length > 4) box.firstChild!.remove();
|
||||
}
|
||||
bus.on('srv:notify', (m: any) => {
|
||||
toast(m.title || '', m.text || '', m.icon || '✨', m.title === 'Promotion!' ? 'gold' : '',
|
||||
m.action?.type === 'visit' ? () => netVisit(m.action.home) : undefined);
|
||||
});
|
||||
bus.on('srv:achievement', (m: any) => {
|
||||
audio.achievement();
|
||||
toast(`🏆 Achievement — ${m.name}`, m.desc, '🏆', 'gold');
|
||||
});
|
||||
|
||||
import { net } from '../net';
|
||||
function netVisit(home: string) { net.send({ t: 'zone', id: home }); }
|
||||
|
||||
export function openDialog(title: string, buildBody: (body: HTMLElement, close: () => void) => void) {
|
||||
const modal = $('#dialog');
|
||||
$('#dialog-title').textContent = title;
|
||||
const body = $('#dialog-body');
|
||||
body.innerHTML = '';
|
||||
modal.classList.remove('hidden');
|
||||
const close = () => modal.classList.add('hidden');
|
||||
($('#dialog-close') as HTMLElement).onclick = close;
|
||||
buildBody(body, close);
|
||||
audio.click();
|
||||
}
|
||||
export function closeDialog() { $('#dialog').classList.add('hidden'); }
|
||||
|
||||
export function fmtMoney(n: number): string {
|
||||
return n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n.toLocaleString('en-US');
|
||||
}
|
||||
|
||||
/** emoji icon per furniture category */
|
||||
export const CAT_EMOJI: Record<string, string> = {
|
||||
bed: '🛏️', sofa: '🛋️', table: '🪵', chair: '🪑', tv: '📺', pc: '🖥️',
|
||||
fridge: '🧊', stove: '🍳', shower: '🚿', toilet: '🚽', sink: '🚰',
|
||||
wardrobe: '🗄️', shelf: '📚', plant: '🪴', decor: '🕯️', light: '💡',
|
||||
rug: '🧶', paint: '🖼️', elec: '🎮',
|
||||
};
|
||||
Reference in New Issue
Block a user