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
@@ -11,13 +11,25 @@ import {
|
||||
} from '../model/Properties';
|
||||
import { ENTITY_HANDLE, type EntityHandle } from './NodeContext';
|
||||
|
||||
/**
|
||||
* A list travelling along a data edge. Unlike a stored one it may hold live object handles, which is
|
||||
* what lets `Raycast all` hand out the objects it met (#157). Nothing of the sort reaches a
|
||||
* PropertyBag: everything written into one goes through `storedList`.
|
||||
*/
|
||||
export type ValueList = readonly (number | string | boolean | StructValue | EntityHandle)[];
|
||||
|
||||
/** A value travelling along a data edge. Objects are live entity handles (play mode only). */
|
||||
export type Value = PropertyValue | EntityHandle | null;
|
||||
export type Value = PropertyValue | EntityHandle | ValueList | null;
|
||||
|
||||
export function isEntity(value: Value | undefined): value is EntityHandle {
|
||||
return typeof value === 'object' && value !== null && ENTITY_HANDLE in value;
|
||||
}
|
||||
|
||||
/** A list of anything a data edge carries – a handle among the items is what `isList` cannot say. */
|
||||
export function isValueList(value: Value | undefined): value is ValueList {
|
||||
return Array.isArray(value);
|
||||
}
|
||||
|
||||
export function asNumber(value: Value | undefined): number {
|
||||
if (typeof value === 'number') return value;
|
||||
if (typeof value === 'boolean') return value ? 1 : 0;
|
||||
@@ -32,14 +44,24 @@ export function asNumber(value: Value | undefined): number {
|
||||
}
|
||||
|
||||
/** Anything that is not a list reads as a one-item list, so a lone value can feed a list input. */
|
||||
export function asList(value: Value | undefined): ListValue {
|
||||
if (isList(value)) return value;
|
||||
export function asList(value: Value | undefined): ValueList {
|
||||
if (isValueList(value)) return value;
|
||||
if (value === null || value === undefined) return [];
|
||||
if (isEntity(value)) return [value.name];
|
||||
if (isEntity(value)) return [value];
|
||||
if (isStructValue(value)) return Object.values(value).map(asListItem);
|
||||
return [value];
|
||||
}
|
||||
|
||||
/** A list as a PropertyBag may hold it: an object handle becomes its name, since nothing stored is live. */
|
||||
export function storedList(items: ValueList): ListValue {
|
||||
return items.map(asListItem);
|
||||
}
|
||||
|
||||
/** A value narrowed to what a *travelling* list may hold: unlike `asListItem`, a handle stays one. */
|
||||
export function asValueItem(value: Value | undefined): ValueList[number] {
|
||||
return isEntity(value) ? value : asListItem(value);
|
||||
}
|
||||
|
||||
/** A value narrowed to what a list may hold: a struct stays one, handles and lists flatten. */
|
||||
export function asListItem(value: Value | undefined): number | string | boolean | StructValue {
|
||||
if (isStructValue(value)) return value;
|
||||
@@ -64,7 +86,9 @@ export function asBoolean(value: Value | undefined): boolean {
|
||||
/** Entities read as their name, so a handle can be logged or joined into text directly. */
|
||||
export function asString(value: Value | undefined): string {
|
||||
if (value === null) return '';
|
||||
return isEntity(value) ? value.name : textOf(value);
|
||||
if (isEntity(value)) return value.name;
|
||||
// A handle among the items would otherwise be printed as the object it is made of.
|
||||
return textOf(isValueList(value) ? storedList(value) : value);
|
||||
}
|
||||
|
||||
export function asEntity(value: Value | undefined): EntityHandle | null {
|
||||
@@ -94,7 +118,7 @@ export function mergeStruct(current: StructValue, value: Value | undefined): Str
|
||||
|
||||
/** `value` converted to the type the slot already holds, so a number stays a number. */
|
||||
export function asTypeOf(current: PropertyValue | undefined, value: Value | undefined): PropertyValue {
|
||||
if (isList(current)) return asList(value);
|
||||
if (isList(current)) return storedList(asList(value));
|
||||
if (isStruct(current)) return mergeStruct(current, value);
|
||||
if (typeof current === 'number') return asNumber(value);
|
||||
if (typeof current === 'boolean') return asBoolean(value);
|
||||
|
||||
Reference in new issue
Block a user