The new node "Raycast all" answers with every collider along the ray instead of only the nearest one: it carries the same inputs and settings as Raycast (Ignore owner, Hit triggers, Layers from #158) plus "max hits" where 0 means all, and it hands back parallel lists sorted by distance - objects, colliders, points x/y, normals x/y, distances - next to count, so index i of every list speaks about the same hit; a collider counts once no matter how many convex parts its shape is built from, while a tilemap counts per cell it is hit in, and the filter sits in Solids.raycastAll like the one of #158, the one path the editor run and Play both take, so all 12 new unit tests run through describe.each against both systems; the node needed the value system to learn something it could not do - a graph list carried numbers, text, booleans and bundles but no object handles, asList turned an object into its name, so objects/colliders would have been lists of names, which loses the object when two carry the same name, and collider children usually do; Value now knows a travelling list that may hold handles and "Item at" gives such an item out as an object, ready for Destroy, Apply force or Name of, while the single door into a stored file - writeTyped/asTypeOf - runs the list through the new storedList() so every object becomes its name again and nothing living reaches a project file, which is its own unit test over JSON.stringify; three list nodes follow the handles: "Index of" compares objects by identity because a name is not the object of that name, "Set item" and "Fill" write an object as an object, and "Join text" prints the names instead of the innards; this decision was mine to make because the question ran into the MCP timeout - the alternative was a list of names plus "Object by name", which is the smaller change but lossy; 4 new e2e checks in the new block 43e (shield and floor in one go reading 2 and "145, 280", the second object answering to its name, which only a real object in a list can do, "max hits" 1 stopping at the nearest, and the node showing the row) and 18 new unit tests, 12 in raycast.test.ts, 4 on the value system and 2 in the node catalog; two counter-checks hold: not counting tilemap cells one by one drops 2 unit tests, and answering with the nearest hit only drops module 43 with 2 FAILs; one finding on the way - the node read "maxHits" while every other input carries a spoken label, because int() took the key as its label, so it has a third parameter now, and the e2e check caught that, no unit test did (#157)
game-maker / gate (push) Successful in 2m4s
game-maker / suite (push) Successful in 7m26s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-20 18:54:12 +02:00
1 parent 0fdde6cb1e
commit ffdf0a8ddb
20 files changed
+607 -33

No files matched your search

+91
View File
@@ -72,6 +72,46 @@ const layerEyeGraph = {
],
};
/**
* Casts downwards like `layerEyeGraph`, but with `Raycast all` (#157): logs how many it met, the
* distances as one line, the name of the object behind the second hit – which only a live object in
* the list can answer – and what a second cast with "max hits" 1 comes back with.
*/
const allEyeGraph = {
nodes: [
node('qa-timer', 'OnTimer', 0, 320, { interval: 0.2 }),
node('qa-owner', 'Owner', 0, 160),
node('qa-where', 'GetPosition', 250, 160),
node('qa-ray', 'RaycastAll', 500, 0, { dirX: 0, dirY: 1, length: 500 }),
node('qa-count', 'Log', 750, 0),
node('qa-distances', 'Log', 1000, 0),
node('qa-second', 'ItemAt', 750, 300, { index: 1 }),
node('qa-name', 'NameOf', 1000, 300),
node('qa-log-name', 'Log', 1250, 0),
node('qa-one', 'RaycastAll', 500, 500, { dirX: 0, dirY: 1, length: 500, maxHits: 1 }),
node('qa-log-one', 'Log', 1500, 0),
],
edges: [
edge('qa-e1', 'qa-timer', 'exec', 'qa-ray', 'exec'),
edge('qa-e2', 'qa-owner', 'object', 'qa-where', 'object'),
edge('qa-e3', 'qa-where', 'worldX', 'qa-ray', 'fromX'),
edge('qa-e4', 'qa-where', 'worldY', 'qa-ray', 'fromY'),
edge('qa-e5', 'qa-ray', 'exec', 'qa-count', 'exec'),
edge('qa-e6', 'qa-ray', 'count', 'qa-count', 'message'),
edge('qa-e7', 'qa-count', 'exec', 'qa-distances', 'exec'),
edge('qa-e8', 'qa-ray', 'distances', 'qa-distances', 'message'),
edge('qa-e9', 'qa-ray', 'objects', 'qa-second', 'list'),
edge('qa-e10', 'qa-second', 'item', 'qa-name', 'object'),
edge('qa-e11', 'qa-distances', 'exec', 'qa-log-name', 'exec'),
edge('qa-e12', 'qa-name', 'name', 'qa-log-name', 'message'),
edge('qa-e13', 'qa-log-name', 'exec', 'qa-one', 'exec'),
edge('qa-e14', 'qa-where', 'worldX', 'qa-one', 'fromX'),
edge('qa-e15', 'qa-where', 'worldY', 'qa-one', 'fromY'),
edge('qa-e16', 'qa-one', 'exec', 'qa-log-one', 'exec'),
edge('qa-e17', 'qa-one', 'distances', 'qa-log-one', 'message'),
],
};
/** Logs how its owner touches the ground (normal y, then impulse) each half second it stays on it. */
const STAY = 0.5;
const feelGraph = {
@@ -359,4 +399,55 @@ export async function queries(t) {
);
await select(null);
await clear();
// 43e. Every hit on the line (#157): the same shield and ground, cast through by `Raycast all`.
// What it met comes out as parallel lists – the count, the distances, and the object behind
// the second hit, which only a live object in the list can be asked for.
const lastLines = (n) =>
consoleLog
.filter((line) => line.startsWith('[behavior All eyes]'))
.slice(-n)
.map((line) => line.split(']').pop().trim());
await setup(`
collider('q-col-ground', { width: 800, height: 40 });
collider('q-col-shield', { width: 300, height: 10 });
collider('q-col-box', {});
behavior('q-all', 'Query all', ${JSON.stringify(allEyeGraph)});
put('q-ground', 'crate', '', 0, ${Y + 300}, 'Query ground', { width: 800, height: 40 });
put('q-ground-col', 'q-col-ground', 'q-ground', 0, 0);
put('q-shield', 'crate', '', 0, ${Y + 150}, 'Shield', { width: 300, height: 10 });
put('q-shield-col', 'q-col-shield', 'q-shield', 0, 0);
put('q-eyebox', 'crate', '', 0, ${Y}, 'Eye', { width: 40, height: 40 });
put('q-eyebox-col', 'q-col-box', 'q-eyebox', 0, 0);
put('q-eyebox-go', 'q-all', 'q-eyebox', 0, 0, 'All eyes', { runInEditor: true });`);
await evaluate(`__gm.lookAt(0, ${Y + 150})`);
await sleep(700);
const [count, distances, behind, limited] = lastLines(4);
check(
'Raycast all meets the shield and the ground behind it, nearest first',
count === '2' && distances === `${SHIELD_TOP - Y}, ${GROUND_TOP - Y}`,
JSON.stringify({ count, distances, toShield: SHIELD_TOP - Y, toGround: GROUND_TOP - Y }),
);
check(
'the objects it met are objects, not their names: the second one answers where it is',
behind === 'Query ground',
JSON.stringify({ behind, lines: lastLines(4) }),
);
check(
'"max hits" 1 keeps the nearest of them and stops there',
limited === String(SHIELD_TOP - Y),
JSON.stringify({ limited, toShield: SHIELD_TOP - Y }),
);
await select({ kind: 'node', assetId: 'q-all', nodeId: 'qa-ray', nodeIds: ['qa-ray'] });
await settle();
const allInputs = await json("__gm.graph.nodeInputs('qa-ray')");
const allSettings = await json("__gm.graph.nodeSettings('qa-ray')");
check(
'the node takes a "max hits" of its own and carries the same Layers setting as Raycast (#158)',
allInputs.some((row) => row.startsWith('max hits')) && allSettings.includes('Layers: All layers'),
JSON.stringify({ allInputs, allSettings }),
);
await shot('43-ray-all');
await select(null);
await clear();
}