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)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0fdde6cb1e
commit
ffdf0a8ddb
20 files changed
+607
-33
No files matched your search
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user