samengine 1.9.1 → 1.10.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.
Files changed (93) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +168 -0
  3. package/dist/config/buildconfig.d.ts +146 -0
  4. package/dist/config/buildconfig.js +115 -0
  5. package/dist/config/index.d.ts +9 -0
  6. package/dist/config/index.js +1 -0
  7. package/dist/core.d.ts +17 -0
  8. package/dist/core.js +24 -0
  9. package/dist/html.d.ts +29 -0
  10. package/dist/html.js +20 -0
  11. package/dist/input.d.ts +51 -0
  12. package/dist/input.js +44 -3
  13. package/dist/keys.d.ts +6 -0
  14. package/dist/keys.js +6 -2
  15. package/dist/logger.d.ts +8 -0
  16. package/dist/logger.js +8 -1
  17. package/dist/nonbrowser/getversion.d.ts +13 -0
  18. package/dist/nonbrowser/getversion.js +35 -0
  19. package/dist/nonbrowser/ghresolver.d.ts +1 -0
  20. package/dist/nonbrowser/ghresolver.js +7 -0
  21. package/dist/nonbrowser/index.d.ts +9 -0
  22. package/dist/nonbrowser/index.js +9 -0
  23. package/dist/nonbrowser/internal/buildhelper.d.ts +42 -0
  24. package/dist/nonbrowser/internal/buildhelper.js +144 -0
  25. package/dist/nonbrowser/internal/cli/argparser.d.ts +20 -0
  26. package/dist/nonbrowser/internal/cli/argparser.js +36 -0
  27. package/dist/nonbrowser/internal/cli/main.d.ts +13 -0
  28. package/dist/nonbrowser/internal/cli/main.js +262 -0
  29. package/dist/nonbrowser/internal/config.d.ts +9 -0
  30. package/dist/nonbrowser/internal/config.js +40 -0
  31. package/dist/nonbrowser/internal/exporthtml.d.ts +37 -0
  32. package/dist/nonbrowser/internal/exporthtml.js +622 -0
  33. package/dist/nonbrowser/internal/projcreator/downloadZip.d.ts +4 -0
  34. package/dist/nonbrowser/internal/projcreator/downloadZip.js +83 -0
  35. package/dist/nonbrowser/internal/projcreator/main.d.ts +1 -0
  36. package/dist/nonbrowser/internal/projcreator/main.js +81 -0
  37. package/dist/nonbrowser/utils.d.ts +8 -0
  38. package/dist/nonbrowser/utils.js +18 -0
  39. package/dist/physics/collision.d.ts +33 -0
  40. package/dist/physics/collision.js +27 -0
  41. package/dist/physics/physicsEngine.d.ts +18 -0
  42. package/dist/physics/physicsEngine.js +18 -0
  43. package/dist/physics/physicsObject.d.ts +20 -0
  44. package/dist/physics/physicsObject.js +20 -0
  45. package/dist/renderer.d.ts +78 -0
  46. package/dist/renderer.js +72 -9
  47. package/dist/samegui/index.d.ts +29 -0
  48. package/dist/samegui/index.js +26 -0
  49. package/dist/save.d.ts +12 -0
  50. package/dist/save.js +10 -0
  51. package/dist/sound/audioplayer.d.ts +39 -0
  52. package/dist/sound/audioplayer.js +39 -5
  53. package/dist/storage/index.d.ts +40 -2
  54. package/dist/storage/index.js +34 -3
  55. package/dist/text/index.d.ts +14 -0
  56. package/dist/text/index.js +58 -0
  57. package/dist/texture.d.ts +100 -0
  58. package/dist/texture.js +75 -41
  59. package/dist/types/button.d.ts +25 -0
  60. package/dist/types/button.js +22 -0
  61. package/dist/types/circle.d.ts +26 -0
  62. package/dist/types/circle.js +21 -7
  63. package/dist/types/color.d.ts +17 -0
  64. package/dist/types/color.js +11 -1
  65. package/dist/types/index.d.ts +1 -1
  66. package/dist/types/index.js +1 -1
  67. package/dist/types/rectangle.d.ts +29 -0
  68. package/dist/types/rectangle.js +23 -7
  69. package/dist/types/triangle.d.ts +23 -0
  70. package/dist/types/triangle.js +20 -6
  71. package/dist/types/vector2d.d.ts +42 -0
  72. package/dist/types/vector2d.js +39 -11
  73. package/dist/types/vector3d.d.ts +38 -0
  74. package/dist/types/vector3d.js +35 -11
  75. package/dist/utils/index.d.ts +11 -4
  76. package/dist/utils/index.js +11 -4
  77. package/dist/utils/logger/index.d.ts +24 -0
  78. package/dist/utils/logger/index.js +44 -0
  79. package/dist/utils/math.d.ts +18 -0
  80. package/dist/utils/math.js +18 -4
  81. package/package.json +36 -11
  82. package/dist/utils/csv/index.d.ts +0 -3
  83. package/dist/utils/csv/index.js +0 -2
  84. package/dist/utils/csv/parser.d.ts +0 -25
  85. package/dist/utils/csv/parser.js +0 -212
  86. package/dist/utils/csv/stringifier.d.ts +0 -30
  87. package/dist/utils/csv/stringifier.js +0 -130
  88. package/dist/utils/csv/types.d.ts +0 -63
  89. package/dist/utils/csv/types.js +0 -1
  90. package/dist/utils/jsonc-parser.d.ts +0 -4
  91. package/dist/utils/jsonc-parser.js +0 -166
  92. package/dist/utils/markdown.d.ts +0 -41
  93. package/dist/utils/markdown.js +0 -699
@@ -0,0 +1,622 @@
1
+ import { parseMarkdown } from "samengine-cli";
2
+ import { getPackageVersion } from "../getversion.js";
3
+ /**
4
+ * Function to get the samengine Version
5
+ *
6
+ * @deprecated WARN Function should not be used outside samengine. Use getPackageVersion instead!
7
+ */
8
+ export function getVersion() {
9
+ return getPackageVersion("samengine");
10
+ }
11
+ /** Builds the start screen shown before the game code runs. */
12
+ function getStartScreen(config) {
13
+ return `<div id="startscreen">
14
+ <h2>made with samengine</h2>
15
+ <h1>${config.title}</h1>
16
+ <p>${config.version}</p>
17
+ <p>by ${config.gameauthor}</p>
18
+
19
+ <button class="startbutton" id="startBtn">${config.htmlMenu.text.startbutton}</button>
20
+
21
+ <p>${config.description}</p>
22
+ </div>`;
23
+ }
24
+ /**
25
+ * Returns optional JavaScript that unlocks browser audio after a user click.
26
+ *
27
+ * Most browsers block audio playback until the page receives a user gesture.
28
+ * Running this inside the start-button handler gives the game an AudioContext it
29
+ * can reuse through `window.__audioCtx`.
30
+ */
31
+ function getAudioCode(c) {
32
+ if (c.enable_audio == false) {
33
+ return "";
34
+ }
35
+ return `// Unlock browser audio after the start click.
36
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
37
+
38
+ const ctx = new AudioContext();
39
+ await ctx.resume();
40
+
41
+ // Expose the shared audio context for the game runtime.
42
+ window.__audioCtx = ctx;`;
43
+ }
44
+ /** Creates the base CSS for the generated page, start screen, and start button. */
45
+ function getStandardCSS(config) {
46
+ return `* {
47
+ margin: 0;
48
+ padding: 0;
49
+ box-sizing: border-box;
50
+ }
51
+ body {
52
+ margin: 0;
53
+ background: ${config.htmlMenu.style.bgcolor};
54
+ color: ${config.htmlMenu.style.color};
55
+ font-family: sans-serif;
56
+ display: flex;
57
+ justify-content: center;
58
+ align-items: center;
59
+ height: 100vh;
60
+ overflow: hidden; /* Prevent scrollbars around fullscreen game content. */
61
+ }
62
+
63
+ #startscreen {
64
+ text-align: center;
65
+ height: 50vh;
66
+ }
67
+
68
+ h1 {
69
+ font-size: 3rem;
70
+ margin-bottom: 0.5rem;
71
+ }
72
+
73
+ h2 {
74
+ font-weight: normal;
75
+ opacity: 0.7;
76
+ }
77
+
78
+ .startbutton {
79
+ margin: 1.3rem 0;
80
+ padding: 1rem 2rem;
81
+ font-size: 1.2rem;
82
+ background: ${config.htmlMenu.style.startbutton_bgcolor};
83
+ border: none;
84
+ border-radius: 8px;
85
+ cursor: pointer;
86
+ }
87
+
88
+ .startbutton:hover {
89
+ background: ${config.htmlMenu.style.startbutton_bgc_hover};
90
+ }`;
91
+ }
92
+ /**
93
+ * Converts configured Markdown notes into collapsible `<details>` sections.
94
+ *
95
+ * Each note can provide CSS variables for its own text and background color.
96
+ * The Markdown itself is rendered through `samengine/utils`.
97
+ */
98
+ function getMDNotes(config) {
99
+ let mdnotes_str = "";
100
+ if (config.markdown_notes.length > 0) {
101
+ mdnotes_str += '<div id="mdnotes">';
102
+ for (let i = 0; i < config.markdown_notes.length; i++) {
103
+ const note = config.markdown_notes[i];
104
+ let vars = "";
105
+ if (note.style?.bg) {
106
+ vars += `--note-bg:${note.style.bg};`;
107
+ }
108
+ if (note.style?.color) {
109
+ vars += `--note-color:${note.style.color};`;
110
+ }
111
+ mdnotes_str += `
112
+ <details style="${vars}">
113
+ <summary>${note.title}</summary>
114
+ ${parseMarkdown(note.content)}
115
+ </details>`;
116
+ }
117
+ mdnotes_str += "</div>";
118
+ mdnotes_str += `
119
+ <style>
120
+ /* Markdown notes container */
121
+ #mdnotes {
122
+ position: absolute;
123
+ bottom: 0;
124
+ left: 10px;
125
+ max-width: 400px;
126
+ max-height: 40vh;
127
+ overflow-y: auto;
128
+ z-index: 900;
129
+ font-size: 0.9rem;
130
+ width: 60%;
131
+ }
132
+
133
+ /* Single note */
134
+ #mdnotes details {
135
+ --note-bg: rgba(15,23,42,0.85);
136
+ --note-color: #e2e8f0;
137
+
138
+ background: var(--note-bg);
139
+ color: var(--note-color);
140
+
141
+ border: 1px solid rgba(255,255,255,0.1);
142
+ border-radius: 8px;
143
+ margin-bottom: 8px;
144
+ padding: 8px 10px;
145
+ backdrop-filter: blur(6px);
146
+ }
147
+
148
+ /* Note title */
149
+ #mdnotes summary {
150
+ cursor: pointer;
151
+ font-weight: bold;
152
+ color: inherit;
153
+ list-style: none;
154
+ }
155
+
156
+ /* Hide the browser default marker for a cleaner custom toggle. */
157
+ #mdnotes summary::-webkit-details-marker {
158
+ display: none;
159
+ }
160
+
161
+ /* Custom toggle marker */
162
+ #mdnotes summary::before {
163
+ content: "▶";
164
+ display: inline-block;
165
+ margin-right: 6px;
166
+ transition: transform 0.2s ease;
167
+ }
168
+
169
+ /* Rotate the marker when the note is open. */
170
+ #mdnotes details[open] summary::before {
171
+ transform: rotate(90deg);
172
+ }
173
+
174
+ /* Markdown content */
175
+ #mdnotes details p {
176
+ margin: 8px 0;
177
+ line-height: 1.4;
178
+ color: #e2e8f0;
179
+ }
180
+
181
+ #mdnotes details h1,
182
+ #mdnotes details h2,
183
+ #mdnotes details h3 {
184
+ margin-top: 10px;
185
+ margin-bottom: 5px;
186
+ color: #f8fafc;
187
+ }
188
+
189
+ #mdnotes details code {
190
+ background: rgba(0,0,0,0.4);
191
+ padding: 2px 4px;
192
+ border-radius: 4px;
193
+ font-family: monospace;
194
+ }
195
+
196
+ #mdnotes details pre {
197
+ background: rgba(0,0,0,0.5);
198
+ padding: 8px;
199
+ border-radius: 6px;
200
+ overflow-x: auto;
201
+ }
202
+
203
+ #mdnotes details a {
204
+ color: #22c55e;
205
+ text-decoration: none;
206
+ }
207
+
208
+ #mdnotes details a:hover {
209
+ text-decoration: underline;
210
+ }
211
+ </style>`;
212
+ }
213
+ return mdnotes_str;
214
+ }
215
+ /** Creates CSS for the optional fullscreen button. */
216
+ function getFullscreenButton(config) {
217
+ let fullscreenbutton = "";
218
+ if (config.show_fullscreen_button) {
219
+ fullscreenbutton = `#fullscreenBtn {
220
+ position: fixed;
221
+ top: 10px;
222
+ right: 10px;
223
+
224
+ padding: 10px 15px;
225
+ font-size: 16px;
226
+
227
+ background: rgba(0, 0, 0, 0.6);
228
+ color: white;
229
+ border: none;
230
+ border-radius: 6px;
231
+
232
+ cursor: pointer;
233
+ z-index: 1000;
234
+ }
235
+
236
+ #fullscreenBtn:hover {
237
+ background: rgba(0, 0, 0, 0.8);
238
+ }`;
239
+ }
240
+ return fullscreenbutton;
241
+ }
242
+ /** Creates HTML for the optional fullscreen button. */
243
+ function getFullscreenButtonHTML(config) {
244
+ let fullscreenBtn = "";
245
+ if (config.show_fullscreen_button) {
246
+ fullscreenBtn = `<!-- Button to make it fullscreen -->
247
+ <button id="fullscreenBtn">⛶ Fullscreen</button>`;
248
+ }
249
+ return fullscreenBtn;
250
+ }
251
+ /** Builds the optional settings popup from `config.htmlMenu.settings`. */
252
+ function getSettingsButton(config) {
253
+ if (!config.htmlMenu.enable_menu) {
254
+ return "";
255
+ }
256
+ let settingsHTML = "";
257
+ for (const setting of config.htmlMenu.settings) {
258
+ settingsHTML += `
259
+ <div class="settingGroup">
260
+
261
+ <p>${setting.title}</p>
262
+ `;
263
+ for (const option of setting.options) {
264
+ settingsHTML += `
265
+ <button
266
+ class="settingBtn ${option.value === setting.default_value ? "active" : ""}"
267
+ data-setting="${setting.id}"
268
+ data-value="${option.value}"
269
+ >
270
+ ${option.text}
271
+ </button>
272
+ `;
273
+ }
274
+ settingsHTML += `
275
+ </div>
276
+ `;
277
+ }
278
+ return `
279
+ <div id="settingsPopup">
280
+
281
+ <div id="settingsWindow">
282
+
283
+ <h2>Settings</h2>
284
+
285
+ ${settingsHTML}
286
+
287
+ </div>
288
+
289
+ </div>
290
+
291
+ <button id="settingsBtn">⚙</button>
292
+ `;
293
+ }
294
+ /** Creates CSS for the settings button, modal popup, and option buttons. */
295
+ function getSettingsButtonCSS(config) {
296
+ if (!config.htmlMenu.enable_menu) {
297
+ return "";
298
+ }
299
+ return `
300
+ #settingsBtn {
301
+ position: fixed;
302
+ right: 20px;
303
+ bottom: 20px;
304
+
305
+ width: 40px;
306
+ height: 40px;
307
+
308
+ border: none;
309
+ border-radius: 12px;
310
+
311
+ font-size: 20px;
312
+
313
+ background: rgba(0,0,0,0.7);
314
+ color: white;
315
+
316
+ cursor: pointer;
317
+ z-index: 2000;
318
+ }
319
+
320
+ #settingsPopup {
321
+ position: fixed;
322
+ inset: 0;
323
+
324
+ background: ${config.htmlMenu.style.settingsmenu_popup_bgcolor};
325
+
326
+ display: none;
327
+
328
+ justify-content: center;
329
+ align-items: center;
330
+
331
+ z-index: 1999;
332
+ }
333
+
334
+ #settingsWindow {
335
+ width: 500px;
336
+ max-width: 90%;
337
+
338
+ background: ${config.htmlMenu.style.settingsmenu_bgcolor};
339
+
340
+ padding: 30px;
341
+
342
+ border-radius: 16px;
343
+
344
+ color: white;
345
+ }
346
+
347
+ .settingGroup {
348
+ margin-top: 20px;
349
+ }
350
+
351
+ .settingGroup p {
352
+ margin-bottom: 10px;
353
+ font-size: 18px;
354
+ }
355
+
356
+ .settingBtn {
357
+ padding: 10px 16px;
358
+
359
+ border: none;
360
+ border-radius: 8px;
361
+
362
+ background: ${config.htmlMenu.style.settingsmenu_button};
363
+ color: ${config.htmlMenu.style.settingsmenu_button_txt};
364
+
365
+ /* Button text color on hover */
366
+ ${config.htmlMenu.style.settingsmenu_button_txt_hover.length != 0 ? "color: " + config.htmlMenu.style.settingsmenu_button_txt_hover + ";" : ""}
367
+
368
+ /* Button hover color */
369
+ ${config.htmlMenu.style.settingsmenu_button_hover.length != 0 ? "color: " + config.htmlMenu.style.settingsmenu_button_hover + ";" : ""}
370
+
371
+ cursor: pointer;
372
+
373
+ margin-right: 10px;
374
+ margin-top: 10px;
375
+ }
376
+
377
+ .settingBtn.active {
378
+ background: ${config.htmlMenu.style.settingsmenu_button_clicked};
379
+ color: black;
380
+ }
381
+ `;
382
+ }
383
+ /**
384
+ * Creates runtime JavaScript for the optional settings menu.
385
+ *
386
+ * Default setting values are written to `window.__GAMESETTINGS__`. When the
387
+ * player clicks an option, the active button state and the global settings
388
+ * object are updated together.
389
+ */
390
+ function getSettingsButtonJS(config) {
391
+ if (!config.htmlMenu.enable_menu) {
392
+ return "";
393
+ }
394
+ const defaultSettings = {};
395
+ for (const setting of config.htmlMenu.settings) {
396
+ defaultSettings[setting.id] = setting.default_value;
397
+ }
398
+ return `
399
+ <script>
400
+
401
+ window.__GAMESETTINGS__ = ${JSON.stringify(defaultSettings, null, 4)};
402
+
403
+ const settingsBtn = document.getElementById("settingsBtn");
404
+ const settingsPopup = document.getElementById("settingsPopup");
405
+
406
+ settingsBtn.addEventListener("click", () => {
407
+
408
+ if (settingsPopup.style.display === "flex") {
409
+ settingsPopup.style.display = "none";
410
+ } else {
411
+ settingsPopup.style.display = "flex";
412
+ }
413
+
414
+ });
415
+
416
+ settingsPopup.addEventListener("click", (e) => {
417
+
418
+ if (e.target === settingsPopup) {
419
+ settingsPopup.style.display = "none";
420
+ }
421
+
422
+ });
423
+
424
+ document.querySelectorAll(".settingBtn").forEach(btn => {
425
+
426
+ btn.addEventListener("click", () => {
427
+
428
+ const setting = btn.dataset.setting;
429
+ const value = btn.dataset.value;
430
+
431
+ document.querySelectorAll(
432
+ '.settingBtn[data-setting="' + setting + '"]'
433
+ ).forEach(b => {
434
+ b.classList.remove("active");
435
+ });
436
+
437
+ btn.classList.add("active");
438
+
439
+ window.__GAMESETTINGS__[setting] = value;
440
+
441
+ console.log(window.__GAMESETTINGS__);
442
+
443
+ });
444
+
445
+ });
446
+
447
+ </script>
448
+ `;
449
+ }
450
+ /** Removes settings UI after the game starts so it does not overlap the canvas. */
451
+ function getSettingsButtonJSrem(config) {
452
+ if (!config.htmlMenu.enable_menu) {
453
+ return "";
454
+ }
455
+ return `// Remove the settings button after the game starts.
456
+ document.getElementById("settingsBtn").remove();
457
+
458
+ // Remove the settings popup after the game starts.
459
+ document.getElementById("settingsPopup").remove();
460
+ `;
461
+ }
462
+ /**
463
+ * Creates a complete single-file HTML document.
464
+ *
465
+ * `bundledJsContent` is the already bundled game code from esbuild.
466
+ * `resourcesMap` contains optional Data URIs that are exposed through
467
+ * `window.__resources`, `window.__getResource`, and `window.__loadResource`.
468
+ *
469
+ * The bundled game is wrapped in `window.__initializeGame` so it does not run
470
+ * until the player clicks the start button. This allows the generated page to
471
+ * show the start screen, unlock audio, and remove temporary UI first.
472
+ */
473
+ export function GetSingleFileHTML(config, bundledJsContent, resourcesMap = {}) {
474
+ let frameworkVersion = getVersion();
475
+ // Embed resource lookup helpers for games that need assets in single-file builds.
476
+ const resourceLoaderScript = `window.__resources = ${JSON.stringify(resourcesMap)};
477
+ window.__getResource = function(path) {
478
+ return window.__resources[path] || null;
479
+ };
480
+ window.__loadResource = function(path) {
481
+ const resource = window.__getResource(path);
482
+ if (!resource) {
483
+ console.warn('Resource not found:', path);
484
+ return null;
485
+ }
486
+ return resource;
487
+ };
488
+ window.__samengine__ = {
489
+ version: "${frameworkVersion}"
490
+ };`;
491
+ // Wrap bundled JS in a function to prevent auto-execution before the start click.
492
+ const wrappedGameCode = `function __initializeGame() {
493
+ ${bundledJsContent.split('\n').map(line => ' ' + line).join('\n')}
494
+ }`;
495
+ const defaulthtml = `<!DOCTYPE html>
496
+ <html>
497
+ <head>
498
+ <meta charset="UTF-8" />
499
+ <title>${config.title}</title>
500
+ <!-- Mobile viewport setup -->
501
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
502
+ ${config.htmlhead}
503
+ <style>
504
+ ${getStandardCSS(config)}
505
+
506
+ ${getFullscreenButton(config)}
507
+
508
+ ${getSettingsButtonCSS(config)}
509
+
510
+ </style>
511
+ </head>
512
+ <body>
513
+ ${getStartScreen(config)}
514
+
515
+ ${getMDNotes(config)}
516
+
517
+ ${getSettingsButton(config)}
518
+
519
+ <script>
520
+ ${resourceLoaderScript}
521
+ </script>
522
+ <script>
523
+ ${wrappedGameCode}
524
+ </script>
525
+ <script type="module">
526
+ const btn = document.getElementById("startBtn");
527
+
528
+ btn.addEventListener("click", async () => {
529
+ ${getAudioCode(config)}
530
+
531
+ // Remove the start screen.
532
+ document.getElementById("startscreen").remove();
533
+
534
+ ${getSettingsButtonJSrem(config)}
535
+
536
+ // Only when there are Markdown notes.
537
+ ${config.markdown_notes.length > 0 ? `
538
+ // Remove Markdown notes.
539
+ document.getElementById("mdnotes").remove();
540
+ ` : ""}
541
+
542
+ // Initialize the game.
543
+ window.__initializeGame();
544
+ });
545
+ </script>
546
+
547
+ ${getSettingsButtonJS(config)}
548
+
549
+ ${getFullscreenButtonHTML(config)}
550
+
551
+ </body>
552
+ </html>
553
+ `;
554
+ return defaulthtml;
555
+ }
556
+ /**
557
+ * Creates the normal multi-file HTML document.
558
+ *
559
+ * This page contains the generated start screen and optional menu UI. The game
560
+ * bundle is loaded with a dynamic import only after the start button is clicked.
561
+ */
562
+ export function GetDefaultHTML(config, releasemode) {
563
+ let frameworkVersion = getVersion();
564
+ const defaulthtml = `<!DOCTYPE html>
565
+ <html>
566
+ <head>
567
+ <meta charset="UTF-8" />
568
+ <title>${config.title}</title>
569
+ <!-- Mobile viewport setup -->
570
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
571
+ ${config.htmlhead}
572
+ <style>
573
+ ${getStandardCSS(config)}
574
+
575
+ ${getFullscreenButton(config)}
576
+
577
+ ${getSettingsButtonCSS(config)}
578
+
579
+ </style>
580
+ </head>
581
+ <body>
582
+ ${getStartScreen(config)}
583
+
584
+ ${getMDNotes(config)}
585
+
586
+ ${getSettingsButton(config)}
587
+
588
+ <script type="module">
589
+ const btn = document.getElementById("startBtn");
590
+
591
+ btn.addEventListener("click", async () => {
592
+ ${getAudioCode(config)}
593
+
594
+ // Remove the start screen.
595
+ document.getElementById("startscreen").remove();
596
+
597
+ ${getSettingsButtonJSrem(config)}
598
+
599
+ // Only when there are Markdown notes.
600
+ ${config.markdown_notes.length > 0 ? `
601
+ // Remove Markdown notes.
602
+ document.getElementById("mdnotes").remove();
603
+ ` : ""}
604
+
605
+ // Load the game bundle.
606
+ import("./${config.entryname}.js");
607
+ });
608
+
609
+ window.__samengine__ = {
610
+ version: "${frameworkVersion}"
611
+ };
612
+ </script>
613
+
614
+ ${getSettingsButtonJS(config)}
615
+
616
+ ${getFullscreenButtonHTML(config)}
617
+
618
+ </body>
619
+ </html>
620
+ `;
621
+ return defaulthtml;
622
+ }
@@ -0,0 +1,4 @@
1
+ export declare function downloadAndExtract(url: string, targetDir: string): Promise<void>;
2
+ export declare function flattenGitHubZip(rootDir: string): Promise<void>;
3
+ export declare function keepOnlySelectedVersion(rootDir: string, selectedVersion: string, selectedStarter: string): Promise<void>;
4
+ export declare function finalizeProject(rootDir: string, version: string, starter: string): Promise<void>;
@@ -0,0 +1,83 @@
1
+ import fetch from "node-fetch";
2
+ import fs from "fs-extra";
3
+ import unzipper from "unzipper";
4
+ import path from "path";
5
+ export async function downloadAndExtract(url, targetDir) {
6
+ await fs.ensureDir(targetDir);
7
+ const res = await fetch(url);
8
+ if (!res.ok || !res.body) {
9
+ throw new Error("Failed to download template");
10
+ }
11
+ const body = res.body;
12
+ const zipPath = path.join(targetDir, "template.zip");
13
+ const fileStream = fs.createWriteStream(zipPath);
14
+ await new Promise((resolve, reject) => {
15
+ body.pipe(fileStream);
16
+ body.on("error", reject);
17
+ fileStream.on("finish", resolve);
18
+ });
19
+ await fs
20
+ .createReadStream(zipPath)
21
+ .pipe(unzipper.Extract({ path: targetDir }))
22
+ .promise();
23
+ await fs.remove(zipPath);
24
+ // flatten GitHub folder
25
+ const items = await fs.readdir(targetDir);
26
+ const rootFolder = items.find(f => f.includes("game-template"));
27
+ if (rootFolder) {
28
+ const rootPath = path.join(targetDir, rootFolder);
29
+ const files = await fs.readdir(rootPath);
30
+ for (const file of files) {
31
+ await fs.move(path.join(rootPath, file), path.join(targetDir, file), { overwrite: true });
32
+ }
33
+ await fs.remove(rootPath);
34
+ }
35
+ }
36
+ export async function flattenGitHubZip(rootDir) {
37
+ const items = await fs.readdir(rootDir);
38
+ // finde GitHub wrapper folder
39
+ const wrapper = items.find(name => name.includes("-main") ||
40
+ name.includes("-master") ||
41
+ name.includes("-"));
42
+ if (!wrapper)
43
+ return;
44
+ const wrapperPath = path.join(rootDir, wrapper);
45
+ const inner = await fs.readdir(wrapperPath);
46
+ for (const file of inner) {
47
+ await fs.move(path.join(wrapperPath, file), path.join(rootDir, file), { overwrite: true });
48
+ }
49
+ await fs.remove(wrapperPath);
50
+ }
51
+ export async function keepOnlySelectedVersion(rootDir, selectedVersion, selectedStarter) {
52
+ const versions = await fs.readdir(rootDir);
53
+ for (const version of versions) {
54
+ const versionPath = path.join(rootDir, version);
55
+ const stat = await fs.stat(versionPath);
56
+ if (!stat.isDirectory())
57
+ continue;
58
+ if (version !== selectedVersion) {
59
+ await fs.remove(versionPath);
60
+ continue;
61
+ }
62
+ // inside selected version → handle starters
63
+ const starters = await fs.readdir(versionPath);
64
+ for (const starter of starters) {
65
+ const starterPath = path.join(versionPath, starter);
66
+ const stat2 = await fs.stat(starterPath);
67
+ if (!stat2.isDirectory())
68
+ continue;
69
+ if (starter !== selectedStarter) {
70
+ await fs.remove(starterPath);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ export async function finalizeProject(rootDir, version, starter) {
76
+ const starterPath = path.join(rootDir, version, starter);
77
+ const files = await fs.readdir(starterPath);
78
+ for (const file of files) {
79
+ await fs.move(path.join(starterPath, file), path.join(rootDir, file), { overwrite: true });
80
+ }
81
+ // cleanup EVERYTHING else
82
+ await fs.remove(path.join(rootDir, version));
83
+ }
@@ -0,0 +1 @@
1
+ export declare function run(): Promise<void>;