littlejsengine 1.19.3 → 1.21.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 (54) hide show
  1. package/FAQ.md +841 -820
  2. package/README.md +29 -25
  3. package/dist/littlejs.d.ts +2857 -955
  4. package/dist/littlejs.esm.js +33428 -20995
  5. package/dist/littlejs.esm.min.js +4 -3
  6. package/dist/littlejs.js +33386 -20996
  7. package/dist/littlejs.min.js +2 -2
  8. package/dist/littlejs.release.js +27841 -20422
  9. package/package.json +10 -5
  10. package/plugins/audioEffects.js +28 -24
  11. package/plugins/box2d.js +2516 -2073
  12. package/plugins/drawUtilities.js +206 -31
  13. package/plugins/gltf.js +786 -0
  14. package/plugins/jsconfig.json +19 -0
  15. package/plugins/lightSystem.js +719 -332
  16. package/plugins/math3d.js +148 -27
  17. package/plugins/medalSystem.js +326 -280
  18. package/plugins/newgrounds.js +400 -199
  19. package/plugins/pathFinder.js +208 -175
  20. package/plugins/pluginExport.js +25 -0
  21. package/plugins/postProcess.js +96 -24
  22. package/plugins/render3d.js +2067 -1632
  23. package/plugins/render3dDebug.js +110 -0
  24. package/plugins/render3dEditor.js +1812 -0
  25. package/plugins/render3dExtras.js +1948 -0
  26. package/plugins/render3dLevel.js +189 -0
  27. package/plugins/render3dVoxels.js +514 -0
  28. package/plugins/sceneSystem.js +58 -0
  29. package/plugins/textureSheet.js +496 -458
  30. package/plugins/threejs.js +183 -163
  31. package/plugins/tweakables.js +411 -0
  32. package/plugins/tweenSystem.js +265 -145
  33. package/plugins/uiSystem.js +2024 -1687
  34. package/plugins/zzfxm.js +159 -159
  35. package/src/engine.js +378 -97
  36. package/src/engineAudio.js +231 -127
  37. package/src/engineBuild.mjs +292 -256
  38. package/src/engineDebug.js +967 -743
  39. package/src/engineDraw.js +391 -198
  40. package/src/engineEditor.js +2452 -0
  41. package/src/engineExport.js +18 -0
  42. package/src/engineInput.js +1466 -1293
  43. package/src/engineLogo.js +19 -0
  44. package/src/engineMath.js +1274 -1252
  45. package/src/engineObject.js +197 -82
  46. package/src/engineParticles.js +583 -532
  47. package/src/engineRelease.js +79 -45
  48. package/src/engineSettings.js +115 -29
  49. package/src/engineTileLayer.js +995 -741
  50. package/src/engineUtilities.js +19 -13
  51. package/src/engineWebGL.js +239 -82
  52. package/src/jsconfig.json +17 -13
  53. package/plugins/box2d.wasm.js +0 -630
  54. package/plugins/box2d.wasm.wasm +0 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * LittleJS Tweakables Plugin
3
+ * - Change values live from a panel while the game runs, for tuning without a reload
4
+ * - tweak('name') adds a global, or a field of an object with the object option, a number gets a slider when it
5
+ * has a range, a boolean a checkbox, a Color a color picker with an alpha slider, a Vector2 or Vector3 a number
6
+ * for each axis
7
+ * - tweakButton adds a button that calls a function, like one to restart the level
8
+ * - Press 9 while the debug overlay is open to show the panel, or set debugTweakables to show it from the start
9
+ * - Changes are saved and come back after a refresh, until the value in the code itself changes
10
+ * - Copy puts the changed values on the clipboard as lines of code, to paste over the values in the code
11
+ * - Debug builds only, the release build has none of this code, its functions are stubs that do nothing, so the
12
+ * code's values are used as they are
13
+ * @namespace Tweakables
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ ///////////////////////////////////////////////////////////////////////////////
19
+
20
+ // the tweaks and dividers in the order they were added
21
+ const tweakList = [];
22
+
23
+ // the saved values by name, each with the code value it was saved over, read when the first tweak is added
24
+ let tweakSaved;
25
+
26
+ // the panel, made the first time it is shown, and if its rows need to be made again
27
+ let tweakPanel, tweakRows, tweakRowsDirty = true;
28
+
29
+ // a global's name, or a dotted path to a field on one
30
+ const tweakPathRegex = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)*$/;
31
+
32
+ /** Options for a tweak
33
+ * @typedef {Object} TweakOptions
34
+ * @property {number} [min] - Lowest value on the slider, the slider shows when min and max are both set
35
+ * @property {number} [max] - Highest value on the slider
36
+ * @property {number} [step] - Step for the slider and number box, a thousandth of the range by default
37
+ * @property {string} [label] - Name to show in place of the path, and the name it is saved by with an object
38
+ * @property {Object} [object] - Object the path is a field of, in place of a global, for an ES module game
39
+ * @memberof Tweakables */
40
+
41
+ /** Add a value to the tweakables panel, so it can be changed while the game runs
42
+ * - The path is the name of a global, or a dotted path to a field on one like 'player.speed'
43
+ * - Only globals of a script can be found by name, an ES module game passes the object its values are on
44
+ * - Its type comes from the value it has now: a number, boolean, Color, Vector2 or Vector3
45
+ * - Call it after the value is set, at the end of gameInit
46
+ * - Adding the same path again keeps its row and brings its tweaked value back, for an object made again;
47
+ * with the object option it is known by its label, so two objects with the same field need their own labels
48
+ * - Does nothing in release builds
49
+ * @param {string} path - Name of a global, or a dotted path to a field on one or on the object option
50
+ * @param {TweakOptions} [options]
51
+ * @memberof Tweakables
52
+ * @example
53
+ * tweak('jumpSpeed', {min: 0, max: 1});
54
+ * tweak('skyColor');
55
+ * tweak('player.size', {label: 'Player Size'});
56
+ * tweak('speed', {object: settings}); // settings.speed, in an ES module */
57
+ function tweak(path, options={})
58
+ {
59
+ if (!debug) return;
60
+ ASSERT(isStringLike(path) && tweakPathRegex.test(path), 'tweak path must be a name like speed or player.speed');
61
+
62
+ const {object} = options;
63
+ let name = path, get, set, toCode;
64
+ if (object)
65
+ {
66
+ // a field of the object, the object at each step is looked up again so one made again is found
67
+ ASSERT(typeof object === 'object', 'tweak object must be an object');
68
+ const keys = path.split('.'), last = keys.pop();
69
+ const parent = ()=> keys.reduce((o, key)=> o?.[key], object);
70
+ name = options.label ?? path;
71
+ get = ()=> parent()?.[last];
72
+ set = (v)=> { parent()[last] = v; };
73
+ toCode = (v)=> `${path}: ${v},`; // as it is written in the object
74
+ }
75
+ else
76
+ {
77
+ // a global, compiled once, a missing object on the path reads as undefined
78
+ get = new Function(`try { return ${path} } catch {}`);
79
+ set = new Function('v', `'use strict'; ${path} = v`);
80
+ toCode = (v)=> `${path} = ${v};`;
81
+ }
82
+ const value = get();
83
+ ASSERT(value !== undefined, object ? `tweak could not find ${path} on the object` :
84
+ `tweak could not find ${path}, it must be a global of a script, an ES module game passes the object option`);
85
+ try { set(value); }
86
+ catch { ASSERT(false, `tweak can not change ${path}, it must not be a const or frozen`); }
87
+ tweakAdd(name, get, set, toCode, options);
88
+ }
89
+
90
+ /** Add a button to the tweakables panel that calls a function, like one to restart the level
91
+ * - Adding the same label again replaces its function
92
+ * - Does nothing in release builds
93
+ * @param {string} label
94
+ * @param {function():void} callback
95
+ * @memberof Tweakables
96
+ * @example
97
+ * tweakButton('Restart', restartLevel); */
98
+ function tweakButton(label, callback)
99
+ {
100
+ if (!debug) return;
101
+ ASSERT(isStringLike(label), 'tweakButton label must be a string');
102
+ ASSERT(typeof callback === 'function', 'tweakButton callback must be a function');
103
+ const t = tweakList.find(t=> t.type === 'button' && t.label === label);
104
+ if (t)
105
+ t.callback = callback;
106
+ else
107
+ {
108
+ tweakList.push({type: 'button', label, callback});
109
+ tweakRowsDirty = true;
110
+ }
111
+ }
112
+
113
+ /** Add a divider to the tweakables panel, with a label for the tweaks after it
114
+ * - Does nothing in release builds
115
+ * @param {string} [label]
116
+ * @memberof Tweakables */
117
+ function tweakDivider(label='')
118
+ {
119
+ if (!debug) return;
120
+ if (label && tweakList.find(t=> t.type === 'divider' && t.label === label))
121
+ return; // added again, as a game that calls its setup again does
122
+ tweakList.push({type: 'divider', label});
123
+ tweakRowsDirty = true;
124
+ }
125
+
126
+ /** Add the engine settings a game most often tunes to the tweakables panel:
127
+ * gravity, timeScale, cameraScale and soundVolume, under an Engine divider
128
+ * - They are changed through their setters, so this works in the ES module build too
129
+ * - Does nothing in release builds
130
+ * @memberof Tweakables */
131
+ function tweakEngineDefaults()
132
+ {
133
+ if (!debug) return;
134
+ tweakDivider('Engine');
135
+ tweakAdd('gravity', ()=> gravity, setGravity, (v)=> `setGravity(${v});`, {min: -.05, max: .05});
136
+ tweakAdd('timeScale', ()=> timeScale, setTimeScale, (v)=> `setTimeScale(${v});`, {min: 0, max: 2});
137
+ tweakAdd('cameraScale', ()=> cameraScale, setCameraScale, (v)=> `setCameraScale(${v});`,
138
+ {min: 1, max: 128, step: 1});
139
+ tweakAdd('soundVolume', ()=> soundVolume, setSoundVolume, (v)=> `setSoundVolume(${v});`, {min: 0, max: 1});
140
+ }
141
+
142
+ ///////////////////////////////////////////////////////////////////////////////
143
+ // tweaks
144
+
145
+ function tweakAdd(name, get, set, toCode, options)
146
+ {
147
+ const value = get();
148
+ const type = isNumber(value) ? 'number' : typeof value === 'boolean' ? 'boolean' :
149
+ isColor(value) ? 'color' : isVector2(value) ? 'vector2' : tweakIsVector3(value) ? 'vector3' : undefined;
150
+ ASSERT(type, `tweak ${name} must be a number, boolean, Color, Vector2 or Vector3`);
151
+ if (!type) return;
152
+
153
+ // added again, the code value stays the one it had first, the value now may be a tweaked one
154
+ let t = tweakList.find(t=> t.name === name);
155
+ if (!t)
156
+ {
157
+ tweakList.push(t = {name, codeValue: tweakCopy(value)});
158
+ tweakRowsDirty = true;
159
+ }
160
+ Object.assign(t, {type, get, set, toCode, options});
161
+
162
+ // a saved value comes back while the code value is the one it was saved over, after an edit it is dropped
163
+ tweakSaved ||= readSaveData(tweakSaveName(), {});
164
+ const saved = tweakSaved[name];
165
+ if (!saved) return;
166
+ if (tweakSaveText(saved.code) === tweakSaveText(tweakToSave(t.codeValue)))
167
+ set(tweakFromSave(type, saved.value));
168
+ else
169
+ {
170
+ delete tweakSaved[name];
171
+ writeSaveData(tweakSaveName(), tweakSaved);
172
+ }
173
+ }
174
+
175
+ // every page keeps its own tweaks
176
+ function tweakSaveName() { return 'LittleJS tweaks ' + (globalThis.location?.pathname ?? ''); }
177
+
178
+ function tweakCopy(v) { return v?.copy ? v.copy() : v; }
179
+
180
+ // a Vector3, a build may leave out the 3D math
181
+ function tweakIsVector3(v) { return typeof Vector3 !== 'undefined' && isVector3(v); }
182
+
183
+ // the value as json can hold it, and back
184
+ function tweakToSave(v)
185
+ {
186
+ return isColor(v) ? [v.r, v.g, v.b, v.a] : isVector2(v) ? [v.x, v.y] :
187
+ tweakIsVector3(v) ? [v.x, v.y, v.z] : v;
188
+ }
189
+ function tweakFromSave(type, v)
190
+ {
191
+ return type === 'color' ? rgb(...v) : type === 'vector2' ? vec2(...v) : type === 'vector3' ? vec3(...v) : v;
192
+ }
193
+ function tweakSaveText(v) { return JSON.stringify(v); }
194
+
195
+ // remember a tweak's value, or forget it when it is back to the code value
196
+ function tweakSave(t)
197
+ {
198
+ const value = tweakToSave(t.get()), code = tweakToSave(t.codeValue);
199
+ if (tweakSaveText(value) === tweakSaveText(code))
200
+ delete tweakSaved[t.name];
201
+ else
202
+ tweakSaved[t.name] = {value, code};
203
+ writeSaveData(tweakSaveName(), tweakSaved);
204
+ }
205
+
206
+ // put every tweak back to its code value and forget the saved ones
207
+ function tweakReset()
208
+ {
209
+ for (const t of tweakList)
210
+ t.set?.(tweakCopy(t.codeValue));
211
+ tweakSaved = {};
212
+ writeSaveData(tweakSaveName(), tweakSaved);
213
+ }
214
+
215
+ // a line of code for each tweak changed from its code value
216
+ function tweakChangedCode()
217
+ {
218
+ // numbers as the code writes them, without float noise or a leading zero,
219
+ // colors as hsl rounded more, which changes nothing that can be seen
220
+ const number = (n, digits=6)=> String(+n.toFixed(digits)).replace(/^(-?)0\./, '$1.');
221
+ const code = (v)=>
222
+ {
223
+ if (isColor(v))
224
+ {
225
+ const [h, s, l, a] = v.HSLA();
226
+ return `hsl(${[h, s, l, ...(a < 1 ? [a] : [])].map((n)=> number(n, 3)).join(', ')})`;
227
+ }
228
+ if (isVector2(v))
229
+ return `vec2(${number(v.x)}, ${number(v.y)})`;
230
+ if (tweakIsVector3(v))
231
+ return `vec3(${number(v.x)}, ${number(v.y)}, ${number(v.z)})`;
232
+ return isNumber(v) ? number(v) : String(v);
233
+ };
234
+ return tweakList.filter(t=> t.get && tweakSaveText(tweakToSave(t.get())) !==
235
+ tweakSaveText(tweakToSave(t.codeValue))).map(t=> t.toCode(code(t.get()))).join('\n');
236
+ }
237
+
238
+ ///////////////////////////////////////////////////////////////////////////////
239
+ // panel
240
+
241
+ // shows or hides the panel each frame, and shows values the game changed itself
242
+ function tweakRender()
243
+ {
244
+ if (headlessMode) return;
245
+ if (!debugTweakables)
246
+ {
247
+ if (tweakPanel && tweakPanel.style.display !== 'none')
248
+ {
249
+ // hidden, a box it had focus in lets go of the keys
250
+ const active = /** @type {HTMLElement} */ (document.activeElement);
251
+ tweakPanel.contains(active) && active.blur();
252
+ tweakPanel.style.display = 'none';
253
+ }
254
+ return;
255
+ }
256
+
257
+ tweakPanel || tweakPanelInit();
258
+ tweakPanel.style.display = '';
259
+ if (tweakRowsDirty)
260
+ {
261
+ tweakRowsDirty = false;
262
+ tweakRows.replaceChildren();
263
+ for (const t of tweakList)
264
+ t.refresh = tweakRow(t);
265
+ tweakList.length || tweakElement('div', tweakRows, 'color:#888', 'Nothing to tweak, add values with tweak()');
266
+ }
267
+ for (const t of tweakList)
268
+ t.refresh?.();
269
+ }
270
+
271
+ function tweakPanelInit()
272
+ {
273
+ tweakPanel = tweakElement('div', document.body,
274
+ 'position:fixed;top:8px;right:8px;width:260px;max-height:calc(100% - 16px);overflow-y:auto;' +
275
+ 'box-sizing:border-box;padding:8px;background:#111d;color:#eee;font:12px monospace;' +
276
+ 'border-radius:4px;z-index:9999');
277
+
278
+ // a click or touch on the panel is not the game's, a mouse up still goes on so a button can let go
279
+ for (const type of ['mousedown','wheel','touchstart','touchmove','touchend','touchcancel'])
280
+ tweakPanel.addEventListener(type, (e)=> e.stopPropagation());
281
+
282
+ tweakElement('div', tweakPanel, 'font-weight:bold;margin-bottom:4px', 'Tweakables');
283
+ tweakRows = tweakElement('div', tweakPanel);
284
+ const buttons = tweakElement('div', tweakPanel, 'display:flex;gap:6px;margin-top:8px');
285
+ const button = (text, onclick)=>
286
+ tweakElement('button', buttons, 'flex:1;padding:4px;cursor:pointer', text).onclick = onclick;
287
+ button('Copy', (e)=>
288
+ {
289
+ // lines for the changed values, to paste over the values in the code
290
+ const code = tweakChangedCode(), target = /** @type {HTMLElement} */ (e.target);
291
+ code && navigator.clipboard?.writeText(code).catch(()=> console.log(code));
292
+ target.textContent = code ? 'Copied' : 'No changes';
293
+ setTimeout(()=> target.textContent = 'Copy', 1e3);
294
+ });
295
+ button('Reset', tweakReset);
296
+ }
297
+
298
+ // the row for a tweak, returns what shows its value
299
+ function tweakRow(t)
300
+ {
301
+ const row = tweakElement('div', tweakRows, 'margin:6px 0');
302
+ if (t.type === 'divider')
303
+ {
304
+ row.style.cssText = 'margin:10px 0 4px;padding-top:4px;border-top:1px solid #555;color:#aaa';
305
+ row.textContent = t.label;
306
+ return;
307
+ }
308
+ if (t.type === 'button')
309
+ {
310
+ // the function is looked up on click, so one added again replaces it
311
+ const button = tweakElement('button', row, 'width:100%;padding:4px;cursor:pointer', t.label);
312
+ button.onclick = ()=> t.callback();
313
+ return;
314
+ }
315
+
316
+ const label = t.options.label ?? t.name;
317
+ const save = ()=> tweakSave(t);
318
+ if (t.type === 'boolean')
319
+ {
320
+ const labelElement = tweakElement('label', row, 'display:flex;gap:6px;align-items:center;cursor:pointer');
321
+ const box = tweakElement('input', labelElement);
322
+ box.type = 'checkbox';
323
+ tweakElement('span', labelElement, '', label);
324
+ box.onchange = ()=> { t.set(box.checked); save(); };
325
+ return ()=> { box.checked = !!t.get(); };
326
+ }
327
+
328
+ tweakElement('div', row, '', label);
329
+ if (t.type === 'number')
330
+ return tweakNumber(row, '', t.get, t.set, t.options, save);
331
+
332
+ // a part of a vector or color is a number of its own, set as a new copy with that part changed
333
+ const part = (key, options)=> tweakNumber(row, key, ()=> t.get()?.[key],
334
+ (n)=> { const v = t.get().copy(); v[key] = n; t.set(v); }, options, save);
335
+ if (t.type === 'vector2' || t.type === 'vector3')
336
+ {
337
+ const axes = (t.type === 'vector2' ? ['x', 'y'] : ['x', 'y', 'z']).map((key)=> part(key, t.options));
338
+ return ()=> axes.forEach((refresh)=> refresh());
339
+ }
340
+
341
+ // a color picker has no alpha, a slider below it has
342
+ const picker = tweakElement('input', row, 'width:100%;height:24px;padding:0;border:0;background:none');
343
+ picker.type = 'color';
344
+ picker.oninput = ()=>
345
+ {
346
+ const color = rgb().setHex(picker.value);
347
+ color.a = t.get().a;
348
+ t.set(color);
349
+ save();
350
+ };
351
+ const alpha = part('a', {min: 0, max: 1});
352
+ return ()=>
353
+ {
354
+ const color = t.get();
355
+ const hex = isColor(color) && color.toString(false);
356
+ hex && picker !== document.activeElement && picker.value !== hex && (picker.value = hex);
357
+ alpha();
358
+ };
359
+ }
360
+
361
+ // a slider when there is a range and a box to type in, returns what shows the value
362
+ function tweakNumber(parent, axis, get, set, options, save)
363
+ {
364
+ const {min, max} = options;
365
+ const hasRange = isNumber(min) && isNumber(max);
366
+ const value = get(), size = abs(value);
367
+ const step = options.step ?? (hasRange ? (max - min) / 1e3 : size >= 10 ? 1 : size >= 1 ? .1 : .001);
368
+
369
+ const row = tweakElement('div', parent, 'display:flex;gap:4px;align-items:center');
370
+ axis && tweakElement('span', row, 'color:#888', axis);
371
+ const slider = hasRange ? tweakElement('input', row, 'flex:1;min-width:0') : undefined;
372
+ const box = tweakElement('input', row,
373
+ 'width:80px;box-sizing:border-box;background:#222;color:#eee;border:1px solid #555' +
374
+ (hasRange ? '' : ';flex:1'));
375
+ if (slider)
376
+ Object.assign(slider, {type: 'range', min, max, step});
377
+ Object.assign(box, {type: 'number', step});
378
+
379
+ const change = (input)=>
380
+ {
381
+ const v = parseFloat(input.value);
382
+ if (!isNumber(v)) return; // a box part way through typing
383
+ set(v);
384
+ save();
385
+ };
386
+ slider && (slider.oninput = ()=> change(slider));
387
+ box.oninput = ()=> change(box);
388
+ return ()=>
389
+ {
390
+ // the one being used keeps what it has, so typing is not undone
391
+ const v = get();
392
+ if (!isNumber(v)) return;
393
+ for (const input of [slider, box])
394
+ input && input !== document.activeElement && (input.value === '' || +input.value !== v) &&
395
+ (input.value = String(v));
396
+ };
397
+ }
398
+
399
+ function tweakElement(tag, parent, style='', text='')
400
+ {
401
+ const element = document.createElement(tag);
402
+ element.style.cssText = style;
403
+ element.textContent = text;
404
+ parent?.appendChild(element);
405
+ return element;
406
+ }
407
+
408
+ ///////////////////////////////////////////////////////////////////////////////
409
+ // plugin
410
+
411
+ debug && engineAddPlugin(undefined, tweakRender);