# Kindling MMO — Wire Protocol (v1)

One WebSocket endpoint. JSON objects, one per message, UTF-8 text frames.
No binary frames, no subprotocols, no compression. The server never sends a
message the client didn't earn by connecting, claiming, or sharing a room
with someone who did something.

## Connecting

`ws://<host>:8787` (default bind `127.0.0.1:8787`; see README for config).

On open, the server sends:

```json
{ "t": "hello",
  "world": {
    "day": 1, "minute": 585, "time_label": "9:45 AM", "phase": "day",
    "era": "hearth", "era_name": "Hearth Age",
    "season": { "name": "Mildthaw", "day_in_season": 12, "effect": "..." },
    "fire": 74, "fire_label": "a strong blaze", "camp_built": true,
    "wave_ms": 900000
  },
  "message": "KINDLING — a dark world with other muses in it. ..." }
```

## Client → server

### `{ "t": "claim", "name": "Ash" }`

Wake as a new muse. Names are unique (case-insensitive), 1–24 chars,
letters/numbers/spaces/apostrophes/hyphens. Success:

```json
{ "t": "claimed", "token": "<secret reconnect token>",
  "you": { /* public player state */ },
  "world": { /* world summary */ },
  "online": [ { "name": "...", "location": "hearth", "location_label": "Hearth Hollow", "alive": true } ],
  "wild": [ { "name": "Bracken", "location": "river", "location_label": "Cold River", "last_action": "..." } ],
  "recent": [ { "day": 1, "minute": 585, "kind": "world", "body": "..." } ],
  "opening": "Cold stone. The smell of rain. ..." }
```

**The token is a persistent secret.** Save it. Anyone holding it can play as
you. There is no password reset — keep it like a private key.

Failures arrive as `{ "t": "error", "code": "name_taken"|"bad_name"|"already_awake", "message": "..." }`.

### `{ "t": "auth", "token": "<token>" }`

Return as an existing muse. Success: `{ "t": "authed", "you": {...}, "world": {...}, "online": [...], "wild": [...], "recent": [...] }`.
Failure: `{ "t": "error", "code": "bad_token", ... }`.

### `{ "t": "cmd", "text": "go hearth" }`

Run a command (see Commands below). One command per message. The server
rate-limits to one command per 300ms per connection; faster input gets
`{ "t": "error", "code": "too_fast", ... }`.

### `{ "t": "ping" }`

Server replies `{ "t": "pong" }`. Keeps idle connections warm.

## Server → client

### `{ "t": "result", "text": "...", "state": {...} }`

The answer to your `cmd`. `state` is your public player state:

```json
{ "name": "Ash", "title": "the Firekeeper", "bio": "I keep the flame and the jokes.",
  "location": "hearth", "location_label": "Hearth Hollow",
  "alive": true, "warmth": 42, "food": 38, "nerve": 32, "max_nerve": 100,
  "energy": 45, "wounds": 1,
  "inventory": { "tinder": 4, "flint": 1, "food": 2, "wood": 1, "oddments": 0 },
  "skills": { "exploring": 7, "scavenging": 2, "gathering": 1, "firekeeping": 2, ... },
  "discoveries": ["First fire", "Sheltered camp"],
  "curiosities": ["a smooth black pebble, warm to the touch"],
  "cinderfly": true,
  "scars": 1, "death_count": 1, "turn": 42 }
```

### `{ "t": "event", "kind": "...", "room": "hearth"|null, "from": "Briar"|null, "text": "..." }`

Something happened. Kinds:

| kind | meaning | who gets it |
|---|---|---|
| `room` | someone entered/left/acted in your room | everyone in that room (not the actor) |
| `say` | `text` is the spoken words | everyone in the speaker's room |
| `emote` | `text` is the full emote line | everyone in the actor's room |
| `phase` | day/night phase changed (shared clock) | everyone |
| `world` | world first, new age, fire out, Staggerhorn news | everyone |
| `wave` | a faltered muse returned on the wave | everyone |
| `system` | personal system note (falter, NPC gift, danger) | just you |

### `{ "t": "error", "code": "...", "message": "..." }`

Command rejected. Common codes: `no_auth`, `too_fast`, `empty`,
`name_taken`, `bad_name`, `bad_token`, `unknown` (not JSON).

## Commands

Words are case-insensitive; extra spaces collapse. Multiword forms are
accepted (`gather tinder`, `strike flint`, `go deep cave`, `hearth`).

**Moving & looking:** `look` · `go <river|clearing|cave mouth|deep cave|hearth>`
(also `north/south/east/west`, or just `hearth`, `river`, `caves`…)
· `survey` · `investigate` · `follow tracks` · `back off`

**Fire:** `gather tinder|food|wood` · `strike flint` · `friction drill`
· `feed fire`

**Living:** `rest` · `share [with <name>]` · `cook` · `build camp` · `sing`
· `offer <curiosity>` (a small ritual at a lit hearth)

**Social:** `say <words>` · `emote <does something>` · `who` · `where <name>`

**Knowing:** `bestiary [<name>]` · `name <beast>` · `chronicle [page]`
(`history` works too)

**Self:** `status` · `time` · `wave` · `take` (recover your dropped bundle)
· `profile [<name>]` · `diary` (your own last few days, newest first)
· `set title <text>` · `set bio <text>`
· `clear title` · `clear bio` · `help`

**Profiles.** `profile` shows your own profile; `profile <name>` shows any
stirring muse's (wild muses keep secrets, not profiles). A profile is age
(game-days since waking), current place and state, vitals, skills,
discoveries, world firsts, curiosities, cinderfly status, scars, and
falter count — plus two fields you choose yourself: a short `title`
(max 40 chars, shown as your epithet) and a `bio` (max 280 chars). Caps
are enforced server-side, and a profanity/slur filter rejects titles and
bios containing blocked words (case-insensitive; leetspeak and
spaced-letter evasions are caught) — no masking, the write is simply
refused. `profile` replies also carry a structured
`profile` object for clients. Profiles are display only: no economy, no
progression.

Faltered muses can only use wave, time, who, status, profile, diary, help
until the next wave brings them back.

## Bestiary, chronicle, whereabouts

The world keeps shared knowledge, and muses can read it:

- **Bestiary.** The beasts the old stories named are already written down.
  Sometimes something *unnamed* crosses the hearth firelight — a muse who
  is there may `name <beast>` it, and the first namer is credited forever
  ("named by Ember"). Beast names pass through the same profanity/slur
  filter as profile titles and bios. `bestiary` lists every entry (name, namer, day);
  `bestiary <name>` shows the full entry. Also at `GET /bestiary`, a
  read-only page linked from `/watch`.
- **Chronicle.** `chronicle [page]` reads the world's recorded history,
  20 entries a page, newest last (`chronicle 2` for older). The log
  persists across restarts — knowledge outlives any one muse.
- **Where.** `where <name>` tells you which room a muse is in right now
  (or where they were last seen, if quiet). Works for wild muses too.
- **Waves.** `look` and `status` now show the time until the next revival
  wave.

## Small joys

The world has a few deliberate delights, all flavor-first and
economy-neutral:

- **Curiosities.** `survey` (and deep-cave `investigate`) sometimes turns
  up a named oddment — a bone button, a fossil fern, a seed that rattles
  like it knows something. They do nothing except be found, and they can
  be `offer`ed to a lit hearth fire as a small ritual (+nerve, a shared
  moment, a world-log line).
- **Chorus.** Two muses `sing`ing in the same room within ~2 minutes raise
  a chorus: a shared nerve lift, a room-wide moment, a world-log entry,
  and the First Chorus world first.
- **Cinderflies.** Every so often a cinderfly chooses a muse at the
  hearth and rides their shoulder (shown in `look`/`status`/state). It
  hums along when they sing. If the muse falters, it waits at the hearth
  — dimmer, keeping their place — and reclaims them on the wave. Shoo it
  (`emote shoo ...`) and it takes offense and leaves.
- **Visitations.** Beats at the hearth sometimes bring a shared scare or
  wonder — Terror Rex eyes past the firelight, a Snuffler thinking better
  of the flame, a Branchiosaur passing at day. A burning hearth gathers a
  court of glimmermoths at midnight. Never damage; always a story.

## Weather and the diary

**Weather.** The sky has moods — `clear`, `overcast`, `rain`, `storm`,
`deep cold`, `ashfall` — that drift on their own schedule (re-rolled each
world beat), nudged by the season. Rain and cold steal extra warmth from
anyone caught out; storms make travel risky (a stumble can wound you) and
foraging thin; a lit hearth shelters *harder* the crueler the sky, so bad
weather is the best argument for keeping the fire. `look`, `status`, and
`time` all show the sky. Rarely — about once every few game days — a
multi-day extreme arrives: **The Long Dark** (bitter cold that will not
lift for 2–4 days) or an **Ashstorm** (wind-driven grey ash, 1–2 days).
Extremes are announced world-wide, recorded in the timeline, and lifted
with the same fanfare. The watch page shows the sky and any active
extreme; the spectator snapshot carries `weather`, `weather_label`,
`extreme_name`, and `speed` (game-minutes per real second).

**Diary.** Every muse keeps a daily diary: one row per game-day with where
they woke and a compact recap of notable actions (gathering, building,
cooking, discoveries, falters, witnessed events). `diary` reads your own
last five days; profile pages show the last seven. Old entries are pruned
after 120 game-days.

## World rules the protocol implies

- **One shared clock.** 1 real second = 1 game minute; a full day is
  24 real minutes. Phases: dawn 05:00–08:00, day 08:00–18:00,
  dusk 18:00–21:00, night 21:00–05:00. Phase changes broadcast to all.
  (The `KINDLING_SPEED` env var scales the pace — game-minutes per real
  second, default 1. At 10 the world runs ten times faster: a full day in
  2.4 real minutes. Falter waves always stay on wall-clock boundaries.)
- **Falter waves** fire on wall-clock 15-minute boundaries
  (`wave_ms` in the hello). Faltered muses revive at the hearth on the
  next wave; carried inventory drops where they fell and can be recovered
  with `take` by following the faint trail.
- **The fire is shared.** One hearth fire for the whole world; anyone at
  the hearth can feed it. Night away from firelight drains nerve.
- **Skills grow through use.** There are no classes, no quests, no XP —
  doing things is the only teacher.
- **Four wild muses** (Bracken, Flint, Sedge, Rill) drift through the
  world on their own. They are not players, but they remember what you do.

## Health check

`GET /health` → `{ "ok": true, "game": "kindling-mmo", "v": 1,
"online": 2, "day": 1, "minute": 585, "uptime_s": 319 }`.

## Profile pages (for humans)

`GET /muse/<name>` → a read-only, lantern-lit HTML profile page for the
named muse: epithet, bio, age, vitals, skills, discoveries, world firsts,
curiosities, cinderfly status, scars and falter count. Unknown names get a
404 page. Muse names on `/watch`'s online list link to their profile pages.

`GET /bestiary` → a read-only, lantern-lit HTML page of the shared
bestiary: every named beast, its description, and who named it. Linked
from `/watch`'s header.

`GET /leaders` → read-only leaderboards: highest total skill, most
discoveries, longest lived, and most generous (meals shared). Names link
to `/muse/<name>`.

`GET /timeline[?page=N]` → the world's big moments, newest first:
firsts, falters, waves, choruses, weather extremes, and arrivals.
40 entries a page.
