@origingame/engine 0.3.0 → 0.5.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 +44 -5
- package/dist/web/origin-engine.js +279 -19
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +18 -5
- package/dist/web-publish/origin-engine-lanes.js +289 -48
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +44 -5
- package/dist/web-publish/origin-engine.js +279 -38
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +18 -5
- package/dist/web-publish-2d/origin-engine-lanes.js +289 -48
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +44 -5
- package/dist/web-publish-2d/origin-engine.js +279 -38
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +18 -5
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/looks/catalogue.json +123 -2
- 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 +209 -27
- package/knowledge/skills/og-audio/SKILL.md +33 -7
- package/knowledge/skills/og-build/SKILL.md +66 -7
- package/knowledge/skills/og-core/SKILL.md +24 -8
- package/knowledge/skills/og-feel/SKILL.md +11 -3
- package/knowledge/skills/og-game/SKILL.md +680 -51
- 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 +7 -1
- package/knowledge/skills/og-taste/SKILL.md +7 -0
- package/knowledge/skills/og-ui/SKILL.md +39 -8
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +537 -63
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +160 -20
- 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 +81 -3
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +83 -57
- package/knowledge/wiki/math.md +102 -102
- 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 +111 -100
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +30 -18
- package/knowledge/wiki/render.md +1243 -176
- 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 +2238 -81
- package/shell/page.ts +4 -0
- package/shell/platform.ts +5 -0
- package/shell/player.ts +8 -2
- package/shell/transport.ts +6 -0
- package/src/generated/abi.ts +40 -1
- package/src/geom.ts +260 -0
- package/src/index.ts +19 -1
- package/src/node/device.ts +5 -2
- package/src/node/playtest-run.ts +1 -0
- package/src/node/polish.ts +34 -15
- package/src/node/run.ts +23 -6
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +8 -0
- package/src/runtime.ts +88 -6
- package/src/ui-kit/settings.ts +9 -0
- package/src/web/gl-state.ts +907 -0
- package/src/web/shell.ts +1 -1
- package/src/web/worker.ts +8 -2
- package/types/bindings.json +1222 -453
- package/types/index.d.ts +1868 -315
|
@@ -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,10 +92,13 @@ 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);
|
|
101
|
+
problems.push(...ogtShape(skillTexts(files)));
|
|
86
102
|
scaffold(roots, rules, manifest.overlay, files, byPath, problems);
|
|
87
103
|
return problems;
|
|
88
104
|
}
|
|
@@ -91,7 +107,7 @@ function packageJson(roots: Roots, problems: string[]): void {
|
|
|
91
107
|
const p = JSON.parse(readFileSync(join(roots.pkg, "package.json"), "utf8")) as { name?: string; version?: string; files?: string[]; scripts?: Record<string, string> };
|
|
92
108
|
if (p.name !== "@origingame/engine-knowledge") problems.push(`package.json: name is ${p.name}; the package is @origingame/engine-knowledge (design §9)`);
|
|
93
109
|
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"]) {
|
|
110
|
+
for (const f of ["wiki", "skills", "guides", "looks", "scaffold", "rules", "knowledge.json"]) {
|
|
95
111
|
if (!p.files?.some((x) => x === f || x === `${f}/`)) problems.push(`package.json: files does not publish ${f}`);
|
|
96
112
|
}
|
|
97
113
|
if (!p.scripts?.test) problems.push("package.json: no test script, so scripts/check.sh bun-test would skip the package");
|
|
@@ -165,6 +181,46 @@ function skills(rules: Rules, files: readonly PayloadFile[], problems: string[])
|
|
|
165
181
|
for (const s of SKILLS) if (!found.has(s)) problems.push(`skills/${s}/SKILL.md: missing`);
|
|
166
182
|
}
|
|
167
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The guides as files: guides/ holds exactly GUIDES, each reaches the
|
|
186
|
+
* payload at GUIDE_MOUNT, names no tool a Session lacks and no wiki page
|
|
187
|
+
* the IDL does not generate, and every `three-to-og §N` the og-cli source
|
|
188
|
+
* cites (GUIDE_CITERS: `og migrate three` points to its sections) is a
|
|
189
|
+
* numbered section of the guide.
|
|
190
|
+
*/
|
|
191
|
+
function guideFiles(roots: Roots, rules: Rules, files: readonly PayloadFile[], problems: string[]): void {
|
|
192
|
+
const dir = join(roots.pkg, "guides");
|
|
193
|
+
const present = existsSync(dir) ? readdirSync(dir).filter((n) => n.endsWith(".md")).map((n) => n.slice(0, -3)) : [];
|
|
194
|
+
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`);
|
|
195
|
+
const pages = new Set(files.map((f) => /^game\/skills\/og-game\/reference\/([^/]+)$/.exec(f.path)?.[1]).filter((n): n is string => n !== undefined));
|
|
196
|
+
const repo = join(roots.pkg, "..", "..");
|
|
197
|
+
for (const g of GUIDES) {
|
|
198
|
+
const f = files.find((x) => x.path === `${GUIDE_MOUNT}${g}.md`);
|
|
199
|
+
if (!f) {
|
|
200
|
+
problems.push(`guides/${g}.md: missing, or not mounted at ${GUIDE_MOUNT} (knowledge.json mounts guides/ there)`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!isText(f.bytes)) continue;
|
|
204
|
+
const text = decoder.decode(f.bytes);
|
|
205
|
+
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`);
|
|
206
|
+
for (const page of new Set([...text.matchAll(/\breference\/([a-z0-9-]+\.md)\b/g)].map((m) => m[1]!))) {
|
|
207
|
+
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)`);
|
|
208
|
+
}
|
|
209
|
+
const have = new Set([...text.matchAll(/^## (\d+)\. /gm)].map((m) => m[1]!));
|
|
210
|
+
for (const citer of GUIDE_CITERS) {
|
|
211
|
+
const path = join(repo, citer);
|
|
212
|
+
if (!existsSync(join(repo, "crates"))) break; // the published package: the sources are not beside it
|
|
213
|
+
if (!existsSync(path)) {
|
|
214
|
+
problems.push(`${citer}: missing; GUIDE_CITERS in scripts/validate.ts names it`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
for (const m of readFileSync(path, "utf8").matchAll(new RegExp(`${g} §(\\d+)`, "g"))) {
|
|
218
|
+
if (!have.has(m[1]!)) problems.push(`${citer}: cites ${g} §${m[1]}, which guides/${g}.md does not have (its sections: ${[...have].join(", ")})`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
168
224
|
function scaffold(roots: Roots, rules: Rules, overlay: string, files: readonly PayloadFile[], byPath: Map<string, PayloadFile>, problems: string[]): void {
|
|
169
225
|
const prefix = `game-scaffold/${overlay}/`;
|
|
170
226
|
if (rules.scaffold.existingOverlays.includes(overlay)) {
|
|
@@ -255,7 +311,15 @@ function coherence(files: readonly PayloadFile[], problems: string[]): void {
|
|
|
255
311
|
const t = textOf(`game/skills/${s}/SKILL.md`);
|
|
256
312
|
if (t !== undefined) texts.set(s, t);
|
|
257
313
|
}
|
|
258
|
-
|
|
314
|
+
// The guides (guides/<name>.md, mounted beside og-game's SKILL.md) are
|
|
315
|
+
// held to the same section references; their owner names each.
|
|
316
|
+
const guides = new Map<string, string>();
|
|
317
|
+
for (const g of GUIDES) {
|
|
318
|
+
const t = textOf(`${GUIDE_MOUNT}${g}.md`);
|
|
319
|
+
if (t !== undefined) guides.set(g, t);
|
|
320
|
+
}
|
|
321
|
+
const numbered = (t: string) => new Set([...t.matchAll(/^## (\d+)\. /gm)].map((m) => m[1]!));
|
|
322
|
+
const sections = new Map([...texts, ...guides].map(([s, t]) => [s, numbered(t)]));
|
|
259
323
|
for (const [module, skill] of home) {
|
|
260
324
|
const t = texts.get(skill);
|
|
261
325
|
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 +329,76 @@ function coherence(files: readonly PayloadFile[], problems: string[]): void {
|
|
|
265
329
|
const owner = home.get(m);
|
|
266
330
|
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
331
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
332
|
+
}
|
|
333
|
+
for (const [name, t] of [...texts, ...guides]) {
|
|
334
|
+
const at = guides.has(name) ? `guides/${name}.md` : `skills/${name}/SKILL.md`;
|
|
335
|
+
problems.push(...sectionRefs(at, name, t, sections));
|
|
336
|
+
}
|
|
337
|
+
// What the guides say of a page, and who names them.
|
|
338
|
+
const names = (t: string) => new Set([...t.matchAll(/\bguides\/([a-z0-9-]+)\.md\b/g)].map((m) => m[1]!));
|
|
339
|
+
for (const [name, t] of [...texts, ...guides]) {
|
|
340
|
+
const at = guides.has(name) ? `guides/${name}.md` : `skills/${name}/SKILL.md`;
|
|
341
|
+
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(", ")})`);
|
|
342
|
+
}
|
|
343
|
+
for (const g of guides.keys()) {
|
|
344
|
+
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`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Every section reference of one text (`og-feel §4`, or `§4` for its own,
|
|
350
|
+
* `three-to-og §2` for a guide's) names a numbered `## N.` section that
|
|
351
|
+
* exists: `at` names the text in the messages, `own` the text itself.
|
|
352
|
+
*/
|
|
353
|
+
export function sectionRefs(at: string, own: string, text: string, sections: ReadonlyMap<string, ReadonlySet<string>>): string[] {
|
|
354
|
+
const problems: string[] = [];
|
|
355
|
+
const prose = text.replace(/^```[\s\S]*?^```/gm, "");
|
|
356
|
+
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.)`);
|
|
357
|
+
for (const m of prose.matchAll(/§(\d+)/g)) {
|
|
358
|
+
// The text this § belongs to: walk back over a list of §s ("og-feel
|
|
359
|
+
// §9–§11", "og-taste §4, §10") to the word before it.
|
|
360
|
+
const before = prose.slice(0, m.index).split(/\s+/);
|
|
361
|
+
let i = before.length - 1;
|
|
362
|
+
while (i >= 0 && (before[i] === "" || /^\(?§\d+(\.\d+)?[–,;)]*$/.test(before[i]!) || before[i] === "and" || before[i] === "or")) i--;
|
|
363
|
+
const word = (before[i] ?? "").replace(/^[(`*]+/, "");
|
|
364
|
+
let target: string | undefined = own;
|
|
365
|
+
if (/^og-[a-z]+(?:-[a-z]+)*$/.test(word) || (GUIDES as readonly string[]).includes(word)) target = word;
|
|
366
|
+
else if (/\.md\b|\//.test(word) || /^design/.test(word)) target = undefined;
|
|
367
|
+
if (target === undefined) continue;
|
|
368
|
+
const have = sections.get(target);
|
|
369
|
+
if (!have) problems.push(`${at}: refers to ${target} §${m[1]}, and there is no skill ${target}`);
|
|
370
|
+
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"})`);
|
|
371
|
+
}
|
|
372
|
+
return problems;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* What a sentence that says "skin" must also name for the word to be one of
|
|
377
|
+
* its other meanings: a glTF rig's skin (joints, morphs, a model), the
|
|
378
|
+
* character controllers' collision gap (the `skin` field, off a surface),
|
|
379
|
+
* a 2D skeleton's skin (a rig2d `skin` block swapping a slot's attachment,
|
|
380
|
+
* `og.Skeleton2D.skin`, an armature's setup pose), or a surface (a skin
|
|
381
|
+
* tone, peach skin, a crowd's or a tile set's looks).
|
|
382
|
+
*/
|
|
383
|
+
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;
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* A skill or wiki page never calls a UI style a "skin": og-ui's word is
|
|
387
|
+
* style (`type style`, `og.load.style`, `UiDesc.style`, `styles/<name>/`).
|
|
388
|
+
* Each sentence holding `skin` or `skins` must name one of the word's
|
|
389
|
+
* other meanings (SKIN_ELSEWHERE).
|
|
390
|
+
*/
|
|
391
|
+
function skinWord(roots: Roots, files: readonly PayloadFile[], problems: string[]): void {
|
|
392
|
+
for (const f of files) {
|
|
393
|
+
if (!f.path.startsWith("game/skills/") || !f.path.endsWith(".md") || !isText(f.bytes)) continue;
|
|
394
|
+
const name = relative(roots.pkg, f.source).split("\\").join("/");
|
|
395
|
+
for (const paragraph of decoder.decode(f.bytes).split(/\n\s*\n/)) {
|
|
396
|
+
// Sentences, and a table's cells, of the paragraph's joined lines.
|
|
397
|
+
for (const sentence of paragraph.replace(/\s*\n\s*/g, " ").split(/(?<=[.;!?])\s+|\s\|\s/)) {
|
|
398
|
+
if (/\bskins?\b/i.test(sentence) && !SKIN_ELSEWHERE.test(sentence)) {
|
|
399
|
+
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>/)`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
284
402
|
}
|
|
285
403
|
}
|
|
286
404
|
}
|
|
@@ -312,6 +430,7 @@ export const NOT_FIELDS: Record<string, string> = {
|
|
|
312
430
|
runs: "PlaytestDesc.runs (packages/engine/src/playtest.ts)",
|
|
313
431
|
then: "PlaytestDesc.then (packages/engine/src/playtest.ts)",
|
|
314
432
|
store: "PlaytestRun.store (packages/engine/src/playtest.ts)",
|
|
433
|
+
setup: "PlaytestDesc.setup (packages/engine/src/playtest.ts)",
|
|
315
434
|
};
|
|
316
435
|
|
|
317
436
|
export function readApiNames(engine: string): ApiNames | undefined {
|
|
@@ -353,8 +472,9 @@ function apiNames(roots: Roots, files: readonly PayloadFile[], problems: string[
|
|
|
353
472
|
if (!api) return;
|
|
354
473
|
for (const f of files) {
|
|
355
474
|
const m = /^game\/skills\/([^/]+)\/SKILL\.md$/.exec(f.path);
|
|
356
|
-
|
|
357
|
-
|
|
475
|
+
const guide = f.path.startsWith(GUIDE_MOUNT) && f.path.endsWith(".md") ? f.path.slice(GUIDE_MOUNT.length) : undefined;
|
|
476
|
+
if ((!m && !guide) || !isText(f.bytes)) continue;
|
|
477
|
+
const at = guide ? `guides/${guide}` : `skills/${m![1]}/SKILL.md`;
|
|
358
478
|
const text = decoder.decode(f.bytes);
|
|
359
479
|
const bad = new Set<string>();
|
|
360
480
|
for (const x of text.matchAll(/(?<![\w./@-])og\.([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?/g)) {
|
|
@@ -398,7 +518,7 @@ interface Style {
|
|
|
398
518
|
descriptor: string;
|
|
399
519
|
checks: string[];
|
|
400
520
|
premise: Record<string, string>;
|
|
401
|
-
look: { preset: string; presetReady: boolean; support: string; primitives: string[]; missing: string[] };
|
|
521
|
+
look: { preset: string; presetReady: boolean; support: string; profiles: string[]; primitives: string[]; missing: string[] };
|
|
402
522
|
ui: string;
|
|
403
523
|
palette: { size: string; rules: string };
|
|
404
524
|
assets: Record<string, string>;
|
|
@@ -407,6 +527,8 @@ interface Style {
|
|
|
407
527
|
|
|
408
528
|
/** What `look.support` may say: full (every primitive draws), partial or planned (`missing` names what is still to land), content (the look is authored art). */
|
|
409
529
|
export const SUPPORT = ["full", "partial", "planned", "content"] as const;
|
|
530
|
+
/** What `look.profiles` may name: the project profiles a style draws for (create's premise match never offers a style outside the game's). */
|
|
531
|
+
export const PROFILES = ["2d", "3d"] as const;
|
|
410
532
|
/** The placeholders the art-bible template may use. */
|
|
411
533
|
export const BIBLE_FIELDS = ["title", "premise", "camera", "platform", "name", "descriptor", "direction", "palette", "assets", "ui", "avoid", "game", "slug", "checks"] as const;
|
|
412
534
|
const PREMISE = ["genre", "mood", "era", "audience"];
|
|
@@ -418,7 +540,7 @@ const ASSET_SPECS = ["logicalResolution", "spriteScale", "faceBudget", "outlineP
|
|
|
418
540
|
* bible direction), its preset path `templates/looks/<slug>.ogt` (a
|
|
419
541
|
* `type look` file when presetReady), its support honest (`missing` names
|
|
420
542
|
* the primitives a style that is not `full` lacks, and is empty for one
|
|
421
|
-
* that is), every `Resource.field` it names a member of the generated API,
|
|
543
|
+
* that is), the profiles it draws for (`look.profiles`, 2d and/or 3d), every `Resource.field` it names a member of the generated API,
|
|
422
544
|
* og-taste naming every slug, and no text phrased as an imitation.
|
|
423
545
|
*/
|
|
424
546
|
function looks(roots: Roots, files: readonly PayloadFile[], problems: string[]): void {
|
|
@@ -472,6 +594,10 @@ function looks(roots: Roots, files: readonly PayloadFile[], problems: string[]):
|
|
|
472
594
|
}
|
|
473
595
|
if (look.preset !== `templates/looks/${s.slug}.ogt`) problems.push(`${id}: look.preset is ${look.preset}, expected templates/looks/${s.slug}.ogt`);
|
|
474
596
|
if (!(SUPPORT as readonly string[]).includes(look.support)) problems.push(`${id}: look.support ${look.support} is none of ${SUPPORT.join(", ")}`);
|
|
597
|
+
const profiles = Array.isArray(look.profiles) ? look.profiles : [];
|
|
598
|
+
if (profiles.length === 0 || new Set(profiles).size !== profiles.length || !profiles.every((p) => (PROFILES as readonly string[]).includes(p))) {
|
|
599
|
+
problems.push(`${id}: look.profiles must name the profiles the style draws for, one or both of ${PROFILES.join(", ")} (create matches a premise only among the game's)`);
|
|
600
|
+
}
|
|
475
601
|
const missing = look.missing ?? [];
|
|
476
602
|
if (look.support === "full" && missing.length > 0) problems.push(`${id}: support full, yet it names missing primitives`);
|
|
477
603
|
if (look.support !== "full" && !missing.some(text)) problems.push(`${id}: support ${look.support} must name what is missing (the primitive or the authored art)`);
|
|
@@ -571,7 +697,7 @@ export const GRAMMARS: readonly Grammar[] = [
|
|
|
571
697
|
source: "crates/og-anim/src/graph.rs",
|
|
572
698
|
region: [/pub fn from_document/, /\/\/\/ The slot of a parameter/],
|
|
573
699
|
reads: readKeys,
|
|
574
|
-
names: { param: 1, clip: 1, blend1d: 2, blend2d: 3, child: 1, state: 2, entry: 1, transition: 2, when: 1, weight: 1 },
|
|
700
|
+
names: { param: 1, clip: 1, blend1d: 2, blend2d: 3, child: 1, state: 2, entry: 1, transition: 2, when: 1, weight: 1, sync: 1 },
|
|
575
701
|
},
|
|
576
702
|
{ type: "navmesh", source: "crates/og-cook/src/cook/e6.rs", region: [/pub\(super\) fn cook_navmesh/, /og_nav::bake::bake\(/], reads: readKeys, home: "og-ai" },
|
|
577
703
|
{
|
|
@@ -723,14 +849,70 @@ export function grammarDrift(skills: ReadonlyMap<string, string>, read: (path: s
|
|
|
723
849
|
return problems;
|
|
724
850
|
}
|
|
725
851
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (!existsSync(join(repo, "crates"))) return; // the published package: the parsers are not beside it
|
|
852
|
+
/** Each skill's SKILL.md text, by skill name. */
|
|
853
|
+
function skillTexts(files: readonly PayloadFile[]): Map<string, string> {
|
|
729
854
|
const skills = new Map<string, string>();
|
|
730
855
|
for (const f of files) {
|
|
731
856
|
const m = /^game\/skills\/([^/]+)\/SKILL\.md$/.exec(f.path);
|
|
732
857
|
if (m && isText(f.bytes)) skills.set(m[1]!, decoder.decode(f.bytes));
|
|
733
858
|
}
|
|
859
|
+
return skills;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Every `.ogt` example a skill writes in a code block (```text or a bare
|
|
864
|
+
* fence, as a whole file or a fragment) holds to og-core's parser
|
|
865
|
+
* (crates/og-core/src/ogt.rs `parse`) line by line, so an author who copies
|
|
866
|
+
* it cooks: a `{` ends its key's line, a `}` stands alone on its own, the
|
|
867
|
+
* blocks close, and a file that opens with `type` has `version` and `---`
|
|
868
|
+
* next. A block with no brace and no `type` line is not checked (shell,
|
|
869
|
+
* prose). Strings and `#` comments are read as the parser reads them.
|
|
870
|
+
*/
|
|
871
|
+
export function ogtShape(skills: ReadonlyMap<string, string>): string[] {
|
|
872
|
+
const problems: string[] = [];
|
|
873
|
+
for (const [skill, md] of skills) {
|
|
874
|
+
for (const m of md.matchAll(/^( *)```([\w-]*)\n([\s\S]*?)^ *```/gm)) {
|
|
875
|
+
if (m[2] !== "text" && m[2] !== "") continue;
|
|
876
|
+
const indent = m[1]!.length;
|
|
877
|
+
const lines = m[3]!.split("\n").map((l) => l.slice(Math.min(indent, l.length - l.trimStart().length)));
|
|
878
|
+
const words = lines.map((l) => {
|
|
879
|
+
const out: string[] = [];
|
|
880
|
+
for (const t of l.matchAll(/"(?:[^"\\]|\\.)*"|\S+/g)) {
|
|
881
|
+
if (t[0].startsWith("#")) break;
|
|
882
|
+
out.push(t[0]);
|
|
883
|
+
}
|
|
884
|
+
return out;
|
|
885
|
+
});
|
|
886
|
+
const typed = words.find((w) => w.length > 0)?.[0] === "type";
|
|
887
|
+
if (!typed && !words.some((w) => w.includes("{") || w.includes("}"))) continue;
|
|
888
|
+
const first = md.slice(0, m.index).split("\n").length + 1;
|
|
889
|
+
const at = (i: number) => `skills/${skill}/SKILL.md:${first + i}`;
|
|
890
|
+
if (typed) {
|
|
891
|
+
const head = words.map((w, i) => [w, i] as const).filter(([w]) => w.length > 0).slice(0, 3);
|
|
892
|
+
if (head[1]?.[0][0] !== "version" || head[2]?.[0].join(" ") !== "---") problems.push(`${at(head[0]![1])}: an .ogt example opens with \`type\` but not \`version <n>\` and \`---\` after it; the parser refuses a file without its header`);
|
|
893
|
+
}
|
|
894
|
+
let depth = 0;
|
|
895
|
+
words.forEach((w, i) => {
|
|
896
|
+
const open = w.indexOf("{");
|
|
897
|
+
if (open >= 0 && open !== w.length - 1) problems.push(`${at(i)}: \`${lines[i]!.trim()}\` opens a block mid-line; the .ogt parser refuses it ('{' must end its line): put each line of the block on its own`);
|
|
898
|
+
if (w.includes("}") && w.length !== 1) problems.push(`${at(i)}: \`${lines[i]!.trim()}\` closes a block beside other words; the .ogt parser refuses it ('}' must be alone on its line)`);
|
|
899
|
+
if (open >= 0) depth++;
|
|
900
|
+
if (w.includes("}")) depth--;
|
|
901
|
+
if (depth < 0) {
|
|
902
|
+
problems.push(`${at(i)}: an unmatched '}'`);
|
|
903
|
+
depth = 0;
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
if (depth > 0) problems.push(`${at(lines.length - 1)}: an .ogt example leaves ${depth} block(s) open`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return problems;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function grammar(roots: Roots, files: readonly PayloadFile[], problems: string[]): void {
|
|
913
|
+
const repo = join(roots.pkg, "..", "..");
|
|
914
|
+
if (!existsSync(join(repo, "crates"))) return; // the published package: the parsers are not beside it
|
|
915
|
+
const skills = skillTexts(files);
|
|
734
916
|
problems.push(...grammarDrift(skills, (p) => (existsSync(join(repo, p)) ? readFileSync(join(repo, p), "utf8") : undefined)));
|
|
735
917
|
}
|
|
736
918
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: og-audio
|
|
3
|
-
description: Sound on Origin Engine — one-shot voices for game events, the procedural sound presets og cook synthesises (coin, jump, hit, click and more), UI sounds, the four buses and their effects (filters, reverb send, compressor or limiter, ducking), music with stems, bar-aligned starts, crossfades and stingers on og.Music's clock, spatial sources with occlusion
|
|
3
|
+
description: Sound on Origin Engine — one-shot voices for game events, the procedural sound presets og cook synthesises (coin, jump, hit, click and more), UI sounds, the four buses and their effects (filters, reverb send, compressor or limiter, ducking), music with stems, bar-aligned starts, crossfades and stingers on og.Music's clock, spatial sources with occlusion, distance dulling, Doppler, cones and HRTF for headphones, voice priority and the cap, and proving it all headless with listen and og run --audio. Use when a game is silent, when adding any sound or music, or when the mix is muddy, clipping or wrong.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Audio on Origin Engine is world state: an entity with `og.AudioSource`
|
|
@@ -34,9 +34,11 @@ given and taken, pickups, a door, a death, a win and a loss, every UI press.
|
|
|
34
34
|
|
|
35
35
|
- One voice entity per sound, spawned in `start` with `playing: false`, and
|
|
36
36
|
a `Voices` system that plays each requested one: systems set bits of a
|
|
37
|
-
game resource (`Sounds.sfx |= Sound.Jump`), Voices sets `
|
|
38
|
-
matching voice (
|
|
39
|
-
|
|
37
|
+
game resource (`Sounds.sfx |= Sound.Jump`), Voices sets `retrigger: true`
|
|
38
|
+
on the matching voice (it starts again from the top in that tick,
|
|
39
|
+
playing or not, and the engine clears it; archetypes written before
|
|
40
|
+
`retrigger` set `playing`, stopping a playing voice one tick and
|
|
41
|
+
starting it the next). Every archetype in `templates/archetypes` does
|
|
40
42
|
this; copy a `Voices`.
|
|
41
43
|
- Vary repeated sounds: `pitch` 0.92–1.08 from the world's `og.Rng` for
|
|
42
44
|
footsteps, hits and coins, so a stream of them does not machine-gun.
|
|
@@ -58,14 +60,14 @@ game's style needs it; presets are a floor, not a style.
|
|
|
58
60
|
## 3. UI sounds
|
|
59
61
|
|
|
60
62
|
`og.ui.configure(world, { sounds: { hover, press, open, close, success, error } })`
|
|
61
|
-
(or the
|
|
63
|
+
(or the style's `sound` lines) plays them on the `ui` bus: `click` for
|
|
62
64
|
press, `select` for success, `back` for close, `error` for a refused press
|
|
63
65
|
(and for `og.ui.shake({ sound: true })`, og-ui §6). Each starts in the
|
|
64
66
|
tick of its input. `hover` also answers keyboard and gamepad focus moving
|
|
65
67
|
to a control, and `press` a press by Enter, Space or the pad's A, so a
|
|
66
68
|
menu walked with a pad is heard step by step; keep `hover` short and quiet
|
|
67
69
|
(volume ≤ 0.3). `configure` changes only the fields it is given: setting
|
|
68
|
-
the sounds after a
|
|
70
|
+
the sounds after a style (or a look that brings one) keeps the style and its
|
|
69
71
|
fonts, and `sounds: null` silences them again.
|
|
70
72
|
|
|
71
73
|
## 4. Buses and the mix
|
|
@@ -173,10 +175,34 @@ at 1.0 is a mix bug, not loudness.
|
|
|
173
175
|
the listener is. It follows the listener's world transform, so put the
|
|
174
176
|
og.AudioListener where the ears are (the camera, or the player); a zone
|
|
175
177
|
on a child of a moving room moves with it.
|
|
178
|
+
- Motion is heard: a spatial source or listener that moves shifts the
|
|
179
|
+
pitch (Doppler, from tick velocities down `og.Parent`, so a car on a
|
|
180
|
+
rig or a listener on a moving camera just works). `dopplerFactor` 1 is
|
|
181
|
+
physical; 0.3–0.6 on a racing game's engines keeps a fly-by readable;
|
|
182
|
+
0 on a source that must keep its pitch (a voice line, a musical
|
|
183
|
+
instrument in the world) or on the `og.AudioListener` to turn it off
|
|
184
|
+
everywhere. A warp or respawn (a jump faster than sound in one tick)
|
|
185
|
+
shifts nothing. `og.Audio.speedOfSound` (343.3) is the scale.
|
|
186
|
+
- Directional sources: `coneInnerAngle`, `coneOuterAngle` (full angles,
|
|
187
|
+
degrees, about the entity's +Z, as three.js's `setDirectionalCone`)
|
|
188
|
+
and `coneOuterGain`: a loudspeaker or an NPC talking forward `90`,
|
|
189
|
+
`240`, `0.3`; a siren on a vehicle `120`, `300`, `0.5`. Rotate the
|
|
190
|
+
entity to aim it; the default 360/360 is omnidirectional.
|
|
191
|
+
- Headphones: `og.AudioListener.panningModel: "hrtf"` places spatial
|
|
192
|
+
sources by interaural delay and head shadow (a spherical-head model),
|
|
193
|
+
clearer left and right than the stereo panner, without front/back or
|
|
194
|
+
height cues; offer it as a setting ("Headphones"), keep `equalpower`
|
|
195
|
+
for speakers (the default). Non-spatial sources keep their `pan`.
|
|
176
196
|
- `og.Audio.maxVoices` (32) caps voices; past it the lowest `priority`,
|
|
177
197
|
then the quietest, then the oldest is culled (`SOUND_CULLED`). Give the
|
|
178
198
|
player's own sounds and the music a high priority (200+), ambient props a
|
|
179
|
-
low one.
|
|
199
|
+
low one. Ambient loops and music stems that should keep their place
|
|
200
|
+
when culled get `virtualize: true`: past the cap they go silent with
|
|
201
|
+
their playhead running and come back where they would be, not from the
|
|
202
|
+
top (`og.AudioPlayback.virtual` says which).
|
|
203
|
+
- `og.Audio.resampler` is `sinc` (clean highs at any pitch); `linear` is
|
|
204
|
+
the older, cheaper mix, kept for a game that compares its exact output
|
|
205
|
+
(a WAV hash in a test). Unpitched 48 kHz sounds play the same either way.
|
|
180
206
|
|
|
181
207
|
## 7. Prove it
|
|
182
208
|
|
|
@@ -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
|
|
@@ -53,8 +53,9 @@ a number and a sound (taste 98, feel 79).
|
|
|
53
53
|
`get:<Resource>[.<field>]` or `get:<entity>[.<Component>[.<field>]]`
|
|
54
54
|
(logs the value as game code sees it, og.Time whole and a light og.DayNight
|
|
55
55
|
drives as it is drawn, and writes `get-<tick>.json`, then
|
|
56
|
-
`get-<tick>-2.json`, ... for more on one tick;
|
|
57
|
-
resources by name only, so `get:`
|
|
56
|
+
`get-<tick>-2.json`, ... for more on one tick; `get:Game:game.json`
|
|
57
|
+
names the file; a world dump lists resources by name only, so `get:`
|
|
58
|
+
is how to read one),
|
|
58
59
|
`set:<entity>.<Component>.<field>=<value>` or
|
|
59
60
|
`set:<Resource>.<field>=<value>` (`set:og.Camera.trauma=0.5`,
|
|
60
61
|
`set:Game.coins=10`, `set:og.Stylise.ink=false`: a number, `true` or
|
|
@@ -98,7 +99,46 @@ each its argument form and one sentence in `GameDesc.commandHelp`
|
|
|
98
99
|
(`{ spawn_wave: { usage: "<count>", doc: "Spawns a wave now." } }`)
|
|
99
100
|
so `help` explains it (it lists `spawn_wave:<count>`: the name, then the
|
|
100
101
|
usage). An entry naming no action is logged when the game
|
|
101
|
-
starts.
|
|
102
|
+
starts. A step's arguments are its colon-delimited fields after the
|
|
103
|
+
action, never shell words: `0.5:records:12:34.5` hands the handler
|
|
104
|
+
`args[0]` `"12"` and `args[1]` `"34.5"`, and its usage is written the same
|
|
105
|
+
way, `usage: "<berries>:<seconds>"` (a space stays inside its field:
|
|
106
|
+
`records:12 34.5` is one argument). A game may not take a core action's
|
|
107
|
+
name; og-game §3 lists them, with `open_pause` beside the engine's `pause`.
|
|
108
|
+
|
|
109
|
+
**File names.** Every output of one script has its own file. An unnamed
|
|
110
|
+
output takes the action and the tick (`get-000030.json`), and a second on
|
|
111
|
+
the same tick is suffixed quietly (`get-000030-2.json`). A named one that
|
|
112
|
+
an earlier step of the script already wrote, at any time (a copy-pasted
|
|
113
|
+
`0.5:get:Game:met.json`), is written as `met-2.json`, then `met-3.json`,
|
|
114
|
+
never over the first, and logs a WARN naming both steps ("'met.json' was
|
|
115
|
+
already written by the 0.5s get step, so this one is 'met-2.json'"). The
|
|
116
|
+
WARN is in `run.warnings`, so a playtest that asserts a clean run
|
|
117
|
+
(`t.equal(run.warnings.length, 0, ...)`) fails on the clash, and
|
|
118
|
+
`run.json("met.json")` reads the first step's: give each named step its
|
|
119
|
+
own file.
|
|
120
|
+
|
|
121
|
+
**Values in the JSON.** A dump and a `get` answer write a vector as an
|
|
122
|
+
array in lane order (`Vector3` `[x, y, z]`, `Quaternion` `[x, y, z, w]`,
|
|
123
|
+
`Color` `[r, g, b]`), where the API's values are objects
|
|
124
|
+
(`{ x, y, z }`); `set:` takes the lanes comma-separated, without spaces
|
|
125
|
+
(`set:box.og.Transform.position=4,5,6`). Read a dump by index:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// 0.3:get:box.og.Transform:t.json writes {"kind":"get","target":"box.og.Transform","tick":18,"value":{"position":[1,2.5,-3],...}}
|
|
129
|
+
const [x, y, z] = run.json<{ value: { position: [number, number, number] } }>("t.json").value.position;
|
|
130
|
+
t.near(y, 2.5, 1e-4, "the box stands on the ledge");
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
An `f32` field is IEEE-754 single precision. Game code reads it as the
|
|
134
|
+
f32's exact value promoted to a JavaScript number, so `2.7` written to an
|
|
135
|
+
f32 reads back as `2.700000047683716`, and what game code sends carries
|
|
136
|
+
that number as JSON (the facts, an og.platform payload, so
|
|
137
|
+
`dump_platform`); `get` and the dumps print the f32's shortest form,
|
|
138
|
+
`2.7`. Compare numbers with a tolerance in a test (`t.near(a, b, 1e-4,
|
|
139
|
+
...)`), round only to display one (`toFixed(1)` on the label), and keep a
|
|
140
|
+
value that must round-trip exactly (a payload a test compares with `===`)
|
|
141
|
+
in an `f64` field.
|
|
102
142
|
|
|
103
143
|
## 2. Live sessions: the probe and hot reload
|
|
104
144
|
|
|
@@ -169,6 +209,15 @@ same store, for a relaunch (og-platform-sdk §4).
|
|
|
169
209
|
- Determinism is checkable: two runs with the same seed and script give the
|
|
170
210
|
same world hash in `dump_world`. When they do not, find the wall-clock
|
|
171
211
|
read or the unseeded randomness before anything else.
|
|
212
|
+
- A model built as code (og-model) is judged on numbers and one sheet,
|
|
213
|
+
not on a frame read by eye: `view:<entity>:front|back|side|top`,
|
|
214
|
+
`view:<entity>:orbit:<degrees>` and `turntable:<entity>:<n>` frame it
|
|
215
|
+
from fixed cameras, and `og compare <reference.png> <render.png>`
|
|
216
|
+
prints the silhouette IoU and the ΔE00 per mask region and writes the
|
|
217
|
+
reference beside the render as one sheet PNG (`--mask`, `--align`,
|
|
218
|
+
`--json`). Two runs give the same numbers and the same sheet bytes, so
|
|
219
|
+
quote them with the command. og-model §3 has the commands and how to
|
|
220
|
+
read them, og-model §4 the gate each pass must meet.
|
|
172
221
|
- Report what ran and what did not. A build that compiled is not a game
|
|
173
222
|
that plays.
|
|
174
223
|
|
|
@@ -196,7 +245,7 @@ every screen that matters.
|
|
|
196
245
|
| 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 |
|
|
197
246
|
| taste | Locale keys | `og cook`'s warnings | none | og-i18n §1 |
|
|
198
247
|
| 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 |
|
|
199
|
-
| 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 |
|
|
248
|
+
| 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, per moment (a moment with three owes no fourth: a bump that holds, shakes and sounds is not asked for a number) | og-feel §3, §4, §7, og-audio §1 |
|
|
200
249
|
| 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 |
|
|
201
250
|
| feel | Sound on input | inputs answered by a sound within 6 ticks | every action | og-audio §1 |
|
|
202
251
|
| 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 |
|
|
@@ -237,14 +286,24 @@ run it again after the fix: the report is the evidence.
|
|
|
237
286
|
`og build .` bundles `src/` to `dist/game.js`, cooks, and copies the engine
|
|
238
287
|
runtime and wasm next to it. `og build . --publish` produces the published
|
|
239
288
|
form: the probe keeps only its player-facing members (hello, tick, state,
|
|
240
|
-
pick, entity, input, capture) and the command channel is gone.
|
|
289
|
+
pick, entity, input, capture) and the command channel is gone. A `2d`
|
|
290
|
+
project publishes the 2D module, which has no 3D world
|
|
291
|
+
(`RenderCapabilities.world3d` false): the build refuses scenes holding
|
|
292
|
+
`Terrain`, `TerrainScatter`, `Water`, `WaterSurface` or `Fluid3D`, naming
|
|
293
|
+
each by scene file and line (og-game §2).
|
|
241
294
|
What ships is still checked by script, through what it keeps: real input
|
|
242
295
|
on the published page and the probe's `tick`, `state` and `capture`. A
|
|
243
296
|
playtest written in input steps (`key_down`, `type`, `touch_move_to` an
|
|
244
297
|
id, `move_to_text`, `touch_start`) and `screenshot` steps runs there as it
|
|
245
298
|
runs under `og test`, each frame compared with the same references; a
|
|
246
299
|
game command (warp, give) has no channel there and is refused, so keep
|
|
247
|
-
the steps a published check needs free of them. The
|
|
300
|
+
the steps a published check needs free of them. The page shows the
|
|
301
|
+
Player's Back over the canvas's top-left corner (12, 12, 40 by 40 CSS px
|
|
302
|
+
on a desktop page; og-ui §5), which anchored HUD groups keep out of, so
|
|
303
|
+
a playtest the page is held to gives its native run the same cover with
|
|
304
|
+
`setup: "cover:12,12,40,40"` (`PlaytestDesc.setup`: command steps run
|
|
305
|
+
once after `start`, before the first tick, never part of the page's
|
|
306
|
+
script) and its frames lay out as the page's. The engine runs this on
|
|
248
307
|
the published build of its own games (its `tests/web/published.ts`: the
|
|
249
308
|
boot, the title, a press, a typed name and a frame against the metal
|
|
250
309
|
reference, with a clean console).
|
|
@@ -218,16 +218,32 @@ 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
|
-
`Asset()`, `Texture()`, `Sound()`, `
|
|
229
|
-
`Entity()` (each handle type of the API
|
|
230
|
-
|
|
243
|
+
`Asset()`, `Texture()`, `Sound()`, `Font()`, `Style()`, `Look()`,
|
|
244
|
+
`Geometry()`, `Material()` or `Entity()` (each handle type of the API
|
|
245
|
+
has its factory, named as the type: a font is a `Font()`, never an
|
|
246
|
+
`Asset()` cast), in a component or a resource. A `u32` field refuses one (`4294967298 is not
|
|
231
247
|
a u32`).
|
|
232
248
|
- Per-entity calls (`world.get`, `world.set`) are for commands, setup and
|
|
233
249
|
the few named entities a system touches (a camera, a listener, the
|
|
@@ -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.
|
|
@@ -169,7 +169,10 @@ it is a cutscene or a reward.
|
|
|
169
169
|
## 7. Floating text
|
|
170
170
|
|
|
171
171
|
`og.render.floatingText(world, { text, position, ... })` pops a text that
|
|
172
|
-
rises, drifts and fades by itself (world state, deterministic).
|
|
172
|
+
rises, drifts and fades by itself (world state, deterministic). A label
|
|
173
|
+
that stays with a character (a name tag, a player's number) is not
|
|
174
|
+
floating text: it is screen text where the og.Camera resource's
|
|
175
|
+
`worldToScreen` puts the point, og-ui §3 (Text over a world point).
|
|
173
176
|
|
|
174
177
|
- Damage and score numbers above the hit point, not on the HUD: the eye is
|
|
175
178
|
already there. Pickups: what was gained (`+1`, `key`), short.
|
|
@@ -180,6 +183,11 @@ rises, drifts and fades by itself (world state, deterministic).
|
|
|
180
183
|
biggest number on screen should be the most important.
|
|
181
184
|
- Lifetime 0.6–1.0 s, rise 0.8–1.5 units, spread 0.2–0.4 so stacked hits do
|
|
182
185
|
not overlap. At most 6–8 on screen: sum rapid hits into one number.
|
|
186
|
+
- Not every impact wants a number. An impact is held by three of four
|
|
187
|
+
signals (hit stop §3, shake §4, floating text, a sound): `og polish`
|
|
188
|
+
counts them per damage moment and asks for the missing ones only of a
|
|
189
|
+
moment with fewer than three, so a party game's bump that holds, shakes
|
|
190
|
+
and sounds owes no floating text.
|
|
183
191
|
|
|
184
192
|
## 8. Camera: look-ahead, dead zones, zones, rails
|
|
185
193
|
|