Scenes

Scenes are collaborative roleplay logs — structured writing sessions between
players. The scene system tracks who participated, what was written, and where
it happened, and lets you export the finished log as Markdown or JSON.

All scene endpoints require a Bearer JWT token in the Authorization header.

Overview

Player A                   Player B                 Server
────────                   ────────                 ──────
POST /scenes               POST /scenes/:id/join
  name, location      ──▶  (joins after creation)
  ← 201 scene object

POST /scenes/:id/pose ──▶                    ──▶  broadcasts to room
  msg, type=pose

                           POST /scenes/:id/pose
                      ◀──  msg, type=pose      ──▶  broadcasts to room

PATCH /scenes/:id          (close the scene)
  status=closed

GET /scenes/:id/export
  ?format=markdown    ──▶  ← Markdown log file

Creating a Scene

POST /api/v1/scenes
Authorization: Bearer <token>
Content-Type: application/json

Request body:

Field Type Required Description
name string Yes Scene title
location string Yes Room dbref (e.g. #5)
desc string No Scene description or opening set
sceneType string No social, event, vignette, plot, training, other (default: social)
private boolean No If true, only invited players can see or post (default: false)

Example:

curl -X POST https://yourgame.example.com/api/v1/scenes \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "A Meeting in the Rain",
    "location": "#5",
    "desc": "Two characters cross paths on a wet street corner.",
    "sceneType": "social"
  }'

Response (201 Created):

{
  "id": "42",
  "name": "A Meeting in the Rain",
  "location": "#5",
  "desc": "Two characters cross paths on a wet street corner.",
  "owner": "#3",
  "participants": ["#3"],
  "allowed": ["#3"],
  "private": false,
  "poses": [],
  "startTime": 1710000000000,
  "status": "active",
  "sceneType": "social"
}

The creator is automatically added as the first participant.

Listing Scenes

GET /api/v1/scenes
Authorization: Bearer <token>

Returns all scenes visible to the authenticated user, sorted newest first.
Private scenes are filtered — only scenes where you are the owner, a participant,
or in the allowed list are returned.

GET /api/v1/scenes/locations
Authorization: Bearer <token>

Returns all rooms you have permission to enter, sorted alphabetically. Useful
for scene creation UIs that let the player pick a location from a list.

[
  { "id": "#1", "name": "The Nexus", "type": "public" },
  { "id": "#12", "name": "Staff Lounge", "type": "private" }
]

type is "private" if the room has an enter lock set, "public" otherwise.

Joining a Scene

Any player can join a public scene. Private scenes require an invitation first
(see Private Scenes).

POST /api/v1/scenes/:id/join
Authorization: Bearer <token>

No body required. If successful, the player is added to participants.

{ "success": true, "scene": { ... } }

Writing Poses

POST /api/v1/scenes/:id/pose
Authorization: Bearer <token>
Content-Type: application/json

Request body:

Field Type Required Description
msg string Yes (except set) The pose text
type string No pose (default), ooc, or set

Pose types:

Type Use Broadcast format
pose In-character action or speech CharName <msg>
ooc Out-of-character comment [OOC] CharName: <msg>
set Scene description / setter (no msg required) [Scene Set] <msg>

Poses are broadcast to the scene’s room in real time so players in the grid
also see them. The maximum pose length is 4000 characters.

Example — in-character pose:

curl -X POST https://yourgame.example.com/api/v1/scenes/42/pose \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"msg": "glances up as rain spatters her coat, then freezes.", "type": "pose"}'

Example — scene setter:

curl -X POST https://yourgame.example.com/api/v1/scenes/42/pose \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"msg": "The street is empty except for the two of them.", "type": "set"}'

Response (201 Created):

{
  "id": "a1b2c3d4-...",
  "charId": "#3",
  "charName": "Talia",
  "moniker": "%ch%crT%cna%chl%cni%cra%cn",
  "msg": "glances up as rain spatters her coat, then freezes.",
  "type": "pose",
  "timestamp": 1710000060000
}

Posting automatically adds you to participants if you weren’t already there.

Editing a Pose

You can correct a pose after posting. Only the original author or the scene
owner can edit a pose.

PATCH /api/v1/scenes/:id/pose/:poseId
Authorization: Bearer <token>
Content-Type: application/json

Request body:

{ "msg": "corrected pose text" }

The type and timestamp cannot be changed after posting. Max 4000 characters.

Response (200 OK): the updated pose object.

Updating a Scene

The scene owner (or admin/wizard) can update metadata and status.

PATCH /api/v1/scenes/:id
Authorization: Bearer <token>
Content-Type: application/json

Updatable fields:

Field Type Description
name string Scene title
desc string Scene description
status string active, paused, or closed
sceneType string social, event, vignette, plot, training, other
endTime number Unix timestamp (ms) when the scene ended

Example — close a scene:

curl -X PATCH https://yourgame.example.com/api/v1/scenes/42 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"status": "closed", "endTime": 1710003600000}'

Private Scenes

Set "private": true when creating a scene to restrict access.

Action Who can do it
View scene Owner, participants, allowed list, admin/wizard
Post a pose Owner, participants, allowed list, admin/wizard
Join Only if already in allowed list
Invite Owner, anyone in allowed list, admin/wizard

Inviting a player

POST /api/v1/scenes/:id/invite
Authorization: Bearer <token>
Content-Type: application/json
{ "target": "#7" }

target can be a dbref (#7) or a player name. The player is added to the
allowed list and can then join the scene.

Exporting a Scene

GET /api/v1/scenes/:id/export
Authorization: Bearer <token>

Optional query parameter format:

format Content-Type Description
markdown (default) text/markdown Formatted log with header and poses
json application/json Full scene object

Example:

# Markdown log
curl -H "Authorization: Bearer <token>" \
     https://yourgame.example.com/api/v1/scenes/42/export

# Raw JSON
curl -H "Authorization: Bearer <token>" \
     https://yourgame.example.com/api/v1/scenes/42/export?format=json

Markdown export format:

# A Meeting in the Rain

**Type:** social | **Status:** closed
**Location:** The Corner of Fifth and Ash
**Started:** 2026-03-18
**Ended:** 2026-03-18
**Participants:** Talia, Marcus
---

**Talia** glances up as rain spatters her coat, then freezes.

**Marcus** turns up his collar, not yet noticing her.

*[OOC] Talia: great opener!*
---
*Exported 2026-03-18*

MUSH color codes are stripped from all character names and pose content in the
export, so the output is clean plain text.

Scene Hooks

Every scene mutation fires a typed event on gameHooks, allowing plugins
(AI GM assistants, logging systems, etc.) to react without touching the
scene REST router.

Import gameHooks from jsr:@ursamu/ursamu:

import { gameHooks } from "jsr:@ursamu/ursamu";
import type { SceneSetEvent, SceneClearEvent } from "jsr:@ursamu/ursamu";

Hook reference

Event When it fires Key payload fields
scene:created New scene opened (POST /scenes) sceneId, sceneName, roomId, actorId, actorName, sceneType
scene:pose Any pose posted (POST /scenes/:id/pose) sceneId, sceneName, roomId, actorId, actorName, msg, type
scene:set Pose with type: "set" posted sceneId, sceneName, roomId, actorId, actorName, description
scene:title Scene renamed (PATCH /scenes/:id with new name) sceneId, oldName, newName, actorId, actorName
scene:clear Scene closed/finished/archived sceneId, sceneName, actorId, actorName, status

scene:set and scene:pose both fire when a "set" pose is posted —
scene:pose for anything that needs to see all pose types, scene:set for
handlers that only care about scene descriptions.

Example: AI GM assistant

import { gameHooks } from "jsr:@ursamu/ursamu";
import { send } from "../../services/broadcast/index.ts";
import type { SceneSetEvent, SceneTitleEvent, SceneClearEvent } from "jsr:@ursamu/ursamu";

// Narrate the new setting in the GM's voice
gameHooks.on("scene:set", ({ roomId, description }: SceneSetEvent) => {
  send([roomId], `%ch%cm[GM]%cn ${description}`, {});
});

// Announce title changes
gameHooks.on("scene:title", ({ roomId, oldName, newName }: SceneTitleEvent) => {
  // roomId is not in SceneTitleEvent — look it up from sceneId if needed
  console.log(`[GM] Scene renamed: "${oldName}" → "${newName}"`);
});

// Wrap up when a scene closes
gameHooks.on("scene:clear", ({ sceneName, status }: SceneClearEvent) => {
  console.log(`[GM] Scene "${sceneName}" ${status}.`);
});

See Plugin Hooks & Events for the complete GameHooks API.

API Reference

Method Endpoint Auth Description
GET /api/v1/scenes Required List visible scenes
POST /api/v1/scenes Required Create a scene
GET /api/v1/scenes/locations Required List accessible rooms
GET /api/v1/scenes/:id Required Scene detail with participants
PATCH /api/v1/scenes/:id Required Update name, desc, status, sceneType, endTime
GET /api/v1/scenes/:id/export Required Export as ?format=markdown or ?format=json
POST /api/v1/scenes/:id/pose Required Add a pose, ooc, or set entry
PATCH /api/v1/scenes/:id/pose/:poseId Required Edit a pose (author or scene owner)
POST /api/v1/scenes/:id/join Required Join a scene
POST /api/v1/scenes/:id/invite Required Add a player to the allowed list