Phase 2: the public UI for the four new boards, following the ChampSpawns live pattern (snapshot via useAsync + merge SSE deltas with useShardFeed). - Players Online widget (components/PlayersOnline.jsx): total + region breakdown rolled up into display buckets (data/regionBuckets.js — the one place to retune the grouping); live via presence.online. Placed on the Shard page, replacing the static players-online stat tile. - Guilds (/site/guilds): searchable board of rosters/alliances/leaders with a "recently joined" strip from guild.join. - Governors (/site/governors): one card per city with a placeholder crest (data/cityCrests.js — swap for real art without touching components), election phase badge + autoPickAt countdown, and an on-demand "past governors" term history (the look-back reads the ledger captured in Phase 1). Clean empty state when City Loyalty isn't enabled. - Houses (/site/houses): searchable registry with decay badges; price labelled "placement value", not a for-sale flag. - API client methods + nav links (Guilds / Governors / Houses). Client build clean (240 modules). Refs .plans/protocol2-integration.md (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
63 lines
2.2 KiB
JavaScript
63 lines
2.2 KiB
JavaScript
// Roll the sidecar's raw presence.online `byRegion` map (many named ServUO
|
|
// regions) up into a handful of labelled display buckets for the "Players Online"
|
|
// widget. This is the ONE place to retune the grouping — edit BUCKETS (order +
|
|
// membership) and the widget follows. Anything not matched lands in "Wilderness"
|
|
// so the bucket counts always reconcile to the true total.
|
|
|
|
// Ordered list of buckets. `label` shows in the widget; `match(region)` decides
|
|
// membership. First matching bucket wins; the last bucket is the catch-all.
|
|
export const BUCKETS = [
|
|
{
|
|
id: 'britain',
|
|
label: 'Britain',
|
|
// Passthrough for the capital + its immediate surrounds.
|
|
match: (r) => /^britain/i.test(r),
|
|
},
|
|
{
|
|
id: 'towns',
|
|
label: 'Towns',
|
|
// The other named cities/towns.
|
|
match: (r) =>
|
|
/^(moonglow|minoc|trinsic|jhelom|yew|skara ?brae|magincia|new ?magincia|vesper|nujelm|cove|ocllo|serpent'?s? hold|wind|delucia|papua)/i.test(
|
|
r,
|
|
),
|
|
},
|
|
{
|
|
id: 'dungeons',
|
|
label: 'Dungeons',
|
|
match: (r) =>
|
|
/(despise|destard|deceit|shame|hythloth|covetous|wrong|terathan|fire|ice|orc cave|dungeon|abyss|doom|khaldun|wrong|blackthorn|exodus|labyrinth|underworld)/i.test(
|
|
r,
|
|
),
|
|
},
|
|
{
|
|
id: 'housing',
|
|
label: 'Housing',
|
|
// House regions expose themselves as named house/townhouse regions.
|
|
match: (r) => /(house|townhouse|homestead|tent)/i.test(r),
|
|
},
|
|
{
|
|
id: 'wilderness',
|
|
label: 'Wilderness',
|
|
// Catch-all: the unnamed "Wilderness" region + anything unmatched above.
|
|
match: () => true,
|
|
},
|
|
]
|
|
|
|
// Given a raw { region: count } map, return [{ id, label, count }] in BUCKETS
|
|
// order, dropping empty buckets, with the summed total also returned.
|
|
export function bucketize(byRegion = {}) {
|
|
const totals = new Map(BUCKETS.map((b) => [b.id, 0]))
|
|
let total = 0
|
|
for (const [region, n] of Object.entries(byRegion || {})) {
|
|
const count = Number(n) || 0
|
|
total += count
|
|
const bucket = BUCKETS.find((b) => b.match(String(region))) || BUCKETS[BUCKETS.length - 1]
|
|
totals.set(bucket.id, totals.get(bucket.id) + count)
|
|
}
|
|
const rows = BUCKETS.map((b) => ({ id: b.id, label: b.label, count: totals.get(b.id) })).filter(
|
|
(r) => r.count > 0,
|
|
)
|
|
return { rows, total }
|
|
}
|