littlejsengine 1.14.19 → 1.14.28
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 +217 -38
- package/dist/littlejs.esm.js +879 -324
- package/dist/littlejs.esm.min.js +1 -1
- package/dist/littlejs.js +861 -324
- package/dist/littlejs.min.js +1 -1
- package/dist/littlejs.release.js +799 -325
- package/examples/box2d/gameObjects.js +6 -10
- package/examples/box2d/scenes.js +2 -2
- package/examples/breakout/gameObjects.js +1 -1
- package/examples/electron/game.js +1 -41
- package/examples/electron/index.html +2 -2
- package/examples/htmlMenu/game.js +1 -1
- package/examples/index.html +19 -10
- package/examples/module/game.js +5 -7
- package/examples/particles/index.html +6 -9
- package/examples/platformer/gameCharacter.js +4 -3
- package/examples/platformer/gameEffects.js +7 -2
- package/examples/platformer/gameObjects.js +6 -11
- package/examples/shorts/animation.js +2 -2
- package/examples/shorts/base.html +4 -1
- package/examples/shorts/box2d.js +2 -2
- package/examples/shorts/box2dCar.js +18 -7
- package/examples/shorts/box2dPool.js +108 -0
- package/examples/shorts/cameraDrag.js +22 -0
- package/examples/shorts/debugDraw.js +37 -0
- package/examples/shorts/flappyGame.js +1 -0
- package/examples/shorts/fps.js +90 -0
- package/examples/shorts/helloWorld.js +1 -1
- package/examples/shorts/hillGlideGame.js +1 -3
- 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/postProcess.js +1 -1
- package/examples/shorts/sequencer.js +3 -3
- package/examples/shorts/slidingPuzzle.js +1 -1
- package/examples/shorts/spaceGame.js +1 -3
- package/examples/shorts/spriteAtlas.js +6 -8
- package/examples/shorts/starfield.js +1 -1
- package/examples/shorts/tileRaycast.js +39 -0
- package/examples/shorts/tiles.png +0 -0
- package/examples/shorts/tiltedView.js +1 -4
- package/examples/shorts/timers.js +1 -2
- package/examples/shorts/topDown.js +0 -2
- package/examples/starter/game.js +5 -7
- package/examples/starter/index.html +2 -2
- package/examples/typescript/game.js +3 -2
- package/examples/typescript/game.ts +5 -7
- package/examples/uiSystem/game.js +2 -2
- package/package.json +1 -1
- package/plugins/box2d.js +170 -50
- package/plugins/pluginExport.js +2 -0
- package/plugins/uiSystem.js +55 -27
- package/src/engine.js +19 -16
- package/src/engineAudio.js +27 -15
- package/src/engineDebug.js +64 -0
- package/src/engineDraw.js +88 -32
- package/src/engineExport.js +16 -0
- package/src/engineInput.js +35 -25
- package/src/engineMedals.js +2 -2
- package/src/engineObject.js +55 -10
- package/src/engineParticles.js +36 -20
- package/src/engineRelease.js +2 -1
- package/src/engineSettings.js +20 -1
- package/src/engineTileLayer.js +64 -59
- package/src/engineUtilities.js +213 -59
- package/src/engineWebGL.js +13 -8
- package/examples/shorts/raycasting.js +0 -79
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.0';
|
|
37
37
|
|
|
38
38
|
/** Frames per second to update
|
|
39
39
|
* @type {number}
|
|
@@ -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
|
|
@@ -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
|
{
|
|
@@ -516,7 +519,7 @@ function engineObjectsCollect(pos, size, objects=engineObjects)
|
|
|
516
519
|
|
|
517
520
|
/**
|
|
518
521
|
* @callback ObjectCallbackFunction - Function that processes an object
|
|
519
|
-
* @param {EngineObject}
|
|
522
|
+
* @param {EngineObject} object
|
|
520
523
|
* @memberof Engine
|
|
521
524
|
*/
|
|
522
525
|
|
|
@@ -564,7 +567,7 @@ function drawEngineSplashScreen(t)
|
|
|
564
567
|
// background
|
|
565
568
|
const p3 = percent(t, 1, .8);
|
|
566
569
|
const p4 = percent(t, 0, .5);
|
|
567
|
-
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,
|
|
570
|
+
const g = x.createRadialGradient(w/2,h/2,0,w/2,h/2,hypot(w,h)*.7);
|
|
568
571
|
g.addColorStop(0,hsl(0,0,lerp(0,p3/2,p4),p3).toString());
|
|
569
572
|
g.addColorStop(1,hsl(0,0,0,p3).toString());
|
|
570
573
|
x.save();
|
|
@@ -755,7 +758,8 @@ function debugShowErrors(){}
|
|
|
755
758
|
function debugVideoCaptureIsActive(){ return false; }
|
|
756
759
|
function debugVideoCaptureStart (){}
|
|
757
760
|
function debugVideoCaptureStop (){}
|
|
758
|
-
function debugVideoCaptureUpdate(){}
|
|
761
|
+
function debugVideoCaptureUpdate(){}
|
|
762
|
+
function debugProtectConstant(o){ return o; }
|
|
759
763
|
/**
|
|
760
764
|
* LittleJS Utility Classes and Functions
|
|
761
765
|
* - General purpose math library
|
|
@@ -766,7 +770,7 @@ function debugVideoCaptureUpdate(){}
|
|
|
766
770
|
* @namespace Utilities
|
|
767
771
|
*/
|
|
768
772
|
|
|
769
|
-
/**
|
|
773
|
+
/** The value of PI
|
|
770
774
|
* @type {number}
|
|
771
775
|
* @default Math.PI
|
|
772
776
|
* @memberof Utilities */
|
|
@@ -776,25 +780,80 @@ const PI = Math.PI;
|
|
|
776
780
|
* @param {number} value
|
|
777
781
|
* @return {number}
|
|
778
782
|
* @memberof Utilities */
|
|
779
|
-
|
|
783
|
+
const abs = Math.abs;
|
|
784
|
+
|
|
785
|
+
/** Returns floored value of value passed in
|
|
786
|
+
* @param {number} value
|
|
787
|
+
* @return {number}
|
|
788
|
+
* @memberof Utilities */
|
|
789
|
+
const floor = Math.floor;
|
|
790
|
+
|
|
791
|
+
/** Returns ceiled value of value passed in
|
|
792
|
+
* @param {number} value
|
|
793
|
+
* @return {number}
|
|
794
|
+
* @memberof Utilities */
|
|
795
|
+
const ceil = Math.ceil;
|
|
796
|
+
|
|
797
|
+
/** Returns rounded value passed in
|
|
798
|
+
* @param {number} value
|
|
799
|
+
* @return {number}
|
|
800
|
+
* @memberof Utilities */
|
|
801
|
+
const round = Math.round;
|
|
780
802
|
|
|
781
803
|
/** Returns lowest value passed in
|
|
782
804
|
* @param {...number} values
|
|
783
805
|
* @return {number}
|
|
784
806
|
* @memberof Utilities */
|
|
785
|
-
|
|
807
|
+
const min = Math.min;
|
|
786
808
|
|
|
787
809
|
/** Returns highest value passed in
|
|
788
810
|
* @param {...number} values
|
|
789
811
|
* @return {number}
|
|
790
812
|
* @memberof Utilities */
|
|
791
|
-
|
|
813
|
+
const max = Math.max;
|
|
792
814
|
|
|
793
815
|
/** Returns the sign of value passed in
|
|
794
816
|
* @param {number} value
|
|
795
817
|
* @return {number}
|
|
796
818
|
* @memberof Utilities */
|
|
797
|
-
|
|
819
|
+
const sign = Math.sign;
|
|
820
|
+
|
|
821
|
+
/** Returns hypotenuse of values passed in
|
|
822
|
+
* @param {...number} values
|
|
823
|
+
* @return {number}
|
|
824
|
+
* @memberof Utilities */
|
|
825
|
+
const hypot = Math.hypot;
|
|
826
|
+
|
|
827
|
+
/** Returns log2 of value passed in
|
|
828
|
+
* @param {number} value
|
|
829
|
+
* @return {number}
|
|
830
|
+
* @memberof Utilities */
|
|
831
|
+
const log2 = Math.log2;
|
|
832
|
+
|
|
833
|
+
/** Returns sin of value passed in
|
|
834
|
+
* @param {number} value
|
|
835
|
+
* @return {number}
|
|
836
|
+
* @memberof Utilities */
|
|
837
|
+
const sin = Math.sin;
|
|
838
|
+
|
|
839
|
+
/** Returns cos of value passed in
|
|
840
|
+
* @param {number} value
|
|
841
|
+
* @return {number}
|
|
842
|
+
* @memberof Utilities */
|
|
843
|
+
const cos = Math.cos;
|
|
844
|
+
|
|
845
|
+
/** Returns tan of value passed in
|
|
846
|
+
* @param {number} value
|
|
847
|
+
* @return {number}
|
|
848
|
+
* @memberof Utilities */
|
|
849
|
+
const tan = Math.tan;
|
|
850
|
+
|
|
851
|
+
/** Returns atan2 of values passed in
|
|
852
|
+
* @param {number} y
|
|
853
|
+
* @param {number} x
|
|
854
|
+
* @return {number}
|
|
855
|
+
* @memberof Utilities */
|
|
856
|
+
const atan2 = Math.atan2;
|
|
798
857
|
|
|
799
858
|
/** Returns first parm modulo the second param, but adjusted so negative numbers work as expected
|
|
800
859
|
* @param {number} dividend
|
|
@@ -899,7 +958,7 @@ function isPowerOfTwo(value) { return !(value & (value - 1)); }
|
|
|
899
958
|
* @param {number} value
|
|
900
959
|
* @return {number}
|
|
901
960
|
* @memberof Utilities */
|
|
902
|
-
function nearestPowerOfTwo(value) { return 2**
|
|
961
|
+
function nearestPowerOfTwo(value) { return 2**ceil(log2(value)); }
|
|
903
962
|
|
|
904
963
|
/** Returns true if two axis aligned bounding boxes are overlapping
|
|
905
964
|
* this can be used for simple collision detection between objects
|
|
@@ -967,7 +1026,7 @@ function isIntersecting(start, end, pos, size)
|
|
|
967
1026
|
* @return {number} - Value waving between 0 and amplitude
|
|
968
1027
|
* @memberof Utilities */
|
|
969
1028
|
function wave(frequency=1, amplitude=1, t=time, offset=0)
|
|
970
|
-
{ return amplitude/2 * (1 -
|
|
1029
|
+
{ return amplitude/2 * (1 - cos(offset + t*frequency*2*PI)); }
|
|
971
1030
|
|
|
972
1031
|
/** Formats seconds to mm:ss style for display purposes
|
|
973
1032
|
* @param {number} t - time in seconds
|
|
@@ -1006,6 +1065,103 @@ function isNumber(n) { return typeof n === 'number' && !isNaN(n); }
|
|
|
1006
1065
|
* @memberof Utilities */
|
|
1007
1066
|
function isString(s) { return s !== undefined && s !== null && typeof s.toString() === 'string'; }
|
|
1008
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
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* @callback LineTestFunction - Checks if a position is colliding
|
|
1077
|
+
* @param {Vector2} pos
|
|
1078
|
+
* @memberof Draw
|
|
1079
|
+
*/
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Casts a ray and returns position of the first collision found, or undefined if none are found
|
|
1083
|
+
* @param {Vector2} posStart
|
|
1084
|
+
* @param {Vector2} posEnd
|
|
1085
|
+
* @param {LineTestFunction} testFunction - Check if colliding
|
|
1086
|
+
* @param {Vector2} [normal] - Optional vector to store the normal
|
|
1087
|
+
* @return {Vector2|undefined} - Position of the collision or undefined if none found
|
|
1088
|
+
* @memberof Utilities */
|
|
1089
|
+
function lineTest(posStart, posEnd, testFunction, normal)
|
|
1090
|
+
{
|
|
1091
|
+
ASSERT(isVector2(posStart), 'posStart must be a vec2');
|
|
1092
|
+
ASSERT(isVector2(posEnd), 'posEnd must be a vec2');
|
|
1093
|
+
ASSERT(typeof testFunction === 'function', 'testFunction must be a function');
|
|
1094
|
+
ASSERT(!normal || isVector2(normal), 'normal must be a vec2');
|
|
1095
|
+
|
|
1096
|
+
// get ray direction and length
|
|
1097
|
+
const dx = posEnd.x - posStart.x;
|
|
1098
|
+
const dy = posEnd.y - posStart.y;
|
|
1099
|
+
const totalLength = hypot(dx, dy);
|
|
1100
|
+
if (!totalLength)
|
|
1101
|
+
return;
|
|
1102
|
+
|
|
1103
|
+
// current integer cell we are in
|
|
1104
|
+
const pos = posStart.floor();
|
|
1105
|
+
|
|
1106
|
+
// normalize ray direction
|
|
1107
|
+
const dirX = dx / totalLength;
|
|
1108
|
+
const dirY = dy / totalLength;
|
|
1109
|
+
|
|
1110
|
+
// step direction in grid
|
|
1111
|
+
const stepX = sign(dirX);
|
|
1112
|
+
const stepY = sign(dirY);
|
|
1113
|
+
|
|
1114
|
+
// distance along the ray to cross one full cell in X or Y
|
|
1115
|
+
const tDeltaX = dirX ? abs(1 / dirX) : Infinity;
|
|
1116
|
+
const tDeltaY = dirY ? abs(1 / dirY) : Infinity;
|
|
1117
|
+
|
|
1118
|
+
// distance along the ray from start to the first grid boundary
|
|
1119
|
+
const nextGridX = stepX > 0 ? pos.x + 1 : pos.x;
|
|
1120
|
+
const nextGridY = stepY > 0 ? pos.y + 1 : pos.y;
|
|
1121
|
+
const tMaxX = dirX ? (nextGridX - posStart.x) / dirX : Infinity;
|
|
1122
|
+
const tMaxY = dirY ? (nextGridY - posStart.y) / dirY : Infinity;
|
|
1123
|
+
|
|
1124
|
+
// use line drawing algorithm to test for collisions
|
|
1125
|
+
let t = 0, tX = tMaxX, tY = tMaxY, wasX = tDeltaX < tDeltaY;
|
|
1126
|
+
while (t < totalLength)
|
|
1127
|
+
{
|
|
1128
|
+
if (testFunction(pos))
|
|
1129
|
+
{
|
|
1130
|
+
// set hit point
|
|
1131
|
+
const hitPos = vec2(posStart.x + dirX*t, posStart.y + dirY*t);
|
|
1132
|
+
|
|
1133
|
+
// move inside of tile if on positive edge
|
|
1134
|
+
const e = 1e-9;
|
|
1135
|
+
if (wasX)
|
|
1136
|
+
{
|
|
1137
|
+
if (stepX < 0)
|
|
1138
|
+
hitPos.x -= e;
|
|
1139
|
+
}
|
|
1140
|
+
if (stepY < 0)
|
|
1141
|
+
hitPos.y -= e;
|
|
1142
|
+
|
|
1143
|
+
// set normal
|
|
1144
|
+
if (normal)
|
|
1145
|
+
wasX ? normal.set(-stepX,0) : normal.set(0,-stepY);
|
|
1146
|
+
return hitPos;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// advance to the next grid boundary
|
|
1150
|
+
if (wasX = tX < tY)
|
|
1151
|
+
{
|
|
1152
|
+
pos.x += stepX;
|
|
1153
|
+
t = tX;
|
|
1154
|
+
tX += tDeltaX;
|
|
1155
|
+
}
|
|
1156
|
+
else
|
|
1157
|
+
{
|
|
1158
|
+
pos.y += stepY;
|
|
1159
|
+
t = tY;
|
|
1160
|
+
tY += tDeltaY;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1009
1165
|
///////////////////////////////////////////////////////////////////////////////
|
|
1010
1166
|
|
|
1011
1167
|
/** Random global functions
|
|
@@ -1024,7 +1180,7 @@ function rand(valueA=1, valueB=0) { return valueB + Math.random() * (valueA-valu
|
|
|
1024
1180
|
* @param {number} [valueB]
|
|
1025
1181
|
* @return {number}
|
|
1026
1182
|
* @memberof Random */
|
|
1027
|
-
function randInt(valueA, valueB=0) { return
|
|
1183
|
+
function randInt(valueA, valueB=0) { return floor(rand(valueA,valueB)); }
|
|
1028
1184
|
|
|
1029
1185
|
/** Randomly returns true or false given the chance of true passed in
|
|
1030
1186
|
* @param {number} [chance]
|
|
@@ -1103,7 +1259,7 @@ class RandomGenerator
|
|
|
1103
1259
|
* @param {number} valueA
|
|
1104
1260
|
* @param {number} [valueB]
|
|
1105
1261
|
* @return {number} */
|
|
1106
|
-
int(valueA, valueB=0) { return
|
|
1262
|
+
int(valueA, valueB=0) { return floor(this.float(valueA, valueB)); }
|
|
1107
1263
|
|
|
1108
1264
|
/** Randomly returns true or false given the chance of true passed in
|
|
1109
1265
|
* @param {number} [chance]
|
|
@@ -1130,6 +1286,39 @@ class RandomGenerator
|
|
|
1130
1286
|
* @return {Vector2} */
|
|
1131
1287
|
vec2(valueA=1, valueB=0)
|
|
1132
1288
|
{ return vec2(this.float(valueA, valueB), this.float(valueA, valueB)); }
|
|
1289
|
+
|
|
1290
|
+
/** Returns a random color between the two passed in colors, combine components if linear
|
|
1291
|
+
* @param {Color} [colorA=(1,1,1,1)]
|
|
1292
|
+
* @param {Color} [colorB=(0,0,0,1)]
|
|
1293
|
+
* @param {boolean} [linear]
|
|
1294
|
+
* @return {Color} */
|
|
1295
|
+
randColor(colorA=new Color, colorB=new Color(0,0,0,1), linear=false)
|
|
1296
|
+
{
|
|
1297
|
+
return linear ? colorA.lerp(colorB, this.float()) :
|
|
1298
|
+
new Color(
|
|
1299
|
+
this.float(colorA.r,colorB.r),
|
|
1300
|
+
this.float(colorA.g,colorB.g),
|
|
1301
|
+
this.float(colorA.b,colorB.b),
|
|
1302
|
+
this.float(colorA.a,colorB.a));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/** Returns a new color that has each component randomly adjusted
|
|
1306
|
+
* @param {Color} color
|
|
1307
|
+
* @param {number} [amount]
|
|
1308
|
+
* @param {number} [alphaAmount]
|
|
1309
|
+
* @return {Color} */
|
|
1310
|
+
mutateColor(color, amount=.05, alphaAmount=0)
|
|
1311
|
+
{
|
|
1312
|
+
ASSERT_NUMBER_VALID(amount);
|
|
1313
|
+
ASSERT_NUMBER_VALID(alphaAmount);
|
|
1314
|
+
return new Color
|
|
1315
|
+
(
|
|
1316
|
+
color.r + this.float(amount, -amount),
|
|
1317
|
+
color.g + this.float(amount, -amount),
|
|
1318
|
+
color.b + this.float(amount, -amount),
|
|
1319
|
+
color.a + this.float(alphaAmount, -alphaAmount)
|
|
1320
|
+
).clamp();
|
|
1321
|
+
}
|
|
1133
1322
|
}
|
|
1134
1323
|
|
|
1135
1324
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -1281,7 +1470,7 @@ class Vector2
|
|
|
1281
1470
|
|
|
1282
1471
|
/** Returns the clockwise angle of this vector, up is angle 0
|
|
1283
1472
|
* @return {number} */
|
|
1284
|
-
angle() { return
|
|
1473
|
+
angle() { return atan2(this.x, this.y); }
|
|
1285
1474
|
|
|
1286
1475
|
/** Sets this vector with clockwise angle and length passed in
|
|
1287
1476
|
* @param {number} [angle]
|
|
@@ -1291,8 +1480,8 @@ class Vector2
|
|
|
1291
1480
|
{
|
|
1292
1481
|
ASSERT_NUMBER_VALID(angle);
|
|
1293
1482
|
ASSERT_NUMBER_VALID(length);
|
|
1294
|
-
this.x = length*
|
|
1295
|
-
this.y = length*
|
|
1483
|
+
this.x = length*sin(angle);
|
|
1484
|
+
this.y = length*cos(angle);
|
|
1296
1485
|
return this;
|
|
1297
1486
|
}
|
|
1298
1487
|
|
|
@@ -1302,7 +1491,7 @@ class Vector2
|
|
|
1302
1491
|
rotate(angle)
|
|
1303
1492
|
{
|
|
1304
1493
|
ASSERT_NUMBER_VALID(angle);
|
|
1305
|
-
const c =
|
|
1494
|
+
const c = cos(-angle), s = sin(-angle);
|
|
1306
1495
|
return new Vector2(this.x*c - this.y*s, this.x*s + this.y*c);
|
|
1307
1496
|
}
|
|
1308
1497
|
|
|
@@ -1334,7 +1523,7 @@ class Vector2
|
|
|
1334
1523
|
|
|
1335
1524
|
/** Returns a copy of this vector with each axis floored
|
|
1336
1525
|
* @return {Vector2} */
|
|
1337
|
-
floor() { return new Vector2(
|
|
1526
|
+
floor() { return new Vector2(floor(this.x), floor(this.y)); }
|
|
1338
1527
|
|
|
1339
1528
|
/** Returns new vec2 with modded values
|
|
1340
1529
|
* @param {number} [divisor]
|
|
@@ -1582,7 +1771,6 @@ class Color
|
|
|
1582
1771
|
* @return {string} */
|
|
1583
1772
|
toString(useAlpha = true)
|
|
1584
1773
|
{
|
|
1585
|
-
ASSERT(typeof useAlpha === 'boolean', 'Use alpha boolean is invalid.', useAlpha);
|
|
1586
1774
|
if (debug && !this.isValid())
|
|
1587
1775
|
return `#000`;
|
|
1588
1776
|
const toHex = (c)=> ((c=clamp(c)*255|0)<16 ? '0' : '') + c.toString(16);
|
|
@@ -1642,67 +1830,67 @@ class Color
|
|
|
1642
1830
|
/** Color - White #ffffff
|
|
1643
1831
|
* @type {Color}
|
|
1644
1832
|
* @memberof Utilities */
|
|
1645
|
-
const WHITE =
|
|
1833
|
+
const WHITE = debugProtectConstant(rgb());
|
|
1646
1834
|
|
|
1647
1835
|
/** Color - Clear White #757474ff with 0 alpha
|
|
1648
1836
|
* @type {Color}
|
|
1649
1837
|
* @memberof Utilities */
|
|
1650
|
-
const CLEAR_WHITE =
|
|
1838
|
+
const CLEAR_WHITE = debugProtectConstant(rgb(1,1,1,0));
|
|
1651
1839
|
|
|
1652
1840
|
/** Color - Black #000000
|
|
1653
1841
|
* @type {Color}
|
|
1654
1842
|
* @memberof Utilities */
|
|
1655
|
-
const BLACK =
|
|
1843
|
+
const BLACK = debugProtectConstant(rgb(0,0,0));
|
|
1656
1844
|
|
|
1657
1845
|
/** Color - Clear Black #000000 with 0 alpha
|
|
1658
1846
|
* @type {Color}
|
|
1659
1847
|
* @memberof Utilities */
|
|
1660
|
-
const CLEAR_BLACK =
|
|
1848
|
+
const CLEAR_BLACK = debugProtectConstant(rgb(0,0,0,0));
|
|
1661
1849
|
|
|
1662
1850
|
/** Color - Gray #808080
|
|
1663
1851
|
* @type {Color}
|
|
1664
1852
|
* @memberof Utilities */
|
|
1665
|
-
const GRAY =
|
|
1853
|
+
const GRAY = debugProtectConstant(rgb(.5,.5,.5));
|
|
1666
1854
|
|
|
1667
1855
|
/** Color - Red #ff0000
|
|
1668
1856
|
* @type {Color}
|
|
1669
1857
|
* @memberof Utilities */
|
|
1670
|
-
const RED =
|
|
1858
|
+
const RED = debugProtectConstant(rgb(1,0,0));
|
|
1671
1859
|
|
|
1672
1860
|
/** Color - Orange #ff8000
|
|
1673
1861
|
* @type {Color}
|
|
1674
1862
|
* @memberof Utilities */
|
|
1675
|
-
const ORANGE =
|
|
1863
|
+
const ORANGE = debugProtectConstant(rgb(1,.5,0));
|
|
1676
1864
|
|
|
1677
1865
|
/** Color - Yellow #ffff00
|
|
1678
1866
|
* @type {Color}
|
|
1679
1867
|
* @memberof Utilities */
|
|
1680
|
-
const YELLOW =
|
|
1868
|
+
const YELLOW = debugProtectConstant(rgb(1,1,0));
|
|
1681
1869
|
|
|
1682
1870
|
/** Color - Green #00ff00
|
|
1683
1871
|
* @type {Color}
|
|
1684
1872
|
* @memberof Utilities */
|
|
1685
|
-
const GREEN =
|
|
1873
|
+
const GREEN = debugProtectConstant(rgb(0,1,0));
|
|
1686
1874
|
|
|
1687
1875
|
/** Color - Cyan #00ffff
|
|
1688
1876
|
* @type {Color}
|
|
1689
1877
|
* @memberof Utilities */
|
|
1690
|
-
const CYAN =
|
|
1878
|
+
const CYAN = debugProtectConstant(rgb(0,1,1));
|
|
1691
1879
|
|
|
1692
1880
|
/** Color - Blue #0000ff
|
|
1693
1881
|
* @type {Color}
|
|
1694
1882
|
* @memberof Utilities */
|
|
1695
|
-
const BLUE =
|
|
1883
|
+
const BLUE = debugProtectConstant(rgb(0,0,1));
|
|
1696
1884
|
|
|
1697
1885
|
/** Color - Purple #8000ff
|
|
1698
1886
|
* @type {Color}
|
|
1699
1887
|
* @memberof Utilities */
|
|
1700
|
-
const PURPLE =
|
|
1888
|
+
const PURPLE = debugProtectConstant(rgb(.5,0,1));
|
|
1701
1889
|
|
|
1702
1890
|
/** Color - Magenta #ff00ff
|
|
1703
1891
|
* @type {Color}
|
|
1704
1892
|
* @memberof Utilities */
|
|
1705
|
-
const MAGENTA =
|
|
1893
|
+
const MAGENTA = debugProtectConstant(rgb(1,0,1));
|
|
1706
1894
|
|
|
1707
1895
|
///////////////////////////////////////////////////////////////////////////////
|
|
1708
1896
|
|
|
@@ -1765,41 +1953,11 @@ class Timer
|
|
|
1765
1953
|
|
|
1766
1954
|
/** Returns this timer expressed as a string
|
|
1767
1955
|
* @return {string} */
|
|
1768
|
-
toString() { return this.isSet() ?
|
|
1956
|
+
toString() { return this.isSet() ? abs(this.get()) + ' seconds ' + (this.get()<0 ? 'before' : 'after' ) : 'unset'; }
|
|
1769
1957
|
|
|
1770
1958
|
/** Get how long since elapsed, returns 0 if not set (returns negative if currently active)
|
|
1771
1959
|
* @return {number} */
|
|
1772
1960
|
valueOf() { return this.get(); }
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
1776
|
-
// Helper functions used by the engine
|
|
1777
|
-
|
|
1778
|
-
// make color constants immutable with debug assertions
|
|
1779
|
-
function protectEngineConstant(obj)
|
|
1780
|
-
{
|
|
1781
|
-
if (debug)
|
|
1782
|
-
{
|
|
1783
|
-
// get properties and store original values
|
|
1784
|
-
const props = Object.keys(obj), values = {};
|
|
1785
|
-
props.forEach(prop => values[prop] = obj[prop]);
|
|
1786
|
-
|
|
1787
|
-
// replace with getters/setters that assert
|
|
1788
|
-
props.forEach(prop =>
|
|
1789
|
-
{
|
|
1790
|
-
Object.defineProperty(obj, prop, {
|
|
1791
|
-
get: () => values[prop],
|
|
1792
|
-
set: (value) =>
|
|
1793
|
-
{
|
|
1794
|
-
ASSERT(false, `Cannot modify engine constant. Attempted to set constant (${obj}) property '${prop}' to '${value}'.`);
|
|
1795
|
-
},
|
|
1796
|
-
enumerable: true
|
|
1797
|
-
});
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// freeze the object to prevent adding new properties
|
|
1802
|
-
return Object.freeze(obj);
|
|
1803
1961
|
}
|
|
1804
1962
|
/**
|
|
1805
1963
|
* LittleJS Engine Settings
|
|
@@ -2017,6 +2175,14 @@ let touchInputEnable = true;
|
|
|
2017
2175
|
* @memberof Settings */
|
|
2018
2176
|
let touchGamepadEnable = false;
|
|
2019
2177
|
|
|
2178
|
+
/** True if touch gamepad should have start button in the center
|
|
2179
|
+
* - When the game is paused, any touch will press the button
|
|
2180
|
+
* - This can function as a way to pause/unpause the game
|
|
2181
|
+
* @type {boolean}
|
|
2182
|
+
* @default
|
|
2183
|
+
* @memberof Settings */
|
|
2184
|
+
let touchGamepadCenterButton = true;
|
|
2185
|
+
|
|
2020
2186
|
/** True if touch gamepad should be analog stick or false to use if 8 way dpad
|
|
2021
2187
|
* @type {boolean}
|
|
2022
2188
|
* @default
|
|
@@ -2192,9 +2358,14 @@ function setHeadlessMode(headless) { headlessMode = headless; }
|
|
|
2192
2358
|
* @memberof Settings */
|
|
2193
2359
|
function setGLEnable(enable)
|
|
2194
2360
|
{
|
|
2361
|
+
if (enable && !glCanBeEnabled)
|
|
2362
|
+
{
|
|
2363
|
+
console.warn('Can not enable WebGL if it was disabled on start.');
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2195
2366
|
glEnable = enable;
|
|
2196
2367
|
if (glCanvas) // hide glCanvas if WebGL is disabled
|
|
2197
|
-
glCanvas.style.
|
|
2368
|
+
glCanvas.style.display = enable ? '' : 'none';
|
|
2198
2369
|
}
|
|
2199
2370
|
|
|
2200
2371
|
/** Set how many sided polygons to use when drawing circles and ellipses with WebGL
|
|
@@ -2282,6 +2453,12 @@ function setTouchInputEnable(enable) { touchInputEnable = enable; }
|
|
|
2282
2453
|
* @memberof Settings */
|
|
2283
2454
|
function setTouchGamepadEnable(enable) { touchGamepadEnable = enable; }
|
|
2284
2455
|
|
|
2456
|
+
/** True if touch gamepad should have start button in the center
|
|
2457
|
+
* - This can function as a way to pause/unpause the game
|
|
2458
|
+
* @param {boolean} enable
|
|
2459
|
+
* @memberof Settings */
|
|
2460
|
+
function setTouchGamepadCenterButton(enable) { touchGamepadCenterButton = enable; }
|
|
2461
|
+
|
|
2285
2462
|
/** Set if touch gamepad should be analog stick or 8 way dpad
|
|
2286
2463
|
* @param {boolean} analog
|
|
2287
2464
|
* @memberof Settings */
|
|
@@ -2491,11 +2668,10 @@ class EngineObject
|
|
|
2491
2668
|
}
|
|
2492
2669
|
|
|
2493
2670
|
/** Update the object physics, called automatically by engine once each frame */
|
|
2494
|
-
|
|
2671
|
+
updatePhysics()
|
|
2495
2672
|
{
|
|
2496
2673
|
// child objects do not have physics
|
|
2497
|
-
|
|
2498
|
-
return;
|
|
2674
|
+
ASSERT(!this.parent);
|
|
2499
2675
|
|
|
2500
2676
|
if (this.clampSpeed)
|
|
2501
2677
|
{
|
|
@@ -2545,7 +2721,7 @@ class EngineObject
|
|
|
2545
2721
|
continue;
|
|
2546
2722
|
|
|
2547
2723
|
// check collision
|
|
2548
|
-
if (!
|
|
2724
|
+
if (!this.isOverlappingObject(o))
|
|
2549
2725
|
continue;
|
|
2550
2726
|
|
|
2551
2727
|
// notify objects of collision and check if should be resolved
|
|
@@ -2643,6 +2819,27 @@ class EngineObject
|
|
|
2643
2819
|
// test which side we bounced off (or both if a corner)
|
|
2644
2820
|
const blockedLayerY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
|
|
2645
2821
|
const blockedLayerX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
|
|
2822
|
+
|
|
2823
|
+
if (blockedLayerX)
|
|
2824
|
+
{
|
|
2825
|
+
// try to move up a tiny bit
|
|
2826
|
+
const epsilon = 1e-3;
|
|
2827
|
+
const maxMoveUp = .1;
|
|
2828
|
+
const y = floor(oldPos.y-this.size.y/2+1) +
|
|
2829
|
+
this.size.y/2 + epsilon;
|
|
2830
|
+
const delta = y - this.pos.y;
|
|
2831
|
+
if (delta < maxMoveUp)
|
|
2832
|
+
if (!tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
2833
|
+
{
|
|
2834
|
+
this.pos.y = y;
|
|
2835
|
+
debugPhysics && debugRect(this.pos, this.size, '#ff0');
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// move to previous position and bounce
|
|
2840
|
+
this.pos.x = oldPos.x;
|
|
2841
|
+
this.velocity.x *= -this.restitution;
|
|
2842
|
+
}
|
|
2646
2843
|
if (blockedLayerY || !blockedLayerX)
|
|
2647
2844
|
{
|
|
2648
2845
|
// bounce velocity
|
|
@@ -2666,18 +2863,15 @@ class EngineObject
|
|
|
2666
2863
|
this.groundObject = undefined;
|
|
2667
2864
|
}
|
|
2668
2865
|
}
|
|
2669
|
-
if (blockedLayerX)
|
|
2670
|
-
{
|
|
2671
|
-
// move to previous position and bounce
|
|
2672
|
-
this.pos.x = oldPos.x;
|
|
2673
|
-
this.velocity.x *= -this.restitution;
|
|
2674
|
-
}
|
|
2675
2866
|
debugPhysics && debugRect(this.pos, this.size, '#f00');
|
|
2676
2867
|
}
|
|
2677
2868
|
}
|
|
2678
2869
|
}
|
|
2679
2870
|
}
|
|
2680
2871
|
|
|
2872
|
+
/** Update the object, called automatically by engine once each frame. Does nothing by default. */
|
|
2873
|
+
update() {}
|
|
2874
|
+
|
|
2681
2875
|
/** Render the object, draws a tile by default, automatically called each frame, sorted by renderOrder */
|
|
2682
2876
|
render()
|
|
2683
2877
|
{
|
|
@@ -2729,10 +2923,24 @@ class EngineObject
|
|
|
2729
2923
|
*/
|
|
2730
2924
|
collideWithObject(object) { return true; }
|
|
2731
2925
|
|
|
2926
|
+
/** Get this object's up vector
|
|
2927
|
+
* @param {number} [scale] - length of the vector
|
|
2928
|
+
* @return {Vector2} */
|
|
2929
|
+
getUp(scale=1) { return vec2().setAngle(this.angle, scale); }
|
|
2930
|
+
|
|
2931
|
+
/** Get this object's right vector
|
|
2932
|
+
* @param {number} [scale] - length of the vector
|
|
2933
|
+
* @return {Vector2} */
|
|
2934
|
+
getRight(scale=1) { return vec2().setAngle(this.angle+PI/2, scale); }
|
|
2935
|
+
|
|
2732
2936
|
/** How long since the object was created
|
|
2733
2937
|
* @return {number} */
|
|
2734
2938
|
getAliveTime() { return time - this.spawnTime; }
|
|
2735
2939
|
|
|
2940
|
+
/** Get the speed of this object
|
|
2941
|
+
* @return {number} */
|
|
2942
|
+
getSpeed() { return this.velocity.length(); }
|
|
2943
|
+
|
|
2736
2944
|
/** Apply acceleration to this object (adjust velocity, not affected by mass)
|
|
2737
2945
|
* @param {Vector2} acceleration */
|
|
2738
2946
|
applyAcceleration(acceleration)
|
|
@@ -2777,6 +2985,20 @@ class EngineObject
|
|
|
2777
2985
|
child.parent = 0;
|
|
2778
2986
|
}
|
|
2779
2987
|
|
|
2988
|
+
/** Check if overlapping another engine object
|
|
2989
|
+
* Collisions are resoloved to prevent overlaps
|
|
2990
|
+
* @param {EngineObject} object
|
|
2991
|
+
* @return {boolean} */
|
|
2992
|
+
isOverlappingObject(object)
|
|
2993
|
+
{ return this.isOverlapping(object.pos, object.size); }
|
|
2994
|
+
|
|
2995
|
+
/** Check if overlapping a point or aligned bounding box
|
|
2996
|
+
* @param {Vector2} pos - Center of box
|
|
2997
|
+
* @param {Vector2} [size=(0,0)] - Size of box, uses a point if undefined
|
|
2998
|
+
* @return {boolean} */
|
|
2999
|
+
isOverlapping(pos, size=vec2())
|
|
3000
|
+
{ return isOverlapping(this.pos, this.size, pos, size); }
|
|
3001
|
+
|
|
2780
3002
|
/** Set how this object collides
|
|
2781
3003
|
* @param {boolean} [collideSolidObjects] - Does it collide with solid objects?
|
|
2782
3004
|
* @param {boolean} [isSolid] - Does it collide with and block other objects? (expensive in large numbers)
|
|
@@ -3080,6 +3302,7 @@ function drawTile(pos, size=new Vector2(1), tileInfo, color=WHITE,
|
|
|
3080
3302
|
const bleedScale = tileInfo ? tileInfo.bleedScale : 0;
|
|
3081
3303
|
if (useWebGL)
|
|
3082
3304
|
{
|
|
3305
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3083
3306
|
if (screenSpace)
|
|
3084
3307
|
{
|
|
3085
3308
|
// convert to world space
|
|
@@ -3175,6 +3398,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3175
3398
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3176
3399
|
if (useWebGL)
|
|
3177
3400
|
{
|
|
3401
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3178
3402
|
if (screenSpace)
|
|
3179
3403
|
{
|
|
3180
3404
|
// convert to world space
|
|
@@ -3186,7 +3410,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3186
3410
|
const halfSizeX = size.x/2, halfSizeY = size.y/2;
|
|
3187
3411
|
const colorTopInt = colorTop.rgbaInt();
|
|
3188
3412
|
const colorBottomInt = colorBottom.rgbaInt();
|
|
3189
|
-
const c =
|
|
3413
|
+
const c = cos(-angle), s = sin(-angle);
|
|
3190
3414
|
for (let i=4; i--;)
|
|
3191
3415
|
{
|
|
3192
3416
|
const x = i & 1 ? halfSizeX : -halfSizeX;
|
|
@@ -3229,7 +3453,7 @@ function drawRectGradient(pos, size, colorTop=WHITE, colorBottom=BLACK, angle=0,
|
|
|
3229
3453
|
* @memberof Draw */
|
|
3230
3454
|
function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace, context)
|
|
3231
3455
|
{
|
|
3232
|
-
ASSERT(
|
|
3456
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
3233
3457
|
ASSERT(isNumber(width), 'width must be a number');
|
|
3234
3458
|
ASSERT(isColor(color), 'color is invalid');
|
|
3235
3459
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -3237,6 +3461,7 @@ function drawLineList(points, width=.1, color, wrap=false, pos=vec2(), angle=0,
|
|
|
3237
3461
|
ASSERT(!context || !useWebGL, 'context only supported in canvas 2D mode');
|
|
3238
3462
|
if (useWebGL)
|
|
3239
3463
|
{
|
|
3464
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3240
3465
|
let scale = 1;
|
|
3241
3466
|
if (screenSpace)
|
|
3242
3467
|
{
|
|
@@ -3286,6 +3511,8 @@ function drawLine(posA, posB, width=.1, color, pos=vec2(), angle=0, useWebGL, sc
|
|
|
3286
3511
|
const halfDelta = vec2((posB.x - posA.x)/2, (posB.y - posA.y)/2);
|
|
3287
3512
|
const size = vec2(width, halfDelta.length()*2);
|
|
3288
3513
|
pos = pos.add(posA.add(halfDelta));
|
|
3514
|
+
if (screenSpace)
|
|
3515
|
+
halfDelta.y *= -1; // flip angle Y if screen space
|
|
3289
3516
|
angle += halfDelta.angle();
|
|
3290
3517
|
drawRect(pos, size, color, angle, useWebGL, screenSpace, context);
|
|
3291
3518
|
}
|
|
@@ -3313,7 +3540,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
3313
3540
|
for (let i=sides; i--;)
|
|
3314
3541
|
{
|
|
3315
3542
|
const a = (i/sides)*PI*2;
|
|
3316
|
-
points.push(vec2(
|
|
3543
|
+
points.push(vec2(sin(a)*sizeX, cos(a)*sizeY));
|
|
3317
3544
|
}
|
|
3318
3545
|
drawPoly(points, color, lineWidth, lineColor, pos, angle, useWebGL, screenSpace, context);
|
|
3319
3546
|
}
|
|
@@ -3332,7 +3559,7 @@ function drawRegularPoly(pos, size=vec2(1), sides=3, color=WHITE, lineWidth=0, l
|
|
|
3332
3559
|
function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(), angle=0, useWebGL=glEnable, screenSpace=false, context=undefined)
|
|
3333
3560
|
{
|
|
3334
3561
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
3335
|
-
ASSERT(
|
|
3562
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
3336
3563
|
ASSERT(isColor(color) && isColor(lineColor), 'color is invalid');
|
|
3337
3564
|
ASSERT(isNumber(lineWidth), 'lineWidth must be a number');
|
|
3338
3565
|
ASSERT(isNumber(angle), 'angle must be a number');
|
|
@@ -3340,6 +3567,7 @@ function drawPoly(points, color=WHITE, lineWidth=0, lineColor=BLACK, pos=vec2(),
|
|
|
3340
3567
|
|
|
3341
3568
|
if (useWebGL)
|
|
3342
3569
|
{
|
|
3570
|
+
ASSERT(!!glContext, 'WebGL is not enabled!');
|
|
3343
3571
|
let scale = 1;
|
|
3344
3572
|
if (screenSpace)
|
|
3345
3573
|
{
|
|
@@ -3483,11 +3711,12 @@ function drawCanvas2D(pos, size, angle=0, mirror=false, drawFunction, screenSpac
|
|
|
3483
3711
|
* @param {string} [font=fontDefault]
|
|
3484
3712
|
* @param {string} [fontStyle]
|
|
3485
3713
|
* @param {number} [maxWidth]
|
|
3714
|
+
* @param {number} [angle]
|
|
3486
3715
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=drawContext]
|
|
3487
3716
|
* @memberof Draw */
|
|
3488
|
-
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, context=drawContext)
|
|
3717
|
+
function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0, context=drawContext)
|
|
3489
3718
|
{
|
|
3490
|
-
drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, context);
|
|
3719
|
+
drawTextScreen(text, worldToScreen(pos), size*cameraScale, color, lineWidth*cameraScale, lineColor, textAlign, font, fontStyle, maxWidth, angle, context);
|
|
3491
3720
|
}
|
|
3492
3721
|
|
|
3493
3722
|
/** Draw text on overlay canvas in world space
|
|
@@ -3502,10 +3731,11 @@ function drawText(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, f
|
|
|
3502
3731
|
* @param {string} [font=fontDefault]
|
|
3503
3732
|
* @param {string} [fontStyle]
|
|
3504
3733
|
* @param {number} [maxWidth]
|
|
3734
|
+
* @param {number} [angle]
|
|
3505
3735
|
* @memberof Draw */
|
|
3506
|
-
function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth)
|
|
3736
|
+
function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textAlign, font, fontStyle, maxWidth, angle=0)
|
|
3507
3737
|
{
|
|
3508
|
-
drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, overlayContext);
|
|
3738
|
+
drawText(text, pos, size, color, lineWidth, lineColor, textAlign, font, fontStyle, maxWidth, angle, overlayContext);
|
|
3509
3739
|
}
|
|
3510
3740
|
|
|
3511
3741
|
/** Draw text on overlay canvas in screen space
|
|
@@ -3520,9 +3750,10 @@ function drawTextOverlay(text, pos, size=1, color, lineWidth=0, lineColor, textA
|
|
|
3520
3750
|
* @param {string} [font=fontDefault]
|
|
3521
3751
|
* @param {string} [fontStyle]
|
|
3522
3752
|
* @param {number} [maxWidth]
|
|
3753
|
+
* @param {number} [angle]
|
|
3523
3754
|
* @param {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} [context=overlayContext]
|
|
3524
3755
|
* @memberof Draw */
|
|
3525
|
-
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, context=overlayContext)
|
|
3756
|
+
function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=BLACK, textAlign='center', font=fontDefault, fontStyle='', maxWidth, angle=0, context=overlayContext)
|
|
3526
3757
|
{
|
|
3527
3758
|
ASSERT(isString(text), 'text must be a string');
|
|
3528
3759
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
@@ -3533,6 +3764,7 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
3533
3764
|
ASSERT(['left','center','right'].includes(textAlign), 'align must be left, center, or right');
|
|
3534
3765
|
ASSERT(isString(font), 'font must be a string');
|
|
3535
3766
|
ASSERT(isString(fontStyle), 'fontStyle must be a string');
|
|
3767
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
3536
3768
|
|
|
3537
3769
|
context.fillStyle = color.toString();
|
|
3538
3770
|
context.strokeStyle = lineColor.toString();
|
|
@@ -3542,14 +3774,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
3542
3774
|
context.textBaseline = 'middle';
|
|
3543
3775
|
|
|
3544
3776
|
const lines = (text+'').split('\n');
|
|
3545
|
-
|
|
3546
|
-
|
|
3777
|
+
const posY = pos.y - (lines.length-1) * size/2; // center vertically
|
|
3778
|
+
context.save();
|
|
3779
|
+
context.translate(pos.x, posY);
|
|
3780
|
+
context.rotate(-angle);
|
|
3781
|
+
let yOffset = 0;
|
|
3547
3782
|
lines.forEach(line=>
|
|
3548
3783
|
{
|
|
3549
|
-
lineWidth && context.strokeText(line,
|
|
3550
|
-
context.fillText(line,
|
|
3551
|
-
|
|
3784
|
+
lineWidth && context.strokeText(line, 0, yOffset, maxWidth);
|
|
3785
|
+
context.fillText(line, 0, yOffset, maxWidth);
|
|
3786
|
+
yOffset += size;
|
|
3552
3787
|
});
|
|
3788
|
+
context.restore();
|
|
3553
3789
|
}
|
|
3554
3790
|
|
|
3555
3791
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -3561,18 +3797,18 @@ function drawTextScreen(text, pos, size=1, color=WHITE, lineWidth=0, lineColor=B
|
|
|
3561
3797
|
* @memberof Draw */
|
|
3562
3798
|
function screenToWorld(screenPos)
|
|
3563
3799
|
{
|
|
3564
|
-
let
|
|
3565
|
-
let
|
|
3800
|
+
let x = (screenPos.x - mainCanvasSize.x/2 + .5) / cameraScale;
|
|
3801
|
+
let y = (screenPos.y - mainCanvasSize.y/2 + .5) / -cameraScale;
|
|
3566
3802
|
if (cameraAngle)
|
|
3567
3803
|
{
|
|
3568
3804
|
// apply camera rotation
|
|
3569
|
-
const
|
|
3570
|
-
const rotatedX =
|
|
3571
|
-
const rotatedY =
|
|
3572
|
-
|
|
3573
|
-
|
|
3805
|
+
const c = cos(-cameraAngle), s = sin(-cameraAngle);
|
|
3806
|
+
const rotatedX = x * c - y * s;
|
|
3807
|
+
const rotatedY = x * s + y * c;
|
|
3808
|
+
x = rotatedX;
|
|
3809
|
+
y = rotatedY;
|
|
3574
3810
|
}
|
|
3575
|
-
return new Vector2(
|
|
3811
|
+
return new Vector2(x + cameraPos.x, y + cameraPos.y);
|
|
3576
3812
|
}
|
|
3577
3813
|
|
|
3578
3814
|
/** Convert from world to screen space coordinates
|
|
@@ -3581,24 +3817,64 @@ function screenToWorld(screenPos)
|
|
|
3581
3817
|
* @memberof Draw */
|
|
3582
3818
|
function worldToScreen(worldPos)
|
|
3583
3819
|
{
|
|
3584
|
-
let
|
|
3585
|
-
let
|
|
3820
|
+
let x = worldPos.x - cameraPos.x;
|
|
3821
|
+
let y = worldPos.y - cameraPos.y;
|
|
3586
3822
|
if (cameraAngle)
|
|
3587
3823
|
{
|
|
3588
3824
|
// apply inverse camera rotation
|
|
3589
|
-
const
|
|
3590
|
-
const rotatedX =
|
|
3591
|
-
const rotatedY =
|
|
3592
|
-
|
|
3593
|
-
|
|
3825
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
3826
|
+
const rotatedX = x * c - y * s;
|
|
3827
|
+
const rotatedY = x * s + y * c;
|
|
3828
|
+
x = rotatedX;
|
|
3829
|
+
y = rotatedY;
|
|
3594
3830
|
}
|
|
3595
3831
|
return new Vector2
|
|
3596
3832
|
(
|
|
3597
|
-
|
|
3598
|
-
|
|
3833
|
+
x * cameraScale + mainCanvasSize.x/2 - .5,
|
|
3834
|
+
y * -cameraScale + mainCanvasSize.y/2 - .5
|
|
3599
3835
|
);
|
|
3600
3836
|
}
|
|
3601
3837
|
|
|
3838
|
+
/** Convert from screen to world space coordinates for a directional vector (no translation)
|
|
3839
|
+
* @param {Vector2} screenDelta
|
|
3840
|
+
* @return {Vector2}
|
|
3841
|
+
* @memberof Draw */
|
|
3842
|
+
function screenToWorldDelta(screenDelta)
|
|
3843
|
+
{
|
|
3844
|
+
let x = screenDelta.x / cameraScale;
|
|
3845
|
+
let y = screenDelta.y / -cameraScale;
|
|
3846
|
+
if (cameraAngle)
|
|
3847
|
+
{
|
|
3848
|
+
// apply camera rotation
|
|
3849
|
+
const c = cos(-cameraAngle), s = sin(-cameraAngle);
|
|
3850
|
+
const rotatedX = x * c - y * s;
|
|
3851
|
+
const rotatedY = x * s + y * c;
|
|
3852
|
+
x = rotatedX;
|
|
3853
|
+
y = rotatedY;
|
|
3854
|
+
}
|
|
3855
|
+
return new Vector2(x, y);
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
/** Convert from screen to world space coordinates for a directional vector (no translation)
|
|
3859
|
+
* @param {Vector2} worldDelta
|
|
3860
|
+
* @return {Vector2}
|
|
3861
|
+
* @memberof Draw */
|
|
3862
|
+
function worldToScreenDelta(worldDelta)
|
|
3863
|
+
{
|
|
3864
|
+
let x = worldDelta.x;
|
|
3865
|
+
let y = worldDelta.y;
|
|
3866
|
+
if (cameraAngle)
|
|
3867
|
+
{
|
|
3868
|
+
// apply inverse camera rotation
|
|
3869
|
+
const c = cos(cameraAngle), s = sin(cameraAngle);
|
|
3870
|
+
const rotatedX = x * c - y * s;
|
|
3871
|
+
const rotatedY = x * s + y * c;
|
|
3872
|
+
x = rotatedX;
|
|
3873
|
+
y = rotatedY;
|
|
3874
|
+
}
|
|
3875
|
+
return new Vector2(x * cameraScale, y * -cameraScale);
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3602
3878
|
/** Get the camera's visible area in world space
|
|
3603
3879
|
* @return {Vector2}
|
|
3604
3880
|
* @memberof Draw */
|
|
@@ -3651,6 +3927,8 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3651
3927
|
function isBlack(c) { return c.r <= 0 && c.g <= 0 && c.b <= 0 && c.a <= 0; }
|
|
3652
3928
|
const sx2 = bleedScale;
|
|
3653
3929
|
const sy2 = bleedScale;
|
|
3930
|
+
sWidth = max(1,sWidth|0);
|
|
3931
|
+
sHeight = max(1,sHeight|0);
|
|
3654
3932
|
const sWidth2 = sWidth - 2*bleedScale;
|
|
3655
3933
|
const sHeight2 = sHeight - 2*bleedScale;
|
|
3656
3934
|
if (!canvasColorTiles || (additiveColor ? isWhite(color.add(additiveColor)) && additiveColor.a <= 0 : isWhite(color)))
|
|
@@ -3665,7 +3943,7 @@ function drawImageColor(context, image, sx, sy, sWidth, sHeight, dx, dy, dWidth,
|
|
|
3665
3943
|
// copy to offscreen canvas
|
|
3666
3944
|
workCanvas.width = sWidth;
|
|
3667
3945
|
workCanvas.height = sHeight;
|
|
3668
|
-
workContext.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
3946
|
+
workContext.drawImage(image, sx|0, sy|0, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
3669
3947
|
|
|
3670
3948
|
// tint image using offscreen work context
|
|
3671
3949
|
const imageData = workContext.getImageData(0, 0, sWidth, sHeight);
|
|
@@ -3939,6 +4217,11 @@ let mouseDeltaScreen = vec2();
|
|
|
3939
4217
|
* @memberof Input */
|
|
3940
4218
|
let mouseWheel = 0;
|
|
3941
4219
|
|
|
4220
|
+
/** True if mouse was inside the document window, set to false when mouse leaves
|
|
4221
|
+
* @type {boolean}
|
|
4222
|
+
* @memberof Input */
|
|
4223
|
+
let mouseInWindow = true;
|
|
4224
|
+
|
|
3942
4225
|
/** Returns true if user is using gamepad (has more recently pressed a gamepad button)
|
|
3943
4226
|
* @type {boolean}
|
|
3944
4227
|
* @memberof Input */
|
|
@@ -4002,9 +4285,9 @@ function inputUpdate()
|
|
|
4002
4285
|
if(!(touchInputEnable && isTouchDevice) && !document.hasFocus())
|
|
4003
4286
|
inputClear();
|
|
4004
4287
|
|
|
4005
|
-
// update mouse world space position
|
|
4288
|
+
// update mouse world space position and delta
|
|
4006
4289
|
mousePos = screenToWorld(mousePosScreen);
|
|
4007
|
-
mouseDelta = mouseDeltaScreen
|
|
4290
|
+
mouseDelta = screenToWorldDelta(mouseDeltaScreen);
|
|
4008
4291
|
|
|
4009
4292
|
// update gamepads if enabled
|
|
4010
4293
|
gamepadsUpdate();
|
|
@@ -4033,10 +4316,10 @@ function inputInit()
|
|
|
4033
4316
|
document.addEventListener('mousedown', onMouseDown);
|
|
4034
4317
|
document.addEventListener('mouseup', onMouseUp);
|
|
4035
4318
|
document.addEventListener('mousemove', onMouseMove);
|
|
4319
|
+
document.addEventListener('mouseleave', onMouseLeave);
|
|
4036
4320
|
document.addEventListener('wheel', onMouseWheel);
|
|
4037
4321
|
document.addEventListener('contextmenu', onContextMenu);
|
|
4038
4322
|
document.addEventListener('blur', onBlur);
|
|
4039
|
-
document.addEventListener('mouseleave', onMouseLeave);
|
|
4040
4323
|
|
|
4041
4324
|
// init touch input
|
|
4042
4325
|
if (isTouchDevice && touchInputEnable)
|
|
@@ -4058,14 +4341,14 @@ function inputInit()
|
|
|
4058
4341
|
if (inputWASDEmulateDirection)
|
|
4059
4342
|
inputData[0][remapKey(e.code)] = 4;
|
|
4060
4343
|
}
|
|
4061
|
-
function remapKey(
|
|
4344
|
+
function remapKey(k)
|
|
4062
4345
|
{
|
|
4063
4346
|
// handle remapping wasd keys to directions
|
|
4064
4347
|
return inputWASDEmulateDirection ?
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4348
|
+
k === 'KeyW' ? 'ArrowUp' :
|
|
4349
|
+
k === 'KeyS' ? 'ArrowDown' :
|
|
4350
|
+
k === 'KeyA' ? 'ArrowLeft' :
|
|
4351
|
+
k === 'KeyD' ? 'ArrowRight' : k : k;
|
|
4069
4352
|
}
|
|
4070
4353
|
function onMouseDown(e)
|
|
4071
4354
|
{
|
|
@@ -4078,7 +4361,10 @@ function inputInit()
|
|
|
4078
4361
|
|
|
4079
4362
|
isUsingGamepad = false;
|
|
4080
4363
|
inputData[0][e.button] = 3;
|
|
4364
|
+
|
|
4365
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4081
4366
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4367
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4082
4368
|
inputPreventDefault && e.button && e.preventDefault();
|
|
4083
4369
|
}
|
|
4084
4370
|
function onMouseUp(e)
|
|
@@ -4089,18 +4375,20 @@ function inputInit()
|
|
|
4089
4375
|
}
|
|
4090
4376
|
function onMouseMove(e)
|
|
4091
4377
|
{
|
|
4378
|
+
mouseInWindow = true;
|
|
4379
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4092
4380
|
mousePosScreen = mouseEventToScreen(vec2(e.x,e.y));
|
|
4093
|
-
|
|
4381
|
+
|
|
4382
|
+
// when pointer is locked use movementX/Y for delta
|
|
4383
|
+
const movement = pointerLockIsActive() ?
|
|
4384
|
+
vec2(e.movementX, e.movementY) :
|
|
4385
|
+
mousePosScreen.subtract(mousePosScreenLast);
|
|
4386
|
+
mouseDeltaScreen = mouseDeltaScreen.add(movement);
|
|
4094
4387
|
}
|
|
4388
|
+
function onMouseLeave() { mouseInWindow = false; } // mouse moved off window
|
|
4095
4389
|
function onMouseWheel(e) { mouseWheel = e.ctrlKey ? 0 : sign(e.deltaY); }
|
|
4096
4390
|
function onContextMenu(e) { e.preventDefault(); } // prevent right click menu
|
|
4097
4391
|
function onBlur() { inputClear(); } // reset input when focus is lost
|
|
4098
|
-
function onMouseLeave()
|
|
4099
|
-
{
|
|
4100
|
-
// set mouse position and delta when leaving canvas
|
|
4101
|
-
mousePosScreen = vec2(-1);
|
|
4102
|
-
mouseDeltaScreen = vec2(0);
|
|
4103
|
-
}
|
|
4104
4392
|
}
|
|
4105
4393
|
|
|
4106
4394
|
// convert a mouse or touch event position to screen space
|
|
@@ -4144,8 +4432,8 @@ function gamepadsUpdate()
|
|
|
4144
4432
|
else if (touchGamepadStick.lengthSquared() > .3)
|
|
4145
4433
|
{
|
|
4146
4434
|
// convert to 8 way dpad
|
|
4147
|
-
sticks[0].x =
|
|
4148
|
-
sticks[0].y = -
|
|
4435
|
+
sticks[0].x = round(touchGamepadStick.x);
|
|
4436
|
+
sticks[0].y = -round(touchGamepadStick.y);
|
|
4149
4437
|
sticks[0] = sticks[0].clampLength();
|
|
4150
4438
|
}
|
|
4151
4439
|
|
|
@@ -4236,7 +4524,7 @@ let touchGamepadTimer = new Timer, touchGamepadButtons = [], touchGamepadStick =
|
|
|
4236
4524
|
function touchGamepadButtonCenter()
|
|
4237
4525
|
{
|
|
4238
4526
|
// draw right face buttons
|
|
4239
|
-
|
|
4527
|
+
const center = vec2(mainCanvasSize.x-touchGamepadSize, mainCanvasSize.y-touchGamepadSize);
|
|
4240
4528
|
if (touchGamepadButtonCount <= 2)
|
|
4241
4529
|
center.x += touchGamepadSize/2;
|
|
4242
4530
|
return center;
|
|
@@ -4246,14 +4534,13 @@ function touchGamepadButtonCenter()
|
|
|
4246
4534
|
function touchInputInit()
|
|
4247
4535
|
{
|
|
4248
4536
|
// add non passive touch event listeners
|
|
4249
|
-
let handleTouch = handleTouchDefault;
|
|
4250
4537
|
document.addEventListener('touchstart', (e) => handleTouch(e), { passive: false });
|
|
4251
4538
|
document.addEventListener('touchmove', (e) => handleTouch(e), { passive: false });
|
|
4252
4539
|
document.addEventListener('touchend', (e) => handleTouch(e), { passive: false });
|
|
4253
4540
|
|
|
4254
4541
|
// handle all touch events the same way
|
|
4255
4542
|
let wasTouching;
|
|
4256
|
-
function
|
|
4543
|
+
function handleTouch(e)
|
|
4257
4544
|
{
|
|
4258
4545
|
if (!touchInputEnable)
|
|
4259
4546
|
return;
|
|
@@ -4273,11 +4560,11 @@ function touchInputInit()
|
|
|
4273
4560
|
{
|
|
4274
4561
|
// set event pos and pass it along
|
|
4275
4562
|
const pos = vec2(e.touches[0].clientX, e.touches[0].clientY);
|
|
4276
|
-
const
|
|
4563
|
+
const mousePosScreenLast = mousePosScreen;
|
|
4277
4564
|
mousePosScreen = mouseEventToScreen(pos);
|
|
4278
4565
|
if (wasTouching)
|
|
4279
4566
|
{
|
|
4280
|
-
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(
|
|
4567
|
+
mouseDeltaScreen = mouseDeltaScreen.add(mousePosScreen.subtract(mousePosScreenLast));
|
|
4281
4568
|
isUsingGamepad = touchGamepadEnable;
|
|
4282
4569
|
}
|
|
4283
4570
|
else
|
|
@@ -4309,7 +4596,7 @@ function touchInputInit()
|
|
|
4309
4596
|
if (touching)
|
|
4310
4597
|
{
|
|
4311
4598
|
touchGamepadTimer.set();
|
|
4312
|
-
if (
|
|
4599
|
+
if (touchGamepadCenterButton && !wasTouching && paused)
|
|
4313
4600
|
{
|
|
4314
4601
|
// touch anywhere to press start when paused
|
|
4315
4602
|
touchGamepadButtons[9] = 1;
|
|
@@ -4348,7 +4635,8 @@ function touchInputInit()
|
|
|
4348
4635
|
if (button < touchGamepadButtonCount)
|
|
4349
4636
|
touchGamepadButtons[button] = 1;
|
|
4350
4637
|
}
|
|
4351
|
-
else if (
|
|
4638
|
+
else if (touchGamepadCenterButton && !wasTouching &&
|
|
4639
|
+
startCenter.distance(touchPos) < touchGamepadSize)
|
|
4352
4640
|
{
|
|
4353
4641
|
// virtual start button in center
|
|
4354
4642
|
touchGamepadButtons[9] = 1;
|
|
@@ -4403,7 +4691,7 @@ function touchGamepadRender()
|
|
|
4403
4691
|
const buttonSize = touchGamepadButtonCount > 1 ? touchGamepadSize/4 : touchGamepadSize/2;
|
|
4404
4692
|
for (let i=0; i<touchGamepadButtonCount; i++)
|
|
4405
4693
|
{
|
|
4406
|
-
|
|
4694
|
+
const j = mod(i-1, 4);
|
|
4407
4695
|
let button = touchGamepadButtonCount > 2 ?
|
|
4408
4696
|
j : min(j, touchGamepadButtonCount-1);
|
|
4409
4697
|
// fix button locations (swap 2 and 3 to match gamepad layout)
|
|
@@ -4505,6 +4793,10 @@ class Sound
|
|
|
4505
4793
|
{
|
|
4506
4794
|
if (!soundEnable || headlessMode) return;
|
|
4507
4795
|
|
|
4796
|
+
ASSERT(!zzfxSound || isArray(zzfxSound), 'zzfxSound is invalid');
|
|
4797
|
+
ASSERT(isNumber(range), 'range must be a number');
|
|
4798
|
+
ASSERT(isNumber(taper), 'taper must be a number');
|
|
4799
|
+
|
|
4508
4800
|
/** @property {number} - World space max range of sound */
|
|
4509
4801
|
this.range = range;
|
|
4510
4802
|
/** @property {number} - At what percentage of range should it start tapering */
|
|
@@ -4543,6 +4835,12 @@ class Sound
|
|
|
4543
4835
|
*/
|
|
4544
4836
|
play(pos, volume=1, pitch=1, randomnessScale=1, loop=false, paused=false)
|
|
4545
4837
|
{
|
|
4838
|
+
ASSERT(!pos || isVector2(pos), 'pos must be a vec2');
|
|
4839
|
+
ASSERT(isNumber(volume), 'volume must be a number');
|
|
4840
|
+
ASSERT(isNumber(pitch), 'pitch must be a number');
|
|
4841
|
+
ASSERT(isNumber(randomnessScale), 'randomnessScale must be a number');
|
|
4842
|
+
|
|
4843
|
+
|
|
4546
4844
|
if (!soundEnable || headlessMode) return;
|
|
4547
4845
|
if (!this.sampleChannels) return;
|
|
4548
4846
|
|
|
@@ -4588,6 +4886,7 @@ class Sound
|
|
|
4588
4886
|
*/
|
|
4589
4887
|
playNote(semitoneOffset=0, pos, volume)
|
|
4590
4888
|
{
|
|
4889
|
+
ASSERT(isNumber(semitoneOffset), 'semitoneOffset must be a number');
|
|
4591
4890
|
const pitch = getNoteFrequency(semitoneOffset, 1);
|
|
4592
4891
|
return this.play(pos, volume, pitch, 0);
|
|
4593
4892
|
}
|
|
@@ -4596,7 +4895,7 @@ class Sound
|
|
|
4596
4895
|
* @return {number} - How long the sound is in seconds (undefined if loading)
|
|
4597
4896
|
*/
|
|
4598
4897
|
getDuration()
|
|
4599
|
-
{ return this.sampleChannels && this.sampleChannels[0].length / this.sampleRate; }
|
|
4898
|
+
{ return this.sampleChannels && this.sampleRate ? this.sampleChannels[0].length / this.sampleRate : 0; }
|
|
4600
4899
|
|
|
4601
4900
|
/** Check if sound is loaded, for sounds fetched from a url
|
|
4602
4901
|
* @return {boolean} - True if sound is loaded and ready to play
|
|
@@ -4638,6 +4937,7 @@ class SoundWave extends Sound
|
|
|
4638
4937
|
super(undefined, range, taper);
|
|
4639
4938
|
if (!soundEnable || headlessMode) return;
|
|
4640
4939
|
ASSERT(!filename || isString(filename), 'filename must be a string');
|
|
4940
|
+
ASSERT(isNumber(randomness), 'randomness must be a number');
|
|
4641
4941
|
|
|
4642
4942
|
/** @property {SoundLoadCallback} - callback function to call when sound is loaded */
|
|
4643
4943
|
this.onloadCallback = onloadCallback;
|
|
@@ -4854,7 +5154,7 @@ class SoundInstance
|
|
|
4854
5154
|
/** Get the total duration of this sound
|
|
4855
5155
|
* @return {number} - Total duration in seconds
|
|
4856
5156
|
*/
|
|
4857
|
-
getDuration() { return this.sound.getDuration() / this.rate; }
|
|
5157
|
+
getDuration() { return this.rate ? this.sound.getDuration() / this.rate : 0; }
|
|
4858
5158
|
|
|
4859
5159
|
/** Get source of this sound instance
|
|
4860
5160
|
* @return {AudioBufferSourceNode}
|
|
@@ -5044,10 +5344,10 @@ function zzfxG
|
|
|
5044
5344
|
|
|
5045
5345
|
// biquad LP/HP filter
|
|
5046
5346
|
quality = 2, w = PI2 * abs(filter) * 2 / sampleRate,
|
|
5047
|
-
|
|
5048
|
-
a0 = 1 + alpha, a1 = -2*
|
|
5049
|
-
b0 = (1 + sign(filter) *
|
|
5050
|
-
b1 = -(sign(filter) +
|
|
5347
|
+
cosw = cos(w), alpha = sin(w) / 2 / quality,
|
|
5348
|
+
a0 = 1 + alpha, a1 = -2*cosw / a0, a2 = (1 - alpha) / a0,
|
|
5349
|
+
b0 = (1 + sign(filter) * cosw) / 2 / a0,
|
|
5350
|
+
b1 = -(sign(filter) + cosw) / a0, b2 = b0,
|
|
5051
5351
|
x2 = 0, x1 = 0, y2 = 0, y1 = 0;
|
|
5052
5352
|
|
|
5053
5353
|
// scale by sample rate
|
|
@@ -5070,15 +5370,15 @@ function zzfxG
|
|
|
5070
5370
|
if (!(++crush%(bitCrush*100|0))) // bit crush
|
|
5071
5371
|
{
|
|
5072
5372
|
s = shape? shape>1? shape>2? shape>3? shape>4? // wave shape
|
|
5073
|
-
(t/PI2%1 < shapeCurve/2? 1 : -1) :
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
1-(2*t/PI2%2+2)%2:
|
|
5077
|
-
1-4*abs(
|
|
5078
|
-
|
|
5373
|
+
(t/PI2%1 < shapeCurve/2? 1 : -1) : // 5 square duty
|
|
5374
|
+
sin(t**3) : // 4 noise
|
|
5375
|
+
max(min(tan(t),1),-1): // 3 tan
|
|
5376
|
+
1-(2*t/PI2%2+2)%2: // 2 saw
|
|
5377
|
+
1-4*abs(round(t/PI2)-t/PI2): // 1 triangle
|
|
5378
|
+
sin(t); // 0 sin
|
|
5079
5379
|
|
|
5080
5380
|
s = (repeatTime ?
|
|
5081
|
-
1 - tremolo + tremolo*
|
|
5381
|
+
1 - tremolo + tremolo*sin(PI2*i/repeatTime) // tremolo
|
|
5082
5382
|
: 1) *
|
|
5083
5383
|
(shape>4?s:sign(s)*abs(s)**shapeCurve) * // shape curve
|
|
5084
5384
|
(i < attack ? i/attack : // attack
|
|
@@ -5100,8 +5400,8 @@ function zzfxG
|
|
|
5100
5400
|
}
|
|
5101
5401
|
|
|
5102
5402
|
f = (frequency += slide += deltaSlide) *// frequency
|
|
5103
|
-
|
|
5104
|
-
t += f + f*noise*
|
|
5403
|
+
cos(modulation*modOffset++); // modulation
|
|
5404
|
+
t += f + f*noise*sin(i**5); // noise
|
|
5105
5405
|
|
|
5106
5406
|
if (jump && ++jump > pitchJumpTime) // pitch jump
|
|
5107
5407
|
{
|
|
@@ -5167,23 +5467,23 @@ function tileCollisionTest(pos, size=vec2(), object, solidOnly=true)
|
|
|
5167
5467
|
}
|
|
5168
5468
|
}
|
|
5169
5469
|
|
|
5170
|
-
/** Return the
|
|
5171
|
-
*
|
|
5470
|
+
/** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
|
|
5471
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5172
5472
|
* @param {Vector2} posStart
|
|
5173
5473
|
* @param {Vector2} posEnd
|
|
5174
5474
|
* @param {EngineObject} [object] - An object or undefined for generic test
|
|
5475
|
+
* @param {Vector2} [normal] - Optional normal of the surface hit
|
|
5175
5476
|
* @param {boolean} [solidOnly=true] - Only check solid layers if true
|
|
5176
|
-
* @return {Vector2}
|
|
5477
|
+
* @return {Vector2|undefined} - position of the center of the tile hit or undefined if no hit
|
|
5177
5478
|
* @memberof TileLayers */
|
|
5178
|
-
function tileCollisionRaycast(posStart, posEnd, object, solidOnly=true)
|
|
5479
|
+
function tileCollisionRaycast(posStart, posEnd, object, normal, solidOnly=true)
|
|
5179
5480
|
{
|
|
5180
5481
|
for (const layer of tileCollisionLayers)
|
|
5181
5482
|
{
|
|
5182
5483
|
if (!solidOnly || layer.isSolid)
|
|
5183
5484
|
{
|
|
5184
|
-
const hitPos = layer.collisionRaycast(posStart, posEnd, object)
|
|
5185
|
-
if (hitPos)
|
|
5186
|
-
return hitPos;
|
|
5485
|
+
const hitPos = layer.collisionRaycast(posStart, posEnd, object, normal)
|
|
5486
|
+
if (hitPos) return hitPos;
|
|
5187
5487
|
}
|
|
5188
5488
|
}
|
|
5189
5489
|
}
|
|
@@ -5321,6 +5621,8 @@ class CanvasLayer extends EngineObject
|
|
|
5321
5621
|
/** @property {TextureInfo} - Texture info to use for this object rendering */
|
|
5322
5622
|
const useWebGL = false; // do not use webgl by default
|
|
5323
5623
|
this.textureInfo = new TextureInfo(this.canvas, useWebGL);
|
|
5624
|
+
/** @property {boolean} - True if WebGL texture needs to be refreshed */
|
|
5625
|
+
this.refreshWebGL = false;
|
|
5324
5626
|
|
|
5325
5627
|
// disable physics by default
|
|
5326
5628
|
this.mass = this.gravityScale = this.friction = this.restitution = 0;
|
|
@@ -5354,8 +5656,15 @@ class CanvasLayer extends EngineObject
|
|
|
5354
5656
|
* @memberof Draw */
|
|
5355
5657
|
draw(pos, size, angle=0, color=WHITE, mirror=false, additiveColor, screenSpace=false, context)
|
|
5356
5658
|
{
|
|
5357
|
-
// draw the canvas layer as a single tile that uses the whole texture
|
|
5358
5659
|
const useWebGL = glEnable && this.textureInfo.hasWebGL();
|
|
5660
|
+
if (useWebGL && this.refreshWebGL)
|
|
5661
|
+
{
|
|
5662
|
+
// update the WebGL texture
|
|
5663
|
+
this.textureInfo.createWebGLTexture();
|
|
5664
|
+
this.refreshWebGL = false;
|
|
5665
|
+
}
|
|
5666
|
+
|
|
5667
|
+
// draw the canvas layer as a single tile that uses the whole texture
|
|
5359
5668
|
const tileInfo = new TileInfo().setFullImage(this.textureInfo);
|
|
5360
5669
|
drawTile(pos, size, tileInfo, color, angle, mirror, additiveColor, useWebGL, screenSpace, context);
|
|
5361
5670
|
}
|
|
@@ -5425,9 +5734,18 @@ class CanvasLayer extends EngineObject
|
|
|
5425
5734
|
{ this.drawTile(pos, size, undefined, color, angle); }
|
|
5426
5735
|
|
|
5427
5736
|
/** Create or update the WebGL texture for this layer
|
|
5428
|
-
* @param {boolean} [enable] - enable WebGL rendering and update the texture
|
|
5429
|
-
|
|
5737
|
+
* @param {boolean} [enable] - enable WebGL rendering and update the texture
|
|
5738
|
+
* @param {boolean} [immediate] - shoulkd the texture be updated immediately
|
|
5739
|
+
*/
|
|
5740
|
+
useWebGL(enable=true, immediate=false)
|
|
5430
5741
|
{
|
|
5742
|
+
if (!immediate && enable && this.textureInfo.hasWebGL())
|
|
5743
|
+
{
|
|
5744
|
+
// refresh the texture when needed
|
|
5745
|
+
this.refreshWebGL = true;
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5431
5749
|
if (enable)
|
|
5432
5750
|
this.textureInfo.createWebGLTexture();
|
|
5433
5751
|
else
|
|
@@ -5458,7 +5776,7 @@ class TileLayer extends CanvasLayer
|
|
|
5458
5776
|
*/
|
|
5459
5777
|
constructor(position, size, tileInfo=tile(), renderOrder=0)
|
|
5460
5778
|
{
|
|
5461
|
-
const canvasSize = size.multiply(tileInfo.size);
|
|
5779
|
+
const canvasSize = tileInfo ? size.multiply(tileInfo.size) : size;
|
|
5462
5780
|
super(position, size, 0, renderOrder, canvasSize);
|
|
5463
5781
|
|
|
5464
5782
|
// set tile info
|
|
@@ -5511,6 +5829,13 @@ class TileLayer extends CanvasLayer
|
|
|
5511
5829
|
{
|
|
5512
5830
|
ASSERT(drawContext !== this.context, 'must call redrawEnd() after drawing tiles!');
|
|
5513
5831
|
|
|
5832
|
+
if (this.refreshWebGL)
|
|
5833
|
+
{
|
|
5834
|
+
// update the WebGL texture
|
|
5835
|
+
this.textureInfo.createWebGLTexture();
|
|
5836
|
+
this.refreshWebGL = false;
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5514
5839
|
// draw the tile layer as a single tile
|
|
5515
5840
|
const tileInfo = new TileInfo().setFullImage(this.textureInfo);
|
|
5516
5841
|
const size = this.drawSize || this.size;
|
|
@@ -5547,8 +5872,9 @@ class TileLayer extends CanvasLayer
|
|
|
5547
5872
|
drawCanvas = this.canvas;
|
|
5548
5873
|
drawContext = this.context;
|
|
5549
5874
|
cameraPos = this.size.scale(.5);
|
|
5550
|
-
|
|
5551
|
-
|
|
5875
|
+
const tileSize = this.tileInfo ? this.tileInfo.size : vec2(1);
|
|
5876
|
+
cameraScale = tileSize.x;
|
|
5877
|
+
mainCanvasSize = this.size.multiply(tileSize);
|
|
5552
5878
|
if (clear)
|
|
5553
5879
|
{
|
|
5554
5880
|
// clear and set size
|
|
@@ -5708,66 +6034,45 @@ class TileCollisionLayer extends TileLayer
|
|
|
5708
6034
|
// check if the object should collide with this tile
|
|
5709
6035
|
const tileData = this.collisionData[y*this.size.x+x];
|
|
5710
6036
|
if (tileData)
|
|
5711
|
-
if (!object || object.collideWithTile(tileData,
|
|
6037
|
+
if (!object || object.collideWithTile(tileData,
|
|
6038
|
+
hitPos.set(x + this.pos.x, y + this.pos.y)))
|
|
5712
6039
|
return true;
|
|
5713
6040
|
}
|
|
5714
6041
|
return false;
|
|
5715
6042
|
}
|
|
5716
6043
|
|
|
5717
|
-
/** Return the
|
|
5718
|
-
*
|
|
6044
|
+
/** Return the exact position of the boudnary of first tile hit, undefined if nothing was hit.
|
|
6045
|
+
* The point will be inside the colliding tile if it hits (may have a tiny shift)
|
|
5719
6046
|
* @param {Vector2} posStart
|
|
5720
6047
|
* @param {Vector2} posEnd
|
|
5721
|
-
* @param {EngineObject} [object]
|
|
5722
|
-
* @
|
|
5723
|
-
|
|
6048
|
+
* @param {EngineObject} [object] - An object or undefined for generic test
|
|
6049
|
+
* @param {Vector2} [normal] - Optional normal of the surface hit
|
|
6050
|
+
* @return {Vector2|undefined} */
|
|
6051
|
+
collisionRaycast(posStart, posEnd, object, normal)
|
|
5724
6052
|
{
|
|
5725
6053
|
ASSERT(isVector2(posStart) && isVector2(posEnd), 'positions must be Vector2s');
|
|
5726
6054
|
ASSERT(!object || object instanceof EngineObject, 'object must be an EngineObject');
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
const
|
|
5730
|
-
const posStartY = posStart.y - this.pos.y;
|
|
5731
|
-
const posEndX = posEnd.x - this.pos.x;
|
|
5732
|
-
const posEndY = posEnd.y - this.pos.y;
|
|
5733
|
-
|
|
5734
|
-
// test if a ray collides with tiles from start to end
|
|
5735
|
-
const deltaX = posEndX - posStartX;
|
|
5736
|
-
const deltaY = posEndY - posStartY;
|
|
5737
|
-
const totalLength = (deltaX**2 + deltaY**2)**.5;
|
|
5738
|
-
const unitX = abs(totalLength/deltaX);
|
|
5739
|
-
const unitY = abs(totalLength/deltaY);
|
|
5740
|
-
|
|
5741
|
-
// setup iteration variables
|
|
5742
|
-
const pos = posStart.floor(), signDeltaX = sign(deltaX), signDeltaY = sign(deltaY);
|
|
5743
|
-
let xi = unitX * (deltaX < 0 ? posStart.x - pos.x : pos.x - posStart.x + 1) || 0;
|
|
5744
|
-
let yi = unitY * (deltaY < 0 ? posStart.y - pos.y : pos.y - posStart.y + 1) || 0;
|
|
5745
|
-
|
|
5746
|
-
// use line drawing algorithm to test for collisions
|
|
5747
|
-
while (true)
|
|
6055
|
+
|
|
6056
|
+
const localPos = new Vector2;
|
|
6057
|
+
const collisionTest = (pos)=>
|
|
5748
6058
|
{
|
|
5749
6059
|
// check for tile collision
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
pos.x += .5; pos.y += .5;
|
|
5754
|
-
debugRaycast && debugLine(posStart, posEnd, '#f00', .02);
|
|
5755
|
-
debugRaycast && debugPoint(pos, '#ff0');
|
|
5756
|
-
return pos;
|
|
5757
|
-
}
|
|
5758
|
-
|
|
5759
|
-
// check if past the end
|
|
5760
|
-
if (xi >= totalLength && yi >= totalLength)
|
|
5761
|
-
break;
|
|
5762
|
-
|
|
5763
|
-
// get coordinates of next tile to check
|
|
5764
|
-
if (xi > yi)
|
|
5765
|
-
pos.y += signDeltaY, yi += unitY;
|
|
5766
|
-
else
|
|
5767
|
-
pos.x += signDeltaX, xi += unitX;
|
|
6060
|
+
localPos.set(pos.x - this.pos.x, pos.y - this.pos.y);
|
|
6061
|
+
const tileData = this.getCollisionData(localPos);
|
|
6062
|
+
return tileData && (!object || object.collideWithTile(tileData, pos));
|
|
5768
6063
|
}
|
|
5769
|
-
|
|
5770
6064
|
debugRaycast && debugLine(posStart, posEnd, '#00f', .02);
|
|
6065
|
+
const hitPos = lineTest(posStart, posEnd, collisionTest, normal);
|
|
6066
|
+
if (hitPos)
|
|
6067
|
+
{
|
|
6068
|
+
const tilePos = hitPos.floor().add(vec2(.5));
|
|
6069
|
+
debugRaycast && debugRect(tilePos, vec2(1), '#f008');
|
|
6070
|
+
debugRaycast && debugLine(posStart, hitPos, '#f00', .02);
|
|
6071
|
+
debugRaycast && debugPoint(hitPos, '#0f0');
|
|
6072
|
+
debugRaycast && normal &&
|
|
6073
|
+
debugLine(hitPos, hitPos.add(normal), '#ff0', .02);
|
|
6074
|
+
return hitPos;
|
|
6075
|
+
}
|
|
5771
6076
|
}
|
|
5772
6077
|
}
|
|
5773
6078
|
/**
|
|
@@ -5922,19 +6227,34 @@ class ParticleEmitter extends EngineObject
|
|
|
5922
6227
|
this.particleCreateCallback = undefined;
|
|
5923
6228
|
/** @property {number} - Track particle emit time */
|
|
5924
6229
|
this.emitTimeBuffer = 0;
|
|
6230
|
+
/** @property {number} - Percentage of velocity to pass to particles (0-1) */
|
|
6231
|
+
this.velocityInheritance = 0;
|
|
6232
|
+
|
|
6233
|
+
// track previous position and angle
|
|
6234
|
+
this.previousAngle = this.angle;
|
|
6235
|
+
this.previousPos = this.pos.copy();
|
|
5925
6236
|
}
|
|
5926
6237
|
|
|
5927
6238
|
/** Update the emitter to spawn particles, called automatically by engine once each frame */
|
|
5928
6239
|
update()
|
|
5929
6240
|
{
|
|
5930
|
-
|
|
5931
|
-
|
|
6241
|
+
if (this.velocityInheritance)
|
|
6242
|
+
{
|
|
6243
|
+
// pass emitter velocity to particles
|
|
6244
|
+
const p = this.velocityInheritance;
|
|
6245
|
+
this.velocity.x = p * (this.pos.x - this.previousPos.x);
|
|
6246
|
+
this.velocity.y = p * (this.pos.y - this.previousPos.y);
|
|
6247
|
+
this.angleVelocity = p * (this.angle - this.previousAngle);
|
|
6248
|
+
this.previousAngle = this.angle;
|
|
6249
|
+
this.previousPos.x = this.pos.x;
|
|
6250
|
+
this.previousPos.y = this.pos.y;
|
|
6251
|
+
}
|
|
5932
6252
|
|
|
5933
6253
|
// update emitter
|
|
5934
6254
|
if (!this.emitTime || this.getAliveTime() <= this.emitTime)
|
|
5935
6255
|
{
|
|
5936
6256
|
// emit particles
|
|
5937
|
-
if (this.emitRate
|
|
6257
|
+
if (this.emitRate && particleEmitRateScale)
|
|
5938
6258
|
{
|
|
5939
6259
|
const rate = 1/this.emitRate/particleEmitRateScale;
|
|
5940
6260
|
for (this.emitTimeBuffer += timeDelta; this.emitTimeBuffer > 0; this.emitTimeBuffer -= rate)
|
|
@@ -5987,7 +6307,7 @@ class ParticleEmitter extends EngineObject
|
|
|
5987
6307
|
const particle = new Particle(pos, this.tileInfo, angle, colorStart, colorEnd, particleTime, sizeStart, sizeEnd, this.fadeRate, this.additive, this.trailScale, this.localSpace && this, this.particleDestroyCallback);
|
|
5988
6308
|
particle.velocity = vec2().setAngle(velocityAngle, speed);
|
|
5989
6309
|
particle.angleVelocity = angleSpeed;
|
|
5990
|
-
if (!this.localSpace)
|
|
6310
|
+
if (!this.localSpace && this.velocityInheritance > 0)
|
|
5991
6311
|
{
|
|
5992
6312
|
// apply emitter velocity to particle
|
|
5993
6313
|
particle.velocity.x += this.velocity.x;
|
|
@@ -6072,8 +6392,6 @@ class Particle extends EngineObject
|
|
|
6072
6392
|
/** Update the object physics, called automatically by engine once each frame */
|
|
6073
6393
|
update()
|
|
6074
6394
|
{
|
|
6075
|
-
super.update();
|
|
6076
|
-
|
|
6077
6395
|
if (this.collideTiles || this.collideSolidObjects)
|
|
6078
6396
|
{
|
|
6079
6397
|
// only apply max circular speed if particle can collide
|
|
@@ -6085,6 +6403,16 @@ class Particle extends EngineObject
|
|
|
6085
6403
|
this.velocity.y *= s;
|
|
6086
6404
|
}
|
|
6087
6405
|
}
|
|
6406
|
+
|
|
6407
|
+
if (this.lifeTime > 0 && time - this.spawnTime > this.lifeTime)
|
|
6408
|
+
{
|
|
6409
|
+
// destroy particle when its time runs out
|
|
6410
|
+
const c = this.colorEnd;
|
|
6411
|
+
this.color.set(c.r, c.g, c.b, c.a);
|
|
6412
|
+
this.size.set(this.sizeEnd, this.sizeEnd);
|
|
6413
|
+
this.destroyCallback && this.destroyCallback(this);
|
|
6414
|
+
this.destroyed = 1;
|
|
6415
|
+
}
|
|
6088
6416
|
}
|
|
6089
6417
|
|
|
6090
6418
|
/** Render the particle, automatically called each frame, sorted by renderOrder */
|
|
@@ -6093,7 +6421,7 @@ class Particle extends EngineObject
|
|
|
6093
6421
|
// lerp color and size
|
|
6094
6422
|
const p1 = this.lifeTime > 0 ? min((time - this.spawnTime) / this.lifeTime, 1) : 1, p2 = 1-p1;
|
|
6095
6423
|
const radius = p2 * this.sizeStart + p1 * this.sizeEnd;
|
|
6096
|
-
|
|
6424
|
+
const size = vec2(radius);
|
|
6097
6425
|
this.color.r = p2 * this.colorStart.r + p1 * this.colorEnd.r;
|
|
6098
6426
|
this.color.g = p2 * this.colorStart.g + p1 * this.colorEnd.g;
|
|
6099
6427
|
this.color.b = p2 * this.colorStart.b + p1 * this.colorEnd.b;
|
|
@@ -6113,7 +6441,7 @@ class Particle extends EngineObject
|
|
|
6113
6441
|
{
|
|
6114
6442
|
// in local space of emitter
|
|
6115
6443
|
const a = this.localSpaceEmitter.angle;
|
|
6116
|
-
const c =
|
|
6444
|
+
const c = cos(a), s = sin(a);
|
|
6117
6445
|
pos = this.localSpaceEmitter.pos.add(
|
|
6118
6446
|
new Vector2(pos.x*c - pos.y*s, pos.x*s + pos.y*c));
|
|
6119
6447
|
angle += this.localSpaceEmitter.angle;
|
|
@@ -6129,22 +6457,15 @@ class Particle extends EngineObject
|
|
|
6129
6457
|
{
|
|
6130
6458
|
// stretch in direction of motion
|
|
6131
6459
|
const trailLength = speed * this.trailScale;
|
|
6132
|
-
|
|
6133
|
-
angle =
|
|
6134
|
-
drawTile(pos,
|
|
6460
|
+
size.y = max(size.x, trailLength);
|
|
6461
|
+
angle = atan2(direction.x, direction.y);
|
|
6462
|
+
drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
|
|
6135
6463
|
}
|
|
6136
6464
|
}
|
|
6137
6465
|
else
|
|
6138
|
-
drawTile(pos,
|
|
6466
|
+
drawTile(pos, size, this.tileInfo, this.color, angle, this.mirror);
|
|
6139
6467
|
this.additive && setBlendMode();
|
|
6140
|
-
debugParticles && debugRect(pos,
|
|
6141
|
-
|
|
6142
|
-
if (p1 === 1)
|
|
6143
|
-
{
|
|
6144
|
-
// destroy particle when its time runs out
|
|
6145
|
-
this.destroyCallback && this.destroyCallback(this);
|
|
6146
|
-
this.destroyed = 1;
|
|
6147
|
-
}
|
|
6468
|
+
debugParticles && debugRect(pos, size, '#f005', 0, angle);
|
|
6148
6469
|
}
|
|
6149
6470
|
}
|
|
6150
6471
|
/**
|
|
@@ -6314,11 +6635,11 @@ class Medal
|
|
|
6314
6635
|
const descriptionSize = height*.3;
|
|
6315
6636
|
const pos = vec2(x + medalDisplayIconSize + 2*gap.x, y + gap.y*2 + nameSize/2);
|
|
6316
6637
|
const textWidth = width - medalDisplayIconSize - 3*gap.x;
|
|
6317
|
-
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
6638
|
+
drawTextScreen(this.name, pos, nameSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
6318
6639
|
|
|
6319
6640
|
// draw the description
|
|
6320
6641
|
pos.y = y + height - gap.y*2 - descriptionSize/2;
|
|
6321
|
-
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, textWidth);
|
|
6642
|
+
drawTextScreen(this.description, pos, descriptionSize, BLACK, 0, undefined, 'left', undefined, undefined, textWidth);
|
|
6322
6643
|
context.restore();
|
|
6323
6644
|
}
|
|
6324
6645
|
|
|
@@ -6367,7 +6688,7 @@ let glContext;
|
|
|
6367
6688
|
let glAntialias = true;
|
|
6368
6689
|
|
|
6369
6690
|
// WebGL internal variables not exposed to documentation
|
|
6370
|
-
let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos;
|
|
6691
|
+
let glShader, glPolyShader, glPolyMode, glAdditive, glBatchAdditive, glActiveTexture, glArrayBuffer, glGeometryBuffer, glPositionData, glColorData, glBatchCount, glTextureInfos, glCanBeEnabled = true;
|
|
6371
6692
|
|
|
6372
6693
|
// WebGL internal constants
|
|
6373
6694
|
const gl_ARRAY_BUFFER_SIZE = 5e5;
|
|
@@ -6386,7 +6707,11 @@ function glInit()
|
|
|
6386
6707
|
// keep set of texture infos so they can be restored if context is lost
|
|
6387
6708
|
glTextureInfos = new Set;
|
|
6388
6709
|
|
|
6389
|
-
if (!glEnable || headlessMode)
|
|
6710
|
+
if (!glEnable || headlessMode)
|
|
6711
|
+
{
|
|
6712
|
+
glCanBeEnabled = false;
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6390
6715
|
|
|
6391
6716
|
// create the canvas and textures
|
|
6392
6717
|
glCanvas = document.createElement('canvas');
|
|
@@ -6397,10 +6722,11 @@ function glInit()
|
|
|
6397
6722
|
console.warn('WebGL2 not supported, falling back to 2D canvas rendering!');
|
|
6398
6723
|
glCanvas = glContext = undefined;
|
|
6399
6724
|
glEnable = false;
|
|
6725
|
+
glCanBeEnabled = false;
|
|
6400
6726
|
return;
|
|
6401
6727
|
}
|
|
6402
6728
|
|
|
6403
|
-
//
|
|
6729
|
+
// attach the WebGL canvas
|
|
6404
6730
|
const rootElement = mainCanvas.parentElement;
|
|
6405
6731
|
rootElement.appendChild(glCanvas);
|
|
6406
6732
|
|
|
@@ -6423,7 +6749,7 @@ function glInit()
|
|
|
6423
6749
|
});
|
|
6424
6750
|
glCanvas.addEventListener('webglcontextrestored', ()=>
|
|
6425
6751
|
{
|
|
6426
|
-
glEnable = true; //
|
|
6752
|
+
glEnable = true; // re-enable WebGL rendering
|
|
6427
6753
|
glCanvas.style.display = ''; // show the gl canvas
|
|
6428
6754
|
LOG('WebGL context restored, reinitializing...');
|
|
6429
6755
|
|
|
@@ -6575,8 +6901,8 @@ function glPreRender()
|
|
|
6575
6901
|
const s = vec2(2*cameraScale).divide(mainCanvasSize);
|
|
6576
6902
|
const rotatedCam = cameraPos.rotate(-cameraAngle);
|
|
6577
6903
|
const p = vec2(-1).subtract(rotatedCam.multiply(s));
|
|
6578
|
-
const ca =
|
|
6579
|
-
const sa =
|
|
6904
|
+
const ca = cos(cameraAngle);
|
|
6905
|
+
const sa = sin(cameraAngle);
|
|
6580
6906
|
const transform = [
|
|
6581
6907
|
s.x * ca, s.y * sa, 0, 0,
|
|
6582
6908
|
-s.x * sa, s.y * ca, 0, 0,
|
|
@@ -6881,8 +7207,8 @@ function glDrawPointsTransform(points, rgba, x, y, sx, sy, angle, tristrip=true)
|
|
|
6881
7207
|
// transform the point
|
|
6882
7208
|
const px = p.x*sx;
|
|
6883
7209
|
const py = p.y*sy;
|
|
6884
|
-
const sa =
|
|
6885
|
-
const ca =
|
|
7210
|
+
const sa = sin(-angle);
|
|
7211
|
+
const ca = cos(-angle);
|
|
6886
7212
|
pointsOut.push(vec2(x + ca*px - sa*py, y + sa*px + ca*py));
|
|
6887
7213
|
}
|
|
6888
7214
|
const drawPoints = tristrip ? glPolyStrip(pointsOut) : pointsOut;
|
|
@@ -7799,12 +8125,12 @@ class UISystemPlugin
|
|
|
7799
8125
|
/** Draw a rectangle to the UI context
|
|
7800
8126
|
* @param {Vector2} pos
|
|
7801
8127
|
* @param {Vector2} size
|
|
7802
|
-
* @param {Color} [color
|
|
7803
|
-
* @param {number} [lineWidth
|
|
7804
|
-
* @param {Color} [lineColor
|
|
7805
|
-
* @param {number} [cornerRadius
|
|
7806
|
-
* @param {Color} [gradientColor
|
|
7807
|
-
drawRect(pos, size, color=
|
|
8128
|
+
* @param {Color} [color]
|
|
8129
|
+
* @param {number} [lineWidth]
|
|
8130
|
+
* @param {Color} [lineColor]
|
|
8131
|
+
* @param {number} [cornerRadius]
|
|
8132
|
+
* @param {Color} [gradientColor] */
|
|
8133
|
+
drawRect(pos, size, color=WHITE, lineWidth=0, lineColor=BLACK, cornerRadius=0, gradientColor)
|
|
7808
8134
|
{
|
|
7809
8135
|
ASSERT(isVector2(pos), 'pos must be a vec2');
|
|
7810
8136
|
ASSERT(isVector2(size), 'size must be a vec2');
|
|
@@ -7883,10 +8209,14 @@ class UISystemPlugin
|
|
|
7883
8209
|
* @param {string} [align]
|
|
7884
8210
|
* @param {string} [font=uiSystem.defaultFont]
|
|
7885
8211
|
* @param {string} [fontStyle]
|
|
7886
|
-
* @param {boolean} [applyMaxWidth=true]
|
|
7887
|
-
|
|
8212
|
+
* @param {boolean} [applyMaxWidth=true]
|
|
8213
|
+
* @param {Vector2} [textShadow]
|
|
8214
|
+
*/
|
|
8215
|
+
drawText(text, pos, size, color=uiSystem.defaultColor, lineWidth=uiSystem.defaultLineWidth, lineColor=uiSystem.defaultLineColor, align='center', font=uiSystem.defaultFont, fontStyle='', applyMaxWidth=true, textShadow=undefined)
|
|
7888
8216
|
{
|
|
7889
|
-
|
|
8217
|
+
if (textShadow)
|
|
8218
|
+
drawTextScreen(text, pos.add(textShadow), size.y, BLACK, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
|
|
8219
|
+
drawTextScreen(text, pos, size.y, color, lineWidth, lineColor, align, font, fontStyle, applyMaxWidth ? size.x : undefined, 0, uiSystem.uiContext);
|
|
7890
8220
|
}
|
|
7891
8221
|
|
|
7892
8222
|
/**
|
|
@@ -7913,6 +8243,25 @@ class UISystemPlugin
|
|
|
7913
8243
|
setCallback(onDragLeave, 'dragleave');
|
|
7914
8244
|
setCallback(onDragOver, 'dragover');
|
|
7915
8245
|
}
|
|
8246
|
+
|
|
8247
|
+
/** Convert a screen space position to native UI position
|
|
8248
|
+
* @param {Vector2} pos
|
|
8249
|
+
* @return {Vector2}
|
|
8250
|
+
*/
|
|
8251
|
+
screenToNative(pos)
|
|
8252
|
+
{
|
|
8253
|
+
if (!uiSystem.nativeHeight)
|
|
8254
|
+
return pos;
|
|
8255
|
+
|
|
8256
|
+
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
8257
|
+
const sInv = 1/s;
|
|
8258
|
+
const p = pos.copy();
|
|
8259
|
+
p.x += s*mainCanvasSize.x/2;
|
|
8260
|
+
p.x *= sInv;
|
|
8261
|
+
p.y *= sInv;
|
|
8262
|
+
p.x -= sInv*mainCanvasSize.x/2;
|
|
8263
|
+
return p;
|
|
8264
|
+
}
|
|
7916
8265
|
}
|
|
7917
8266
|
|
|
7918
8267
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -7989,6 +8338,9 @@ class UIObject
|
|
|
7989
8338
|
/** @property {boolean} - True if this can be a hover object */
|
|
7990
8339
|
this.canBeHover = true;
|
|
7991
8340
|
uiSystem.uiObjects.push(this);
|
|
8341
|
+
|
|
8342
|
+
/** @property {Vector2} - How much to offset the text shadow or undefined */
|
|
8343
|
+
this.textShadow = undefined;
|
|
7992
8344
|
}
|
|
7993
8345
|
|
|
7994
8346
|
/** Add a child UIObject to this object
|
|
@@ -8016,24 +8368,20 @@ class UIObject
|
|
|
8016
8368
|
*/
|
|
8017
8369
|
isMouseOverlapping()
|
|
8018
8370
|
{
|
|
8371
|
+
if (!mouseInWindow) return false;
|
|
8372
|
+
|
|
8019
8373
|
const size = !isTouchDevice ? this.size :
|
|
8020
8374
|
this.size.add(vec2(this.extraTouchSize || 0));
|
|
8021
|
-
|
|
8022
|
-
return isOverlapping(this.pos, size, mousePosScreen);
|
|
8023
|
-
|
|
8024
|
-
const s = mainCanvasSize.y / uiSystem.nativeHeight;
|
|
8025
|
-
const sInv = 1/s;
|
|
8026
|
-
let pos = mousePosScreen.copy();
|
|
8027
|
-
pos.x += s*mainCanvasSize.x/2;
|
|
8028
|
-
pos.x *= sInv;
|
|
8029
|
-
pos.y *= sInv;
|
|
8030
|
-
pos.x -= sInv*mainCanvasSize.x/2;
|
|
8375
|
+
const pos = uiSystem.screenToNative(mousePosScreen);
|
|
8031
8376
|
return isOverlapping(this.pos, size, pos);
|
|
8032
8377
|
}
|
|
8033
8378
|
|
|
8034
8379
|
/** Update the object, called automatically by plugin once each frame */
|
|
8035
8380
|
update()
|
|
8036
8381
|
{
|
|
8382
|
+
// call the custom update callback
|
|
8383
|
+
this.onUpdate();
|
|
8384
|
+
|
|
8037
8385
|
const wasHover = uiSystem.lastHoverObject === this;
|
|
8038
8386
|
const isActive = this.isActiveObject();
|
|
8039
8387
|
const mouseDown = mouseIsDown(0);
|
|
@@ -8090,7 +8438,7 @@ class UIObject
|
|
|
8090
8438
|
|
|
8091
8439
|
const lineColor = this.interactive && this.isActiveObject() && !this.disabled ? this.color : this.lineColor;
|
|
8092
8440
|
const color = this.disabled ? this.disabledColor : this.interactive ? this.isActiveObject() ? this.activeColor || this.color : this.isHoverObject() ? this.hoverColor : this.color : this.color;
|
|
8093
|
-
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius);
|
|
8441
|
+
uiSystem.drawRect(this.pos, this.size, color, this.lineWidth, lineColor, this.cornerRadius, this.gradientColor);
|
|
8094
8442
|
}
|
|
8095
8443
|
|
|
8096
8444
|
/** Special update when object is not visible */
|
|
@@ -8117,6 +8465,9 @@ class UIObject
|
|
|
8117
8465
|
/** @return {boolean} - Is the mouse held onto this element */
|
|
8118
8466
|
isActiveObject() { return uiSystem.activeObject === this; }
|
|
8119
8467
|
|
|
8468
|
+
/** Called each frame when object updates */
|
|
8469
|
+
onUpdate() {}
|
|
8470
|
+
|
|
8120
8471
|
/** Called when the mouse enters the object */
|
|
8121
8472
|
onEnter() {}
|
|
8122
8473
|
|
|
@@ -8171,8 +8522,9 @@ class UIText extends UIObject
|
|
|
8171
8522
|
}
|
|
8172
8523
|
render()
|
|
8173
8524
|
{
|
|
8525
|
+
// only render the text
|
|
8174
8526
|
const textSize = this.getTextSize();
|
|
8175
|
-
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle);
|
|
8527
|
+
uiSystem.drawText(this.text, this.pos, textSize, this.textColor, this.lineWidth, this.lineColor, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8176
8528
|
}
|
|
8177
8529
|
}
|
|
8178
8530
|
|
|
@@ -8248,7 +8600,7 @@ class UIButton extends UIObject
|
|
|
8248
8600
|
// draw the text scaled to fit
|
|
8249
8601
|
const textSize = this.getTextSize();
|
|
8250
8602
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8251
|
-
this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
|
|
8603
|
+
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8252
8604
|
}
|
|
8253
8605
|
}
|
|
8254
8606
|
|
|
@@ -8302,7 +8654,7 @@ class UICheckbox extends UIObject
|
|
|
8302
8654
|
const textSize = this.getTextSize();
|
|
8303
8655
|
const pos = this.pos.add(vec2(this.size.x,0));
|
|
8304
8656
|
uiSystem.drawText(this.text, pos, textSize,
|
|
8305
|
-
this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false);
|
|
8657
|
+
this.textColor, 0, undefined, 'left', this.font, this.fontStyle, false, this.textShadow);
|
|
8306
8658
|
}
|
|
8307
8659
|
}
|
|
8308
8660
|
|
|
@@ -8357,9 +8709,11 @@ class UIScrollbar extends UIObject
|
|
|
8357
8709
|
const p1 = centerPos - handleWidth/2;
|
|
8358
8710
|
const p2 = centerPos + handleWidth/2;
|
|
8359
8711
|
const oldValue = this.value;
|
|
8712
|
+
|
|
8713
|
+
const p = uiSystem.screenToNative(mousePosScreen);
|
|
8360
8714
|
this.value = isHorizontal ?
|
|
8361
|
-
percent(
|
|
8362
|
-
percent(
|
|
8715
|
+
percent(p.x, p1, p2) :
|
|
8716
|
+
percent(p.y, p2, p1);
|
|
8363
8717
|
this.value === oldValue || this.onChange();
|
|
8364
8718
|
}
|
|
8365
8719
|
}
|
|
@@ -8381,20 +8735,20 @@ class UIScrollbar extends UIObject
|
|
|
8381
8735
|
vec2(lerp(p1, p2, this.value), this.pos.y) :
|
|
8382
8736
|
vec2(this.pos.x, lerp(p2, p1, this.value))
|
|
8383
8737
|
const handleColor = this.disabled ? this.disabledColor : this.handleColor;
|
|
8384
|
-
uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius);
|
|
8738
|
+
uiSystem.drawRect(handlePos, vec2(handleSize), handleColor, this.lineWidth, this.lineColor, this.cornerRadius, this.gradientColor);
|
|
8385
8739
|
|
|
8386
8740
|
// draw the text scaled to fit on the scrollbar
|
|
8387
8741
|
const textSize = this.getTextSize();
|
|
8388
8742
|
uiSystem.drawText(this.text, this.pos, textSize,
|
|
8389
|
-
this.textColor, 0, undefined, this.align, this.font, this.fontStyle);
|
|
8743
|
+
this.textColor, 0, undefined, this.align, this.font, this.fontStyle, true, this.textShadow);
|
|
8390
8744
|
}
|
|
8391
8745
|
}
|
|
8392
8746
|
/**
|
|
8393
8747
|
* LittleJS Box2D Physics Plugin
|
|
8394
8748
|
* - Box2dObject extends EngineObject with Box2D physics
|
|
8395
|
-
* - Call box2dInit()
|
|
8749
|
+
* - Call box2dInit() to enable
|
|
8396
8750
|
* - You will also need to include box2d.wasm.js
|
|
8397
|
-
* - Uses a super fast web assembly port of Box2D
|
|
8751
|
+
* - Uses a super fast web assembly port of Box2D v2.3.1
|
|
8398
8752
|
* - More info: https://github.com/kripken/box2d.js
|
|
8399
8753
|
* - Functions to create polygon, circle, and edge shapes
|
|
8400
8754
|
* - Contact begin and end callbacks
|
|
@@ -8424,9 +8778,9 @@ function box2dSetDebug(enable) { box2dDebug = enable; }
|
|
|
8424
8778
|
///////////////////////////////////////////////////////////////////////////////
|
|
8425
8779
|
/**
|
|
8426
8780
|
* Box2D Object - extend with your own custom physics objects
|
|
8427
|
-
* - A LittleJS object with Box2D physics
|
|
8428
|
-
* - Each object has a Box2D body which can have multiple fixtures and joints
|
|
8781
|
+
* - A LittleJS object with Box2D physics, dynamic by default
|
|
8429
8782
|
* - Provides interface for Box2D body and fixture functions
|
|
8783
|
+
* - Each object can have multiple fixtures and joints
|
|
8430
8784
|
* @extends EngineObject
|
|
8431
8785
|
* @memberof Box2D
|
|
8432
8786
|
*/
|
|
@@ -8440,7 +8794,7 @@ class Box2dObject extends EngineObject
|
|
|
8440
8794
|
* @param {Color} [color]
|
|
8441
8795
|
* @param {number} [bodyType]
|
|
8442
8796
|
* @param {number} [renderOrder] */
|
|
8443
|
-
constructor(pos, size, tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
|
|
8797
|
+
constructor(pos=vec2(), size=vec2(), tileInfo, angle=0, color, bodyType=box2d.bodyTypeDynamic, renderOrder=0)
|
|
8444
8798
|
{
|
|
8445
8799
|
super(pos, size, tileInfo, angle, color, renderOrder);
|
|
8446
8800
|
|
|
@@ -8452,24 +8806,27 @@ class Box2dObject extends EngineObject
|
|
|
8452
8806
|
this.body = box2d.world.CreateBody(bodyDef);
|
|
8453
8807
|
this.body.object = this;
|
|
8454
8808
|
this.lineColor = BLACK;
|
|
8809
|
+
box2d.objects.push(this);
|
|
8810
|
+
|
|
8811
|
+
// edge lists and loops for drawing
|
|
8812
|
+
this.edgeLists = [];
|
|
8813
|
+
this.edgeLoops = [];
|
|
8455
8814
|
}
|
|
8456
8815
|
|
|
8457
8816
|
/** Destroy this object and its physics body */
|
|
8458
8817
|
destroy()
|
|
8459
8818
|
{
|
|
8819
|
+
if (this.destroyed)
|
|
8820
|
+
return;
|
|
8821
|
+
|
|
8460
8822
|
// destroy physics body, fixtures, and joints
|
|
8461
|
-
this.body
|
|
8462
|
-
this.body
|
|
8823
|
+
ASSERT(this.body, 'Box2dObject has no body to destroy');
|
|
8824
|
+
box2d.world.DestroyBody(this.body);
|
|
8463
8825
|
super.destroy();
|
|
8464
8826
|
}
|
|
8465
8827
|
|
|
8466
|
-
/**
|
|
8467
|
-
|
|
8468
|
-
{
|
|
8469
|
-
// use box2d physics update instead of normal engine update
|
|
8470
|
-
this.pos = box2d.vec2From(this.body.GetPosition());
|
|
8471
|
-
this.angle = -this.body.GetAngle();
|
|
8472
|
-
}
|
|
8828
|
+
/** Box2d objects updated with Box2d world step */
|
|
8829
|
+
updatePhysics() {}
|
|
8473
8830
|
|
|
8474
8831
|
/** Render the object, uses box2d drawing if no tile info exists */
|
|
8475
8832
|
render()
|
|
@@ -8495,10 +8852,23 @@ class Box2dObject extends EngineObject
|
|
|
8495
8852
|
* @param {Color} [lineColor]
|
|
8496
8853
|
* @param {number} [lineWidth]
|
|
8497
8854
|
* @param {CanvasRenderingContext2D} [context] */
|
|
8498
|
-
drawFixtures(color=WHITE, lineColor, lineWidth=.1, context)
|
|
8855
|
+
drawFixtures(color=WHITE, lineColor=BLACK, lineWidth=.1, context)
|
|
8499
8856
|
{
|
|
8500
|
-
|
|
8501
|
-
|
|
8857
|
+
// draw non-edge fixtures
|
|
8858
|
+
this.getFixtureList().forEach((fixture)=>
|
|
8859
|
+
{
|
|
8860
|
+
const shape = box2d.castObjectType(fixture.GetShape());
|
|
8861
|
+
if (shape.GetType() !== box2d.instance.b2Shape.e_edge)
|
|
8862
|
+
{
|
|
8863
|
+
box2d.drawFixture(fixture, this.pos, this.angle, color, lineColor, lineWidth, context);
|
|
8864
|
+
}
|
|
8865
|
+
});
|
|
8866
|
+
|
|
8867
|
+
// draw edges using a single draw line for better connections
|
|
8868
|
+
this.edgeLists.forEach(points=>
|
|
8869
|
+
drawLineList(points, lineWidth, lineColor, false, this.pos, this.angle));
|
|
8870
|
+
this.edgeLoops.forEach(points=>
|
|
8871
|
+
drawLineList(points, lineWidth, lineColor, true, this.pos, this.angle));
|
|
8502
8872
|
}
|
|
8503
8873
|
|
|
8504
8874
|
///////////////////////////////////////////////////////////////////////////////
|
|
@@ -8523,6 +8893,10 @@ class Box2dObject extends EngineObject
|
|
|
8523
8893
|
* @param {boolean} [isSensor] */
|
|
8524
8894
|
addShape(shape, density=1, friction=.2, restitution=0, isSensor=false)
|
|
8525
8895
|
{
|
|
8896
|
+
ASSERT(isNumber(density), 'density must be a number');
|
|
8897
|
+
ASSERT(isNumber(friction), 'friction must be a number');
|
|
8898
|
+
ASSERT(isNumber(restitution), 'restitution must be a number');
|
|
8899
|
+
|
|
8526
8900
|
const fd = new box2d.instance.b2FixtureDef();
|
|
8527
8901
|
fd.set_shape(shape);
|
|
8528
8902
|
fd.set_density(density);
|
|
@@ -8542,6 +8916,11 @@ class Box2dObject extends EngineObject
|
|
|
8542
8916
|
* @param {boolean} [isSensor] */
|
|
8543
8917
|
addBox(size=vec2(1), offset=vec2(), angle=0, density, friction, restitution, isSensor)
|
|
8544
8918
|
{
|
|
8919
|
+
ASSERT(isVector2(size), 'size must be a Vector2');
|
|
8920
|
+
ASSERT(size.x > 0 && size.y > 0, 'size must be positive');
|
|
8921
|
+
ASSERT(isVector2(offset), 'offset must be a Vector2');
|
|
8922
|
+
ASSERT(isNumber(angle), 'angle must be a number');
|
|
8923
|
+
|
|
8545
8924
|
const shape = new box2d.instance.b2PolygonShape();
|
|
8546
8925
|
shape.SetAsBox(size.x/2, size.y/2, box2d.vec2dTo(offset), angle);
|
|
8547
8926
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
@@ -8555,6 +8934,8 @@ class Box2dObject extends EngineObject
|
|
|
8555
8934
|
* @param {boolean} [isSensor] */
|
|
8556
8935
|
addPoly(points, density, friction, restitution, isSensor)
|
|
8557
8936
|
{
|
|
8937
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
8938
|
+
|
|
8558
8939
|
function box2dCreatePolygonShape(points)
|
|
8559
8940
|
{
|
|
8560
8941
|
function box2dCreatePointList(points)
|
|
@@ -8590,6 +8971,9 @@ class Box2dObject extends EngineObject
|
|
|
8590
8971
|
* @param {boolean} [isSensor] */
|
|
8591
8972
|
addRegularPoly(diameter=1, sides=8, density, friction, restitution, isSensor)
|
|
8592
8973
|
{
|
|
8974
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
8975
|
+
ASSERT(isNumber(sides) && sides>2, 'sides must be a positive number greater than 2');
|
|
8976
|
+
|
|
8593
8977
|
const points = [];
|
|
8594
8978
|
const radius = diameter/2;
|
|
8595
8979
|
for (let i=sides; i--;)
|
|
@@ -8605,6 +8989,8 @@ class Box2dObject extends EngineObject
|
|
|
8605
8989
|
* @param {boolean} [isSensor] */
|
|
8606
8990
|
addRandomPoly(diameter=1, density, friction, restitution, isSensor)
|
|
8607
8991
|
{
|
|
8992
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
8993
|
+
|
|
8608
8994
|
const sides = randInt(3, 9);
|
|
8609
8995
|
const points = [];
|
|
8610
8996
|
const radius = diameter/2;
|
|
@@ -8622,6 +9008,9 @@ class Box2dObject extends EngineObject
|
|
|
8622
9008
|
* @param {boolean} [isSensor] */
|
|
8623
9009
|
addCircle(diameter=1, offset=vec2(), density, friction, restitution, isSensor)
|
|
8624
9010
|
{
|
|
9011
|
+
ASSERT(isNumber(diameter) && diameter>0, 'diameter must be a positive number');
|
|
9012
|
+
ASSERT(isVector2(offset), 'offset must be a Vector2');
|
|
9013
|
+
|
|
8625
9014
|
const shape = new box2d.instance.b2CircleShape();
|
|
8626
9015
|
shape.set_m_p(box2d.vec2dTo(offset));
|
|
8627
9016
|
shape.set_m_radius(diameter/2);
|
|
@@ -8637,53 +9026,63 @@ class Box2dObject extends EngineObject
|
|
|
8637
9026
|
* @param {boolean} [isSensor] */
|
|
8638
9027
|
addEdge(point1, point2, density, friction, restitution, isSensor)
|
|
8639
9028
|
{
|
|
9029
|
+
ASSERT(isVector2(point1), 'point1 must be a Vector2');
|
|
9030
|
+
ASSERT(isVector2(point2), 'point2 must be a Vector2');
|
|
9031
|
+
|
|
8640
9032
|
const shape = new box2d.instance.b2EdgeShape();
|
|
8641
9033
|
shape.Set(box2d.vec2dTo(point1), box2d.vec2dTo(point2));
|
|
8642
9034
|
return this.addShape(shape, density, friction, restitution, isSensor);
|
|
8643
9035
|
}
|
|
8644
9036
|
|
|
8645
|
-
/** Add an edge
|
|
9037
|
+
/** Add an edge list to the body
|
|
8646
9038
|
* @param {Array<Vector2>} points
|
|
8647
9039
|
* @param {number} [density]
|
|
8648
9040
|
* @param {number} [friction]
|
|
8649
9041
|
* @param {number} [restitution]
|
|
8650
9042
|
* @param {boolean} [isSensor] */
|
|
8651
|
-
|
|
9043
|
+
addEdgeList(points, density, friction, restitution, isSensor)
|
|
8652
9044
|
{
|
|
8653
|
-
|
|
8654
|
-
const
|
|
8655
|
-
for (let i=0; i<points.length; ++i)
|
|
9045
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9046
|
+
const fixtures = [], edgePoints = [];
|
|
9047
|
+
for (let i=0; i<points.length-1; ++i)
|
|
8656
9048
|
{
|
|
8657
9049
|
const shape = new box2d.instance.b2EdgeShape();
|
|
8658
|
-
shape.set_m_vertex0(box2d.vec2dTo(
|
|
8659
|
-
shape.set_m_vertex1(box2d.vec2dTo(
|
|
8660
|
-
shape.set_m_vertex2(box2d.vec2dTo(
|
|
8661
|
-
shape.set_m_vertex3(box2d.vec2dTo(
|
|
9050
|
+
points[i-1] && shape.set_m_vertex0(box2d.vec2dTo(points[i-1]));
|
|
9051
|
+
points[i+0] && shape.set_m_vertex1(box2d.vec2dTo(points[i+0]));
|
|
9052
|
+
points[i+1] && shape.set_m_vertex2(box2d.vec2dTo(points[i+1]));
|
|
9053
|
+
points[i+2] && shape.set_m_vertex3(box2d.vec2dTo(points[i+2]));
|
|
8662
9054
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
8663
9055
|
fixtures.push(f);
|
|
9056
|
+
edgePoints.push(points[i].copy());
|
|
8664
9057
|
}
|
|
9058
|
+
edgePoints.push(points[points.length-1].copy());
|
|
9059
|
+
this.edgeLists.push(edgePoints);
|
|
8665
9060
|
return fixtures;
|
|
8666
9061
|
}
|
|
8667
9062
|
|
|
8668
|
-
/** Add an edge
|
|
9063
|
+
/** Add an edge loop to the body, an edge loop connects the end points
|
|
8669
9064
|
* @param {Array<Vector2>} points
|
|
8670
9065
|
* @param {number} [density]
|
|
8671
9066
|
* @param {number} [friction]
|
|
8672
9067
|
* @param {number} [restitution]
|
|
8673
9068
|
* @param {boolean} [isSensor] */
|
|
8674
|
-
|
|
9069
|
+
addEdgeLoop(points, density, friction, restitution, isSensor)
|
|
8675
9070
|
{
|
|
8676
|
-
|
|
8677
|
-
|
|
9071
|
+
ASSERT(isArray(points), 'points must be an array');
|
|
9072
|
+
const fixtures = [], edgePoints = [];
|
|
9073
|
+
const getPoint = i=> points[mod(i,points.length)];
|
|
9074
|
+
for (let i=0; i<points.length; ++i)
|
|
8678
9075
|
{
|
|
8679
9076
|
const shape = new box2d.instance.b2EdgeShape();
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
9077
|
+
shape.set_m_vertex0(box2d.vec2dTo(getPoint(i-1)));
|
|
9078
|
+
shape.set_m_vertex1(box2d.vec2dTo(getPoint(i+0)));
|
|
9079
|
+
shape.set_m_vertex2(box2d.vec2dTo(getPoint(i+1)));
|
|
9080
|
+
shape.set_m_vertex3(box2d.vec2dTo(getPoint(i+2)));
|
|
8684
9081
|
const f = this.addShape(shape, density, friction, restitution, isSensor);
|
|
8685
9082
|
fixtures.push(f);
|
|
9083
|
+
i < points.length && edgePoints.push(points[i].copy());
|
|
8686
9084
|
}
|
|
9085
|
+
this.edgeLoops.push(edgePoints);
|
|
8687
9086
|
return fixtures;
|
|
8688
9087
|
}
|
|
8689
9088
|
|
|
@@ -8718,6 +9117,10 @@ class Box2dObject extends EngineObject
|
|
|
8718
9117
|
* @return {number} */
|
|
8719
9118
|
getBodyType() { return this.body.GetType(); }
|
|
8720
9119
|
|
|
9120
|
+
/** Get the speed of this object
|
|
9121
|
+
* @return {number} */
|
|
9122
|
+
getSpeed() { return this.getLinearVelocity().length(); }
|
|
9123
|
+
|
|
8721
9124
|
///////////////////////////////////////////////////////////////////////////////
|
|
8722
9125
|
// physics set functions
|
|
8723
9126
|
|
|
@@ -8733,33 +9136,40 @@ class Box2dObject extends EngineObject
|
|
|
8733
9136
|
|
|
8734
9137
|
/** Sets the position
|
|
8735
9138
|
* @param {Vector2} pos */
|
|
8736
|
-
setPosition(pos)
|
|
9139
|
+
setPosition(pos)
|
|
9140
|
+
{ this.setTransform(pos, this.body.GetAngle()); }
|
|
8737
9141
|
|
|
8738
9142
|
/** Sets the angle
|
|
8739
9143
|
* @param {number} angle */
|
|
8740
|
-
setAngle(angle)
|
|
9144
|
+
setAngle(angle)
|
|
9145
|
+
{ this.setTransform(box2d.vec2From(this.body.GetPosition()), -angle); }
|
|
8741
9146
|
|
|
8742
9147
|
/** Sets the linear velocity
|
|
8743
9148
|
* @param {Vector2} velocity */
|
|
8744
|
-
setLinearVelocity(velocity)
|
|
9149
|
+
setLinearVelocity(velocity)
|
|
9150
|
+
{ this.body.SetLinearVelocity(box2d.vec2dTo(velocity)); }
|
|
8745
9151
|
|
|
8746
9152
|
/** Sets the angular velocity
|
|
8747
9153
|
* @param {number} angularVelocity */
|
|
8748
|
-
setAngularVelocity(angularVelocity)
|
|
9154
|
+
setAngularVelocity(angularVelocity)
|
|
9155
|
+
{ this.body.SetAngularVelocity(angularVelocity); }
|
|
8749
9156
|
|
|
8750
9157
|
/** Sets the linear damping
|
|
8751
9158
|
* @param {number} damping */
|
|
8752
|
-
setLinearDamping(damping)
|
|
9159
|
+
setLinearDamping(damping)
|
|
9160
|
+
{ this.body.SetLinearDamping(damping); }
|
|
8753
9161
|
|
|
8754
9162
|
/** Sets the angular damping
|
|
8755
9163
|
* @param {number} damping */
|
|
8756
|
-
setAngularDamping(damping)
|
|
9164
|
+
setAngularDamping(damping)
|
|
9165
|
+
{ this.body.SetAngularDamping(damping); }
|
|
8757
9166
|
|
|
8758
9167
|
/** Sets the gravity scale
|
|
8759
9168
|
* @param {number} [scale] */
|
|
8760
|
-
setGravityScale(scale=1)
|
|
9169
|
+
setGravityScale(scale=1)
|
|
9170
|
+
{ this.body.SetGravityScale(this.gravityScale = scale); }
|
|
8761
9171
|
|
|
8762
|
-
/** Should
|
|
9172
|
+
/** Should be like a bullet for continuous collision detection?
|
|
8763
9173
|
* @param {boolean} [isBullet] */
|
|
8764
9174
|
setBullet(isBullet=true) { this.body.SetBullet(isBullet); }
|
|
8765
9175
|
|
|
@@ -8773,11 +9183,13 @@ class Box2dObject extends EngineObject
|
|
|
8773
9183
|
|
|
8774
9184
|
/** Set whether the body is allowed to sleep
|
|
8775
9185
|
* @param {boolean} [isAllowed] */
|
|
8776
|
-
setSleepingAllowed(isAllowed=true)
|
|
9186
|
+
setSleepingAllowed(isAllowed=true)
|
|
9187
|
+
{ this.body.SetSleepingAllowed(isAllowed); }
|
|
8777
9188
|
|
|
8778
9189
|
/** Set whether the body can rotate
|
|
8779
9190
|
* @param {boolean} [isFixed] */
|
|
8780
|
-
setFixedRotation(isFixed=true)
|
|
9191
|
+
setFixedRotation(isFixed=true)
|
|
9192
|
+
{ this.body.SetFixedRotation(isFixed); }
|
|
8781
9193
|
|
|
8782
9194
|
/** Set the center of mass of the body
|
|
8783
9195
|
* @param {Vector2} center */
|
|
@@ -8789,10 +9201,11 @@ class Box2dObject extends EngineObject
|
|
|
8789
9201
|
|
|
8790
9202
|
/** Set the moment of inertia of the body
|
|
8791
9203
|
* @param {number} momentOfInertia */
|
|
8792
|
-
setMomentOfInertia(momentOfInertia)
|
|
9204
|
+
setMomentOfInertia(momentOfInertia)
|
|
9205
|
+
{ this.setMassData(undefined, undefined, momentOfInertia) }
|
|
8793
9206
|
|
|
8794
9207
|
/** Reset the mass, center of mass, and moment */
|
|
8795
|
-
resetMassData()
|
|
9208
|
+
resetMassData() { this.body.ResetMassData(); }
|
|
8796
9209
|
|
|
8797
9210
|
/** Set the mass data of the body
|
|
8798
9211
|
* @param {Vector2} [localCenter]
|
|
@@ -9434,6 +9847,50 @@ class Box2dPrismaticJoint extends Box2dJoint
|
|
|
9434
9847
|
getMotorForce(time) { return this.box2dJoint.GetMotorForce(1/time); }
|
|
9435
9848
|
}
|
|
9436
9849
|
|
|
9850
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9851
|
+
/**
|
|
9852
|
+
* Box2D Static Object - Box2d with a static physics body
|
|
9853
|
+
* @extends Box2dObject
|
|
9854
|
+
* @memberof Box2D
|
|
9855
|
+
*/
|
|
9856
|
+
class Box2dStaticObject extends Box2dObject
|
|
9857
|
+
{
|
|
9858
|
+
/** Create a LittleJS object with Box2d physics
|
|
9859
|
+
* @param {Vector2} [pos]
|
|
9860
|
+
* @param {Vector2} [size]
|
|
9861
|
+
* @param {TileInfo} [tileInfo]
|
|
9862
|
+
* @param {number} [angle]
|
|
9863
|
+
* @param {Color} [color]
|
|
9864
|
+
* @param {number} [renderOrder] */
|
|
9865
|
+
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
|
|
9866
|
+
{
|
|
9867
|
+
const bodyType = box2d.bodyTypeStatic;
|
|
9868
|
+
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
|
|
9869
|
+
}
|
|
9870
|
+
}
|
|
9871
|
+
|
|
9872
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
9873
|
+
/**
|
|
9874
|
+
* Box2D Kiematic Object - Box2d with a kinematic physics body
|
|
9875
|
+
* @extends Box2dObject
|
|
9876
|
+
* @memberof Box2D
|
|
9877
|
+
*/
|
|
9878
|
+
class Box2dKiematicObject extends Box2dObject
|
|
9879
|
+
{
|
|
9880
|
+
/** Create a LittleJS object with Box2d physics
|
|
9881
|
+
* @param {Vector2} [pos]
|
|
9882
|
+
* @param {Vector2} [size]
|
|
9883
|
+
* @param {TileInfo} [tileInfo]
|
|
9884
|
+
* @param {number} [angle]
|
|
9885
|
+
* @param {Color} [color]
|
|
9886
|
+
* @param {number} [renderOrder] */
|
|
9887
|
+
constructor(pos, size, tileInfo, angle=0, color, renderOrder=0)
|
|
9888
|
+
{
|
|
9889
|
+
const bodyType = box2d.bodyTypeKinematic;
|
|
9890
|
+
super(pos, size, tileInfo, angle, color, bodyType, renderOrder);
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
|
|
9437
9894
|
///////////////////////////////////////////////////////////////////////////////
|
|
9438
9895
|
/**
|
|
9439
9896
|
* Box2D Wheel Joint
|
|
@@ -9796,6 +10253,7 @@ class Box2dPlugin
|
|
|
9796
10253
|
box2d = this;
|
|
9797
10254
|
this.instance = instance;
|
|
9798
10255
|
this.world = new box2d.instance.b2World();
|
|
10256
|
+
this.objects = [];
|
|
9799
10257
|
|
|
9800
10258
|
/** @property {number} - Velocity iterations per update*/
|
|
9801
10259
|
this.velocityIterations = 8;
|
|
@@ -10038,9 +10496,10 @@ class Box2dPlugin
|
|
|
10038
10496
|
|
|
10039
10497
|
/** converts a box2d vec2 pointer to a Vector2
|
|
10040
10498
|
* @param {Object} v */
|
|
10041
|
-
vec2FromPointer(
|
|
10499
|
+
vec2FromPointer(vp)
|
|
10042
10500
|
{
|
|
10043
|
-
|
|
10501
|
+
const v = box2d.instance.wrapPointer(vp, box2d.instance.b2Vec2);
|
|
10502
|
+
return box2d.vec2From(v);
|
|
10044
10503
|
}
|
|
10045
10504
|
|
|
10046
10505
|
/** converts a Vector2 to a box2 vec2
|
|
@@ -10114,8 +10573,23 @@ async function box2dInit()
|
|
|
10114
10573
|
// add the box2d plugin to the engine
|
|
10115
10574
|
function box2dUpdate()
|
|
10116
10575
|
{
|
|
10117
|
-
if (
|
|
10118
|
-
|
|
10576
|
+
if (paused)
|
|
10577
|
+
return;
|
|
10578
|
+
|
|
10579
|
+
box2d.step();
|
|
10580
|
+
|
|
10581
|
+
// remove destroyed objects
|
|
10582
|
+
box2d.objects = box2d.objects.filter(o=>!o.destroyed);
|
|
10583
|
+
|
|
10584
|
+
// copy box2d physics results to engine objects
|
|
10585
|
+
for (const o of box2d.objects)
|
|
10586
|
+
{
|
|
10587
|
+
if (o.body)
|
|
10588
|
+
{
|
|
10589
|
+
o.pos = box2d.vec2From(o.body.GetPosition());
|
|
10590
|
+
o.angle = -o.body.GetAngle();
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10119
10593
|
}
|
|
10120
10594
|
function box2dRender()
|
|
10121
10595
|
{
|