Fix collider tunneling, seed the container demo, split the e2e fixture

Move with "Collide with solids" now steps in sub-steps of at most half the
thinnest collider, so a fast step can no longer jump across a collider (the
crates are only 60 high, which is why it showed up mostly on the y axis). A
pre-existing overlap is still never resolved, but it can no longer be
deepened either, so a small overlap cannot open a wall permanently.

seed-demo.mjs now writes the saved demo scene: a "Player" container (green
sprite with collider and the Mover behavior) against two "Enemy" container
instances, camera at zoom 0.5, Spawner dropping another enemy on Space.

The e2e checks assert against the old two-crate layout and locate widgets by
text, which the demo can no longer provide (three identically named sprite
instances, both crates nested in containers). Its fixture therefore moves to
tools/seed-e2e.mjs so the shipped demo can evolve without rewriting 100+
checks: seed-e2e -> e2e -> seed-demo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-07 19:35:11 +02:00
1 parent 3379232e64
commit 4900d7bbf6
6 files changed
+269 -39

No files matched your search

+27 -5
View File
@@ -150,6 +150,8 @@ describe('CollisionSystem', () => {
});
describe('move and slide', () => {
const SKIN = 0.05; // mirrors CollisionSystem: overlaps this shallow count as contact
function runtime() {
const s = setup();
const world = World.instantiate(s.project, s.sceneId);
@@ -197,13 +199,33 @@ describe('move and slide', () => {
expect(crateA.x).toBe(260);
});
it('pairs that already overlap are not resolved (no jolt), leaving is always possible', () => {
it('an existing overlap is neither resolved (no jolt) nor deepened; leaving is always possible', () => {
const { world, collisions, crateA, crateB } = runtime();
crateB.x = 60; // start overlapped
expect(collisions.moveAndSlide(world, crateA, 5, 0)).toEqual({ blockedX: false, blockedY: false });
expect(crateA.x).toBe(5);
crateB.x = 60; // start overlapped by 40
expect(collisions.moveAndSlide(world, crateA, 5, 0)).toEqual({ blockedX: true, blockedY: false });
expect(crateA.x).toBe(0); // no jolt out, but no burrowing deeper either
expect(collisions.moveAndSlide(world, crateA, -20, 0)).toEqual({ blockedX: false, blockedY: false });
expect(crateA.x).toBe(-20); // moving away is always free
expect(collisions.moveAndSlide(world, crateA, -100, 0)).toEqual({ blockedX: false, blockedY: false });
expect(crateA.x).toBe(-95);
expect(crateA.x).toBe(-120);
});
it('keeps blocking while pressed against a wall, however long and at any speed', () => {
const { world, collisions, crateA, crateB } = runtime();
crateB.x = 0;
crateB.y = 300; // wall straight above, colliders are 100×60 → contact at y = 240
for (let i = 0; i < 500; i++) {
// Uneven steps, so contact is never hit at a round number.
const { blockedY } = collisions.moveAndSlide(world, crateA, 0, 3.7 + (i % 7) * 0.13);
expect(crateA.y).toBeLessThanOrEqual(240 + SKIN);
if (i > 60) expect(blockedY).toBe(true);
}
expect(crateA.y).toBeGreaterThan(240 - SKIN);
// A step larger than the obstacle used to jump clean over it: after the move nothing
// overlapped any more, so nothing pushed back and the mover was through the wall.
crateA.y = 0;
expect(collisions.moveAndSlide(world, crateA, 0, 5000)).toEqual({ blockedX: false, blockedY: true });
expect(crateA.y).toBeCloseTo(240, 6);
});
it('the Move node with "Collide with solids" moves the owner and outputs the blocked axes', () => {