Play Web UI
Overview
The play experience is served by a single HTML shell at /play and
/play/<world_id>. That shell contains three UI states:
Login (logged out)
World select (user portal)
In-world UI
Client-side code in play.js toggles those states by setting the hidden
attribute on each section. World-specific CSS and JS are optionally loaded
when /play/<world_id> is used.
Routing model
The following routes all return the same shell:
/play/play/<world_id>/play/<world_id>/<any-subpath>
This keeps routing simple and lets the client manage navigation inside the
world UI. World selection updates the location to /play/<world_id>.
Files and structure
The play UI is split into shared assets and per-world assets:
src/mud_server/web/templates/play_shell.html
src/mud_server/web/static/play/css/
├── fonts.css
├── shared-base.css
├── shell.css
└── worlds/
└── <world_id>.css
src/mud_server/web/static/play/js/
├── play.js
└── worlds/
└── <world_id>.js
CSS layering order
The shell loads styles in this order:
fonts.css(font-face declarations)shared-base.css(tokens, resets, shared utilities)shell.css(layout for login + world select states)worlds/<world_id>.css(world-specific layout and overrides)
World styles should override shared defaults and may add layout rules for
.main-layout and its child elements.
Play shell sections
All three UI states live in play_shell.html and are shown/hidden by
play.js. The states are identified by data-play-state attributes.
<!-- LOGIN STATE (LOGGED OUT) -->
<section class="play-state play-state--logged-out" data-play-state="logged-out">
...
</section>
<!-- WORLD SELECT STATE (USER PORTAL) -->
<section class="play-state play-state--select" data-play-state="select-world" hidden>
...
</section>
<!-- GAME UI (IN WORLD) -->
<div class="main-layout" data-play-state="in-world" hidden>
...
</div>
play.js sets hidden on these sections to switch the visible state.
If you add new rules that change display for these sections, keep the global
[hidden] { display: none !important; } rule so the toggling remains
reliable.
Account Dashboard Layout (Select-World State)
The select-world state now renders as a centered 3-column dashboard:
Left column: 25% (world navigation and policy hints)
Center column: 50% (character selector, feedback, actions)
Right column: 25% (account/policy summary)
On mobile (max-width: 900px), columns collapse to a single stacked layout.
Character creation and world-entry behavior:
Enter worldremains disabled until a concrete character is selected.Generate charactercallsPOST /characters/createfor the selected world.Invite-only worlds can appear in the selector with locked labels for visibility.
Locked worlds do not fetch characters and block create/select actions.
World UI scaffold
The game UI skeleton lives in the in-world section of play_shell.html.
It mirrors the Daily Undertaking layout and provides these main blocks:
.character-panel(left column).content-area(center output + command input).right-panel(inventory, quests, notes)
You can keep the structure and replace the placeholder text as you wire in real data.
Creating a new world UI
Pick a world id
The world id is the slug used by the server (for example
pipeworks_web). This must match the world registry entry that the API returns inavailable_worlds.Add world CSS
Create
src/mud_server/web/static/play/css/worlds/<world_id>.css. Use shared tokens fromshared-base.cssand override layout rules as needed.Example:
/* worlds/ledgerfall.css */ body { background: var(--paper); color: var(--ink-newsprint-black); font-family: var(--font-body); } .main-layout { grid-template-columns: 260px 1fr 260px; }
Add world JS
Create
src/mud_server/web/static/play/js/worlds/<world_id>.js. The module runs only when/play/<world_id>is loaded.Example:
// worlds/ledgerfall.js (() => { const worldId = document.body?.dataset?.worldId; if (worldId !== 'ledgerfall') { return; } const output = document.getElementById('gameOutput'); if (output) { output.insertAdjacentHTML( 'beforeend', '<div class="output-text">Ledgerfall UI ready.</div>' ); } })();
Navigate to the world
Visit
/play/<world_id>or use the world selector after login.
Security note
The play shell is intentionally public HTML. It does not include live game
state. All real data and actions must go through authenticated API endpoints.
If you need to restrict the shell itself, you can add session checks to the
/play routes, but most deployments rely on API auth.
Testing and docs build
Quick checks for play-shell changes:
pytest tests/test_web/test_web_routes.py -v
Build the documentation locally:
cd docs
make html
Local dev + CORS sanity checklist
Same-origin local dev (no CORS needed): Run the MUD server locally and load the UI from the same host/port. Example:
mud-server run --host 127.0.0.1 --port 7860then visithttp://localhost:7860/play.Different-origin local dev (CORS required): If the UI is served from a separate dev server (for example Nova Panic at
http://localhost:9000) and you point it at the remote API, the remote nginx CORS map must allow that origin.