@origingame/engine 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/origin-engine.d.ts +38 -2
- package/dist/web/origin-engine.js +250 -10
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +14 -2
- package/dist/web-publish/origin-engine-lanes.js +255 -34
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +39 -3
- package/dist/web-publish/origin-engine.js +253 -32
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +15 -3
- package/dist/web-publish-2d/origin-engine-lanes.js +253 -32
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +38 -2
- package/dist/web-publish-2d/origin-engine.js +250 -29
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +14 -2
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scripts/validate.ts +140 -21
- package/knowledge/skills/og-audio/SKILL.md +2 -2
- package/knowledge/skills/og-build/SKILL.md +22 -3
- package/knowledge/skills/og-core/SKILL.md +20 -5
- package/knowledge/skills/og-feel/SKILL.md +2 -2
- package/knowledge/skills/og-game/SKILL.md +481 -35
- package/knowledge/skills/og-i18n/SKILL.md +7 -7
- package/knowledge/skills/og-model/SKILL.md +374 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +1 -1
- package/knowledge/skills/og-ui/SKILL.md +15 -8
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +63 -55
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +18 -18
- package/knowledge/wiki/camera.md +13 -13
- package/knowledge/wiki/commands.md +11 -11
- package/knowledge/wiki/debug.md +13 -13
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +57 -3
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +57 -57
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +10 -10
- package/knowledge/wiki/path.md +14 -14
- package/knowledge/wiki/physics.md +95 -95
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +16 -4
- package/knowledge/wiki/render.md +1168 -170
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +727 -0
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +24 -20
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +14 -14
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +245 -269
- package/knowledge/wiki/video.md +5 -5
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -4
- package/schema/api.schema.json +1834 -305
- package/shell/platform.ts +5 -0
- package/shell/player.ts +8 -2
- package/shell/transport.ts +6 -0
- package/src/generated/abi.ts +37 -1
- package/src/geom.ts +260 -0
- package/src/index.ts +8 -1
- package/src/node/playtest-run.ts +1 -0
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +8 -0
- package/src/runtime.ts +87 -6
- package/src/ui-kit/settings.ts +9 -0
- package/src/web/shell.ts +1 -1
- package/src/web/worker.ts +2 -2
- package/types/bindings.json +697 -402
- package/types/index.d.ts +1045 -127
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
|
|
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) {
|
package/shell/transport.ts
CHANGED
|
@@ -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
|
},
|
package/src/generated/abi.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/node/playtest-run.ts
CHANGED
|
@@ -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;
|
package/src/platform-local.ts
CHANGED
|
@@ -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
|
/**
|