# SIMULATION.md — The local game layer

The API hands out complete, seeded characters and items. The local game layer
is our own game on top of them: weapons, movement, combat, physics and
rendering in the browser. Any game can build its own the same way.

The local game layer lives in `three-isometric-engine/` and `components/`. It
reads API responses and never changes API math, generation, stored births or
`contentVersion`. Its math is in [ALGORITHMS.md](https://set.world/algorithms); section
numbers below (§12, §13.x, §14, §15) refer to that file.

## 1. The layers

```text
┌──────────────────────────────────────────────────────────────┐
│ LOCAL GAME LAYER (browser only; never sent to the API)       │
│  Rendering: Anima model and motion, armor cloth, portraits,  │
│    craft rock, labels, effects                               │
│  Rules: weapon mass, handling, range, contacts, stamina,     │
│    ragdolls, targeting, teams, initiative dungeon,           │
│    level/orb scaling                                         │
├──────────────────────────────────────────────────────────────┤
│ OPTIONAL COMBAT FALLBACK (API, §12)                          │
│  one attack's damage, or a window of initiative              │
├──────────────────────────────────────────────────────────────┤
│ CORE API (the product)                                       │
│  seeded generation, grades, rarity, orb economy, salvage,    │
│  drops, derived attributes, power rating, item budgets,      │
│  contentVersion, birth certificates                          │
└──────────────────────────────────────────────────────────────┘
```

| Layer | Supplies | Code and math | Over the network? |
| --- | --- | --- | --- |
| Core API | Births, enriched items, grades, prices, attributes, budgets, `seed`, `contentVersion` | `common/`, `pages/api/`, §1–§11 | Yes, one soul per request |
| Combat fallback | One attack or one initiative window | `common/basic-lightweight-combat.ts`, §12 | Only if a game calls it; our demos import its helpers |
| Local game layer | Weapons, range, contacts, resources, AI, physics, progression projection, models, motion, cloth, portraits | `three-isometric-engine/`, `components/`, §13–§15 | No; nothing is sent per hit |

Every demo follows the same rules:

- **Births stay untouched.** A demo keeps the full API response and stores
  changing state beside it. Traits and equipment are never applied twice.
  Only `/roll/character` stores anything, and it stores the original roll.
- **One request at a time.** Rosters use `deriveSeed(base, i)`, one
  `/api/roll/character` call per soul. Completed members are kept; mixed
  content snapshots are rejected.
- **No server combat state.** Damage, stamina, health and targeting run in
  the browser. No API request is made per hit.
- **Honest clocks.** Hidden, offscreen, printing, forced-colour and paused
  time grants no catch-up. Reduced motion starts paused.
- **Explicit cleanup.** Each effect owns its canvas, requests, workers,
  skeletons and GPU uploads, and releases them on Clear, replacement or
  unmount.

## 2. Catalogue

| Demo | Where | Section |
| --- | --- | --- |
| Roll an item | `/`, column 1 | §3 |
| Roll a character | `/`, column 2 | §3 |
| Roll a party | `/`, column 3 | §3 |
| Adventure mode with party | `/`, column 4 | §3 |
| Craft an item using orbs | `/`, column 5 | §3 |
| Battle with armies | `/`, column 6 | §3 |
| Reference columns | `/`, columns 7+ | §3 |
| Character portraits | Home columns, character pages | §4 |
| Anima workspace | `/anima` | §5 |
| Anima scene | `/anima-scene` | §6 |
| All weapons | `/anima-all-weapons` | §7 |
| Item models | `/item-models` | §8 |
| Combat fallback (API) | `/api/basic-lightweight-combat` | §9 |
| Research studies | `scripts/` | §10 |

## 3. The home workspace

The home page (`pages/index.tsx`, `components/HorizontalLayout.tsx`) is a
horizontal rail of 48ch columns. The first six are live demos; the rest show
the remaining `/api/spec` entries.

### Roll an item

The item name links to its raw-roll URL (`/roll/item/<rolls>`), beside its
grade, material, rarity score, orb value, slot domains and portrait.

- **API supplies:** one item from `/api/roll/item` with its `breakdown` row.
- **Local game layer adds:** a model portrait in the rolled material
  ([Item portraits](#item-portraits)).
- **Code:** `components/GeneratorSection.tsx`, `components/ItemPreview.tsx`;
  the weighted-table page is `pages/roll/item/[id].tsx`.
- **Limits:** no inventory; the URL is the whole item.

### Roll a character, with the Level and Orbs per item projection

The full character sheet (`CharacterElement`) with two sliders: **Orbs per
item** (default 0) and **Level** (default 1).

- **API supplies:** one ordinary birth from `/api/roll/character`.
- **Local game layer adds:** a level and equipment-spend projection, using
  the same helper as `/anima-scene`
  ([Compare levels and equipment spend in the scene](#compare-levels-and-equipment-spend-in-the-scene)).
  At level 1 and 0 orbs the sheet is the plain birth. The UUID link passes
  seed, `contentVersion`, name and a versioned recipe to `/roll/character`,
  which stores only the original birth; the detail page replays the
  projection without updating the database.
- **Code:** `components/CharacterElement.tsx`, `common/character-sheet.ts`,
  `common/character-simulation.ts`,
  `three-isometric-engine/anima-scene/progression.ts`; math §13.30.
- **Limits:** the projection is one hypothetical game's progression; the API
  grants no levels or stat points. Projected stats may exceed 24 and are not
  labelled Perfect.

### Roll a party

A roster (default four) of `CharacterPortrait` cards: UUID link, class,
LVL/PWR, dressed Anima face and resource strips.

- **API supplies:** one birth per member, requested in order at
  `deriveSeed(seed, i)`.
- **Local game layer adds:** the sequential queue and the face portraits.
  Completed members survive an error; a snapshot change stops the run. JSON
  export is optional.
- **Code:** `components/GeneratorSection.tsx`,
  `components/CharacterPortrait.tsx`.
- **Limits:** no party endpoint, batch or stored party. The same roster, even
  partial, feeds the adventure column.

### Adventure mode with party — the initiative dungeon

A first-person maze with a minimap and the party's cards. The party walks on
its own (arrows and WASD also work), stops at a faced enemy and fights.
Survivors walk to the far end and back.

- **API supplies:** the party's births; up to six enemy births, requested one
  at a time from a seed derived from the first member's seed and
  `contentVersion`; and the fallback's initiative and damage helpers.
- **Local game layer adds:** a 15×15 maze keyed by the first member's UUID,
  seed and snapshot; initiative turns in five-second windows; 20-stamina
  attempts with 10/s recovery; health that persists between fights; enemy
  Animas in their generated armor, holding their generated tool and offhand.
- **Code:** `components/AdventureSection.tsx`, `components/Dungeon.tsx`,
  `three-isometric-engine/dungeon/`; math §14; contracts in
  `three-isometric-engine/dungeon/AGENTS.md` and
  `components/adventure/AGENTS.md`.
- **Limits:** no rewards, progression, gear effects, status effects or
  persistence. Replay needs the local rules, assets and control history, not
  just the seed. The same seed with a new display name can give another maze.

### Craft an item using orbs — the fracturing marble rock

A charcoal marble slab covers the result card. Clicking the stone or
submitting the form crafts; the card renders underneath and the slab breaks
from the strike point. Recycle covers the card with a fresh slab. The stone's
sheen follows the orb amount.

- **API supplies:** one lottery craft from `/api/craft?orbs=N&seed=` (capped
  at 15,625), with `analysis` and `gradeExpectations`.
- **Local game layer adds:** a Voronoi slab fractured by Ammo/Bullet physics,
  with its own visual seed and an SVG fallback. The card is revealed if the
  renderer stalls.
- **Code:** `components/CraftingSection.tsx`, `components/CraftRock.tsx`,
  `components/CraftResult.tsx`, `three-isometric-engine/craft-rock-stage.ts`;
  contract in `three-isometric-engine/AGENTS.md`.
- **Limits:** the rock is decoration. Its sheen shows the slider, never an
  outcome or probability. No wallet, refund or inventory.

### Battle with armies

Two teams of sixteen Animas fight with their generated hand items under an
overhead camera (drag to rotate, wheel or pinch to zoom, click or Space to
pause). Cards show class, LVL and live resources.

- **API supplies:** 32 level-one births at `deriveSeed(base, 0..31)`,
  alternating Army I and Army II, equipment bonus off.
- **Local game layer adds:** the `/anima-scene` combat model (§6) on an
  embedded stage. Elimination is checked after each complete 60 Hz contact
  batch; simultaneous elimination is a draw. Reload battle reuses the roster
  with no new requests.
- **Code:** `components/ArmySection.tsx`, `components/army/ArmyBattle.tsx`,
  `three-isometric-engine/anima-scene/battle.ts`; math §13.28; contract in
  `components/army/AGENTS.md`.
- **Limits:** no time limit or forced damage, so a healing-heavy battle may
  never end. No rewards and no level or orb scaling. Pickup and WorldSpheres
  exist only in the full scene.

### Reference columns and the combat guide

One `EndpointSection` per remaining spec entry: title, description and a
collapsed reference. `BasicCombatGuide` shows notes on the combat fallback.

- **API supplies:** the `/api/spec` entries and the combat spec.
- **Local game layer adds:** nothing; these columns are documentation.
- **Code:** `components/EndpointSection.tsx`,
  `components/BasicCombatGuide.tsx`. A future demo goes in
  `EndpointSection.children`.
- **Limits:** no forms or requests.

## 4. Character portraits

Every character, party and adventure-roster portrait is the character's Anima
in the `/anima` Portrait face view, dressed in its rolled armor and materials
and coloured by its traits (§13.64).

- **API supplies:** each birth's equipment and traits.
- **Local game layer adds:** one shared offscreen WebGL renderer that draws
  stills one at a time, only for portraits on screen, into 2D canvases.
- **Code:** `components/AnimaPortrait.tsx`, `components/Portrait.tsx`,
  `three-isometric-engine/avatar/character-portraits.ts`.
- **Limits:** no painted artwork. Item portraits are separate
  ([Item portraits](#item-portraits)).

## 5. `/anima` — the base avatar and motion previews

Eight columns, each a live Anima on the concrete board: **Base avatar**
(Color and Grayscale sliders recolour every column), **Portrait face**,
**Walking** (walk-to-run slider), **Jump** (height slider), **Attacking**,
**Taking a hit** (frequency slider, eight directions), **Kneeling** and
**Lobbing a WorldSphere**. The `/item-models` catalog and Materials sidebars
sit beside them: the chosen outfit, in its chosen materials, is worn in all
eight views. The selection lives in the query, so an outfit link is
shareable.

**Attacking** plays each held item's full attack, alternating complete right
and left actions when both hands hold an item, at the weapon field's all-16
baseline stats. With nothing held it punches. **Lobbing a WorldSphere** first
drops any item in the left hand onto the mat, as the scene does, then picks up
and lobs the sphere; Replay returns the item to the hand.

- **API supplies:** nothing.
- **Local game layer adds:** the Anima model (24,216 body triangles, a
  48-bone rig), its motion (§15), directional hit reactions with 2.4 seconds
  of recovery, and GLB export
  ([Download Anima and its motions](#download-anima-and-its-motions)).
- **Code:** `pages/anima.tsx`, `components/ItemSidebars.tsx`,
  `components/CharacterAvatar.tsx`, `three-isometric-engine/avatar/`;
  contract in `three-isometric-engine/avatar/AGENTS.md`.
- **Limits:** visual only: no roll, request, damage or combat state. Idle
  draws at up to 30 fps and demonstrations at up to 60 fps while visible.

## 6. `/anima-scene` — the performance scene

The fullest example of a game built on the API. Click the floor to add real
characters, place teams, turn on Combat and watch them fight with their
generated equipment.

- **API supplies:** one real birth per Anima, one request at a time.
- **Local game layer adds:** weapons, teams, AI, health and stamina,
  ragdolls, pickup, cloth armor, WorldSpheres and level/orb scaling.
- **Code:** `pages/anima-scene.tsx`, `components/AnimaScene.tsx`,
  `three-isometric-engine/anima-scene/`; contract in
  `three-isometric-engine/anima-scene/AGENTS.md`.
- **Limits:** no births are stored and no endpoint changes. No forced winner
  or proven end to a battle. `report()` is a snapshot, not a replayable
  battle.

### Controls

| Control | What happens |
| --- | --- |
| Click or tap the mat | Queue one neutral Anima at that point |
| Place Team A / Place Team B | Queue sixteen births into a 4×4 formation (one squad per button until Clear) |
| Place WorldSphere | Place one sphere at a random clear point; no request |
| Orbs per item / Level | Set the scaling profile for the next queued actors |
| Combat | Toggle pursuit, weapons and WorldSphere priority |
| Pause, Shadows, Labels | Pause freezes the simulation; Shadows and Labels toggle those layers |
| Clear | Abort queued work, remove everything and release GPU uploads |

Each placement is one `/api/roll/character` request. Only one is in flight;
retries keep the job's seed and identity; Clear or unmount rejects late
results. An actor appears only once it is dressed and ready to draw.

### Rules

- **Routine.** With Combat off, living neutrals rank by final Strength, PWR,
  then spawn order. The strongest leads a square around the origin; each
  other follows the next stronger. Team members hold their stations.
- **Combat.** Animas run, look for opponents in a forward cone, and hunt the
  nearest attackable one when none is in view. Team members never damage or
  harpoon each other; neutrals never damage neutrals. Healing rings include
  everyone.
- **Colour.** Each Anima takes its trait colour (§13.64). Team identity is in
  the labels (`A` on black, `B` on white) and the flags' emblems.
- **Resources.** Health and stamina come from returned attributes. Damage
  uses the fallback's physical-hit helper, floored to whole values (§13.65).
  Attempts spend stamina; recovery is 10/s. A damaging hit blocks new
  attacks until 2.4 seconds after the latest hit.
- **Movement.** Attacks can start and finish while moving (§13.20). Living
  bodies block movement without shoving (§13.31).
- **Bodies.** Death turns an Anima into a sixteen-body ragdoll; corpses stay
  pushable. Press any Anima or corpse to pick it up by that body part, then
  drag or flick it. Living actors land and rejoin their routine; held and
  recovering actors cannot be attacked.
- **Armor.** Each actor wears its generated armor in its rolled materials.
  Cloth solves in workers and rests still unless its wearer moves or the
  camera turns (§13.52–§13.54).
- **Crowds.** At small on-screen sizes only visual work scales down;
  contacts, damage, AI and labels never change (§13.67).

### Weapons of every family

Both hand items come from the gear factor (`weapons/generated.ts`) and are
measured for relative mass and grip inertia. Each hand runs complete actions
with its own reload.

| Family | Action | Math |
| --- | --- | --- |
| Blades, polearms, axes, hammers, clubs | Cut, thrust, chop, bash at 2× playback | Handling and heft §13.2, §13.47; tempo §13.32 |
| Shields | Shieldbash; stable carry | §13.35, §13.40 |
| Bows / crossbows | Aim / Fire with ethereal arrows and bolts along the launch tangent | §13.33, §13.60, §13.62 |
| Rifle, Hand cannon | Gunfire: instant shot, 0.14-second beam | §13.26–§13.27 |
| Thrown tools, Chair | Throw on the Lash chain, reeled back on a miss | §13.49–§13.50 |
| Javelin, Pilum | Hurl, tip first | §13.17 |
| Harpoon gun | Skewer: chained bolt; Strength decides pull or struggle | §13.17, §13.50 |
| Boomerang, Chakram | Return along curved flat flights with a catch | §13.16 |
| Sling | Whirl: two-cord XPBD sling releasing a stone | §13.10, §13.13 |
| Whip, chain, rope dart, urumi | Lash on a tapered XPBD strand | §13.9, §13.15, §13.51 |
| Nunchaku | Twirl | §13.15 |
| Orb (item) | Repulse: full-circle damaging pulse at 0.70× magical damage | §13.23, §13.25 |
| Books, instruments | Present / Play / Strum healing rings | §13.7, §13.11 |
| Rods | Flick: forward half-ring waves that damage | §13.11 |
| Knuckle, Fang, Claw, bare hands | Punch at 2× playback | §13.2, §13.55 |

Ranged hands are preferred beyond five world units, melee hands inside
(§13.32). New ranged attacks target at most 30 world units, softening above
20 (§13.34). A fixed reticle marks each aim point; contact decides hits
(§13.21). Knockback uses one shared 0.5 scale (§13.36).

### WorldSpheres

WorldSpheres are large, button-placed props. With Combat on, free Animas
fetch them first, dropping their offhand item, then Lob them at an opponent
for a fixed 222 damage. Combat off returns the dropped item. The small item
Orb is different: it stays in hand and casts Repulse (§13.23–§13.24).

### Level and orb sliders (`anima-scaling-4`)

The Orbs per item (0–15,625) and Level (1–99) sliders apply to newly queued
actors or teams; existing actors never change. See
[Compare levels and equipment spend in the scene](#compare-levels-and-equipment-spend-in-the-scene).

## 7. `/anima-all-weapons` — the weapon field

One Anima per mainhand in the tool table (145) holds its tool on the concrete
board. **Show attacks** loops each action; **Stop attacks** returns to the
breathing idle; **Unique** keeps one Anima per animation type and distinct
mechanism; **Dual wield** puts the item in both hands and alternates complete
actions. Actors can be picked up and flicked; they recover and walk home. The
item and material sidebars (worn slots only, no mainhand or offhand) dress
every Anima in the same outfit, for testing armor across every weapon action.

- **API supplies:** nothing. Every Anima has fixed all-16 stats, shown as
  `LVL 1`.
- **Local game layer adds:** the same geometry, grips, handling, tempo,
  range, reload, projectiles, flexible solvers and auras as the scene.
- **Code:** `pages/anima-all-weapons.tsx`, `components/AnimaWeapons.tsx`,
  `three-isometric-engine/weapons/`; contract in
  `three-isometric-engine/weapons/AGENTS.md`.
- **Limits:** no combat, roll, request or weapon download.

## 8. `/item-models` — the item inspector

A sidebar lists every gear entry by slot; one choice per slot builds an
outfit. **On Anima** shows the outfit with Start attack and Start walking
toggles; **Item portrait** shows the latest choice alone. A **Materials**
sidebar lists the focused item's material table (31 wearable, 23 tool) by
weight with each grade. Choices live in shareable query parameters (for
example `?tool=sword&chest=tunic&focus=chest`, with `<slot>Material` per
slot).

- **API supplies:** nothing at runtime; the page reads the catalog tables.
- **Local game layer adds:** fitted cloth-and-plate armor, rigid pieces,
  jewellery, held-item effects and procedural material shaders (§13.63).
- **Code:** `pages/item-models.tsx`, `components/ItemModels.tsx`,
  `common/item-models.ts`, `three-isometric-engine/items/`,
  `three-isometric-engine/armor/`; contracts in
  `three-isometric-engine/items/AGENTS.md` and
  `three-isometric-engine/armor/AGENTS.md`.
- **Limits:** no generation, persistence, combat, item variants or download
  route. Armor has no stat effect.

## 9. The optional API fallback: basic-lightweight-combat

The one piece of combat that is API, for games with no combat rules of their
own.

- **API supplies:** `mode=damage` takes two nine-stat blocks (integers 8–24),
  an optional physical/magical/action kind, bounded health and stamina,
  elapsed recovery time and a uint32 seed, and returns one attack's rolls,
  damage and resource changes. `mode=initiative` takes actors and a
  `(startMs, endMs]` window and returns ordered action opportunities and
  per-actor `moves`. Responses carry `experimental: true`, `rulesVersion` and
  `contentVersion`; `/api/basic-lightweight-combat/spec` and the skill
  publish formulas, limits and versions.
- **Local game layer adds:** our demos import its helpers directly. The army
  and scene use the physical-hit and stamina helpers; the dungeon uses the
  initiative helper. None sends it HTTP requests.
- **Code:** `common/basic-lightweight-combat.ts`,
  `common/basic-lightweight-combat-skill.md`; math §12.
- **Limits:** no whole battles, status or weapon effects, or state. `moves`
  are opportunities, not distance or guaranteed attacks. It accepts only
  attributes derived from nine bounded stats, so a 10,000-HP boss or
  equipment-adjusted maxima need local rules.

## 10. Research studies

Three Node scripts test the core and the local game layer on the CPU, with no
HTTP, database, browser or renderer. They run only for requested research.

### Campaign study — `scripts/simulation.cjs`

**Snapshot.** Recorded 2026-09-09 for package 0.3.11 under content revision 3
(`3.p2f7KrzkpbvCklHHlZ227Q`), rules `set-simulation-20260909-v1`, combat
profile `basic-lightweight-combat-1`. Its lottery, craft, salvage and campaign
figures describe revision 3 and were not re-run for the current economy
(ALGORITHMS.md §3).

**Artifacts.** `node scripts/simulation.cjs` (model in
`scripts/simulation-study.ts`) overwrites
SIMULATION_DATA.json (source: `SIMULATION_DATA.json`) (configuration, source hashes,
arithmetic, checkpoints, summaries) and
SIMULATION_RECORDS.json.gz (source: `SIMULATION_RECORDS.json.gz`) (birth specimens,
encounter and purchase ledgers, army records, action logs). Specimens come
from the character handler's helpers, not captured HTTP responses.

| Evidence | Scale |
| --- | --- |
| Local births | 1,432, with 1,432 distinct seeds |
| Level-99 campaigns | 24 births × three equipment interpretations = 72 |
| Spatial army executions | 24 rosters × original/exchanged sides = 48 |
| 40-versus-40 initiative battles | 8 rosters × two sides = 16 |
| Maze keys | 24 |
| Campaign totals | 147,217 encounters, 26,747 lottery crafts, 5,323,075 candidates |

**Campaign rules** (a declared consumer game, not Set's): level =
`min(99, 1 + floor(victories / 20))`; enemies have nine copies of
`8 + floor(16 × (level − 1) / 98)`; each twenty-attempt cycle has fifteen
trash, three veterans, one elite and one boss (a world-boss every hundredth
attempt); stationary physical duels on 100 ms ticks with the shared damage
and stamina helpers; **every encounter starts at full resources and defeat
costs nothing**; victory calls `enemyDrop`; half the orbs go to crafting and
half to raw primary-stat boosts; a strictly better grade replaces the worst
item in its slot and the discard is salvaged.

| Interpretation | Equipment behaviour |
| --- | --- |
| Default | No item effects (the API default) |
| Approximation | The opt-in grade/slot stat bonus, clamped at 24 |
| Budget | A study-only split of each item's budget over its slot domains (e.g. 2 HP or 0.25 damage per point) |

**Findings.**

| Interpretation | Reached 99 | Median attempts | Median defeats | Median final HP | Median S items / 15 |
| --- | ---: | ---: | ---: | ---: | ---: |
| Default | 24 / 24 | 2,084.5 | 124.5 | 230 | 11 |
| Approximation | 24 / 24 | 2,023.5 | 63.5 | 256 | 11 |
| Budget | 24 / 24 | 2,007 | 47 | 333 | 11 |

- Completion depends on free recovery: **none of the 72 runs completes under
  permadeath**; the median first defeat is at level 5 in all three.
- Final heroes against stat-24 enemies (one seeded fight each): elite wins
  were 0, 0 and 14 of 24; no boss or world-boss fell. Only the budget
  interpretation gives equipment an effect beyond the stat ceiling. Doubling
  budget coefficients raised elite wins to 24/24 and boss wins to 2/24;
  halving enemy threat raised boss wins to 22/24. These are consumer tuning
  choices, not Set balance.
- Purchases that changed none of the 23 attributes: 18, 317 and 22. The
  approximation's clamp makes many raw points useless, so a shop screen needs
  the consumer's before/after sheet.
- One full ledger (seed `20260909`, budget) closes exactly:
  `129,567 drops + 20,860 salvage − 84,850 crafts − 59,582 boosts = 5,995`.
  Every campaign's accounting residual is zero.

**Spatial army model.** The 16-versus-16 grid model
(`scripts/simulation-army-model.ts`), separate from the home page's Anima
army, finished 48/48 battles with no draws; side I/II won 23/25; median
modelled duration 16.75 s; higher summed PWR won 35/48; the same roster won
from both sides in 21/24 pairs. PWR is not a win probability; placement,
timing and RNG change winners.

**One hero and eighty NPCs.** A nonspatial 40-versus-40 consumer used the
initiative helper with physical hits and stamina: 16/16 battles finished
(median 30.48 modelled seconds). Every battle had fewer attack attempts than
living opportunities: stamina, not the queue, limits attacks. Per-character
behaviour did not depend on roster size. The 80-actor initiative GET with
short IDs was 7,883–7,910 bytes of path plus query, near the 8,000-octet
minimum URI support that
[RFC 9110 §4.1](https://www.rfc-editor.org/rfc/rfc9110.html#section-4.1)
recommends; 36-character UUID IDs made the first sample 10,213 bytes. That is
arithmetic, not an HTTP observation.

**Maze geometry.** All 24 sampled mazes (a 7×7 spanning tree in a 15×15 grid)
have 97 open tiles; entrance-to-end paths span 50–86 steps, median 70.
Encounters were not studied.

**Lore.** The tables are a large vocabulary (counts in
[Evidence and limits](#12-evidence-and-limits)), but trait impacts are
hash-derived, not semantic: a "dreaming mind" advantage can mainly change
dexterity. Some effect and skill prose reads like rules Set never implements
(Wither halving healing, Berserk doubling damage, Haste acting twice,
formation fighting implying a member-count multiplier). Read returned
impacts, not names.

### Gameplay mathematics review — `scripts/gameplay-math-review.cjs`

**Method.** A CPU review (2026-09-18, Node v26.5.0, content revision 3) of
mass, handling, stamina, range and initiative using the real catalog and
helpers (`scripts/gameplay-math-review.ts`). Results, with every weapon's
baseline row and source hashes, are in
[GAMEPLAY_MATH_REVIEW_DATA.json](https://set.world/gameplay-math-review.json). Its 33
character cases are the 32 corners of the five-primary domain plus all-16.

| Subject | Coverage and result |
| --- | --- |
| Mass and inertia | 144 catalog entries mirrored and rotated; mass and grip inertia agree within tolerance. Cube fixtures match analytic moments; mass ∝ length³, inertia ∝ length⁵ |
| Handling | 9,504 profiles stay finite; every attempt fits the 40-point minimum stamina pool |
| Burden and stats | 28,512 burden checks never raise speed or cut cost; 17,728 stat comparisons show no reversal |
| Mixed dual wield | 20,736 ordered pairs keep the support penalty; two hands do not mean two action clocks |
| Frame partitioning | 30 Hz, 60 Hz and irregular steps agree within 5.7 × 10⁻¹¹ |
| Ballistics | Endpoints, inverse arcs, uphill targets and unreachable cases pass |
| Initiative | 257 actors (over one 128-actor slice) give 17,542 opportunities; adjacent windows equal one whole window; ties keep input order |
| Gunfire | Five target heights keep the 3D ray budget and floor clip |

**Conclusions.**

- **The review found one defect.** The aiming helper rejected exact-tangent
  shots when roundoff made the discriminant slightly negative (167 of 851
  cases). The helper tolerates roundoff only: all 851 tangent cases solve at
  45°, and all 851 cases just outside stay unreachable (§13.29).
- Gunfire range is a finite 3D ray length; reports separate ray distance from
  horizontal distance (from a Rifle muzzle at 1.6 aiming at height 8, a
  31.750671 budget projects to 31.124659).
- Mass is a relative surface-shell measure, not kilograms, rarity or price.
  In the review, heavy weapons traded tempo and stamina for reach and contact
  with no damage bonus (a Knife dealt about 9.069 raw physical damage per
  second versus a Tree trunk's 3.505 against one target). The review predates
  the heft multiplier (§13.47), which changes those tempos and costs.
- At the reviewed 1.30× melee tempo, every birth-range stat block at any
  burden regained each cycle's cost before the next (drain below 10/s). The
  current 2× melee and `anima-scaling-4` are outside that proof and can
  outspend recovery; the dungeon's faster logical clock also needs rests.
- Healing rings include opponents, fixed-point shots can miss and hit
  recovery can block attacks, so spatial battles have no proven end. None of
  this shows competitive weapon balance.

Not measured: XPBD solvers, ragdoll stability, pose-based melee contact, GLB
compatibility, frame rates.

### Scene scaling study — `scripts/scene-scaling-study.cjs`

**Method.** CPU checks of the scene's level/orb projection and collision,
written to [SCENE_SCALING_DATA.json](https://set.world/api/guides/scene-scaling-data) (Node v26.5.0).
It uses all-8/all-16/all-24 starting stats with seeded equipment at levels 1,
25, 50, 75 and 99, a separate birth for item-budget comparisons, and the real
scene model with fixed poses for collision. It was recorded for
`anima-scaling-3` under content revision 3; its reforge and grade-dependent
values were not re-run for the current lottery, material grades and godhair.

**Findings.**

- 882 level pools checked: 3 points per level in 0.25 steps, 294 points at
  level 99, only the five primary stats changed.
- An all-16 block with seed-42 equipment and no reforge spend grew from 171
  health at level 1 to 799.5 at level 99, and physical damage from 27.625 to
  138.25 before acting-item power.
- A Grade S acting weapon doubled a Grade F weapon's 26-damage hit to 52.
- Collision: stationary living blockers, head-on approaches and fast
  crossings were blocked; corpses were pushed; overlapping spawns avoided.

Recorded scope: "CPU helper and model checks, no browser or renderer
acceptance". It shows no visual feel or balance.

## 11. Building your own game on top

Your game is your own local game layer. Reuse our math, our rendering, both
or neither. There is no client SDK and no hosted weight or range endpoint;
other engines can implement ALGORITHMS.md §12–§14 directly.

| Layer | Reuse | You own |
| --- | --- | --- |
| Generation and economy | [API skill](https://set.world/skill), [crafting skill](https://set.world/craft-guide), `/api/spec` | Issuance, spending, persistence, what drops what |
| Optional damage and initiative | [Combat skill](https://set.world/basic-lightweight-combat), `common/basic-lightweight-combat.ts` | Health, stamina, scheduling, targets, history |
| Turn-based example | `three-isometric-engine/dungeon/adventure.ts`, `initiative.ts` | Encounters, rewards, progression |
| Spatial weapon rules | `three-isometric-engine/weapons/handling.ts`, `range.ts`, `ballistics.ts`, `tempo.ts`, `impact.ts` | Damage, recovery and utility tuning, balance |
| Full scene integration | `three-isometric-engine/anima-scene/equipment.ts`, `model.ts` | Your world, netcode, saves |
| Visual assets and motion | `/anima` downloads; `three-isometric-engine/avatar/` | Asset clearance (see below) |

**Build one consistent character.**

1. Roll one character per request; fan out a roster with `deriveSeed(base, i)`
   and keep each full response, display UUID, seed and `contentVersion`. Keep
   the queue sequential and reject mixed snapshots.
2. Read `finalStats` and `attributes` once. Traits are already applied;
   equipment bonuses are off unless you pass `equipmentBonus=1`. Keep your
   health, stamina, equipment changes and position beside the response.
3. Resolve hands with `generatedHandItem` in `weapons/generated.ts`: `tool` is
   the right (main) hand, `offhand` the left, read from the gear factor
   (`item.props[3].name`). Never parse the composed name.
4. Map to `weaponCatalog()`, build and cache geometry, measure it with
   `measureWeaponMass`, and mirror geometry and attachments together with
   `mirrorWeapon`.
5. Compute `weaponHandling(activeMass, finalStats, otherMass)`, then
   `actionHandling(handling, weaponUsage(entry))`. Read `usage.launch` and
   `usage.sling` instead of guessing projectile rules from an animation.
6. Commit once, pay once, run one complete hand action at a time, start each
   hand's reload at release, and resolve each contact once. Never also send an
   API hit request for a hit you resolved locally.

**Keep clocks and units explicit.**

| Quantity | Meaning |
| --- | --- |
| Relative mass, inertia | Model units, one shell per welded solid; the Sword is one unit. Not kilograms or rarity |
| Heft | `min(sqrt(1+(M+J)/2), 1/0.35)`; multiplies melee and physical projectile hits. Projectiles also take `0.9 × 26/24.4` |
| Scene placement | Anima at scale 0.60; convert reach and release points to world units once |
| Handling rate | Multiplier on a four-second authored cycle (`seconds = 4 / rate`), not attacks per second; melee plays at 2× |
| Fallback timing | `1000 / attackSpeed` ms, separate from scene animation |
| Initiative | `1000 / initiative` ms between opportunities, `(start, end]` windows |
| Stamina | Continuous; recover 10/s once per interval, capped at the returned maximum; display whole numbers |
| Projectile reach | `projectileRange(profile, height)` is the maximum at any angle; arcs are solved afterward |
| Aura factor | A radius multiplier; its square is the area factor |

Do not add initiative, attack-speed and animation clocks together into extra
attacks. Cache geometry and mass by geometry and handling versions, and reset
handling and grip bindings when equipment changes.

### Compare levels and equipment spend in the scene

Use the **Orbs per item** and **Level** sliders in `/anima-scene` to queue
Team A and Team B under different profiles. Each job keeps its settings.
Clear keeps the base seed, so repeating the same placements compares the same
births under another profile. Existing actors never change. The home page's
Roll a character column uses the same helper and sliders, and its detail link
replays the projection from the stored birth.

The `anima-scaling-4` profile models one hypothetical game (§13.30; code in
`anima-scene/progression.ts`, `generation-rules.ts`, `generation.ts`):

- **Levels.** Level 1 gets nothing; each later level gives 3 points as twelve
  seeded quarter-point grants among strength, dexterity, agility,
  intelligence and vitality. At level 99 the pool is 294 points, an expected
  58.8 per primary stat, varying by character. Growth is fractional and
  continues above 24. Wisdom, perception, resolve and luck get none.
  `SCENE_GENERATION_RULES.statPointsPerLevel` is the single pool constant.
- **Equipment spend.** Zero orbs keeps the original gear, whose grade still
  counts. Orbs draw the lottery's diminishing-returns candidate count per item
  from a stable stream, keeping slot, gear and class fixed. More orbs cannot
  lower a grade; Grade S is the ceiling. This is not the public crafting
  distribution.
- **Item budgets.** Each item's budget B becomes B/8 local stat points by
  slot, and the acting weapon's power is `1 + B/32` (Grade F 1, Grade S 2),
  multiplying its damage or healing.
- **One effective attribute block** drives health, stamina, damage, movement,
  attack speed, initiative, carrying, handling, reload and knockback.
  Probability and resistance channels have diminishing returns toward 0.85.
- **Accounting.** Stat growth is priced with the canonical boost steps
  through 24 and a local cubic continuation above it; equipment spend is 15 ×
  the per-item orbs. `report()` has the full receipts.

None of this changes the API's birth range, budgets, prices or the opt-in
equipment bonus.

### Item portraits

Home item and craft results and every character sheet's equipment rows show
`ItemPortrait` stills for each slot with a model: tool, offhand, feet, legs,
hand, finger, chest, waist, shoulders, wrist, back, neck and head.

- `common/item-portrait.ts` reads slot, gear and material from validated raw
  rolls, never from the composed name.
- `three-isometric-engine/items/` assembles the same meshes, strings and
  flexible parts as the scenes and shows each item in its rolled material
  (§13.63).
- Stills are drawn into Canvas 2D with no WebGL context per row
  (`items/portraits.ts`).
- Grade, affixes and value stay as text; the portrait adds no decoration.
  Portraits carry no rigged equipment or downloads.

For a full-size model, open `/item-models` and pick the slot and material; to
see every held tool in motion, open `/anima-all-weapons`.

### Download Anima and its motions

Open `/anima`, scroll a column into view and wait for its model. In Base
avatar or Portrait face choose **Download model**; in a demonstration column
set its controls and choose **Download motion**.

| Column | File | Content |
| --- | --- | --- |
| Base avatar / Portrait face | `anima.glb` | Rigged Anima with the Breathing clip |
| Walking | `anima-walk.glb` | Current walk-to-run blend as an in-place loop |
| Jump | `anima-jump.glb` | Selected jump height and its clip |
| Attacking | `anima-punch.glb` | The unarmed punch; held-item attacks are runtime controllers and are not exported |
| Taking a hit | `anima-hit.glb` | Eight contacts at the selected frequency/direction, then full recovery |
| Kneeling | `anima-kneel.glb` | Kneel, hold and rise |
| Lobbing a WorldSphere | `anima-throw.glb` | Throw motion, prop choreography and Lob metadata |

The browser builds these files with Three.js `GLTFExporter`
(`avatar/model.ts`). They are download names, not static URLs; there is no
model API. Exports contain geometry, the 48-bone rig, skin weights, a standard
metal material (glTF carries the plain colour, not the runtime liquid-metal
shader), the selected clip sampled from the same pose function, and versions
and metadata. The stage floor is left out. The model has 24,216 body
triangles plus about 2,600 eye triangles; that is not a performance promise
for any engine or roster.

```ts
import { AnimationMixer } from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const asset = await new GLTFLoader().loadAsync('/assets/anima-walk.glb');
scene.add(asset.scene);
const mixer = new AnimationMixer(asset.scene);
if (asset.animations[0]) mixer.clipAction(asset.animations[0]).play();
```

Put the file at your own asset path and call `mixer.update(deltaSeconds)` in
your visible loop. For many characters, clone with skeletons, give each its
own mixer, share geometry, and dispose each skeleton and mixer.

A baked clip does **not** include the live IK, grip fitting, ragdoll,
targeting, combat, armor or motion blending; those live in source. There is no
weapon download: tools are built by `weapons/meshes.ts`, armor by
`three-isometric-engine/armor/`.

**Model license caution.** Set's code license does not cover the Anima
geometry. Parts of the base derive from a maintainer-supplied model by tramdrey
whose metadata records SKETCHFAB Standard, and exports keep that attribution.
Do not present the GLBs as an unrestricted MIT asset pack; clear permissions
before redistributing them or shipping them in a game. You can use the code
and math with your own cleared assets. See
[README.md, "Assets and licenses"](https://set.world/readme#assets-and-licenses).

### Retain enough to replay

Keep the full roll, seed, `contentVersion`, your rule version, the geometry,
handling, ballistic, motion and visual versions you used, and your simulation
inputs. A content stamp alone cannot replay a changing spatial game, and a
scene report is not a save file. Keep action randomness apart from visual
randomness, recover resources once, freeze hidden and paused time, and break
ties consistently.

**Genre sketches.** A roguelike rolls a hero from a daily seed, uses crafting
as its shop and salvages a fallen hero's fifteen slots into the next run's
orbs. An autobattler expands one integer into an army with `deriveSeed`,
matches sides on its own cap (equal PWR or orb value does not mean equal
strength) and prices kills with `/api/drop`. A party RPG drafts four, maps
`initiative`, `reflexes` and `clarity` to turn order and ambush, and treats
the 48 effects as an unpriced condition vocabulary. In each case **Set
generates, grades and prices; your game resolves, persists and progresses.**

## 12. Evidence and limits

| Established | How |
| --- | --- |
| Current table counts and the arithmetic below | Computed from the current tables and helpers |
| The API's pieces compose into births, economies, a spatial battle and an 80-actor consumer | The revision-3 campaign study |
| The local weapon math is finite, monotone and frame-rate independent where claimed | The gameplay math review (at its recorded rule versions) |
| Level/orb pools and collision behave as specified | The scene scaling study (at `anima-scaling-3`) |
| Each demo's boundaries | Source review against the owning AGENTS contracts |

| Not established | Why |
| --- | --- |
| Frame rates, supported actor counts, device or mobile performance | Caps and budgets are not measurements |
| Runtime, browser and visual acceptance | Owned by the maintainer; source review is not verification |
| Competitive weapon balance, class balance, fun, retention | No encounter tuning or player testing |
| That every spatial battle ends | Healing includes opponents; no forced winner |
| Current-economy campaign outcomes | The campaign study predates revision 4 |
| Cross-engine parity, GLB import compatibility | Not tested |
| Hosted latency, availability, commercial adoption, integration time | No telemetry, customers or external trials |
| A persistent world with agency or a coherent canon | Set supplies vocabulary, not places, chronology or memory |

**Integration limits.** The first response is easy: `/api/spec`,
`/api/skill` and one complete character with grades and replay data. Friction
comes after:

- Births do not carry item budgets and themes, so an HTTP-only consumer that
  appraises every item makes 15 extra calls per character (1,200 for 80).
  `/api/modifiers` carries no `contentVersion`.
- `/api/spec` describes types in prose; there are no machine-readable
  response schemas.
- Seeds reproduce objects but do not authorize rewards or make writes
  idempotent; exactly-once issuance is the game's code.
- Large initiative rosters approach common URI limits over GET.

**Current exact facts.**

| Quantity | Current value |
| --- | ---: |
| Classes / mainhand tools | 145 |
| Skills | 288 |
| Advantages / disadvantages | 144 / 144 |
| Status effects (names and prose only) | 48 |
| Active item-factor combinations (quality × prefix × suffix × Σ slot material × gear) | 3,883,267,440 |
| Unordered skill bundles of one to four | 284,701,992 |
| Raw nine-stat blocks (`17^9`) | 118,587,876,497 |
| Seed inputs per operation, options and snapshot | 4,294,967,296 |

- Buying a raw stat from 8 to 24 costs 53,337 orbs; the last point costs
  32,768. About 42.05% of births have a raw 24 somewhere and 26.15% in a
  primary: Perfect is expensive to buy but possible at birth.
- Canonical HP at all-8/16/24 primaries is 80/168/256, stamina 40/88/136. At
  20 stamina per attempt and 10/s recovery, the long-run limit is 0.5
  attempts per second; opportunities are not guaranteed attacks.
- At fixed stats of 16 and two skills, PWR rises from 1,222 at level 1 to
  11,022 at 99 with identical attributes; the difference is the level term.
- Counts measure variety, not reachability from the seed space, distinct
  gameplay or story quality. A seed is not a unique person.

## 13. Where Set fits

Set suits games that want generated, explainable, reproducible content and
keep their own rules and state: NPC and tabletop tools, narrative games and
MUDs, roguelikes, small party RPGs and autobattlers. For online competitive
games and persistent worlds it is one backend part; issuance, authoritative
simulation, persistence and operations stay with the game. These are design
judgments, not forecasts.

The tables give real writing material. The main risk is prose that sounds
like a mechanic Set does not implement (see the campaign study's lore note).
