game-maker / suite (push) Failing after 3m59s
A shader is an asset carrying a node graph (Texture color, Output, Property, Color, Add/Subtract/Multiply/Divide, Mix, Luminance) that compiles to the GLSL fragment of a Pixi filter. The Shader tab reuses the Behavior canvas; a sprite wears a shader through its Shader picker, and the rows below it are the graph's Property nodes. Property schema, resolution and deviation take the project because a sprite's rows now live in another asset. The built-in registry of the prototype is gone; "Adjust colors" ships as a graph in the fixture and the three example games. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
import type { PropertyBag, PropertyValue } from '../model/Properties';
|
||
import { isKindWith, plainKindOf } from '../properties/kinds';
|
||
import { flatDef, type PropertyDef } from '../properties/PropertySchema';
|
||
import type { DeclaredPort, Graph, GraphNode, PortType } from './Graph';
|
||
import type { GraphLookup } from './lookup';
|
||
import type { NodeContext } from './NodeContext';
|
||
import { asList, type Value } from './values';
|
||
|
||
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;
|
||
/** The property this port carries; its typed-in value is edited as that property (color, enum, …). */
|
||
field?: PropertyDef;
|
||
}
|
||
|
||
/**
|
||
* `state` and `shader` are the categories no behavior graph offers: their nodes are the states of
|
||
* a state machine and the GLSL of a shader asset.
|
||
*/
|
||
export type NodeCategory = 'event' | 'flow' | 'data' | 'variable' | 'action' | 'state' | 'shader';
|
||
|
||
/**
|
||
* How a loop node asks for one of its exec outputs to be run repeatedly: `port` carries the body,
|
||
* `index` is the number output seeded with the round number.
|
||
*/
|
||
export interface RepeatResult {
|
||
port: string;
|
||
index: string;
|
||
times: number;
|
||
}
|
||
|
||
/** How a node asks for another behavior's graph to be run in place, `inputs` seeding its start node. */
|
||
export interface CallResult {
|
||
behaviorId: string;
|
||
inputs: Record<string, Value>;
|
||
}
|
||
|
||
export interface RunResult {
|
||
/** Cached for the rest of the pulse. */
|
||
outputs?: Record<string, Value>;
|
||
/** Exec output(s) to continue along; omitted = the node ends the flow. */
|
||
next?: string | string[];
|
||
/** Runs `port` that many times before continuing along `next`. */
|
||
repeat?: RepeatResult;
|
||
/** Runs another behavior before continuing along `next`; its results join `outputs`. */
|
||
call?: CallResult;
|
||
}
|
||
|
||
/** What a node may compute its ports from: itself, the graph it sits in, and the project around it. */
|
||
export interface PortSource {
|
||
node: GraphNode;
|
||
graph: Graph;
|
||
lookup: GraphLookup;
|
||
}
|
||
|
||
/** Ports a single node has on top of the fixed ones of its type, and a label naming what it does. */
|
||
export interface DerivedShape {
|
||
label?: string;
|
||
inputs?: PortDef[];
|
||
outputs?: PortDef[];
|
||
}
|
||
|
||
/** Palette, port rendering, connection validation and inspector fields are all derived from this. */
|
||
export interface NodeDef {
|
||
type: string;
|
||
label: string;
|
||
category: NodeCategory;
|
||
description: string;
|
||
inputs: PortDef[];
|
||
outputs: PortDef[];
|
||
/** Node settings that are not ports (key name, operator, variable). */
|
||
params: PropertyDef[];
|
||
/**
|
||
* Settings that say nothing at this value set, so the node body leaves them off; the inspector
|
||
* still offers them. Without it a switched-off repeat is two rows of "0" on every key node.
|
||
*/
|
||
hiddenParams?(params: PropertyBag): string[];
|
||
/**
|
||
* Which side the ports this node declares for itself (`GraphNode.ports`) appear on. Setting it
|
||
* is what gives a node the port editor in the inspector. `both`: every port is an output, and an
|
||
* input as well where it is marked `write`.
|
||
*/
|
||
declaresPorts?: 'inputs' | 'outputs' | 'both';
|
||
/** How a declared port becomes a port; without it, named after its key and typed by its kind. */
|
||
declaredPort?(port: DeclaredPort, node: GraphNode): PortDef;
|
||
/**
|
||
* Ports this node computes beyond the fixed and the declared ones – how many outputs a Sequence
|
||
* offers, which values the behavior a call node names takes. `resolveDef` applies it.
|
||
*/
|
||
derivePorts?(source: PortSource): DerivedShape;
|
||
/**
|
||
* Outputs readable at any moment, even while the node's exec input has not fired in this pulse.
|
||
* Never cached, so a read after a write sees the new value. `input` pulls one input on demand:
|
||
* pulling all of them would loop through an edge that feeds the node's own output back in.
|
||
*/
|
||
read?(ctx: NodeContext, params: PropertyBag, input: (key: string) => Value): Record<string, Value>;
|
||
/**
|
||
* Event nodes have no `run` – the interpreter seeds their outputs. `def` is resolved for the node at
|
||
* hand, the only way a node with derived ports knows how many it has. `entry` is the exec input
|
||
* the pulse arrived through – what a node with one exec input per meaning reads.
|
||
*/
|
||
run?(
|
||
ctx: NodeContext,
|
||
inputs: Record<string, Value>,
|
||
params: PropertyBag,
|
||
def: NodeDef,
|
||
entry: string,
|
||
): RunResult;
|
||
}
|
||
|
||
const defs = new Map<string, NodeDef>();
|
||
|
||
export function registerNode(def: NodeDef): NodeDef {
|
||
if (defs.has(def.type)) throw new Error(`Node type "${def.type}" registered twice`);
|
||
defs.set(def.type, def);
|
||
return def;
|
||
}
|
||
|
||
export function nodeDef(type: string): NodeDef | undefined {
|
||
return defs.get(type);
|
||
}
|
||
|
||
export function allNodeDefs(): NodeDef[] {
|
||
return [...defs.values()];
|
||
}
|
||
|
||
export function isEventNode(def: NodeDef): boolean {
|
||
return def.category === 'event';
|
||
}
|
||
|
||
export function hasExecInput(def: NodeDef): boolean {
|
||
return def.inputs.some((p) => p.type === 'exec');
|
||
}
|
||
|
||
/**
|
||
* Data inputs that may carry an inline value while unconnected. An object is a live handle and a
|
||
* struct is assembled from its parts, so neither has a value that could be typed on one line; a
|
||
* shader colour has one only where the node gives it a colour field.
|
||
*/
|
||
export function inlineInputs(def: NodeDef): PortDef[] {
|
||
return def.inputs.filter(isInlinePort);
|
||
}
|
||
|
||
function isInlinePort(port: PortDef): boolean {
|
||
if (port.type === 'vec3') return port.field !== undefined;
|
||
return port.type !== 'exec' && port.type !== 'object' && port.type !== 'struct';
|
||
}
|
||
|
||
/** Untyped, an input writes the zero of its kind; as its default the node body shows it, not just the name. */
|
||
export function withInlineDefault(port: PortDef): PortDef {
|
||
return isInlinePort(port) ? { ...port, default: inlineInputDef(port).default } : port;
|
||
}
|
||
|
||
export function inlineInputDef(port: PortDef): PropertyDef {
|
||
if (port.field) return { ...port.field, key: port.key, label: port.label };
|
||
const kind = port.type === 'any' ? 'text' : plainKindOf(port.type);
|
||
if (kind === 'list') return { key: port.key, label: port.label, kind, default: asList(port.default) };
|
||
const flat = kind !== undefined && isKindWith('field', kind) ? kind : 'number';
|
||
const def = flatDef(port.key, flat, { label: port.label, value: port.default });
|
||
return def.kind === 'number' ? { ...def, step: 1 } : def;
|
||
}
|
||
|
||
/** 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;
|
||
}
|