@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
package/shell/platform.ts CHANGED
@@ -43,6 +43,7 @@ export interface PlayerLink {
43
43
  readonly framed: boolean;
44
44
  readonly hosted: boolean;
45
45
  readonly capabilities: readonly string[];
46
+ readonly time?: number | undefined;
46
47
  request(method: string, args?: Record<string, unknown>): Promise<PlayerResult>;
47
48
  event(name: string, attrs?: Record<string, string | number | boolean | null>): void;
48
49
  }
@@ -169,6 +170,7 @@ export function playerPlatform(player: PlayerLink, local: LocalPersistence): Pla
169
170
  return {
170
171
  host: "player",
171
172
  capabilities,
173
+ time: player.time,
172
174
  handle(calls, answer) {
173
175
  // In order: a store write through the save reads it first, so calls never interleave.
174
176
  for (const call of calls) {
@@ -201,6 +203,9 @@ export function deferredPlatform(ready: Promise<PlatformHost>, capabilities: rea
201
203
  return {
202
204
  host: "player",
203
205
  capabilities,
206
+ get time() {
207
+ return real?.time;
208
+ },
204
209
  handle(calls, answer) {
205
210
  if (real) real.handle(calls, answer);
206
211
  else held.push([calls, answer]);
package/shell/player.ts CHANGED
@@ -59,9 +59,9 @@ export type PlayerResult =
59
59
  | { readonly ok: true; readonly data: unknown; readonly time?: number }
60
60
  | { readonly ok: false; readonly error: { readonly code: string; readonly message: string; readonly retryable: boolean; readonly status?: number; readonly revision?: number }; readonly time?: number };
61
61
 
62
- /** A message's `time` field, when it is a finite number. */
62
+ /** A message's `time` field, when it is finite UTC milliseconds since the epoch. */
63
63
  export const timeOf = (m: Record<string, unknown>): { time?: number } =>
64
- typeof m["time"] === "number" && Number.isFinite(m["time"]) ? { time: m["time"] } : {};
64
+ typeof m["time"] === "number" && Number.isFinite(m["time"]) && m["time"] >= 0 ? { time: m["time"] } : {};
65
65
 
66
66
  /** How long a call waits for its `result` before it fails with `timeout`. */
67
67
  export const CALL_TIMEOUT_MS = 15_000;
@@ -93,6 +93,7 @@ export function connectPlayer(events: PlayerEvents) {
93
93
  let started = false;
94
94
  let paused = false;
95
95
  let capabilities: readonly string[] = [];
96
+ let time: number | undefined;
96
97
  let nextCall = 1;
97
98
  const calls = new Map<string, (r: PlayerResult) => void>();
98
99
 
@@ -116,6 +117,7 @@ export function connectPlayer(events: PlayerEvents) {
116
117
  heard = true;
117
118
  if (m["started"] === true) started = true;
118
119
  paused = m["paused"] === true;
120
+ time = timeOf(m).time;
119
121
  capabilities = Array.isArray(m["capabilities"]) ? (m["capabilities"] as unknown[]).filter((c): c is string => typeof c === "string") : [];
120
122
  events.hello?.({ started: m["started"] === true, paused, capabilities });
121
123
  break;
@@ -196,6 +198,10 @@ export function connectPlayer(events: PlayerEvents) {
196
198
  get capabilities(): readonly string[] {
197
199
  return capabilities;
198
200
  },
201
+ /** The most recent hello's clock, before any calls have been answered. */
202
+ get time(): number | undefined {
203
+ return time;
204
+ },
199
205
  loading(value: number | null, label?: string) {
200
206
  if (readySent) return;
201
207
  if (value !== null) {
@@ -139,6 +139,7 @@ export interface PageConnection {
139
139
  readonly started: boolean;
140
140
  readonly paused: boolean;
141
141
  readonly capabilities: readonly string[];
142
+ readonly time: number | undefined;
142
143
  readonly page: PageInfo | null;
143
144
  /** Resolves once the hello is answered (true) or failed (false). */
144
145
  readonly hello: Promise<boolean>;
@@ -249,6 +250,7 @@ export function connectPage(game: PageGame, env: TransportEnv, events: PlayerEve
249
250
  // ── hello ──
250
251
  let hosted = false;
251
252
  let capabilities: readonly string[] = [];
253
+ let time: number | undefined;
252
254
  let page: PageInfo | null = null;
253
255
  const str = (v: unknown) => (typeof v === "string" && v ? v : null);
254
256
  const hello = serial(async () => {
@@ -256,6 +258,7 @@ export function connectPage(game: PageGame, env: TransportEnv, events: PlayerEve
256
258
  const m = r.data;
257
259
  if (r.status !== 200 || !m || m["kind"] !== "hello") return false;
258
260
  hosted = true;
261
+ time = timeOf(m).time;
259
262
  capabilities = Array.isArray(m["capabilities"]) ? (m["capabilities"] as unknown[]).filter((c): c is string => typeof c === "string") : [];
260
263
  const p = (m["page"] ?? null) as Record<string, unknown> | null;
261
264
  page = p && typeof p === "object" ? { title: str(p["title"]), cover: str(p["cover"]), backdrop: str(p["backdrop"]), back: str(p["back"]), portal: str(p["portal"]) } : null;
@@ -446,6 +449,9 @@ export function connectPage(game: PageGame, env: TransportEnv, events: PlayerEve
446
449
  get capabilities() {
447
450
  return capabilities;
448
451
  },
452
+ get time() {
453
+ return time;
454
+ },
449
455
  get page() {
450
456
  return page;
451
457
  },
@@ -29,7 +29,7 @@ export interface HostAbi<A, V> {
29
29
  og_engine_take_log(engine: number): string;
30
30
  /** Write some fields of a resource as JSON: the runtime's path for a write a publish build refuses (logged once per system). A Result. */
31
31
  og_engine_write_resource(world: number, resource: string, value: string): string;
32
- /** A resource's packed layout, how the runtime reads and writes its fields as bytes: a Result of `{ epoch, size, fields: [{ name, kind, offset, size, readonly, capacity, variants }] }` (`kind`: f32, f64, u32, i32, bool, Vector2, Vector3, Quaternion, Color, Matrix4, handle, str, enum). Refused for og.Time, og.Rng and og.Input, whose handles are computed, and for what is not a registered resource. */
32
+ /** A resource's packed layout, how the runtime reads and writes its fields as bytes: a Result of `{ epoch, size, fields: [{ name, kind, offset, size, readonly, capacity, variants, element }] }` (`kind`: f32, f64, u32, i32, bool, Vector2, Vector3, Quaternion, Color, Matrix4, handle, str, enum, or list: `element` one of f32, f64, u32, i32, handle and `capacity` its elements, a count lane first, each lane the element's size). Refused for og.Time, og.Rng and og.Input, whose handles are computed, and for what is not a registered resource. */
33
33
  og_engine_resource_layout(world: number, resource: string): string;
34
34
  /** A resource's packed bytes, then the layout epoch as a little-endian u32 (it moves when any layout is declared again: a new engine, a reload). Empty when refused (undeclared in the running system, not a resource): the runtime then asks og_world_resource for the reason. */
35
35
  og_engine_read_resource(world: number, resource: string): Uint8Array;
@@ -227,6 +227,8 @@ export interface HostAbi<A, V> {
227
227
  og_render_set_shader_param(world: number, material: number, name: string, value: string): string;
228
228
  /** `og.render.applyLook` */
229
229
  og_render_apply_look(world: number, look: number): string;
230
+ /** `og.render.applyQuality` */
231
+ og_render_apply_quality(world: number, preset: string): string;
230
232
  /** `og.render.look` */
231
233
  og_render_look(world: number): string;
232
234
  /** `og.render.capabilities` */
@@ -253,6 +255,28 @@ export interface HostAbi<A, V> {
253
255
  og_render_circle_geometry(world: number, desc: string | undefined): string;
254
256
  /** `og.render.tubeGeometry` */
255
257
  og_render_tube_geometry(world: number, desc: string | undefined): string;
258
+ /** `og.render.shapeGeometry` */
259
+ og_render_shape_geometry(world: number, desc: string | undefined): string;
260
+ /** `og.render.extrudeGeometry` */
261
+ og_render_extrude_geometry(world: number, desc: string | undefined): string;
262
+ /** `og.render.latheGeometry` */
263
+ og_render_lathe_geometry(world: number, desc: string | undefined): string;
264
+ /** `og.render.polyhedronGeometry` */
265
+ og_render_polyhedron_geometry(world: number, desc: string | undefined): string;
266
+ /** `og.render.icosahedronGeometry` */
267
+ og_render_icosahedron_geometry(world: number, desc: string | undefined): string;
268
+ /** `og.render.dodecahedronGeometry` */
269
+ og_render_dodecahedron_geometry(world: number, desc: string | undefined): string;
270
+ /** `og.render.octahedronGeometry` */
271
+ og_render_octahedron_geometry(world: number, desc: string | undefined): string;
272
+ /** `og.render.tetrahedronGeometry` */
273
+ og_render_tetrahedron_geometry(world: number, desc: string | undefined): string;
274
+ /** `og.render.sweepGeometry` */
275
+ og_render_sweep_geometry(world: number, desc: string | undefined): string;
276
+ /** `og.render.csgGeometry` */
277
+ og_render_csg_geometry(world: number, desc: string | undefined): string;
278
+ /** `og.render.mergeGeometries` */
279
+ og_render_merge_geometries(world: number, desc: string | undefined): string;
256
280
  /** `og.render.lines` */
257
281
  og_render_lines(world: number, desc: string | undefined): string;
258
282
  /** `og.render.points` */
@@ -869,6 +893,7 @@ export const SYMBOLS: readonly string[] = [
869
893
  "og_render_set_material_shader",
870
894
  "og_render_set_shader_param",
871
895
  "og_render_apply_look",
896
+ "og_render_apply_quality",
872
897
  "og_render_look",
873
898
  "og_render_capabilities",
874
899
  "og_render_text",
@@ -882,6 +907,17 @@ export const SYMBOLS: readonly string[] = [
882
907
  "og_render_ring_geometry",
883
908
  "og_render_circle_geometry",
884
909
  "og_render_tube_geometry",
910
+ "og_render_shape_geometry",
911
+ "og_render_extrude_geometry",
912
+ "og_render_lathe_geometry",
913
+ "og_render_polyhedron_geometry",
914
+ "og_render_icosahedron_geometry",
915
+ "og_render_dodecahedron_geometry",
916
+ "og_render_octahedron_geometry",
917
+ "og_render_tetrahedron_geometry",
918
+ "og_render_sweep_geometry",
919
+ "og_render_csg_geometry",
920
+ "og_render_merge_geometries",
885
921
  "og_render_lines",
886
922
  "og_render_points",
887
923
  "og_render_raycast",
package/src/geom.ts ADDED
@@ -0,0 +1,260 @@
1
+ // Procedural modelling (IDL `shapes` and the og-geom members of `render`):
2
+ // og.Shape and og.Path2, three.js's Shape and Path with its methods and its
3
+ // curve formulas, and the render calls that take them. A shape never
4
+ // crosses the wire: it is flattened to points here (three.js's
5
+ // extractPoints(curveSegments)), so og-geom triangulates the same points
6
+ // three.js would.
7
+ //
8
+ // Determinism: the points may end up in world state through the geometry
9
+ // made from them, so the arcs read sincos (basic arithmetic, the camera
10
+ // systems' series), never Math.sin or Math.cos.
11
+
12
+ import type * as Api from "@origingame/engine";
13
+ import type { HostAbi } from "./generated/abi.ts";
14
+ import { boundHost } from "./runtime.ts";
15
+ import { sincos } from "./systems/cameras.ts";
16
+
17
+ type V2 = { x: number; y: number };
18
+ type W = { readonly id: number };
19
+
20
+ const TAU = 2 * 3.141592653589793;
21
+ const EPSILON = 2.220446049250313e-16;
22
+ const v2 = (x: number, y: number): V2 => ({ x, y });
23
+ const equals = (a: V2, b: V2) => a.x === b.x && a.y === b.y;
24
+
25
+ // three.js's Interpolations.
26
+ function catmullRom(t: number, p0: number, p1: number, p2: number, p3: number): number {
27
+ const v0 = (p2 - p0) * 0.5;
28
+ const v1 = (p3 - p1) * 0.5;
29
+ const t2 = t * t;
30
+ const t3 = t * t2;
31
+ return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (-3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
32
+ }
33
+ function quadratic(t: number, p0: number, p1: number, p2: number): number {
34
+ const k = 1 - t;
35
+ return k * k * p0 + 2 * (1 - t) * t * p1 + t * t * p2;
36
+ }
37
+ function cubic(t: number, p0: number, p1: number, p2: number, p3: number): number {
38
+ const k = 1 - t;
39
+ return k * k * k * p0 + 3 * k * k * t * p1 + 3 * (1 - t) * t * t * p2 + t * t * t * p3;
40
+ }
41
+
42
+ /** One of three.js's 2D curves, by its getPoint(t) and how finely CurvePath.getPoints divides it. */
43
+ interface Curve {
44
+ point(t: number): V2;
45
+ resolution(divisions: number): number;
46
+ }
47
+
48
+ const line = (a: V2, b: V2): Curve => ({
49
+ // three.js LineCurve.getPoint: the end exactly at 1.
50
+ point: (t) => (t === 1 ? v2(b.x, b.y) : v2((b.x - a.x) * t + a.x, (b.y - a.y) * t + a.y)),
51
+ resolution: () => 1,
52
+ });
53
+
54
+ const quadraticCurve = (a: V2, c: V2, b: V2): Curve => ({
55
+ point: (t) => v2(quadratic(t, a.x, c.x, b.x), quadratic(t, a.y, c.y, b.y)),
56
+ resolution: (d) => d,
57
+ });
58
+
59
+ const cubicCurve = (a: V2, c1: V2, c2: V2, b: V2): Curve => ({
60
+ point: (t) => v2(cubic(t, a.x, c1.x, c2.x, b.x), cubic(t, a.y, c1.y, c2.y, b.y)),
61
+ resolution: (d) => d,
62
+ });
63
+
64
+ function splineCurve(points: readonly V2[]): Curve {
65
+ return {
66
+ point: (t) => {
67
+ const p = (points.length - 1) * t;
68
+ const i = Math.floor(p);
69
+ const w = p - i;
70
+ const p0 = points[i === 0 ? i : i - 1]!;
71
+ const p1 = points[i]!;
72
+ const p2 = points[i > points.length - 2 ? points.length - 1 : i + 1]!;
73
+ const p3 = points[i > points.length - 3 ? points.length - 1 : i + 2]!;
74
+ return v2(catmullRom(w, p0.x, p1.x, p2.x, p3.x), catmullRom(w, p0.y, p1.y, p2.y, p3.y));
75
+ },
76
+ resolution: (d) => d * points.length,
77
+ };
78
+ }
79
+
80
+ function ellipseCurve(x: number, y: number, rx: number, ry: number, start: number, end: number, clockwise: boolean, rotation: number): Curve {
81
+ return {
82
+ point: (t) => {
83
+ let delta = end - start;
84
+ const same = Math.abs(delta) < EPSILON;
85
+ while (delta < 0) delta += TAU;
86
+ while (delta > TAU) delta -= TAU;
87
+ if (delta < EPSILON) delta = same ? 0 : TAU;
88
+ if (clockwise && !same) delta = delta === TAU ? -TAU : delta - TAU;
89
+ const [s, c] = sincos(start + t * delta);
90
+ let px = x + rx * c;
91
+ let py = y + ry * s;
92
+ if (rotation !== 0) {
93
+ const [rs, rc] = sincos(rotation);
94
+ const tx = px - x;
95
+ const ty = py - y;
96
+ px = tx * rc - ty * rs + x;
97
+ py = tx * rs + ty * rc + y;
98
+ }
99
+ return v2(px, py);
100
+ },
101
+ resolution: (d) => d * 2,
102
+ };
103
+ }
104
+
105
+ const point = (p: Api.Vector2): V2 => v2(p.x, p.y);
106
+
107
+ /** three.js's Path: curves from a current point. */
108
+ export class Path2 {
109
+ #curves: Curve[] = [];
110
+ #current: V2 = v2(0, 0);
111
+
112
+ constructor(points?: readonly Api.Vector2[] | null) {
113
+ if (points) this.setFromPoints(points);
114
+ }
115
+
116
+ setFromPoints(points: readonly Api.Vector2[]): this {
117
+ if (points.length === 0) return this;
118
+ this.moveTo(points[0]!.x, points[0]!.y);
119
+ for (let i = 1; i < points.length; i++) this.lineTo(points[i]!.x, points[i]!.y);
120
+ return this;
121
+ }
122
+
123
+ moveTo(x: number, y: number): this {
124
+ this.#current = v2(x, y);
125
+ return this;
126
+ }
127
+
128
+ lineTo(x: number, y: number): this {
129
+ this.#curves.push(line(this.#current, v2(x, y)));
130
+ this.#current = v2(x, y);
131
+ return this;
132
+ }
133
+
134
+ quadraticCurveTo(cpX: number, cpY: number, x: number, y: number): this {
135
+ this.#curves.push(quadraticCurve(this.#current, v2(cpX, cpY), v2(x, y)));
136
+ this.#current = v2(x, y);
137
+ return this;
138
+ }
139
+
140
+ bezierCurveTo(cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, x: number, y: number): this {
141
+ this.#curves.push(cubicCurve(this.#current, v2(cp1X, cp1Y), v2(cp2X, cp2Y), v2(x, y)));
142
+ this.#current = v2(x, y);
143
+ return this;
144
+ }
145
+
146
+ splineThru(points: readonly Api.Vector2[]): this {
147
+ if (points.length === 0) return this;
148
+ this.#curves.push(splineCurve([this.#current, ...points.map(point)]));
149
+ this.#current = point(points[points.length - 1]!);
150
+ return this;
151
+ }
152
+
153
+ arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, clockwise = false): this {
154
+ return this.absarc(x + this.#current.x, y + this.#current.y, radius, startAngle, endAngle, clockwise);
155
+ }
156
+
157
+ absarc(x: number, y: number, radius: number, startAngle: number, endAngle: number, clockwise = false): this {
158
+ return this.absellipse(x, y, radius, radius, startAngle, endAngle, clockwise);
159
+ }
160
+
161
+ ellipse(x: number, y: number, xRadius: number, yRadius: number, startAngle: number, endAngle: number, clockwise = false, rotation = 0): this {
162
+ return this.absellipse(x + this.#current.x, y + this.#current.y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation);
163
+ }
164
+
165
+ absellipse(x: number, y: number, xRadius: number, yRadius: number, startAngle: number, endAngle: number, clockwise = false, rotation = 0): this {
166
+ const curve = ellipseCurve(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation);
167
+ if (this.#curves.length > 0) {
168
+ // A previous curve: join it with a line when the arc starts elsewhere.
169
+ const first = curve.point(0);
170
+ if (!equals(first, this.#current)) this.lineTo(first.x, first.y);
171
+ }
172
+ this.#curves.push(curve);
173
+ this.#current = curve.point(1);
174
+ return this;
175
+ }
176
+
177
+ closePath(): this {
178
+ if (this.#curves.length === 0) return this;
179
+ const start = this.#curves[0]!.point(0);
180
+ const end = this.#curves[this.#curves.length - 1]!.point(1);
181
+ if (!equals(start, end)) this.#curves.push(line(end, start));
182
+ return this;
183
+ }
184
+
185
+ getPoints(divisions = 12): Api.Vector2[] {
186
+ const out: V2[] = [];
187
+ let last: V2 | undefined;
188
+ for (const curve of this.#curves) {
189
+ const resolution = curve.resolution(divisions);
190
+ for (let d = 0; d <= resolution; d++) {
191
+ const p = curve.point(d / resolution);
192
+ if (last && equals(last, p)) continue;
193
+ out.push(p);
194
+ last = p;
195
+ }
196
+ }
197
+ return out;
198
+ }
199
+ }
200
+
201
+ /** three.js's Shape: a path with holes. */
202
+ export class Shape extends Path2 {
203
+ readonly holes: Path2[] = [];
204
+
205
+ extractPoints(divisions = 12): Api.ShapePoints {
206
+ return { shape: this.getPoints(divisions), holes: this.holes.map((h) => h.getPoints(divisions)) };
207
+ }
208
+ }
209
+
210
+ // ── the render calls ────────────────────────────────────────────────────────
211
+
212
+ function abi(): HostAbi<Uint32Array, unknown> {
213
+ const h = boundHost();
214
+ if (!h) throw new Error("no engine host is loaded: start the game through @origingame/engine's web or node loader");
215
+ return h.abi;
216
+ }
217
+
218
+ const parse = <T>(text: string): T => JSON.parse(text) as T;
219
+ const json = (value: unknown): string | undefined => (value === undefined ? undefined : JSON.stringify(value));
220
+
221
+ type Desc = Record<string, unknown>;
222
+
223
+ /** A shape as it crosses: its points and its holes' points. */
224
+ function flat(s: Path2, divisions: number): Api.ShapePoints {
225
+ return s instanceof Shape ? s.extractPoints(divisions) : { shape: s.getPoints(divisions), holes: [] };
226
+ }
227
+
228
+ /** `shapes` (a Shape, a list of them, or the outline's points) as points. */
229
+ function shapes(desc: unknown): string | undefined {
230
+ if (desc === undefined || desc === null || typeof desc !== "object") return json(desc);
231
+ const d: Desc = { ...(desc as Desc) };
232
+ const divisions = typeof d.curveSegments === "number" ? d.curveSegments : 12;
233
+ const v = d.shapes;
234
+ if (v instanceof Path2) d.shapes = [flat(v, divisions)];
235
+ else if (Array.isArray(v) && v.length > 0 && v[0] instanceof Path2) d.shapes = (v as Path2[]).map((s) => flat(s, divisions));
236
+ return JSON.stringify(d);
237
+ }
238
+
239
+ /** A sweep's `section`: a Shape or Path2 flattened, its holes dropped. */
240
+ function section(desc: unknown): string | undefined {
241
+ if (desc === undefined || desc === null || typeof desc !== "object") return json(desc);
242
+ const d: Desc = { ...(desc as Desc) };
243
+ const divisions = typeof d.curveSegments === "number" ? d.curveSegments : 12;
244
+ if (d.section instanceof Path2) d.section = d.section.getPoints(divisions);
245
+ return JSON.stringify(d);
246
+ }
247
+
248
+ export const geomRender = Object.freeze({
249
+ shapeGeometry: (world: W, desc?: unknown) => parse(abi().og_render_shape_geometry(world.id, shapes(desc))),
250
+ extrudeGeometry: (world: W, desc?: unknown) => parse(abi().og_render_extrude_geometry(world.id, shapes(desc))),
251
+ latheGeometry: (world: W, desc?: unknown) => parse(abi().og_render_lathe_geometry(world.id, json(desc))),
252
+ polyhedronGeometry: (world: W, desc?: unknown) => parse(abi().og_render_polyhedron_geometry(world.id, json(desc))),
253
+ icosahedronGeometry: (world: W, desc?: unknown) => parse(abi().og_render_icosahedron_geometry(world.id, json(desc))),
254
+ dodecahedronGeometry: (world: W, desc?: unknown) => parse(abi().og_render_dodecahedron_geometry(world.id, json(desc))),
255
+ octahedronGeometry: (world: W, desc?: unknown) => parse(abi().og_render_octahedron_geometry(world.id, json(desc))),
256
+ tetrahedronGeometry: (world: W, desc?: unknown) => parse(abi().og_render_tetrahedron_geometry(world.id, json(desc))),
257
+ sweepGeometry: (world: W, desc?: unknown) => parse(abi().og_render_sweep_geometry(world.id, section(desc))),
258
+ csgGeometry: (world: W, desc?: unknown) => parse(abi().og_render_csg_geometry(world.id, json(desc))),
259
+ mergeGeometries: (world: W, desc?: unknown) => parse(abi().og_render_merge_geometries(world.id, json(desc))),
260
+ });
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import { cameras } from "./systems/cameras.ts";
28
28
  import { ai, i18n, nav, net, platform } from "./e6.ts";
29
29
  import { path, stream, terrain, timeline } from "./p2.ts";
30
30
  import { debug, depthRender } from "./depth.ts";
31
+ import { geomRender, Path2, Shape } from "./geom.ts";
31
32
  import { targetsLoad, targetsRender } from "./targets.ts";
32
33
  import { lightRender } from "./light.ts";
33
34
  import { uiKit } from "./ui-kit/index.ts";
@@ -54,10 +55,14 @@ export const Matrix4 = ((value = { elements: identity() }, options) => spec("Mat
54
55
  export const Color = ((value = { r: 0, g: 0, b: 0 }, options) => spec("Color", value, options)) as typeof Api.Color;
55
56
  // The capacity crosses with the spec; the engine refuses one outside 1 to 4096 at declaration.
56
57
  export const str = ((capacity, value = "", options) => Object.freeze({ ...spec("str", value, options), capacity })) as typeof Api.str;
58
+ // A list of another factory's kind: the engine refuses a kind that is not a number or a handle, and a capacity outside 1 to 1024, at declaration.
59
+ export const list = ((of, capacity, options) =>
60
+ Object.freeze({ kind: of.kind, default: Object.freeze([]), options: Object.freeze({ ...(options ?? {}) }), list: capacity })) as typeof Api.list;
57
61
  export const Entity = ((value = 0 as Api.Entity, options) => spec("Entity", value, options)) as typeof Api.Entity;
58
62
  export const Geometry = ((value = 0 as Api.Geometry, options) => spec("Geometry", value, options)) as typeof Api.Geometry;
59
63
  export const Material = ((value = 0 as Api.Material, options) => spec("Material", value, options)) as typeof Api.Material;
60
64
  export const ToneMapping = ((value = "aces" as Api.ToneMapping, options) => spec("ToneMapping", value, options)) as typeof Api.ToneMapping;
65
+ export const QualityPreset = ((value = "none" as Api.QualityPreset, options) => spec("QualityPreset", value, options)) as typeof Api.QualityPreset;
61
66
  export const Texture = ((value = 0 as Api.Texture, options) => spec("Texture", value, options)) as typeof Api.Texture;
62
67
  export const Lut = ((value = 0 as Api.Lut, options) => spec("Lut", value, options)) as typeof Api.Lut;
63
68
  export const EnvironmentMap = ((value = 0 as Api.EnvironmentMap, options) => spec("EnvironmentMap", value, options)) as typeof Api.EnvironmentMap;
@@ -189,7 +194,9 @@ export const og: typeof Api.og = Object.freeze({
189
194
  ...BUILTINS,
190
195
  game: (desc?: Api.GameDesc) => defineGame(desc as never),
191
196
  transform,
192
- render: Object.freeze({ ...render, ...depthRender, ...targetsRender, ...lightRender }),
197
+ Shape,
198
+ Path2,
199
+ render: Object.freeze({ ...render, ...depthRender, ...geomRender, ...targetsRender, ...lightRender }),
193
200
  load: Object.freeze({ ...load, ...targetsLoad }),
194
201
  commands,
195
202
  physics,
@@ -112,6 +112,7 @@ const command = (commands: string, dir: string, clock: number, room?: { url: str
112
112
  if (root) c.push("--root", root);
113
113
  if (desc!.seed !== undefined) c.push("--seed", String(desc!.seed));
114
114
  if (desc!.size) c.push("--size", desc!.size);
115
+ for (const step of (desc!.setup ?? "").split(";").map((s) => s.trim()).filter((s) => s !== "")) c.push("--setup", step);
115
116
  const store = storeOf(room?.place ?? 0);
116
117
  if (store) c.push("--platform-store", store);
117
118
  return c;
@@ -28,6 +28,8 @@ export interface PlatformHost {
28
28
  /** `player`, `local` or `none` (og.platform.info). */
29
29
  readonly host: string;
30
30
  readonly capabilities: readonly string[];
31
+ /** The Player hello's UTC milliseconds, sampled when this host attaches. */
32
+ readonly time?: number | undefined;
31
33
  /** Handle calls in order; answer every one but `telemetry`, now or later. */
32
34
  handle(calls: readonly PlatformCall[], answer: (request: number, result: PlatformAnswer) => void): void;
33
35
  }
package/src/playtest.ts CHANGED
@@ -127,6 +127,14 @@ export interface PlaytestDesc {
127
127
  readonly seed?: number;
128
128
  /** The render target, `WIDTHxHEIGHT` (default 1280x720). */
129
129
  readonly size?: string;
130
+ /**
131
+ * `action[:args];...` run once through the command channel after `start`, before the first
132
+ * tick (`og run --setup`), in every process of the playtest: the stand-ins for what a host does
133
+ * around the game, such as `cover:12,12,40,40`, the published page's Back over the play surface
134
+ * (`Viewport.covered`), so the frames lay out as the page's. Not part of `commands`: the
135
+ * published page plays a script without it and covers itself.
136
+ */
137
+ readonly setup?: string;
130
138
  /** Run the script this many times in fresh processes (default 1) and report every end hash in `run.hashes`: a determinism check. The files are the first run's. */
131
139
  readonly runs?: number;
132
140
  /**