// Shared formatting for shard events — used by the public Shard page, the // Activity feed, and the admin live feed. One place decides how each kind reads // and which category/badge it belongs to. function nameOf(who) { if (!who) return 'Someone' if (typeof who === 'string') return who return who.name || who.acct || 'Someone' } const n = (v) => Number(v || 0).toLocaleString() // A one-line human description of each event kind, keyed by kind. Each formatter // takes the payload and returns a string. Conditional suffixes are pulled into // locals so no template literal is nested inside another. const DESCRIBERS = { 'vendor.sale': (p) => { const qty = p.amount > 1 ? ` ×${p.amount}` : '' return `${p.itemType || 'An item'}${qty} sold for ${n(p.price)}gp` }, 'player.death': (p) => { const by = p.killer ? ` by ${nameOf(p.killer)}` : '' return `${nameOf(p.who)} was slain${by}` }, 'player.murdered': (p) => { const by = p.murderer ? ` by ${nameOf(p.murderer)}` : '' return `${nameOf(p.victim)} was murdered${by}` }, 'mob.killed': (p) => `${nameOf(p.killer)} killed ${nameOf(p.killed)}`, 'skill.gain': (p) => { const base = p.base != null ? ` (${p.base})` : '' return `${nameOf(p.who)} gained ${p.skill}${base}` }, 'fame.change': (p) => `${nameOf(p.who)}’s fame changed to ${n(p.new)}`, 'karma.change': (p) => `${nameOf(p.who)}’s karma changed to ${n(p.new)}`, 'quest.complete': (p) => `${nameOf(p.who)} completed “${p.quest}”`, 'house.decay': (p) => { const region = p.region ? ` — ${p.region}` : '' return `${p.name || 'A house'} is now ${p.to || p.stage}${region}` }, 'mob.login': (p) => `${nameOf(p.who)} entered the world`, 'mob.logout': (p) => `${nameOf(p.who)} left the world`, 'economy.supply': (p) => `Gold supply: ${n(p.gold)} across ${n(p.accounts)} accounts`, 'server.hello': (p) => `Shard online — ${n(p.accounts)} accounts, ${n(p.mobiles)} mobiles`, 'server.shutdown': () => 'Shard shut down', 'server.crashed': (p) => { const err = p.error ? `: ${p.error}` : '' return `Shard crashed${err}` }, 'champ.update': (p) => { const where = p.name || p.type || 'A champion spawn' if (p.status === 'active' && p.bossUp) { const boss = p.boss ? ` (${p.boss})` : '' return `${where}: boss is up${boss}` } if (p.status === 'active') { const level = p.level != null ? ` — level ${p.level}` : '' return `${where} is active${level}` } if (p.status === 'cooldown') return `${where} is on cooldown` return `${where} is ${p.status || 'idle'}` }, 'champ.remove': () => `A champion spawn ended`, // Support (help-page) queue + in-game moderation (admin channel only) 'page.new': (p) => `New ${p.type || 'help'} page from ${nameOf(p.sender)}`, 'page.updated': (p) => { const claimed = p.handled ? ' (claimed)' : '' return `Help page from ${nameOf(p.sender)} updated${claimed}` }, 'page.closed': (p) => `Help page ${p.pageId || ''} closed`, 'admin.audit': (p) => { const on = p.target ? ` on ${p.target}` : '' const origin = p.origin ? ` [${p.origin}]` : '' return `${p.actor || 'Staff'} ${p.action || 'acted'}${on}${origin}` }, // Staff / sensitive (admin channel only) 'audit.set': (p) => `${nameOf(p.staff) || 'Staff'} set ${p.prop} on ${p.target || p.targetSerial} (${p.old} → ${p.new})`, 'audit.command': (p) => { const args = p.args ? ` ${p.args}` : '' return `${nameOf(p.staff) || 'Staff'} ran ${p.command}${args}` }, 'cheat.fastwalk': (p) => { const ip = p.ip ? ` (${p.ip})` : '' return `Fast-walk flagged: ${nameOf(p.who)}${ip}` }, 'account.login.attempt': (p) => { const ip = p.ip ? ` from ${p.ip}` : '' return `Login attempt: ${p.acct}${ip}` }, 'gold.change': (p) => { const sign = p.delta >= 0 ? '+' : '' return `${p.acct}: gold ${sign}${n(p.delta)} → ${n(p.new)}` }, } // A one-line human description of an event. Accepts either a stored event // (with .payload) or a raw live frame (fields at top level). export function describe(ev) { const fmt = DESCRIBERS[ev.kind] return fmt ? fmt(ev.payload || ev) : ev.kind } // Category grouping for the filter tabs. // Vendor sales are intentionally NOT a public category — they are owner-private // (a linked player sees their own under the portal). The admin live feed still // describes vendor.sale via describe() below. export const CATEGORIES = [ { id: 'all', label: 'All', kinds: null }, { id: 'pvp', label: 'Deaths & PvP', kinds: ['player.death', 'player.murdered', 'mob.killed'] }, { id: 'progress', label: 'Progression', kinds: ['skill.gain', 'fame.change', 'karma.change', 'quest.complete'] }, { id: 'world', label: 'World', kinds: ['house.decay', 'mob.login', 'mob.logout', 'server.hello', 'server.shutdown', 'server.crashed', 'economy.supply'] }, ] const CATEGORY_OF = (() => { const m = {} for (const c of CATEGORIES) if (c.kinds) for (const k of c.kinds) m[k] = c.id return m })() export function categoryOf(kind) { return CATEGORY_OF[kind] || 'other' } // Short badge label for a kind (the part after the dot, title-cased-ish). export function kindLabel(kind) { return String(kind || '').replace(/[._]/g, ' ') }