@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.
- package/README.md +63 -54
- package/demo/app.html +227 -0
- package/demo/canvas-renderer.js +153 -0
- package/demo/components/asset-picker.html +137 -0
- package/demo/components/call-log.html +33 -0
- package/demo/components/engine-header.html +78 -0
- package/demo/components/lifecycle-bar.html +69 -0
- package/demo/components/property-table.html +119 -0
- package/demo/demo.css +26 -130
- package/demo/demo.sdk.js +405 -203
- package/demo/index.html +7 -56
- package/demo/tsconfig.json +17 -0
- package/dist/engine-specs.d.ts +106 -0
- package/dist/engine-specs.d.ts.map +1 -0
- package/dist/engine-specs.js +3 -0
- package/dist/engine-specs.js.map +1 -0
- package/package.json +7 -12
- package/src/engine-specs.ts +117 -0
- package/demo/README.md +0 -155
- package/demo/checksums.js +0 -101
- package/demo/components/drop-area.html +0 -171
- package/demo/components/esc-menu.html +0 -540
- package/demo/components/launcher.html +0 -220
- package/demo/components/sdk-info.html +0 -54
- package/demo/demo.js +0 -622
- package/demo/sdk.js +0 -19
- package/demo/storage.js +0 -88
- package/dist/engine.d.ts +0 -427
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -83
- package/dist/engine.js.map +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +0 -1
- package/dist/manifest.d.ts +0 -69
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -8
- package/dist/manifest.js.map +0 -1
- package/dist/validate.d.ts +0 -10
- package/dist/validate.d.ts.map +0 -1
- package/dist/validate.js +0 -79
- package/dist/validate.js.map +0 -1
- package/schema/engine-manifest.schema.json +0 -74
- package/src/engine.ts +0 -516
- package/src/index.ts +0 -55
- package/src/manifest.ts +0 -82
- package/src/validate.ts +0 -92
package/demo/demo.sdk.js
CHANGED
|
@@ -1,211 +1,413 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
158
|
+
bios: {
|
|
159
|
+
value: this.#assets.bios,
|
|
160
|
+
description: 'System BIOS firmware file.',
|
|
161
|
+
allowedTypes: ['.bin', '.rom', '.sys'],
|
|
162
|
+
live: false,
|
|
196
163
|
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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>
|
|
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
|
|
14
|
-
import sdk from './demo.sdk.js'
|
|
13
|
+
import { C79 } from 'https://jgermade.github.io/jq79/jq79.js'
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
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>
|