@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,330 @@
1
+ ---
2
+ name: og-build
3
+ description: The Origin Engine build loop — og cook, og run with a scripted command channel (inputs, captures, dumps, get and set), screenshots and world dumps read as evidence, og dev hot reload, og test playtests, og polish's taste and feel scores (the one list of what it measures and where each finding is fixed), og build for publishing, og run --device for the first frame and frame times on a real browser or phone, and big games: streaming groups and what they may hold, .vox models, compressed animation, chunked music, the lanes and cross-origin isolation. Use whenever a change to a project with a project.ogt must be run, observed, scored or shipped (not a Godot project.godot).
4
+ ---
5
+
6
+ The `og` tool is the whole loop: it cooks assets, runs the game headless on
7
+ the native host with a command script, serves the probe, reloads on save,
8
+ runs playtests, scores them and builds the publishable bundle. Nothing here
9
+ needs a browser or a Rust toolchain. The command channel is
10
+ `reference/commands.md`, the probe `reference/probe.md`.
11
+
12
+ ## What works
13
+
14
+ The loop held for two whole games written from this package alone
15
+ (`examples/lantern-vale`, `examples/hollowbell`): `og cook` took 47 and 69
16
+ assets cold in 8.2 s and 3.6 s and under 100 ms after a change, without a
17
+ warning, and every refusal named the line and the fix (a copied skin
18
+ without its CJK `fallback` named the line and og-taste §1;
19
+ `PostEffects.vignette is true or false` named the line). Scripted runs gave
20
+ the same frames every time, `store: "keep"` relaunches and two-process
21
+ replays worked the first time they were asked for, and `og polish` scored
22
+ taste 99 for both and feel 90 and 92, naming the section that fixes each
23
+ finding. The third, The Kestrel Stair (`examples/kestrel-stair`, 3D
24
+ third-person), cooked 62 assets cold in 8.5 s (110 outputs, 9.9 MB) and
25
+ about 0.1 s after a change, without a warning, its refusals naming the
26
+ line; two processes of a full session (chases, bites, strikes, a rest, the
27
+ gate, the bell) ended on one world hash, and `og polish` found every
28
+ input answered within a tick and every damage moment with hit stop, shake,
29
+ a number and a sound (taste 98, feel 79).
30
+
31
+ ## 1. The loop
32
+
33
+ 1. **Cook** after any change under `assets/`: `og cook`. It is incremental
34
+ (only changed sources re-cook) and writes `cooked/`, the only thing the
35
+ runtime reads. A load that names an uncooked path, or a blob whose cook
36
+ stamp does not match the manifest, is refused with the path; cook again.
37
+ 2. **Run with a script**: `og run . --headless --commands "<script>"`. The
38
+ script is `t:action[:args];...` with `t` in simulation seconds from the
39
+ start of the run, so the same script always lands on the same ticks:
40
+
41
+ ```sh
42
+ og run . --headless --seed 1 --commands "0.5:touch_move_to:Start;0.6:touch_start;0.7:touch_end;2.0:screenshot;2.2:dump_world;2.5:quit"
43
+ ```
44
+
45
+ Core actions: `screenshot[:path][:full|:N]`, `listen[:path][:seconds]`,
46
+ `touch_move_to:<ui id>`, `move_to_text:[=]text[@N]`, `touch_move:x,y`,
47
+ `touch_start`, `touch_end`, `touch:<id>:<down|move|up>:<x,y | ui id>`
48
+ (a second finger), `gamepad:<a+b+...|->[@pad][:lx,ly][:lt,rt]`, `scroll:dx,dy`,
49
+ `key_down:<Key>`, `key_up:<Key>`, `type:<text>`, `compose:<text>` and
50
+ `commit:<text>` (an input method, og-ui §7),
51
+ `teleport:<entity>:x,y,z`, `frame_entity:<entity>`, `dump_world[:path]`,
52
+ `dump_entity:<entity>[:path]`, `dump_ui[:path]`,
53
+ `get:<Resource>[.<field>]` or `get:<entity>[.<Component>[.<field>]]`
54
+ (logs the value as game code sees it, og.Time whole and a light og.DayNight
55
+ drives as it is drawn, and writes `get-<tick>.json`, then
56
+ `get-<tick>-2.json`, ... for more on one tick; a world dump lists
57
+ resources by name only, so `get:` is how to read one),
58
+ `set:<entity>.<Component>.<field>=<value>` or
59
+ `set:<Resource>.<field>=<value>` (`set:og.Camera.trauma=0.5`,
60
+ `set:Game.coins=10`, `set:og.Stylise.ink=false`: a number, `true` or
61
+ `false`, or `x,y,z`; a value outside the field's `min`/`max` is refused
62
+ naming the range, never clamped, while game code's own writes are stored
63
+ as given: widen the range in the field's options to try one), `seed:<n>`, `pause`, `resume`, `advance:<ticks>`,
64
+ `quit`, and `help` for the list. Modules add their own: `dump_ai` and
65
+ `dump_nav` (og-ai §5), `dump_platform` (og-platform-sdk §4), `locale:<tag>`
66
+ (og-i18n §6), `dump_stream` and `terrain_stream:<stream|resident>[:<MiB>]`
67
+ (og-game §6 and §11). Unknown actions warn and the script continues; read the
68
+ warnings.
69
+ 3. **Read what it wrote.** Screenshots are PNG files, dumps are JSON files
70
+ (`<action>-<tick>.png|json` unless the step names a path), written
71
+ under `og-out/` in the project (`--out <dir>` chooses); `og run`
72
+ prints each path. Open the frame and describe what it shows; read
73
+ `dump_world` for the entity count, the archetype tables and each system's
74
+ matched count. A system that matched 0 entities is usually a missing
75
+ component on the spawn, not a logic bug.
76
+
77
+ Where there is no renderer (a sandbox with no GPU: `og run --json` says
78
+ `"tier": "none"`, `og doctor` says the renderer is `none`), the run is
79
+ the same run: the same ticks, dumps and world hash. A `screenshot`
80
+ writes no PNG and logs "no renderer, frame unchecked" with the file it
81
+ would have written, not as a warning; `og run --json` lists those files in
82
+ `unchecked`, and the probe's `capture` answers null. Read the dumps
83
+ there, and look at the frames where there is a renderer (§4).
84
+ 4. **Fix and repeat** with the same script and seed. Keep the script in the
85
+ change so the next run is the same run.
86
+
87
+ A game's own actions (`GameDesc.commands`, og-game §3) join the list; give
88
+ each its argument form and one sentence in `GameDesc.commandHelp`
89
+ (`{ spawn_wave: { usage: "<count>", doc: "Spawns a wave now." } }`)
90
+ so `help` explains it (it lists `spawn_wave:<count>`: the name, then the
91
+ usage). An entry naming no action is logged when the game
92
+ starts.
93
+
94
+ ## 2. Live sessions: the probe and hot reload
95
+
96
+ - `og run .` without `--headless` keeps running and serves the probe over a
97
+ loopback WebSocket; `og probe <method> [json]` calls it (`og probe hello`,
98
+ `og probe state`, `og probe dump '{"what":"world"}'`,
99
+ `og probe capture '{"width":640,"height":360}'`, `og probe patch …`). A
100
+ refusal is `false` or `null`, never an error; an error means the call was
101
+ malformed.
102
+ - `og dev .` watches `src/` and `assets/`: it rebuilds the bundle, re-cooks
103
+ changed assets and reloads the running game through the probe, keeping the
104
+ world state (components migrate field by field by name). The budget is
105
+ 300 ms from save to the first new frame; `og dev` reports each reload's
106
+ latency and any dropped components or fields. A reload that fails keeps
107
+ the old bundle running and prints why.
108
+
109
+ ## 3. Tests
110
+
111
+ `og test .` runs the project's bun tests and every `tests/*.og.ts` playtest
112
+ script through `og run --headless --commands`. A playtest asserts on what the
113
+ game exposes (dumps, `state()` facts, entity fields, `get` answers), never on
114
+ how the code is written. Each playtest writes into its own
115
+ `og-out/test/<name>/`, emptied when it starts, and its `check` reads a
116
+ step's file by the name the step gave (`run.json("shop.json")`,
117
+ `run.png("title.png")`); give each step's file its own name. A test that ran no checks fails. Run it before
118
+ claiming a change works.
119
+
120
+ Put every check that reads a frame (`run.png`, a decoded screenshot, a
121
+ comparison with `references/`) inside `t.frame(files, () => ...)`:
122
+
123
+ ```ts
124
+ check(run, t) {
125
+ t.equal(run.json<Game>("shop.json").coins, 10, "the coin was banked");
126
+ t.frame(["title.png", "shop.png"], () => checkFrames(run, t, ["title.png", "shop.png"], ""));
127
+ },
128
+ ```
129
+
130
+ Without a renderer (`run.rendered` false, `run.tier` `"none"`) the scope
131
+ does not run: `og test` prints `~ <file>: no renderer, frame unchecked` for
132
+ each screenshot, counts them in its summary (`og test: 7 suite(s) passed;
133
+ 9 frame(s) unchecked`) and passes on every other check, which runs as it
134
+ would with a renderer. A check that reads a frame outside `t.frame` stops
135
+ there (the report says the checks after it did not run), so keep frames in
136
+ the scope. Unchecked frames never change `og test`'s exit status; they are
137
+ verified where there is a renderer: the author's machine, the
138
+ browser-check sandbox (the published page, §6) or an executor with a GPU. A playtest runs with the host clock at
139
+ 2026-01-01 00:00 UTC (`clock` to change it) and a fresh store; `store:
140
+ "keep"` with `then: [{ commands, clock }]` runs further processes over the
141
+ same store, for a relaunch (og-platform-sdk §4).
142
+
143
+ ## 4. Evidence discipline
144
+
145
+ - Every change that alters rendering, animation, physics or the tick carries
146
+ a scripted `og run --commands` run whose frame was opened, read and
147
+ described in the change: what is on screen, where, and how that differs
148
+ from before. A frame nobody looked at is not evidence. Where there is no
149
+ renderer the frame is unchecked, not seen: say so, and look at it where
150
+ there is one (§3) before calling a visible change done.
151
+ - Name the exact command, seed and script with the frame and the dump, so
152
+ anyone can rerun it and get the same bytes.
153
+ - Determinism is checkable: two runs with the same seed and script give the
154
+ same world hash in `dump_world`. When they do not, find the wall-clock
155
+ read or the unseeded randomness before anything else.
156
+ - Report what ran and what did not. A build that compiled is not a game
157
+ that plays.
158
+
159
+ ## 5. Polish, measured: `og polish`
160
+
161
+ This is the one list of what `og polish` measures; og-taste §10 and
162
+ og-feel §13 are the checklists it scores, and every other skill points
163
+ here. `og polish .` plays every playtest headless, samples every tick
164
+ (`dump_pulse`: time scale and hit stop, camera trauma, punch and kick,
165
+ floating texts, afterimages, tweens, particle systems, voices by bus, the
166
+ named entities' positions and flipbook frames, step and render time), takes
167
+ `dump_ui` beside every `screenshot` and captures the audio. It writes
168
+ `og-out/polish/report.md`: a **taste** and a **feel** score (0 to 100 each,
169
+ the mean of their rows; a row is n/a when no playtest exercises it), the
170
+ frames, and for each finding the skill section that fixes it. It refuses a
171
+ project without playtests: write them first (§3), with a `screenshot` on
172
+ every screen that matters.
173
+
174
+ | Axis | Measure | From | Passes at | Fixed in |
175
+ |---|---|---|---|---|
176
+ | taste | Contrast | each text element's rectangle in a shot: the ground (its median) against its ink, read over the text's own glyphs (the pixels at least half-way to the strongest ink; the farthest twentieth of them, the glyph cores) | 4.5:1 | og-taste §3 |
177
+ | taste | Text size | the tallest run of ink rows in each text element, CSS px | 9 px, a 13 px face's capitals | og-taste §4, §10 |
178
+ | taste | HUD occupancy | the union of leaf elements on gameplay shots, touch controls (joystick, d-pad, action buttons) left out; a shot with a dialog or loading layer, a conversation, a list over a quarter of the surface or three texts in the central third is a screen, not gameplay | at most 15 % of the surface and 5 % of the central third | og-taste §2 |
179
+ | taste | Screen transitions | a change of three or more elements with UI motion or a tween in the 20 ticks around it | every change | og-taste §5 |
180
+ | taste | UI sounds | a press that changed the UI and started a voice on the ui bus within 8 ticks | every press | og-taste §6, og-audio §3 |
181
+ | taste | Locale keys | `og cook`'s warnings | none | og-i18n §1 |
182
+ | feel | Input latency | ticks from the tick an input lands to the first thing seen or heard to change (a named entity accelerating or changing clip, the UI, a sound, a tween, a floating text, shake, hit stop, a particle burst); a press answered on its release counts from the release | 0–1 ticks | og-feel §1 |
183
+ | feel | Hit feedback | each damage moment (a fact named `hp`, `health`, `lives`… falling, or `hits`, `damage`, `kills`… rising): hit stop, shake, floating text and a sound within 8 ticks | three of the four | og-feel §3, §4, §7, og-audio §1 |
184
+ | feel | Idle frames | ticks after the first ten where nothing moves or animates, no UI moves, no sound starts or plays, no tween, text or particle runs | at most 10 % | og-feel §9, og-taste §5 |
185
+ | feel | Sound on input | inputs answered by a sound within 6 ticks | every action | og-audio §1 |
186
+ | feel | Frame time | step + render, p95, on the tier it ran on (`metal` on a Mac, `vulkan` on a GPU); the probe's own screenshots (render, read back, PNG) are not the game's step and are left out. Without a renderer the row says `n/a (no renderer)`, as do contrast, text size and HUD occupancy, which read frames | 8.3 ms (half the frame) | og-game §14 |
187
+
188
+ The report also lists clipped samples in the mix (og-audio §4). Contrast and
189
+ text size read text only: icons and images (their "text" is a picture's
190
+ name), disabled controls and what they hold (WCAG exempts them), the
191
+ slivers a scroll container cuts, what a dialog's or a tutorial's scrim
192
+ dims on purpose and what is mid-transition in the shot (an element whose
193
+ bounds differ on the ticks before and after it: a toast sliding out over
194
+ a dialog, a panel popping in) are left out, and each element is measured
195
+ at the pixels of the surface `dump_ui` reports, so a phone preview
196
+ (390×844 at 3×) is read at its own size. The ink is measured over the
197
+ glyphs, not the rectangle, so a short word in a wide label (a three-letter
198
+ name on a 356 px leaderboard row) reads what its letters show, as a full
199
+ line does, and not its antialiased edges diluted by the empty rest of the
200
+ row. The ink is what the pixels show,
201
+ so it depends on the glyphs' coverage: thin strokes cover few pixels
202
+ whole, and a regular-weight CJK face at body sizes measures lower than
203
+ Latin in the same colours (a 15 px "设置" 3.1:1 where "Settings" reads
204
+ 4.6:1 on one button), as it reads lower to the eye. Darken the ink in the
205
+ skin's palette, give the CJK face a heavier weight or the label a larger
206
+ step; the measure is not wrong there. Facts are the game's `facts`
207
+ (og-game §3): expose `hp`, `hits` or `kills` there or the hit measures stay
208
+ n/a. A baseline line says how often things change with no input at all;
209
+ when it is high (a runner that always moves), read latency together with
210
+ the frames.
211
+
212
+ The numbers cannot judge a style. Read the frames before changing anything,
213
+ fix the cause in the skin, the template or the system, never per call, and
214
+ run it again after the fix: the report is the evidence.
215
+
216
+ ## 6. Shipping
217
+
218
+ `og build .` bundles `src/` to `dist/game.js`, cooks, and copies the engine
219
+ runtime and wasm next to it. `og build . --publish` produces the published
220
+ form: the probe keeps only its player-facing members (hello, tick, state,
221
+ pick, entity, input, capture) and the command channel is gone.
222
+ What ships is still checked by script, through what it keeps: real input
223
+ on the published page and the probe's `tick`, `state` and `capture`. A
224
+ playtest written in input steps (`key_down`, `type`, `touch_move_to` an
225
+ id, `move_to_text`, `touch_start`) and `screenshot` steps runs there as it
226
+ runs under `og test`, each frame compared with the same references; a
227
+ game command (warp, give) has no channel there and is refused, so keep
228
+ the steps a published check needs free of them. The engine runs this on
229
+ the published build of its own games (its `tests/web/published.ts`: the
230
+ boot, the title, a press, a typed name and a frame against the metal
231
+ reference, with a clean console).
232
+ `og run --device chrome .` plays that publish build in a real browser
233
+ (`android[:serial]` over adb, `iphone[:udid]` over safaridriver), passes
234
+ the play gate and writes `og-out/device/report.md`: the first frame against
235
+ its budget (2.0 s, 1.5 s on an iPhone), the frame-time percentiles on the
236
+ tier the device chose, the device itself, and a fix per finding (og-game
237
+ §6 for a slow first frame, og-game §14 for missed frames). Run it on the
238
+ phone you ship for: og polish's native numbers are not a phone's. `og doctor`
239
+ names each missing tool (bun, the native addon, the wasm artifact) with its
240
+ fix, and says whether this machine has a renderer (none is not a problem:
241
+ the playtests' frames are then unchecked, §3).
242
+
243
+ ## 7. Big games: what the cook packs and the page fetches
244
+
245
+ What `og cook` takes is decided by the file: `.glb`/`.gltf` and MagicaVoxel
246
+ `.vox` (meshed into a glTF: flat-shaded quads, palette colours, `model {
247
+ voxScale voxOrigin }` for the size and origin, frames as clips; og-game §5)
248
+ are models; `.ogt` files by their `type` (scene,
249
+ look, environment, texture-array, flipbook, particles, terrain, shader…);
250
+ `.cube` LUTs, `.hex` and `.gpl` palettes, images, WAVs and fonts by
251
+ extension. A look's built-in `@canvas/…` and `@palettes/…` assets are added
252
+ to the bundle only when a look names them; a game's own `assets/@canvas/`
253
+ is refused.
254
+
255
+ **Streaming groups** (og-game §6) are cooked into one pack each,
256
+ `cooked/stream/<name>.ogb`, and `og build --publish` ships each as its own
257
+ file the page fetches when the group loads. A group may hold glTF models
258
+ without skins or morph targets, textures, texture arrays, sounds,
259
+ environment maps, palettes, looks and LUTs:
260
+
261
+ ```text
262
+ stream "night-market" {
263
+ asset "models/stalls.glb"
264
+ asset "env/night.ogt" # an environment map
265
+ asset "tiles/market.ogt" # a texture array
266
+ asset "palettes/neon.hex"
267
+ asset "looks/night-market.ogt" # applied once resident
268
+ asset "grade/night.cube"
269
+ volume "market-gate"
270
+ }
271
+ ```
272
+
273
+ A sound cooked in chunks may join a group too: its first chunk goes in
274
+ the pack and every other chunk stays a file of its own, read ahead of the
275
+ playheads once the group is resident (og-audio §5). The cook refuses any
276
+ other kind (a skinned model, a scene) in a group, naming the scene and
277
+ the asset. Prove a group in a script:
278
+ move the camera into its trigger (`teleport`, or the game's own command),
279
+ `advance` a few ticks, then `dump_stream` lists each group's state,
280
+ memory and the assets kept past an unload (`retained`), and
281
+ `get:` of a field the game copies from `og.stream.budget` shows `used` and
282
+ `kept` against `budget`. A run with the same script must end on the same
283
+ world hash streamed or not: nothing a tick reads depends on what is
284
+ resident.
285
+
286
+ **Size knobs** in a model's or a sound's import settings (`<file>.ogt`,
287
+ `type import`):
288
+
289
+ ```text
290
+ animation {
291
+ compress true # quantised clips, keyframes the curve rebuilds dropped
292
+ tolerance 0.0001 # the most a sampled value may move (og-game §9)
293
+ }
294
+ sound {
295
+ stream true # cooked in chunks (the default past 1 MiB of QOA, about 27 s of stereo)
296
+ chunk 4 # seconds per chunk (og-audio §5)
297
+ }
298
+ ```
299
+
300
+ **What a player downloads** is what `og build --publish` prints under its
301
+ size table, brotli `-q 11` per file as the size gate measures the engine:
302
+
303
+ ```text
304
+ download (brotli-11, what a player fetches; a WebGPU and a WebGL2 page fetch the same files):
305
+ boot, lanes page (isolated) 92 files 4323854 B (engine 2976.8 kB, cooked 1044.3 kB, shell 85.5 kB, basis 217.3 kB)
306
+ boot, single-threaded page 91 files 4284988 B (engine 2937.9 kB, cooked 1044.3 kB, shell 85.5 kB, basis 217.3 kB)
307
+ later: 20 sound chunks 2427050 B (ahead of their playheads); the audio worklet 875 B (first gesture); the service worker and icons 32634 B
308
+ ```
309
+
310
+ The boot is everything fetched before the first frame: one engine module
311
+ (the lanes module on a cross-origin isolated page, else the
312
+ single-threaded one), the shell, the Basis transcoder and the cooked first
313
+ group (everything no streaming group holds). A sound cooked in chunks
314
+ costs the boot its first chunk only (about `chunk` seconds of QOA): the
315
+ rest are fetched ahead of its playheads, and none before it plays; in a
316
+ streaming group it costs the boot nothing (its first chunk comes in the
317
+ group's pack). The
318
+ table's `total` is every file of the directory (both engine modules, the
319
+ licences, the icons), never a download. Compare the boot line before and
320
+ after a change; `--json` carries the same as `downloads`.
321
+
322
+ **The lanes** spread physics, skinning, culling and extraction over the
323
+ cores with results identical to one thread (og-game §14). Natively
324
+ `OG_LANES=off` (or `1`) runs serial and `OG_LANES=<n>` uses n threads; a
325
+ world hash must not change between them, which is a check worth a
326
+ playtest of its own on a physics-heavy game. On the web the lanes need a
327
+ cross-origin isolated page: `project.ogt`'s `publish { isolation
328
+ required | preferred | off }` (`preferred` by default: lanes where the
329
+ host serves COOP/COEP, one thread otherwise, the same game either way).
330
+ The lanes build caps memory at 2 GiB. `dump_lanes` reports what ran where.
@@ -0,0 +1,285 @@
1
+ ---
2
+ name: og-core
3
+ description: The rules every Origin Engine game obeys — TypeScript only, a fixed deterministic tick, Results instead of throws, typed views per archetype, no wall clock, and reflection as the one schema. Read before writing or reviewing any code in a project with a project.ogt.
4
+ ---
5
+
6
+ Origin Engine is a Rust engine with a TypeScript API, generated from one IDL
7
+ (`crates/og-api/api.ogt` in the engine repository). The same game bundle runs
8
+ in the browser (wasm in a Worker) and headless on the native host under
9
+ `og run`. These rules are what makes that true; breaking one produces a game
10
+ that works on one host, one seed or one machine and not the next.
11
+
12
+ ## What works
13
+
14
+ Two games written from this package alone (`examples/lantern-vale`, a 3D
15
+ adventure, and `examples/hollowbell`, a 2D platformer) hold these rules
16
+ for a whole game, not a toy: each ends a full session run in two processes
17
+ on one world hash, a player's name and a save live in `str` fields as
18
+ world state, and a resource or column a system did not declare is refused
19
+ with the system and the record named, so the fix is one line. A third,
20
+ The Kestrel Stair (`examples/kestrel-stair`, 3D third-person), restored
21
+ `world.snapshot()` after a relaunch with `world.restore()`: 93 entities
22
+ came back with their cloth, navmesh paths and behaviour state, and a full
23
+ session in two processes ended on one hash.
24
+
25
+ ## 1. Game code is TypeScript, and only TypeScript
26
+
27
+ A game is `src/*.ts` against `@origingame/engine`. Never add Rust, a second
28
+ engine, a WebGL context, DOM access or a `<canvas>` of your own: the engine
29
+ renders, the shell owns the page. Anything that would need engine-side code
30
+ is an engine feature request, not something to work around in the game.
31
+
32
+ ## 2. The tick is fixed and the world is deterministic
33
+
34
+ - Simulation runs in fixed ticks (`GameDesc.tick`, 60 Hz by default). A
35
+ system sees `time.dt`, `time.tick` and `time.elapsed` from the `Time`
36
+ resource; these are functions of the tick count.
37
+ - Never read `Date`, `performance.now()` or any other clock inside a system,
38
+ a command or a facts function. Two runs with the same seed and the same
39
+ inputs must produce the same world hash; a wall-clock read breaks that.
40
+ - Randomness comes from the `Rng` resource (`world.resource(og.Rng)`) or a
41
+ stream forked from it with a label (`rng.fork("spawner")`). Never
42
+ `Math.random()`. The project seed lives in `project.ogt` and `GameDesc.seed`;
43
+ `og run --seed` overrides it for a replay. A draw (`next`, `range`,
44
+ `integer`, `chance`) moves the stream, so it needs `og.Rng` in `writes`;
45
+ `fork` reads the seed alone and moves nothing, so a system that only
46
+ forks declares `og.Rng` in `reads` (its forks are the same on every host
47
+ and in every system).
48
+ - The wall clock reaches the game only as host input, between ticks:
49
+ `og.platform.now(world)` (UTC ms, the host's clock when the platform
50
+ attached or last answered, advanced by the ticks since) and the `time`
51
+ on every og.platform answer. A replay records them like a key press.
52
+ - Rendering never feeds back into simulation: do not read camera or screen
53
+ state to decide game logic.
54
+ - The engine may spread its own work over the cores (physics, skinning,
55
+ culling: the lanes), and it gives the same result to the bit as one
56
+ thread. Game systems always run on the engine thread, one after another,
57
+ so nothing in game code needs a lock; it needs only the rules above.
58
+
59
+ ### Saving progress
60
+
61
+ The world is one snapshot: `world.snapshot()` returns it as text
62
+ (`og-save:1:...`: every entity, component, name, resource, rng state,
63
+ pending event and module state, with each type's schema hash) in every
64
+ build, publish included, and `world.restore(save)` puts it back, so the
65
+ restored world hashes as the saved one did, on either host. The session
66
+ stays across a restore (input, the player's volumes, the platform's calls,
67
+ the language, scheduled command steps); og.Time takes the saved tick.
68
+ Inside a system the snapshot is the world in the middle of that tick, as
69
+ that system sees it (its own queued spawns and despawns are not in it),
70
+ and it names the system: the world restored from it completes the tick on
71
+ its next step (the systems after that one, then the engine's, physics
72
+ included) and then goes on tick for tick as the run that saved did, so a
73
+ replay from it matches. Make the snapshot the system's last write of the
74
+ tick. A restore asked for in a system applies when the tick ends. A save whose types' fields changed is refused naming them, or
75
+ mapped by name with `{ migrate: true }`. Only what is in the world is
76
+ saved: progress in module variables is lost. The text is a few tens of
77
+ KiB (11 KiB for a few entities, 25 KiB for a 2D level of 189, 78 KB for a
78
+ 3D world of 93 with cloths, minds and skinned characters), often over the
79
+ store's 64 KiB a key: save it with `og.platform.saveSlot(world, { slot,
80
+ text, meta })`, which spreads it over as many keys as it needs and writes
81
+ the slot's manifest last, so a save cut short leaves the last one whole,
82
+ and read it back with `og.platform.loadSlot` (og-game §17, og-platform-sdk
83
+ §2).
84
+
85
+ ### Recording, replaying and ghosts
86
+
87
+ The engine has no in-game recorder: determinism is the recorder. The same
88
+ world and the same inputs give the same run to the tick, so a game records
89
+ its own inputs and replays them. Three things are called "replay": the
90
+ probe's and playtests' re-runs (`og run --commands`, `--seed`: tests,
91
+ og-build §4), a **watch replay** (the player's run driven again) and a
92
+ **ghost** (a past run drawn beside the player). The last two are game code;
93
+ examples/meridian-loop does both (`src/replay.ts`, `src/tape.ts`,
94
+ `src/tapes.ts`, `src/ghost.ts`; `tests/replay.og.ts` checks the replay
95
+ crosses the line on the race's own ticks).
96
+
97
+ Watch replay: snapshot the start, tape the inputs, restore and feed the tape.
98
+
99
+ ```ts
100
+ // 1. The start, taken by the FIRST system of a tick (later in a tick, the
101
+ // restored world skips the rest of that tick and runs a different race).
102
+ r.replayMark = true; // saved set: the restored world knows it is a replay
103
+ const snap = world.snapshot();
104
+ r.replayMark = false;
105
+ const asked = snap.ok ? og.platform.saveSlot(world, { slot: "replay", text: snap.value }) : snap;
106
+ if (asked.ok) r.replaySave = asked.value; // its answer comes on a later tick (og.platform.result)
107
+
108
+ // 2. Every tick: the intent drives the car only through its quantised code,
109
+ // live and replayed alike, so the tape reproduces it exactly.
110
+ const code = r.replaying ? codeAt(tape, r.runLen) : encode({ steer, throttle, brake });
111
+ const i = decode(code); // steer 33 steps, throttle 17, brake 5: 12 bits
112
+ world.set(car, og.Vehicle, { steer: i.steer, throttle: i.throttle, brake: i.brake });
113
+ if (!r.replaying) putCode(tape, r.runLen, code); // two characters a tick
114
+ r.runLen += 1;
115
+
116
+ // 3. At the finish: the tape beside the start.
117
+ og.platform.storeSet(world, { key: "replay.tape", value: { ticks: r.runLen, inputs: tapeText } });
118
+
119
+ // 4. Watch replay: loadSlot, then world.restore(text) when it answers (it
120
+ // applies when the tick ends). The next tick sees replayMark set: clear
121
+ // it, set replaying, read "replay.tape" back, feed it from tick 0.
122
+ ```
123
+
124
+ Keep the tape and the replay's flags in components and resources (a
125
+ restore rolls back everything else). A component has no array field and a
126
+ `str` field holds at most 4,096 bytes, so a tape of a few thousand ticks
127
+ is cut into `str` chunks, one entity each, written through the column's
128
+ bytes by the system that owns them (§4). The start is the whole world:
129
+ triangle-mesh colliders make it large (a 956 m circuit's world is 400 KB),
130
+ so it goes through `saveSlot`, which spreads it over the store's keys.
131
+ Request numbers are not carried across a restore: ask the store again
132
+ after it.
133
+
134
+ A ghost: record poses, draw a model. While a lap runs, tape the car's pose
135
+ every second tick (position and yaw, pitch, roll: 15 characters); from the
136
+ next lap draw a model with no body where the best lap's car was at the
137
+ same time into its lap, between the two poses around it:
138
+
139
+ ```ts
140
+ if (into % 2 === 0) putPose(cur, into / 2, { ...car.position, ...euler(car.quaternion) });
141
+ const f = into / 2, i0 = Math.floor(f), u = f - i0;
142
+ const a = poseAt(best, i0), b = poseAt(best, i0 + 1);
143
+ world.set(ghost, og.Transform, {
144
+ position: { x: a.x + (b.x - a.x) * u, y: a.y + (b.y - a.y) * u, z: a.z + (b.z - a.z) * u },
145
+ quaternion: quat(lerpAngle(a.yaw, b.yaw, u), lerpAngle(a.pitch, b.pitch, u), lerpAngle(a.roll, b.roll, u)),
146
+ });
147
+ ```
148
+
149
+ Do not re-drive a second physics body from the tape for a ghost: it takes
150
+ part in the simulation the player drives in (a second og.Vehicle's wheel
151
+ rays land on whatever collider is under them, the player's car included),
152
+ so the player's run is no longer the one taped.
153
+
154
+ ## 3. Every failure is a value
155
+
156
+ Engine calls that can fail return `Result<T>`: `{ ok: true, value }` or
157
+ `{ ok: false, reason }`. Check `ok` before reading `value`; the `reason` is a
158
+ sentence that names the fix. A dead handle is a refusal, never a crash. Do not
159
+ wrap engine calls in `try`/`catch` to hide refusals; handle the `reason`.
160
+
161
+ ## 4. Systems work on archetypes, never on single entities
162
+
163
+ A system declares what it reads and writes and receives the matching
164
+ archetypes once per tick:
165
+
166
+ ```ts
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; // Float32Array, stride 3
170
+ const v = a.column(Velocity).v;
171
+ for (let i = 0; i < a.count; i++) {
172
+ p[i * 3] += v[i * 3] * time.dt;
173
+ p[i * 3 + 1] += v[i * 3 + 1] * time.dt;
174
+ p[i * 3 + 2] += v[i * 3 + 2] * time.dt;
175
+ }
176
+ }
177
+ });
178
+ ```
179
+
180
+ - Columns are typed arrays over engine memory. Write only components listed
181
+ in `writes`; reading needs `reads` or `writes`. An undeclared column throws
182
+ inside the system and fails the tick with the system's name.
183
+ - The same holds for resources and events: `world.resource`, an event read
184
+ (`world.events`) and an `og.Rng` fork need the record among `reads` or
185
+ `writes`; a resource write, an `og.Rng` draw and `world.emit` need it in
186
+ `writes`. Undeclared, it throws in `og run` and `og test` (dev and
187
+ release builds) with the system and the record; a publish build refuses
188
+ it, logs it once per system and access, and the game runs on.
189
+ - A resource handle is a typed view of the resource's packed record:
190
+ `world.resource(R)` hands back the same handle in a world every time, and
191
+ a field read or write is a typed-array access (about 0.02 µs, whatever
192
+ the resource's size; a 130-field run state costs what an 11-field one
193
+ does). The record is fetched again, in one engine call answering its
194
+ bytes (no JSON), only by the first read after another engine call
195
+ (`world.*`, `og.*`) or a system boundary, the only times the engine can
196
+ change it; writes are kept and written back in one call before the next
197
+ engine call and when the system or command returns. So two handles
198
+ always agree, a read after an engine call sees the engine's value, and
199
+ the world hash is what writing field by field gives. The rule: keep the
200
+ run in one resource and read and write its fields where they are used;
201
+ no plain copies, no handle passed around to save a fetch. What costs is
202
+ alternating an engine call with a read of a resource the system wrote
203
+ (one write-back and one fetch each time, a few µs): in a hot loop over
204
+ entities, read the fields into locals before the loop's engine calls.
205
+ - Views belong to one system call. Never keep an `ArchetypeView` or a column
206
+ array across ticks or in module state: a stale view throws with the tick
207
+ and the system it belonged to.
208
+ - Text is world state like a number: a `str(capacity)` field holds UTF-8 of
209
+ at most `capacity` bytes inline, zero-padded (`name: str(48)` takes any 12
210
+ characters, four bytes covering every script). `world.get`, `world.set`
211
+ and a resource's fields move it as a JS string; text over the capacity is
212
+ refused with the fix named (an error value from `world.insert`/`world.set`,
213
+ a throw from a resource's setter), never truncated. Its column is the raw
214
+ bytes (`Uint8Array`, stride the capacity). The probe reads it (`get:`,
215
+ `dump_entity`), `set:Prefs.name=Ada Lovelace` writes it (a JSON literal for
216
+ escapes: `set:Prefs.name=""`), a scene gives it one quoted string, and
217
+ snapshots and the world hash carry its bytes, so a name never lives in a
218
+ module variable or as numbers of code points.
219
+ A `str` holds 1 to 4,096 bytes: a larger capacity typechecks and is
220
+ refused when the game starts (`a string field holds 1 to 4096 bytes`).
221
+ - A field holds one value: a component has no array or list field. A few
222
+ values are numbered fields (`p0`..`p7`, as og.CameraRail's rail points);
223
+ a list is an entity per element with its own component, looped over by
224
+ a query (a ring of skid marks, the laps of a race); a long record (a
225
+ lap's taped inputs) is text in `str(4096)` chunks, an entity each.
226
+ - A handle is a 64-bit number (an `og.load` answer, a texture, a sound,
227
+ a geometry, a material, an entity): keep it in a field of its own kind,
228
+ `Asset()`, `Texture()`, `Sound()`, `Geometry()`, `Material()` or
229
+ `Entity()` (each handle type of the API has its factory), in a
230
+ component or a resource. A `u32` field refuses one (`4294967298 is not
231
+ a u32`).
232
+ - Per-entity calls (`world.get`, `world.set`) are for commands, setup and
233
+ the few named entities a system touches (a camera, a listener, the
234
+ player), not for hot loops. A system's `reads` and `writes` borrow
235
+ nothing by themselves: a column is borrowed when the system takes its
236
+ view (`a.column(C)`), and held until the system returns. So `world.get`
237
+ and `world.set` reach any entity its declarations match until the system
238
+ takes that archetype's column of the component; after it, they refuse
239
+ (naming the view), and the entity is read and written through the view's
240
+ arrays at its row (`a.entities`). Take per-entity calls first, views
241
+ after, or keep to one of the two in a system.
242
+ - A system has one query: every component in its `reads` and `writes`
243
+ together, so an entity matches only when it has all of them (and none of
244
+ `without`). A component declared only to `world.get` one named entity
245
+ (the player's car) narrows the loop to entities that also have it, often
246
+ to none, with no error. `world.query` is refused inside a system, by
247
+ design (a system reaches only what it declared): loop over each
248
+ archetype in a system of its own that declares only its components, and
249
+ pass what crosses between them through a resource.
250
+ - Order systems with `after` and `before` by name; declaration order is the
251
+ default.
252
+
253
+ ## 5. Reflection is the one schema
254
+
255
+ A component or resource is declared once with `defineComponent` /
256
+ `defineResource` and its column factories (`f32(100, { min: 0, max: 1000,
257
+ tunable: true })`, `Vector3()`). That one layout is what scenes, dumps, the
258
+ probe's `bounds` and `patch`, Tune and the wiki read. Never describe the same
259
+ data a second time (a JSON config mirroring a component, a hand-written
260
+ schema for the inspector). Put ranges, units and `tunable` on the field where
261
+ it is declared.
262
+
263
+ ## 6. Desc objects and names
264
+
265
+ - Desc objects (`SpawnDesc`, `SystemDesc`, `GameDesc`) may omit any field;
266
+ the zero value or the documented default applies. Pass `{}` rather than
267
+ inventing values the engine already defaults.
268
+ - Names follow the API: components, resources and systems PascalCase;
269
+ command actions snake_case; entity names are stable strings, because the
270
+ probe and the command channel address entities by them.
271
+ - Vector, quaternion, matrix, colour, transform and material vocabulary is
272
+ three.js's (`position`, `quaternion`, `scale`, `metalness`, `roughness`).
273
+
274
+ ## 7. Where the API is described
275
+
276
+ The generated wiki (`reference/index.md` next to the `og-game` skill, or
277
+ `wiki/index.md` in `@origingame/engine-knowledge`) has one page per module
278
+ with every signature quoted from the IDL, grouped by the skill that teaches
279
+ it. This skill's are the ECS and its rules: `reference/world.md` (entities,
280
+ queries, resources, events), `reference/system.md` (systems and their
281
+ order), `reference/reflect.md` (components, resources and their column
282
+ factories), `reference/rng.md` (seeded streams) and `reference/math.md`
283
+ (vectors, quaternions, colours as data). When the wiki and memory disagree,
284
+ the wiki is right: it is regenerated from the same IDL the engine is built
285
+ from. The other skills, and which modules each teaches, are og-game §1.