Replace Move 'per second' setting with a dt data input

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-07 14:21:46 +02:00
1 parent 06b1007015
commit 9060da615a
8 files changed
+210 -40

No files matched your search

+2 -2
View File
@@ -1,5 +1,5 @@
import { edgesInto, edgesOutOf, type Graph, type GraphNode } from './Graph';
import { hasExecInput, isEventNode, type NodeContext, requireNodeDef, type Value } from './NodeRegistry';
import { hasExecInput, inlineValue, isEventNode, type NodeContext, requireNodeDef, type Value } from './NodeRegistry';
/**
* Guards against runaway flows (a Sequence looping back into itself, …). The interpreter
@@ -83,7 +83,7 @@ export class GraphInterpreter {
if (port.type === 'exec') continue;
const edge = edgesInto(this.graph, node.id, port.key)[0];
if (edge) inputs[port.key] = this.readOutput(edge.from.node, edge.from.port, ctx);
else inputs[port.key] = node.params[port.key] ?? null;
else inputs[port.key] = inlineValue(node, port);
}
return inputs;
}
+10 -3
View File
@@ -44,6 +44,8 @@ export interface PortDef {
key: string;
label: string;
type: PortType;
/** Inline value used while the input is unconnected and the node has no own value (default 0/''/false). */
default?: PropertyValue;
}
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action';
@@ -117,15 +119,20 @@ export function inlineInputs(def: NodeDef): PortDef[] {
export function inlineInputDef(port: PortDef): PropertyDef {
switch (port.type) {
case 'boolean':
return { key: port.key, label: port.label, kind: 'boolean', default: false };
return { key: port.key, label: port.label, kind: 'boolean', default: asBoolean(port.default ?? false) };
case 'string':
case 'any':
return { key: port.key, label: port.label, kind: 'text', default: '' };
return { key: port.key, label: port.label, kind: 'text', default: asString(port.default ?? '') };
default:
return { key: port.key, label: port.label, kind: 'number', default: 0, step: 1 };
return { key: port.key, label: port.label, kind: 'number', default: asNumber(port.default ?? 0), step: 1 };
}
}
/** The value an unconnected input carries: the node's own inline value, else the port default. */
export function inlineValue(node: { params: PropertyBag }, port: PortDef): Value {
return node.params[port.key] ?? port.default ?? null;
}
// --- value helpers for node implementations ---------------------------------------------
export function asNumber(value: Value | undefined): number {
+72 -5
View File
@@ -166,6 +166,72 @@ const MATH: [string, string, string, (a: number, b: number) => number][] = [
for (const [type, label, description, fn] of MATH) {
registerNode({ type, label, category: 'data', description, inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: fn(asNumber(i.a), asNumber(i.b)) } }) });
}
registerNode({ type: 'Min', label: 'Min', category: 'data', description: 'The smaller of a and b.', inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.min(asNumber(i.a), asNumber(i.b)) } }) });
registerNode({ type: 'Max', label: 'Max', category: 'data', description: 'The larger of a and b.', inputs: [num('a'), num('b')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.max(asNumber(i.a), asNumber(i.b)) } }) });
registerNode({ type: 'Abs', label: 'Abs', category: 'data', description: 'Absolute value (drops the sign).', inputs: [num('value')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.abs(asNumber(i.value)) } }) });
registerNode({ type: 'Sqrt', label: 'Sqrt', category: 'data', description: 'Square root (0 for negative input).', inputs: [num('value')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.sqrt(Math.max(0, asNumber(i.value))) } }) });
registerNode({
type: 'Clamp',
label: 'Clamp',
category: 'data',
description: 'Limits a value to the range min…max.',
inputs: [num('value'), { ...num('min'), default: 0 }, { ...num('max'), default: 1 }],
outputs: [num('result')],
params: [],
run: (_c, i) => ({ outputs: { result: Math.min(Math.max(asNumber(i.value), asNumber(i.min)), asNumber(i.max)) } }),
});
// --- data: vectors -------------------------------------------------------------------------
registerNode({ type: 'Length', label: 'Length', category: 'data', description: 'Length of the vector (x, y).', inputs: [num('x'), num('y')], outputs: [num('result')], params: [], run: (_c, i) => ({ outputs: { result: Math.hypot(asNumber(i.x), asNumber(i.y)) } }) });
registerNode({
type: 'Distance',
label: 'Distance',
category: 'data',
description: 'Distance between two objects.',
inputs: [obj('a'), obj('b')],
outputs: [num('result')],
params: [],
run: (_c, i) => {
const a = asEntity(i.a);
const b = asEntity(i.b);
return { outputs: { result: a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0 } };
},
});
registerNode({
type: 'Normalize',
label: 'Normalize',
category: 'data',
description: 'Scales the vector (x, y) to length 1; (0, 0) stays (0, 0).',
inputs: [num('x'), num('y')],
outputs: [num('x'), num('y'), num('length')],
params: [],
run: (_c, i) => ({ outputs: normalize(asNumber(i.x), asNumber(i.y)) }),
});
registerNode({
type: 'AxisInput',
label: 'Axis input',
category: 'data',
description: 'Direction from four keys as a normalized vector (diagonals are not faster).',
inputs: [],
outputs: [num('x'), num('y'), bool('active')],
params: [
{ key: 'left', label: 'Left', kind: 'key', default: 'ArrowLeft' },
{ key: 'right', label: 'Right', kind: 'key', default: 'ArrowRight' },
{ key: 'up', label: 'Up', kind: 'key', default: 'ArrowUp' },
{ key: 'down', label: 'Down', kind: 'key', default: 'ArrowDown' },
],
run: (ctx, _i, p) => {
const held = (key: string, fallback: string) => (ctx.isKeyDown(String(p[key] ?? fallback)) ? 1 : 0);
const { x, y, length } = normalize(held('right', 'ArrowRight') - held('left', 'ArrowLeft'), held('down', 'ArrowDown') - held('up', 'ArrowUp'));
return { outputs: { x, y, active: length > 0 } };
},
});
function normalize(x: number, y: number): { x: number; y: number; length: number } {
const length = Math.hypot(x, y);
return length === 0 ? { x: 0, y: 0, length: 0 } : { x: x / length, y: y / length, length };
}
const COMPARE: Record<string, (a: number, b: number) => boolean> = {
'==': (a, b) => a === b,
@@ -196,15 +262,16 @@ registerNode({
type: 'Move',
label: 'Move',
category: 'action',
description: 'Moves an object by dx/dy.',
inputs: [exec(), obj('object'), num('dx'), num('dy')],
description: 'Moves an object by (dx, dy) × scale × dt. Connect dt to an event\'s dt for per-second movement; leave it at 1 for per-frame.',
inputs: [exec(), obj('object'), num('dx'), num('dy'), { ...num('scale'), default: 1 }, { ...num('dt'), default: 1 }],
outputs: [exec()],
params: [],
run: (_c, i) => {
run: (_ctx, i) => {
const target = asEntity(i.object);
if (target) {
target.x += asNumber(i.dx);
target.y += asNumber(i.dy);
const factor = asNumber(i.scale) * asNumber(i.dt);
target.x += asNumber(i.dx) * factor;
target.y += asNumber(i.dy) * factor;
}
return { next: 'exec' };
},