Files
feuerwerk/tools/e2e/08-colliders.mjs
T
rootandClaude Opus 5 d54a620ebd Add ESLint and Prettier, and close the node test gaps
Tooling:
- Prettier as the single formatting authority (110 columns, single quotes,
  measured against the existing style); Markdown excluded because table-cell
  padding turns the README glossary into 1000-character lines.
- Type-aware ESLint ending in eslint-config-prettier, so a rule always means
  "this is a bug", never "this looks different".
- .editorconfig and an `npm run check` that runs typecheck, lint, format and
  tests in one go.

Findings fixed on the first run: three more duplicate imports, four
String(unknown) calls (parameter types narrowed instead of casts), two dead
initialisers, two redundant casts, a console.log for a real error. Two
warnings were false here and are disabled with a reason in place.

Tests 132 -> 269:
- node-catalog.test.ts covers every node with a run(), with a guard that fails
  when a new node has no row.
- behavior-events.test.ts covers the ten event nodes through a running World,
  checking timing rather than existence.
- Input's state machine (press/release/releaseAll) is reachable without DOM
  events now; the DOM handlers are thin adapters.

Compare fell back to == without a configured operator although the schema
default is <; aligned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:02:22 +02:00

134 lines
5.5 KiB
JavaScript
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.
/**
* Colliders: "Add collider" from the context menu, collision events reaching the owner's
* behavior, and Move with "Collide with solids" – blocking, triggers, no tunnelling.
*/
export async function colliders(t) {
const {
check,
sleep,
evaluate,
json,
shot,
click,
clickText,
locate,
press,
holdKey,
undo,
mutate,
select,
state,
object,
objects,
asset,
world,
entity,
stop,
} = t;
// 15. Right-click the red crate → "Add collider": a collider asset sized like the crate
// (100×60) is placed as its child at the centre, selected and drawn in the overlay. The
// seed already gives both crates a "Crate collider" child (oc1/oc2), so the added one is
// undone again to keep exactly one collider per body for the checks below.
const objectsBefore = (await objects()).length;
const redRow = await locate('Red crate');
await click(redRow.x, redRow.y, 'right');
await clickText('Add collider');
await sleep(100);
const s = await state();
const colObj = s.selection?.kind === 'object' ? await object(s.selection.objectId) : null;
const colAsset = colObj ? await asset(colObj.assetId) : null;
check(
'Add collider creates a collider child sized like the sprite',
colObj?.parentId === 'o1' &&
colAsset?.type === 'collider' &&
colAsset.props.width === 100 &&
colAsset.props.height === 60,
JSON.stringify({ colObj, colAsset }),
);
check(
'collider is drawn in the overlay layer',
colObj && (await evaluate(`__gm.layerOf('${colObj.id}')`)) === 'overlay',
);
check('collider inspector shows the shape menu', Boolean(await evaluate(`__gm.locate('Box ▾')`)));
await shot('21-collider');
await undo();
const colliderAssetGone = colAsset ? !(await asset(colAsset.id)) : false;
check(
'undo removes the added collider object and its asset',
(await objects()).length === objectsBefore && colliderAssetGone,
);
// 15a. Wire "On collision enter → Log(other)" into the Mover (owner = red crate) and play with
// the crates overlapping: the log names the blue crate exactly once (enter, not stay),
// and colliders are not rendered in play mode.
await mutate(`
const mover = __gm.state.project.assets.find(a => a.id === 'mover');
mover.graph.nodes.push({ id: 'n-hit', type: 'OnCollisionEnter', x: 0, y: 600, params: {} }, { id: 'n-hitlog', type: 'Log', x: 340, y: 600, params: {} });
mover.graph.edges.push(
{ id: 'e-hit1', from: { node: 'n-hit', port: 'exec' }, to: { node: 'n-hitlog', port: 'exec' } },
{ id: 'e-hit2', from: { node: 'n-hit', port: 'other' }, to: { node: 'n-hitlog', port: 'message' } });
__gm.state.activeScene.objects.find(o => o.id === 'o2').position = { x: -100, y: 0, z: 0 }; // 50 from the red crate → 100-wide boxes overlap
__gm.state.select(null)`);
await press('Enter', { ctrl: true });
await sleep(500);
const collisionWorld = await world();
check(
'On collision enter fired once with the other body',
collisionWorld.log.filter((l) => l === 'Blue crate').length === 1,
JSON.stringify(collisionWorld.log),
);
const spriteCount = await evaluate(
`__gm.state.activeScene.objects.filter(o => __gm.state.project.assets.find(a => a.id === o.assetId)?.type === 'sprite').length`,
);
const info = await json('__gm.gameInfo()');
check(
'colliders are not rendered in the game view',
info.visibleItems === spriteCount,
`${JSON.stringify(info)} sprites=${spriteCount}`,
);
await stop();
// 15b. Solid vs trigger. Put the blue crate 150 to the right of the red one (red 100 wide, the
// blue collider 140 wide → 30 of free travel) and hold ArrowRight: the Mover's Move
// (Collide with solids) parks the red crate flush against the blue one at world x = −120.
// Making the blue collider a trigger lets it pass through instead (drawn amber). A frame
// step far larger than the collider must not jump across it either (sub-steps).
const playAgainstBlue = async (trigger, speed = 0) => {
await mutate(`
const scene = __gm.state.activeScene;
scene.objects.find(o => o.id === 'o2').position = { x: 0, y: 0, z: 0 };
scene.objects.find(o => o.id === 'oc2').overrides = ${trigger ? '{ width: 140, isTrigger: true }' : '{ width: 140 }'};
${speed ? `__gm.state.project.assets.find(a => a.id === 'mover').props.speed = ${speed};` : ''}
__gm.state.select(null)`);
await press('Enter', { ctrl: true });
await sleep(300);
await holdKey('ArrowRight', 700);
const red = await entity('o1');
await stop();
return red;
};
const blocked = await playAgainstBlue(false);
check(
'Move with "Collide with solids" stops the red crate flush against the blue crate\'s solid collider',
blocked && Math.abs(blocked.x - -120) < 0.01,
`world x=${blocked?.x}`,
);
const passed = await playAgainstBlue(true);
check(
'a trigger collider does not block (the crate passes through)',
passed && passed.x > -120 + 20,
`world x=${passed?.x}`,
);
const fast = await playAgainstBlue(false, 40000);
check(
'a very fast mover cannot tunnel through a solid collider',
fast && Math.abs(fast.x - -120) < 0.5,
`world x=${fast?.x}`,
);
await mutate(`__gm.state.project.assets.find(a => a.id === 'mover').props.speed = 200`); // later steps expect the seeded speed
await select({ kind: 'object', objectId: 'oc2' });
await sleep(200);
await shot('22-trigger-collider');
}