Tier 3: Visual Authoring Tools¶
Status: Draft (R3 — post-review revision) Author: MAID Core Team Created: 2025-07-18 Revised: 2025-07-18 Depends on: Tier 1 — YAML Content Pipeline (6-phase loader) Integrates with: Tier 2 — AI Content Generation (optional — AI features degrade gracefully) Package:
maid-engine(admin frontend, backend API),maid-stdlib(API routes), content packs (extensions)
Cross-Tier Glossary¶
This document uses the following terms consistently:
| Term | Definition |
|---|---|
| Content pack | A loadable unit with ContentPackManifest. The only term for game content modules. |
| Data pack | A content pack containing only YAML data (no Python code). |
| Builder | A user editing content in-game or via the visual editor. |
| Author | A user editing YAML files in a repository / text editor. |
| Zone | The formal world-grouping concept managed via @zone commands. The primary organizational unit used throughout the editor for filtering, clustering, and organization. |
| Area | Room metadata label in the admin API (area_id/area_name fields in RoomResponse/GraphNode). In the current API, area_id maps to a room's zone assignment. The field name is an API-layer convention; the underlying concept is always "zone." |
| Live world | The runtime entity state in the running MAID server. |
| Definition | The canonical YAML file that declares an entity (Tier 1 pipeline input). |
| Instance | A runtime entity created from a definition or via the admin API. |
Table of Contents¶
- Executive Summary
- Design Principles
- Architecture Overview
- World Map Editor
- Room Inspector Panel
- NPC Editor
- Item Editor
- Quest Editor
- Dialogue Editor
- Bulk Data Editor
- Content Browser
- Balance Dashboard
- Live Preview / Play Mode
- Mobile Builder Experience
- Content Pack Editor Extensions
- Collaboration Features
- Performance Considerations
- Mix-and-Match Flexibility
- Implementation Plan
- Appendices
1. Executive Summary¶
MUDs are text-based graph worlds — rooms connected by directed exits, populated by NPCs, items, and scripted behaviors. Unlike 2D tile-map games, the fundamental data structure is a directed graph, not a grid. Visual authoring tools for MUDs must therefore be graph-oriented: node-and-edge editors, not paint-with-tiles editors.
MAID already has the building blocks for visual tooling:
| Existing Asset | Location | What It Provides |
|---|---|---|
| Admin Frontend | admin_frontend/ |
React 18 + TypeScript + Vite + Tailwind, 7 pages, React Flow already installed |
| World Page | pages/World.tsx |
Room graph visualization with React Flow, undo/redo, zone filtering (via area_id) |
| Undo/Redo Hook | hooks/useUndoRedo.ts |
Command-pattern undo for room/exit CRUD (50-operation stack, keyboard shortcuts) |
| World API | api/world.ts |
getRooms, getWorldGraph, createRoom, updateRoom, createExit, deleteExit |
| Entity API | api/entities.ts |
Full CRUD for entities, components, tags |
| Types | types/api.ts |
GraphNode, GraphEdge, WorldGraphResponse, RoomResponse, ExitData |
| WebSocket | WS /admin/ws |
Real-time channels: metrics, entities, logs, events |
| Player Frontend | player_frontend/ |
xterm.js terminal, GMCP, 8 Zustand stores |
| World Map CLI | maid world map |
DOT/ASCII/SVG/HTML export, BFS layout, zone clustering |
| Builder Commands | commands/building/ |
30 command files: @create, @dig, @describe, @zone, @export, etc. |
| Zustand Stores | stores/ |
authStore, dashboardStore (admin); 8 stores (player) |
Tier 3 extends this foundation into a comprehensive visual authoring suite.
The World Map Editor is the crown jewel — a web-based room graph editor built on
the existing React Flow integration in World.tsx. Around it, specialized editors
for rooms, NPCs, items, quests, and dialogue provide Unity/Godot-style inspector
panels. A Content Browser, Balance Dashboard, and embedded Play Mode round out the
experience.
What Tier 3 Delivers¶
| Capability | Description |
|---|---|
| World Map Editor | Full-featured room graph editor with force-directed layout, layers, filters, minimap |
| Room Inspector | Unity-style panel: descriptions, exits, contents, components, history |
| NPC Editor | Identity, AI dialogue, behavior archetypes, schedule timeline, memory viewer |
| Item Editor | Type-driven forms, requirements, crafting recipes, balance comparison |
| Quest Editor | Twine-inspired node graph for objective flow, branching, simulation |
| Dialogue Editor | Visual conversation tree with conditions, effects, AI hybrid preview |
| Bulk Data Editor | Spreadsheet-style entity grid with inline editing and CSV import/export |
| Content Browser | Searchable library with drag-and-drop placement onto map |
| Balance Dashboard | Combat curves, economy Sankey, content density heatmap |
| Play Mode | Embedded xterm.js terminal with hot-reload and ghost mode |
| Tablet Layout | Tablet-responsive layout (≥768px) with touch-optimized quick edit mode |
| Pack Extensions | API for content packs to register custom editor panels |
| Collaboration | Presence cursors, exclusive entity locks, change log |
What Tier 3 Does NOT Do¶
- Replace in-game building. Visual tools complement
@create,@dig,@describe. - Replace YAML authoring. Visual tools generate and consume YAML files.
- Require a specific content pack. Editors adapt to registered component types.
- Introduce new data formats. Everything uses existing entity/component/room APIs.
2. Design Principles¶
2.1 Extend, Don't Replace — Source of Truth Model¶
Visual tools are one surface among three — in-game commands, YAML files, and the web editor. Each has strengths:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ In-Game CLI │ │ YAML Files │ │ Visual Editor │
│ │ │ │ │ │
│ • Fast iteration │ │ • Version ctrl │ │ • Spatial view │
│ • Context-aware │ │ • Diff/merge │ │ • Drag-and-drop │
│ • No tab switch │ │ • CI/CD ready │ │ • Inspector UI │
│ • Text-native │ │ • Bulk editing │ │ • Collaboration │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Mutates live │ │ Tier 1 pipeline │ │ Mutates live │
│ world directly │ │ loads into live │ │ world via admin │
│ (@dig, @create) │ │ world on start │ │ REST API │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
┌───────────▼───────────┐
│ Live World State │
│ (runtime entities) │
└───────────────────────┘
Source of Truth:
MAID has two layers of truth, not one:
-
YAML definitions are canonical for repeatable content. They are version- controlled, validated by the Tier 1 pipeline, and loaded on server start. Entities loaded from YAML carry
DataProvenanceComponent(definition hash, source file) andInstanceStateComponent(runtime dirty tracking). -
The live world is authoritative at runtime. In-game commands (
@dig,@create) and the visual editor both mutate the live world directly via the entity/world APIs. These mutations are not automatically written back to YAML files — they exist only in the running server (and in persistence storage ifEntityPersistenceManageris enabled).
The visual editor is a live-world editor. It reads and writes runtime entity state through the admin REST API. It can export to Tier 1-compatible YAML (see §4.8), but it does not edit YAML files directly. This means:
- Changes made in the visual editor are immediately visible in-game (once room/world event broadcasting is implemented — see §3.5).
- To persist visual editor changes as version-controlled YAML, the builder must explicitly export (visual editor → YAML → git).
- To detect drift between YAML definitions and runtime state, use
maid data diff(a Tier 1 feature).
2.2 Real-Time Collaboration¶
Multiple builders work simultaneously. The WebSocket-based protocol provides:
- Presence awareness: See other builders' cursors on the map
- Exclusive locking: Select-to-edit, server-enforced entity locks (see §16.2)
- Live updates: Entity changes push to all connected editors (requires new broadcasting — see §3.5 for required backend changes)
- Change attribution: Every edit is tagged with the author
2.3 Tablet-Responsive¶
The admin UI is used on tablets by builders doing walk-through reviews. Layouts collapse gracefully: map fills the screen, inspector becomes a slide-over panel, toolbars become floating action buttons. Phone-based graph editing is not a realistic target; mobile scope is limited to tablets (≥768px). See §14.
2.4 Progressive Complexity¶
New builders see a simple map with click-to-create and a basic room form. Advanced builders unlock raw component editing, batch operations, scripting panels, and the balance dashboard. Complexity is layered, not gated.
┌─────────────────────────────────────────────┐
│ Level 1: Map + Room Inspector (default) │ ← New builders
├─────────────────────────────────────────────┤
│ Level 2: NPC/Item/Quest editors │ ← Regular builders
├─────────────────────────────────────────────┤
│ Level 3: Components, bulk ops, scripts │ ← Advanced builders
├─────────────────────────────────────────────┤
│ Level 4: Balance dashboard, play mode │ ← Lead designers
└─────────────────────────────────────────────┘
2.5 Plugin-Extensible¶
Content packs register custom editor panels. maid-classic-rpg adds combat stat
sliders, spell editors, and faction relationship graphs. Third-party packs can add
entirely new editor pages.
2.6 Offline Fallback¶
The visual editor requires a running MAID server for its primary workflow. For
offline work, builders export YAML from the editor and edit in VS Code or any text
editor, then import back via maid data load or the visual editor's import feature.
Browser-side caching (localStorage for layout positions, React Query cache for
recently fetched data) provides short-term resilience for brief disconnections. Full
offline-first editing with IndexedDB sync queues is a potential future enhancement
but is not in v1 scope (see §14.4).
3. Architecture Overview¶
3.1 System Architecture¶
┌─────────────────────────────────────────────────────────────────────┐
│ Browser (Admin UI) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Map │ │ Room │ │ NPC │ │ Quest │ │ Balance │ │
│ │ Editor │ │ Inspector│ │ Editor │ │ Editor │ │ Dashboard│ │
│ │(ReactFlow│ │ │ │ │ │ │ │(Recharts)│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │ │
│ ┌────▼─────────────▼────────────▼─────────────▼─────────────▼────┐ │
│ │ Zustand Store Layer │ │
│ │ mapEditorStore │ inspectorStore │ entityEditorStore │ collab │ │
│ └────┬──────────────────────┬───────────────────────────────┬────┘ │
│ │ │ │ │
│ ┌────▼──────────────────────▼───────────────────────────────▼────┐ │
│ │ API + WebSocket Layer │ │
│ │ api/world.ts │ api/entities.ts │ api/editor.ts │ ws client │ │
│ └────┬──────────────────────┬───────────────────────────────┬────┘ │
│ │ │ │ │
└───────┼──────────────────────┼───────────────────────────────┼──────┘
│ HTTP │ HTTP WS │
┌───────▼──────────────────────▼───────────────────────────────▼──────┐
│ MAID Engine Server │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Admin API │ │ World API │ │ WebSocket Manager │ │
│ │ /admin/* │ │ /admin/world │ │ /admin/ws │ │
│ │ │ │ │ │ channels: metrics, │ │
│ │ │ │ │ │ entities, logs, events, │ │
│ │ │ │ │ │ editor (NEW) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────┬───────────────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼──────────────────────▼───────────────┐ │
│ │ World / ECS Layer │ │
│ │ World │ EntityManager │ ComponentRegistry │ EventBus │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
3.2 Frontend Architecture¶
The visual tools extend the existing admin frontend. New pages are added alongside
Dashboard.tsx, Entities.tsx, World.tsx, etc. Shared components are extracted
from the existing component library.
New Pages:
| Page | Route | Description |
|---|---|---|
MapEditor.tsx |
/map |
Full-screen world map editor (replaces simple World.tsx) |
NPCEditor.tsx |
/npcs/:id? |
NPC creation and editing |
ItemEditor.tsx |
/items/:id? |
Item creation and editing |
QuestEditor.tsx |
/quests/:id? |
Quest flow editor |
DialogueEditor.tsx |
/dialogue/:id? |
Conversation tree editor |
BulkEditor.tsx |
/bulk |
Spreadsheet-style entity grid |
ContentBrowser.tsx |
/content |
Searchable content library |
BalanceDashboard.tsx |
/balance |
Game balance analytics |
New Components:
components/
├── editor/
│ ├── MapCanvas.tsx # React Flow wrapper with custom nodes/edges
│ ├── RoomNode.tsx # Custom React Flow node for rooms
│ ├── ExitEdge.tsx # Custom React Flow edge for exits
│ ├── Minimap.tsx # Minimap overlay for large maps
│ ├── MapToolbar.tsx # Create/connect/select/pan tools
│ ├── MapLayers.tsx # Layer toggle panel (NPCs, items, zones)
│ ├── MapFilters.tsx # Filter by zone, level, tags
│ └── MapContextMenu.tsx # Right-click context menu
├── inspector/
│ ├── RoomInspector.tsx # Room detail panel
│ ├── DescriptionEditor.tsx # Rich text with time/season variants
│ ├── ExitList.tsx # Exit management panel
│ ├── ContentsPanel.tsx # NPCs/items in room
│ ├── ComponentEditor.tsx # Raw component JSON editor
│ ├── PropertiesPanel.tsx # Flags, attributes, metadata
│ └── HistoryPanel.tsx # Modification log
├── npc/
│ ├── NPCIdentityForm.tsx # Name, description, archetype
│ ├── StatSliders.tsx # Visual stat editing
│ ├── AIDialogueConfig.tsx # AI provider, personality, knowledge
│ ├── ScheduleTimeline.tsx # 24-hour visual timeline
│ ├── BehaviorRadarChart.tsx # Need weights radar chart
│ ├── RelationshipGraph.tsx # NPC relationship network
│ ├── MemoryViewer.tsx # Episodic memory browser
│ └── DialoguePreview.tsx # Live AI dialogue test
├── item/
│ ├── ItemTypeForm.tsx # Type selector + type-specific fields
│ ├── RequirementsEditor.tsx # Level, class, skill requirements
│ ├── CraftingRecipeEditor.tsx # Recipe graph editor
│ ├── LootTableEditor.tsx # Weighted drop table
│ └── BalanceComparison.tsx # Side-by-side item comparison
├── quest/
│ ├── QuestCanvas.tsx # React Flow for objective graph
│ ├── ObjectiveNode.tsx # Custom node for quest objectives
│ ├── BranchEdge.tsx # Conditional edge with logic
│ ├── QuestSimulator.tsx # Step-through quest flow
│ └── RewardEditor.tsx # Quest reward configuration
├── dialogue/
│ ├── DialogueCanvas.tsx # React Flow for conversation tree
│ ├── DialogueNode.tsx # Conversation beat node
│ ├── ResponseEdge.tsx # Player response edge
│ ├── ConditionEditor.tsx # Skill check/item/relationship conditions
│ ├── EffectEditor.tsx # Give/take items, change relationships
│ └── DialoguePreviewPane.tsx # Simulated player conversation
├── bulk/
│ ├── EntityGrid.tsx # AG Grid or similar spreadsheet
│ ├── InlineEditor.tsx # Cell-level editing
│ ├── BulkOperations.tsx # Multi-select operations
│ ├── CSVImportExport.tsx # CSV bridge
│ └── DiffPreview.tsx # Before/after change preview
├── browser/
│ ├── ContentSearch.tsx # Full-text search with filters
│ ├── ContentCard.tsx # Preview card for entity
│ ├── CategoryTree.tsx # Hierarchical category navigation
│ ├── TemplateLibrary.tsx # Reusable templates
│ └── DragDropHandler.tsx # Drag-to-map integration
├── balance/
│ ├── CombatCurves.tsx # DPS/HP by level chart
│ ├── EconomySankey.tsx # Gold flow Sankey diagram
│ ├── ContentHeatmap.tsx # Room content density map
│ ├── ProgressionChart.tsx # XP/level curves
│ └── LootDistribution.tsx # Drop rate analysis
├── preview/
│ ├── EmbeddedTerminal.tsx # xterm.js in a panel
│ ├── HotReloadBridge.tsx # Editor → live world sync
│ ├── GhostModeToggle.tsx # Invisible builder mode
│ └── SessionRecorder.tsx # Record test sessions
└── collaboration/
├── PresenceCursors.tsx # Other builders' cursor overlays
├── LockIndicator.tsx # Entity lock status badges
├── ChangeLog.tsx # Recent changes feed
└── ReviewPanel.tsx # Change review workflow (post-v1)
3.2a API Data Gap Analysis¶
The visual editor panels require data that the current backend endpoints do not fully provide. This table identifies gaps that must be closed before each panel is functional.
Current GraphNode payload (world.py:192-208, types/api.ts:206-219):
| Field | Available | Notes |
|---|---|---|
id |
✓ | Room UUID |
label |
✓ | Room name |
area_id |
✓ | Zone UUID (API field maps to zone assignment) |
area_name |
✓ | Zone display name (API field maps to zone assignment) |
player_count |
✓ | Live player count |
npc_count |
✗ | Needed for NPC layer badges |
item_count |
✗ | Needed for item layer badges |
tags |
✗ | Needed for tag-based filtering |
zone_id |
✗ | Needed for zone overlay/filtering |
coordinates |
✗ | Needed for grid mode (only if GridManager active) |
lock_state |
✗ | Needed for collaboration lock badges |
Current RoomResponse payload (world.py:42-54, types/api.ts:161-171):
| Field | Available | Needed By |
|---|---|---|
id, name, description |
✓ | Inspector header, description tab |
area_id, area_name |
✓ | Inspector header (maps to zone assignment) |
exits[] (direction, dest, lock) |
✓ | Exits tab |
entity_count, player_count |
✓ | Contents tab (count only) |
metadata |
✓ | Properties tab (custom data) |
tags[] |
✗ | Inspector header, properties tab, filtering |
npc_list[] (id, name) |
✗ | Contents tab (NPC names) |
item_list[] (id, name) |
✗ | Contents tab (item names) |
zone_id |
✗ | Inspector header |
updated_at |
✗ | History tab, revision tracking (see §16.2) |
revision |
✗ | Required for exclusive locking (see §16.2) |
extended_room data |
✗ | Description tab (time/season/weather/mood) |
flags[] |
✗ | Properties tab |
Current EntityResponse payload (entities.py, types/api.ts:85-98):
| Field | Available | Needed By |
|---|---|---|
id, tags[], components[] |
✓ | NPC/Item editors |
name (derived from components) |
✗ | Content browser, search results |
entity_type (npc/item/room) |
✗ | Content browser categorization |
room_id (location) |
✗ | "In room" display, drag-and-drop |
Required Backend Milestones (before Tier 3 panels are fully functional):
| Milestone | Endpoints Affected | Enables |
|---|---|---|
Enrich GraphNode with npc/item counts, tags, zone |
GET /admin/world/graph |
Map layers, filtering |
Add tags/zone/revision to RoomResponse |
GET /admin/world/rooms/{id} |
Inspector, locking |
Add entity list to RoomResponse (or new sub-endpoint) |
New: GET /admin/world/rooms/{id}/entities |
Contents tab |
Add extended room data to RoomResponse |
Enrich existing endpoint | Description tab variants |
| Add room/world broadcasting | world.py route handlers |
Live sync across editor clients |
Add revision field to rooms |
Room model + API | Exclusive locking |
| Add NPC dialogue preview endpoint | New: POST /admin/npc/{id}/dialogue/preview |
NPC editor live preview (§6.3) |
| Add viewport-paginated graph endpoint | New: GET /admin/world/graph/viewport |
Large world support |
3.3 State Management¶
New Zustand stores extend the existing stores/ directory:
// stores/mapEditorStore.ts
interface MapEditorState {
// Canvas state
viewport: { x: number; y: number; zoom: number };
selectedNodes: string[];
selectedEdges: string[];
activeTool: 'select' | 'create' | 'connect' | 'pan' | 'erase';
// Layer visibility
layers: {
rooms: boolean;
exits: boolean;
npcs: boolean;
items: boolean;
zones: boolean;
grid: boolean;
wilderness: boolean;
};
// Filters
filters: {
zoneId: string | null;
areaId: string | null;
levelRange: [number, number] | null;
tags: string[];
searchQuery: string;
};
// Layout
layoutMode: 'force' | 'manual' | 'grid' | 'hierarchical';
nodePositions: Record<string, { x: number; y: number }>;
positionsDirty: boolean;
// Actions
setViewport: (viewport: Partial<MapEditorState['viewport']>) => void;
selectNodes: (ids: string[]) => void;
setActiveTool: (tool: MapEditorState['activeTool']) => void;
toggleLayer: (layer: keyof MapEditorState['layers']) => void;
setFilter: (filter: Partial<MapEditorState['filters']>) => void;
saveLayout: () => Promise<void>;
}
// stores/inspectorStore.ts
interface InspectorState {
// Current target
targetId: string | null;
targetType: 'room' | 'npc' | 'item' | 'exit' | null;
// Active tab
activeTab: string;
// Pending edits (not yet saved)
pendingEdits: Record<string, unknown>;
isDirty: boolean;
// Actions
openInspector: (id: string, type: InspectorState['targetType']) => void;
closeInspector: () => void;
setActiveTab: (tab: string) => void;
updateField: (path: string, value: unknown) => void;
saveChanges: () => Promise<void>;
discardChanges: () => void;
}
// stores/collaborationStore.ts
interface CollaborationState {
// Connected users
users: Map<string, { id: string; name: string; color: string; cursor?: Position }>;
// Locks
locks: Map<string, { userId: string; userName: string; acquiredAt: string }>;
// Change feed
changes: ChangeEntry[];
// Actions
acquireLock: (entityId: string) => Promise<boolean>;
releaseLock: (entityId: string) => void;
updateCursor: (position: Position) => void;
}
// stores/entityEditorStore.ts
interface EntityEditorState {
// Current editor target (NPC, item, quest, etc.)
entityId: string | null;
entityData: Record<string, unknown> | null;
originalData: Record<string, unknown> | null;
// Form state
isDirty: boolean;
validationErrors: Record<string, string>;
// Template
templateId: string | null;
// Actions
loadEntity: (id: string) => Promise<void>;
createEntity: (type: string, data: Record<string, unknown>) => Promise<string>;
updateField: (path: string, value: unknown) => void;
validate: () => boolean;
save: () => Promise<void>;
reset: () => void;
}
3.4 Undo/Redo Architecture¶
Current State (honest assessment): The existing useUndoRedo hook in
hooks/useUndoRedo.ts provides a local UI convenience but is not identity-
preserving. Undoing a room deletion recreates a new room with a new UUID (via
createRoom), losing the original entity identity, exits, and component state.
Redoing a creation similarly produces a fresh room. The hook stores only the data
needed for API calls (name, description, area_id, metadata), not the full
entity state including UUID, components, tags, or exit references.
Tier 3 requires a new durable operation log that cannot be built on top of the existing hook. The new system must:
- Preserve entity identity: Undo of delete must restore the original UUID, components, tags, and exit connections — not create a new entity.
- Store reversible payloads: Each operation records both the forward mutation and the complete reverse state (full entity snapshot for deletes).
- Integrate with the server: Operations are acknowledged by the server with a revision number (see §16.2) before being committed to the undo stack.
- Support composite operations: A "dig room" action (create room + create exit + create reverse exit) is a single undo unit.
New Design:
// hooks/useEditorHistory.ts — replaces useUndoRedo for Tier 3 editors
export interface EditorOperation {
/** Unique operation ID (UUID) */
id: string;
/** Operation type */
type: EditorOperationType;
/** Timestamp when performed */
timestamp: number;
/** Human-readable description */
description: string;
/** Entity IDs affected (for lock coordination) */
entityIds: string[];
/** Server revision number after this operation was applied */
serverRevision: number;
/** Complete data needed to redo this operation */
forward: OperationPayload;
/** Complete data needed to undo this operation (full entity snapshots) */
reverse: OperationPayload;
}
export type EditorOperationType =
// Room operations
| 'create_room'
| 'delete_room'
| 'update_room'
| 'create_exit'
| 'delete_exit'
// Entity operations
| 'create_entity'
| 'delete_entity'
| 'update_entity'
| 'add_component'
| 'remove_component'
| 'update_component'
// Composite
| 'composite';
interface OperationPayload {
/** For create: full entity data including desired UUID */
entities?: EntitySnapshot[];
/** For update: field-level patches */
patches?: FieldPatch[];
/** For delete: full entity snapshots for restoration */
snapshots?: EntitySnapshot[];
/** For composite: ordered list of sub-operations */
operations?: EditorOperation[];
}
interface EntitySnapshot {
id: string; // Preserves original UUID
tags: string[];
components: Record<string, unknown>; // Full component data
exits?: ExitData[]; // For rooms
metadata?: Record<string, unknown>;
}
The existing useUndoRedo hook remains in World.tsx for backward compatibility
during the migration period (see Appendix F). New editor pages use
useEditorHistory exclusively.
Keyboard shortcuts: Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y are retained from the existing hook's implementation pattern.
3.5 Real-Time Protocol: WebSocket and Broadcasting Changes¶
Current State: The existing /admin/ws endpoint (websocket.py) supports four
channels via WebSocketChannel enum: metrics, entities, logs, events. The
message handler only processes ping, pong, subscribe, and unsubscribe —
unknown message types are logged and dropped. The broadcaster has methods for
broadcast_entity_created/updated/deleted, broadcast_metrics, broadcast_log_entry,
and broadcast_system_event. Critically, room/world topology changes are not
broadcast — the world admin router does not call the broadcaster.
Required Backend Changes (prerequisite for Tier 3):
- Add
EDITORchannel toWebSocketChannelenum:
class WebSocketChannel(str, Enum):
METRICS = "metrics"
ENTITIES = "entities"
LOGS = "logs"
EVENTS = "events"
EDITOR = "editor" # NEW: collaboration presence, locks, selections
- Add
WORLDchannel (or extendENTITIES) for room/exit topology changes:
- Add new
WebSocketMessageTypevalues:
class WebSocketMessageType(str, Enum):
# ... existing types ...
# World topology (broadcast on WORLD channel)
ROOM_CREATED = "room_created"
ROOM_UPDATED = "room_updated"
ROOM_DELETED = "room_deleted"
EXIT_CREATED = "exit_created"
EXIT_DELETED = "exit_deleted"
# Editor collaboration (broadcast on EDITOR channel)
EDITOR_CURSOR = "editor_cursor"
EDITOR_LOCK_GRANTED = "editor_lock_granted"
EDITOR_LOCK_DENIED = "editor_lock_denied"
EDITOR_LOCK_RELEASED = "editor_lock_released"
EDITOR_PRESENCE = "editor_presence"
EDITOR_SELECTION = "editor_selection"
- Extend message handler to process editor-specific incoming messages:
# In _handle_client_message:
elif msg_type == "editor_cursor":
# Broadcast cursor position to other EDITOR subscribers
await self._broadcast_to_others(client_id, WebSocketChannel.EDITOR, data)
elif msg_type == "editor_lock_acquire":
# Attempt to acquire exclusive lock (see §16.2)
result = await self._lock_manager.acquire(
entity_id=data["payload"]["entity_id"],
user_id=client.user_id,
timeout_seconds=300,
)
# Send lock_granted or lock_denied back to requester
elif msg_type == "editor_lock_release":
await self._lock_manager.release(
entity_id=data["payload"]["entity_id"],
user_id=client.user_id,
)
# Broadcast lock_released to all EDITOR subscribers
- Add room broadcast calls to the world admin router (
world.py):
# In create_room handler, after world.register_room():
await broadcaster.broadcast(
WebSocketMessage(
type=WebSocketMessageType.ROOM_CREATED,
channel=WebSocketChannel.WORLD,
data={"room_id": room_id, "name": name, "area_id": area_id},
),
channel=WebSocketChannel.WORLD,
)
Similar broadcasts must be added to update_room, delete_room, create_exit,
and delete_exit.
Scope limitation (v1): These broadcasts cover changes made via the admin REST API (visual editor, API clients). Changes made via in-game builder commands (
@dig,@create,@describe) callworld.register_room()directly — they bypass the admin API and therefore do not trigger these broadcasts. There is noRoomCreatedEventin the EventBus today; the closest event isGridRoomAddedEvent(for grid-registered rooms only).Post-v1 enhancement: To achieve true cross-surface sync, either: (a) Add a
RoomRegisteredEventtoWorld.register_room()and subscribe the WebSocket broadcaster to it, or (b) Have in-game builder commands call the admin API instead ofworld.register_room().Until then, builders must manually refresh the map to see in-game changes. The map toolbar includes a "Refresh" button for this purpose.
-
Rate limiting for cursor messages: Cursor updates are throttled server-side to 10 updates/second per client to prevent flooding.
-
Authentication: Editor channel inherits the same JWT-based auth as the existing
/admin/wsendpoint. Theeditorchannel requires at minimumAdminRole.VIEWERto subscribe (presence-only); lock acquisition requiresAdminRole.BUILDER.
Client-Side Protocol:
// Client → Server (via existing /admin/ws connection)
interface EditorClientMessage {
type: 'editor_cursor' | 'editor_lock_acquire' | 'editor_lock_release'
| 'editor_selection';
payload: Record<string, unknown>;
}
// Server → Client (broadcast to EDITOR subscribers)
interface EditorBroadcast {
type: 'editor_cursor' | 'editor_lock_granted' | 'editor_lock_denied'
| 'editor_lock_released' | 'editor_presence' | 'editor_selection';
channel: 'editor';
data: Record<string, unknown>;
timestamp: string;
}
// Server → Client (broadcast to WORLD subscribers)
interface WorldBroadcast {
type: 'room_created' | 'room_updated' | 'room_deleted'
| 'exit_created' | 'exit_deleted';
channel: 'world';
data: Record<string, unknown>;
timestamp: string;
}
Conflict Resolution: For v1, the editor uses server-enforced exclusive locking (not optimistic concurrency). Only one builder can edit a given entity at a time. If User A holds the lock on a room and User B attempts to edit it, User B sees a "Locked by User A" badge and cannot open the inspector in edit mode. See §16.2 for full locking design.
3.6 Content Pack Editor Extensions API¶
Current State: The ContentPack Protocol (plugins/protocol.py) is a
@runtime_checkable Protocol with 10 required methods. It does not include any
editor-related methods. Adding required methods to the Protocol would be a breaking
change for all existing content packs.
Design Decision: Duck-Typed Optional Method
Content packs that want to provide editor extensions implement an optional
get_editor_extensions() method. The engine discovers it via hasattr():
# packages/maid-engine/src/maid_engine/api/admin/editor_extensions.py
@router.get("/editor/extensions")
async def get_editor_extensions(
engine: GameEngine = Depends(get_engine),
admin: AdminUser = Depends(require_role(AdminRole.VIEWER)),
):
"""Return editor extension metadata from loaded content packs.
Uses duck-typed discovery — packs without get_editor_extensions() are skipped.
"""
extensions = []
for pack in engine.content_packs:
if hasattr(pack, 'get_editor_extensions'):
pack_extensions = pack.get_editor_extensions()
extensions.extend(pack_extensions)
return {"extensions": extensions}
Extension Manifest (metadata only — no frontend code):
For v1, extensions are metadata descriptors that tell the admin frontend which additional inspector tabs, pages, or toolbar buttons to render. The frontend implementation of these extensions is bundled into the admin frontend at build time, not loaded dynamically at runtime.
This is a deliberate simplification. Dynamic loading of pack-provided JavaScript introduces significant security concerns (XSS, supply chain attacks) and infrastructure complexity (static asset serving, CORS, CSP headers). For v1, the admin frontend ships with built-in support for known extension types.
// Extension manifest — describes what to render, not how
interface EditorExtensionManifest {
/** Content pack name */
packName: string;
/** Custom inspector tabs for specific component types */
inspectorTabs?: {
id: string;
label: string;
icon: string;
/** Which component type triggers this tab */
componentType: string;
/** Sort order */
priority: number;
}[];
/** Custom pages (metadata only — frontend must have matching route) */
pages?: {
id: string;
label: string;
icon: string;
route: string;
}[];
}
Build-Time Bundling:
Content packs that ship editor UI contribute React components to the admin frontend build. These are imported statically and registered in a component registry:
// components/extensions/registry.ts
// Built-in extension components (added at build time)
const EXTENSION_COMPONENTS: Record<string, React.LazyComponent> = {
'CombatStatsComponent': lazy(() => import('./classic-rpg/CombatStatsTab')),
'SpellCasterComponent': lazy(() => import('./classic-rpg/SpellListTab')),
};
// At runtime, match componentType from extension manifest to bundled component
function getExtensionComponent(componentType: string): React.ComponentType | null {
return EXTENSION_COMPONENTS[componentType] ?? null;
}
Future Enhancement (post-v1): A sandboxed iframe-based extension loading system
could allow runtime loading of third-party pack UI code with restricted permissions.
This would require:
- Content Security Policy (CSP) sandboxing
- postMessage-based communication protocol
- Static asset serving for pack bundles (/admin/packs/{name}/static/)
- Extension signature verification
This is explicitly out of scope for v1.
4. World Map Editor¶
The World Map Editor is the crown jewel of Tier 3 — a full-screen, web-based room
graph editor. It extends the existing World.tsx page (which already uses React Flow
for room graph visualization) into a complete spatial editing environment.
4.1 Full Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Toolbar ───────────────────────────────────────────────────────────────────┐ │
│ │ [🖱Select] [➕Create] [🔗Connect] [✋Pan] [🗑Erase] │ Layout: [Force▾] │ │
│ │ [↩Undo] [↪Redo] [💾Save Layout] │ Zoom: [−][100%][+] │ [👁Layers] [🔍Find]│ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ ┌─ Layer Panel ──┐ ┌─ Map Canvas ─────────────────────────────┐ ┌─Inspector──┐ │
│ │ │ │ │ │ │ │
│ │ ☑ Rooms │ │ ┌─────────┐ ┌─────────┐ │ │ Town Square│ │
│ │ ☑ Exits │ │ │ Town │ north │ Market │ │ │ ──────────-│ │
│ │ ☑ NPCs (12) │ │ │ Square │────────▶│ Street │ │ │ │ │
│ │ ☐ Items (47) │ │ │ 🧑×3 │◀────────│ 🧑×1 │ │ │ [Desc][Ex] │ │
│ │ ☑ Zones │ │ └────┬────┘ south └────┬────┘ │ │ [Con][Cmp] │ │
│ │ ☐ Grid Overlay │ │ │ │ │ │ [Prop][His]│ │
│ │ ☐ Wilderness │ │ south│ east │ │ │ │ │
│ │ │ │ │ │ │ │ A large │ │
│ │ ── Filters ── │ │ ┌────▼────┐ ┌────▼────┐ │ │ open plaza │ │
│ │ Zone: [All ▾] │ │ │ Temple │ │ Armory │ │ │ with a │ │
│ │ Level: [1-50] │ │ │ ⛪ │ │ ⚔ │ │ │ fountain │ │
│ │ Level: [1-50] │ │ │ │────────▶│ 🧑×1 │ │ │ in the │ │
│ │ Tags: [ ] │ │ └─────────┘ east └─────────┘ │ │ center. │ │
│ │ │ │ │ │ │ │
│ │ ── Search ── │ │ ┌───────────┐ │ │ NPCs: 3 │ │
│ │ [🔍 Find room ]│ │ │ Minimap │ [👤A] [👤B] │ │ Items: 0 │ │
│ │ │ │ │ ····· │ ↑ collaborator cursors │ │ Exits: 3 │ │
│ │ Results: │ │ │ ··■·· │ │ │ │ │
│ │ Town Square │ │ │ ····· │ Zone: "Starter Village" │ │ [Edit Full]│ │
│ │ Temple │ │ └───────────┘ ── zone color legend ── │ │ [Teleport] │ │
│ │ Market Street │ │ │ │ [Delete] │ │
│ └────────────────┘ └───────────────────────────────────────────┘ └────────────┘ │
│ ┌─ Status Bar ────────────────────────────────────────────────────────────────┐ │
│ │ Rooms: 247 │ Exits: 412 │ Selected: 1 │ Unsaved: 3 │ Users: 2 │ Zoom: 100%│ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
4.2 Canvas: Rooms as Nodes, Exits as Edges¶
The map canvas renders rooms as React Flow custom nodes and exits as directed edges.
This leverages the reactflow dependency already in admin_frontend/package.json
and the GraphNode/GraphEdge types from types/api.ts.
Room Node Design:
// components/editor/RoomNode.tsx
interface RoomNodeData {
id: string;
label: string; // Room name
area_id: string | null; // Zone UUID (API field name)
area_name: string; // Zone display name (API field name)
area_color: string; // Derived from zone for visual grouping (via area_id)
player_count: number;
npc_count: number;
item_count: number;
is_locked: boolean; // Locked by another editor
locked_by: string | null;
is_selected: boolean;
is_highlighted: boolean; // Search result highlight
zone_id: string | null;
tags: string[];
}
Each room node renders as a rounded rectangle with: - Header bar: Color-coded by zone, with room name - Icons row: NPC count (🧑), item count (📦), player count (🎮), lock (🔒) - Connection handles: Cardinal direction ports (N/S/E/W) + up/down indicators
Exit Edge Design:
// components/editor/ExitEdge.tsx
interface ExitEdgeData {
direction: string; // "north", "south", "east", "west", "up", "down"
is_locked: boolean;
is_bidirectional: boolean;
key_id: string | null;
label: string; // Direction label on edge
}
Edges are drawn as curved arrows with direction labels. Bidirectional exits show double arrows. Locked exits have a dashed stroke with a lock icon.
4.3 Room Creation¶
Click-to-Create: With the Create tool active, clicking empty canvas space opens an inline form:
┌──────────────────────────┐
│ New Room │
│ Name: [ ] │
│ Zone: [Starter Town ▾] │
│ Template: [Empty Room ▾] │
│ [Create] [Cancel] │
└──────────────────────────┘
On creation, the room appears at the click position and a create_room undo
operation is recorded via useUndoRedo. The API call uses createRoom() from
api/world.ts.
Drag-to-Connect: With the Connect tool, dragging from one room's handle to another creates an exit. A direction picker appears:
┌────────────────────────┐
│ Connect Rooms │
│ From: Town Square │
│ To: Market Street │
│ Direction: [north ▾] │
│ ☑ Bidirectional │
│ ☐ Locked │
│ [Create Exit] [Cancel] │
└────────────────────────┘
4.4 Multi-Select and Bulk Operations¶
Shift-Click: Add/remove individual rooms from selection. Drag-Select: Rubber-band selection rectangle. Ctrl+A: Select all visible rooms.
Bulk operations on selected rooms:
- Move: Drag to reposition layout (batch move_node undo operations)
- Set Zone: Assign all selected to a zone
- Add/Remove Tags: Bulk tag management
- Delete: Bulk delete with confirmation and dependency check
- Export: Export selected rooms as YAML
4.5 Zoom, Pan, and Minimap¶
Standard canvas navigation with React Flow's built-in controls: - Scroll wheel: Zoom (0.1x to 4x) - Middle-click drag or Pan tool: Pan viewport - Minimap: Fixed bottom-left overlay showing entire graph with viewport rectangle - Fit View: Double-click minimap to fit all rooms - Focus Room: Double-click a room to center and zoom to it
4.6 Layers¶
Toggle overlays on the map canvas. Each layer adds visual indicators to room nodes or the canvas background:
| Layer | Default | What It Shows |
|---|---|---|
| Rooms | ☑ On | Room nodes with names |
| Exits | ☑ On | Directed edges between rooms |
| NPCs | ☑ On | NPC count badges on rooms, NPC icons at high zoom |
| Items | ☐ Off | Item count badges, item type icons at high zoom |
| Zones | ☑ On | Zone boundary overlays (colored regions) |
| Grid Overlay | ☐ Off | Coordinate grid for GridManager-enabled worlds |
| Wilderness | ☐ Off | Procedural terrain zone boundaries and biome colors |
| Players | ☐ Off | Live player positions (green dots) |
| Heatmap | ☐ Off | Room visit frequency heatmap overlay |
4.7 Filters¶
Filters reduce which rooms are visible on the canvas:
interface MapFilters {
zoneId: string | null; // Show only rooms in zone
areaId: string | null; // Filter by zone (API field name is area_id)
levelRange: [number, number]; // Show rooms matching level range
tags: string[]; // Show rooms with ALL of these tags
searchQuery: string; // Full-text search on room name/description
hasNPCs: boolean | null; // Only rooms with/without NPCs
hasItems: boolean | null; // Only rooms with/without items
connectedTo: string | null; // Only rooms reachable from this room (BFS)
}
Filtered-out rooms are either hidden or shown as ghosted (semi-transparent), controlled by a "Hide filtered" / "Ghost filtered" toggle.
4.8 Import/Export (YAML Round-Trip)¶
The map editor can export live-world rooms to Tier 1-compatible YAML files and import YAML files into the live world. This bridges the visual editor (live-world authoring) with the YAML workflow (version-controlled definitions).
Export:
- Selected rooms → YAML file in Tier 1 loader-compatible format with room-specific
top-level fields (exits, zone) that the loader accepts as convenience aliases
(defined in entity_types.py:allowed_top_level_fields), plus components: for
everything else
- The visual editor produces loader-compatible YAML. While structurally similar to
@export output, it includes room-specific top-level fields (exits, zone)
that @export does not emit — @export produces a pure component-centric
format with only _id, components, and tags per entity
- Includes all component data, tags, and exit references
- Layout positions saved in a separate sidecar file (*.layout.json) so the YAML
file is clean for version control and the Tier 1 pipeline
- Export is a snapshot of live-world state, not a diff
Import:
- Load YAML files in Tier 1 canonical format into the live world
- Client-side validation (schema shape, required fields) before sending to server
- Diff preview before import ("3 rooms added, 1 modified, 0 deleted")
- Rooms appear at saved layout positions (from sidecar file) or auto-layout
- Import creates/updates entities via the admin REST API (POST /admin/world/rooms,
PUT /admin/world/rooms/{id}, POST /admin/world/exits). This is a different code
path from maid data load, which goes through the full Tier 1 pipeline (6 phases:
Discover → Parse → Prepare → ResolveRefs → Validate → Apply). The visual editor import
calls the same REST endpoints that the map editor uses for interactive editing.
Visual editor export format (Tier 1 loader-compatible):
The visual editor exports rooms in the Tier 1 loader input format, which accepts
top-level exits and zone fields for rooms (defined in entity_types.py as
allowed_top_level_fields). This is the same shape builders author by hand and the
format the visual editor's import feature expects:
# Visual editor export — Tier 1 loader-compatible shape (NOT identical to @export)
_meta:
schema: "maid:room:v1"
exported_by: "visual-editor"
exported_at: "2025-07-18T10:30:00Z"
rooms:
- _id: town_square
exits:
north: "@ref:market_street"
south: "@ref:temple"
zone: starter_area
components:
DescriptionComponent:
name: "Town Square"
short_desc: "A large open plaza with a fountain in the center."
long_desc: >
A large open plaza with a fountain in the center.
Cobblestone paths radiate outward to the surrounding
shops and temples. The fountain burbles softly.
ExtendedRoomComponent:
mood: "peaceful"
time_descriptions:
dawn: "The plaza is quiet, dew glistening on the cobblestones."
night: "Moonlight glints off the fountain's surface."
random_details:
- text: "A cat naps on the fountain rim."
weight: 3
tags:
- safe_zone
- spawn_point
- _id: market_street
exits:
south: "@ref:town_square"
components:
DescriptionComponent:
name: "Market Street"
short_desc: "A narrow cobblestone street lined with merchant stalls."
tags: []
Contrast: @export output format (component-centric, different shape):
The @export builder command (commands/building/export.py) iterates over all
non-runtime components on the entity and serializes them into a pure component-
centric format: each entity has only _id, components, and tags. It does
not emit top-level exits or zone — those room-specific convenience fields
are a Tier 1 loader feature, not part of @export's output contract. The @export
output is:
# @export output — component-centric shape
_meta:
schema: "maid:room:v1"
rooms:
- _id: town_square
components:
DescriptionComponent:
name: "Town Square"
short_desc: "A large open plaza with a fountain in the center."
long_desc: "A large open plaza with a fountain..."
ExtendedRoomComponent:
mood: "peaceful"
time_descriptions:
dawn: "The plaza is quiet, dew glistening on the cobblestones."
night: "Moonlight glints off the fountain's surface."
random_details:
- text: "A cat naps on the fountain rim."
weight: 3
tags:
- safe_zone
- spawn_point
Note: Room exits in MAID are stored on
RoomDataobjects registered withworld.register_room(), not as ECS components. The@exportcommand only serializes ECS components (viaentity.components), so exits may not appear in the export output. The visual editor's export feature must handle exits specially — reading them from the world's room registry and emitting them as top-levelexitsin the Tier 1 authoring format.
Round-trip strategy: The visual editor exports in the Tier 1 loader-compatible
format (with top-level exits and zone), ensuring that exported YAML can be
directly loaded by maid data load. This is deliberately different from @export's
pure component-centric format — the visual editor adds room-specific convenience
fields that the Tier 1 loader understands.
Sidecar layout file (not processed by Tier 1 pipeline):
{
"format": "maid-editor-layout-v1",
"positions": {
"town_square": { "x": 0, "y": 0 },
"market_street": { "x": 200, "y": 0 },
"temple": { "x": 0, "y": 200 }
}
}
Round-trip fidelity: Exporting a room that was originally loaded from YAML will
produce output that can be loaded back by the Tier 1 pipeline (same _meta.schema,
top-level exits, component keys), modulo:
- Whitespace normalization
- Runtime-only components stripped (DataProvenanceComponent, InstanceStateComponent)
- UUID references resolved to @ref:definition_id where provenance data exists
- Exits hoisted from runtime RoomData to top-level exits field
4.9 Grid Mode¶
For worlds using GridManager, an optional grid overlay snaps rooms to coordinates:
- Toggle with the "Grid Overlay" layer
- Rooms display their
(x, y, z)coordinate - New rooms snap to the nearest unoccupied grid cell
@grid.createequivalent via drag-select on empty grid cells- Color-coded by terrain type when wilderness data is available
4.10 Wilderness Overlay¶
For worlds with WildernessManager, the map shows procedural terrain zones:
- Biome regions rendered as semi-transparent colored overlays
- Landmark icons for
@wilderness.landmarkentries - "Preview" mode renders terrain without generating rooms
- Click a wilderness coordinate to force-generate a room (equivalent to
@wilderness.generate x y)
4.11 Layout Algorithms¶
Multiple layout strategies accessible from the toolbar dropdown:
| Layout | Description | Best For |
|---|---|---|
| Force-Directed | d3-force simulation, rooms repel, exits attract | Organic exploration areas |
| Manual | Free placement, positions saved per-user | Fine-tuned layouts |
| Grid | Snap to grid based on exit directions | Structured dungeons |
| Hierarchical | Tree layout from a root room | Quest progression paths |
| Zone-Clustered | Force-directed within zones, zones repel | Large worlds with multiple zones |
Layout positions are saved to the user's browser (localStorage) and optionally to the
server for sharing with collaborators. (IndexedDB caching is deferred to post-v1 — see §14.4.)
4.12 Map Context Menu¶
Right-click on the canvas or a room node for context-sensitive actions:
On Canvas (empty space):
┌──────────────────────┐
│ Create Room Here │
│ Paste Room(s) │
│ ───────────────── │
│ Fit All Rooms │
│ Reset Layout │
│ ───────────────── │
│ Import YAML... │
│ Export Selection... │
└──────────────────────┘
On Room Node:
┌──────────────────────┐
│ Edit Room │
│ Connect To... │
│ ───────────────── │
│ Teleport Here │
│ Play From Here │
│ ───────────────── │
│ Copy Room │
│ Duplicate Room │
│ Delete Room │
│ ───────────────── │
│ Select Connected │
│ Select Zone │
│ ───────────────── │
│ Export as YAML │
│ View in Terminal │
└──────────────────────┘
4.13 Technology Stack¶
| Concern | Technology | Rationale |
|---|---|---|
| Node graph | React Flow (already in package.json) |
Mature, performant, custom nodes/edges |
| Layout | d3-force (via React Flow layout) | Standard force-directed layout |
| Canvas rendering | SVG (React Flow default) | Good for <5000 nodes; see §17 for WebGL |
| State | Zustand (mapEditorStore) |
Consistent with existing stores |
| Undo/Redo | useUndoRedo hook (existing) |
Already implemented for room/exit ops |
| Real-time | WebSocket (/admin/ws) |
Existing infrastructure |
| Keyboard shortcuts | React Flow + custom handlers | Ctrl+Z/Y already wired |
5. Room Inspector Panel¶
The Room Inspector is a Unity/Godot-style detail panel that opens when a room is selected on the map (or directly from the Content Browser). It slides in from the right side of the map canvas.
5.1 Full Layout Wireframe¶
┌─ Room Inspector ──────────────────────────────────┐
│ │
│ ┌─ Header ──────────────────────────────────────┐ │
│ │ 📍 Town Square [✕] │ │
│ │ UUID: 3f2a...8c1d Zone: Starter Village │ │
│ │ Tags: [safe] [spawn] [+] │ │
│ │ Created: 2025-07-10 Modified: 2 min ago │ │
│ │ [🔒Lock for editing] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─ Tabs ────────────────────────────────────────┐ │
│ │ [Description] [Exits] [Contents] [Components] │ │
│ │ [Properties] [History] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─ Description Tab ─────────────────────────────┐ │
│ │ │ │
│ │ Base Description: │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ A large open plaza with a fountain in │ │ │
│ │ │ the center. Cobblestone paths radiate │ │ │
│ │ │ outward to the surrounding shops and │ │ │
│ │ │ temples. The fountain burbles softly. │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ── Time Variants ── │ │
│ │ 🌅 Dawn: [The plaza is quiet, dew...] │ │
│ │ ☀️ Day: [Merchants call out their...] │ │
│ │ 🌙 Night: [Moonlight glints off the...] │ │
│ │ [+ Add Time Variant] │ │
│ │ │ │
│ │ ── Season Variants ── │ │
│ │ 🌸 Spring: [Cherry blossoms drift...] │ │
│ │ ❄️ Winter: [Snow blankets the plaza...] │ │
│ │ [+ Add Season Variant] │ │
│ │ │ │
│ │ ── Weather Effects ── │ │
│ │ 🌧️ Rain: [Puddles form between the...] │ │
│ │ [+ Add Weather Effect] │ │
│ │ │ │
│ │ ── Random Details ── │ │
│ │ 1. "A cat naps on the fountain rim" (w:3) │ │
│ │ 2. "Children chase each other" (w:2) │ │
│ │ [+ Add Detail] │ │
│ │ │ │
│ │ Mood: [Peaceful ▾] Atmosphere: [ ] │ │
│ │ │ │
│ │ ── Preview ── │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Town Square │ │ │
│ │ │ A large open plaza with a fountain in │ │ │
│ │ │ the center. Cherry blossoms drift │ │ │
│ │ │ across the cobblestones. A cat naps on │ │ │
│ │ │ the fountain rim. │ │ │
│ │ │ │ │ │
│ │ │ Exits: north, south, east │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ Preview: [Spring ▾] [Day ▾] [Clear ▾] │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌─ Actions ─────────────────────────────────────┐ │
│ │ [💾 Save Changes] [↩ Discard] [🗑 Delete] │ │
│ └───────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
5.2 Description Tab¶
The Description tab provides a rich editing experience for room descriptions,
matching the capabilities of the in-game @describe, @room.desc.time,
@room.desc.season, @room.desc.weather, and @room.detail commands.
Base Description: A textarea with: - Character count (MUDs typically aim for 3-5 lines) - ANSI color code preview (renders color codes visually) - AI assist button ("Improve description" → sends to Tier 2 AI pipeline)
Time/Season/Weather Variants: Expandable sections matching
ExtendedRoomComponent fields. Each variant has its own text editor.
Random Details: Weighted detail list with drag-to-reorder, matching
@room.detail.add functionality.
Live Preview: Renders the combined description as a player would see it, using selectable time/season/weather dropdowns to preview variants.
5.3 Exits Tab¶
┌─ Exits Tab ──────────────────────────────────────┐
│ │
│ Direction │ Destination │ Locked │ Acts │
│ ──────────────────────────────────────────────── │
│ ▶ north │ 📍 Market Street │ ☐ │ [✎🗑]│
│ ▶ south │ 📍 Temple │ ☐ │ [✎🗑]│
│ ▶ east │ 📍 Armory │ ☑ │ [✎🗑]│
│ │ Key: iron_key │ │ │
│ │
│ [+ Add Exit] [🔗 Quick Connect...] │
│ │
│ ── Destination Preview ── │
│ (hover over an exit to preview destination) │
│ ┌──────────────────────────────────────────┐ │
│ │ Market Street │ │
│ │ A narrow cobblestone street lined with │ │
│ │ merchant stalls... │ │
│ │ NPCs: 1 (Merchant Giles) │ │
│ │ Items: 3 │ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
- Click an exit row to expand and edit its properties (lock state, key, description)
- "Quick Connect" opens a searchable room picker
- Destination preview shows on hover/focus
- Clicking the destination name navigates the map to that room
5.4 Contents Tab¶
┌─ Contents Tab ───────────────────────────────────┐
│ │
│ ── NPCs (3) ── │
│ ┌──────────────────────────────────────────┐ │
│ │ 🧑 Guard Captain Mara [✎] [↗] [×] │ │
│ │ 🧑 Old Fisherman [✎] [↗] [×] │ │
│ │ 🧑 Stray Dog [✎] [↗] [×] │ │
│ └──────────────────────────────────────────┘ │
│ [+ Add NPC] [📋 From Template] │
│ │
│ ── Items (0) ── │
│ (no items) │
│ [+ Add Item] [📋 From Template] │
│ │
│ ── Drop Zone ──────────────────────────────┐ │
│ │ Drag NPCs or items from Content Browser │ │
│ │ to add them to this room │ │
│ └──────────────────────────────────────────┘ │
│ │
│ [✎] = Edit [↗] = Open in editor [×] = Remove │
└───────────────────────────────────────────────────┘
- Lists all entities currently positioned in the room
- Drag-and-drop from the Content Browser to add entities
- [↗] opens the entity in its dedicated editor (NPC Editor, Item Editor)
- Supports drag-to-reorder (visual only; room contents are unordered)
5.5 Components Tab (Advanced)¶
Raw component editing for power users. Shows all components attached to the room entity, editable as JSON with schema validation.
┌─ Components Tab ─────────────────────────────────┐
│ │
│ PositionComponent [−] [✎] │
│ { "room_id": "3f2a...8c1d", "x": 0, "y": 0 } │
│ │
│ ExtendedRoomComponent [−] [✎] │
│ { "mood": "peaceful", │
│ "time_descriptions": { ... }, │
│ "random_details": [ ... ] } │
│ │
│ [+ Add Component ▾] │
│ ┌─────────────────────────┐ │
│ │ HealthComponent │ │
│ │ InventoryComponent │ │
│ │ CombatStatsComponent │ ← from classic-rpg │
│ │ SpawnPointComponent │ │
│ │ ... │ │
│ └─────────────────────────┘ │
│ │
│ Component types from: maid-engine (3), │
│ maid-stdlib (8), maid-classic-rpg (12) │
└───────────────────────────────────────────────────┘
The component type dropdown is populated dynamically from getComponentTypes()
(the /admin/entities/types endpoint), which returns types registered by all
loaded content packs.
5.6 Properties Tab¶
┌─ Properties Tab ─────────────────────────────────┐
│ │
│ ── Flags ── │
│ ☑ safe_zone ☑ spawn_point ☐ no_combat │
│ ☐ no_magic ☐ no_teleport ☐ dark │
│ ☐ underwater ☐ indoors ☐ persistent │
│ [+ Custom Flag: [ ] [Add]] │
│ │
│ ── Attributes ── │
│ light_level: [████████░░] 80 │
│ noise_level: [██░░░░░░░░] 20 │
│ difficulty: [█████░░░░░] 50 │
│ [+ Add Attribute] │
│ │
│ ── Custom Data ── │
│ { "respawn_timer": 300, │
│ "max_occupancy": 20, │
│ "ambient_sound": "fountain.ogg" } │
│ [Edit JSON] │
└───────────────────────────────────────────────────┘
5.7 History Tab¶
┌─ History Tab ────────────────────────────────────┐
│ │
│ 2 min ago │ admin │ Updated description │
│ 15 min ago │ builder_a │ Added exit: east │
│ 1 hour ago │ admin │ Created room │
│ 1 hour ago │ system │ Set zone: tutorial │
│ │
│ [Load More...] │
│ │
│ ── Diff View ── │
│ (click an entry to see diff) │
│ - "A plaza with a fountain." │
│ + "A large open plaza with a fountain in the │
│ + center. Cobblestone paths radiate outward." │
└───────────────────────────────────────────────────┘
History is sourced from the audit log (AuditLogger) and the entity change
tracking already built into the persistence layer.
6. NPC Editor¶
The NPC Editor is a dedicated full-page editor for creating and modifying NPCs. It combines identity fields, stat sliders, AI dialogue configuration, behavior archetypes, schedule timelines, and relationship graphs in a tabbed interface.
6.1 Full Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Header ──────────────────────────────────────────────────────────────────┐ │
│ │ NPC Editor: Guard Captain Mara [💾Save] [↩Discard] [🗑Delete] │ │
│ │ UUID: 7b4e...2f9a Zone: Starter Village Template: guard_captain │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Tabs ────────────────────────────────────────────────────────────────────┐ │
│ │[Identity][Stats][AI Dialogue][Behavior][Schedule][Inventory][Relations] │ │
│ │[Memory][Spawn] │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Identity Tab ───────────────────────────┐ ┌─ Preview ──────────────────┐ │
│ │ │ │ │ │
│ │ Name: [Guard Captain Mara ] │ │ Guard Captain Mara │ │
│ │ Short: [a stern guard captain ] │ │ A stern guard captain in │ │
│ │ Title: [Captain of the Town Guard ] │ │ polished armor stands at │ │
│ │ │ │ attention. Her hand rests │ │
│ │ Description: │ │ on the pommel of her │ │
│ │ ┌────────────────────────────────────┐ │ │ sword. She surveys the │ │
│ │ │ A stern guard captain in polished │ │ │ plaza with watchful eyes. │ │
│ │ │ armor stands at attention. Her │ │ │ │ │
│ │ │ hand rests on the pommel of her │ │ │ Level 15 Human Fighter │ │
│ │ │ sword. She surveys the plaza │ │ │ HP: ████████████░░ 85/100 │ │
│ │ │ with watchful eyes. │ │ │ Zone: Starter Village │ │
│ │ └────────────────────────────────────┘ │ │ Room: Town Square │ │
│ │ │ │ │ │
│ │ Race: [Human ▾] │ │ ── Quick Test ── │ │
│ │ Class: [Fighter ▾] │ │ You: [Hello, Captain ] │ │
│ │ Level: [███████████████░] 15 │ │ [Send Test Message] │ │
│ │ Gender: [Female ▾] │ │ │ │
│ │ Faction: [Town Guard ▾] │ │ Mara nods curtly. │ │
│ │ │ │ "Welcome to the village. │ │
│ │ Tags: [guard] [questgiver] [+] │ │ Keep your weapons │ │
│ │ │ │ sheathed within the │ │
│ │ [🤖 Generate Description with AI] │ │ walls." │ │
│ └──────────────────────────────────────────┘ └────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
6.2 Stats Tab (Visual Sliders)¶
Sliders for core attributes, backed by CombatStatsComponent or similar from
the loaded content pack. The component types are discovered dynamically.
┌─ Stats Tab ──────────────────────────────────────────────┐
│ │
│ ── Core Attributes ── │
│ Strength: [█████████░░░░░░░░░░░] 45 [−][+] │
│ Dexterity: [████████████░░░░░░░░] 60 [−][+] │
│ Constitution: [██████████████░░░░░░] 70 [−][+] │
│ Intelligence: [███████░░░░░░░░░░░░░] 35 [−][+] │
│ Wisdom: [████████████████░░░░] 80 [−][+] │
│ Charisma: [██████████░░░░░░░░░░] 50 [−][+] │
│ │
│ ── Derived Stats ── │
│ HP: 85/100 MP: 20/20 AC: 18 │
│ Attack: +7 Damage: 2d6+3 Speed: 30ft │
│ │
│ ── Resistances ── │
│ Fire: [░░░░░░░░░░] 0% Cold: [██░░░░░░░░] 20% │
│ Poison: [████░░░░░░] 40% Magic: [░░░░░░░░░░] 0% │
│ │
│ Total Point Budget: 340/400 [Rebalance Suggestions] │
└───────────────────────────────────────────────────────────┘
The "Rebalance Suggestions" button sends the NPC stats to the Tier 2 AI Balance Analysis engine and shows recommendations.
6.3 AI Dialogue Tab¶
Configuration for the NPC's AI dialogue behavior, backed by
NPCPromptConfig from maid_stdlib.ai.npc_prompts.
┌─ AI Dialogue Tab ────────────────────────────────────────┐
│ │
│ ── Provider ── │
│ Provider: [Default (anthropic) ▾] │
│ Model: [claude-sonnet-4-20250514 ▾] │
│ Max Tokens: [150 ] Temperature: [0.7 ] │
│ │
│ ── Personality ── │
│ ┌──────────────────────────────────────────────────┐ │
│ │ You are Mara, Captain of the Town Guard. You are │ │
│ │ stern, duty-focused, and fiercely protective of │ │
│ │ the village. You speak in clipped, military │ │
│ │ phrases. You respect strength but value │ │
│ │ discipline above all. │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ── Knowledge Base ── │
│ • Knows about: goblin raids, town history, guard duty │
│ • Doesn't know: magic, other regions, politics │
│ [+ Add Knowledge] [🤖 Generate from Lore] │
│ │
│ ── Conversation Boundaries ── │
│ ☑ Stays in character ☑ No meta-game knowledge │
│ ☑ Acknowledges ignorance ☐ Can break character for OOC │
│ │
│ ── Live Preview ── │
│ Player: "What do you know about the goblins?" │
│ Mara: "The green menaces struck again last night. │
│ Raided the outlying farms. I've doubled the patrols, │
│ but we need adventurers willing to track them to their │
│ camp." │
│ │
│ [Regenerate Response] [👍Good] [👎Bad] [✎Edit] │
└───────────────────────────────────────────────────────────┘
Backend gap: NPC dialogue preview. No admin-facing dialogue endpoint exists today.
The NPCDialogueSystem is an in-game system that handles talk/ask/greet commands
within a player session — it has no admin REST or WebSocket API. For the live preview
to work, a new endpoint is required:
| Gap | Current State | Required for Tier 3 |
|---|---|---|
| Dialogue preview endpoint | None — dialogue is in-game only | POST /admin/npc/{id}/dialogue/preview accepting { message, context_overrides }, returning AI response. Alternatively, a new NPC_DIALOGUE WS message family on the editor channel. |
This endpoint must be implemented as a Phase 2 backend prerequisite (NPC Editor). Until it exists, the live preview panel displays a placeholder: "Live preview requires the dialogue preview API (see §3.2a gap analysis)."
6.4 Behavior Tab (Archetype + Needs)¶
┌─ Behavior Tab ────────────────────────────────────────────┐
│ │
│ ── Archetype ── │
│ [Guard ▾] (from maid-classic-rpg archetypes) │
│ Patrol routes, react to threats, enforce laws. │
│ │
│ ── Need Weights (Radar Chart) ── │
│ │
│ Safety │
│ 100 │
│ /\ │
│ / \ │
│ Social/ \Duty │
│ 40 / ····\ 90 │
│ / ·/ \·\ │
│ /·/ \·\ │
│ Curiosity·/ ★ \·Rest │
│ 20 ·\ /· 30 │
│ ·\· ·/ │
│ ·\· · · ·/· │
│ ·\ /· │
│ ·\ /· │
│ ·\/· │
│ Hunger │
│ 50 │
│ │
│ ☑ Safety: 100 ☑ Duty: 90 ☐ Hunger: 50 │
│ ☐ Social: 40 ☐ Rest: 30 ☐ Curiosity: 20 │
│ │
│ ── Goal Generation ── │
│ ☑ Auto-generate goals from needs │
│ Max concurrent goals: [3 ] │
│ Goal evaluation interval: [60 ] seconds │
│ │
│ Current Goals: │
│ 1. 🟢 Patrol town square perimeter (active, 85%) │
│ 2. 🟡 Report to commander at dusk (pending) │
│ 3. ⚪ Find dinner (low priority) │
└────────────────────────────────────────────────────────────┘
The radar chart is rendered with Recharts (already in admin_frontend/package.json).
Need weights map directly to NeedsComponent from
maid_stdlib.models.npc.autonomy.
6.5 Schedule Tab (24-Hour Timeline)¶
A visual timeline editor for NPC daily schedules:
┌─ Schedule Tab ────────────────────────────────────────────┐
│ │
│ ── Daily Schedule ── │
│ │
│ 00 02 04 06 08 10 12 14 16 18 20 22 24 │
│ ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ │
│ │░░░░░░░░░░│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│████│▓▓▓▓│░░░│ │
│ │ Sleep │ Patrol Town Square │Meal│Patro│Slp│ │
│ ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ │
│ │
│ ░ = Sleep (Barracks) │
│ ▓ = Patrol (Town Square → Market → Gate → Town Square) │
│ █ = Meal (Tavern) │
│ │
│ ── Edit Block ── │
│ (drag edges to resize, click to edit) │
│ │
│ Selected: Patrol (06:00 - 14:00) │
│ Activity: [Patrol ▾] │
│ Location: [Town Square ▾] → [Market St] → [Gate] │
│ Priority: [█████████░] 90 │
│ Interruptible: [☑ Yes] │
│ │
│ [+ Add Block] [Copy from Template: [Guard Shift ▾]] │
└────────────────────────────────────────────────────────────┘
Schedule blocks are drag-to-create and drag-to-resize on the timeline. They map
directly to ScheduleComponent from maid_stdlib.models.npc.autonomy.
6.6 Inventory Tab¶
Standard item list with add/remove and quantity editing: - List of items the NPC carries - "Shop inventory" section for merchant NPCs - Drop table configuration for combat NPCs - Drag-and-drop from Content Browser
6.7 Relationships Tab¶
Interactive graph visualization of the NPC's relationships with other entities:
┌─ Relationships Tab ──────────────────────────────────────┐
│ │
│ [Mara]──trust:80──▶[Commander] │
│ │ │ │
│ respect:60 trust:70 │
│ │ │ │
│ ▼ ▼ │
│ [Recruit] [Mayor] │
│ │ │
│ friendliness:-20 │
│ │ │
│ ▼ │
│ [Thief Rogue] │
│ │
│ ── Selected: Mara → Commander ── │
│ Trust: [████████░░] 80 │
│ Respect: [█████████░] 90 │
│ Friendliness: [██████░░░░] 60 │
│ │
│ [+ Add Relationship] │
└───────────────────────────────────────────────────────────┘
Uses React Flow (mini graph) or a simple force-directed layout.
Relationships are three-dimensional (trust, respect, friendliness) matching
RelationshipManager from maid_stdlib.relationships.manager.
6.8 Memory Tab¶
Browse and manage the NPC's episodic memories:
┌─ Memory Tab ─────────────────────────────────────────────┐
│ │
│ ── Recent Memories (12 total) ── │
│ 🧠 2 min ago │ "Player helped fight goblins" │ [×] │
│ 🧠 1 hour ago │ "Received supply report" │ [×] │
│ 🧠 3 hours ago │ "Warned player about bandits" │ [×] │
│ 🧠 1 day ago │ "Promoted recruit Jenkins" │ [×] │
│ │
│ [Load More...] [Clear All] [+ Add Memory] │
│ │
│ Memory Type: [All ▾] Player: [All ▾] │
│ │
│ Consolidation Status: Last run 2 hours ago │
│ Total memories: 47 Consolidated: 12 Pending: 3 │
└───────────────────────────────────────────────────────────┘
Backed by MemoryService from maid_stdlib.memory.service and the
/admin/memory/{npc_id} API endpoint.
6.9 Spawn Rules Tab¶
Configure where and how the NPC spawns: - Home room (where the NPC respawns after death) - Respawn timer - Spawn conditions (time of day, quest state) - Wander radius - Maximum instances (for template-based spawning)
7. Item Editor¶
The Item Editor provides type-driven forms for creating and editing items. When the item type is selected (weapon, armor, potion, container, key, quest item, etc.), the form dynamically shows type-specific fields.
7.1 Layout Overview¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Header ────────────────────────────────────────────────────────────────┐ │
│ │ Item Editor: Iron Longsword [💾Save] [↩Discard] [📋Duplicate] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Identity ──────────────────────┐ ┌─ Type: Weapon ─────────────────────┐ │
│ │ Name: [Iron Longsword ] │ │ │ │
│ │ Short: [an iron longsword ] │ │ Weapon Type: [Sword ▾] │ │
│ │ Type: [Weapon ▾] │ │ Damage: [2d6+2 ] │ │
│ │ Rarity:[Uncommon ▾] │ │ Damage Type: [Slashing ▾] │ │
│ │ Value: [150 ] gold │ │ Speed: [████████░░] 80 │ │
│ │ Weight:[3.5 ] lbs │ │ Range: [Melee ▾] │ │
│ │ Level: [5 ] │ │ Two-Handed: [☐] │ │
│ │ │ │ Durability: [███████████░] 110 │ │
│ │ Description: │ │ │ │
│ │ ┌─────────────────────────┐ │ │ Special: [+ Add Effect] │ │
│ │ │ A well-forged iron │ │ │ • +1 Strength when wielded │ │
│ │ │ longsword with a │ │ │ │ │
│ │ │ leather-wrapped grip. │ │ │ Proficiency: [Martial Weapons ▾] │ │
│ │ └─────────────────────────┘ │ └────────────────────────────────────┘ │
│ │ Tags: [weapon] [sword] [+] │ │
│ └─────────────────────────────────┘ │
│ │
│ ┌─ Requirements ───────────────┐ ┌─ Balance Comparison ─────────────────┐ │
│ │ Min Level: [5 ] │ │ │ │
│ │ Min Strength: [12 ] │ │ ── Level 5 Weapons ── │ │
│ │ Class: [Any ▾] │ │ Item │ Dmg │ Val │ Spd │ │
│ │ Quest: [none ▾] │ │ Iron Sword │ 2d6 │ 150 │ 80 ◀── │ │
│ │ Skill: [none ▾] │ │ Battle Axe │ 2d8 │ 200 │ 60 │ │
│ │ │ │ Shortbow │ 1d8 │ 120 │ 90 │ │
│ └──────────────────────────────┘ │ Staff │ 1d6 │ 80 │ 70 │ │
│ │ │ │
│ ┌─ Crafting Recipe ────────────┐ │ Average DPS at level 5: 8.2 │ │
│ │ ☐ Craftable │ │ This item: 9.1 (+11% above avg) │ │
│ │ Station: [Forge ▾] │ │ [Show Chart] │ │
│ │ Skill: Blacksmithing 15 │ └──────────────────────────────────────┘ │
│ │ Materials: │ │
│ │ 2x Iron Ingot │ ┌─ Loot Tables ────────────────────────┐ │
│ │ 1x Leather Strip │ │ Drops from: │ │
│ │ 1x Weapon Mold │ │ • Goblin Warrior (5%) │ │
│ │ [+ Add Material] │ │ • Armory Chest (15%) │ │
│ └──────────────────────────────┘ │ • Blacksmith Shop (purchasable) │ │
│ │ [+ Add to Loot Table] │ │
│ └──────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
7.2 Type-Specific Forms¶
When the item type dropdown changes, the right panel dynamically loads the appropriate form:
| Item Type | Fields |
|---|---|
| Weapon | Damage dice, damage type, speed, range, proficiency, special effects |
| Armor | AC bonus, armor type, stealth penalty, material, slot |
| Potion | Effect type, duration, magnitude, charges, cooldown |
| Container | Capacity, lock type, key ID, weight reduction |
| Key | Unlocks (list of lock IDs), single-use flag |
| Quest Item | Quest ID, objective, tradeable flag |
| Food | Healing, hunger restoration, buff effects, spoil timer |
| Scroll | Spell stored, charges, caster level, consumed on use |
| Currency | Denomination, exchange rate |
7.3 Balance Comparison View¶
The right sidebar shows a comparison table of similar items (same type, ±2 levels). This helps builders ensure new items are appropriately balanced relative to existing content. Data sourced from a balance query endpoint (see §12).
7.4 Economy View¶
For items with gold value, show: - Where the item can be purchased (which shops, at what markup) - Where it drops (loot tables with drop rates) - Crafting cost vs purchase cost - Historical price data if economy simulation is running
8. Quest Editor¶
The Quest Editor is a Twine/Ink-inspired visual flow editor for designing quest objective graphs. Quests are modeled as directed acyclic graphs (DAGs) where objectives are nodes and dependencies are edges.
8.1 Full Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Header ──────────────────────────────────────────────────────────────────┐ │
│ │ Quest Editor: The Goblin Menace [💾Save] [▶Simulate] [📋Export] │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Quest Info ───────────────┐ ┌─ Objective Graph ───────────────────────────┐ │
│ │ Name: [The Goblin Menace] │ │ │ │
│ │ Level: [3-5] │ │ ┌──────────┐ │ │
│ │ Type: [Main Quest ▾] │ │ │ START │ │ │
│ │ Repeatable: [☐] │ │ │ Talk to │ │ │
│ │ │ │ │ Mara │ │ │
│ │ Description: │ │ └────┬─────┘ │ │
│ │ ┌──────────────────────┐ │ │ │ │ │
│ │ │ Goblins threaten the │ │ │ ▼ │ │
│ │ │ village. Captain Mara│ │ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ needs adventurers... │ │ │ │ Scout │───────▶│ OPTIONAL │ │ │
│ │ └──────────────────────┘ │ │ │ the Camp │ │ Free │ │ │
│ │ │ │ │ (explore)│ │ Prisoner │ │ │
│ │ Prerequisites: │ │ └────┬─────┘ │ (escort) │ │ │
│ │ Min level: [3] │ │ │ └────┬─────┘ │ │
│ │ Required items: [] │ │ ▼ │ │ │
│ │ Required quests: [] │ │ ┌──────────┐ │ │ │
│ │ │ │ │ Kill 10 │ │ │ │
│ │ ── Rewards ── │ │ │ Goblins │ │ │ │
│ │ XP: [500 ] │ │ │ (kill) │◀────────────┘ │ │
│ │ Gold: [200 ] │ │ └────┬─────┘ │ │
│ │ Items: │ │ │ │ │
│ │ Iron Longsword (choice) │ │ ▼ │ │
│ │ Healing Potion x3 │ │ ┌──────────┐ │ │
│ │ [+ Add Reward] │ │ │ CHOICE │ │ │
│ │ │ │ │ Confront │ │ │
│ │ ── Journal Entries ── │ │ │ or Sneak │ │ │
│ │ Start: "Mara has asked..." │ │ └──┬────┬──┘ │ │
│ │ Progress: "You've found.." │ │ │ │ │ │
│ │ Complete: "The goblins..." │ │ ▼ ▼ │ │
│ │ [+ Add Entry] │ │ ┌─────┐ ┌─────┐ │ │
│ └────────────────────────────┘ │ │Fight│ │Sneak│ │ │
│ │ │Boss │ │Past │ │ │
│ │ └──┬──┘ └──┬──┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────┐ │ │
│ │ │ COMPLETE │ │ │
│ │ │ Return │ │ │
│ │ │ to Mara │ │ │
│ │ └──────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─ Objective Inspector ─────────────────────────────────────────────────────┐ │
│ │ Selected: "Kill 10 Goblins" Type: [Kill ▾] │ │
│ │ Target: [Goblin ▾] Count: [10] Zone: [Goblin Camp ▾] │ │
│ │ On Complete: [Advance to "CHOICE: Confront or Sneak"] │ │
│ │ On Fail: [Quest fails — return to Mara] │ │
│ │ Journal: "You've slain [progress]/10 goblins." │ │
│ │ Hint: "Goblins are found in the forest east of the village." │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
8.2 Objective Types¶
| Type | Description | Fields |
|---|---|---|
| Kill | Defeat N enemies of type | Target NPC/type, count, zone restriction |
| Collect | Gather N items | Item type, count, source (loot/craft/buy) |
| Deliver | Bring item to NPC | Item, recipient NPC, dialogue on deliver |
| Escort | Protect NPC to destination | NPC, destination room, fail conditions |
| Explore | Visit specific room(s) | Room list, order-dependent flag |
| Dialogue | Speak to NPC about topic | NPC, required dialogue keywords |
| Interact | Use/activate an object | Object entity, interaction type |
| Choice | Branch point (no task) | Branch options with conditions |
| Custom | Scripted objective | Event trigger, custom validation |
8.3 Branching and Conditions¶
Edges between objectives can have conditions:
interface QuestEdgeCondition {
type: 'always' | 'choice' | 'conditional';
// For 'choice': label shown to player
choiceLabel?: string;
// For 'conditional': expression evaluated at runtime
condition?: string; // Lock expression syntax: "char_level(5) AND has_item(key)"
}
8.4 Quest Simulator¶
The "Simulate" button opens a step-through simulation:
┌─ Quest Simulator ────────────────────────────────────────┐
│ │
│ Step 1/6: Talk to Mara │
│ Status: ✅ Complete │
│ │
│ Step 2/6: Scout the Camp │
│ Status: ⏳ In Progress │
│ │
│ [⏮Back] [⏭Next] [⏩Skip to End] [🔀Random Path] │
│ │
│ ── Validation ── │
│ ✅ All paths lead to completion │
│ ✅ No orphaned objectives │
│ ⚠️ Optional "Free Prisoner" has no failure consequence │
│ ✅ Rewards are balanced for level range │
└───────────────────────────────────────────────────────────┘
The simulator walks every possible path through the DAG, checking for: - Dead ends (objectives with no outgoing edges and not marked as terminal) - Orphaned objectives (unreachable from START) - Circular dependencies - Missing rewards on completion nodes - Balance warnings (XP/gold vs level range)
9. Dialogue Editor¶
The Dialogue Editor provides a visual conversation tree for designing scripted dialogue flows. It complements the AI dialogue system by providing structured "critical beats" that anchor important narrative moments.
9.1 Full Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Header ──────────────────────────────────────────────────────────────────┐ │
│ │ Dialogue: Mara - Quest Introduction [💾Save] [▶Preview] [📋Export] │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Dialogue Graph ──────────────────────────────────┐ ┌─ Preview ──────────┐ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ Mara says: │ │
│ │ │ GREETING │ │ │ "Adventurer! I've │ │
│ │ │ "Adventurer!" │ │ │ been hoping someone│ │
│ │ └───┬──────┬────┘ │ │ capable would come │ │
│ │ │ │ │ │ along." │ │
│ │ │ │ │ │ │ │
│ │ ▼ ▼ │ │ ── Responses ── │ │
│ │ ┌───────┐ ┌───────┐ │ │ 1. "What's the │ │
│ │ │"What │ │"Not │ │ │ trouble?" │ │
│ │ │trouble│ │inter- │ │ │ 2. "Not inter- │ │
│ │ │?" │ │ested" │ │ │ ested." │ │
│ │ └───┬───┘ └───┬───┘ │ │ 3. [AI fills in │ │
│ │ │ │ │ │ ambient chat] │ │
│ │ ▼ ▼ │ │ │ │
│ │ ┌───────┐ ┌───────┐ │ │ [Simulate Player] │ │
│ │ │Quest │ │Dismiss│ │ │ │ │
│ │ │Briefing│ │"Very │ │ └────────────────────┘ │
│ │ │(give │ │well." │ │ │
│ │ │quest) │ │[END] │ │ ┌─ Node Inspector ──┐ │
│ │ └───┬───┘ └───────┘ │ │ ID: greeting │ │
│ │ │ │ │ Type: [NPC Say ▾] │ │
│ │ ▼ │ │ │ │
│ │ ┌───────────┐ │ │ Text: │ │
│ │ │ AI_AMBIENT │ │ │ ┌────────────────┐│ │
│ │ │ (AI fills │ │ │ │Adventurer! I've││ │
│ │ │ additional │ │ │ │been hoping... ││ │
│ │ │ context) │ │ │ └────────────────┘│ │
│ │ └────────────┘ │ │ │ │
│ │ │ │ Conditions: none │ │
│ └────────────────────────────────────────────────────┘ │ Effects: none │ │
│ │ AI Fallback: ☑ │ │
│ └────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
9.2 Node Types¶
| Node Type | Description | Color |
|---|---|---|
| NPC Say | NPC speaks a line | Blue |
| Player Response | Player chooses from options | Green |
| Condition | Branch based on game state | Yellow |
| Effect | Apply game state change | Orange |
| AI Ambient | AI generates contextual dialogue | Purple |
| End | Conversation terminates | Red |
9.3 Conditions and Effects¶
Conditions (branch the conversation):
- has_item(item_id) — Player has specific item
- level_gte(N) — Player level check
- skill_check(skill, DC) — Skill check with random roll
- relationship(npc_id, dimension, threshold) — Relationship check
- quest_state(quest_id, state) — Quest progress check
- flag(flag_name) — Boolean flag check
Effects (triggered when a node is reached):
- give_item(item_id, count) — Give item to player
- take_item(item_id, count) — Remove item from player
- give_quest(quest_id) — Start a quest
- set_relationship(npc_id, dim, delta) — Modify relationship
- set_flag(flag_name, value) — Set a boolean flag
- teleport(room_id) — Move player to room
- spawn_entity(template) — Spawn an entity
9.4 AI Hybrid Mode¶
The dialogue tree supports a hybrid approach:
- Scripted beats: Important narrative moments are authored as fixed nodes
- AI ambient: Between beats, the AI generates contextual filler dialogue
- AI fallback: If the player says something not matching any response option, the AI generates an in-character response that steers back to the nearest beat
This maps directly to NPCDialogueSystem's existing scripted_beats and
ai_ambient configuration in NPCPromptConfig.
9.5 Preview Pane¶
The preview pane simulates a player conversation: - Click through scripted paths manually - "Simulate Player" button uses AI to generate realistic player inputs - Shows condition evaluation results - Highlights the current node on the graph - Displays effects as they would fire
10. Bulk Data Editor¶
A spreadsheet-style grid for viewing and editing large numbers of entities at once. Optimized for batch operations like rebalancing all weapons or updating NPC levels across a zone.
10.1 Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Toolbar ───────────────────────────────────────────────────────────────────┐ │
│ │ Entity Type: [NPCs ▾] Zone: [All ▾] [🔍 Search] │ │
│ │ [📥Import CSV] [📤Export CSV] [🔄Refresh] │ Selected: 5 │ [Bulk Edit ▾] │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Data Grid ─────────────────────────────────────────────────────────────────┐ │
│ │ ☐ │ Name │ Level │ HP │ Zone │ Room │ Tags │ │
│ │───┼───────────────────┼───────┼───────┼───────────────┼─────────────┼───────│ │
│ │ ☑ │ Guard Captain Mara│ 15 │ 85 │ Starter Villa │ Town Square │ guard │ │
│ │ ☑ │ Old Fisherman │ 3 │ 25 │ Starter Villa │ Docks │ quest │ │
│ │ ☐ │ Merchant Giles │ 8 │ 40 │ Starter Villa │ Market St │ shop │ │
│ │ ☑ │ Goblin Scout │ 4 │ 18 │ Goblin Camp │ Forest Path │ enemy │ │
│ │ ☑ │ Goblin Warrior │ 6 │ 30 │ Goblin Camp │ Camp Center │ enemy │ │
│ │ ☑ │ Goblin Chief │ 10 │ 65 │ Goblin Camp │ Chief's Hut │ boss │ │
│ │ ☐ │ Traveling Bard │ 7 │ 35 │ Starter Villa │ Tavern │ quest │ │
│ │ ☐ │ Blacksmith Torv │ 12 │ 55 │ Starter Villa │ Forge │ shop │ │
│ │───┼───────────────────┼───────┼───────┼───────────────┼─────────────┼───────│ │
│ │ │ │ │ │ │ │ │ │
│ │ │ (click a cell to edit inline) │ │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Status Bar ────────────────────────────────────────────────────────────────┐ │
│ │ Showing 8 of 247 entities │ 5 selected │ 2 modified │ Page 1/31 [◀][▶] │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
10.2 Features¶
Inline Editing: Click any cell to edit. Changes are highlighted with a yellow background until saved. Validation runs on blur.
Column Configuration: Columns are configurable per entity type. Component fields
can be added as columns (e.g., add "Damage" column for weapons by selecting
CombatStatsComponent.attack_damage).
Bulk Operations: With rows selected, the "Bulk Edit" dropdown offers: - Set field to value (e.g., set all selected NPCs to level 10) - Increment/decrement field (e.g., +5 HP to all selected) - Add/remove tag - Move to room/zone - Delete all selected
CSV Import/Export: - Export visible rows as CSV (with all displayed columns) - Import CSV with column mapping wizard - Diff preview before applying import
Diff Preview:
┌─ Pending Changes ────────────────────────────────────────┐
│ │
│ 5 entities modified: │
│ │
│ Guard Captain Mara: │
│ - HP: 85 → 100 │
│ - Level: 15 → 16 │
│ │
│ Goblin Scout: │
│ - HP: 18 → 22 │
│ │
│ ... (3 more) │
│ │
│ [✅ Apply All] [↩ Discard All] [✎ Review Each] │
└───────────────────────────────────────────────────────────┘
11. Content Browser¶
A searchable library of all entities, templates, and content across loaded packs. The Content Browser serves as both a reference tool and a drag-and-drop source for populating rooms on the map.
11.1 Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Search Bar ────────────────────────────────────────────────────────────────┐ │
│ │ [🔍 Search content... ] [Filters]│ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Categories ────┐ ┌─ Results ────────────────────────┐ ┌─ Preview ────────┐ │
│ │ │ │ │ │ │ │
│ │ 📁 All (584) │ │ ┌──────────────────────────────┐ │ │ Iron Longsword │ │
│ │ 📁 Rooms (247) │ │ │ 🗡 Iron Longsword │ │ │ ────────────── │ │
│ │ 📁 NPCs (89) │ │ │ Weapon • Level 5 • Uncommon │ │ │ │ │
│ │ 📁 Items (198) │ │ │ 2d6+2 slashing • 150 gold │ │ │ A well-forged │ │
│ │ ├ Weapons │ │ └──────────────────────────────┘ │ │ iron longsword │ │
│ │ ├ Armor │ │ ┌──────────────────────────────┐ │ │ with a leather- │ │
│ │ ├ Potions │ │ │ 🛡 Steel Breastplate │ │ │ wrapped grip. │ │
│ │ ├ Keys │ │ │ Armor • Level 8 • Rare │ │ │ │ │
│ │ └ Quest Items │ │ │ AC +6 • 500 gold │ │ │ Damage: 2d6+2 │ │
│ │ 📁 Quests (24) │ │ └──────────────────────────────┘ │ │ Type: Slashing │ │
│ │ 📁 Templates │ │ ┌──────────────────────────────┐ │ │ Speed: 80 │ │
│ │ ├ NPC Tmpls │ │ │ 🧪 Healing Potion │ │ │ Value: 150 gold │ │
│ │ ├ Item Tmpls │ │ │ Potion • Level 1 • Common │ │ │ Level Req: 5 │ │
│ │ └ Room Tmpls │ │ │ Heals 2d4+2 HP • 25 gold │ │ │ │ │
│ │ 📁 Lore │ │ └──────────────────────────────┘ │ │ [Edit] [Dup] │ │
│ │ 📁 Dialogue │ │ │ │ [Drag to Map] │ │
│ │ │ │ Showing 3 of 198 items │ │ │ │
│ └──────────────────┘ └──────────────────────────────────┘ └──────────────────┘ │
│ │
│ ┌─ Filters (expanded) ───────────────────────────────────────────────────────┐ │
│ │ Type: [Any ▾] Rarity: [Any ▾] Level: [1] to [50] Pack: [All ▾] │ │
│ │ Tags: [weapon] [×] Sort: [Name ▾] [↑Asc] │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
11.2 Features¶
Full-Text Search: Searches entity names, descriptions, tags, and component data.
Uses the existing /admin/entities/ endpoint with search parameter.
Category Tree: Hierarchical navigation based on entity type and subtypes. Categories are derived from registered component types and tags.
Preview Card: Hover or select a result to see a rich preview with: - Full description - Key stats (type-dependent) - Source pack - Relationships (what references this entity)
Drag-and-Drop: Items and NPCs can be dragged from the browser directly onto a room node on the map canvas. This creates a spawn/placement via the entity API.
Template Library: Reusable templates for quick entity creation: - Built-in templates from content packs (e.g., "Generic Guard", "Basic Sword") - User-created templates saved to server - "Create from template" with customization overlay
12. Balance Dashboard¶
Tier boundary: The Balance Dashboard is a Tier 3 presentation layer. It visualizes data aggregated from entity components in the live world. The underlying balance analysis engine (statistical models, simulation framework, anomaly detection algorithms) is a Tier 2 responsibility defined in the Tier 2 AI Content Pipeline. Tier 3 provides the frontend charts, the REST API endpoints that aggregate raw entity data, and the dashboard UI. Tier 2 provides any AI-powered analysis, simulation, or recommendation features. If Tier 2's balance engine is not implemented, the dashboard still works with basic aggregation (entity counts, stat ranges, distribution charts) — the AI-driven insights (anomaly detection, simulation) degrade gracefully to "not available."
A game design analytics view showing combat balance, economy health, content distribution, and progression curves. Data is aggregated from entity components across the world.
12.1 Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Balance Dashboard ─────────────────────────────────────────────────────────┐ │
│ │ [Combat] [Economy] [Content] [Progression] [Loot] Period: [All Time ▾]│ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ Combat Curves ──────────────────────┐ ┌─ Content Density Heatmap ────────┐ │
│ │ │ │ │ │
│ │ DPS/HP by Level │ │ (Map overlay with color-coded │ │
│ │ │ │ entity density per room) │ │
│ │ HP │ ╱‾‾‾‾ │ │ │ │
│ │ │ ╱╱ │ │ ██ = High density (10+) │ │
│ │ │ ╱╱ ← Expected │ │ ▓▓ = Medium (5-9) │ │
│ │ │╱╱ │ │ ░░ = Low (1-4) │ │
│ │ ╱╱ │ │ ·· = Empty │ │
│ │ ╱╱ ····· ← Actual (items) │ │ │ │
│ │ ╱╱ ··· │ │ ⚠ 12 empty rooms found │ │
│ │ └─────────────────Level──────▶ │ │ ⚠ 3 rooms over-populated │ │
│ │ │ │ │ │
│ │ ⚠ Level 8-12 gap: player DPS │ └───────────────────────────────────┘ │
│ │ outpaces monster HP by 40% │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌─ Economy Sankey ─────────────────────┐ ┌─ XP Progression ────────────────┐ │
│ │ │ │ │ │
│ │ Gold Sources → Sinks │ │ Level │ XP Required │ Hours │ │
│ │ │ │ ──────┼─────────────┼────── │ │
│ │ [Drops]═══╗ ╔═══[Shops] │ │ 1 │ 0 │ 0.0 │ │
│ │ ╠════╣ │ │ 2 │ 100 │ 0.5 │ │
│ │ [Quests]══╝ ╚═══[Repairs] │ │ 3 │ 300 │ 1.2 │ │
│ │ [Taxes] │ │ 4 │ 600 │ 2.1 │ │
│ │ [Crafting] │ │ 5 │ 1000 │ 3.5 │ │
│ │ │ │ ... │ ... │ ... │ │
│ │ Net flow: +120 gold/hour/player │ │ │ │
│ │ ⚠ Economy inflating 8%/day │ │ ⚠ Level 7→8 takes 2x as long │ │
│ └──────────────────────────────────────┘ │ as 6→7 (may cause drop-off) │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌─ Loot Distribution ─────────────────────────────────────────────────────────┐ │
│ │ Item Rarity by Level Range │ │
│ │ Level 1-5: [████████████████████░░░░░░░░░░] Common: 80% Uncommon: 15% │ │
│ │ Level 6-10: [███████████████░░░░░░░░░░░░░░░] Common: 60% Uncommon: 30% │ │
│ │ Level 11-15:[███████████░░░░░░░░░░░░░░░░░░░] Common: 45% Uncommon: 35% │ │
│ │ Level 16-20:[██████░░░░░░░░░░░░░░░░░░░░░░░░] Common: 25% Uncommon: 40% │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
12.2 Dashboard Panels¶
| Panel | Library | Data Source |
|---|---|---|
| Combat Curves | Recharts (line chart) | NPC stats by level vs item stats by level |
| Economy Sankey | Recharts or d3-sankey | Gold source/sink analysis from economy config |
| Content Heatmap | Custom canvas overlay on React Flow | Entity count per room |
| XP Progression | Recharts (area chart) | XP table from content pack config |
| Loot Distribution | Recharts (stacked bar) | Loot table aggregation |
12.3 Player Simulation¶
An advanced feature that runs Monte Carlo simulations of player progression: - Configure player archetype (class, playstyle, hours/day) - Simulate N sessions of play - Output: average level curve, gold curve, gear score curve - Highlight points where progression stalls or accelerates unexpectedly
13. Live Preview / Play Mode¶
An embedded game terminal that lets builders test their changes in real time
without leaving the editor. Built on the existing player_frontend's xterm.js
integration.
13.1 Layout Wireframe¶
┌─────────────────────────────────────────────────────────────────────────────────┐
│ ┌─ Map Editor (left 60%) ──────────────────┐ ┌─ Play Mode (right 40%) ──────┐ │
│ │ │ │ ┌─ Terminal ───────────────┐ │ │
│ │ ┌─────────┐ ┌─────────┐ │ │ │ │ │ │
│ │ │ Town │ north │ Market │ │ │ │ Town Square │ │ │
│ │ │ Square ★│────────▶│ Street │ │ │ │ A large open plaza with │ │ │
│ │ │ │◀────────│ │ │ │ │ a fountain in the center.│ │ │
│ │ └────┬────┘ south └─────────┘ │ │ │ │ │ │
│ │ │ │ │ │ Exits: north, south │ │ │
│ │ south│ │ │ │ │ │ │
│ │ │ │ │ │ Guard Captain Mara is │ │ │
│ │ ┌────▼────┐ │ │ │ here. │ │ │
│ │ │ Temple │ │ │ │ │ │ │
│ │ │ │ │ │ │ > look mara │ │ │
│ │ └─────────┘ │ │ │ A stern guard captain... │ │ │
│ │ │ │ │ │ │ │
│ │ ★ = Your position (ghost mode) │ │ │ > _ │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ │ │
│ │ │ │ ┌─ Quick Actions ─────────┐ │ │
│ │ │ │ │ [🔄Hot Reload] [👻Ghost]│ │ │
│ │ │ │ │ [📍Go To Edit] [✎→▶] │ │ │
│ │ │ │ │ [⏺Record] [⏹Stop] │ │ │
│ │ │ │ └─────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
13.2 Features¶
Embedded Terminal: An xterm.js instance connected to a builder session on the MAID server. Renders ANSI colors, supports command input, shows room descriptions exactly as players see them.
Hot Reload: When a room, NPC, or item is saved in the editor, the change is
immediately reflected in the terminal session. The builder sees a notification:
[Editor] Room "Town Square" updated — type 'look' to see changes.
Ghost Mode: The builder character is invisible to players. They can observe
rooms, trigger NPC dialogue, and test quest interactions without disrupting
live gameplay. Uses the existing @teleport and builder access level flags.
Teleport-to-Edit: Click "Edit" on a room in the terminal to select it on the map and open its inspector. Click "Go To" on a room in the editor to teleport the terminal session there.
Session Recording: Record a test session (command inputs + outputs + timestamps) for playback review or sharing with other builders. Stored as JSON.
interface SessionRecording {
id: string;
builder: string;
startTime: string;
endTime: string;
entries: SessionEntry[];
}
interface SessionEntry {
timestamp: string;
type: 'input' | 'output' | 'event';
content: string;
room_id?: string;
}
14. Mobile Builder Experience¶
Scope limitation: Phone-based graph editing is not realistic — the node-and-edge canvas requires precision interaction that touch screens below ~768px cannot provide. Mobile scope is limited to tablets (≥768px viewport).
14.1 Responsive Layout (Tablet)¶
The editor adapts to tablet screen sizes using Tailwind CSS breakpoints:
| Breakpoint | Layout |
|---|---|
| Desktop (≥1280px) | Three-column: layers + map + inspector |
| Tablet (768-1279px) | Two-column: map + slide-over inspector |
| Phone (<768px) | Not supported for editing. Read-only map view only. |
14.2 Touch Optimizations (Tablet)¶
- Pinch-to-zoom on the map canvas (React Flow supports this natively)
- Long-press on a room to open context menu (replaces right-click)
- Swipe inspector panel to dismiss
- Floating Action Button (FAB) for Create Room (replaces toolbar)
- Bottom Sheet inspector that slides up from the bottom
14.3 Quick Edit Mode¶
A simplified tablet workflow: 1. Tap a room on the map 2. Bottom sheet shows: name, description (editable), exit list 3. Swipe to dismiss or tap "Full Edit" to open complete inspector 4. No bulk operations, no advanced component editing on tablet
14.4 PWA / Offline (Future Enhancement — Not in v1)¶
Descoped from v1. The current admin frontend has no service worker, no PWA manifest, and no IndexedDB sync layer. Adding offline-first capability with conflict resolution for queued mutations is a substantial engineering effort.
For v1, the visual editor requires a live server connection. Brief disconnections are handled by React Query's retry logic. For offline work, use YAML export → text editor → import.
Future PWA work would require:
- Service worker for UI asset caching
- IndexedDB for entity/graph data caching
- Mutation queue with conflict resolution on reconnect
- manifest.json with editor icons and theme colors
- Background sync API integration
- This is estimated at 4-6 weeks of dedicated work and should be a separate track.
15. Content Pack Editor Extensions¶
The extension architecture is defined in §3.6. This section covers the pack-side implementation and the admin frontend's extension rendering.
15.1 Pack-Side Implementation¶
Content packs provide editor extensions via a duck-typed optional method (not part
of the ContentPack Protocol — see §3.6 for rationale):
# packages/maid-classic-rpg/src/maid_classic_rpg/pack.py
class ClassicRPGContentPack:
"""Classic RPG content pack with optional editor extensions."""
# ... required ContentPack Protocol methods ...
def get_editor_extensions(self) -> list[dict]:
"""Optional: provide editor extension metadata.
Returns metadata descriptors only — no frontend code.
The admin frontend must have matching built-in components
for each componentType listed here (see §3.6).
"""
return [
{
"type": "inspector_tab",
"id": "combat_stats",
"label": "Combat Stats",
"icon": "sword",
"componentType": "CombatStatsComponent",
"priority": 10,
},
{
"type": "inspector_tab",
"id": "spell_list",
"label": "Spells",
"icon": "wand",
"componentType": "SpellCasterComponent",
"priority": 20,
},
{
"type": "page",
"id": "faction_editor",
"label": "Factions",
"icon": "flag",
"route": "factions",
},
]
15.2 Frontend Extension Rendering¶
The admin frontend discovers extensions at startup and renders matching components:
// hooks/useEditorExtensions.ts
function useEditorExtensions() {
return useQuery({
queryKey: ['editorExtensions'],
queryFn: () => get<{ extensions: ExtensionManifest[] }>('/editor/extensions'),
staleTime: Infinity, // Extensions don't change at runtime
});
}
// In RoomInspector.tsx — render extension tabs for matching components
function InspectorTabs({ entityComponents }: Props) {
const { data } = useEditorExtensions();
const extensionTabs = data?.extensions
.filter(ext => ext.type === 'inspector_tab')
.filter(ext => entityComponents.some(c => c.type === ext.componentType))
.map(ext => ({
...ext,
Component: getExtensionComponent(ext.componentType),
}))
.filter(ext => ext.Component !== null);
// Render built-in tabs + matching extension tabs
}
15.3 Extension Hooks (Future Enhancement)¶
Pack-provided lifecycle hooks (before/after save, validation) are a post-v1 feature.
For v1, all validation runs through the standard component schema validation in the
entity API. Custom validation logic requires server-side implementation in the
content pack's register_api_routes() method.
16. Collaboration Features¶
16.1 Real-Time Presence¶
When multiple builders have the Map Editor open, each builder sees: - Colored cursors on the map showing other builders' mouse positions - Selection outlines showing which rooms other builders have selected - Lock badges on rooms being edited by another builder - User list in the toolbar showing who's online
┌─ Online Builders ────────┐
│ 🔴 admin (you) - editing │
│ 🔵 builder_a - viewing │
│ 🟢 builder_b - editing │
│ │
│ [Invite Builder...] │
└───────────────────────────┘
16.2 Server-Enforced Exclusive Locking¶
Current State: The room API has no version field, no ETag, no updated_at, and
no lock state. PUT /admin/world/rooms/{room_id} immediately overwrites fields with
no compare-and-swap semantics. Optimistic concurrency control is not possible
with the current data model.
v1 Design: Exclusive Locking
For v1, the editor uses server-enforced exclusive locks. Only one builder can edit a given entity at a time. This is simpler and safer than optimistic concurrency.
Prerequisites (backend changes required):
- Add
revisionfield to room model and API responses:
class RoomResponse(BaseModel):
# ... existing fields ...
revision: int # Incremented on every mutation
updated_at: str | None # ISO 8601 timestamp of last change
locked_by: str | None # User ID holding edit lock (or null)
locked_until: str | None # Lock expiry time (or null)
- Add lock manager (in-memory with optional Redis backend):
class EntityLockManager:
"""Server-side exclusive lock manager for editor entities."""
async def acquire(
self, entity_id: str, user_id: str, timeout_seconds: int = 300
) -> LockResult:
"""Acquire exclusive edit lock. Returns granted/denied."""
async def release(self, entity_id: str, user_id: str) -> bool:
"""Release lock. Returns True if released, False if not held."""
async def force_release(self, entity_id: str, admin_user_id: str) -> bool:
"""Admin force-release. Logs the override."""
async def cleanup_expired(self) -> int:
"""Periodic cleanup of expired locks. Returns count released."""
- Add revision check to mutation endpoints:
# In update_room handler:
async def update_room(
...,
if_revision: int | None = Header(None, alias="If-Match"),
):
current = world.get_room(room_id)
if if_revision is not None and current.revision != if_revision:
raise HTTPException(409, "Room was modified by another user")
# Apply update, increment revision
Editor Flow:
- Builder clicks room → inspector opens in read-only mode
- Builder clicks "Edit" →
editor_lock_acquiresent via WebSocket - Server checks lock state:
- Not locked: Grant lock, broadcast
editor_lock_grantedto all, set 5-min expiry. Inspector switches to edit mode. - Locked by another user: Send
editor_lock_deniedwith lock holder info. Inspector stays read-only with "Locked by {user}" badge. - Locked by same user (reentry): Extend expiry, grant.
- Builder edits and saves → API call with
If-Match: {revision}header - On save success →
editor_lock_releasesent, lock freed, revision incremented - On save conflict (409) → reload room data, re-display (stale data cleared)
- Locks auto-expire after 5 minutes of no save or keep-alive
- Admins can force-unlock via
DELETE /admin/editor/locks/{entity_id}
Future Enhancement (post-v1): Field-level optimistic concurrency with automatic merge for non-conflicting changes. Requires: per-field revision tracking, three-way merge logic, and conflict resolution UI. This is significantly more complex and is deferred.
16.3 Change Log¶
A real-time feed of all changes across the world:
┌─ Change Log ─────────────────────────────────────────────┐
│ [All] [Rooms] [NPCs] [Items] [Quests] [🔍 Filter] │
│ │
│ 2 min ago │ admin │ Updated room "Town Square" │
│ │ │ description changed │
│ 5 min ago │ builder_a │ Created NPC "Goblin Scout" │
│ 8 min ago │ builder_a │ Created room "Forest Clearing" │
│ 12 min ago │ admin │ Deleted exit: Temple → Crypt │
│ 15 min ago │ system │ Auto-save: 3 entities persisted │
│ │
│ [Load More...] │
└───────────────────────────────────────────────────────────┘
16.4 Review Workflow (Future Enhancement — Not in v1)¶
Descoped from v1. The changeset/review workflow described below is a major backend subsystem requiring: a diff storage model, transaction boundaries, reconciliation with live-world state, rollback behavior, and authorization rules. The current admin APIs mutate the live world immediately with no staging boundary. Building a staging/review layer on top would require fundamental changes to how room/entity CRUD works.
For v1, the collaboration model is: builders edit the live world directly with exclusive locks (§16.2) and a change log (§16.3). Teams that need review workflows should use the YAML export → git PR →
maid data loadpath.
Future changeset design (post-v1 scope):
A changeset system would need to address: - Storage: Pending mutations stored as a diff against current world state - Isolation: Changes in a changeset are not visible to other users or the live game until applied - Reconciliation: When the underlying world changes while a changeset is pending, the changeset must be rebased or marked as conflicting - Atomicity: Applying a changeset is all-or-nothing - Rollback: Applied changesets can be reversed (requires the undo/redo architecture from §3.4 at scale) - Authorization: Who can create/submit/approve/apply changesets
This is a substantial engineering effort (estimated 8-12 weeks standalone) and is better delivered as a dedicated Tier 3.5 or Tier 4 feature after the core visual editor is stable.
16.5 Role-Based Access in Editor¶
The editor respects the existing admin API role system. Permissions are enforced server-side by route decorators — the frontend disables UI elements based on the user's role but does not enforce access.
Admin role hierarchy (from AdminRole IntEnum in auth.py):
| Role | Value | Purpose |
|---|---|---|
| VIEWER | 10 | Read-only access to dashboards and stats |
| MODERATOR | 20 | Player management (ban/kick/mute) |
| BUILDER | 30 | Create/edit world content |
| ADMIN | 40 | Full admin access, manage users |
| SUPERADMIN | 50 | System-level access, configuration |
Important: MODERATOR (20) < BUILDER (30). Moderators do not inherit builder permissions. A moderator cannot create or edit rooms, NPCs, or items. The editor frontend detects this and shows a read-only view identical to VIEWER for moderators.
Actual API permissions (from world.py and entities.py route decorators):
| Operation | Required Role | Source |
|---|---|---|
| View rooms, graph, entities | VIEWER | world.py, entities.py |
| Create room | BUILDER | world.py:557 |
| Update room | BUILDER | world.py:619 |
| Delete room | ADMIN | world.py:679 |
| Create exit | BUILDER | world.py:777 |
| Delete exit | BUILDER | world.py:864 |
| Create entity | BUILDER | entities.py |
| Delete entity | ADMIN | entities.py |
| Update component | BUILDER | entities.py |
| Add/remove tag | BUILDER | entities.py |
| Force-unlock entity | ADMIN | New: editor_extensions.py |
| View balance dashboard | VIEWER | New: balance.py |
Editor UI role mapping:
| Role | Map View | Create | Edit | Delete | Locks | Bulk Ops |
|---|---|---|---|---|---|---|
| VIEWER | View only | ✗ | ✗ | ✗ | View only | ✗ |
| MODERATOR | View only | ✗ | ✗ | ✗ | View only | ✗ |
| BUILDER | Full | ✓ | ✓ | ✗ (rooms/entities require ADMIN) | Acquire/release own | ✓ (create/update only) |
| ADMIN | Full | ✓ | ✓ | ✓ | Force-unlock others | ✓ (all operations) |
| SUPERADMIN | Full | ✓ | ✓ | ✓ | Full | ✓ |
17. Performance Considerations¶
17.1 Large World Scaling¶
Current State: GET /admin/world/graph returns the entire room graph,
optionally filtered by area_id (which maps to zone assignment). There is no viewport-based pagination, no
coordinate index, and no persisted room layout positions. For worlds with >1,000
rooms, this endpoint becomes a bottleneck.
Large-world support is a first-class backend milestone, not an incremental frontend optimization. The following staged approach addresses this:
Stage 1 (Phase 1): Full-graph mode (up to ~1,000 rooms)
- Use existing getWorldGraph() API as-is
- Client-side React Flow virtualization handles rendering
- Layout positions stored in browser localStorage
- This covers the majority of MUD worlds
Stage 2 (Phase 2): Enriched graph + zone filtering (1,000-3,000 rooms)
- Enrich GraphNode with npc_count, item_count, tags, zone_id (see §3.2a)
- Use zone filtering (via area_id API parameter) to load subsets of the world
- Client-side LOD: simplified rendering at low zoom levels
Stage 3 (Phase 4): Viewport-paginated loading (3,000-10,000 rooms)
- Prerequisite: Rooms must have persistent coordinates. These can come from:
- GridManager coordinates (for grid-based worlds)
- Server-persisted layout positions (from a new PUT /admin/editor/layout endpoint)
- Force-directed layout computed server-side and cached
- New endpoint: GET /admin/world/graph/viewport with spatial query bounds
- Server needs a spatial index (R-tree or quadtree) over room coordinates
- Client loads rooms in viewport + buffer zone, lazy-loads on pan
// Stage 3 endpoint — only available once rooms have persisted coordinates
GET /admin/world/graph/viewport?minX=-500&minY=-500&maxX=500&maxY=500
interface ViewportGraphResponse {
nodes: EnrichedGraphNode[]; // Rooms within bounds
edges: GraphEdge[]; // Exits between visible rooms
boundary_edges: GraphEdge[]; // Exits to rooms outside bounds (stubs)
total_rooms: number; // Total in world (for minimap)
viewport_rooms: number; // Count in this response
}
Viewport Culling (client-side, all stages): React Flow's built-in virtualization removes nodes outside the viewport from the DOM. This is automatic and requires no custom code for basic operation.
Level-of-Detail (LOD):
| Zoom Level | Room Rendering |
|---|---|
| <0.3x (far) | Colored dots, no labels |
| 0.3x-0.7x | Small boxes with truncated names |
| 0.7x-1.5x | Full room nodes with icons and stats |
| >1.5x (near) | Expanded nodes with description preview |
17.2 Canvas Technology¶
SVG (default, via React Flow v11): - Pros: Crisp at all zoom levels, DOM accessibility, easy event handling - Cons: Performance degrades above ~3,000-5,000 visible nodes - Best for: Stage 1-2 (worlds up to ~3,000 rooms) - This is the only renderer in v1.
Canvas2D (future, Stage 3+): - Pros: Handles 10,000+ nodes at 60fps - Cons: No DOM nodes (custom hit testing), no CSS styling, no a11y - Consideration: Only if demand materializes for worlds above 5,000 rooms - Would require a custom React Flow renderer or a separate canvas overlay
WebGL is explicitly out of scope. No current MUD world justifies the implementation complexity. If a world grows beyond 50,000 rooms, the authoring workflow should use zone-based partitioning rather than rendering everything.
17.3 Data Transfer Optimization¶
WebSocket Batching: Entity update broadcasts are batched into 100ms windows. Multiple changes within a window are sent as a single message to reduce overhead.
// Server-side batching
interface BatchedUpdate {
channel: 'world';
type: 'batch_update';
payload: {
updates: WorldUpdate[];
timestamp: string;
};
}
Browser Caching: Room graph data is cached in React Query with a configurable stale time. Layout positions are persisted to localStorage. For Stage 3, an IndexedDB cache with a version hash enables delta-only fetches on reconnect.
17.4 Memory Management¶
- React Flow virtualization: Built-in — nodes outside viewport are not rendered
- Edge simplification: At low zoom levels, merge parallel edges
- Lazy loading: NPC/item details load only when inspector opens
- Cache eviction: React Query's garbage collection clears stale data
17.5 Performance Targets (Staged)¶
Stage 1 — Full-graph mode (v1 target):
| Metric | Target | Notes |
|---|---|---|
| Initial load (100 rooms) | <1s | Current getWorldGraph() + React Flow |
| Initial load (1,000 rooms) | <3s | With LOD at default zoom |
| Pan/zoom latency | <16ms (60fps) | React Flow virtualization |
| Room creation round-trip | <300ms | API call + React Query invalidation |
| WebSocket update latency | <200ms | Server broadcast + client render |
| Memory usage (1,000 rooms) | <150MB | Browser heap snapshot |
Stage 2 — Zone filtering (post-Phase 2):
| Metric | Target | Notes |
|---|---|---|
| Initial load (3,000 rooms, filtered to 500) | <2s | Zone filter reduces payload |
| Switch zone filter | <1s | New API call + re-render |
Stage 3 — Viewport pagination (post-Phase 4):
| Metric | Target | Notes |
|---|---|---|
| Initial load (10,000 rooms, viewport of 200) | <2s | Viewport query |
| Pan to new region | <500ms | Lazy load adjacent viewport |
| Full minimap render | <1s | Separate lightweight endpoint |
18. Mix-and-Match Flexibility¶
The visual editor is one tool in a multi-surface authoring ecosystem. Builders can mix and match approaches freely, understanding the source-of-truth model (§2.1).
18.1 Visual Tools + YAML Workflow¶
The visual editor is a live-world editor. To integrate with YAML-based workflows, builders use an explicit export → edit → import cycle:
┌─ Visual Editor ─┐ ┌─ YAML Files ─┐ ┌─ Tier 1 Pipeline ─┐
│ Edit live world │ │ data/areas/ │ │ Discover → Parse │
│ via admin API │ │ village.yaml │ │ → Prepare → Refs │
│ │ │ │ │ → Instantiate │
│ Export to YAML │───▶│ Version- │───▶│ → PostLoad │
│ (Tier 1 format) │ │ controlled │ │ │
│ │ │ in git │ │ │
│ Import YAML │◀───│ │◀───│ Validate & report │
│ into live world │ │ │ │ errors │
└──────────────────┘ └───────────────┘ └────────────────────┘
Key distinction from earlier draft: The visual editor does NOT directly edit
YAML files. It mutates the live world, and the "Export" action produces a YAML
snapshot in Tier 1 canonical format (_meta.schema, component-centric). See §4.8
for the exact format.
Detecting drift: After editing in the visual editor, builders can use
maid data diff (a Tier 1 feature) to compare YAML definitions against live-world
state and see what has changed.
18.2 Visual Tools + In-Game Commands¶
Changes made via the admin REST API (visual editor, API clients) are reflected in
real time via the world WebSocket channel (see §3.5).
Changes made via in-game builder commands (@dig, @create, @describe) are
not automatically reflected in the visual editor in v1. These commands call
world.register_room() directly, bypassing the admin API and its WebSocket
broadcasting. Builders must use the map toolbar's Refresh button to pull in
in-game changes.
┌─ In-Game Terminal ──────┐ ┌─ Visual Editor ─────────┐
│ > @dig north = Armory │ │ │
│ Room "Armory" created. │ │ (No automatic update — │
│ Exit north created. │ │ in-game commands bypass │
│ │ │ admin API broadcasts) │
│ │ │ │
│ │ │ Builder clicks [Refresh] │
│ │ │ → New room appears on │
│ │ │ map after re-fetch. │
│ │ │ │
│ │◀────│ Builder clicks room on │
│ [Teleported to Armory] │ │ map and hits "Teleport" │
└─────────────────────────┘ └──────────────────────────┘
Post-v1 enhancement: Adding a RoomRegisteredEvent to World.register_room()
and subscribing the WebSocket broadcaster to it would enable true cross-surface
real-time sync (see §3.5 scope limitation note).
18.3 Visual Tools + Python Packs¶
Content pack developers writing Python code can use the visual tools for testing and visualization:
- Write NPC logic in Python (content pack code)
- Load the pack via
maid server start - Open the visual editor to see NPCs placed in rooms
- Use Play Mode to test NPC behavior interactively
- Iterate on Python code, hot-reload, test again
18.4 Offline Editing¶
For builders without server access:
- Export zone as YAML from editor (or from
maid dataCLI) - Edit YAML in VS Code (or any text editor) with MAID schema validation
- Import YAML back into the editor (or use
maid data load) - Visual diff shows what changed before applying
18.5 CI/CD Integration¶
YAML files generated by the visual tools can be validated in CI:
# .github/workflows/validate-content.yml
- name: Validate game content
run: |
uv run maid data validate data/
uv run maid data lint data/
The editor can also connect to a staging server for preview deployments, allowing designers to review changes before they reach production.
18a. Testing Strategy¶
18a.1 Test Pyramid¶
┌──────────┐
│ E2E (8) │ Playwright: critical builder workflows
┌┴──────────┴┐
│ Integration │ WebSocket protocol, API round-trips,
│ (20+) │ multi-client collaboration
┌┴─────────────┴┐
│ Component │ React Testing Library: editor panels,
│ (60+) │ inspector tabs, tool interactions
┌┴────────────────┴┐
│ Unit (100+) │ Vitest: stores, hooks, transforms,
│ │ layout algorithms, undo/redo
└───────────────────┘
18a.2 Frontend Unit Tests (Vitest)¶
| Area | Tests | Description |
|---|---|---|
useEditorHistory |
15+ | Undo/redo identity preservation, composite ops, stack limits |
mapEditorStore |
10+ | Selection, layers, filters, viewport state |
inspectorStore |
8+ | Open/close, tab switching, form state |
| Graph transforms | 10+ | GraphNode → React Flow Node, exit → edge conversion |
| Layout algorithms | 8+ | Force layout convergence, grid snapping, manual positioning |
| Lock management | 8+ | Acquire, release, timeout, conflict detection |
| Export/import | 10+ | YAML generation, canonical format validation, round-trip |
18a.3 Component Tests (React Testing Library)¶
| Component | Key scenarios |
|---|---|
RoomNode |
Renders with correct label, color-coded by zone, lock indicator |
RoomInspector |
Tab switching, form edits, save/cancel, exit management |
MapToolbar |
Tool selection, keyboard shortcuts, active state |
LayerPanel |
Toggle visibility, filter by zone, count badges |
BulkEditor |
Multi-select actions, confirmation dialogs |
CollaborationBar |
Presence avatars, lock indicators, cursor positions |
18a.4 WebSocket Protocol Tests¶
Test the message handler extensions (§3.5) in isolation:
- Subscribe to editor channel → receive presence updates
- Cursor broadcast → all subscribers except sender receive cursor position
- Lock acquire → success returns
EDITOR_LOCK_GRANTED, conflict returnsEDITOR_LOCK_DENIED - Lock timeout → expired lock auto-releases, broadcast
EDITOR_LOCK_RELEASED - Room CRUD broadcast → world channel subscribers receive
ROOM_CREATED/ROOM_UPDATED/ROOM_DELETED - Rate limiting → cursor messages throttled to 10Hz per client
- Auth enforcement → unauthenticated clients cannot subscribe to editor channel
18a.5 Multi-Client Collaboration Tests¶
Playwright tests with two browser contexts simulating concurrent editing:
- Exclusive lock conflict: Builder A locks room → Builder B attempts lock → B sees "locked by A"
- Presence join/leave: Builder A opens editor → Builder B sees A's avatar → A disconnects → B sees A removed
- Admin-API sync: Builder A creates room via editor → Builder B's map shows new room within 2s (via
worldWS channel) - Manual refresh for in-game changes: In-game
@dig north→ map editor does NOT auto-update → click Refresh → new room appears - Concurrent zone editing: A edits room 1, B edits room 2 → both succeed, no interference
- Lock expiry recovery: A locks room, closes browser → lock expires after 5 min → B can lock
18a.6 Visual Regression Tests¶
Use Playwright screenshot comparison for canvas-heavy components:
- Map canvas with 20 rooms, colored by zone
- Room node in selected, locked, and default states
- Inspector panel at various tab states
- Balance dashboard charts (Recharts snapshots)
18a.7 Performance Tests¶
Not automated in CI (too environment-dependent). Run manually before releases:
- Load 1,000 rooms, measure initial render time (Stage 1 target: <3s)
- Rapid node creation (50 rooms), measure input lag (<100ms per room)
- WebSocket message throughput (100 cursor updates/sec from 5 clients)
18b. Accessibility Requirements¶
Target: WCAG 2.1 AA for all editor features.
18b.1 Keyboard Navigation¶
The visual editor must be fully operable via keyboard:
| Context | Keys | Action |
|---|---|---|
| Map canvas | Tab |
Cycle through rooms in DOM order |
| Map canvas | Enter |
Open inspector for focused room |
| Map canvas | Delete |
Delete focused room (with confirmation) |
| Map canvas | Arrow keys |
Pan viewport |
| Map canvas | + / - |
Zoom in / out |
| Map canvas | Ctrl+A |
Select all rooms |
| Inspector | Tab / Shift+Tab |
Navigate between fields |
| Inspector | Escape |
Close inspector, return focus to map |
| Tool bar | V, C, X, P, E |
Select tool (select, create, connect, pan, erase) |
| Tool bar | Ctrl+Z / Ctrl+Shift+Z |
Undo / redo |
| Layer panel | Space |
Toggle layer visibility |
React Flow v11 provides keyboard support for node focus and selection. We extend
it with custom onKeyDown handlers for editor-specific operations.
18b.2 Focus Management¶
- Inspector slide-over: Focus trapped inside panel when open (react-focus-lock or equivalent). Focus returns to the triggering room node on close.
- Modals (delete confirmation, bulk edit): Standard focus trap.
Escapeto dismiss. - Context menu: Focus moves to first menu item. Arrow keys navigate. Escape closes.
- Tool palette: roving tabindex pattern — arrow keys move between tools.
18b.3 Screen Reader Support¶
- Map canvas: Each room node has
role="treeitem"witharia-labelcontaining room name, zone, exit count, and lock status. - Minimap: Decorative (
aria-hidden="true"), not interactive. - Inspector tabs:
role="tablist"/role="tab"/role="tabpanel"witharia-selectedstate. - Presence bar: Live region (
aria-live="polite") announces builder join/leave. - Lock notifications:
aria-live="assertive"for lock denied messages. - Status messages: "Room saved", "Undo: delete room" announced via live region.
18b.4 Color and Visual¶
- Zone color coding: All color-coded zones include a text label or icon pattern as a secondary signal. No information conveyed by color alone.
- Lock indicators: Red border (color) + padlock icon (shape) + "Locked by X" tooltip (text). Triple-redundant.
- Contrast: All text meets 4.5:1 contrast ratio (AA). Editor controls meet 3:1.
- Presence cursors: Each cursor has a name label in addition to color.
18b.5 Reduced Motion¶
When prefers-reduced-motion: reduce is detected:
- Force-directed layout snaps to final position (no animation)
- Panel slide-overs open instantly (no slide transition)
- Presence cursors teleport instead of smooth-follow
- Map viewport changes are instant (no zoom/pan animation)
@media (prefers-reduced-motion: reduce) {
.react-flow__node, .react-flow__edge { transition: none !important; }
.inspector-panel { transition: none !important; }
.presence-cursor { transition: none !important; }
}
19. Implementation Plan¶
19.1 Phase Overview¶
Phase 1 (8 weeks) Phase 2 (6 weeks) Phase 3 (6 weeks)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ World Map Editor │ │ NPC Editor │ │ Quest Editor │
│ Room Inspector │ │ Item Editor │ │ Dialogue Editor │
│ Basic collab │ │ Content Browser │ │ Balance Dash │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
Phase 4 (4 weeks) Phase 5 (4 weeks)
┌─────────────────┐ ┌─────────────────┐
│ Play Mode │ │ Collaboration │
│ Bulk Editor │ │ Tablet Layout │
│ Import/Export │ │ Pack Extensions │
└─────────────────┘ └─────────────────┘
19.2 Phase 1: World Map Editor + Room Inspector (8 weeks)¶
Dependencies: Existing admin frontend, World API, WebSocket infrastructure.
Backend prerequisites (must be completed before or during Phase 1):
- Enrich GraphNode with npc_count, item_count, tags (§3.2a)
- Add revision, updated_at, locked_by to RoomResponse (§16.2)
- Add EDITOR and WORLD channels to WebSocket (§3.5)
- Add room CRUD broadcasting to world admin router (§3.5)
- Implement EntityLockManager for exclusive locks (§16.2)
| Week | Deliverable |
|---|---|
| 1-2 | Backend prerequisites: enrich GraphNode, add revision/lock fields, add WS channels + room broadcasting, implement lock manager. |
| 3 | Extract World.tsx into MapEditor.tsx with MapCanvas, RoomNode, ExitEdge components. Add mapEditorStore. Set up new routing. Implement useEditorHistory (§3.4). |
| 4 | Implement Create and Connect tools. Room creation form, direction picker. |
| 5 | Room Inspector panel: header, description tab with time/season/weather variants. |
| 6 | Room Inspector: exits tab, contents tab, properties tab. |
| 7 | Layers, filters, minimap. Layout algorithm selector (force/manual/grid). Multi-select, bulk operations. Context menu. |
| 8 | Collaboration: presence cursors, exclusive entity locks, change log. Polish and testing. |
Exit Criteria:
- Builder can create, edit, connect, and delete rooms entirely from the visual editor
- Map supports 1,000 rooms with <3s initial load (Stage 1 target)
- Two builders can edit simultaneously with exclusive locks (no conflicts)
- Undo/redo preserves entity identity (new useEditorHistory hook)
- Room changes from one editor client appear on another editor client's map via WebSocket
- Manual refresh button pulls in changes from in-game commands
19.3 Phase 2: NPC/Item Editors + Content Browser (6 weeks)¶
Dependencies: Phase 1 complete. Entity API, component type registry.
Backend prerequisites (must be completed before or during Phase 2):
- Implement POST /admin/npc/{id}/dialogue/preview endpoint for NPC live dialogue
testing from the editor (see §6.3 gap analysis)
| Week | Deliverable |
|---|---|
| 1-2 | NPC Editor: identity, stats sliders, AI dialogue config, dialogue preview endpoint + live test panel |
| 3 | NPC Editor: behavior radar chart, schedule timeline, relationships graph |
| 4 | Item Editor: type-driven forms, requirements, crafting recipe editor |
| 5 | Content Browser: search, categories, preview cards, drag-and-drop to map |
| 6 | Integration testing, template library, polish |
Exit Criteria: - NPCs can be created and fully configured (including AI dialogue) from the editor - Live dialogue preview works from the editor (via new admin endpoint) - Items of all types can be created with type-specific forms - Content Browser allows searching and placing entities onto the map
19.4 Phase 3: Quest/Dialogue Editors + Balance Dashboard (6 weeks)¶
Dependencies: Phase 2 complete. Quest system from maid-classic-rpg.
| Week | Deliverable |
|---|---|
| 1-2 | Quest Editor: objective graph canvas, objective types, branching |
| 3 | Quest Editor: simulator, reward editor, validation |
| 4 | Dialogue Editor: conversation tree canvas, node types, conditions/effects |
| 5 | Dialogue Editor: AI hybrid mode, preview pane |
| 6 | Balance Dashboard: combat curves, economy Sankey, content heatmap |
Exit Criteria: - Quests can be designed visually with branching paths and simulated - Dialogue trees can be authored with conditions, effects, and AI fallback - Balance dashboard shows actionable insights about combat/economy balance
19.5 Phase 4: Play Mode + Bulk Editor + Import/Export (4 weeks)¶
Dependencies: Phase 1-3 complete. Player frontend xterm.js integration.
| Week | Deliverable |
|---|---|
| 1 | Embedded terminal (xterm.js panel), ghost mode, teleport integration |
| 2 | Hot-reload bridge: editor saves → terminal sees changes. Session recording. |
| 3 | Bulk Data Editor: entity grid, inline editing, CSV import/export |
| 4 | YAML import/export with diff preview, round-trip fidelity testing |
Exit Criteria: - Builder can test changes in an embedded terminal without leaving the editor - Bulk operations work on 100+ entities with preview/undo - YAML export produces Tier 1-compatible format; import loads and validates correctly
19.6 Phase 5: Tablet Layout + Pack Extensions (4 weeks)¶
Dependencies: Phase 1-4 complete.
| Week | Deliverable |
|---|---|
| 1 | Tablet responsive layout (≥768px), touch optimization, quick edit mode |
| 2 | Content pack extension API: duck-typed get_editor_extensions(), metadata endpoint, component registry |
| 3 | maid-classic-rpg built-in extension tabs (combat stats, spells). Extension rendering in inspector. |
| 4 | Accessibility audit, documentation, Stage 2 scalability work (zone filtering) |
Exit Criteria:
- Editor is usable on tablet devices (≥768px)
- maid-classic-rpg has at least 2 extension tabs (combat stats, spells)
- Keyboard navigation works for all editor operations
- Documentation covers all editor features
19.7 Tier Dependencies¶
Tier 1 (YAML Pipeline) ─── required ───▶ Tier 3 (Visual Tools)
Tier 2 (AI Generation) ─── optional ───▶ Tier 3 (Visual Tools)
Tier 3 Phase Dependencies:
Phase 1: Requires Tier 1 YAML format spec (for export)
Phase 2: Optionally uses Tier 2 AI for dialogue preview
Phase 3: Optionally uses Tier 2 AI for balance analysis
Phase 4: Uses Tier 1 pipeline for import validation
Tier 3 can begin development before Tier 2 is complete. The AI-dependent features (dialogue preview, balance analysis, AI-assisted description writing) degrade gracefully: buttons are disabled with tooltips like "Requires AI provider configuration."
Note: Tier 3 does NOT depend on Tier 2 for any core functionality. The dependency header has been updated to reflect this: "Integrates with Tier 2 (optional)."
20. Appendices¶
Appendix A: New API Endpoints for Editor Backend¶
These endpoints extend the existing admin API (/admin/):
# Editor-specific endpoints
GET /admin/editor/extensions # Content pack editor extension metadata
GET /admin/editor/layout/{user_id} # Get user's saved map layout
PUT /admin/editor/layout/{user_id} # Save user's map layout
GET /admin/editor/locks # List all active entity locks
DELETE /admin/editor/locks/{entity_id} # Force-release a lock (ADMIN only)
# NPC dialogue preview (Phase 2 prerequisite — see §6.3)
POST /admin/npc/{id}/dialogue/preview # Send test message, get AI response
# Enhanced world endpoints (additions to existing)
GET /admin/world/graph/viewport # Viewport-paginated graph (Stage 3)
GET /admin/world/graph/neighbors/{id} # Rooms connected to a room (1-N hops)
GET /admin/world/graph/path/{from}/{to} # Shortest path between two rooms
GET /admin/world/rooms/{id}/entities # List entities in a room (NPCs, items)
# Balance analysis endpoints (Tier 3 presentation layer over Tier 2 engine — see §12)
GET /admin/balance/combat # Combat balance data (DPS/HP by level)
GET /admin/balance/economy # Economy flow data (sources/sinks)
GET /admin/balance/content # Content density per room/zone
GET /admin/balance/progression # XP/level curve data
GET /admin/balance/loot # Loot distribution analysis
# Session recording
POST /admin/editor/recordings # Save a session recording
GET /admin/editor/recordings # List recordings
GET /admin/editor/recordings/{id} # Get recording for playback
DELETE /admin/editor/recordings/{id} # Delete recording
Endpoints NOT included in v1 (see §16.4 for rationale):
- Changeset CRUD (/admin/changesets/*) — deferred to post-v1
- Changeset review/approval workflow — deferred to post-v1
Appendix B: WebSocket Message Formats¶
Editor Channel Messages (via /admin/ws with channel: "editor"):
These message types correspond to WebSocketMessageType enum values added in §3.5.
All type values use lowercase with underscores, matching the existing
WebSocketMessageType convention in websocket.py.
// --- Client → Server ---
// Cursor movement (throttled to 10Hz max)
// Type: editor_cursor
{
channel: "editor",
type: "editor_cursor",
payload: { x: 150.5, y: 320.0, viewport_id: "map" },
client_id: "abc-123",
timestamp: "2025-07-18T10:30:00Z"
}
// Lock request
// Type: editor_lock_acquire
{
channel: "editor",
type: "editor_lock_acquire",
payload: { entity_id: "3f2a...8c1d", entity_type: "room" },
client_id: "abc-123",
timestamp: "2025-07-18T10:30:01Z"
}
// Selection change
// Type: editor_selection
{
channel: "editor",
type: "editor_selection",
payload: { selected_ids: ["3f2a...8c1d", "7b4e...2f9a"] },
client_id: "abc-123",
timestamp: "2025-07-18T10:30:10Z"
}
// --- Server → Client ---
// Presence update (join/leave/ping)
// Type: editor_presence
{
channel: "editor",
type: "editor_presence",
payload: {
action: "join", // "join" | "leave" | "ping"
user: {
id: "user-456",
name: "builder_a",
color: "#3b82f6",
role: "BUILDER"
}
},
source_user: "builder_a",
timestamp: "2025-07-18T10:30:00Z"
}
// Lock granted
// Type: editor_lock_granted
{
channel: "editor",
type: "editor_lock_granted",
payload: {
entity_id: "3f2a...8c1d",
expires_at: "2025-07-18T10:35:01Z"
},
source_user: "admin",
timestamp: "2025-07-18T10:30:01Z"
}
// Lock denied (conflict)
// Type: editor_lock_denied
{
channel: "editor",
type: "editor_lock_denied",
payload: {
entity_id: "3f2a...8c1d",
locked_by: "builder_a",
locked_since: "2025-07-18T10:28:00Z",
expires_at: "2025-07-18T10:33:00Z"
},
source_user: "system",
timestamp: "2025-07-18T10:30:01Z"
}
World Channel Messages (via /admin/ws with channel: "world"):
These messages notify subscribers when room state changes made via the admin REST
API (visual editor, API clients). In-game builder commands (@dig, @create) do
not trigger these broadcasts in v1 (see §3.5 scope limitation).
// Room created
// Type: room_created
{
channel: "world",
type: "room_created",
payload: {
room_id: "3f2a...8c1d",
name: "Grand Plaza",
area_id: "town-center",
source: "editor", // "editor" | "api"
user: "admin"
},
source_user: "system",
timestamp: "2025-07-18T10:30:05Z"
}
// Room updated
// Type: room_updated
{
channel: "world",
type: "room_updated",
payload: {
room_id: "3f2a...8c1d",
changed_fields: ["description", "metadata"],
revision: 3,
source: "editor",
user: "builder_a"
},
source_user: "system",
timestamp: "2025-07-18T10:30:05Z"
}
// Room deleted
// Type: room_deleted
{
channel: "world",
type: "room_deleted",
payload: {
room_id: "3f2a...8c1d",
source: "editor",
user: "admin"
},
source_user: "system",
timestamp: "2025-07-18T10:30:10Z"
}
Appendix C: React Component Hierarchy¶
App.tsx
├── QueryClientProvider
├── BrowserRouter (basename="/admin-ui")
│ ├── /login → LoginPage
│ └── ProtectedRoute → Layout
│ ├── / → DashboardPage
│ ├── /entities → EntitiesPage
│ ├── /players → PlayersPage
│ ├── /world → WorldPage (legacy, kept for backward compat)
│ ├── /logs → LogsPage
│ ├── /config → ConfigPage
│ │
│ │ ── NEW PAGES (Tier 3) ──
│ │
│ ├── /map → MapEditorPage
│ │ ├── MapToolbar
│ │ │ ├── ToolSelector (select/create/connect/pan/erase)
│ │ │ ├── LayoutDropdown
│ │ │ ├── ZoomControls
│ │ │ ├── UndoRedoButtons
│ │ │ ├── LayerToggle
│ │ │ └── SearchBox
│ │ ├── MapLayerPanel
│ │ │ ├── LayerCheckbox (per layer)
│ │ │ ├── FilterSection
│ │ │ │ ├── ZoneFilter
│ │ │ │ ├── AreaFilter
│ │ │ │ ├── LevelRangeSlider
│ │ │ │ └── TagFilter
│ │ │ └── SearchResults
│ │ ├── MapCanvas (React Flow)
│ │ │ ├── RoomNode (custom node)
│ │ │ ├── ExitEdge (custom edge)
│ │ │ ├── Minimap
│ │ │ ├── PresenceCursors (overlay)
│ │ │ └── MapContextMenu
│ │ ├── RoomInspector (slide-in panel)
│ │ │ ├── InspectorHeader
│ │ │ ├── TabBar
│ │ │ ├── DescriptionTab
│ │ │ │ ├── BaseDescriptionEditor
│ │ │ │ ├── TimeVariantList
│ │ │ │ ├── SeasonVariantList
│ │ │ │ ├── WeatherEffectList
│ │ │ │ ├── RandomDetailList
│ │ │ │ ├── MoodSelector
│ │ │ │ └── DescriptionPreview
│ │ │ ├── ExitsTab
│ │ │ │ ├── ExitRow
│ │ │ │ ├── DestinationPreview
│ │ │ │ └── QuickConnectDialog
│ │ │ ├── ContentsTab
│ │ │ │ ├── EntityList (NPCs)
│ │ │ │ ├── EntityList (Items)
│ │ │ │ └── DropZone
│ │ │ ├── ComponentsTab
│ │ │ │ ├── ComponentCard
│ │ │ │ └── AddComponentDropdown
│ │ │ ├── PropertiesTab
│ │ │ │ ├── FlagCheckboxes
│ │ │ │ ├── AttributeSliders
│ │ │ │ └── CustomDataEditor
│ │ │ ├── HistoryTab
│ │ │ │ ├── ChangeEntry
│ │ │ │ └── DiffViewer
│ │ │ └── [ExtensionTabs...] (from content packs)
│ │ ├── CollaborationPanel
│ │ │ ├── OnlineUsers
│ │ │ └── ChangeLog
│ │ └── StatusBar
│ │
│ ├── /npcs/:id? → NPCEditorPage
│ │ ├── NPCHeader
│ │ ├── NPCTabBar
│ │ ├── NPCIdentityForm
│ │ ├── StatSliders
│ │ ├── AIDialogueConfig
│ │ ├── BehaviorRadarChart
│ │ ├── ScheduleTimeline
│ │ ├── InventoryList
│ │ ├── RelationshipGraph
│ │ ├── MemoryViewer
│ │ ├── SpawnRulesForm
│ │ └── NPCPreviewPanel (with DialoguePreview)
│ │
│ ├── /items/:id? → ItemEditorPage
│ │ ├── ItemHeader
│ │ ├── ItemIdentityForm
│ │ ├── ItemTypeForm (dynamic)
│ │ ├── RequirementsEditor
│ │ ├── CraftingRecipeEditor
│ │ ├── LootTableEditor
│ │ └── BalanceComparison
│ │
│ ├── /quests/:id? → QuestEditorPage
│ │ ├── QuestHeader
│ │ ├── QuestInfoPanel
│ │ ├── QuestCanvas (React Flow)
│ │ │ ├── ObjectiveNode (custom node)
│ │ │ └── BranchEdge (custom edge)
│ │ ├── ObjectiveInspector
│ │ ├── RewardEditor
│ │ └── QuestSimulator
│ │
│ ├── /dialogue/:id? → DialogueEditorPage
│ │ ├── DialogueHeader
│ │ ├── DialogueCanvas (React Flow)
│ │ │ ├── DialogueNode (custom node)
│ │ │ └── ResponseEdge (custom edge)
│ │ ├── NodeInspector
│ │ │ ├── ConditionEditor
│ │ │ └── EffectEditor
│ │ └── DialoguePreviewPane
│ │
│ ├── /bulk → BulkEditorPage
│ │ ├── BulkToolbar
│ │ ├── EntityGrid
│ │ ├── InlineEditor
│ │ ├── BulkOperationsDropdown
│ │ ├── CSVImportExport
│ │ └── DiffPreview
│ │
│ ├── /content → ContentBrowserPage
│ │ ├── ContentSearchBar
│ │ ├── CategoryTree
│ │ ├── ResultsList
│ │ │ └── ContentCard
│ │ ├── ContentPreview
│ │ └── TemplateLibrary
│ │
│ ├── /balance → BalanceDashboardPage
│ │ ├── CombatCurves (Recharts)
│ │ ├── EconomySankey (Recharts/d3)
│ │ ├── ContentHeatmap (canvas)
│ │ ├── ProgressionChart (Recharts)
│ │ └── LootDistribution (Recharts)
│ │
│ └── /ext/:packName/:page → ExtensionPage (dynamic)
│ └── [Loaded from content pack extension manifest]
Appendix D: State Management Patterns¶
Store Organization:
// stores/index.ts (updated)
export { useAuthStore } from './authStore';
export { useDashboardStore } from './dashboardStore';
// New Tier 3 stores
export { useMapEditorStore } from './mapEditorStore';
export { useInspectorStore } from './inspectorStore';
export { useEntityEditorStore } from './entityEditorStore';
export { useCollaborationStore } from './collaborationStore'; // presence + locks (no changesets in v1)
export { useContentBrowserStore } from './contentBrowserStore';
export { useBalanceStore } from './balanceStore';
export { useBulkEditorStore } from './bulkEditorStore';
export { usePlayModeStore } from './playModeStore';
Cross-Store Communication Pattern:
Stores communicate via Zustand's subscribe API, not direct imports:
// Example: When a room is selected in mapEditorStore,
// inspectorStore opens with that room's data
useMapEditorStore.subscribe(
(state) => state.selectedNodes,
(selectedNodes) => {
if (selectedNodes.length === 1) {
useInspectorStore.getState().openInspector(selectedNodes[0], 'room');
} else {
useInspectorStore.getState().closeInspector();
}
}
);
Persistence Pattern:
Editor preferences (layout positions, layer visibility, filter state) persist
to localStorage with a store middleware:
import { persist, createJSONStorage } from 'zustand/middleware';
export const useMapEditorStore = create<MapEditorState>()(
persist(
(set, get) => ({
// ... state and actions
}),
{
name: 'maid-map-editor',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
// Only persist user preferences, not ephemeral state
layers: state.layers,
filters: state.filters,
layoutMode: state.layoutMode,
nodePositions: state.nodePositions,
viewport: state.viewport,
}),
}
)
);
Appendix E: Key TypeScript Type Additions¶
// types/editor.ts — New types for visual editor features
// Map editor
export interface MapViewport {
x: number;
y: number;
zoom: number;
}
export interface MapTool {
id: 'select' | 'create' | 'connect' | 'pan' | 'erase';
label: string;
icon: string;
shortcut: string; // e.g., "V" for select, "C" for create
}
export interface MapLayer {
id: string;
label: string;
visible: boolean;
icon: string;
count?: number;
}
// Collaboration
export interface EditorUser {
id: string;
name: string;
color: string;
role: string;
cursor?: { x: number; y: number };
selectedNodes?: string[];
lastSeen: string;
}
export interface EntityLock {
entityId: string;
userId: string;
userName: string;
acquiredAt: string;
expiresAt: string;
}
// Editor undo/redo operation log (see §3.4)
export interface EditorOperation {
id: string; // Unique operation ID
type: 'create_room' | 'update_room' | 'delete_room'
| 'create_exit' | 'delete_exit' | 'update_entity'
| 'composite'; // For grouped operations
entityId: string;
entityType: 'room' | 'exit' | 'entity';
timestamp: string;
serverRevision?: number; // Revision after apply
forwardPatch: FieldPatch; // Data to re-apply
reversePatch: FieldPatch; // Data to undo
children?: EditorOperation[]; // For composite operations
}
export interface FieldPatch {
entityId: string;
entityType: string;
snapshot?: Record<string, unknown>; // Full entity snapshot (for create/delete)
fields?: Record<string, unknown>; // Partial field updates (for update)
}
// Change log entry (audit trail, NOT a changeset/review system)
export interface ChangeEntry {
id: string;
timestamp: string;
userId: string;
userName: string;
entityId: string;
entityType: string;
operation: string;
description: string;
fields?: string[];
}
// Quest editor
export interface QuestObjective {
id: string;
type: 'kill' | 'collect' | 'deliver' | 'escort' | 'explore'
| 'dialogue' | 'interact' | 'choice' | 'custom';
label: string;
description: string;
config: Record<string, unknown>; // Type-specific configuration
journalEntry?: string;
hint?: string;
}
export interface QuestEdge {
id: string;
source: string; // Objective ID
target: string; // Objective ID
condition: QuestEdgeCondition;
}
export interface QuestEdgeCondition {
type: 'always' | 'choice' | 'conditional';
choiceLabel?: string;
condition?: string;
}
// Dialogue editor
export interface DialogueNode {
id: string;
type: 'npc_say' | 'player_response' | 'condition' | 'effect'
| 'ai_ambient' | 'end';
text?: string;
conditions?: DialogueCondition[];
effects?: DialogueEffect[];
aiConfig?: {
fallback: boolean;
personality?: string;
contextHints?: string[];
};
}
export interface DialogueCondition {
type: string; // "has_item", "level_gte", "skill_check", etc.
args: unknown[]; // Type-specific arguments
label: string; // Human-readable description
}
export interface DialogueEffect {
type: string; // "give_item", "set_flag", "teleport", etc.
args: unknown[]; // Type-specific arguments
label: string; // Human-readable description
}
// Balance dashboard
export interface CombatBalanceData {
levels: number[];
playerDPS: number[];
playerHP: number[];
monsterDPS: number[];
monsterHP: number[];
expectedDPS: number[];
expectedHP: number[];
}
export interface EconomyFlowData {
sources: { name: string; value: number }[];
sinks: { name: string; value: number }[];
netFlow: number;
inflationRate: number;
}
export interface ContentDensityData {
roomId: string;
roomName: string;
npcCount: number;
itemCount: number;
exitCount: number;
totalDensity: number;
coordinates?: { x: number; y: number };
}
// Session recording
export interface SessionRecording {
id: string;
builder: string;
title: string;
startTime: string;
endTime: string;
roomsVisited: string[];
entries: SessionEntry[];
}
export interface SessionEntry {
timestamp: string;
type: 'input' | 'output' | 'event' | 'navigation';
content: string;
roomId?: string;
metadata?: Record<string, unknown>;
}
// Extension system (see §3.6 — build-time bundled, not dynamic)
export interface EditorExtensionManifest {
packName: string;
extensions: EditorExtension[];
}
export interface EditorExtension {
type: 'inspector_tab' | 'toolbar_button'; // v1 scope; 'page' | 'map_node_type' are post-v1
id: string;
label: string;
icon: string;
priority?: number;
entityTypes?: string[]; // Show tab only for these entity types
componentType?: string; // Show tab only when entity has this component
// Note: No frontendModule path — v1 uses build-time bundled React components
// registered via lazy(() => import(...)) in a static registry. See §3.6.
}
Appendix F: Migration Path from Current World.tsx¶
The existing World.tsx page remains functional during Tier 3 development. The
migration path:
-
Phase 1 Start: Create
MapEditor.tsxas a new page at/map.World.tsxremains at/worldunchanged. -
Phase 1 Mid: Extract shared utilities from
World.tsxintolib/mapUtils.ts(graph data transformation, React Flow node/edge conversion). -
Phase 1 End: Add "Open in Map Editor" link from
World.tsxto/map. Both pages coexist. -
Phase 2: Update
World.tsxto redirect to/mapwith a deprecation notice. Keep/worldroute active for bookmark compatibility. -
Phase 5 (final): Remove
World.tsx. Redirect/worldto/mapat the router level.
Appendix G: Dependency Summary¶
New npm dependencies (additions to admin_frontend/package.json):
{
"dependencies": {
"d3-force": "^3.0.0",
"d3-sankey": "^0.12.0"
},
"devDependencies": {
"@types/d3-force": "^3.0.0",
"@types/d3-sankey": "^0.12.0"
}
}
React Flow version strategy:
The admin frontend currently uses reactflow: "^11.11.4". This is the v11 monolith
package. React Flow v12 splits into scoped packages (@xyflow/react,
@reactflow/minimap, @reactflow/controls), which are incompatible with v11.
For Tier 3 Phase 1:
- Stay on reactflow v11. It includes minimap and controls built-in.
- Do NOT add @reactflow/minimap or @reactflow/controls (these are v12-only).
- v11 provides all features needed: custom nodes/edges, minimap, controls, keyboard
shortcuts, touch events, viewport management.
After Phase 1:
- Evaluate v12 migration as a dedicated task (separate from feature work).
- v12 migration involves changing imports from reactflow to @xyflow/react and
updating hook signatures. It is a mechanical refactor, not a design change.
Note: recharts, zustand, @tanstack/react-query, and react-router-dom
are already installed. No new major frameworks are introduced.
Removed from v1 dependency list:
- idb-keyval — IndexedDB for offline caching is deferred (see §14.4)
Backend dependencies: None. All new endpoints use existing FastAPI, Pydantic,
and WebSocket infrastructure already in maid-engine.
End of Tier 3 Visual Authoring Tools Design Document