feat(site): phase 17 - per-game module pages, /runicnpc/, and checkTerms over the built site
All checks were successful
PR checks / checks (pull_request) Successful in 2m37s

PLAN.md §16 phase 17, built on the org lead's answers of 2026-10-22,
recorded as D75-D79 (count of record 79).

- D75: core marketing pages describe what a game module adds in core's
  words. "Game intelligence" became "What a game module adds"; each
  game's own list moved to src/data/games/ and renders only on the new
  /modules/uo/ and /modules/rust/. /modules/ is a card per game.
- The page-side capability check is per game (assertGameCoverage), the
  item phase 13 moved here.
- D76: the homepage keeps its game screenshot, labelled. screens.mjs
  entries may carry `game` (D67); Screenshot.astro and the app grid
  print the game's name and mark the figure data-game.
- D77: /app/ describes the Rust screens (released the same day).
- D78: /runicnpc/, the standalone page, is in the header and footer;
  the narrow header was re-measured so five links keep two rows.
- D79: checkTerms.mjs --built reads every built page's visible text
  after the build; game routes and data-game elements allow their
  game's words. Proven against ten plants. Still the twelfth check.
- Re-read fixes: /architecture/'s "two machines is also the maximum",
  Discord "guild" on /integrations/, and collection.mjs against what
  Steam sign-in and the Rust module keep (PLAY_DATA_SAFETY.md
  regenerated; the Play answers are unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-22 20:10:51 -05:00
parent ec65b2d5aa
commit 56473d4d51
32 changed files with 1924 additions and 451 deletions

View File

@@ -4,9 +4,9 @@
* ---------------------------------------------------------------------------------------
* WHY THIS IS NOT capabilities.mjs
* ---------------------------------------------------------------------------------------
* `capabilities.mjs` describes what a DEPLOYMENT does, and it is checked against
* `module-uo`'s manifest because the module declares its capabilities in a machine-readable
* file. The app declares nothing of the kind: what it does is a set of screens in
* `capabilities.mjs` and `games/` describe what a DEPLOYMENT does, and each game's list is
* checked against its module's manifest because a module declares its capabilities in a
* machine-readable file. The app declares nothing of the kind: what it does is a set of screens in
* `Routes.kt`, and there is no manifest to diff against. So these are written from that
* file and carry the route names, which is the closest thing to a citation available — a
* reader who wants to check a claim here has a file to open.
@@ -18,11 +18,19 @@
* ---------------------------------------------------------------------------------------
* "GATED BY WHAT THE DEPLOYMENT PUBLISHES" IS LOAD-BEARING, NOT A DISCLAIMER
* ---------------------------------------------------------------------------------------
* Almost everything here is conditional on the site the app is pointed at: shard screens
* Almost everything here is conditional on the site the app is pointed at: game screens
* appear only when the deployment runs a game module and has that visibility feature
* turned on, staff screens only for a staff account, push only where the operator runs an
* ntfy. A features list that omitted that would be describing an app nobody will see,
* because there is no default deployment — see `requiresDeployment` below.
*
* ---------------------------------------------------------------------------------------
* ONE ITEM PER GAME, NAMED BY THE GAME (phase 17, D75 and D77)
* ---------------------------------------------------------------------------------------
* `/app/` is a core page, so it names the games but not their vocabulary. The game screens
* are one item per game, titled with the game's name and described in core's words. The
* Rust screens (`ui/rust/` on `Android-app`) were on `edge` when this was written; the org
* lead released them the same day (D77), so the page describes them as shipped.
*/
/**
@@ -54,33 +62,29 @@ export const requiresDeployment = {
/** @type {{ heading: string, blurb: string, items: AppFeature[] }[]} */
export const appFeatures = [
{
heading: 'The shard, live',
heading: 'The game, live',
blurb:
'The same feed the website shows, on a phone. Every one of these appears only when ' +
'the deployment runs a game module and the operator has published that surface — ' +
'the visibility settings are per-feature and default to off.',
items: [
{
title: 'Status, and the boards',
title: 'Ultima Online: the server, its boards and its market',
body:
'Whether the game server is up, plus champion spawns, guilds, city governors and ' +
'houses as the shard reports them.',
gate: 'A game module, and the matching visibility feature',
'Whether the game server is up and what it reports — its champions, Teams, ' +
'governors and houses — plus the player marketplace down to a single vendor, the ' +
"published ruleset, the leaderboards and the creature atlas. Item names are " +
"resolved against the game's own string table, so they read the way they read in " +
'the client.',
gate: 'The Ultima Online module, per feature',
},
{
title: 'The player marketplace',
title: 'Rust: every server, live',
body:
'Player-run vendors and what is on their shelves, down to a single vendor. Item ' +
"names arrive as the game's own string ids and are resolved against its string " +
'table, so they read the way they read in the client.',
gate: 'A game module publishing the market',
},
{
title: 'Rules, leaderboards and the spawn atlas',
body:
"The deployment's published ruleset, its points and loyalty boards, and the " +
'creature atlas — what spawns where, and what it drops.',
gate: 'A game module, per feature',
'Every server the community runs, and a page for each with its feed, its ' +
'leaderboard, who is online, its history on the site, and its live map with the ' +
'layers you may see — positions refreshed every ten seconds while the map is open.',
gate: 'The Rust module, per feature',
},
],
},
@@ -117,10 +121,11 @@ export const appFeatures = [
'per device from the app.',
},
{
title: 'Your characters, vendors and houses',
title: 'Your own side of the game',
body:
'Character sheets, the vendors you run and the houses you own, visible to the ' +
'account they belong to and to nobody else.',
'On Ultima Online, your character sheets, the vendors you run and the houses you ' +
'own; on Rust, the Steam accounts you have linked, your coins and your backpack. ' +
'Visible to the account they belong to and to nobody else.',
gate: 'A game module',
},
],

View File

@@ -16,23 +16,17 @@
* the homepage without an argument to go with it.
*
* ---------------------------------------------------------------------------------------
* THE PART THAT IS A CHECK, NOT A LIST
* THE ONE GROUP CORE DOES NOT SUPPLY, IN CORE'S WORDS
* ---------------------------------------------------------------------------------------
* "Game intelligence" is the only group core does not supply — it comes from whichever
* module is installed, and today that is `module-uo`. Its items therefore carry the
* capability slugs the module actually declares in its `module.json`, and
* `assertCapabilityCoverage()` fails the build if the two lists drift apart.
* "What a game module adds" is the only group core does not supply — it comes from
* whichever module is installed. Until phase 17 it was "Game intelligence" and listed the
* Ultima Online module's features in that game's words, on the homepage and `/features/`.
* Two modules exist now, and D63 keeps a game's vocabulary on its own pages, so the group
* says what BOTH modules add, in core's words, and each game's real list lives in
* `src/data/games/` and on its page under `/modules/` (D75).
*
* That closes a real gap. `platform.json` holds `games.uo.module.capabilities` and
* `scripts/checkFacts.mjs` re-reads it from the module's manifest on every build — so the
* day `module-uo` gains a capability, the JSON goes red and someone updates it. Before this
* function, updating the JSON was the end of it and the page kept the old list. Now the
* page is what goes red next.
*
* Note that slugs are NOT one-per-item in either direction: `shard` is the source of four
* separate user-facing capabilities, and the marketplace draws on `market` and `cliloc`
* together (item names arrive as cliloc ids and are resolved against the shard's own
* string table). The check is coverage in both directions, not a bijection.
* The capability check moved with the lists: `assertGameCoverage()` in `games/index.mjs`
* holds each game's features to the capabilities its module declares, in both directions.
*
* ---------------------------------------------------------------------------------------
* `needsModule` — THE THIRD STATE, WHICH PHASE 3 DID NOT HAVE
@@ -47,8 +41,8 @@
* the tables, the roster resolver, the forums, the notification streams, the Discord bridge,
* the voice channels, the activity feed and `/admin/teams` — and cannot *originate* a Team.
* They arrive from the installed module, which is exactly the point: core does not own the
* word for a Team, so `module-uo` calls them guilds and builds the pages, and a future
* module can call them something else on the same primitive.
* word for a Team, so each module calls them what its game calls them and builds the pages
* on the same primitive.
*
* So `needsModule` marks an item that is core machinery a module has to populate. On a bare
* core it is present, correct and permanently empty. `/` renders the requalified group
@@ -68,11 +62,13 @@
* character sheets are reachable only by the account they belong to, and a Team forum lives
* behind a Team id no static page can know. Paths are read from the real route tables —
* core's `client/src/App.jsx` and `module-uo`'s `client/src/entry.jsx` on `main` — never
* guessed. Note the module's public pages are namespaced under its own id, so a UO route is
* `/uo/…`; a deployment running a different module would deep-link somewhere else, which is
* why these sit beside the `caps` slugs on the module-supplied items.
* guessed. A module's public pages are namespaced under its own id, so a route into the
* Ultima Online module is `/uo/…`: the demo §15 plans runs that game, which is why the
* Teams item points there and the module-supplied items in `games/` carry their own.
*/
import { games } from './games/index.mjs';
/**
* The shape of a capability, written out because TypeScript otherwise infers it per group
* from whichever fields that group's items happen to use — and then `/features/` cannot
@@ -83,7 +79,7 @@
* @typedef {object} Capability
* @property {string} label What it is called, on every page that lists it.
* @property {string} detail The argument for it. `/features/` only; see D20.
* @property {string[]} [caps] Module capability slugs, on module-supplied items only.
* @property {string[]} [caps] Module capability slugs, on a game's own items (`games/`) only.
* @property {boolean} [needsModule] Core machinery a module has to populate (D24).
* @property {string} [demoPath] A stable public route, deep-linked into a demo (D25).
*
@@ -124,8 +120,8 @@ const community = {
'A roster, a leader, a private forum, its own notification streams and a Discord ' +
'voice channel, all hanging off one group. Core owns every part of that machinery ' +
'and deliberately cannot create a Team: they arrive from the installed module, ' +
'which is how a guild inside the game becomes a Team on the site — and why a ' +
'different game can call them something else without core learning a new word.',
'which is how a group the game already has becomes a Team on the site — and why ' +
'each game keeps its own name for one without core learning a new word.',
},
{
label: 'Team forums',
@@ -178,102 +174,66 @@ const community = {
};
/**
* Game intelligence — module-supplied. The `caps` arrays are the contract with
* `platform.json`; see `assertCapabilityCoverage` below.
* What a game module adds — module-supplied, and written in core's words (D63, D75). Each
* item is something BOTH modules supply, each in its own game's way; the game's own list,
* in its own words, is on its page under `/modules/`, which `/features/` and the homepage
* link to. No `caps` here: they belong to a game's own items in `src/data/games/`, where
* `assertGameCoverage()` checks them.
*
* @type {CapabilityGroup}
*/
const gameIntelligence = {
id: 'game-intelligence',
title: 'Game intelligence',
const gameModule = {
id: 'game-module',
title: 'What a game module adds',
moduleSupplied: true,
summary:
'Supplied by the installed game module, not by the core site. Two modules exist, for ' +
'Ultima Online and for Rust; this is what the Ultima Online module publishes from a ' +
'live game server, and the Rust module brings its own set in its place.',
'Ultima Online and for Rust, and each adds its own set from a live game server — these ' +
'are the parts they share.',
items: [
{
label: 'Live server status',
caps: ['shard'],
demoPath: '/uo/shard',
detail:
'Whether the server is up, who is on it, and how long ago the site last heard from ' +
'it. When the game is down this page is the thing that says so — the site does not ' +
'go down with it.',
'Whether each game server is up, who is on it, and how long ago the site last heard ' +
'from it. When the game is down the site is the thing that says so — it does not go ' +
'down with it.',
},
{
label: 'Economy and activity',
caps: ['shard'],
demoPath: '/uo/shard/activity',
label: 'A live feed of the world',
detail:
'A live feed of what is happening in the world, and the economy underneath it. ' +
'Every event passes an allowlist before it can reach a public page; staff read a ' +
'second stream carrying the rest.',
'What is happening in the game, as it happens. Every kind of event passes an ' +
'allowlist the module holds before it can reach a public page; staff read a second ' +
'stream carrying the rest.',
},
{
label: 'Character sheets',
caps: ['shard'],
label: 'Leaderboards',
detail:
'Skills, stats and equipment, drawn from the live world rather than from a form ' +
'somebody filled in. Reachable by the account the character is linked to, and by ' +
'staff — not by the public.',
'The numbers the game already keeps, published without anyone exporting a ' +
'spreadsheet on a Sunday — and kept by the site after the game has moved on.',
},
{
label: 'Points and loyalty boards',
caps: ['shard'],
demoPath: '/uo/leaderboards',
label: 'Maps of the world',
detail:
'The leaderboards the game already keeps, published without anyone exporting a ' +
'spreadsheet on a Sunday.',
'Where things are, drawn from the server rather than from someone else’s idea of the ' +
'game: one module maps what spawns where, the other shows what is moving now.',
},
{
label: 'Player-vendor marketplace',
caps: ['market', 'cliloc'],
demoPath: '/uo/market',
label: 'Teams, from the game',
detail:
'Every player vendor on the server and what is on it, searchable without logging ' +
'in to the game. Item names arrive from the world as numeric ids and are resolved ' +
"against the game's own string table, so they read as names rather than numbers — " +
"beside the item's own picture, in the colour it was dyed.",
'The groups players already form in the game arrive as Teams on the site, with the ' +
'forum, notifications and voice channel core attaches to one.',
},
{
label: 'Houses and IDOC decay',
caps: ['houses'],
demoPath: '/uo/houses',
label: 'Players’ own game accounts',
detail:
'Who owns what and where it stands, including which houses are decaying — ' +
'published while it is still information rather than after it has become a rumour.',
'A player links their game identity to their site account and sees their own side ' +
'of the game on it. Who may see another player at all is the operator’s choice.',
},
{
label: 'Spawn atlas',
caps: ['atlas'],
demoPath: '/uo/atlas',
label: 'Game steps for scheduled events',
detail:
"A bestiary and spawn map built by reading your shard's own spawn tables, so it " +
"describes your server rather than someone else's idea of the game. Regions, " +
'landmarks and champion altars come with it, and each creature is shown as the ' +
'artwork your own client draws it with.',
},
{
label: 'Champion boards',
caps: ['champs'],
demoPath: '/uo/champs',
detail: 'Which altars are running, how far along they are, and what turned up.',
},
{
label: 'Guilds',
caps: ['guilds'],
demoPath: '/uo/guilds',
detail:
'Guild rosters and standings, kept in step with the game. This is also what fills ' +
'the Teams primitive above: a guild in the world becomes a Team on the site, with ' +
"the forum, the notifications and the voice channel that core attaches to one.",
},
{
label: 'City governors',
caps: ['governors'],
demoPath: '/uo/governors',
detail: 'Who holds which city, and what they did with it.',
'What an event may do inside the world comes from the module: core runs the ' +
'calendar, the budget and the undo, and the module brings the steps.',
},
],
};
@@ -372,7 +332,7 @@ const integration = {
{
label: 'Discord: slash commands, notifications, voice',
detail:
'A bot for the guild you already have. Commands answer from your site, ' +
'A bot for the Discord server you already have. Commands answer from your site, ' +
'notifications bridge into channels, and a Team can be granted a voice channel ' +
'that maintains its own membership.',
},
@@ -429,7 +389,7 @@ const infrastructure = {
export const capabilityGroups = [
community,
gameIntelligence,
gameModule,
administration,
integration,
infrastructure,
@@ -456,46 +416,11 @@ export function capabilityGroup(id) {
);
}
/**
* Fails the build when the module's declared capabilities and this page's list disagree.
*
* Called from the component rather than from a check script on purpose: the failure needs
* to reach whoever is editing the page, and an Astro build error names the component. It
* also means the rule cannot be skipped by running `astro build` without `npm run verify`.
/*
* The capability check that lived here — "Game intelligence" against the module's declared
* capabilities — moved to `assertGameCoverage()` in `games/index.mjs` in phase 17, with the
* lists it checks.
*/
export function assertCapabilityCoverage(declared) {
const claimed = new Set();
for (const item of gameIntelligence.items) {
for (const cap of item.caps || []) claimed.add(cap);
}
const known = new Set(declared);
const unlisted = declared.filter((cap) => !claimed.has(cap));
const invented = [...claimed].filter((cap) => !known.has(cap));
if (!unlisted.length && !invented.length) return;
const lines = [];
if (unlisted.length) {
lines.push(
`the installed module declares ${unlisted.map((c) => `"${c}"`).join(', ')}, which no ` +
`capability on the homepage claims — the site is under-selling what it can show.`
);
}
if (invented.length) {
lines.push(
`the homepage claims ${invented.map((c) => `"${c}"`).join(', ')}, which the module no ` +
`longer declares — the site is advertising something that is gone (§1).`
);
}
throw new Error(
`src/data/capabilities.mjs disagrees with platform.json's games.uo.module.capabilities:\n` +
lines.map((line) => ` - ${line}`).join('\n') +
`\n\nUpdate the "Game intelligence" items, or the JSON if the module itself changed.\n`
);
}
/**
* Fails the build when a capability has no `detail`.
@@ -505,8 +430,9 @@ export function assertCapabilityCoverage(declared) {
* asymmetry between the two renderings is the whole of D20, and this is what keeps the
* thinner one from being the only one anybody notices.
*
* Called from `/features/` for the same reason `assertCapabilityCoverage` is called from
* the homepage: the build error should name the page that would have shipped wrong.
* Called from `/features/` for the same reason `assertGameCoverage` is called from the
* homepage: the build error should name the page that would have shipped wrong. It also
* reads each game's own list, which its page under `/modules/` renders with the detail too.
*/
export function assertDetailCoverage() {
const missing = [];
@@ -515,6 +441,11 @@ export function assertDetailCoverage() {
if (!item.detail?.trim()) missing.push(`${group.title} → ${item.label}`);
}
}
for (const game of games) {
for (const item of game.features) {
if (!item.detail?.trim()) missing.push(`${game.name} → ${item.label}`);
}
}
if (!missing.length) return;

View File

@@ -284,7 +284,7 @@ export const collected = [
scope: 'app',
title: 'Everything you read and post in the app',
body:
'Forum posts, Team activity, character and shard information, notification ' +
'Forum posts, Team activity, what the game module shows you, notification ' +
'preferences: all of it is a live read or write against the deployment. Apart from ' +
'the notification snapshot described in the next entry, nothing is cached for ' +
'offline use and nothing is duplicated anywhere else — the app with no signal is ' +
@@ -376,9 +376,12 @@ export const collected = [
body:
'A username, a password hash, an optional email address, the role, and — where the ' +
'operator has enabled it — a two-factor secret and single-use recovery codes. The ' +
'time and IP address of the last sign-in are stored on the account row.',
'time and IP address of the last sign-in are stored on the account row. A sign-in ' +
'identity linked to the account — Google, Discord, an OIDC provider or Steam — keeps ' +
'that provider’s id for the person; a Steam link also keeps the Steam display name ' +
'and the address of the Steam avatar. Linked identities are deleted with the account.',
retention: { summary: 'Set by the operator; nothing expires on its own' },
source: 'website server/db/schema.sql — users',
source: 'website server/db/schema.sql — users, user_identities',
},
{
id: 'deploy-sessions',
@@ -505,12 +508,23 @@ export const collected = [
scope: 'deployment',
title: 'Game data from the connected server',
body:
'Where a game module is installed, information about characters, guilds, houses and ' +
'the in-game economy flows from the game server to the site through the bridge. ' +
'Which of it is visible to the public is the operator’s decision, made in the ' +
'admin panel — the bridge itself forwards, and the site decides.',
retention: { summary: 'Operator-configured' },
source: 'docs/link/v7.md — the visibility framework',
'Where a game module is installed, what happens on the game server flows to the site ' +
'through the bridge, and the module keeps it. With Ultima Online that is characters ' +
'and their accounts, Teams, houses and the in-game economy. With Rust it is each ' +
'player’s Steam id and in-game name, their statistics, who is online, and the ' +
'server’s event record — which includes in-game chat, player reports and the IP ' +
'addresses on sign-in attempts and bans, readable by staff only. A linked Steam ' +
'account is kept against the site account and deleted with it. Which of any of this ' +
'the public sees is the operator’s decision, made in the admin panel — the bridge ' +
'forwards, and the site decides.',
retention: {
summary:
'Operator-configured. The Rust module keeps its event record 30 days, and its ' +
'sidecar keeps its own copy 14 days by default; statistics until the operator removes them',
},
source:
'docs/link/v7.md — the visibility framework; Module-Rust server/db/schema.sql, ' +
'server/catalogue.js, server/boot.js (EVENT_RETENTION_DAYS); Rust-Link sidecar/src/config.rs',
},
];

View File

@@ -30,6 +30,7 @@ export function footerColumns(brand) {
{ href: '/features/', label: 'Features' },
{ href: '/architecture/', label: 'Architecture' },
{ href: '/modules/', label: 'Modules' },
{ href: '/runicnpc/', label: 'RunicNPC' },
{ href: '/app/', label: 'Android app' },
],
},

104
src/data/games/index.mjs Normal file
View File

@@ -0,0 +1,104 @@
/**
* games/index.mjs — the games the platform supports, and the check that holds each one's
* feature list to its module's declared capabilities.
*
* PLAN.md §16, phase 17. Each game has a file of its own (`uo.mjs`, `rust.mjs`) written in
* that game's words, which only that game's pages render. This file is what the core pages
* read instead: the game's name and where its page is, which D63 lets any page say.
*
* The order is the order the games were supported in, and every list of games on the site
* follows it.
*/
import platform from '../platform.json' with { type: 'json' };
import * as uo from './uo.mjs';
import * as rust from './rust.mjs';
/**
* @typedef {object} Game
* @property {'uo'|'rust'} key A key of `platform.json`'s `games`.
* @property {string} name The game's own name, from `platform.json`.
* @property {string} href Its marketing page.
* @property {string} docs Its docs group.
* @property {string} release Its module's release, from `platform.json`.
* @property {string} blurb One sentence in core's words, for the core pages' game cards.
* @property {import('../capabilities.mjs').Capability[]} features
*/
/** @type {Game[]} */
export const games = [
{
key: 'uo',
name: platform.games.uo.name,
href: '/modules/uo/',
docs: '/docs/ultima-online/',
release: platform.releases['Module-uo'],
blurb:
'The first module, and the one the seam was cut around: the server’s live state, its ' +
'economy and its players’ own groups, extracted from the site without changing a URL.',
features: uo.features,
},
{
key: 'rust',
name: platform.games.rust.name,
href: '/modules/rust/',
docs: '/docs/rust/',
release: platform.releases['Module-Rust'],
blurb:
'Any number of servers, each with its own sidecar: a live feed, leaderboards that ' +
'outlast a reset, a live map, and permissions and events run from the site.',
features: [...rust.features, ...rust.staffFeatures],
},
];
for (const key of Object.keys(platform.games)) {
if (!games.some((game) => game.key === key)) {
throw new Error(`src/data/games/index.mjs has no entry for "${key}", which platform.json's games declares.`);
}
}
/** One game by key, or a build failure naming it. @param {string} key */
export function gameByKey(key) {
const game = games.find((candidate) => candidate.key === key);
if (game) return game;
throw new Error(`src/data/games/index.mjs has no game "${key}". Known: ${games.map((g) => g.key).join(', ')}.`);
}
/**
* Fails the build when a game's feature list and its module's declared capabilities
* disagree, in either direction.
*
* Called from the pages rather than a check script, for the reason phase 3 gave: the failure
* should reach whoever is editing the page, and an Astro build error names the component. It
* also means `astro build` alone cannot skip it.
*
* @param {string} key
*/
export function assertGameCoverage(key) {
const game = gameByKey(key);
const declared = platform.games[key].module.capabilities;
const claimed = new Set(game.features.flatMap((item) => item.caps || []));
const unlisted = declared.filter((cap) => !claimed.has(cap));
const invented = [...claimed].filter((cap) => !declared.includes(cap));
if (!unlisted.length && !invented.length) return;
const lines = [];
if (unlisted.length) {
lines.push(
`the module declares ${unlisted.map((c) => `"${c}"`).join(', ')}, which no feature on ` +
`its page claims — the site is under-selling what it can show.`,
);
}
if (invented.length) {
lines.push(
`the page claims ${invented.map((c) => `"${c}"`).join(', ')}, which the module no ` +
`longer declares — the site is advertising something that is gone (§1).`,
);
}
throw new Error(
`src/data/games/${key}.mjs disagrees with platform.json's games.${key}.module.capabilities:\n` +
lines.map((line) => ` - ${line}`).join('\n') +
`\n\nUpdate the features in src/data/games/${key}.mjs, or the JSON if the module itself changed.\n`,
);
}

122
src/data/games/rust.mjs Normal file
View File

@@ -0,0 +1,122 @@
/**
* games/rust.mjs — what the Rust module adds, in Rust's own words.
*
* PLAN.md §16, phase 17. `/modules/rust/` renders this; the core pages describe what a game
* module adds in core's words and link there (D75). It is the Rust half of the coverage
* assertion phase 13 moved here: `caps` is the contract with `platform.json`'s
* `games.rust.module.capabilities`, re-read by `checkFacts.mjs` from `Module-Rust`'s own
* `module.json`, and `assertGameCoverage()` in `index.mjs` holds the two together.
*
* Every item was written from the Rust docs group, which phase 15 walked on the walk site
* and checked against the module's source (D36) — this file says less than those pages and
* nothing they do not. `rust` names the module itself rather than a surface (Module-Rust's
* README: "the single gate a whole navigation group hangs on"), so it rides on the server
* list, which is the page that gate opens.
*
* The staff items carry no `caps`: a capability is what a client feature-detects on, and
* the admin screens are not something any client detects.
*
* No `demoPath` anywhere: the demo instance planned in §15 runs Ultima Online.
*/
/** @typedef {import('../capabilities.mjs').Capability} Capability */
/** For visitors and players. @type {Capability[]} */
export const features = [
{
label: 'Every server, live',
caps: ['rust', 'servers'],
detail:
'Every Rust server the community runs, as it last reported itself: up or down, who is ' +
'on, which map, and when the next wipe is. One sidecar serves one server, so a ' +
'community with six servers sees six — and a server that is down says so while the ' +
'site carries on.',
},
{
label: 'The kill feed',
caps: ['killfeed'],
detail:
'Kills, chat, comings and goings, RunicNPC bosses, and the world itself — cargo, ' +
'helicopters, airdrops — as they happen. Every kind passes a default-deny allowlist ' +
'before it reaches a public page; staff read the rest, connection records and player ' +
'reports included.',
},
{
label: 'Leaderboards, per wipe',
caps: ['leaderboard'],
detail:
'Kills, deaths, NPC kills, structures and time played, for one wipe or all-time — with ' +
'the chat titles players have earned. The record survives the wipe: the game forgets, ' +
'and the site does not.',
},
{
label: 'Who is online',
caps: ['presence'],
detail:
'Who is on right now, and who is asleep. Nothing names a player to the public by ' +
'default: how far a name reaches — staff, signed-in players, everyone — is the ' +
'operator’s choice, made per surface.',
},
{
label: 'Wipes',
caps: ['wipes'],
detail:
'Every wipe the site has seen, and the next one once the server has a wipe schedule — ' +
'the monthly forced wipe or a rule of your own.',
},
{
label: 'The live map',
caps: ['map'],
detail:
'The server’s own map picture with layers on it: monuments and world events, the ' +
'zones and NPCs your events placed, players, bases as positions only, and a player’s ' +
'own clan. Each layer has its own audience, and positions are never stored.',
},
{
label: 'Steam accounts, linked',
caps: ['identity'],
detail:
'A player joins their Steam account to their site account with a code from /link in ' +
'game, or by signing in with Steam. Staff can undo a wrong link. Rewards go to Steam ' +
'ids, so a player earns them before they ever link.',
},
{
label: 'Clans are Teams',
detail:
'Each clan on a server is a Team on the site: a page, a feed for its members and ' +
'core’s Team notifications. Who may see a clan’s roster is set alongside who may see ' +
'a name at all.',
},
];
/** For staff, one per admin screen the module adds. @type {Capability[]} */
export const staffFeatures = [
{
label: 'Permissions and groups, from the site',
detail:
'The site holds every server’s Oxide or Carbon groups and permissions and pushes them ' +
'in. One vip on the site is one vip in game, on the servers it covers, and a change ' +
'made in game is noticed rather than silently kept.',
},
{
label: 'Mod config, from the site',
detail:
'Edit your plugins’ config files on the game host from the admin panel, with a backup ' +
'first, a watched reload, and an automatic undo if the plugin does not come back.',
},
{
label: 'Events, zones and rewards',
detail:
'Rust steps for core’s scheduled events: open a ZoneManager zone with a dome, place ' +
'NPCs, count who is present, reward a kit, pay coins, start a raid base, or run ' +
'console commands. Every step arrives switched off, on the server as well as on the ' +
'site.',
},
{
label: 'Optional plugins, recognised',
detail:
'BetterChat, Economics, Backpacks, TruePVE, Dynamic PVP, Dangerous Treasures, Guarded ' +
'Crate, RaidableBases and more each add something where they are installed: coins, ' +
'titles, map layers, event steps. None is required.',
},
];

117
src/data/games/uo.mjs Normal file
View File

@@ -0,0 +1,117 @@
/**
* games/uo.mjs — what the Ultima Online module adds, in Ultima Online's own words.
*
* PLAN.md §16, phase 17. Until this phase these items were the "Game intelligence" group of
* `capabilities.mjs`, which the homepage and `/features/` render — core pages, which D63
* says may name the games but not use their vocabulary. They moved here, to a file that
* belongs to one game, and `/modules/uo/` renders them. The core pages now describe what a
* game module adds in core's words and link here (D75).
*
* `caps` is the contract with `platform.json`'s `games.uo.module.capabilities`, which
* `checkFacts.mjs` re-reads from the module's own `module.json`. `assertGameCoverage()` in
* `index.mjs` fails the build when the two drift apart, in either direction — so the day the
* module gains a capability, the JSON goes red, and the day the JSON moves, this page does.
*
* Slugs are NOT one-per-item: `shard` is the source of four separate user-facing features,
* and the marketplace draws on `market` and `cliloc` together (item names arrive as cliloc
* ids and are resolved against the shard's own string table). Coverage, not a bijection.
*
* `demoPath` is a stable public route on the module's own pages, deep-linked into a demo
* when one is mounted (D25). Paths are read from `module-uo`'s `client/src/entry.jsx`.
*/
/** @typedef {import('../capabilities.mjs').Capability} Capability */
/** @type {Capability[]} */
export const features = [
{
label: 'Live shard status',
caps: ['shard'],
demoPath: '/uo/shard',
detail:
'Whether the shard is up, who is on it, and how long ago the site last heard from it. ' +
'When the game is down this page is the thing that says so — the site does not go ' +
'down with it.',
},
{
label: 'Economy and activity',
caps: ['shard'],
demoPath: '/uo/shard/activity',
detail:
'A live feed of what is happening in the world, and the economy underneath it. Every ' +
'event passes an allowlist before it can reach a public page; staff read a second ' +
'stream carrying the rest.',
},
{
label: 'Character sheets',
caps: ['shard'],
detail:
'Skills, stats and equipment, drawn from the live world rather than from a form ' +
'somebody filled in. Reachable by the account the character is linked to, and by ' +
'staff — not by the public.',
},
{
label: 'Points and loyalty boards',
caps: ['shard'],
demoPath: '/uo/leaderboards',
detail:
'The leaderboards the shard already keeps, published without anyone exporting a ' +
'spreadsheet on a Sunday.',
},
{
label: 'Player-vendor marketplace',
caps: ['market', 'cliloc'],
demoPath: '/uo/market',
detail:
'Every player vendor on the shard and what is on it, searchable without logging in to ' +
'the game. Item names arrive from the world as cliloc ids and are resolved against the ' +
"shard's own string table, so they read as names rather than numbers — beside the " +
"item's own picture, in the colour it was dyed.",
},
{
label: 'Houses and IDOC decay',
caps: ['houses'],
demoPath: '/uo/houses',
detail:
'Who owns what and where it stands, including which houses are decaying — published ' +
'while it is still information rather than after it has become a rumour.',
},
{
label: 'Spawn atlas',
caps: ['atlas'],
demoPath: '/uo/atlas',
detail:
"A bestiary and spawn map built by reading your shard's own spawn tables, so it " +
"describes your server rather than someone else's idea of the game. Regions, " +
'landmarks and champion altars come with it, and each creature is shown as the ' +
'artwork your own client draws it with.',
},
{
label: 'Champion boards',
caps: ['champs'],
demoPath: '/uo/champs',
detail: 'Which altars are running, how far along they are, and what turned up.',
},
{
label: 'Guilds',
caps: ['guilds'],
demoPath: '/uo/guilds',
detail:
'Guild rosters and standings, kept in step with the shard. Each guild is also a Team ' +
'on the site, with the forum, the notifications and the voice channel that core ' +
'attaches to one.',
},
{
label: 'City governors',
caps: ['governors'],
demoPath: '/uo/governors',
detail: 'Who holds which city, and what they did with it.',
},
];
/**
* The screenshots `/modules/uo/` shows beside the list, by id from `screens.mjs`. Every one
* is tagged `game: 'uo'` there, so it prints the game's name beside it (D67) wherever it is
* shown. They lived on `/features/` until phase 17.
*/
export const screens = ['shard-status', 'marketplace', 'spawn-atlas', 'guilds', 'houses'];

View File

@@ -40,6 +40,15 @@
* the screenshots show the platform, not somebody's private shard.
*
* Nothing here is a mock-up, and nothing here was drawn.
*
* ---------------------------------------------------------------------------------------
* A SCREENSHOT BELONGS TO ITS GAME (D67)
* ---------------------------------------------------------------------------------------
* A capture of one game's screen shows that game's words in its pixels, and its alt text has
* to describe the image honestly. So an entry may carry `game`: its alt and caption may then
* use that game's words, and `Screenshot.astro` prints the game's name beside it wherever it
* appears, marking the figure `data-game` for `checkTerms.mjs --built`. A core screen — the
* news, the dashboard, the modules list — carries no tag.
*/
/** Web capture geometry. The capture script and the check both read these. */
@@ -63,6 +72,7 @@ export const PHONE = { width: 1440, height: 3088 };
* @property {string} alt What the screen shows, for somebody who cannot see it.
* @property {string} caption The sentence printed under the figure.
* @property {'web'|'phone'} family Which geometry the file follows.
* @property {'uo'|'rust'} [game] The game whose screen this is (D67), a key of `platform.json`'s `games`.
*/
/** @type {Screen[]} */
@@ -70,6 +80,7 @@ export const screens = [
// ── The product's public surfaces ───────────────────────────────────────────────────
{
id: 'shard-status',
game: 'uo',
route: '/uo/shard',
admin: false,
alt: 'The shard page of a Runic Gateway site, showing the shard online, its gold supply, the state of the shard link, and one player online in Britain.',
@@ -79,6 +90,7 @@ export const screens = [
},
{
id: 'marketplace',
game: 'uo',
route: '/uo/market',
admin: false,
scrollY: 470,
@@ -89,6 +101,7 @@ export const screens = [
},
{
id: 'spawn-atlas',
game: 'uo',
route: '/uo/atlas',
admin: false,
scrollY: 466,
@@ -99,6 +112,7 @@ export const screens = [
},
{
id: 'guilds',
game: 'uo',
route: '/uo/guilds',
admin: false,
alt: 'The guilds page, listing each guild on the shard with its abbreviation, how many members are online, and its leader.',
@@ -108,6 +122,7 @@ export const screens = [
},
{
id: 'houses',
game: 'uo',
route: '/uo/houses',
admin: false,
alt: 'The houses page, listing homes that have entered their final decay stage with their owner and location.',
@@ -145,6 +160,7 @@ export const screens = [
},
{
id: 'admin-shard',
game: 'uo',
route: '/admin/uo/link',
admin: true,
alt: 'The shard connection screen, showing the sidecar connected, the websocket ingest live, and fields for the sidecar URL, token and protocol version.',
@@ -154,6 +170,7 @@ export const screens = [
},
{
id: 'admin-client-files',
game: 'uo',
route: '/admin/uo/files',
admin: true,
alt: 'The client files screen, showing the creature portraits section: 1,095 pictures held of 1,095 catalogued, 746 of 800 creatures matched, the last import and the extractor version, above Update and Re-import everything buttons.',
@@ -195,6 +212,7 @@ export const screens = [
},
{
id: 'app-shard',
game: 'uo',
route: '/shard',
admin: false,
alt: "The app's shard screen, showing the shard online with one player and its gold supply, links to champion spawns, guilds, governors and falling houses, and a live activity feed.",
@@ -204,6 +222,7 @@ export const screens = [
},
{
id: 'app-market',
game: 'uo',
route: '/market',
admin: false,
alt: "The app's marketplace, listing weapons for sale with their price, the shop selling them and where it stands.",