samengine 1.9.1 → 1.10.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/LICENSE +201 -0
- package/README.md +168 -0
- package/dist/config/buildconfig.d.ts +146 -0
- package/dist/config/buildconfig.js +115 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +1 -0
- package/dist/core.d.ts +17 -0
- package/dist/core.js +24 -0
- package/dist/html.d.ts +29 -0
- package/dist/html.js +20 -0
- package/dist/input.d.ts +51 -0
- package/dist/input.js +44 -3
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +6 -2
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +8 -1
- package/dist/nonbrowser/getversion.d.ts +13 -0
- package/dist/nonbrowser/getversion.js +35 -0
- package/dist/nonbrowser/ghresolver.d.ts +1 -0
- package/dist/nonbrowser/ghresolver.js +7 -0
- package/dist/nonbrowser/index.d.ts +9 -0
- package/dist/nonbrowser/index.js +9 -0
- package/dist/nonbrowser/internal/buildhelper.d.ts +42 -0
- package/dist/nonbrowser/internal/buildhelper.js +144 -0
- package/dist/nonbrowser/internal/cli/argparser.d.ts +18 -0
- package/dist/nonbrowser/internal/cli/argparser.js +36 -0
- package/dist/nonbrowser/internal/cli/main.d.ts +13 -0
- package/dist/nonbrowser/internal/cli/main.js +265 -0
- package/dist/nonbrowser/internal/config.d.ts +9 -0
- package/dist/nonbrowser/internal/config.js +40 -0
- package/dist/nonbrowser/internal/exporthtml.d.ts +37 -0
- package/dist/nonbrowser/internal/exporthtml.js +622 -0
- package/dist/nonbrowser/utils.d.ts +8 -0
- package/dist/nonbrowser/utils.js +18 -0
- package/dist/physics/collision.d.ts +33 -0
- package/dist/physics/collision.js +27 -0
- package/dist/physics/physicsEngine.d.ts +18 -0
- package/dist/physics/physicsEngine.js +18 -0
- package/dist/physics/physicsObject.d.ts +20 -0
- package/dist/physics/physicsObject.js +20 -0
- package/dist/renderer.d.ts +78 -0
- package/dist/renderer.js +72 -9
- package/dist/samegui/index.d.ts +29 -0
- package/dist/samegui/index.js +26 -0
- package/dist/save.d.ts +12 -0
- package/dist/save.js +10 -0
- package/dist/sound/audioplayer.d.ts +39 -0
- package/dist/sound/audioplayer.js +39 -5
- package/dist/storage/index.d.ts +40 -2
- package/dist/storage/index.js +34 -3
- package/dist/text/index.d.ts +14 -0
- package/dist/text/index.js +58 -0
- package/dist/texture.d.ts +100 -0
- package/dist/texture.js +75 -41
- package/dist/types/button.d.ts +25 -0
- package/dist/types/button.js +22 -0
- package/dist/types/circle.d.ts +26 -0
- package/dist/types/circle.js +21 -7
- package/dist/types/color.d.ts +17 -0
- package/dist/types/color.js +11 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/rectangle.d.ts +29 -0
- package/dist/types/rectangle.js +23 -7
- package/dist/types/triangle.d.ts +23 -0
- package/dist/types/triangle.js +20 -6
- package/dist/types/vector2d.d.ts +42 -0
- package/dist/types/vector2d.js +39 -11
- package/dist/types/vector3d.d.ts +38 -0
- package/dist/types/vector3d.js +35 -11
- package/dist/utils/index.d.ts +11 -4
- package/dist/utils/index.js +11 -4
- package/dist/utils/logger/index.d.ts +24 -0
- package/dist/utils/logger/index.js +44 -0
- package/dist/utils/math.d.ts +18 -0
- package/dist/utils/math.js +18 -4
- package/package.json +29 -11
- package/dist/utils/csv/index.d.ts +0 -3
- package/dist/utils/csv/index.js +0 -2
- package/dist/utils/csv/parser.d.ts +0 -25
- package/dist/utils/csv/parser.js +0 -212
- package/dist/utils/csv/stringifier.d.ts +0 -30
- package/dist/utils/csv/stringifier.js +0 -130
- package/dist/utils/csv/types.d.ts +0 -63
- package/dist/utils/csv/types.js +0 -1
- package/dist/utils/jsonc-parser.d.ts +0 -4
- package/dist/utils/jsonc-parser.js +0 -166
- package/dist/utils/markdown.d.ts +0 -41
- 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,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minifies generated HTML for release builds.
|
|
3
|
+
*
|
|
4
|
+
* The minifier also processes inline CSS and JavaScript, which matters for
|
|
5
|
+
* single-file builds because they can place the whole game shell into one
|
|
6
|
+
* `index.html`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function compressHTML(html: string): Promise<string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { minify } from "html-minifier-terser";
|
|
2
|
+
/**
|
|
3
|
+
* Minifies generated HTML for release builds.
|
|
4
|
+
*
|
|
5
|
+
* The minifier also processes inline CSS and JavaScript, which matters for
|
|
6
|
+
* single-file builds because they can place the whole game shell into one
|
|
7
|
+
* `index.html`.
|
|
8
|
+
*/
|
|
9
|
+
export async function compressHTML(html) {
|
|
10
|
+
return await minify(html, {
|
|
11
|
+
collapseWhitespace: true,
|
|
12
|
+
removeComments: true,
|
|
13
|
+
removeRedundantAttributes: true,
|
|
14
|
+
removeEmptyAttributes: true,
|
|
15
|
+
minifyCSS: true,
|
|
16
|
+
minifyJS: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Vector2d } from "../types/vector2d.js";
|
|
2
2
|
import { PhysicsObject } from "./physicsObject.js";
|
|
3
|
+
/**
|
|
4
|
+
* Collision shape used by `PhysicsObject`.
|
|
5
|
+
*
|
|
6
|
+
* Circle and box colliders are centered on `body.position`. Box width and height
|
|
7
|
+
* are full extents, not half extents.
|
|
8
|
+
*/
|
|
3
9
|
export type Collider = {
|
|
4
10
|
type: "circle";
|
|
5
11
|
radius: number;
|
|
@@ -8,17 +14,44 @@ export type Collider = {
|
|
|
8
14
|
width: number;
|
|
9
15
|
height: number;
|
|
10
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Tests two axis-aligned box colliders for overlap.
|
|
19
|
+
*
|
|
20
|
+
* @returns Collision normal and penetration depth, or `null` when separated.
|
|
21
|
+
*/
|
|
11
22
|
export declare function aabbCollision(a: PhysicsObject, b: PhysicsObject): {
|
|
12
23
|
normal: Vector2d;
|
|
13
24
|
penetration: number;
|
|
14
25
|
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* Tests two circle colliders for overlap.
|
|
28
|
+
*
|
|
29
|
+
* @returns Collision normal from `a` to `b` and penetration depth, or `null`.
|
|
30
|
+
*/
|
|
15
31
|
export declare function circleCollision(a: PhysicsObject, b: PhysicsObject): {
|
|
16
32
|
normal: Vector2d;
|
|
17
33
|
penetration: number;
|
|
18
34
|
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* Tests a circle collider against a box collider.
|
|
37
|
+
*
|
|
38
|
+
* The returned normal points from the closest box point toward the circle.
|
|
39
|
+
*/
|
|
19
40
|
export declare function circleBoxCollision(circleObj: PhysicsObject, boxObj: PhysicsObject): {
|
|
20
41
|
normal: Vector2d;
|
|
21
42
|
penetration: number;
|
|
22
43
|
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* Applies an impulse to two bodies so their velocities respond to a collision.
|
|
46
|
+
*
|
|
47
|
+
* Static bodies are treated as having inverse mass `0`, so only dynamic bodies
|
|
48
|
+
* receive velocity changes.
|
|
49
|
+
*/
|
|
23
50
|
export declare function resolveCollision(a: PhysicsObject, b: PhysicsObject, normal: Vector2d): void;
|
|
51
|
+
/**
|
|
52
|
+
* Moves overlapping bodies apart to remove visible penetration.
|
|
53
|
+
*
|
|
54
|
+
* The correction is distributed by inverse mass, so lighter dynamic bodies move
|
|
55
|
+
* more than heavier ones and static bodies do not move.
|
|
56
|
+
*/
|
|
24
57
|
export declare function positionalCorrection(a: PhysicsObject, b: PhysicsObject, normal: Vector2d, penetration: number): void;
|