@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.
- package/dist/web/origin-engine.d.ts +38 -2
- package/dist/web/origin-engine.js +250 -10
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +14 -2
- package/dist/web-publish/origin-engine-lanes.js +255 -34
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +39 -3
- package/dist/web-publish/origin-engine.js +253 -32
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +15 -3
- package/dist/web-publish-2d/origin-engine-lanes.js +253 -32
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +38 -2
- package/dist/web-publish-2d/origin-engine.js +250 -29
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +14 -2
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scripts/validate.ts +140 -21
- package/knowledge/skills/og-audio/SKILL.md +2 -2
- package/knowledge/skills/og-build/SKILL.md +22 -3
- package/knowledge/skills/og-core/SKILL.md +20 -5
- package/knowledge/skills/og-feel/SKILL.md +2 -2
- package/knowledge/skills/og-game/SKILL.md +481 -35
- package/knowledge/skills/og-i18n/SKILL.md +7 -7
- package/knowledge/skills/og-model/SKILL.md +374 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +1 -1
- package/knowledge/skills/og-ui/SKILL.md +15 -8
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +63 -55
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +18 -18
- package/knowledge/wiki/camera.md +13 -13
- package/knowledge/wiki/commands.md +11 -11
- package/knowledge/wiki/debug.md +13 -13
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +57 -3
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +57 -57
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +10 -10
- package/knowledge/wiki/path.md +14 -14
- package/knowledge/wiki/physics.md +95 -95
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +16 -4
- package/knowledge/wiki/render.md +1168 -170
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +727 -0
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +24 -20
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +14 -14
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +245 -269
- package/knowledge/wiki/video.md +5 -5
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -4
- package/schema/api.schema.json +1834 -305
- package/shell/platform.ts +5 -0
- package/shell/player.ts +8 -2
- package/shell/transport.ts +6 -0
- package/src/generated/abi.ts +37 -1
- package/src/geom.ts +260 -0
- package/src/index.ts +8 -1
- package/src/node/playtest-run.ts +1 -0
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +8 -0
- package/src/runtime.ts +87 -6
- package/src/ui-kit/settings.ts +9 -0
- package/src/web/shell.ts +1 -1
- package/src/web/worker.ts +2 -2
- package/types/bindings.json +697 -402
- 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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
357
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
|
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.
|