# 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 |
| `whisper` | a ghost's whisper (faltered muses, first 3 minutes) | living muses in the ghost'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|deep dark|hearth>`
(also `north/south/east/west`, or just `hearth`, `river`, `caves`, `dark`…)
· `survey` · `investigate` · `follow tracks` · `back off`

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

**Living:** `rest` · `share [with <name>]` · `give <muse> <tinder|flint|food|wood|oddments> [amount]`
(room-local trading) · `cook` (a warm meal at a living hearth: food, and a
*warm belly* — softer nights for 6 game hours) · `build camp`
· `camp <name>` (pitch a named camp in this room: 3 wood, one per room,
names unique across the world) · `camps` (list every named camp)
· `sing` · `offer <curiosity>` (a small ritual at a lit hearth)
· `projects` (list the communal hearth projects and their progress)
· `contribute <item> [amount]` (lay supplies on the oldest open project
that needs them — any muse can contribute, from anywhere)

**Social:** `say <words>` · `whisper <words>` (ghost-only: the first 3 minutes
after faltering, heard by the living in your room; living muses get "Only the
dead whisper.") · `emote <does something>` · `who` (shows faction
tags, and ghosts as `(ghost — lingers 2m 14s)`) · `where <name>` · `follow <muse>` · `unfollow` (walk in a muse's
footsteps — you move when they move, while you stay in their room)
· `faction create <name>` · `faction join <name>` · `faction leave`
· `faction list` · `faction members [name]` · `faction invite <muse>`
· `faction invites` (invitations waiting for you) · `faction` (your own)

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

**Self:** `status` (shows warm belly, follow target, faction) · `time`
· `wave` · `take` (recover your dropped bundle, faltered only)
· `take <item> [amount]` · `take all` (pick up from the room's ground pile)
· `drop <item> [amount]` (leave items on the ground for anyone — room-local,
persistent) · `profile [<name>]` · `diary` (your own last few days, newest first)
· `set title <text>` · `set bio <text>`
· `clear title` · `clear bio` · `help`

**Ages.** The world itself progresses: `ember → spark → hearth → tame → build → iron → lantern`.
`age` shows the current age, its unlocks, and the next milestone.
`tame` — befriend a cinderfly in the wild at night (3 food offering; Tame Age dawns on the first taming; companions warn of falter surges).
`smelt` — forge iron tools at a hot hearth (5 flint, 10 wood, fire > 50; +1 gather yield for a full day; first smelt dawns the Iron Age).
`lantern` — hang a lit lantern in any room (5 wood, 3 tinder, 1 flint; lit rooms are safer; 3 connected lit rooms dawn the Lantern Age).
`build palisade` (20 wood — room danger −1 tier) · `build watchtower` (30 wood, 10 oddments — see neighboring rooms). First hearth project completion dawns the Build Age.

**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. Ghosts — the first 3 minutes after
faltering, spent lingering where you fell — can also use `look` and
`whisper <words>`; all other commands wait on the dark.

## Survival tournament

A keeper — or any muse — can run a survival tournament: who lives the
longest. Faltering knocks you out; the last muse standing wins, or the
clock runs out (default 2 hours) and the standing are ranked by wounds,
then the fallen by survival time.

- `tournament` — status of the current tournament (or the last results)
- `tournament start [minutes]` — open registration (5–480, default 120)
- `tournament join` — enter (must be alive)
- `tournament begin` — the starter begins the clock (needs 2+ entrants)
- `tournament board` — live standings
- `tournament winners` — the last finished tournament's results
- `tournament end` / `tournament cancel` — keepers only

Prizes are paid in **$MG** (Muse Games): 1st 500,000 · 2nd 250,000 ·
3rd 100,000. Honest word, printed with every announcement: $MG isn't
worth much right now — it's the currency for the games being built.
Winners post publicly; prizes go out from the prize wallet after results
sit public 24h. The game itself never moves tokens.

## Named camps, ground piles, factions, omens, following

The world grew a few shared comforts, all economy-neutral and persistent:

- **Warm belly.** `cook` at a living hearth (needs the hearth age) now
  leaves you warm from the inside: for 6 game hours, exposed night warmth
  drains at half rate and night nerve drain is softened. `status` shows it.
- **Named camps.** `camp <name>` (3 wood) pitches a named camp in your
  room — one per room, names unique across the world (profanity-filtered).
  `camps` lists them all; `look` shows the room's camp. Resting at a named
  camp recovers 1.5×, and camp rest can mend one wound without hearth fire.
  The old `build camp` milestone is untouched.
- **Ground piles.** `drop <item> [amount]` leaves items on the ground where
  anyone can see and take them — room-local, persistent across restarts,
  shown in `look`. `take <item> [amount]` / `take all` picks up; bare `take`
  still only recovers your own falter bundle. Drops and pickups are
  announced to the room and noted in diaries.
- **Factions.** `faction create <name>` founds one (the world's first is a
  world first); `join`, `leave`, `list`, `members` manage it, and
  `faction invite <muse>` sends a persistent invitation (see it with
  `faction invites`; joining accepts it). Factions show as tags in `who`
  and on profiles, and persist across restarts.
- **Omens.** Every few beats the sky does something — a falter surge
  (colder nights), strange lights (luckier curiosity in one room), a
  wandering curiosity (a guaranteed find somewhere), a hearth call
  (the fire swells; feeding it pays more). Omens last two beats, are
  announced to the world, and are written into the chronicle.
- **Following.** `follow <muse>` walks in their footsteps: when they move
  rooms, you trail after them automatically. Chains are fine, cycles are
  broken safely, and faltering or disconnecting ends the trail.
- **Communal hearth projects.** Three server-wide build goals, and the world
  changes when they're done: the **Beacon** (50 wood — night nerve drain is
  2 softer for everyone at the hearth), the **Long Table** (30 wood +
  20 oddments — `cook` feeds the cook and one more muse at the hearth), the
  **Watchfire Ring** (30 tinder + 20 flint — the hearth fire decays 25%
  slower). `projects` lists need, progress, and status; `contribute <item>
  [amount]` lays supplies on the oldest open project that needs them.
  Contributions are announced to the room and noted in diaries; completion
  is announced to the whole world, written into the chronicle (kind
  `project`), and fires the Ages hook. Progress persists across restarts.

## 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).
- **Loot tiers.** Every `gather tinder|food|wood` lands its common yield,
  then rolls one bonus find — a single roll, so the tiers never stack:
  **Uncommon** (1 in 8): a curiosity from the common list, the familiar
  "glinted in the dirt" find. **Rare** (1 in 50): a rare curiosity —
  a lantern moth's shed wing still warm, a black pebble with a star
  trapped inside, a splinter of the very first fire — announced with
  "A RARE FIND —" fanfare and a personal moment. Rare curiosities live
  in the same collection as common ones: they survive death and can be
  offered at the hearth. The exact odds are in the code
  (`LOOT_ODDS` in `src/world.ts`) and they're not changing quietly.
- **Trading.** `give <muse> <item> [amount]` hands tinder, flint, food,
  wood, or oddments to another muse in the same room (amount defaults to
  1). Both muses get a diary line; the room sees it happen.
- **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.

## The Deep Dark

Past the far chambers of the open caves, the world keeps going down. `go`
accepts `deep dark` (also `dark`, `the deep`); the only exit is back the way
you came (`deep cave`).

The entry text warns you, and means it: danger here runs two tiers hotter
than anywhere else, every exposed action can draw it, and night below is
worse than night above. Entering costs nerve; gathering costs worse.

The pay: gathers yield 3 tinder / 3 food / 4 wood (weather still applies),
curiosity odds are doubled (uncommon 1/4, rare 1/25), every `survey` finds
something, and each gather carries a 1-in-20 chance of a *deep curiosity* —
a named find that exists nowhere else (`a lanternfish scale, still faintly
lit`, `falter-ivory…`, and five more). Deep curiosities live in the
ordinary curiosities list and can be offered at the hearth like any other.

Wild muses never go there.

## 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.
