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

+17
View File
@@ -102,6 +102,13 @@ export class NodeView extends Widget {
private problem: string | null = null;
selected = false;
hot = false;
/** The state a machine begins in wears a ring, so the entry point is visible at a glance. */
isStart = false;
/**
* While something is running: `held` for a state some instance of the machine is in, `current`
* for the one the selected object's instance holds. Nothing outside a run.
*/
stateMark: 'none' | 'held' | 'current' = 'none';
/** Port a running edge drag would connect to, drawn with a ring so the drop is predictable. */
targetPort: { port: string; isOutput: boolean } | null = null;
/** Data inputs that currently have a connection (their inline value is hidden). */
@@ -390,10 +397,20 @@ export class NodeView extends Widget {
.rect(0, TITLE_HEIGHT - Theme.radius, this.w, Theme.radius)
.fill(categoryColor(this.def.category));
this.drawChevron(PAD + 4, TITLE_HEIGHT / 2);
// The ring sits where a state's ports never are: a state draws none (see `portCentres`).
if (this.isStart)
this.box
.circle(this.w - PAD - PORT_RADIUS, TITLE_HEIGHT / 2, PORT_RADIUS)
.stroke({ width: 2, color: c.text });
const border = this.hot ? c.hot : this.selected ? c.nodeSelected : this.problem ? c.danger : c.nodeBorder;
this.box
.roundRect(0.5, 0.5, this.w - 1, this.h - 1, Theme.radius)
.stroke({ width: this.selected || this.hot || this.problem ? 2 : 1, color: border });
// Inside the border, so a selected state still shows both: what is picked, and what is live.
if (this.stateMark !== 'none')
this.box
.roundRect(2.5, 2.5, this.w - 5, this.h - 5, Theme.radius)
.stroke({ width: 2, color: c.hot, alpha: this.stateMark === 'current' ? 1 : 0.35 });
this.ports.clear();
const r = PORT_RADIUS;