@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.
Files changed (92) hide show
  1. package/dist/web/origin-engine.d.ts +44 -5
  2. package/dist/web/origin-engine.js +279 -19
  3. package/dist/web/origin-engine_bg.wasm +0 -0
  4. package/dist/web/origin-engine_bg.wasm.d.ts +18 -5
  5. package/dist/web-publish/origin-engine-lanes.js +289 -48
  6. package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
  7. package/dist/web-publish/origin-engine.d.ts +44 -5
  8. package/dist/web-publish/origin-engine.js +279 -38
  9. package/dist/web-publish/origin-engine_bg.wasm +0 -0
  10. package/dist/web-publish/origin-engine_bg.wasm.d.ts +18 -5
  11. package/dist/web-publish-2d/origin-engine-lanes.js +289 -48
  12. package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
  13. package/dist/web-publish-2d/origin-engine.d.ts +44 -5
  14. package/dist/web-publish-2d/origin-engine.js +279 -38
  15. package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
  16. package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +18 -5
  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/looks/catalogue.json +123 -2
  21. package/knowledge/package.json +3 -2
  22. package/knowledge/scaffold/origin-engine.json +2 -2
  23. package/knowledge/scaffold/template/package.json +1 -1
  24. package/knowledge/scaffold/template/project.ogt +1 -1
  25. package/knowledge/scripts/validate.ts +209 -27
  26. package/knowledge/skills/og-audio/SKILL.md +33 -7
  27. package/knowledge/skills/og-build/SKILL.md +66 -7
  28. package/knowledge/skills/og-core/SKILL.md +24 -8
  29. package/knowledge/skills/og-feel/SKILL.md +11 -3
  30. package/knowledge/skills/og-game/SKILL.md +680 -51
  31. package/knowledge/skills/og-i18n/SKILL.md +7 -7
  32. package/knowledge/skills/og-model/SKILL.md +374 -0
  33. package/knowledge/skills/og-platform-sdk/SKILL.md +7 -1
  34. package/knowledge/skills/og-taste/SKILL.md +7 -0
  35. package/knowledge/skills/og-ui/SKILL.md +39 -8
  36. package/knowledge/wiki/ai.md +20 -20
  37. package/knowledge/wiki/anim.md +537 -63
  38. package/knowledge/wiki/atmosphere.md +13 -13
  39. package/knowledge/wiki/audio.md +160 -20
  40. package/knowledge/wiki/camera.md +13 -13
  41. package/knowledge/wiki/commands.md +11 -11
  42. package/knowledge/wiki/debug.md +13 -13
  43. package/knowledge/wiki/game.md +8 -8
  44. package/knowledge/wiki/i18n.md +7 -7
  45. package/knowledge/wiki/index.md +81 -3
  46. package/knowledge/wiki/input.md +47 -47
  47. package/knowledge/wiki/load.md +83 -57
  48. package/knowledge/wiki/math.md +102 -102
  49. package/knowledge/wiki/nav.md +34 -34
  50. package/knowledge/wiki/net.md +10 -10
  51. package/knowledge/wiki/path.md +14 -14
  52. package/knowledge/wiki/physics.md +111 -100
  53. package/knowledge/wiki/platform.md +44 -44
  54. package/knowledge/wiki/probe.md +86 -86
  55. package/knowledge/wiki/reflect.md +30 -18
  56. package/knowledge/wiki/render.md +1243 -176
  57. package/knowledge/wiki/rng.md +14 -14
  58. package/knowledge/wiki/shapes.md +727 -0
  59. package/knowledge/wiki/stream.md +15 -15
  60. package/knowledge/wiki/system.md +8 -8
  61. package/knowledge/wiki/systems.md +2 -2
  62. package/knowledge/wiki/terrain.md +24 -20
  63. package/knowledge/wiki/tilemap.md +13 -13
  64. package/knowledge/wiki/time.md +4 -4
  65. package/knowledge/wiki/timeline.md +14 -14
  66. package/knowledge/wiki/transform.md +8 -8
  67. package/knowledge/wiki/tween.md +17 -17
  68. package/knowledge/wiki/ui.md +245 -269
  69. package/knowledge/wiki/video.md +5 -5
  70. package/knowledge/wiki/world.md +36 -36
  71. package/package.json +4 -4
  72. package/schema/api.schema.json +2238 -81
  73. package/shell/page.ts +4 -0
  74. package/shell/platform.ts +5 -0
  75. package/shell/player.ts +8 -2
  76. package/shell/transport.ts +6 -0
  77. package/src/generated/abi.ts +40 -1
  78. package/src/geom.ts +260 -0
  79. package/src/index.ts +19 -1
  80. package/src/node/device.ts +5 -2
  81. package/src/node/playtest-run.ts +1 -0
  82. package/src/node/polish.ts +34 -15
  83. package/src/node/run.ts +23 -6
  84. package/src/platform-local.ts +2 -0
  85. package/src/playtest.ts +8 -0
  86. package/src/runtime.ts +88 -6
  87. package/src/ui-kit/settings.ts +9 -0
  88. package/src/web/gl-state.ts +907 -0
  89. package/src/web/shell.ts +1 -1
  90. package/src/web/worker.ts +8 -2
  91. package/types/bindings.json +1222 -453
  92. package/types/index.d.ts +1868 -315
package/shell/page.ts CHANGED
@@ -600,6 +600,10 @@ export async function play(): Promise<PlayHandle> {
600
600
  pausedByPlayer = m.paused;
601
601
  if (m.started && phase === "gate") start();
602
602
  };
603
+ // The usual case with a real Player: it answered the shell's hello while the engine loaded,
604
+ // and the handler above was not installed yet (the first one only released og.platform).
605
+ // What that hello says applies now: its rooms, and a Play link's room through them.
606
+ if (!page && player.hosted) playerEvents.hello({ started: player.started, paused: player.paused, capabilities: player.capabilities });
603
607
  playerEvents.start = () => {
604
608
  byPlayer = true;
605
609
  if (phase === "gate") start();
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` */
@@ -373,6 +397,8 @@ export interface HostAbi<A, V> {
373
397
  og_anim_humanoid(world: number, desc: string | undefined): string;
374
398
  /** `og.anim.retarget` */
375
399
  og_anim_retarget(world: number, desc: string | undefined): string;
400
+ /** `og.anim.mirror` */
401
+ og_anim_mirror(world: number, desc: string | undefined): string;
376
402
  /** `og.anim.rig2dInfo` */
377
403
  og_anim_rig2d_info(world: number, rig: number): string;
378
404
  /** `og.anim.skeleton2dBoxes` */
@@ -869,6 +895,7 @@ export const SYMBOLS: readonly string[] = [
869
895
  "og_render_set_material_shader",
870
896
  "og_render_set_shader_param",
871
897
  "og_render_apply_look",
898
+ "og_render_apply_quality",
872
899
  "og_render_look",
873
900
  "og_render_capabilities",
874
901
  "og_render_text",
@@ -882,6 +909,17 @@ export const SYMBOLS: readonly string[] = [
882
909
  "og_render_ring_geometry",
883
910
  "og_render_circle_geometry",
884
911
  "og_render_tube_geometry",
912
+ "og_render_shape_geometry",
913
+ "og_render_extrude_geometry",
914
+ "og_render_lathe_geometry",
915
+ "og_render_polyhedron_geometry",
916
+ "og_render_icosahedron_geometry",
917
+ "og_render_dodecahedron_geometry",
918
+ "og_render_octahedron_geometry",
919
+ "og_render_tetrahedron_geometry",
920
+ "og_render_sweep_geometry",
921
+ "og_render_csg_geometry",
922
+ "og_render_merge_geometries",
885
923
  "og_render_lines",
886
924
  "og_render_points",
887
925
  "og_render_raycast",
@@ -942,6 +980,7 @@ export const SYMBOLS: readonly string[] = [
942
980
  "og_anim_graph",
943
981
  "og_anim_humanoid",
944
982
  "og_anim_retarget",
983
+ "og_anim_mirror",
945
984
  "og_anim_rig2d_info",
946
985
  "og_anim_skeleton2d_boxes",
947
986
  "og_tween_to",
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;
@@ -67,6 +72,7 @@ export const LineMode = ((value = "strip" as Api.LineMode, options) => spec("Lin
67
72
  export const DofQuality = ((value = "low" as Api.DofQuality, options) => spec("DofQuality", value, options)) as typeof Api.DofQuality;
68
73
  export const ReflectionProbeMode = ((value = "baked" as Api.ReflectionProbeMode, options) => spec("ReflectionProbeMode", value, options)) as typeof Api.ReflectionProbeMode;
69
74
  export const SpriteBlend = ((value = "alpha" as Api.SpriteBlend, options) => spec("SpriteBlend", value, options)) as typeof Api.SpriteBlend;
75
+ export const SpriteFacing = ((value = "none" as Api.SpriteFacing, options) => spec("SpriteFacing", value, options)) as typeof Api.SpriteFacing;
70
76
  export const StripBlend = ((value = "additive" as Api.StripBlend, options) => spec("StripBlend", value, options)) as typeof Api.StripBlend;
71
77
  export const Palette = ((value = 0 as Api.Palette, options) => spec("Palette", value, options)) as typeof Api.Palette;
72
78
  export const DitherPattern = ((value = "none" as Api.DitherPattern, options) => spec("DitherPattern", value, options)) as typeof Api.DitherPattern;
@@ -74,6 +80,11 @@ export const StyliseSpace = ((value = "screen" as Api.StyliseSpace, options) =>
74
80
  export const TextureArray = ((value = 0 as Api.TextureArray, options) => spec("TextureArray", value, options)) as typeof Api.TextureArray;
75
81
  export const Asset = ((value = 0 as Api.Asset, options) => spec("Asset", value, options)) as typeof Api.Asset;
76
82
  export const Sound = ((value = 0 as Api.Sound, options) => spec("Sound", value, options)) as typeof Api.Sound;
83
+ export const Font = ((value = 0 as Api.Font, options) => spec("Font", value, options)) as typeof Api.Font;
84
+ export const Style = ((value = 0 as Api.Style, options) => spec("Style", value, options)) as typeof Api.Style;
85
+ export const Look = ((value = 0 as Api.Look, options) => spec("Look", value, options)) as typeof Api.Look;
86
+ export const Shader = ((value = 0 as Api.Shader, options) => spec("Shader", value, options)) as typeof Api.Shader;
87
+ export const Rig = ((value = 0 as Api.Rig, options) => spec("Rig", value, options)) as typeof Api.Rig;
77
88
  export const SkeletonAsset = ((value = 0 as Api.SkeletonAsset, options) => spec("SkeletonAsset", value, options)) as typeof Api.SkeletonAsset;
78
89
  export const ClipAsset = ((value = 0 as Api.ClipAsset, options) => spec("ClipAsset", value, options)) as typeof Api.ClipAsset;
79
90
  export const GraphAsset = ((value = 0 as Api.GraphAsset, options) => spec("GraphAsset", value, options)) as typeof Api.GraphAsset;
@@ -94,6 +105,10 @@ export const BlendshapeSource = ((value = "component" as Api.BlendshapeSource, o
94
105
  export const IkSolver = ((value = "twoBone" as Api.IkSolver, options) => spec("IkSolver", value, options)) as typeof Api.IkSolver;
95
106
  export const IkSpace = ((value = "world" as Api.IkSpace, options) => spec("IkSpace", value, options)) as typeof Api.IkSpace;
96
107
  export const AudioBus = ((value = "master" as Api.AudioBus, options) => spec("AudioBus", value, options)) as typeof Api.AudioBus;
108
+ export const PanningModel = ((value = "equalpower" as Api.PanningModel, options) => spec("PanningModel", value, options)) as typeof Api.PanningModel;
109
+ export const AudioResampler = ((value = "linear" as Api.AudioResampler, options) => spec("AudioResampler", value, options)) as typeof Api.AudioResampler;
110
+ export const SpringSpace = ((value = "world" as Api.SpringSpace, options) => spec("SpringSpace", value, options)) as typeof Api.SpringSpace;
111
+ export const SpringColliderShape = ((value = "sphere" as Api.SpringColliderShape, options) => spec("SpringColliderShape", value, options)) as typeof Api.SpringColliderShape;
97
112
  export const Navigation = ((value = 0 as Api.Navigation, options) => spec("Navigation", value, options)) as typeof Api.Navigation;
98
113
  export const Behaviour = ((value = 0 as Api.Behaviour, options) => spec("Behaviour", value, options)) as typeof Api.Behaviour;
99
114
 
@@ -142,6 +157,7 @@ export const SOUND_STOPPED: typeof Api.SOUND_STOPPED = 1;
142
157
  export const SOUND_REMOVED: typeof Api.SOUND_REMOVED = 2;
143
158
  export const SOUND_REPLACED: typeof Api.SOUND_REPLACED = 3;
144
159
  export const SOUND_CULLED: typeof Api.SOUND_CULLED = 4;
160
+ export const SOUND_RETRIGGERED: typeof Api.SOUND_RETRIGGERED = 5;
145
161
  export const NAV_IDLE: typeof Api.NAV_IDLE = 0;
146
162
  export const NAV_MOVING: typeof Api.NAV_MOVING = 1;
147
163
  export const NAV_ARRIVED: typeof Api.NAV_ARRIVED = 2;
@@ -189,7 +205,9 @@ export const og: typeof Api.og = Object.freeze({
189
205
  ...BUILTINS,
190
206
  game: (desc?: Api.GameDesc) => defineGame(desc as never),
191
207
  transform,
192
- render: Object.freeze({ ...render, ...depthRender, ...targetsRender, ...lightRender }),
208
+ Shape,
209
+ Path2,
210
+ render: Object.freeze({ ...render, ...depthRender, ...geomRender, ...targetsRender, ...lightRender }),
193
211
  load: Object.freeze({ ...load, ...targetsLoad }),
194
212
  commands,
195
213
  physics,
@@ -514,7 +514,7 @@ async function connectAndroid(wanted: string | null, port: number, scheme: strin
514
514
 
515
515
  const ELEMENT = "element-6066-11e4-a52e-4f735466cecf";
516
516
  const IPHONE_FIX =
517
- "on the iPhone turn on Settings > Apps > Safari > Advanced > Web Inspector and Remote Automation; on the Mac run `safaridriver --enable` once and pair the phone in Xcode (Window > Devices and Simulators)";
517
+ "on the iPhone turn on Settings > Apps > Safari > Advanced > Web Inspector and Remote Automation; on the Mac turn on Safari > Develop > Allow Remote Automation, run `safaridriver --enable` once and pair the phone in Xcode (Window > Devices and Simulators)";
518
518
 
519
519
  /** Safari on an iPhone over WebDriver (safaridriver). */
520
520
  async function connectIphone(udid: string | null): Promise<Driver> {
@@ -568,7 +568,10 @@ async function connectIphone(udid: string | null): Promise<Driver> {
568
568
  );
569
569
  session = s.sessionId;
570
570
  } catch (e) {
571
- return refuse(`safaridriver could not open Safari on the iPhone (${(e as Error).message.split("\n")[0]}): ${IPHONE_FIX}`);
571
+ // The whole answer: safaridriver names the device it found and why it
572
+ // could not use it on the lines after the first.
573
+ const why = (e as Error).message.split("\n").map((l) => l.trim()).filter(Boolean).join("; ");
574
+ return refuse(`safaridriver could not open Safari on the iPhone (${why}): ${IPHONE_FIX}`);
572
575
  }
573
576
  await call("POST", `/session/${session}/timeouts`, { script: 120_000, pageLoad: 120_000 });
574
577
  return {
@@ -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;