@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.js DELETED
@@ -1,622 +0,0 @@
1
- import { checksumAlgorithms, computeChecksums } from "./checksums.js";
2
- import { loadStoredAsset, saveStoredAsset, clearStoredAsset } from "./storage.js";
3
-
4
- let isInitialized = false;
5
- let currentConfig = {};
6
- let currentInstance = null;
7
-
8
- const listeners = new Map();
9
- const loadedFiles = {
10
- rom: null,
11
- bios: null,
12
- };
13
-
14
- const DEFAULT_MESSAGES = {
15
- // Page / Document
16
- title: "WASM Gaming Engine Specs Demo",
17
-
18
- // Launcher & Status
19
- launchButton: "Launch Engine",
20
- launchingStatus: "Launching with",
21
- statusReady: "Ready to launch!",
22
- statusSelectRom: "Ready - Please select a ROM file to launch.",
23
- statusSelectBoth: "Ready - Please select ROM and BIOS files to launch.",
24
- statusBiosRequired: "BIOS file is required to launch.",
25
-
26
- // Asset Drop Zones
27
- dropRomPrompt: "Drop ROM File",
28
- dropBiosPromptRequired: "Drop BIOS File (Required)",
29
- dropBiosPromptOptional: "Drop BIOS File (Optional)",
30
- romAssetHint: "or choose ROM file from computer",
31
- biosAssetHintOptional: "optional BIOS firmware file",
32
- biosAssetHintRequired: "required BIOS firmware file",
33
- pickRomButton: "Pick ROM",
34
- pickBiosButton: "Pick BIOS",
35
- changeFileButton: "Change File",
36
- removeFileButton: "Remove",
37
- selectedPrefix: "Selected: ",
38
-
39
- // Metadata
40
- versionPrefix: "Version ",
41
- assetKeyPrefix: "Asset key: ",
42
-
43
- // ESC Menu
44
- escMenuTitle: "Settings",
45
- escMenuBadge: "PAUSED",
46
- escMenuResume: "Resume",
47
- escMenuRestoreDefaults: "Restore defaults",
48
- escMenuReset: "Reset game",
49
- escMenuSaveState: "Save State",
50
- escMenuLoadState: "Load State",
51
- escMenuExit: "Exit to Launcher",
52
- escMenuNeedsResetTag: "needs reset",
53
- escMenuKeysHint: "↑ ↓ select · ← → change · Enter apply · Esc close",
54
- };
55
-
56
- async function toArrayBuffer(input) {
57
- if (!input) return null;
58
- if (input instanceof ArrayBuffer) return input;
59
- if (ArrayBuffer.isView(input)) return input.buffer;
60
- if (typeof input.arrayBuffer === "function") return await input.arrayBuffer();
61
- if (input.detail) return await toArrayBuffer(input.detail);
62
- throw new TypeError("Provided input is not a File, Blob, or ArrayBuffer.");
63
- }
64
-
65
- function resolveBiosConfig(biosOption) {
66
- if (typeof location === "undefined") {
67
- return { showBIOS: false, requiresBIOS: false };
68
- }
69
- const pageUrl = new URL(location.href);
70
- const scriptUrl = new URL(import.meta.url, location.href);
71
- const biosParam = scriptUrl.searchParams.get("bios") ?? pageUrl.searchParams.get("bios");
72
-
73
- if (biosOption !== undefined) {
74
- if (biosOption === "required") {
75
- return { showBIOS: true, requiresBIOS: true };
76
- }
77
- if (biosOption === "optional" || biosOption === true || biosOption === "true") {
78
- return { showBIOS: true, requiresBIOS: false };
79
- }
80
- return { showBIOS: false, requiresBIOS: false };
81
- }
82
-
83
- const show = biosParam === "true" || biosParam === "optional" || biosParam === "required";
84
- const req = biosParam === "required";
85
- return { showBIOS: show, requiresBIOS: req };
86
- }
87
-
88
- function getDefaultOptionGroups() {
89
- return [
90
- {
91
- id: "video",
92
- label: "Video",
93
- options: [
94
- {
95
- key: "filterMode",
96
- label: "Image filtering",
97
- description: "Texture sampling used when scaling emulated output. Nearest keeps pixel art crisp; linear softens it.",
98
- type: "enum",
99
- value: "Nearest",
100
- values: [
101
- { value: "Nearest", label: "Nearest" },
102
- { value: "Linear", label: "Linear" },
103
- ],
104
- },
105
- {
106
- key: "aspectRatio",
107
- label: "Display aspect ratio",
108
- description: "Output display aspect ratio for emulated console screen.",
109
- type: "enum",
110
- value: "4:3",
111
- values: [
112
- { value: "4:3", label: "4:3" },
113
- { value: "8:7", label: "8:7" },
114
- { value: "1:1", label: "1:1" },
115
- ],
116
- },
117
- {
118
- key: "autoPrescale",
119
- label: "Prescaling",
120
- description: "Integer-prescale the frame before filtering to sharpen upscaled output.",
121
- type: "boolean",
122
- value: true,
123
- requiresReset: false,
124
- },
125
- ],
126
- },
127
- {
128
- id: "audio",
129
- label: "Audio",
130
- options: [
131
- {
132
- key: "lowPassFilter",
133
- label: "Audio low-pass filter",
134
- description: "Emulate the console's hardware audio low-pass filter.",
135
- type: "boolean",
136
- value: false,
137
- },
138
- ],
139
- },
140
- ];
141
- }
142
-
143
- const demo = {
144
- on(event, fn) {
145
- if (!listeners.has(event)) {
146
- listeners.set(event, []);
147
- }
148
- listeners.get(event).push(fn);
149
- return demo;
150
- },
151
-
152
- off(event, fn) {
153
- if (!listeners.has(event)) return demo;
154
- const list = listeners.get(event).filter((cb) => cb !== fn);
155
- listeners.set(event, list);
156
- return demo;
157
- },
158
-
159
- async emit(event, payload) {
160
- if (!listeners.has(event)) return [];
161
- const callbacks = listeners.get(event);
162
- const results = [];
163
- for (const cb of callbacks) {
164
- try {
165
- const res = await cb(payload);
166
- results.push(res);
167
- } catch (err) {
168
- console.error(`[Demo] Event '${event}' handler error:`, err);
169
- }
170
- }
171
- return results;
172
- },
173
-
174
- async loadToMEMFS(key, target) {
175
- if (!key) {
176
- const assets = {};
177
- for (const [k, file] of Object.entries(loadedFiles)) {
178
- if (file) {
179
- assets[k] = await toArrayBuffer(file);
180
- }
181
- }
182
- return assets;
183
- }
184
- const file = loadedFiles[key];
185
- if (!file) return null;
186
- const buffer = await toArrayBuffer(file);
187
- if (target && typeof target === "object") {
188
- if (typeof target.set === "function") {
189
- target.set(key, buffer);
190
- } else if (typeof target.FS?.writeFile === "function") {
191
- target.FS.writeFile(key, new Uint8Array(buffer));
192
- }
193
- }
194
- return buffer;
195
- },
196
-
197
- getFile(key) {
198
- if (!key) return loadedFiles;
199
- return loadedFiles[key] || null;
200
- },
201
-
202
- hasFile(key) {
203
- if (!key) return Boolean(loadedFiles.rom || loadedFiles.bios);
204
- return Boolean(loadedFiles[key]);
205
- },
206
-
207
- isLoaded(key) {
208
- return demo.hasFile(key);
209
- },
210
-
211
- bindInstance(instance) {
212
- currentInstance = instance;
213
- return demo;
214
- },
215
-
216
- setInstance(instance) {
217
- return demo.bindInstance(instance);
218
- },
219
-
220
- init(config = {}) {
221
- isInitialized = true;
222
- currentConfig = config;
223
-
224
- const boot = async () => {
225
- if (typeof window === "undefined" && !config.target) {
226
- return;
227
- }
228
- let jq;
229
- if (config.jq79) {
230
- jq = config.jq79;
231
- } else {
232
- jq = await import("https://jgermade.github.io/jq79/jq79.js");
233
- }
234
-
235
- const { $, $create, Component79 } = jq;
236
- const fetchComponent = (path) => Component79.fetch(new URL(path, import.meta.url).href);
237
-
238
- // Fetch components
239
- const [CLauncher, CDropArea, CSdkInfo, CEscMenu] = await Promise.all([
240
- fetchComponent("./components/launcher.html"),
241
- fetchComponent("./components/drop-area.html"),
242
- fetchComponent("./components/sdk-info.html"),
243
- fetchComponent("./components/esc-menu.html"),
244
- ]);
245
-
246
- let mainEl;
247
- if (config.target instanceof HTMLElement) {
248
- mainEl = config.target;
249
- } else if (typeof config.target === "string") {
250
- mainEl = document.querySelector(config.target);
251
- } else {
252
- mainEl = $("main") || document.body;
253
- }
254
-
255
- if (!mainEl) {
256
- throw new Error("Demo page boot failed: missing target element.");
257
- }
258
-
259
- const messages = { ...DEFAULT_MESSAGES, ...config.messages };
260
- const escMenuConfig = config.escMenu || {};
261
-
262
- if (typeof document !== "undefined") {
263
- if (config.messages?.title) {
264
- document.title = config.messages.title;
265
- } else if (config.manifest?.name) {
266
- document.title = `${config.manifest.name} - Demo`;
267
- } else if (config.sdkInfo?.name) {
268
- document.title = `${config.sdkInfo.name} - Demo`;
269
- }
270
- }
271
-
272
- const { showBIOS, requiresBIOS } = resolveBiosConfig(config.bios);
273
-
274
- const [storedRom, storedBios] = await Promise.all([
275
- loadStoredAsset("rom"),
276
- showBIOS ? loadStoredAsset("bios") : null,
277
- ]);
278
-
279
- loadedFiles.rom = storedRom;
280
- loadedFiles.bios = storedBios;
281
-
282
- const manifestAssets = config.manifest?.assets || config.sdkInfo?.assets;
283
- const romSpec = manifestAssets?.find((a) => a.key === "rom") ?? manifestAssets?.[0];
284
- const biosSpec = manifestAssets?.find((a) => a.key === "bios") ?? manifestAssets?.[1];
285
-
286
- const romAccept = config.romAccept ?? config.accept?.rom ?? (romSpec?.accept ? romSpec.accept.join(",") : ".rom,.bin,.zip,.dat");
287
- const romAssetHint = config.messages?.romAssetHint ?? (romSpec?.accept ? `Supported: ${romSpec.accept.join(", ")}` : messages.romAssetHint);
288
-
289
- const biosAccept = config.biosAccept ?? config.accept?.bios ?? (biosSpec?.accept ? biosSpec.accept.join(",") : ".bin,.rom,.sys");
290
- const biosAssetHint = config.messages?.biosAssetHint ?? (biosSpec?.description ?? (biosSpec?.accept ? `Supported: ${biosSpec.accept.join(", ")}` : (requiresBIOS ? messages.biosAssetHintRequired : messages.biosAssetHintOptional)));
291
-
292
- const sdkInfo = config.sdkInfo || (config.manifest ? {
293
- name: config.manifest.name,
294
- id: config.manifest.id,
295
- description: config.manifest.description,
296
- version: config.manifest.version,
297
- baseWidth: config.manifest.video?.baseWidth ?? 320,
298
- baseHeight: config.manifest.video?.baseHeight ?? 240,
299
- assetKey: romSpec?.key ?? "rom",
300
- } : {
301
- name: messages.title || "WASM Gaming Engine Demo",
302
- id: "engine-demo",
303
- description: "Standalone Web Interface",
304
- version: "1.0.0",
305
- baseWidth: 320,
306
- baseHeight: 240,
307
- assetKey: "rom",
308
- });
309
-
310
- // Handlers bound to a jq79 mount are called as (event, payload) — the
311
- // DOM CustomEvent first, the emitted value second. `demo.on()` below is
312
- // a different emitter that passes the payload alone, so the two must not
313
- // be read the same way.
314
- const handleSaveRom = (_event, file) => {
315
- loadedFiles.rom = file;
316
- saveStoredAsset("rom", file);
317
- const payload = { file, key: "rom", type: "rom", action: "save" };
318
- demo.emit("rom:save", payload);
319
- demo.emit("save:rom", payload);
320
- demo.emit("rom:select", payload);
321
- demo.emit("file", payload);
322
- };
323
-
324
- const handleRemoveRom = () => {
325
- loadedFiles.rom = null;
326
- clearStoredAsset("rom");
327
- const payload = { file: null, key: "rom", type: "rom", action: "remove" };
328
- demo.emit("rom:remove", payload);
329
- demo.emit("remove:rom", payload);
330
- demo.emit("rom:clear", payload);
331
- demo.emit("file", payload);
332
- };
333
-
334
- const handleSaveBios = (_event, file) => {
335
- loadedFiles.bios = file;
336
- saveStoredAsset("bios", file);
337
- const payload = { file, key: "bios", type: "bios", action: "save" };
338
- demo.emit("bios:save", payload);
339
- demo.emit("save:bios", payload);
340
- demo.emit("bios:select", payload);
341
- demo.emit("file", payload);
342
- };
343
-
344
- const handleRemoveBios = () => {
345
- loadedFiles.bios = null;
346
- clearStoredAsset("bios");
347
- const payload = { file: null, key: "bios", type: "bios", action: "remove" };
348
- demo.emit("bios:remove", payload);
349
- demo.emit("remove:bios", payload);
350
- demo.emit("bios:clear", payload);
351
- demo.emit("file", payload);
352
- };
353
-
354
- let currentLauncherMount = null;
355
-
356
- const renderLauncher = () => {
357
- currentInstance = null;
358
- if (mainEl.classList?.remove) {
359
- mainEl.classList.remove("running");
360
- }
361
- if (mainEl.replaceChildren) {
362
- mainEl.replaceChildren();
363
- }
364
-
365
- currentLauncherMount = CLauncher
366
- .render({
367
- CSdkInfo,
368
- CDropArea,
369
- sdkInfo,
370
- initialRomFile: loadedFiles.rom,
371
- initialBiosFile: loadedFiles.bios,
372
- romAccept,
373
- romAssetHint,
374
- biosAccept,
375
- biosAssetHint,
376
- requiresBIOS,
377
- showBIOS,
378
- messages,
379
- })
380
- .on("save:rom", handleSaveRom)
381
- .on("remove:rom", handleRemoveRom)
382
- .on("save:bios", handleSaveBios)
383
- .on("remove:bios", handleRemoveBios)
384
- .on("launch", (_event, payload) => bootWithFiles(payload))
385
- .mount(mainEl);
386
- };
387
-
388
- const bootWithFiles = async ({ rom, bios }) => {
389
- loadedFiles.rom = rom;
390
- loadedFiles.bios = bios;
391
-
392
- mainEl.classList?.add("running");
393
-
394
- const runtimeEl = $create("div", {
395
- className: "runtime",
396
- });
397
-
398
- mainEl.replaceChildren(runtimeEl);
399
-
400
- const primaryFile = rom || bios;
401
- console.log(`[Demo] Loaded file: ${primaryFile?.name} (${primaryFile?.size?.toLocaleString()} bytes)`);
402
-
403
- if (primaryFile) {
404
- toArrayBuffer(primaryFile).then((bytes) => {
405
- if (bytes) {
406
- computeChecksums(bytes).then((checksums) => {
407
- const checksumMap = checksums.reduce((acc, c) => ({ ...acc, [c.label]: c.value }), {});
408
- console.log(`[Demo] Checksums for ${primaryFile.name}:`, checksumMap);
409
- });
410
- }
411
- });
412
- }
413
-
414
- const eventPayload = {
415
- attachTo: runtimeEl,
416
- container: runtimeEl,
417
- target: runtimeEl,
418
- rom,
419
- bios,
420
- fileName: primaryFile?.name,
421
- byteLength: primaryFile?.size,
422
- files: { rom, bios },
423
- };
424
-
425
- const results = await demo.emit("launch", eventPayload);
426
- const fileResults = await demo.emit("file", { action: "launch", ...eventPayload });
427
-
428
- const allResults = [...results, ...fileResults];
429
- for (const res of allResults) {
430
- if (res && typeof res === "object" && (typeof res.start === "function" || typeof res.pause === "function" || typeof res.destroy === "function")) {
431
- currentInstance = res;
432
- break;
433
- }
434
- }
435
-
436
- if (escMenuConfig.enabled !== false) {
437
- let isMenuOpen = false;
438
- let escMenuMount = null;
439
-
440
- const escContainer = $create("div", { className: "esc-container" });
441
- runtimeEl.appendChild(escContainer);
442
-
443
- const optionGroups = escMenuConfig.optionGroups ||
444
- (escMenuConfig.options ? [{ id: "settings", label: "Settings", options: escMenuConfig.options }] : getDefaultOptionGroups());
445
-
446
- const exitToLauncher = () => {
447
- if (typeof window !== "undefined") {
448
- window.removeEventListener("keydown", handleKeyDown);
449
- }
450
- if (escMenuMount) {
451
- escMenuMount.destroy();
452
- escMenuMount = null;
453
- }
454
- if (currentInstance && typeof currentInstance.destroy === "function") {
455
- currentInstance.destroy();
456
- }
457
- currentInstance = null;
458
- renderLauncher();
459
- };
460
-
461
- const updateEscMenu = () => {
462
- if (escMenuMount) {
463
- escMenuMount.destroy();
464
- }
465
- escMenuMount = CEscMenu
466
- .renderShadow({
467
- open: isMenuOpen,
468
- title: escMenuConfig.title || config.messages?.escMenuTitle || messages.escMenuTitle,
469
- subtitle: primaryFile?.name || "Game",
470
- hasSaveStates: Boolean(config.manifest?.capabilities?.saveStates || escMenuConfig.onSaveState || currentInstance?.saveState),
471
- optionGroups,
472
- customItems: escMenuConfig.items || [],
473
- messages,
474
- })
475
- .mount(escContainer);
476
- };
477
-
478
- // Bound once, not per render. The $emit channel lives on the
479
- // Component79 instance and deliberately survives destroy() — that is
480
- // what lets it work while detached — so re-registering these inside
481
- // updateEscMenu() stacked a fresh set of listeners on every open,
482
- // close and restore-defaults. After N re-renders a single click
483
- // reached the handler N times, growing without bound.
484
- const bindEscMenuEvents = () => {
485
- CEscMenu
486
- .on("close", () => closeMenu())
487
- .on("reset", () => {
488
- closeMenu();
489
- demo.emit("reset", { instance: currentInstance });
490
- if (typeof escMenuConfig.onReset === "function") {
491
- escMenuConfig.onReset(currentInstance);
492
- } else if (currentInstance && typeof currentInstance.reset === "function") {
493
- currentInstance.reset();
494
- }
495
- })
496
- .on("restore-defaults", () => {
497
- const defaults = getDefaultOptionGroups();
498
- for (const group of optionGroups) {
499
- const defaultGroup = defaults.find((g) => g.id === group.id);
500
- if (!defaultGroup) continue;
501
- for (const opt of group.options) {
502
- const defaultOpt = defaultGroup.options.find((o) => o.key === opt.key);
503
- if (defaultOpt) {
504
- opt.value = defaultOpt.value;
505
- if (currentInstance && typeof currentInstance.setOption === "function") {
506
- currentInstance.setOption(opt.key, opt.value);
507
- }
508
- }
509
- }
510
- }
511
- updateEscMenu();
512
- demo.emit("restoreDefaults", { instance: currentInstance });
513
- if (typeof escMenuConfig.onRestoreDefaults === "function") {
514
- escMenuConfig.onRestoreDefaults(currentInstance);
515
- }
516
- })
517
- .on("option-change", (_event, { key, value, option }) => {
518
- if (currentInstance && typeof currentInstance.setOption === "function") {
519
- currentInstance.setOption(key, value);
520
- }
521
- demo.emit("option", { key, value, option, instance: currentInstance });
522
- if (typeof escMenuConfig.onOptionChange === "function") {
523
- escMenuConfig.onOptionChange(key, value, option, currentInstance);
524
- }
525
- })
526
- .on("save-state", () => {
527
- closeMenu();
528
- demo.emit("saveState", { instance: currentInstance });
529
- if (typeof escMenuConfig.onSaveState === "function") {
530
- escMenuConfig.onSaveState(currentInstance);
531
- } else if (currentInstance && typeof currentInstance.saveState === "function") {
532
- currentInstance.saveState();
533
- }
534
- })
535
- .on("load-state", () => {
536
- closeMenu();
537
- demo.emit("loadState", { instance: currentInstance });
538
- if (typeof escMenuConfig.onLoadState === "function") {
539
- escMenuConfig.onLoadState(currentInstance);
540
- } else if (currentInstance && typeof currentInstance.loadState === "function") {
541
- currentInstance.loadState();
542
- }
543
- })
544
- .on("exit", () => {
545
- demo.emit("exit", { instance: currentInstance });
546
- if (typeof escMenuConfig.onExit === "function") {
547
- escMenuConfig.onExit(currentInstance);
548
- }
549
- exitToLauncher();
550
- })
551
- .on("custom-action", (_event, item) => {
552
- closeMenu();
553
- demo.emit("customAction", { item, instance: currentInstance });
554
- if (typeof item?.action === "function") {
555
- item.action(currentInstance);
556
- }
557
- });
558
- };
559
-
560
- const openMenu = () => {
561
- if (isMenuOpen) return;
562
- isMenuOpen = true;
563
- demo.emit("pause", { instance: currentInstance });
564
- demo.emit("esc", { open: true, instance: currentInstance });
565
- if (currentInstance && typeof currentInstance.pause === "function") {
566
- currentInstance.pause();
567
- }
568
- updateEscMenu();
569
- };
570
-
571
- const closeMenu = () => {
572
- if (!isMenuOpen) return;
573
- isMenuOpen = false;
574
- demo.emit("resume", { instance: currentInstance });
575
- demo.emit("esc", { open: false, instance: currentInstance });
576
- if (currentInstance && typeof currentInstance.resume === "function") {
577
- currentInstance.resume();
578
- }
579
- updateEscMenu();
580
- };
581
-
582
- const toggleMenu = () => {
583
- if (isMenuOpen) closeMenu();
584
- else openMenu();
585
- };
586
-
587
- const shortcutKey = escMenuConfig.shortcutKey || "Escape";
588
- const handleKeyDown = (event) => {
589
- if (event.key === shortcutKey || event.code === shortcutKey) {
590
- event.preventDefault();
591
- toggleMenu();
592
- }
593
- };
594
-
595
- if (typeof window !== "undefined") {
596
- window.addEventListener("keydown", handleKeyDown);
597
- }
598
- bindEscMenuEvents();
599
- updateEscMenu();
600
- }
601
- };
602
-
603
- renderLauncher();
604
- };
605
-
606
- boot().catch((err) => {
607
- console.error("[Demo] Boot failed:", err);
608
- });
609
-
610
- return demo;
611
- },
612
- };
613
-
614
- // Automatic initialization fallback for legacy script tags (<script type="module" src="./demo.js"></script>)
615
- setTimeout(() => {
616
- if (!isInitialized && typeof document !== "undefined" && typeof window !== "undefined") {
617
- demo.init();
618
- }
619
- }, 0);
620
-
621
- export default demo;
622
- export { demo };
package/demo/sdk.js DELETED
@@ -1,19 +0,0 @@
1
- import { createDummySdk } from "./demo.sdk.js";
2
-
3
- /**
4
- * Resolves the Engine SDK to use for the demo.
5
- *
6
- * To use this template in another repository:
7
- * 1. Modify this file to import your engine's SDK:
8
- * import sdk from "../src/index.js"; // or "../dist/index.js"
9
- * export async function getSdk() { return sdk; }
10
- *
11
- * 2. Or attach window.SDK before demo.js boots:
12
- * window.SDK = myEngineSdk;
13
- */
14
- export async function getSdk() {
15
- if (typeof window !== "undefined" && window.SDK) {
16
- return window.SDK;
17
- }
18
- return createDummySdk();
19
- }