454 lines
20 KiB
JavaScript
454 lines
20 KiB
JavaScript
import { edge, node } from '../lib/builders.mjs';
|
||
import { inView, LANES, MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* What a game asks the collision about, and platforms one jumps through: one-way colliders (#155),
|
||
* the Raycast node (#153) and On collision stay with an interval and contact data (#149). What only a
|
||
* running editor shows: the switch in the inspector, the engine and Move landing on a platform and
|
||
* passing it from below, the ray drawn in the Scene tab by the editor run, and a stay that fires on
|
||
* its own clock.
|
||
*/
|
||
|
||
const FALL = 300;
|
||
/** Falls with Move and "Collide with solids"; 1.5 s after it starts it drops through one-way platforms. */
|
||
const dropGraph = {
|
||
nodes: [
|
||
node('qd-update', 'OnUpdate', 0, 0),
|
||
node('qd-owner', 'Owner', 0, 160),
|
||
node('qd-move', 'Move', 300, 0, { dy: 1, scale: FALL, collide: true }),
|
||
node('qd-timer', 'OnTimer', 0, 320, { interval: 1.5 }),
|
||
node('qd-drop', 'IgnoreOneWay', 300, 320, { seconds: 0.3 }),
|
||
],
|
||
edges: [
|
||
edge('qd-e1', 'qd-update', 'exec', 'qd-move', 'exec'),
|
||
edge('qd-e2', 'qd-update', 'dt', 'qd-move', 'dt'),
|
||
edge('qd-e3', 'qd-owner', 'object', 'qd-move', 'object'),
|
||
edge('qd-e4', 'qd-timer', 'exec', 'qd-drop', 'exec'),
|
||
edge('qd-e5', 'qd-owner', 'object', 'qd-drop', 'object'),
|
||
],
|
||
};
|
||
|
||
/** Shoots its owner – a dynamic body below the platform – straight up in On start. */
|
||
const jumpGraph = {
|
||
nodes: [
|
||
node('qj-start', 'OnStart', 0, 0),
|
||
node('qj-owner', 'Owner', 0, 160),
|
||
node('qj-shoot', 'SetVelocity', 300, 0, { x: 0, y: -700 }),
|
||
],
|
||
edges: [
|
||
edge('qj-e1', 'qj-start', 'exec', 'qj-shoot', 'exec'),
|
||
edge('qj-e2', 'qj-owner', 'object', 'qj-shoot', 'object'),
|
||
],
|
||
};
|
||
|
||
/** Casts a ray straight down from its owner every frame. */
|
||
const eyeGraph = {
|
||
nodes: [
|
||
node('qe-update', 'OnUpdate', 0, 0),
|
||
node('qe-owner', 'Owner', 0, 160),
|
||
node('qe-where', 'GetPosition', 250, 160),
|
||
node('qe-ray', 'Raycast', 500, 0, { dirX: 0, dirY: 1, length: 500 }),
|
||
],
|
||
edges: [
|
||
edge('qe-e1', 'qe-update', 'exec', 'qe-ray', 'exec'),
|
||
edge('qe-e2', 'qe-owner', 'object', 'qe-where', 'object'),
|
||
edge('qe-e3', 'qe-where', 'worldX', 'qe-ray', 'fromX'),
|
||
edge('qe-e4', 'qe-where', 'worldY', 'qe-ray', 'fromY'),
|
||
],
|
||
};
|
||
|
||
/** Casts downwards like `eyeGraph` and logs how far it reached, so the answer needs no drawing. */
|
||
const layerEyeGraph = {
|
||
nodes: [
|
||
...eyeGraph.nodes,
|
||
node('qe-log', 'Log', 750, 0),
|
||
node('qe-timer', 'OnTimer', 0, 320, { interval: 0.2 }),
|
||
],
|
||
edges: [
|
||
...eyeGraph.edges.filter((e) => e.from.node !== 'qe-update'),
|
||
edge('qe-e5', 'qe-timer', 'exec', 'qe-ray', 'exec'),
|
||
edge('qe-e6', 'qe-ray', 'exec', 'qe-log', 'exec'),
|
||
edge('qe-e7', 'qe-ray', 'distance', 'qe-log', 'message'),
|
||
],
|
||
};
|
||
|
||
/**
|
||
* 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 = {
|
||
nodes: [
|
||
node('qf-stay', 'OnCollisionStay', 0, 0, { interval: STAY }),
|
||
node('qf-log-n', 'Log', 300, 0),
|
||
node('qf-log-i', 'Log', 550, 0),
|
||
],
|
||
edges: [
|
||
edge('qf-e1', 'qf-stay', 'exec', 'qf-log-n', 'exec'),
|
||
edge('qf-e2', 'qf-stay', 'normalY', 'qf-log-n', 'message'),
|
||
edge('qf-e3', 'qf-log-n', 'exec', 'qf-log-i', 'exec'),
|
||
edge('qf-e4', 'qf-stay', 'impulse', 'qf-log-i', 'message'),
|
||
],
|
||
};
|
||
|
||
export async function queries(t) {
|
||
const {
|
||
check,
|
||
json,
|
||
evaluate,
|
||
mutate,
|
||
select,
|
||
click,
|
||
object,
|
||
settle,
|
||
sleep,
|
||
play,
|
||
stop,
|
||
shot,
|
||
consoleLog,
|
||
} = t;
|
||
const Y = LANES.queries;
|
||
const GROUND_TOP = Y + 280;
|
||
const PLATFORM_TOP = Y + 95;
|
||
const setup = (body) =>
|
||
mutate(`
|
||
const scene = __gm.state.activeScene;
|
||
const assets = __gm.state.project.assets;
|
||
${MAKE_OBJECT}
|
||
const collider = (id, props) =>
|
||
assets.push({ id, type: 'collider', name: id, props: { shape: 'box', width: 40, height: 40, radius: 20, isTrigger: false, ...props } });
|
||
const behavior = (id, name, graph) => {
|
||
const asset = structuredClone(assets.find((a) => a.id === 'mover'));
|
||
Object.assign(asset, { id, name, props: { enabled: true, runInEditor: false }, variables: [], graph });
|
||
assets.push(asset);
|
||
};
|
||
const put = (id, assetId, parentId, x, y, name, overrides = {}) => {
|
||
const o = make(id, assetId, parentId, y, name);
|
||
o.position.x = x;
|
||
o.overrides = overrides;
|
||
scene.objects.push(o);
|
||
};
|
||
${body}
|
||
__gm.state.select(null)`);
|
||
const clear = () =>
|
||
mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('q-'));
|
||
p.assets = p.assets.filter((a) => !a.id.startsWith('q-'));
|
||
__gm.state.select(null)`);
|
||
const sample = async (seconds, ids) => {
|
||
await evaluate(`(() => {
|
||
const out = (window.__querySamples = []);
|
||
const start = performance.now();
|
||
const sample = () => {
|
||
const w = __gm.world();
|
||
if (!w) return;
|
||
const y = (id) => w.entities.find((e) => e.objectId === id)?.worldY;
|
||
out.push({ t: (performance.now() - start) / 1000, ...Object.fromEntries(${JSON.stringify(ids)}.map((id) => [id, y(id)])) });
|
||
if (performance.now() - start < ${seconds * 1000}) requestAnimationFrame(sample);
|
||
};
|
||
requestAnimationFrame(sample);
|
||
})()`);
|
||
await sleep(seconds * 1000 + 300);
|
||
return json('window.__querySamples');
|
||
};
|
||
|
||
// 43a. A platform one jumps through (#155), switched to one-way in the inspector. A crate falls
|
||
// onto it with Move and drops through 1.5 s later; a dynamic crate shot up from below
|
||
// passes it and lands on it.
|
||
await setup(`
|
||
collider('q-col-ground', { width: 800, height: 40 });
|
||
collider('q-col-platform', { width: 300, height: 10 });
|
||
collider('q-col-box', {});
|
||
assets.push({ id: 'q-rb', type: 'rigidbody', name: 'q-rb', props: { bodyType: 'dynamic', mass: 0, fixedRotation: true } });
|
||
behavior('q-drop', 'Query drop', ${JSON.stringify(dropGraph)});
|
||
behavior('q-jump', 'Query jump', ${JSON.stringify(jumpGraph)});
|
||
const box = { width: 40, height: 40 };
|
||
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-platform', 'crate', '', 0, ${Y + 100}, 'Query platform', { width: 300, height: 10 });
|
||
put('q-platform-col', 'q-col-platform', 'q-platform', 0, 0, 'Platform collider');
|
||
put('q-dropper', 'crate', '', -100, ${Y}, 'Dropper', box);
|
||
put('q-dropper-col', 'q-col-box', 'q-dropper', 0, 0);
|
||
put('q-dropper-go', 'q-drop', 'q-dropper', 0, 0);
|
||
put('q-jumper', 'crate', '', 100, ${Y + 200}, 'Jumper', box);
|
||
put('q-jumper-col', 'q-col-box', 'q-jumper', 0, 0);
|
||
put('q-jumper-rb', 'q-rb', 'q-jumper', 0, 0);
|
||
put('q-jumper-go', 'q-jump', 'q-jumper', 0, 0);`);
|
||
await select({
|
||
kind: 'object',
|
||
holderId: 'scene1',
|
||
objectId: 'q-platform-col',
|
||
objectIds: ['q-platform-col'],
|
||
});
|
||
const toggle = await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('One-way')");
|
||
// Side and angle mean nothing on a collider blocking from everywhere, so they wait for the switch (#159).
|
||
const hidden = await json(
|
||
"[__gm.inspectorField('Blocks from'), __gm.inspectorField('One-way angle')].every((f) => f === null)",
|
||
);
|
||
if (toggle) await click(toggle.x, toggle.y);
|
||
await settle();
|
||
const blocksFrom = await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('Blocks from')");
|
||
const angleRow = await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('One-way angle')");
|
||
check(
|
||
'Blocks from and One-way angle show only once One-way is switched on',
|
||
hidden === true && Boolean(blocksFrom) && Boolean(angleRow),
|
||
JSON.stringify({ hidden, blocksFrom, angleRow }),
|
||
);
|
||
check(
|
||
"a collider's One-way switch in the inspector turns it one-way, blocking from above",
|
||
Boolean(toggle) &&
|
||
Boolean(blocksFrom) &&
|
||
(await object('q-platform-col'))?.overrides?.oneWay === true &&
|
||
(await t.has('Above')),
|
||
JSON.stringify({ toggle, blocksFrom, overrides: (await object('q-platform-col'))?.overrides }),
|
||
);
|
||
await select(null);
|
||
await evaluate(`__gm.lookAt(0, ${Y + 150})`);
|
||
await settle();
|
||
await shot('43-one-way');
|
||
|
||
await play();
|
||
const run = await sample(2.8, ['q-dropper', 'q-jumper']);
|
||
await stop();
|
||
const at = (seconds) =>
|
||
run.reduce((best, s) => (Math.abs(s.t - seconds) < Math.abs(best.t - seconds) ? s : best));
|
||
const onPlatform = PLATFORM_TOP - 20;
|
||
const onGround = GROUND_TOP - 20;
|
||
check(
|
||
'Move lands on the one-way platform, and Ignore one-way drops it through onto the ground',
|
||
run.length > 60 &&
|
||
Math.abs(at(0.7)['q-dropper'] - onPlatform) < 0.01 &&
|
||
Math.abs(run.at(-1)['q-dropper'] - onGround) < 0.01,
|
||
JSON.stringify({ frames: run.length, early: at(0.7), last: run.at(-1), onPlatform, onGround }),
|
||
);
|
||
const highest = Math.min(...run.map((s) => s['q-jumper']));
|
||
check(
|
||
'a dynamic body shot up from below passes through the platform and lands on top of it',
|
||
highest < PLATFORM_TOP - 60 && Math.abs(run.at(-1)['q-jumper'] - onPlatform) < 1,
|
||
JSON.stringify({ highest, last: run.at(-1)['q-jumper'], onPlatform }),
|
||
);
|
||
await clear();
|
||
|
||
// 43b. The Raycast node in the editor run (#153): a crate casting down every frame, the ray drawn
|
||
// in the Scene tab to where it meets the ground.
|
||
await setup(`
|
||
collider('q-col-ground', { width: 800, height: 40 });
|
||
collider('q-col-box', {});
|
||
behavior('q-eye', 'Query eye', ${JSON.stringify(eyeGraph)});
|
||
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-eyebox', 'crate', '', 200, ${Y}, 'Eye', { width: 40, height: 40 });
|
||
put('q-eyebox-col', 'q-col-box', 'q-eyebox', 0, 0);
|
||
put('q-eyebox-go', 'q-eye', 'q-eyebox', 0, 0, 'Eye logic', { runInEditor: true });`);
|
||
await evaluate(`__gm.lookAt(0, ${Y + 150})`);
|
||
await sleep(500);
|
||
const rays = await json('__gm.rays()');
|
||
const ray = rays.at(-1);
|
||
check(
|
||
'the editor run casts the ray and the Scene tab draws it, down to the ground it met',
|
||
(await json('__gm.editorRun()')).running === true &&
|
||
ray?.hit === true &&
|
||
ray.from.x === 200 &&
|
||
ray.from.y === Y &&
|
||
Math.abs(ray.to.y - GROUND_TOP) < 1e-6,
|
||
JSON.stringify({ count: rays.length, ray, groundTop: GROUND_TOP }),
|
||
);
|
||
await shot('43-ray');
|
||
await clear();
|
||
await sleep(300);
|
||
|
||
// 43c. On collision stay with an interval (#149): a crate resting on the ground logs how it touches
|
||
// every half second – the normal from the ground towards it, and the impulse holding it up.
|
||
const logFrom = consoleLog.length;
|
||
await setup(`
|
||
collider('q-col-ground', { width: 800, height: 40 });
|
||
collider('q-col-box', {});
|
||
assets.push({ id: 'q-rb', type: 'rigidbody', name: 'q-rb', props: { bodyType: 'dynamic', mass: 0, fixedRotation: true } });
|
||
behavior('q-feel', 'Query feel', ${JSON.stringify(feelGraph)});
|
||
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-rest', 'crate', '', -300, ${Y + 220}, 'Rester', { width: 40, height: 40 });
|
||
put('q-rest-col', 'q-col-box', 'q-rest', 0, 0);
|
||
put('q-rest-rb', 'q-rb', 'q-rest', 0, 0);
|
||
put('q-rest-go', 'q-feel', 'q-rest', 0, 0, 'Feel');`);
|
||
await play();
|
||
await sleep(2200);
|
||
await stop();
|
||
const logged = consoleLog
|
||
.slice(logFrom)
|
||
.filter((line) => line.startsWith('[behavior Feel]'))
|
||
.map((line) => Number(line.split(']').pop()));
|
||
const pairs = Array.from({ length: Math.floor(logged.length / 2) }, (_, i) =>
|
||
logged.slice(2 * i, 2 * i + 2),
|
||
);
|
||
check(
|
||
'On collision stay with an interval fires every half second, with the normal and the impulse of the contact',
|
||
pairs.length >= 2 && pairs.length <= 4 && pairs.every(([ny, impulse]) => ny === -1 && impulse > 0),
|
||
JSON.stringify({ pairs, interval: STAY }),
|
||
);
|
||
await clear();
|
||
|
||
// 43d. Which layers a ray looks at (#158). A shield on a layer of its own stands between the
|
||
// caster and the ground: with every layer picked the ray stops at the shield, and once that
|
||
// layer is unticked in the node's "Layers" setting it goes straight through to the ground.
|
||
const SHIELD_TOP = Y + 145;
|
||
const reached = () => {
|
||
const seen = consoleLog.filter((line) => line.startsWith('[behavior Eye logic]'));
|
||
return seen.length === 0 ? null : Number(seen.at(-1).split(']').pop());
|
||
};
|
||
await setup(`
|
||
if (__gm.state.project.collisionLayers.length < 2) __gm.state.project.collisionLayers = ['Default', 'Shots'];
|
||
collider('q-col-ground', { width: 800, height: 40 });
|
||
collider('q-col-shield', { width: 300, height: 10, layer: 1 });
|
||
collider('q-col-box', {});
|
||
behavior('q-eye', 'Query eye', ${JSON.stringify(layerEyeGraph)});
|
||
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-eye', 'q-eyebox', 0, 0, 'Eye logic', { runInEditor: true });`);
|
||
await evaluate(`__gm.lookAt(0, ${Y + 150})`);
|
||
await sleep(500);
|
||
const blocked = reached();
|
||
check(
|
||
'a ray looking at every layer stops at the shield, whatever layer it is on',
|
||
blocked === SHIELD_TOP - Y,
|
||
JSON.stringify({ reached: blocked, toShield: SHIELD_TOP - Y, toGround: GROUND_TOP - Y }),
|
||
);
|
||
await select({ kind: 'node', assetId: 'q-eye', nodeId: 'qe-ray', nodeIds: ['qe-ray'] });
|
||
const layersRow = await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('Layers')");
|
||
check(
|
||
'the Raycast node carries a Layers setting, reading as every layer',
|
||
Boolean(layersRow) && (await t.has('All layers')),
|
||
JSON.stringify({ layersRow, allLayers: await t.has('All layers') }),
|
||
);
|
||
await click(layersRow.x, layersRow.y);
|
||
const layerTicks = await json('__gm.menuRows()');
|
||
const names = await json('__gm.state.project.collisionLayers');
|
||
check(
|
||
'the setting lists the layers of the project, all of them ticked',
|
||
layerTicks.length === names.length && layerTicks.every((row) => row.startsWith('☑')),
|
||
JSON.stringify({ layerTicks, names }),
|
||
);
|
||
await t.clickText(layerTicks[1]);
|
||
await shot('43-ray-layers');
|
||
await t.press('Escape');
|
||
await settle();
|
||
const settings = await json("__gm.graph.nodeSettings('qe-ray')");
|
||
check(
|
||
'the node itself spells the mask out by name rather than as the bits it is stored in',
|
||
settings.includes(`Layers: ${names[0]}`),
|
||
JSON.stringify(settings),
|
||
);
|
||
// A running instance keeps the graph it started with – measured here: the setting stands in the
|
||
// project at once, what the node casts does not. So the behavior is switched off and on, which
|
||
// ends the instance and builds a new one on the graph as it now stands.
|
||
await select({ kind: 'object', holderId: 'scene1', objectId: 'q-eyebox-go', objectIds: ['q-eyebox-go'] });
|
||
for (let i = 0; i < 2; i++) {
|
||
const runRow = await inView(t, '__gm.inspectorRect()', "__gm.inspectorField('Run in editor')");
|
||
await click(runRow.x, runRow.y);
|
||
await settle();
|
||
}
|
||
await sleep(600);
|
||
const through = reached();
|
||
check(
|
||
"unticking the shield's layer lets the ray pass it and meet the ground behind it",
|
||
through === GROUND_TOP - Y,
|
||
JSON.stringify({ reached: through, toGround: GROUND_TOP - Y, toShield: SHIELD_TOP - Y }),
|
||
);
|
||
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();
|
||
}
|