littlejsengine 1.18.24 → 1.18.26
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/README.md +3 -0
- package/dist/littlejs.d.ts +23 -1
- package/dist/littlejs.esm.js +643 -584
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +640 -584
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +640 -584
- package/package.json +58 -55
- package/src/engine.js +612 -580
- package/src/engineDraw.js +9 -3
- package/src/engineExport.js +3 -0
- package/src/engineSettings.js +13 -0
- package/src/engineUtilities.js +8 -3
package/dist/littlejs.esm.js
CHANGED
|
@@ -3,584 +3,616 @@
|
|
|
3
3
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* LittleJS - The Tiny Fast JavaScript Game Engine
|
|
8
|
-
* MIT License - Copyright 2021 Frank Force
|
|
9
|
-
*
|
|
10
|
-
* Engine Features
|
|
11
|
-
* - Object oriented system with EngineObject base class
|
|
12
|
-
* - Automatic object lifecycle (update, physics, collision, rendering)
|
|
13
|
-
* - Engine helper classes: Vector2, Color, Timer, RandomGenerator
|
|
14
|
-
* - Hybrid rendering with WebGL batching and Canvas2D fallback
|
|
15
|
-
* - Audio system with wave, mp3, or ZzFX sound effects
|
|
16
|
-
* - Input system with keyboard, mouse, gamepad, and touch support
|
|
17
|
-
* - Tile layer rendering and collision detection
|
|
18
|
-
* - Particle effect system with emitters
|
|
19
|
-
* - Medal/achievement system with local storage
|
|
20
|
-
* - Comprehensive debug tools and visualizations
|
|
21
|
-
* - Fixed 60 FPS timestep with configurable time scale
|
|
22
|
-
* - Raycast and spatial query utilities
|
|
23
|
-
* - Plugin system for extending engine functionality
|
|
24
|
-
* - Start with engineInit() and provide your game callbacks
|
|
25
|
-
* @namespace Engine
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
/** Name of engine
|
|
29
|
-
* @type {string}
|
|
30
|
-
* @default
|
|
31
|
-
* @memberof Engine */
|
|
32
|
-
const engineName = 'LittleJS';
|
|
33
|
-
|
|
34
|
-
/** Version of engine
|
|
35
|
-
* @type {string}
|
|
36
|
-
* @default
|
|
37
|
-
* @memberof Engine */
|
|
38
|
-
const engineVersion = '1.18.
|
|
39
|
-
|
|
40
|
-
/** Frames per second to update
|
|
41
|
-
* @type {number}
|
|
42
|
-
* @default
|
|
43
|
-
* @memberof Engine */
|
|
44
|
-
const frameRate = 60;
|
|
45
|
-
|
|
46
|
-
/** How many seconds each frame lasts, engine uses a fixed time step
|
|
47
|
-
* @type {number}
|
|
48
|
-
* @default 1/60
|
|
49
|
-
* @memberof Engine */
|
|
50
|
-
const timeDelta = 1/frameRate;
|
|
51
|
-
|
|
52
|
-
/** Array containing all engine objects
|
|
53
|
-
* @type {Array<EngineObject>}
|
|
54
|
-
* @memberof Engine */
|
|
55
|
-
let engineObjects = [];
|
|
56
|
-
|
|
57
|
-
/** Array with only objects set to collide with other objects this frame (for optimization)
|
|
58
|
-
* @type {Array<EngineObject>}
|
|
59
|
-
* @memberof Engine */
|
|
60
|
-
let engineObjectsCollide = [];
|
|
61
|
-
|
|
62
|
-
/** Current update frame, used to calculate time
|
|
63
|
-
* @type {number}
|
|
64
|
-
* @memberof Engine */
|
|
65
|
-
let frame = 0;
|
|
66
|
-
|
|
67
|
-
/** Current engine time since start in seconds
|
|
68
|
-
* @type {number}
|
|
69
|
-
* @memberof Engine */
|
|
70
|
-
let time = 0;
|
|
71
|
-
|
|
72
|
-
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
73
|
-
* @type {number}
|
|
74
|
-
* @memberof Engine */
|
|
75
|
-
let timeReal = 0;
|
|
76
|
-
|
|
77
|
-
/** Is the game paused? Causes time and objects to not be updated
|
|
78
|
-
* @type {boolean}
|
|
79
|
-
* @default false
|
|
80
|
-
* @memberof Engine */
|
|
81
|
-
let paused = false;
|
|
82
|
-
|
|
83
|
-
/** Get if game is paused
|
|
84
|
-
* @return {boolean}
|
|
85
|
-
* @memberof Engine */
|
|
86
|
-
function getPaused() { return paused; }
|
|
87
|
-
|
|
88
|
-
/** Set if game is paused
|
|
89
|
-
* @param {boolean} [isPaused]
|
|
90
|
-
* @memberof Engine */
|
|
91
|
-
function setPaused(isPaused=true) { paused = isPaused; }
|
|
92
|
-
|
|
93
|
-
// Engine internal variables
|
|
94
|
-
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
95
|
-
let
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* @
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
* @param {PluginCallback} [
|
|
120
|
-
* @param {PluginCallback} [
|
|
121
|
-
* @param {PluginCallback} [
|
|
122
|
-
* @
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
p.
|
|
129
|
-
p.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* @
|
|
141
|
-
* @
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* @
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* @param {
|
|
151
|
-
* @param {GameCallback}
|
|
152
|
-
* @param {GameCallback}
|
|
153
|
-
* @param {GameCallback}
|
|
154
|
-
* @param {
|
|
155
|
-
* @param {
|
|
156
|
-
* @
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* ()=> {
|
|
161
|
-
* ()=> {
|
|
162
|
-
* ()=> {
|
|
163
|
-
* ()=> {
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
frameTimeLastMS =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
// setup
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
// setup
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
'-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
*
|
|
552
|
-
* @
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
6
|
+
/**
|
|
7
|
+
* LittleJS - The Tiny Fast JavaScript Game Engine
|
|
8
|
+
* MIT License - Copyright 2021 Frank Force
|
|
9
|
+
*
|
|
10
|
+
* Engine Features
|
|
11
|
+
* - Object oriented system with EngineObject base class
|
|
12
|
+
* - Automatic object lifecycle (update, physics, collision, rendering)
|
|
13
|
+
* - Engine helper classes: Vector2, Color, Timer, RandomGenerator
|
|
14
|
+
* - Hybrid rendering with WebGL batching and Canvas2D fallback
|
|
15
|
+
* - Audio system with wave, mp3, or ZzFX sound effects
|
|
16
|
+
* - Input system with keyboard, mouse, gamepad, and touch support
|
|
17
|
+
* - Tile layer rendering and collision detection
|
|
18
|
+
* - Particle effect system with emitters
|
|
19
|
+
* - Medal/achievement system with local storage
|
|
20
|
+
* - Comprehensive debug tools and visualizations
|
|
21
|
+
* - Fixed 60 FPS timestep with configurable time scale
|
|
22
|
+
* - Raycast and spatial query utilities
|
|
23
|
+
* - Plugin system for extending engine functionality
|
|
24
|
+
* - Start with engineInit() and provide your game callbacks
|
|
25
|
+
* @namespace Engine
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Name of engine
|
|
29
|
+
* @type {string}
|
|
30
|
+
* @default
|
|
31
|
+
* @memberof Engine */
|
|
32
|
+
const engineName = 'LittleJS';
|
|
33
|
+
|
|
34
|
+
/** Version of engine
|
|
35
|
+
* @type {string}
|
|
36
|
+
* @default
|
|
37
|
+
* @memberof Engine */
|
|
38
|
+
const engineVersion = '1.18.26';
|
|
39
|
+
|
|
40
|
+
/** Frames per second to update
|
|
41
|
+
* @type {number}
|
|
42
|
+
* @default
|
|
43
|
+
* @memberof Engine */
|
|
44
|
+
const frameRate = 60;
|
|
45
|
+
|
|
46
|
+
/** How many seconds each frame lasts, engine uses a fixed time step
|
|
47
|
+
* @type {number}
|
|
48
|
+
* @default 1/60
|
|
49
|
+
* @memberof Engine */
|
|
50
|
+
const timeDelta = 1/frameRate;
|
|
51
|
+
|
|
52
|
+
/** Array containing all engine objects
|
|
53
|
+
* @type {Array<EngineObject>}
|
|
54
|
+
* @memberof Engine */
|
|
55
|
+
let engineObjects = [];
|
|
56
|
+
|
|
57
|
+
/** Array with only objects set to collide with other objects this frame (for optimization)
|
|
58
|
+
* @type {Array<EngineObject>}
|
|
59
|
+
* @memberof Engine */
|
|
60
|
+
let engineObjectsCollide = [];
|
|
61
|
+
|
|
62
|
+
/** Current update frame, used to calculate time
|
|
63
|
+
* @type {number}
|
|
64
|
+
* @memberof Engine */
|
|
65
|
+
let frame = 0;
|
|
66
|
+
|
|
67
|
+
/** Current engine time since start in seconds
|
|
68
|
+
* @type {number}
|
|
69
|
+
* @memberof Engine */
|
|
70
|
+
let time = 0;
|
|
71
|
+
|
|
72
|
+
/** Actual clock time since start in seconds (not affected by pause, timescale, or frame rate clamping)
|
|
73
|
+
* @type {number}
|
|
74
|
+
* @memberof Engine */
|
|
75
|
+
let timeReal = 0;
|
|
76
|
+
|
|
77
|
+
/** Is the game paused? Causes time and objects to not be updated
|
|
78
|
+
* @type {boolean}
|
|
79
|
+
* @default false
|
|
80
|
+
* @memberof Engine */
|
|
81
|
+
let paused = false;
|
|
82
|
+
|
|
83
|
+
/** Get if game is paused
|
|
84
|
+
* @return {boolean}
|
|
85
|
+
* @memberof Engine */
|
|
86
|
+
function getPaused() { return paused; }
|
|
87
|
+
|
|
88
|
+
/** Set if game is paused
|
|
89
|
+
* @param {boolean} [isPaused]
|
|
90
|
+
* @memberof Engine */
|
|
91
|
+
function setPaused(isPaused=true) { paused = isPaused; }
|
|
92
|
+
|
|
93
|
+
// Engine internal variables
|
|
94
|
+
let frameTimeLastMS = 0, frameTimeBufferMS = 0, averageFPS = 0;
|
|
95
|
+
let engineUpdateInternal; // assigned by engineInit so engineStep can drive it
|
|
96
|
+
let showEngineVersion = true;
|
|
97
|
+
|
|
98
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
99
|
+
// plugin hooks
|
|
100
|
+
|
|
101
|
+
const pluginList = [];
|
|
102
|
+
class EnginePlugin
|
|
103
|
+
{
|
|
104
|
+
constructor(update, render, glContextLost, glContextRestored)
|
|
105
|
+
{
|
|
106
|
+
this.update = update;
|
|
107
|
+
this.render = render;
|
|
108
|
+
this.glContextLost = glContextLost;
|
|
109
|
+
this.glContextRestored = glContextRestored;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @callback PluginCallback - Update or render function for a plugin
|
|
115
|
+
* @memberof Engine
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/** Add a new update function for a plugin
|
|
119
|
+
* @param {PluginCallback} [update]
|
|
120
|
+
* @param {PluginCallback} [render]
|
|
121
|
+
* @param {PluginCallback} [glContextLost]
|
|
122
|
+
* @param {PluginCallback} [glContextRestored]
|
|
123
|
+
* @memberof Engine */
|
|
124
|
+
function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
125
|
+
{
|
|
126
|
+
// make sure plugin functions are unique
|
|
127
|
+
ASSERT(!pluginList.find(p=>
|
|
128
|
+
p.update === update && p.render === render &&
|
|
129
|
+
p.glContextLost === glContextLost &&
|
|
130
|
+
p.glContextRestored === glContextRestored));
|
|
131
|
+
|
|
132
|
+
const plugin = new EnginePlugin(update, render, glContextLost, glContextRestored);
|
|
133
|
+
pluginList.push(plugin);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
137
|
+
// Main Engine Functions
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @callback GameInitCallback - Called after the engine starts, can be async
|
|
141
|
+
* @return {void|Promise<void>}
|
|
142
|
+
* @memberof Engine
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* @callback GameCallback - Update or render function for the game
|
|
146
|
+
* @memberof Engine
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/** Startup LittleJS engine with your callback functions
|
|
150
|
+
* @param {GameInitCallback} gameInit - Called once after the engine starts up, can be async for loading
|
|
151
|
+
* @param {GameCallback} gameUpdate - Called every frame before objects are updated (60fps), use for game logic
|
|
152
|
+
* @param {GameCallback} gameUpdatePost - Called after physics and objects are updated, even when paused, use for UI updates
|
|
153
|
+
* @param {GameCallback} gameRender - Called before objects are rendered, use for drawing backgrounds/world elements
|
|
154
|
+
* @param {GameCallback} gameRenderPost - Called after objects are rendered, use for drawing UI/overlays
|
|
155
|
+
* @param {Array<string>} [imageSources=[]] - List of image file paths to preload (e.g., ['player.png', 'tiles.png'])
|
|
156
|
+
* @param {HTMLElement} [rootElement] - Root DOM element to attach canvas to, defaults to document.body
|
|
157
|
+
* @example
|
|
158
|
+
* // Basic engine startup
|
|
159
|
+
* engineInit(
|
|
160
|
+
* ()=> { LOG('Game initialized!'); }, // gameInit
|
|
161
|
+
* ()=> { updateGameLogic(); }, // gameUpdate
|
|
162
|
+
* ()=> { updateUI(); }, // gameUpdatePost
|
|
163
|
+
* ()=> { drawBackground(); }, // gameRender
|
|
164
|
+
* ()=> { drawHUD(); }, // gameRenderPost
|
|
165
|
+
* ['tiles.png', 'tilesLevel.png'] // images to load
|
|
166
|
+
* );
|
|
167
|
+
* @memberof Engine */
|
|
168
|
+
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement)
|
|
169
|
+
{
|
|
170
|
+
showEngineVersion && console.log(`${engineName} Engine v${engineVersion}`);
|
|
171
|
+
ASSERT(!mainContext, 'engine already initialized');
|
|
172
|
+
// runtime guard so release builds (where the assert is stripped) don't
|
|
173
|
+
// double-register listeners / double-add canvases on a second call
|
|
174
|
+
if (mainContext) return;
|
|
175
|
+
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
176
|
+
|
|
177
|
+
// ensure body exists for minimal HTML where the script runs before <body> is parsed
|
|
178
|
+
if (!document.body)
|
|
179
|
+
document.documentElement.appendChild(document.createElement('body'));
|
|
180
|
+
rootElement ||= document.body;
|
|
181
|
+
|
|
182
|
+
// allow passing in empty functions
|
|
183
|
+
gameInit ||= ()=>{};
|
|
184
|
+
gameUpdate ||= ()=>{};
|
|
185
|
+
gameUpdatePost ||= ()=>{};
|
|
186
|
+
gameRender ||= ()=>{};
|
|
187
|
+
gameRenderPost ||= ()=>{};
|
|
188
|
+
|
|
189
|
+
// Called automatically by engine to setup render system
|
|
190
|
+
function enginePreRender()
|
|
191
|
+
{
|
|
192
|
+
// save canvas size
|
|
193
|
+
mainCanvasSize = vec2(mainCanvas.width, mainCanvas.height);
|
|
194
|
+
|
|
195
|
+
// disable smoothing for pixel art
|
|
196
|
+
mainContext.imageSmoothingEnabled = !tilesPixelated;
|
|
197
|
+
|
|
198
|
+
// setup gl rendering if enabled
|
|
199
|
+
glPreRender();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// internal update loop for engine
|
|
203
|
+
function engineUpdate(frameTimeMS=0)
|
|
204
|
+
{
|
|
205
|
+
// update time keeping
|
|
206
|
+
let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
|
|
207
|
+
// skip delta on the very first frame so timeReal doesn't jump
|
|
208
|
+
// by ~page-load-time when RAF starts handing real timestamps
|
|
209
|
+
if (!frameTimeLastMS) frameTimeDeltaMS = 0;
|
|
210
|
+
frameTimeLastMS = frameTimeMS;
|
|
211
|
+
if (debug || debugWatermark)
|
|
212
|
+
averageFPS = lerp(averageFPS, 1e3/(frameTimeDeltaMS||1), .05);
|
|
213
|
+
const debugSpeedUp = debug && keyIsDown('Equal'); // +
|
|
214
|
+
const debugSpeedDown = debug && keyIsDown('Minus'); // -
|
|
215
|
+
const debugScale = debugSpeedUp ? 10 : debugSpeedDown ? .1 : 1;
|
|
216
|
+
|
|
217
|
+
// apply time deltas
|
|
218
|
+
timeReal += frameTimeDeltaMS * debugScale / 1e3;
|
|
219
|
+
const combinedScale = timeScale * debugScale;
|
|
220
|
+
frameTimeDeltaMS *= combinedScale;
|
|
221
|
+
frameTimeBufferMS += paused ? 0 : frameTimeDeltaMS;
|
|
222
|
+
if (combinedScale <= 1)
|
|
223
|
+
frameTimeBufferMS = min(frameTimeBufferMS, 50); // clamp min framerate
|
|
224
|
+
|
|
225
|
+
let wasUpdated = false;
|
|
226
|
+
if (paused)
|
|
227
|
+
{
|
|
228
|
+
// update everything except the game and objects
|
|
229
|
+
wasUpdated = true;
|
|
230
|
+
updateCanvas();
|
|
231
|
+
inputUpdate();
|
|
232
|
+
pluginList.forEach(plugin=>plugin.update?.());
|
|
233
|
+
|
|
234
|
+
// update object transforms even when paused
|
|
235
|
+
for (const o of engineObjects)
|
|
236
|
+
o.parent || o.updateTransforms();
|
|
237
|
+
|
|
238
|
+
// do post update
|
|
239
|
+
debugUpdate();
|
|
240
|
+
gameUpdatePost();
|
|
241
|
+
inputUpdatePost();
|
|
242
|
+
if (debugVideoCaptureIsActive())
|
|
243
|
+
renderFrame();
|
|
244
|
+
}
|
|
245
|
+
else
|
|
246
|
+
{
|
|
247
|
+
// apply time delta smoothing, improves smoothness of framerate in some browsers
|
|
248
|
+
let deltaSmooth = 0;
|
|
249
|
+
if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9)
|
|
250
|
+
{
|
|
251
|
+
// force at least one update each frame since it is waiting for refresh
|
|
252
|
+
deltaSmooth = frameTimeBufferMS;
|
|
253
|
+
frameTimeBufferMS = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// update multiple frames if necessary in case of slow framerate
|
|
257
|
+
for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / frameRate)
|
|
258
|
+
{
|
|
259
|
+
// increment frame and update time
|
|
260
|
+
time = frame++ / frameRate;
|
|
261
|
+
|
|
262
|
+
// update game and objects
|
|
263
|
+
wasUpdated = true;
|
|
264
|
+
updateCanvas();
|
|
265
|
+
inputUpdate();
|
|
266
|
+
gameUpdate();
|
|
267
|
+
pluginList.forEach(plugin=>plugin.update?.());
|
|
268
|
+
engineObjectsUpdate();
|
|
269
|
+
|
|
270
|
+
// do post update
|
|
271
|
+
debugUpdate();
|
|
272
|
+
gameUpdatePost();
|
|
273
|
+
inputUpdatePost();
|
|
274
|
+
if (debugVideoCaptureIsActive())
|
|
275
|
+
renderFrame();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// add the time smoothing back in
|
|
279
|
+
frameTimeBufferMS += deltaSmooth;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!debugVideoCaptureIsActive())
|
|
283
|
+
renderFrame();
|
|
284
|
+
if (!engineManualStep)
|
|
285
|
+
requestAnimationFrame(engineUpdate);
|
|
286
|
+
|
|
287
|
+
function renderFrame()
|
|
288
|
+
{
|
|
289
|
+
if (headlessMode) return;
|
|
290
|
+
|
|
291
|
+
// canvas must be updated before rendering
|
|
292
|
+
if (!wasUpdated)
|
|
293
|
+
updateCanvas();
|
|
294
|
+
|
|
295
|
+
// render the game and objects
|
|
296
|
+
enginePreRender();
|
|
297
|
+
gameRender();
|
|
298
|
+
engineObjects.sort((a,b)=> a.renderOrder - b.renderOrder);
|
|
299
|
+
for (const o of engineObjects)
|
|
300
|
+
o.destroyed || o.render();
|
|
301
|
+
|
|
302
|
+
// post rendering
|
|
303
|
+
gameRenderPost();
|
|
304
|
+
pluginList.forEach(plugin=>plugin.render?.());
|
|
305
|
+
inputRender();
|
|
306
|
+
debugRender();
|
|
307
|
+
glFlush();
|
|
308
|
+
debugRenderPost();
|
|
309
|
+
drawCount = 0;
|
|
310
|
+
primitiveCount = 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
engineUpdateInternal = engineUpdate;
|
|
314
|
+
|
|
315
|
+
function updateCanvas()
|
|
316
|
+
{
|
|
317
|
+
if (headlessMode) return;
|
|
318
|
+
|
|
319
|
+
if (canvasFixedSize.x)
|
|
320
|
+
{
|
|
321
|
+
// set canvas fixed size
|
|
322
|
+
mainCanvasSize = canvasFixedSize.copy();
|
|
323
|
+
|
|
324
|
+
// fit to window using css width and height
|
|
325
|
+
const innerAspect = innerWidth / innerHeight;
|
|
326
|
+
const fixedAspect = canvasFixedSize.x / canvasFixedSize.y;
|
|
327
|
+
const w = innerAspect < fixedAspect ? '100%' : '';
|
|
328
|
+
const h = innerAspect < fixedAspect ? '' : '100%';
|
|
329
|
+
mainCanvas.style.width = w;
|
|
330
|
+
mainCanvas.style.height = h;
|
|
331
|
+
if (glCanvas)
|
|
332
|
+
{
|
|
333
|
+
glCanvas.style.width = w;
|
|
334
|
+
glCanvas.style.height = h;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else
|
|
338
|
+
{
|
|
339
|
+
// apply device pixel ratio for crisp rendering
|
|
340
|
+
const dpr = canvasPixelRatio ?? (devicePixelRatio || 1);
|
|
341
|
+
const viewWidth = innerWidth * dpr | 0;
|
|
342
|
+
const viewHeight = innerHeight * dpr | 0;
|
|
343
|
+
|
|
344
|
+
// get main canvas size based on window size
|
|
345
|
+
mainCanvasSize.x = min(viewWidth, canvasMaxSize.x);
|
|
346
|
+
mainCanvasSize.y = min(viewHeight, canvasMaxSize.y);
|
|
347
|
+
|
|
348
|
+
// responsive aspect ratio with native resolution
|
|
349
|
+
const innerAspect = viewWidth / viewHeight;
|
|
350
|
+
ASSERT(canvasMinAspect <= canvasMaxAspect);
|
|
351
|
+
if (canvasMaxAspect && innerAspect > canvasMaxAspect)
|
|
352
|
+
{
|
|
353
|
+
// full height
|
|
354
|
+
const w = mainCanvasSize.y * canvasMaxAspect | 0;
|
|
355
|
+
mainCanvasSize.x = min(w, canvasMaxSize.x);
|
|
356
|
+
}
|
|
357
|
+
else if (innerAspect < canvasMinAspect)
|
|
358
|
+
{
|
|
359
|
+
// full width
|
|
360
|
+
const h = mainCanvasSize.x / canvasMinAspect | 0;
|
|
361
|
+
mainCanvasSize.y = min(h, canvasMaxSize.y);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// set CSS display size so backing store renders at viewport size
|
|
365
|
+
const cssW = (mainCanvasSize.x / dpr | 0) + 'px';
|
|
366
|
+
const cssH = (mainCanvasSize.y / dpr | 0) + 'px';
|
|
367
|
+
mainCanvas.style.width = cssW;
|
|
368
|
+
mainCanvas.style.height = cssH;
|
|
369
|
+
if (glCanvas)
|
|
370
|
+
{
|
|
371
|
+
glCanvas.style.width = cssW;
|
|
372
|
+
glCanvas.style.height = cssH;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// clear main canvas and set size
|
|
377
|
+
mainCanvas.width = mainCanvasSize.x;
|
|
378
|
+
mainCanvas.height = mainCanvasSize.y;
|
|
379
|
+
|
|
380
|
+
// apply the clear color to main canvas
|
|
381
|
+
if (canvasClearColor.a > 0 && !glEnable)
|
|
382
|
+
{
|
|
383
|
+
mainContext.fillStyle = canvasClearColor.toString();
|
|
384
|
+
mainContext.fillRect(0, 0, mainCanvasSize.x, mainCanvasSize.y);
|
|
385
|
+
mainContext.fillStyle = BLACK.toString();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// set default line join and cap
|
|
389
|
+
mainContext.lineJoin = 'round';
|
|
390
|
+
mainContext.lineCap = 'round';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// skip setup if headless
|
|
394
|
+
if (headlessMode) return startEngine();
|
|
395
|
+
|
|
396
|
+
// setup webgl
|
|
397
|
+
glInit(rootElement);
|
|
398
|
+
|
|
399
|
+
// setup html
|
|
400
|
+
const styleRoot =
|
|
401
|
+
'margin:0;' + // fill the window
|
|
402
|
+
'overflow:hidden;' + // no scroll bars
|
|
403
|
+
'background:#000;' + // set background color
|
|
404
|
+
'user-select:none;' + // prevent hold to select
|
|
405
|
+
'-webkit-user-select:none;' + // compatibility for ios
|
|
406
|
+
'touch-action:none;' + // prevent mobile pinch to resize
|
|
407
|
+
'-webkit-touch-callout:none'; // compatibility for ios
|
|
408
|
+
rootElement.style.cssText = styleRoot;
|
|
409
|
+
mainCanvas = rootElement.appendChild(document.createElement('canvas'));
|
|
410
|
+
drawContext = mainContext = mainCanvas.getContext('2d');
|
|
411
|
+
|
|
412
|
+
// init stuff and start engine
|
|
413
|
+
inputInit();
|
|
414
|
+
audioInit();
|
|
415
|
+
debugInit();
|
|
416
|
+
|
|
417
|
+
// setup canvases
|
|
418
|
+
// transform way is still more reliable than flexbox or grid
|
|
419
|
+
const styleCanvas = 'position:absolute;'+ // allow canvases to overlap
|
|
420
|
+
'top:50%;left:50%;transform:translate(-50%,-50%)'; // center on screen
|
|
421
|
+
mainCanvas.style.cssText = styleCanvas;
|
|
422
|
+
if (glCanvas)
|
|
423
|
+
glCanvas.style.cssText = styleCanvas;
|
|
424
|
+
setCanvasPixelated(canvasPixelated);
|
|
425
|
+
updateCanvas();
|
|
426
|
+
glPreRender();
|
|
427
|
+
|
|
428
|
+
// create offscreen canvases for image processing
|
|
429
|
+
workCanvas = new OffscreenCanvas(64, 64);
|
|
430
|
+
workContext = workCanvas.getContext('2d');
|
|
431
|
+
workReadCanvas = new OffscreenCanvas(64, 64);
|
|
432
|
+
workReadContext = workReadCanvas.getContext('2d', { willReadFrequently: true });
|
|
433
|
+
|
|
434
|
+
// create promises for loading images
|
|
435
|
+
const promises = imageSources.map((src, i)=> loadTexture(i, src));
|
|
436
|
+
|
|
437
|
+
// no images to load
|
|
438
|
+
if (!imageSources.length)
|
|
439
|
+
promises.push(loadTexture(0));
|
|
440
|
+
|
|
441
|
+
// load engine font image
|
|
442
|
+
promises.push(imageFontInit());
|
|
443
|
+
|
|
444
|
+
if (showSplashScreen)
|
|
445
|
+
{
|
|
446
|
+
// draw splash screen
|
|
447
|
+
promises.push(new Promise(resolve =>
|
|
448
|
+
{
|
|
449
|
+
let t = 0;
|
|
450
|
+
updateSplash();
|
|
451
|
+
function updateSplash()
|
|
452
|
+
{
|
|
453
|
+
inputClear();
|
|
454
|
+
drawEngineLogo(t+=.01);
|
|
455
|
+
t>1 ? resolve() : setTimeout(updateSplash, 16);
|
|
456
|
+
}
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// wait for all the promises to finish
|
|
461
|
+
await Promise.all(promises);
|
|
462
|
+
return startEngine();
|
|
463
|
+
|
|
464
|
+
async function startEngine()
|
|
465
|
+
{
|
|
466
|
+
// wait for gameInit to load
|
|
467
|
+
await gameInit();
|
|
468
|
+
engineManualStep || engineUpdate();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// max frames engineStep can advance in one call, 10 minutes at 60fps
|
|
473
|
+
// large counts block until they finish, so this catches runaway values
|
|
474
|
+
const engineStepMaxFrames = 36000;
|
|
475
|
+
|
|
476
|
+
/** Advance the engine by a number of frames
|
|
477
|
+
* Requires setEngineManualStep(true) before engineInit
|
|
478
|
+
* Respects paused exactly as the normal update loop does
|
|
479
|
+
* @param {number} [frames] - number of engine update ticks, max 36000, each running one fixed update at timeScale 1
|
|
480
|
+
* @example
|
|
481
|
+
* setHeadlessMode(true);
|
|
482
|
+
* setEngineManualStep(true);
|
|
483
|
+
* await engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost);
|
|
484
|
+
* engineStep(600); // advance 10 seconds of game time
|
|
485
|
+
* @memberof Engine */
|
|
486
|
+
function engineStep(frames=1)
|
|
487
|
+
{
|
|
488
|
+
ASSERT(engineManualStep,
|
|
489
|
+
'engineStep requires setEngineManualStep(true) before engineInit');
|
|
490
|
+
ASSERT(engineUpdateInternal, 'engineStep requires engineInit to complete');
|
|
491
|
+
// runtime guard so release builds (where the asserts are stripped) can't
|
|
492
|
+
// start a second requestAnimationFrame chain or call an undefined update
|
|
493
|
+
if (!engineManualStep || !engineUpdateInternal) return;
|
|
494
|
+
ASSERT(Number.isInteger(frames) && frames >= 0 && frames <= engineStepMaxFrames,
|
|
495
|
+
'engineStep requires a whole frame count from 0 to ' + engineStepMaxFrames);
|
|
496
|
+
frames = min(frames, engineStepMaxFrames); // release has no asserts, don't freeze
|
|
497
|
+
for (let i = frames; i > 0; --i)
|
|
498
|
+
engineUpdateInternal(frameTimeLastMS + 1e3 / frameRate);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** Update each engine object, remove destroyed objects, and update time
|
|
502
|
+
* can be called manually if objects need to be updated outside of main loop
|
|
503
|
+
* @memberof Engine */
|
|
504
|
+
function engineObjectsUpdate()
|
|
505
|
+
{
|
|
506
|
+
// get list of solid objects for physics optimization
|
|
507
|
+
engineObjectsCollide = engineObjects.filter(o=>o.collideSolidObjects);
|
|
508
|
+
|
|
509
|
+
// update physics before object update
|
|
510
|
+
for (const o of engineObjects)
|
|
511
|
+
if (!o.parent && !o.destroyed)
|
|
512
|
+
o.updatePhysics();
|
|
513
|
+
|
|
514
|
+
// recursive object update
|
|
515
|
+
function updateChildObject(o)
|
|
516
|
+
{
|
|
517
|
+
if (o.destroyed) return;
|
|
518
|
+
|
|
519
|
+
o.update();
|
|
520
|
+
for (const child of o.children)
|
|
521
|
+
updateChildObject(child);
|
|
522
|
+
}
|
|
523
|
+
for (const o of engineObjects)
|
|
524
|
+
{
|
|
525
|
+
if (o.parent || o.destroyed) continue;
|
|
526
|
+
|
|
527
|
+
// update top level objects
|
|
528
|
+
o.update();
|
|
529
|
+
for (const child of o.children)
|
|
530
|
+
updateChildObject(child);
|
|
531
|
+
o.updateTransforms();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// remove destroyed objects
|
|
535
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Destroy and remove all objects
|
|
539
|
+
* - This can be used to clear out all objects when restarting a level
|
|
540
|
+
* - Objects can override their destroy function to do cleanup or stick around
|
|
541
|
+
* @param {boolean} [immediate] - should attached effects be allowed to die off?
|
|
542
|
+
* @memberof Engine */
|
|
543
|
+
function engineObjectsDestroy(immediate=true)
|
|
544
|
+
{
|
|
545
|
+
for (const o of engineObjects)
|
|
546
|
+
o.parent || o.destroy(immediate);
|
|
547
|
+
engineObjects = engineObjects.filter(o=>!o.destroyed);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Collects all object within a given area
|
|
551
|
+
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
552
|
+
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
553
|
+
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
554
|
+
* @return {Array<EngineObject>} - List of collected objects
|
|
555
|
+
* @memberof Engine */
|
|
556
|
+
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
557
|
+
{
|
|
558
|
+
const collectedObjects = [];
|
|
559
|
+
if (!pos)
|
|
560
|
+
{
|
|
561
|
+
// all objects
|
|
562
|
+
for (const o of objects)
|
|
563
|
+
collectedObjects.push(o);
|
|
564
|
+
}
|
|
565
|
+
else if (size instanceof Vector2)
|
|
566
|
+
{
|
|
567
|
+
// bounding box test
|
|
568
|
+
for (const o of objects)
|
|
569
|
+
o.isOverlapping(pos, size) && collectedObjects.push(o);
|
|
570
|
+
}
|
|
571
|
+
else
|
|
572
|
+
{
|
|
573
|
+
// circle test
|
|
574
|
+
const sizeSquared = size*size;
|
|
575
|
+
for (const o of objects)
|
|
576
|
+
pos.distanceSquared(o.pos) < sizeSquared && collectedObjects.push(o);
|
|
577
|
+
}
|
|
578
|
+
return collectedObjects;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* @callback ObjectCallbackFunction - Function that processes an object
|
|
583
|
+
* @param {EngineObject} object
|
|
584
|
+
* @memberof Engine
|
|
585
|
+
*/
|
|
586
|
+
|
|
587
|
+
/** Triggers a callback for each object within a given area
|
|
588
|
+
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
589
|
+
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
590
|
+
* @param {ObjectCallbackFunction} [callbackFunction] - Calls this function on every object that passes the test
|
|
591
|
+
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
592
|
+
* @memberof Engine */
|
|
593
|
+
function engineObjectsCallback(pos, size, callbackFunction, objects=engineObjects)
|
|
594
|
+
{ engineObjectsCollect(pos, size, objects).forEach(o => callbackFunction(o)); }
|
|
595
|
+
|
|
596
|
+
/** Return a list of objects intersecting a ray
|
|
597
|
+
* @param {Vector2} start
|
|
598
|
+
* @param {Vector2} end
|
|
599
|
+
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
600
|
+
* @return {Array<EngineObject>} - List of objects hit
|
|
601
|
+
* @memberof Engine */
|
|
602
|
+
function engineObjectsRaycast(start, end, objects=engineObjects)
|
|
603
|
+
{
|
|
604
|
+
const hitObjects = [];
|
|
605
|
+
for (const o of objects)
|
|
606
|
+
{
|
|
607
|
+
if (o.collideRaycast && isIntersecting(start, end, o.pos, o.size))
|
|
608
|
+
{
|
|
609
|
+
debugRaycast && debugRect(o.pos, o.size, '#f00');
|
|
610
|
+
hitObjects.push(o);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
debugRaycast && debugLine(start, end, hitObjects.length ? '#f00' : '#00f', .02);
|
|
615
|
+
return hitObjects;
|
|
584
616
|
}
|
|
585
617
|
/**
|
|
586
618
|
* LittleJS Debug System
|
|
@@ -2688,12 +2720,17 @@ function shareURL(title, url, callback)
|
|
|
2688
2720
|
|
|
2689
2721
|
/** Read save data from local storage
|
|
2690
2722
|
* @param {string} saveName - unique name for the game/save
|
|
2691
|
-
* @param {Object} [defaultSaveData] - default values
|
|
2723
|
+
* @param {Object} [defaultSaveData] - default values, result is {...default, ...loaded} so this must be an object
|
|
2692
2724
|
* @return {Object}
|
|
2693
2725
|
* @memberof Utilities */
|
|
2694
2726
|
function readSaveData(saveName, defaultSaveData)
|
|
2695
2727
|
{
|
|
2696
|
-
ASSERT(isStringLike(saveName), '
|
|
2728
|
+
ASSERT(isStringLike(saveName), 'readSaveData requires saveName string');
|
|
2729
|
+
ASSERT(defaultSaveData === undefined ||
|
|
2730
|
+
(typeof defaultSaveData === 'object' && defaultSaveData !== null),
|
|
2731
|
+
'readSaveData: default must be an object - the result is ' +
|
|
2732
|
+
'{...default, ...loaded}, so a scalar default yields {}. ' +
|
|
2733
|
+
'Use readSaveData(key, {best:0}).best');
|
|
2697
2734
|
|
|
2698
2735
|
// tolerate localStorage being unavailable (iOS private mode, sandboxed
|
|
2699
2736
|
// iframes) and corrupt JSON in stored data
|
|
@@ -2717,7 +2754,7 @@ function readSaveData(saveName, defaultSaveData)
|
|
|
2717
2754
|
* @memberof Utilities */
|
|
2718
2755
|
function writeSaveData(saveName, saveData)
|
|
2719
2756
|
{
|
|
2720
|
-
ASSERT(isStringLike(saveName), '
|
|
2757
|
+
ASSERT(isStringLike(saveName), 'writeSaveData requires saveName string');
|
|
2721
2758
|
// tolerate localStorage being unavailable or quota exceeded
|
|
2722
2759
|
try { localStorage[saveName] = JSON.stringify(saveData); }
|
|
2723
2760
|
catch { LOG('writeSaveData: failed to write', saveName); }
|
|
@@ -2885,6 +2922,13 @@ let showSplashScreen = false;
|
|
|
2885
2922
|
* @memberof Settings */
|
|
2886
2923
|
let headlessMode = false;
|
|
2887
2924
|
|
|
2925
|
+
/** Disables the automatic requestAnimationFrame loop so the engine only
|
|
2926
|
+
* advances when engineStep is called, for tests and frame-stepping tools
|
|
2927
|
+
* @type {boolean}
|
|
2928
|
+
* @default
|
|
2929
|
+
* @memberof Settings */
|
|
2930
|
+
let engineManualStep = false;
|
|
2931
|
+
|
|
2888
2932
|
///////////////////////////////////////////////////////////////////////////////
|
|
2889
2933
|
// WebGL settings
|
|
2890
2934
|
|
|
@@ -3235,6 +3279,12 @@ function setShowSplashScreen(show) { showSplashScreen = show; }
|
|
|
3235
3279
|
* @memberof Settings */
|
|
3236
3280
|
function setHeadlessMode(headless) { headlessMode = headless; }
|
|
3237
3281
|
|
|
3282
|
+
/** Set if the engine only advances when engineStep is called
|
|
3283
|
+
* Must be set before engineInit
|
|
3284
|
+
* @param {boolean} [enable]
|
|
3285
|
+
* @memberof Settings */
|
|
3286
|
+
function setEngineManualStep(enable=true) { engineManualStep = enable; }
|
|
3287
|
+
|
|
3238
3288
|
/** Set if WebGL rendering is enabled
|
|
3239
3289
|
* @param {boolean} enable
|
|
3240
3290
|
* @memberof Settings */
|
|
@@ -5475,9 +5525,15 @@ class ImageFont
|
|
|
5475
5525
|
tileInfo.pos.x = x*sizePaddedX + padding;
|
|
5476
5526
|
tileInfo.pos.y = y*sizePaddedY + padding;
|
|
5477
5527
|
|
|
5478
|
-
//
|
|
5479
|
-
|
|
5480
|
-
|
|
5528
|
+
// snap the glyph edges to whole pixels
|
|
5529
|
+
// tiles are drawn from their center, so snapping the center
|
|
5530
|
+
// to a whole pixel puts the edges on half pixels when the
|
|
5531
|
+
// size is even, and a row or column of the glyph then has
|
|
5532
|
+
// no pixel center inside it and is not rasterized at all
|
|
5533
|
+
// ceil picks the nearest aligned position, breaking ties
|
|
5534
|
+
// downward to match how this used to truncate
|
|
5535
|
+
drawPos.x = ceil(pos.x + i * size.x - centerOffset - size.x/2) + size.x/2 - .5;
|
|
5536
|
+
drawPos.y = ceil(pos.y + j * size.y - size.y/2) + size.y/2 - .5;
|
|
5481
5537
|
drawTile(drawPos, size, tileInfo, color, 0, false, undefined, useWebGL, true, context);
|
|
5482
5538
|
}
|
|
5483
5539
|
});
|
|
@@ -16871,6 +16927,7 @@ export
|
|
|
16871
16927
|
getPaused,
|
|
16872
16928
|
setPaused,
|
|
16873
16929
|
engineInit,
|
|
16930
|
+
engineStep,
|
|
16874
16931
|
engineObjectsUpdate,
|
|
16875
16932
|
engineObjectsDestroy,
|
|
16876
16933
|
engineObjectsCollect,
|
|
@@ -16918,6 +16975,7 @@ export
|
|
|
16918
16975
|
fontDefault,
|
|
16919
16976
|
showSplashScreen,
|
|
16920
16977
|
headlessMode,
|
|
16978
|
+
engineManualStep,
|
|
16921
16979
|
tileDefaultSize,
|
|
16922
16980
|
tileDefaultPadding,
|
|
16923
16981
|
tileDefaultBleed,
|
|
@@ -16972,6 +17030,7 @@ export
|
|
|
16972
17030
|
setFontDefault,
|
|
16973
17031
|
setShowSplashScreen,
|
|
16974
17032
|
setHeadlessMode,
|
|
17033
|
+
setEngineManualStep,
|
|
16975
17034
|
setGLEnable,
|
|
16976
17035
|
setTileDefaultSize,
|
|
16977
17036
|
setTileDefaultPadding,
|