@wasm-gaming/engine-specs 0.3.1 → 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/README.md +107 -3
- package/demo/app.html +59 -19
- package/demo/components/asset-picker.html +33 -14
- package/demo/components/engine-header.html +17 -28
- package/demo/components/site-toolbar.html +31 -0
- package/demo/demo.css +22 -12
- package/demo/demo.sdk.js +104 -322
- package/demo/index.html +17 -0
- package/dist/engine-specs.d.ts +388 -0
- package/dist/engine-specs.d.ts.map +1 -1
- package/dist/engine-specs.js +644 -1
- package/dist/engine-specs.js.map +1 -1
- package/package.json +1 -1
- package/src/engine-specs.ts +977 -0
package/demo/demo.sdk.js
CHANGED
|
@@ -2,54 +2,91 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The demo engine, written against the contract in `src/engine-specs.ts`.
|
|
4
4
|
*
|
|
5
|
-
* It is a real implementation, not a mock
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* It is a real implementation, not a mock, and it is now written the way an engine is
|
|
6
|
+
* meant to be written: `EnginePlayBase` and `EngineSDKBase` carry the state, the
|
|
7
|
+
* descriptors, the cumulative merge, the chainable promise and the channel, and what
|
|
8
|
+
* is left here is the part that is actually about drawing on a canvas.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* `demo/tsconfig.json` checks it against the contract with `checkJs`, so "the demo
|
|
11
|
+
* conforms" is verified rather than claimed — and it is also the check that the base
|
|
12
|
+
* classes are usable from plain JS with JSDoc, not only from TypeScript.
|
|
13
|
+
*
|
|
14
|
+
* What used to live in a manifest lives here now: `id`/`version`/`name`/`description`
|
|
15
|
+
* are static on the class, the video geometry is `config`, and what an asset accepts
|
|
16
|
+
* is its descriptor's `allowedTypes`.
|
|
13
17
|
*/
|
|
14
18
|
|
|
19
|
+
import { EnginePlayBase, EngineSDKBase } from '../src/engine-specs.js'
|
|
15
20
|
import { createCanvasRenderer } from './canvas-renderer.js'
|
|
16
21
|
|
|
17
22
|
/**
|
|
23
|
+
* This engine omits `events` from its payload bundle, unlike the reference engine in
|
|
24
|
+
* `tests/engine.conformance.ts` which declares one of its own. Both are legal, and
|
|
25
|
+
* between the two they cover both branches of `EventsOf`: here it resolves to the
|
|
26
|
+
* core set alone.
|
|
27
|
+
*
|
|
18
28
|
* @typedef {{ baseWidth: number, baseHeight: number }} DemoConfig
|
|
19
29
|
* @typedef {{ fileName?: string, byteLength?: number }} DemoOptions
|
|
20
30
|
* @typedef {{ rom?: Uint8Array, bios?: Uint8Array }} DemoAssets
|
|
21
31
|
* @typedef {{ namespace: string }} DemoStorage
|
|
22
32
|
* @typedef {{ config: DemoConfig, options: DemoOptions, assets: DemoAssets, storage: DemoStorage }} DemoPayloads
|
|
23
33
|
*
|
|
34
|
+
* @typedef {import('../src/engine-specs.js').EventsOf<DemoPayloads>} DemoEvents
|
|
24
35
|
* @typedef {import('../src/engine-specs.js').EngineInit<DemoPayloads>} DemoInit
|
|
25
|
-
* @typedef {import('../src/engine-specs.js').
|
|
26
|
-
* @typedef {import('../src/engine-specs.js').
|
|
27
|
-
* @typedef {import('../src/engine-specs.js').EngineSDK<DemoPayloads>} DemoSDKContract
|
|
36
|
+
* @typedef {import('../src/engine-specs.js').EngineSpec<DemoPayloads>} DemoSpec
|
|
37
|
+
* @typedef {import('../src/engine-specs.js').EventEmitter<DemoEvents>} DemoChannel
|
|
28
38
|
* @typedef {import('../src/engine-specs.js').EngineSDKConstructor<DemoPayloads>} DemoSDKConstructor
|
|
29
39
|
*/
|
|
30
40
|
|
|
31
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* What this engine declares about its properties. `default` appears here and nowhere
|
|
43
|
+
* else — the base class seeds its starting state from it, so the initial value and
|
|
44
|
+
* the advertised default cannot drift apart.
|
|
45
|
+
*
|
|
46
|
+
* @type {DemoSpec}
|
|
47
|
+
*/
|
|
48
|
+
const spec = {
|
|
49
|
+
config: {
|
|
50
|
+
baseWidth: { default: 320, description: 'Native render width, in pixels.', live: false },
|
|
51
|
+
baseHeight: { default: 256, description: 'Native render height, in pixels.', live: false },
|
|
52
|
+
},
|
|
53
|
+
options: {
|
|
54
|
+
fileName: { description: 'Name of the loaded ROM.', live: true },
|
|
55
|
+
byteLength: { description: 'Size of the loaded ROM, in bytes.', live: true },
|
|
56
|
+
},
|
|
57
|
+
assets: {
|
|
58
|
+
// `allowedTypes` is what a host file picker needs: it replaces the manifest's
|
|
59
|
+
// `AssetSpec.accept`, and now travels with the value it describes.
|
|
60
|
+
rom: {
|
|
61
|
+
description: 'Any game ROM file works in this demo.',
|
|
62
|
+
allowedTypes: ['.rom', '.bin', '.zip', '.dat'],
|
|
63
|
+
live: false,
|
|
64
|
+
},
|
|
65
|
+
bios: {
|
|
66
|
+
description: 'System BIOS firmware file.',
|
|
67
|
+
allowedTypes: ['.bin', '.rom', '.sys'],
|
|
68
|
+
live: false,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
storage: {
|
|
72
|
+
namespace: { default: 'engine-specs/demo', description: 'Folder the engine owns.', live: false },
|
|
73
|
+
},
|
|
74
|
+
}
|
|
32
75
|
|
|
33
76
|
/**
|
|
34
77
|
* The live engine.
|
|
35
78
|
*
|
|
79
|
+
* Everything the contract prescribes is inherited. What is below is this engine and
|
|
80
|
+
* nothing else: a canvas, a renderer, and a pretend file system.
|
|
81
|
+
*
|
|
36
82
|
* `pause()` and `resume()` are **not** part of the contract — they are this engine's
|
|
37
|
-
* own
|
|
38
|
-
*
|
|
83
|
+
* own methods, which the demo's ESC menu drives. What *is* in the contract is what
|
|
84
|
+
* they emit: `pause` and `resume` are core events, so a host can react to the ESC
|
|
85
|
+
* menu without knowing this class has those methods.
|
|
39
86
|
*
|
|
40
|
-
* @
|
|
87
|
+
* @extends {EnginePlayBase<DemoPayloads>}
|
|
41
88
|
*/
|
|
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
|
|
89
|
+
class DemoPlay extends EnginePlayBase {
|
|
53
90
|
/** @type {HTMLCanvasElement | undefined} */
|
|
54
91
|
#canvas
|
|
55
92
|
/** @type {ReturnType<typeof createCanvasRenderer> | undefined} */
|
|
@@ -57,173 +94,60 @@ class DemoPlay {
|
|
|
57
94
|
/** The engine's own file system, keyed by mount path. */
|
|
58
95
|
#memfs = new Map()
|
|
59
96
|
|
|
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,
|
|
96
|
-
},
|
|
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,
|
|
125
|
-
},
|
|
126
|
-
byteLength: {
|
|
127
|
-
value: this.#options.byteLength,
|
|
128
|
-
description: 'Size of the loaded ROM, in bytes.',
|
|
129
|
-
live: true,
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
Object.assign(this.#options, patch)
|
|
134
|
-
return hydrate(Promise.resolve(this))
|
|
135
|
-
}
|
|
136
|
-
|
|
137
97
|
/**
|
|
138
|
-
*
|
|
139
|
-
* `
|
|
98
|
+
* The spec goes up to the base rather than being a field here: a subclass field
|
|
99
|
+
* initialiser runs after `super()`, which would be too late to seed state from.
|
|
140
100
|
*
|
|
141
|
-
* @
|
|
142
|
-
* @
|
|
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,
|
|
157
|
-
},
|
|
158
|
-
bios: {
|
|
159
|
-
value: this.#assets.bios,
|
|
160
|
-
description: 'System BIOS firmware file.',
|
|
161
|
-
allowedTypes: ['.bin', '.rom', '.sys'],
|
|
162
|
-
live: false,
|
|
163
|
-
},
|
|
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}
|
|
101
|
+
* @param {DemoInit} [init]
|
|
102
|
+
* @param {DemoChannel} [events]
|
|
179
103
|
*/
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
return Promise.resolve({
|
|
183
|
-
namespace: {
|
|
184
|
-
value: this.#storage.namespace,
|
|
185
|
-
description: 'Folder the engine owns.',
|
|
186
|
-
live: false,
|
|
187
|
-
},
|
|
188
|
-
})
|
|
189
|
-
}
|
|
190
|
-
Object.assign(this.#storage, patch)
|
|
191
|
-
return hydrate(Promise.resolve(this))
|
|
104
|
+
constructor(init, events) {
|
|
105
|
+
super(spec, init, events)
|
|
192
106
|
}
|
|
193
107
|
|
|
194
108
|
/**
|
|
195
109
|
* Where the picture goes. The engine owns the canvas; the host owns the container.
|
|
110
|
+
* The element is already recorded by the base when this runs.
|
|
111
|
+
*
|
|
112
|
+
* @protected
|
|
196
113
|
* @param {HTMLElement} target
|
|
197
|
-
* @returns {DemoPromisePlay}
|
|
198
114
|
*/
|
|
199
|
-
|
|
200
|
-
this.#
|
|
201
|
-
if (!this.#canvas) {
|
|
202
|
-
this.#canvas = document.createElement('canvas')
|
|
203
|
-
}
|
|
115
|
+
mounted(target) {
|
|
116
|
+
this.#canvas ??= document.createElement('canvas')
|
|
204
117
|
target.appendChild(this.#canvas)
|
|
205
118
|
this.#renderer ??= createCanvasRenderer(this.#canvas)
|
|
206
|
-
return hydrate(Promise.resolve(this))
|
|
207
119
|
}
|
|
208
120
|
|
|
209
121
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
122
|
+
* A session opens — or reopens. The assets are read back through the contract
|
|
123
|
+
* rather than kept in a second field here, which is the point of the base owning
|
|
124
|
+
* the state.
|
|
125
|
+
*
|
|
126
|
+
* @protected
|
|
127
|
+
* @param {DemoOptions} options
|
|
213
128
|
*/
|
|
214
|
-
|
|
215
|
-
|
|
129
|
+
async open(options) {
|
|
130
|
+
void options
|
|
131
|
+
const assets = await this.assets()
|
|
132
|
+
for (const [key, property] of Object.entries(assets)) {
|
|
133
|
+
const data = property.value
|
|
134
|
+
if (!data) continue
|
|
135
|
+
this.#memfs.set(`/${key}.bin`, data)
|
|
136
|
+
console.log(`[MEMFS] ${key} → /${key}.bin (${data.byteLength} bytes)`)
|
|
137
|
+
}
|
|
216
138
|
this.#renderer?.reset()
|
|
217
139
|
this.#renderer?.start()
|
|
218
|
-
return hydrate(Promise.resolve(this))
|
|
219
140
|
}
|
|
220
141
|
|
|
221
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Stop and release. `exit` is emitted by the base once this returns.
|
|
144
|
+
* @protected
|
|
145
|
+
*/
|
|
146
|
+
close() {
|
|
222
147
|
this.#renderer?.destroy()
|
|
223
148
|
this.#renderer = undefined
|
|
224
149
|
this.#canvas?.remove()
|
|
225
150
|
this.#canvas = undefined
|
|
226
|
-
this.#mount = undefined
|
|
227
151
|
this.#memfs.clear()
|
|
228
152
|
}
|
|
229
153
|
|
|
@@ -231,178 +155,36 @@ class DemoPlay {
|
|
|
231
155
|
|
|
232
156
|
pause() {
|
|
233
157
|
this.#renderer?.pause()
|
|
158
|
+
this.emit('pause', { owner: 'demo-esc-menu' })
|
|
234
159
|
}
|
|
235
160
|
|
|
236
161
|
resume() {
|
|
237
162
|
this.#renderer?.resume()
|
|
163
|
+
this.emit('resume', { owner: 'demo-esc-menu' })
|
|
238
164
|
}
|
|
239
165
|
}
|
|
240
166
|
|
|
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
167
|
/**
|
|
287
168
|
* The factory. Identity is static, so a launcher can list engines without
|
|
288
|
-
* constructing one
|
|
169
|
+
* constructing one — and the only thing it has to say beyond that is which engine to
|
|
170
|
+
* build.
|
|
289
171
|
*
|
|
290
|
-
* @
|
|
172
|
+
* @extends {EngineSDKBase<DemoPayloads>}
|
|
291
173
|
*/
|
|
292
|
-
export class DemoEngineSDK {
|
|
174
|
+
export class DemoEngineSDK extends EngineSDKBase {
|
|
293
175
|
static id = 'dummy-canvas-sdk'
|
|
294
176
|
static version = '0.1.0-demo'
|
|
295
177
|
static name = 'Dummy Canvas Engine'
|
|
296
178
|
static description = 'A placeholder engine that renders animated content on canvas.'
|
|
297
179
|
|
|
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
180
|
/**
|
|
378
|
-
*
|
|
379
|
-
* @param {
|
|
380
|
-
* @
|
|
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.
|
|
181
|
+
* @protected
|
|
182
|
+
* @param {DemoInit} init
|
|
183
|
+
* @param {DemoChannel} events
|
|
391
184
|
* @returns {DemoPlay}
|
|
392
185
|
*/
|
|
393
|
-
|
|
394
|
-
|
|
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)
|
|
186
|
+
createPlay(init, events) {
|
|
187
|
+
return new DemoPlay(init, events)
|
|
406
188
|
}
|
|
407
189
|
}
|
|
408
190
|
|
package/demo/index.html
CHANGED
|
@@ -4,7 +4,24 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>engine-specs demo</title>
|
|
7
|
+
<!--
|
|
8
|
+
TypeDoc's stylesheet paints the toolbar and carries the palette the demo maps
|
|
9
|
+
itself onto. The demo only ever runs from `site/demo/` — `demo.sdk.js` imports
|
|
10
|
+
`../src/engine-specs.js` as a value, which is why `make site` puts the built JS
|
|
11
|
+
there — so this path always resolves.
|
|
12
|
+
|
|
13
|
+
Order matters, but only downwards: everything TypeDoc ships is inside
|
|
14
|
+
`@layer typedoc`, so demo.css and every component's styles win over it without
|
|
15
|
+
trying.
|
|
16
|
+
-->
|
|
17
|
+
<link rel="stylesheet" href="../assets/style.css" />
|
|
7
18
|
<link rel="stylesheet" href="./demo.css" />
|
|
19
|
+
<script>
|
|
20
|
+
// The theme the reference was left on. TypeDoc writes this key; both pages read
|
|
21
|
+
// it, so one switch in the docs sidebar drives both. `os` is TypeDoc's own
|
|
22
|
+
// default and means "follow the system".
|
|
23
|
+
document.documentElement.dataset.theme = localStorage.getItem('tsd-theme') || 'os'
|
|
24
|
+
</script>
|
|
8
25
|
</head>
|
|
9
26
|
<body>
|
|
10
27
|
<main></main>
|