161 lines
6.4 KiB
TypeScript
161 lines
6.4 KiB
TypeScript
import { beforeAll, describe, expect, it } from 'vitest';
|
||
import { SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||
import { AddColliderCommand, AddRigidbodyCommand } from '../../src/core/commands/colliderCommands';
|
||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||
import type { BehaviorAsset } from '../../src/core/model/Asset';
|
||
import type { GraphNode } from '../../src/core/graph';
|
||
import { loadPhysics } from '../../src/runtime/physics/engine';
|
||
import {
|
||
COLLISION_SYSTEMS,
|
||
graphEditor,
|
||
makeBehavior,
|
||
makeSpriteScene,
|
||
place,
|
||
type SceneFixture,
|
||
startWorld,
|
||
} from '../helpers';
|
||
|
||
beforeAll(async () => {
|
||
await loadPhysics();
|
||
});
|
||
|
||
/** Wires `event → Log(port) → Log(port) …`, one entry in `world.log` per port, or `message` when none. */
|
||
function logOn(
|
||
f: SceneFixture,
|
||
behavior: BehaviorAsset,
|
||
event: string,
|
||
params: Record<string, number>,
|
||
ports: string[],
|
||
message = event,
|
||
): GraphNode {
|
||
const edit = graphEditor(f, behavior);
|
||
const node = edit.addNode(event, params);
|
||
let from = node;
|
||
for (const port of ports.length > 0 ? ports : ['']) {
|
||
const log = edit.addNode('Log', port ? {} : { message });
|
||
edit.connect(from, 'exec', log, 'exec');
|
||
if (port) edit.connect(node, port, log, 'message');
|
||
from = log;
|
||
}
|
||
return node;
|
||
}
|
||
|
||
/** Two 100 × 60 crates with a collider each, the second 300 to the right; a behavior sits on the first. */
|
||
function crates() {
|
||
const f = makeSpriteScene('Crate');
|
||
f.stack.execute(new SetAssetPropertyCommand(f.spriteId, 'height', 60));
|
||
const behavior = makeBehavior(f, 'Hit');
|
||
const a = place(f, f.spriteId, { x: 0, y: 0 });
|
||
const b = place(f, f.spriteId, { x: 300, y: 0 });
|
||
f.stack.execute(new AddColliderCommand(f.sceneId, a));
|
||
f.stack.execute(new AddColliderCommand(f.sceneId, b));
|
||
place(f, behavior.id, { x: 0, y: 0 }, a);
|
||
return { ...f, behavior, b };
|
||
}
|
||
|
||
describe.each(COLLISION_SYSTEMS)('$system: On collision stay with an interval (#149)', ({ physics }) => {
|
||
it('fires each time the interval has passed since the pair met, and starts over when they meet again', () => {
|
||
const f = crates();
|
||
logOn(f, f.behavior, 'OnCollisionStay', { interval: 0.5 }, [], 'slow');
|
||
logOn(f, f.behavior, 'OnCollisionStay', {}, [], 'each');
|
||
const { world, frame } = startWorld(f, { physics, collisions: true, behaviors: true });
|
||
const other = world.findByObjectId(f.b)!;
|
||
const slow = () => world.log.filter((line) => line === 'slow').length;
|
||
const each = () => world.log.filter((line) => line === 'each').length;
|
||
|
||
other.x = 50;
|
||
frame(0.1); // they meet
|
||
for (let i = 0; i < 4; i++) frame(0.1);
|
||
expect([slow(), each()]).toEqual([0, 4]);
|
||
frame(0.1); // half a second since they met
|
||
expect([slow(), each()]).toEqual([1, 5]);
|
||
for (let i = 0; i < 5; i++) frame(0.1);
|
||
expect([slow(), each()]).toEqual([2, 10]);
|
||
frame(0.35); // a long frame counts as long as it is
|
||
frame(0.35);
|
||
expect(slow()).toBe(3);
|
||
|
||
other.x = 500;
|
||
frame(0.1);
|
||
other.x = 50;
|
||
frame(0.1); // they meet again: the clock starts over
|
||
for (let i = 0; i < 4; i++) frame(0.1);
|
||
expect(slow()).toBe(3);
|
||
frame(0.1);
|
||
expect(slow()).toBe(4);
|
||
});
|
||
});
|
||
|
||
describe('what On collision stay knows of the contact (#149)', () => {
|
||
/** A 32 px crate with a rigidbody falling onto a 400 × 40 ground whose top is at y = 180. */
|
||
function landing(density = 1) {
|
||
const f = makeSpriteScene('Box');
|
||
const behavior = makeBehavior(f, 'Feel');
|
||
const size = (id: string, w: number, h: number) => {
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'width', w));
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'height', h));
|
||
};
|
||
const ground = place(f, f.spriteId, { x: 0, y: 200 });
|
||
size(ground, 400, 40);
|
||
const groundCollider = new AddColliderCommand(f.sceneId, ground);
|
||
f.stack.execute(groundCollider);
|
||
size(groundCollider.objectId, 400, 40);
|
||
const box = place(f, f.spriteId, { x: 10, y: 100 });
|
||
size(box, 32, 32);
|
||
const boxCollider = new AddColliderCommand(f.sceneId, box);
|
||
f.stack.execute(boxCollider);
|
||
size(boxCollider.objectId, 32, 32);
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, boxCollider.objectId, 'density', density));
|
||
const rigidbody = new AddRigidbodyCommand(f.sceneId, box);
|
||
f.stack.execute(rigidbody);
|
||
f.stack.execute(new SetOverrideCommand(f.sceneId, rigidbody.objectId, 'fixedRotation', true));
|
||
place(f, behavior.id, { x: 0, y: 0 }, box);
|
||
return { ...f, behavior };
|
||
}
|
||
|
||
it('while playing: the normal from the ground towards the box, the impulse that holds it up and the contact point', () => {
|
||
const rest = (density: number) => {
|
||
const f = landing(density);
|
||
logOn(f, f.behavior, 'OnCollisionStay', { interval: 1 }, [
|
||
'normalX',
|
||
'normalY',
|
||
'impulse',
|
||
'pointX',
|
||
'pointY',
|
||
]);
|
||
const { world, frame } = startWorld(f, { physics: true, collisions: true, behaviors: true });
|
||
for (let i = 0; i < 150; i++) frame(1 / 60);
|
||
return world.log.slice(-5).map(Number);
|
||
};
|
||
const [nx, ny, light, px, py] = rest(1);
|
||
expect([nx, ny]).toEqual([0, -1]);
|
||
expect(px).toBeCloseTo(10, 3);
|
||
expect(py).toBeCloseTo(180, 1);
|
||
// The engine's own number: it is not exactly m·g·dt (1.25 times that, measured), but it grows with the weight.
|
||
const [, , heavy] = rest(3);
|
||
expect(light).toBeGreaterThan(0);
|
||
expect(heavy! / light!).toBeCloseTo(3, 3);
|
||
});
|
||
|
||
it('On collision enter says where the pair met as well', () => {
|
||
const f = landing();
|
||
logOn(f, f.behavior, 'OnCollisionEnter', {}, ['pointX', 'pointY']);
|
||
const { world, frame } = startWorld(f, { physics: true, collisions: true, behaviors: true });
|
||
for (let i = 0; i < 60; i++) frame(1 / 60);
|
||
const [px, py] = world.log.slice(0, 2).map(Number);
|
||
expect(px).toBeCloseTo(10, 3);
|
||
expect(Math.abs(py! - 180)).toBeLessThan(2);
|
||
});
|
||
|
||
it('in the editor run nothing is simulated: the contact outputs stay 0', () => {
|
||
const f = landing();
|
||
logOn(f, f.behavior, 'OnCollisionStay', {}, ['normalY', 'impulse', 'pointY']);
|
||
const { world, frame } = startWorld(f, { collisions: true, behaviors: true });
|
||
const box = world.entities.find((e) => e.type === 'sprite' && e.y === 100)!;
|
||
box.y = 170; // overlapping the ground
|
||
frame(1 / 60);
|
||
frame(1 / 60);
|
||
expect(world.log).toEqual(['0', '0', '0']);
|
||
});
|
||
});
|