Clears the 124 CODE_SMELL findings from the SonarQube scan (server, client, and bot). All changes are behaviour-preserving refactors — no route, protocol, schema, or config changes — verified against the full server (381) and client (43) test suites plus a clean client build. By rule: - S3776 (20, cognitive complexity): extract helpers/handlers so each function drops under the threshold — shard model upsert builders, page/wiki update, block validation, notification stream mapping (dispatch table), SSO mobile login, shard ingest deps, uo-link socket backfill/connect, the bot slash- command dispatchers + discord manager, and the Shard/UserDetail/HeroEditor/ CharacterStats React components. - S4624 (34, nested template literals): pull inner templates into locals / a withQs() helper; rewrite shardEvents.describe() as a formatter table. - S3358 (35, nested ternaries): lift to if/else vars, lookup maps, small components, or guarded JSX expressions. - S6479 (12, array-index React keys): key by stable content instead of index (two in-editor lists left as-is; index matches their by-index edit model). - S6353 (6): [0-9]/[^0-9] -> \d/\D. S125 (5): reword state-shape comments that parsed as code. S3800/S3782 (botScore): JSDoc-type PATH_WEIGHTS tuples. - S6481 (2): memoize Auth/Site context values (and SiteContext brand). - S4144: dedupe HeroEditor upload handler into useImageUpload(). - S1126 (2), S6035, S5869 (redundant A-Z under /i), S5843 (town-name regex -> prefix list): assorted one-liners. Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
5.2 KiB
JavaScript
129 lines
5.2 KiB
JavaScript
// 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, ' ')
|
||
}
|