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

+48 -3
View File
@@ -1,14 +1,26 @@
import { describe, expect, it } from 'vitest';
import { AddVariableCommand } from '../../src/core/commands/variableCommands';
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
import { sameValue, tagged, textOf } from '../../src/core/model/Properties';
import { type PropertyBag, sameValue, tagged, textOf } from '../../src/core/model/Properties';
import { findAsset, objectHolders, type Project } from '../../src/core/model/Project';
import { deserializeProject } from '../../src/core/store/serialize';
import { coerceValue, variableDef } from '../../src/core/properties/PropertySchema';
import { coerceField } from '../../src/core/properties/kinds';
import { deviatesFromAsset } from '../../src/core/properties/resolve';
import { asBoolean, asEntity, asStruct, writeTyped } from '../../src/core/graph';
import { fakeEntity, makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers';
import {
asBoolean,
asEntity,
asList,
asNumber,
asString,
asStruct,
storedList,
type Value,
writeTyped,
} from '../../src/core/graph';
import { IndexOf } from '../../src/core/graph/nodes/data/IndexOf';
import { NO_LOOKUP } from '../../src/core/graph/lookup';
import { fakeContext, fakeEntity, makeBehavior, makeSpriteScene, place, roundTrip } from '../helpers';
const findObject = (project: Project, id: string) =>
objectHolders(project)
@@ -93,6 +105,39 @@ describe('a list as a value', () => {
});
});
describe('a list carrying objects (#157)', () => {
const hero = fakeEntity('Hero', 10);
const crate = fakeEntity('Crate', 20);
it('hands the objects on as they are, and a lone object reads as a list of one', () => {
expect(asList([hero, crate])).toEqual([hero, crate]);
expect(asEntity(asList(hero)[0])).toBe(hero);
expect(storedList([hero, 3, crate])).toEqual(['Hero', 3, 'Crate']);
});
it('never lets a live object into a stored value', () => {
// The one door into a PropertyBag: whatever arrives is narrowed to what a stored list may hold.
const bag: PropertyBag = { seen: ['x'] };
writeTyped(bag, 'seen', [hero, crate]);
expect(bag.seen).toEqual(['Hero', 'Crate']);
expect(JSON.parse(JSON.stringify(bag))).toEqual({ seen: ['Hero', 'Crate'] });
});
it('reads as text by the names of its objects, not by what they are made of', () => {
expect(asString([hero, 3, crate])).toBe('Hero, 3, Crate');
});
it('is searched by identity, since two objects have no content to compare', () => {
const indexOf = (list: Value, value: Value) =>
asNumber(IndexOf.run!(fakeContext(), { list, value }, {}, IndexOf, 'exec', NO_LOOKUP).outputs?.index);
expect(indexOf([hero, crate], crate)).toBe(1);
expect(indexOf([hero, crate], fakeEntity('Crate', 20))).toBe(-1);
// A name is not the object of that name, and neither stands in for the other.
expect(indexOf([hero, crate], 'Crate')).toBe(-1);
expect(indexOf(['Hero', 'Crate'], crate)).toBe(-1);
});
});
describe('a struct as a value', () => {
it('compares by content, one level deep, and tells a different shape apart', () => {
expect(sameValue({ x: 1, tags: ['a'] }, { x: 1, tags: ['a'] })).toBe(true);