@wasm-gaming/engine-specs 0.2.7 → 0.3.1

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 (48) hide show
  1. package/README.md +63 -54
  2. package/demo/app.html +227 -0
  3. package/demo/canvas-renderer.js +153 -0
  4. package/demo/components/asset-picker.html +137 -0
  5. package/demo/components/call-log.html +33 -0
  6. package/demo/components/engine-header.html +78 -0
  7. package/demo/components/lifecycle-bar.html +69 -0
  8. package/demo/components/property-table.html +119 -0
  9. package/demo/demo.css +26 -130
  10. package/demo/demo.sdk.js +405 -203
  11. package/demo/index.html +7 -56
  12. package/demo/tsconfig.json +17 -0
  13. package/dist/engine-specs.d.ts +106 -0
  14. package/dist/engine-specs.d.ts.map +1 -0
  15. package/dist/engine-specs.js +3 -0
  16. package/dist/engine-specs.js.map +1 -0
  17. package/package.json +7 -12
  18. package/src/engine-specs.ts +117 -0
  19. package/demo/README.md +0 -155
  20. package/demo/checksums.js +0 -101
  21. package/demo/components/drop-area.html +0 -171
  22. package/demo/components/esc-menu.html +0 -540
  23. package/demo/components/launcher.html +0 -220
  24. package/demo/components/sdk-info.html +0 -54
  25. package/demo/demo.js +0 -622
  26. package/demo/sdk.js +0 -19
  27. package/demo/storage.js +0 -88
  28. package/dist/engine.d.ts +0 -427
  29. package/dist/engine.d.ts.map +0 -1
  30. package/dist/engine.js +0 -83
  31. package/dist/engine.js.map +0 -1
  32. package/dist/index.d.ts +0 -13
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js +0 -12
  35. package/dist/index.js.map +0 -1
  36. package/dist/manifest.d.ts +0 -69
  37. package/dist/manifest.d.ts.map +0 -1
  38. package/dist/manifest.js +0 -8
  39. package/dist/manifest.js.map +0 -1
  40. package/dist/validate.d.ts +0 -10
  41. package/dist/validate.d.ts.map +0 -1
  42. package/dist/validate.js +0 -79
  43. package/dist/validate.js.map +0 -1
  44. package/schema/engine-manifest.schema.json +0 -74
  45. package/src/engine.ts +0 -516
  46. package/src/index.ts +0 -55
  47. package/src/manifest.ts +0 -82
  48. package/src/validate.ts +0 -92
package/demo/demo.sdk.js CHANGED
@@ -1,211 +1,413 @@
1
- export function createDummySdk() {
2
- const manifest = {
3
- id: "dummy-canvas-sdk",
4
- version: "0.1.0-demo",
5
- name: "Dummy Canvas Engine",
6
- description: "A placeholder SDK that renders animated content on canvas.",
7
- artifacts: {
8
- wasm: "./dummy.wasm",
9
- js: "./dummy.js",
10
- },
11
- assets: [
12
- {
13
- key: "rom",
14
- mountPath: "/rom.bin",
15
- required: true,
16
- accept: [".rom", ".bin", ".zip", ".dat"],
17
- description: "Any game ROM file works in this demo.",
18
- },
19
- {
20
- key: "bios",
21
- mountPath: "/bios.bin",
22
- required: false,
23
- accept: [".bin", ".rom", ".sys"],
24
- description: "System BIOS firmware file.",
25
- },
26
- ],
27
- input: "keyboard-default",
28
- video: {
29
- baseWidth: 320,
30
- baseHeight: 256,
31
- aspect: "5:4",
32
- },
33
- capabilities: {
34
- saveStates: false,
35
- sram: false,
36
- },
37
- };
38
-
39
- return {
40
- manifest,
41
- async load(config) {
42
- const ownsCanvas = !config.canvasEl;
43
- const canvasEl = config.canvasEl ?? document.createElement("canvas");
44
- if (ownsCanvas) {
45
- config.attachTo.appendChild(canvasEl);
46
- }
47
-
48
- const ctx = canvasEl.getContext("2d");
49
- if (!ctx) {
50
- const error = new Error("Could not create 2D canvas context.");
51
- config.onEvent?.({ type: "error", error });
52
- throw error;
53
- }
54
-
55
- // Copy loaded ROM and BIOS assets to MEMFS (In-Memory Virtual File System)
56
- const MEMFS = new Map();
57
- if (config.assets) {
58
- for (const assetSpec of manifest.assets) {
59
- const data = config.assets[assetSpec.key];
60
- if (data) {
61
- const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
62
- MEMFS.set(assetSpec.mountPath, bytes);
63
- console.log(`[MEMFS] Copied asset '${assetSpec.key}' (${bytes.byteLength} bytes) to ${assetSpec.mountPath}`);
64
- }
65
- }
66
- }
67
-
68
- let raf = 0;
69
- let running = false;
70
- let t0 = performance.now();
71
- let lastFrame = t0;
72
-
73
- const fontSize = 30;
74
- let drops = [];
75
-
76
- const fitCanvasToWindow = () => {
77
- const dpr = window.devicePixelRatio || 1;
78
- const rect = canvasEl.getBoundingClientRect();
79
- const width = Math.max(1, Math.floor(rect.width * dpr));
80
- const height = Math.max(1, Math.floor(rect.height * dpr));
81
- canvasEl.width = width;
82
- canvasEl.height = height;
83
- };
84
-
85
- const render = (time) => {
86
- if (!running) {
87
- return;
88
- }
89
-
90
- const width = canvasEl.width;
91
- const height = canvasEl.height;
92
- const dt = Math.min((time - lastFrame) / 1000, 0.1);
93
- const fps = Math.max(1, 1000 / Math.max(1, time - lastFrame));
94
- lastFrame = time;
95
-
96
- const expectedColumns = Math.floor(width / fontSize);
97
- const maxRows = Math.floor(height / fontSize) + 30;
98
-
99
- if (drops.length !== expectedColumns) {
100
- drops = [];
101
- for (let x = 0; x < expectedColumns; x++) {
102
- drops[x] = {
103
- y: Math.random() * maxRows,
104
- speed: Math.random() * 1.5 + 0.5,
105
- opacity: Math.random() * 0.7 + 0.3,
106
- length: Math.floor(Math.random() * 20 + 5),
107
- chars: Array.from({ length: maxRows }, () => String.fromCharCode(0x30A0 + Math.random() * 96))
108
- };
109
- }
110
- }
111
-
112
- // Dark background inside the canvas to contrast with the light UI components
113
- ctx.fillStyle = "#060910ff";
114
- ctx.fillRect(0, 0, width, height);
115
-
116
- ctx.font = "bold " + fontSize + "px 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans CJK JP', sans-serif";
117
- ctx.textAlign = "center";
118
-
119
- for (let x = 0; x < drops.length; x++) {
120
- let drop = drops[x];
121
-
122
- if (Math.random() > 0.4) {
123
- let randIdx = Math.floor(Math.random() * drop.chars.length);
124
- drop.chars[randIdx] = String.fromCharCode(0x30A0 + Math.random() * 96);
125
- }
126
-
127
- drop.y += drop.speed * (dt * 12);
128
-
129
- let startY = Math.floor(drop.y);
130
-
131
- for (let i = 0; i < drop.length; i++) {
132
- let charY = startY - i;
133
- if (charY < 0 || charY >= maxRows) continue;
134
-
135
- let text = drop.chars[charY];
136
- let alpha = drop.opacity * (1 - (i / drop.length));
137
-
138
- if (i === 0) {
139
- ctx.fillStyle = `rgba(220, 255, 255, ${drop.opacity})`;
140
- } else {
141
- const hue = ((time / 15) + (x * 10)) % 360;
142
- ctx.fillStyle = `hsla(${hue}, 80%, 65%, ${alpha})`;
143
- }
144
-
145
- ctx.fillText(text, x * fontSize + fontSize / 2, charY * fontSize);
146
- }
147
-
148
- if ((drop.y - drop.length) * fontSize > height) {
149
- drop.y = 0;
150
- drop.speed = Math.random() * 1.5 + 0.5;
151
- drop.opacity = Math.random() * 0.7 + 0.3;
152
- drop.length = Math.floor(Math.random() * 20 + 5);
153
- }
154
- }
155
-
156
- ctx.fillStyle = "rgba(255,255,255,0.85)";
157
- ctx.fillRect(width - 210, 20, 180, 64);
158
- ctx.fillStyle = "#0f172a";
159
- ctx.font = "600 36px 'Outfit', system-ui, sans-serif";
160
- ctx.textAlign = "left";
161
- ctx.fillText(`FPS ${fps.toFixed(0)}`, width - 182, 65);
162
-
163
- config.onEvent?.({ type: "frame", fps });
164
- raf = requestAnimationFrame(render);
165
- };
166
-
167
- fitCanvasToWindow();
168
- window.addEventListener("resize", fitCanvasToWindow);
169
- config.onEvent?.({ type: "ready" });
170
-
171
- return {
172
- start() {
173
- if (running) {
174
- return;
175
- }
176
- running = true;
177
- raf = requestAnimationFrame(render);
1
+ // @ts-check
2
+ /**
3
+ * The demo engine, written against the contract in `src/engine-specs.ts`.
4
+ *
5
+ * It is a real implementation, not a mock: the four payloads are closed with named
6
+ * types, every getter returns descriptors, every setter chains, and a factory
7
+ * governs at most one live engine. `demo/tsconfig.json` checks it against the
8
+ * contract, so "the demo conforms" is verified rather than claimed.
9
+ *
10
+ * What used to live in a manifest lives here now:
11
+ * `id`/`version`/`name`/`description` are static on the class, the video geometry is
12
+ * `config`, and what an asset accepts is its descriptor's `allowedTypes`.
13
+ */
14
+
15
+ import { createCanvasRenderer } from './canvas-renderer.js'
16
+
17
+ /**
18
+ * @typedef {{ baseWidth: number, baseHeight: number }} DemoConfig
19
+ * @typedef {{ fileName?: string, byteLength?: number }} DemoOptions
20
+ * @typedef {{ rom?: Uint8Array, bios?: Uint8Array }} DemoAssets
21
+ * @typedef {{ namespace: string }} DemoStorage
22
+ * @typedef {{ config: DemoConfig, options: DemoOptions, assets: DemoAssets, storage: DemoStorage }} DemoPayloads
23
+ *
24
+ * @typedef {import('../src/engine-specs.js').EngineInit<DemoPayloads>} DemoInit
25
+ * @typedef {import('../src/engine-specs.js').EnginePlay<DemoPayloads>} DemoPlayContract
26
+ * @typedef {import('../src/engine-specs.js').PromisePlay<DemoPayloads>} DemoPromisePlay
27
+ * @typedef {import('../src/engine-specs.js').EngineSDK<DemoPayloads>} DemoSDKContract
28
+ * @typedef {import('../src/engine-specs.js').EngineSDKConstructor<DemoPayloads>} DemoSDKConstructor
29
+ */
30
+
31
+ /** @template T @typedef {import('../src/engine-specs.js').PropertiesDefinition<T>} Props */
32
+
33
+ /**
34
+ * The live engine.
35
+ *
36
+ * `pause()` and `resume()` are **not** part of the contract — they are this engine's
37
+ * own additions, which the demo's ESC menu drives. Anything the contract declares is
38
+ * below them.
39
+ *
40
+ * @implements {DemoPlayContract}
41
+ */
42
+ class DemoPlay {
43
+ /** @type {DemoConfig} */
44
+ #config = { baseWidth: 320, baseHeight: 256 }
45
+ /** @type {DemoOptions} */
46
+ #options = {}
47
+ /** @type {DemoAssets} */
48
+ #assets = {}
49
+ /** @type {DemoStorage} */
50
+ #storage = { namespace: 'engine-specs/demo' }
51
+ /** @type {HTMLElement | undefined} */
52
+ #mount
53
+ /** @type {HTMLCanvasElement | undefined} */
54
+ #canvas
55
+ /** @type {ReturnType<typeof createCanvasRenderer> | undefined} */
56
+ #renderer
57
+ /** The engine's own file system, keyed by mount path. */
58
+ #memfs = new Map()
59
+
60
+ /** @param {DemoInit} [init] */
61
+ constructor(init) {
62
+ if (init?.config) Object.assign(this.#config, init.config)
63
+ if (init?.options) Object.assign(this.#options, init.options)
64
+ if (init?.assets) Object.assign(this.#assets, init.assets)
65
+ if (init?.storage) Object.assign(this.#storage, init.storage)
66
+ if (init?.mount) this.mount(init.mount)
67
+ this.#copyAssetsToMemfs()
68
+ }
69
+
70
+ #copyAssetsToMemfs() {
71
+ for (const [key, data] of Object.entries(this.#assets)) {
72
+ if (!data) continue
73
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data)
74
+ this.#memfs.set(`/${key}.bin`, bytes)
75
+ console.log(`[MEMFS] ${key} → /${key}.bin (${bytes.byteLength} bytes)`)
76
+ }
77
+ }
78
+
79
+ /**
80
+ * @overload
81
+ * @returns {Promise<Props<DemoConfig>>}
82
+ * @overload
83
+ * @param {Partial<DemoConfig>} patch
84
+ * @returns {DemoPromisePlay}
85
+ * @param {Partial<DemoConfig>} [patch]
86
+ * @returns {any}
87
+ */
88
+ config(patch) {
89
+ if (arguments.length === 0) {
90
+ return Promise.resolve({
91
+ baseWidth: {
92
+ value: this.#config.baseWidth,
93
+ default: 320,
94
+ description: 'Native render width, in pixels.',
95
+ live: false,
178
96
  },
179
- pause() {
180
- running = false;
181
- cancelAnimationFrame(raf);
97
+ baseHeight: {
98
+ value: this.#config.baseHeight,
99
+ default: 256,
100
+ description: 'Native render height, in pixels.',
101
+ live: false,
102
+ },
103
+ })
104
+ }
105
+ Object.assign(this.#config, patch)
106
+ return hydrate(Promise.resolve(this))
107
+ }
108
+
109
+ /**
110
+ * @overload
111
+ * @returns {Promise<Props<DemoOptions>>}
112
+ * @overload
113
+ * @param {Partial<DemoOptions>} patch
114
+ * @returns {DemoPromisePlay}
115
+ * @param {Partial<DemoOptions>} [patch]
116
+ * @returns {any}
117
+ */
118
+ options(patch) {
119
+ if (arguments.length === 0) {
120
+ return Promise.resolve({
121
+ fileName: {
122
+ value: this.#options.fileName,
123
+ description: 'Name of the loaded ROM.',
124
+ live: true,
182
125
  },
183
- resume() {
184
- if (running) {
185
- return;
186
- }
187
- running = true;
188
- raf = requestAnimationFrame(render);
126
+ byteLength: {
127
+ value: this.#options.byteLength,
128
+ description: 'Size of the loaded ROM, in bytes.',
129
+ live: true,
189
130
  },
190
- reset() {
191
- t0 = performance.now();
192
- lastFrame = t0;
131
+ })
132
+ }
133
+ Object.assign(this.#options, patch)
134
+ return hydrate(Promise.resolve(this))
135
+ }
136
+
137
+ /**
138
+ * `allowedTypes` is what a host file picker needs: it replaces the manifest's
139
+ * `AssetSpec.accept`, and now travels with the value it describes.
140
+ *
141
+ * @overload
142
+ * @returns {Promise<Props<DemoAssets>>}
143
+ * @overload
144
+ * @param {Partial<DemoAssets>} patch
145
+ * @returns {DemoPromisePlay}
146
+ * @param {Partial<DemoAssets>} [patch]
147
+ * @returns {any}
148
+ */
149
+ assets(patch) {
150
+ if (arguments.length === 0) {
151
+ return Promise.resolve({
152
+ rom: {
153
+ value: this.#assets.rom,
154
+ description: 'Any game ROM file works in this demo.',
155
+ allowedTypes: ['.rom', '.bin', '.zip', '.dat'],
156
+ live: false,
193
157
  },
194
- setInput() {
195
- return;
158
+ bios: {
159
+ value: this.#assets.bios,
160
+ description: 'System BIOS firmware file.',
161
+ allowedTypes: ['.bin', '.rom', '.sys'],
162
+ live: false,
196
163
  },
197
- destroy() {
198
- running = false;
199
- cancelAnimationFrame(raf);
200
- window.removeEventListener("resize", fitCanvasToWindow);
201
- if (ownsCanvas) {
202
- canvasEl.remove();
203
- }
164
+ })
165
+ }
166
+ Object.assign(this.#assets, patch)
167
+ this.#copyAssetsToMemfs()
168
+ return hydrate(Promise.resolve(this))
169
+ }
170
+
171
+ /**
172
+ * @overload
173
+ * @returns {Promise<Props<DemoStorage>>}
174
+ * @overload
175
+ * @param {Partial<DemoStorage>} patch
176
+ * @returns {DemoPromisePlay}
177
+ * @param {Partial<DemoStorage>} [patch]
178
+ * @returns {any}
179
+ */
180
+ storage(patch) {
181
+ if (arguments.length === 0) {
182
+ return Promise.resolve({
183
+ namespace: {
184
+ value: this.#storage.namespace,
185
+ description: 'Folder the engine owns.',
186
+ live: false,
204
187
  },
205
- };
206
- },
207
- };
188
+ })
189
+ }
190
+ Object.assign(this.#storage, patch)
191
+ return hydrate(Promise.resolve(this))
192
+ }
193
+
194
+ /**
195
+ * Where the picture goes. The engine owns the canvas; the host owns the container.
196
+ * @param {HTMLElement} target
197
+ * @returns {DemoPromisePlay}
198
+ */
199
+ mount(target) {
200
+ this.#mount = target
201
+ if (!this.#canvas) {
202
+ this.#canvas = document.createElement('canvas')
203
+ }
204
+ target.appendChild(this.#canvas)
205
+ this.#renderer ??= createCanvasRenderer(this.#canvas)
206
+ return hydrate(Promise.resolve(this))
207
+ }
208
+
209
+ /**
210
+ * Opens or reopens a session. The payload is cumulative, merged flat, one level.
211
+ * @param {Partial<DemoOptions>} [options]
212
+ * @returns {DemoPromisePlay}
213
+ */
214
+ restart(options) {
215
+ if (options) Object.assign(this.#options, options)
216
+ this.#renderer?.reset()
217
+ this.#renderer?.start()
218
+ return hydrate(Promise.resolve(this))
219
+ }
220
+
221
+ async destroy() {
222
+ this.#renderer?.destroy()
223
+ this.#renderer = undefined
224
+ this.#canvas?.remove()
225
+ this.#canvas = undefined
226
+ this.#mount = undefined
227
+ this.#memfs.clear()
228
+ }
229
+
230
+ // ---- Beyond the contract: this engine's own members ----
231
+
232
+ pause() {
233
+ this.#renderer?.pause()
234
+ }
235
+
236
+ resume() {
237
+ this.#renderer?.resume()
238
+ }
208
239
  }
209
240
 
210
- export const sdk = createDummySdk();
211
- export default sdk;
241
+ /**
242
+ * Wraps a promise of the engine in the chainable surface.
243
+ *
244
+ * Structural, as the contract allows: not a `Promise` subclass, a delegation to one
245
+ * — plus `[Symbol.toStringTag]`, which is easy to forget and required.
246
+ *
247
+ * @param {Promise<DemoPlay>} inner
248
+ * @returns {DemoPromisePlay}
249
+ */
250
+ function hydrate(inner) {
251
+ /** @param {(play: DemoPlay) => unknown} step */
252
+ const chain = (step) => hydrate(inner.then(async (play) => { await step(play); return play }))
253
+
254
+ /** @type {any} */
255
+ const hydrated = {
256
+ /** @param {any} onfulfilled @param {any} onrejected */
257
+ then: (onfulfilled, onrejected) => inner.then(onfulfilled, onrejected),
258
+ /** @param {any} onrejected */
259
+ catch: (onrejected) => inner.catch(onrejected),
260
+ /** @param {any} onfinally */
261
+ finally: (onfinally) => inner.finally(onfinally),
262
+ [Symbol.toStringTag]: 'PromisePlay',
263
+
264
+ /** @param {Partial<DemoConfig>} [patch] */
265
+ config: (patch) =>
266
+ patch === undefined ? inner.then((p) => p.config()) : chain((p) => p.config(patch)),
267
+ /** @param {Partial<DemoOptions>} [patch] */
268
+ options: (patch) =>
269
+ patch === undefined ? inner.then((p) => p.options()) : chain((p) => p.options(patch)),
270
+ /** @param {Partial<DemoAssets>} [patch] */
271
+ assets: (patch) =>
272
+ patch === undefined ? inner.then((p) => p.assets()) : chain((p) => p.assets(patch)),
273
+ /** @param {Partial<DemoStorage>} [patch] */
274
+ storage: (patch) =>
275
+ patch === undefined ? inner.then((p) => p.storage()) : chain((p) => p.storage(patch)),
276
+
277
+ /** @param {HTMLElement} target */
278
+ mount: (target) => chain((p) => p.mount(target)),
279
+ /** @param {Partial<DemoOptions>} [options] */
280
+ restart: (options) => chain((p) => p.restart(options)),
281
+ destroy: () => inner.then((p) => p.destroy()),
282
+ }
283
+ return hydrated
284
+ }
285
+
286
+ /**
287
+ * The factory. Identity is static, so a launcher can list engines without
288
+ * constructing one.
289
+ *
290
+ * @implements {DemoSDKContract}
291
+ */
292
+ export class DemoEngineSDK {
293
+ static id = 'dummy-canvas-sdk'
294
+ static version = '0.1.0-demo'
295
+ static name = 'Dummy Canvas Engine'
296
+ static description = 'A placeholder engine that renders animated content on canvas.'
297
+
298
+ /** @type {DemoInit} */
299
+ #init = {}
300
+ /** @type {DemoPlay | undefined} */
301
+ #play
302
+
303
+ /** @param {DemoInit} [init] */
304
+ constructor(init) {
305
+ if (init) this.#init = { ...init }
306
+ }
307
+
308
+ /** Answers a read before there is an engine: the settings the next one will use. */
309
+ #pending() {
310
+ return this.#play ?? new DemoPlay(this.#init)
311
+ }
312
+
313
+ /**
314
+ * @overload
315
+ * @returns {Promise<Props<DemoConfig>>}
316
+ * @overload
317
+ * @param {Partial<DemoConfig>} patch
318
+ * @returns {this}
319
+ * @param {Partial<DemoConfig>} [patch]
320
+ * @returns {any}
321
+ */
322
+ config(patch) {
323
+ if (arguments.length === 0) return this.#pending().config()
324
+ this.#init.config = { ...this.#init.config, ...patch }
325
+ if (this.#play && patch) this.#play.config(patch)
326
+ return this
327
+ }
328
+
329
+ /**
330
+ * @overload
331
+ * @returns {Promise<Props<DemoOptions>>}
332
+ * @overload
333
+ * @param {Partial<DemoOptions>} patch
334
+ * @returns {this}
335
+ * @param {Partial<DemoOptions>} [patch]
336
+ * @returns {any}
337
+ */
338
+ options(patch) {
339
+ if (arguments.length === 0) return this.#pending().options()
340
+ this.#init.options = { ...this.#init.options, ...patch }
341
+ if (this.#play && patch) this.#play.options(patch)
342
+ return this
343
+ }
344
+
345
+ /**
346
+ * @overload
347
+ * @returns {Promise<Props<DemoAssets>>}
348
+ * @overload
349
+ * @param {Partial<DemoAssets>} patch
350
+ * @returns {this}
351
+ * @param {Partial<DemoAssets>} [patch]
352
+ * @returns {any}
353
+ */
354
+ assets(patch) {
355
+ if (arguments.length === 0) return this.#pending().assets()
356
+ this.#init.assets = { ...this.#init.assets, ...patch }
357
+ if (this.#play && patch) this.#play.assets(patch)
358
+ return this
359
+ }
360
+
361
+ /**
362
+ * @overload
363
+ * @returns {Promise<Props<DemoStorage>>}
364
+ * @overload
365
+ * @param {Partial<DemoStorage>} patch
366
+ * @returns {this}
367
+ * @param {Partial<DemoStorage>} [patch]
368
+ * @returns {any}
369
+ */
370
+ storage(patch) {
371
+ if (arguments.length === 0) return this.#pending().storage()
372
+ this.#init.storage = { ...this.#init.storage, ...patch }
373
+ if (this.#play && patch) this.#play.storage(patch)
374
+ return this
375
+ }
376
+
377
+ /**
378
+ * Recorded now, applied when the engine exists — same as `new SDK({ mount })`.
379
+ * @param {HTMLElement} target
380
+ * @returns {this}
381
+ */
382
+ mount(target) {
383
+ this.#init.mount = target
384
+ if (this.#play) this.#play.mount(target)
385
+ return this
386
+ }
387
+
388
+ /**
389
+ * Bringing the engine up is this engine's own business, not a step the contract
390
+ * exposes — so it stays private. A factory governs at most one live engine.
391
+ * @returns {DemoPlay}
392
+ */
393
+ #boot() {
394
+ this.#play ??= new DemoPlay(this.#init)
395
+ return this.#play
396
+ }
397
+
398
+ /**
399
+ * The only way in. On a live engine it reopens the session rather than
400
+ * rebuilding one.
401
+ * @param {Partial<DemoOptions>} [options]
402
+ * @returns {DemoPromisePlay}
403
+ */
404
+ start(options) {
405
+ return hydrate(Promise.resolve(this.#boot())).restart(options)
406
+ }
407
+ }
408
+
409
+ /** Conformance, checked by the compiler: does the class satisfy the contract? */
410
+ /** @type {DemoSDKConstructor} */
411
+ export const DemoEngine = DemoEngineSDK
412
+
413
+ export default DemoEngine
package/demo/index.html CHANGED
@@ -3,69 +3,20 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>WASM Gaming Engine Specs Demo</title>
6
+ <title>engine-specs demo</title>
7
7
  <link rel="stylesheet" href="./demo.css" />
8
8
  </head>
9
9
  <body>
10
10
  <main></main>
11
11
 
12
12
  <script type="module">
13
- import demo from './demo.js'
14
- import sdk from './demo.sdk.js'
13
+ import { C79 } from 'https://jgermade.github.io/jq79/jq79.js'
15
14
 
16
- demo
17
- .init({
18
- messages: {
19
- title: 'WASM Gaming Engine Demo',
20
- dropRomPrompt: 'Drop ROM File',
21
- launchButton: 'Launch Engine',
22
- escMenuTitle: 'Settings',
23
- escMenuResume: 'Resume',
24
- escMenuReset: 'Reset game',
25
- escMenuRestoreDefaults: 'Restore defaults',
26
- escMenuExit: 'Exit to Launcher',
27
- },
28
- bios: false, // 'required' | 'optional' | false
29
- escMenu: {
30
- enabled: true,
31
- },
32
- })
33
- .on('rom:save', ({ file }) => {
34
- console.log('[Demo] ROM loaded:', file?.name)
35
- })
36
- .on('bios:save', ({ file }) => {
37
- console.log('[Demo] BIOS loaded:', file?.name)
38
- })
39
- .on('launch', async (e) => {
40
- const assets = {}
41
-
42
- // if rom loaded
43
- if (demo.hasFile('rom')) {
44
- assets.rom = await demo.loadToMEMFS('rom')
45
- }
46
-
47
- // if bios required and loaded
48
- if (demo.hasFile('bios')) {
49
- assets.bios = await demo.loadToMEMFS('bios')
50
- }
51
-
52
- const instance = await sdk.load({
53
- attachTo: e.attachTo,
54
- assets,
55
- options: {
56
- fileName: e.fileName,
57
- byteLength: e.byteLength,
58
- },
59
- onEvent(event) {
60
- if (event.type === 'error') {
61
- console.error(event.error)
62
- }
63
- },
64
- })
65
-
66
- instance.start()
67
- return instance
15
+ C79.fetch('./app.html')
16
+ .mount('body > main')
17
+ .catch(err => {
18
+ console.error('Failed to load app.html:', err)
68
19
  })
69
20
  </script>
70
21
  </body>
71
- </html>
22
+ </html>