- 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
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
// 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: '🎮',
|
|
};
|