feat(shard): Protocol 2.0 cross-links — titles, guild, governor, houses
Phase 3: surface the new board data on existing character/user pages. - Character sheet: render the char.profile titles block (fame/karma + skill + selected reward title; numeric clilocs skipped since the site has no cliloc table yet), plus "Guildmaster" and "Governor of <city>" chips. - Char profile enrichment (player/admin /shard/char/:serial, one shared path): attach guild + governorOf from our own boards. Guild is LEADERSHIP-ONLY — it's verifiable from current board state, whereas guessing membership from stale guild.join events risks showing a wrong guild, so we return null instead. - Admin user detail (/admin/users/:id): new "Standing" section (governorships held + guilds led) via GET /users/:id/shard/standing; Houses rows now show the registry fields (decay level, placement price, co-owner/friend counts) already returned by listHousesForAccounts. Server 179/179, client build clean, swagger regenerated. Refs .plans/protocol2-integration.md (Phase 3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,38 @@ import ShardAccountActions from './ShardAccountActions.jsx'
|
||||
|
||||
const RESIST_LABELS = { phys: 'Physical', fire: 'Fire', cold: 'Cold', pois: 'Poison', energy: 'Energy' }
|
||||
|
||||
// The char.profile `titles` block (Protocol 2.0). fameKarma/skill are already
|
||||
// computed display strings; reward entries may be a cliloc NUMBER-as-string or a
|
||||
// literal string. Without a cliloc table on the site we can only show literals, so
|
||||
// numeric reward entries are skipped rather than shown as a raw number. Returns a
|
||||
// de-duped list of human-readable title chips.
|
||||
function displayTitles(titles) {
|
||||
if (!titles) return []
|
||||
const out = []
|
||||
if (titles.fameKarma) out.push(titles.fameKarma)
|
||||
if (titles.skill) out.push(titles.skill)
|
||||
const reward = Array.isArray(titles.reward) ? titles.reward : []
|
||||
const sel = typeof titles.selected === 'number' ? titles.selected : -1
|
||||
// Prefer the selected reward title; fall back to the first literal one.
|
||||
const candidate = sel >= 0 && sel < reward.length ? reward[sel] : reward.find((r) => r && !/^\d+$/.test(String(r)))
|
||||
if (candidate && !/^\d+$/.test(String(candidate))) out.push(String(candidate))
|
||||
return [...new Set(out.filter(Boolean))]
|
||||
}
|
||||
|
||||
function TitleChip({ children, tone = 'var(--muted)' }) {
|
||||
return (
|
||||
<span
|
||||
className="sans"
|
||||
style={{
|
||||
fontSize: '0.72rem', padding: '3px 9px', borderRadius: 999,
|
||||
border: `1px solid ${tone}55`, color: tone, whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function StatTile({ value, label }) {
|
||||
return (
|
||||
<div className="panel" style={{ padding: '14px 12px', textAlign: 'center' }}>
|
||||
@@ -64,6 +96,21 @@ export default function CharacterSheet({ char, moderation = false }) {
|
||||
<span className="sans dim" style={{ fontSize: '0.76rem', marginLeft: 'auto' }}>{char.serial}</span>
|
||||
</div>
|
||||
|
||||
{/* Titles + standing (guild led / governorship) — all optional */}
|
||||
{(displayTitles(char.titles).length > 0 || char.guild || (char.governorOf && char.governorOf.length > 0)) && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: -8 }}>
|
||||
{char.governorOf && char.governorOf.map((city) => (
|
||||
<TitleChip key={`gov-${city}`} tone="#c9a24b">Governor of {city}</TitleChip>
|
||||
))}
|
||||
{char.guild && (
|
||||
<TitleChip tone="var(--accent)">
|
||||
Guildmaster{char.guild.abbr ? `, [${char.guild.abbr}]` : ''} {char.guild.name}
|
||||
</TitleChip>
|
||||
)}
|
||||
{displayTitles(char.titles).map((t) => <TitleChip key={t}>{t}</TitleChip>)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Staff moderation for this character's account (self-gates to staff). */}
|
||||
{moderation && char.acct && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, padding: '12px 14px', border: '1px solid var(--line-soft)', borderRadius: 10, background: 'rgba(255,255,255,0.02)' }}>
|
||||
|
||||
Reference in New Issue
Block a user