Skip to content

Behaviour graph: a node canvas replaces the rule list - #65

Merged
sp00ktober merged 47 commits into
mainfrom
moveset/graph
Oct 13, 2026
Merged

sp00ktober merged 47 commits into
mainfrom
moveset/graph

Conversation

@sp00ktober

Copy link
Copy Markdown
Contributor

Phase 2 of the moveset editor: a port's behaviour is a graph of blocks, edited on a node canvas. It replaces [[rule]]. Stacked on #64.

  • The manifest:
    • Schema 2 adds [behaviour]: blocks (kind, params, next, play, label, at) and the moves' during links.
    • mhfu_port.behaviour.KINDS is the one schema of a block: the loader, the compiler and the studio all read it.
    • A schema 1 file migrates on load, one chain per old rule.
    • The rule table is compiled at build: one path is one rule, and higher on the canvas is checked first.
    • The Zinogre's migrated graph compiles to its 7 old rules in order, so its moves module is byte-identical.
  • The Behaviour dock:
    • Every move is a node, with "while playing" and "then" (after).
    • Blocks come from a right-click palette: events, the base monster's state, conditions, modifiers. Their values are edited on the node.
    • Badges show each path's priority; unfinished chains are dimmed; refused paths get a warning and a finding.
    • Every gesture is one undo step. The old rule form is gone.
  • NodeGraphQt 0.6.44:
    • New dependencies:
      • NodeGraphQt itself, through Qt.py, pinned below 2 because 2.x ships stubs that break mypy strict;
      • setuptools on Python 3.12 and later, which supplies the distutils NodeGraphQt imports.
    • The view uses only its public API: signals, the ports' connect_to, and an undo stack that holds nothing.

Tests: 2199 passed in CI's test job on Linux 3.11 and 3.14; 2762 passed with game data on macOS.

🤖 Generated with Claude Code

sp00ktober and others added 15 commits October 10, 2026 00:21
Manifest schema 2 holds [behaviour]: blocks with kind-checked params and moves
wired in. behaviour.compile makes the rule table from the graph's paths, in
canvas order; moves.py emits it unchanged. A schema 1 file migrates on load,
and both ports are re-saved as schema 2.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ypy), setuptools for its distutils on 3.12+

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…owner's document drives

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…no private handler is overridden

NodeGraph applies each gesture itself; port_connected, port_disconnected and
moved_nodes become the intents, and the revert pass undoes what it applied.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds as schema 1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The graph's edits are pure functions on a document (add, delete, link, unlink, param, note,
place, then), gathered by a Gesture into one undo step. What the canvas shows of it is read
once: priority, loose blocks, refused paths, the seam's cap, the moves' automatic places.

rules.py and the Moves dock's rule section go. Moves, renames and removals (clips, sequences)
go through mhfu_port.behaviour, so a rename keeps the graph and a move a block plays cannot
be deleted. A refused path and the cap are findings; a graph naming a move the validator
leaves out no longer crashes it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…colour, frames a node

A re-route arrives as an unlink and a link, a group drag as one move a node: `settled` follows
the intents of one gesture so the owner can apply them as one step before the view would put
the canvas back. An option of value None is the optional combo's own "none", under the owner's
label ("any" for a part).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The port's behaviour graph on a node canvas: a node a move or a block, blocks coloured by
role and carrying their params, a palette read from KINDS, wires stored as next, play, during
or after. A path's place shows on the block that plays it, a refused path as a warning with
its reason, a block on no path faded. A refused wire goes back; a gesture is one undo step.
Picking a move node picks the move; a finding on a block frames it. The Moves dock opens it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the studio's nodes

`params` is `_params`, public: a block's params with the kind's defaults. `refused` names every
block on a path `compile` refuses and why, from `_rule`'s own checks, so the studio no longer
compiles a one-path copy.

`migrate` lays the chains on 200 by 280 from x = 400: the studio draws a block up to 241 by 174
and a move 355 by 77, so a migrated graph no longer overlaps. A rule is still a row. The
Zinogre is laid out again through `migrate`; its rules, and so the golden test, are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…est that runs

The one-path copy that found the refused paths goes: the refused blocks come from
`behaviour.refused`, and a finding names the blocks that share a reason. A move's automatic
column clears the grid's own spacing. Spread out keeps the order of rows and columns, so no
path's priority changes (tested on the Zinogre); NodeGraphQt's auto_layout_nodes does not,
it put the flinch path first. The dock opens in a window offscreen, over the real Behaviour
dock spec, and the display-only assertions are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
sp00ktober and others added 14 commits October 13, 2026 12:57
The panel showed the edit a turn after the window's sync, so the view had
put the old state back first: old, then new, and a new block was never
selected. The panel now shows right after the edit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…th a pinch

Two-finger scroll (pixels or a scroll phase) pans; a pinch, Ctrl/Cmd+scroll and
a mouse wheel zoom, kept between 5% and 300% (NodeGraphQt's wheel zoom had no
limit). Public NodeViewer API only: scene_rect, set_scene_rect, scale.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A change of a combo's options redrew the node, and NodeGraphQt's redraw
skips the widgets it hides on a node drawn small (the fit of a large
graph): the node shrank to its 160x60 minimum, every counter and flag node
at once when a name was typed. The options do not change the node's
geometry, so nothing is redrawn.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…th docks of its own

A Dock whose area is "centre" becomes a tab of the window's centre, its panel in the
central stack. Each tab keeps its own dock layout: leaving one saves it, entering
restores it, and a centre tab starts with no docks. The layouts persist per tab
(STATE_VERSION 4). View > Panels, focus and a finding switch to the tab; Reset layout
resets every tab. The map workspace, with no centre dock, has no tab bar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… across tabs

The Tools toolbar shows on the 3D view tab only. Findings is about the document: open, it
stays open when the tab changes, a finding that lands on Behaviour included. The Moves
button's tip and the README say tab, not dock.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Kind.summary turns a block's params into a line or two ("250 - 1000", "30 frames",
"moving away"), for the compact node. Every kind has one; summary() falls back to
title/value pairs for a kind that adds none. part_labels() is the one place a part
index gets its name, which the studio's part options now read too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nvas

A node is its title and a line or two of its kind's summary, 168 wide, with no editors
and no note on its face; its tooltip holds the kind's tip and the note. The selected
block's values and note are edited in a bar under the canvas: one editor per param,
through the same intents, one undo step each; a refused edit goes back. A selected
move shows its name. The editors moved out of the view, so the view lost its param
and note signals.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… its summary at every zoom

The bar shows a move's note and edits it through set_label, and the move's tooltip carries
it. The summary no longer hides below a width that copied NodeGraphQt's private threshold.
The Behaviour panel is a tab now: the docstrings say so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
sources() is the roots paths() walks from, extracted. triggers() lists every event kind
(a kind with `by` once per value in use), every state kind and every move with a
while-playing port, each with its rules and their places in paths(): the outline and the
scoped views read it, so they cannot drift from the order the brain checks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… rows

An outline beside the Behaviour canvas lists All rules, every event kind, every state kind and
every move with a while-playing port, each with its rule count and its places in the order the
brain checks them. Picking one shows only its rules, one framed row each (rank and note): its
blocks left to right, the move it plays as a dashed reference with the number of rules that play
it, a move trigger once on the left. The layout is for display and never written to a block's
`at`. The scope lives on the workspace, a finding on a block outside it switches to the block's
trigger, and until editing in a trigger exists a drag snaps back and create and wire say so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
+ Rule adds a rule for the trigger (checked last, ending in a placeholder until a move is
picked). Right-click a block inserts one after it, and a reference or the placeholder inserts
before the play. Delete closes the chain up round a block, takes the whole rule with its first
block or its frame, and only the play with a reference. The bar picks the move a reference or
the placeholder plays, and a frame shows the rule's note. Each is one undo step through the
model's own edits; a rule with no move yet is an open chain the rows draw after the others.
Free blocks and wires stay a gesture of All.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ove in the game from the rows

Dragging a rule's frame up or down gives the rule the place the drop is at among its trigger's
rules. The places of the rules' first blocks are handed round, so every other rule keeps its
place in the whole order; a rule's own blocks move with its first, a move node never; one undo
step. A drop that would not change the rank is refused and the status says why, and a rule with
no move yet has no place to move. A reference, the move a while-playing trigger starts from and
the bar carry a Play in game entry that goes through the workspace's existing action, so it says
so when no game is there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Only a rule with no move yet ends in the placeholder; a rule that plays nothing on purpose,
which a later kind of block allows, keeps its last block as its end.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
sp00ktober and others added 18 commits October 13, 2026 15:03
…ide the stored canvas

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ules

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…move's sequence

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eason in its tip

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e title

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…lection writes nothing

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ame hint

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Only the rank rules, what writes a position, the Canvas wiring, the palette's source and the
tab's docks; the outline, the bar, the tips and the markers are visible.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
mhfu.live.brain finds the config block by its canary word, which the block holds at a fixed
offset though its address moves with the PRX build, checks that its board names a live monster,
and reads the rules in one request: how often each has fired. The canary's offset, the AI tick
count and the PRX's base are in addresses.toml.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nd watch them

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…h how often the game fired it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… in an ellipsis, not a wider panel

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…colour that reads as an error

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tus is empty

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sp00ktober
sp00ktober deleted the branch main October 13, 2026 19:26
@sp00ktober sp00ktober closed this Oct 13, 2026
@sp00ktober sp00ktober reopened this Oct 13, 2026
@sp00ktober
sp00ktober changed the base branch from moveset/sequences to main October 13, 2026 19:27
@sp00ktober
sp00ktober merged commit 7960540 into main Oct 13, 2026
10 checks passed
@sp00ktober
sp00ktober deleted the moveset/graph branch October 13, 2026 19:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant