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.release.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
|
{
|
|
@@ -905,7 +908,7 @@ function percentLerp(value, percentA, percentB, lerpA, lerpB)
|
|
|
905
908
|
* @param {number} valueA
|
|
906
909
|
* @param {number} valueB
|
|
907
910
|
* @param {number} [wrapSize]
|
|
908
|
-
* @
|
|
911
|
+
* @return {number}
|
|
909
912
|
* @memberof Utilities */
|
|
910
913
|
function distanceWrap(valueA, valueB, wrapSize=1)
|
|
911
914
|
{ const d = (valueA - valueB) % wrapSize; return d*2 % wrapSize - d; }
|
|
@@ -915,7 +918,7 @@ function distanceWrap(valueA, valueB, wrapSize=1)
|
|
|
915
918
|
* @param {number} valueB
|
|
916
919
|
* @param {number} percent
|
|
917
920
|
* @param {number} [wrapSize]
|
|
918
|
-
* @
|
|
921
|
+
* @return {number}
|
|
919
922
|
* @memberof Utilities */
|
|
920
923
|
function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
921
924
|
{
|
|
@@ -927,7 +930,7 @@ function lerpWrap(valueA, valueB, percent, wrapSize=1)
|
|
|
927
930
|
/** Returns signed wrapped distance between the two angles passed in
|
|
928
931
|
* @param {number} angleA
|
|
929
932
|
* @param {number} angleB
|
|
930
|
-
* @
|
|
933
|
+
* @return {number}
|
|
931
934
|
* @memberof Utilities */
|
|
932
935
|
function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*PI); }
|
|
933
936
|
|
|
@@ -935,7 +938,7 @@ function distanceAngle(angleA, angleB) { return distanceWrap(angleA, angleB, 2*P
|
|
|
935
938
|
* @param {number} angleA
|
|
936
939
|
* @param {number} angleB
|
|
937
940
|
* @param {number} percent
|
|
938
|
-
* @
|
|
941
|
+
* @return {number}
|
|
939
942
|
* @memberof Utilities */
|
|
940
943
|
function lerpAngle(angleA, angleB, percent) { return lerpWrap(angleA, angleB, percent, 2*PI); }
|
|
941
944
|
|
|
@@ -1062,6 +1065,13 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
|
1062
1065
|
* @memberof Utilities */
|
|
1063
1066
|
function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
|
|
1064
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* Check if object is an array
|
|
1070
|
+
* @param {any} a
|
|
1071
|
+
* @return {boolean}
|
|
1072
|
+
* @memberof Utilities */
|
|
1073
|
+
function isArray(a) { return Array.isArray(a); }
|
|
1074
|
+
|
|
1065
1075
|
/**
|
|
1066
1076
|
* @callback LineTestFunction - Checks if a position is colliding
|
|
1067
1077
|
* @param {Vector2} pos
|
|
@@ -1761,7 +1771,6 @@ class Color
|
|
|
1761
1771
|
* @return {string} */
|
|
1762
1772
|
toString(useAlpha = true)
|
|
1763
1773
|
{
|
|
1764
|
-
ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
|
|
1765
1774
|
if (debug && !this.isValid())
|
|
1766
1775
|
return `#000`;
|
|
1767
1776
|
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
@@ -1898,11 +1907,14 @@ const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
|
1898
1907
|
class Timer
|
|
1899
1908
|
{
|
|
1900
1909
|
/** Create a timer object set time passed in
|
|
1901
|
-
* @param {number} [timeLeft] - How much time left before the timer
|
|
1902
|
-
|
|
1910
|
+
* @param {number} [timeLeft] - How much time left before the timer
|
|
1911
|
+
* @param {boolean} [useRealTime] - Should the timer keep running even when the game is paused? (useful for UI) */
|
|
1912
|
+
constructor(timeLeft, useRealTime=false)
|
|
1903
1913
|
{
|
|
1904
1914
|
ASSERT(timeLeft === undefined || isNumber(timeLeft), 'Constructed Timer is invalid.', timeLeft);
|
|
1905
|
-
this.
|
|
1915
|
+
this.useRealTime = useRealTime;
|
|
1916
|
+
const globalTime = this.getGlobalTime();
|
|
1917
|
+
this.time = timeLeft === undefined ? undefined : globalTime + timeLeft;
|
|
1906
1918
|
this.setTime = timeLeft;
|
|
1907
1919
|
}
|
|
1908
1920
|
|
|
@@ -1911,10 +1923,19 @@ class Timer
|
|
|
1911
1923
|
set(timeLeft=0)
|
|
1912
1924
|
{
|
|
1913
1925
|
ASSERT(isNumber(timeLeft), 'Timer is invalid.', timeLeft);
|
|
1914
|
-
|
|
1926
|
+
const globalTime = this.getGlobalTime();
|
|
1927
|
+
this.time = globalTime + timeLeft;
|
|
1915
1928
|
this.setTime = timeLeft;
|
|
1916
1929
|
}
|
|
1917
1930
|
|
|
1931
|
+
/** Set if the timer should keep running even when the game is paused
|
|
1932
|
+
* @param {boolean} [useRealTime] */
|
|
1933
|
+
setUseRealTime(useRealTime=true)
|
|
1934
|
+
{
|
|
1935
|
+
ASSERT(!this.isSet(), 'Cannot change global time setting while timer is set.');
|
|
1936
|
+
this.useRealTime = useRealTime;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1918
1939
|
/** Unset the timer */
|
|
1919
1940
|
unset() { this.time = undefined; }
|
|
1920
1941
|
|
|
@@ -1924,24 +1945,28 @@ class Timer
|
|
|
1924
1945
|
|
|
1925
1946
|
/** Returns true if set and has not elapsed
|
|
1926
1947
|
* @return {boolean} */
|
|
1927
|
-
active() { return
|
|
1948
|
+
active() { return this.getGlobalTime() < this.time; }
|
|
1928
1949
|
|
|
1929
1950
|
/** Returns true if set and elapsed
|
|
1930
1951
|
* @return {boolean} */
|
|
1931
|
-
elapsed() { return
|
|
1952
|
+
elapsed() { return this.getGlobalTime() >= this.time; }
|
|
1932
1953
|
|
|
1933
1954
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1934
1955
|
* @return {number} */
|
|
1935
|
-
get() { return this.isSet()?
|
|
1956
|
+
get() { return this.isSet()? this.getGlobalTime() - this.time : 0; }
|
|
1936
1957
|
|
|
1937
1958
|
/** Get percentage elapsed based on time it was set to, returns 0 if not set
|
|
1938
1959
|
* @return {number} */
|
|
1939
|
-
getPercent() { return this.isSet()? 1-percent(this.time -
|
|
1960
|
+
getPercent() { return this.isSet()? 1-percent(this.time - this.getGlobalTime(), 0, this.setTime) : 0; }
|
|
1940
1961
|
|
|
1941
1962
|
/** Get the time this timer was set to, returns 0 if not set
|
|
1942
1963
|
* @return {number} */
|
|
1943
1964
|
getSetTime() { return this.isSet() ? this.setTime : 0; }
|
|
1944
1965
|
|
|
1966
|
+
/** Get the current global time this timer is based on
|
|
1967
|
+
* @return {number} */
|
|
1968
|
+
getGlobalTime() { return this.useRealTime ? timeReal : time; }
|
|
1969
|
+
|
|
1945
1970
|
/** Returns this timer expressed as a string
|
|
1946
1971
|
* @return {string} */
|
|
1947
1972
|
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
@@ -2659,11 +2684,10 @@ class EngineObject
|
|
|
2659
2684
|
}
|
|
2660
2685
|
|
|
2661
2686
|
/** Update the object physics, called automatically by engine once each frame */
|
|
2662
|
-
|
|
2687
|
+
updatePhysics()
|
|
2663
2688
|
{
|
|
2664
2689
|
// child objects do not have physics
|
|
2665
|
-
|
|
2666
|
-
return;
|
|
2690
|
+
ASSERT(!this.parent);
|
|
2667
2691
|
|
|
2668
2692
|
if (this.clampSpeed)
|
|
2669
2693
|
{
|
|
@@ -2713,7 +2737,7 @@ class EngineObject
|
|
|
2713
2737
|
continue;
|
|
2714
2738
|
|
|
2715
2739
|
// check collision
|
|
2716
|
-
if (!
|
|
2740
|
+
if (!this.isOverlappingObject(o))
|
|
2717
2741
|
continue;
|
|
2718
2742
|
|
|
2719
2743
|
// notify objects of collision and check if should be resolved
|
|
@@ -2861,6 +2885,9 @@ class EngineObject
|
|
|
2861
2885
|
}
|
|
2862
2886
|
}
|
|
2863
2887
|
|
|
2888
|
+
/** Update the object, called automatically by engine once each frame. Does nothing by default. */
|
|
2889
|
+
update() {}
|
|
2890
|
+
|
|
2864
2891
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
2865
2892
|
render()
|
|
2866
2893
|
{
|
|
@@ -2926,6 +2953,10 @@ class EngineObject
|
|
|
2926
2953
|
* @return {number} */
|
|
2927
2954
|
getAliveTime() { return time - this.spawnTime; }
|
|
2928
2955
|
|
|
2956
|
+
/** Get the speed of this object
|
|
2957
|
+
* @return {number} */
|
|
2958
|
+
getSpeed() { return this.velocity.length(); }
|
|
2959
|
+
|
|
2929
2960
|
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
2930
2961
|
* @param {Vector2} acceleration */
|
|
2931
2962
|
applyAcceleration(acceleration)
|
|
@@ -2970,6 +3001,20 @@ class EngineObject
|
|
|
2970
3001
|
child.parent = 0;
|
|
2971
3002
|
}
|
|
2972
3003
|
|
|
3004
|
+
/** Check if overlapping another engine object
|
|
3005
|
+
* Collisions are resoloved to prevent overlaps
|
|
3006
|
+
* @param {EngineObject} object
|
|
3007
|
+
* @return {boolean} */
|
|
3008
|
+
isOverlappingObject(object)
|
|
3009
|
+
{ return this.isOverlapping(object.pos, object.size); }
|
|
3010
|
+
|
|
3011
|
+
/** Check if overlapping a point or aligned bounding box
|
|
3012
|
+
* @param {Vector2} pos - Center of box
|
|
3013
|
+
* @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
|
|
3014
|
+
* @return {boolean} */
|
|
3015
|
+
isOverlapping(pos, size=vec2())
|
|
3016
|
+
{ return isOverlapping(this.pos, this.size, pos, size); }
|
|
3017
|
+
|
|
2973
3018
|
/** Set how this object collides
|
|
2974
3019
|
* @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
|
|
2975
3020
|
* @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
|
|
@@ -3315,7 +3360,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3315
3360
|
{
|
|
3316
3361
|
// normal canvas 2D rendering method (slower)
|
|
3317
3362
|
++drawCount;
|
|
3318
|
-
size = new Vector2(size.x, -size.y); //
|
|
3363
|
+
size = new Vector2(size.x, -size.y); // flip upside down sprites
|
|
3319
3364
|
drawCanvas2D(pos, size, angle, mirror, (context)=>
|
|
3320
3365
|
{
|
|
3321
3366
|
if (textureInfo)
|
|
@@ -3424,7 +3469,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3424
3469
|
* @memberof Draw */
|
|
3425
3470
|
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
3426
3471
|
{
|
|
3427
|
-
ASSERT(
|
|
3472
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
3428
3473
|
ASSERT(isNumber(width), 'width must be a number');
|
|
3429
3474
|
ASSERT(isColor(color), 'color is invalid');
|
|
3430
3475
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -3530,7 +3575,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
3530
3575
|
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
|
|
3531
3576
|
{
|
|
3532
3577
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3533
|
-
ASSERT(
|
|
3578
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
3534
3579
|
ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
|
|
3535
3580
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
3536
3581
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
@@ -3682,11 +3727,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3682
3727
|
* @param {string} [font=fontDefault]
|
|
3683
3728
|
* @param {string} [fontStyle]
|
|
3684
3729
|
* @param {number} [maxWidth]
|
|
3730
|
+
* @param {number} [angle]
|
|
3685
3731
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3686
3732
|
* @memberof Draw */
|
|
3687
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
|
|
3733
|
+
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
3688
3734
|
{
|
|
3689
|
-
drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
|
|
3735
|
+
drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
3690
3736
|
}
|
|
3691
3737
|
|
|
3692
3738
|
/** Draw text on overlay canvas in world space
|
|
@@ -3701,10 +3747,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
3701
3747
|
* @param {string} [font=fontDefault]
|
|
3702
3748
|
* @param {string} [fontStyle]
|
|
3703
3749
|
* @param {number} [maxWidth]
|
|
3750
|
+
* @param {number} [angle]
|
|
3704
3751
|
* @memberof Draw */
|
|
3705
|
-
function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
|
|
3752
|
+
function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
|
|
3706
3753
|
{
|
|
3707
|
-
drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
|
|
3754
|
+
drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
|
|
3708
3755
|
}
|
|
3709
3756
|
|
|
3710
3757
|
/** Draw text on overlay canvas in screen space
|
|
@@ -3719,9 +3766,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3719
3766
|
* @param {string} [font=fontDefault]
|
|
3720
3767
|
* @param {string} [fontStyle]
|
|
3721
3768
|
* @param {number} [maxWidth]
|
|
3769
|
+
* @param {number} [angle]
|
|
3722
3770
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
3723
3771
|
* @memberof Draw */
|
|
3724
|
-
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
|
|
3772
|
+
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
|
|
3725
3773
|
{
|
|
3726
3774
|
ASSERT(isString(text), 'text must be a string');
|
|
3727
3775
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -3732,6 +3780,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
3732
3780
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
3733
3781
|
ASSERT(isString(font), 'font must be a string');
|
|
3734
3782
|
ASSERT(isString(fontStyle), 'fontStyle must be a string');
|
|
3783
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3735
3784
|
|
|
3736
3785
|
context.fillStyle = color.toString();
|
|
3737
3786
|
context.strokeStyle = lineColor.toString();
|
|
@@ -3741,14 +3790,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
3741
3790
|
context.textBaseline = 'middle';
|
|
3742
3791
|
|
|
3743
3792
|
const lines = (text+'').split('\n');
|
|
3744
|
-
|
|
3745
|
-
|
|
3793
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
3794
|
+
context.save();
|
|
3795
|
+
context.translate(pos.x, posY);
|
|
3796
|
+
context.rotate(-angle);
|
|
3797
|
+
let yOffset = 0;
|
|
3746
3798
|
lines.forEach(line=>
|
|
3747
3799
|
{
|
|
3748
|
-
lineWidth && context.strokeText(line,
|
|
3749
|
-
context.fillText(line,
|
|
3750
|
-
|
|
3800
|
+
lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
|
|
3801
|
+
context.fillText(line, 0, yOffset, maxWidth);
|
|
3802
|
+
yOffset += size;
|
|
3751
3803
|
});
|
|
3804
|
+
context.restore();
|
|
3752
3805
|
}
|
|
3753
3806
|
|
|
3754
3807
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -4180,6 +4233,11 @@ let mouseDeltaScreen = vec2();
|
|
|
4180
4233
|
* @memberof Input */
|
|
4181
4234
|
let mouseWheel = 0;
|
|
4182
4235
|
|
|
4236
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4237
|
+
* @type {boolean}
|
|
4238
|
+
* @memberof Input */
|
|
4239
|
+
let mouseInWindow = true;
|
|
4240
|
+
|
|
4183
4241
|
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
4184
4242
|
* @type {boolean}
|
|
4185
4243
|
* @memberof Input */
|
|
@@ -4274,6 +4332,7 @@ function inputInit()
|
|
|
4274
4332
|
document.addEventListener('mousedown', onMouseDown);
|
|
4275
4333
|
document.addEventListener('mouseup', onMouseUp);
|
|
4276
4334
|
document.addEventListener('mousemove', onMouseMove);
|
|
4335
|
+
document.addEventListener('mouseleave', onMouseLeave);
|
|
4277
4336
|
document.addEventListener('wheel', onMouseWheel);
|
|
4278
4337
|
document.addEventListener('contextmenu', onContextMenu);
|
|
4279
4338
|
document.addEventListener('blur', onBlur);
|
|
@@ -4298,14 +4357,14 @@ function inputInit()
|
|
|
4298
4357
|
if (inputWASDEmulateDirection)
|
|
4299
4358
|
inputData[0][remapKey(e.code)] = 4;
|
|
4300
4359
|
}
|
|
4301
|
-
function remapKey(
|
|
4360
|
+
function remapKey(k)
|
|
4302
4361
|
{
|
|
4303
4362
|
// handle remapping wasd keys to directions
|
|
4304
4363
|
return inputWASDEmulateDirection ?
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4364
|
+
k === 'KeyW' ? 'ArrowUp' :
|
|
4365
|
+
k === 'KeyS' ? 'ArrowDown' :
|
|
4366
|
+
k === 'KeyA' ? 'ArrowLeft' :
|
|
4367
|
+
k === 'KeyD' ? 'ArrowRight' : k : k;
|
|
4309
4368
|
}
|
|
4310
4369
|
function onMouseDown(e)
|
|
4311
4370
|
{
|
|
@@ -4319,7 +4378,7 @@ function inputInit()
|
|
|
4319
4378
|
isUsingGamepad = false;
|
|
4320
4379
|
inputData[0][e.button] = 3;
|
|
4321
4380
|
|
|
4322
|
-
|
|
4381
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4323
4382
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4324
4383
|
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4325
4384
|
inputPreventDefault && e.button && e.preventDefault();
|
|
@@ -4332,10 +4391,17 @@ function inputInit()
|
|
|
4332
4391
|
}
|
|
4333
4392
|
function onMouseMove(e)
|
|
4334
4393
|
{
|
|
4335
|
-
|
|
4394
|
+
mouseInWindow = true;
|
|
4395
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4336
4396
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4337
|
-
|
|
4397
|
+
|
|
4398
|
+
// when pointer is locked use movementX/Y for delta
|
|
4399
|
+
const movement = pointerLockIsActive() ?
|
|
4400
|
+
vec2(e.movementX, e.movementY) :
|
|
4401
|
+
mousePosScreen.subtract(mousePosScreenLast);
|
|
4402
|
+
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
4338
4403
|
}
|
|
4404
|
+
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
4339
4405
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
4340
4406
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
4341
4407
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
@@ -4474,7 +4540,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
|
|
|
4474
4540
|
function touchGamepadButtonCenter()
|
|
4475
4541
|
{
|
|
4476
4542
|
// draw right face buttons
|
|
4477
|
-
|
|
4543
|
+
const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4478
4544
|
if (touchGamepadButtonCount <= 2)
|
|
4479
4545
|
center.x += touchGamepadSize/2;
|
|
4480
4546
|
return center;
|
|
@@ -4484,14 +4550,13 @@ function touchGamepadButtonCenter()
|
|
|
4484
4550
|
function touchInputInit()
|
|
4485
4551
|
{
|
|
4486
4552
|
// add non passive touch event listeners
|
|
4487
|
-
let handleTouch = handleTouchDefault;
|
|
4488
4553
|
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4489
4554
|
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4490
4555
|
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4491
4556
|
|
|
4492
4557
|
// handle all touch events the same way
|
|
4493
4558
|
let wasTouching;
|
|
4494
|
-
function
|
|
4559
|
+
function handleTouch(e)
|
|
4495
4560
|
{
|
|
4496
4561
|
if (!touchInputEnable)
|
|
4497
4562
|
return;
|
|
@@ -4642,7 +4707,7 @@ function touchGamepadRender()
|
|
|
4642
4707
|
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
4643
4708
|
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4644
4709
|
{
|
|
4645
|
-
|
|
4710
|
+
const j = mod(i-1, 4);
|
|
4646
4711
|
let button = touchGamepadButtonCount > 2 ?
|
|
4647
4712
|
j : min(j, touchGamepadButtonCount-1);
|
|
4648
4713
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
@@ -4744,6 +4809,10 @@ class Sound
|
|
|
4744
4809
|
{
|
|
4745
4810
|
if (!soundEnable || headlessMode) return;
|
|
4746
4811
|
|
|
4812
|
+
ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
|
|
4813
|
+
ASSERT(isNumber(range), 'range must be a number');
|
|
4814
|
+
ASSERT(isNumber(taper), 'taper must be a number');
|
|
4815
|
+
|
|
4747
4816
|
/** @property {number} - World space max range of sound */
|
|
4748
4817
|
this.range = range;
|
|
4749
4818
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
@@ -4782,6 +4851,12 @@ class Sound
|
|
|
4782
4851
|
*/
|
|
4783
4852
|
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
4784
4853
|
{
|
|
4854
|
+
ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
|
|
4855
|
+
ASSERT(isNumber(volume), 'volume must be a number');
|
|
4856
|
+
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
4857
|
+
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
4858
|
+
|
|
4859
|
+
|
|
4785
4860
|
if (!soundEnable || headlessMode) return;
|
|
4786
4861
|
if (!this.sampleChannels) return;
|
|
4787
4862
|
|
|
@@ -4827,6 +4902,7 @@ class Sound
|
|
|
4827
4902
|
*/
|
|
4828
4903
|
playNote(semitoneOffset=0, pos, volume)
|
|
4829
4904
|
{
|
|
4905
|
+
ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
|
|
4830
4906
|
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
4831
4907
|
return this.play(pos, volume, pitch, 0);
|
|
4832
4908
|
}
|
|
@@ -4835,7 +4911,7 @@ class Sound
|
|
|
4835
4911
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
4836
4912
|
*/
|
|
4837
4913
|
getDuration()
|
|
4838
|
-
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
4914
|
+
{ return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
|
|
4839
4915
|
|
|
4840
4916
|
/** Check if sound is loaded, for sounds fetched from a url
|
|
4841
4917
|
* @return {boolean} - True if sound is loaded and ready to play
|
|
@@ -4877,6 +4953,7 @@ class SoundWave extends Sound
|
|
|
4877
4953
|
super(undefined, range, taper);
|
|
4878
4954
|
if (!soundEnable || headlessMode) return;
|
|
4879
4955
|
ASSERT(!filename || isString(filename), 'filename must be a string');
|
|
4956
|
+
ASSERT(isNumber(randomness), 'randomness must be a number');
|
|
4880
4957
|
|
|
4881
4958
|
/** @property {SoundLoadCallback} - callback function to call when sound is loaded */
|
|
4882
4959
|
this.onloadCallback = onloadCallback;
|
|
@@ -5093,7 +5170,7 @@ class SoundInstance
|
|
|
5093
5170
|
/** Get the total duration of this sound
|
|
5094
5171
|
* @return {number} - Total duration in seconds
|
|
5095
5172
|
*/
|
|
5096
|
-
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
5173
|
+
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
5097
5174
|
|
|
5098
5175
|
/** Get source of this sound instance
|
|
5099
5176
|
* @return {AudioBufferSourceNode}
|
|
@@ -5407,6 +5484,7 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
5407
5484
|
}
|
|
5408
5485
|
|
|
5409
5486
|
/** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
|
|
5487
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5410
5488
|
* @param {Vector2} posStart
|
|
5411
5489
|
* @param {Vector2} posEnd
|
|
5412
5490
|
* @param {EngineObject} [object] - An object or undefined for generic test
|
|
@@ -5980,6 +6058,7 @@ class TileCollisionLayer extends TileLayer
|
|
|
5980
6058
|
}
|
|
5981
6059
|
|
|
5982
6060
|
/** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
|
|
6061
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5983
6062
|
* @param {Vector2} posStart
|
|
5984
6063
|
* @param {Vector2} posEnd
|
|
5985
6064
|
* @param {EngineObject} [object] - An object or undefined for generic test
|
|
@@ -6172,12 +6251,12 @@ class ParticleEmitter extends EngineObject
|
|
|
6172
6251
|
this.previousPos = this.pos.copy();
|
|
6173
6252
|
}
|
|
6174
6253
|
|
|
6254
|
+
/** Emitters do not have physics */
|
|
6255
|
+
updatePhysics() {}
|
|
6256
|
+
|
|
6175
6257
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
6176
6258
|
update()
|
|
6177
6259
|
{
|
|
6178
|
-
// only do default update to apply parent transforms
|
|
6179
|
-
this.parent && super.update();
|
|
6180
|
-
|
|
6181
6260
|
if (this.velocityInheritance)
|
|
6182
6261
|
{
|
|
6183
6262
|
// pass emitter velocity to particles
|
|
@@ -6194,7 +6273,7 @@ class ParticleEmitter extends EngineObject
|
|
|
6194
6273
|
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
6195
6274
|
{
|
|
6196
6275
|
// emit particles
|
|
6197
|
-
if (this.emitRate
|
|
6276
|
+
if (this.emitRate && particleEmitRateScale)
|
|
6198
6277
|
{
|
|
6199
6278
|
const rate = 1/this.emitRate/particleEmitRateScale;
|
|
6200
6279
|
for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
|
|
@@ -6332,8 +6411,6 @@ class Particle extends EngineObject
|
|
|
6332
6411
|
/** Update the object physics, called automatically by engine once each frame */
|
|
6333
6412
|
update()
|
|
6334
6413
|
{
|
|
6335
|
-
super.update();
|
|
6336
|
-
|
|
6337
6414
|
if (this.collideTiles || this.collideSolidObjects)
|
|
6338
6415
|
{
|
|
6339
6416
|
// only apply max circular speed if particle can collide
|
|
@@ -6577,11 +6654,11 @@ class Medal
|
|
|
6577
6654
|
const descriptionSize = height*.3;
|
|
6578
6655
|
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
6579
6656
|
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
6580
|
-
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
6657
|
+
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
6581
6658
|
|
|
6582
6659
|
// draw the description
|
|
6583
6660
|
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
6584
|
-
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
6661
|
+
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
6585
6662
|
context.restore();
|
|
6586
6663
|
}
|
|
6587
6664
|
|
|
@@ -8030,11 +8107,16 @@ class UISystemPlugin
|
|
|
8030
8107
|
// reset hover object at start of update
|
|
8031
8108
|
uiSystem.lastHoverObject = uiSystem.hoverObject;
|
|
8032
8109
|
uiSystem.hoverObject = undefined;
|
|
8110
|
+
|
|
8111
|
+
// update in reverse order so topmost objects get priority
|
|
8033
8112
|
for (let i = uiSystem.uiObjects.length; i--;)
|
|
8034
8113
|
{
|
|
8035
8114
|
const o = uiSystem.uiObjects[i];
|
|
8036
8115
|
o.parent || updateObject(o)
|
|
8037
8116
|
}
|
|
8117
|
+
|
|
8118
|
+
// remove destroyed objects
|
|
8119
|
+
uiSystem.uiObjects = uiSystem.uiObjects.filter(o=>!o.destroyed);
|
|
8038
8120
|
}
|
|
8039
8121
|
function uiRender()
|
|
8040
8122
|
{
|
|
@@ -8067,12 +8149,12 @@ class UISystemPlugin
|
|
|
8067
8149
|
/** Draw a rectangle to the UI context
|
|
8068
8150
|
* @param {Vector2} pos
|
|
8069
8151
|
* @param {Vector2} size
|
|
8070
|
-
* @param {Color} [color
|
|
8071
|
-
* @param {number} [lineWidth
|
|
8072
|
-
* @param {Color} [lineColor
|
|
8073
|
-
* @param {number} [cornerRadius
|
|
8074
|
-
* @param {Color} [gradientColor
|
|
8075
|
-
drawRect(pos, size, color=
|
|
8152
|
+
* @param {Color} [color]
|
|
8153
|
+
* @param {number} [lineWidth]
|
|
8154
|
+
* @param {Color} [lineColor]
|
|
8155
|
+
* @param {number} [cornerRadius]
|
|
8156
|
+
* @param {Color} [gradientColor] */
|
|
8157
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
|
|
8076
8158
|
{
|
|
8077
8159
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
8078
8160
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -8151,10 +8233,14 @@ class UISystemPlugin
|
|
|
8151
8233
|
* @param {string} [align]
|
|
8152
8234
|
* @param {string} [font=uiSystem.defaultFont]
|
|
8153
8235
|
* @param {string} [fontStyle]
|
|
8154
|
-
* @param {boolean} [applyMaxWidth=true]
|
|
8155
|
-
|
|
8236
|
+
* @param {boolean} [applyMaxWidth=true]
|
|
8237
|
+
* @param {Vector2} [textShadow]
|
|
8238
|
+
*/
|
|
8239
|
+
drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
|
|
8156
8240
|
{
|
|
8157
|
-
|
|
8241
|
+
if (textShadow)
|
|
8242
|
+
drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
|
|
8243
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
|
|
8158
8244
|
}
|
|
8159
8245
|
|
|
8160
8246
|
/**
|
|
@@ -8181,6 +8267,34 @@ class UISystemPlugin
|
|
|
8181
8267
|
setCallback(onDragLeave, 'dragleave');
|
|
8182
8268
|
setCallback(onDragOver, 'dragover');
|
|
8183
8269
|
}
|
|
8270
|
+
|
|
8271
|
+
/** Convert a screen space position to native UI position
|
|
8272
|
+
* @param {Vector2} pos
|
|
8273
|
+
* @return {Vector2}
|
|
8274
|
+
*/
|
|
8275
|
+
screenToNative(pos)
|
|
8276
|
+
{
|
|
8277
|
+
if (!uiSystem.nativeHeight)
|
|
8278
|
+
return pos;
|
|
8279
|
+
|
|
8280
|
+
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
8281
|
+
const sInv = 1/s;
|
|
8282
|
+
const p = pos.copy();
|
|
8283
|
+
p.x += s*mainCanvasSize.x/2;
|
|
8284
|
+
p.x *= sInv;
|
|
8285
|
+
p.y *= sInv;
|
|
8286
|
+
p.x -= sInv*mainCanvasSize.x/2;
|
|
8287
|
+
return p;
|
|
8288
|
+
}
|
|
8289
|
+
|
|
8290
|
+
/** Destroy and remove all objects
|
|
8291
|
+
* @memberof Engine */
|
|
8292
|
+
destroyObjects()
|
|
8293
|
+
{
|
|
8294
|
+
for (const o of this.uiObjects)
|
|
8295
|
+
o.parent || o.destroy();
|
|
8296
|
+
this.uiObjects = this.uiObjects.filter(o=>!o.destroyed);
|
|
8297
|
+
}
|
|
8184
8298
|
}
|
|
8185
8299
|
|
|
8186
8300
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -8257,6 +8371,9 @@ class UIObject
|
|
|
8257
8371
|
/** @property {boolean} - True if this can be a hover object */
|
|
8258
8372
|
this.canBeHover = true;
|
|
8259
8373
|
uiSystem.uiObjects.push(this);
|
|
8374
|
+
|
|
8375
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
8376
|
+
this.textShadow = undefined;
|
|
8260
8377
|
}
|
|
8261
8378
|
|
|
8262
8379
|
/** Add a child UIObject to this object
|
|
@@ -8279,29 +8396,41 @@ class UIObject
|
|
|
8279
8396
|
child.parent = undefined;
|
|
8280
8397
|
}
|
|
8281
8398
|
|
|
8399
|
+
|
|
8400
|
+
/** Destroy this object, destroy its children, detach its parent, and mark it for removal */
|
|
8401
|
+
destroy()
|
|
8402
|
+
{
|
|
8403
|
+
if (this.destroyed)
|
|
8404
|
+
return;
|
|
8405
|
+
|
|
8406
|
+
// disconnect from parent and destroy children
|
|
8407
|
+
this.destroyed = 1;
|
|
8408
|
+
this.parent && this.parent.removeChild(this);
|
|
8409
|
+
for (const child of this.children)
|
|
8410
|
+
{
|
|
8411
|
+
child.parent = 0;
|
|
8412
|
+
child.destroy();
|
|
8413
|
+
}
|
|
8414
|
+
}
|
|
8282
8415
|
/** Check if the mouse is overlapping a box in screen space
|
|
8283
8416
|
* @return {boolean} - True if overlapping
|
|
8284
8417
|
*/
|
|
8285
8418
|
isMouseOverlapping()
|
|
8286
8419
|
{
|
|
8420
|
+
if (!mouseInWindow) return false;
|
|
8421
|
+
|
|
8287
8422
|
const size = !isTouchDevice ? this.size :
|
|
8288
8423
|
this.size.add(vec2(this.extraTouchSize || 0));
|
|
8289
|
-
|
|
8290
|
-
return isOverlapping(this.pos, size, mousePosScreen);
|
|
8291
|
-
|
|
8292
|
-
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
8293
|
-
const sInv = 1/s;
|
|
8294
|
-
let pos = mousePosScreen.copy();
|
|
8295
|
-
pos.x += s*mainCanvasSize.x/2;
|
|
8296
|
-
pos.x *= sInv;
|
|
8297
|
-
pos.y *= sInv;
|
|
8298
|
-
pos.x -= sInv*mainCanvasSize.x/2;
|
|
8424
|
+
const pos = uiSystem.screenToNative(mousePosScreen);
|
|
8299
8425
|
return isOverlapping(this.pos, size, pos);
|
|
8300
8426
|
}
|
|
8301
8427
|
|
|
8302
8428
|
/** Update the object, called automatically by plugin once each frame */
|
|
8303
8429
|
update()
|
|
8304
8430
|
{
|
|
8431
|
+
// call the custom update callback
|
|
8432
|
+
this.onUpdate();
|
|
8433
|
+
|
|
8305
8434
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
8306
8435
|
const isActive = this.isActiveObject();
|
|
8307
8436
|
const mouseDown = mouseIsDown(0);
|
|
@@ -8358,7 +8487,7 @@ class UIObject
|
|
|
8358
8487
|
|
|
8359
8488
|
const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
|
|
8360
8489
|
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
8361
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
8490
|
+
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
|
|
8362
8491
|
}
|
|
8363
8492
|
|
|
8364
8493
|
/** Special update when object is not visible */
|
|
@@ -8385,6 +8514,9 @@ class UIObject
|
|
|
8385
8514
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
8386
8515
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
8387
8516
|
|
|
8517
|
+
/** Called each frame when object updates */
|
|
8518
|
+
onUpdate() {}
|
|
8519
|
+
|
|
8388
8520
|
/** Called when the mouse enters the object */
|
|
8389
8521
|
onEnter() {}
|
|
8390
8522
|
|
|
@@ -8439,8 +8571,9 @@ class UIText extends UIObject
|
|
|
8439
8571
|
}
|
|
8440
8572
|
render()
|
|
8441
8573
|
{
|
|
8574
|
+
// only render the text
|
|
8442
8575
|
const textSize = this.getTextSize();
|
|
8443
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
|
|
8576
|
+
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8444
8577
|
}
|
|
8445
8578
|
}
|
|
8446
8579
|
|
|
@@ -8516,7 +8649,7 @@ class UIButton extends UIObject
|
|
|
8516
8649
|
// draw the text scaled to fit
|
|
8517
8650
|
const textSize = this.getTextSize();
|
|
8518
8651
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8519
|
-
this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
|
|
8652
|
+
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8520
8653
|
}
|
|
8521
8654
|
}
|
|
8522
8655
|
|
|
@@ -8570,7 +8703,7 @@ class UICheckbox extends UIObject
|
|
|
8570
8703
|
const textSize = this.getTextSize();
|
|
8571
8704
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
8572
8705
|
uiSystem.drawText(this.text, pos, textSize,
|
|
8573
|
-
this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
|
|
8706
|
+
this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
8574
8707
|
}
|
|
8575
8708
|
}
|
|
8576
8709
|
|
|
@@ -8625,9 +8758,11 @@ class UIScrollbar extends UIObject
|
|
|
8625
8758
|
const p1 = centerPos - handleWidth/2;
|
|
8626
8759
|
const p2 = centerPos + handleWidth/2;
|
|
8627
8760
|
const oldValue = this.value;
|
|
8761
|
+
|
|
8762
|
+
const p = uiSystem.screenToNative(mousePosScreen);
|
|
8628
8763
|
this.value = isHorizontal ?
|
|
8629
|
-
percent(
|
|
8630
|
-
percent(
|
|
8764
|
+
percent(p.x, p1, p2) :
|
|
8765
|
+
percent(p.y, p2, p1);
|
|
8631
8766
|
this.value === oldValue || this.onChange();
|
|
8632
8767
|
}
|
|
8633
8768
|
}
|
|
@@ -8649,20 +8784,164 @@ class UIScrollbar extends UIObject
|
|
|
8649
8784
|
vec2(lerp(p1, p2, this.value), this.pos.y) :
|
|
8650
8785
|
vec2(this.pos.x, lerp(p2, p1, this.value))
|
|
8651
8786
|
const handleColor = this.disabled ? this.disabledColor : this.handleColor;
|
|
8652
|
-
uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
8787
|
+
uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
8653
8788
|
|
|
8654
8789
|
// draw the text scaled to fit on the scrollbar
|
|
8655
8790
|
const textSize = this.getTextSize();
|
|
8656
8791
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8657
|
-
this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
|
|
8792
|
+
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8796
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
8797
|
+
/**
|
|
8798
|
+
* VideoPlayerUIObject - A UI object that plays video
|
|
8799
|
+
* @extends UIObject
|
|
8800
|
+
* @example
|
|
8801
|
+
* // Create a video player UI object
|
|
8802
|
+
* const video = new VideoPlayerUIObject(vec2(400, 300), vec2(320, 240), 'cutscene.mp4', true);
|
|
8803
|
+
* video.play();
|
|
8804
|
+
* @memberof UISystem
|
|
8805
|
+
*/
|
|
8806
|
+
class UIVideo extends UIObject
|
|
8807
|
+
{
|
|
8808
|
+
/** Create a video player UI object
|
|
8809
|
+
* @param {Vector2} [pos]
|
|
8810
|
+
* @param {Vector2} [size]
|
|
8811
|
+
* @param {string} src - Video file path or URL
|
|
8812
|
+
* @param {boolean} [autoplay=false] - Start playing immediately?
|
|
8813
|
+
* @param {boolean} [loop=false] - Loop the video?
|
|
8814
|
+
* @param {number} [volume=1] - Volume percent scaled by global volume (0-1)
|
|
8815
|
+
*/
|
|
8816
|
+
constructor(pos, size, src, autoplay=false, loop=false, volume=1)
|
|
8817
|
+
{
|
|
8818
|
+
super(pos, size || vec2());
|
|
8819
|
+
|
|
8820
|
+
ASSERT(isString(src), 'video src must be a string');
|
|
8821
|
+
ASSERT(isNumber(volume), 'video volume must be a number');
|
|
8822
|
+
|
|
8823
|
+
this.color = BLACK; // default to black background
|
|
8824
|
+
this.cornerRadius = 0; // default to no corner radius
|
|
8825
|
+
|
|
8826
|
+
/** @property {float} - The video volume */
|
|
8827
|
+
this.volume = volume;
|
|
8828
|
+
|
|
8829
|
+
// create video element
|
|
8830
|
+
/** @property {HTMLVideoElement} - The video player */
|
|
8831
|
+
this.video = document.createElement('video');
|
|
8832
|
+
this.video.loop = loop;
|
|
8833
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
8834
|
+
this.video.muted = !soundEnable;
|
|
8835
|
+
this.video.style.display = 'none';
|
|
8836
|
+
this.video.src = src;
|
|
8837
|
+
document.body.appendChild(this.video);
|
|
8838
|
+
autoplay && this.play();
|
|
8839
|
+
}
|
|
8840
|
+
|
|
8841
|
+
/** Play or resume the video
|
|
8842
|
+
* @return {Promise} Promise that resolves when playback starts */
|
|
8843
|
+
play()
|
|
8844
|
+
{
|
|
8845
|
+
// try to play the video, catch any errors (autoplay may be blocked)
|
|
8846
|
+
const promise = this.video.play();
|
|
8847
|
+
promise?.catch(()=>{});
|
|
8848
|
+
return promise;
|
|
8849
|
+
}
|
|
8850
|
+
|
|
8851
|
+
/** Pause the video */
|
|
8852
|
+
pause() { this.video.pause(); }
|
|
8853
|
+
|
|
8854
|
+
/** Stop and reset the video */
|
|
8855
|
+
stop() { this.video.pause(); this.video.currentTime = 0; }
|
|
8856
|
+
|
|
8857
|
+
/** Check if video is currently loading
|
|
8858
|
+
* @return {boolean} */
|
|
8859
|
+
isLoadng()
|
|
8860
|
+
{ return this.video.readyState < this.video.HAVE_CURRENT_DATA; }
|
|
8861
|
+
|
|
8862
|
+
/** Check if video is currently paused
|
|
8863
|
+
* @return {boolean} */
|
|
8864
|
+
isPaused() { return this.video.paused; }
|
|
8865
|
+
|
|
8866
|
+
/** Check if video is currently playing
|
|
8867
|
+
* @return {boolean} */
|
|
8868
|
+
isPlaying()
|
|
8869
|
+
{ return !this.isPaused() && !this.hasEnded() && !this.isLoadng(); }
|
|
8870
|
+
|
|
8871
|
+
/** Check if video has ended playing
|
|
8872
|
+
* @return {boolean} */
|
|
8873
|
+
hasEnded() { return this.video.ended; }
|
|
8874
|
+
|
|
8875
|
+
/** Set volume (0-1)
|
|
8876
|
+
* @param {number} volume - Volume level (0-1) */
|
|
8877
|
+
setVolume(volume)
|
|
8878
|
+
{
|
|
8879
|
+
this.volume = volume;
|
|
8880
|
+
this.video.volume = clamp(volume * soundVolume);
|
|
8881
|
+
}
|
|
8882
|
+
|
|
8883
|
+
/** Set playback speed
|
|
8884
|
+
* @param {number} rate - Playback rate multiplier */
|
|
8885
|
+
setPlaybackRate(rate) { this.video.playbackRate = rate; }
|
|
8886
|
+
|
|
8887
|
+
/** Get current time in seconds
|
|
8888
|
+
* @return {number} Current playback time */
|
|
8889
|
+
getCurrentTime() { return this.video.currentTime || 0; }
|
|
8890
|
+
|
|
8891
|
+
/** Get duration in seconds
|
|
8892
|
+
* @return {number} Total video duration */
|
|
8893
|
+
getDuration() { return this.video.duration || 0; }
|
|
8894
|
+
|
|
8895
|
+
/** Get the native video dimensions
|
|
8896
|
+
* @return {Vector2} Video dimensions (may be 0,0 if metadata not loaded) */
|
|
8897
|
+
getVideoSize()
|
|
8898
|
+
{ return vec2(this.video.videoWidth, this.video.videoHeight); }
|
|
8899
|
+
|
|
8900
|
+
/** Seek to time in seconds
|
|
8901
|
+
* @param {number} time - Time in seconds to seek to */
|
|
8902
|
+
setTime(time)
|
|
8903
|
+
{ this.video.currentTime = clamp(time, 0, this.getDuration()); }
|
|
8904
|
+
|
|
8905
|
+
update()
|
|
8906
|
+
{
|
|
8907
|
+
super.update();
|
|
8908
|
+
|
|
8909
|
+
// update volume based on global sound volume
|
|
8910
|
+
this.video.volume = clamp(this.volume * soundVolume);
|
|
8911
|
+
}
|
|
8912
|
+
|
|
8913
|
+
/** Render video to UI canvas */
|
|
8914
|
+
render()
|
|
8915
|
+
{
|
|
8916
|
+
super.render();
|
|
8917
|
+
|
|
8918
|
+
if (this.isLoadng())
|
|
8919
|
+
return;
|
|
8920
|
+
const context = uiSystem.uiContext;
|
|
8921
|
+
const s = this.size;
|
|
8922
|
+
context.save();
|
|
8923
|
+
context.translate(this.pos.x, this.pos.y);
|
|
8924
|
+
context.drawImage(this.video, -s.x/2, -s.y/2, s.x, s.y);
|
|
8925
|
+
context.restore();
|
|
8926
|
+
}
|
|
8927
|
+
|
|
8928
|
+
/** Clean up video on destroy */
|
|
8929
|
+
destroy()
|
|
8930
|
+
{
|
|
8931
|
+
if (this.destroyed)
|
|
8932
|
+
return;
|
|
8933
|
+
|
|
8934
|
+
this.video.pause();
|
|
8935
|
+
this.video.remove();
|
|
8936
|
+
super.destroy();
|
|
8658
8937
|
}
|
|
8659
8938
|
}
|
|
8660
8939
|
/**
|
|
8661
8940
|
* LittleJS Box2D Physics Plugin
|
|
8662
8941
|
* - Box2dObject extends EngineObject with Box2D physics
|
|
8663
|
-
* - Call box2dInit()
|
|
8942
|
+
* - Call box2dInit() to enable
|
|
8664
8943
|
* - You will also need to include box2d.wasm.js
|
|
8665
|
-
* - Uses a super fast web assembly port of Box2D
|
|
8944
|
+
* - Uses a super fast web assembly port of Box2D v2.3.1
|
|
8666
8945
|
* - More info: https://github.com/kripken/box2d.js
|
|
8667
8946
|
* - Functions to create polygon, circle, and edge shapes
|
|
8668
8947
|
* - Contact begin and end callbacks
|
|
@@ -8692,9 +8971,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
|
|
|
8692
8971
|
///////////////////////////////////////////////////////////////////////////////
|
|
8693
8972
|
/**
|
|
8694
8973
|
* Box2D Object - extend with your own custom physics objects
|
|
8695
|
-
* - A LittleJS object with Box2D physics
|
|
8696
|
-
* - Each object has a Box2D body which can have multiple fixtures and joints
|
|
8974
|
+
* - A LittleJS object with Box2D physics, dynamic by default
|
|
8697
8975
|
* - Provides interface for Box2D body and fixture functions
|
|
8976
|
+
* - Each object can have multiple fixtures and joints
|
|
8698
8977
|
* @extends EngineObject
|
|
8699
8978
|
* @memberof Box2D
|
|
8700
8979
|
*/
|
|
@@ -8708,7 +8987,7 @@ class Box2dObject extends EngineObject
|
|
|
8708
8987
|
* @param {Color} [color]
|
|
8709
8988
|
* @param {number} [bodyType]
|
|
8710
8989
|
* @param {number} [renderOrder] */
|
|
8711
|
-
constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
|
|
8990
|
+
constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
|
|
8712
8991
|
{
|
|
8713
8992
|
super(pos, size, tileInfo, angle, color, renderOrder);
|
|
8714
8993
|
|
|
@@ -8720,24 +8999,27 @@ class Box2dObject extends EngineObject
|
|
|
8720
8999
|
this.body = box2d.world.CreateBody(bodyDef);
|
|
8721
9000
|
this.body.object = this;
|
|
8722
9001
|
this.lineColor = BLACK;
|
|
9002
|
+
box2d.objects.push(this);
|
|
9003
|
+
|
|
9004
|
+
// edge lists and loops for drawing
|
|
9005
|
+
this.edgeLists = [];
|
|
9006
|
+
this.edgeLoops = [];
|
|
8723
9007
|
}
|
|
8724
9008
|
|
|
8725
9009
|
/** Destroy this object and its physics body */
|
|
8726
9010
|
destroy()
|
|
8727
9011
|
{
|
|
9012
|
+
if (this.destroyed)
|
|
9013
|
+
return;
|
|
9014
|
+
|
|
8728
9015
|
// destroy physics body, fixtures, and joints
|
|
8729
|
-
this.body
|
|
8730
|
-
this.body
|
|
9016
|
+
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
9017
|
+
box2d.world.DestroyBody(this.body);
|
|
8731
9018
|
super.destroy();
|
|
8732
9019
|
}
|
|
8733
9020
|
|
|
8734
|
-
/**
|
|
8735
|
-
|
|
8736
|
-
{
|
|
8737
|
-
// use box2d physics update instead of normal engine update
|
|
8738
|
-
this.pos = box2d.vec2From(this.body.GetPosition());
|
|
8739
|
-
this.angle = -this.body.GetAngle();
|
|
8740
|
-
}
|
|
9021
|
+
/** Box2d objects updated with Box2d world step */
|
|
9022
|
+
updatePhysics() {}
|
|
8741
9023
|
|
|
8742
9024
|
/** Render the object, uses box2d drawing if no tile info exists */
|
|
8743
9025
|
render()
|
|
@@ -8763,10 +9045,23 @@ class Box2dObject extends EngineObject
|
|
|
8763
9045
|
* @param {Color} [lineColor]
|
|
8764
9046
|
* @param {number} [lineWidth]
|
|
8765
9047
|
* @param {CanvasRenderingContext2D} [context] */
|
|
8766
|
-
drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
|
|
9048
|
+
drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
|
|
8767
9049
|
{
|
|
8768
|
-
|
|
8769
|
-
|
|
9050
|
+
// draw non-edge fixtures
|
|
9051
|
+
this.getFixtureList().forEach((fixture)=>
|
|
9052
|
+
{
|
|
9053
|
+
const shape = box2d.castObjectType(fixture.GetShape());
|
|
9054
|
+
if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
|
|
9055
|
+
{
|
|
9056
|
+
box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
|
|
9057
|
+
}
|
|
9058
|
+
});
|
|
9059
|
+
|
|
9060
|
+
// draw edges using a single draw line for better connections
|
|
9061
|
+
this.edgeLists.forEach(points=>
|
|
9062
|
+
drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
|
|
9063
|
+
this.edgeLoops.forEach(points=>
|
|
9064
|
+
drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
|
|
8770
9065
|
}
|
|
8771
9066
|
|
|
8772
9067
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -8791,6 +9086,10 @@ class Box2dObject extends EngineObject
|
|
|
8791
9086
|
* @param {boolean} [isSensor] */
|
|
8792
9087
|
addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
|
|
8793
9088
|
{
|
|
9089
|
+
ASSERT(isNumber(density), 'density must be a number');
|
|
9090
|
+
ASSERT(isNumber(friction), 'friction must be a number');
|
|
9091
|
+
ASSERT(isNumber(restitution), 'restitution must be a number');
|
|
9092
|
+
|
|
8794
9093
|
const fd = new box2d.instance.b2FixtureDef();
|
|
8795
9094
|
fd.set_shape(shape);
|
|
8796
9095
|
fd.set_density(density);
|
|
@@ -8810,6 +9109,11 @@ class Box2dObject extends EngineObject
|
|
|
8810
9109
|
* @param {boolean} [isSensor] */
|
|
8811
9110
|
addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
|
|
8812
9111
|
{
|
|
9112
|
+
ASSERT(isVector2(size), 'size must be a Vector2');
|
|
9113
|
+
ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
|
|
9114
|
+
ASSERT(isVector2(offset), 'offset must be a Vector2');
|
|
9115
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
9116
|
+
|
|
8813
9117
|
const shape = new box2d.instance.b2PolygonShape();
|
|
8814
9118
|
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
8815
9119
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
@@ -8823,6 +9127,8 @@ class Box2dObject extends EngineObject
|
|
|
8823
9127
|
* @param {boolean} [isSensor] */
|
|
8824
9128
|
addPoly(points, density, friction, restitution, isSensor)
|
|
8825
9129
|
{
|
|
9130
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9131
|
+
|
|
8826
9132
|
function box2dCreatePolygonShape(points)
|
|
8827
9133
|
{
|
|
8828
9134
|
function box2dCreatePointList(points)
|
|
@@ -8858,6 +9164,9 @@ class Box2dObject extends EngineObject
|
|
|
8858
9164
|
* @param {boolean} [isSensor] */
|
|
8859
9165
|
addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
|
|
8860
9166
|
{
|
|
9167
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9168
|
+
ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
|
|
9169
|
+
|
|
8861
9170
|
const points = [];
|
|
8862
9171
|
const radius = diameter/2;
|
|
8863
9172
|
for (let i=sides; i--;)
|
|
@@ -8873,6 +9182,8 @@ class Box2dObject extends EngineObject
|
|
|
8873
9182
|
* @param {boolean} [isSensor] */
|
|
8874
9183
|
addRandomPoly(diameter=1, density, friction, restitution, isSensor)
|
|
8875
9184
|
{
|
|
9185
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9186
|
+
|
|
8876
9187
|
const sides = randInt(3, 9);
|
|
8877
9188
|
const points = [];
|
|
8878
9189
|
const radius = diameter/2;
|
|
@@ -8890,6 +9201,9 @@ class Box2dObject extends EngineObject
|
|
|
8890
9201
|
* @param {boolean} [isSensor] */
|
|
8891
9202
|
addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
|
|
8892
9203
|
{
|
|
9204
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9205
|
+
ASSERT(isVector2(offset), 'offset must be a Vector2');
|
|
9206
|
+
|
|
8893
9207
|
const shape = new box2d.instance.b2CircleShape();
|
|
8894
9208
|
shape.set_m_p(box2d.vec2dTo(offset));
|
|
8895
9209
|
shape.set_m_radius(diameter/2);
|
|
@@ -8905,53 +9219,63 @@ class Box2dObject extends EngineObject
|
|
|
8905
9219
|
* @param {boolean} [isSensor] */
|
|
8906
9220
|
addEdge(point1, point2, density, friction, restitution, isSensor)
|
|
8907
9221
|
{
|
|
9222
|
+
ASSERT(isVector2(point1), 'point1 must be a Vector2');
|
|
9223
|
+
ASSERT(isVector2(point2), 'point2 must be a Vector2');
|
|
9224
|
+
|
|
8908
9225
|
const shape = new box2d.instance.b2EdgeShape();
|
|
8909
9226
|
shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
|
|
8910
9227
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
8911
9228
|
}
|
|
8912
9229
|
|
|
8913
|
-
/** Add an edge
|
|
9230
|
+
/** Add an edge list to the body
|
|
8914
9231
|
* @param {Array<Vector2>} points
|
|
8915
9232
|
* @param {number} [density]
|
|
8916
9233
|
* @param {number} [friction]
|
|
8917
9234
|
* @param {number} [restitution]
|
|
8918
9235
|
* @param {boolean} [isSensor] */
|
|
8919
|
-
|
|
9236
|
+
addEdgeList(points, density, friction, restitution, isSensor)
|
|
8920
9237
|
{
|
|
8921
|
-
|
|
8922
|
-
const
|
|
8923
|
-
for (let i=0; i<points.length; ++i)
|
|
9238
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9239
|
+
const fixtures = [], edgePoints = [];
|
|
9240
|
+
for (let i=0; i<points.length-1; ++i)
|
|
8924
9241
|
{
|
|
8925
9242
|
const shape = new box2d.instance.b2EdgeShape();
|
|
8926
|
-
shape.set_m_vertex0(box2d.vec2dTo(
|
|
8927
|
-
shape.set_m_vertex1(box2d.vec2dTo(
|
|
8928
|
-
shape.set_m_vertex2(box2d.vec2dTo(
|
|
8929
|
-
shape.set_m_vertex3(box2d.vec2dTo(
|
|
9243
|
+
points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
|
|
9244
|
+
points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
|
|
9245
|
+
points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
|
|
9246
|
+
points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
|
|
8930
9247
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
8931
9248
|
fixtures.push(f);
|
|
9249
|
+
edgePoints.push(points[i].copy());
|
|
8932
9250
|
}
|
|
9251
|
+
edgePoints.push(points[points.length-1].copy());
|
|
9252
|
+
this.edgeLists.push(edgePoints);
|
|
8933
9253
|
return fixtures;
|
|
8934
9254
|
}
|
|
8935
9255
|
|
|
8936
|
-
/** Add an edge
|
|
9256
|
+
/** Add an edge loop to the body, an edge loop connects the end points
|
|
8937
9257
|
* @param {Array<Vector2>} points
|
|
8938
9258
|
* @param {number} [density]
|
|
8939
9259
|
* @param {number} [friction]
|
|
8940
9260
|
* @param {number} [restitution]
|
|
8941
9261
|
* @param {boolean} [isSensor] */
|
|
8942
|
-
|
|
9262
|
+
addEdgeLoop(points, density, friction, restitution, isSensor)
|
|
8943
9263
|
{
|
|
8944
|
-
|
|
8945
|
-
|
|
9264
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9265
|
+
const fixtures = [], edgePoints = [];
|
|
9266
|
+
const getPoint = i=> points[mod(i,points.length)];
|
|
9267
|
+
for (let i=0; i<points.length; ++i)
|
|
8946
9268
|
{
|
|
8947
9269
|
const shape = new box2d.instance.b2EdgeShape();
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
9270
|
+
shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
|
|
9271
|
+
shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
|
|
9272
|
+
shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
|
|
9273
|
+
shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
|
|
8952
9274
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
8953
9275
|
fixtures.push(f);
|
|
9276
|
+
i < points.length && edgePoints.push(points[i].copy());
|
|
8954
9277
|
}
|
|
9278
|
+
this.edgeLoops.push(edgePoints);
|
|
8955
9279
|
return fixtures;
|
|
8956
9280
|
}
|
|
8957
9281
|
|
|
@@ -8986,6 +9310,10 @@ class Box2dObject extends EngineObject
|
|
|
8986
9310
|
* @return {number} */
|
|
8987
9311
|
getBodyType() { return this.body.GetType(); }
|
|
8988
9312
|
|
|
9313
|
+
/** Get the speed of this object
|
|
9314
|
+
* @return {number} */
|
|
9315
|
+
getSpeed() { return this.getLinearVelocity().length(); }
|
|
9316
|
+
|
|
8989
9317
|
///////////////////////////////////////////////////////////////////////////////
|
|
8990
9318
|
// physics set functions
|
|
8991
9319
|
|
|
@@ -9001,33 +9329,40 @@ class Box2dObject extends EngineObject
|
|
|
9001
9329
|
|
|
9002
9330
|
/** Sets the position
|
|
9003
9331
|
* @param {Vector2} pos */
|
|
9004
|
-
setPosition(pos)
|
|
9332
|
+
setPosition(pos)
|
|
9333
|
+
{ this.setTransform(pos, this.body.GetAngle()); }
|
|
9005
9334
|
|
|
9006
9335
|
/** Sets the angle
|
|
9007
9336
|
* @param {number} angle */
|
|
9008
|
-
setAngle(angle)
|
|
9337
|
+
setAngle(angle)
|
|
9338
|
+
{ this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
|
|
9009
9339
|
|
|
9010
9340
|
/** Sets the linear velocity
|
|
9011
9341
|
* @param {Vector2} velocity */
|
|
9012
|
-
setLinearVelocity(velocity)
|
|
9342
|
+
setLinearVelocity(velocity)
|
|
9343
|
+
{ this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
|
|
9013
9344
|
|
|
9014
9345
|
/** Sets the angular velocity
|
|
9015
9346
|
* @param {number} angularVelocity */
|
|
9016
|
-
setAngularVelocity(angularVelocity)
|
|
9347
|
+
setAngularVelocity(angularVelocity)
|
|
9348
|
+
{ this.body.SetAngularVelocity(angularVelocity); }
|
|
9017
9349
|
|
|
9018
9350
|
/** Sets the linear damping
|
|
9019
9351
|
* @param {number} damping */
|
|
9020
|
-
setLinearDamping(damping)
|
|
9352
|
+
setLinearDamping(damping)
|
|
9353
|
+
{ this.body.SetLinearDamping(damping); }
|
|
9021
9354
|
|
|
9022
9355
|
/** Sets the angular damping
|
|
9023
9356
|
* @param {number} damping */
|
|
9024
|
-
setAngularDamping(damping)
|
|
9357
|
+
setAngularDamping(damping)
|
|
9358
|
+
{ this.body.SetAngularDamping(damping); }
|
|
9025
9359
|
|
|
9026
9360
|
/** Sets the gravity scale
|
|
9027
9361
|
* @param {number} [scale] */
|
|
9028
|
-
setGravityScale(scale=1)
|
|
9362
|
+
setGravityScale(scale=1)
|
|
9363
|
+
{ this.body.SetGravityScale(this.gravityScale = scale); }
|
|
9029
9364
|
|
|
9030
|
-
/** Should
|
|
9365
|
+
/** Should be like a bullet for continuous collision detection?
|
|
9031
9366
|
* @param {boolean} [isBullet] */
|
|
9032
9367
|
setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
|
|
9033
9368
|
|
|
@@ -9041,11 +9376,13 @@ class Box2dObject extends EngineObject
|
|
|
9041
9376
|
|
|
9042
9377
|
/** Set whether the body is allowed to sleep
|
|
9043
9378
|
* @param {boolean} [isAllowed] */
|
|
9044
|
-
setSleepingAllowed(isAllowed=true)
|
|
9379
|
+
setSleepingAllowed(isAllowed=true)
|
|
9380
|
+
{ this.body.SetSleepingAllowed(isAllowed); }
|
|
9045
9381
|
|
|
9046
9382
|
/** Set whether the body can rotate
|
|
9047
9383
|
* @param {boolean} [isFixed] */
|
|
9048
|
-
setFixedRotation(isFixed=true)
|
|
9384
|
+
setFixedRotation(isFixed=true)
|
|
9385
|
+
{ this.body.SetFixedRotation(isFixed); }
|
|
9049
9386
|
|
|
9050
9387
|
/** Set the center of mass of the body
|
|
9051
9388
|
* @param {Vector2} center */
|
|
@@ -9057,10 +9394,11 @@ class Box2dObject extends EngineObject
|
|
|
9057
9394
|
|
|
9058
9395
|
/** Set the moment of inertia of the body
|
|
9059
9396
|
* @param {number} momentOfInertia */
|
|
9060
|
-
setMomentOfInertia(momentOfInertia)
|
|
9397
|
+
setMomentOfInertia(momentOfInertia)
|
|
9398
|
+
{ this.setMassData(undefined, undefined, momentOfInertia) }
|
|
9061
9399
|
|
|
9062
9400
|
/** Reset the mass, center of mass, and moment */
|
|
9063
|
-
resetMassData()
|
|
9401
|
+
resetMassData() { this.body.ResetMassData(); }
|
|
9064
9402
|
|
|
9065
9403
|
/** Set the mass data of the body
|
|
9066
9404
|
* @param {Vector2} [localCenter]
|
|
@@ -9702,6 +10040,50 @@ class Box2dPrismaticJoint extends Box2dJoint
|
|
|
9702
10040
|
getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
|
|
9703
10041
|
}
|
|
9704
10042
|
|
|
10043
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10044
|
+
/**
|
|
10045
|
+
* Box2D Static Object - Box2d with a static physics body
|
|
10046
|
+
* @extends Box2dObject
|
|
10047
|
+
* @memberof Box2D
|
|
10048
|
+
*/
|
|
10049
|
+
class Box2dStaticObject extends Box2dObject
|
|
10050
|
+
{
|
|
10051
|
+
/** Create a LittleJS object with Box2d physics
|
|
10052
|
+
* @param {Vector2} [pos]
|
|
10053
|
+
* @param {Vector2} [size]
|
|
10054
|
+
* @param {TileInfo} [tileInfo]
|
|
10055
|
+
* @param {number} [angle]
|
|
10056
|
+
* @param {Color} [color]
|
|
10057
|
+
* @param {number} [renderOrder] */
|
|
10058
|
+
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
|
|
10059
|
+
{
|
|
10060
|
+
const bodyType = box2d.bodyTypeStatic;
|
|
10061
|
+
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
|
|
10062
|
+
}
|
|
10063
|
+
}
|
|
10064
|
+
|
|
10065
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
10066
|
+
/**
|
|
10067
|
+
* Box2D Kiematic Object - Box2d with a kinematic physics body
|
|
10068
|
+
* @extends Box2dObject
|
|
10069
|
+
* @memberof Box2D
|
|
10070
|
+
*/
|
|
10071
|
+
class Box2dKiematicObject extends Box2dObject
|
|
10072
|
+
{
|
|
10073
|
+
/** Create a LittleJS object with Box2d physics
|
|
10074
|
+
* @param {Vector2} [pos]
|
|
10075
|
+
* @param {Vector2} [size]
|
|
10076
|
+
* @param {TileInfo} [tileInfo]
|
|
10077
|
+
* @param {number} [angle]
|
|
10078
|
+
* @param {Color} [color]
|
|
10079
|
+
* @param {number} [renderOrder] */
|
|
10080
|
+
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
|
|
10081
|
+
{
|
|
10082
|
+
const bodyType = box2d.bodyTypeKinematic;
|
|
10083
|
+
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
|
|
9705
10087
|
///////////////////////////////////////////////////////////////////////////////
|
|
9706
10088
|
/**
|
|
9707
10089
|
* Box2D Wheel Joint
|
|
@@ -10064,6 +10446,7 @@ class Box2dPlugin
|
|
|
10064
10446
|
box2d = this;
|
|
10065
10447
|
this.instance = instance;
|
|
10066
10448
|
this.world = new box2d.instance.b2World();
|
|
10449
|
+
this.objects = [];
|
|
10067
10450
|
|
|
10068
10451
|
/** @property {number} - Velocity iterations per update*/
|
|
10069
10452
|
this.velocityIterations = 8;
|
|
@@ -10306,9 +10689,10 @@ class Box2dPlugin
|
|
|
10306
10689
|
|
|
10307
10690
|
/** converts a box2d vec2 pointer to a Vector2
|
|
10308
10691
|
* @param {Object} v */
|
|
10309
|
-
vec2FromPointer(
|
|
10692
|
+
vec2FromPointer(vp)
|
|
10310
10693
|
{
|
|
10311
|
-
|
|
10694
|
+
const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
|
|
10695
|
+
return box2d.vec2From(v);
|
|
10312
10696
|
}
|
|
10313
10697
|
|
|
10314
10698
|
/** converts a Vector2 to a box2 vec2
|
|
@@ -10382,8 +10766,23 @@ async function box2dInit()
|
|
|
10382
10766
|
// add the box2d plugin to the engine
|
|
10383
10767
|
function box2dUpdate()
|
|
10384
10768
|
{
|
|
10385
|
-
if (
|
|
10386
|
-
|
|
10769
|
+
if (paused)
|
|
10770
|
+
return;
|
|
10771
|
+
|
|
10772
|
+
box2d.step();
|
|
10773
|
+
|
|
10774
|
+
// remove destroyed objects
|
|
10775
|
+
box2d.objects = box2d.objects.filter(o=>!o.destroyed);
|
|
10776
|
+
|
|
10777
|
+
// copy box2d physics results to engine objects
|
|
10778
|
+
for (const o of box2d.objects)
|
|
10779
|
+
{
|
|
10780
|
+
if (o.body)
|
|
10781
|
+
{
|
|
10782
|
+
o.pos = box2d.vec2From(o.body.GetPosition());
|
|
10783
|
+
o.angle = -o.body.GetAngle();
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10387
10786
|
}
|
|
10388
10787
|
function box2dRender()
|
|
10389
10788
|
{
|