littlejsengine 1.22.0 → 1.23.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/COPYRIGHT.txt +65 -38
- package/EDITOR.md +359 -0
- package/README.md +12 -7
- package/REFERENCE.md +2314 -0
- package/dist/littlejs.d.ts +640 -100
- package/dist/littlejs.esm.js +3596 -1444
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +3583 -1444
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +2106 -1355
- package/package.json +4 -3
- package/plugins/gltf.js +29 -13
- package/plugins/lightSystem.js +8 -6
- package/plugins/parallax.js +174 -0
- package/plugins/particleEffects.js +37 -22
- package/plugins/pluginExport.js +8 -0
- package/plugins/postProcess.js +5 -2
- package/plugins/render3d.js +59 -21
- package/plugins/render3dEditor.js +1058 -118
- package/plugins/render3dExtras.js +82 -21
- package/plugins/render3dLevel.js +328 -8
- package/plugins/textureSheet.js +1 -0
- package/plugins/tweenSystem.js +39 -34
- package/plugins/uiSystem.js +7 -3
- package/plugins/zzfxm.js +161 -159
- package/src/engine.js +1 -1
- package/src/engineAudio.js +17 -3
- package/src/engineBuild.mjs +18 -2
- package/src/engineDraw.js +26 -10
- package/src/engineEditor.js +515 -52
- package/src/engineExport.js +5 -0
- package/src/engineInput.js +48 -6
- package/src/engineObject.js +675 -675
- package/src/engineRelease.js +17 -2
- package/src/engineSettings.js +14 -0
- package/src/engineTileLayer.js +2 -1
- package/src/engineUtilities.js +291 -291
- package/src/engineWebGL.js +12 -3
package/src/engineRelease.js
CHANGED
|
@@ -52,8 +52,23 @@ function tweak(){}
|
|
|
52
52
|
function tweakButton(){}
|
|
53
53
|
function tweakDivider(){}
|
|
54
54
|
function tweakEngineDefaults(){}
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
class LevelEditor
|
|
56
|
+
{
|
|
57
|
+
constructor()
|
|
58
|
+
{
|
|
59
|
+
this.paletteTiles = this.use3D = this.tool = undefined;
|
|
60
|
+
this.keys = {}, this.buttons = [], this.tools = {};
|
|
61
|
+
}
|
|
62
|
+
get isOpen() { return false; }
|
|
63
|
+
get is3D() { return false; }
|
|
64
|
+
get edit2D() { return undefined; }
|
|
65
|
+
get edit3D() { return undefined; }
|
|
66
|
+
open(){} close(){} addKey(){} addButton(){} addTool(){}
|
|
67
|
+
onTile(){} onRestart(){} onPlayFrom(){} onOpen(){} onClose(){} onUpdate(){} onDraw(){} onPanel(){}
|
|
68
|
+
onSave(){ return false; }
|
|
69
|
+
}
|
|
70
|
+
let levelEditor = new LevelEditor;
|
|
71
|
+
function setLevelEditor(editor){ levelEditor = editor; }
|
|
57
72
|
function editorMapRestore(map){ return map; }
|
|
58
73
|
function editorMapLoaded(){}
|
|
59
74
|
function editorJSONFetched(){}
|
package/src/engineSettings.js
CHANGED
|
@@ -313,6 +313,15 @@ let touchGamepadEnable = false;
|
|
|
313
313
|
* @memberof Settings */
|
|
314
314
|
let touchGamepadPassthrough = false;
|
|
315
315
|
|
|
316
|
+
/** True if a pinch of two fingers turns the mouse wheel: touchPinch is added to mouseWheel, so what zooms with
|
|
317
|
+
* the wheel zooms with a pinch, the mouse is between the two fingers, and a second finger lets go of the button
|
|
318
|
+
* the first pressed, since a pinch is not a drag; turn it off for a game with its own use for two fingers, the
|
|
319
|
+
* first finger stays the mouse and touchPinch can still be read
|
|
320
|
+
* @type {boolean}
|
|
321
|
+
* @default
|
|
322
|
+
* @memberof Settings */
|
|
323
|
+
let touchPinchWheel = true;
|
|
324
|
+
|
|
316
325
|
/** Size of center button if touch gamepad should have start button in the center
|
|
317
326
|
* - Prevents activating when pressed near virtual stick or face buttons
|
|
318
327
|
* - When the game is paused, any touch will press the button
|
|
@@ -708,6 +717,11 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
|
|
|
708
717
|
* @memberof Settings */
|
|
709
718
|
function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
710
719
|
|
|
720
|
+
/** Set if a pinch of two fingers turns the mouse wheel
|
|
721
|
+
* @param {boolean} enable
|
|
722
|
+
* @memberof Settings */
|
|
723
|
+
function setTouchPinchWheel(enable) { touchPinchWheel = enable; }
|
|
724
|
+
|
|
711
725
|
/** Set if touches outside the gamepad controls should still drive mouse/touch input
|
|
712
726
|
* @param {boolean} passthrough
|
|
713
727
|
* @memberof Settings */
|
package/src/engineTileLayer.js
CHANGED
|
@@ -331,7 +331,8 @@ function objectLayersMake(tileMapData, object)
|
|
|
331
331
|
const name = object.type || object.class, type = objectLayersTypes.get(name);
|
|
332
332
|
if (!type)
|
|
333
333
|
{
|
|
334
|
-
|
|
334
|
+
// an object with no type, a shape or a text someone drew in Tiled, is not one a game makes
|
|
335
|
+
debug && name && console.warn(`objectLayersLoad: no type added for ${name}, skipped`);
|
|
335
336
|
return;
|
|
336
337
|
}
|
|
337
338
|
const {height=0, tilewidth=1, tileheight=1} = tileMapData;
|
package/src/engineUtilities.js
CHANGED
|
@@ -1,292 +1,292 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Utility Classes and Functions
|
|
3
|
-
* - Timer - tracks time automatically with support for pause and real-time modes
|
|
4
|
-
* - Time formatting helper
|
|
5
|
-
* - JSON file fetching
|
|
6
|
-
* - File saving (text, canvas, data URLs)
|
|
7
|
-
* - Native share dialog support
|
|
8
|
-
* - Local storage save data management
|
|
9
|
-
* - Value noise (1D and 2D)
|
|
10
|
-
* @namespace Utilities
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
'use strict';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Timer object tracks how long has passed since it was set
|
|
17
|
-
* @memberof Engine
|
|
18
|
-
* @example
|
|
19
|
-
* let a = new Timer; // creates a timer that is not set
|
|
20
|
-
* a.set(3); // sets the timer to 3 seconds
|
|
21
|
-
*
|
|
22
|
-
* let b = new Timer(1); // creates a timer with 1 second left
|
|
23
|
-
* b.unset(); // unset the timer
|
|
24
|
-
*/
|
|
25
|
-
class Timer
|
|
26
|
-
{
|
|
27
|
-
/** Create a timer object set time passed in
|
|
28
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
29
|
-
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
30
|
-
constructor(timeLeft, useRealTime=false)
|
|
31
|
-
{
|
|
32
|
-
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
33
|
-
this.useRealTime = useRealTime;
|
|
34
|
-
const globalTime = this.getGlobalTime();
|
|
35
|
-
/** @type {number|undefined} */
|
|
36
|
-
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
37
|
-
/** @type {number|undefined} */
|
|
38
|
-
this.setTime = timeLeft;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Set the timer with seconds passed in
|
|
42
|
-
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
43
|
-
set(timeLeft=0)
|
|
44
|
-
{
|
|
45
|
-
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
46
|
-
const globalTime = this.getGlobalTime();
|
|
47
|
-
this.time = globalTime + timeLeft;
|
|
48
|
-
this.setTime = timeLeft;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Set if the timer should keep running even when the game is paused
|
|
52
|
-
* @param {boolean} [useRealTime] */
|
|
53
|
-
setUseRealTime(useRealTime=true)
|
|
54
|
-
{
|
|
55
|
-
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
56
|
-
this.useRealTime = useRealTime;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Unset the timer */
|
|
60
|
-
unset() { this.time = undefined; }
|
|
61
|
-
|
|
62
|
-
/** Returns true if set
|
|
63
|
-
* @return {boolean} */
|
|
64
|
-
isSet() { return this.time !== undefined; }
|
|
65
|
-
|
|
66
|
-
/** Returns true if set and has not elapsed
|
|
67
|
-
* @return {boolean} */
|
|
68
|
-
active() { return this.getGlobalTime() < this.time; }
|
|
69
|
-
|
|
70
|
-
/** Returns true if set and elapsed
|
|
71
|
-
* @return {boolean} */
|
|
72
|
-
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
73
|
-
|
|
74
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
75
|
-
* @return {number} */
|
|
76
|
-
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
77
|
-
|
|
78
|
-
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
79
|
-
* Zero-duration timers report 1 (already elapsed).
|
|
80
|
-
* @return {number} */
|
|
81
|
-
getPercent()
|
|
82
|
-
{
|
|
83
|
-
if (!this.isSet()) return 0;
|
|
84
|
-
if (!this.setTime) return 1;
|
|
85
|
-
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Get the time this timer was set to, returns 0 if not set
|
|
89
|
-
* @return {number} */
|
|
90
|
-
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
91
|
-
|
|
92
|
-
/** Get the current global time this timer is based on
|
|
93
|
-
* @return {number} */
|
|
94
|
-
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
95
|
-
|
|
96
|
-
/** Returns this timer expressed as a string
|
|
97
|
-
* @return {string} */
|
|
98
|
-
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
99
|
-
|
|
100
|
-
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
101
|
-
* @return {number} */
|
|
102
|
-
valueOf() { return this.get(); }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
106
|
-
|
|
107
|
-
/** Formats seconds to mm:ss style for display purposes
|
|
108
|
-
* @param {number} t - time in seconds
|
|
109
|
-
* @return {string}
|
|
110
|
-
* @memberof Utilities */
|
|
111
|
-
function formatTime(t)
|
|
112
|
-
{
|
|
113
|
-
const signStr = t <= -1 ? '-' : ''; // no sign when it shows 0:00
|
|
114
|
-
t = abs(t)|0;
|
|
115
|
-
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
119
|
-
* @param {string} url - URL of JSON file
|
|
120
|
-
* @return {Promise<object>}
|
|
121
|
-
* @memberof Utilities */
|
|
122
|
-
async function fetchJSON(url)
|
|
123
|
-
{
|
|
124
|
-
const response = await fetch(url);
|
|
125
|
-
if (!response.ok)
|
|
126
|
-
throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
|
|
127
|
-
const json = await response.json();
|
|
128
|
-
editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
|
|
129
|
-
return json;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
133
|
-
|
|
134
|
-
/** Save a text file to disk
|
|
135
|
-
* @param {string} text
|
|
136
|
-
* @param {string} [filename]
|
|
137
|
-
* @param {string} [type]
|
|
138
|
-
* @memberof Utilities */
|
|
139
|
-
function saveText(text, filename='text', type='text/plain')
|
|
140
|
-
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename, 1e3); } // freed once the download has it
|
|
141
|
-
|
|
142
|
-
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
143
|
-
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
144
|
-
* @param {number} width - In pixels
|
|
145
|
-
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
146
|
-
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
147
|
-
* @return {OffscreenCanvasRenderingContext2D}
|
|
148
|
-
* @memberof Utilities */
|
|
149
|
-
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
150
|
-
{
|
|
151
|
-
ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
152
|
-
return new OffscreenCanvas(width, height).getContext('2d', {willReadFrequently});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/** Save a canvas to disk
|
|
156
|
-
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
157
|
-
* @param {string} [filename]
|
|
158
|
-
* @param {string} [type]
|
|
159
|
-
* @memberof Utilities */
|
|
160
|
-
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
161
|
-
{
|
|
162
|
-
if (canvas instanceof OffscreenCanvas)
|
|
163
|
-
{
|
|
164
|
-
// copy to temporary canvas and save
|
|
165
|
-
const saveCanvas = document.createElement('canvas');
|
|
166
|
-
saveCanvas.width = canvas.width;
|
|
167
|
-
saveCanvas.height = canvas.height;
|
|
168
|
-
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
169
|
-
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
170
|
-
}
|
|
171
|
-
else
|
|
172
|
-
saveDataURL(canvas.toDataURL(type), filename);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Save a data url to disk
|
|
176
|
-
* @param {string} url
|
|
177
|
-
* @param {string} [filename]
|
|
178
|
-
* @param {number} [revokeTime] - how long before revoking the url
|
|
179
|
-
* @memberof Utilities */
|
|
180
|
-
function saveDataURL(url, filename='download', revokeTime)
|
|
181
|
-
{
|
|
182
|
-
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
183
|
-
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
184
|
-
|
|
185
|
-
// create link for saving screenshots
|
|
186
|
-
const link = document.createElement('a');
|
|
187
|
-
link.download = filename;
|
|
188
|
-
link.href = url;
|
|
189
|
-
link.click();
|
|
190
|
-
if (revokeTime !== undefined)
|
|
191
|
-
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Share content using the native share dialog if available
|
|
195
|
-
* @param {string} title - title of the share
|
|
196
|
-
* @param {string} url - url to share
|
|
197
|
-
* @param {Function} [callback] - Called when share is complete
|
|
198
|
-
* @memberof Utilities */
|
|
199
|
-
function shareURL(title, url, callback)
|
|
200
|
-
{
|
|
201
|
-
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
202
|
-
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
203
|
-
navigator.share?.({title, url}).then(()=>callback?.(), ()=>{}); // a player who cancels is not an error
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
207
|
-
|
|
208
|
-
/** Read save data from local storage
|
|
209
|
-
* - The result has the type of defaultSaveData, or any when there is none
|
|
210
|
-
* @template {Object<string, any>} [T=any]
|
|
211
|
-
* @param {string} saveName - unique name for the game/save
|
|
212
|
-
* @param {T} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
213
|
-
* @return {T}
|
|
214
|
-
* @memberof Utilities */
|
|
215
|
-
function readSaveData(saveName, defaultSaveData)
|
|
216
|
-
{
|
|
217
|
-
ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
218
|
-
ASSERT(defaultSaveData === undefined ||
|
|
219
|
-
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
220
|
-
'readSaveData: default must be an object - the result is ' +
|
|
221
|
-
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
222
|
-
'Use readSaveData(key, {best:0}).best');
|
|
223
|
-
|
|
224
|
-
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
225
|
-
// iframes) and corrupt JSON in stored data
|
|
226
|
-
let loadedData = {};
|
|
227
|
-
try
|
|
228
|
-
{
|
|
229
|
-
const data = localStorage.getItem(saveName); // not the brackets, a name like 'key' or 'length' is a Storage member
|
|
230
|
-
if (data)
|
|
231
|
-
{
|
|
232
|
-
try { loadedData = JSON.parse(data); }
|
|
233
|
-
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
237
|
-
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Write save data to local storage
|
|
241
|
-
* @param {string} saveName - unique name for the game/save
|
|
242
|
-
* @param {object} saveData - object containing data to be saved
|
|
243
|
-
* @memberof Utilities */
|
|
244
|
-
function writeSaveData(saveName, saveData)
|
|
245
|
-
{
|
|
246
|
-
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
247
|
-
// tolerate localStorage being unavailable or quota exceeded
|
|
248
|
-
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
249
|
-
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
253
|
-
|
|
254
|
-
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
255
|
-
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
256
|
-
function noiseHash(i)
|
|
257
|
-
{
|
|
258
|
-
let h = (i | 0) ^ 0x9e3779b9;
|
|
259
|
-
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
260
|
-
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
261
|
-
h ^= h >>> 16;
|
|
262
|
-
return (h >>> 0) / 2**32;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/** 1D value noise — returns a smooth value in [0, 1] for any real x.
|
|
266
|
-
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
267
|
-
* are interpolated with smoothStep for C1 continuity.
|
|
268
|
-
* @param {number} x
|
|
269
|
-
* @return {number}
|
|
270
|
-
* @memberof Utilities */
|
|
271
|
-
function noise1D(x)
|
|
272
|
-
{
|
|
273
|
-
const i = floor(x);
|
|
274
|
-
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
278
|
-
* @param {number} x
|
|
279
|
-
* @param {number} y
|
|
280
|
-
* @return {number}
|
|
281
|
-
* @memberof Utilities */
|
|
282
|
-
function noise2D(x, y)
|
|
283
|
-
{
|
|
284
|
-
const ix = floor(x), iy = floor(y);
|
|
285
|
-
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
286
|
-
// large prime decorrelates neighboring rows
|
|
287
|
-
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
288
|
-
return lerp(
|
|
289
|
-
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
290
|
-
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
291
|
-
fy);
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Utility Classes and Functions
|
|
3
|
+
* - Timer - tracks time automatically with support for pause and real-time modes
|
|
4
|
+
* - Time formatting helper
|
|
5
|
+
* - JSON file fetching
|
|
6
|
+
* - File saving (text, canvas, data URLs)
|
|
7
|
+
* - Native share dialog support
|
|
8
|
+
* - Local storage save data management
|
|
9
|
+
* - Value noise (1D and 2D)
|
|
10
|
+
* @namespace Utilities
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Timer object tracks how long has passed since it was set
|
|
17
|
+
* @memberof Engine
|
|
18
|
+
* @example
|
|
19
|
+
* let a = new Timer; // creates a timer that is not set
|
|
20
|
+
* a.set(3); // sets the timer to 3 seconds
|
|
21
|
+
*
|
|
22
|
+
* let b = new Timer(1); // creates a timer with 1 second left
|
|
23
|
+
* b.unset(); // unset the timer
|
|
24
|
+
*/
|
|
25
|
+
class Timer
|
|
26
|
+
{
|
|
27
|
+
/** Create a timer object set time passed in
|
|
28
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds (undefined = unset)
|
|
29
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
30
|
+
constructor(timeLeft, useRealTime=false)
|
|
31
|
+
{
|
|
32
|
+
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
33
|
+
this.useRealTime = useRealTime;
|
|
34
|
+
const globalTime = this.getGlobalTime();
|
|
35
|
+
/** @type {number|undefined} */
|
|
36
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
37
|
+
/** @type {number|undefined} */
|
|
38
|
+
this.setTime = timeLeft;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Set the timer with seconds passed in
|
|
42
|
+
* @param {number} [timeLeft] - How much time left before the timer is elapsed in seconds */
|
|
43
|
+
set(timeLeft=0)
|
|
44
|
+
{
|
|
45
|
+
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
46
|
+
const globalTime = this.getGlobalTime();
|
|
47
|
+
this.time = globalTime + timeLeft;
|
|
48
|
+
this.setTime = timeLeft;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Set if the timer should keep running even when the game is paused
|
|
52
|
+
* @param {boolean} [useRealTime] */
|
|
53
|
+
setUseRealTime(useRealTime=true)
|
|
54
|
+
{
|
|
55
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
56
|
+
this.useRealTime = useRealTime;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Unset the timer */
|
|
60
|
+
unset() { this.time = undefined; }
|
|
61
|
+
|
|
62
|
+
/** Returns true if set
|
|
63
|
+
* @return {boolean} */
|
|
64
|
+
isSet() { return this.time !== undefined; }
|
|
65
|
+
|
|
66
|
+
/** Returns true if set and has not elapsed
|
|
67
|
+
* @return {boolean} */
|
|
68
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
69
|
+
|
|
70
|
+
/** Returns true if set and elapsed
|
|
71
|
+
* @return {boolean} */
|
|
72
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
73
|
+
|
|
74
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
75
|
+
* @return {number} */
|
|
76
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
77
|
+
|
|
78
|
+
/** Get percentage elapsed based on time it was set to, returns 0 if not set.
|
|
79
|
+
* Zero-duration timers report 1 (already elapsed).
|
|
80
|
+
* @return {number} */
|
|
81
|
+
getPercent()
|
|
82
|
+
{
|
|
83
|
+
if (!this.isSet()) return 0;
|
|
84
|
+
if (!(this.setTime > 0)) return 1;
|
|
85
|
+
return 1 - percent(this.time - this.getGlobalTime(), 0, this.setTime);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Get the time this timer was set to, returns 0 if not set
|
|
89
|
+
* @return {number} */
|
|
90
|
+
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
91
|
+
|
|
92
|
+
/** Get the current global time this timer is based on
|
|
93
|
+
* @return {number} */
|
|
94
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
95
|
+
|
|
96
|
+
/** Returns this timer expressed as a string
|
|
97
|
+
* @return {string} */
|
|
98
|
+
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
99
|
+
|
|
100
|
+
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
101
|
+
* @return {number} */
|
|
102
|
+
valueOf() { return this.get(); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
106
|
+
|
|
107
|
+
/** Formats seconds to mm:ss style for display purposes
|
|
108
|
+
* @param {number} t - time in seconds
|
|
109
|
+
* @return {string}
|
|
110
|
+
* @memberof Utilities */
|
|
111
|
+
function formatTime(t)
|
|
112
|
+
{
|
|
113
|
+
const signStr = t <= -1 ? '-' : ''; // no sign when it shows 0:00
|
|
114
|
+
t = abs(t)|0;
|
|
115
|
+
return signStr + (t/60|0) + ':' + (t%60<10?'0':'') + t%60;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Fetches a JSON file from a URL and returns the parsed JSON object. Must be used with await!
|
|
119
|
+
* @param {string} url - URL of JSON file
|
|
120
|
+
* @return {Promise<object>}
|
|
121
|
+
* @memberof Utilities */
|
|
122
|
+
async function fetchJSON(url)
|
|
123
|
+
{
|
|
124
|
+
const response = await fetch(url);
|
|
125
|
+
if (!response.ok)
|
|
126
|
+
throw new Error(`Failed to fetch JSON from ${url}: ${response.status} ${response.statusText}`);
|
|
127
|
+
const json = await response.json();
|
|
128
|
+
editorJSONFetched(url, json); // debug builds remember the file a level came from, to save it by that name
|
|
129
|
+
return json;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
133
|
+
|
|
134
|
+
/** Save a text file to disk
|
|
135
|
+
* @param {string} text
|
|
136
|
+
* @param {string} [filename]
|
|
137
|
+
* @param {string} [type]
|
|
138
|
+
* @memberof Utilities */
|
|
139
|
+
function saveText(text, filename='text', type='text/plain')
|
|
140
|
+
{ saveDataURL(URL.createObjectURL(new Blob([text], {'type':type})), filename, 1e3); } // freed once the download has it
|
|
141
|
+
|
|
142
|
+
/** Create an offscreen canvas to draw into, and return its 2D context
|
|
143
|
+
* - The canvas is context.canvas, which is what TextureInfo and the like take
|
|
144
|
+
* @param {number} width - In pixels
|
|
145
|
+
* @param {number} [height] - In pixels, defaults to the width for a square
|
|
146
|
+
* @param {boolean} [willReadFrequently] - Keep it in software, faster when getImageData is called on it often
|
|
147
|
+
* @return {OffscreenCanvasRenderingContext2D}
|
|
148
|
+
* @memberof Utilities */
|
|
149
|
+
function createCanvasContext(width, height=width, willReadFrequently=false)
|
|
150
|
+
{
|
|
151
|
+
ASSERT(isNumber(width) && isNumber(height), 'canvas width and height must be numbers', width, height);
|
|
152
|
+
return new OffscreenCanvas(width, height).getContext('2d', {willReadFrequently});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Save a canvas to disk
|
|
156
|
+
* @param {HTMLCanvasElement|OffscreenCanvas} canvas
|
|
157
|
+
* @param {string} [filename]
|
|
158
|
+
* @param {string} [type]
|
|
159
|
+
* @memberof Utilities */
|
|
160
|
+
function saveCanvas(canvas, filename='screenshot', type='image/png')
|
|
161
|
+
{
|
|
162
|
+
if (canvas instanceof OffscreenCanvas)
|
|
163
|
+
{
|
|
164
|
+
// copy to temporary canvas and save
|
|
165
|
+
const saveCanvas = document.createElement('canvas');
|
|
166
|
+
saveCanvas.width = canvas.width;
|
|
167
|
+
saveCanvas.height = canvas.height;
|
|
168
|
+
saveCanvas.getContext('2d').drawImage(canvas, 0, 0);
|
|
169
|
+
saveDataURL(saveCanvas.toDataURL(type), filename);
|
|
170
|
+
}
|
|
171
|
+
else
|
|
172
|
+
saveDataURL(canvas.toDataURL(type), filename);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Save a data url to disk
|
|
176
|
+
* @param {string} url
|
|
177
|
+
* @param {string} [filename]
|
|
178
|
+
* @param {number} [revokeTime] - how long before revoking the url
|
|
179
|
+
* @memberof Utilities */
|
|
180
|
+
function saveDataURL(url, filename='download', revokeTime)
|
|
181
|
+
{
|
|
182
|
+
ASSERT(isStringLike(url), 'saveDataURL requires url string');
|
|
183
|
+
ASSERT(isStringLike(filename), 'saveDataURL requires filename string');
|
|
184
|
+
|
|
185
|
+
// create link for saving screenshots
|
|
186
|
+
const link = document.createElement('a');
|
|
187
|
+
link.download = filename;
|
|
188
|
+
link.href = url;
|
|
189
|
+
link.click();
|
|
190
|
+
if (revokeTime !== undefined)
|
|
191
|
+
setTimeout(()=> URL.revokeObjectURL(url), revokeTime);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Share content using the native share dialog if available
|
|
195
|
+
* @param {string} title - title of the share
|
|
196
|
+
* @param {string} url - url to share
|
|
197
|
+
* @param {Function} [callback] - Called when share is complete
|
|
198
|
+
* @memberof Utilities */
|
|
199
|
+
function shareURL(title, url, callback)
|
|
200
|
+
{
|
|
201
|
+
ASSERT(isStringLike(title), 'shareURL requires title string');
|
|
202
|
+
ASSERT(isStringLike(url), 'shareURL requires url string');
|
|
203
|
+
navigator.share?.({title, url}).then(()=>callback?.(), ()=>{}); // a player who cancels is not an error
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
207
|
+
|
|
208
|
+
/** Read save data from local storage
|
|
209
|
+
* - The result has the type of defaultSaveData, or any when there is none
|
|
210
|
+
* @template {Object<string, any>} [T=any]
|
|
211
|
+
* @param {string} saveName - unique name for the game/save
|
|
212
|
+
* @param {T} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
213
|
+
* @return {T}
|
|
214
|
+
* @memberof Utilities */
|
|
215
|
+
function readSaveData(saveName, defaultSaveData)
|
|
216
|
+
{
|
|
217
|
+
ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
218
|
+
ASSERT(defaultSaveData === undefined ||
|
|
219
|
+
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
220
|
+
'readSaveData: default must be an object - the result is ' +
|
|
221
|
+
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
222
|
+
'Use readSaveData(key, {best:0}).best');
|
|
223
|
+
|
|
224
|
+
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
225
|
+
// iframes) and corrupt JSON in stored data
|
|
226
|
+
let loadedData = {};
|
|
227
|
+
try
|
|
228
|
+
{
|
|
229
|
+
const data = localStorage.getItem(saveName); // not the brackets, a name like 'key' or 'length' is a Storage member
|
|
230
|
+
if (data)
|
|
231
|
+
{
|
|
232
|
+
try { loadedData = JSON.parse(data); }
|
|
233
|
+
catch { LOG('readSaveData: corrupt JSON for', saveName, '— using defaults'); }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch { LOG('readSaveData: localStorage unavailable — using defaults'); }
|
|
237
|
+
return { .../** @type {object} */ (defaultSaveData), ...loadedData };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Write save data to local storage
|
|
241
|
+
* @param {string} saveName - unique name for the game/save
|
|
242
|
+
* @param {object} saveData - object containing data to be saved
|
|
243
|
+
* @memberof Utilities */
|
|
244
|
+
function writeSaveData(saveName, saveData)
|
|
245
|
+
{
|
|
246
|
+
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
247
|
+
// tolerate localStorage being unavailable or quota exceeded
|
|
248
|
+
try { localStorage.setItem(saveName, JSON.stringify(saveData)); }
|
|
249
|
+
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
253
|
+
|
|
254
|
+
// Deterministic well-distributed hash of an integer lattice index to [0, 1).
|
|
255
|
+
// Murmur3 finalizer — adjacent integers produce uncorrelated outputs.
|
|
256
|
+
function noiseHash(i)
|
|
257
|
+
{
|
|
258
|
+
let h = (i | 0) ^ 0x9e3779b9;
|
|
259
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
260
|
+
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
|
|
261
|
+
h ^= h >>> 16;
|
|
262
|
+
return (h >>> 0) / 2**32;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 1D value noise — returns a smooth value in [0, 1] for any real x.
|
|
266
|
+
* Integer inputs land on deterministic lattice values; non-integer inputs
|
|
267
|
+
* are interpolated with smoothStep for C1 continuity.
|
|
268
|
+
* @param {number} x
|
|
269
|
+
* @return {number}
|
|
270
|
+
* @memberof Utilities */
|
|
271
|
+
function noise1D(x)
|
|
272
|
+
{
|
|
273
|
+
const i = floor(x);
|
|
274
|
+
return lerp(noiseHash(i), noiseHash(i + 1), smoothStep(x - i));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 2D value noise — returns a smooth value in [0, 1] for any real (x, y).
|
|
278
|
+
* @param {number} x
|
|
279
|
+
* @param {number} y
|
|
280
|
+
* @return {number}
|
|
281
|
+
* @memberof Utilities */
|
|
282
|
+
function noise2D(x, y)
|
|
283
|
+
{
|
|
284
|
+
const ix = floor(x), iy = floor(y);
|
|
285
|
+
const fx = smoothStep(x - ix), fy = smoothStep(y - iy);
|
|
286
|
+
// large prime decorrelates neighboring rows
|
|
287
|
+
const h = (a, b) => noiseHash(a + b * 374761393);
|
|
288
|
+
return lerp(
|
|
289
|
+
lerp(h(ix, iy ), h(ix + 1, iy ), fx),
|
|
290
|
+
lerp(h(ix, iy + 1), h(ix + 1, iy + 1), fx),
|
|
291
|
+
fy);
|
|
292
292
|
}
|