/** * 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'); 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 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 shot('22-trigger-collider'); }