@origingame/engine 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/origin-engine.d.ts +44 -5
- package/dist/web/origin-engine.js +279 -19
- package/dist/web/origin-engine_bg.wasm +0 -0
- package/dist/web/origin-engine_bg.wasm.d.ts +18 -5
- package/dist/web-publish/origin-engine-lanes.js +289 -48
- package/dist/web-publish/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish/origin-engine.d.ts +44 -5
- package/dist/web-publish/origin-engine.js +279 -38
- package/dist/web-publish/origin-engine_bg.wasm +0 -0
- package/dist/web-publish/origin-engine_bg.wasm.d.ts +18 -5
- package/dist/web-publish-2d/origin-engine-lanes.js +289 -48
- package/dist/web-publish-2d/origin-engine-lanes_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine.d.ts +44 -5
- package/dist/web-publish-2d/origin-engine.js +279 -38
- package/dist/web-publish-2d/origin-engine_bg.wasm +0 -0
- package/dist/web-publish-2d/origin-engine_bg.wasm.d.ts +18 -5
- package/knowledge/README.md +2 -1
- package/knowledge/guides/three-to-og.md +393 -0
- package/knowledge/knowledge.json +2 -1
- package/knowledge/looks/catalogue.json +123 -2
- package/knowledge/package.json +3 -2
- package/knowledge/scaffold/origin-engine.json +2 -2
- package/knowledge/scaffold/template/package.json +1 -1
- package/knowledge/scaffold/template/project.ogt +1 -1
- package/knowledge/scripts/validate.ts +209 -27
- package/knowledge/skills/og-audio/SKILL.md +33 -7
- package/knowledge/skills/og-build/SKILL.md +66 -7
- package/knowledge/skills/og-core/SKILL.md +24 -8
- package/knowledge/skills/og-feel/SKILL.md +11 -3
- package/knowledge/skills/og-game/SKILL.md +680 -51
- package/knowledge/skills/og-i18n/SKILL.md +7 -7
- package/knowledge/skills/og-model/SKILL.md +374 -0
- package/knowledge/skills/og-platform-sdk/SKILL.md +7 -1
- package/knowledge/skills/og-taste/SKILL.md +7 -0
- package/knowledge/skills/og-ui/SKILL.md +39 -8
- package/knowledge/wiki/ai.md +20 -20
- package/knowledge/wiki/anim.md +537 -63
- package/knowledge/wiki/atmosphere.md +13 -13
- package/knowledge/wiki/audio.md +160 -20
- package/knowledge/wiki/camera.md +13 -13
- package/knowledge/wiki/commands.md +11 -11
- package/knowledge/wiki/debug.md +13 -13
- package/knowledge/wiki/game.md +8 -8
- package/knowledge/wiki/i18n.md +7 -7
- package/knowledge/wiki/index.md +81 -3
- package/knowledge/wiki/input.md +47 -47
- package/knowledge/wiki/load.md +83 -57
- package/knowledge/wiki/math.md +102 -102
- package/knowledge/wiki/nav.md +34 -34
- package/knowledge/wiki/net.md +10 -10
- package/knowledge/wiki/path.md +14 -14
- package/knowledge/wiki/physics.md +111 -100
- package/knowledge/wiki/platform.md +44 -44
- package/knowledge/wiki/probe.md +86 -86
- package/knowledge/wiki/reflect.md +30 -18
- package/knowledge/wiki/render.md +1243 -176
- package/knowledge/wiki/rng.md +14 -14
- package/knowledge/wiki/shapes.md +727 -0
- package/knowledge/wiki/stream.md +15 -15
- package/knowledge/wiki/system.md +8 -8
- package/knowledge/wiki/systems.md +2 -2
- package/knowledge/wiki/terrain.md +24 -20
- package/knowledge/wiki/tilemap.md +13 -13
- package/knowledge/wiki/time.md +4 -4
- package/knowledge/wiki/timeline.md +14 -14
- package/knowledge/wiki/transform.md +8 -8
- package/knowledge/wiki/tween.md +17 -17
- package/knowledge/wiki/ui.md +245 -269
- package/knowledge/wiki/video.md +5 -5
- package/knowledge/wiki/world.md +36 -36
- package/package.json +4 -4
- package/schema/api.schema.json +2238 -81
- package/shell/page.ts +4 -0
- package/shell/platform.ts +5 -0
- package/shell/player.ts +8 -2
- package/shell/transport.ts +6 -0
- package/src/generated/abi.ts +40 -1
- package/src/geom.ts +260 -0
- package/src/index.ts +19 -1
- package/src/node/device.ts +5 -2
- package/src/node/playtest-run.ts +1 -0
- package/src/node/polish.ts +34 -15
- package/src/node/run.ts +23 -6
- package/src/platform-local.ts +2 -0
- package/src/playtest.ts +8 -0
- package/src/runtime.ts +88 -6
- package/src/ui-kit/settings.ts +9 -0
- package/src/web/gl-state.ts +907 -0
- package/src/web/shell.ts +1 -1
- package/src/web/worker.ts +8 -2
- package/types/bindings.json +1222 -453
- package/types/index.d.ts +1868 -315
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
|
|
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` */
|
|
@@ -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
|
-
|
|
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,
|
package/src/node/device.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
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;
|