A behavior that played an animation on the parent of its owner never
heard it finish. On animation finished listened to the owner and to
what stands below it, and the parent stands above.
- Play animation notes on the player which behavior asked for it.
- On animation finished hears, as before, every animation on the
owner or below it, and now also every animation the behavior
played itself, on whatever object. Its output object names that
object.
- Two behaviors that both played the same animation on the same
object each hear its end once.
- A behavior that is called by Run behavior or heard by an On node
counts as the behavior it sits in.
- The description of the node and the engine prompt say so.
Tests:
- Gate on melektaus-atx: 218 files, 3392 tests, CHECK EXIT 0.
- Two new unit tests: the behavior hears the animation on the parent
of its owner while a bystander does not, and two starters hear it
once each.
- Both counter-probes bite.
- e2e 6,17-26 on melektaus-atx: 480 checks, all pass.
- No new e2e check, and nothing was looked at in the browser. The
rule for called and heard behaviors has no test of its own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A behavior run by another one had a single flow port at its Start
and at its End, so it could not branch for its caller. And it could
not be used as an event: a call needs a pulse at its flow input,
and the events of a called graph never fire. An input like
"Confirm" (Space or Enter) had to be built again in every graph.
- A declared port of a Start or an End node can be of the kind
Flow. It sits above the values and shares their names, exec stays
reserved. Only these two nodes declare one, a struct does not.
- Run behavior shows a flow port of the Start as a further flow
input. A call arriving through it leaves the Start through the
output of that name and not through exec.
- Run behavior shows a flow port of the End as a further flow
output. The call goes on through the one the End was reached by
last, and then through exec. exec fires always, as before.
- On behavior is a new event node without a flow input. The
behavior it names runs at the node for as long as the behavior
around it runs, with all of its own events and one set of
variables per node.
- Its outputs are those of the End. It fires once the inner pulse
is through, by the output the End was reached by last, exec only
for End.exec. A pulse that never reaches the End is not seen.
- The value ports of the Start are its inputs, read anew for each
inner pulse. The Start itself never fires there.
- It nests to any depth. Under a Run behavior it stays silent. A
behavior hearing itself, however far round, is not started and
the node says "A behavior cannot listen to itself".
- The palette offers every other behavior with an End once more as
"On <name>". A node naming one without End says so.
- An On node removed while the game runs ends its behavior with On
disable and On destroy. The sounds of the entity end only with
the behavior placed on it.
- Renaming and deleting a declared port follows the edges at On
behavior nodes as it does at Run behavior nodes.
- The flow outputs of the Start of a behavior placed on an object
never fire, like its exec.
- The engine prompt of the benchmark names flow ports and On
behavior.
Tests:
- Gate on melektaus-atx right before this commit: 215 files, 3319
tests (3278 + 41), exit 0.
- e2e 19 on melektaus-atx: 39 checks (+4), all pass. e2e
1-6,17-24,26,36,51: 628 checks, all pass.
- 19 unit tests in flow-ports.test.ts, 19 in on-behavior.test.ts
on a real world, the rest in the node catalog.
- Four new e2e checks in module 19: a flow port declared by mouse
at the End, On Confirm in the graph, the call leaving through the
further output while playing, Enter and Space on a real keyboard.
- 19 unit counter-probes all fail their tests. One did not at first
(a removed node kept its instance), a test was added. Two browser
counter-probes fail their checks.
- One screenshot looked at: On Confirm with exec and pressed, the
second flow pin at the calling node.
- The e2e sum of 1680 is counted (1676 + 4), the full suite and
the smokes did not run.
- Not seen in the browser: flow inputs at the Start, nesting, the
ring, values through On behavior, the palette entry. No test
holds that the sounds of the entity go on when a heard behavior
ends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tile could be solid, one-way and scaled, but not report without
blocking. Water, lava, spikes or a goal zone needed a trigger object
of their own laid over the map. Now a tile has a switch Trigger, and
the collision events name the tile and the cell they met.
- A tile has the switch "Trigger (reports, never blocks)" under
Solid. Its cells hold nothing up: no Move, no falling body, no
ray. They raise On collision enter, stay and exit with the tilemap
as other.
- The three events have the outputs tile, cell x and cell y. Without
a tile, tile is empty and the cell is -1, -1.
- The three events have the input per cell. Off, the default, gives
one event per area: cells of the same tile that touch side by
side. On gives one event per cell.
- The scene view outlines trigger cells in yellow like a trigger
collider, merged to rectangles but never with solid cells.
- Is touching with the map counts trigger tiles, and Set tile works
while the game runs.
- Both systems find the touches with the same overlap check in
runtime/systems/tileTriggers.ts, not through the physics engine.
Decided here:
- Trigger wins over Solid. A tile with both does not block, as with
a collider.
- Water beside lava is two areas, and so are two ponds that touch
only at a corner.
- An area names the cell nearest to the centre of the collider, and
on exit the cell it named last.
- The solid cells of a map still do not name their tile.
- Normal, impulse and point are 0, as with a trigger collider.
- Collision scale and outlines shape a trigger cell like a solid
one. One-way, friction and bounciness have no effect.
Tests:
- Gate on stein2: 205 files, 3104 tests (3067 + 37), exit 0
- e2e 39,40: 106 checks. e2e 1-8,39-43: 490 checks. Both on
melektaus-atx. No full run, the sum of 1648 (1645 + 3) is
reckoned and CI measures it.
- 37 unit tests in tests/core/tile-triggers.test.ts, nearly all
against both systems
- Three e2e checks with a real mouse in block 40l-iv
- 24 unit counter-probes fail tests. Two did not at first, tests
were added for them.
- Two browser counter-probes fail their checks
- Two screenshots looked at (yellow outline, the node with per cell)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Which of two events ran first depended on hidden lists: the order of
the behavior instances and the order in which nodes were created. A
graph had no way to say that one On update must run before another,
and an On update of one behavior could run before a key event of
another.
- A frame now runs event kind by event kind over all behaviors: On
disable and On enable, key down, key up, key held, timer, collision
enter, stay and exit, animation finished, click, mouse wheel and at
last On update.
- All 17 event nodes have a field Order, a whole number, default 0,
negative allowed. Within one kind a lower Order runs earlier, no
matter which behavior holds the node.
- On a tie the current hierarchy decides from top to bottom, then the
order in which the nodes were created. The old rule that a behavior
enabled later goes to the end is gone.
- The node shows Order: 5 only when the value is not 0, so existing
graphs look the same.
- The rule lives as pure functions in core/graph/eventOrder.ts. The
interpreter can now fire a single event node.
Changes in existing projects:
- An object destroyed in a frame gets no further event in that frame.
On disable and On destroy still come at the end of the frame.
- New instances get all their On start first, then all On enable.
Teardown runs all On disable first, then all On destroy.
- State events follow right after the event kind that caused them.
- Four unit tests had fixed the old order and are adjusted:
two in behavior-events, one in physics-system, one in key-repeat.
Tests:
- ./check.sh: 191 files, 2784 tests, CHECK EXIT 0.
- GM_STEPS=1-6: ALL PASSED, 280 checks.
- Full e2e run (local): ALL PASSED, 1551 checks, measured.
- tests/core/event-order.test.ts: 11 tests for the rule, the field on
all 17 nodes, order across behaviors, key before update, two nodes
in one graph, On start, hierarchy after late enable and reorder at
runtime, state events, save and load.
- e2e 6z-viii, 2 checks with real mouse and keyboard: Order -1 set in
the inspector shows on the node, and a run logs B, A although B is
lower in the hierarchy.
- Counter-checks: 5 in unit tests and 1 in the browser, all fail as
expected. The originals were restored and checked with cmp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An inventory grid could not scroll, because nothing could cut a child
off at an edge. A game also had no way to hear the mouse wheel. Both
are needed for a scrolling list, a dialog text or a window with more
content than room.
- A new object type Clip area has width and height and turns, scales
and mirrors with its parent. The size gizmo drags width and height.
- Everything below a clip area, on every level, is drawn only inside
its rectangle, in the Scene view as in the game. Nested areas cut to
their intersection. The rule works on a convex polygon, so a turned
area cuts right as well.
- On click, Is hovered and On mouse wheel do not hit a cut-off part.
The Scene view does not pick it either.
- In the Scene view the area is a dashed frame that is grabbed only at
its edge, so a click inside lands on the content. The eye under
Shape hides the frame. In the game the area is invisible.
- While an object below an area or one of its parents is selected, the
Scene view shows the cut-off part at 30 % opacity and it can be
grabbed there. The rule lives in core/properties/clip.ts.
- The Properties node knows the type Clip area.
- The new event node On mouse wheel fires by the rule of On click. Its
delta counts in notches, positive is down, and a frame's turns
arrive as one value. It also gives x and y.
Limits:
- Only sprites and texts show the faded rest. Tilemaps and particles
are cut and can be grabbed when open, but are not shown faded.
- Masks by texture shape are out, as noted in the issue.
Tests:
- ./check.sh: 2773 tests, CHECK EXIT 0.
- Full e2e run (local): ALL PASSED, 1549 checks, measured.
- tests/core/clip-areas.test.ts: 12 tests for the cut rule, scene and
game, save and load, hits, the wheel with and without owner, the
notch conversion and the open rule.
- e2e 58, 5 checks with real mouse and wheel: the scene cuts (74 of 74
red inside, 0 outside), a click on the cut part picks nothing, the
selected rest is faded and grabbable, the game cuts, one notch
logs 1.
- e2e 04 and 46 count the object types and the eye rows. They now
include Clip area.
- Counter-checks: 6 in unit tests and 3 in the browser, all fail as
expected. The originals were restored and checked with cmp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A timer can only fire on a frame. It fires on the first frame past each
due time, so Time.time at that moment is the due time plus up to one
frame. On a display that is not exactly 60 Hz this delay creeps up by
a millisecond every few seconds and falls back to zero once it reaches
a whole frame. The issue saw 82.009 after 82 ticks. The timer itself
does not drift, it keeps the remainder when it fires.
- On timer gets a number output late. It is what is left of the
timer's clock after taking one interval, so time - late is exactly
the due time.
- When one long frame catches up several ticks, each gets its own
late. The first one was due longest ago and is the latest.
Tests:
- New unit test in behavior-events: late 0.125 after a frame past
1.0 s, 0 on the dot, 1.5 and 0.5 for two ticks caught up in one
frame.
- Counter-check: late taken before the interval is subtracted fails
exactly that test. Original restored with cmp.
- ./check.sh: 156 files, 2300 tests, CHECK EXIT 0 (was 2299).
- No e2e change. The e2e modules only wire the exec of On timer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Animation tab shows the root asset over the timeline, with every row's value at the
playhead written onto a copy of it – the sprite, text or container placed at the origin as a
scene would place it, a scene as itself. Rows reach the root through that placed object and its
children through the object's instance edits, the route a placed container's content takes,
so the ids match what the runtime gives them. The runtime player and the preview evaluate a row
through one function (trackWriteAt), relative rows included; past the end of a once-reset pass
the preview shows the starting values, as the player would. Play, pause and the ruler drive the
preview through the timeline's playhead; a drag pans, the wheel zooms, Centre frames it. The
document is never touched and undo never hears of scrubbing.
On animation finished no longer describes itself as hearing "a sprite of the owner".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
Order per instance: On start -> On enable -> On update ... -> On disable
-> On destroy. On enable fires right after On start and whenever the
behavior comes back on (itself and its parents); On disable in the frame
it goes off, the instance stays frozen. On destroy fires when the entity
leaves the world (Destroy node, child of a destroyed object) and when the
world is torn down on stop/restart, preceded by On disable if still on.
A new On start node added while playing still does not fire for a
running instance (kept as is, documented in the wiki).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An animation asset is a list of pictures (whole textures or frames) with
frames per second and Loop/Once/Ping-pong playback. A sprite wears it
through its texture property, so the picker, instance overrides and the
Properties node work unchanged; there is no dedicated play node.
A different animation starts from its first frame, the same one set again
runs on, so a graph may choose Run or Idle every frame. The clock lives on
the entity, not in props, which the world writes back as overrides. The
AnimationSystem ticks before behaviors, and On animation finished fires
once when a Once animation reaches its end.
The Animation tab plays a preview above a frame strip: reorder by dragging,
Delete, duplicate, + Frames, pause/resume, and wheel zoom about the pointer
with pan and Fit. Scene and Container tabs show the first frame; edit mode
has no clock. Right-clicking a texture creates an animation from its frames.
Button now measures its label with the bold font it draws in, which cut
the Animation tab and red dialog buttons short.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two On key down fields read in milliseconds now, and say so in their
labels. A number field shows three decimals unless a property asks for
fewer, so both params set decimals: 0 – the field reads 300, not 300.000,
and rounds to whole milliseconds while typing or scrubbing. The clock
behind it still counts seconds; the conversion sits at the one place that
reads the properties.
A held key fired once and then nothing, so sliding a Tetris piece sideways or
walking a menu meant building a timer next to every key node. Two settings on
the node do it now: Repeat after and Repeat interval, both in milliseconds so
the usual values stay whole numbers (300, 50) instead of 0.3 and 0.05. A delay
of 0 keeps the node exactly as it was, an interval of 0 means every frame.
The browser's own auto-repeat is deliberately not used - Input.press has always
dropped it - because its delay and rate are an operating-system setting of the
player's, not a game's. The repeat is built from the measured hold time instead,
per node, so two nodes on one key keep their own clocks. The rule itself is a
pure function in keyRepeat.ts and is checked without a world.
Both rows stay off the node body while the delay is 0: two lines of "0" on every
key node in every graph would be noise, and the screenshot said so. NodeDef gains
hiddenParams for that; the inspector still shows them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>