487 lines
20 KiB
TypeScript
487 lines
20 KiB
TypeScript
import { beforeAll, describe, expect, it } from 'vitest';
|
||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||
import { AddColliderCommand, AddRigidbodyCommand } from '../../src/core/commands/colliderCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import { PaintTilesCommand } from '../../src/core/commands/tilemapCommands';
|
||
import { worldPositionOf } from '../../src/core/graph';
|
||
import type { TilemapAsset } from '../../src/core/model/Asset';
|
||
import type { PropertyValue } from '../../src/core/model/Properties';
|
||
import { findAsset } from '../../src/core/model/Project';
|
||
import { TILE_SOLID_KEY } from '../../src/core/model/tile';
|
||
import { TILEMAP_COLUMNS_KEY, TILEMAP_ROWS_KEY } from '../../src/core/model/tilemap';
|
||
import type { Entity } from '../../src/runtime/Entity';
|
||
import { loadPhysics } from '../../src/runtime/physics/engine';
|
||
import { PHYSICS_STEP, type PhysicsSystem } from '../../src/runtime/physics/PhysicsSystem';
|
||
import type { CollisionPair } from '../../src/runtime/systems/solids';
|
||
import {
|
||
graphEditor,
|
||
makeBehavior,
|
||
makeSpriteScene,
|
||
place,
|
||
roundTrip,
|
||
type SceneFixture,
|
||
startWorld,
|
||
} from '../helpers';
|
||
|
||
beforeAll(async () => {
|
||
await loadPhysics();
|
||
});
|
||
|
||
interface Thing {
|
||
/** The object's size; its collider is made to match. */
|
||
w: number;
|
||
h?: number;
|
||
circle?: boolean;
|
||
/** Overrides on the collider (friction, bounciness, density, isTrigger). */
|
||
collider?: Record<string, PropertyValue>;
|
||
/** With a rigidbody, and these overrides on it (bodyType, fixedRotation, …). */
|
||
body?: Record<string, PropertyValue>;
|
||
/** Overrides on the object itself (rotation, scale). */
|
||
props?: Record<string, PropertyValue>;
|
||
parent?: string;
|
||
}
|
||
|
||
/** A scene of sprites with colliders, each maybe with a rigidbody; `add` returns the object's id. */
|
||
function physicsScene() {
|
||
const f = makeSpriteScene('Box');
|
||
const bodies = new Map<string, string>();
|
||
const add = (x: number, y: number, thing: Thing): string => {
|
||
const id = place(f, f.spriteId, { x, y }, thing.parent);
|
||
const set = (objectId: string, key: string, value: PropertyValue) =>
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, objectId, key, value));
|
||
set(id, 'width', thing.w);
|
||
set(id, 'height', thing.h ?? thing.w);
|
||
for (const [key, value] of Object.entries(thing.props ?? {})) set(id, key, value);
|
||
const collider = new AddColliderCommand(f.sceneId, id);
|
||
f.stack.execute(collider);
|
||
if (thing.circle) {
|
||
set(collider.objectId, 'shape', 'circle');
|
||
set(collider.objectId, 'radius', thing.w / 2);
|
||
}
|
||
for (const [key, value] of Object.entries(thing.collider ?? {})) set(collider.objectId, key, value);
|
||
if (thing.body) {
|
||
const rigidbody = new AddRigidbodyCommand(f.sceneId, id);
|
||
f.stack.execute(rigidbody);
|
||
for (const [key, value] of Object.entries(thing.body)) set(rigidbody.objectId, key, value);
|
||
bodies.set(id, rigidbody.objectId);
|
||
}
|
||
return id;
|
||
};
|
||
const gravity = (x: number, y: number) => {
|
||
f.stack.execute(new SetAssetPropertyCommand(f.sceneId, 'gravityX', x));
|
||
f.stack.execute(new SetAssetPropertyCommand(f.sceneId, 'gravityY', y));
|
||
};
|
||
return { ...f, add, gravity, rigidbodyOf: (id: string) => bodies.get(id)! };
|
||
}
|
||
|
||
/** The ground used throughout: 400 × 40 at y = 200, its top face at y = 180. */
|
||
const GROUND: Thing = { w: 400, h: 40 };
|
||
|
||
function run(f: SceneFixture) {
|
||
const started = startWorld(f, { physics: true, collisions: true, behaviors: true });
|
||
const physics = started.collisions as PhysicsSystem;
|
||
const seconds = (s: number, each?: () => void) => {
|
||
for (let i = 0; i < Math.round(s * 60); i++) {
|
||
started.frame(1 / 60);
|
||
each?.();
|
||
}
|
||
};
|
||
const entity = (id: string) => started.world.findByObjectId(id)!;
|
||
return { ...started, physics, seconds, entity };
|
||
}
|
||
|
||
describe('AddRigidbodyCommand', () => {
|
||
it('puts a rigidbody below the object, one at most, and undo takes it away', () => {
|
||
const f = physicsScene();
|
||
const box = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const command = new AddRigidbodyCommand(f.sceneId, box);
|
||
f.stack.execute(command);
|
||
const asset = findAsset(f.project, command.assetId)!;
|
||
expect(asset).toMatchObject({ type: 'rigidbody', name: 'Box rigidbody' });
|
||
expect(asset.props).toMatchObject({
|
||
bodyType: 'dynamic',
|
||
mass: 0,
|
||
gravityScale: 1,
|
||
fixedRotation: false,
|
||
});
|
||
expect(f.object(command.objectId).parentId).toBe(box);
|
||
expect(() => f.stack.execute(new AddRigidbodyCommand(f.sceneId, box))).toThrow(/has a rigidbody already/);
|
||
expect(() => f.stack.execute(new AddRigidbodyCommand(f.sceneId, command.objectId))).toThrow(
|
||
/rigidbody cannot have a rigidbody/,
|
||
);
|
||
const collider = new AddColliderCommand(f.sceneId, box);
|
||
f.stack.execute(collider);
|
||
expect(() => f.stack.execute(new AddRigidbodyCommand(f.sceneId, collider.objectId))).toThrow(
|
||
/collider cannot have a rigidbody/,
|
||
);
|
||
f.stack.undo();
|
||
f.stack.undo();
|
||
expect(f.project.assets.some((a) => a.id === command.assetId)).toBe(false);
|
||
expect(f.sceneOf().objects.some((o) => o.id === command.objectId)).toBe(false);
|
||
});
|
||
|
||
it('keeps the material and the body settings through a round trip', () => {
|
||
const f = physicsScene();
|
||
const box = f.add(0, 0, {
|
||
w: 32,
|
||
collider: { friction: 0.2, bounciness: 0.7 },
|
||
body: { bodyType: 'kinematic' },
|
||
});
|
||
const loaded = roundTrip(f.project);
|
||
const scene = loaded.assets.find((a) => a.id === f.sceneId)!;
|
||
expect(scene.props).toMatchObject({ gravityX: 0, gravityY: 980 });
|
||
const objects = scene.type === 'scene' ? scene.objects : [];
|
||
const child = (type: string) =>
|
||
objects.find((o) => o.parentId === box && findAsset(loaded, o.assetId)?.type === type)!;
|
||
expect(child('collider').overrides).toMatchObject({ friction: 0.2, bounciness: 0.7 });
|
||
expect(child('rigidbody').overrides).toMatchObject({ bodyType: 'kinematic' });
|
||
});
|
||
});
|
||
|
||
describe('falling and resting', () => {
|
||
it('a dynamic box falls at 980 per second squared and comes to rest on a collider without a rigidbody', () => {
|
||
const f = physicsScene();
|
||
f.add(0, 200, GROUND);
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { seconds, entity, physics } = run(f);
|
||
seconds(0.5);
|
||
// ½·g·t² = 122.5 after 30 steps; the picture is drawn between the last two steps, and with
|
||
// frames exactly one step long that is the one before: ½·g·(29/60)² ≈ 114.5.
|
||
expect(entity(box).y).toBeGreaterThan(114.5 - 2);
|
||
expect(entity(box).y).toBeLessThan(122.5 + 2);
|
||
seconds(2.5);
|
||
expect(entity(box).y).toBeCloseTo(180 - 16, 0);
|
||
expect(Math.abs(physics.velocityOf(entity(box))!.y)).toBeLessThan(1);
|
||
seconds(2);
|
||
expect(entity(box).y).toBeCloseTo(180 - 16, 0);
|
||
expect(entity(box).x).toBeCloseTo(0, 3);
|
||
});
|
||
|
||
it('comes to rest on a painted tilemap', () => {
|
||
const f = physicsScene();
|
||
const solid = new CreateAssetCommand('tile', 'Wall');
|
||
const map = new CreateAssetCommand('tilemap', 'Ground');
|
||
f.stack.execute(solid);
|
||
f.stack.execute(map);
|
||
f.stack.execute(new SetAssetPropertyCommand(solid.asset.id, TILE_SOLID_KEY, true));
|
||
f.stack.execute(new SetAssetPropertyCommand(map.asset.id, TILEMAP_COLUMNS_KEY, 10));
|
||
f.stack.execute(new SetAssetPropertyCommand(map.asset.id, TILEMAP_ROWS_KEY, 10));
|
||
place(f, map.asset.id, { x: 0, y: 0 });
|
||
const layer = (findAsset(f.project, map.asset.id) as TilemapAsset).layers[0]!.id;
|
||
// The bottom row: cells of 48, the grid centred on the origin, so its top face is at 4·48 = 192.
|
||
const row = Array.from({ length: 10 }, (_, x) => ({ x, y: 9, tileId: solid.asset.id }));
|
||
f.stack.execute(new PaintTilesCommand(map.asset.id, layer, row));
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { seconds, entity } = run(f);
|
||
seconds(3);
|
||
expect(entity(box).y).toBeCloseTo(192 - 16, 0);
|
||
});
|
||
|
||
it('a static, a switched-off or a weightless rigidbody stays where it is, and so does all in a scene without gravity', () => {
|
||
const f = physicsScene();
|
||
const still = f.add(-150, 0, { w: 32, body: { bodyType: 'static' } });
|
||
const weightless = f.add(-50, 0, { w: 32, body: { gravityScale: 0 } });
|
||
const off = f.add(50, 0, { w: 32, body: {} });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, f.rigidbodyOf(off), 'enabled', false));
|
||
const falls = f.add(150, 0, { w: 32, body: {} });
|
||
const { seconds, entity } = run(f);
|
||
seconds(1);
|
||
expect(entity(still).y).toBe(0);
|
||
expect(entity(weightless).y).toBe(0);
|
||
expect(entity(off).y).toBe(0);
|
||
expect(entity(falls).y).toBeGreaterThan(400);
|
||
|
||
const g = physicsScene();
|
||
g.gravity(0, 0);
|
||
const floating = g.add(0, 0, { w: 32, body: {} });
|
||
const second = run(g);
|
||
second.seconds(1);
|
||
expect(second.entity(floating).y).toBe(0);
|
||
});
|
||
|
||
it('gravity is the scene’s own and can pull sideways', () => {
|
||
const f = physicsScene();
|
||
f.gravity(490, 0);
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { seconds, entity } = run(f);
|
||
seconds(1);
|
||
expect(Math.abs(entity(box).x - 245)).toBeLessThan(10);
|
||
expect(entity(box).y).toBeCloseTo(0, 6);
|
||
});
|
||
});
|
||
|
||
describe('the material', () => {
|
||
/** How high a ball dropped from 184 above the ground climbs again after its first bounce. */
|
||
function rebound(bounciness: number): number {
|
||
const f = physicsScene();
|
||
f.add(0, 200, GROUND);
|
||
const ball = f.add(0, 0, { w: 32, circle: true, body: {}, collider: { bounciness } });
|
||
const { seconds, entity } = run(f);
|
||
let landed = false;
|
||
let highest = Infinity;
|
||
seconds(4, () => {
|
||
const y = entity(ball).y;
|
||
if (y > 180 - 16 - 1) landed = true;
|
||
else if (landed) highest = Math.min(highest, y);
|
||
});
|
||
return Number.isFinite(highest) ? 180 - 16 - highest : 0;
|
||
}
|
||
|
||
it('bounciness 0 stays down, 0.5 climbs back a quarter of the drop, 1 nearly all of it', () => {
|
||
const drop = 180 - 16;
|
||
expect(rebound(0)).toBeLessThan(1);
|
||
const half = rebound(0.5) / drop;
|
||
expect(half).toBeGreaterThan(0.2);
|
||
expect(half).toBeLessThan(0.3);
|
||
expect(rebound(1) / drop).toBeGreaterThan(0.9);
|
||
});
|
||
|
||
/** A box set flat on a slope turned 30°, left to itself for half a second: how far it went. */
|
||
function slide(friction: number): number {
|
||
const f = physicsScene();
|
||
f.add(0, 200, { ...GROUND, props: { rotation: 30 }, collider: { friction } });
|
||
// The slope's top face, lifted by half the box: along the face normal (sin 30°, −cos 30°).
|
||
const lift = 20 + 16 + 0.5;
|
||
const x = lift * Math.sin(Math.PI / 6);
|
||
const y = 200 - lift * Math.cos(Math.PI / 6);
|
||
const box = f.add(x, y, { w: 32, props: { rotation: 30 }, body: {}, collider: { friction } });
|
||
const { seconds, entity } = run(f);
|
||
seconds(0.5);
|
||
return Math.hypot(entity(box).x - x, entity(box).y - y);
|
||
}
|
||
|
||
it('slides down a slope without friction and holds on it with enough', () => {
|
||
// Frictionless the slope pulls with g·sin 30° = 490: about 61 along it in half a second.
|
||
expect(slide(0)).toBeGreaterThan(40);
|
||
// tan 30° ≈ 0.58, so a friction of 1 holds it.
|
||
expect(slide(1)).toBeLessThan(2);
|
||
});
|
||
|
||
it('a push between two masses keeps the momentum', () => {
|
||
const f = physicsScene();
|
||
f.gravity(0, 0);
|
||
const light = f.add(0, 0, { w: 32, body: {}, collider: { bounciness: 1 } });
|
||
const heavy = f.add(100, 0, { w: 32, body: {}, collider: { bounciness: 1, density: 3 } });
|
||
const { seconds, entity, physics } = run(f);
|
||
seconds(1 / 60);
|
||
physics.setVelocity(entity(light), { x: 300, y: 0 });
|
||
seconds(1);
|
||
const after = physics.velocityOf(entity(light))!.x + 3 * physics.velocityOf(entity(heavy))!.x;
|
||
expect(physics.velocityOf(entity(heavy))!.x).toBeGreaterThan(100);
|
||
expect(Math.abs(after - 300)).toBeLessThan(3);
|
||
});
|
||
|
||
it('a fixed mass counts instead of the density', () => {
|
||
const f = physicsScene();
|
||
f.gravity(0, 0);
|
||
const light = f.add(0, 0, { w: 32, body: {}, collider: { bounciness: 1 } });
|
||
// Density 50 would make it fifty times the light one; the mass says three times.
|
||
const heavy = f.add(100, 0, {
|
||
w: 32,
|
||
body: { mass: 3 * 0.32 * 0.32 },
|
||
collider: { bounciness: 1, density: 50 },
|
||
});
|
||
const { seconds, entity, physics } = run(f);
|
||
seconds(1 / 60);
|
||
physics.setVelocity(entity(light), { x: 300, y: 0 });
|
||
seconds(1);
|
||
const after = physics.velocityOf(entity(light))!.x + 3 * physics.velocityOf(entity(heavy))!.x;
|
||
expect(Math.abs(after - 300)).toBeLessThan(3);
|
||
});
|
||
});
|
||
|
||
describe('turning', () => {
|
||
/** A box dropped with its centre just past the edge of a narrow ledge. */
|
||
function tip(body: Record<string, PropertyValue>): number {
|
||
const f = physicsScene();
|
||
f.add(0, 200, { w: 100, h: 40 });
|
||
const box = f.add(60, 150, { w: 32, body });
|
||
const { seconds, entity } = run(f);
|
||
seconds(1);
|
||
return Number(entity(box).props.rotation ?? 0);
|
||
}
|
||
|
||
it('a box falling past an edge tips over; with fixed rotation it does not', () => {
|
||
expect(Math.abs(tip({}))).toBeGreaterThan(5);
|
||
expect(tip({ fixedRotation: true })).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('objects and bodies', () => {
|
||
it('simulates below a turned parent in world units and hands back a local position', () => {
|
||
const f = physicsScene();
|
||
f.add(0, 200, GROUND);
|
||
const parent = place(f, f.spriteId, { x: 100, y: -100 });
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, parent, 'rotation', 90));
|
||
const box = f.add(20, 0, { w: 32, body: {}, parent });
|
||
const { seconds, entity } = run(f);
|
||
seconds(3);
|
||
const world = worldPositionOf(entity(box));
|
||
expect(world.x).toBeCloseTo(20, 1);
|
||
expect(world.y).toBeCloseTo(164, 0);
|
||
// Turned a quarter clockwise, the parent's own x axis points down the world's y.
|
||
expect(entity(box).x).toBeCloseTo(264, 0);
|
||
expect(Math.abs(Number(entity(box).props.rotation ?? 0))).toBeLessThan(0.5);
|
||
});
|
||
|
||
it('putting a falling object somewhere else keeps its speed', () => {
|
||
const f = physicsScene();
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { seconds, entity, physics } = run(f);
|
||
seconds(0.5);
|
||
const speed = physics.velocityOf(entity(box))!.y;
|
||
expect(speed).toBeGreaterThan(400);
|
||
entity(box).y = -500;
|
||
seconds(1 / 60);
|
||
expect(physics.velocityOf(entity(box))!.y).toBeGreaterThan(speed);
|
||
expect(entity(box).y).toBeLessThan(-490);
|
||
});
|
||
|
||
it('a scaled object rests on its grown shape', () => {
|
||
const f = physicsScene();
|
||
f.add(0, 200, GROUND);
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { seconds, entity } = run(f);
|
||
seconds(2);
|
||
expect(entity(box).y).toBeCloseTo(164, 0);
|
||
entity(box).props.scale = 2;
|
||
seconds(2);
|
||
expect(entity(box).y).toBeCloseTo(180 - 32, 0);
|
||
});
|
||
|
||
it('a kinematic body driven sideways pushes a dynamic box along the ground', () => {
|
||
const f = physicsScene();
|
||
f.add(0, 200, GROUND);
|
||
const box = f.add(0, 164, { w: 32, body: {} });
|
||
const paddle = f.add(-40, 160, { w: 16, h: 30, body: { bodyType: 'kinematic' } });
|
||
const { seconds, entity } = run(f);
|
||
seconds(0.5);
|
||
const start = entity(box).x;
|
||
seconds(1, () => {
|
||
entity(paddle).x += 2;
|
||
});
|
||
// The paddle travels 120; the box, 24 ahead of it, is shoved along for the rest of the way.
|
||
expect(entity(box).x - start).toBeGreaterThan(80);
|
||
// Flush would be 24. While shoving the two sink into each other by about a step's travel, and
|
||
// the box is drawn a step behind – measured 20 at 2 units a frame – but it stays in front.
|
||
expect(entity(box).x - entity(paddle).x).toBeGreaterThan(16);
|
||
});
|
||
|
||
it('a kinematic body goes where it is put and carries nothing of gravity', () => {
|
||
const f = physicsScene();
|
||
const lift = f.add(0, 0, { w: 64, h: 16, body: { bodyType: 'kinematic' } });
|
||
const { seconds, entity } = run(f);
|
||
seconds(0.5);
|
||
expect(entity(lift).y).toBe(0);
|
||
entity(lift).x = 40;
|
||
seconds(0.5);
|
||
expect(entity(lift).x).toBe(40);
|
||
});
|
||
});
|
||
|
||
describe('the engine’s clock', () => {
|
||
function fallAfter(frames: readonly number[]): number {
|
||
const f = physicsScene();
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { world, entity } = run(f);
|
||
for (const dt of frames) world.tick(dt);
|
||
return entity(box).y;
|
||
}
|
||
|
||
it('steps the same whatever the frames were, and never catches up more than a few steps', () => {
|
||
const even = fallAfter(Array.from({ length: 60 }, () => 1 / 60));
|
||
const uneven: number[] = [];
|
||
for (let left = 1; left > 1e-9;) {
|
||
const dt = Math.min(left, [0.004, 0.031, 0.012, 0.02, 0.009][uneven.length % 5]!);
|
||
uneven.push(dt);
|
||
left -= dt;
|
||
}
|
||
expect(Math.abs(fallAfter(uneven) - even)).toBeLessThan(0.5);
|
||
// A frame of a whole second is a hitch: the world moves a few steps on, not sixty.
|
||
expect(fallAfter([1])).toBeLessThan(fallAfter(Array.from({ length: 9 }, () => PHYSICS_STEP)));
|
||
});
|
||
});
|
||
|
||
describe('collision events from the engine', () => {
|
||
it('a body falling through a trigger enters and leaves it and is not stopped', () => {
|
||
const f = physicsScene();
|
||
f.add(0, 100, { w: 200, h: 20, collider: { isTrigger: true } });
|
||
const ball = f.add(0, 0, { w: 16, circle: true, body: {} });
|
||
const { seconds, entity, physics } = run(f);
|
||
const enters: CollisionPair[] = [];
|
||
const exits: CollisionPair[] = [];
|
||
seconds(1, () => {
|
||
enters.push(...physics.enters);
|
||
exits.push(...physics.exits);
|
||
});
|
||
expect(enters).toHaveLength(1);
|
||
expect(exits).toHaveLength(1);
|
||
expect(entity(ball).y).toBeGreaterThan(300);
|
||
});
|
||
|
||
it('On collision enter tells which way the other side pushes and how hard they met', () => {
|
||
const f = physicsScene();
|
||
const ground = f.add(0, 200, GROUND);
|
||
const ball = f.add(0, 0, { w: 32, circle: true, body: {} });
|
||
for (const [owner, name] of [
|
||
[ball, 'Ball'],
|
||
[ground, 'Ground'],
|
||
] as const) {
|
||
const behavior = makeBehavior(f, name);
|
||
place(f, behavior.id, { x: 0, y: 0 }, owner);
|
||
const edit = graphEditor(f, behavior);
|
||
const enter = edit.addNode('OnCollisionEnter');
|
||
const normal = edit.addNode('Log');
|
||
const impulse = edit.addNode('Log');
|
||
edit.wire([
|
||
[enter, 'exec', normal, 'exec'],
|
||
[enter, 'normalY', normal, 'message'],
|
||
[normal, 'exec', impulse, 'exec'],
|
||
[enter, 'impulse', impulse, 'message'],
|
||
]);
|
||
}
|
||
const { seconds, world } = run(f);
|
||
seconds(1);
|
||
// Each side once: its normal, then the impulse.
|
||
expect(world.log).toHaveLength(4);
|
||
const [ballNormal, i1, groundNormal, i2] = world.log.map(Number) as [number, number, number, number];
|
||
// The ball's behavior runs first. The ground pushes the ball up; the ball pushes the ground down.
|
||
expect(ballNormal).toBe(-1);
|
||
expect(groundNormal).toBe(1);
|
||
// The ball of 32 hits at about √(2·980·164) ≈ 567 per second, and its mass is π·0.16² ≈ 0.08.
|
||
expect(i1).toBeGreaterThan(20);
|
||
expect(i2).toBe(i1);
|
||
});
|
||
|
||
it('two objects without a rigidbody only collide while they overlap, not while they merely touch', () => {
|
||
const f = physicsScene();
|
||
const a = f.add(0, 0, { w: 100 });
|
||
const b = f.add(100, 0, { w: 100 });
|
||
const { seconds, entity, physics, world } = run(f);
|
||
seconds(1 / 60);
|
||
expect(physics.touching(world, entity(a), entity(b))).toBe(false);
|
||
entity(b).x = 99.5;
|
||
seconds(1 / 60);
|
||
expect(physics.enters).toHaveLength(1);
|
||
entity(b).x = 100;
|
||
seconds(1 / 60);
|
||
expect(physics.exits).toHaveLength(1);
|
||
});
|
||
|
||
it('a switched-off collider leaves the pair it was in', () => {
|
||
const f = physicsScene();
|
||
f.add(0, 200, GROUND);
|
||
const box = f.add(0, 0, { w: 32, body: {} });
|
||
const { seconds, entity, physics, world } = run(f);
|
||
seconds(2);
|
||
expect(physics.stays).toHaveLength(1);
|
||
const collider = world.entities.find((e: Entity) => e.type === 'collider' && e.parent === entity(box))!;
|
||
collider.props.enabled = false;
|
||
seconds(1 / 60);
|
||
expect(physics.exits).toHaveLength(1);
|
||
// And without a collider nothing holds the box up.
|
||
seconds(1);
|
||
expect(entity(box).y).toBeGreaterThan(300);
|
||
});
|
||
});
|