From d554dfddbf09fc8d657e27d78a7973ec7e8c1e29 Mon Sep 17 00:00:00 2001 From: meleki Date: Fri, 2 Oct 2026 19:33:39 +0200 Subject: [PATCH] Describe flow ports at Start and End and the On behavior node (#294) --- Architecture.md | 15 +++++++++++++++ Behaviors.md | 48 +++++++++++++++++++++++++++++++++++++++++++++--- Development.md | 2 +- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/Architecture.md b/Architecture.md index bc7dcf6..9213217 100644 --- a/Architecture.md +++ b/Architecture.md @@ -569,6 +569,21 @@ system `adopt`s them before it starts, so they are no new instances and hear no trace is cleared and timed by the new world, which starts at second zero. `PhysicsSystem.resume` gives each body the velocity when it is made, and forgets the rest after the first sync. +**A behavior heard by an `On behavior` node is an instance of its own** (#294), on the entity of +the behavior holding the node: its own `GraphInterpreter`, trace, local states and `variables` (a +copy of the heard behavior's defaults; `behaviorContext` hands these out instead of the entity's +props), and a `host` naming the instance and the node that hear it. `BehaviorSystem.syncHeard` +builds them when an instance is born and again after every edit to the project, following the +chain of behaviors hearing each other and stopping where one would hear itself; a node that is +gone takes its instance along, with `On disable` and `On destroy`. Because they stand in the same +list as every other instance, the event kinds of a frame, the state events, pause and the scene +swap reach them without a second code path. `runPulses` does the two things that differ: before a +pulse it reads the inputs of the hearing node into the interpreter's `given`, which the start node +hands out, and after it `surface` asks the interpreter for its `ending` — the flow input the end +node was reached by last — and pulses the hearing node through the output of that name, and so on +up the chain. A call learns the same way which output to take first: `call` returns the `Ending` +of the nested frame, and `CallResult.entry` names the start node's output it begins at. + **A run keeps what each scene stored before it built it.** `GameRun` copies a scene's objects and groups the first time it builds it, and puts them back before it builds it again: the run writes into the scene it is in (`syncToModel`, baking, `spawn`), and a level loaded twice must not start diff --git a/Behaviors.md b/Behaviors.md index fa10122..576e8c1 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -193,7 +193,7 @@ playing use the same two words. set to `a` also hears `A`, and a key picked with Shift held is stored lower-case. A key is let go under the name it went down as, even when Shift changes in between (`1` down, `!` up). - **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On enable`, `On update`, `On disable`, `On destroy` (see *Lifecycle*), `On key down/up/held`, - `On timer` (fires on the first frame past each due time; `late` says by how much, so `time - late` is exactly a multiple of the interval), `On collision enter/stay/exit` (stay every frame, or with an interval; while playing enter and stay tell normal, impulse and point of the contact; on a trigger tile of a tilemap all three tell `tile` and `cell x/y`, once per area or with the input `per cell` once per cell, see [Tilemaps](Tilemaps#trigger-tiles)), `On click` (the mouse pressed on the owner — anywhere when it has none), `On mouse wheel` (the wheel turned over the owner, or anywhere without one; `delta` in notches, positive downwards, a trackpad gives fractions), `On animation finished`, `On state entered`, `On state exited` (one output per state, see *State machines*), `Start` · flow `Branch`, `Sequence`, `Repeat`, `For each`, `Find` … `Match`, `Run behavior`, `End`, and the four that move or follow a state machine, `State branch`, `Set state`, `Set state by name`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Child by name` (the nearest object of that name below the given one: its children, then their children, and so on; within a level the first in hierarchy order, nothing when none is called so), `Name of`, + `On timer` (fires on the first frame past each due time; `late` says by how much, so `time - late` is exactly a multiple of the interval), `On collision enter/stay/exit` (stay every frame, or with an interval; while playing enter and stay tell normal, impulse and point of the contact; on a trigger tile of a tilemap all three tell `tile` and `cell x/y`, once per area or with the input `per cell` once per cell, see [Tilemaps](Tilemaps#trigger-tiles)), `On click` (the mouse pressed on the owner — anywhere when it has none), `On mouse wheel` (the wheel turned over the owner, or anywhere without one; `delta` in notches, positive downwards, a trackpad gives fractions), `On animation finished`, `On state entered`, `On state exited` (one output per state, see *State machines*), `On behavior` (another behavior heard as an event, see *A behavior as an event* below), `Start` · flow `Branch`, `Sequence`, `Repeat`, `For each`, `Find` … `Match`, `Run behavior`, `End`, and the four that move or follow a state machine, `State branch`, `Set state`, `Set state by name`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Child by name` (the nearest object of that name below the given one: its children, then their children, and so on; within a level the first in hierarchy order, nothing when none is called so), `Name of`, `Is asset` (is this object an instance of asset X — how you tell what you collided with), and the plural lookups, each handing on a `list` of objects taken at that moment (destroyed objects are never in it, switched-off ones are, as with the single lookups): `Objects by name` @@ -253,8 +253,8 @@ playing use the same two words. so the canvas and the inspector see a definition that fits *this* node; a node whose ports are derived reads them back off that definition, which is why `run` is handed it. `Sequence` keeps one free output below the last connected one — connecting the spare grows it, unplugging the - bottom edge shrinks it again (keys `a`, `b`, `c`, … up to 26). `Run behavior` takes its ports - from the behavior it names, and a behavior's `Start`/`End` node declares them. Pointing a call + bottom edge shrinks it again (keys `a`, `b`, `c`, … up to 26). `Run behavior` and `On behavior` take their ports + from the behavior they name, and a behavior's `Start`/`End` node declares them. Pointing a call at a different behavior **takes the edges of the ports that go away** and gives them back on undo. - **Ports that carry one and the same value** say so with `sharedType` (`{inputs, outputs}`), and @@ -358,6 +358,48 @@ playing use the same two words. A behavior **without a start node** is called from its **On start** nodes instead, all of them in graph order, as when it starts on its own. It takes no inputs then, but its end node still hands back what it is given. With a start node in the graph, its On start nodes stay out of the call. +- **More than one way in and out** (flow ports): next to numbers, texts and the other kinds, the + port list of a **Start** and an **End** node offers the kind **Flow**. A flow port declared at + the End is a further flow *output* of every node running the behavior, one declared at the Start + a further flow *input*. They stand above the value ports, below the fixed `exec`, and share + their names with the values of the same node; `exec` stays taken. + A call that comes in by the input `open` begins at the Start's output `open` and nowhere else; + the Start's plain `exec` fires only for a call that came in by `exec`. The values are the same + whichever way the call came. + On the way out the node goes on **first through the output named like the End's flow input that + was reached last, then through `exec`**. `exec` therefore always fires, as it did before there + were flow ports, and reads as "and then". A call whose End was reached by `exec` last, or not at + all, goes on through `exec` only. One call takes at most one of the further outputs: when its + flow reaches the End several times, the last one decides, as it does for the values. Every call + decides anew, so a call inside a loop may leave differently each round. + A behavior without a Start node has no further ways in, since they are declared there; the ones + at its End work all the same. In a behavior that runs on an object and is not called, the + Start's outputs never fire, the further ones no more than `exec`. +- **A behavior as an event** (`On behavior`): the palette lists every other behavior that has an + End node a second time, as **On *name*** among the events. That node has no flow input. The + behavior it names **lives at the node** for as long as the behavior around it runs: all of its + own events fire as if it were placed on the same object (keys, mouse, `On update`, timers, + collisions, animations, state events, and `On start`, `On enable`, `On disable` and `On destroy` + in step with the behavior around it), and its variables, timers and local state machines keep + their state, **one set per node**. Two nodes hearing the same behavior share nothing, and Keep + plays no part here. + The node fires **whenever a flow in there reaches the End**: through the output named like the + flow input the End was reached by, with the values the End was handed. Unlike a call, the plain + `exec` fires only when the End was reached by `exec`; a flow that never gets to the End is not + heard at all. The outer flow starts once the inner one has run out, so a flow reaching the End + twice is heard once, by the way it took last. + The value inputs of the Start node are **inputs of the node**, typed in or connected, and read + anew for each of the inner flows; the Start itself never fires, and its flow ports do not show. + Example, a "Confirm" that is Space or Enter everywhere: in the behavior *Confirm*, two + `On key down` nodes lead to a flow port `pressed` of its End. Any other behavior then holds one + node **On Confirm** and wires its `pressed` output; a third key is added in *Confirm* alone. + Behaviors may hear each other as deep as you like. A behavior that would hear itself, directly + or through others, says so on the node and is not started a second time. Inside a behavior that + is *run as a node* an `On behavior` stays silent, like every event there. The node has no + **Order** of its own: the events inside carry theirs and run in their kind with everyone else's. + Double-click opens the behavior heard, with the breadcrumb of a call. While it runs, its graph + lights up like a called one. A node that cannot fire says why: no behavior picked, the behavior + deleted, or no End node in it. - **Keep** (the pin on each variable of the called behavior, in its inspector): a kept variable picks up where the last call **from this node, on this object** left it; the others start at their default every call. Every variable starts out not kept, a new one as well as one from a diff --git a/Development.md b/Development.md index bde925b..b5ad7a3 100644 --- a/Development.md +++ b/Development.md @@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun # has and the web server's container has not (SKIP there) php tests/php/snapshots.test.php # the snapshot store (#231), the same way php tests/php/releases.test.php # the release store (#266), the same way -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1676 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1680 editor checks, screenshots in /tmp/gm-e2e-*.png GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step ./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh status # live progress, or the last run's result