Files
feuerwerk/tests/core/collision-stay.test.ts
T

161 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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']);
});
});