Files
feuerwerk/tools/e2e/08-colliders.mjs
T
rootandClaude Fable 5.1 6471671f7d Refactor round: ship web/index.html, drop the remote browser, fix stale comments and README
- `.gitignore` said `index.html` unanchored, which also caught the Vite source page;
  a fresh clone could neither `dev` nor `build`. Anchored to `/index.html`, page added.
- Removed GM_REMOTE (tools/remote/gm-browser.ps1 and the ssh/scp branch in browser.mjs):
  the Windows test machine is gone and the only flaky check that lived on it is closed (#37).
- Removed `npm run preview`, undocumented and unused.
- Comments: wrong number in collider.test.ts (250 → 200), the "declarations used to…"
  history in listItems.ts and friends, the 24 px/28 contradiction, section banners in
  nodes/index.ts, node-catalog.test.ts and api/index.php, name paraphrases, and the stale
  step numbers in tools/e2e/11-…23- from the old global count.
- README: asset types include `animation`, kind order Number/Text/Boolean, tagged colour
  and texture items, 6/5 kinds, the `Texture` node, `./build.sh`, PROJECT_FORMAT_VERSION,
  and the file tree (kinds.ts, listItems.ts, propertyPorts.ts, Breadcrumb.ts, beads.ts,
  tools/assets/).
- tests/helpers.ts takes the duplicated `findObject` and `sceneWithCamera`.

Proof: ./check.sh 797 tests, full e2e ALL PASSED 633 checks against this bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 15:44:05 +02:00

134 lines
5.4 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;
// 8a. 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,
);
// 8b. 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.sceneObjects().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();
// 8c. 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');
}