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.
- package/FAQ.md +841 -820
- package/README.md +29 -25
- package/dist/littlejs.d.ts +2857 -955
- package/dist/littlejs.esm.js +33428 -20995
- package/dist/littlejs.esm.min.js +4 -3
- package/dist/littlejs.js +33386 -20996
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +27841 -20422
- package/package.json +10 -5
- package/plugins/audioEffects.js +28 -24
- package/plugins/box2d.js +2516 -2073
- package/plugins/drawUtilities.js +206 -31
- package/plugins/gltf.js +786 -0
- package/plugins/jsconfig.json +19 -0
- package/plugins/lightSystem.js +719 -332
- package/plugins/math3d.js +148 -27
- package/plugins/medalSystem.js +326 -280
- package/plugins/newgrounds.js +400 -199
- package/plugins/pathFinder.js +208 -175
- package/plugins/pluginExport.js +25 -0
- package/plugins/postProcess.js +96 -24
- package/plugins/render3d.js +2067 -1632
- package/plugins/render3dDebug.js +110 -0
- package/plugins/render3dEditor.js +1812 -0
- package/plugins/render3dExtras.js +1948 -0
- package/plugins/render3dLevel.js +189 -0
- package/plugins/render3dVoxels.js +514 -0
- package/plugins/sceneSystem.js +58 -0
- package/plugins/textureSheet.js +496 -458
- package/plugins/threejs.js +183 -163
- package/plugins/tweakables.js +411 -0
- package/plugins/tweenSystem.js +265 -145
- package/plugins/uiSystem.js +2024 -1687
- package/plugins/zzfxm.js +159 -159
- package/src/engine.js +378 -97
- package/src/engineAudio.js +231 -127
- package/src/engineBuild.mjs +292 -256
- package/src/engineDebug.js +967 -743
- package/src/engineDraw.js +391 -198
- package/src/engineEditor.js +2452 -0
- package/src/engineExport.js +18 -0
- package/src/engineInput.js +1466 -1293
- package/src/engineLogo.js +19 -0
- package/src/engineMath.js +1274 -1252
- package/src/engineObject.js +197 -82
- package/src/engineParticles.js +583 -532
- package/src/engineRelease.js +79 -45
- package/src/engineSettings.js +115 -29
- package/src/engineTileLayer.js +995 -741
- package/src/engineUtilities.js +19 -13
- package/src/engineWebGL.js +239 -82
- package/src/jsconfig.json +17 -13
- package/plugins/box2d.wasm.js +0 -630
- package/plugins/box2d.wasm.wasm +0 -0
package/src/engineTileLayer.js
CHANGED
|
@@ -1,742 +1,996 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS Tile Layer System
|
|
3
|
-
* - Renders large tile-based levels efficiently using cached canvases
|
|
4
|
-
* - Unlimited tile layers with automatic canvas allocation
|
|
5
|
-
* - Layers support both rendering and collision detection
|
|
6
|
-
* - Direct canvas2d drawing access for custom tile rendering
|
|
7
|
-
* - TileLayer for rendering, TileCollisionLayer for physics
|
|
8
|
-
* - Collision callbacks for tile interactions with objects
|
|
9
|
-
* - Optimized raycast support for tile-based physics
|
|
10
|
-
* - Integration with Box2D physics via Box2DTileLayer plugin
|
|
11
|
-
* @namespace TileLayers
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
'use strict';
|
|
15
|
-
|
|
16
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
17
|
-
// Tile Layer System
|
|
18
|
-
|
|
19
|
-
/** Keep track of all tile layers with collision
|
|
20
|
-
* @type {Array<TileCollisionLayer>}
|
|
21
|
-
* @memberof TileLayers */
|
|
22
|
-
const tileCollisionLayers = [];
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
function
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* @
|
|
65
|
-
* @param {
|
|
66
|
-
* @param {
|
|
67
|
-
* @
|
|
68
|
-
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
*/
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
this.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
this.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1
|
+
/**
|
|
2
|
+
* LittleJS Tile Layer System
|
|
3
|
+
* - Renders large tile-based levels efficiently using cached canvases
|
|
4
|
+
* - Unlimited tile layers with automatic canvas allocation
|
|
5
|
+
* - Layers support both rendering and collision detection
|
|
6
|
+
* - Direct canvas2d drawing access for custom tile rendering
|
|
7
|
+
* - TileLayer for rendering, TileCollisionLayer for physics
|
|
8
|
+
* - Collision callbacks for tile interactions with objects
|
|
9
|
+
* - Optimized raycast support for tile-based physics
|
|
10
|
+
* - Integration with Box2D physics via Box2DTileLayer plugin
|
|
11
|
+
* @namespace TileLayers
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
// Tile Layer System
|
|
18
|
+
|
|
19
|
+
/** Keep track of all tile layers with collision
|
|
20
|
+
* @type {Array<TileCollisionLayer>}
|
|
21
|
+
* @memberof TileLayers */
|
|
22
|
+
const tileCollisionLayers = [];
|
|
23
|
+
|
|
24
|
+
// a tile collision layer's position is whole numbers, so its cells are the world grid the physics lands objects on
|
|
25
|
+
function tileCollisionAssertWhole(layer)
|
|
26
|
+
{ ASSERT(layer.pos.x % 1 === 0 && layer.pos.y % 1 === 0, 'a tile collision layer must sit at a whole number position', layer.pos); }
|
|
27
|
+
|
|
28
|
+
// the test a tile query applies to a cell's data: the callback, the object's collideWithTile, or solid data
|
|
29
|
+
function tileCollisionTester(callbackObject)
|
|
30
|
+
{
|
|
31
|
+
ASSERT(!callbackObject || typeof callbackObject === 'function' || callbackObject instanceof EngineObject, 'callbackObject must be a function or EngineObject');
|
|
32
|
+
return !callbackObject ? (tileData)=> tileData > 0 :
|
|
33
|
+
typeof callbackObject === 'function' ? (tileData, pos)=> callbackObject(tileData, pos) :
|
|
34
|
+
(tileData, pos)=> callbackObject.collideWithTile(tileData, pos);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Get tile collision data for a given cell in the grid
|
|
38
|
+
* @param {Vector2} pos
|
|
39
|
+
* @param {boolean} [solidOnly] - Only check solid layers?
|
|
40
|
+
* @return {number}
|
|
41
|
+
* @memberof TileLayers */
|
|
42
|
+
function tileCollisionGetData(pos, solidOnly=true)
|
|
43
|
+
{
|
|
44
|
+
// check all tile collision layers, in scalars since particles ask this every frame
|
|
45
|
+
// solid (positive) data wins, a negative marker is returned only when no layer is solid there
|
|
46
|
+
let found = 0;
|
|
47
|
+
for (const layer of tileCollisionLayers)
|
|
48
|
+
if (!solidOnly || layer.isSolid)
|
|
49
|
+
{
|
|
50
|
+
// world pos to the layer's cell
|
|
51
|
+
const x = pos.x - layer.pos.x, y = pos.y - layer.pos.y, size = layer.size;
|
|
52
|
+
if (x >= 0 && y >= 0 && x < size.x && y < size.y)
|
|
53
|
+
{
|
|
54
|
+
const data = layer.collisionData[(y|0)*size.x + (x|0)];
|
|
55
|
+
if (data > 0) return data;
|
|
56
|
+
if (data && !found) found = data;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Check if a tile layer collides with another object
|
|
63
|
+
* @param {Vector2} pos
|
|
64
|
+
* @param {Vector2} [size=vec2()]
|
|
65
|
+
* @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
|
|
66
|
+
* @param {boolean} [solidOnly] - Only check solid layers?
|
|
67
|
+
* @return {TileCollisionLayer|undefined}
|
|
68
|
+
* @memberof TileLayers */
|
|
69
|
+
function tileCollisionTest(pos, size=vec2(), callbackObject, solidOnly=true)
|
|
70
|
+
{
|
|
71
|
+
for (const layer of tileCollisionLayers)
|
|
72
|
+
{
|
|
73
|
+
if (!solidOnly || layer.isSolid)
|
|
74
|
+
if (layer.collisionTest(pos, size, callbackObject))
|
|
75
|
+
return layer;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @callback TileCollisionCallback - Decides whether a tile counts as solid for a collision test or raycast
|
|
81
|
+
* @param {number} tileData - the value of the tile at the position
|
|
82
|
+
* @param {Vector2} pos - world space position of tile where the collision occurred
|
|
83
|
+
* @return {boolean} - true for a hit; a callback that returns nothing lets everything through
|
|
84
|
+
* @memberof TileLayers
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
88
|
+
* The point will be inside the colliding tile if it hits
|
|
89
|
+
* @param {Vector2} posStart
|
|
90
|
+
* @param {Vector2} posEnd
|
|
91
|
+
* @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
|
|
92
|
+
* @param {Vector2} [normal] - Optional normal of the surface hit
|
|
93
|
+
* @param {boolean} [solidOnly=true] - Only check solid layers?
|
|
94
|
+
* @return {Vector2|undefined} - where the ray meets the first tile hit, nudged just inside it, or undefined if no hit
|
|
95
|
+
* @memberof TileLayers */
|
|
96
|
+
function tileCollisionRaycast(posStart, posEnd, callbackObject, normal, solidOnly=true)
|
|
97
|
+
{
|
|
98
|
+
// check every layer and keep the closest hit so a far hit in an
|
|
99
|
+
// earlier-registered layer doesn't shadow a closer hit in a later one
|
|
100
|
+
let closestHit, closestDistSq, closestNormal;
|
|
101
|
+
const scratchNormal = normal && vec2();
|
|
102
|
+
for (const layer of tileCollisionLayers)
|
|
103
|
+
{
|
|
104
|
+
if (!solidOnly || layer.isSolid)
|
|
105
|
+
{
|
|
106
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, callbackObject, scratchNormal);
|
|
107
|
+
if (hitPos)
|
|
108
|
+
{
|
|
109
|
+
const d = posStart.distanceSquared(hitPos);
|
|
110
|
+
if (closestHit === undefined || d < closestDistSq)
|
|
111
|
+
{
|
|
112
|
+
closestHit = hitPos;
|
|
113
|
+
closestDistSq = d;
|
|
114
|
+
if (normal) closestNormal = scratchNormal.copy();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (closestHit && normal) normal.setFrom(closestNormal);
|
|
120
|
+
return closestHit;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
124
|
+
// Tiled's flip flags, horizontal, vertical and diagonal as bits 2, 1 and 0, as [direction, mirror]
|
|
125
|
+
const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [1,1]];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load tile layers from exported data
|
|
129
|
+
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
130
|
+
* tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
|
|
131
|
+
* - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
|
|
132
|
+
* (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
|
|
133
|
+
* visibility carry to the layers inside it
|
|
134
|
+
* - An object or image layer keeps its index, with its slot in the returned array left empty
|
|
135
|
+
* - A hidden layer (visible false) is loaded, its collision included, but not drawn; its render
|
|
136
|
+
* is a no-op, delete that and call redraw() to show it
|
|
137
|
+
* @param {Object} tileMapData - Level data from exported data
|
|
138
|
+
* @param {TileInfo} [tileInfo] - Default tile info (used for size and texture), tile() by default, none when no image is loaded
|
|
139
|
+
* @param {number} [renderOrder] - Render order of the top layer
|
|
140
|
+
* @param {number} [collisionLayer] - Layer to use for collision if any
|
|
141
|
+
* @param {boolean} [draw] - Should the layer be drawn automatically
|
|
142
|
+
* @return {Array<TileCollisionLayer>}
|
|
143
|
+
* @memberof TileLayers */
|
|
144
|
+
function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrder=0, collisionLayer, draw=true)
|
|
145
|
+
{
|
|
146
|
+
if (!tileMapData)
|
|
147
|
+
{
|
|
148
|
+
// default level data if loading failed
|
|
149
|
+
const s = 50;
|
|
150
|
+
tileMapData = {};
|
|
151
|
+
tileMapData.height = tileMapData.width = s;
|
|
152
|
+
tileMapData.layers = [{}];
|
|
153
|
+
tileMapData.layers[0].data = new Array(s*s).fill(0);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// the editor, in debug builds, keeps the map as the source of its edits and brings back autosaved ones
|
|
157
|
+
tileMapData = editorMapRestore(tileMapData);
|
|
158
|
+
|
|
159
|
+
// validate the tile map data
|
|
160
|
+
ASSERT(tileMapData.width && tileMapData.height);
|
|
161
|
+
ASSERT(tileMapData.layers && tileMapData.layers.length);
|
|
162
|
+
|
|
163
|
+
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
164
|
+
/** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
|
|
165
|
+
const layers = [];
|
|
166
|
+
const addLayers = (dataLayers, groupColor, groupVisible)=>
|
|
167
|
+
{
|
|
168
|
+
for (const dataLayer of dataLayers)
|
|
169
|
+
{
|
|
170
|
+
const type = dataLayer.type;
|
|
171
|
+
if (type && type !== 'tilelayer' && type !== 'group')
|
|
172
|
+
{
|
|
173
|
+
layers.push({dataLayer}); // an object or image layer has no tiles, its slot is left empty
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// apply layer color, Tiled writes a tint with alpha as #AARRGGBB
|
|
178
|
+
const tint = dataLayer.tintcolor;
|
|
179
|
+
const color = tint ? tileLayersColor(tint) : (dataLayer.color || WHITE).copy();
|
|
180
|
+
ASSERT(isColor(color), 'layer color is not a color');
|
|
181
|
+
color.a *= dataLayer.opacity ?? 1;
|
|
182
|
+
const visible = groupVisible && dataLayer.visible !== false;
|
|
183
|
+
if (type === 'group')
|
|
184
|
+
addLayers(dataLayer.layers || [], groupColor.multiply(color), visible);
|
|
185
|
+
else
|
|
186
|
+
layers.push({dataLayer, color: groupColor.multiply(color), visible});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
addLayers(tileMapData.layers, WHITE, true);
|
|
190
|
+
|
|
191
|
+
// create tile layers and fill with data
|
|
192
|
+
const tileLayers = [];
|
|
193
|
+
const levelSize = vec2(tileMapData.width, tileMapData.height);
|
|
194
|
+
const layerCount = layers.length;
|
|
195
|
+
for (let layerIndex=layerCount; layerIndex--;)
|
|
196
|
+
{
|
|
197
|
+
const {dataLayer, color: layerColor, visible} = layers[layerIndex];
|
|
198
|
+
if (!layerColor)
|
|
199
|
+
continue;
|
|
200
|
+
ASSERT(dataLayer.data && dataLayer.data.length, 'tile layer has no data, infinite maps and compressed layers are not supported');
|
|
201
|
+
ASSERT(levelSize.area() === dataLayer.data.length);
|
|
202
|
+
|
|
203
|
+
const layerRenderOrder = renderOrder - (layerCount - 1 - layerIndex);
|
|
204
|
+
const tileLayer = new TileCollisionLayer(vec2(), levelSize, tileInfo, layerRenderOrder);
|
|
205
|
+
tileLayer.isSolid = layerIndex === collisionLayer; // the others are art, the solid tests skip them
|
|
206
|
+
tileLayers[layerIndex] = tileLayer;
|
|
207
|
+
if (!visible)
|
|
208
|
+
tileLayer.render = ()=> {}; // a hidden layer keeps its tiles and collision but is not drawn
|
|
209
|
+
|
|
210
|
+
for (let x=levelSize.x; x--;)
|
|
211
|
+
for (let y=levelSize.y; y--;)
|
|
212
|
+
{
|
|
213
|
+
const pos = vec2(x, levelSize.y-1-y);
|
|
214
|
+
const data = dataLayer.data[x + y*levelSize.x];
|
|
215
|
+
if (data)
|
|
216
|
+
{
|
|
217
|
+
// Tiled keeps a tile's flips in its top bits, horizontal, vertical and diagonal, the diagonal
|
|
218
|
+
// applied first; each of the 8 is a quarter turn direction with or without a mirror
|
|
219
|
+
const [direction, mirror] = tileLayersTiledFlips[data >>> 29];
|
|
220
|
+
const tileIndex = (data & 0x0fffffff) - 1; // bit 28, a hexagonal turn, is not read
|
|
221
|
+
const layerData = new TileLayerData(tileIndex, direction, !!mirror, layerColor);
|
|
222
|
+
tileLayer.setData(pos, layerData);
|
|
223
|
+
|
|
224
|
+
// set collision for top layer
|
|
225
|
+
if (layerIndex === collisionLayer)
|
|
226
|
+
tileLayer.setCollisionData(pos, 1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (draw && visible)
|
|
230
|
+
tileLayer.redraw();
|
|
231
|
+
}
|
|
232
|
+
editorMapLoaded(tileMapData, tileLayers, layers);
|
|
233
|
+
return tileLayers;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// a color as Tiled writes it, #AARRGGBB, or #RRGGBB
|
|
237
|
+
function tileLayersColor(hex)
|
|
238
|
+
{ return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
|
|
239
|
+
|
|
240
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
241
|
+
// Object layers
|
|
242
|
+
|
|
243
|
+
// the types a map's objects are made from, by the name they have in Tiled
|
|
244
|
+
const objectLayersTypes = new Map;
|
|
245
|
+
|
|
246
|
+
/** Add a type of object, so objectLayersLoad makes one wherever a map's object layer has an object of that type
|
|
247
|
+
* - The name is the object's type in Tiled (its class in Tiled 1.9); it is a string because minified builds
|
|
248
|
+
* rename classes
|
|
249
|
+
* - A class, or any function with a prototype, is made with new make(pos); an arrow function is called as
|
|
250
|
+
* make(pos), for what is not an object, like a player start
|
|
251
|
+
* - The defaults, and then the properties the object has in Tiled, are set on what it made
|
|
252
|
+
* - Adding a name again replaces it
|
|
253
|
+
* @param {string} name - The type the objects have in Tiled
|
|
254
|
+
* @param {Function} make - A class made at each object's position, or a function called with it
|
|
255
|
+
* @param {Object} [defaults] - Properties set on each one made, the level editor shows inputs for them
|
|
256
|
+
* @param {TileInfo} [tileInfo] - An icon for the level editor
|
|
257
|
+
* @memberof TileLayers
|
|
258
|
+
* @example
|
|
259
|
+
* objectLayersAddType('Coin', Coin, {value: 1}, tile(5, 16));
|
|
260
|
+
* objectLayersAddType('PlayerStart', (pos)=> playerStartPos = pos); */
|
|
261
|
+
function objectLayersAddType(name, make, defaults={}, tileInfo)
|
|
262
|
+
{
|
|
263
|
+
ASSERT(isStringLike(name), 'object type name must be a string');
|
|
264
|
+
ASSERT(typeof make === 'function', 'make must be a class or function');
|
|
265
|
+
ASSERT(!!defaults && typeof defaults === 'object', 'defaults must be an object');
|
|
266
|
+
objectLayersTypes.set(String(name), {make, defaults, tileInfo});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Make the objects in a map's object layers, each from the type added for its name with objectLayersAddType
|
|
270
|
+
* - An object is made at its position, the world y up as tileLayersLoad places the layers; layer offsets are
|
|
271
|
+
* not read, and a shape or tile object is made at its position too
|
|
272
|
+
* - Group layers are flattened in order, as tileLayersLoad does
|
|
273
|
+
* - The object's properties in Tiled are set over the type's defaults: numbers, booleans, strings, and colors,
|
|
274
|
+
* and for a Vector2 default the string x,y
|
|
275
|
+
* - An object whose type was not added is skipped, with a warning in debug builds
|
|
276
|
+
* @param {Object} tileMapData - The same Tiled map given to tileLayersLoad
|
|
277
|
+
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
278
|
+
* @memberof TileLayers */
|
|
279
|
+
function objectLayersLoad(tileMapData)
|
|
280
|
+
{
|
|
281
|
+
// the level editor, in debug builds, keeps the map and brings back its autosaved objects before they are made
|
|
282
|
+
tileMapData && editorMapRestore(tileMapData);
|
|
283
|
+
const made = [];
|
|
284
|
+
const addObjects = (dataLayers)=>
|
|
285
|
+
{
|
|
286
|
+
for (const dataLayer of dataLayers || [])
|
|
287
|
+
{
|
|
288
|
+
if (dataLayer.type === 'group')
|
|
289
|
+
addObjects(dataLayer.layers);
|
|
290
|
+
if (dataLayer.type !== 'objectgroup')
|
|
291
|
+
continue;
|
|
292
|
+
for (const object of dataLayer.objects || [])
|
|
293
|
+
{
|
|
294
|
+
const result = objectLayersMake(tileMapData, object);
|
|
295
|
+
editorObjectMade(tileMapData, dataLayer, object, result); // debug builds link it for the level editor
|
|
296
|
+
result && made.push(result);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
addObjects(tileMapData?.layers);
|
|
301
|
+
return made;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// the defaults of an object's type, a Color or Vector2 copied for each, then its properties in Tiled over them
|
|
305
|
+
function objectLayersProperties(type, object)
|
|
306
|
+
{
|
|
307
|
+
const properties = {};
|
|
308
|
+
for (const [key, value] of Object.entries(type.defaults))
|
|
309
|
+
properties[key] = value?.copy ? value.copy() : value;
|
|
310
|
+
for (const property of object.properties || [])
|
|
311
|
+
{
|
|
312
|
+
const {name, value} = property;
|
|
313
|
+
if (property.type === 'color')
|
|
314
|
+
value && (properties[name] = tileLayersColor(value)); // an empty color is unset
|
|
315
|
+
else if (isVector2(type.defaults[name]))
|
|
316
|
+
{
|
|
317
|
+
// Tiled has no Vector2, one is the string x,y, and one it can not read keeps the default
|
|
318
|
+
const parts = String(value).split(','), [x, y] = parts.map(Number);
|
|
319
|
+
parts.length === 2 && parts.every((part)=> part.trim()) && isNumber(x) && isNumber(y) &&
|
|
320
|
+
(properties[name] = vec2(x, y));
|
|
321
|
+
}
|
|
322
|
+
else
|
|
323
|
+
properties[name] = value;
|
|
324
|
+
}
|
|
325
|
+
return properties;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// make one Tiled object from the type added for its name, undefined when there is no such type or it made nothing
|
|
329
|
+
function objectLayersMake(tileMapData, object)
|
|
330
|
+
{
|
|
331
|
+
const name = object.type || object.class, type = objectLayersTypes.get(name);
|
|
332
|
+
if (!type)
|
|
333
|
+
{
|
|
334
|
+
debug && console.warn(`objectLayersLoad: no type added for ${name}, skipped`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const {height=0, tilewidth=1, tileheight=1} = tileMapData;
|
|
338
|
+
const pos = vec2(object.x / tilewidth, height - object.y / tileheight);
|
|
339
|
+
const {make} = type, result = make.prototype ? new make(pos) : make(pos);
|
|
340
|
+
if (!result || typeof result !== 'object') return;
|
|
341
|
+
return Object.assign(result, objectLayersProperties(type, object));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
345
|
+
/**
|
|
346
|
+
* Tile layer data object stores info about how to draw a tile
|
|
347
|
+
* @memberof TileLayers
|
|
348
|
+
* @example
|
|
349
|
+
* // create tile layer data with tile index 0 and random orientation and color
|
|
350
|
+
* const tileIndex = 0;
|
|
351
|
+
* const direction = randInt(4)
|
|
352
|
+
* const mirror = randBool();
|
|
353
|
+
* const color = randColor();
|
|
354
|
+
* const data = new TileLayerData(tileIndex, direction, mirror, color);
|
|
355
|
+
*/
|
|
356
|
+
class TileLayerData
|
|
357
|
+
{
|
|
358
|
+
/** Create a tile layer data object, one for each tile in a TileLayer
|
|
359
|
+
* @param {number} [tile] - The tile to use, from 0 like tile(); undefined is an empty cell that draws nothing
|
|
360
|
+
* @param {number} [direction] - Integer direction of tile, in 90 degree increments
|
|
361
|
+
* @param {boolean} [mirror] - If the tile should be mirrored along the x axis
|
|
362
|
+
* @param {Color} [color] - Color of the tile */
|
|
363
|
+
constructor(tile, direction=0, mirror=false, color=new Color)
|
|
364
|
+
{
|
|
365
|
+
/** @property {number|undefined} - The tile to use, from 0 like tile(); undefined is an empty cell that draws nothing
|
|
366
|
+
* @type {number|undefined} */
|
|
367
|
+
this.tile = tile;
|
|
368
|
+
/** @property {number} - Integer direction of tile, in 90 degree increments */
|
|
369
|
+
this.direction = direction;
|
|
370
|
+
/** @property {boolean} - If the tile should be mirrored along the x axis */
|
|
371
|
+
this.mirror = mirror;
|
|
372
|
+
/** @property {Color} - Color of the tile */
|
|
373
|
+
this.color = color.copy();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Set this tile to clear, it will not be rendered */
|
|
377
|
+
clear() { this.tile = undefined; this.direction = 0; this.mirror = false; this.color = new Color; }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
381
|
+
/**
|
|
382
|
+
* Canvas Layer - cached off screen rendering system
|
|
383
|
+
* - Contains an offscreen canvas that can be rendered to
|
|
384
|
+
* - WebGL rendering is optional, call updateWebGL to enable/update
|
|
385
|
+
* - A TileLayer using WebGL redraws into its texture and leaves this canvas blank, so drawing on its context
|
|
386
|
+
* only shows on a layer made with useWebGL=false (or with WebGL off); use drawLayerTile/drawLayerRect inside
|
|
387
|
+
* redrawStart/End for drawing that works both ways
|
|
388
|
+
* @extends EngineObject
|
|
389
|
+
* @memberof TileLayers
|
|
390
|
+
* @example
|
|
391
|
+
* const canvasLayer = new CanvasLayer(vec2(), vec2(200,100));
|
|
392
|
+
*/
|
|
393
|
+
class CanvasLayer extends EngineObject
|
|
394
|
+
{
|
|
395
|
+
/** Create a canvas layer object
|
|
396
|
+
* @param {Vector2} [pos] - World space position of the layer
|
|
397
|
+
* @param {Vector2} [size] - World space size of the layer
|
|
398
|
+
* @param {number} [angle] - Angle the layer is rotated by
|
|
399
|
+
* @param {number} [renderOrder] - Objects sorted by renderOrder
|
|
400
|
+
* @param {Vector2} [canvasSize] - Default size of canvas, can be changed later
|
|
401
|
+
* @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
|
|
402
|
+
*/
|
|
403
|
+
constructor(pos, size, angle=0, renderOrder=0, canvasSize=vec2(512), useWebGL=true)
|
|
404
|
+
{
|
|
405
|
+
ASSERT(isVector2(canvasSize), 'canvasSize must be a Vector2');
|
|
406
|
+
super(pos, size, undefined, angle, WHITE, renderOrder);
|
|
407
|
+
|
|
408
|
+
/** @property {OffscreenCanvasRenderingContext2D} - The 2D canvas context used by this layer */
|
|
409
|
+
this.context = headlessMode ? undefined : createCanvasContext(canvasSize.x, canvasSize.y);
|
|
410
|
+
/** @property {OffscreenCanvas} - The canvas used by this layer */
|
|
411
|
+
this.canvas = this.context?.canvas;
|
|
412
|
+
/** @property {TextureInfo} - Texture info to use for this object rendering */
|
|
413
|
+
this.textureInfo = new TextureInfo(this.canvas, useWebGL);
|
|
414
|
+
|
|
415
|
+
// a texture past the device's limit fails with only a WebGL warning and draws black, and phones often
|
|
416
|
+
// allow 4096 where a desktop allows 16384, so say so in release builds too
|
|
417
|
+
const maxSize = useWebGL && glContext ? glContext.getParameter(glContext.MAX_TEXTURE_SIZE) : 0;
|
|
418
|
+
if (maxSize && max(canvasSize.x, canvasSize.y) > maxSize)
|
|
419
|
+
console.warn(`LittleJS: a ${canvasSize.x}x${canvasSize.y} layer is over this device's ${maxSize} pixel texture limit and draws black, split it into smaller layers`);
|
|
420
|
+
|
|
421
|
+
// disable physics by default
|
|
422
|
+
this.mass = 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Destroy this canvas layer
|
|
426
|
+
* @param {boolean} [immediate] - Remove it now, as EngineObject.destroy does, children included */
|
|
427
|
+
destroy(immediate=false)
|
|
428
|
+
{
|
|
429
|
+
if (this.destroyed) return;
|
|
430
|
+
|
|
431
|
+
this.textureInfo.destroyWebGLTexture();
|
|
432
|
+
super.destroy(immediate);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Render the layer, called automatically by the engine
|
|
436
|
+
render()
|
|
437
|
+
{
|
|
438
|
+
this.draw(this.pos, this.size, this.color, this.angle, this.mirror, this.additiveColor, false);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Draw this canvas layer centered in world space
|
|
442
|
+
* @param {Vector2} pos - Center in world space
|
|
443
|
+
* @param {Vector2} [size] - Size in world space
|
|
444
|
+
* @param {Color} [color] - Color to modulate with
|
|
445
|
+
* @param {number} [angle] - Angle to rotate by
|
|
446
|
+
* @param {boolean} [mirror] - If true image is flipped along the Y axis
|
|
447
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any
|
|
448
|
+
* @param {boolean} [screenSpace=drawScreenSpace] - If true the pos and size are in screen space
|
|
449
|
+
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context] - Canvas 2D context to draw to */
|
|
450
|
+
draw(pos, size, color=WHITE, angle=0, mirror=false, additiveColor, screenSpace=drawScreenSpace, context)
|
|
451
|
+
{
|
|
452
|
+
// the canvas may have been resized since, updateWebGL only refreshes the size for WebGL
|
|
453
|
+
const t = this.textureInfo, c = this.canvas;
|
|
454
|
+
if (c && !this.hasWebGL() && (c.width !== t.size.x || c.height !== t.size.y))
|
|
455
|
+
{
|
|
456
|
+
t.size = vec2(c.width, c.height);
|
|
457
|
+
t.sizeInverse = vec2(1/c.width, 1/c.height);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// draw the canvas layer as a single tile that uses the whole texture
|
|
461
|
+
const tileInfo = new TileInfo().setFullImage(t);
|
|
462
|
+
const useWebGL = !context && this.hasWebGL(); // a context given is drawn to with Canvas2D
|
|
463
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Create WebGL texture if necessary and copy layer canvas to it */
|
|
467
|
+
updateWebGL()
|
|
468
|
+
{ this.textureInfo.createWebGLTexture(); }
|
|
469
|
+
|
|
470
|
+
/** Check if this layer is using WebGL
|
|
471
|
+
* @return {boolean} */
|
|
472
|
+
hasWebGL()
|
|
473
|
+
{ return glEnable && this.textureInfo.hasWebGL(); }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
477
|
+
// a layer's default tile, none when no image is loaded, so a collision only layer works in a game with no images
|
|
478
|
+
function tileLayerDefaultTile() { return textureInfos[0]?.size.x ? tile() : undefined; }
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Tile Layer - cached rendering system for tile layers
|
|
482
|
+
* - Tiles are drawn once into a texture, a WebGL render target, or the layer's canvas when WebGL is off
|
|
483
|
+
* or useWebGL is false, and the layer draws that as one image
|
|
484
|
+
* - Some devices like mobile phones are limited to 4k textures, which with 16x16 tiles limits a layer to 256x256
|
|
485
|
+
* - Tile layers are centered on their corner, so normal levels are at (0,0)
|
|
486
|
+
* @extends CanvasLayer
|
|
487
|
+
* @memberof TileLayers
|
|
488
|
+
* @example
|
|
489
|
+
* const tileLayer = new TileLayer(vec2(), vec2(200,100));
|
|
490
|
+
*/
|
|
491
|
+
class TileLayer extends CanvasLayer
|
|
492
|
+
{
|
|
493
|
+
/** Create a tile layer object
|
|
494
|
+
* @param {Vector2} pos - World space position
|
|
495
|
+
* @param {Vector2} size - World space size
|
|
496
|
+
* @param {TileInfo} [tileInfo] - Default tile info for layer (used for size and texture), tile() by default, none when no image is loaded
|
|
497
|
+
* @param {number} [renderOrder] - Objects are sorted by renderOrder
|
|
498
|
+
* @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
|
|
499
|
+
*/
|
|
500
|
+
constructor(pos, size, tileInfo=tileLayerDefaultTile(), renderOrder=0, useWebGL=true)
|
|
501
|
+
{
|
|
502
|
+
ASSERT(!tileInfo || tileInfo.size.x > 0 && tileInfo.size.y > 0,
|
|
503
|
+
'the tile has no size yet, a loadSprite tile is filled in once spritesReady resolves');
|
|
504
|
+
size = size.floor(); // whole cells, a fractional size would never finish filling the data
|
|
505
|
+
const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
|
|
506
|
+
super(pos, size, 0, renderOrder, canvasSize, useWebGL);
|
|
507
|
+
|
|
508
|
+
/** @property {TileInfo|undefined} - Default tile info for layer
|
|
509
|
+
* @type {TileInfo|undefined} */
|
|
510
|
+
this.tileInfo = undefined;
|
|
511
|
+
/** @property {Array<TileLayerData>} - Array of tile data for the layer */
|
|
512
|
+
this.data = [];
|
|
513
|
+
/** @property {boolean} - Is this layer using a webgl texture? */
|
|
514
|
+
this.isUsingWebGL = false;
|
|
515
|
+
// which side holds the whole layer, set by a full redraw, undefined before the first one; a partial redraw
|
|
516
|
+
// or a render that finds WebGL turned on or off since then draws it all again on the side now in use
|
|
517
|
+
this.tilesInWebGL = undefined;
|
|
518
|
+
/** @property {boolean} - Show this layer's bounds and values when the debug overlay's Debug Tiles is on,
|
|
519
|
+
* turn it off for layers that only add noise */
|
|
520
|
+
this.debugShow = true;
|
|
521
|
+
|
|
522
|
+
if (tileInfo)
|
|
523
|
+
{
|
|
524
|
+
// set tile info
|
|
525
|
+
this.tileInfo = tileInfo.frame(0);
|
|
526
|
+
this.tileInfo.bleed = 0; // disable bleed for tile layers
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// init tile data
|
|
530
|
+
for (let j = this.size.area(); j--;)
|
|
531
|
+
this.data.push(new TileLayerData);
|
|
532
|
+
|
|
533
|
+
if (headlessMode)
|
|
534
|
+
{
|
|
535
|
+
// disable rendering in headless mode
|
|
536
|
+
this.render = ()=> {};
|
|
537
|
+
this.redraw = ()=> {};
|
|
538
|
+
this.redrawStart = ()=> {};
|
|
539
|
+
this.redrawEnd = ()=> {};
|
|
540
|
+
this.drawTileData = ()=> {};
|
|
541
|
+
this.redrawTileData = ()=> {};
|
|
542
|
+
this.drawLayerTile = ()=> {};
|
|
543
|
+
this.drawLayerRect = ()=> {};
|
|
544
|
+
this.drawTile = ()=> {};
|
|
545
|
+
this.drawRect = ()=> {};
|
|
546
|
+
this.clearLayerRect = ()=> {};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Set data at a given position in the array
|
|
551
|
+
* @param {Vector2} layerPos - Local position in array
|
|
552
|
+
* @param {TileLayerData} data - Data to set
|
|
553
|
+
* @param {boolean} [redraw] - Force the tile to redraw if true */
|
|
554
|
+
setData(layerPos, data, redraw=false)
|
|
555
|
+
{
|
|
556
|
+
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
557
|
+
ASSERT(data instanceof TileLayerData, 'data must be a TileLayerData');
|
|
558
|
+
layerPos = layerPos.floor();
|
|
559
|
+
|
|
560
|
+
if (!layerPos.arrayCheck(this.size)) return;
|
|
561
|
+
this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] = data;
|
|
562
|
+
|
|
563
|
+
if (!redraw) return;
|
|
564
|
+
const isRedraw = drawContext === this.context;
|
|
565
|
+
isRedraw ? this.drawTileData(layerPos) : this.redrawTileData(layerPos);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Clear data at a given position in the array
|
|
569
|
+
* @param {Vector2} layerPos - Local position in array
|
|
570
|
+
* @param {boolean} [redraw] - Force the tile to redraw if true */
|
|
571
|
+
clearData(layerPos, redraw=false)
|
|
572
|
+
{ this.setData(layerPos, new TileLayerData, redraw) }
|
|
573
|
+
|
|
574
|
+
/** Get data at a given position in the array
|
|
575
|
+
* @param {Vector2} layerPos - Local position in array
|
|
576
|
+
* @return {TileLayerData|undefined} */
|
|
577
|
+
getData(layerPos)
|
|
578
|
+
{
|
|
579
|
+
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
580
|
+
return layerPos.arrayCheck(this.size) ? this.data[(layerPos.y|0)*this.size.x + (layerPos.x|0)] : undefined;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Update the tile layer, a layer has no physics
|
|
584
|
+
update() {}
|
|
585
|
+
|
|
586
|
+
// Render the tile layer, called automatically by the engine
|
|
587
|
+
render()
|
|
588
|
+
{
|
|
589
|
+
ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
|
|
590
|
+
|
|
591
|
+
// redraw here, not in update, which does not run while paused, if WebGL was turned off or lost, or came back
|
|
592
|
+
this.redrawIfSwitched();
|
|
593
|
+
|
|
594
|
+
const size = this.drawSize || this.size;
|
|
595
|
+
const pos = this.pos.add(size.scale(.5));
|
|
596
|
+
this.draw(pos, size, this.color, this.angle, this.mirror, this.additiveColor, false);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Called after this layer is redrawn, does nothing by default */
|
|
600
|
+
onRedraw() {}
|
|
601
|
+
|
|
602
|
+
/** Draw all the tile data to an offscreen canvas
|
|
603
|
+
* - This may be slow if not using webgl but only needs to be done once */
|
|
604
|
+
redraw()
|
|
605
|
+
{
|
|
606
|
+
this.redrawStart(true);
|
|
607
|
+
for (let x = this.size.x; x--;)
|
|
608
|
+
for (let y = this.size.y; y--;)
|
|
609
|
+
this.drawTileData(vec2(x,y), false);
|
|
610
|
+
this.isUsingWebGL && glFlush();
|
|
611
|
+
this.onRedraw();
|
|
612
|
+
this.redrawEnd();
|
|
613
|
+
this.tilesInWebGL = this.isUsingWebGL;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// draw the whole layer again if the side that holds it is not the one in use now
|
|
617
|
+
redrawIfSwitched()
|
|
618
|
+
{
|
|
619
|
+
if (this.tilesInWebGL !== undefined && this.hasWebGL() !== this.tilesInWebGL)
|
|
620
|
+
this.redraw();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Call to start the redraw process
|
|
624
|
+
* - This can be used to manually update parts of the level
|
|
625
|
+
* @param {boolean} [clear] - Should it clear the canvas before drawing */
|
|
626
|
+
redrawStart(clear=false)
|
|
627
|
+
{
|
|
628
|
+
if (!this.context) return;
|
|
629
|
+
ASSERT(drawContext !== this.context);
|
|
630
|
+
clear || this.redrawIfSwitched(); // a partial redraw goes on top of the whole layer on the side in use
|
|
631
|
+
|
|
632
|
+
// save current render settings
|
|
633
|
+
/** @type {[CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D, Vector2, Vector2, number, number, Color, Shader|undefined]} */
|
|
634
|
+
this.savedRenderSettings = [drawContext, mainCanvasSize, cameraPos, cameraScale, cameraAngle, canvasClearColor, glCustomShader];
|
|
635
|
+
setShader(); // the tiles are drawn plain, a layer's own Shader applies when the layer is drawn
|
|
636
|
+
// a redraw from inside another target's pass, like the light system's shadow map, draws the tiles in color
|
|
637
|
+
// and hands that target back after
|
|
638
|
+
this.savedRenderTarget = [glRenderTarget, glColorMask, glColorAdditive, glSkipScreenSpace];
|
|
639
|
+
glColorMask = -1;
|
|
640
|
+
glColorAdditive = 0;
|
|
641
|
+
glSkipScreenSpace = false; // screen space is the layer's own pixels here
|
|
642
|
+
|
|
643
|
+
// set the draw canvas and context to this layer
|
|
644
|
+
// use camera settings to match this layer's canvas
|
|
645
|
+
drawContext = this.context;
|
|
646
|
+
const tileSize = this.tileInfo?.size ?? vec2(1);
|
|
647
|
+
mainCanvasSize = this.size.multiply(tileSize);
|
|
648
|
+
canvasClearColor = CLEAR_BLACK;
|
|
649
|
+
cameraPos = this.size.multiply(tileSize).scale(.5);
|
|
650
|
+
cameraScale = 1;
|
|
651
|
+
cameraAngle = 0; // the tiles are drawn flat, the world camera turns the whole layer later
|
|
652
|
+
|
|
653
|
+
// set render target to this layer
|
|
654
|
+
this.isUsingWebGL = this.hasWebGL();
|
|
655
|
+
if (this.isUsingWebGL)
|
|
656
|
+
glSetRenderTarget(this.textureInfo.glTexture, clear);
|
|
657
|
+
else
|
|
658
|
+
{
|
|
659
|
+
if (clear)
|
|
660
|
+
{
|
|
661
|
+
// clear and set size
|
|
662
|
+
this.canvas.width = mainCanvasSize.x;
|
|
663
|
+
this.canvas.height = mainCanvasSize.y;
|
|
664
|
+
}
|
|
665
|
+
// disable smoothing for pixel art, after the resize which resets it
|
|
666
|
+
this.context.imageSmoothingEnabled = !tilesPixelated;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Call to end the redraw process */
|
|
671
|
+
redrawEnd()
|
|
672
|
+
{
|
|
673
|
+
if (!this.context) return;
|
|
674
|
+
ASSERT(drawContext === this.context);
|
|
675
|
+
|
|
676
|
+
// set stuff back to normal, the camera first, so a target that was drawing before gets its own transform back
|
|
677
|
+
[drawContext, mainCanvasSize, cameraPos, cameraScale, cameraAngle, canvasClearColor, glCustomShader] = this.savedRenderSettings;
|
|
678
|
+
const [target, colorMask, colorAdditive, skipScreenSpace] = this.savedRenderTarget;
|
|
679
|
+
if (this.isUsingWebGL)
|
|
680
|
+
glSetRenderTarget(target);
|
|
681
|
+
glColorMask = colorMask;
|
|
682
|
+
glColorAdditive = colorAdditive;
|
|
683
|
+
glSkipScreenSpace = skipScreenSpace;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/** Draw the tile at a given position in the tile layer
|
|
687
|
+
* This can be used to clear out tiles when they are destroyed
|
|
688
|
+
* Tiles can also be redrawn if inside a redrawStart/End block
|
|
689
|
+
* @param {Vector2} layerPos
|
|
690
|
+
* @param {boolean} [clear] - should the old tile be cleared out
|
|
691
|
+
*/
|
|
692
|
+
drawTileData(layerPos, clear=true)
|
|
693
|
+
{
|
|
694
|
+
if (!this.context) return;
|
|
695
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before drawing tiles');
|
|
696
|
+
|
|
697
|
+
// clear out where the tile was, can be skipped for fully opaque tiles
|
|
698
|
+
const cellPixels = this.tileInfo?.size ?? vec2(1);
|
|
699
|
+
const drawPos = layerPos.multiply(cellPixels);
|
|
700
|
+
clear && this.clearLayerRect(drawPos, cellPixels);
|
|
701
|
+
|
|
702
|
+
// draw the tile if it has layer data, an empty cell has no tile and tile 0 is a tile like any other
|
|
703
|
+
const d = this.getData(layerPos);
|
|
704
|
+
if (!d || d.tile === undefined) return;
|
|
705
|
+
|
|
706
|
+
// a tileset packed by loadSprite keeps its own columns, counted from its first tile, not the sheet's grid
|
|
707
|
+
const t = this.tileInfo, tileInfo = t && (t.columns ? t.frame(d.tile) : t.index(d.tile));
|
|
708
|
+
this.drawLayerTile(drawPos, cellPixels, tileInfo, d.color, d.direction*PI/2, d.mirror);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** Draw the tile at a given position in the tile layer
|
|
712
|
+
* This can be used to clear tiles when they are destroyed
|
|
713
|
+
* For better performance use drawTileData inside a redrawStart/End block
|
|
714
|
+
* @param {Vector2} layerPos
|
|
715
|
+
* @param {boolean} [clear] - should the old tile be cleared
|
|
716
|
+
*/
|
|
717
|
+
redrawTileData(layerPos, clear=true)
|
|
718
|
+
{
|
|
719
|
+
if (!this.context) return;
|
|
720
|
+
ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
|
|
721
|
+
|
|
722
|
+
this.redrawStart();
|
|
723
|
+
this.drawTileData(layerPos, clear);
|
|
724
|
+
this.redrawEnd();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** Draw textured tile in layer space
|
|
728
|
+
* @param {Vector2} pos - Position in pixel coordinates
|
|
729
|
+
* @param {Vector2} [size=vec2(1)] - Size of the tile
|
|
730
|
+
* @param {TileInfo} [tileInfo] - Tile info to use, untextured if undefined
|
|
731
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
732
|
+
* @param {number} [angle] - Angle to rotate by
|
|
733
|
+
* @param {boolean} [mirror] - Is image flipped along the Y axis?
|
|
734
|
+
* @param {Color} [additiveColor] - Additive color to be applied if any */
|
|
735
|
+
drawLayerTile(pos, size=vec2(1), tileInfo, color=WHITE,
|
|
736
|
+
angle=0, mirror, additiveColor)
|
|
737
|
+
{
|
|
738
|
+
const drawPos = pos.add(size.scale(.5));
|
|
739
|
+
drawTile(drawPos, size, tileInfo, color, angle, mirror, additiveColor, this.isUsingWebGL, false);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Draw a rectangle in layer space
|
|
743
|
+
* @param {Vector2} pos
|
|
744
|
+
* @param {Vector2} size
|
|
745
|
+
* @param {Color} [color=WHITE] - Color to modulate with
|
|
746
|
+
* @param {number} [angle] - Angle to rotate by
|
|
747
|
+
*/
|
|
748
|
+
drawLayerRect(pos, size, color, angle=0)
|
|
749
|
+
{ this.drawLayerTile(pos, size, undefined, color, angle); }
|
|
750
|
+
|
|
751
|
+
/** Draw a tile onto the layer canvas in world space
|
|
752
|
+
* @param {Vector2} pos
|
|
753
|
+
* @param {Vector2} [size=vec2(1)]
|
|
754
|
+
* @param {TileInfo} [tileInfo]
|
|
755
|
+
* @param {Color} [color=WHITE]
|
|
756
|
+
* @param {number} [angle]
|
|
757
|
+
* @param {boolean} [mirror] */
|
|
758
|
+
drawTile(pos, size=vec2(1), tileInfo, color=new Color, angle=0, mirror=false)
|
|
759
|
+
{
|
|
760
|
+
const tileSize = this.tileInfo?.size ?? vec2(1); // a layer made without a tile info draws a pixel a cell
|
|
761
|
+
pos = pos.subtract(this.pos).multiply(tileSize);
|
|
762
|
+
size = size.multiply(tileSize);
|
|
763
|
+
// a screen position is the center of a pixel, so the layer pixel coordinate moves back half a pixel
|
|
764
|
+
pos.x -= .5;
|
|
765
|
+
pos.y = this.canvas.height - pos.y - .5;
|
|
766
|
+
|
|
767
|
+
// draw the tile onto the layer canvas
|
|
768
|
+
// in color and handing back a target that was drawing before, like the light system's shadow map
|
|
769
|
+
const oldMainCanvasSize = mainCanvasSize, oldTarget = glRenderTarget, oldColorMask = glColorMask;
|
|
770
|
+
const oldSkip = glSkipScreenSpace, oldColorAdditive = glColorAdditive, oldShader = glCustomShader;
|
|
771
|
+
mainCanvasSize = vec2(this.canvas.width, this.canvas.height);
|
|
772
|
+
glColorMask = -1;
|
|
773
|
+
glColorAdditive = 0;
|
|
774
|
+
setShader(); // plain, as a redraw draws, a layer's own Shader applies when the layer is drawn
|
|
775
|
+
glSkipScreenSpace = false; // its screen space is the layer's own canvas
|
|
776
|
+
const useWebGL = this.hasWebGL();
|
|
777
|
+
useWebGL && glSetRenderTarget(this.textureInfo.glTexture);
|
|
778
|
+
const drawContext = useWebGL ? undefined : this.context;
|
|
779
|
+
drawTile(pos, size, tileInfo, color, angle, mirror, undefined, useWebGL, true, drawContext);
|
|
780
|
+
mainCanvasSize = oldMainCanvasSize;
|
|
781
|
+
useWebGL && glSetRenderTarget(oldTarget);
|
|
782
|
+
glColorMask = oldColorMask;
|
|
783
|
+
glColorAdditive = oldColorAdditive;
|
|
784
|
+
glSkipScreenSpace = oldSkip;
|
|
785
|
+
setShader(oldShader);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Draw a rectangle onto the layer canvas in world space
|
|
789
|
+
* @param {Vector2} pos
|
|
790
|
+
* @param {Vector2} [size=vec2(1)]
|
|
791
|
+
* @param {Color} [color=WHITE]
|
|
792
|
+
* @param {number} [angle] */
|
|
793
|
+
drawRect(pos, size, color, angle)
|
|
794
|
+
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
795
|
+
|
|
796
|
+
/** Clear a rectangle in layer space
|
|
797
|
+
* @param {Vector2} pos - position in pixel coordinates
|
|
798
|
+
* @param {Vector2} size
|
|
799
|
+
*/
|
|
800
|
+
clearLayerRect(pos, size)
|
|
801
|
+
{
|
|
802
|
+
ASSERT(drawContext === this.context, 'must call redrawStart() before clearing tiles');
|
|
803
|
+
|
|
804
|
+
const x = pos.x, y = this.canvas.height - pos.y - size.y;
|
|
805
|
+
const useWebGL = this.hasWebGL();
|
|
806
|
+
if (useWebGL)
|
|
807
|
+
glClearRect(x, y, size.x, size.y);
|
|
808
|
+
else
|
|
809
|
+
this.context.clearRect(x, y, size.x, size.y);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
814
|
+
/**
|
|
815
|
+
* Tile Collision Layer - a tile layer with collision
|
|
816
|
+
* - adds collision data and functions to TileLayer
|
|
817
|
+
* - there can be multiple tile collision layers
|
|
818
|
+
* - its pos must be whole numbers, so its cells line up with the world grid objects land on
|
|
819
|
+
* @extends TileLayer
|
|
820
|
+
* @memberof TileLayers
|
|
821
|
+
*/
|
|
822
|
+
class TileCollisionLayer extends TileLayer
|
|
823
|
+
{
|
|
824
|
+
/** Create a tile layer object
|
|
825
|
+
* @param {Vector2} pos - World space position
|
|
826
|
+
* @param {Vector2} size - World space size
|
|
827
|
+
* @param {TileInfo} [tileInfo] - Tile info for layer, tile() by default, none when no image is loaded
|
|
828
|
+
* @param {number} [renderOrder] - Objects are sorted by renderOrder
|
|
829
|
+
* @param {boolean} [useWebGL] - Should this layer use WebGL for rendering
|
|
830
|
+
*/
|
|
831
|
+
constructor(pos, size, tileInfo=tileLayerDefaultTile(), renderOrder=0, useWebGL=true)
|
|
832
|
+
{
|
|
833
|
+
super(pos, size.floor(), tileInfo, renderOrder, useWebGL);
|
|
834
|
+
|
|
835
|
+
/** @property {Array<number>} - The tile collision grid
|
|
836
|
+
* @type {Array<number>} */
|
|
837
|
+
this.collisionData = [];
|
|
838
|
+
this.initCollision(this.size);
|
|
839
|
+
|
|
840
|
+
// keep track of all collision layers
|
|
841
|
+
tileCollisionLayers.push(this);
|
|
842
|
+
|
|
843
|
+
/** @property {boolean} - Solid layers block objects and particles, the solidOnly tests skip the others */
|
|
844
|
+
this.isSolid = true;
|
|
845
|
+
/** @property {boolean} - In the light system's shadow pass, cast only from the cells with collision, drawn
|
|
846
|
+
* as the layer shows them, so a floor in the same layer stays lit; false casts every tile */
|
|
847
|
+
this.shadowSolidOnly = true;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Draw this layer's shadow shape: the cells with collision in the part the shadow map covers, each row of
|
|
851
|
+
* them in one draw from that part of the layer's texture, so see through pixels in a tile cast nothing;
|
|
852
|
+
* every tile when shadowSolidOnly is off, or when the layer is turned or mirrored */
|
|
853
|
+
renderShadow()
|
|
854
|
+
{
|
|
855
|
+
if (!this.shadowSolidOnly || this.angle || this.mirror)
|
|
856
|
+
return super.renderShadow();
|
|
857
|
+
|
|
858
|
+
// the cells in view, which in the shadow pass is the shadow map
|
|
859
|
+
const size = this.size, drawSize = this.drawSize || size;
|
|
860
|
+
const cellWorld = drawSize.divide(size), cellPixels = this.tileInfo ? this.tileInfo.size : vec2(1);
|
|
861
|
+
const view = getCameraSize().scale(.5), low = cameraPos.subtract(view), high = cameraPos.add(view);
|
|
862
|
+
const x0 = max(0, floor((low.x - this.pos.x) / cellWorld.x)), x1 = min(size.x, ceil((high.x - this.pos.x) / cellWorld.x));
|
|
863
|
+
const y0 = max(0, floor((low.y - this.pos.y) / cellWorld.y)), y1 = min(size.y, ceil((high.y - this.pos.y) / cellWorld.y));
|
|
864
|
+
const textureHeight = size.y * cellPixels.y, useWebGL = this.hasWebGL();
|
|
865
|
+
for (let y = y0; y < y1; ++y)
|
|
866
|
+
for (let x = x0; x < x1; ++x)
|
|
867
|
+
{
|
|
868
|
+
if (!this.collisionData[y*size.x + x]) continue;
|
|
869
|
+
let end = x + 1; // a run of solid cells along the row
|
|
870
|
+
while (end < x1 && this.collisionData[y*size.x + end]) ++end;
|
|
871
|
+
const count = end - x;
|
|
872
|
+
const tileInfo = new TileInfo(vec2(x*cellPixels.x, textureHeight - (y+1)*cellPixels.y),
|
|
873
|
+
vec2(count*cellPixels.x, cellPixels.y), this.textureInfo, 0, 0);
|
|
874
|
+
const pos = vec2(this.pos.x + (x + count/2)*cellWorld.x, this.pos.y + (y + .5)*cellWorld.y);
|
|
875
|
+
drawTile(pos, vec2(count*cellWorld.x, cellWorld.y), tileInfo, this.color, 0, false, undefined, useWebGL, false);
|
|
876
|
+
x = end;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Destroy this tile layer
|
|
881
|
+
* @param {boolean} [immediate] - Remove it now, as EngineObject.destroy does, children included */
|
|
882
|
+
destroy(immediate=false)
|
|
883
|
+
{
|
|
884
|
+
if (this.destroyed) return;
|
|
885
|
+
|
|
886
|
+
// remove from collision layers array and destroy
|
|
887
|
+
const index = tileCollisionLayers.indexOf(this);
|
|
888
|
+
ASSERT(index >= 0, 'tile collision layer not found in array');
|
|
889
|
+
index >= 0 && tileCollisionLayers.splice(index, 1);
|
|
890
|
+
super.destroy(immediate);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** Clear and initialize tile collision, the size is the layer's own, the tile data and canvas keep it
|
|
894
|
+
* @param {Vector2} size - width and height of tile collision 2d grid */
|
|
895
|
+
initCollision(size)
|
|
896
|
+
{
|
|
897
|
+
ASSERT(isVector2(size), 'size must be a Vector2');
|
|
898
|
+
ASSERT(!this.collisionData.length || (size.x|0) === this.size.x && (size.y|0) === this.size.y, 'initCollision cannot resize a layer');
|
|
899
|
+
this.size = size.floor();
|
|
900
|
+
this.collisionData = new Array(this.size.area()).fill(0);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Set tile collision data for a given cell in the layer
|
|
904
|
+
* @param {Vector2} layerPos
|
|
905
|
+
* @param {number} [data] */
|
|
906
|
+
setCollisionData(layerPos, data=1)
|
|
907
|
+
{
|
|
908
|
+
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
909
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
910
|
+
layerPos.arrayCheck(this.size) && (this.collisionData[i] = data);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Clear tile collision data for a given cell in the layer
|
|
914
|
+
* @param {Vector2} layerPos */
|
|
915
|
+
clearCollisionData(layerPos)
|
|
916
|
+
{ this.setCollisionData(layerPos, 0); }
|
|
917
|
+
|
|
918
|
+
/** Get tile collision data for a given cell in the layer
|
|
919
|
+
* @param {Vector2} layerPos
|
|
920
|
+
* @return {number} */
|
|
921
|
+
getCollisionData(layerPos)
|
|
922
|
+
{
|
|
923
|
+
ASSERT(isVector2(layerPos), 'layerPos must be a Vector2');
|
|
924
|
+
const i = (layerPos.y|0)*this.size.x + (layerPos.x|0);
|
|
925
|
+
return layerPos.arrayCheck(this.size) ? this.collisionData[i] : 0;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/** Check if collision with another object should occur
|
|
929
|
+
* @param {Vector2} pos
|
|
930
|
+
* @param {Vector2} [size=vec2()]
|
|
931
|
+
* @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
|
|
932
|
+
* @return {boolean} */
|
|
933
|
+
collisionTest(pos, size=new Vector2, callbackObject)
|
|
934
|
+
{
|
|
935
|
+
ASSERT(isVector2(pos) && isVector2(size), 'pos and size must be Vector2s');
|
|
936
|
+
tileCollisionAssertWhole(this);
|
|
937
|
+
const collisionTest = tileCollisionTester(callbackObject);
|
|
938
|
+
|
|
939
|
+
// check any tiles in the area for collision
|
|
940
|
+
const posX = pos.x - this.pos.x;
|
|
941
|
+
const posY = pos.y - this.pos.y;
|
|
942
|
+
// reject AABBs entirely past either edge; without this, the negative
|
|
943
|
+
// side leaks into row/col 0 because minX/minY clamp to 0 and the
|
|
944
|
+
// point-test floor below forces maxX/maxY up to 1
|
|
945
|
+
if (posX + size.x/2 < 0 || posX - size.x/2 > this.size.x) return false;
|
|
946
|
+
if (posY + size.y/2 < 0 || posY - size.y/2 > this.size.y) return false;
|
|
947
|
+
const minX = max(posX - size.x/2|0, 0);
|
|
948
|
+
const minY = max(posY - size.y/2|0, 0);
|
|
949
|
+
// a zero size is a point test, one cell even when pos lands exactly on an integer boundary
|
|
950
|
+
const maxX = min(size.x ? posX + size.x/2 : minX + 1, this.size.x);
|
|
951
|
+
const maxY = min(size.y ? posY + size.y/2 : minY + 1, this.size.y);
|
|
952
|
+
for (let y = minY; y < maxY; ++y)
|
|
953
|
+
for (let x = minX; x < maxX; ++x)
|
|
954
|
+
{
|
|
955
|
+
// check if the object should collide with this tile, the callback gets its own vector, one it can keep
|
|
956
|
+
const tileData = this.collisionData[y*this.size.x+x];
|
|
957
|
+
if (tileData && collisionTest(tileData, vec2(x+this.pos.x, y+this.pos.y)))
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** Return the exact position of the boundary of first tile hit, undefined if nothing was hit.
|
|
964
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
965
|
+
* @param {Vector2} posStart
|
|
966
|
+
* @param {Vector2} posEnd
|
|
967
|
+
* @param {EngineObject|TileCollisionCallback} [callbackObject] - Callback, engine object, or undefined
|
|
968
|
+
* @param {Vector2} [normal] - Optional normal of the surface hit
|
|
969
|
+
* @return {Vector2|undefined} */
|
|
970
|
+
collisionRaycast(posStart, posEnd, callbackObject, normal)
|
|
971
|
+
{
|
|
972
|
+
ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
|
|
973
|
+
tileCollisionAssertWhole(this);
|
|
974
|
+
const collisionTest = tileCollisionTester(callbackObject);
|
|
975
|
+
// the line is walked in the layer's own space, so its cells are the tiles wherever the layer sits
|
|
976
|
+
const offset = this.pos;
|
|
977
|
+
const testFunction = (pos)=>
|
|
978
|
+
{
|
|
979
|
+
const tileData = this.getCollisionData(pos);
|
|
980
|
+
return tileData && collisionTest(tileData, vec2(pos.x + offset.x, pos.y + offset.y));
|
|
981
|
+
}
|
|
982
|
+
const hitPos = lineTest(posStart.subtract(offset), posEnd.subtract(offset), testFunction, normal);
|
|
983
|
+
if (hitPos)
|
|
984
|
+
hitPos.x += offset.x, hitPos.y += offset.y;
|
|
985
|
+
if (debugRaycast && hitPos)
|
|
986
|
+
{
|
|
987
|
+
const tilePos = hitPos.floor().add(vec2(.5));
|
|
988
|
+
debugRect(tilePos, vec2(1), '#f008', 0, 0, false, false);
|
|
989
|
+
debugLine(posStart, posEnd, '#00f', .02, 0, false);
|
|
990
|
+
debugLine(posStart, hitPos, '#f00', .02, 0, false);
|
|
991
|
+
debugPoint(hitPos, '#0f0', undefined, undefined, false);
|
|
992
|
+
normal && debugLine(hitPos, hitPos.add(normal), '#ff0', .02, 0, false);
|
|
993
|
+
}
|
|
994
|
+
return hitPos;
|
|
995
|
+
}
|
|
742
996
|
}
|