Let a variable hold a list
A variable could only be a number, a text or a yes/no, so every set of values had to be smuggled through a string - which is where Tetris got its unreadable "03066326048763714" colours from. A list is now a value of its own, with an item kind (number, text, yes/no, colour) that decides how the inspector reads the one comma-separated line it is typed as, and eight nodes to work with: List, Fill, Count, Item at, Set item, Sub list, Join lists, Index of. Two traps came with it. An array is typeof 'object', so every place that told a value from an entity had to learn the difference, or a list would have been passed around as a game object. And a value was compared by identity in three places - with lists that marks everything as an overridden property after each reload, so those comparisons now compare contents. There is no list of objects: a list is written to the project file and an object is a live handle that no reload survives. A set of objects stays a list of names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c6f907b4e0
commit
a9c3111377
25 files changed
+469
-25
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { PropertyBag } from '../model/Properties';
|
||||
|
||||
/** `exec` carries control flow, the others values; `any` accepts every value type. */
|
||||
export type PortType = 'exec' | 'number' | 'boolean' | 'string' | 'object' | 'any';
|
||||
export type PortType = 'exec' | 'number' | 'boolean' | 'string' | 'object' | 'list' | 'any';
|
||||
|
||||
export interface PortRef {
|
||||
node: string;
|
||||
@@ -31,7 +31,7 @@ export interface DeclaredPort {
|
||||
kind: DeclaredPortKind;
|
||||
}
|
||||
|
||||
export const DECLARED_PORT_KINDS = ['number', 'boolean', 'text', 'object'] as const;
|
||||
export const DECLARED_PORT_KINDS = ['number', 'boolean', 'text', 'object', 'list'] as const;
|
||||
export type DeclaredPortKind = (typeof DECLARED_PORT_KINDS)[number];
|
||||
|
||||
export function declaredPortType(kind: DeclaredPortKind): PortType {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList } from '../../values';
|
||||
import { list } from '../shared';
|
||||
|
||||
export const Concat: NodeDef = {
|
||||
type: 'Concat',
|
||||
label: 'Join lists',
|
||||
category: 'data',
|
||||
description: 'Two lists one after the other. A single value counts as a list of one.',
|
||||
inputs: [list('a'), list('b')],
|
||||
outputs: [list('result')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => ({ outputs: { result: [...asList(inputs.a), ...asList(inputs.b)] } }),
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList } from '../../values';
|
||||
import { list, num } from '../shared';
|
||||
|
||||
export const Count: NodeDef = {
|
||||
type: 'Count',
|
||||
label: 'Count',
|
||||
category: 'data',
|
||||
description: 'How many items a list holds.',
|
||||
inputs: [list('list')],
|
||||
outputs: [num('count')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => ({ outputs: { count: asList(inputs.list).length } }),
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asListItem, asNumber } from '../../values';
|
||||
import { any, list, num, withDefault } from '../shared';
|
||||
|
||||
/** Same order of magnitude as the interpreter's step limit – a typo in `count` must not eat memory. */
|
||||
const MAX_ITEMS = 100_000;
|
||||
|
||||
export const Fill: NodeDef = {
|
||||
type: 'Fill',
|
||||
label: 'Fill',
|
||||
category: 'data',
|
||||
description: 'A new list of the same value repeated – how an empty board is made.',
|
||||
inputs: [any('value'), withDefault(num('count'), 1)],
|
||||
outputs: [list('result')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const count = Math.min(MAX_ITEMS, Math.max(0, Math.floor(asNumber(inputs.count))));
|
||||
return { outputs: { result: new Array(count).fill(asListItem(inputs.value)) } };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asListItem } from '../../values';
|
||||
import { any, list, num } from '../shared';
|
||||
|
||||
export const IndexOf: NodeDef = {
|
||||
type: 'IndexOf',
|
||||
label: 'Index of',
|
||||
category: 'data',
|
||||
description: 'Where a value sits in a list, or -1 if it is not in there – how "is this row full" is asked.',
|
||||
inputs: [list('list'), any('value')],
|
||||
outputs: [num('index')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => ({
|
||||
outputs: { index: asList(inputs.list).indexOf(asListItem(inputs.value)) },
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asNumber } from '../../values';
|
||||
import { any, list, num } from '../shared';
|
||||
|
||||
export const ItemAt: NodeDef = {
|
||||
type: 'ItemAt',
|
||||
label: 'Item at',
|
||||
category: 'data',
|
||||
description: 'The item at an index, counting from 0. Out of range gives 0.',
|
||||
inputs: [list('list'), num('index')],
|
||||
outputs: [any('item')],
|
||||
run: (_ctx, inputs) => {
|
||||
const items = asList(inputs.list);
|
||||
const i = Math.floor(asNumber(inputs.index));
|
||||
return { outputs: { item: items[i] ?? 0 } };
|
||||
},
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList } from '../../values';
|
||||
import { list } from '../shared';
|
||||
|
||||
export const ListLiteral: NodeDef = {
|
||||
type: 'List',
|
||||
label: 'List',
|
||||
category: 'data',
|
||||
description: 'A constant list, typed as one comma-separated line.',
|
||||
inputs: [],
|
||||
outputs: [list('value')],
|
||||
params: [{ key: 'value', label: 'Value', kind: 'list', default: [], items: 'number' }],
|
||||
run: (_ctx, _inputs, params) => ({ outputs: { value: asList(params.value) } }),
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asListItem, asNumber } from '../../values';
|
||||
import { any, list, num } from '../shared';
|
||||
|
||||
export const SetItem: NodeDef = {
|
||||
type: 'SetItem',
|
||||
label: 'Set item',
|
||||
category: 'data',
|
||||
description: 'The same list with one item replaced. The list that came in is left alone.',
|
||||
inputs: [list('list'), num('index'), any('value')],
|
||||
outputs: [list('result')],
|
||||
run: (_ctx, inputs) => {
|
||||
const items = asList(inputs.list);
|
||||
const i = Math.floor(asNumber(inputs.index));
|
||||
if (i < 0 || i >= items.length) return { outputs: { result: items } };
|
||||
const next = [...items];
|
||||
next[i] = asListItem(inputs.value);
|
||||
return { outputs: { result: next } };
|
||||
},
|
||||
params: [],
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { NodeDef } from '../../NodeRegistry';
|
||||
import { asList, asNumber } from '../../values';
|
||||
import { list, num, withDefault } from '../shared';
|
||||
|
||||
export const SubList: NodeDef = {
|
||||
type: 'SubList',
|
||||
label: 'Sub list',
|
||||
category: 'data',
|
||||
description: 'A part of a list: "count" items starting at "start".',
|
||||
inputs: [list('list'), num('start'), withDefault(num('count'), 1)],
|
||||
outputs: [list('result')],
|
||||
params: [],
|
||||
run: (_ctx, inputs) => {
|
||||
const items = asList(inputs.list);
|
||||
const start = Math.max(0, Math.floor(asNumber(inputs.start)));
|
||||
const count = Math.max(0, Math.floor(asNumber(inputs.count)));
|
||||
return { outputs: { result: items.slice(start, start + count) } };
|
||||
},
|
||||
};
|
||||
@@ -55,6 +55,14 @@ import { Subtract } from './data/Subtract';
|
||||
import { Time } from './data/Time';
|
||||
import { ToNumber } from './data/ToNumber';
|
||||
import { ToText } from './data/ToText';
|
||||
import { Concat } from './data/Concat';
|
||||
import { Count } from './data/Count';
|
||||
import { Fill } from './data/Fill';
|
||||
import { IndexOf } from './data/IndexOf';
|
||||
import { ItemAt } from './data/ItemAt';
|
||||
import { ListLiteral } from './data/ListLiteral';
|
||||
import { SetItem } from './data/SetItem';
|
||||
import { SubList } from './data/SubList';
|
||||
import { OnClick } from './events/OnClick';
|
||||
import { OnCollisionEnter } from './events/OnCollisionEnter';
|
||||
import { OnCollisionExit } from './events/OnCollisionExit';
|
||||
@@ -140,6 +148,15 @@ export const BUILT_IN_NODES: readonly NodeDef[] = [
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
// data: lists
|
||||
ListLiteral,
|
||||
Fill,
|
||||
Count,
|
||||
ItemAt,
|
||||
SetItem,
|
||||
SubList,
|
||||
Concat,
|
||||
IndexOf,
|
||||
// data: vectors and input
|
||||
Length,
|
||||
Distance,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const num = (key: string, label = key): PortDef => ({ key, label, type: '
|
||||
export const bool = (key: string, label = key): PortDef => ({ key, label, type: 'boolean' });
|
||||
export const str = (key: string, label = key): PortDef => ({ key, label, type: 'string' });
|
||||
export const obj = (key: string, label = key): PortDef => ({ key, label, type: 'object' });
|
||||
export const list = (key: string, label = key): PortDef => ({ key, label, type: 'list' });
|
||||
export const any = (key: string, label = key): PortDef => ({ key, label, type: 'any' });
|
||||
|
||||
export const withDefault = (port: PortDef, value: PropertyValue): PortDef => ({ ...port, default: value });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||||
import { isList, type ListValue, type PropertyBag, type PropertyValue } from '../model/Properties';
|
||||
import type { EntityHandle } from './NodeContext';
|
||||
|
||||
/** A value travelling along a data edge. Objects are live entity handles (play mode only). */
|
||||
@@ -11,9 +11,24 @@ export function asNumber(value: Value | undefined): number {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
if (isList(value)) return value.length;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
if (value === null || value === undefined) return [];
|
||||
return typeof value === 'object' ? [value.name] : [value];
|
||||
}
|
||||
|
||||
/** A value narrowed to what a list may hold: handles and nested lists have no stored form. */
|
||||
export function asListItem(value: Value | undefined): number | string | boolean {
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'string') return value;
|
||||
if (value === null || value === undefined) return 0;
|
||||
return asString(value);
|
||||
}
|
||||
|
||||
export function asBoolean(value: Value | undefined): boolean {
|
||||
if (typeof value === 'string') return value !== '' && value !== 'false' && value !== '0';
|
||||
return Boolean(value);
|
||||
@@ -22,12 +37,14 @@ 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 || value === undefined) return '';
|
||||
if (isList(value)) return value.join(', ');
|
||||
if (typeof value === 'object') return value.name;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
export function asEntity(value: Value | undefined): EntityHandle | null {
|
||||
return value && typeof value === 'object' ? value : null;
|
||||
if (!value || typeof value !== 'object' || isList(value)) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +53,8 @@ export function asEntity(value: Value | undefined): EntityHandle | null {
|
||||
*/
|
||||
export function writeTyped(bag: PropertyBag, key: string, value: Value | undefined): void {
|
||||
const current = bag[key];
|
||||
if (typeof current === 'number') bag[key] = asNumber(value);
|
||||
if (isList(current)) bag[key] = asList(value);
|
||||
else if (typeof current === 'number') bag[key] = asNumber(value);
|
||||
else if (typeof current === 'boolean') bag[key] = asBoolean(value);
|
||||
else bag[key] = asString(value);
|
||||
}
|
||||
Reference in new issue
Block a user