From b0860ca07f1612c6d781b00a1d91251466c95247 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 7 Sep 2026 15:50:52 +0200 Subject: [PATCH] Merge Move and slide into Move with a 'Collide with solids' toggle Co-Authored-By: Claude Fable 5.1 --- README.md | 11 ++++++----- src/core/graph/nodes.ts | 33 +++++++++++---------------------- tests/core/collider.test.ts | 6 +++--- tools/e2e.mjs | 4 ++-- tools/seed-demo.mjs | 4 ++-- 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index d5518f39..321da0ab 100644 --- a/README.md +++ b/README.md @@ -103,15 +103,16 @@ and `collider` (the own one). `Is touching(a, b)` answers the same question on d object stands for its collider children, a collider for itself. The geometry lives in `src/core/properties/collider.ts` so editor and runtime agree on it. -**Blocking** is opt-in per movement: the `Move and slide` node (same inputs as `Move`) moves -an object one axis at a time and, after each axis, pushes its *solid* colliders back to just -touch any solid collider they ran into – so the other axis still moves and the object slides -along walls. Its `blocked x` / `blocked y` outputs tell which axes hit. A collider with +**Blocking** is opt-in per movement: the `Move` node has a **Collide with solids** setting +(default off). When enabled it moves an object one axis at a time and, after each axis, pushes +its *solid* colliders back to just touch any solid collider they ran into – so the other axis +still moves and the object slides along walls. Its `blocked x` / `blocked y` outputs tell which +axes hit (always `false` with the setting off). A collider with **Trigger (no blocking)** enabled (`isTrigger`, drawn amber instead of green) still produces collision events but never blocks. Pairs that already overlapped before the step are left alone, so an object that starts inside a wall can leave it and stacked objects never jolt apart. Colliders of the moving object and of anything parented under it travel with it and cannot -block it. Plain `Move` / `Set position` ignore colliders entirely. +block it. `Move` without the setting and `Set position` ignore colliders entirely. ## Stack diff --git a/src/core/graph/nodes.ts b/src/core/graph/nodes.ts index bc6e70e1..456a6f48 100644 --- a/src/core/graph/nodes.ts +++ b/src/core/graph/nodes.ts @@ -293,35 +293,24 @@ registerNode({ type: 'Move', label: 'Move', category: 'action', - description: 'Moves an object by (dx, dy) × scale × dt. Connect dt to an event\'s dt for per-second movement; leave it at 1 for per-frame.', - inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }], - outputs: [exec()], - params: [], - run: (_ctx, i) => { - const target = asEntity(i.object); - if (target) { - const factor = asNumber(i.scale) * asNumber(i.dt); - target.x += asNumber(i.dx) * factor; - target.y += asNumber(i.dy) * factor; - } - return { next: 'exec' }; - }, -}); -registerNode({ - type: 'MoveAndSlide', - label: 'Move and slide', - category: 'action', - description: 'Like Move, but solid colliders of the object stop at other solid colliders and slide along them. Outputs which axes were blocked.', + description: 'Moves an object by (dx, dy) × scale × dt. Connect dt to an event\'s dt for per-second movement; leave it at 1 for per-frame. With "Collide with solids" on, the object\'s solid colliders stop at other solid colliders and slide along them; blocked x/y tell which axes hit.', inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }], outputs: [exec(), bool('blockedX', 'blocked x'), bool('blockedY', 'blocked y')], - params: [], - run: (ctx, i) => { + params: [{ key: 'collide', label: 'Collide with solids', kind: 'boolean', default: false }], + run: (ctx, i, p) => { const target = asEntity(i.object); let blockedX = false; let blockedY = false; if (target) { const factor = asNumber(i.scale) * asNumber(i.dt); - ({ blockedX, blockedY } = ctx.moveAndSlide(target, asNumber(i.dx) * factor, asNumber(i.dy) * factor)); + const dx = asNumber(i.dx) * factor; + const dy = asNumber(i.dy) * factor; + if (asBoolean(p.collide)) { + ({ blockedX, blockedY } = ctx.moveAndSlide(target, dx, dy)); + } else { + target.x += dx; + target.y += dy; + } } return { next: 'exec', outputs: { blockedX, blockedY } }; }, diff --git a/tests/core/collider.test.ts b/tests/core/collider.test.ts index 870133b7..ec3ffdf2 100644 --- a/tests/core/collider.test.ts +++ b/tests/core/collider.test.ts @@ -206,12 +206,12 @@ describe('move and slide', () => { expect(crateA.x).toBe(-95); }); - it('the Move and slide node moves the owner and outputs the blocked axes', () => { + it('the Move node with "Collide with solids" moves the owner and outputs the blocked axes', () => { const { project, stack, sceneId, behavior, a } = setup(); - // On update → Move and slide(Owner, dx = 1000, dt) → Log(blocked x) + // On update → Move(Owner, dx = 1000, dt, collide) → Log(blocked x) const update = new AddNodeCommand(behavior.id, 'OnUpdate', 0, 0); const owner = new AddNodeCommand(behavior.id, 'Owner', 0, 0); - const move = new AddNodeCommand(behavior.id, 'MoveAndSlide', 0, 0, { dx: 1000 }); + const move = new AddNodeCommand(behavior.id, 'Move', 0, 0, { dx: 1000, collide: true }); const log = new AddNodeCommand(behavior.id, 'Log', 0, 0); for (const c of [update, owner, move, log]) stack.execute(c); const connect = (from: AddNodeCommand, fp: string, to: AddNodeCommand, tp: string) => diff --git a/tools/e2e.mjs b/tools/e2e.mjs index bda62ddb..6d3f5967 100644 --- a/tools/e2e.mjs +++ b/tools/e2e.mjs @@ -582,7 +582,7 @@ try { await sleep(200); // 15b. Solid vs trigger. Put the blue crate 150 to the right of the red one (both 100 wide → - // 50 of free travel) and hold ArrowRight: the Mover's "Move and slide" parks the red + // 50 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 = −100. Making the blue collider a // trigger lets it pass through instead (the collider is then drawn amber). const holdRight = async (ms) => { @@ -607,7 +607,7 @@ try { return w.entities.find((e) => e.objectId === 'o1'); }; const blocked = await playAgainstBlue(false); - check('Move and slide stops the red crate flush against the blue crate\'s solid collider', blocked && Math.abs(blocked.x - -100) < 0.01, `world x=${blocked?.x}`); + check('Move with "Collide with solids" stops the red crate flush against the blue crate\'s solid collider', blocked && Math.abs(blocked.x - -100) < 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 > -100 + 20, `world x=${passed?.x}`); await evaluate(`__gm.state.select({ kind: 'object', objectId: 'oc2' })`); diff --git a/tools/seed-demo.mjs b/tools/seed-demo.mjs index 9b11531d..d4f58044 100644 --- a/tools/seed-demo.mjs +++ b/tools/seed-demo.mjs @@ -14,7 +14,7 @@ if (!projectId) { * "Mover" behavior in five nodes: every frame, move the owner along the normalized arrow-key * axis, scaled by the `speed` variable per second (On update's dt feeds the dt input). * Diagonals are not faster than straight moves because Axis input normalizes the vector, and - * "Move and slide" stops the owner's solid colliders at other solid colliders. + * Move's "Collide with solids" stops the owner's solid colliders at other solid colliders. */ const moverGraph = { nodes: [ @@ -22,7 +22,7 @@ const moverGraph = { { id: 'n-owner', type: 'Owner', x: 0, y: 120, params: {} }, { id: 'n-axis', type: 'AxisInput', x: 0, y: 200, params: {} }, { id: 'n-speed', type: 'GetVariable', x: 0, y: 400, params: { variable: 'speed' } }, - { id: 'n-move', type: 'MoveAndSlide', x: 340, y: 0, params: {} }, + { id: 'n-move', type: 'Move', x: 340, y: 0, params: { collide: true } }, ], edges: [ { id: 'e1', from: { node: 'n-update', port: 'exec' }, to: { node: 'n-move', port: 'exec' } },