Player Frontend¶
The player frontend is a React-based web client that provides a modern browser interface for playing MAID games. It connects via WebSocket and renders a full MUD experience with terminal output, side panels for game data, and mobile-responsive layout.
Overview¶
The frontend is served at /play/ by the MAID server and provides:
- Terminal — xterm.js-based output with full ANSI color support
- Game panels — Character stats, inventory, room info, map, chat, quest log (stub)
- NPC dialogue — Dedicated overlay for AI-powered conversations
- Mobile support — Responsive layout with tab navigation
- PWA — Installable as a standalone app on mobile/desktop
- Theming — Dark, light, and high-contrast themes
- i18n — English and Spanish translations
Building¶
cd packages/maid-engine/player_frontend
# Install dependencies
npm install
# Build for production (output: dist/)
npm run build
# The MAID server automatically serves the built files at /play/
After building, start the MAID server and navigate to http://localhost:8080/play/.
Development¶
cd packages/maid-engine/player_frontend
# Start dev server with hot reload
npm run dev
# Run tests
npm run test
# Run tests in watch mode
npm run test:watch
# Preview production build
npm run preview
The dev server proxies WebSocket connections to the running MAID server, so you'll need both running during development.
Architecture¶
Tech Stack¶
| Layer | Technology |
|---|---|
| Framework | React 18 |
| Language | TypeScript |
| Build | Vite |
| Terminal | @xterm/xterm |
| State | Zustand |
| Data fetching | @tanstack/react-query |
| Routing | react-router-dom |
| Styling | Tailwind CSS + PostCSS |
| i18n | react-i18next |
| Sanitization | DOMPurify |
| Testing | Vitest + JSDOM |
Directory Structure¶
player_frontend/
├── public/
│ ├── manifest.json # PWA manifest (scope: /play/)
│ └── sw.js # Service worker (no-op, see below)
├── src/
│ ├── main.tsx # Entry point, i18n init, SW registration
│ ├── App.tsx # Router + theme application
│ ├── i18n.ts # i18next configuration
│ ├── pages/
│ │ ├── LoginPage.tsx # Authentication
│ │ ├── CharacterSelectPage.tsx
│ │ └── GamePage.tsx # Main game shell
│ ├── components/
│ │ ├── SafeHTML.tsx # DOMPurify sanitization component
│ │ ├── auth/ # Auth-related components
│ │ ├── dialogue/ # NPC dialogue overlay
│ │ ├── game/ # Terminal output, command input, macro bar
│ │ ├── layouts/ # Layout wrappers
│ │ ├── panels/ # Side panels (stats, inventory, room, etc.)
│ │ └── ui/ # Shared UI primitives
│ ├── stores/ # Zustand state stores
│ ├── hooks/ # Custom hooks (useGameWebSocket, etc.)
│ ├── lib/ # Utilities (auth-storage, gmcp-dispatch, sanitize)
│ ├── types/ # TypeScript types (GMCP, messages, settings)
│ ├── providers/ # React context providers (WebSocket)
│ ├── styles/ # Global CSS + theme variables
│ ├── locales/ # Translation JSON files
│ │ ├── en/translation.json
│ │ └── es/translation.json
│ └── test/ # Test utilities
├── index.html
├── vite.config.ts # Base path: /play/
├── tailwind.config.js
└── package.json
Page Flow¶
LoginPage → CharacterSelectPage → GamePage
│ │
└── Auth via REST API └── WebSocket connection
+ GMCP subscription
Zustand Stores¶
The application state is split across eight focused stores:
| Store | Purpose |
|---|---|
connection-store |
WebSocket status, latency, offline queue (defined but not wired to command input — see note below) |
character-store |
Character name, vitals, status effects |
inventory-store |
Inventory items + equipment slots |
room-store |
Current room name, description, exits, players |
map-store |
Visited rooms, viewport position, zoom level |
chat-store |
Chat channels, message history, unread counts |
dialogue-store |
Active NPC dialogue state and conversation history |
settings-store |
Theme, font, layout, macros, aliases, keybindings, soundEnabled, notificationsEnabled |
Settings are persisted to localStorage under the key maid-player-settings.
Send queue architecture
The codebase defines two queue layers, but only one is currently active:
connection-store.offlineQueue— Zustand store withsend()andaddToQueue()methods for buffering player commands. Entries expire after 30 seconds and are capped at 20. However, the active command input path (CommandInput→useGameWebSocket.sendCommand()→GameWebSocket.send()) does not use this queue — it is infrastructure for future use.GameWebSocket.queue— Private queue inside theGameWebSocketclass (src/lib/game-websocket.ts) that buffersClientMessageobjects when the underlyingWebSocketis not in theOPENstate. Flushed automatically on reconnect viaonopen. This is the active transport queue.
GMCP Integration¶
The frontend subscribes to GMCP (Generic MUD Communication Protocol) packages for structured game data. This keeps the terminal clean for narrative text while updating panels with machine-readable data.
Subscribed Packages¶
// Subscribed on connection
const gmcpPackages = [
"Core",
"Char.Vitals",
"Char.Status",
"Char.Items.Inv",
"Room.Info",
"Room.Players",
"Comm.Channel",
];
Data Flow¶
- Server sends GMCP messages over the WebSocket alongside terminal output
useGameWebSockethook receives messages and dispatches by typegmcpmessages are routed todispatchGMCP();gmcp_batchmessages are routed todispatchGMCPBatch()- The dispatcher updates the appropriate Zustand store
- React components re-render with new data
// Simplified GMCP dispatch flow
function dispatchGMCP(packageName: string, data: unknown) {
switch (packageName) {
case "Char.Vitals":
useCharacterStore.getState().updateVitals(data);
break;
case "Room.Info":
useRoomStore.getState().setRoom(data);
break;
case "Char.Items.Inv":
useInventoryStore.getState().setInventory(data);
break;
// ...
}
}
GMCP Types¶
Type definitions for all GMCP data are in src/types/gmcp.ts:
interface GMCPCharVitals {
hp: number;
maxhp: number;
mp?: number;
maxmp?: number;
mv?: number;
maxmv?: number;
}
interface GMCPRoomInfo {
num: string;
name: string;
area: string;
desc?: string;
exits: Record<string, string>;
coord?: { x: number; y: number; z: number };
npcs?: GMCPEntityInfo[];
items?: GMCPEntityInfo[];
}
Customization¶
Theming¶
Three built-in themes are defined in src/styles/globals.css:
:root[data-theme='dark'] {
--bg-primary: #1a1a1a;
--bg-secondary: #252525;
--bg-tertiary: #2d2d2d;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--text-muted: #666666;
--border: #3d3d3d;
--accent: #4a9eff;
/* ... */
}
:root[data-theme='light'] {
--bg-primary: #f7f7f7;
--bg-secondary: #ffffff;
--bg-tertiary: #ececec;
--text-primary: #1c1c1c;
--text-secondary: #4d4d4d;
--text-muted: #7a7a7a;
--border: #d0d0d0;
--accent: #2463eb;
/* ... */
}
:root[data-theme='high-contrast'] {
--bg-primary: #000000;
--bg-secondary: #111111;
--bg-tertiary: #1a1a1a;
--text-primary: #ffffff;
--text-secondary: #f2f2f2;
--text-muted: #bdbdbd;
--border: #ffffff;
--accent: #00ffff;
/* ... */
}
Players can set their theme via the settings store (e.g., programmatically or through an API call), and it's applied via document.documentElement.dataset.theme. There is currently no in-game settings UI for theme selection — theme state exists in the store but must be toggled programmatically or via direct store manipulation.
Layout Customization¶
The settings store supports:
- Font size and family — Customizable terminal font
- Compact mode — Stored in settings (not yet consumed by components)
- Scrollback — Terminal history buffer size
Note
These settings are available at the store and API level. There is currently no visible settings UI panel — customization must be done programmatically or via localStorage.
Macros and Aliases¶
Players can define:
- Macros — Clickable buttons that send commands
- Aliases — Text substitutions (e.g.,
kk→kill kobold) - Keybindings — Stored in settings (not yet wired to key event handlers)
These are configured through the settings store and persisted in localStorage. No in-game configuration UI is currently implemented; these fields are stored but not yet fully consumed by the UI.
Adding a New Panel¶
To add a custom panel:
-
Create a component in
src/components/panels/: -
Add a Zustand store if needed in
src/stores/ - Handle the relevant GMCP package in the dispatcher
- Add the panel to
GamePage.tsxlayout
Internationalization (i18n)¶
The frontend uses react-i18next with JSON translation files:
Usage in components:
import { useTranslation } from "react-i18next";
function MyComponent() {
const { t } = useTranslation();
return <h1>{t("game.welcome")}</h1>;
}
The language defaults to English (lng: 'en' is hardcoded in the i18next init) and falls back to English for missing keys. There is currently no settings UI for language switching — the language is fixed at initialization time.
Adding a New Language¶
- Create
src/locales/<code>/translation.json - Add the import to
src/i18n.ts - Register the resource in the i18next config
PWA Support¶
The frontend is installable as a Progressive Web App:
- Manifest —
public/manifest.jsonwithstart_url: "/play/"and app icons - Service Worker — Registered in
src/main.tsx; currently only callsskipWaiting()andclients.claim()(no offline caching) - Scope — Limited to
/play/path
Players on mobile can "Add to Home Screen" for a native-like experience. Note that without offline caching in the service worker, the app requires a network connection to function.
Deployment¶
How It's Served¶
The MAID server's web layer mounts the built frontend at /play/:
- Vite builds with
base: "/play/"(configured invite.config.ts), which prefixes all asset URLs with/play/ - If
player_frontend/dist/exists, the web server mounts it as aStaticFilesdirectory at/play/ - If the dist directory doesn't exist, a fallback HTML page is served at
/play/instead - The root URL
/redirects to/play/
Note
BrowserRouter in main.tsx does not set basename="/play". Client-side
routes (/, /characters, /game) are relative to the document root, not to
/play/. This works because the server mounts the SPA's index.html at /play/
and Vite's base handles asset paths. If you deploy behind a different prefix,
you must update both vite.config.ts (base) and add a matching basename
prop to BrowserRouter.
Production Build¶
# From project root
cd packages/maid-engine/player_frontend
npm install
npm run build
# Start the server — frontend is available at http://localhost:8080/play/
cd ../../..
uv run maid server start
Environment¶
The frontend doesn't require separate environment variables. It connects to the same server that hosts it, using relative WebSocket URLs.
Sanitization¶
HTML content rendered in ChatPanel and RoomInfoPanel uses DOMPurify via a SafeHTML component. This prevents XSS attacks from game content or player input that might contain HTML.
import { SafeHTML } from "../SafeHTML";
// Safe to render untrusted content
<SafeHTML html={roomDescription} />
See Also¶
- Getting Started — Server setup and first connection
- Building Commands — Creating commands that players interact with
- NPC Dialogue Guide — AI-powered NPC conversations shown in the dialogue panel