A state change is something the graph can react to, so the machine announces it (#53, #54, #55, #57)
game-maker / suite (push) Successful in 10m58s

Four issues that all reach for the same line: how the current state is
read and written. Splitting them into four commits would mean four
states of the tree that do not compile, so they arrive together.

#55 start state — the machine asset carries an optional `start` holding
a State node's id. Right-click a state to set or remove it, a ring in
the node's title says which one it is. A dead id (the state was deleted)
reads as "no start", so nothing has to clean up after a deletion.

#53 entered/exited — `On state entered` and `On state exited`, one exec
output per state. Every change now goes through a single `setState`
that reports it; BehaviorSystem delivers it after the exec flow that
caused it, `exited` of the old before `entered` of the new, round after
round until the queue is empty, with a cap that stops a ping-pong graph
with an endless-loop error. A local machine tells the instance that
switched, a global one tells every instance holding a matching event
node. The start state fires `entered` too: per instance for a local
machine, once per run for a global one.

#57 reading — `Current state` with `name` for showing and `id` for
comparing, `Is in state` with one boolean per state. Neither has a
`run`, so `read` learned `def` and `lookup`, and the catalog test knows
this third kind of node.

#54 seeing it — during a run the state machine tab marks every state
some instance is holding faintly, and the one held by the selected
scene object's instance strongly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-15 06:18:20 +02:00
1 parent bc57f61c91
commit 7babf88636
30 files changed
+906 -44

No files matched your search

+24 -13
View File
@@ -1,6 +1,6 @@
import { textOf } from '../model/Properties';
import { flatDef } from '../properties/PropertySchema';
import { edgesInto, type Graph, type GraphNode } from './Graph';
import { edgesInto, type Graph, type GraphNode, type PortType } from './Graph';
import type { MachineState, MachineTransition, StateMachine } from './lookup';
import type { NodeDef, PortDef, PortSource } from './NodeRegistry';
@@ -47,6 +47,15 @@ export function statesOf(graph: Graph): MachineState[] {
return graph.nodes.filter(isStateNode).map((node) => ({ id: node.id, name: stateName(node) }));
}
/**
* The state a machine starts in, as far as its graph agrees: the marked id when a state of that id
* is still there, else none. A start pointing at a deleted state is not an error to repair – it
* reads as "no start", which is exactly what the machine had before anyone marked one.
*/
export function startStateOf(graph: Graph, start: string | undefined): string {
return start && graph.nodes.some((n) => n.id === start && isStateNode(n)) ? start : '';
}
/** The transitions of a machine's graph: every edge between two states, in edge order. */
export function transitionsOf(graph: Graph): MachineTransition[] {
const states = new Set(graph.nodes.filter(isStateNode).map((n) => n.id));
@@ -86,22 +95,24 @@ export const machinePort = (): PortDef => ({
});
/**
* The machine a node's `machine` input names while the graph is drawn: the inline pick, or the
* The machine asset a node's `machine` input names while the graph is drawn: the inline pick, or the
* `State machine` literal an edge brings in. A port is derived from the graph, not from a running
* value, so a variable or a list item feeding the input yields no state ports.
* value, so a variable or a list item feeding the input yields nothing.
*/
export function machineOf({ node, graph, lookup }: PortSource): StateMachine | null {
export function machineIdOf({ node, graph }: PortSource): string {
const edge = edgesInto(graph, node.id, MACHINE_PORT)[0];
const source = edge && graph.nodes.find((n) => n.id === edge.from.node);
const id = edge
? source?.type === STATE_MACHINE
? textOf(source.params.value ?? '')
: ''
: textOf(node.params[MACHINE_PORT] ?? '');
return id ? lookup.machine(id) : null;
if (!edge) return textOf(node.params[MACHINE_PORT] ?? '');
return source?.type === STATE_MACHINE ? textOf(source.params.value ?? '') : '';
}
/** One exec port per state, keyed by the state's id – a rename keeps every edge. */
export function statePorts(machine: StateMachine | null): PortDef[] {
return (machine?.states ?? []).map((state) => ({ key: state.id, label: state.name, type: 'exec' }));
/** What that machine is, looked up – null where the input names none, or none that exists. */
export function machineOf(source: PortSource): StateMachine | null {
const id = machineIdOf(source);
return id ? source.lookup.machine(id) : null;
}
/** One port per state, keyed by the state's id – a rename keeps every edge. */
export function statePorts(machine: StateMachine | null, type: PortType = 'exec'): PortDef[] {
return (machine?.states ?? []).map((state) => ({ key: state.id, label: state.name, type }));
}