@origingame/engine 0.3.0 → 0.4.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 (84) hide show
  1. package/dist/web/origin-engine.d.ts +38 -2
  2. package/dist/web/origin-engine.js +250 -10
  3. package/dist/web/origin-engine_bg.wasm +0 -0
  4. package/dist/web/origin-engine_bg.wasm.d.ts +14 -2
  5. package/dist/web-publish/origin-engine-lanes.js +255 -34
  6. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  7. package/dist/web-publish/origin-engine.d.ts +39 -3
  8. package/dist/web-publish/origin-engine.js +253 -32
  9. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  10. package/dist/web-publish/origin-engine_bg.wasm.d.ts +15 -3
  11. package/dist/web-publish-2d/origin-engine-lanes.js +253 -32
  12. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  13. package/dist/web-publish-2d/origin-engine.d.ts +38 -2
  14. package/dist/web-publish-2d/origin-engine.js +250 -29
  15. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  16. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +14 -2
  17. package/knowledge/README.md +2 -1
  18. package/knowledge/guides/three-to-og.md +393 -0
  19. package/knowledge/knowledge.json +2 -1
  20. package/knowledge/package.json +3 -2
  21. package/knowledge/scaffold/origin-engine.json +2 -2
  22. package/knowledge/scaffold/template/package.json +1 -1
  23. package/knowledge/scaffold/template/project.ogt +1 -1
  24. package/knowledge/scripts/validate.ts +140 -21
  25. package/knowledge/skills/og-audio/SKILL.md +2 -2
  26. package/knowledge/skills/og-build/SKILL.md +22 -3
  27. package/knowledge/skills/og-core/SKILL.md +20 -5
  28. package/knowledge/skills/og-feel/SKILL.md +2 -2
  29. package/knowledge/skills/og-game/SKILL.md +481 -35
  30. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  31. package/knowledge/skills/og-model/SKILL.md +374 -0
  32. package/knowledge/skills/og-platform-sdk/SKILL.md +1 -1
  33. package/knowledge/skills/og-ui/SKILL.md +15 -8
  34. package/knowledge/wiki/ai.md +20 -20
  35. package/knowledge/wiki/anim.md +63 -55
  36. package/knowledge/wiki/atmosphere.md +13 -13
  37. package/knowledge/wiki/audio.md +18 -18
  38. package/knowledge/wiki/camera.md +13 -13
  39. package/knowledge/wiki/commands.md +11 -11
  40. package/knowledge/wiki/debug.md +13 -13
  41. package/knowledge/wiki/game.md +8 -8
  42. package/knowledge/wiki/i18n.md +7 -7
  43. package/knowledge/wiki/index.md +57 -3
  44. package/knowledge/wiki/input.md +47 -47
  45. package/knowledge/wiki/load.md +57 -57
  46. package/knowledge/wiki/nav.md +34 -34
  47. package/knowledge/wiki/net.md +10 -10
  48. package/knowledge/wiki/path.md +14 -14
  49. package/knowledge/wiki/physics.md +95 -95
  50. package/knowledge/wiki/platform.md +44 -44
  51. package/knowledge/wiki/probe.md +86 -86
  52. package/knowledge/wiki/reflect.md +16 -4
  53. package/knowledge/wiki/render.md +1168 -170
  54. package/knowledge/wiki/rng.md +14 -14
  55. package/knowledge/wiki/shapes.md +727 -0
  56. package/knowledge/wiki/stream.md +15 -15
  57. package/knowledge/wiki/system.md +8 -8
  58. package/knowledge/wiki/systems.md +2 -2
  59. package/knowledge/wiki/terrain.md +24 -20
  60. package/knowledge/wiki/tilemap.md +13 -13
  61. package/knowledge/wiki/time.md +4 -4
  62. package/knowledge/wiki/timeline.md +14 -14
  63. package/knowledge/wiki/transform.md +8 -8
  64. package/knowledge/wiki/tween.md +17 -17
  65. package/knowledge/wiki/ui.md +245 -269
  66. package/knowledge/wiki/video.md +5 -5
  67. package/knowledge/wiki/world.md +36 -36
  68. package/package.json +4 -4
  69. package/schema/api.schema.json +1834 -305
  70. package/shell/platform.ts +5 -0
  71. package/shell/player.ts +8 -2
  72. package/shell/transport.ts +6 -0
  73. package/src/generated/abi.ts +37 -1
  74. package/src/geom.ts +260 -0
  75. package/src/index.ts +8 -1
  76. package/src/node/playtest-run.ts +1 -0
  77. package/src/platform-local.ts +2 -0
  78. package/src/playtest.ts +8 -0
  79. package/src/runtime.ts +87 -6
  80. package/src/ui-kit/settings.ts +9 -0
  81. package/src/web/shell.ts +1 -1
  82. package/src/web/worker.ts +2 -2
  83. package/types/bindings.json +697 -402
  84. package/types/index.d.ts +1045 -127
@@ -44,7 +44,20 @@ export function readRules(pkg: string): Rules {
44
44
  }
45
45
 
46
46
  /** The skills this package ships. */
47
- export const SKILLS = ["og-ai", "og-audio", "og-build", "og-core", "og-feel", "og-game", "og-i18n", "og-platform-sdk", "og-taste", "og-ui"] as const;
47
+ export const SKILLS = ["og-ai", "og-audio", "og-build", "og-core", "og-feel", "og-game", "og-i18n", "og-model", "og-platform-sdk", "og-taste", "og-ui"] as const;
48
+
49
+ /**
50
+ * The guides this package ships: hand-written pages in guides/, mounted
51
+ * beside og-game's SKILL.md (knowledge.json), each named by og-game and
52
+ * held to the skills' rules (API names, section references, tools).
53
+ */
54
+ export const GUIDES = ["three-to-og"] as const;
55
+
56
+ /** Where knowledge.json mounts guides/ in the production tree. */
57
+ export const GUIDE_MOUNT = "game/skills/og-game/guides/";
58
+
59
+ /** The og-cli source that cites the guides by section, in the engine repository. */
60
+ export const GUIDE_CITERS = ["crates/og-cli/src/migrate.rs"] as const;
48
61
 
49
62
  /** What a scaffolded project must contain (scaffold/README.md describes each). */
50
63
  export const TEMPLATE_FILES = ["project.ogt", "src/main.ts", "package.json", "tsconfig.json", "tests/smoke.og.ts"] as const;
@@ -79,7 +92,9 @@ export function validate(roots: Roots = defaultRoots()): string[] {
79
92
  }
80
93
 
81
94
  skills(rules, files, problems);
95
+ guideFiles(roots, rules, files, problems);
82
96
  coherence(files, problems);
97
+ skinWord(roots, files, problems);
83
98
  apiNames(roots, files, problems);
84
99
  looks(roots, files, problems);
85
100
  grammar(roots, files, problems);
@@ -91,7 +106,7 @@ function packageJson(roots: Roots, problems: string[]): void {
91
106
  const p = JSON.parse(readFileSync(join(roots.pkg, "package.json"), "utf8")) as { name?: string; version?: string; files?: string[]; scripts?: Record<string, string> };
92
107
  if (p.name !== "@origingame/engine-knowledge") problems.push(`package.json: name is ${p.name}; the package is @origingame/engine-knowledge (design §9)`);
93
108
  if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(p.version ?? "")) problems.push(`package.json: version ${p.version} is not semver`);
94
- for (const f of ["wiki", "skills", "looks", "scaffold", "rules", "knowledge.json"]) {
109
+ for (const f of ["wiki", "skills", "guides", "looks", "scaffold", "rules", "knowledge.json"]) {
95
110
  if (!p.files?.some((x) => x === f || x === `${f}/`)) problems.push(`package.json: files does not publish ${f}`);
96
111
  }
97
112
  if (!p.scripts?.test) problems.push("package.json: no test script, so scripts/check.sh bun-test would skip the package");
@@ -165,6 +180,46 @@ function skills(rules: Rules, files: readonly PayloadFile[], problems: string[])
165
180
  for (const s of SKILLS) if (!found.has(s)) problems.push(`skills/${s}/SKILL.md: missing`);
166
181
  }
167
182
 
183
+ /**
184
+ * The guides as files: guides/ holds exactly GUIDES, each reaches the
185
+ * payload at GUIDE_MOUNT, names no tool a Session lacks and no wiki page
186
+ * the IDL does not generate, and every `three-to-og §N` the og-cli source
187
+ * cites (GUIDE_CITERS: `og migrate three` points to its sections) is a
188
+ * numbered section of the guide.
189
+ */
190
+ function guideFiles(roots: Roots, rules: Rules, files: readonly PayloadFile[], problems: string[]): void {
191
+ const dir = join(roots.pkg, "guides");
192
+ const present = existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -3)) : [];
193
+ for (const g of present) if (!(GUIDES as readonly string[]).includes(g)) problems.push(`guides/${g}.md: not in GUIDES (scripts/validate.ts); list it there and name it from og-game, or remove it`);
194
+ const pages = new Set(files.map((f) => /^game\/skills\/og-game\/reference\/([^/]+)$/.exec(f.path)?.[1]).filter((n): n is string => n !== undefined));
195
+ const repo = join(roots.pkg, "..", "..");
196
+ for (const g of GUIDES) {
197
+ const f = files.find((x) => x.path === `${GUIDE_MOUNT}${g}.md`);
198
+ if (!f) {
199
+ problems.push(`guides/${g}.md: missing, or not mounted at ${GUIDE_MOUNT} (knowledge.json mounts guides/ there)`);
200
+ continue;
201
+ }
202
+ if (!isText(f.bytes)) continue;
203
+ const text = decoder.decode(f.bytes);
204
+ for (const t of namedTools(text, rules)) problems.push(`guides/${g}.md: names \`${t}\`, which is no tool a Session can be offered; rename it or put it in a code block if it is not a tool`);
205
+ for (const page of new Set([...text.matchAll(/\breference\/([a-z0-9-]+\.md)\b/g)].map((m) => m[1]!))) {
206
+ if (!pages.has(page)) problems.push(`guides/${g}.md: names reference/${page}, which the generated wiki does not have (a module's page is reference/<module>.md)`);
207
+ }
208
+ const have = new Set([...text.matchAll(/^## (\d+)\. /gm)].map((m) => m[1]!));
209
+ for (const citer of GUIDE_CITERS) {
210
+ const path = join(repo, citer);
211
+ if (!existsSync(join(repo, "crates"))) break; // the published package: the sources are not beside it
212
+ if (!existsSync(path)) {
213
+ problems.push(`${citer}: missing; GUIDE_CITERS in scripts/validate.ts names it`);
214
+ continue;
215
+ }
216
+ for (const m of readFileSync(path, "utf8").matchAll(new RegExp(`${g} §(\\d+)`, "g"))) {
217
+ if (!have.has(m[1]!)) problems.push(`${citer}: cites ${g} §${m[1]}, which guides/${g}.md does not have (its sections: ${[...have].join(", ")})`);
218
+ }
219
+ }
220
+ }
221
+ }
222
+
168
223
  function scaffold(roots: Roots, rules: Rules, overlay: string, files: readonly PayloadFile[], byPath: Map<string, PayloadFile>, problems: string[]): void {
169
224
  const prefix = `game-scaffold/${overlay}/`;
170
225
  if (rules.scaffold.existingOverlays.includes(overlay)) {
@@ -255,7 +310,15 @@ function coherence(files: readonly PayloadFile[], problems: string[]): void {
255
310
  const t = textOf(`game/skills/${s}/SKILL.md`);
256
311
  if (t !== undefined) texts.set(s, t);
257
312
  }
258
- const sections = new Map([...texts].map(([s, t]) => [s, new Set([...t.matchAll(/^## (\d+)\. /gm)].map((m) => m[1]!))]));
313
+ // The guides (guides/<name>.md, mounted beside og-game's SKILL.md) are
314
+ // held to the same section references; their owner names each.
315
+ const guides = new Map<string, string>();
316
+ for (const g of GUIDES) {
317
+ const t = textOf(`${GUIDE_MOUNT}${g}.md`);
318
+ if (t !== undefined) guides.set(g, t);
319
+ }
320
+ const numbered = (t: string) => new Set([...t.matchAll(/^## (\d+)\. /gm)].map((m) => m[1]!));
321
+ const sections = new Map([...texts, ...guides].map(([s, t]) => [s, numbered(t)]));
259
322
  for (const [module, skill] of home) {
260
323
  const t = texts.get(skill);
261
324
  if (t !== undefined && !t.includes(`reference/${module}.md`)) problems.push(`skills/${skill}/SKILL.md: teaches the ${module} module (wiki/index.md) but never names reference/${module}.md`);
@@ -265,22 +328,76 @@ function coherence(files: readonly PayloadFile[], problems: string[]): void {
265
328
  const owner = home.get(m);
266
329
  if (owner && owner !== skill) problems.push(`skills/${skill}/SKILL.md: names reference/${m}.md, which ${owner} teaches; point to ${owner} instead, so each module is taught in one place`);
267
330
  }
268
- const prose = t.replace(/^```[\s\S]*?^```/gm, "");
269
- for (const m of prose.matchAll(/(og-[a-z]+(?:-[a-z]+)*) §([A-Za-z][\w ]*)/g)) problems.push(`skills/${skill}/SKILL.md: refers to ${m[1]} §${m[2]!.split(" ")[0]} by name; refer to a skill's sections by their numbers (## N.)`);
270
- for (const m of prose.matchAll(/§(\d+)/g)) {
271
- // The skill this § belongs to: walk back over a list of §s ("og-feel
272
- // §9–§11", "og-taste §4, §10") to the word before it.
273
- const before = prose.slice(0, m.index).split(/\s+/);
274
- let i = before.length - 1;
275
- while (i >= 0 && (before[i] === "" || /^\(?§\d+(\.\d+)?[–,;)]*$/.test(before[i]!) || before[i] === "and" || before[i] === "or")) i--;
276
- const word = (before[i] ?? "").replace(/^[(`*]+/, "");
277
- let target: string | undefined = skill;
278
- if (/^og-[a-z]+(?:-[a-z]+)*$/.test(word)) target = word;
279
- else if (/\.md\b|\//.test(word) || /^design/.test(word)) target = undefined;
280
- if (target === undefined) continue;
281
- const have = sections.get(target);
282
- if (!have) problems.push(`skills/${skill}/SKILL.md: refers to ${target} §${m[1]}, and there is no skill ${target}`);
283
- else if (!have.has(m[1]!)) problems.push(`skills/${skill}/SKILL.md: refers to ${target === skill ? "its own" : target} §${m[1]}, which ${target === skill ? "it does not have" : "does not exist there"} (its sections: ${[...have].join(", ") || "none numbered"})`);
331
+ }
332
+ for (const [name, t] of [...texts, ...guides]) {
333
+ const at = guides.has(name) ? `guides/${name}.md` : `skills/${name}/SKILL.md`;
334
+ problems.push(...sectionRefs(at, name, t, sections));
335
+ }
336
+ // What the guides say of a page, and who names them.
337
+ const names = (t: string) => new Set([...t.matchAll(/\bguides\/([a-z0-9-]+)\.md\b/g)].map((m) => m[1]!));
338
+ for (const [name, t] of [...texts, ...guides]) {
339
+ const at = guides.has(name) ? `guides/${name}.md` : `skills/${name}/SKILL.md`;
340
+ for (const g of names(t)) if (!(GUIDES as readonly string[]).includes(g)) problems.push(`${at}: names guides/${g}.md, which this package does not ship (its guides: ${GUIDES.join(", ")})`);
341
+ }
342
+ for (const g of guides.keys()) {
343
+ if (!texts.get("og-game")?.includes(`guides/${g}.md`)) problems.push(`skills/og-game/SKILL.md: never names guides/${g}.md, so no agent finds the guide; name it where its subject is taught`);
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Every section reference of one text (`og-feel §4`, or `§4` for its own,
349
+ * `three-to-og §2` for a guide's) names a numbered `## N.` section that
350
+ * exists: `at` names the text in the messages, `own` the text itself.
351
+ */
352
+ export function sectionRefs(at: string, own: string, text: string, sections: ReadonlyMap<string, ReadonlySet<string>>): string[] {
353
+ const problems: string[] = [];
354
+ const prose = text.replace(/^```[\s\S]*?^```/gm, "");
355
+ for (const m of prose.matchAll(/(og(?:-[a-z]+)+|three-to-og) §([A-Za-z][\w ]*)/g)) problems.push(`${at}: refers to ${m[1]} §${m[2]!.split(" ")[0]} by name; refer to a skill's sections by their numbers (## N.)`);
356
+ for (const m of prose.matchAll(/§(\d+)/g)) {
357
+ // The text this § belongs to: walk back over a list of §s ("og-feel
358
+ // §9–§11", "og-taste §4, §10") to the word before it.
359
+ const before = prose.slice(0, m.index).split(/\s+/);
360
+ let i = before.length - 1;
361
+ while (i >= 0 && (before[i] === "" || /^\(?§\d+(\.\d+)?[–,;)]*$/.test(before[i]!) || before[i] === "and" || before[i] === "or")) i--;
362
+ const word = (before[i] ?? "").replace(/^[(`*]+/, "");
363
+ let target: string | undefined = own;
364
+ if (/^og-[a-z]+(?:-[a-z]+)*$/.test(word) || (GUIDES as readonly string[]).includes(word)) target = word;
365
+ else if (/\.md\b|\//.test(word) || /^design/.test(word)) target = undefined;
366
+ if (target === undefined) continue;
367
+ const have = sections.get(target);
368
+ if (!have) problems.push(`${at}: refers to ${target} §${m[1]}, and there is no skill ${target}`);
369
+ else if (!have.has(m[1]!)) problems.push(`${at}: refers to ${target === own ? "its own" : target} §${m[1]}, which ${target === own ? "it does not have" : "does not exist there"} (its sections: ${[...have].join(", ") || "none numbered"})`);
370
+ }
371
+ return problems;
372
+ }
373
+
374
+ /**
375
+ * What a sentence that says "skin" must also name for the word to be one of
376
+ * its other meanings: a glTF rig's skin (joints, morphs, a model), the
377
+ * character controllers' collision gap (the `skin` field, off a surface),
378
+ * a 2D skeleton's skin (a rig2d `skin` block swapping a slot's attachment,
379
+ * `og.Skeleton2D.skin`, an armature's setup pose), or a surface (a skin
380
+ * tone, peach skin, a crowd's or a tile set's looks).
381
+ */
382
+ export const SKIN_ELSEWHERE = /gltf|joint|skelet|morph|\brig\b|bone|mesh|model|clips?\b|anim|`skins?`|slot|attach|armature|\bswap\b|setup pose|surface|\bgap\b|collid|\btones?\b|peach|crowd|tiles/i;
383
+
384
+ /**
385
+ * A skill or wiki page never calls a UI style a "skin": og-ui's word is
386
+ * style (`type style`, `og.load.style`, `UiDesc.style`, `styles/<name>/`).
387
+ * Each sentence holding `skin` or `skins` must name one of the word's
388
+ * other meanings (SKIN_ELSEWHERE).
389
+ */
390
+ function skinWord(roots: Roots, files: readonly PayloadFile[], problems: string[]): void {
391
+ for (const f of files) {
392
+ if (!f.path.startsWith("game/skills/") || !f.path.endsWith(".md") || !isText(f.bytes)) continue;
393
+ const name = relative(roots.pkg, f.source).split("\\").join("/");
394
+ for (const paragraph of decoder.decode(f.bytes).split(/\n\s*\n/)) {
395
+ // Sentences, and a table's cells, of the paragraph's joined lines.
396
+ for (const sentence of paragraph.replace(/\s*\n\s*/g, " ").split(/(?<=[.;!?])\s+|\s\|\s/)) {
397
+ if (/\bskins?\b/i.test(sentence) && !SKIN_ELSEWHERE.test(sentence)) {
398
+ problems.push(`${name}: calls a UI style a skin ("${sentence.trim().slice(0, 120)}"); og-ui's word is style (type style, og.load.style, styles/<name>/)`);
399
+ }
400
+ }
284
401
  }
285
402
  }
286
403
  }
@@ -312,6 +429,7 @@ export const NOT_FIELDS: Record<string, string> = {
312
429
  runs: "PlaytestDesc.runs (packages/engine/src/playtest.ts)",
313
430
  then: "PlaytestDesc.then (packages/engine/src/playtest.ts)",
314
431
  store: "PlaytestRun.store (packages/engine/src/playtest.ts)",
432
+ setup: "PlaytestDesc.setup (packages/engine/src/playtest.ts)",
315
433
  };
316
434
 
317
435
  export function readApiNames(engine: string): ApiNames | undefined {
@@ -353,8 +471,9 @@ function apiNames(roots: Roots, files: readonly PayloadFile[], problems: string[
353
471
  if (!api) return;
354
472
  for (const f of files) {
355
473
  const m = /^game\/skills\/([^/]+)\/SKILL\.md$/.exec(f.path);
356
- if (!m || !isText(f.bytes)) continue;
357
- const at = `skills/${m[1]}/SKILL.md`;
474
+ const guide = f.path.startsWith(GUIDE_MOUNT) && f.path.endsWith(".md") ? f.path.slice(GUIDE_MOUNT.length) : undefined;
475
+ if ((!m && !guide) || !isText(f.bytes)) continue;
476
+ const at = guide ? `guides/${guide}` : `skills/${m![1]}/SKILL.md`;
358
477
  const text = decoder.decode(f.bytes);
359
478
  const bad = new Set<string>();
360
479
  for (const x of text.matchAll(/(?<![\w./@-])og\.([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?/g)) {
@@ -58,14 +58,14 @@ game's style needs it; presets are a floor, not a style.
58
58
  ## 3. UI sounds
59
59
 
60
60
  `og.ui.configure(world, { sounds: { hover, press, open, close, success, error } })`
61
- (or the skin's `sound` lines) plays them on the `ui` bus: `click` for
61
+ (or the style's `sound` lines) plays them on the `ui` bus: `click` for
62
62
  press, `select` for success, `back` for close, `error` for a refused press
63
63
  (and for `og.ui.shake({ sound: true })`, og-ui §6). Each starts in the
64
64
  tick of its input. `hover` also answers keyboard and gamepad focus moving
65
65
  to a control, and `press` a press by Enter, Space or the pad's A, so a
66
66
  menu walked with a pad is heard step by step; keep `hover` short and quiet
67
67
  (volume ≤ 0.3). `configure` changes only the fields it is given: setting
68
- the sounds after a skin (or a look that brings one) keeps the skin and its
68
+ the sounds after a style (or a look that brings one) keeps the style and its
69
69
  fonts, and `sounds: null` silences them again.
70
70
 
71
71
  ## 4. Buses and the mix
@@ -14,7 +14,7 @@ needs a browser or a Rust toolchain. The command channel is
14
14
  The loop held for two whole games written from this package alone
15
15
  (`examples/lantern-vale`, `examples/hollowbell`): `og cook` took 47 and 69
16
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 UI skin, as
17
+ warning, and every refusal named the line and the fix (a copied UI style, as
18
18
  styles were then called, without its CJK `fallback` named the line and og-taste §1;
19
19
  `PostEffects.vignette is true or false` named the line). Scripted runs gave
20
20
  the same frames every time, `store: "keep"` relaunches and two-process
@@ -169,6 +169,15 @@ same store, for a relaunch (og-platform-sdk §4).
169
169
  - Determinism is checkable: two runs with the same seed and script give the
170
170
  same world hash in `dump_world`. When they do not, find the wall-clock
171
171
  read or the unseeded randomness before anything else.
172
+ - A model built as code (og-model) is judged on numbers and one sheet,
173
+ not on a frame read by eye: `view:<entity>:front|back|side|top`,
174
+ `view:<entity>:orbit:<degrees>` and `turntable:<entity>:<n>` frame it
175
+ from fixed cameras, and `og compare <reference.png> <render.png>`
176
+ prints the silhouette IoU and the ΔE00 per mask region and writes the
177
+ reference beside the render as one sheet PNG (`--mask`, `--align`,
178
+ `--json`). Two runs give the same numbers and the same sheet bytes, so
179
+ quote them with the command. og-model §3 has the commands and how to
180
+ read them, og-model §4 the gate each pass must meet.
172
181
  - Report what ran and what did not. A build that compiled is not a game
173
182
  that plays.
174
183
 
@@ -237,14 +246,24 @@ run it again after the fix: the report is the evidence.
237
246
  `og build .` bundles `src/` to `dist/game.js`, cooks, and copies the engine
238
247
  runtime and wasm next to it. `og build . --publish` produces the published
239
248
  form: the probe keeps only its player-facing members (hello, tick, state,
240
- pick, entity, input, capture) and the command channel is gone.
249
+ pick, entity, input, capture) and the command channel is gone. A `2d`
250
+ project publishes the 2D module, which has no 3D world
251
+ (`RenderCapabilities.world3d` false): the build refuses scenes holding
252
+ `Terrain`, `TerrainScatter`, `Water`, `WaterSurface` or `Fluid3D`, naming
253
+ each by scene file and line (og-game §2).
241
254
  What ships is still checked by script, through what it keeps: real input
242
255
  on the published page and the probe's `tick`, `state` and `capture`. A
243
256
  playtest written in input steps (`key_down`, `type`, `touch_move_to` an
244
257
  id, `move_to_text`, `touch_start`) and `screenshot` steps runs there as it
245
258
  runs under `og test`, each frame compared with the same references; a
246
259
  game command (warp, give) has no channel there and is refused, so keep
247
- the steps a published check needs free of them. The engine runs this on
260
+ the steps a published check needs free of them. The page shows the
261
+ Player's Back over the canvas's top-left corner (12, 12, 40 by 40 CSS px
262
+ on a desktop page; og-ui §5), which anchored HUD groups keep out of, so
263
+ a playtest the page is held to gives its native run the same cover with
264
+ `setup: "cover:12,12,40,40"` (`PlaytestDesc.setup`: command steps run
265
+ once after `start`, before the first tick, never part of the page's
266
+ script) and its frames lay out as the page's. The engine runs this on
248
267
  the published build of its own games (its `tests/web/published.ts`: the
249
268
  boot, the title, a press, a typed name and a frame against the metal
250
269
  reference, with a clean console).
@@ -218,11 +218,26 @@ export const Move = defineSystem("Move", { reads: [Velocity], writes: [og.Transf
218
218
  module variable or as numbers of code points.
219
219
  A `str` holds 1 to 4,096 bytes: a larger capacity typechecks and is
220
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.
221
+ - A field holds one value, or a fixed-capacity list of one number or
222
+ handle kind: `list(u32(), 8)`, `list(Asset(), 4)` (1 to 1,024 elements,
223
+ starting empty, never tunable). Reading it answers a frozen array, a
224
+ copy: `push`, `splice` and index writes throw, so write it whole
225
+ (`v.passes = [...v.passes, h]`, `v.passes = []`, `world.set(e, Chain,
226
+ { passes: [3, 5] })`). One over the capacity is refused with the fix
227
+ named (`declare the field list(u32(), 9) or larger`), never truncated.
228
+ Its column is the element's typed array with (capacity + 1) lanes per
229
+ entity, the count first, zeros past it, so `[]` and `[0]` are different
230
+ worlds. The probe reads it as a JSON array (`get:`, `dump_entity`),
231
+ `set:Chain.passes=[3,5]` (or `3,5`, nothing for `[]`) writes it whole, a
232
+ scene or a look gives it on one line (`passes 3 5`, the key alone for
233
+ `[]`), and snapshots and the hash carry it; a migrating restore keeps a
234
+ list that fits a new capacity and empties one that does not.
235
+ A list holds numbers only: a few vectors are numbered fields
236
+ (`p0`..`p7`, as og.CameraRail's rail points); a list of things with
237
+ fields of their own is an entity per element with its own component,
238
+ looped over by a query (a ring of skid marks, the laps of a race); a
239
+ long record (a lap's taped inputs) is text in `str(4096)` chunks, an
240
+ entity each.
226
241
  - A handle is a 64-bit number (an `og.load` answer, a texture, a sound,
227
242
  a geometry, a material, an entity): keep it in a field of its own kind,
228
243
  `Asset()`, `Texture()`, `Sound()`, `Geometry()`, `Material()` or
@@ -135,7 +135,7 @@ is what a sprite wants.
135
135
  | Take-off | (0.85, 1.15) | 0.1 s |
136
136
  | Hit taken | (1.2, 0.8) | 0.15 s, with a flash |
137
137
  | Pickup collected (on the pickup) | (1.4, 1.4) then shrink to 0 | 0.15 s |
138
- | UI button press (the skin's `press` does it for og.ui) | (0.92, 0.92) | 0.08 s, unscaled |
138
+ | UI button press (the style's `press` does it for og.ui) | (0.92, 0.92) | 0.08 s, unscaled |
139
139
 
140
140
  Do not squash rigid things (crates, walls, the camera's target in a
141
141
  cutscene) or anything more than once per 6 ticks.
@@ -161,7 +161,7 @@ motion and hit stop (UI motion). `og.tween.squash` (§5) and
161
161
  | Constant motion, timers, scrolling, colour cycles | `linear` |
162
162
  | Most small motion when unsure | `quadOut` (the default) |
163
163
 
164
- Durations: 0.1–0.2 s for feedback, 0.12–0.3 s for UI (the skin's
164
+ Durations: 0.1–0.2 s for feedback, 0.12–0.3 s for UI (the style's
165
165
  durations, og-taste §5), 0.2–0.35 s for swaps, 0.4–0.8 s for camera moves,
166
166
  reveals and screen transitions (up to 0.7 s). Longer feels sluggish unless
167
167
  it is a cutscene or a reward.