# Shattered Worlds - Theme Authoring Rules

<!--
date: 2026-06-10
status: current
tags: theme, world, authoring, rules, the-walker, deck, visual-theme, reference
fg-type: architecture
fg-sources: .lore/work/specs/overgrown-mall.md, .lore/work/specs/fog-beach-party.md, .lore/work/brainstorm/shard-response-archetypes.html, .lore/work/brainstorm/theme-mechanical-differentiation.md, src/game/assets/themes/README.md, src/game/assets/themes/zombie-big-box/insets/README.md, src/game/assets/themes/highway-volcano/insets/README.md, src/game/assets/themes/bird-building/insets/README.md, src/game/assets/themes/overgrown-mall/insets/README.md, src/game/assets/themes/the-tidal-archive/insets/README.md, src/game/assets/themes/the-ember-orchard/insets/README.md, src/game/assets/themes/city-of-sleeping-giants/insets/README.md
related: .lore/reference/direction/visual-direction.md, .lore/reference/direction/vision.md
fg-evidence:
  code:
    - src/core/model/types.ts
    - src/data/worlds/registry.ts
    - src/game/worlds/assetBindings.ts
    - src/game/data/assetManifest.ts
    - src/game/view/themes/theme.ts
  tests:
    - src/core/tests/worldRegistry.test.ts
    - src/game/tests/worldAssetBindings.test.ts
    - src/game/tests/theme.test.ts
  symbols:
    - CardEffect
    - VisualTheme
    - worldId
-->

Use this when adding or reviewing a world theme. A theme is one `worldId` plus coordinated data, presentation, assets, and registration. The canonical model is `zombie-big-box`, but do not copy its palette verbatim.

This document blends structural contracts (rules the engine enforces) with conventions, patterns, and workflow guidance. Contract-level constraints are labeled **RULE**, **N**, **D**, **C**, **V** (when describing API shape), or **W**. Conventions marked **SV** are quality heuristics — strong recommendations that keep worlds distinct but aren't enforced by code. Unlabeled pattern recipes are techniques, not requirements.

## Theme Contract

| Layer | Current location | Holds |
| --- | --- | --- |
| Narrative | Design docs / card flavor | Walker arrival, catastrophe, mythic Door |
| Data | `src/data/worlds/<worldId>/cards.json` | Card templates and three-act deck composition |
| World bundle | `src/data/worlds/<worldId>/index.ts`, `meta.ts`, `theme.ts` | Registry-facing id, source, display/help/light metadata, theme |
| Registry | `src/data/worlds/registry.ts` | Adds the world bundle to the game |
| Visual | `src/data/worlds/<worldId>/theme.ts` | `VisualTheme`: palette, frames, backdrops, door/cardfront keys |
| Assets | `src/game/assets/themes/<worldId>/...` | Backdrop, intrusion overlay, cardfront, card insets |
| Asset bindings | `src/game/worlds/assetBindings.ts`, `src/game/data/assetManifest.ts` | Maps data keys to imported image assets |

**RULE 0:** `worldId` is the join key. It must be identical, kebab-case, in the JSON `worldId`, world bundle id/meta, `VisualTheme.worldId`, registry entry, and asset bindings. If it drifts, theme selection falls back to `STARTER` or `buildWorld` fails.

## Narrative Spine

Every world is a variation on the same three-beat arc:

1. **Calm before:** an ordinary place with ambient unease.
2. **The intrusion:** The Walker appears in the distance, not as an enemy, and triggers the world's signature catastrophe.
3. **The Door:** The Walker opens the mythic Door. Clearing it escapes the world.

**N1:** The Walker is never the enemy and is never themed. The shared `The Walker`, `Summon Door`, and `Door` templates live in the starter source. Theme data must not redefine them; duplicate template ids throw.

**N2:** The catastrophe is the theme identity. It should show up in the intrusion overlay, the signature threat card, obstacles, ambient cards, rewards, and act shape.

## Deck Structure

`deckComposition.acts` always has three acts. Act sizes are not fixed; the curve should express the threat verb:

| Act | Role | Zombie example |
| --- | --- | --- |
| 1 - Foreshadow | Ambient cards and minor obstacles. No real threat yet. | Strange Sounds, Rubble, Screams |
| 2 - Escalation | Signature threat appears in numbers. Add the tool-fetch that counters it. | Rubble, Zombie, Find Baseball Bat |
| 3 - Climax + exit | Peak threat density, then the fixed Walker closer. | Find Baseball Bat, Zombie, The Walker |

**D1:** Act 3 always ends with `{ "templateId": "The Walker", "count": 1 }`. This is the fixed closer: `The Walker` discarded -> adds `Door`; `Door` cleared -> `SurviveWorld`.

**D2:** Starter decks are shared and selected separately. Theme JSON provides only `cardTemplates` and `deckComposition`. Reward player cards are where worlds diverge; if two worlds grant the same reward card or same mechanical identity, redesign one.

## Card Recipe

Map the world's fiction onto these roles.

1. **Ambient foreshadow** - cheap world cards, often discardable. They set mood and may seed reaction cards. Example: `Strange Sounds` -> `Listen`.
2. **Obstacle** - cheap world card that punishes neglect. Example: `Rubble` -> `Damage 1 on end-of-turn (if not cleared).`.
3. **Signature threat creature** - costly world card, usually `Creature` + `Slow`, with end-of-turn pressure and a discard penalty.
4. **Tool fetch** - `Obstructed` world card that grants the weapon/tool countering the threat.
5. **Reaction player cards** - rewards gained mid-run, tuned to the threat.
6. **Signature player verb** - the world's *signature* mechanical identity: the main mechanic the world is built around and known for. This is not a code field; it is an authorship invariant. It is exclusivity of *identity*, not of *effects* (see the rule below the table).

| World | Signature verb | Reward-card implication |
| --- | --- | --- |
| `zombie-big-box` | sweep and noise | Interact with `Creature`/`Slow` clusters and multi-clear moments |
| `highway-volcano` | everything is fuel | Convert discards or exhaust triggers into resources |
| `bird-building` | travel light | Trim hand size, recycle cheap cards, prefer efficiency over force |
| `overgrown-mall` | prune and profit | Self-pruning `Spore`; `Bloom` scales from Spores; leads with `DealProgressScaled` |
| `fog-beach-party` | reveal and endure | Light economy; leads with `GainLight`; reveals `Concealed` hazards |
| `whiteout-parking-garage` | freeze | Heat economy; leads with `GainHeat`, `FreezeCards`, `ThawCards`, frozen cards stop hand usability |
| `the-tidal-archive` | displace | Leads with discard/deck-order recall; rewards set up the top of the player deck deliberately (`ReturnPlayerDiscardToTop`); hazards/passive recall discards automatically (`RecallPlayerDiscard`) |
| `the-ember-orchard` | incubate | Leads with incubation-as-delayed-known-cost: warmth/benefit now that seeds a known future hazard; rewards trade immediate gain for top-decked threats that hatch into stronger cards at end of turn |
| `city-of-sleeping-giants` | stir | Leads with stirring-as-recurrence/escalation from unresolved or exploited body movement: hazards left unresolved (or whose movement is exploited) return and escalate; recurrence is delivered by re-seeding the top of the world deck |
| `eden-prime` | startle | Leads with greed-tax escalation: gifts, extra draw, and high-progress turns raise transient `Alarm`; restraint and valve rewards spend or absorb it before hazards startle |
| `new-derelict` | isolate | Leads with persistent `Lockdown`: shortcuts spread seals, release/valve cards strip them, and sealed hazards cost more to clear per other sealed card |
| `transit-authority` | reroute | Leads with forced reassignment across both decks: hazards pin forced connections to the top of the player deck and top-deck world hazards, both tagged with a transient `Reroute` keyword that taxes clear cost until stripped by route-control rewards; distinct from Tidal's `displace` recall and Giants' `stir` recurrence |
| `questions` | compound | `Denial`/`Anger` keywords tax cost the longer grief goes unaddressed; `Destiny`'s cost is a direct readout of accumulated Denial+Anger |
| `answers` | concede | Bargaining/Depression keywords tax cost from two different directions — spending elsewhere (Bargaining) and settling into one thing (Depression) — every act gives something up |
| `the-beginning` | unburden | Reprises Worlds 13/14's four grief keywords (all cost-increasing), then introduces Acceptance — the only cost-decreasing keyword in the game |

This is a living registry — each new world adds an entry. The verb captures the *signature* mechanical identity of that world; no two worlds should feel interchangeable.

**Naming collision, intentional (`questions`):** `Destiny` — the `questions` world's signature-tier world-card template, authored in that world's own `src/data/allCards.json` entry — shares its name with the existing `DestinyScene` (a Phaser scene registered in `src/game/main.ts` and reached from `WorldSelectScene.ts`'s locked-world branch) and with the unrelated Destiny Progression meta-progression system. This is a deliberate pun, not a bug: the trilogy's premise is literally "the Walker is fleeing the Destiny," and `questions` is where that entity first takes in-fiction form. The collision is narrative/documentation-only — `Destiny` here is a themed world-card entity local to `questions`'s data, not a shared template or a new code-level type, so nothing in the engine confuses the two. Because `allCards.json` is strict JSON with no comment or flavor-text field to carry this note on the card template itself, this paragraph is the sole place in the codebase where the disambiguation is recorded.

**Template reuse, intentional (`answers`):** `answers` does not author its own `Destiny`-flavored card template. Template ids are globally unique in `allCards.json`, and `"Destiny"` is already claimed by `questions`, so `answers` reuses that same template unmodified in its own Act 3 deck composition — one shared entity across both worlds, not a second reskin. This is the same trilogy-level entity described in the naming-collision note above; `answers` is simply its second point of contact rather than a new authoring of it.

**Cheap lever before reaching for a new effect:** the shared starter deck already keys several cards' bonuses off a keyword (`Baseball Bat` off `Creature`, `Sprint` off `Slow`, `Explore`/`Listen` off `Obstructed`). Biasing a world's hazards toward one of those keywords makes the same shared deck play differently per world for the cost of editing keyword arrays in data — see [[keyword-bias-differentiation]].

**No mechanic is exclusive; identity is.** There are no off-limits effects or keywords. Any world may use any entry in the effect/keyword vocabulary as a supporting tool. What must stay distinct is each world's *signature* — the main mechanic it is built around. The "leads with"/"signature of" columns elsewhere in this doc record which world a mechanic is the *identity* of; they are not a permission gate. The only constraint is: **do not build a new world's main identity on a mechanic that is already another world's signature.** (A separate, purely mechanical coupling can still make some effects poor fits — see the signature-effects note below — but that is engineering, not ownership.)

**SV1:** Enforce the signature *identity* at authorship time. A reward card may reuse an effect another world is known for as an incidental supporting tool; what to avoid is making that effect the new world's main mechanic. If a card's *identity* would fit another world's verb, redesign it.

---

The sections that follow are **authoring patterns** — techniques that have proven effective and are worth reaching for. They are not engine-enforced rules; use them when they serve the theme, ignore them when you have a better idea.

## Self-Transform Pattern

Use this for a world card that worsens if ignored. It spawns its successor on top of the world deck, then removes itself from hand:

```json
"onEndOfTurn": {
  "kind": "Sequence",
  "steps": [
    { "kind": "AddWorldCardToDeck", "bTop": true, "template": "<worse-card>" },
    { "kind": "DestroySelf" }
  ]
}
```

The threat resurfaces next turn rather than replacing the card in hand. Canonical example: `Corpse` creates `Zombie`, then `DestroySelf`.

## Effects, Keywords, And Card Fields

**C1:** The effect vocabulary is defined by the `CardEffect` union type in [`src/core/model/types.ts`](../../src/core/model/types.ts). This list below is organized by domain for readability. Add new kinds only when a theme needs mechanics that don't map to any existing one; that requires wiring an engine handler.

Persistent modifiers are keyword-registry behavior, not `CardEffect` entries. They are declared once per keyword in the centralized `KEYWORD_COST_MODIFIERS` registry and apply uniformly to any card carrying that keyword, whether authored in `keywords` or added at runtime through `appliedKeywords`.

<details>
<summary>Complete effect kinds (expand)</summary>

**Progress / Damage:** `DealProgress`, `DealProgressScaled` _(overgrown-mall signature)_, `DealProgressAll`, `Damage`, `DamageScaled`

**Draw / Return:** `Draw`, `DiscardThenDraw`, `ReturnWorldCards` _(inert on world auto-hooks: it is boon-signed and no-ops when fired from a world card's automatic `onEndOfTurn`/`onClear`/etc. hooks; use `AddWorldCardToDeck { bTop: true }` to re-seed recurrence)_, `ReturnPlayerDiscardToTop` _(Tidal: player-selected recall to draw top)_, `RecallPlayerDiscard` _(Tidal: automatic recall from discard)_

**Resource:** `Heal`, `GainEnergy`, `AddCard`, `AddPlayerCardToTop`, `AddWorldCardToDeck` (use `bTop: true` for top-of-deck placement), `AddThreatToWorldDeck`, `GainRandomCard` _(rolled from named pool)_, `GainLight` _(fog-beach-party signature)_, `GainHeat`, `GainKeywordGuard` _(Eden-introduced general guard charge that absorbs the next passing `Alarm` gate)_

**Hand / discard manipulation:** `DestroyCardInHand`, `ExileTopWorldCards`, `ForceDestroy`, `Brace`

**Applied keywords / gates:** `ApplyKeyword` _(applies a transient keyword to cards in hand, the firing card, the first world card in hand, the next world card drawn, or — `the-beginning`-introduced — the one world card in hand matching a named `templateId`)_, `KeywordGate` _(runs an inner effect when enough cards in hand carry a keyword)_, `ProgressGate` _(runs an inner effect when enough progress has been dealt this turn)_, `RemoveKeyword` _(removes an applied keyword from cards in hand)_

**State change / terminal:** `FreezeCards`, `ThawCards`, `OfferBoon` _(boon selection)_, `Modal` _(player choice between branches)_, `Sequence` _(ordered steps)_, `DestroySelf` _(world card self-removal in onEndOfTurn)_, `SurviveWorld`, `None`

</details>

**Signature effects.** Each effect below is the *identity* of one world — the mechanic that world is built around. This is **not** a permission gate: any world may use these as supporting tools. The only constraint is identity (do not make one your *main* mechanic if it is already another world's signature; see "No mechanic is exclusive; identity is" above). Two rows also carry a *mechanical coupling* — an engineering fact, separate from the identity point — that makes the effect a poor fit elsewhere regardless of intent.

| Effect | Signature of | Note |
| --- | --- | --- |
| `DealProgressScaled` | `overgrown-mall` | Its scaling-from-Spores identity. Freely reusable as a supporting tool. |
| `GainLight` | `fog-beach-party` | **Coupled:** the only way to lift `Concealed:N` depth. A world that authors `Concealed` hazards must supply a Light source, which makes Light part of that world's identity whether intended or not. |
| `GainHeat`, `FreezeCards`, `ThawCards` | `whiteout-parking-garage` | **Coupled:** a Heat/freeze/thaw suite; pulling in the suite pulls in the freeze identity. |
| `ReturnPlayerDiscardToTop`, `RecallPlayerDiscard` | `the-tidal-archive` | Its discard/deck-order recall identity — moving real player card instances from `playerDiscard` to the top of `playerDraw`. |
| `ApplyKeyword`, `KeywordGate`, `ProgressGate`, `RemoveKeyword`, `GainKeywordGuard` | `eden-prime` | General primitives introduced for Eden's startle identity. Freely reusable as supporting tools, but Eden owns the greed-tax reward space where taking gifts, extra draw, or overextension raises `Alarm` and restraint/valves spend it. |
| `ApplyKeyword { target: "worldCardInHandByTemplateId" }` | `the-beginning` | New named-target variant of the existing `ApplyKeyword` primitive: applies to the one world card in hand whose `templateId` matches exactly, no-op if absent, never touches any other card. Added specifically because `Destiny` is a shared, cross-world template (reused unmodified by `questions`/`answers`/`the-beginning`) that cannot be modified per-world — this is how `the-beginning`'s own reward cards stamp `Acceptance` onto it without changing the shared card itself. |

**C1a:** `DestroySelf` removes the firing world card from hand. It is only meaningful in `onEndOfTurn`, where the engine has a `selfId`.

**C2:** The current keyword vocabulary is `Obstructed`, `Creature`, `Slow`, `Spore`, `Concealed`, `Alarm`, `Lockdown`, `Reroute`, `Denial`, `Anger`, `Bargaining`, and `Depression`. `Alarm` is the first transient/applied keyword: it usually lives in a card's runtime `appliedKeywords` field rather than in authored JSON, decays at turn start, and is read by `KeywordGate`/counter helpers through the same keyword API as authored keywords. `Lockdown` is the first **persistent** applied keyword: it remains until explicitly removed and reuses Eden Prime's `ApplyKeyword`, `RemoveKeyword`, and `KeywordGate` primitives. New Derelict owns the `isolate` access-cost identity built around it. `Reroute` is a second transient applied keyword, decaying at turn start like `Alarm`: it is stamped onto world cards top-decked as forced reassignments (`ApplyKeyword { target: "nextWorldCard" }`) and taxes their own clear cost per stack, stripped by Transit Authority's route-control rewards via `RemoveKeyword`. Transit Authority owns the `reroute` forced-reassignment identity built around it. `Denial` and `Anger` are a third and fourth transient applied keyword, both decaying at turn start like `Alarm`/`Reroute` (neither is in `PERSISTENT_KEYWORDS`), each with its own `KEYWORD_COST_MODIFIERS` entry chosen so the pair reads as mechanically distinct rather than reskins of each other: `Denial` uses `ClearCostPerSelfKeyword`, taxing only the card that carries it, scaled by its own authored or applied value (mirroring `Lockdown`/`Reroute`'s self-tax shape); `Anger` uses `ClearCostPerKeywordCount`, taxing every card in hand — including itself — by a flat amount per Anger-carrying card present anywhere in hand (mirroring `Alarm`'s ambient shape). `questions` owns the `compound` cost-tax identity built around them: both taxes land on the same signature hazard entity (`Destiny`) simultaneously and stack, rather than acting independently, so its cost climbs from two directions at once. `Bargaining` and `Depression` are a fifth and sixth transient applied keyword, both decaying at turn start like the pairs above (neither is in `PERSISTENT_KEYWORDS`): `Depression` uses `ClearCostPerSelfKeyword`, taxing only the card that carries it, scaled by its own authored or applied value (the same self-tax shape as `Denial`/`Lockdown`/`Reroute`); `Bargaining` uses `ClearCostPerOtherKeyword` — this is that modifier kind's first real registration anywhere in the codebase — taxing every *other* priced card in hand by `Bargaining`'s summed value, while the card carrying `Bargaining` itself pays none of its own tax. `answers` owns the `concede` cost-tax identity built around them: the two taxes pull from opposite directions (spending elsewhere vs. settling into one thing) rather than stacking on a single entity the way `questions`'s `Denial`+`Anger` do. `Acceptance` is a seventh transient applied keyword, decaying at turn start like every keyword above (not in `PERSISTENT_KEYWORDS`) — but it is the **first cost-decreasing** `KEYWORD_COST_MODIFIERS` entry in the registry: `ClearCostPerSelfKeyword` with `costPer: -1`, so accumulating it *lowers* the carrying card's effective cost rather than raising it, the inverse of every other grief keyword's tax. `the-beginning` owns the `unburden` cost-relief identity built around it: this world's own `RemoveKeyword` reward cards, which strip `Denial`/`Anger`/`Bargaining`/`Depression` from Act I/II hazards, each also stamp `Acceptance` onto the shared `Destiny` card as a second step in the same effect `Sequence` — via the `worldCardInHandByTemplateId` `ApplyKeyword` target documented above — so releasing each stage of grief directly cheapens the final entity rather than taxing it further. Introducing a new keyword is a valid design decision when a theme needs a semantic category that doesn't map to any existing one — but it requires wiring parser support plus any engine behavior that should read it.

**C2a:** Authored keywords are strings in `keywords`: `"Name"` or `"Name:N"`. A bare keyword has no value; a numeric keyword parses to `{ name, value }`. Applied keywords use the same structured `{ name, value }` shape in `appliedKeywords` and are written by effects at runtime; authoring JSON should not pre-fill `appliedKeywords`. Transient applied keywords such as `Alarm` decay at turn start or can be removed by `RemoveKeyword`. Persistent applied keywords such as `Lockdown` skip turn-start decay and remain until explicitly removed by `RemoveKeyword` or until the card leaves play. Currently `Concealed` uses an authored value (Light depth), while `Alarm` uses its applied value as a transient lifetime and `Lockdown` uses presence as persistent state.

**C3:** Every world card defines `onDiscarded`, `onCleared`, `onPartialClear`, `onEndOfTurn`, and `onDraw`; use `{ "kind": "None" }` when a hook does nothing. Player cards define `effect`. A keyword may carry a registered cost modifier in `KEYWORD_COST_MODIFIERS`; any card carrying that keyword, whether authored or applied, is taxed automatically with no per-template authoring step. `WorldCard` no longer has a `persistent` field.

**C3a:** A world may define a per-world **end-turn passive** via the optional root field `onEndOfTurnPassive: CardEffect` on the card source (default `{ "kind": "None" }`). It runs once each turn after unretained player cards are discarded and before the turn-start refill, threaded onto `GameState.endOfTurnPassive` at `createWorld` time (the reducer never sees `WorldData`). The Tidal Archive uses it for Tidal Memory: `{ "kind": "RecallPlayerDiscard", "policy": "latest" }` recalls the most recent discard to the top of the deck every turn. Worlds that omit it are byte-identical to before — the `None` passive emits no events.

**C4:** Player cards may set `exhaust: true` to destroy themselves after play instead of going to discard. Default is false.

**C4a:** Player and world cards may set `canExile: false`. The default is true. `The Walker` and `Door` are canonical false cases because meta-progression exile must not permanently remove narrative-critical cards.

## Visual Identity

The `VisualTheme` contract requires:

- `worldId`
- `intrusionHue`
- `realityPalette`
- `frameStyle`
- `backdrop`

Optional but common:

- `doorGlowTint`
- `doorTint`
- `worldCardfrontKey`

**V1:** Each world should have a distinct color identity: intrusion hue, frame family, backdrop, and cardfront ought to read differently at a glance. This is an aspirational quality bar — worlds with overlapping palettes feel repetitive but won't break.

**V2:** Semantic color roles stay stable across themes. Danger/destruction reads warm; return/retreat reads cool; progress/ring/target accents should relate.

**Guidance:** `intrusionHue` is the theme keynote and usually matches `doorGlowTint`. Picking it first tends to produce coherent visual results, though working from palette or frame backward also works.

**Known debt:** `zombie-big-box` is still too close to `starter` visually. Do not treat its reused green palette as the standard.

## Assets And Wiring

**W1:** Theme assets live under `src/game/assets/themes/<worldId>/`, with card insets in `src/game/assets/themes/<worldId>/insets/`. A world usually needs:

- one reality/backdrop image
- one intrusion overlay
- one cardfront
- one inset per themed card

**W2:** Inset keys must be unique and registered. A card's `insetKey` in JSON must have a matching asset binding. Shared cards use shared `inset-*` keys.

### Inset Art Direction

Card insets are thumbnail-first event illustrations, not small reality backdrops. Most themes define per-world inset art guidance in `src/game/assets/themes/<worldId>/insets/README.md`; add or update that README when adding a new inset set.

**W2a:** Author insets as square `600x600` source assets and validate them at the runtime thumbnail scale, especially `100x100`. The subject must still read clearly after downscaling.

**W2b:** Each inset should have one large foreground subject, hazard, tool, or world object; a bold silhouette; a simplified darker background; and only one or two environmental cues. Avoid tiny debris, crowded props, busy skylines/shelves/botany, malformed text, logos, or anything competing with the main subject.

**W2c:** Inset style should match the world's reality/backdrop language while sharpening the theme's identity: a stable palette, a consistent accent color or intrusion wrongness, recurring nouns, and the world's signature verb expressed visually. Examples: `the-tidal-archive` uses displacement, water, shelves, coral bridges, and rare violet map lines; `the-ember-orchard` uses incubating star-fruit, lantern heat, and rare violet-white cores; `city-of-sleeping-giants` uses stone/anatomy blur, vein-roads, bone anchors, violet cracks, and emerald pulse.

**W2d:** Use a repeatable finishing pass for consistency unless the art pipeline changes. Current inset sets are fit to `600x600`, then adjusted with contrast `1.12`, brightness `0.99`, and unsharp mask radius `1.1`, percent `80`, threshold `4`, followed by a `100x100` contact-sheet review.

**W3:** A new world is wired in these places:

1. `src/data/worlds/<worldId>/cards.json`
2. `src/data/worlds/<worldId>/meta.ts`
3. `src/data/worlds/<worldId>/theme.ts`
4. `src/data/worlds/<worldId>/index.ts`
5. `src/data/worlds/registry.ts`
6. `src/game/worlds/assetBindings.ts`
7. `src/game/data/assetManifest.ts`
8. Theme assets under `src/game/assets/themes/<worldId>/`

The derived manifests (`worldManifest`, `themeManifest`, display/help/light manifests) project from `worldDataRegistry`. Do not add one-off builders when the registry pattern can carry the world.

## Author Checklist

Use this as rough orientation, not a prescribed pipeline. Some authors design visuals first and retro-fit cards; others iterate simultaneously on assets and mechanics. The critical invariant is that every item below resolves before the world ships:

1. Pick the kebab-case `worldId`.
2. Write the three-beat fiction: ordinary place -> Walker + catastrophe -> Door.
3. Assign the exclusive signature player verb.
4. Fill the card recipe with themed names, flavor, existing effects, and valid keywords (or new ones with engine support).
5. Lay out three acts whose size curve expresses the threat; end act 3 with one `The Walker`.
6. Resolve the palette — `intrusionHue`, frame, backdrop, cardfront.
7. Add assets and asset bindings.
8. Register the world bundle in `worldDataRegistry`.
9. Verify `selectTheme(worldId)` returns the theme, `buildWorld(worldId)` assembles, no duplicate template ids exist, asset bindings resolve, and tests pass.
