Skip to content

Commit 3e601fa

Browse files
fix: tile collision ignored layer position offset
tileCollisionGetData() passed world positions directly to layer.getCollisionData(), which expects layer-local coordinates. A layer placed away from the origin rendered at the offset position but reported collision as if it were still at (0,0), because collisionTest() and collisionRaycast() subtract layer.pos while tileCollisionGetData() did not. This affected particle tile collision and the debug overlay mouse collision readout; EngineObject physics was unaffected since it uses collisionTest(). Also guard against undefined hitLayer in Particle.update(): a custom collideCallback can report a hit where the solid-only tileCollisionTest() does not, so the bounce now falls back to the particle emitter's own restitution and friction instead of crashing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f4ec68b commit 3e601fa

8 files changed

Lines changed: 66 additions & 23 deletions

dist/littlejs.esm.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7482,10 +7482,14 @@ function tileCollisionGetData(pos, solidOnly=true)
74827482
// check all tile collision layers
74837483
for (const layer of tileCollisionLayers)
74847484
if (!solidOnly || layer.isSolid)
7485-
if (pos.arrayCheck(layer.size))
74867485
{
7487-
const data = layer.getCollisionData(pos);
7488-
if (data) return data;
7486+
// convert world pos to layer local space
7487+
const layerPos = pos.subtract(layer.pos);
7488+
if (layerPos.arrayCheck(layer.size))
7489+
{
7490+
const data = layer.getCollisionData(layerPos);
7491+
if (data) return data;
7492+
}
74897493
}
74907494
return 0;
74917495
}
@@ -8624,8 +8628,9 @@ class Particle
86248628
// test which side we bounced off (or both if a corner)
86258629
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
86268630
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
8627-
const hitRestitution = max(restitution, hitLayer.restitution);
8628-
const hitFriction = max(friction, hitLayer.friction);
8631+
// collide callback may hit where the layer test does not, so hitLayer can be undefined
8632+
const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
8633+
const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
86298634
if (isBlockedX)
86308635
{
86318636
// move to previous X position and bounce

dist/littlejs.esm.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/littlejs.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7482,10 +7482,14 @@ function tileCollisionGetData(pos, solidOnly=true)
74827482
// check all tile collision layers
74837483
for (const layer of tileCollisionLayers)
74847484
if (!solidOnly || layer.isSolid)
7485-
if (pos.arrayCheck(layer.size))
74867485
{
7487-
const data = layer.getCollisionData(pos);
7488-
if (data) return data;
7486+
// convert world pos to layer local space
7487+
const layerPos = pos.subtract(layer.pos);
7488+
if (layerPos.arrayCheck(layer.size))
7489+
{
7490+
const data = layer.getCollisionData(layerPos);
7491+
if (data) return data;
7492+
}
74897493
}
74907494
return 0;
74917495
}
@@ -8624,8 +8628,9 @@ class Particle
86248628
// test which side we bounced off (or both if a corner)
86258629
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
86268630
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
8627-
const hitRestitution = max(restitution, hitLayer.restitution);
8628-
const hitFriction = max(friction, hitLayer.friction);
8631+
// collide callback may hit where the layer test does not, so hitLayer can be undefined
8632+
const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
8633+
const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
86298634
if (isBlockedX)
86308635
{
86318636
// move to previous X position and bounce

dist/littlejs.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/littlejs.release.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6791,10 +6791,14 @@ function tileCollisionGetData(pos, solidOnly=true)
67916791
// check all tile collision layers
67926792
for (const layer of tileCollisionLayers)
67936793
if (!solidOnly || layer.isSolid)
6794-
if (pos.arrayCheck(layer.size))
67956794
{
6796-
const data = layer.getCollisionData(pos);
6797-
if (data) return data;
6795+
// convert world pos to layer local space
6796+
const layerPos = pos.subtract(layer.pos);
6797+
if (layerPos.arrayCheck(layer.size))
6798+
{
6799+
const data = layer.getCollisionData(layerPos);
6800+
if (data) return data;
6801+
}
67986802
}
67996803
return 0;
68006804
}
@@ -7933,8 +7937,9 @@ class Particle
79337937
// test which side we bounced off (or both if a corner)
79347938
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
79357939
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
7936-
const hitRestitution = max(restitution, hitLayer.restitution);
7937-
const hitFriction = max(friction, hitLayer.friction);
7940+
// collide callback may hit where the layer test does not, so hitLayer can be undefined
7941+
const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
7942+
const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
79387943
if (isBlockedX)
79397944
{
79407945
// move to previous X position and bounce

src/engineParticles.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,9 @@ class Particle
437437
// test which side we bounced off (or both if a corner)
438438
const isBlockedX = testCollision(vec2(this.pos.x, oldPos.y));
439439
const isBlockedY = testCollision(vec2(oldPos.x, this.pos.y));
440-
const hitRestitution = max(restitution, hitLayer.restitution);
441-
const hitFriction = max(friction, hitLayer.friction);
440+
// collide callback may hit where the layer test does not, so hitLayer can be undefined
441+
const hitRestitution = hitLayer ? max(restitution, hitLayer.restitution) : restitution;
442+
const hitFriction = hitLayer ? max(friction, hitLayer.friction) : friction;
442443
if (isBlockedX)
443444
{
444445
// move to previous X position and bounce

src/engineTileLayer.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ function tileCollisionGetData(pos, solidOnly=true)
3131
// check all tile collision layers
3232
for (const layer of tileCollisionLayers)
3333
if (!solidOnly || layer.isSolid)
34-
if (pos.arrayCheck(layer.size))
3534
{
36-
const data = layer.getCollisionData(pos);
37-
if (data) return data;
35+
// convert world pos to layer local space
36+
const layerPos = pos.subtract(layer.pos);
37+
if (layerPos.arrayCheck(layer.size))
38+
{
39+
const data = layer.getCollisionData(layerPos);
40+
if (data) return data;
41+
}
3842
}
3943
return 0;
4044
}

test/tileCollision.test.mjs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from 'node:test';
22
import assert from 'node:assert/strict';
3-
import { TileCollisionLayer, tile, vec2 } from '../dist/littlejs.esm.js';
3+
import { TileCollisionLayer, tileCollisionGetData, tile, vec2 } from '../dist/littlejs.esm.js';
44

55
// Regression for the negative-edge ghost-collision bug:
66
// collisionTest() clamped minX/minY to 0 and then forced maxX/maxY to at least
@@ -57,3 +57,26 @@ test('collisionTest: AABB entirely off the positive Y edge does not collide', ()
5757
const layer = makeLayer();
5858
assert.equal(layer.collisionTest(vec2(0.5, 100), vec2(1, 1)), false);
5959
});
60+
61+
// Regression for the layer-offset bug: tileCollisionGetData() passed the world
62+
// position directly to layer.getCollisionData(), which expects layer-local
63+
// coordinates. A layer placed away from the origin would report collision as
64+
// if it were still at (0,0) even though rendering honored the offset.
65+
66+
test('tileCollisionGetData: honors layer position offset', () =>
67+
{
68+
const layer = new TileCollisionLayer(vec2(2, 2), vec2(4, 4), tile(0, 16), 0, false);
69+
layer.setCollisionData(vec2(1, 1), 1);
70+
// tile at local (1,1) occupies world cell (3,3)
71+
assert.equal(tileCollisionGetData(vec2(3.5, 3.5)), 1);
72+
layer.destroy();
73+
});
74+
75+
test('tileCollisionGetData: no collision at the unoffset position', () =>
76+
{
77+
const layer = new TileCollisionLayer(vec2(2, 2), vec2(4, 4), tile(0, 16), 0, false);
78+
layer.setCollisionData(vec2(1, 1), 1);
79+
// world (1.5, 1.5) is local (-0.5, -0.5), outside the layer
80+
assert.equal(tileCollisionGetData(vec2(1.5, 1.5)), 0);
81+
layer.destroy();
82+
});

0 commit comments

Comments
 (0)