From fe6f93481bb11b6fab6a4be6d189345fe65c1487 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 02:51:50 -0500 Subject: [PATCH] Add player portal + character-sheet front end (phase 4 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns the raw shard endpoints into proper, navigable pages in the site's visual language. - components/CharacterSheet.jsx: reusable sheet — attribute tiles, vitals bars, resistances, skills (with bars), and equipment — styled with the shared panel/grid vocabulary. - Player portal with a nav bar: PlayerPortalLayout (Characters / Account tabs + sign-out) wraps /player and /account. /player (PlayerCharacters) tells the logged-in player if they haven't linked a game account (with the [link code prompt) or, once linked, shows their characters grouped by account; each character opens its sheet at /player/char/:serial. Account security moved into the same shell (the buried "Game accounts" block was removed from it). Login/register now land on /player. - Public: GET /public/shard/online (redacted name+serial+map) drives an "Online now" list on /site/shard that links to public character sheets at /site/shard/char/:serial (ShardChar). Swagger: ShardOnlinePlayer + regenerated. - api.shard.online added. Verified live against the running shard: Darrow's full sheet (STR 120, 58 skills, 3 equipment) renders through the browser-facing proxy; the online list returns the live roster; player routes 401 without a session. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_011qPmpmVH1xGCiZoz9m9vW3 --- client/src/App.jsx | 14 +- client/src/api/client.js | 1 + client/src/components/CharacterSheet.jsx | 141 +++++++++++ client/src/routes/player/PlayerAccount.jsx | 226 ++---------------- client/src/routes/player/PlayerCharacter.jsx | 27 +++ client/src/routes/player/PlayerCharacters.jsx | 166 +++++++++++++ client/src/routes/player/PlayerLogin.jsx | 2 +- .../src/routes/player/PlayerPortalLayout.jsx | 54 +++++ client/src/routes/player/PlayerRegister.jsx | 4 +- client/src/routes/public/Shard.jsx | 36 ++- client/src/routes/public/ShardChar.jsx | 37 +++ server/src/router/v1/public/public.routes.js | 7 + .../src/router/v1/public/shard.controller.js | 15 +- server/swagger/swagger-output.json | 88 +++++++ server/swagger/swagger.js | 9 + 15 files changed, 609 insertions(+), 218 deletions(-) create mode 100644 client/src/components/CharacterSheet.jsx create mode 100644 client/src/routes/player/PlayerCharacter.jsx create mode 100644 client/src/routes/player/PlayerCharacters.jsx create mode 100644 client/src/routes/player/PlayerPortalLayout.jsx create mode 100644 client/src/routes/public/ShardChar.jsx diff --git a/client/src/App.jsx b/client/src/App.jsx index 1560d18..9149a78 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -17,6 +17,7 @@ import NewsletterIssue from './routes/public/NewsletterIssue.jsx' import About from './routes/public/About.jsx' import Status from './routes/public/Status.jsx' import Shard from './routes/public/Shard.jsx' +import ShardChar from './routes/public/ShardChar.jsx' import Wiki from './routes/wiki/Wiki.jsx' import WikiArticle from './routes/wiki/WikiArticle.jsx' import CmsPage from './routes/public/CmsPage.jsx' @@ -44,6 +45,9 @@ import ModerationUser from './routes/admin/views/ModerationUser.jsx' // Player portal import PlayerLogin from './routes/player/PlayerLogin.jsx' import PlayerRegister from './routes/player/PlayerRegister.jsx' +import PlayerPortalLayout from './routes/player/PlayerPortalLayout.jsx' +import PlayerCharacters from './routes/player/PlayerCharacters.jsx' +import PlayerCharacter from './routes/player/PlayerCharacter.jsx' import PlayerAccount from './routes/player/PlayerAccount.jsx' export default function App() { @@ -69,6 +73,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> {/* CMS pages: top-level /:slug, matched only after the named routes @@ -123,13 +128,16 @@ export default function App() { } /> } /> - + } - /> + > + } /> + } /> + } /> + } /> diff --git a/client/src/api/client.js b/client/src/api/client.js index 8263791..648cf04 100644 --- a/client/src/api/client.js +++ b/client/src/api/client.js @@ -92,6 +92,7 @@ export const api = { return req(`/public/shard/feed${s ? `?${s}` : ''}`) }, economy: (limit) => req(`/public/shard/economy${limit ? `?limit=${limit}` : ''}`), + online: () => req('/public/shard/online'), idoc: () => req('/public/shard/idoc'), char: (serial) => req(`/public/shard/char/${encodeURIComponent(serial)}`), }, diff --git a/client/src/components/CharacterSheet.jsx b/client/src/components/CharacterSheet.jsx new file mode 100644 index 0000000..34bdb9e --- /dev/null +++ b/client/src/components/CharacterSheet.jsx @@ -0,0 +1,141 @@ +// 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). + +const RESIST_LABELS = { phys: 'Physical', fire: 'Fire', cold: 'Cold', pois: 'Poison', energy: 'Energy' } + +function StatTile({ value, label }) { + return ( +
+
{value}
+
{label}
+
+ ) +} + +function Vital({ label, cur, max }) { + const pct = max ? Math.min(100, Math.round((cur / max) * 100)) : 0 + return ( +
+
+ {label} + {cur ?? '—'} / {max ?? '—'} +
+
+
+
+
+ ) +} + +export default function CharacterSheet({ char }) { + if (!char) return null + const stats = char.stats || {} + const resist = stats.resist || {} + // Skills the character actually has, best first. + const skills = (char.skills || []) + .filter((s) => (s.value || s.base || 0) > 0) + .sort((a, b) => (b.value || 0) - (a.value || 0)) + const equipment = char.equipment || [] + + return ( +
+ {/* Identity */} +
+

{char.name || 'Unknown'}

+ {char.title && {char.title}} + + + {char.online ? 'Online' : 'Offline'} + + {char.serial} +
+ + {/* Core stats */} +
+
Attributes
+
+ + + +
+
+ + + +
+
+ + {/* Resistances */} + {Object.keys(resist).length > 0 && ( +
+
Resistances
+
+ {['phys', 'fire', 'cold', 'pois', 'energy'].map((k) => ( +
+
{resist[k] ?? 0}
+
{RESIST_LABELS[k]}
+
+ ))} +
+
+ )} + + {/* Skills */} + {skills.length > 0 && ( +
+
Skills ({skills.length})
+
+ {skills.map((s) => { + const cap = s.cap || 100 + const pct = Math.min(100, Math.round(((s.value || 0) / cap) * 100)) + return ( +
+
+ {s.n} + {s.value} +
+
+
+
+
+ ) + })} +
+
+ )} + + {/* Equipment */} + {equipment.length > 0 && ( +
+
Equipment
+
+ {equipment.map((it) => ( +
+ +
+
{it.layer || 'Item'}
+
id {it.itemId}{it.hue ? ` · hue ${it.hue}` : ''}
+
+ {it.mods && Object.keys(it.mods).length > 0 && ( +
+ {Object.entries(it.mods).map(([k, v]) => ( + {k} {v} + ))} +
+ )} +
+ ))} +
+
+ )} +
+ ) +} diff --git a/client/src/routes/player/PlayerAccount.jsx b/client/src/routes/player/PlayerAccount.jsx index 6d0ea7d..ee240bd 100644 --- a/client/src/routes/player/PlayerAccount.jsx +++ b/client/src/routes/player/PlayerAccount.jsx @@ -1,6 +1,4 @@ import { useCallback, useEffect, useState } from 'react' -import { Link } from 'react-router-dom' -import MoonDot from '../../components/MoonDot.jsx' import ProviderIcon from '../../components/ProviderIcon.jsx' import { Loading, ErrorState } from '../../components/PageState.jsx' import { useAuth } from '../../contexts/AuthContext.jsx' @@ -297,175 +295,6 @@ function LinkedAccounts() { ) } -// ── Game accounts (uo-link) ──────────────────────────────────────────────── -function GameAccounts() { - const [accounts, setAccounts] = useState(null) - const [error, setError] = useState('') - const [code, setCode] = useState('') - const [busy, setBusy] = useState(false) - const [msg, setMsg] = useState('') - const [linkError, setLinkError] = useState('') - const [selected, setSelected] = useState(null) // account being inspected - - const load = useCallback(async () => { - try { - setAccounts(await api.player.shard.accounts()) - } catch { - setError('Could not load your linked game accounts.') - } - }, []) - useEffect(() => { load() }, [load]) - - async function link(e) { - e.preventDefault() - setMsg('') - setLinkError('') - if (!code.trim()) return - setBusy(true) - try { - const { account } = await api.player.shard.link(code.trim()) - setMsg(`Linked ${account}.`) - setCode('') - await load() - } catch (err) { - setLinkError(err.message || 'Could not link that code.') - } finally { - setBusy(false) - } - } - - if (error) return - if (!accounts) return null - - return ( -
-

- Link your in-game account to see your characters and player vendors here. In game, type{' '} - [link to get a one-time code, then enter it below. -

- -
- - -
- - - {accounts.length > 0 && ( -
- {accounts.map((a) => ( -
-
-
-
{a.account}
-
Linked {new Date(a.linkedAt).toLocaleDateString()}
-
- -
- {selected === a.account && } -
- ))} -
- )} - {accounts.length === 0 && ( -

No game accounts linked yet.

- )} -
- ) -} - -// Roster + vendors for one linked account, loaded on demand. Handles the shard -// restart (503) path with a retry-able banner. -function AccountDetail({ account }) { - const [roster, setRoster] = useState(null) - const [vendors, setVendors] = useState(null) - const [error, setError] = useState('') - const [unavailable, setUnavailable] = useState(false) - - const load = useCallback(async () => { - setError('') - setUnavailable(false) - try { - const [r, v] = await Promise.all([ - api.player.shard.roster(account), - api.player.shard.vendors(account).catch(() => null), - ]) - setRoster(r) - setVendors(v) - } catch (err) { - if (err.status === 503) setUnavailable(true) - else setError(err.message || 'Could not load this account.') - } - }, [account]) - useEffect(() => { load() }, [load]) - - if (unavailable) { - return ( -
-

- The game server is restarting — try again shortly. -

- -
- ) - } - if (error) return

{error}

- if (!roster) return

Loading…

- - const chars = roster.chars || [] - const shops = (vendors && vendors.vendors) || [] - - return ( -
-
-
Characters
- {chars.length === 0 ? ( -

No characters found.

- ) : ( -
- {chars.map((c) => ( -
- {c.name} - {c.online ? 'Online' : 'Offline'} -
- ))} -
- )} -
- {shops.length > 0 && ( -
-
Player vendors
-
- {shops.map((s) => ( -
- {s.shopName || 'Vendor'} - {Number(s.holdGold || 0).toLocaleString()}gp -
- ))} -
-
- )} -
- ) -} - // ── Shared bits ──────────────────────────────────────────────────────────── function Section({ title, children }) { return ( @@ -482,7 +311,7 @@ function Note({ msg, error }) { // ── Page ─────────────────────────────────────────────────────────────────── export default function PlayerAccount() { - const { logout, refresh } = useAuth() + const { refresh } = useAuth() const [account, setAccount] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState('') @@ -505,41 +334,22 @@ export default function PlayerAccount() { }, [load, refresh]) return ( -
-
-
- - - My Account - -
-
- - ← Site - - -
-
- -
- {loading && } - {error && } - {!loading && !error && account && ( - <> -
-

- Signed in as {account.username} - {account.email ? ` · ${account.email}` : ''} -

-
- - - - - - - )} -
-
+
+

Account

+ {loading && } + {error && } + {!loading && !error && account && ( + <> +

+ Signed in as {account.username} + {account.email ? ` · ${account.email}` : ''} +

+ + + + + + )} +
) } diff --git a/client/src/routes/player/PlayerCharacter.jsx b/client/src/routes/player/PlayerCharacter.jsx new file mode 100644 index 0000000..2ddf27e --- /dev/null +++ b/client/src/routes/player/PlayerCharacter.jsx @@ -0,0 +1,27 @@ +import { useParams, Link } from 'react-router-dom' +import { Loading, ErrorState } from '../../components/PageState.jsx' +import CharacterSheet from '../../components/CharacterSheet.jsx' +import { useAsync } from '../../lib/useAsync.js' +import { api } from '../../api/client.js' + +// A player's character sheet inside the portal. Character data is public MMO +// data, so it uses the same cached public endpoint the site does. +export default function PlayerCharacter() { + const { serial } = useParams() + const { loading, error, data } = useAsync(() => api.shard.char(serial), [serial]) + const restarting = error && error.status === 503 + + return ( +
+

+ + ← Back to characters + +

+ {loading && } + {restarting && } + {error && !restarting && } + {!loading && !error && data && } +
+ ) +} diff --git a/client/src/routes/player/PlayerCharacters.jsx b/client/src/routes/player/PlayerCharacters.jsx new file mode 100644 index 0000000..07cf03a --- /dev/null +++ b/client/src/routes/player/PlayerCharacters.jsx @@ -0,0 +1,166 @@ +import { useCallback, useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import { Loading, ErrorState } from '../../components/PageState.jsx' +import { api } from '../../api/client.js' + +// One-time-code linking form (shared by the empty state and "add another"). +function LinkForm({ onLinked, compact }) { + const [code, setCode] = useState('') + const [busy, setBusy] = useState(false) + const [msg, setMsg] = useState('') + const [error, setError] = useState('') + + async function submit(e) { + e.preventDefault() + setMsg(''); setError('') + if (!code.trim()) return + setBusy(true) + try { + const { account } = await api.player.shard.link(code.trim()) + setMsg(`Linked ${account}.`) + setCode('') + await onLinked() + } catch (err) { + setError(err.message || 'Could not link that code.') + } finally { + setBusy(false) + } + } + + return ( +
+ + + {msg && {msg}} + {error && {error}} +
+ ) +} + +// Roster of one linked account → character cards linking to the sheet. +function AccountRoster({ account }) { + const [roster, setRoster] = useState(null) + const [error, setError] = useState('') + const [unavailable, setUnavailable] = useState(false) + + const load = useCallback(async () => { + setError(''); setUnavailable(false) + try { + setRoster(await api.player.shard.roster(account)) + } catch (err) { + if (err.status === 503) setUnavailable(true) + else setError(err.message || 'Could not load this account.') + } + }, [account]) + useEffect(() => { load() }, [load]) + + if (unavailable) { + return ( +
+

The game server is restarting — try again shortly.

+ +
+ ) + } + if (error) return

{error}

+ if (!roster) return

Loading…

+ + const chars = roster.chars || [] + if (chars.length === 0) return

No characters on this account.

+ + return ( +
+ {chars.map((c) => ( + + + {(c.name || '?').charAt(0)} + +
+
{c.name}
+
{c.online ? 'Online' : 'Offline'}
+
+ + + ))} +
+ ) +} + +export default function PlayerCharacters() { + const [accounts, setAccounts] = useState(null) + const [error, setError] = useState('') + + const load = useCallback(async () => { + setError('') + try { + setAccounts(await api.player.shard.accounts()) + } catch { + setError('Could not load your game accounts.') + } + }, []) + useEffect(() => { load() }, [load]) + + if (error) return + if (!accounts) return + + // Not linked yet — prompt to link. + if (accounts.length === 0) { + return ( +
+

Your characters

+

+ You haven’t linked a game account yet. Link one to see your characters, stats, skills and vendors here. +

+
+
Link your game account
+

+ In game, type [link to get a one-time code, then enter it below. +

+ +
+
+ ) + } + + // Linked — show characters grouped by account. + return ( +
+
+

Your characters

+
+ +
+ {accounts.map((a) => ( +
+
+ {a.account} +
+ +
+ ))} +
+ +
+
Link another account
+ +
+
+ ) +} diff --git a/client/src/routes/player/PlayerLogin.jsx b/client/src/routes/player/PlayerLogin.jsx index 4958c46..8e054e6 100644 --- a/client/src/routes/player/PlayerLogin.jsx +++ b/client/src/routes/player/PlayerLogin.jsx @@ -20,7 +20,7 @@ export default function PlayerLogin() { const { user, login, loginTotp, ssoLoginTotp } = useAuth() const navigate = useNavigate() const location = useLocation() - const dest = location.state?.from?.pathname || '/account' + const dest = location.state?.from?.pathname || '/player' // A staff member who signs in here belongs in the admin shell, not the portal. const destFor = (u) => (u && u.role !== 'player' ? '/admin' : dest) diff --git a/client/src/routes/player/PlayerPortalLayout.jsx b/client/src/routes/player/PlayerPortalLayout.jsx new file mode 100644 index 0000000..4c6a878 --- /dev/null +++ b/client/src/routes/player/PlayerPortalLayout.jsx @@ -0,0 +1,54 @@ +import { NavLink, Link, Outlet, useNavigate } from 'react-router-dom' +import MoonDot from '../../components/MoonDot.jsx' +import { useAuth } from '../../contexts/AuthContext.jsx' + +// Shared shell for the logged-in player portal: a header with a nav bar +// (Characters / Account) and the page content in an . Matches the +// site's dark theme vocabulary. +const tab = ({ isActive }) => ({ + textDecoration: 'none', + fontFamily: 'var(--sans)', + fontSize: '0.9rem', + padding: '8px 4px', + color: isActive ? 'var(--head)' : 'var(--muted)', + borderBottom: `2px solid ${isActive ? 'var(--accent)' : 'transparent'}`, +}) + +export default function PlayerPortalLayout() { + const { user, logout } = useAuth() + const navigate = useNavigate() + + async function signOut() { + await logout() + navigate('/account/login', { replace: true }) + } + + return ( +
+
+
+
+ +
+
UOMysticmoon
+
Player Portal
+
+
+
+ {user?.username} + ← Site + +
+
+ +
+ +
+ +
+
+ ) +} diff --git a/client/src/routes/player/PlayerRegister.jsx b/client/src/routes/player/PlayerRegister.jsx index 9aaf2b1..4da1c5f 100644 --- a/client/src/routes/player/PlayerRegister.jsx +++ b/client/src/routes/player/PlayerRegister.jsx @@ -21,7 +21,7 @@ export default function PlayerRegister() { const [providers, setProviders] = useState([]) useEffect(() => { - if (user && user.role === 'player') navigate('/account', { replace: true }) + if (user && user.role === 'player') navigate('/player', { replace: true }) }, [user, navigate]) useEffect(() => { @@ -51,7 +51,7 @@ export default function PlayerRegister() { setBusy(true) try { await register(username.trim(), password, { email: email.trim() || undefined, company }) - navigate('/account', { replace: true }) + navigate('/player', { replace: true }) } catch (err) { if (err.status === 409) setError('That username is already taken.') else if (err.status === 403) setError('Registration is not open right now.') diff --git a/client/src/routes/public/Shard.jsx b/client/src/routes/public/Shard.jsx index d6e768a..9917524 100644 --- a/client/src/routes/public/Shard.jsx +++ b/client/src/routes/public/Shard.jsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom' import PublicLayout from '../../components/PublicLayout.jsx' import PageHeader from '../../components/PageHeader.jsx' import { Loading, ErrorState } from '../../components/PageState.jsx' @@ -75,9 +76,13 @@ function nameOf(who) { export default function Shard() { const { loading, error, data } = useAsync(() => - Promise.all([api.shard.status(), api.shard.feed({ kind: 'vendor.sale', limit: 8 }), api.shard.idoc(), api.shard.economy(60)]).then( - ([status, sales, idoc, economy]) => ({ status, sales, idoc, economy }), - ), + Promise.all([ + api.shard.status(), + api.shard.feed({ kind: 'vendor.sale', limit: 8 }), + api.shard.idoc(), + api.shard.economy(60), + api.shard.online(), + ]).then(([status, sales, idoc, economy, online]) => ({ status, sales, idoc, economy, online })), ) const { events, connected } = useShardFeed({ max: 30 }) @@ -141,6 +146,31 @@ export default function Shard() { + {/* Online now */} +
+
+ Online now +
+ {(!data.online || data.online.length === 0) ? ( +

No one is online right now.

+ ) : ( +
+ {data.online.map((p) => ( + + + {p.name || p.serial} + {p.map && · {p.map}} + + ))} +
+ )} +
+ {/* Economy sparkline */} {data.economy && data.economy.length > 1 && (
diff --git a/client/src/routes/public/ShardChar.jsx b/client/src/routes/public/ShardChar.jsx new file mode 100644 index 0000000..9104d85 --- /dev/null +++ b/client/src/routes/public/ShardChar.jsx @@ -0,0 +1,37 @@ +import { useParams, Link } from 'react-router-dom' +import PublicLayout from '../../components/PublicLayout.jsx' +import PageHeader from '../../components/PageHeader.jsx' +import { Loading, ErrorState } from '../../components/PageState.jsx' +import CharacterSheet from '../../components/CharacterSheet.jsx' +import { useAsync } from '../../lib/useAsync.js' +import { api } from '../../api/client.js' + +// Public character viewer: /site/shard/char/:serial. Renders the live sheet from +// the sidecar (cached server-side). A 503 means the shard is restarting. +export default function ShardChar() { + const { serial } = useParams() + const { loading, error, data } = useAsync(() => api.shard.char(serial), [serial]) + + const restarting = error && error.status === 503 + const notFound = error && error.status === 404 + + return ( + +
+ + +

+ + ← Back to shard + +

+ + {loading && } + {restarting && } + {notFound && } + {error && !restarting && !notFound && } + {!loading && !error && data && } +
+
+ ) +} diff --git a/server/src/router/v1/public/public.routes.js b/server/src/router/v1/public/public.routes.js index f741838..3f77764 100644 --- a/server/src/router/v1/public/public.routes.js +++ b/server/src/router/v1/public/public.routes.js @@ -162,6 +162,13 @@ publicRouter.get( validate, shard.getEconomy, ) +publicRouter.get( + '/shard/online', + // #swagger.tags = ['Public · Shard'] + // #swagger.summary = 'Players online now (name + serial + map only)' + /* #swagger.responses[200] = { description: 'Online players', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/ShardOnlinePlayer" } } } } } */ + shard.getOnline, +) publicRouter.get( '/shard/idoc', // #swagger.tags = ['Public · Shard'] diff --git a/server/src/router/v1/public/shard.controller.js b/server/src/router/v1/public/shard.controller.js index 5b09c04..14e72d7 100644 --- a/server/src/router/v1/public/shard.controller.js +++ b/server/src/router/v1/public/shard.controller.js @@ -69,6 +69,19 @@ async function getEconomy(req, res) { } } +// GET /public/shard/online — who is online now (redacted: name + serial + map, +// no coordinates, vitals or account). Feeds the public "online now" list, which +// links to the public character sheet. +async function getOnline(req, res) { + try { + const rows = await shardState.listOnline() + return res.json(rows.map((r) => ({ serial: r.serial, name: r.name, map: r.map }))) + } catch (err) { + log.error('shard.getOnline', err) + return res.status(500).json({ message: 'Internal Server Error' }) + } +} + // GET /public/shard/idoc — houses currently in danger (stage IDOC). async function getIdoc(req, res) { try { @@ -118,4 +131,4 @@ function stream(req, res) { broadcast.subscribe(req, res, 'public') } -module.exports = { getStatus, getFeed, getEconomy, getIdoc, getChar, stream } +module.exports = { getStatus, getFeed, getEconomy, getOnline, getIdoc, getChar, stream } diff --git a/server/swagger/swagger-output.json b/server/swagger/swagger-output.json index f7770f8..f91b8b0 100644 --- a/server/swagger/swagger-output.json +++ b/server/swagger/swagger-output.json @@ -1410,6 +1410,33 @@ } } }, + "/api/v1/public/shard/online": { + "get": { + "tags": [ + "Public · Shard" + ], + "summary": "Players online now (name + serial + map only)", + "description": "", + "responses": { + "200": { + "description": "Online players", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ShardOnlinePlayer" + } + } + } + } + }, + "500": { + "description": "Internal Server Error" + } + } + } + }, "/api/v1/public/shard/idoc": { "get": { "tags": [ @@ -10307,6 +10334,67 @@ } } }, + "ShardOnlinePlayer": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "object" + }, + "description": { + "type": "string", + "example": "A player online now (redacted for the public list)." + }, + "properties": { + "type": "object", + "properties": { + "serial": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "string" + }, + "example": { + "type": "string", + "example": "0x24C" + } + } + }, + "name": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "string" + }, + "example": { + "type": "string", + "example": "Darrow" + } + } + }, + "map": { + "type": "object", + "properties": { + "type": { + "type": "string", + "example": "string" + }, + "nullable": { + "type": "boolean", + "example": true + }, + "example": { + "type": "string", + "example": "Trammel" + } + } + } + } + } + } + }, "ShardHouse": { "type": "object", "properties": { diff --git a/server/swagger/swagger.js b/server/swagger/swagger.js index 56a1bfe..0d245b8 100644 --- a/server/swagger/swagger.js +++ b/server/swagger/swagger.js @@ -544,6 +544,15 @@ const doc = { t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 }, }, }, + ShardOnlinePlayer: { + type: 'object', + description: 'A player online now (redacted for the public list).', + properties: { + serial: { type: 'string', example: '0x24C' }, + name: { type: 'string', example: 'Darrow' }, + map: { type: 'string', nullable: true, example: 'Trammel' }, + }, + }, ShardHouse: { type: 'object', description: 'A house at its current decay stage.',