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
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:
@@ -54,8 +54,8 @@ import { playPolicy } from '../data/beta.mjs';
|
||||
*/
|
||||
const title = 'The Android app';
|
||||
const description =
|
||||
'A native Android client for a Runic Gateway deployment: the shard, the site and your ' +
|
||||
'account, themed by whichever community you point it at.';
|
||||
'A native Android client for a Runic Gateway deployment: the game, the site and your ' +
|
||||
'account, themed by whichever community you point it at — Ultima Online or Rust.';
|
||||
|
||||
const apk = platform.androidApk;
|
||||
const release = platform.releases['Android-app'];
|
||||
|
||||
@@ -3,13 +3,10 @@ import Base from '../layouts/Base.astro';
|
||||
import PageHeader from '../components/PageHeader.astro';
|
||||
import NotBuilt from '../components/NotBuilt.astro';
|
||||
import Screenshot from '../components/Screenshot.astro';
|
||||
import SupportedGames from '../components/SupportedGames.astro';
|
||||
|
||||
import platform from '../data/platform.json';
|
||||
import {
|
||||
capabilityGroups,
|
||||
assertCapabilityCoverage,
|
||||
assertDetailCoverage,
|
||||
} from '../data/capabilities.mjs';
|
||||
import { capabilityGroups, assertDetailCoverage } from '../data/capabilities.mjs';
|
||||
import { games, assertGameCoverage } from '../data/games/index.mjs';
|
||||
|
||||
/**
|
||||
* `/features/` — PLAN.md §13 phase 4, built to D20.
|
||||
@@ -24,8 +21,8 @@ import {
|
||||
* a thing somebody has to notice.
|
||||
*
|
||||
* Both assertions below run at build time and both name this page in their message.
|
||||
* `assertCapabilityCoverage` is the module contract the homepage also runs — repeated here
|
||||
* deliberately, since either page can be built alone and each should fail on its own.
|
||||
* `assertGameCoverage` is the module contract the homepage and each game page also run —
|
||||
* repeated here deliberately, since any page can be built alone and each should fail on its own.
|
||||
* `assertDetailCoverage` is this page's own: a capability with no detail renders as a
|
||||
* heading with nothing under it, and nothing else in the repo would notice.
|
||||
*
|
||||
@@ -53,7 +50,7 @@ import {
|
||||
* matches bytes, and a stock build hides every one of these links, so a mistake here is
|
||||
* invisible until the day somebody configures a demo.
|
||||
*/
|
||||
assertCapabilityCoverage(platform.games.uo.module.capabilities);
|
||||
for (const game of games) assertGameCoverage(game.key);
|
||||
assertDetailCoverage();
|
||||
|
||||
/**
|
||||
@@ -66,19 +63,21 @@ assertDetailCoverage();
|
||||
* the container runs and who owns the database, and a picture of a web page says nothing
|
||||
* true about either.
|
||||
*
|
||||
* The module-supplied group has none since phase 17: its screenshots were of one game's
|
||||
* screens, and they moved with that game's list to its page under `/modules/` (D75).
|
||||
*
|
||||
* `Screenshot` throws on an id that `screens.mjs` does not declare, so a typo here fails the
|
||||
* build rather than rendering a broken image.
|
||||
*/
|
||||
const groupScreens: Record<string, string[]> = {
|
||||
community: ['news'],
|
||||
'game-intelligence': ['marketplace', 'spawn-atlas', 'guilds', 'houses'],
|
||||
administration: ['admin-dashboard', 'admin-users'],
|
||||
integration: ['admin-modules'],
|
||||
};
|
||||
|
||||
const title = 'Features';
|
||||
const description =
|
||||
'What a Runic Gateway deployment does — core, and what the installed game module adds.';
|
||||
'What a Runic Gateway deployment does — core, and what the installed game module adds, for Ultima Online or Rust.';
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
@@ -86,16 +85,17 @@ const description =
|
||||
<p>
|
||||
Grouped the way the software is actually divided, because that division is the thing
|
||||
most worth understanding before you install it: the core site is game-agnostic and does
|
||||
not know what a shard is, and everything that does arrives as an <a href="/modules/"
|
||||
not know what game you run, and everything that does arrives as an <a href="/modules/"
|
||||
>installable module</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
Two modules exist today, for Ultima Online and for Rust, and a deployment runs one of
|
||||
them. The second group below is what the Ultima Online module adds; the Rust module
|
||||
brings its own set in its place. On a deployment with no module, that group is simply
|
||||
absent and the other four are unchanged.
|
||||
them. The second group below is what both of them add, each in its own game’s way;
|
||||
each game’s own list is on its page. On a deployment with no module, that group is
|
||||
simply absent and the other four are unchanged.
|
||||
</p>
|
||||
<SupportedGames class="features__games" />
|
||||
</PageHeader>
|
||||
|
||||
{
|
||||
@@ -110,6 +110,16 @@ const description =
|
||||
|
||||
<p class="prose group__summary">{group.summary}</p>
|
||||
|
||||
{group.moduleSupplied && (
|
||||
<div class="group__games">
|
||||
{games.map((game) => (
|
||||
<a class="btn btn--ghost" href={game.href}>
|
||||
What the {game.name} module adds
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul class="group__items">
|
||||
{group.items.map((item) => (
|
||||
<li class="panel group__item">
|
||||
@@ -175,6 +185,17 @@ const description =
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.group__games {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.features__games {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.group__items {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -43,7 +43,7 @@ const integrations = [
|
||||
{
|
||||
title: 'Slash commands',
|
||||
body:
|
||||
'Commands registered with your guild that answer from your site — so the thing ' +
|
||||
'Commands registered with your Discord server that answer from your site — so the thing ' +
|
||||
'somebody wants to look up is available where the conversation is happening, ' +
|
||||
'rather than one tab away.',
|
||||
},
|
||||
@@ -60,13 +60,13 @@ const integrations = [
|
||||
body:
|
||||
'A Team can be granted its own voice channel, with membership maintained by the ' +
|
||||
'bot rather than by whoever is online. The bot creates the category, and the ' +
|
||||
'panel reports how many roles your guild has left before Discord’s own limit.',
|
||||
'panel reports how many roles your Discord server has left before Discord’s own limit.',
|
||||
},
|
||||
],
|
||||
caveat:
|
||||
'Voice access is granted with a Discord role, and roles are visible on a member’s ' +
|
||||
'profile — so a Team with a voice channel is a Team anyone in your guild can see the ' +
|
||||
'membership of. That was a deliberate trade for a limit that counts per guild rather ' +
|
||||
'profile — so a Team with a voice channel is a Team anyone in your Discord server can ' +
|
||||
'see the membership of. That was a deliberate trade for a limit that counts per server rather ' +
|
||||
'than per channel, and it is the right one for most communities, but it is not private.',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,26 +2,27 @@
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PageHeader from '../components/PageHeader.astro';
|
||||
import NotBuilt from '../components/NotBuilt.astro';
|
||||
import SupportedGames from '../components/SupportedGames.astro';
|
||||
|
||||
import platform from '../data/platform.json';
|
||||
import { capabilityGroup } from '../data/capabilities.mjs';
|
||||
import { games } from '../data/games/index.mjs';
|
||||
|
||||
/**
|
||||
* `/modules/` — PLAN.md §13 phase 4.
|
||||
* `/modules/` — PLAN.md §13 phase 4, restructured in §16 phase 17.
|
||||
*
|
||||
* §10 gives this page four jobs: what a module is, `module-uo` as the worked example,
|
||||
* writing your own, and the Integration Kit with its draft badge (D8). They are in that
|
||||
* order because they are increasing commitment — a reader deciding whether to install one,
|
||||
* a reader wondering what they get, a reader considering building one.
|
||||
* §10 gave this page four jobs: what a module is, a worked example, writing your own, and
|
||||
* the Integration Kit with its draft badge (D8). They are in that order because they are
|
||||
* increasing commitment — a reader deciding whether to install one, a reader wondering what
|
||||
* they get, a reader considering building one.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* THE WORKED EXAMPLE READS ITS OWN CAPABILITIES
|
||||
* TWO GAMES, EACH ON ITS OWN PAGE
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* The `module-uo` section lists what the module publishes, and it takes that list from
|
||||
* `capabilities.mjs` rather than retyping it — the same list the homepage names and
|
||||
* `/features/` expands, which is already checked against the module's own manifest through
|
||||
* `platform.json` (§12). A third hand-maintained copy on this page is exactly the failure
|
||||
* that machinery exists to prevent, and this is the page where it would be least visible.
|
||||
* The worked example was the Ultima Online module, listed in that game's words. Since phase
|
||||
* 17 there are two modules, and D63 keeps a game's vocabulary on its own pages, so this page
|
||||
* is core: a card per game, in core's words, from `games/index.mjs`, each linking to
|
||||
* `/modules/uo/` or `/modules/rust/`, where the game's real feature list is (D75). What the
|
||||
* modules share — a sidecar, their own tables, their own releases — stays here, said once.
|
||||
*
|
||||
* ---------------------------------------------------------------------------------------
|
||||
* THE DRAFT CHIP IS A DECISION, NOT A DISCLAIMER
|
||||
@@ -34,14 +35,12 @@ import { capabilityGroup } from '../data/capabilities.mjs';
|
||||
*/
|
||||
const title = 'Modules';
|
||||
const description =
|
||||
'What a module is, what the Ultima Online module publishes, and what it takes to write ' +
|
||||
'one for another game.';
|
||||
'What a module is, the two game modules that exist — Ultima Online and Rust — and what it ' +
|
||||
'takes to write one for another game.';
|
||||
|
||||
const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
|
||||
const docs = `${gitea}/docs/src/branch/main`;
|
||||
|
||||
const gameIntelligence = capabilityGroup('game-intelligence');
|
||||
|
||||
/** The three ways a module reaches a running deployment. None of them is a build. */
|
||||
const installPaths = [
|
||||
{
|
||||
@@ -81,8 +80,10 @@ const installPaths = [
|
||||
<p>
|
||||
That division is not an aspiration bolted on afterwards. The Ultima Online support was
|
||||
extracted out of the site into a module, and every URL it had before the move it still
|
||||
has — which is the only version of this claim worth making.
|
||||
has — which is the only version of this claim worth making. Rust followed, built
|
||||
against the same seam.
|
||||
</p>
|
||||
<SupportedGames class="mod__supported" />
|
||||
</PageHeader>
|
||||
|
||||
<section class="page section mod" id="what">
|
||||
@@ -146,60 +147,65 @@ const installPaths = [
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="page section mod" id="module-uo">
|
||||
<div class="mod__head">
|
||||
<p class="eyebrow">The worked example</p>
|
||||
<h2>module-uo</h2>
|
||||
<span class="chip chip--version">{platform.releases['Module-uo']}</span>
|
||||
</div>
|
||||
|
||||
<section class="page section mod" id="games">
|
||||
<p class="eyebrow">The game modules</p>
|
||||
<h2>Two games, one module each</h2>
|
||||
<p class="prose mod__lede">
|
||||
The Ultima Online module, and the reference every module that follows is measured
|
||||
against. It is what turns a general-purpose community site into something that knows
|
||||
what a shard is — and it is the proof that the seam described on
|
||||
<a href="/architecture/">the architecture page</a> is real, because the code on the far
|
||||
side of it was moved there rather than designed there.
|
||||
Both were built inside this project, and a deployment runs one of them — the site
|
||||
refuses to start a second. Each has a page of its own, in its own game's words, and a
|
||||
documentation group that starts from installing it.
|
||||
</p>
|
||||
|
||||
<div class="mod__example">
|
||||
<div class="panel mod__caps">
|
||||
<h3>What it publishes</h3>
|
||||
<ul>
|
||||
{gameIntelligence.items.map((item) => <li>{item.label}</li>)}
|
||||
</ul>
|
||||
<p class="mod__caps-note">
|
||||
The same list <a href="/features/">features</a> expands, read from one file that is
|
||||
checked against the module's own manifest on every build.
|
||||
</p>
|
||||
</div>
|
||||
<ul class="mod__games">
|
||||
{
|
||||
games.map((game) => (
|
||||
<li class="panel mod__game">
|
||||
<div class="mod__head">
|
||||
<h3>{game.name}</h3>
|
||||
<span class="chip chip--version">
|
||||
{game.release}
|
||||
</span>
|
||||
</div>
|
||||
<p>{game.blurb}</p>
|
||||
<div class="mod__game-links">
|
||||
<a class="btn btn--primary" href={game.href}>
|
||||
What the {game.name} module adds
|
||||
</a>
|
||||
<a class="btn btn--ghost" href={game.docs}>
|
||||
Its documentation
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="mod__facts">
|
||||
<section>
|
||||
<h3>It connects to a real server</h3>
|
||||
<p>
|
||||
The module talks to the sidecar beside your game server, not to the game. You
|
||||
deploy that side with the installer and paste four values into the admin panel;
|
||||
nothing here requires the game to exist, and with no server configured the site
|
||||
renders normally and shows it offline.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>It owns its own tables</h3>
|
||||
<p>
|
||||
Its schema is applied by the site on every boot and its data is its own. The
|
||||
module declares which versions of the core interface it speaks — the site runs
|
||||
{' '}{platform.moduleApi} — and refuses to load against one it does not.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>It is a separate release</h3>
|
||||
<p>
|
||||
Versioned, tagged and published on its own cadence, independently of the site.
|
||||
Upgrading one does not mean upgrading the other, as long as the declared interface
|
||||
range still holds.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="mod__facts mod__facts--shared">
|
||||
<section>
|
||||
<h3>Each connects to a real server</h3>
|
||||
<p>
|
||||
A module talks to the sidecar beside your game server, not to the game. You deploy
|
||||
that side with the installer and give the admin panel its address and token;
|
||||
nothing here requires the game to exist, and with no server configured the site
|
||||
renders normally and shows it offline.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Each owns its own tables</h3>
|
||||
<p>
|
||||
Its schema is applied by the site on every boot and its data is its own. A module
|
||||
declares which versions of the core interface it speaks — the site runs
|
||||
{' '}{platform.moduleApi} — and refuses to load against one it does not.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Each is a separate release</h3>
|
||||
<p>
|
||||
Versioned, tagged and published on its own cadence, independently of the site.
|
||||
Upgrading one does not mean upgrading the other, as long as the declared interface
|
||||
range still holds.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -239,9 +245,6 @@ const installPaths = [
|
||||
<a class="btn btn--ghost" href={`${docs}/website/MODULE_API.md`} rel="noopener noreferrer">
|
||||
The module contract
|
||||
</a>
|
||||
<a class="btn btn--ghost" href={`${docs}/modules/uo/README.md`} rel="noopener noreferrer">
|
||||
module-uo in depth
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -287,7 +290,6 @@ const installPaths = [
|
||||
|
||||
.mod__grid h3,
|
||||
.mod__paths h3,
|
||||
.mod__caps h3,
|
||||
.mod__facts h3,
|
||||
.mod__draft h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
@@ -317,55 +319,53 @@ const installPaths = [
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mod__example {
|
||||
.mod__games {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.mod__caps ul {
|
||||
margin: 0;
|
||||
margin: 2rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
|
||||
}
|
||||
|
||||
.mod__game {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mod__game h3 {
|
||||
margin: 0;
|
||||
color: var(--gold);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.mod__game p {
|
||||
margin: 0.75rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.mod__caps li {
|
||||
position: relative;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--text);
|
||||
.mod__game-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6rem;
|
||||
margin-top: auto;
|
||||
padding-top: 1.25rem;
|
||||
}
|
||||
|
||||
.mod__caps li + li {
|
||||
margin-top: 0.3rem;
|
||||
.mod__supported {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* The same drawn marker the homepage's capability lists use, so a reader who has
|
||||
seen this list once recognises it as the same list. */
|
||||
.mod__caps li::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.62em;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--portal);
|
||||
opacity: 0.75;
|
||||
.mod__facts--shared {
|
||||
display: grid;
|
||||
gap: 1.4rem;
|
||||
margin-top: 2.25rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
|
||||
}
|
||||
|
||||
.mod__caps-note {
|
||||
margin: 1rem 0 0;
|
||||
padding-top: 0.85rem;
|
||||
border-top: 1px solid var(--line-soft);
|
||||
color: var(--dim);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.mod__facts section + section {
|
||||
margin-top: 1.4rem;
|
||||
.mod__facts--shared section + section {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mod__facts p {
|
||||
@@ -395,10 +395,4 @@ const installPaths = [
|
||||
gap: 0.7rem;
|
||||
margin-top: 1.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.mod__example {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
199
src/pages/modules/rust.astro
Normal file
199
src/pages/modules/rust.astro
Normal file
@@ -0,0 +1,199 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import PageHeader from '../../components/PageHeader.astro';
|
||||
import GameFeatures from '../../components/GameFeatures.astro';
|
||||
|
||||
import platform from '../../data/platform.json';
|
||||
import { features, staffFeatures } from '../../data/games/rust.mjs';
|
||||
import { assertGameCoverage } from '../../data/games/index.mjs';
|
||||
|
||||
/**
|
||||
* `/modules/rust/` — PLAN.md §16, phase 17.
|
||||
*
|
||||
* The Rust module's own page, in Rust's words (D63; `GAME_ROUTES` in `checkTerms.mjs`). The
|
||||
* lists are `games/rust.mjs`, written from the Rust docs group that phase 15 walked, and the
|
||||
* call below holds them to the capabilities `Module-Rust` declares — the page-side half of
|
||||
* the coverage assertion phase 13 moved here.
|
||||
*
|
||||
* It carries the RunicNPC card D61 asked for, pointing at the standalone `/runicnpc/` page.
|
||||
*
|
||||
* No screenshots yet, and no slots for them: D65 brings the Rust captures in phase 18, from
|
||||
* the rigs, and D4 rules out a placeholder in the meantime.
|
||||
*/
|
||||
assertGameCoverage('rust');
|
||||
|
||||
const rust = platform.games.rust;
|
||||
|
||||
const pieces = [
|
||||
{
|
||||
role: 'The module',
|
||||
name: rust.module.repo,
|
||||
version: platform.releases['Module-Rust'],
|
||||
what: 'Every Rust screen on the site, installed from Admin → Modules.',
|
||||
},
|
||||
{
|
||||
role: 'The sidecar',
|
||||
name: 'rust-link',
|
||||
version: rust.bundle.sidecar,
|
||||
what: 'One beside each server, and the only part of the bridge that listens on a network.',
|
||||
},
|
||||
{
|
||||
role: 'The bridge plugin',
|
||||
name: 'RunicGateway.cs',
|
||||
version: rust.bundle.plugin,
|
||||
what: 'One file that runs unchanged on Oxide and Carbon, plus its optional helpers.',
|
||||
},
|
||||
{
|
||||
role: 'NPCs',
|
||||
name: 'RunicNPC',
|
||||
version: rust.bundle.runicnpc,
|
||||
what: 'Our NPC plugin, which the bridge drives for every NPC an event places.',
|
||||
},
|
||||
];
|
||||
|
||||
const title = 'The Rust module';
|
||||
const description =
|
||||
'What the Rust module adds to a Runic Gateway site: every server live, the kill feed, ' +
|
||||
'leaderboards per wipe, the live map, Steam linking, and permissions, mod config and events run from the site.';
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<PageHeader eyebrow="Game module" title="Rust">
|
||||
<p>
|
||||
The <code>rust</code> module turns a Runic Gateway site into a Rust community’s site. It
|
||||
follows any number of servers, each running Oxide or Carbon with a sidecar of its own,
|
||||
and keeps what each one tells it after the wipe has taken it from the game.
|
||||
</p>
|
||||
<p>
|
||||
The server side is installed as one bundle — <strong>{rust.bundle.tag}</strong>,
|
||||
speaking protocol {rust.protocol} — with the installer, from a Pterodactyl egg, or by
|
||||
hand. It needs Oxide {rust.bundle.oxideMin} or Carbon {rust.bundle.carbonMin} or newer,
|
||||
and the uMod plugins {rust.bundle.requiresPlugins.join(' and ')}.
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<section class="page section pieces" aria-labelledby="pieces-heading">
|
||||
<h2 id="pieces-heading">The pieces</h2>
|
||||
<ul class="pieces__list">
|
||||
{
|
||||
pieces.map((piece) => (
|
||||
<li class="panel">
|
||||
<p class="pieces__role">{piece.role}</p>
|
||||
<p class="pieces__name">
|
||||
<code>{piece.name}</code>
|
||||
<span class="chip chip--version">{piece.version}</span>
|
||||
</p>
|
||||
<p class="pieces__what">{piece.what}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<GameFeatures
|
||||
title="What players see"
|
||||
lede="Every public surface is the operator's to widen: nothing names a player to the public by default."
|
||||
items={features}
|
||||
/>
|
||||
|
||||
<GameFeatures
|
||||
title="What staff run from the site"
|
||||
lede="Seven admin screens, and a Rust section on each user's page."
|
||||
items={staffFeatures}
|
||||
/>
|
||||
|
||||
<section class="page section npc">
|
||||
<div class="panel npc__card">
|
||||
<p class="eyebrow">Also from this project</p>
|
||||
<h2>RunicNPC</h2>
|
||||
<p>
|
||||
Our own NPC plugin for Rust: NPCs where you want them, with the kit, health, aim and
|
||||
behaviour you give them — roamers, sentries, guards, quest-givers and bosses with
|
||||
phases. On a Runic Gateway server it is required and the site authors its profiles;
|
||||
on any other Rust server it runs on its own, with <code>/rnpc</code> commands and an
|
||||
API for other plugins.
|
||||
</p>
|
||||
<div class="npc__links">
|
||||
<a class="btn btn--primary" href="/runicnpc/">About RunicNPC</a>
|
||||
<a class="btn btn--ghost" href="/docs/rust/runicnpc/">Its documentation</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page section next">
|
||||
<h2>Setting it up</h2>
|
||||
<p class="prose">
|
||||
The documentation’s Rust group starts from installing the bridge and covers every admin
|
||||
screen the module adds, with the reference for each file it reads.
|
||||
</p>
|
||||
<div class="next__links">
|
||||
<a class="btn btn--primary" href="/docs/rust/">The Rust docs</a>
|
||||
<a class="btn btn--ghost" href="/modules/">All modules</a>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.pieces h2,
|
||||
.next h2 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: clamp(1.5rem, 3vw, 1.95rem);
|
||||
}
|
||||
|
||||
.pieces__list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin: 1.25rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
|
||||
}
|
||||
|
||||
.pieces__role {
|
||||
margin: 0;
|
||||
color: var(--dim);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pieces__name {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.pieces__what {
|
||||
margin: 0.6rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.npc__card h2 {
|
||||
margin: 0;
|
||||
color: var(--gold);
|
||||
font-size: clamp(1.4rem, 2.8vw, 1.8rem);
|
||||
}
|
||||
|
||||
.npc__card p:not(.eyebrow) {
|
||||
margin: 0.75rem 0 0;
|
||||
max-width: var(--measure);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.npc__links,
|
||||
.next__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.7rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.next p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
165
src/pages/modules/uo.astro
Normal file
165
src/pages/modules/uo.astro
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import PageHeader from '../../components/PageHeader.astro';
|
||||
import GameFeatures from '../../components/GameFeatures.astro';
|
||||
import Screenshot from '../../components/Screenshot.astro';
|
||||
|
||||
import platform from '../../data/platform.json';
|
||||
import { features, screens } from '../../data/games/uo.mjs';
|
||||
import { assertGameCoverage } from '../../data/games/index.mjs';
|
||||
|
||||
/**
|
||||
* `/modules/uo/` — PLAN.md §16, phase 17.
|
||||
*
|
||||
* The Ultima Online module's own page, and so one of the few marketing pages that may use
|
||||
* that game's words (D63; `GAME_ROUTES` in `checkTerms.mjs`). The feature list is
|
||||
* `games/uo.mjs`, held to the module's declared capabilities by the call below, and the
|
||||
* screenshots are the ones `/features/` showed until this phase — every one tagged with its
|
||||
* game in `screens.mjs` (D67).
|
||||
*
|
||||
* Versions are read from `platform.json`, which `checkFacts.mjs` holds to the module's
|
||||
* release, the installer's bundle and both halves of the bridge.
|
||||
*/
|
||||
assertGameCoverage('uo');
|
||||
|
||||
const uo = platform.games.uo;
|
||||
const gitea = `${platform.gitea.base}/${platform.gitea.org}`;
|
||||
const docs = `${gitea}/docs/src/branch/main`;
|
||||
|
||||
const pieces = [
|
||||
{
|
||||
role: 'The module',
|
||||
name: uo.module.repo,
|
||||
version: platform.releases['Module-uo'],
|
||||
what: 'Every Ultima Online screen on the site, installed from Admin → Modules.',
|
||||
},
|
||||
{
|
||||
role: 'The sidecar',
|
||||
name: 'uo-link',
|
||||
version: uo.bundle.sidecar,
|
||||
what: 'Beside the shard, and the only part of the bridge that listens on a network.',
|
||||
},
|
||||
{
|
||||
role: 'The bridge plugin',
|
||||
name: uo.pluginRepo,
|
||||
version: uo.bundle.overlay,
|
||||
what: `An overlay compiled by ServUO at boot, on ServUO ${uo.bundle.servuoMin} or newer.`,
|
||||
},
|
||||
];
|
||||
|
||||
const title = 'The Ultima Online module';
|
||||
const description =
|
||||
'What the Ultima Online module adds to a Runic Gateway site: live shard status, the ' +
|
||||
'player-vendor marketplace, the spawn atlas, guilds, houses and more, from a running ServUO shard.';
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<PageHeader eyebrow="Game module" title="Ultima Online">
|
||||
<p>
|
||||
The <code>uo</code> module turns a Runic Gateway site into an Ultima Online shard’s
|
||||
site, fed live from a running ServUO shard. It was the first module, and the one the
|
||||
seam was cut around: this support was extracted out of the site, and every URL it had
|
||||
before the move it still has.
|
||||
</p>
|
||||
<p>
|
||||
The shard side is installed with the installer as one bundle —
|
||||
{' '}<strong>{uo.bundle.tag}</strong>, speaking protocol {uo.protocol} — and the site
|
||||
side is a paste in the admin panel.
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<section class="page section pieces" aria-labelledby="pieces-heading">
|
||||
<h2 id="pieces-heading">The pieces</h2>
|
||||
<ul class="pieces__list">
|
||||
{
|
||||
pieces.map((piece) => (
|
||||
<li class="panel">
|
||||
<p class="pieces__role">{piece.role}</p>
|
||||
<p class="pieces__name">
|
||||
<code>{piece.name}</code>
|
||||
<span class="chip chip--version">{piece.version}</span>
|
||||
</p>
|
||||
<p class="pieces__what">{piece.what}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<GameFeatures
|
||||
title="What it adds"
|
||||
lede="Everything here is published from the live shard, and every public surface passes the visibility settings first — each one is the operator's to switch on."
|
||||
items={features}
|
||||
/>
|
||||
|
||||
<section class="page section shots" aria-label="Screenshots">
|
||||
{screens.map((id) => <Screenshot id={id} />)}
|
||||
</section>
|
||||
|
||||
<section class="page section next">
|
||||
<h2>Setting it up</h2>
|
||||
<p class="prose">
|
||||
The documentation’s Ultima Online group starts from connecting a ServUO shard and ends
|
||||
at the reference for every file the bridge reads.
|
||||
</p>
|
||||
<div class="next__links">
|
||||
<a class="btn btn--primary" href="/docs/ultima-online/">The Ultima Online docs</a>
|
||||
<a class="btn btn--ghost" href={`${docs}/modules/uo/README.md`} rel="noopener noreferrer">
|
||||
The module in depth
|
||||
</a>
|
||||
<a class="btn btn--ghost" href="/modules/">All modules</a>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.pieces h2,
|
||||
.next h2 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: clamp(1.5rem, 3vw, 1.95rem);
|
||||
}
|
||||
|
||||
.pieces__list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin: 1.25rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
|
||||
}
|
||||
|
||||
.pieces__role {
|
||||
margin: 0;
|
||||
color: var(--dim);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.pieces__name {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.pieces__what {
|
||||
margin: 0.6rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.next p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.next__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.7rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
238
src/pages/runicnpc.astro
Normal file
238
src/pages/runicnpc.astro
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PageHeader from '../components/PageHeader.astro';
|
||||
|
||||
import platform from '../data/platform.json';
|
||||
import { runicnpcCalls, runicnpcHooks } from '../data/runicnpc.mjs';
|
||||
|
||||
/**
|
||||
* `/runicnpc/` — PLAN.md §16, D61, built in phase 17.
|
||||
*
|
||||
* The standalone page for RunicNPC, written for the owner of a Rust server who runs no
|
||||
* Runic Gateway site at all and should be able to find and use the plugin without first
|
||||
* learning what the platform is. It is in the header (D78) for the same reason. It may use
|
||||
* Rust's words: it is one of the Rust routes in `checkTerms.mjs`.
|
||||
*
|
||||
* It says less than the docs subgroup (`/docs/rust/runicnpc/`), which phase 16 walked and
|
||||
* drift-checked, and nothing that subgroup does not. Every number is read: the release and
|
||||
* the API version and framework floors from `platform.json`, which `checkFacts.mjs` holds to
|
||||
* `runicnpc-rust`'s own `plugin.toml`, and the counts of calls and hooks from
|
||||
* `runicnpc.mjs`, which `checkReference.mjs` holds to `docs/runicnpc/API.md`.
|
||||
*/
|
||||
const npc = platform.runicnpc;
|
||||
const release = platform.releases['runicnpc-rust'];
|
||||
const repo = `${platform.gitea.base}/${platform.gitea.org}/runicnpc-rust`;
|
||||
const calls = Object.keys(runicnpcCalls).length;
|
||||
const hooks = Object.keys(runicnpcHooks).length;
|
||||
|
||||
const roles = [
|
||||
{ name: 'Roamer', does: 'Moves, chases and fights.' },
|
||||
{ name: 'Sentry', does: 'Stands still and shoots.' },
|
||||
{ name: 'Guard', does: 'Holds its spot, chases within its leash, and walks back.' },
|
||||
{
|
||||
name: 'Passive',
|
||||
does: 'Never fights. Pressing E says a line or opens a window — a quest-giver, a quartermaster.',
|
||||
},
|
||||
];
|
||||
|
||||
const title = 'RunicNPC';
|
||||
const description =
|
||||
'An NPC plugin for Rust on Oxide and Carbon: roamers, sentries, guards, quest-givers and ' +
|
||||
'bosses with phases, placed in game or from a site, with an API for other plugins. Runs on any server.';
|
||||
---
|
||||
|
||||
<Base title={title} description={description}>
|
||||
<PageHeader eyebrow="A Rust plugin from this project" title="RunicNPC">
|
||||
<p>
|
||||
NPCs where you want them, with the kit, health, aim, ranges and behaviour you give them,
|
||||
brought back after they die. One file, <code>RunicNPC.cs</code>, compiled by Oxide or
|
||||
Carbon from source like any uMod plugin — and it runs on any Rust server, whether or not
|
||||
a Runic Gateway site is anywhere near it.
|
||||
</p>
|
||||
</PageHeader>
|
||||
|
||||
<section class="page section facts" aria-label="At a glance">
|
||||
<dl class="facts__list">
|
||||
<div class="panel">
|
||||
<dt>Release</dt>
|
||||
<dd><span class="chip chip--version">{release}</span></dd>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<dt>Runs on</dt>
|
||||
<dd>Oxide {npc.oxideMin} or Carbon {npc.carbonMin}, or newer</dd>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<dt>Needs</dt>
|
||||
<dd>{npc.requiresPlugins.join(' and ')}. ZoneManager is optional.</dd>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<dt>API version</dt>
|
||||
<dd>{npc.api} — {calls} calls and {hooks} hooks</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="page section npc">
|
||||
<h2>What it does</h2>
|
||||
|
||||
<ul class="npc__roles">
|
||||
{
|
||||
roles.map((role) => (
|
||||
<li class="panel">
|
||||
<h3>{role.name}</h3>
|
||||
<p>{role.does}</p>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="npc__grid">
|
||||
<section>
|
||||
<h3>Bosses</h3>
|
||||
<p>
|
||||
Any role but passive can be a boss: a health bar at the top of the screen, phases as
|
||||
its health falls, lines said in game, and loot of its own.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Placements and routes</h3>
|
||||
<p>
|
||||
A placement is a spot where a profile’s NPCs stand and come back after they die. It
|
||||
survives a restart; the NPCs themselves are never saved, and each boot spawns fresh ones. A
|
||||
route is a line of points you record in game for an NPC to walk.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Factions</h3>
|
||||
<p>
|
||||
With no settings an NPC fights players only. Profiles can be put on sides, turned on
|
||||
Rust’s scientists or its animals, or allied to a clan, a team or one player.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>A cost it tells you</h3>
|
||||
<p>
|
||||
What NPCs cost a server was measured on both frameworks, not guessed, and the
|
||||
warning saying so appears wherever NPCs are added. Caps are an operator’s setting.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page section ways">
|
||||
<h2>Two ways to run it</h2>
|
||||
<div class="ways__grid">
|
||||
<div class="panel">
|
||||
<h3>On its own</h3>
|
||||
<p>
|
||||
Place NPCs in game with <code>/rnpc</code>, edit their profiles at the console, and
|
||||
let other plugins drive it through its API. Nothing else is needed. It makes no
|
||||
network call, downloads nothing and scans nothing at boot.
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>On a Runic Gateway server</h3>
|
||||
<p>
|
||||
It is required there, and installed with the bridge from the same bundle. The site
|
||||
authors the profiles and the faction table and pushes them to each server, places
|
||||
NPCs from its live map, and runs events that place them and wait on them dying
|
||||
or on a boss’s health.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="page section next">
|
||||
<h2>Get it</h2>
|
||||
<p class="prose">
|
||||
Releases carry the plugin file. The documentation covers installing it by hand, every
|
||||
command, every profile field, and the API in full for plugin authors.
|
||||
</p>
|
||||
<div class="next__links">
|
||||
<a class="btn btn--primary" href={`${repo}/releases`} rel="noopener noreferrer">
|
||||
Download {release}
|
||||
</a>
|
||||
<a class="btn btn--ghost" href="/docs/rust/runicnpc/install/">Install it</a>
|
||||
<a class="btn btn--ghost" href="/docs/rust/runicnpc/commands/">Commands</a>
|
||||
<a class="btn btn--ghost" href="/docs/rust/runicnpc/api/">The API</a>
|
||||
<a class="btn btn--ghost" href="/modules/rust/">The Rust module</a>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
|
||||
<style>
|
||||
.npc h2,
|
||||
.ways h2,
|
||||
.next h2 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: clamp(1.5rem, 3vw, 1.95rem);
|
||||
}
|
||||
|
||||
.facts__list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
}
|
||||
|
||||
.facts__list dt {
|
||||
color: var(--dim);
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.facts__list dd {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.npc__roles,
|
||||
.ways__grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin: 1.25rem 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
|
||||
}
|
||||
|
||||
.ways__grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
|
||||
}
|
||||
|
||||
.npc__roles h3,
|
||||
.ways__grid h3,
|
||||
.npc__grid h3 {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--gold);
|
||||
font-size: 1.02rem;
|
||||
}
|
||||
|
||||
.npc__roles p,
|
||||
.ways__grid p,
|
||||
.npc__grid p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.npc__grid {
|
||||
display: grid;
|
||||
gap: 1.4rem;
|
||||
margin-top: 2rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
|
||||
}
|
||||
|
||||
.next p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.next__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.7rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user