Skip to content

Commit bcfa4ea

Browse files
committed
Merge the Behaviour tab, compact nodes and the property bar
2 parents a962cd9 + eec1e77 commit bcfa4ea

15 files changed

Lines changed: 1658 additions & 798 deletions

File tree

‎apps/studio/README.md‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,16 @@ starting where the last stood, facing as it faced, and the Timeline's bar above
8989
shows the steps (click one to pick it). Picking or playing anything else starts clips where
9090
they normally start.
9191

92-
The Behaviour dock (the button under the moves, or View > Panels) draws the port's `[behaviour]`
93-
graph: every move and every block is a node, and right-click adds a block (the palette is
94-
`mhfu_port.behaviour.KINDS`). A wire from a block's `out` feeds the next block's `in`, or plays a
95-
move at its `play`; a move's `while playing` starts a path while it plays, and its `then` hands to
96-
another move (`after`). Any other wire is refused in the status line. `#2` on a block is its path's
97-
rank (higher on the canvas is checked first, but every path that plays nothing runs before all
98-
the others), `⚠` a path the game refuses (the tip says why),
99-
faded a block on no complete path. Delete removes blocks and their links; a move is deleted in
100-
Moves. Every gesture is one undo step.
92+
The Behaviour tab beside the 3D view (the button under the moves, or View > Panels) draws the
93+
port's `[behaviour]` graph: every move and block is a node, and right-click adds a block (the
94+
palette is `mhfu_port.behaviour.KINDS`). A wire from a block's `out` feeds the next block's `in`,
95+
or plays a move at its `play`; a move's `while playing` starts a path while it plays, and its
96+
`then` hands to another move (`after`). Any other wire is refused in the status line. `#2` is a
97+
block's path rank (higher on the canvas is checked first, but every path that plays nothing runs
98+
before all the others), `⚠` a path the game refuses (the tip says why), faded a block on no
99+
complete path. Delete removes blocks and their links; a move is
100+
deleted in Moves. Every gesture is one undo step. The tab has no docks until you open one on it,
101+
and keeps them apart from the 3D view's; Findings stays open across tabs.
101102

102103
Effect blocks (violet) change the monster's counters and flags when their path fires. A path may
103104
end at an effect with nothing to play: it applies its effects, keeps its `#n` on that block, and

‎apps/studio/src/mhfu_studio/monster/behaviour.py‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""A port's behaviour graph (`mhfu_port.behaviour`) as document edits, and what the canvas shows
44
of it.
55
6-
No toolkit: the Behaviour dock calls these, one undo step per gesture (`Gesture`); a refusal is
6+
No toolkit: the Behaviour tab calls these, one undo step per gesture (`Gesture`); a refusal is
77
the manifest's own `ManifestError`, so its checks are the only ones. Paths, priority, validity
88
and references are the package's; this module names the canvas's nodes and ports and reads the
99
graph for them.
@@ -180,19 +180,29 @@ def capped(m: Manifest, ps: Sequence[Path] | None = None) -> str | None:
180180

181181
def part_options(m: Manifest, optional: bool) -> list[tuple[str, int | None]]:
182182
"""The parts to pick from: the manifest's named ones, then `part k`; `any` for none."""
183-
named: dict[int, str] = {}
184-
for name, part in m.parts.items():
185-
named.setdefault(part.index, name)
186183
out: list[tuple[str, int | None]] = [("any", None)] if optional else []
187-
out += [(name, k) for k, name in named.items()]
188-
out += [(f"part {k}", k) for k in behaviour.PARTS if k not in named]
184+
out += [(label, k) for k, label in behaviour.part_labels(m).items()]
189185
return out
190186

191187

192188
def main_options() -> list[tuple[str, int]]:
193189
return [(str(k), k) for k in behaviour.MAIN_STATES]
194190

195191

192+
def options(m: Manifest, kind: str) -> dict[str, list[tuple[str, object]]]:
193+
"""The (label, value) choices of `kind`'s parts and main states, and the names in use, by
194+
param."""
195+
out: dict[str, list[tuple[str, object]]] = {}
196+
for p in KINDS[kind].params:
197+
if p.type == "part":
198+
out[p.name] = list(part_options(m, p.optional))
199+
elif p.type == "mains":
200+
out[p.name] = list(main_options())
201+
elif p.type in behaviour.NAMES:
202+
out[p.name] = [(n, n) for n in behaviour.names(m, p.type)]
203+
return out
204+
205+
196206
def palette() -> list[tuple[str, list[tuple[str, str, str]]]]:
197207
"""The kinds a block can be, a group per role: (kind, title, tip). All from `KINDS`."""
198208
roles = typing.get_args(behaviour.Role)

‎apps/studio/src/mhfu_studio/monster/panels/behaviour.py‎

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# SPDX-License-Identifier: GPL-3.0-or-later
22
# SPDX-FileCopyrightText: 2026 sp00ktober
3-
"""The Behaviour dock: the port's behaviour graph (`mhfu_port.behaviour`) on a node canvas.
3+
"""The Behaviour tab: the port's behaviour graph (`mhfu_port.behaviour`) on a node canvas.
44
5-
A node is a move or a block; a wire is a link the manifest stores (`monster/behaviour.py`). The
6-
document is the owner: every gesture of the canvas arrives as intents, is applied as one undo
7-
step, and the canvas is shown again from the document.
5+
A node is a move or a block, drawn compact; a wire is a link the manifest stores
6+
(`monster/behaviour.py`). The selected node's values and note are edited in the bar under the
7+
canvas. The document is the owner: every gesture of the canvas and every edit of the bar arrives
8+
as intents, is applied as one undo step, and is shown again from the document.
89
"""
910

1011
from __future__ import annotations
@@ -13,13 +14,14 @@
1314
from typing import TYPE_CHECKING
1415

1516
from mhfu_port import behaviour as model
16-
from mhfu_port.behaviour import KINDS
17-
from mhfu_port.manifest import Manifest, ManifestError
17+
from mhfu_port.behaviour import KINDS, Kind
18+
from mhfu_port.manifest import Manifest, ManifestError, Move
1819
from PySide6.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
1920

2021
from mhfu_studio.monster import behaviour as graph
2122
from mhfu_studio.monster.panels.common import kind
22-
from mhfu_studio.monster.panels.node_graph import NAMES, GraphView, LinkSpec, NodeSpec
23+
from mhfu_studio.monster.panels.node_graph import GraphView, LinkSpec, NodeSpec
24+
from mhfu_studio.monster.panels.properties import Detail, PropertyBar
2325
from mhfu_studio.monster.panels.widgets import NoScene
2426
from mhfu_studio.shell.overlay import Color, Ink
2527
from mhfu_studio.ui import kit, theme
@@ -65,6 +67,8 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
6567
self._dropped: dict[str, graph.Point] = {}
6668
#: the block the gesture creates
6769
self._made: str | None = None
70+
#: what the canvas was last drawn from, for the bar
71+
self._read: graph.Reading | None = None
6872

6973
self.title = kit.label(role="title", wrap=False)
7074
self.hint = kit.label(HINT, role="muted", wrap=False)
@@ -88,21 +92,21 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
8892
v.link_requested.connect(lambda *w: self._collect(lambda e: graph.link(e, *w)))
8993
v.unlink_requested.connect(lambda *w: self._collect(lambda e: graph.unlink(e, *w)))
9094
v.delete_requested.connect(lambda ids: self._collect(lambda e: graph.delete(e, ids)))
91-
v.param_changed.connect(
92-
lambda i, n, x: self._collect(lambda e: graph.set_param(e, i, n, x))
93-
)
94-
v.label_changed.connect(lambda i, t: self._collect(lambda e: graph.set_label(e, i, t)))
9595
v.moved.connect(lambda i, x, y: self._dropped.__setitem__(i, (x, y)))
9696
v.create_requested.connect(self._create)
9797
v.picked.connect(self._picked)
9898
v.settled.connect(self._settle)
99+
self.bar = PropertyBar()
100+
self.bar.param_changed.connect(self._param)
101+
self.bar.label_changed.connect(self._label)
99102

100103
page = QWidget()
101104
lay = QVBoxLayout(page)
102105
lay.setContentsMargins(10, 8, 10, 8)
103106
lay.setSpacing(6)
104107
lay.addWidget(kit.row(self.title, self.hint, self.fit, self.spread, stretch=True))
105108
lay.addWidget(self.view, 1)
109+
lay.addWidget(self.bar)
106110
self.no_scene = NoScene(studio)
107111
self.no_scene.say(
108112
"No port manifest",
@@ -117,6 +121,16 @@ def _spread(self) -> str:
117121
raise ManifestError("no port manifest open")
118122
return graph.spread(doc)
119123

124+
# what the bar tells: the same intents as the canvas's, one undo step each
125+
126+
def _param(self, node: str, name: str, value: object) -> None:
127+
self._collect(lambda e: graph.set_param(e, node, name, value))
128+
self._settle()
129+
130+
def _label(self, node: str, text: str) -> None:
131+
self._collect(lambda e: graph.set_label(e, node, text))
132+
self._settle()
133+
120134
# what the canvas tells
121135

122136
def _collect(self, fn: Callable[[graph.Edits], str]) -> None:
@@ -154,9 +168,11 @@ def edit() -> None:
154168
m = self.ws.manifest
155169
if made is not None and m is not None and made in m.behaviour.blocks:
156170
self.view.select([made])
171+
self._show_bar()
157172

158173
def _picked(self, node: object) -> None:
159-
"""A move node picks its move, as the Moves table does."""
174+
"""A move node picks its move, as the Moves table does; the bar shows the node."""
175+
self._show_bar()
160176
name = graph.move_name(node) if isinstance(node, str) else None
161177
if name is None or name == self.ws.move:
162178
return
@@ -182,9 +198,45 @@ def sync(self) -> None:
182198
self._drawn = m
183199
self._draw(m)
184200
self._follow(m)
201+
self._show_bar()
202+
203+
def _show_bar(self) -> None:
204+
"""The bar shows the one node selected, from the document: a refused edit goes back."""
205+
m, ids = self.ws.manifest, self.view.selected()
206+
self.bar.show_detail(self._detail(m, ids[0]) if m is not None and len(ids) == 1 else None)
207+
208+
def _detail(self, m: Manifest, node: str) -> Detail | None:
209+
name = graph.move_name(node)
210+
if name is not None:
211+
mv = m.moves.get(name)
212+
return None if mv is None else Detail(node, name, self._move_tip(mv), label=mv.label)
213+
blk = m.behaviour.blocks.get(node)
214+
if blk is None or self._read is None:
215+
return None
216+
k = KINDS[blk.kind]
217+
values = model.params(blk)
218+
tip = self._block_tip(k, self._read.refused.get(node))
219+
return Detail(
220+
node,
221+
k.title,
222+
tip,
223+
k.params,
224+
{p.name: values.get(p.name) for p in k.params},
225+
graph.options(m, blk.kind),
226+
blk.label,
227+
)
228+
229+
@staticmethod
230+
def _block_tip(k: Kind, why: str | None) -> str:
231+
return "\n".join([k.tip, *([f"{WARN} {why}"] if why else [])])
232+
233+
@staticmethod
234+
def _move_tip(mv: Move) -> str:
235+
plays = f"plays {mv.clip}" if mv.clip else f"plays anim {mv.anim}"
236+
return "\n".join([f"{kind(mv)} · {plays}", *([f"Note: {mv.label}"] if mv.label else [])])
185237

186238
def _draw(self, m: Manifest) -> None:
187-
r = graph.read(m)
239+
r = self._read = graph.read(m)
188240
nodes = self._blocks(m, r) + self._moves(m)
189241
self.view.show(nodes, [LinkSpec(*w) for w in graph.wires(m)])
190242
loose = f" · {len(r.loose)} loose" if r.loose else ""
@@ -199,15 +251,7 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
199251
k = KINDS[blk.kind]
200252
why = r.refused.get(i)
201253
marks = [*(f"#{n}" for n in r.priority.get(i, [])), *([WARN] if why else [])]
202-
values = model.params(blk)
203-
options: dict[str, list[tuple[str, object]]] = {}
204-
for p in k.params:
205-
if p.type == "part":
206-
options[p.name] = list(graph.part_options(m, p.optional))
207-
elif p.type == "mains":
208-
options[p.name] = list(graph.main_options())
209-
elif p.type in NAMES:
210-
options[p.name] = [(n, n) for n in model.names(m, p.type)]
254+
note = [f"Note: {blk.label}"] if blk.label else []
211255
out.append(
212256
NodeSpec(
213257
i,
@@ -216,13 +260,10 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
216260
blk.at,
217261
(graph.IN,),
218262
(graph.OUT,),
219-
k.params,
220-
{p.name: values.get(p.name) for p in k.params},
221-
options,
263+
model.summary(m, blk),
222264
" ".join(marks),
223265
i in r.loose,
224-
blk.label,
225-
"\n".join([k.tip, *([f"{WARN} {why}"] if why else [])]),
266+
"\n".join([BehaviourPanel._block_tip(k, why), *note]),
226267
)
227268
)
228269
return out
@@ -232,7 +273,6 @@ def _moves(m: Manifest) -> list[NodeSpec]:
232273
spots = graph.spots(m)
233274
out = []
234275
for name, mv in m.moves.items():
235-
plays = f"plays {mv.clip}" if mv.clip else f"plays anim {mv.anim}"
236276
out.append(
237277
NodeSpec(
238278
graph.move_id(name),
@@ -241,8 +281,7 @@ def _moves(m: Manifest) -> list[NodeSpec]:
241281
spots[name],
242282
(graph.PLAY,),
243283
(graph.WHILE, graph.THEN),
244-
label=mv.label,
245-
tip=f"{kind(mv)} · {plays}",
284+
tip=BehaviourPanel._move_tip(mv),
246285
)
247286
)
248287
return out

‎apps/studio/src/mhfu_studio/monster/panels/moves.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
254254

255255
self.behaviour = kit.button(
256256
"Open the behaviour graph",
257-
tip="Opens the Behaviour dock: the events, states and conditions that play your moves",
257+
tip="Opens the Behaviour tab: the events, states and conditions that play your moves",
258258
on=act("open behaviour", lambda: ws.focus("Behaviour")),
259259
icon="ph.tree-structure",
260260
)

0 commit comments

Comments
 (0)