// Reusable character-sheet renderer for the char.profile shape returned by // /public/shard/char/:serial. Presentational only — the parent handles loading // and errors. Styled with the shared theme vocabulary (panel/grid/stat tiles). // // `moderation` opts in the in-game kick/ban controls for the character's account; // they self-gate to staff (ShardAccountActions), so passing it from a page a // player can reach is safe. import ShardAccountActions from './ShardAccountActions.jsx' const RESIST_LABELS = { phys: 'Physical', fire: 'Fire', cold: 'Cold', pois: 'Poison', energy: 'Energy' } // What to call an equipped item. // // Items on the wire carry a `LabelNumber`, not a name, so this used to be able // to show nothing but the layer and `id 12345`. The server now resolves the // cliloc against its own table and attaches `clilocName` (see // docs/website/CLILOCS.md); a shard with no cliloc file configured sends none, // and the layer fallback below is exactly what the sheet did before. // // A player-given `name` outranks the resolved type name — "Bob's lucky axe" // should not be relabelled "hatchet" — and the server applies the same // precedence, so this only re-states it for a profile that arrived with both. const itemName = (it) => it.name || it.clilocName || it.layer || 'Item' // 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. // // `rewardResolved` is the server's parallel array with the numeric entries turned // into words (null where the cliloc table had nothing, or is not configured at // all). Prefer it, and keep the literal-only path as the fallback for a profile // served before the cliloc table existed — a numeric entry with no resolution is // still skipped rather than shown as a raw number. function displayTitles(titles) { if (!titles) return [] const out = [] if (titles.fameKarma) out.push(titles.fameKarma) if (titles.skill) out.push(titles.skill) const raw = Array.isArray(titles.reward) ? titles.reward : [] const resolved = Array.isArray(titles.rewardResolved) ? titles.rewardResolved : null const reward = raw.map((r, i) => resolved?.[i] ?? (/^\d+$/.test(String(r)) ? null : String(r))) const sel = typeof titles.selected === 'number' ? titles.selected : -1 // Prefer the selected reward title; fall back to the first one that resolved. // The `??` matters: a selected title whose cliloc did not resolve must fall // through to the fallback rather than suppress the chip entirely. const candidate = (sel >= 0 && sel < reward.length ? reward[sel] : null) ?? reward.find(Boolean) if (candidate) out.push(String(candidate)) return [...new Set(out.filter(Boolean))] } // The char.profile `points` block (Protocol 3.0 §7.3): one entry per point system // the character actually holds a score in. Systems at zero are omitted by the // shard, so an empty list means "this character has earned nothing anywhere", // which is a normal state for a new character and renders as nothing at all. // // `nameString` may be null when the system's name is a cliloc; fall back to // humanising the PointsType key, exactly as the leaderboards page does. `rank` is // absent unless the shard runs with Bridge.cfg PointsProfileRank=true — absent and // "unranked" are different, so the chip only appears when it was actually sent. const humanisePoints = (key) => String(key || '') .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .replace(/^./, (c) => c.toUpperCase()) function PointsRow({ entry }) { const label = entry.nameString || humanisePoints(entry.system) const max = Number.isFinite(entry.maxPoints) && entry.maxPoints > 0 ? entry.maxPoints : 0 const pct = max ? Math.min(100, Math.round((entry.points / max) * 100)) : 0 return (