littlejsengine 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/FAQ.md +3 -2
- package/README.md +4 -3
- package/REFERENCE.md +47 -3
- package/dist/littlejs.d.ts +204 -19
- package/dist/littlejs.esm.js +855 -113
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +854 -113
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +834 -107
- package/package.json +1 -1
- package/plugins/lightSystem.js +84 -2
- package/plugins/newgrounds.js +92 -0
- package/plugins/parallax.js +3 -1
- package/plugins/postProcess.js +5 -1
- package/plugins/render3d.js +14 -4
- package/plugins/render3dEditor.js +20 -6
- package/plugins/render3dExtras.js +1 -0
- package/plugins/wavedash.js +235 -4
- package/src/engine.js +39 -7
- package/src/engineDraw.js +3 -0
- package/src/engineExport.js +1 -0
- package/src/engineInput.js +7 -6
- package/src/engineObject.js +244 -65
- package/src/engineParticles.js +8 -3
- package/src/engineTileLayer.js +95 -10
- package/src/engineWebGL.js +4 -4
package/src/engineObject.js
CHANGED
|
@@ -11,6 +11,110 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
+
// a one way solid lets a box at from pass, when it was not wholly on the solid's far side, the way it is passed
|
|
15
|
+
// through; both are where they were before this frame's moves, so it does not matter which updated first
|
|
16
|
+
function engineObjectOneWayPass(solid, solidPos, from, fromSize)
|
|
17
|
+
{
|
|
18
|
+
const way = solid.oneWay, size = solid.size, epsilon = 1e-3;
|
|
19
|
+
return way.x > .5 ? from.x - fromSize.x/2 < solidPos.x + size.x/2 - epsilon :
|
|
20
|
+
way.x < -.5 ? from.x + fromSize.x/2 > solidPos.x - size.x/2 + epsilon :
|
|
21
|
+
way.y > .5 ? from.y - fromSize.y/2 < solidPos.y + size.y/2 - epsilon :
|
|
22
|
+
from.y + fromSize.y/2 > solidPos.y - size.y/2 + epsilon;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// where a move from start to end goes into the cell at its corner, the first point of it inside the cell's box, and
|
|
26
|
+
// whether it goes in through a side, x, or the top or bottom, the axis whose slab it enters last; one starting on a
|
|
27
|
+
// face is on that face, as its entry there is 0 and the other's is before it
|
|
28
|
+
function engineObjectRayEntry(start, end, cell)
|
|
29
|
+
{
|
|
30
|
+
const dx = end.x - start.x, dy = end.y - start.y;
|
|
31
|
+
const tx = dx ? min((cell.x - start.x) / dx, (cell.x + 1 - start.x) / dx) : -Infinity;
|
|
32
|
+
const ty = dy ? min((cell.y - start.y) / dy, (cell.y + 1 - start.y) / dy) : -Infinity;
|
|
33
|
+
const t = clamp(max(tx, ty));
|
|
34
|
+
return {pos: vec2(start.x + dx*t, start.y + dy*t), side: tx >= ty};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// a bullet's move from one point to another through the tiles: the first tile its collideWithTile takes, nearest
|
|
38
|
+
// first across the solid layers, with where the move goes into it, the face's normal and the layer; undefined for none
|
|
39
|
+
// - the cell it starts in, a hair back along its move so one stopped on a tile's face going into it is outside, is not
|
|
40
|
+
// tested, so it moves out of a tile it is inside, as a box that starts in one does, and a tile after it still stops it
|
|
41
|
+
// - collideWithTile is asked with the bullet where the move goes into the tile
|
|
42
|
+
// - only the stretches of the move near a solid layer are walked, nearest first and joined where they meet, so a long
|
|
43
|
+
// move through empty space, or between layers far apart, costs nothing; the layers are whole numbers, so the cells
|
|
44
|
+
// are the same as over the whole move
|
|
45
|
+
/** @param {EngineObject} object
|
|
46
|
+
* @param {Vector2} from
|
|
47
|
+
* @param {Vector2} to
|
|
48
|
+
* @return {{pos: Vector2, normal: Vector2, layer: TileCollisionLayer}|undefined} */
|
|
49
|
+
function engineObjectBulletSweep(object, from, to)
|
|
50
|
+
{
|
|
51
|
+
const move = to.subtract(from), length = move.length();
|
|
52
|
+
if (!length) return;
|
|
53
|
+
const back = from.subtract(move.scale(1e-6 / length)), startX = floor(back.x), startY = floor(back.y);
|
|
54
|
+
|
|
55
|
+
/** @type {{pos: Vector2, normal: Vector2, layer: TileCollisionLayer}|undefined} */
|
|
56
|
+
let hit;
|
|
57
|
+
const testCell = (cell)=>
|
|
58
|
+
{
|
|
59
|
+
// a bullet its collideWithTile destroyed is asked about nothing more
|
|
60
|
+
if (cell.x === startX && cell.y === startY || object.destroyed) return false;
|
|
61
|
+
for (const layer of tileCollisionLayers)
|
|
62
|
+
{
|
|
63
|
+
if (!layer.isSolid) continue;
|
|
64
|
+
const x = cell.x - layer.pos.x, y = cell.y - layer.pos.y;
|
|
65
|
+
if (x < 0 || y < 0 || x >= layer.size.x || y >= layer.size.y) continue;
|
|
66
|
+
const data = layer.collisionData[y*layer.size.x + x];
|
|
67
|
+
if (!data || tileCollisionOneWayPass(layer, x, y, from.x, from.y)) continue;
|
|
68
|
+
const tilePos = vec2(cell.x, cell.y), entry = engineObjectRayEntry(from, to, tilePos);
|
|
69
|
+
object.pos = entry.pos;
|
|
70
|
+
if (!object.collideWithTile(data, tilePos))
|
|
71
|
+
{
|
|
72
|
+
if (object.destroyed) return false; // nor the other layers' tiles in this cell
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const normal = entry.side ? vec2(-sign(move.x), 0) : vec2(0, -sign(move.y));
|
|
76
|
+
hit = {pos: entry.pos, normal, layer};
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const stretches = [];
|
|
83
|
+
for (const layer of tileCollisionLayers)
|
|
84
|
+
{
|
|
85
|
+
if (!layer.isSolid) continue;
|
|
86
|
+
tileCollisionAssertWhole(layer);
|
|
87
|
+
let t0 = 0, t1 = 1;
|
|
88
|
+
for (const [p, d, a, b] of [[from.x, move.x, layer.pos.x - 1, layer.pos.x + layer.size.x + 1],
|
|
89
|
+
[from.y, move.y, layer.pos.y - 1, layer.pos.y + layer.size.y + 1]])
|
|
90
|
+
{
|
|
91
|
+
if (!d)
|
|
92
|
+
{
|
|
93
|
+
if (p < a || p > b) t0 = 2;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const u = (a - p) / d, v = (b - p) / d;
|
|
97
|
+
t0 = max(t0, min(u, v)), t1 = min(t1, max(u, v));
|
|
98
|
+
}
|
|
99
|
+
t0 < t1 && stretches.push([t0, t1]);
|
|
100
|
+
}
|
|
101
|
+
stretches.sort((a, b)=> a[0] - b[0]);
|
|
102
|
+
const at = (t)=> t <= 0 ? from : t >= 1 ? to : from.add(move.scale(t));
|
|
103
|
+
for (let i = 0; i < stretches.length && !hit;)
|
|
104
|
+
{
|
|
105
|
+
const [t0] = stretches[i];
|
|
106
|
+
let t1 = stretches[i][1];
|
|
107
|
+
while (++i < stretches.length && stretches[i][0] <= t1)
|
|
108
|
+
t1 = max(t1, stretches[i][1]);
|
|
109
|
+
lineTest(at(t0), at(t1), testCell);
|
|
110
|
+
}
|
|
111
|
+
return hit;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// where an object was before this frame's moves, for a one way test: kept by the update when a one way solid is in
|
|
115
|
+
// it, or where it is when updatePhysics is called on its own
|
|
116
|
+
function engineObjectStartPos(o) { return engineObjectsOneWayStart?.get(o) || o.pos; }
|
|
117
|
+
|
|
14
118
|
/**
|
|
15
119
|
* LittleJS Object Base Object Class
|
|
16
120
|
* - Top level object class used by the engine
|
|
@@ -122,9 +226,27 @@ class EngineObject
|
|
|
122
226
|
this.children = [];
|
|
123
227
|
/** @property {boolean} - Limit object speed along x and y axis */
|
|
124
228
|
this.clampSpeed = true;
|
|
229
|
+
/** @property {boolean} - Move through the tiles as a point along a ray, so it can not pass through one at
|
|
230
|
+
* any speed, for small fast things like bullets: its speed is not held to objectMaxSpeed for the tiles,
|
|
231
|
+
* collideWithTile is asked with this.pos where the ray goes into the tile, and on a hit it stops at the
|
|
232
|
+
* surface, bounces off it by restitution and goes on with the rest of its move, so it slides along a floor
|
|
233
|
+
* - Its collision with solid objects is the usual one, so one with a size that collides with them is still held
|
|
234
|
+
* to objectMaxSpeed; one of no size is not, it never collides with them
|
|
235
|
+
* - this.pos is set for each tile asked about and put where the move ends after, so a change collideWithTile
|
|
236
|
+
* makes to it is not kept; move it after the physics, in update
|
|
237
|
+
* - Once destroyed, as by its own collideWithTile, it is asked about no more tiles that frame, where an object
|
|
238
|
+
* that is not a bullet still is */
|
|
239
|
+
this.isBullet = false;
|
|
125
240
|
/** @property {EngineObject|undefined} - Object we are standing on, if any
|
|
126
241
|
* @type {EngineObject|undefined} */
|
|
127
242
|
this.groundObject = undefined;
|
|
243
|
+
/** @property {Vector2|undefined} - Makes a solid object one way, like a platform jumped up through: up, down,
|
|
244
|
+
* left or right, the way others pass through it; it blocks only what was wholly on its far side before it
|
|
245
|
+
* moved, or stood on it, and the rest are not stopped or asked through collideWithObject; with no mass it
|
|
246
|
+
* moves before the other objects each frame, so a lift works the same at any render order; tiles are made one
|
|
247
|
+
* way by the layer's setOneWay
|
|
248
|
+
* @type {Vector2|undefined} */
|
|
249
|
+
this.oneWay = undefined;
|
|
128
250
|
|
|
129
251
|
// parent child system
|
|
130
252
|
/** @property {EngineObject|undefined} - Parent of object if in local space
|
|
@@ -212,7 +334,8 @@ class EngineObject
|
|
|
212
334
|
// limit max speed to prevent missing collisions, after gravity so no move is past it, only for what collides:
|
|
213
335
|
// with solids, or with tiles while it has a mass, which tile collision needs; anything else moves as fast as
|
|
214
336
|
// it is told
|
|
215
|
-
if (this.clampSpeed && enablePhysicsSolver &&
|
|
337
|
+
if (this.clampSpeed && enablePhysicsSolver &&
|
|
338
|
+
(this.collideSolidObjects && this.size.x && this.size.y || this.collideLevel && this.mass && !this.isBullet))
|
|
216
339
|
{
|
|
217
340
|
this.velocity.x = clamp(this.velocity.x, -objectMaxSpeed, objectMaxSpeed);
|
|
218
341
|
this.velocity.y = clamp(this.velocity.y, -objectMaxSpeed, objectMaxSpeed);
|
|
@@ -227,6 +350,7 @@ class EngineObject
|
|
|
227
350
|
// which way is down for this object, a negative gravityScale falls up and lands on ceilings
|
|
228
351
|
const gravityY = this.gravityScale < 0 ? -gravity.y : gravity.y;
|
|
229
352
|
const wasFalling = this.velocity.y < 0 && gravityY < 0 || this.velocity.y > 0 && gravityY > 0;
|
|
353
|
+
const wasOn = this.groundObject; // a one way solid it stood on still holds it
|
|
230
354
|
if (this.groundObject)
|
|
231
355
|
{
|
|
232
356
|
// apply friction in local space of ground object
|
|
@@ -253,6 +377,11 @@ class EngineObject
|
|
|
253
377
|
// check collision
|
|
254
378
|
if (!this.isOverlappingObject(o)) continue;
|
|
255
379
|
|
|
380
|
+
// a one way solid lets through what was not wholly on its far side before it moved, either way round
|
|
381
|
+
if (o.oneWay && wasOn !== o && engineObjectOneWayPass(o, engineObjectStartPos(o), oldPos, this.size))
|
|
382
|
+
continue;
|
|
383
|
+
if (this.oneWay && engineObjectOneWayPass(this, oldPos, engineObjectStartPos(o), o.size)) continue;
|
|
384
|
+
|
|
256
385
|
// each moving object checks its own contacts, so a pair the other one already asked about this frame
|
|
257
386
|
// is not asked twice: left overlapping, ignored or only nudged apart it is skipped, and one both said
|
|
258
387
|
// to resolve, pushed back together since, is resolved again
|
|
@@ -387,71 +516,121 @@ class EngineObject
|
|
|
387
516
|
}
|
|
388
517
|
if (this.collideLevel)
|
|
389
518
|
{
|
|
390
|
-
// check collision against tiles
|
|
391
|
-
const
|
|
392
|
-
|
|
519
|
+
// check collision against tiles, one way tiles go by where it was; put back after, a callback may throw
|
|
520
|
+
const fromObject = tileCollisionFromObject, fromPos = tileCollisionFromPos;
|
|
521
|
+
tileCollisionFromObject = this, tileCollisionFromPos = oldPos;
|
|
522
|
+
try
|
|
393
523
|
{
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
|
|
401
|
-
const restitution = max(this.restitution, hitLayer.restitution);
|
|
402
|
-
if (isBlockedX)
|
|
403
|
-
{
|
|
404
|
-
// a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
|
|
405
|
-
// down off a ceiling ledge when gravity points up; zero gravity counts as down
|
|
406
|
-
const epsilon = 1e-3;
|
|
407
|
-
const maxMove = .1;
|
|
408
|
-
const y = gravityY > 0 ?
|
|
409
|
-
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
|
|
410
|
-
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
|
|
411
|
-
if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
412
|
-
{
|
|
413
|
-
this.pos.y = y;
|
|
414
|
-
debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
524
|
+
this.isBullet ? this.updatePhysicsBullet(oldPos, gravityY, wasFalling) :
|
|
525
|
+
this.updatePhysicsTiles(oldPos, gravityY, wasFalling);
|
|
526
|
+
}
|
|
527
|
+
finally { tileCollisionFromObject = fromObject, tileCollisionFromPos = fromPos; }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
417
530
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
531
|
+
// a bullet's move from oldPos through the tiles, as a point along a ray, called by updatePhysics
|
|
532
|
+
/** @private */
|
|
533
|
+
updatePhysicsBullet(oldPos, gravityY, wasFalling)
|
|
534
|
+
{
|
|
535
|
+
// up to three legs: the move, then what is left of it after each hit, along the surface or bounced off it as
|
|
536
|
+
// its velocity is, so one rolling along the floor under gravity keeps going as a box does
|
|
537
|
+
let from = oldPos, to = this.pos.copy();
|
|
538
|
+
for (let leg = 0; leg < 3; ++leg)
|
|
539
|
+
{
|
|
540
|
+
const hit = engineObjectBulletSweep(this, from, to);
|
|
541
|
+
if (!hit)
|
|
542
|
+
{
|
|
543
|
+
this.pos = to;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// stop at the surface, a hair off it, more than a one way tile's margin, so going on along a ceiling a
|
|
548
|
+
// one way tile it rose through lets it on; bounce off it by the more bouncy of it and the layer
|
|
549
|
+
const {normal, layer} = hit, restitution = max(this.restitution, layer.restitution);
|
|
550
|
+
const stop = hit.pos.add(normal.scale(2e-3));
|
|
551
|
+
const into = this.velocity.dot(normal);
|
|
552
|
+
if (into < 0)
|
|
553
|
+
this.velocity = this.velocity.subtract(normal.scale(into * (1 + restitution)));
|
|
554
|
+
if (normal.y * gravityY < 0 && wasFalling) // landed, against its gravity
|
|
555
|
+
this.groundObject = layer;
|
|
556
|
+
this.pos = stop;
|
|
557
|
+
|
|
558
|
+
// the rest of the move from there, turned the same way; none once its callback destroyed it
|
|
559
|
+
const rest = to.subtract(hit.pos), restInto = rest.dot(normal);
|
|
560
|
+
const next = stop.add(restInto < 0 ? rest.subtract(normal.scale(restInto * (1 + restitution))) : rest);
|
|
561
|
+
if (this.destroyed || next.distanceSquared(stop) < 1e-12) break;
|
|
562
|
+
from = stop, to = next;
|
|
563
|
+
}
|
|
564
|
+
debugPhysics && debugPoint(this.pos, '#f00', undefined, undefined, false);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// resolve the tile collision of a move from oldPos, called by updatePhysics
|
|
568
|
+
/** @private */
|
|
569
|
+
updatePhysicsTiles(oldPos, gravityY, wasFalling)
|
|
570
|
+
{
|
|
571
|
+
const hitLayer = tileCollisionTest(this.pos, this.size, this);
|
|
572
|
+
if (hitLayer)
|
|
573
|
+
{
|
|
574
|
+
// if already was stuck in collision, don't do anything
|
|
575
|
+
// this should not happen unless something starts in collision
|
|
576
|
+
if (!tileCollisionTest(oldPos, this.size, this))
|
|
577
|
+
{
|
|
578
|
+
// test which side we bounced off (or both if a corner)
|
|
579
|
+
const isBlockedX = tileCollisionTest(vec2(this.pos.x, oldPos.y), this.size, this);
|
|
580
|
+
const isBlockedY = tileCollisionTest(vec2(oldPos.x, this.pos.y), this.size, this);
|
|
581
|
+
const restitution = max(this.restitution, hitLayer.restitution);
|
|
582
|
+
if (isBlockedX)
|
|
583
|
+
{
|
|
584
|
+
// a ledge caught less than maxMove below its top lifts the object onto it instead of stopping it,
|
|
585
|
+
// down off a ceiling ledge when gravity points up; zero gravity counts as down
|
|
586
|
+
const epsilon = 1e-3;
|
|
587
|
+
const maxMove = .1;
|
|
588
|
+
const y = gravityY > 0 ?
|
|
589
|
+
ceil( oldPos.y+this.size.y/2-1) - this.size.y/2 - epsilon :
|
|
590
|
+
floor(oldPos.y-this.size.y/2+1) + this.size.y/2 + epsilon;
|
|
591
|
+
if (abs(y - this.pos.y) < maxMove && !tileCollisionTest(vec2(this.pos.x, y), this.size, this))
|
|
433
592
|
{
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// floor, and fall through; back to its previous Y when that spot is not clear
|
|
438
|
-
const epsilon = .0001;
|
|
439
|
-
const offset = this.size.y/2 + epsilon;
|
|
440
|
-
const layerY = hitLayer.pos.y;
|
|
441
|
-
const y = layerY + (this.pos.y < oldPos.y ?
|
|
442
|
-
floor(oldPos.y - layerY - this.size.y/2) + offset :
|
|
443
|
-
ceil( oldPos.y - layerY + this.size.y/2) - offset);
|
|
444
|
-
const isClear = this.pos.y !== oldPos.y && !tileCollisionTest(vec2(this.pos.x, y), this.size, this);
|
|
445
|
-
this.pos.y = isClear ? y : oldPos.y;
|
|
446
|
-
|
|
447
|
-
// set ground object for tile collision
|
|
448
|
-
this.groundObject = wasFalling ? hitLayer : undefined;
|
|
449
|
-
|
|
450
|
-
// bounce velocity
|
|
451
|
-
this.velocity.y *= -restitution;
|
|
593
|
+
this.pos.y = y;
|
|
594
|
+
debugPhysics && debugRect(this.pos, this.size, '#ff0', 0, 0, false, false);
|
|
595
|
+
return;
|
|
452
596
|
}
|
|
453
|
-
|
|
597
|
+
|
|
598
|
+
// move against the wall and bounce, its side on the tile edge it moved toward, rounded in the
|
|
599
|
+
// layer's space as its collision test is; back to its previous X when that spot is not clear
|
|
600
|
+
const snapEpsilon = .0001, layerX = hitLayer.pos.x, offsetX = this.size.x/2 + snapEpsilon;
|
|
601
|
+
// never back past where it was: a leading edge already on a grid line would step back by
|
|
602
|
+
// the epsilon and take its trailing edge into the tile behind, which nothing tests, since
|
|
603
|
+
// only X is blocked; between there and the wall it covers no column it did not cover before
|
|
604
|
+
const movingLeft = this.pos.x < oldPos.x;
|
|
605
|
+
const snap = layerX + (movingLeft ?
|
|
606
|
+
floor(oldPos.x - layerX - this.size.x/2) + offsetX :
|
|
607
|
+
ceil( oldPos.x - layerX + this.size.x/2) - offsetX);
|
|
608
|
+
const x = movingLeft ? min(snap, oldPos.x) : max(snap, oldPos.x);
|
|
609
|
+
this.pos.x = tileCollisionTest(vec2(x, oldPos.y), this.size, this) ? oldPos.x : x;
|
|
610
|
+
this.velocity.x *= -restitution;
|
|
611
|
+
}
|
|
612
|
+
if (isBlockedY || !isBlockedX)
|
|
613
|
+
{
|
|
614
|
+
// adjust position to slightly away from the nearest tile, the floor or the ceiling it moved
|
|
615
|
+
// toward, which prevents a gap between them; rounded in the layer's space as its collision
|
|
616
|
+
// test is, or a bottom a hair below a grid line would round to the row under it, inside the
|
|
617
|
+
// floor, and fall through; back to its previous Y when that spot is not clear
|
|
618
|
+
const epsilon = .0001;
|
|
619
|
+
const offset = this.size.y/2 + epsilon;
|
|
620
|
+
const layerY = hitLayer.pos.y;
|
|
621
|
+
const y = layerY + (this.pos.y < oldPos.y ?
|
|
622
|
+
floor(oldPos.y - layerY - this.size.y/2) + offset :
|
|
623
|
+
ceil( oldPos.y - layerY + this.size.y/2) - offset);
|
|
624
|
+
const isClear = this.pos.y !== oldPos.y && !tileCollisionTest(vec2(this.pos.x, y), this.size, this);
|
|
625
|
+
this.pos.y = isClear ? y : oldPos.y;
|
|
626
|
+
|
|
627
|
+
// set ground object for tile collision
|
|
628
|
+
this.groundObject = wasFalling ? hitLayer : undefined;
|
|
629
|
+
|
|
630
|
+
// bounce velocity
|
|
631
|
+
this.velocity.y *= -restitution;
|
|
454
632
|
}
|
|
633
|
+
debugPhysics && debugRect(this.pos, this.size, '#f00', 0, 0, false, false);
|
|
455
634
|
}
|
|
456
635
|
}
|
|
457
636
|
}
|
|
@@ -519,10 +698,10 @@ class EngineObject
|
|
|
519
698
|
/** Called to check if a tile collision should be resolved. Return true for physics to resolve the collision or false to ignore and resolve it manually
|
|
520
699
|
* - Called for each solid tile the physics tests, which can be several times a frame for the same tile, and for
|
|
521
700
|
* positions it only tries, so keep it free of side effects or guard them to once a frame
|
|
522
|
-
* - this.pos has already moved, so a check on where it came from
|
|
523
|
-
*
|
|
524
|
-
* - For the point of impact, like sparks where a bullet hit,
|
|
525
|
-
*
|
|
701
|
+
* - this.pos has already moved, so a check on where it came from needs the position saved in update; for a one
|
|
702
|
+
* way platform the layer's setOneWay does that, and this is not asked about a one way tile it passes through
|
|
703
|
+
* - For the point of impact, like sparks where a bullet hit, set isBullet: this.pos is then where it meets the
|
|
704
|
+
* tile, as the platformer's Bullet uses it
|
|
526
705
|
* @param {number} tileData - the value of the tile at the position
|
|
527
706
|
* @param {Vector2} pos - the tile's bottom left corner in world space
|
|
528
707
|
* @return {boolean} - true if the collision should be resolved by modifying it's position and velocity */
|
package/src/engineParticles.js
CHANGED
|
@@ -440,10 +440,13 @@ function particleTileSet(tileInfo)
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// where the particle being tested was before its move, which one way tiles go by
|
|
444
|
+
let particleCollideFromX = 0, particleCollideFromY = 0;
|
|
445
|
+
|
|
443
446
|
// tests if a particle collides with tiles at x, y, through its emitter's collide callback if it has one
|
|
444
447
|
function particleCollideTest(particle, collideCallback, x, y)
|
|
445
448
|
{
|
|
446
|
-
const data =
|
|
449
|
+
const data = tileCollisionGetDataFrom(particleCollidePos.set(x, y), particleCollideFromX, particleCollideFromY);
|
|
447
450
|
if (!collideCallback)
|
|
448
451
|
return data > 0;
|
|
449
452
|
|
|
@@ -575,10 +578,12 @@ class Particle
|
|
|
575
578
|
|
|
576
579
|
// check collision against tiles
|
|
577
580
|
this.groundObject = undefined;
|
|
581
|
+
particleCollideFromX = oldX, particleCollideFromY = oldY;
|
|
578
582
|
if (particleCollideTest(this, collideCallback, this.pos.x, this.pos.y))
|
|
579
583
|
{
|
|
580
|
-
//
|
|
581
|
-
|
|
584
|
+
// the layer that stopped it, as the test found it, a one way layer it passes is not; if already was stuck
|
|
585
|
+
// in collision, don't do anything
|
|
586
|
+
const hitLayer = tileCollisionDataLayer;
|
|
582
587
|
if (!particleCollideTest(this, collideCallback, oldX, oldY))
|
|
583
588
|
{
|
|
584
589
|
// test which side we bounced off (or both if a corner)
|
package/src/engineTileLayer.js
CHANGED
|
@@ -47,6 +47,55 @@ const tileCollisionLayers = [];
|
|
|
47
47
|
function tileCollisionAssertWhole(layer)
|
|
48
48
|
{ ASSERT(layer.pos.x % 1 === 0 && layer.pos.y % 1 === 0, 'a tile collision layer must sit at a whole number position', layer.pos); }
|
|
49
49
|
|
|
50
|
+
// the object the physics is moving and where it was before this frame's move, for one way tiles
|
|
51
|
+
let tileCollisionFromObject, tileCollisionFromPos;
|
|
52
|
+
|
|
53
|
+
// a one way tile at a layer's cell lets a box at from pass, when the cell draws a tile set one way and the box was not
|
|
54
|
+
// wholly on its solid side, the side it can be passed through toward; the way is turned and mirrored as the tile's
|
|
55
|
+
// art is, mirror first, then its quarter turns clockwise; a point is a box of no size
|
|
56
|
+
function tileCollisionOneWayPass(layer, x, y, fromX, fromY, sizeX=0, sizeY=0)
|
|
57
|
+
{
|
|
58
|
+
const oneWayTiles = layer.oneWayTiles;
|
|
59
|
+
if (!oneWayTiles.size) return false;
|
|
60
|
+
const data = layer.data[y*layer.size.x + x], way = data && oneWayTiles.get(data.tile);
|
|
61
|
+
if (!way) return false;
|
|
62
|
+
let wayX = data.mirror ? -way.x : way.x, wayY = way.y;
|
|
63
|
+
for (let turn = data.direction & 3; turn--;)
|
|
64
|
+
[wayX, wayY] = [wayY, -wayX];
|
|
65
|
+
const epsilon = 1e-3, cellX = layer.pos.x + x, cellY = layer.pos.y + y;
|
|
66
|
+
return wayX > .5 ? fromX - sizeX/2 < cellX + 1 - epsilon :
|
|
67
|
+
wayX < -.5 ? fromX + sizeX/2 > cellX + epsilon :
|
|
68
|
+
wayY > .5 ? fromY - sizeY/2 < cellY + 1 - epsilon :
|
|
69
|
+
fromY + sizeY/2 > cellY + epsilon;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// the layer whose tile tileCollisionGetDataFrom last found solid, undefined for none, the one a particle bounces off
|
|
73
|
+
let tileCollisionDataLayer;
|
|
74
|
+
|
|
75
|
+
// tileCollisionGetData of the solid layers for particles, which pass a one way tile when they were not on its far
|
|
76
|
+
// side at from: a solid tile's data, else a negative marker, else 0
|
|
77
|
+
function tileCollisionGetDataFrom(pos, fromX, fromY)
|
|
78
|
+
{
|
|
79
|
+
let found = 0;
|
|
80
|
+
tileCollisionDataLayer = undefined;
|
|
81
|
+
for (const layer of tileCollisionLayers)
|
|
82
|
+
if (layer.isSolid)
|
|
83
|
+
{
|
|
84
|
+
const x = pos.x - layer.pos.x, y = pos.y - layer.pos.y, size = layer.size;
|
|
85
|
+
if (x >= 0 && y >= 0 && x < size.x && y < size.y)
|
|
86
|
+
{
|
|
87
|
+
const data = layer.collisionData[(y|0)*size.x + (x|0)];
|
|
88
|
+
if (data > 0)
|
|
89
|
+
{
|
|
90
|
+
if (!tileCollisionOneWayPass(layer, x|0, y|0, fromX, fromY))
|
|
91
|
+
return tileCollisionDataLayer = layer, data;
|
|
92
|
+
}
|
|
93
|
+
else if (data && !found) found = data;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return found;
|
|
97
|
+
}
|
|
98
|
+
|
|
50
99
|
// the test a tile query applies to a cell's data: the callback, the object's collideWithTile, or solid data
|
|
51
100
|
function tileCollisionTester(callbackObject)
|
|
52
101
|
{
|
|
@@ -821,13 +870,17 @@ class TileLayer extends CanvasLayer
|
|
|
821
870
|
* - This may be slow if not using webgl but only needs to be done once */
|
|
822
871
|
redraw()
|
|
823
872
|
{
|
|
873
|
+
// the camera, canvas and target are the game's again even when a tile draw or onRedraw throws
|
|
824
874
|
this.redrawStart(true);
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
this.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
875
|
+
try
|
|
876
|
+
{
|
|
877
|
+
for (let x = this.size.x; x--;)
|
|
878
|
+
for (let y = this.size.y; y--;)
|
|
879
|
+
this.drawTileData(vec2(x,y), false);
|
|
880
|
+
this.isUsingWebGL && glFlush();
|
|
881
|
+
this.onRedraw();
|
|
882
|
+
}
|
|
883
|
+
finally { this.redrawEnd(); }
|
|
831
884
|
this.tilesInWebGL = this.isUsingWebGL;
|
|
832
885
|
}
|
|
833
886
|
|
|
@@ -940,8 +993,8 @@ class TileLayer extends CanvasLayer
|
|
|
940
993
|
ASSERT(drawContext !== this.context, 'redrawStart() should not be active when calling redrawTileData(), instead use drawTileData()');
|
|
941
994
|
|
|
942
995
|
this.redrawStart();
|
|
943
|
-
this.drawTileData(layerPos, clear);
|
|
944
|
-
this.redrawEnd();
|
|
996
|
+
try { this.drawTileData(layerPos, clear); }
|
|
997
|
+
finally { this.redrawEnd(); }
|
|
945
998
|
}
|
|
946
999
|
|
|
947
1000
|
/** Draw textured tile in layer space
|
|
@@ -1065,6 +1118,31 @@ class TileCollisionLayer extends TileLayer
|
|
|
1065
1118
|
/** @property {boolean} - In the light system's shadow pass, cast only from the cells with collision, drawn
|
|
1066
1119
|
* as the layer shows them, so a floor in the same layer stays lit; false casts every tile */
|
|
1067
1120
|
this.shadowSolidOnly = true;
|
|
1121
|
+
/** @property {Map<number, Vector2>} - The tiles set one way, by tile index, and the way each can be passed
|
|
1122
|
+
* through, see setOneWay
|
|
1123
|
+
* @type {Map<number, Vector2>} */
|
|
1124
|
+
this.oneWayTiles = new Map;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/** Make the cells that draw a tile one way, like a platform jumped up through and landed on: an object, a
|
|
1128
|
+
* particle or a ray passes through moving that way, and the tile blocks only what was wholly on its far side
|
|
1129
|
+
* before it moved, the side it is passed toward; turned and mirrored as each cell's tile is, so a platform turned
|
|
1130
|
+
* a quarter is one way to the side
|
|
1131
|
+
* - A moving object is not stopped or asked through collideWithTile until the tile would block it, so a
|
|
1132
|
+
* collideWithTile that returns false lets it drop through
|
|
1133
|
+
* - tileCollisionTest with an object goes by the object's pos, a raycast by its start, and a test with no object
|
|
1134
|
+
* or with a callback finds every one way tile solid
|
|
1135
|
+
* @param {number} tile - The tile index, as tile() and TileLayerData take it
|
|
1136
|
+
* @param {Vector2} [direction] - Up, down, left or right, the way it can be passed through: vec2(0, 1), the
|
|
1137
|
+
* default, is a platform landed on from above; layer.oneWayTiles.delete(tile) makes the tile solid again
|
|
1138
|
+
* @example
|
|
1139
|
+
* layer.setOneWay(5); // tile 5 is a platform, jumped up through and stood on */
|
|
1140
|
+
setOneWay(tile, direction=vec2(0, 1))
|
|
1141
|
+
{
|
|
1142
|
+
ASSERT(isNumber(tile), 'setOneWay: tile is a tile index, a number', tile);
|
|
1143
|
+
ASSERT(isVector2(direction) && abs(direction.x) + abs(direction.y) === 1 && !(direction.x && direction.y),
|
|
1144
|
+
'setOneWay: direction is up, down, left or right, like vec2(0, 1)', direction);
|
|
1145
|
+
this.oneWayTiles.set(tile, direction.copy());
|
|
1068
1146
|
}
|
|
1069
1147
|
|
|
1070
1148
|
/** Draw this layer's shadow shape: the cells with collision in the part the shadow map covers, each row of
|
|
@@ -1156,6 +1234,10 @@ class TileCollisionLayer extends TileLayer
|
|
|
1156
1234
|
tileCollisionAssertWhole(this);
|
|
1157
1235
|
const collisionTest = tileCollisionTester(callbackObject);
|
|
1158
1236
|
|
|
1237
|
+
// a one way tile goes by where an object was before the physics moved it, or where it is now
|
|
1238
|
+
const object = callbackObject instanceof EngineObject && this.oneWayTiles.size ? callbackObject : undefined;
|
|
1239
|
+
const from = object && (object === tileCollisionFromObject ? tileCollisionFromPos : object.pos);
|
|
1240
|
+
|
|
1159
1241
|
// check any tiles in the area for collision
|
|
1160
1242
|
const posX = pos.x - this.pos.x;
|
|
1161
1243
|
const posY = pos.y - this.pos.y;
|
|
@@ -1174,7 +1256,8 @@ class TileCollisionLayer extends TileLayer
|
|
|
1174
1256
|
{
|
|
1175
1257
|
// check if the object should collide with this tile, the callback gets its own vector, one it can keep
|
|
1176
1258
|
const tileData = this.collisionData[y*this.size.x+x];
|
|
1177
|
-
if (tileData &&
|
|
1259
|
+
if (tileData && !(from && tileCollisionOneWayPass(this, x, y, from.x, from.y, size.x, size.y)) &&
|
|
1260
|
+
collisionTest(tileData, vec2(x+this.pos.x, y+this.pos.y)))
|
|
1178
1261
|
return true;
|
|
1179
1262
|
}
|
|
1180
1263
|
return false;
|
|
@@ -1194,10 +1277,12 @@ class TileCollisionLayer extends TileLayer
|
|
|
1194
1277
|
const collisionTest = tileCollisionTester(callbackObject);
|
|
1195
1278
|
// the line is walked in the layer's own space, so its cells are the tiles wherever the layer sits
|
|
1196
1279
|
const offset = this.pos;
|
|
1280
|
+
// a one way tile blocks a ray that starts on its far side
|
|
1197
1281
|
const testFunction = (pos)=>
|
|
1198
1282
|
{
|
|
1199
1283
|
const tileData = this.getCollisionData(pos);
|
|
1200
|
-
return tileData &&
|
|
1284
|
+
return tileData && !tileCollisionOneWayPass(this, pos.x|0, pos.y|0, posStart.x, posStart.y) &&
|
|
1285
|
+
collisionTest(tileData, vec2(pos.x + offset.x, pos.y + offset.y));
|
|
1201
1286
|
}
|
|
1202
1287
|
// only the part of the line over the layer, and a cell around it, can meet a tile, so a ray toward a point
|
|
1203
1288
|
// far beyond is walked across the layer and no farther; the cell around keeps the step into the layer as it was
|
package/src/engineWebGL.js
CHANGED
|
@@ -530,15 +530,15 @@ function glShaderTrack(shader) { glShaderObjects.includes(shader) || glShaderObj
|
|
|
530
530
|
// let go of a Shader's programs, 2D and 3D, and of its place in the list; a batch drawing with it is drawn first
|
|
531
531
|
function glShaderDispose(shader)
|
|
532
532
|
{
|
|
533
|
+
// a batch drawing with it is drawn first, the batch holds the Shader; its first draw compiles it and lists it, so
|
|
534
|
+
// it comes off the list after
|
|
535
|
+
if (glContext && !headlessMode && glBatchShader === shader)
|
|
536
|
+
glFlush();
|
|
533
537
|
const i = glShaderObjects.indexOf(shader);
|
|
534
538
|
i < 0 || glShaderObjects.splice(i, 1);
|
|
535
539
|
if (glContext && !headlessMode)
|
|
536
|
-
{
|
|
537
|
-
if (glBatchShader === shader) // a batch drawing with it is drawn first, the batch holds the Shader
|
|
538
|
-
glFlush();
|
|
539
540
|
for (const program of [shader.program, shader.program3D])
|
|
540
541
|
program && glContext.deleteProgram(program);
|
|
541
|
-
}
|
|
542
542
|
shader.program = shader.program3D = undefined;
|
|
543
543
|
}
|
|
544
544
|
|