@origingame/engine 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/bin/og.mjs +25 -0
  2. package/dist/web/origin-engine.d.ts +1233 -0
  3. package/dist/web/origin-engine.js +8628 -0
  4. package/dist/web/origin-engine_bg.wasm +0 -0
  5. package/dist/web/origin-engine_bg.wasm.d.ts +366 -0
  6. package/dist/web-publish/origin-engine-lanes.js +8509 -0
  7. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  8. package/dist/web-publish/origin-engine.d.ts +1176 -0
  9. package/dist/web-publish/origin-engine.js +8313 -0
  10. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  11. package/dist/web-publish/origin-engine_bg.wasm.d.ts +347 -0
  12. package/dist/web-publish/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  13. package/dist/web-publish-2d/origin-engine-lanes.js +8509 -0
  14. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  15. package/dist/web-publish-2d/origin-engine.d.ts +1176 -0
  16. package/dist/web-publish-2d/origin-engine.js +8313 -0
  17. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  18. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +347 -0
  19. package/dist/web-publish-2d/snippets/wasm-bindgen-rayon-38edf6e439f6d70d/src/workerHelpers.no-bundler.js +77 -0
  20. package/knowledge/README.md +31 -0
  21. package/knowledge/knowledge.json +13 -0
  22. package/knowledge/looks/catalogue.json +1871 -0
  23. package/knowledge/package.json +33 -0
  24. package/knowledge/rules/platform.json +329 -0
  25. package/knowledge/scaffold/README.md +27 -0
  26. package/knowledge/scaffold/distribution.json +48 -0
  27. package/knowledge/scaffold/origin-engine.json +19 -0
  28. package/knowledge/scaffold/template/.origin/setup +14 -0
  29. package/knowledge/scaffold/template/LICENSES.md +23 -0
  30. package/knowledge/scaffold/template/README.md +37 -0
  31. package/knowledge/scaffold/template/assets/ai/guard.ogt +48 -0
  32. package/knowledge/scaffold/template/assets/audio/click.ogt +9 -0
  33. package/knowledge/scaffold/template/assets/fonts/NotoSansSC-Regular-subset.ttf.ogt +12 -0
  34. package/knowledge/scaffold/template/assets/fonts/OFL.txt +93 -0
  35. package/knowledge/scaffold/template/assets/looks/origin-game.ogt +12 -0
  36. package/knowledge/scaffold/template/assets/nav/yard.ogt +27 -0
  37. package/knowledge/scaffold/template/assets/scenes/main.ogt +34 -0
  38. package/knowledge/scaffold/template/assets/strings/en.ogt +14 -0
  39. package/knowledge/scaffold/template/assets/textures/stone_n.png.ogt +7 -0
  40. package/knowledge/scaffold/template/gitattributes +22 -0
  41. package/knowledge/scaffold/template/package.json +18 -0
  42. package/knowledge/scaffold/template/project.ogt +25 -0
  43. package/knowledge/scaffold/template/src/main.ts +246 -0
  44. package/knowledge/scaffold/template/tests/guard.og.ts +67 -0
  45. package/knowledge/scaffold/template/tests/smoke.og.ts +43 -0
  46. package/knowledge/scaffold/template/tsconfig.json +17 -0
  47. package/knowledge/scripts/lib.ts +203 -0
  48. package/knowledge/scripts/mount.ts +210 -0
  49. package/knowledge/scripts/pack.ts +24 -0
  50. package/knowledge/scripts/scaffold.ts +72 -0
  51. package/knowledge/scripts/validate.ts +749 -0
  52. package/knowledge/skills/og-ai/SKILL.md +386 -0
  53. package/knowledge/skills/og-audio/SKILL.md +191 -0
  54. package/knowledge/skills/og-build/SKILL.md +330 -0
  55. package/knowledge/skills/og-core/SKILL.md +285 -0
  56. package/knowledge/skills/og-feel/SKILL.md +385 -0
  57. package/knowledge/skills/og-game/SKILL.md +1479 -0
  58. package/knowledge/skills/og-i18n/SKILL.md +220 -0
  59. package/knowledge/skills/og-platform-sdk/SKILL.md +199 -0
  60. package/knowledge/skills/og-taste/SKILL.md +537 -0
  61. package/knowledge/skills/og-ui/SKILL.md +444 -0
  62. package/knowledge/wiki/ai.md +627 -0
  63. package/knowledge/wiki/anim.md +1187 -0
  64. package/knowledge/wiki/atmosphere.md +636 -0
  65. package/knowledge/wiki/audio.md +732 -0
  66. package/knowledge/wiki/camera.md +711 -0
  67. package/knowledge/wiki/commands.md +357 -0
  68. package/knowledge/wiki/game.md +220 -0
  69. package/knowledge/wiki/i18n.md +213 -0
  70. package/knowledge/wiki/index.md +1027 -0
  71. package/knowledge/wiki/input.md +1714 -0
  72. package/knowledge/wiki/load.md +1280 -0
  73. package/knowledge/wiki/math.md +166 -0
  74. package/knowledge/wiki/nav.md +1154 -0
  75. package/knowledge/wiki/physics.md +3142 -0
  76. package/knowledge/wiki/platform.md +857 -0
  77. package/knowledge/wiki/probe.md +1692 -0
  78. package/knowledge/wiki/reflect.md +285 -0
  79. package/knowledge/wiki/render.md +3511 -0
  80. package/knowledge/wiki/rng.md +393 -0
  81. package/knowledge/wiki/stream.md +376 -0
  82. package/knowledge/wiki/system.md +197 -0
  83. package/knowledge/wiki/systems.md +43 -0
  84. package/knowledge/wiki/terrain.md +727 -0
  85. package/knowledge/wiki/tilemap.md +422 -0
  86. package/knowledge/wiki/time.md +150 -0
  87. package/knowledge/wiki/transform.md +230 -0
  88. package/knowledge/wiki/tween.md +580 -0
  89. package/knowledge/wiki/ui.md +8000 -0
  90. package/knowledge/wiki/world.md +909 -0
  91. package/package.json +68 -4
  92. package/schema/api.schema.json +29675 -0
  93. package/shell/README.md +22 -0
  94. package/shell/brand/README.md +23 -0
  95. package/shell/brand/icon-192.png +0 -0
  96. package/shell/brand/icon-512.png +0 -0
  97. package/shell/brand/mark-on-paper.svg +6 -0
  98. package/shell/brand/origin-mark.svg +5 -0
  99. package/shell/brand/origin-motion.css +251 -0
  100. package/shell/brand/tokens.css +20 -0
  101. package/shell/brand.ts +132 -0
  102. package/shell/page.ts +715 -0
  103. package/shell/platform.ts +203 -0
  104. package/shell/player.ts +238 -0
  105. package/shell/shell.css +185 -0
  106. package/shell/sw.ts +177 -0
  107. package/shell/transport.ts +515 -0
  108. package/shell/wire.ts +369 -0
  109. package/src/e6.ts +69 -0
  110. package/src/env.d.ts +100 -0
  111. package/src/generated/abi.ts +980 -0
  112. package/src/index.ts +193 -0
  113. package/src/node/audio.ts +86 -0
  114. package/src/node/build.ts +109 -0
  115. package/src/node/device.ts +981 -0
  116. package/src/node/doctor.ts +60 -0
  117. package/src/node/index.ts +102 -0
  118. package/src/node/platform.ts +24 -0
  119. package/src/node/playtest-run.ts +249 -0
  120. package/src/node/png.ts +130 -0
  121. package/src/node/polish.ts +976 -0
  122. package/src/node/run.ts +373 -0
  123. package/src/node/web-build.ts +808 -0
  124. package/src/p2.ts +35 -0
  125. package/src/platform-local.ts +211 -0
  126. package/src/playtest.ts +131 -0
  127. package/src/runtime.ts +1509 -0
  128. package/src/systems/cameras.ts +247 -0
  129. package/src/ui-kit/bind.ts +141 -0
  130. package/src/ui-kit/huds.ts +303 -0
  131. package/src/ui-kit/index.ts +36 -0
  132. package/src/ui-kit/menus.ts +187 -0
  133. package/src/ui-kit/settings.ts +131 -0
  134. package/src/ui-kit/store.ts +222 -0
  135. package/src/ui-kit/story.ts +89 -0
  136. package/src/ui-kit/util.ts +106 -0
  137. package/src/web/a11y.ts +429 -0
  138. package/src/web/audio-worklet.ts +193 -0
  139. package/src/web/audio.ts +307 -0
  140. package/src/web/basis.ts +239 -0
  141. package/src/web/gamepad.ts +123 -0
  142. package/src/web/host.ts +166 -0
  143. package/src/web/input.ts +237 -0
  144. package/src/web/lanes.ts +21 -0
  145. package/src/web/play.ts +20 -0
  146. package/src/web/rpc-ring.ts +110 -0
  147. package/src/web/settle.ts +122 -0
  148. package/src/web/shell.ts +643 -0
  149. package/src/web/text-input.ts +248 -0
  150. package/src/web/worker.ts +1002 -0
  151. package/types/bindings.json +13654 -0
  152. package/types/index.d.ts +20090 -0
  153. package/vendor/basis/LICENSE +201 -0
  154. package/vendor/basis/NOTICE +15 -0
  155. package/vendor/basis/README.md +18 -0
  156. package/vendor/basis/basis_transcoder.js +19 -0
  157. package/vendor/basis/basis_transcoder.wasm +0 -0
  158. package/README.md +0 -3
@@ -0,0 +1,1479 @@
1
+ ---
2
+ name: og-game
3
+ description: Building a game on Origin Engine — the project layout, components and systems on the ECS, the game entry, scenes as .ogt text, assets cooked from assets/ to cooked/ (glTF and MagicaVoxel models), the og.load loaders, streaming groups, sprites, texture arrays and drop shadows, skeletal and stepped animation, effects and physical materials, looks (the frame's art direction as data: og.Stylise, og.MaterialStyle, presets), tone mapping, exposure, environment maps, shadows and antialiasing, fog, sky, day and night, weather and water, terrain, material shaders in WGSL, physics beyond rigid bodies (joints, character and platformer controllers, vehicles, ropes, cloth, 2D and 3D fluids, ragdolls) and its queries, and the performance budget; the entry point, with the map of the other nine skills and the modules each teaches. Use first, and when creating, structuring or extending a TypeScript Origin Engine game (a project with a project.ogt; not a Godot project.godot).
4
+ ---
5
+
6
+ An Origin Engine project is plain files: TypeScript in `src/`, source assets
7
+ in `assets/`, text scenes, and a manifest. `npm create @origingame/game`
8
+ produces it; everything after that is code and text, never an editor. This
9
+ skill is where to start: what a game is made of, and which skill to read
10
+ next for each part.
11
+
12
+
13
+ ## What works
14
+
15
+ Three games were written from this package alone: `examples/lantern-vale`
16
+ (a 3D top-down adventure), `examples/hollowbell` (a 2D platformer) and
17
+ `examples/kestrel-stair` (a 3D third-person action-adventure). What they
18
+ asked for the documented way worked the first time: a Tiled map
19
+ with a blob47 auto-tiled terrain, Tiled's parallax and objects with
20
+ properties (§8); flipbook frame events driving footsteps, strikes and a
21
+ bell; texture arrays as world sprites, screen sprites and emitter layers;
22
+ streaming groups of `.vox` models and textures loading as the camera
23
+ neared them (§6); a look applied before `start` and a second one at a tick
24
+ for the results (§15); `og.physics.overlap` on a layer and with sensors;
25
+ a character controller shoving a pile of bodies (§13); a skinned glTF on
26
+ twos (§9). In The Kestrel Stair: one clip library retargeted at cook time
27
+ onto a Mixamo-named, A-posed character, and at run time
28
+ (`og.anim.retarget`) onto an Unreal-named one (its bones detected) and
29
+ onto one whose bones only a `type rig` file names, all three playing
30
+ through blend trees the first time, the clips' `hit` markers coming
31
+ through both retargets (§9); a slam lunging about a metre by root motion,
32
+ `Skeleton.rootMotion` planar for its length; a navmesh baked from the
33
+ level's glTF (og-ai §1); the 3D character controller over triangle-mesh
34
+ slopes, steps and ledges, refusing a scree too steep (§13); a cape, four
35
+ banners in a gusting wind and a chain with a lamp at its last link (§13);
36
+ `world.snapshot()` and `world.restore()` across a relaunch, bringing back
37
+ 93 entities with their cloth, paths and minds (§17). `og cook` took their
38
+ 47, 69 and 62 assets cold in 8.2 s, 3.6 s and 8.5 s, about 0.1 s after a
39
+ change, without a warning, and its refusals named the line and the fix.
40
+
41
+ ## Writing a game from this package
42
+
43
+ The order the games found productive:
44
+
45
+ 1. **`project.ogt`** (§2): name, id, the engine pin, the profile, tick and
46
+ seed. The scaffold writes it; change it there and nowhere else.
47
+ 2. **`GameDesc`** in `src/main.ts` (§3): the scene, the look, the systems,
48
+ and from the start a command for each state a test must reach (`play`,
49
+ `warp:<where>`) and facts for what it must check.
50
+ 3. **Assets by script**: `tools/make-assets.*` writes every texture,
51
+ tileset, map, flipbook, emitter, sound and look deterministically (every
52
+ game and archetype does), with import settings beside each source
53
+ (§5, §8, §10). Rerun it after a change; never hand-edit its outputs.
54
+ 4. **Cook** (`og cook`, og-build §1): read every refusal, it names the line
55
+ and the fix.
56
+ 5. **In 3D, the navmesh before the minds** (og-ai §1): bake it from the
57
+ level (its glTF's `model` line), load it in `start` and check
58
+ `og.nav.nearest` at every spawn and route point; only then write the
59
+ behaviours.
60
+ 6. **Run with commands** (og-build §1, §4): `og run . --headless
61
+ --commands "...;screenshot:x.png;...;quit"`, then open each frame and
62
+ say what it shows. Do this after every change that shows on screen.
63
+ 7. **Playtests with reference frames** (og-build §3): `tests/*.og.ts`
64
+ asserting on facts, `get:` answers and dumps, one per feature, and a
65
+ frames playtest comparing screenshots with `references/<tier>/`, its
66
+ frame reads inside `t.frame`: in a sandbox without a renderer each
67
+ screenshot is reported "no renderer, frame unchecked" and passes while
68
+ every other check runs, and the frames are verified where there is a
69
+ renderer (the author's machine, the browser-check sandbox, an executor
70
+ with a GPU).
71
+ 8. **Polish** (og-build §5): `og polish` until its findings are gone; each
72
+ names the section that fixes it (og-taste §10, og-feel §13).
73
+
74
+ Mistakes the games made, or would have, before reading the right page:
75
+
76
+ - **The resource handle.** A system reads or writes a resource only when
77
+ it lists it among its `reads` or `writes` (og-core §4: undeclared, it
78
+ throws naming the system and the record). A field read or write is a
79
+ typed-array access whatever the resource's size, and `world.resource(R)`
80
+ answers the same handle wherever it is called: keep the run's state in
81
+ one resource and write its fields where they change, never through a
82
+ plain copy (og-core §4 has the cost model: a field read or write about
83
+ 0.03 µs, the first read after an engine call about 2 µs while the record
84
+ is fetched again, writes sent in one call before the next engine call
85
+ and when the system returns).
86
+ - **A look replaces resources whole.** `GameDesc.look` and
87
+ `og.render.applyLook` set every field of each resource the look names
88
+ (the rest to their defaults), so a per-game value written before the
89
+ look is lost: write it in the look, or in `start` after it (§15).
90
+ - **Where probe outputs go.** `og run` writes screenshots and dumps under
91
+ `og-out/` (`--out` chooses), `og test` each playtest's under
92
+ `og-out/test/<name>/`, and a playtest reads them back by the name its
93
+ step gave (`run.json("shop.json")`). Give every step's file its own
94
+ name, so a check reads the step it means (og-build §1, §3).
95
+ - **An agent off the mesh.** Place every navmesh agent at its
96
+ `og.nav.nearest` point (idle or seeking a target on the mesh), and have a
97
+ playtest read each agent's position and `status` a few ticks in: still
98
+ where it was put, its height too, and not `NAV_UNREACHABLE` (og-ai §2).
99
+
100
+ ## 1. The skills
101
+
102
+ | Skill | Read it for | Its modules (`reference/<module>.md`) |
103
+ |---|---|---|
104
+ | **og-core** | the rules every line obeys: TypeScript only, the fixed tick, Results, archetype views, reflection | math, reflect, world, system, rng |
105
+ | **og-game** (this one) | the project, scenes, assets and loading, input, 2D, animation, effects, atmosphere, terrain, shaders, physics, performance, looks, light and shadows | game, transform, input, load, stream, render, physics, atmosphere, anim, tilemap, terrain |
106
+ | **og-build** | the loop: cook, run with a script, read frames and dumps, hot reload, playtests, `og polish` (§5 there: the one list of what it scores), shipping | commands, probe |
107
+ | **og-feel** | how much: hit stop, shake, squash, easing, floating text, the camera, frame rates, sorting, parallax, controller tunables | time, tween, camera, systems |
108
+ | **og-ui** | screens and HUDs: templates, widgets, focus, phones, skins and motion | ui |
109
+ | **og-taste** | the look: a style from the premise, composition, colour, type, motion, feedback, atmosphere by intent, the done checklist | — (it uses ui and atmosphere) |
110
+ | **og-ai** | enemies and NPCs: navigation and minds as data | nav, ai |
111
+ | **og-audio** | sound: events, presets, UI sounds, the mix, music, space | audio |
112
+ | **og-i18n** | languages: string tables, messages, fonts, line breaking, right to left | i18n |
113
+ | **og-platform-sdk** | the platform from inside the game: saves, settings, leaderboards, telemetry, points, AI marks | platform |
114
+
115
+ Each module's page names its skill, and `reference/index.md` groups the
116
+ modules the same way. When two skills seem to touch the same thing, the one
117
+ in this table owns it and the other points there.
118
+
119
+ ## 2. The project
120
+
121
+ | Path | What it is |
122
+ |---|---|
123
+ | `project.ogt` | The manifest: `name`, `id`, the pinned `engine` version, `entry`, `profile` (`2d` or `3d`), `tick`, `seed`, `scenes`, the first `scene`, `publish` settings |
124
+ | `src/main.ts` | The game entry: its default export is `og.game({...})` |
125
+ | `assets/` | Source assets: glTF and `.vox` models, PNG/JPEG textures, WAV sounds, TTF/OTF fonts, `.ogt` scenes and looks, and `<file>.ogt` import settings |
126
+ | `cooked/` | `og cook` output (`.ogb` blobs and `.cook.ogt`); never committed, the only thing the runtime reads |
127
+ | `tests/*.og.ts` | Playtest scripts `og test` runs headless |
128
+ | `dist/` | `og build` output |
129
+
130
+ The engine version in `project.ogt` is a pin. Upgrading it is an explicit
131
+ change with its own run, never a side effect. Its `tick`, `seed`, `scenes`
132
+ and `scene` are the defaults of the `GameDesc` fields of those names: what
133
+ `og.game({...})` leaves unset takes the manifest's value (else 60, 0, none
134
+ and `main`). Write each in one place; set in both to different values, the
135
+ engine refuses to start and names both.
136
+
137
+ `og check` type-checks the project (tsrs, a Rust port of TypeScript 7,
138
+ installed with the engine package; where it has no package, tsgo through
139
+ `bun x`). `og run`, `og test` and `og build` check before they bundle and
140
+ stop on a type error, printing each diagnostic as `file(line,col): error
141
+ TS<code>: message`; with `--json` the diagnostics are a `check` event
142
+ (`{"event":"check","ok":false,"diagnostics":[{"file":"src/main.ts","line":12,"col":7,"code":2322,"message":"..."}]}`)
143
+ on the same stream as the run's other events. `og dev` checks each save
144
+ beside the rebuild: the reload still happens, the `reload` event carries a
145
+ `check` object of `ok`, `ms` and `errors` (`null` while the check runs)
146
+ and a `check` event with the diagnostics follows; `og test --json` ends
147
+ with a `test` event whose `check` object has the same three. Read the
148
+ diagnostics and fix them before anything else; `--no-check` skips the
149
+ check (never for `og build --publish`). A created project's
150
+ `tsconfig.json` resolves `@origingame/engine` (the generated
151
+ `types/index.d.ts`) and `@origingame/engine/playtest` from `node_modules`.
152
+ An example inside the engine repository has no `node_modules`: its
153
+ `tsconfig.json` maps both through `paths` to
154
+ `packages/engine/types/index.d.ts` and `packages/engine/src/playtest.ts`
155
+ (with `packages/engine/src/env.d.ts` and a `host.d.ts` for the `node:`
156
+ modules and globals its bun scripts touch), and `og check .` reads it
157
+ (`examples/hollowbell`, `examples/lantern-vale`).
158
+
159
+ ## 3. Components, resources, systems, the game
160
+
161
+ ```ts
162
+ import { og, defineComponent, defineSystem, f32, Vector3 } from "@origingame/engine";
163
+
164
+ export const Health = defineComponent("Health", { hp: f32(100, { min: 0, max: 1000, tunable: true }), max: f32(100) });
165
+ export const Velocity = defineComponent("Velocity", { v: Vector3() });
166
+
167
+ export const Move = defineSystem("Move", { reads: [Velocity], writes: [og.Transform] }, (q, time) => {
168
+ for (const a of q.archetypes()) {
169
+ const p = a.column(og.Transform).position;
170
+ const v = a.column(Velocity).v;
171
+ for (let i = 0; i < a.count * 3; i++) p[i] += v[i] * time.dt;
172
+ }
173
+ });
174
+
175
+ export default og.game({
176
+ tick: 60,
177
+ scenes: { main: "scenes/main.ogt" },
178
+ systems: [Move],
179
+ commands: { spawn_wave: (world, args) => spawnWave(world, Number(args[0])) },
180
+ commandHelp: { spawn_wave: { usage: "<count>", doc: "Spawns a wave of <count> enemies now." } },
181
+ });
182
+ ```
183
+
184
+ - A **component** is plain data per entity; a **resource** is a reflected
185
+ singleton (tuning tables, game state) read with `world.resource(token)`.
186
+ Engine-owned ones are tokens on `og`: `og.Transform`, `og.Time`, `og.Rng`,
187
+ `og.Input`, `og.Camera`.
188
+ - A **system** is a function over its matched archetypes, once per tick
189
+ (og-core §4 has the rules). Keep each system to one job; share state through
190
+ components and resources, never module variables.
191
+ - **Entities** are spawned with `world.spawn({ name, parent, position })` and
192
+ addressed by name everywhere outside the hot loop: `world.find(name)`, the
193
+ probe's ids, `teleport`, `set`, `dump_entity`. Give every entity a person
194
+ will ask about a stable name.
195
+ - **Commands** in `GameDesc.commands` are snake_case actions the command
196
+ channel and playtests can call (`0.5:spawn_wave:3`), with their usage in
197
+ `GameDesc.commandHelp` for `help` (og-build §1). Add one for each state a
198
+ test needs to reach quickly, instead of playing there.
199
+ - **Facts** (`GameDesc.facts`) return a small JSON object for the probe's
200
+ `state()`: score, phase, lives. Tests assert on facts and dumps.
201
+
202
+ `reference/game.md` is `og.game` and every `GameDesc` field;
203
+ `reference/transform.md` is `og.Transform`, parents and the matrix helpers.
204
+
205
+ ## 4. Scenes are text
206
+
207
+ A scene is an `.ogt` file under `assets/`, cooked like any asset:
208
+
209
+ ```text
210
+ type scene
211
+ version 1
212
+ ---
213
+ name "Main"
214
+ entity "ship" {
215
+ model "models/dropship.glb"
216
+ Transform {
217
+ position 0 1.5 0
218
+ }
219
+ entity "light" {
220
+ Transform {
221
+ position 0 3 0
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ Each `entity` has a name, optional `model` (a glTF under `assets/` whose
228
+ default scene becomes the entity's children), component blocks keyed by the
229
+ component's reflected name with one line per field, and nested child
230
+ entities. Fields not written take their defaults. Edit scenes as text and
231
+ diff them like code; the cook checks field names and lane counts against the
232
+ registered schema and refuses a model path that does not exist.
233
+
234
+ ## 5. Assets: assets/ to cooked/
235
+
236
+ - Put sources in `assets/`; `og cook` turns them into `cooked/`: glTF into
237
+ meshopt-compressed meshes with LODs, textures into KTX2 Basis Universal
238
+ (ETC1S for colour, UASTC for normal and linear maps), WAV into QOA, fonts
239
+ into MSDF atlases, scenes into component tables. Import settings live next
240
+ to the source as `<file>.ogt` (`texture { kind normal }`,
241
+ `font { charset latin chars "开始游戏" }`).
242
+ - At load a cooked texture reaches the GPU block-compressed in the device's
243
+ format with its mips (BC7, and BC5 for normal maps, on desktop GPUs; ASTC
244
+ on Apple and most Android; ETC2 and EAC elsewhere), a quarter of RGBA8's
245
+ memory. Give textures sizes in multiples of 4 (powers of two are best): a
246
+ size that is not, pixel art (`kind pixel`) and fonts stay RGBA8.
247
+ - Declare the characters a font must show (`chars`) rather than cooking a
248
+ whole CJK charset: the full set is several times the size of the font.
249
+ - MagicaVoxel `.vox` files are models: the cook meshes every model of the
250
+ file (greedy quads, flat-shaded, the palette as vertex colours) and its
251
+ scene graph into a glTF, z-up turned y-up, each model about its centre
252
+ (MagicaVoxel's pivot, with or without a scene graph), with one white
253
+ rough material your `og.MaterialStyle` or own material replaces. Load
254
+ and place it as any glTF: `og.load.gltf(world, "models/hut.vox")`, or a
255
+ scene's `model "models/hut.vox"`. The `voxel-worlds` look (§15) suits
256
+ them. Its import settings (`models/hut.vox.ogt`) set the size and the
257
+ origin; without them a voxel is one metre and the origin is the editor's
258
+ (a character 16 voxels tall would be 16 m, its feet 8 m below its
259
+ position):
260
+
261
+ ```text
262
+ type import
263
+ version 1
264
+ ---
265
+ model {
266
+ voxScale 0.1 # metres per voxel (default 1)
267
+ voxOrigin bottom # center (default: the editor's origin) | bottom (the lowest voxel on y = 0)
268
+ }
269
+ ```
270
+
271
+ A `.vox` with frames (MagicaVoxel's animation) cooks to clips that swap
272
+ the frames' models: play them as skinned clips (§9). `voxFps 8` sets the
273
+ rate (default 10), `voxClip walk 0 3` names frames 0 to 3 (repeatable;
274
+ without it one clip `frames` holds every frame). Each frame is centred on
275
+ its own pivot, as in the editor: give the frames one height (or pad them)
276
+ for a character whose feet stay on the ground.
277
+ - Reusable production assets come through the platform's asset catalog into
278
+ `assets/`; record each file's licence next to it.
279
+
280
+ ## 6. Loading
281
+
282
+ `og.load.*` returns a handle at once and completes at a tick boundary; under
283
+ `og run` and in replays every load completes before the next tick, so the
284
+ tick it lands on is deterministic. Paths are asset ids (the path under
285
+ `assets/`). Check `og.load.status` (or the `AssetReady` / `AssetFailed`
286
+ events) before using an asset, and spawn a loaded scene or model with
287
+ `world.instantiate(asset, { name, position })`. The scene in
288
+ `GameDesc.scene` is spawned for you when the world starts. `reference/load.md`
289
+ lists every loader and what each returns.
290
+
291
+ **Streaming groups** (`reference/stream.md`) keep a big world's assets off
292
+ the GPU until the player is near them. A scene names glTF models (no skins
293
+ or morph targets), textures, texture arrays, sounds, environment maps,
294
+ palettes, looks and colour LUTs (`.cube`) that load and unload together,
295
+ and what loads them:
296
+
297
+ ```
298
+ stream "harbour" {
299
+ asset "models/harbour.glb"
300
+ asset "textures/harbour-sign.png"
301
+ asset "audio/gulls.wav"
302
+ distance 0 0 -200 80 # the camera within 80 m of (0, 0, -200)
303
+ unload 120 # kept until past 120 m (1.25 x the radius by default)
304
+ priority 2 # higher loads first and is evicted last
305
+ }
306
+ stream "cave" {
307
+ asset "models/cave.glb"
308
+ volume "cave-mouth" # the camera inside that entity's box
309
+ }
310
+ stream "boss" {
311
+ asset "models/boss-arena.glb" # no trigger: og.stream.request only
312
+ }
313
+ ```
314
+
315
+ Load and spawn grouped assets as usual: until the group is resident a model
316
+ draws its proxy (its coarsest LOD, untextured), a texture is a grey 1x1 and
317
+ a sound plays silence of its own length; then the real ones take over the
318
+ same handles. Physics always collides with the proxy, so the simulation and
319
+ the world hash never depend on what is resident. Texture arrays,
320
+ environment maps, palettes, looks and LUTs have no stand-in: load them once
321
+ the group is resident; when it unloads their handles wait (the sky lights
322
+ the scene, `og.render.sprite` of the array is refused, an `og.Sprite` or a
323
+ particle emitter of it draws nothing, `PostEffects.lut` grades nothing) and
324
+ come back with the group. One the frame still uses when its group unloads
325
+ is kept, with a warning, until the frame stops using it, and its bytes
326
+ count in the budget meanwhile (`og.stream.budget().kept`). Groups are admitted by
327
+ priority within `og.stream.setBudget` (256 MiB by default); a lower-priority
328
+ group is evicted to make room for a higher one. `og.stream.status`,
329
+ `og.stream.budget`, `og.StreamEvent` (state `STREAM_RESIDENT` and the rest)
330
+ and the probe's `dump_stream` say where each group is. `og build --publish`
331
+ ships each group as its own file, fetched only when the group loads.
332
+
333
+ There is no unload call for a single asset: memory comes back when a
334
+ group unloads (its trigger lets go, or `og.stream.release(world, "boss")`
335
+ after `og.stream.request`). Keep the handles you have; they wait and come
336
+ back with the group, never load them again. To let a kept asset go, stop
337
+ using it (`og.Environment.map` or `PostEffects.lut` to 0, another look).
338
+ `StreamStatus` (`og.stream.status(world, "harbour")`) has the group's
339
+ `state`, `trigger`, `memory`, `kept`, `loads` and `reason`; `StreamBudget`
340
+ has `budget`, `used` (kept bytes included), `kept` and `peak`. At most one
341
+ group is put in place per tick boundary, and a group larger than the whole
342
+ budget fails. A chunked sound (og-audio §5) may join a group: its first
343
+ chunk comes in the pack and its other chunks are read ahead of its
344
+ playheads only while the group is resident.
345
+
346
+ ## 7. Input
347
+
348
+ `world.resource(og.Input)` is the keyboard, the mouse and the first touch
349
+ (`down("KeyW")`, `pressed`, `axis`, `buttonDown`). Two more resources carry
350
+ what one pointer cannot: `og.Pointers` (pointer 0 is og.Input's, 1 to 4
351
+ further touches: `down(id)`, `pressed(id)`, `position(id)`, `start(id)`)
352
+ and `og.Gamepad` (up to four pads: `connected(pad)`, `down("a", pad)`,
353
+ `pressed`, `released`, `axis("leftX", pad)`; sticks are y down). All three
354
+ are world state, so playtests and replays reproduce them; script them with
355
+ `touch:<id>:<down|move|up>:<x,y>` and `gamepad:<a+b+...|->[@pad][:lx,ly][:lt,rt]`
356
+ (og-build §1). The fields and methods are `reference/input.md`.
357
+
358
+ A press happens somewhere inside a tick, and the tick reads it at its
359
+ start: `og.KeyDown`, `og.KeyUp`, `og.PointerDown` and `og.PointerUp` carry
360
+ `at`, when it happened in ticks from the start of the tick that reads it
361
+ (-1 to 1; from the browser event's time in a page, from the step's time
362
+ in a command script; 0 for the edges a tick's end raises and for gamepad
363
+ presses, which are polled). Timing that matters to the millisecond (a
364
+ rhythm game's judgement) uses `time.elapsed + e.at * time.dt`; everything
365
+ else ignores it. The stamp is world state, so a replay reproduces it.
366
+
367
+ ## 8. 2D: sprites, flipbooks, tilemaps
368
+
369
+ - **Sprites** are components: an entity with `og.Transform` and `og.Sprite`
370
+ (`width`, `height`, `anchor`, `color`, `opacity`, `layer`, `sortY`,
371
+ `flipX`, `flash`, `shadow`, nine-slice `sliceX`/`sliceY`) is drawn every
372
+ frame at its transform, its scale multiplying the size (so squash and
373
+ stretch is a tween of `og.Transform.scale`). `og.render.sprite` stays for
374
+ one-tick, immediate drawing (HUDs, debug).
375
+ - **Pixel sprites in a 3D set** stand on the ground with `og.Sprite.dropShadow`
376
+ (or `SpriteDesc.dropShadow`): 0.5 casts the sprite's shape, darkened and
377
+ flattened onto the ground plane (y 0) along the sun, the first
378
+ `og.DirectionalLight`, as a 3D model's shadow would fall. It needs a
379
+ perspective camera over the xz ground (a 2D camera sees the plane edge
380
+ on), draws nothing with the sun below the horizon, and set geometry in
381
+ front hides it. `shadow` is the soft blob under the feet instead; the
382
+ `pixel-3d-hybrid` look (§15) wants both.
383
+
384
+ ```ts
385
+ world.insert(hero, og.Sprite, { texture: tex, width: 1, height: 1.5, anchor: { x: 0.5, y: 0 }, dropShadow: 0.5, shadow: 0.3 });
386
+ ```
387
+ - **World UI that must not change with the light** (speech bubbles, name
388
+ tags, markers) takes `og.Sprite.toneMapped: false` (or
389
+ `SpriteDesc.toneMapped`), three.js's `Material.toneMapped`: still placed
390
+ and depth tested in the world, but shown in its colours as given,
391
+ outside auto exposure, bloom, fog and the tone curve, so a cream bubble
392
+ stays cream at dusk instead of blowing out to white. Alpha-blended
393
+ sprites only.
394
+ - **Sprite sheets** are `.ogt` files of `type flipbook` next to the PNG
395
+ (grid or named frames, or an Aseprite / TexturePacker JSON `atlas`; clips
396
+ with `fps`, `mode loop | pingPong | once` and `event <frame> "name"`).
397
+ `og.load.flipbook(world, "sprites/hero.ogt")` answers the sheet and its
398
+ clips; `og.Flipbook { sheet, clip: og.anim.clip(info, "walk") }` on the
399
+ sprite's entity plays it, and `og.FlipbookEvent` reports the frames named
400
+ by `event` (footsteps, the frame a hit lands). Give pixel art
401
+ `texture { kind pixel }` import settings (lossless, nearest).
402
+ - **Texture arrays** batch frames: `og.load.textureArray` (a `type
403
+ texture-array` of same-sized images) and `og.Sprite { array, arrayLayer }`
404
+ (or `og.render.sprite`'s `array`, world or `screen`) draw every sprite of
405
+ one array in one draw whichever layer each shows, glow and blend
406
+ included. A flipbook with `array true` cooks its frames into an array
407
+ (frame n is layer n, one size); a 2D emitter's `array "<id>" random` (or
408
+ `life <cycles>`, `fps <n>`) draws its particles from the layers.
409
+
410
+ ```text
411
+ type texture-array
412
+ version 1
413
+ ---
414
+ kind pixel # color (default), normal, linear or pixel
415
+ layer "tiles/grass.png" # 1 to 256 layers, all one size
416
+ layer "tiles/dirt.png"
417
+ ```
418
+
419
+ ```ts
420
+ const tiles = must(og.load.textureArray(world, "tiles/ground.ogt")); // TextureArrayInfo: array, width, height, layers, bytes
421
+ world.insert(cell, og.Sprite, { array: tiles.array, arrayLayer: 1, width: 1, height: 1 });
422
+ ```
423
+
424
+ `og.Sprite.arrayLayer` past the last layer draws the last, while
425
+ `SpriteDesc.arrayLayer` past it is refused. An array sheet's
426
+ `FlipbookInfo.array` is its array (its `texture` is 0). Emitters take
427
+ arrays in sprite mode only (`render sprite <layer>`), with `round true` to
428
+ fade each particle to a round edge; materials and terrain layers do not
429
+ sample arrays.
430
+ - **Tilemaps**: a tileset `.ogt` (`type tileset`: grid, `solid` tiles,
431
+ `collide` rects, animated tiles, `terrain` auto-tiling rules) and a map:
432
+ a Tiled `.tmj`, an LDtk `.ldtk` or a hand-written `.ogt` of `type tilemap`
433
+ (layers of rows, objects). `og.load.tilemap` answers its layers and
434
+ objects; an entity with `og.Tilemap { map }` draws it from its position
435
+ (top-left corner) and gives the collision layers static colliders.
436
+ `og.tilemap.setTile` edits a cell (terrain layers re-tile their
437
+ neighbours, colliders follow); `getTile` answers the tile there (on a
438
+ terrain layer the one auto-tiling chose, -1 when empty), `cellAt` and
439
+ `cellCenter` convert positions. Layers scroll by their `parallax`;
440
+ `repeat x` tiles a background (`reference/tilemap.md`). Every line of a
441
+ tileset and a map, and what the cook reads of a Tiled map, is the
442
+ grammar at the end of this section.
443
+
444
+ How many frames, how fast, which layer and how much parallax is og-feel
445
+ §9–§11. `templates/archetypes/topdown` (a Zelda-like dungeon: a Tiled map
446
+ with auto-tiled walls and collision, a four-direction flipbook hero on a
447
+ top-down character controller, an enemy with a hurt flash and knockback,
448
+ pickups with floating text, a door to a second room the camera scrolls to),
449
+ `templates/archetypes/runner` and `templates/archetypes/match3` use all of
450
+ this; `templates/archetypes/roguelike` carves a tilemap at run time from
451
+ og.Rng and keeps its fog of war on a layer, and
452
+ `templates/archetypes/tower-defense` writes placements to a map's layer.
453
+
454
+ ### Tilesets, maps and Tiled: the grammar
455
+
456
+ A **tileset** is an `.ogt` of `type tileset` beside its atlas PNG (give the
457
+ PNG `texture { kind pixel }` import settings). Tile ids count from 0, left
458
+ to right and top to bottom of the grid. Every line, with its default:
459
+
460
+ ```text
461
+ type tileset
462
+ version 1
463
+ ---
464
+ texture "tiles/dungeon.png" # the atlas, an asset id (required)
465
+ grid 16 16 8 8 # <tile width px> <tile height px> <columns> <rows> (required)
466
+ margin 0 # pixels round the grid (default 0)
467
+ spacing 0 # pixels between tiles (default 0)
468
+ filter nearest # nearest (default) | linear
469
+ solid 0 1 2 3 # tiles that collide over the whole cell (any number of lines)
470
+ tile 50 {
471
+ collide 0 8 16 8 # a collision rectangle: x y w h in pixels from the tile's top-left, y down
472
+ }
473
+ tile 60 {
474
+ solid # the whole cell (as a `solid` line)
475
+ }
476
+ tile 51 {
477
+ frames 51 52 53 52 # an animated tile: these tiles in turn
478
+ fps 7 # its rate, or `durations 0.1 0.1 0.2 0.1` (seconds, one per frame)
479
+ }
480
+ terrain wall { # an auto-tiling terrain, named by a map layer
481
+ mode blob47 # blob47 (eight neighbours, 47 shapes) | bitmask16 (four edges, 16 shapes)
482
+ rule 0 0 # <neighbour mask> <tile>: one line per shape drawn
483
+ default 46 # the tile for a mask no rule names (required)
484
+ edge filled # filled (default): cells off the map count as filled | empty
485
+ }
486
+ ```
487
+
488
+ The neighbour mask sums the neighbours filled with the same terrain:
489
+ bitmask16 counts N 1, E 2, S 4, W 8; blob47 counts N 1, NE 2, E 4, SE 8,
490
+ S 16, SW 32, W 64, NW 128, a corner only when both its edges are set, so
491
+ exactly 47 masks occur and the cook refuses a `rule` for any other with
492
+ its line. The collision of a tile counts only on a layer marked
493
+ `collision`. `templates/archetypes/topdown/tools/make-assets.py` draws the
494
+ 47 wall shapes and writes their rules (`BLOB`, the sorted reduced masks,
495
+ tile id = index); its `assets/tiles/dungeon.ogt`, the rules shortened:
496
+
497
+ ```text
498
+ type tileset
499
+ version 1
500
+ ---
501
+ # tiles 0-46 are the wall terrain's 47 blob shapes, 47-49 floor, 50 moss, 51-53 the torch, 54-55 the door
502
+ texture "tiles/dungeon.png"
503
+ grid 16 16 8 8
504
+ filter nearest
505
+ solid 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
506
+ tile 51 {
507
+ frames 51 52 53 52
508
+ fps 7
509
+ }
510
+ terrain wall {
511
+ mode blob47
512
+ rule 0 0
513
+ rule 1 1
514
+ rule 4 2
515
+ # ... one rule per reduced mask, in order: 5 7 16 17 20 21 23 28 29 31 64 65 68 69 71 80 81 84 85 87
516
+ # 92 93 95 112 113 116 117 119 124 125 127 193 197 199 209 213 215 221 223 241 245 247 253
517
+ rule 255 46
518
+ default 46
519
+ edge filled
520
+ }
521
+ ```
522
+
523
+ A **hand-written map** is an `.ogt` of `type tilemap` (the runner's and
524
+ the tower-defense archetype's `assets/maps/`):
525
+
526
+ ```text
527
+ type tilemap
528
+ version 1
529
+ ---
530
+ size 32 18 # cells across and down (required)
531
+ tile 16 16 # a tile's pixels (required)
532
+ unit 1 # world units per tile width (default 1)
533
+ tileset "tiles/dungeon.ogt" # one line per tileset, indexed from 0 (at least one)
534
+ layer walls {
535
+ tileset 0 # which tileset (default 0)
536
+ z 5 # sprite layer offset from og.Tilemap.layer (default: the layer's index)
537
+ parallax 0.5 1 # scroll factors (default 1 1; 0 stays with the camera)
538
+ offset 0 0 # world units (default 0 0)
539
+ opacity 1 # (default 1)
540
+ visible true # (default true)
541
+ collision true # its tiles' collision becomes static colliders (default false)
542
+ lit false # the layer takes no 2D light (og.Lighting2D); default true: lit like a sprite in the lit range
543
+ repeat x # x, y or both: tile the layer endlessly (a background)
544
+ terrain wall # a terrain layer: cells are 1 (filled) or 0, the terrain picks the tiles
545
+ legend "#" 1 # a character and its tile id (-1 empty; on a terrain layer 1 filled)
546
+ row "################" # one row per map row, top first, `size` wide; "." is empty unless legended
547
+ # or numeric rows: row 3 3 4 -1 5 (tile ids, -1 empty)
548
+ }
549
+ object door {
550
+ kind "door"
551
+ rect 15 8 1 2 # x y w h in cells (y down); or `at 4 6` for a point
552
+ prop target "room2" # a property, answered as text in TileObject.properties
553
+ }
554
+ ```
555
+
556
+ **Tiled** (`.tmj`, saved as JSON): orthogonal and finite maps, layer data
557
+ as CSV or uncompressed Base64 (a compressed layer is refused with the menu
558
+ to change), horizontal and vertical flips kept (a rotated tile is
559
+ refused), group layers flattened, one tileset per layer. An external
560
+ tileset's `source` resolves against the map's folder and its extension
561
+ becomes `.ogt`: `maps/tomb.tmj` naming `../tiles/dungeon.tsx` reads
562
+ `assets/tiles/dungeon.ogt`, which holds the collision, animation and
563
+ terrains (the `.tsx` is Tiled's own and may sit beside it; the cook
564
+ refuses a missing `.ogt`, naming the path). An embedded single-image
565
+ tileset is imported with its tiles' animations, the first rectangle of
566
+ each tile's collision editor and a boolean `solid` tile property. What the
567
+ cook reads of a layer and an object:
568
+
569
+ <!-- grammar: tiled -->
570
+ | Tiled | Where | Read as |
571
+ |---|---|---|
572
+ | `autotile` | tile layer custom property (string) | the layer is a terrain layer of that terrain of its tileset; any tile in a cell marks it filled, the terrain picks the drawn tile |
573
+ | `collision` | tile layer custom property (bool) | the layer's tile collision becomes static colliders |
574
+ | `lit` | tile layer custom property (bool) | false keeps the layer out of og.Lighting2D (a sky or window layer); default true |
575
+ | `z` | tile layer custom property (int) | the sprite layer offset from og.Tilemap.layer (default: the layer's index among the tile layers) |
576
+ | `repeatx`, `repeaty` | tile layer custom properties (bool), or Tiled's own layer fields | the layer repeats along x or y (`repeat` is not a name the cook reads) |
577
+ | `parallaxx`, `parallaxy` | Tiled's own layer fields (Layer Properties, Parallax Factor) | the layer's parallax |
578
+ | `offsetx`, `offsety` | Tiled's own layer and group fields | the layer's offset, a group's added to its layers' |
579
+ | `opacity`, `visible` | Tiled's own layer fields | as named |
580
+ | `type` or `class` | an object's | TileObject.kind |
581
+ | any other | an object's custom properties | TileObject.properties, every value as text |
582
+ | `solid` | an embedded tileset's tile custom property (bool) | the tile collides as a whole cell |
583
+
584
+ `og cook` warns on every custom property it does not read, naming the
585
+ property, its layer, object or tile and the names read there: a tile
586
+ layer's other than the six names above (or one whose value has another
587
+ type: a `collision` written as the string "true"), a tile's other than
588
+ `solid`, the map's, a group's, an object layer's or an image layer's own
589
+ (none is imported), and an object's class-typed one (no text to give). A
590
+ warning fails nothing; the map cooks without the property.
591
+
592
+ Objects keep their name, size and position (a tile object is anchored at
593
+ its bottom, as Tiled draws it). Positions arrive in map-local world units,
594
+ y up from the map's top-left corner. A map's import settings
595
+ (`maps/tomb.tmj.ogt` beside `maps/tomb.tmj`, for any map format):
596
+
597
+ ```text
598
+ type import
599
+ version 1
600
+ ---
601
+ tilemap {
602
+ unit 1 # world units per tile width (default 1)
603
+ tileset "tiles/x.ogt" # use this tileset for every tileset of the map
604
+ level "Level_0" # LDtk: the level, by identifier (default the first)
605
+ solid 1 2 # LDtk: the IntGrid values that collide (default 1)
606
+ }
607
+ ```
608
+
609
+ `templates/archetypes/topdown/tools/make-assets.py` writes
610
+ `assets/maps/tomb.tmj` (a floor layer, a `walls` layer with `autotile`
611
+ `wall` and `collision` true, a `decor` layer at `z` 15 drawn over the hero,
612
+ and an object layer of spawns, coins, the door and two rooms) beside its
613
+ tileset: the pattern for a map made by a script.
614
+
615
+ ## 9. Animation
616
+
617
+ Skinned glTF models animate through `og.anim` (`reference/anim.md`):
618
+ `og.load.animation(world, "models/walker.glb")` answers the rig's skeleton
619
+ and clips (`og.load.gltfInfo(world, model).animations` lists the clip
620
+ names too); on the instantiated model's entity, `og.Skeleton { asset }`
621
+ poses it and `og.AnimationPlayer { clip, speed, loop, fade }` plays a clip
622
+ (changing `clip` cross-fades over `fade`; `stepFps` holds each pose for
623
+ 1/stepFps seconds: 12 is stop motion on twos, 8 on threes, for clay and
624
+ puppet looks, while events and root motion stay smooth).
625
+ `og.AnimationGraph` runs a blend
626
+ tree with layered state machines registered by `og.anim.graph` (parameters
627
+ such as speed pick the motion; its grammar ends this section), `og.IkChain`
628
+ turns a chain of bones to a target after the clips, `og.Blendshapes` weighs
629
+ morph targets, and `og.AnimationEvent` reports a clip's markers (below).
630
+ An `og.AnimationPlayer` beside a graph plays over it by its `weight`: a
631
+ strike, a bite or a fall at weight 1, back to 0 when `finished`.
632
+ **Root motion** is `og.Skeleton.rootMotion`: `planar` moves the entity's
633
+ Transform by the root joint's xz travel and yaw, `full` by all of it,
634
+ `off` (the default) leaves the hips travelling inside the model; switch it
635
+ to `planar` for a lunge's clip and back to `off` after.
636
+ `templates/archetypes/third-person` (`src/anim.ts`: a 1D blend by speed,
637
+ a hero retargeted at cook time and sentinels at run time) is the pattern,
638
+ and for a 3D action game as a whole: a CharacterController over a level,
639
+ clips retargeted at cook and at run time, blend trees, a navmesh and
640
+ behaviour machines (og-ai).
641
+ `og.anim.clip` finds a clip by name, a model's as a flipbook's (§8):
642
+
643
+ ```ts
644
+ const anim = must(og.load.animation(world, "models/slime.vox"));
645
+ const chase = og.anim.clip(anim, "chase"); // a ClipAsset; 0 when the model has none
646
+ world.insert(slime, og.Skeleton, { asset: anim.skeleton });
647
+ world.insert(slime, og.AnimationPlayer, { clip: chase, loop: true, stepFps: 12 });
648
+ ```
649
+
650
+ A `.vox` with frames (§5) is played the same way: its clips (`frames`, or
651
+ the `voxClip` names) swap the frames' models on the frame boundaries.
652
+
653
+ **Markers** are a clip's named times: the frame a strike lands, a
654
+ footstep. The cook reads them from the glTF, in each animation's extras
655
+ (`animations[i].extras."og.events"`, no other key):
656
+
657
+ ```json
658
+ { "name": "attack", "extras": { "og.events": [{ "name": "hit", "time": 0.42 }] } }
659
+ ```
660
+
661
+ or from the model's import settings, `event "attack" "hit" 0.42` (clip,
662
+ name, seconds; the `animation` block below). A name is 1 to 64 bytes, a
663
+ time inside the clip and an `event` line's clip one of the glTF's own, or
664
+ the cook refuses it naming the clip; a library's clips bring the markers
665
+ the library carries (in its glTF or its own import settings).
666
+ `ClipInfo.markers` lists a clip's marker names in time order (an empty
667
+ list: the glTF carried none). On the tick playback passes one, through a
668
+ player or a graph, `og.AnimationEvent { entity, clip, name, time, weight }`
669
+ fires; a system that lists `og.AnimationEvent` in its `reads` reads them
670
+ with `world.events(og.AnimationEvent)`. Markers survive retargeting, at
671
+ cook time and at run time, so a library's `hit` lands on every character.
672
+
673
+ **Retargeting** — one clip on characters of other proportions and bone
674
+ names. Keep shared clips in an animation library (a glTF with a skeleton
675
+ and clips, no mesh). At cook time, a character's import settings
676
+ (`models/hero.glb.ogt`) pull them in. A model's `animation` block, every
677
+ line with its default:
678
+
679
+ ```text
680
+ type import
681
+ version 1
682
+ ---
683
+ animation {
684
+ rate 30 # resampling rate of every clip, 1 to 120 Hz (default: the source's key rate)
685
+ event "attack" "hit" 0.42 # a marker on a clip of this glTF's own: clip, name, seconds (repeatable; or glTF extras."og.events")
686
+ library "anims/humanoid-clips.glb" # its clips land in this model's #anim, retargeted (repeatable)
687
+ rig "rigs/hero.ogt" # how this model's bones are named (`type rig`); default: detected from the node names
688
+ footIk true # retargeting solves the legs so planted feet stay planted (default true)
689
+ compress false # quantised samples and keyframe reduction (default false: f32)
690
+ tolerance 0.0001 # the most compression may move a sampled value (default 0.0001, at most 0.1)
691
+ }
692
+ ```
693
+
694
+ A clip the model has of its own wins over a library's of the same name.
695
+
696
+ At run time, `og.anim.retarget(world, { clip, source, target })` answers a
697
+ new clip for `target` (both skeletons from `og.load.animation`). The bones
698
+ are found from the node names (Mixamo, VRM, Unity, Rigify, Unreal, Blender);
699
+ when they are not, write a `type rig` .ogt (`scheme mixamo`, or
700
+ `hips "Root_Pelvis"` per bone), load it with `og.load.rig` and pass it as
701
+ `sourceRig`/`targetRig`. `og.anim.humanoid(world, { skeleton })` says which
702
+ bones were found. The target keeps its bone lengths, its hips travel the
703
+ source's distance scaled by the leg ratio (so root motion matches its
704
+ stride), and its legs are solved so planted feet stay on the ground
705
+ (`footIk`, on by default). The fx-showcase `retarget` station is the
706
+ pattern.
707
+
708
+ **Compression** shrinks a model's clips in the bundle (not in memory: they
709
+ are rebuilt at load). In the same `animation` block, `compress true`
710
+ quantises rotations to 48 bits and the rest to 16 and drops the keyframes
711
+ interpolation rebuilds within `tolerance` (default 0.0001: about 0.1 mm
712
+ and 0.01°); a channel 16 bits cannot hold within it stays exact. Turn it on
713
+ for a game with many clips; leave it off while a clip's motion is still
714
+ being judged.
715
+
716
+ ### Blend trees: the graph grammar
717
+
718
+ A graph is `.ogt` text of `type animgraph` passed to `og.anim.graph(world,
719
+ { text, clips })`; each `clip` line names a clip by `ClipInfo.name` among
720
+ `clips` (a model's `og.load.animation(...).clips`, or retargeted ones).
721
+ Motions (clips and blends of them) form a tree; layers run state machines
722
+ over them. Every line, with its default:
723
+
724
+ ```text
725
+ type animgraph
726
+ version 1
727
+ ---
728
+ param speed # a parameter: og.AnimationGraph.p0, the next p1, ... (at most 8)
729
+ param wave
730
+ param x
731
+ param y
732
+ clip idle "idle" # clip <motion> "<clip name>"
733
+ clip walk "walk" {
734
+ speed 1 # duration multiplier, above 0 (default 1)
735
+ loop true # false: holds its last frame (default true)
736
+ }
737
+ clip run "run"
738
+ clip waving "wave"
739
+ blend1d move speed { # blend1d <motion> <param>: the two children around the value blend linearly, clamped at the ends
740
+ child idle 0 # child <motion> <threshold>; any order (at least one)
741
+ child walk 1.5
742
+ child run 4
743
+ }
744
+ blend2d strafe x y { # blend2d <motion> <param x> <param y>: children at points, gradient-band weights
745
+ child walk 0 1 # child <motion> <x> <y>
746
+ child idle 0 0
747
+ }
748
+ layer base { # the first layer is the base: root motion and og.AnimationGraph.state come from it
749
+ state still idle # state <name> <motion>
750
+ state move move {
751
+ speed 1 # playback rate, 0 or more (default 1)
752
+ }
753
+ entry still # the state it starts in (default: the first)
754
+ transition still move { # transition <from | any> <to>; in file order, the first that holds fires
755
+ when speed gt 0.2 # when <param> gt | ge | lt | le | eq | ne <number>; every `when` must hold
756
+ exit 0.9 # and the source has played this many cycles (a `when` or an `exit` is required)
757
+ fade 0.25 # cross-fade seconds (default 0.2)
758
+ }
759
+ transition any still { # any: from every state but the target
760
+ when speed le 0.2
761
+ }
762
+ }
763
+ layer upper {
764
+ mode additive # override (default): replaces the pose below | additive: adds each clip's difference from its first frame
765
+ weight wave # a number 0 to 1, or a parameter clamped to it (default 1)
766
+ state wave waving
767
+ }
768
+ ```
769
+
770
+ Each state advances one phase by `dt × AnimationGraph.speed × state speed
771
+ / duration`, the duration weighted across a blend's clips, so blended
772
+ clips stay in step. Per tick and layer the transitions are checked first,
773
+ then the phases advance, then the poses blend; a transition during a fade
774
+ replaces it. `og.anim.graph` refuses a parse error naming the line, a
775
+ clip name `clips` lacks, a parameter not declared and a motion that
776
+ contains itself. Write the parameters from a system every tick
777
+ (`world.set(body, og.AnimationGraph, { p0: speed })`); `state` and
778
+ `stateTime` read back the base layer's.
779
+
780
+ The third-person archetype's hero (`src/anim.ts`): idle, walk and run by
781
+ the speed across the ground, and the run held mid-stride, slowed, in the
782
+ air:
783
+
784
+ ```text
785
+ type animgraph
786
+ version 1
787
+ ---
788
+ param speed
789
+ param air
790
+ clip idle "idle"
791
+ clip walk "walk" {
792
+ speed 1.1
793
+ }
794
+ clip run "run"
795
+ blend1d move speed {
796
+ child idle 0
797
+ child walk 2.2
798
+ child run 5.6
799
+ }
800
+ layer base {
801
+ state ground move
802
+ state air run {
803
+ speed 0.3
804
+ }
805
+ entry ground
806
+ transition ground air {
807
+ when air gt 0.5
808
+ fade 0.15
809
+ }
810
+ transition air ground {
811
+ when air lt 0.5
812
+ fade 0.1
813
+ }
814
+ }
815
+ ```
816
+
817
+ ```ts
818
+ const anim = must(og.load.animation(world, "models/strider.glb"));
819
+ const graph = must(og.anim.graph(world, { text: HERO_GRAPH, clips: anim.clips }));
820
+ world.insert(body, og.Skeleton, { asset: anim.skeleton });
821
+ world.insert(body, og.AnimationGraph, { asset: graph });
822
+ // every tick: world.set(body, og.AnimationGraph, { p0: groundSpeed, p1: airborne ? 1 : 0 });
823
+ ```
824
+
825
+ ## 10. Effects
826
+
827
+ Every effect is off until the game turns it on, as data:
828
+
829
+ - **Post** — the `og.PostEffects` resource: `ao`, `outline`,
830
+ `depthOfField`, `motionBlur`, `godRays`, `glow`, `lut` (a handle from
831
+ `og.load.lut(world, "luts/look.cube")`), `vignette`,
832
+ `chromaticAberration`, `filmGrain`, `flash`, and a fixed shake of the
833
+ frame through `shakeTrauma` (an earthquake that lasts; a hit's shake is
834
+ `og.camera.shake`, which decays by itself: og-feel §4). A game with fast
835
+ motion wants `render: { antialias: "taa" }` in `og.game`.
836
+ - **Particles** — an emitter is an `.ogt` asset of `type particles`
837
+ (shape, rate, bursts, lifetime, speed, curves over life, texture or
838
+ array, 3D or sprite rendering; every line is the grammar at the end of
839
+ this section) loaded with `og.load.emitter`; an entity with `og.ParticleSystem
840
+ { emitter, seed, startTick }` emits it at its transform. Particles are
841
+ drawn by the renderer from the tick, deterministic and never part of the
842
+ simulation.
843
+ - **Trails and beams** — components `og.Trail` (follows the entity) and
844
+ `og.Beam` (between two points); `og.render.ribbon(world, desc)` draws a
845
+ polyline this tick.
846
+ - **Decals** — `og.Decal` projects an atlas rectangle
847
+ (`og.RenderSettings.decalAtlas`) onto what its box covers.
848
+ - **2D lighting** — `og.Lighting2D` (ambient, the lit layer range),
849
+ `og.Light2D` and `og.Occluder2D` components, `castShadow` on sprites;
850
+ sprites also take `flash`, `glow` and `blend`.
851
+ - **Materials** — toon, rim, dissolve, hit flash and outline are
852
+ `og.render.standardMaterial` fields; animate them with
853
+ `og.render.updateMaterial(world, material, { dissolve: t })`. Beside
854
+ three.js's standard fields (metalness, roughness, emissive, `normalMap` in
855
+ glTF's tangent frame, `alphaTest`, `transparent`, `side: "double"`,
856
+ `unlit`), three.js `MeshPhysicalMaterial`'s fields work on every tier:
857
+ `clearcoat` (with `clearcoatMap`, `clearcoatNormalMap`) for car paint and
858
+ varnish, `sheen` and `sheenColor` for cloth and velvet, `transmission`
859
+ with `thickness`, `ior` and `attenuationColor` for glass and water that
860
+ refract what is behind them, `iridescence` for soap film and oil,
861
+ `anisotropy` for brushed metal; a glTF's `KHR_materials_*` extensions
862
+ load into them. A non-uniform `scale` shades correctly. Set `sheenColor`
863
+ with `sheen` (black is no sheen); `thickness` is in the mesh's units and
864
+ `attenuationDistance` 0 absorbs nothing. A transmissive material refracts
865
+ only what was drawn before it (not other glass or water), and unlit and
866
+ toon materials ignore the physical fields:
867
+
868
+ ```ts
869
+ const amber = og.render.standardMaterial(world, { color: { r: 1, g: 1, b: 1 }, roughness: 0.02, transmission: 1, thickness: 1, ior: 1.5, attenuationColor: { r: 0.95, g: 0.45, b: 0.08 }, attenuationDistance: 0.35 });
870
+ ```
871
+ - **Texture repeat** — three.js's `texture.repeat` is the geometry's
872
+ `uvRepeat` here: `og.render.planeGeometry`, `boxGeometry` and
873
+ `sphereGeometry` scale their UVs from 0..1 to 0..`uvRepeat`, and material
874
+ textures sample with repeat wrapping, so the texture tiles. A ground grid
875
+ every 10 m on a 1,600 m plane (`references/metal/ground-grid.png` in
876
+ og-tests):
877
+
878
+ ```ts
879
+ const ground = must(og.render.planeGeometry(world, { width: 1600, height: 1600, uvRepeat: { x: 160, y: 160 } }));
880
+ const grid = og.render.standardMaterial(world, { map: must(og.load.texture(world, "textures/grid.png")), roughness: 0.9 });
881
+ // turn it -pi/2 about x for a floor
882
+ ```
883
+ - **HUD** — `og.render.sprite` / `og.render.text` with `screen: true` draw
884
+ in CSS pixels from the top-left over the finished frame, untouched by
885
+ post effects, with an optional `clip`. `og.ui` draws in the same
886
+ overlay at `UI_LAYER` (1000): screen sprites and text at a layer up to
887
+ 1000 draw under every UI element, above 1000 over every one (modals and
888
+ the screen transition too), so a HUD icon made of sprites sits on a UI
889
+ panel at `layer: UI_LAYER + 1` while its screen shows.
890
+
891
+ `templates/fx-showcase` shows every one of these on its first five stations;
892
+ every field is in `reference/render.md`. Floating text,
893
+ `og.render.floatingText(world, { text: "-12", position, color })`, pops a
894
+ damage number or a pickup that rises and fades by itself (amounts: og-feel
895
+ §7).
896
+
897
+ ### Particle emitters: the grammar
898
+
899
+ An emitter is an `.ogt` of `type particles` under `assets/` (`fx/` by
900
+ custom). Every line is optional; what is left out keeps its default (a
901
+ small fountain of soft white points). A `<min> <max>` pair is a random
902
+ range per particle, one number is both:
903
+
904
+ ```text
905
+ type particles
906
+ version 1
907
+ ---
908
+ maxParticles 256 # the ring: the newest overwrite the oldest (1 to 1048576)
909
+ shape cone 15 0.05 # point | sphere <radius> [surface] | cone <angle°> <radius> | box <x> <y> <z> (half extents)
910
+ spread 0 # degrees of random deviation from the shape's direction (180: every way)
911
+ rate 40 # particles per second; 0 for bursts only
912
+ burst 0 30 # <time s> <count> [<cycles> <interval s>]: at that time of each cycle (repeatable)
913
+ duration 1 # seconds per emission cycle
914
+ looping true # false: one cycle, then the particles live out
915
+ lifetime 1 1.5 # seconds
916
+ speed 1.5 2.5 # units per second, along the shape's direction
917
+ size 0.08 0.12 # the quad's side in world units
918
+ sizeOverLife 0 1 1 0 # <t> <value> pairs, t 0 (birth) to 1 (death), at most 8; multiplies size (default 1)
919
+ colorOverLife { # keys <t> <r> <g> <b> <a>, linear, at most 8 (default: white, fading over the last quarter)
920
+ key 0 1 1 1 1
921
+ key 1 1 1 1 0
922
+ }
923
+ color 1 1 1 1 # a tint, <r> <g> <b> [<a>]; above 1 blooms
924
+ rotation 0 0 # start rotation, degrees
925
+ angularVelocity 0 0 # degrees per second
926
+ gravity 0 -2 0 # acceleration, units per second²
927
+ drag 0 # velocity scales by 1 / (1 + drag × dt)
928
+ noise 0 0.5 0.3 # <strength> [<frequency> <scroll>]: a moving turbulence
929
+ texture "fx/spark.png" # an asset id; left out: a soft round dot
930
+ sheet 4 4 16 life 1 # <columns> <rows> [<frames> [off | life <cycles> | fps <n> | random]]
931
+ blend additive # additive (fire, sparks, magic) | alpha (smoke, dust)
932
+ space world # world: born at the emitter, then free | local: carried with it
933
+ align camera # camera | velocity <stretch>: stretched along its motion (sparks, rain)
934
+ softDistance 0 # fade where the quad nears the scene behind it (0: off)
935
+ collision 0.4 0.2 # <bounce> <friction>: bounce off the depth buffer (3D billboards, world space)
936
+ render billboard # billboard (3D) | sprite <layer>: 2D, drawn in the sprite batch on that layer
937
+ glow 0 # sprite mode: strength in the 2D glow
938
+ array "fx/smoke.ogt" random # sprite mode: draw from a texture array, [<layers>] [off | life <cycles> | fps <n> | random]
939
+ round false # sprite mode: fade each particle to a round edge over its texture or layer
940
+ ```
941
+
942
+ - Directions: `point`, `cone` and `box` emit along the entity's +y
943
+ (`spread 180` sends a point every way), `sphere` outwards. In a 2D game
944
+ set `render sprite <layer>`: positions stay in the xy plane, sizes and
945
+ speeds are in world units like a sprite's, and the particles take the
946
+ sprite batch's order and glow like any sprite.
947
+ - A one-shot effect (a hit, a pickup, landing dust) is `rate 0`,
948
+ `looping false`, one `burst 0 <count>` and a short `duration`; restart
949
+ it by writing the tick to its `og.ParticleSystem.startTick` (moving the
950
+ entity first). A lasting one (embers, motes, rain) keeps the defaults
951
+ `looping true` and a `rate`.
952
+ - `sheet` animates a sprite sheet texture: `life 1` plays its frames once
953
+ over each particle's life, `fps 12` at a rate, `random` one frame per
954
+ particle. `array` does the same over a texture array's layers (§8), one
955
+ draw for the whole emitter; a billboard emitter with an array is refused.
956
+ - `collision` bounces 3D billboards off what the depth buffer shows on the
957
+ full tier (sparks on a floor); sprite-mode emitters skip it. It is a
958
+ look, never gameplay: particles never touch the simulation.
959
+ - The cook refuses an unknown line, a missing texture or array and a
960
+ value of the wrong shape, naming the line.
961
+
962
+ A hit's sparks in a 2D game (Hollowbell's `fx/hit.ogt`):
963
+
964
+ ```text
965
+ type particles
966
+ version 1
967
+ ---
968
+ maxParticles 24
969
+ shape point
970
+ spread 180
971
+ rate 0
972
+ burst 0 10
973
+ duration 0.1
974
+ looping false
975
+ lifetime 0.18 0.32
976
+ speed 3 6
977
+ size 0.12 0.22
978
+ sizeOverLife 0 1 1 0
979
+ colorOverLife {
980
+ key 0 1 0.95 0.75 1
981
+ key 1 0.95 0.55 0.2 0
982
+ }
983
+ drag 4
984
+ texture "fx/spark.png"
985
+ blend additive
986
+ render sprite 12
987
+ ```
988
+
989
+ `templates/archetypes/idle/assets/fx/` (taps, stars, a milestone),
990
+ `templates/fx-showcase/assets/fx/` (fire, smoke, sparks with collision,
991
+ 2D embers) and `templates/archetypes/third-person/assets/fx/` hold more.
992
+
993
+ ## 11. Atmosphere, water and terrain
994
+
995
+ `og.atmosphere` is data too, and off until set (`reference/atmosphere.md`):
996
+
997
+ - **Fog** — the `og.Fog` resource: `enabled`, distance fog (`density` per
998
+ metre from `start`, or a linear ramp to `end`), height fog
999
+ (`heightDensity` below `height`, thinning by `falloff`), `sunColor` for
1000
+ the glow looking into the sun, `sky` for how much of the sky it covers.
1001
+ - **Sky** — `og.Sky { enabled, zenith, horizon, ground, sunDirection,
1002
+ sunColor, sunSize, glow, stars }` draws the sky per pixel and lights the
1003
+ scene by it; `moonSize` (degrees, 0 none), `moonPhase` (0 new, 0.25 first
1004
+ quarter, 0.5 full, 0.75 last quarter), `moonIntensity`, `moonColor` and
1005
+ `moonDirection` (zero: opposite the sun) add a moon disc over the stars.
1006
+ - **Day and night** — `og.DayNight { enabled, start, speed, tilt, azimuth,
1007
+ moonSize, moonPhase }`: the clock is `start + seconds × speed` hours from
1008
+ the tick, so it replays; it drives the first `og.DirectionalLight` (the
1009
+ sun by day, a dim moon by night, where the moon disc hangs), the sky, the
1010
+ ambient light and the fog's tint. Read the clock with
1011
+ `og.atmosphere.timeOfDay(world)` (declare `og.DayNight` among the system's
1012
+ reads) to light lamps at dusk.
1013
+ - **Weather** — `og.Weather { kind: WEATHER_RAIN | WEATHER_SNOW |
1014
+ WEATHER_DUST | WEATHER_MIST, intensity, wind, radius }`: particles around
1015
+ the camera, the preset's fog (an enabled `og.Fog` wins) and dimmed light.
1016
+ With an orthographic camera the particles are 2D sprites on `layer`.
1017
+ - **Water** — `og.Water` on an entity draws a surface on its xz plane
1018
+ (`size`, `shallow`/`deep` colours over `depthFade`, `foamWidth` where it
1019
+ meets geometry, `waveStrength`, `reflectivity`); with an
1020
+ `og.WaterSurface` on the same entity its waves move the surface and bodies
1021
+ splash it (`og.Splash` events) and float. In 2D, `og.WaterLine` with
1022
+ `og.Water2D` is a side-on pool drawn in the sprite order. Three more
1023
+ cost frame time and are off until set: `planarReflection` (0.25 to 1, the
1024
+ share of the screen's resolution) mirrors the scene in a still lake or a
1025
+ pool you look across, where the sky reflection alone looks fake;
1026
+ `shadows` (0 to 1) darkens it under the cascaded shadow (a pier, a cliff);
1027
+ `caustics` throws rippling light on what lies below it. With the camera
1028
+ below the surface the frame's fog becomes `underwaterColor` at
1029
+ `underwaterDensity` and the surface from below shows the sky only
1030
+ straight up (on by default; set the density to 0 to see through).
1031
+
1032
+ **Terrain** (`reference/terrain.md`) is a height field cooked from a
1033
+ `type terrain` .ogt beside a 16-bit grey PNG (or an EXR in metres):
1034
+
1035
+ ```text
1036
+ type terrain
1037
+ version 1
1038
+ ---
1039
+ heightmap "island.png" # 16-bit grey: black and white are `height`'s ends
1040
+ height -20 120 # metres
1041
+ size 2048 2048 # metres along x and z, centred on the entity
1042
+ control "island-control.png" # optional RGBA: layer 0 in R, 1 in G, 2 in B, 3 in A
1043
+ heightBlend 0.15 # sharpen the blends by each albedo's alpha (its height)
1044
+ layer { albedo "textures/grass.png" normal "textures/grass-n.png" tile 6 slope 0 30 }
1045
+ layer { albedo "textures/rock.png" tile 12 triplanar true slope 28 90 }
1046
+ layer { albedo "textures/snow.png" altitude 90 1000 altitudeFade 10 }
1047
+ ```
1048
+
1049
+ A layer shows where its control channel (1 without a map) times its slope
1050
+ band (degrees) times its altitude band (metres above the terrain's entity)
1051
+ is high; give cliffs a `triplanar` layer so the texture does not stretch.
1052
+ In the game, load it in `start` and place it:
1053
+
1054
+ ```ts
1055
+ const info = must(og.terrain.load(world, "terrain/island.ogt"));
1056
+ const ground = must(world.spawn({ name: "island", position: { x: 0, y: 0, z: 0 } }));
1057
+ world.insert(ground, og.Terrain, { data: info.data });
1058
+ world.insert(ground, og.Collider, { shape: SHAPE_HEIGHTFIELD, mesh: info.geometry });
1059
+ ```
1060
+
1061
+ It is drawn around the camera at finer detail nearby (casting and
1062
+ receiving shadows, under the fog), placed by its entity's world transform
1063
+ (turn or scale the entity and the terrain turns and scales with it), and
1064
+ `og.CharacterController` walks on it up to `maxSlope`.
1065
+ `og.terrain.sample(world, { data, origin, quaternion, scale, x, z })`
1066
+ (the entity's position, rotation and scale; the last two may be left out
1067
+ for an unturned, unscaled terrain) gives the height, normal and slope
1068
+ anywhere (to place trees, spawn on the ground, or check a slope); `og.terrain.edit` raises, lowers, flattens,
1069
+ smooths or paints under a round brush (craters, building pads, a paint
1070
+ tool) and `og.terrain.layer` / `setLayer` retune a layer live. Edits are
1071
+ world state: a snapshot keeps them and restoring it brings the ground
1072
+ back as it was then. Grass, flowers and stones are an `og.TerrainScatter` on the
1073
+ terrain's entity: `{ data, geometry, material, layer, density, distance }`
1074
+ grows copies of the mesh where that layer shows, within `distance` of the
1075
+ camera (thinning out over its outer `fade`, never shrinking), each fixed to its spot
1076
+ as the camera moves. Keep `density × distance²` modest: 4 a square metre
1077
+ within 60 m is about 36,000 copies. A navmesh .ogt's `terrain "terrain/island.ogt" { step 4 }` bakes
1078
+ the walkable slopes for og-ai. Lower `lodRing` on og.Terrain to trade
1079
+ detail for frame time.
1080
+ Its tiles stream: only the tiles the camera needs are on the GPU (16 MiB a
1081
+ terrain by default), decoded off the frame, the coarser level standing in
1082
+ until each arrives, and nothing a tick reads changes with it. In a probe
1083
+ script `terrain_stream:resident` decodes every wanted tile each frame (for
1084
+ captures to compare), `terrain_stream:stream:<MiB>` streams within that
1085
+ budget, and `dump_stream` gives the terrain's tiles.
1086
+
1087
+ Which look for which mood is og-taste §8.
1088
+
1089
+ ## 12. Material shaders
1090
+
1091
+ A material can run your own WGSL in four hooks of the lit shader. Write the
1092
+ hooks in a `.wgsl` file and declare it in an `.ogt` of `type shader`:
1093
+
1094
+ ```text
1095
+ type shader
1096
+ version 1
1097
+ ---
1098
+ source "shaders/dissolve.wgsl"
1099
+ param amount float 0
1100
+ param edgeColor color 4 1.6 0.4
1101
+ ```
1102
+
1103
+ ```wgsl
1104
+ // Any of the four; the others stay the identity.
1105
+ fn vertex_offset(h: Hook, p: Params) -> vec3<f32> // world-space offset
1106
+ fn surface_color(h: Hook, p: Params, c: vec4<f32>) -> vec4<f32> // base colour and alpha
1107
+ fn surface_normal(h: Hook, p: Params, n: vec3<f32>) -> vec3<f32> // world normal, unit length
1108
+ fn surface_emissive(h: Hook, p: Params, e: vec3<f32>) -> vec3<f32> // emitted light
1109
+ // h: world, local (object-space) position, normal, uv, color, time (s).
1110
+ // p: the declared parameters by name (p.amount). value_noise3(p, seed) is
1111
+ // the engine's 3D value noise (0..1).
1112
+ ```
1113
+
1114
+ The cook compiles every shader for every back end (WebGPU, Metal, Vulkan,
1115
+ and GLSL ES for WebGL2) and refuses one that does not compile with naga's
1116
+ message, its lines counted in your hooks. In the game:
1117
+ `og.render.shaderMaterial(world, must(og.load.shader(world,
1118
+ "shaders/dissolve.ogt")), { color, alphaTest: 0.5 })` makes the material,
1119
+ `og.render.setShaderParam(world, material, "amount", [0.4])` sets a
1120
+ parameter, and `og.render.setMaterialShader(world, material, shader)` gives
1121
+ an existing material (a loaded model's, from `GltfInfo.materials`) the
1122
+ hooks. Alpha below `alphaTest` discards the fragment (a dissolve). Skinned
1123
+ meshes run the hooks too, and shadows and the depth prepass see the mesh
1124
+ where `vertex_offset` moved it. `templates/fx-showcase/assets/shaders/` has a
1125
+ dissolve, wind-swayed grass and a ripple on the walking walker.
1126
+
1127
+ ## 13. Physics
1128
+
1129
+ `og.RigidBody` and `og.Collider` on an entity make it a body in the
1130
+ profile's simulation (rapier3d in 3D, rapier2d in 2D); contacts and sensors
1131
+ arrive as events, and `og.physics` answers queries. `og.Velocity` is
1132
+ optional: the step adds it to a dynamic body that has none, and a kinematic
1133
+ body (`BODY_KINEMATIC`) moves by it each tick, its Transform written back:
1134
+ a lift is a kinematic body with a Velocity whose system only turns the
1135
+ velocity round at the ends, and a PlatformerController standing on it
1136
+ rides along. A kinematic body without one stays where its Transform puts
1137
+ it (move that by hand), and writing a moving one's Transform teleports it
1138
+ there. `world.set(e, og.Velocity, ...)` refuses on
1139
+ an entity without one (a body in `start`, before its first step):
1140
+ `world.insert` it there, which adds and sets it.
1141
+
1142
+ ```ts
1143
+ const hit = og.physics.raycast(world, { origin: eye, direction: { x: 0, y: 0, z: -1 }, maxDistance: 30, filter: { mask: 0b10, exclude: player } });
1144
+ if (hit.ok && hit.value) { /* hit.value.entity, distance, point, normal */ }
1145
+ const under = og.physics.shapeCast(world, { shape: { shape: SHAPE_SPHERE, radius: 0.4 }, position: feet, direction: { x: 0, y: -1, z: 0 }, maxDistance: 2 });
1146
+ const near = og.physics.overlap(world, { shape: { shape: SHAPE_BOX, size: { x: 4, y: 2, z: 4 } }, position: door }); // entities, by id
1147
+ ```
1148
+
1149
+ Push a dynamic body with `og.physics.applyForce(world, e, force, point?)`
1150
+ (newtons over the next step: call it every tick for thrust, wind or a
1151
+ car's downforce), `applyImpulse(world, e, impulse, point?)` (newton-seconds
1152
+ at once: a kick, a hit, a jump; its velocity changes by impulse / mass),
1153
+ `applyTorque` and `applyTorqueImpulse` (about each world axis; 2D uses z).
1154
+ A point off the centre of mass spins it too. They apply at the og.physics
1155
+ step after the systems, in the order given; a hit stop drops forces and
1156
+ keeps impulses for after it; those not yet applied are in snapshots.
1157
+ `og.RigidBody.centerOfMass` moves the centre of mass (a roly-poly toy, a
1158
+ car that keeps its wheels down).
1159
+
1160
+ Inside a system the queries see the colliders as they were at the start of
1161
+ the tick (`og.physics.queries` records them before the game's systems), so
1162
+ the answer does not depend on system order; `QueryFilter.includeSensors`
1163
+ adds sensors. The step itself runs on the lanes (§14) with the same result
1164
+ as one thread. Beyond rigid bodies, all are components over the same simulation,
1165
+ deterministic and part of snapshots (`reference/physics.md`):
1166
+
1167
+ - `og.Joint` — fixed, hinge (`JOINT_REVOLUTE`, limits, a velocity or
1168
+ position motor), slider, spring, rope, ball; to another body (`other`) or
1169
+ a world point (`otherAnchor`); `value` reads the angle or slide.
1170
+ - `og.CharacterController` (3D, or top-down with `gravityScale` 0) — set
1171
+ `move` every tick and `jump` to jump; it climbs `maxSlope` and
1172
+ `stepHeight`, snaps to the ground, rides moving platforms, slides along
1173
+ walls, pushes bodies; read `grounded`, `velocity`, `ground`. Its
1174
+ og.Collider's `layers` and `mask` decide what blocks it, as for bodies
1175
+ (a ghost through a gate: leave the gate's layer out of its `mask`); the
1176
+ same for the PlatformerController.
1177
+ - `og.PlatformerController` (2D) — `moveX`, `jump`, `down` in; jump height
1178
+ and time to apex, coyote time, jump buffer, wall jumps, one-way layers,
1179
+ corner correction as tunables (og-feel §12 has values by genre);
1180
+ `impulse` (m/s, taken once on the next step) knocks it back or launches
1181
+ it;
1182
+ `og.CharacterEvent` reports jumps and landings for dust and sounds.
1183
+ `templates/archetypes/runner`'s courier is one.
1184
+ - `og.Vehicle` — a raycast car (3D, `throttle`, `steer`, `brake`; wheels
1185
+ from `og.physics.vehicleWheels`) or a car on wheel joints (2D). Its
1186
+ defaults drive a 1,000 kg road car: set the chassis's
1187
+ `og.RigidBody.mass` (og-feel §12 has the numbers for a racer, a truck
1188
+ and a kart). Its wheel rays meet only what its chassis's og.Collider
1189
+ `layers` and `mask` meet, so a ghost car on its own layer drives through
1190
+ the player's. `rollInfluence` 0 (the default) never tips it over in a
1191
+ bend; `og.RigidBody.centerOfMass` lowers its weight. It writes `speed`,
1192
+ `contacts` and `slip` (the most any tyre slides, 0 to 1); each wheel's
1193
+ `slip` and `sideSpeed` come with its pose, for smoke, skid marks and a
1194
+ squeal.
1195
+ - `og.Rope` and `og.Cloth` — Verlet, drawn by the engine when `visible`.
1196
+ A cloth that folds or piles up (a dropped sheet, a cape on the ground)
1197
+ wants `thickness` about its `spacing` so it never passes through
1198
+ itself, `substeps` 2–4 and `friction` near 1 so it lies where it lands
1199
+ (self-collision costs about 1 ms a tick per 1,000 points).
1200
+ - `og.Fluid2D` — a particle fluid drawn as metaballs on a sprite layer.
1201
+ Give two or more fluids the same `group` and they interact: by
1202
+ `density` the heavier sinks under the lighter (oil on water), by
1203
+ `mixing` 0 they stay in layers and by 1 they blend, colour and weight
1204
+ evening out where they touch; a group draws as one surface. Up to
1205
+ sixteen fluids draw a frame (four to a group).
1206
+ - `og.Fluid3D` — a 3D particle fluid (3D profile) drawn as one
1207
+ screen-space surface that refracts and tints what lies behind it,
1208
+ reflects the sky and takes the scene's lights. Give it `bounds` (a
1209
+ container around the entity) or colliders and terrain to hold it; an
1210
+ emitter (`emitRate`, `emitOffset`, `emitVelocity`, up to `maxCount`) pours
1211
+ and a sink box (`sinkSize`, `sinkOffset`) drains. Groups work as in 2D:
1212
+ `density` 3 under 1 gives honey under water, `mixing` 1 blends their
1213
+ colours. Look: `color`, `opacity` per metre, `reflectivity`,
1214
+ `refraction`, `smoothing` (0 shows the beads); fluids drawn in one frame
1215
+ share one surface and the first fluid's look. Cost is the particles:
1216
+ about 0.9 ms of CPU a tick for 2,000 at one substep and 1.8 ms at the
1217
+ default two (native and wasm alike), 4 ms for 4,000 and 10 ms for 8,000
1218
+ at two, roughly double that when they pile deep, so budget a scene to a
1219
+ few thousand; the surface costs about 1.1 ms on the GPU whatever the count,
1220
+ growing with the pixels it covers. `og.physics.fluid3dParticles` reads
1221
+ the positions (the fx-showcase `fluids3d` station).
1222
+ - `og.Ragdoll` — give the entity a rig
1223
+ (`og.physics.setRagdollRig(world, e, must(og.physics.ragdollRigFromSkeleton(world, anim.skeleton)))`),
1224
+ then set `weight` 1 to go limp. At weight 0 its bodies follow the
1225
+ animation's pose every tick, so the fall starts from the pose and its
1226
+ velocity; the model is drawn blended from the animation to the bones over
1227
+ `blendTime`. In a 2D world (`og.Physics.profile` 2) the same works for a
1228
+ side-on skinned model whose bones lie in the xy plane: the bones are
1229
+ rapier2d bodies on hinges about z (the fx-showcase `ragdoll2d` station).
1230
+
1231
+ Readouts (`ropePoints`, `clothPoints`, `fluidParticles`,
1232
+ `fluid3dParticles`, `waterHeight`, `vehicleWheels`, `ragdollPose`) show
1233
+ the last step. `templates/fx-showcase` has a station for each.
1234
+
1235
+ ## 14. Performance
1236
+
1237
+ A frame is 16.7 ms at 60 Hz; keep step + render under half of it on a
1238
+ desktop so phones have room. `og run --json` prints `advanceMs`, the
1239
+ run's `frames` (step and render median, p95 and max) and its `systems`
1240
+ (each system's mean milliseconds a tick, slowest first: the game's and the
1241
+ engine's own, physics, the UI's end of frame, animation and the rest, which the
1242
+ probe's `dump_world` also reports as `ms`), `og
1243
+ polish` the p95 step and render time of every playtest (og-build §5), and
1244
+ `og run --device` the first frame and frame times in a real browser or
1245
+ phone (og-build §6). The usual costs:
1246
+ draws (share geometry and one material per look), particle systems (cap
1247
+ `maxParticles` in the emitter's .ogt, despawn emitters that are done),
1248
+ per-tick TypeScript over many entities (iterate archetype columns, never
1249
+ `world.get` per entity; spread expensive work over ticks, as og.ai's
1250
+ `thinkEvery` does), and loads in the middle of play (load in `start`).
1251
+ Point and spot lights are clustered (Forward+): a pixel shades only the
1252
+ lights whose `distance` reaches it, so a few hundred lamps, torches or
1253
+ neon signs cost little; keep each `distance` as short as the look allows.
1254
+ Up to `MAX_LIGHTS` (256) in view are drawn, the brightest kept past that,
1255
+ and up to `RenderCapabilities.maxShadowedLights` with `castShadow` (8 on
1256
+ the full tier, 4 on WebGL2) cast shadows (shadows are the expensive part;
1257
+ §16).
1258
+ Engine work spreads over the cores on the lanes: physics from about 500
1259
+ bodies, skinning, extraction and culling, with results identical to one
1260
+ thread, so world hashes and replays do not change. Game systems stay on
1261
+ the engine thread. On the web the lanes need cross-origin isolation
1262
+ (`publish { isolation preferred }`, the default, in `project.ogt`; og-build
1263
+ §7); without it the same game runs on one thread.
1264
+ Simulated matter is paid for per point on the CPU every tick: a 3D fluid's
1265
+ particles (about 1 ms per 1,000 at two substeps) and a self-colliding
1266
+ cloth's points (about 1 ms per 1,000) are the ones to count (§13).
1267
+
1268
+ ## 15. Looks: the frame's art direction as data
1269
+
1270
+ A **look** is an `.ogt` of `type look` under `assets/looks/` that sets the
1271
+ whole treatment of the frame at once: the resources `RenderSettings`,
1272
+ `PostEffects`, `Stylise`, `MaterialStyle`, `Fog`, `Sky` and `Environment`
1273
+ (one block each, fields by name), the UI `skin` and the `antialias`.
1274
+ Choosing one for a premise is og-taste §1 and §12; this is the mechanism.
1275
+
1276
+ ```text
1277
+ type look
1278
+ version 1
1279
+ ---
1280
+ name "Harbour"
1281
+ extends "anime-cel-3d" # a built-in preset (templates/looks/<slug>.ogt) or another look's id
1282
+ skin "skins/harbour/skin.ogt"
1283
+ Stylise {
1284
+ ink true
1285
+ inkWidth 1.5
1286
+ }
1287
+ MaterialStyle {
1288
+ shadowTint "#3b3566" # sRGB written, stored linear
1289
+ shadowTintAmount 0.6
1290
+ }
1291
+ ```
1292
+
1293
+ ```ts
1294
+ export default og.game({ look: "looks/harbour.ogt", start, systems }); // GameDesc.look: applied after the first scene, before start
1295
+
1296
+ const flashback = must(og.load.look(world, "looks/flashback.ogt")); // at any tick
1297
+ must(og.render.applyLook(world, flashback));
1298
+ og.render.look(world)?.name; // the look last applied (LookInfo)
1299
+ ```
1300
+
1301
+ - Applying a look replaces each resource it names **whole** (the fields it
1302
+ sets, the rest their defaults; `PostEffects.flash` and `shakeTrauma`
1303
+ keep theirs) and leaves the others alone, then configures the UI with
1304
+ its skin. Write per-game tweaks in `start` after it, or in the look. A
1305
+ system that applies one declares those resources among its writes.
1306
+ - `extends` flattens at cook time, field by field (a field written wins);
1307
+ `assets/looks/<slug>.ogt` of the game's own replaces the preset of that
1308
+ slug for every look that extends it. The cook refuses an unknown field,
1309
+ a value out of range and an asset not in the bundle, naming the line.
1310
+ - `og.load.lookInfo` (and `og.render.look`) answer `LookInfo`: `name`,
1311
+ `extends`, `resources`, `skin`, `antialias`. A look's `antialias` (tier,
1312
+ off, msaa4 or taa) is the renderer's when `GameDesc.render` names none.
1313
+ - **`og.Stylise`** moves the frame away from the photographic, every
1314
+ primitive off at zero: the pixel grid (`pixelHeight`, `pixelUpscale`,
1315
+ `pixelSnap`), fewer tones or a fixed palette through a dither
1316
+ (`posterize`, `palette` from `og.load.palette` of a `.hex` or `.gpl`,
1317
+ `dither`, `ditherStrength`), `halftone`, `hatching`, painterly
1318
+ smoothing (`paintRadius`, `paintEdgeDarkening`, `paintBleed`,
1319
+ `paintCanvas`), `ink` lines (with `inkDepth` for silhouettes and
1320
+ creases), `depthBands`, and early-3D `vertexSnap`, `affineTexturing`
1321
+ and `depthBits`. The UI and screen sprites stay unstylised.
1322
+ - **`og.MaterialStyle`** sets every lit material that leaves the field at
1323
+ its default: `toon` (with a `gradientMap`), `shadowTint` and
1324
+ `shadowTintAmount`, `rimIntensity`, `outlineWidth` (with
1325
+ `outlineDistance` to thin lines with depth) and `flatShading`. Unlit and
1326
+ shader materials, sprites and the UI are untouched, and so is a material
1327
+ made with `styled: false` (exactly as declared under any look). The
1328
+ style's `toon` passes by a material with a physical lobe (`clearcoat`,
1329
+ `sheen`, `transmission`, `iridescence`, `anisotropy`, an `ior` other
1330
+ than 1.5), which keeps its physical shading and takes the rest; give it
1331
+ `toon: true` itself to make it toon anyway. An outline is a hull
1332
+ outline: each object's silhouette and its creases against other objects
1333
+ (the ground under it), never the edges between its own faces, so `.vox`
1334
+ models and flat-shaded meshes keep clean faces; for lines on every
1335
+ crease, `Stylise.inkDepth`.
1336
+ - Built-in assets a look may name without owning a copy:
1337
+ `"@canvas/paper"`, `"@canvas/linen"`, `"@canvas/watercolor"` for
1338
+ `Stylise.paintCanvas`, and `"@palettes/lcd2"`, `"@palettes/handheld4"`,
1339
+ `"@palettes/dusk16"`, `"@palettes/bright32"`, `"@palettes/1bit"` for
1340
+ `Stylise.palette`.
1341
+ - `RenderCapabilities.stylise` (`og.render.capabilities(world)`) names the
1342
+ primitive groups this device draws; a look using another still applies,
1343
+ without that primitive. A look in a streaming group applies once the
1344
+ group is resident (§6).
1345
+
1346
+ ## 16. Light, exposure, shadows and antialiasing
1347
+
1348
+ - **Tone mapping and exposure** (`og.RenderSettings`): `toneMapping` is
1349
+ `aces` (the default), `agx` (bright saturated lights go to white instead
1350
+ of shifting hue: neon, fire, magic) or `none`; `toneMapped false` clamps
1351
+ so 2D colours come out as authored. `exposure` multiplies every camera's
1352
+ `Camera.exposure`. `autoExposure` meters each frame to middle grey;
1353
+ `adaptSpeedUp` and `adaptSpeedDown` (EV per second of game time, so a
1354
+ replay adapts the same) make it adapt like an eye: 1 up and 3 down,
1355
+ walking out of a cave into sun.
1356
+ - **Environment maps** light PBR materials from a photographed sky: an
1357
+ `.ogt` of `type environment` beside an equirectangular `.hdr` or `.exr`
1358
+ (`source "studio.hdr"`, `size 128`, `intensity`, `rotation` in degrees,
1359
+ `samples`), prefiltered at cook time. Set it on `og.Environment`:
1360
+
1361
+ ```ts
1362
+ const env = must(world.resource(og.Environment));
1363
+ env.map = must(og.load.environment(world, "env/studio.ogt"));
1364
+ env.rotation = 40; // degrees about +Y; light and background turn together
1365
+ env.background = true; // draw it behind the scene (Camera.background and og.Sky give way)
1366
+ ```
1367
+
1368
+ `intensity` replaces `RenderSettings.environmentIntensity` while a map is
1369
+ set; `backgroundIntensity` dims the background alone;
1370
+ `StandardMaterialDesc.envMapIntensity` scales it per material. A look's
1371
+ `Environment` block sets it too.
1372
+ - **Shadows**: the sun (`og.DirectionalLight`) casts three cascades;
1373
+ `shadowSoftness` (0.01–0.05) widens the penumbra with the distance from
1374
+ caster to receiver, as a real sun's does, and `cascadeBlend` (0.1–0.2)
1375
+ hides the seams between cascades. Point and spot lights with
1376
+ `castShadow` cast cube shadows on the full tier and paraboloid ones on
1377
+ WebGL2.
1378
+ - **Antialiasing and renderer options** are `GameDesc.render`, read once
1379
+ when the renderer attaches:
1380
+
1381
+ ```ts
1382
+ og.game({ render: { antialias: "taa", taaHistory: "catmullRom", taaSharpen: 0.3, pointShadows: "auto", shadowMapSize: 2048 }, ... });
1383
+ ```
1384
+
1385
+ `antialias`: `tier` (TAA on WebGPU, Metal and Vulkan, 4x MSAA on WebGL2),
1386
+ `taa` (any tier, converges over about 8 frames: fast motion, thin
1387
+ geometry), `msaa4`, `fxaa` (one cheap pass after tone mapping; pixel and
1388
+ low-resolution looks), `off` (pixel art). `taaHistory: "catmullRom"`
1389
+ stays sharper under camera motion and `taaSharpen` (0–1) restores
1390
+ detail TAA softens. `RenderCapabilities.antialias` says what the device
1391
+ resolved it to.
1392
+
1393
+ ## 17. Saving progress
1394
+
1395
+ A save is either the whole world or a record the game writes; both go in
1396
+ the platform's store (og-platform-sdk §2), which takes any JSON value up
1397
+ to 64 KiB a key: a record in one key, the world in a slot
1398
+ (`og.platform.saveSlot`, any size up to 512 KiB).
1399
+
1400
+ - **The world** (`world.snapshot()`, `world.restore(save)`): every entity,
1401
+ component, name, resource (og.Time and og.Rng too), pending event and
1402
+ module state (physics, animation, tweens, tile edits), as text
1403
+ (`og-save:1:...`), in every build including publish and alike on both
1404
+ hosts. Restoring brings that world back to the hash it had; the session
1405
+ stays (input, the player's volumes, the platform's calls, the language,
1406
+ the command channel's scheduled steps). Use it where the moment itself
1407
+ is the save: a puzzle mid-move, a sandbox, a run that resumes exactly.
1408
+
1409
+ ```ts
1410
+ // Save (a lantern lit, Save and quit): the world, and what the title shows of the slot.
1411
+ const s = world.snapshot();
1412
+ const asked = s.ok ? og.platform.saveSlot(world, { slot: "slot1", text: s.value, meta: { name, chimes } }) : s;
1413
+ if (asked.ok) run.saveRequest = asked.value; // one request for the whole slot
1414
+ // The title, on its first tick: the meta alone, no parts read.
1415
+ run.slotRequest = must(og.platform.loadSlot(world, { slot: "slot1", metaOnly: true }));
1416
+ // Continue: the text, once loadSlot's answer landed.
1417
+ run.loadRequest = must(og.platform.loadSlot(world, { slot: "slot1" }));
1418
+ const back = world.restore(answer.data.text); // in a system: applies when the tick ends
1419
+ if (!back.ok) toast(back.reason); // a save from a build whose types changed names them
1420
+ ```
1421
+
1422
+ How big a save is, and so where it goes: a world of a few entities
1423
+ snapshots to about 11 KiB of text (most of it the table of every
1424
+ registered type), Hollowbell (a 2D platformer of 189 entities) to about
1425
+ 25 KiB, Kestrel Stair (a 3D world of 93 entities: five skinned
1426
+ characters on graphs, five cloths and a rope, five navmesh agents with
1427
+ minds) to about 78 KB. The store takes 64 KiB a key (a platform limit,
1428
+ with 64 keys and 1 MiB per player and game), so a world goes through
1429
+ `og.platform.saveSlot`/`loadSlot`, never one `storeSet`: the slot's
1430
+ text is cut into parts of a key each (at most eight: 512 KiB), written
1431
+ in the bank of keys its last save did not use (`slot1.a0..`,
1432
+ `slot1.b0..`), and the manifest (`slot1`: the bank, the parts, the
1433
+ length, a checksum and your `meta`) last, at the revision read, so a
1434
+ save cut short leaves the last one whole and another device's save
1435
+ landing meanwhile is a `conflict`, never a mix. One request answers
1436
+ for all of it; loadSlot checks the parts against the manifest. The
1437
+ text is already deflated: a tighter encoding was measured (deflate at
1438
+ its highest level saves 0.7 %, LZMA 31 % at the cost of a second
1439
+ decoder in every build) and not taken.
1440
+
1441
+ Physics carries a triangle-mesh, convex-hull or height-field collider
1442
+ by its geometry's handle and scale, not its triangles (a 956 m
1443
+ circuit's road and walls, 6,700 triangles, were 384 KB of a save
1444
+ before; they are now a few bytes): load the glTF or call
1445
+ `og.physics.setMesh` before restoring, as for every asset, or the
1446
+ restore is refused naming the mesh.
1447
+
1448
+ A restore asked for in a system or a command applies when the tick
1449
+ ends; in `start`, at once. A save taken in a system is the middle of
1450
+ its tick: restored, the next step completes that tick (the systems
1451
+ after the saver and the physics step), so a replay from it (the
1452
+ countdown's world restored, the taped inputs fed again) runs the saved
1453
+ race tick for tick. Take it as the system's last write of the tick:
1454
+ what the system does after the call is not in it. A saved type whose fields changed since (a
1455
+ release added one) is refused naming the type and fields; pass
1456
+ `{ migrate: true }` to map it by field name (new fields take their
1457
+ defaults), and keep a saved type's fields stable between releases where
1458
+ you can. Keep progress in components and resources, never in module
1459
+ variables (they are not in the save), and keep request numbers and the
1460
+ session's own state (a best score read from the store, an open menu)
1461
+ where a restore cannot roll them back: reset them after it. Asset
1462
+ handles are saved as numbers: load in `start`, in the same order every
1463
+ launch.
1464
+ - **A record** (`storeSet(world, { key: "save", value: { chimes, glimmers,
1465
+ checkpoint } })`): a few hundred bytes of what progress means, rebuilt
1466
+ into a world on Continue. Smaller, readable on a title screen, and it
1467
+ survives any change to the game's types; right for checkpoint games
1468
+ (examples/hollowbell's `src/save.ts`).
1469
+
1470
+ Test either across a relaunch with a playtest's `store: "keep"` and
1471
+ `then` (og-platform-sdk §4). A watch replay (the start's snapshot and a
1472
+ tape of inputs) and a ghost (a tape of poses) are og-core §2's
1473
+ "Recording, replaying and ghosts".
1474
+
1475
+ ## 18. Where to look things up
1476
+
1477
+ `reference/index.md` (the generated wiki shipped with this skill) maps every
1478
+ module (grouped by skill, as in §1), type and function to its page, with the IDL text, defaults, ranges
1479
+ and units of every field.