# Kingdom module

> defineKingdomGame builds a story-led town builder with joystick sorties from one config, with buildings on timers, producers, crops, debris, a hero class, a squad, enemy camps, rewards and chapters of dialogue.

`defineKingdomGame(config)` makes a story-led town builder. The player grows a village (buildings on timers, producers to collect from, crops to plant and harvest, debris to clear) and rides out on joystick sorties with a hero and a squad to clear enemy camps. A story of chapters runs through both: dialogue with portraits, choices, objectives the town answers by itself, rewards and unlocks. Use it for "a kingdom I build and defend", "a farm with a story" or "a village and raids".

```tsx
import { defineKingdomGame } from '@/engine/modules/kingdom';
import type { KingdomConfig } from '@/engine/modules/kingdom';
```

Source: `templates/crescent-game/src/engine/modules/kingdom/`. It uses the engine's [story kit](https://docs.moonspell.app/engine/story.md), the floating `Joystick` ([Input](https://docs.moonspell.app/engine/input.md)), TypeGPU grass ([GPU effects](https://docs.moonspell.app/engine/gpu-effects.md)), generated or packaged models ([Models and scenery](https://docs.moonspell.app/engine/models-and-scenery.md)) and [saves](https://docs.moonspell.app/engine/saves.md).

## The loop

1. The title screen, then chapter 1 starts: `title`, `say` and `choice` beats wait for a tap.
2. A `scene: 'sortie'` beat starts a fight: the hero and the squad walk a foggy field with the joystick, camps wake as they come near, and a Victory sheet grants the mission's rewards.
3. Back in the village, `objective` beats wait for the town's own counts (harvest, build, collect, clear, hold, upgrade, win a sortie). A met objective moves the story on by itself.
4. `reward` beats grant items, `scene: 'unlock'` opens new buildings or crops, and the next chapter raises the stakes.
5. After the last beat the village keeps running: timers, producers, crops and sorties stay available.

## The smallest complete game

Every kingdom needs a theme accent, resources, buildings, units, hero classes, missions and a story. This is about as short as that gets:

```tsx
// src/game/index.tsx
import { defineKingdomGame } from '@/engine/modules/kingdom';

export default defineKingdomGame({
  id: 'small-holding',
  title: 'Small Holding',
  description: 'Drive the raiders off, then grow wheat and build a farm.',
  theme: { accent: '#27B3AD' },
  resources: [
    { id: 'grain', name: 'Grain', icon: 'leaf.fill', start: 40 },
    { id: 'wood', name: 'Wood', icon: 'tree.fill', start: 60 },
  ],
  buildings: [
    { id: 'hall', name: 'Town hall', shape: 'hall', footprint: [2, 2], cost: { wood: 200 }, buildSeconds: 60 },
    { id: 'farm', name: 'Farm', shape: 'farm', cost: { wood: 25 }, buildSeconds: 12, produces: { resource: 'grain', amount: 10, seconds: 25 } },
  ],
  crops: [{ id: 'wheat', name: 'Wheat', seconds: 20, yields: { resource: 'grain', amount: 15 } }],
  units: [{ id: 'raider', name: 'Raider', hp: 30, damage: 5, range: 1.4, cooldown: 1, speed: 3.9, color: '#B33A2E' }],
  classes: [{ id: 'knight', name: 'Knight', detail: 'Sword and shield.', hp: 150, damage: 18, range: 1.9, cooldown: 0.55, speed: 5.4, weapon: 'blade', color: '#2E4FA8' }],
  missions: [{ id: 'fields', name: 'The fields', camps: [{ x: 0, z: -8, units: ['raider', 'raider', 'raider'] }], rewards: { grain: 60, wood: 60 } }],
  town: {
    prebuilt: [{ building: 'hall', lot: [3, 1] }],
    plots: [[1, 6], [2, 6]],
  },
  story: {
    speakers: [{ id: 'elder', name: 'Elder Bryn', role: 'Village elder' }],
    chapters: [
      {
        id: 'raiders',
        title: 'Raiders',
        beats: [
          { kind: 'title', text: 'Chapter I', sub: 'Raiders' },
          { kind: 'say', speaker: 'elder', text: 'Raiders camp past the fields. Drag your thumb anywhere to move.' },
          { kind: 'scene', scene: 'sortie', args: { mission: 'fields' } },
          { kind: 'objective', id: 'sortie:fields', text: 'Drive off the raiders' },
          { kind: 'objective', id: 'harvest:wheat', text: 'Harvest wheat', target: 15, hint: 'Tap a plot to plant' },
          { kind: 'objective', id: 'build:farm', text: 'Build a farm', hint: 'Build, then tap a green lot' },
          { kind: 'say', speaker: 'elder', text: 'The village will eat this winter.' },
        ],
      },
    ],
  },
});
```

Until models and portraits are given, the module draws fallback shapes: a procedural building per `shape`, capsules with weapons for fighters, and SF Symbol icons for resources and portraits. `examples/oath-of-ashvale/index.tsx` is a complete game with two chapters, rigged characters, generated buildings, a sprite sheet of portraits and icons, and a full sound set.

## Controls

- **Village:** tap a lot, a building, a plot or debris to open its panel; drag to pan; pinch to zoom. The build button opens the build sheet.
- **Sortie:** the floating joystick appears under the thumb anywhere on the screen. The hero and the squad attack on their own. In a desktop browser, WASD or the arrows move.

## Config reference

`resolveKingdomConfig` fills defaults, clamps ranges and drops references that point nowhere (an unknown resource in a cost, an unknown unit in a camp), with a warning in development.

### Top level

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | URL-safe id. Also keys the save slot `kingdom:<id>`. |
| `title` | `string` | required | Shown on the title screen. |
| `description` | `string` | required | Shown on the title screen and the game's card. |
| `badge` | `string` | none | Card metadata. |
| `theme` | `KingdomTheme` | see below | Colours, sky and fonts. `accent` is required. |
| `resources` | `ResourceDef[]` | required | The currencies. |
| `buildings` | `BuildingDef[]` | required | What can be built. |
| `crops` | `CropDef[]` | `[]` | What can be planted on plots. |
| `units` | `UnitDef[]` | required | Fighters: allies and enemies. |
| `classes` | `HeroClassDef[]` | required (may be empty) | The hero's options. An empty list gives a default sword hero. |
| `missions` | `MissionDef[]` | required | The sorties. |
| `town` | `TownDef` | see below | Village size and layout. |
| `story` | `Story` | required | Speakers and chapters. |
| `art` | `{ sheet?: { source, cols, rows } }` | none | A sprite sheet of cut-outs (portraits, resource icons, crops). Cells are picked by `frame`. |
| `sounds` | `KingdomSounds` | none | Music and effects. Without it the game is silent. |
| `board` | `string` | `'kingdom-power'` | Leaderboard name for the kingdom's power. |
| `saveVersion` | `number` | `1` | The save slot's version. Bump it with a migration when an existing save's shape changes. |
| `migrations` | `Record<number, (old) => unknown>` | none | `migrations[n]` turns a version n-1 save into version n, passed to the save slot as is. See [Saves and backend](https://docs.moonspell.app/modules/kingdom.md#saves-and-backend). |
| `objectives` | `Record<string, (town: TownState) => number>` | none | The game's own counts for objective ids the module doesn't count, see [Story](https://docs.moonspell.app/modules/kingdom.md#story). |

### `theme`

| Field | Default | Meaning |
| --- | --- | --- |
| `accent` | required | Primary buttons, the quest badge, the hero ring. |
| `onAccent` | `'#FFFFFF'` | Text on accent buttons. |
| `panel` | `'#FFF8EE'` | Dialogue box, sheets and panels. |
| `text` | `'#2A2430'` | Text. |
| `dim` | `'#7A7189'` | Secondary text. |
| `ground` | `'#6FA84F'` | Field colour. |
| `grass` | `'#7FBA5A'` | The village's grass. |
| `path` | `'#C9B48A'` | Paths between lots. |
| `water` | `'#4FA3D9'` | The water row. |
| `sky` | `'day'` | An environment preset: `'day'`, `'sunset'`, `'night'`, `'overcast'`, `'neon'` or `'space'`. |
| `wall` | `'#E6D8BF'` | Fallback building walls. |
| `roof` | `'#8A4A2E'` | Fallback building roofs. |
| `trim` | `'#4A3A2E'` | Fallback building trim. |
| `danger` | `'#FF3B3B'` | Enemy camp rings. |
| `banner` | `'#E2A63A'` | Victory and chapter ribbons. |
| `fog` | `'#DCE9DF'` | Sortie fog and the canvas background. |
| `fonts` | Nunito black and bold | `{ display, ui? }` from `@/engine/ui/fonts`. `ui` defaults to `display` when only `display` is given. |

### `resources`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | Referenced by costs, yields and rewards. |
| `name` | `string` | required | Display name. |
| `icon` | `IconName` | required | SF Symbol name, for example `'leaf.fill'`. |
| `frame` | `number` | none | A cell of `art.sheet`, shown instead of the icon. |
| `color` | CSS colour | `theme.accent` | Its colour in the HUD. |
| `start` | `number` | `0` | What a new village holds. |
| `cap` | `number` | `0` (no cap) | Storage cap. |

### `buildings`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | Referenced by `build:` objectives, `prebuilt` and `unlock` scenes. |
| `name` | `string` | required | Display name. |
| `shape` | `'hall' \| 'house' \| 'farm' \| 'tower' \| 'workshop' \| 'camp' \| 'shrine' \| 'wall' \| 'well'` | required | The fallback shape drawn until a `model` is given. |
| `description` | `string` | `''` | One line on the build card. |
| `icon` | `IconName` | none | Build card icon. |
| `frame` | `number` | none | A cell of `art.sheet` for the build card. |
| `footprint` | `[number, number]` | `[1, 1]` | Lots wide and deep, 1 to 3 each. |
| `cost` | `Costs` | required | Level 1 price, per resource id. |
| `buildSeconds` | `number` | required | Level 1 build time, 0 (instant) to 6 hours. |
| `maxLevel` | `number` | `3` | Highest level, 1 to 10. |
| `upgradeScale` | `number` | `1.8` | Level n costs `cost` times `upgradeScale` to the power n minus 1, and takes as long. 1 to 4. `costAt(building, level)` computes it. |
| `produces` | `{ resource, amount, seconds }` | none | Makes `amount` times level of `resource` every `seconds` (3 s to a day), collected with a tap. |
| `power` | `number` | `10` | Kingdom power per level, the leaderboard score. |
| `troops` | `{ unit, count }` | none | `count` times level of that unit join the hero on every sortie (a barracks). |
| `colors` | `{ wall?, roof?, trim? }` | the theme's | Fallback shape colours. |
| `model` | `number` (a `require`d `.glb`) | none | A building model, placed in its lot at its own proportions. |
| `unlocked` | `boolean` | `true` | `false` hides it until a `scene: 'unlock'` beat opens it. |

### `crops`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | Referenced by `plant:` and `harvest:` objectives. |
| `name` | `string` | required | Display name. |
| `seconds` | `number` | required | Planting to harvest, 3 s to a day. |
| `yields` | `{ resource, amount }` | required | What a harvest gives. A crop yielding an unknown resource is dropped. |
| `cost` | `Costs` | free | Seed price. |
| `icon`, `frame` | `IconName`, `number` | none | Icon or sheet cell. |
| `stages` | `number[]` | none | Sheet cells shown as the crop grows, sprout to ripe. Without them the module draws growing stalks. |
| `color` | CSS colour | `'#9BD35A'` | Fallback crop colour. |
| `unlocked` | `boolean` | `true` | `false` hides it until a `scene: 'unlock'` beat opens it. |

### `units`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | Referenced by camps, `escort` and `troops`. |
| `name` | `string` | required | Display name. |
| `hp` | `number` | required | Hit points, at least 1. |
| `damage` | `number` | required | Damage per hit. |
| `range` | `number` | required | Attack reach in metres, 0.8 to 12. Melee about 1.4, spear 2, bow 6. |
| `cooldown` | `number` | required | Seconds between attacks, 0.2 to 10. |
| `speed` | `number` | required | Metres per second, 0.5 to 12. |
| `color` | CSS colour | required | Fallback capsule colour. |
| `height` | `number` | `1.4` | Height in metres of the capsule and the model. 1.9 to 2.2 reads as a brute. |
| `weapon` | `'blade' \| 'spear' \| 'bow'` | `'blade'` | Fallback weapon and attack style. |
| `model` | `number` | none | A rigged character `.glb`. |
| `clips` | `Record<string, number>` | none | Extra animation files by name. A file with several clips exposes them as `name.Clip`, for example `move.Running_A`. |
| `animations` | `{ idle?, run?, attack?, death? }` | `idle` and `run` | Which clip plays when. |
| `tint` | CSS colour | none | Multiplied into the model's colours, so one model serves several teams. |
| `held` | `{ right?, left?, rightBone?, leftBone?, scale? }` | none | Prop models attached to the rig's hand bones: a sword, a shield, a bow. |

### `classes`

The hero's options. Same fighting fields as a unit (`hp`, `damage`, `range`, `cooldown`, `speed` from 1 to 12, `weapon`, `color`, `model`, `clips`, `animations`, `tint`, `held`; `weapon` is required here), plus:

| Field | Type | Meaning |
| --- | --- | --- |
| `id` | `string` | Must match an option id of the story's `choice` with `id: 'class'`. |
| `name` | `string` | Display name. |
| `detail` | `string` | One line on the choice card. |
| `icon`, `portrait` | `IconName`, `Portrait` | Shown on the choice card. |

The first class is used until the player picks one. The pick is kept in the save.

### `missions`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `id` | `string` | required | Referenced by `scene: 'sortie'` and `sortie:` objectives. |
| `name` | `string` | required | Display name. |
| `size` | `number` | `30` | Half-size of the field in metres, 12 to 80. |
| `heroSpawn` | `{ x, z }` | `{ x: 0, z: size * 0.7 }` | Where the hero starts. |
| `camps` | `CampDef[]` | required | Enemy camps, see below. |
| `rewards` | `Costs` | required | Granted on victory. |
| `ground` | CSS colour | `theme.ground` | Field colour. |
| `fog` | `boolean` | `true` | Fog on the field. |
| `escort` | `string[]` | `[]` | Ally unit ids that follow the hero, on top of the barracks' troops. |

A camp is `{ x, z, units, radius?, banner? }`: a red ring at `(x, z)` with these unit ids in it. `radius` defaults to 3.5 (1.5 to 8). A camp sleeps until the hero or an ally comes within `radius + 2.5` m. `banner` is a label over the ring.

### `town`

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `size` | `[cols, rows]` | `[8, 10]` | Lots across and down, 5 to 14 each. Columns grow to the right of the screen, rows go down it. |
| `lot` | `number` | `4` | Metres per lot, 2.5 to 8. |
| `prebuilt` | `{ building, lot, level? }[]` | `[]` | Buildings standing at the start. Entries naming an unknown building are dropped. |
| `plots` | `[col, row][]` | `[]` | Lots that are crop plots. |
| `debris` | `{ kind, lot, seconds?, yields? }[]` | `[]` | Trees, rocks, rubble or brush to clear. `seconds` defaults to 5. |
| `paths` | `[col, row][][]` | `[]` | Paths drawn on the ground, as lot-to-lot polylines. |
| `water` | `{ row, width? }` | none | A river band across the village at that row, `width` rows wide (default 1). |
| `villagers` | `VillagerDef[]` | `[]` | Rigged models that walk a path back and forth: `{ model, clips?, walk?, idle?, tint?, height?, path?, speed? }`, walking at 1.2 m/s by default. |

### `story`

`speakers` (`{ id, name, role?, color?, portrait?, side? }`) and `chapters` (`{ id, title, beats }`), the engine's [story kit](https://docs.moonspell.app/engine/story.md) types. A `portrait` is a sheet cell (`{ source, cols, rows, frame }`) or `{ icon }`.

| Beat | Fields | Waits for |
| --- | --- | --- |
| `title` | `text`, `sub?` | a tap |
| `say` | `speaker`, `text` | a tap (the first tap finishes the typing). Skip jumps to the next choice, objective or scene. |
| `choice` | `id`, `title`, `options: { id, label, detail, icon?, portrait? }[]`, `confirm?` | the confirm button. `id: 'class'` sets the hero class. |
| `objective` | `id`, `text`, `target?` (default 1), `hint?` | the town's counts (below), or the game's own count in `objectives`, reaching `target` |
| `reward` | `title?`, `items: { resource, amount }[]` | the button; the items are granted then |
| `scene` | `scene`, `args?` | nothing, see below |

The `scene` beats the module performs:

| `scene` | `args` | Does |
| --- | --- | --- |
| `sortie` | `mission` | starts that mission (default: the first) |
| `unlock` | `building` or `crop` | unlocks it and shows a toast |
| `focus` | `col`, `row` | pans the village camera to that lot |
| `town` | none | returns to the village |

Objective ids the module counts by itself:

| Id | Counts |
| --- | --- |
| `build:<building>` | buildings of that kind standing |
| `upgrade:<building>:<n>` | 1 once one reaches level n |
| `plant:<crop>` | plantings |
| `harvest:<crop>` | units harvested |
| `collect:<resource>` | units collected from producers |
| `hold:<resource>` | the current amount in the wallet |
| `clear` | debris cleared |
| `sortie:<mission>` | sorties of that mission won |
| `class` | 1 once a hero class was picked |

Any other id needs the game's own count in `objectives`: a function of the village (`TownState`: `wallet`, `buildings`, `plots`, `stats`, `heroClass` and more) that returns the objective's progress. It is read twice a second like the built-in ones and compared with `target`. Without one, the objective never completes.

```tsx
objectives: {
  feast: (town) => (town.wallet.wheat >= 300 && town.buildings.some((b) => b.kind === 'hall' && b.level >= 2) ? 1 : 0),
  'big-village': (town) => town.buildings.filter((b) => b.level >= 1).length, // with target: 8
},
```

The function returns a total, not an increment. A count that throws or returns something other than a number is skipped. A count for a built-in id replaces the module's.

## Sounds

Every key of `sounds` is optional and takes a `require`d file:

| Key | Plays |
| --- | --- |
| `music` | the village theme, looping |
| `battle` | the sortie theme, crossfading in on every sortie |
| `tap` | taps on dialogue and buttons |
| `build` | a build or upgrade starting or finishing |
| `harvest` | a harvest |
| `collect` | collecting from a producer or a reward |
| `hit` | a hit landing |
| `bow` | a ranged swing (falls back to `hit`) |
| `whoosh` | a melee swing (falls back to `hit`) |
| `step` | the hero's footsteps on a sortie |
| `grunt` | the hero taking a hit |
| `groan` | an enemy going down |
| `horn` | a camp waking |
| `victory` | a sortie won |
| `defeat` | a sortie lost |

## What you can customise

| What | How |
| --- | --- |
| Economy and pacing | `resources`, building `cost`, `buildSeconds`, `upgradeScale`, `produces`, crop `seconds` and `yields`, mission `rewards`. Make every chapter's objectives affordable from what the chapter gives. |
| Fights | `units`, `classes`, camps per mission, `escort`, `troops`. |
| The village | `town` size, prebuilt buildings, plots, debris, paths, water, villagers. |
| The story | `story.speakers` and `story.chapters`, plus `objectives` for objective ids of your own. Keep lines under about 140 characters; the box types them out. |
| Look | `theme`, building `colors`, `model` on buildings and units, `art.sheet` and `frame` for icons and portraits. |
| Sound | `sounds`. |
| Rules | The town rules (`town.ts`) and the sortie simulation (`sortie.ts`) are pure TypeScript with tests, exported for tools and tests: `createTown`, `place`, `upgrade`, `tick`, `collect`, `plant`, `harvest`, `startClear`, `power`, `troopsOf`, `createSortie`, `stepSortie`, `objectiveValue`, `autoObjectives` and more. The scene pieces (`BuildingShapeMesh`, `FighterMesh`, `CampRing`, `PlotMesh`, `DebrisMesh`, `ReadyMarker`) are exported too. |

The kingdom's building blocks also work in any game on their own: the `Joystick`, the story kit's `DialogueBox`, `ChoiceCards` and `RewardSheet`, `Grass` and `LivelyGround`, `Water`, `SheetImage` and `Billboard`. See [Input](https://docs.moonspell.app/engine/input.md), [Story](https://docs.moonspell.app/engine/story.md), [GPU effects](https://docs.moonspell.app/engine/gpu-effects.md) and [Models and scenery](https://docs.moonspell.app/engine/models-and-scenery.md).

## Saves and backend

The village, wallet, timers, story position and hero class live in one save slot, `kingdom:<id>`. Timers are absolute timestamps, so time passes while the app is closed. With a backend the slot syncs to the player's other devices ([Saves sync](https://docs.moonspell.app/backend/saves-sync.md)).

Fields the module adds later reach old saves by themselves. When a game changes the shape of what an existing village keeps, bump `saveVersion` and add a migration: `migrations[n]` gets a version n-1 save (`{ town, story, startedAt, sessions }`) and returns version n. They run in order, each on a copy ([Saves](https://docs.moonspell.app/engine/saves.md)). `kingdomSaveSpec(config)` returns the slot's spec, for testing a migration with `defineSave` and `memoryStore()`.

```tsx
saveVersion: 2,
migrations: { 2: (old) => ({ ...old, town: old.town && { ...old.town, wallet: { ...old.town.wallet, stone: 0 } } }) },
```

With `GAME_BACKEND_URL` set, the kingdom's power is posted to the `board` leaderboard (default `kingdom-power`), and the title shows how many are playing. Power is the sum of each building's `power` times its level, plus 2 per debris cleared and 25 per sortie won. The backend scaffold's `BOARDS` does not list `kingdom-power`: add it, for example `"kingdom-power": { order: "desc", min: 0, max: 1_000_000 }`, or the score stays on the phone. See the [backend overview](https://docs.moonspell.app/backend/overview.md).

## Dev and testing

`npm test` runs `kingdom.test.ts`: config defaults, placing, building, upgrading, crops, debris, power, objectives (built-in and the game's own), the save slot and its migrations, and the sortie simulation.

In development builds the HUD shows a telemetry line (`hud-debug`) and an **AUTO** button (`btn-autopilot`). In the village AUTO answers dialogue, picks the first class, plants, harvests, collects, builds what the objective asks and clears debris; on a sortie it walks the hero into the nearest camp.

Other `testID`s include `title-screen`, `btn-play`, `chapter-title`, `quest-tracker`, `resource-bar`, `res-<id>`, `btn-build`, `build-sheet`, `build-<id>`, `building-panel`, `btn-upgrade`, `btn-collect`, `plot-panel`, `btn-plant-<id>`, `btn-harvest`, `debris-panel`, `btn-clear`, `btn-sortie`, `hud-enemies`, `hud-hp`, `victory-sheet`, `defeat-sheet`, `btn-retry`, `btn-pause`, `pause-sheet` and `hud-power`.

## Gotchas

- The story is required, and it is the only guide the player gets. A chapter with no objective or sortie leaves the player in the village with nothing to do but tap.
- The hero class choice must have `id: 'class'` and option ids that match `classes` ids, or the pick is ignored.
- Objectives read the town's running totals, so work done before the objective appears already counts. A `hold:` objective reads the wallet, so spending can undo it.
- Costs, yields and rewards that name an unknown resource are dropped silently in production.
- Bumping `saveVersion` without a migration for the new version only fills in missing fields; anything renamed or reshaped needs `migrations`. Adding a field to the save needs neither (see [Saves](https://docs.moonspell.app/engine/saves.md)).
- Not in the module: alliances, collectible heroes, player-versus-player, a shared world map and tower placement.
