Skip to content

Commit fef023c

Browse files
committed
Merge the Behaviour tab, compact nodes and the property bar
2 parents 5893d4c + bcfa4ea commit fef023c

15 files changed

Lines changed: 1662 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)
@@ -94,14 +98,13 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
9498
v.link_requested.connect(lambda *w: self._collect(lambda e: graph.link(e, *w)))
9599
v.unlink_requested.connect(lambda *w: self._collect(lambda e: graph.unlink(e, *w)))
96100
v.delete_requested.connect(lambda ids: self._collect(lambda e: graph.delete(e, ids)))
97-
v.param_changed.connect(
98-
lambda i, n, x: self._collect(lambda e: graph.set_param(e, i, n, x))
99-
)
100-
v.label_changed.connect(lambda i, t: self._collect(lambda e: graph.set_label(e, i, t)))
101101
v.moved.connect(lambda i, x, y: self._dropped.__setitem__(i, (x, y)))
102102
v.create_requested.connect(self._create)
103103
v.picked.connect(self._picked)
104104
v.settled.connect(self._settle)
105+
self.bar = PropertyBar()
106+
self.bar.param_changed.connect(self._param)
107+
self.bar.label_changed.connect(self._label)
105108

106109
page = QWidget()
107110
lay = QVBoxLayout(page)
@@ -111,6 +114,7 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
111114
kit.row(self.title, self.hint, self.rage, self.fit, self.spread, stretch=True)
112115
)
113116
lay.addWidget(self.view, 1)
117+
lay.addWidget(self.bar)
114118
self.no_scene = NoScene(studio)
115119
self.no_scene.say(
116120
"No port manifest",
@@ -132,6 +136,16 @@ def _natural_rage(self, on: bool) -> None:
132136
"natural rage", lambda: self.ws.edit("", lambda: graph.set_natural_rage(doc, on))
133137
)()
134138

139+
# what the bar tells: the same intents as the canvas's, one undo step each
140+
141+
def _param(self, node: str, name: str, value: object) -> None:
142+
self._collect(lambda e: graph.set_param(e, node, name, value))
143+
self._settle()
144+
145+
def _label(self, node: str, text: str) -> None:
146+
self._collect(lambda e: graph.set_label(e, node, text))
147+
self._settle()
148+
135149
# what the canvas tells
136150

137151
def _collect(self, fn: Callable[[graph.Edits], str]) -> None:
@@ -169,9 +183,11 @@ def edit() -> None:
169183
m = self.ws.manifest
170184
if made is not None and m is not None and made in m.behaviour.blocks:
171185
self.view.select([made])
186+
self._show_bar()
172187

173188
def _picked(self, node: object) -> None:
174-
"""A move node picks its move, as the Moves table does."""
189+
"""A move node picks its move, as the Moves table does; the bar shows the node."""
190+
self._show_bar()
175191
name = graph.move_name(node) if isinstance(node, str) else None
176192
if name is None or name == self.ws.move:
177193
return
@@ -198,9 +214,45 @@ def sync(self) -> None:
198214
self._draw(m)
199215
kit.put(self.rage, graph.natural_rage(m))
200216
self._follow(m)
217+
self._show_bar()
218+
219+
def _show_bar(self) -> None:
220+
"""The bar shows the one node selected, from the document: a refused edit goes back."""
221+
m, ids = self.ws.manifest, self.view.selected()
222+
self.bar.show_detail(self._detail(m, ids[0]) if m is not None and len(ids) == 1 else None)
223+
224+
def _detail(self, m: Manifest, node: str) -> Detail | None:
225+
name = graph.move_name(node)
226+
if name is not None:
227+
mv = m.moves.get(name)
228+
return None if mv is None else Detail(node, name, self._move_tip(mv), label=mv.label)
229+
blk = m.behaviour.blocks.get(node)
230+
if blk is None or self._read is None:
231+
return None
232+
k = KINDS[blk.kind]
233+
values = model.params(blk)
234+
tip = self._block_tip(k, self._read.refused.get(node))
235+
return Detail(
236+
node,
237+
k.title,
238+
tip,
239+
k.params,
240+
{p.name: values.get(p.name) for p in k.params},
241+
graph.options(m, blk.kind),
242+
blk.label,
243+
)
244+
245+
@staticmethod
246+
def _block_tip(k: Kind, why: str | None) -> str:
247+
return "\n".join([k.tip, *([f"{WARN} {why}"] if why else [])])
248+
249+
@staticmethod
250+
def _move_tip(mv: Move) -> str:
251+
plays = f"plays {mv.clip}" if mv.clip else f"plays anim {mv.anim}"
252+
return "\n".join([f"{kind(mv)} · {plays}", *([f"Note: {mv.label}"] if mv.label else [])])
201253

202254
def _draw(self, m: Manifest) -> None:
203-
r = graph.read(m)
255+
r = self._read = graph.read(m)
204256
nodes = self._blocks(m, r) + self._moves(m)
205257
self.view.show(nodes, [LinkSpec(*w) for w in graph.wires(m)])
206258
loose = f" · {len(r.loose)} loose" if r.loose else ""
@@ -215,15 +267,7 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
215267
k = KINDS[blk.kind]
216268
why = r.refused.get(i)
217269
marks = [*(f"#{n}" for n in r.priority.get(i, [])), *([WARN] if why else [])]
218-
values = model.params(blk)
219-
options: dict[str, list[tuple[str, object]]] = {}
220-
for p in k.params:
221-
if p.type == "part":
222-
options[p.name] = list(graph.part_options(m, p.optional))
223-
elif p.type == "mains":
224-
options[p.name] = list(graph.main_options())
225-
elif p.type in NAMES:
226-
options[p.name] = [(n, n) for n in model.names(m, p.type)]
270+
note = [f"Note: {blk.label}"] if blk.label else []
227271
out.append(
228272
NodeSpec(
229273
i,
@@ -232,13 +276,10 @@ def _blocks(m: Manifest, r: graph.Reading) -> list[NodeSpec]:
232276
blk.at,
233277
(graph.IN,),
234278
(graph.OUT,),
235-
k.params,
236-
{p.name: values.get(p.name) for p in k.params},
237-
options,
279+
model.summary(m, blk),
238280
" ".join(marks),
239281
i in r.loose,
240-
blk.label,
241-
"\n".join([k.tip, *([f"{WARN} {why}"] if why else [])]),
282+
"\n".join([BehaviourPanel._block_tip(k, why), *note]),
242283
)
243284
)
244285
return out
@@ -248,7 +289,6 @@ def _moves(m: Manifest) -> list[NodeSpec]:
248289
spots = graph.spots(m)
249290
out = []
250291
for name, mv in m.moves.items():
251-
plays = f"plays {mv.clip}" if mv.clip else f"plays anim {mv.anim}"
252292
out.append(
253293
NodeSpec(
254294
graph.move_id(name),
@@ -257,8 +297,7 @@ def _moves(m: Manifest) -> list[NodeSpec]:
257297
spots[name],
258298
(graph.PLAY,),
259299
(graph.WHILE, graph.THEN),
260-
label=mv.label,
261-
tip=f"{kind(mv)} · {plays}",
300+
tip=BehaviourPanel._move_tip(mv),
262301
)
263302
)
264303
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)