littlejsengine 1.14.23 → 1.15.2
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/dist/littlejs.d.ts +184 -30
- package/dist/littlejs.esm.js +589 -147
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +583 -147
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +546 -147
- package/examples/box2d/gameObjects.js +6 -10
- package/examples/box2d/scenes.js +2 -2
- package/examples/breakout/gameObjects.js +1 -1
- package/examples/electron/index.html +2 -2
- package/examples/index.html +11 -7
- package/examples/platformer/gameCharacter.js +3 -2
- package/examples/platformer/gameObjects.js +3 -8
- package/examples/shorts/base.html +2 -2
- package/examples/shorts/box2d.js +2 -2
- package/examples/shorts/box2dCar.js +18 -7
- package/examples/shorts/box2dPool.js +108 -0
- package/examples/shorts/debugDraw.js +2 -3
- package/examples/shorts/flappyGame.js +1 -0
- package/examples/shorts/fps.js +1 -1
- package/examples/shorts/hillGlideGame.js +0 -2
- package/examples/shorts/input.js +59 -0
- package/examples/shorts/landerGame.js +1 -3
- package/examples/shorts/medals.js +15 -9
- package/examples/shorts/music.js +15 -17
- package/examples/shorts/musicPlayer.js +24 -24
- package/examples/shorts/platformer.js +0 -2
- package/examples/shorts/slidingPuzzle.js +1 -1
- package/examples/shorts/song.mp3 +0 -0
- package/examples/shorts/spaceGame.js +0 -2
- package/examples/shorts/spriteAtlas.js +0 -2
- package/examples/shorts/tiltedView.js +0 -3
- package/examples/shorts/timers.js +1 -2
- package/examples/shorts/topDown.js +0 -2
- package/examples/shorts/video.webm +0 -0
- package/examples/shorts/videoPlayer.js +33 -0
- package/examples/starter/index.html +3 -3
- package/package.json +1 -1
- package/plugins/box2d.js +170 -50
- package/plugins/pluginExport.js +3 -0
- package/plugins/uiSystem.js +229 -27
- package/src/engine.js +21 -18
- package/src/engineAudio.js +14 -2
- package/src/engineDebug.js +37 -0
- package/src/engineDraw.js +21 -13
- package/src/engineExport.js +3 -0
- package/src/engineInput.js +24 -12
- package/src/engineMedals.js +2 -2
- package/src/engineObject.js +24 -4
- package/src/engineParticles.js +4 -6
- package/src/engineTileLayer.js +2 -0
- package/src/engineUtilities.js +35 -13
package/dist/littlejs.js
CHANGED
|
@@ -33,7 +33,7 @@ const engineName = 'LittleJS';
|
|
|
33
33
|
* @type {string}
|
|
34
34
|
* @default
|
|
35
35
|
* @memberof Engine */
|
|
36
|
-
const engineVersion = '1.
|
|
36
|
+
const engineVersion = '1.15.2';
|
|
37
37
|
|
|
38
38
|
/** Frames per second to update
|
|
39
39
|
* @type {number}
|
|
@@ -134,7 +134,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* @callback GameInitCallback - Called after the engine starts, can be async
|
|
137
|
-
* @
|
|
137
|
+
* @return {void|Promise<void>}
|
|
138
138
|
* @memberof Engine
|
|
139
139
|
*/
|
|
140
140
|
/**
|
|
@@ -164,7 +164,7 @@ function engineAddPlugin(update, render, glContextLost, glContextRestored)
|
|
|
164
164
|
async function engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, imageSources=[], rootElement=document.body)
|
|
165
165
|
{
|
|
166
166
|
ASSERT(!mainContext, 'engine already initialized');
|
|
167
|
-
ASSERT(
|
|
167
|
+
ASSERT(isArray(imageSources), 'pass in images as array');
|
|
168
168
|
|
|
169
169
|
// allow passing in empty functions
|
|
170
170
|
gameInit ||= ()=>{};
|
|
@@ -456,21 +456,24 @@ function engineObjectsUpdate()
|
|
|
456
456
|
// recursive object update
|
|
457
457
|
function updateObject(o)
|
|
458
458
|
{
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
if (o.destroyed)
|
|
460
|
+
return;
|
|
461
|
+
|
|
462
|
+
o.update();
|
|
463
|
+
for (const child of o.children)
|
|
464
|
+
updateObject(child);
|
|
465
465
|
}
|
|
466
466
|
for (const o of engineObjects)
|
|
467
467
|
{
|
|
468
|
+
if (o.parent)
|
|
469
|
+
continue;
|
|
470
|
+
|
|
468
471
|
// update top level objects
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
o.update();
|
|
473
|
+
o.updatePhysics();
|
|
474
|
+
for (const child of o.children)
|
|
475
|
+
updateObject(child);
|
|
476
|
+
o.updateTransforms();
|
|
474
477
|
}
|
|
475
478
|
|
|
476
479
|
// remove destroyed objects
|
|
@@ -487,10 +490,10 @@ function engineObjectsDestroy()
|
|
|
487
490
|
}
|
|
488
491
|
|
|
489
492
|
/** Collects all object within a given area
|
|
490
|
-
* @param {Vector2} [pos]
|
|
491
|
-
* @param {Vector2|number} [size]
|
|
493
|
+
* @param {Vector2} [pos] - Center of test area, or undefined for all objects
|
|
494
|
+
* @param {Vector2|number} [size] - Radius of circle if float, rectangle size if Vector2
|
|
492
495
|
* @param {Array<EngineObject>} [objects=engineObjects] - List of objects to check
|
|
493
|
-
* @return {Array<EngineObject>}
|
|
496
|
+
* @return {Array<EngineObject>} - List of collected objects
|
|
494
497
|
* @memberof Engine */
|
|
495
498
|
function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
496
499
|
{
|
|
@@ -503,7 +506,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
|
503
506
|
else if (size instanceof Vector2) // bounding box test
|
|
504
507
|
{
|
|
505
508
|
for (const o of objects)
|
|
506
|
-
isOverlapping(pos, size
|
|
509
|
+
o.isOverlapping(pos, size) && collectedObjects.push(o);
|
|
507
510
|
}
|
|
508
511
|
else // circle test
|
|
509
512
|
{
|
|
@@ -790,6 +793,12 @@ function LOG(...output) { console.log(...output); }
|
|
|
790
793
|
* @memberof Debug */
|
|
791
794
|
function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
792
795
|
{
|
|
796
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
797
|
+
ASSERT(isVector2(size), 'size must be a vec2');
|
|
798
|
+
ASSERT(isString(color) || isColor(color), 'color is invalid');
|
|
799
|
+
ASSERT(isNumber(time), 'time must be a number');
|
|
800
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
801
|
+
|
|
793
802
|
if (typeof size === 'number')
|
|
794
803
|
size = vec2(size); // allow passing in floats
|
|
795
804
|
if (isColor(color))
|
|
@@ -810,6 +819,12 @@ function debugRect(pos, size=vec2(), color=WHITE, time=0, angle=0, fill=false)
|
|
|
810
819
|
* @memberof Debug */
|
|
811
820
|
function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
812
821
|
{
|
|
822
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
823
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
824
|
+
ASSERT(isString(color) || isColor(color), 'color is invalid');
|
|
825
|
+
ASSERT(isNumber(time), 'time must be a number');
|
|
826
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
827
|
+
|
|
813
828
|
if (isColor(color))
|
|
814
829
|
color = color.toString();
|
|
815
830
|
pos = pos.copy();
|
|
@@ -827,6 +842,11 @@ function debugPoly(pos, points, color=WHITE, time=0, angle=0, fill=false)
|
|
|
827
842
|
* @memberof Debug */
|
|
828
843
|
function debugCircle(pos, size=0, color=WHITE, time=0, fill=false)
|
|
829
844
|
{
|
|
845
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
846
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
847
|
+
ASSERT(isString(color) || isColor(color), 'color is invalid');
|
|
848
|
+
ASSERT(isNumber(time), 'time must be a number');
|
|
849
|
+
|
|
830
850
|
if (isColor(color))
|
|
831
851
|
color = color.toString();
|
|
832
852
|
pos = pos.copy();
|
|
@@ -852,6 +872,10 @@ function debugPoint(pos, color, time, angle)
|
|
|
852
872
|
* @memberof Debug */
|
|
853
873
|
function debugLine(posA, posB, color, width=.1, time)
|
|
854
874
|
{
|
|
875
|
+
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
876
|
+
ASSERT(isVector2(posB), 'posB must be sa vec2');
|
|
877
|
+
ASSERT(isNumber(width), 'width must be a number');
|
|
878
|
+
|
|
855
879
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
856
880
|
const size = vec2(width, halfDelta.length()*2);
|
|
857
881
|
debugRect(posA.add(halfDelta), size, color, time, halfDelta.angle(), true);
|
|
@@ -866,6 +890,11 @@ function debugLine(posA, posB, color, width=.1, time)
|
|
|
866
890
|
* @memberof Debug */
|
|
867
891
|
function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
868
892
|
{
|
|
893
|
+
ASSERT(isVector2(posA), 'posA must be a vec2');
|
|
894
|
+
ASSERT(isVector2(posB), 'posB must be a vec2');
|
|
895
|
+
ASSERT(isVector2(sizeA), 'sizeA must be a vec2');
|
|
896
|
+
ASSERT(isVector2(sizeB), 'sizeB must be a vec2');
|
|
897
|
+
|
|
869
898
|
const minPos = vec2(
|
|
870
899
|
min(posA.x - sizeA.x/2, posB.x - sizeB.x/2),
|
|
871
900
|
min(posA.y - sizeA.y/2, posB.y - sizeB.y/2)
|
|
@@ -888,6 +917,14 @@ function debugOverlap(posA, sizeA, posB, sizeB, color)
|
|
|
888
917
|
* @memberof Debug */
|
|
889
918
|
function debugText(text, pos, size=1, color=WHITE, time=0, angle=0, font='monospace')
|
|
890
919
|
{
|
|
920
|
+
ASSERT(isString(text), 'text must be a string');
|
|
921
|
+
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
922
|
+
ASSERT(isNumber(size), 'size must be a number');
|
|
923
|
+
ASSERT(isString(color) || isColor(color), 'color is invalid');
|
|
924
|
+
ASSERT(isNumber(time), 'time must be a number');
|
|
925
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
926
|
+
ASSERT(isString(font), 'font must be a string');
|
|
927
|
+
|
|
891
928
|
if (isColor(color))
|
|
892
929
|
color = color.toString();
|
|
893
930
|
pos = pos.copy();
|
|
@@ -1341,6 +1378,9 @@ function debugVideoCaptureUpdate()
|
|
|
1341
1378
|
debugVideoCaptureIcon.textContent = '● REC ' + formatTime(debugVideoCaptureTimer);
|
|
1342
1379
|
}
|
|
1343
1380
|
|
|
1381
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
1382
|
+
// debug utility functions
|
|
1383
|
+
|
|
1344
1384
|
// make color constants immutable with debug assertions
|
|
1345
1385
|
function debugProtectConstant(obj)
|
|
1346
1386
|
{
|
|
@@ -1515,7 +1555,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
|
1515
1555
|
* @param {number} valueA
|
|
1516
1556
|
* @param {number} valueB
|
|
1517
1557
|
* @param {number} [wrapSize]
|
|
1518
|
-
* @
|
|
1558
|
+
* @return {number}
|
|
1519
1559
|
* @memberof Utilities */
|
|
1520
1560
|
function distanceWrap(valueA, valueB, wrapSize=1)
|
|
1521
1561
|
{ const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
|
|
@@ -1525,7 +1565,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
|
|
|
1525
1565
|
* @param {number} valueB
|
|
1526
1566
|
* @param {number} percent
|
|
1527
1567
|
* @param {number} [wrapSize]
|
|
1528
|
-
* @
|
|
1568
|
+
* @return {number}
|
|
1529
1569
|
* @memberof Utilities */
|
|
1530
1570
|
function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
1531
1571
|
{
|
|
@@ -1537,7 +1577,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
|
1537
1577
|
/** Returns signed wrapped distance between the two angles passed in
|
|
1538
1578
|
* @param {number} angleA
|
|
1539
1579
|
* @param {number} angleB
|
|
1540
|
-
* @
|
|
1580
|
+
* @return {number}
|
|
1541
1581
|
* @memberof Utilities */
|
|
1542
1582
|
function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
|
|
1543
1583
|
|
|
@@ -1545,7 +1585,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
|
|
|
1545
1585
|
* @param {number} angleA
|
|
1546
1586
|
* @param {number} angleB
|
|
1547
1587
|
* @param {number} percent
|
|
1548
|
-
* @
|
|
1588
|
+
* @return {number}
|
|
1549
1589
|
* @memberof Utilities */
|
|
1550
1590
|
function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
|
|
1551
1591
|
|
|
@@ -1672,6 +1712,13 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
|
1672
1712
|
* @memberof Utilities */
|
|
1673
1713
|
function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
|
|
1674
1714
|
|
|
1715
|
+
/**
|
|
1716
|
+
* Check if object is an array
|
|
1717
|
+
* @param {any} a
|
|
1718
|
+
* @return {boolean}
|
|
1719
|
+
* @memberof Utilities */
|
|
1720
|
+
function isArray(a) { return Array.isArray(a); }
|
|
1721
|
+
|
|
1675
1722
|
/**
|
|
1676
1723
|
* @callback LineTestFunction - Checks if a position is colliding
|
|
1677
1724
|
* @param {Vector2} pos
|
|
@@ -2371,7 +2418,6 @@ class Color
|
|
|
2371
2418
|
* @return {string} */
|
|
2372
2419
|
toString(useAlpha = true)
|
|
2373
2420
|
{
|
|
2374
|
-
ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
|
|
2375
2421
|
if (debug && !this.isValid())
|
|
2376
2422
|
return `#000`;
|
|
2377
2423
|
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
@@ -2508,11 +2554,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
2508
2554
|
class Timer
|
|
2509
2555
|
{
|
|
2510
2556
|
/** Create a timer object set time passed in
|
|
2511
|
-
* @param {number} [timeLeft] - How much time left before the timer
|
|
2512
|
-
|
|
2557
|
+
* @param {number} [timeLeft] - How much time left before the timer
|
|
2558
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
2559
|
+
constructor(timeLeft, useRealTime=false)
|
|
2513
2560
|
{
|
|
2514
2561
|
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
2515
|
-
this.
|
|
2562
|
+
this.useRealTime = useRealTime;
|
|
2563
|
+
const globalTime = this.getGlobalTime();
|
|
2564
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
2516
2565
|
this.setTime = timeLeft;
|
|
2517
2566
|
}
|
|
2518
2567
|
|
|
@@ -2521,10 +2570,19 @@ class Timer
|
|
|
2521
2570
|
set(timeLeft=0)
|
|
2522
2571
|
{
|
|
2523
2572
|
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
2524
|
-
|
|
2573
|
+
const globalTime = this.getGlobalTime();
|
|
2574
|
+
this.time = globalTime + timeLeft;
|
|
2525
2575
|
this.setTime = timeLeft;
|
|
2526
2576
|
}
|
|
2527
2577
|
|
|
2578
|
+
/** Set if the timer should keep running even when the game is paused
|
|
2579
|
+
* @param {boolean} [useRealTime] */
|
|
2580
|
+
setUseRealTime(useRealTime=true)
|
|
2581
|
+
{
|
|
2582
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
2583
|
+
this.useRealTime = useRealTime;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2528
2586
|
/** Unset the timer */
|
|
2529
2587
|
unset() { this.time = undefined; }
|
|
2530
2588
|
|
|
@@ -2534,24 +2592,28 @@ class Timer
|
|
|
2534
2592
|
|
|
2535
2593
|
/** Returns true if set and has not elapsed
|
|
2536
2594
|
* @return {boolean} */
|
|
2537
|
-
active() { return
|
|
2595
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
2538
2596
|
|
|
2539
2597
|
/** Returns true if set and elapsed
|
|
2540
2598
|
* @return {boolean} */
|
|
2541
|
-
elapsed() { return
|
|
2599
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
2542
2600
|
|
|
2543
2601
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
2544
2602
|
* @return {number} */
|
|
2545
|
-
get() { return this.isSet()?
|
|
2603
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
2546
2604
|
|
|
2547
2605
|
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
2548
2606
|
* @return {number} */
|
|
2549
|
-
getPercent() { return this.isSet()? 1-percent(this.time -
|
|
2607
|
+
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
2550
2608
|
|
|
2551
2609
|
/** Get the time this timer was set to, returns 0 if not set
|
|
2552
2610
|
* @return {number} */
|
|
2553
2611
|
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
2554
2612
|
|
|
2613
|
+
/** Get the current global time this timer is based on
|
|
2614
|
+
* @return {number} */
|
|
2615
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
2616
|
+
|
|
2555
2617
|
/** Returns this timer expressed as a string
|
|
2556
2618
|
* @return {string} */
|
|
2557
2619
|
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
@@ -3269,11 +3331,10 @@ class EngineObject
|
|
|
3269
3331
|
}
|
|
3270
3332
|
|
|
3271
3333
|
/** Update the object physics, called automatically by engine once each frame */
|
|
3272
|
-
|
|
3334
|
+
updatePhysics()
|
|
3273
3335
|
{
|
|
3274
3336
|
// child objects do not have physics
|
|
3275
|
-
|
|
3276
|
-
return;
|
|
3337
|
+
ASSERT(!this.parent);
|
|
3277
3338
|
|
|
3278
3339
|
if (this.clampSpeed)
|
|
3279
3340
|
{
|
|
@@ -3323,7 +3384,7 @@ class EngineObject
|
|
|
3323
3384
|
continue;
|
|
3324
3385
|
|
|
3325
3386
|
// check collision
|
|
3326
|
-
if (!
|
|
3387
|
+
if (!this.isOverlappingObject(o))
|
|
3327
3388
|
continue;
|
|
3328
3389
|
|
|
3329
3390
|
// notify objects of collision and check if should be resolved
|
|
@@ -3471,6 +3532,9 @@ class EngineObject
|
|
|
3471
3532
|
}
|
|
3472
3533
|
}
|
|
3473
3534
|
|
|
3535
|
+
/** Update the object, called automatically by engine once each frame. Does nothing by default. */
|
|
3536
|
+
update() {}
|
|
3537
|
+
|
|
3474
3538
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
3475
3539
|
render()
|
|
3476
3540
|
{
|
|
@@ -3536,6 +3600,10 @@ class EngineObject
|
|
|
3536
3600
|
* @return {number} */
|
|
3537
3601
|
getAliveTime() { return time - this.spawnTime; }
|
|
3538
3602
|
|
|
3603
|
+
/** Get the speed of this object
|
|
3604
|
+
* @return {number} */
|
|
3605
|
+
getSpeed() { return this.velocity.length(); }
|
|
3606
|
+
|
|
3539
3607
|
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
3540
3608
|
* @param {Vector2} acceleration */
|
|
3541
3609
|
applyAcceleration(acceleration)
|
|
@@ -3580,6 +3648,20 @@ class EngineObject
|
|
|
3580
3648
|
child.parent = 0;
|
|
3581
3649
|
}
|
|
3582
3650
|
|
|
3651
|
+
/** Check if overlapping another engine object
|
|
3652
|
+
* Collisions are resoloved to prevent overlaps
|
|
3653
|
+
* @param {EngineObject} object
|
|
3654
|
+
* @return {boolean} */
|
|
3655
|
+
isOverlappingObject(object)
|
|
3656
|
+
{ return this.isOverlapping(object.pos, object.size); }
|
|
3657
|
+
|
|
3658
|
+
/** Check if overlapping a point or aligned bounding box
|
|
3659
|
+
* @param {Vector2} pos - Center of box
|
|
3660
|
+
* @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
|
|
3661
|
+
* @return {boolean} */
|
|
3662
|
+
isOverlapping(pos, size=vec2())
|
|
3663
|
+
{ return isOverlapping(this.pos, this.size, pos, size); }
|
|
3664
|
+
|
|
3583
3665
|
/** Set how this object collides
|
|
3584
3666
|
* @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
|
|
3585
3667
|
* @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
|
|
@@ -3925,7 +4007,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3925
4007
|
{
|
|
3926
4008
|
// normal canvas 2D rendering method (slower)
|
|
3927
4009
|
++drawCount;
|
|
3928
|
-
size = new Vector2(size.x, -size.y); //
|
|
4010
|
+
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
3929
4011
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
3930
4012
|
{
|
|
3931
4013
|
if (textureInfo)
|
|
@@ -4034,7 +4116,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
4034
4116
|
* @memberof Draw */
|
|
4035
4117
|
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
4036
4118
|
{
|
|
4037
|
-
ASSERT(
|
|
4119
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
4038
4120
|
ASSERT(isNumber(width), 'width must be a number');
|
|
4039
4121
|
ASSERT(isColor(color), 'color is invalid');
|
|
4040
4122
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -4140,7 +4222,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
4140
4222
|
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
|
|
4141
4223
|
{
|
|
4142
4224
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
4143
|
-
ASSERT(
|
|
4225
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
4144
4226
|
ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
|
|
4145
4227
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
4146
4228
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
@@ -4292,11 +4374,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
4292
4374
|
* @param {string} [font=fontDefault]
|
|
4293
4375
|
* @param {string} [fontStyle]
|
|
4294
4376
|
* @param {number} [maxWidth]
|
|
4377
|
+
* @param {number} [angle]
|
|
4295
4378
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
4296
4379
|
* @memberof Draw */
|
|
4297
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
|
|
4380
|
+
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
4298
4381
|
{
|
|
4299
|
-
drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
|
|
4382
|
+
drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
4300
4383
|
}
|
|
4301
4384
|
|
|
4302
4385
|
/** Draw text on overlay canvas in world space
|
|
@@ -4311,10 +4394,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
4311
4394
|
* @param {string} [font=fontDefault]
|
|
4312
4395
|
* @param {string} [fontStyle]
|
|
4313
4396
|
* @param {number} [maxWidth]
|
|
4397
|
+
* @param {number} [angle]
|
|
4314
4398
|
* @memberof Draw */
|
|
4315
|
-
function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
|
|
4399
|
+
function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
|
|
4316
4400
|
{
|
|
4317
|
-
drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
|
|
4401
|
+
drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
|
|
4318
4402
|
}
|
|
4319
4403
|
|
|
4320
4404
|
/** Draw text on overlay canvas in screen space
|
|
@@ -4329,9 +4413,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
4329
4413
|
* @param {string} [font=fontDefault]
|
|
4330
4414
|
* @param {string} [fontStyle]
|
|
4331
4415
|
* @param {number} [maxWidth]
|
|
4416
|
+
* @param {number} [angle]
|
|
4332
4417
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
4333
4418
|
* @memberof Draw */
|
|
4334
|
-
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
|
|
4419
|
+
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
|
|
4335
4420
|
{
|
|
4336
4421
|
ASSERT(isString(text), 'text must be a string');
|
|
4337
4422
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -4342,6 +4427,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
4342
4427
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
4343
4428
|
ASSERT(isString(font), 'font must be a string');
|
|
4344
4429
|
ASSERT(isString(fontStyle), 'fontStyle must be a string');
|
|
4430
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
4345
4431
|
|
|
4346
4432
|
context.fillStyle = color.toString();
|
|
4347
4433
|
context.strokeStyle = lineColor.toString();
|
|
@@ -4351,14 +4437,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
4351
4437
|
context.textBaseline = 'middle';
|
|
4352
4438
|
|
|
4353
4439
|
const lines = (text+'').split('\n');
|
|
4354
|
-
|
|
4355
|
-
|
|
4440
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
4441
|
+
context.save();
|
|
4442
|
+
context.translate(pos.x, posY);
|
|
4443
|
+
context.rotate(-angle);
|
|
4444
|
+
let yOffset = 0;
|
|
4356
4445
|
lines.forEach(line=>
|
|
4357
4446
|
{
|
|
4358
|
-
lineWidth && context.strokeText(line,
|
|
4359
|
-
context.fillText(line,
|
|
4360
|
-
|
|
4447
|
+
lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
|
|
4448
|
+
context.fillText(line, 0, yOffset, maxWidth);
|
|
4449
|
+
yOffset += size;
|
|
4361
4450
|
});
|
|
4451
|
+
context.restore();
|
|
4362
4452
|
}
|
|
4363
4453
|
|
|
4364
4454
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4790,6 +4880,11 @@ let mouseDeltaScreen = vec2();
|
|
|
4790
4880
|
* @memberof Input */
|
|
4791
4881
|
let mouseWheel = 0;
|
|
4792
4882
|
|
|
4883
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4884
|
+
* @type {boolean}
|
|
4885
|
+
* @memberof Input */
|
|
4886
|
+
let mouseInWindow = true;
|
|
4887
|
+
|
|
4793
4888
|
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4794
4889
|
* @type {boolean}
|
|
4795
4890
|
* @memberof Input */
|
|
@@ -4884,6 +4979,7 @@ function inputInit()
|
|
|
4884
4979
|
document.addEventListener('mousedown', onMouseDown);
|
|
4885
4980
|
document.addEventListener('mouseup', onMouseUp);
|
|
4886
4981
|
document.addEventListener('mousemove', onMouseMove);
|
|
4982
|
+
document.addEventListener('mouseleave', onMouseLeave);
|
|
4887
4983
|
document.addEventListener('wheel', onMouseWheel);
|
|
4888
4984
|
document.addEventListener('contextmenu', onContextMenu);
|
|
4889
4985
|
document.addEventListener('blur', onBlur);
|
|
@@ -4908,14 +5004,14 @@ function inputInit()
|
|
|
4908
5004
|
if (inputWASDEmulateDirection)
|
|
4909
5005
|
inputData[0][remapKey(e.code)] = 4;
|
|
4910
5006
|
}
|
|
4911
|
-
function remapKey(
|
|
5007
|
+
function remapKey(k)
|
|
4912
5008
|
{
|
|
4913
5009
|
// handle remapping wasd keys to directions
|
|
4914
5010
|
return inputWASDEmulateDirection ?
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
5011
|
+
k === 'KeyW' ? 'ArrowUp' :
|
|
5012
|
+
k === 'KeyS' ? 'ArrowDown' :
|
|
5013
|
+
k === 'KeyA' ? 'ArrowLeft' :
|
|
5014
|
+
k === 'KeyD' ? 'ArrowRight' : k : k;
|
|
4919
5015
|
}
|
|
4920
5016
|
function onMouseDown(e)
|
|
4921
5017
|
{
|
|
@@ -4929,7 +5025,7 @@ function inputInit()
|
|
|
4929
5025
|
isUsingGamepad = false;
|
|
4930
5026
|
inputData[0][e.button] = 3;
|
|
4931
5027
|
|
|
4932
|
-
|
|
5028
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4933
5029
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4934
5030
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4935
5031
|
inputPreventDefault && e.button && e.preventDefault();
|
|
@@ -4942,10 +5038,17 @@ function inputInit()
|
|
|
4942
5038
|
}
|
|
4943
5039
|
function onMouseMove(e)
|
|
4944
5040
|
{
|
|
4945
|
-
|
|
5041
|
+
mouseInWindow = true;
|
|
5042
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4946
5043
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4947
|
-
|
|
5044
|
+
|
|
5045
|
+
// when pointer is locked use movementX/Y for delta
|
|
5046
|
+
const movement = pointerLockIsActive() ?
|
|
5047
|
+
vec2(e.movementX, e.movementY) :
|
|
5048
|
+
mousePosScreen.subtract(mousePosScreenLast);
|
|
5049
|
+
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
4948
5050
|
}
|
|
5051
|
+
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
4949
5052
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
4950
5053
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
4951
5054
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
@@ -5084,7 +5187,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
|
|
|
5084
5187
|
function touchGamepadButtonCenter()
|
|
5085
5188
|
{
|
|
5086
5189
|
// draw right face buttons
|
|
5087
|
-
|
|
5190
|
+
const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
5088
5191
|
if (touchGamepadButtonCount <= 2)
|
|
5089
5192
|
center.x += touchGamepadSize/2;
|
|
5090
5193
|
return center;
|
|
@@ -5094,14 +5197,13 @@ function touchGamepadButtonCenter()
|
|
|
5094
5197
|
function touchInputInit()
|
|
5095
5198
|
{
|
|
5096
5199
|
// add non passive touch event listeners
|
|
5097
|
-
let handleTouch = handleTouchDefault;
|
|
5098
5200
|
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
5099
5201
|
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
5100
5202
|
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
5101
5203
|
|
|
5102
5204
|
// handle all touch events the same way
|
|
5103
5205
|
let wasTouching;
|
|
5104
|
-
function
|
|
5206
|
+
function handleTouch(e)
|
|
5105
5207
|
{
|
|
5106
5208
|
if (!touchInputEnable)
|
|
5107
5209
|
return;
|
|
@@ -5252,7 +5354,7 @@ function touchGamepadRender()
|
|
|
5252
5354
|
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
5253
5355
|
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
5254
5356
|
{
|
|
5255
|
-
|
|
5357
|
+
const j = mod(i-1, 4);
|
|
5256
5358
|
let button = touchGamepadButtonCount > 2 ?
|
|
5257
5359
|
j : min(j, touchGamepadButtonCount-1);
|
|
5258
5360
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
@@ -5354,6 +5456,10 @@ class Sound
|
|
|
5354
5456
|
{
|
|
5355
5457
|
if (!soundEnable || headlessMode) return;
|
|
5356
5458
|
|
|
5459
|
+
ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
|
|
5460
|
+
ASSERT(isNumber(range), 'range must be a number');
|
|
5461
|
+
ASSERT(isNumber(taper), 'taper must be a number');
|
|
5462
|
+
|
|
5357
5463
|
/** @property {number} - World space max range of sound */
|
|
5358
5464
|
this.range = range;
|
|
5359
5465
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
@@ -5392,6 +5498,12 @@ class Sound
|
|
|
5392
5498
|
*/
|
|
5393
5499
|
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
5394
5500
|
{
|
|
5501
|
+
ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
|
|
5502
|
+
ASSERT(isNumber(volume), 'volume must be a number');
|
|
5503
|
+
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
5504
|
+
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
5505
|
+
|
|
5506
|
+
|
|
5395
5507
|
if (!soundEnable || headlessMode) return;
|
|
5396
5508
|
if (!this.sampleChannels) return;
|
|
5397
5509
|
|
|
@@ -5437,6 +5549,7 @@ class Sound
|
|
|
5437
5549
|
*/
|
|
5438
5550
|
playNote(semitoneOffset=0, pos, volume)
|
|
5439
5551
|
{
|
|
5552
|
+
ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
|
|
5440
5553
|
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
5441
5554
|
return this.play(pos, volume, pitch, 0);
|
|
5442
5555
|
}
|
|
@@ -5445,7 +5558,7 @@ class Sound
|
|
|
5445
5558
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
5446
5559
|
*/
|
|
5447
5560
|
getDuration()
|
|
5448
|
-
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
5561
|
+
{ return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
|
|
5449
5562
|
|
|
5450
5563
|
/** Check if sound is loaded, for sounds fetched from a url
|
|
5451
5564
|
* @return {boolean} - True if sound is loaded and ready to play
|
|
@@ -5487,6 +5600,7 @@ class SoundWave extends Sound
|
|
|
5487
5600
|
super(undefined, range, taper);
|
|
5488
5601
|
if (!soundEnable || headlessMode) return;
|
|
5489
5602
|
ASSERT(!filename || isString(filename), 'filename must be a string');
|
|
5603
|
+
ASSERT(isNumber(randomness), 'randomness must be a number');
|
|
5490
5604
|
|
|
5491
5605
|
/** @property {SoundLoadCallback} - callback function to call when sound is loaded */
|
|
5492
5606
|
this.onloadCallback = onloadCallback;
|
|
@@ -5703,7 +5817,7 @@ class SoundInstance
|
|
|
5703
5817
|
/** Get the total duration of this sound
|
|
5704
5818
|
* @return {number} - Total duration in seconds
|
|
5705
5819
|
*/
|
|
5706
|
-
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
5820
|
+
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
5707
5821
|
|
|
5708
5822
|
/** Get source of this sound instance
|
|
5709
5823
|
* @return {AudioBufferSourceNode}
|
|
@@ -6017,6 +6131,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
6017
6131
|
}
|
|
6018
6132
|
|
|
6019
6133
|
/** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
|
|
6134
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6020
6135
|
* @param {Vector2} posStart
|
|
6021
6136
|
* @param {Vector2} posEnd
|
|
6022
6137
|
* @param {EngineObject} [object] - An object or undefined for generic test
|
|
@@ -6590,6 +6705,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
6590
6705
|
}
|
|
6591
6706
|
|
|
6592
6707
|
/** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
|
|
6708
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
6593
6709
|
* @param {Vector2} posStart
|
|
6594
6710
|
* @param {Vector2} posEnd
|
|
6595
6711
|
* @param {EngineObject} [object] - An object or undefined for generic test
|
|
@@ -6782,12 +6898,12 @@ class ParticleEmitter extends EngineObject
|
|
|
6782
6898
|
this.previousPos = this.pos.copy();
|
|
6783
6899
|
}
|
|
6784
6900
|
|
|
6901
|
+
/** Emitters do not have physics */
|
|
6902
|
+
updatePhysics() {}
|
|
6903
|
+
|
|
6785
6904
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
6786
6905
|
update()
|
|
6787
6906
|
{
|
|
6788
|
-
// only do default update to apply parent transforms
|
|
6789
|
-
this.parent && super.update();
|
|
6790
|
-
|
|
6791
6907
|
if (this.velocityInheritance)
|
|
6792
6908
|
{
|
|
6793
6909
|
// pass emitter velocity to particles
|
|
@@ -6804,7 +6920,7 @@ class ParticleEmitter extends EngineObject
|
|
|
6804
6920
|
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
6805
6921
|
{
|
|
6806
6922
|
// emit particles
|
|
6807
|
-
if (this.emitRate
|
|
6923
|
+
if (this.emitRate && particleEmitRateScale)
|
|
6808
6924
|
{
|
|
6809
6925
|
const rate = 1/this.emitRate/particleEmitRateScale;
|
|
6810
6926
|
for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
|
|
@@ -6942,8 +7058,6 @@ class Particle extends EngineObject
|
|
|
6942
7058
|
/** Update the object physics, called automatically by engine once each frame */
|
|
6943
7059
|
update()
|
|
6944
7060
|
{
|
|
6945
|
-
super.update();
|
|
6946
|
-
|
|
6947
7061
|
if (this.collideTiles || this.collideSolidObjects)
|
|
6948
7062
|
{
|
|
6949
7063
|
// only apply max circular speed if particle can collide
|
|
@@ -7187,11 +7301,11 @@ class Medal
|
|
|
7187
7301
|
const descriptionSize = height*.3;
|
|
7188
7302
|
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
7189
7303
|
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
7190
|
-
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
7304
|
+
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
7191
7305
|
|
|
7192
7306
|
// draw the description
|
|
7193
7307
|
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
7194
|
-
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
7308
|
+
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
7195
7309
|
context.restore();
|
|
7196
7310
|
}
|
|
7197
7311
|
|
|
@@ -8640,11 +8754,16 @@ class UISystemPlugin
|
|
|
8640
8754
|
// reset hover object at start of update
|
|
8641
8755
|
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8642
8756
|
uiSystem.hoverObject = undefined;
|
|
8757
|
+
|
|
8758
|
+
// update in reverse order so topmost objects get priority
|
|
8643
8759
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8644
8760
|
{
|
|
8645
8761
|
const o = uiSystem.uiObjects[i];
|
|
8646
8762
|
o.parent || updateObject(o)
|
|
8647
8763
|
}
|
|
8764
|
+
|
|
8765
|
+
// remove destroyed objects
|
|
8766
|
+
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
8648
8767
|
}
|
|
8649
8768
|
function uiRender()
|
|
8650
8769
|
{
|
|
@@ -8677,12 +8796,12 @@ class UISystemPlugin
|
|
|
8677
8796
|
/** Draw a rectangle to the UI context
|
|
8678
8797
|
* @param {Vector2} pos
|
|
8679
8798
|
* @param {Vector2} size
|
|
8680
|
-
* @param {Color} [color
|
|
8681
|
-
* @param {number} [lineWidth
|
|
8682
|
-
* @param {Color} [lineColor
|
|
8683
|
-
* @param {number} [cornerRadius
|
|
8684
|
-
* @param {Color} [gradientColor
|
|
8685
|
-
drawRect(pos, size, color=
|
|
8799
|
+
* @param {Color} [color]
|
|
8800
|
+
* @param {number} [lineWidth]
|
|
8801
|
+
* @param {Color} [lineColor]
|
|
8802
|
+
* @param {number} [cornerRadius]
|
|
8803
|
+
* @param {Color} [gradientColor] */
|
|
8804
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
|
|
8686
8805
|
{
|
|
8687
8806
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
8688
8807
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -8761,10 +8880,14 @@ class UISystemPlugin
|
|
|
8761
8880
|
* @param {string} [align]
|
|
8762
8881
|
* @param {string} [font=uiSystem.defaultFont]
|
|
8763
8882
|
* @param {string} [fontStyle]
|
|
8764
|
-
* @param {boolean} [applyMaxWidth=true]
|
|
8765
|
-
|
|
8883
|
+
* @param {boolean} [applyMaxWidth=true]
|
|
8884
|
+
* @param {Vector2} [textShadow]
|
|
8885
|
+
*/
|
|
8886
|
+
drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
|
|
8766
8887
|
{
|
|
8767
|
-
|
|
8888
|
+
if (textShadow)
|
|
8889
|
+
drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
|
|
8890
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
|
|
8768
8891
|
}
|
|
8769
8892
|
|
|
8770
8893
|
/**
|
|
@@ -8791,6 +8914,34 @@ class UISystemPlugin
|
|
|
8791
8914
|
setCallback(onDragLeave, 'dragleave');
|
|
8792
8915
|
setCallback(onDragOver, 'dragover');
|
|
8793
8916
|
}
|
|
8917
|
+
|
|
8918
|
+
/** Convert a screen space position to native UI position
|
|
8919
|
+
* @param {Vector2} pos
|
|
8920
|
+
* @return {Vector2}
|
|
8921
|
+
*/
|
|
8922
|
+
screenToNative(pos)
|
|
8923
|
+
{
|
|
8924
|
+
if (!uiSystem.nativeHeight)
|
|
8925
|
+
return pos;
|
|
8926
|
+
|
|
8927
|
+
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
8928
|
+
const sInv = 1/s;
|
|
8929
|
+
const p = pos.copy();
|
|
8930
|
+
p.x += s*mainCanvasSize.x/2;
|
|
8931
|
+
p.x *= sInv;
|
|
8932
|
+
p.y *= sInv;
|
|
8933
|
+
p.x -= sInv*mainCanvasSize.x/2;
|
|
8934
|
+
return p;
|
|
8935
|
+
}
|
|
8936
|
+
|
|
8937
|
+
/** Destroy and remove all objects
|
|
8938
|
+
* @memberof Engine */
|
|
8939
|
+
destroyObjects()
|
|
8940
|
+
{
|
|
8941
|
+
for (const o of this.uiObjects)
|
|
8942
|
+
o.parent || o.destroy();
|
|
8943
|
+
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
8944
|
+
}
|
|
8794
8945
|
}
|
|
8795
8946
|
|
|
8796
8947
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -8867,6 +9018,9 @@ class UIObject
|
|
|
8867
9018
|
/** @property {boolean} - True if this can be a hover object */
|
|
8868
9019
|
this.canBeHover = true;
|
|
8869
9020
|
uiSystem.uiObjects.push(this);
|
|
9021
|
+
|
|
9022
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
9023
|
+
this.textShadow = undefined;
|
|
8870
9024
|
}
|
|
8871
9025
|
|
|
8872
9026
|
/** Add a child UIObject to this object
|
|
@@ -8889,29 +9043,41 @@ class UIObject
|
|
|
8889
9043
|
child.parent = undefined;
|
|
8890
9044
|
}
|
|
8891
9045
|
|
|
9046
|
+
|
|
9047
|
+
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
9048
|
+
destroy()
|
|
9049
|
+
{
|
|
9050
|
+
if (this.destroyed)
|
|
9051
|
+
return;
|
|
9052
|
+
|
|
9053
|
+
// disconnect from parent and destroy children
|
|
9054
|
+
this.destroyed = 1;
|
|
9055
|
+
this.parent && this.parent.removeChild(this);
|
|
9056
|
+
for (const child of this.children)
|
|
9057
|
+
{
|
|
9058
|
+
child.parent = 0;
|
|
9059
|
+
child.destroy();
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
8892
9062
|
/** Check if the mouse is overlapping a box in screen space
|
|
8893
9063
|
* @return {boolean} - True if overlapping
|
|
8894
9064
|
*/
|
|
8895
9065
|
isMouseOverlapping()
|
|
8896
9066
|
{
|
|
9067
|
+
if (!mouseInWindow) return false;
|
|
9068
|
+
|
|
8897
9069
|
const size = !isTouchDevice ? this.size :
|
|
8898
9070
|
this.size.add(vec2(this.extraTouchSize || 0));
|
|
8899
|
-
|
|
8900
|
-
return isOverlapping(this.pos, size, mousePosScreen);
|
|
8901
|
-
|
|
8902
|
-
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
8903
|
-
const sInv = 1/s;
|
|
8904
|
-
let pos = mousePosScreen.copy();
|
|
8905
|
-
pos.x += s*mainCanvasSize.x/2;
|
|
8906
|
-
pos.x *= sInv;
|
|
8907
|
-
pos.y *= sInv;
|
|
8908
|
-
pos.x -= sInv*mainCanvasSize.x/2;
|
|
9071
|
+
const pos = uiSystem.screenToNative(mousePosScreen);
|
|
8909
9072
|
return isOverlapping(this.pos, size, pos);
|
|
8910
9073
|
}
|
|
8911
9074
|
|
|
8912
9075
|
/** Update the object, called automatically by plugin once each frame */
|
|
8913
9076
|
update()
|
|
8914
9077
|
{
|
|
9078
|
+
// call the custom update callback
|
|
9079
|
+
this.onUpdate();
|
|
9080
|
+
|
|
8915
9081
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
8916
9082
|
const isActive = this.isActiveObject();
|
|
8917
9083
|
const mouseDown = mouseIsDown(0);
|
|
@@ -8968,7 +9134,7 @@ class UIObject
|
|
|
8968
9134
|
|
|
8969
9135
|
const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
|
|
8970
9136
|
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
8971
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
9137
|
+
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
|
|
8972
9138
|
}
|
|
8973
9139
|
|
|
8974
9140
|
/** Special update when object is not visible */
|
|
@@ -8995,6 +9161,9 @@ class UIObject
|
|
|
8995
9161
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
8996
9162
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
8997
9163
|
|
|
9164
|
+
/** Called each frame when object updates */
|
|
9165
|
+
onUpdate() {}
|
|
9166
|
+
|
|
8998
9167
|
/** Called when the mouse enters the object */
|
|
8999
9168
|
onEnter() {}
|
|
9000
9169
|
|
|
@@ -9049,8 +9218,9 @@ class UIText extends UIObject
|
|
|
9049
9218
|
}
|
|
9050
9219
|
render()
|
|
9051
9220
|
{
|
|
9221
|
+
// only render the text
|
|
9052
9222
|
const textSize = this.getTextSize();
|
|
9053
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
|
|
9223
|
+
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9054
9224
|
}
|
|
9055
9225
|
}
|
|
9056
9226
|
|
|
@@ -9126,7 +9296,7 @@ class UIButton extends UIObject
|
|
|
9126
9296
|
// draw the text scaled to fit
|
|
9127
9297
|
const textSize = this.getTextSize();
|
|
9128
9298
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9129
|
-
this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
|
|
9299
|
+
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9130
9300
|
}
|
|
9131
9301
|
}
|
|
9132
9302
|
|
|
@@ -9180,7 +9350,7 @@ class UICheckbox extends UIObject
|
|
|
9180
9350
|
const textSize = this.getTextSize();
|
|
9181
9351
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
9182
9352
|
uiSystem.drawText(this.text, pos, textSize,
|
|
9183
|
-
this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
|
|
9353
|
+
this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
9184
9354
|
}
|
|
9185
9355
|
}
|
|
9186
9356
|
|
|
@@ -9235,9 +9405,11 @@ class UIScrollbar extends UIObject
|
|
|
9235
9405
|
const p1 = centerPos - handleWidth/2;
|
|
9236
9406
|
const p2 = centerPos + handleWidth/2;
|
|
9237
9407
|
const oldValue = this.value;
|
|
9408
|
+
|
|
9409
|
+
const p = uiSystem.screenToNative(mousePosScreen);
|
|
9238
9410
|
this.value = isHorizontal ?
|
|
9239
|
-
percent(
|
|
9240
|
-
percent(
|
|
9411
|
+
percent(p.x, p1, p2) :
|
|
9412
|
+
percent(p.y, p2, p1);
|
|
9241
9413
|
this.value === oldValue || this.onChange();
|
|
9242
9414
|
}
|
|
9243
9415
|
}
|
|
@@ -9259,20 +9431,164 @@ class UIScrollbar extends UIObject
|
|
|
9259
9431
|
vec2(lerp(p1, p2, this.value), this.pos.y) :
|
|
9260
9432
|
vec2(this.pos.x, lerp(p2, p1, this.value))
|
|
9261
9433
|
const handleColor = this.disabled ? this.disabledColor : this.handleColor;
|
|
9262
|
-
uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
9434
|
+
uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
9263
9435
|
|
|
9264
9436
|
// draw the text scaled to fit on the scrollbar
|
|
9265
9437
|
const textSize = this.getTextSize();
|
|
9266
9438
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
9267
|
-
this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
|
|
9439
|
+
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
9440
|
+
}
|
|
9441
|
+
}
|
|
9442
|
+
|
|
9443
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9444
|
+
/**
|
|
9445
|
+
* VideoPlayerUIObject - A UI object that plays video
|
|
9446
|
+
* @extends UIObject
|
|
9447
|
+
* @example
|
|
9448
|
+
* // Create a video player UI object
|
|
9449
|
+
* const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
|
|
9450
|
+
* video.play();
|
|
9451
|
+
* @memberof UISystem
|
|
9452
|
+
*/
|
|
9453
|
+
class UIVideo extends UIObject
|
|
9454
|
+
{
|
|
9455
|
+
/** Create a video player UI object
|
|
9456
|
+
* @param {Vector2} [pos]
|
|
9457
|
+
* @param {Vector2} [size]
|
|
9458
|
+
* @param {string} src - Video file path or URL
|
|
9459
|
+
* @param {boolean} [autoplay=false] - Start playing immediately?
|
|
9460
|
+
* @param {boolean} [loop=false] - Loop the video?
|
|
9461
|
+
* @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
|
|
9462
|
+
*/
|
|
9463
|
+
constructor(pos, size, src, autoplay=false, loop=false, volume=1)
|
|
9464
|
+
{
|
|
9465
|
+
super(pos, size || vec2());
|
|
9466
|
+
|
|
9467
|
+
ASSERT(isString(src), 'video src must be a string');
|
|
9468
|
+
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
9469
|
+
|
|
9470
|
+
this.color = BLACK; // default to black background
|
|
9471
|
+
this.cornerRadius = 0; // default to no corner radius
|
|
9472
|
+
|
|
9473
|
+
/** @property {float} - The video volume */
|
|
9474
|
+
this.volume = volume;
|
|
9475
|
+
|
|
9476
|
+
// create video element
|
|
9477
|
+
/** @property {HTMLVideoElement} - The video player */
|
|
9478
|
+
this.video = document.createElement('video');
|
|
9479
|
+
this.video.loop = loop;
|
|
9480
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
9481
|
+
this.video.muted = !soundEnable;
|
|
9482
|
+
this.video.style.display = 'none';
|
|
9483
|
+
this.video.src = src;
|
|
9484
|
+
document.body.appendChild(this.video);
|
|
9485
|
+
autoplay && this.play();
|
|
9486
|
+
}
|
|
9487
|
+
|
|
9488
|
+
/** Play or resume the video
|
|
9489
|
+
* @return {Promise} Promise that resolves when playback starts */
|
|
9490
|
+
play()
|
|
9491
|
+
{
|
|
9492
|
+
// try to play the video, catch any errors (autoplay may be blocked)
|
|
9493
|
+
const promise = this.video.play();
|
|
9494
|
+
promise?.catch(()=>{});
|
|
9495
|
+
return promise;
|
|
9496
|
+
}
|
|
9497
|
+
|
|
9498
|
+
/** Pause the video */
|
|
9499
|
+
pause() { this.video.pause(); }
|
|
9500
|
+
|
|
9501
|
+
/** Stop and reset the video */
|
|
9502
|
+
stop() { this.video.pause(); this.video.currentTime = 0; }
|
|
9503
|
+
|
|
9504
|
+
/** Check if video is currently loading
|
|
9505
|
+
* @return {boolean} */
|
|
9506
|
+
isLoadng()
|
|
9507
|
+
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
9508
|
+
|
|
9509
|
+
/** Check if video is currently paused
|
|
9510
|
+
* @return {boolean} */
|
|
9511
|
+
isPaused() { return this.video.paused; }
|
|
9512
|
+
|
|
9513
|
+
/** Check if video is currently playing
|
|
9514
|
+
* @return {boolean} */
|
|
9515
|
+
isPlaying()
|
|
9516
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
|
|
9517
|
+
|
|
9518
|
+
/** Check if video has ended playing
|
|
9519
|
+
* @return {boolean} */
|
|
9520
|
+
hasEnded() { return this.video.ended; }
|
|
9521
|
+
|
|
9522
|
+
/** Set volume (0-1)
|
|
9523
|
+
* @param {number} volume - Volume level (0-1) */
|
|
9524
|
+
setVolume(volume)
|
|
9525
|
+
{
|
|
9526
|
+
this.volume = volume;
|
|
9527
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
9528
|
+
}
|
|
9529
|
+
|
|
9530
|
+
/** Set playback speed
|
|
9531
|
+
* @param {number} rate - Playback rate multiplier */
|
|
9532
|
+
setPlaybackRate(rate) { this.video.playbackRate = rate; }
|
|
9533
|
+
|
|
9534
|
+
/** Get current time in seconds
|
|
9535
|
+
* @return {number} Current playback time */
|
|
9536
|
+
getCurrentTime() { return this.video.currentTime || 0; }
|
|
9537
|
+
|
|
9538
|
+
/** Get duration in seconds
|
|
9539
|
+
* @return {number} Total video duration */
|
|
9540
|
+
getDuration() { return this.video.duration || 0; }
|
|
9541
|
+
|
|
9542
|
+
/** Get the native video dimensions
|
|
9543
|
+
* @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
|
|
9544
|
+
getVideoSize()
|
|
9545
|
+
{ return vec2(this.video.videoWidth, this.video.videoHeight); }
|
|
9546
|
+
|
|
9547
|
+
/** Seek to time in seconds
|
|
9548
|
+
* @param {number} time - Time in seconds to seek to */
|
|
9549
|
+
setTime(time)
|
|
9550
|
+
{ this.video.currentTime = clamp(time, 0, this.getDuration()); }
|
|
9551
|
+
|
|
9552
|
+
update()
|
|
9553
|
+
{
|
|
9554
|
+
super.update();
|
|
9555
|
+
|
|
9556
|
+
// update volume based on global sound volume
|
|
9557
|
+
this.video.volume = clamp(this.volume * soundVolume);
|
|
9558
|
+
}
|
|
9559
|
+
|
|
9560
|
+
/** Render video to UI canvas */
|
|
9561
|
+
render()
|
|
9562
|
+
{
|
|
9563
|
+
super.render();
|
|
9564
|
+
|
|
9565
|
+
if (this.isLoadng())
|
|
9566
|
+
return;
|
|
9567
|
+
const context = uiSystem.uiContext;
|
|
9568
|
+
const s = this.size;
|
|
9569
|
+
context.save();
|
|
9570
|
+
context.translate(this.pos.x, this.pos.y);
|
|
9571
|
+
context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
|
|
9572
|
+
context.restore();
|
|
9573
|
+
}
|
|
9574
|
+
|
|
9575
|
+
/** Clean up video on destroy */
|
|
9576
|
+
destroy()
|
|
9577
|
+
{
|
|
9578
|
+
if (this.destroyed)
|
|
9579
|
+
return;
|
|
9580
|
+
|
|
9581
|
+
this.video.pause();
|
|
9582
|
+
this.video.remove();
|
|
9583
|
+
super.destroy();
|
|
9268
9584
|
}
|
|
9269
9585
|
}
|
|
9270
9586
|
/**
|
|
9271
9587
|
* LittleJS Box2D Physics Plugin
|
|
9272
9588
|
* - Box2dObject extends EngineObject with Box2D physics
|
|
9273
|
-
* - Call box2dInit()
|
|
9589
|
+
* - Call box2dInit() to enable
|
|
9274
9590
|
* - You will also need to include box2d.wasm.js
|
|
9275
|
-
* - Uses a super fast web assembly port of Box2D
|
|
9591
|
+
* - Uses a super fast web assembly port of Box2D v2.3.1
|
|
9276
9592
|
* - More info: https://github.com/kripken/box2d.js
|
|
9277
9593
|
* - Functions to create polygon, circle, and edge shapes
|
|
9278
9594
|
* - Contact begin and end callbacks
|
|
@@ -9302,9 +9618,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
|
|
|
9302
9618
|
///////////////////////////////////////////////////////////////////////////////
|
|
9303
9619
|
/**
|
|
9304
9620
|
* Box2D Object - extend with your own custom physics objects
|
|
9305
|
-
* - A LittleJS object with Box2D physics
|
|
9306
|
-
* - Each object has a Box2D body which can have multiple fixtures and joints
|
|
9621
|
+
* - A LittleJS object with Box2D physics, dynamic by default
|
|
9307
9622
|
* - Provides interface for Box2D body and fixture functions
|
|
9623
|
+
* - Each object can have multiple fixtures and joints
|
|
9308
9624
|
* @extends EngineObject
|
|
9309
9625
|
* @memberof Box2D
|
|
9310
9626
|
*/
|
|
@@ -9318,7 +9634,7 @@ class Box2dObject extends EngineObject
|
|
|
9318
9634
|
* @param {Color} [color]
|
|
9319
9635
|
* @param {number} [bodyType]
|
|
9320
9636
|
* @param {number} [renderOrder] */
|
|
9321
|
-
constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
|
|
9637
|
+
constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
|
|
9322
9638
|
{
|
|
9323
9639
|
super(pos, size, tileInfo, angle, color, renderOrder);
|
|
9324
9640
|
|
|
@@ -9330,24 +9646,27 @@ class Box2dObject extends EngineObject
|
|
|
9330
9646
|
this.body = box2d.world.CreateBody(bodyDef);
|
|
9331
9647
|
this.body.object = this;
|
|
9332
9648
|
this.lineColor = BLACK;
|
|
9649
|
+
box2d.objects.push(this);
|
|
9650
|
+
|
|
9651
|
+
// edge lists and loops for drawing
|
|
9652
|
+
this.edgeLists = [];
|
|
9653
|
+
this.edgeLoops = [];
|
|
9333
9654
|
}
|
|
9334
9655
|
|
|
9335
9656
|
/** Destroy this object and its physics body */
|
|
9336
9657
|
destroy()
|
|
9337
9658
|
{
|
|
9659
|
+
if (this.destroyed)
|
|
9660
|
+
return;
|
|
9661
|
+
|
|
9338
9662
|
// destroy physics body, fixtures, and joints
|
|
9339
|
-
this.body
|
|
9340
|
-
this.body
|
|
9663
|
+
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
9664
|
+
box2d.world.DestroyBody(this.body);
|
|
9341
9665
|
super.destroy();
|
|
9342
9666
|
}
|
|
9343
9667
|
|
|
9344
|
-
/**
|
|
9345
|
-
|
|
9346
|
-
{
|
|
9347
|
-
// use box2d physics update instead of normal engine update
|
|
9348
|
-
this.pos = box2d.vec2From(this.body.GetPosition());
|
|
9349
|
-
this.angle = -this.body.GetAngle();
|
|
9350
|
-
}
|
|
9668
|
+
/** Box2d objects updated with Box2d world step */
|
|
9669
|
+
updatePhysics() {}
|
|
9351
9670
|
|
|
9352
9671
|
/** Render the object, uses box2d drawing if no tile info exists */
|
|
9353
9672
|
render()
|
|
@@ -9373,10 +9692,23 @@ class Box2dObject extends EngineObject
|
|
|
9373
9692
|
* @param {Color} [lineColor]
|
|
9374
9693
|
* @param {number} [lineWidth]
|
|
9375
9694
|
* @param {CanvasRenderingContext2D} [context] */
|
|
9376
|
-
drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
|
|
9695
|
+
drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
|
|
9377
9696
|
{
|
|
9378
|
-
|
|
9379
|
-
|
|
9697
|
+
// draw non-edge fixtures
|
|
9698
|
+
this.getFixtureList().forEach((fixture)=>
|
|
9699
|
+
{
|
|
9700
|
+
const shape = box2d.castObjectType(fixture.GetShape());
|
|
9701
|
+
if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
|
|
9702
|
+
{
|
|
9703
|
+
box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
|
|
9704
|
+
}
|
|
9705
|
+
});
|
|
9706
|
+
|
|
9707
|
+
// draw edges using a single draw line for better connections
|
|
9708
|
+
this.edgeLists.forEach(points=>
|
|
9709
|
+
drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
|
|
9710
|
+
this.edgeLoops.forEach(points=>
|
|
9711
|
+
drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
|
|
9380
9712
|
}
|
|
9381
9713
|
|
|
9382
9714
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -9401,6 +9733,10 @@ class Box2dObject extends EngineObject
|
|
|
9401
9733
|
* @param {boolean} [isSensor] */
|
|
9402
9734
|
addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
|
|
9403
9735
|
{
|
|
9736
|
+
ASSERT(isNumber(density), 'density must be a number');
|
|
9737
|
+
ASSERT(isNumber(friction), 'friction must be a number');
|
|
9738
|
+
ASSERT(isNumber(restitution), 'restitution must be a number');
|
|
9739
|
+
|
|
9404
9740
|
const fd = new box2d.instance.b2FixtureDef();
|
|
9405
9741
|
fd.set_shape(shape);
|
|
9406
9742
|
fd.set_density(density);
|
|
@@ -9420,6 +9756,11 @@ class Box2dObject extends EngineObject
|
|
|
9420
9756
|
* @param {boolean} [isSensor] */
|
|
9421
9757
|
addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
|
|
9422
9758
|
{
|
|
9759
|
+
ASSERT(isVector2(size), 'size must be a Vector2');
|
|
9760
|
+
ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
|
|
9761
|
+
ASSERT(isVector2(offset), 'offset must be a Vector2');
|
|
9762
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
9763
|
+
|
|
9423
9764
|
const shape = new box2d.instance.b2PolygonShape();
|
|
9424
9765
|
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
9425
9766
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
@@ -9433,6 +9774,8 @@ class Box2dObject extends EngineObject
|
|
|
9433
9774
|
* @param {boolean} [isSensor] */
|
|
9434
9775
|
addPoly(points, density, friction, restitution, isSensor)
|
|
9435
9776
|
{
|
|
9777
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9778
|
+
|
|
9436
9779
|
function box2dCreatePolygonShape(points)
|
|
9437
9780
|
{
|
|
9438
9781
|
function box2dCreatePointList(points)
|
|
@@ -9468,6 +9811,9 @@ class Box2dObject extends EngineObject
|
|
|
9468
9811
|
* @param {boolean} [isSensor] */
|
|
9469
9812
|
addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
|
|
9470
9813
|
{
|
|
9814
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9815
|
+
ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
|
|
9816
|
+
|
|
9471
9817
|
const points = [];
|
|
9472
9818
|
const radius = diameter/2;
|
|
9473
9819
|
for (let i=sides; i--;)
|
|
@@ -9483,6 +9829,8 @@ class Box2dObject extends EngineObject
|
|
|
9483
9829
|
* @param {boolean} [isSensor] */
|
|
9484
9830
|
addRandomPoly(diameter=1, density, friction, restitution, isSensor)
|
|
9485
9831
|
{
|
|
9832
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9833
|
+
|
|
9486
9834
|
const sides = randInt(3, 9);
|
|
9487
9835
|
const points = [];
|
|
9488
9836
|
const radius = diameter/2;
|
|
@@ -9500,6 +9848,9 @@ class Box2dObject extends EngineObject
|
|
|
9500
9848
|
* @param {boolean} [isSensor] */
|
|
9501
9849
|
addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
|
|
9502
9850
|
{
|
|
9851
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9852
|
+
ASSERT(isVector2(offset), 'offset must be a Vector2');
|
|
9853
|
+
|
|
9503
9854
|
const shape = new box2d.instance.b2CircleShape();
|
|
9504
9855
|
shape.set_m_p(box2d.vec2dTo(offset));
|
|
9505
9856
|
shape.set_m_radius(diameter/2);
|
|
@@ -9515,53 +9866,63 @@ class Box2dObject extends EngineObject
|
|
|
9515
9866
|
* @param {boolean} [isSensor] */
|
|
9516
9867
|
addEdge(point1, point2, density, friction, restitution, isSensor)
|
|
9517
9868
|
{
|
|
9869
|
+
ASSERT(isVector2(point1), 'point1 must be a Vector2');
|
|
9870
|
+
ASSERT(isVector2(point2), 'point2 must be a Vector2');
|
|
9871
|
+
|
|
9518
9872
|
const shape = new box2d.instance.b2EdgeShape();
|
|
9519
9873
|
shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
|
|
9520
9874
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
9521
9875
|
}
|
|
9522
9876
|
|
|
9523
|
-
/** Add an edge
|
|
9877
|
+
/** Add an edge list to the body
|
|
9524
9878
|
* @param {Array<Vector2>} points
|
|
9525
9879
|
* @param {number} [density]
|
|
9526
9880
|
* @param {number} [friction]
|
|
9527
9881
|
* @param {number} [restitution]
|
|
9528
9882
|
* @param {boolean} [isSensor] */
|
|
9529
|
-
|
|
9883
|
+
addEdgeList(points, density, friction, restitution, isSensor)
|
|
9530
9884
|
{
|
|
9531
|
-
|
|
9532
|
-
const
|
|
9533
|
-
for (let i=0; i<points.length; ++i)
|
|
9885
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9886
|
+
const fixtures = [], edgePoints = [];
|
|
9887
|
+
for (let i=0; i<points.length-1; ++i)
|
|
9534
9888
|
{
|
|
9535
9889
|
const shape = new box2d.instance.b2EdgeShape();
|
|
9536
|
-
shape.set_m_vertex0(box2d.vec2dTo(
|
|
9537
|
-
shape.set_m_vertex1(box2d.vec2dTo(
|
|
9538
|
-
shape.set_m_vertex2(box2d.vec2dTo(
|
|
9539
|
-
shape.set_m_vertex3(box2d.vec2dTo(
|
|
9890
|
+
points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
|
|
9891
|
+
points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
|
|
9892
|
+
points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
|
|
9893
|
+
points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
|
|
9540
9894
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
9541
9895
|
fixtures.push(f);
|
|
9896
|
+
edgePoints.push(points[i].copy());
|
|
9542
9897
|
}
|
|
9898
|
+
edgePoints.push(points[points.length-1].copy());
|
|
9899
|
+
this.edgeLists.push(edgePoints);
|
|
9543
9900
|
return fixtures;
|
|
9544
9901
|
}
|
|
9545
9902
|
|
|
9546
|
-
/** Add an edge
|
|
9903
|
+
/** Add an edge loop to the body, an edge loop connects the end points
|
|
9547
9904
|
* @param {Array<Vector2>} points
|
|
9548
9905
|
* @param {number} [density]
|
|
9549
9906
|
* @param {number} [friction]
|
|
9550
9907
|
* @param {number} [restitution]
|
|
9551
9908
|
* @param {boolean} [isSensor] */
|
|
9552
|
-
|
|
9909
|
+
addEdgeLoop(points, density, friction, restitution, isSensor)
|
|
9553
9910
|
{
|
|
9554
|
-
|
|
9555
|
-
|
|
9911
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9912
|
+
const fixtures = [], edgePoints = [];
|
|
9913
|
+
const getPoint = i=> points[mod(i,points.length)];
|
|
9914
|
+
for (let i=0; i<points.length; ++i)
|
|
9556
9915
|
{
|
|
9557
9916
|
const shape = new box2d.instance.b2EdgeShape();
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9917
|
+
shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
|
|
9918
|
+
shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
|
|
9919
|
+
shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
|
|
9920
|
+
shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
|
|
9562
9921
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
9563
9922
|
fixtures.push(f);
|
|
9923
|
+
i < points.length && edgePoints.push(points[i].copy());
|
|
9564
9924
|
}
|
|
9925
|
+
this.edgeLoops.push(edgePoints);
|
|
9565
9926
|
return fixtures;
|
|
9566
9927
|
}
|
|
9567
9928
|
|
|
@@ -9596,6 +9957,10 @@ class Box2dObject extends EngineObject
|
|
|
9596
9957
|
* @return {number} */
|
|
9597
9958
|
getBodyType() { return this.body.GetType(); }
|
|
9598
9959
|
|
|
9960
|
+
/** Get the speed of this object
|
|
9961
|
+
* @return {number} */
|
|
9962
|
+
getSpeed() { return this.getLinearVelocity().length(); }
|
|
9963
|
+
|
|
9599
9964
|
///////////////////////////////////////////////////////////////////////////////
|
|
9600
9965
|
// physics set functions
|
|
9601
9966
|
|
|
@@ -9611,33 +9976,40 @@ class Box2dObject extends EngineObject
|
|
|
9611
9976
|
|
|
9612
9977
|
/** Sets the position
|
|
9613
9978
|
* @param {Vector2} pos */
|
|
9614
|
-
setPosition(pos)
|
|
9979
|
+
setPosition(pos)
|
|
9980
|
+
{ this.setTransform(pos, this.body.GetAngle()); }
|
|
9615
9981
|
|
|
9616
9982
|
/** Sets the angle
|
|
9617
9983
|
* @param {number} angle */
|
|
9618
|
-
setAngle(angle)
|
|
9984
|
+
setAngle(angle)
|
|
9985
|
+
{ this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
|
|
9619
9986
|
|
|
9620
9987
|
/** Sets the linear velocity
|
|
9621
9988
|
* @param {Vector2} velocity */
|
|
9622
|
-
setLinearVelocity(velocity)
|
|
9989
|
+
setLinearVelocity(velocity)
|
|
9990
|
+
{ this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
|
|
9623
9991
|
|
|
9624
9992
|
/** Sets the angular velocity
|
|
9625
9993
|
* @param {number} angularVelocity */
|
|
9626
|
-
setAngularVelocity(angularVelocity)
|
|
9994
|
+
setAngularVelocity(angularVelocity)
|
|
9995
|
+
{ this.body.SetAngularVelocity(angularVelocity); }
|
|
9627
9996
|
|
|
9628
9997
|
/** Sets the linear damping
|
|
9629
9998
|
* @param {number} damping */
|
|
9630
|
-
setLinearDamping(damping)
|
|
9999
|
+
setLinearDamping(damping)
|
|
10000
|
+
{ this.body.SetLinearDamping(damping); }
|
|
9631
10001
|
|
|
9632
10002
|
/** Sets the angular damping
|
|
9633
10003
|
* @param {number} damping */
|
|
9634
|
-
setAngularDamping(damping)
|
|
10004
|
+
setAngularDamping(damping)
|
|
10005
|
+
{ this.body.SetAngularDamping(damping); }
|
|
9635
10006
|
|
|
9636
10007
|
/** Sets the gravity scale
|
|
9637
10008
|
* @param {number} [scale] */
|
|
9638
|
-
setGravityScale(scale=1)
|
|
10009
|
+
setGravityScale(scale=1)
|
|
10010
|
+
{ this.body.SetGravityScale(this.gravityScale = scale); }
|
|
9639
10011
|
|
|
9640
|
-
/** Should
|
|
10012
|
+
/** Should be like a bullet for continuous collision detection?
|
|
9641
10013
|
* @param {boolean} [isBullet] */
|
|
9642
10014
|
setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
|
|
9643
10015
|
|
|
@@ -9651,11 +10023,13 @@ class Box2dObject extends EngineObject
|
|
|
9651
10023
|
|
|
9652
10024
|
/** Set whether the body is allowed to sleep
|
|
9653
10025
|
* @param {boolean} [isAllowed] */
|
|
9654
|
-
setSleepingAllowed(isAllowed=true)
|
|
10026
|
+
setSleepingAllowed(isAllowed=true)
|
|
10027
|
+
{ this.body.SetSleepingAllowed(isAllowed); }
|
|
9655
10028
|
|
|
9656
10029
|
/** Set whether the body can rotate
|
|
9657
10030
|
* @param {boolean} [isFixed] */
|
|
9658
|
-
setFixedRotation(isFixed=true)
|
|
10031
|
+
setFixedRotation(isFixed=true)
|
|
10032
|
+
{ this.body.SetFixedRotation(isFixed); }
|
|
9659
10033
|
|
|
9660
10034
|
/** Set the center of mass of the body
|
|
9661
10035
|
* @param {Vector2} center */
|
|
@@ -9667,10 +10041,11 @@ class Box2dObject extends EngineObject
|
|
|
9667
10041
|
|
|
9668
10042
|
/** Set the moment of inertia of the body
|
|
9669
10043
|
* @param {number} momentOfInertia */
|
|
9670
|
-
setMomentOfInertia(momentOfInertia)
|
|
10044
|
+
setMomentOfInertia(momentOfInertia)
|
|
10045
|
+
{ this.setMassData(undefined, undefined, momentOfInertia) }
|
|
9671
10046
|
|
|
9672
10047
|
/** Reset the mass, center of mass, and moment */
|
|
9673
|
-
resetMassData()
|
|
10048
|
+
resetMassData() { this.body.ResetMassData(); }
|
|
9674
10049
|
|
|
9675
10050
|
/** Set the mass data of the body
|
|
9676
10051
|
* @param {Vector2} [localCenter]
|
|
@@ -10312,6 +10687,50 @@ class Box2dPrismaticJoint extends Box2dJoint
|
|
|
10312
10687
|
getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
|
|
10313
10688
|
}
|
|
10314
10689
|
|
|
10690
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10691
|
+
/**
|
|
10692
|
+
* Box2D Static Object - Box2d with a static physics body
|
|
10693
|
+
* @extends Box2dObject
|
|
10694
|
+
* @memberof Box2D
|
|
10695
|
+
*/
|
|
10696
|
+
class Box2dStaticObject extends Box2dObject
|
|
10697
|
+
{
|
|
10698
|
+
/** Create a LittleJS object with Box2d physics
|
|
10699
|
+
* @param {Vector2} [pos]
|
|
10700
|
+
* @param {Vector2} [size]
|
|
10701
|
+
* @param {TileInfo} [tileInfo]
|
|
10702
|
+
* @param {number} [angle]
|
|
10703
|
+
* @param {Color} [color]
|
|
10704
|
+
* @param {number} [renderOrder] */
|
|
10705
|
+
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
|
|
10706
|
+
{
|
|
10707
|
+
const bodyType = box2d.bodyTypeStatic;
|
|
10708
|
+
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
|
|
10709
|
+
}
|
|
10710
|
+
}
|
|
10711
|
+
|
|
10712
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10713
|
+
/**
|
|
10714
|
+
* Box2D Kiematic Object - Box2d with a kinematic physics body
|
|
10715
|
+
* @extends Box2dObject
|
|
10716
|
+
* @memberof Box2D
|
|
10717
|
+
*/
|
|
10718
|
+
class Box2dKiematicObject extends Box2dObject
|
|
10719
|
+
{
|
|
10720
|
+
/** Create a LittleJS object with Box2d physics
|
|
10721
|
+
* @param {Vector2} [pos]
|
|
10722
|
+
* @param {Vector2} [size]
|
|
10723
|
+
* @param {TileInfo} [tileInfo]
|
|
10724
|
+
* @param {number} [angle]
|
|
10725
|
+
* @param {Color} [color]
|
|
10726
|
+
* @param {number} [renderOrder] */
|
|
10727
|
+
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
|
|
10728
|
+
{
|
|
10729
|
+
const bodyType = box2d.bodyTypeKinematic;
|
|
10730
|
+
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
|
|
10731
|
+
}
|
|
10732
|
+
}
|
|
10733
|
+
|
|
10315
10734
|
///////////////////////////////////////////////////////////////////////////////
|
|
10316
10735
|
/**
|
|
10317
10736
|
* Box2D Wheel Joint
|
|
@@ -10674,6 +11093,7 @@ class Box2dPlugin
|
|
|
10674
11093
|
box2d = this;
|
|
10675
11094
|
this.instance = instance;
|
|
10676
11095
|
this.world = new box2d.instance.b2World();
|
|
11096
|
+
this.objects = [];
|
|
10677
11097
|
|
|
10678
11098
|
/** @property {number} - Velocity iterations per update*/
|
|
10679
11099
|
this.velocityIterations = 8;
|
|
@@ -10916,9 +11336,10 @@ class Box2dPlugin
|
|
|
10916
11336
|
|
|
10917
11337
|
/** converts a box2d vec2 pointer to a Vector2
|
|
10918
11338
|
* @param {Object} v */
|
|
10919
|
-
vec2FromPointer(
|
|
11339
|
+
vec2FromPointer(vp)
|
|
10920
11340
|
{
|
|
10921
|
-
|
|
11341
|
+
const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
|
|
11342
|
+
return box2d.vec2From(v);
|
|
10922
11343
|
}
|
|
10923
11344
|
|
|
10924
11345
|
/** converts a Vector2 to a box2 vec2
|
|
@@ -10992,8 +11413,23 @@ async function box2dInit()
|
|
|
10992
11413
|
// add the box2d plugin to the engine
|
|
10993
11414
|
function box2dUpdate()
|
|
10994
11415
|
{
|
|
10995
|
-
if (
|
|
10996
|
-
|
|
11416
|
+
if (paused)
|
|
11417
|
+
return;
|
|
11418
|
+
|
|
11419
|
+
box2d.step();
|
|
11420
|
+
|
|
11421
|
+
// remove destroyed objects
|
|
11422
|
+
box2d.objects = box2d.objects.filter(o=>!o.destroyed);
|
|
11423
|
+
|
|
11424
|
+
// copy box2d physics results to engine objects
|
|
11425
|
+
for (const o of box2d.objects)
|
|
11426
|
+
{
|
|
11427
|
+
if (o.body)
|
|
11428
|
+
{
|
|
11429
|
+
o.pos = box2d.vec2From(o.body.GetPosition());
|
|
11430
|
+
o.angle = -o.body.GetAngle();
|
|
11431
|
+
}
|
|
11432
|
+
}
|
|
10997
11433
|
}
|
|
10998
11434
|
function box2dRender()
|
|
10999
11435
|
{
|