Skip to content

Commit 33be747

Browse files
committed
Merge editing inside a trigger's rows
2 parents ec1ccf9 + f914df7 commit 33be747

8 files changed

Lines changed: 1604 additions & 113 deletions

File tree

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

Lines changed: 218 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313

1414
import itertools
1515
import typing
16+
from collections import Counter
1617
from collections.abc import Callable, Collection, Mapping, Sequence
1718
from typing import Any, NamedTuple, Protocol
1819

1920
from mhfu_port import behaviour, sequence
20-
from mhfu_port.behaviour import KINDS, Block, MoveNode, Param, Path
21+
from mhfu_port.behaviour import KINDS, Block, MoveNode, Param, Path, Trigger
2122
from mhfu_port.manifest import Manifest, ManifestError
2223

2324
from mhfu_studio.monster.document import PortDocument
@@ -30,6 +31,8 @@
3031
"""A move node's ports: the input, then the two outputs."""
3132
ALL = "all"
3233
"""The scope that is the whole canvas, as stored; any other is a trigger's key."""
34+
INSERTABLE = ("condition", "modifier", "effect")
35+
"""The roles of the blocks a rule holds besides its first."""
3336
ROLE_TITLES = {"state": "State"}
3437
"""A palette group's title where the role in plural is not it."""
3538

@@ -205,9 +208,10 @@ def options(m: Manifest, kind: str) -> dict[str, list[tuple[str, object]]]:
205208
return out
206209

207210

208-
def palette() -> list[tuple[str, list[tuple[str, str, str]]]]:
209-
"""The kinds a block can be, a group per role: (kind, title, tip). All from `KINDS`."""
210-
roles = typing.get_args(behaviour.Role)
211+
def palette(only: Collection[str] | None = None) -> list[tuple[str, list[tuple[str, str, str]]]]:
212+
"""The kinds a block can be, a group per role (only those in `only`, if given): (kind,
213+
title, tip). All from `KINDS`."""
214+
roles = [r for r in typing.get_args(behaviour.Role) if only is None or r in only]
211215
groups = [
212216
(
213217
ROLE_TITLES.get(role, role.capitalize() + "s"),
@@ -218,6 +222,11 @@ def palette() -> list[tuple[str, list[tuple[str, str, str]]]]:
218222
return [g for g in groups if g[1]]
219223

220224

225+
def insertable() -> list[tuple[str, list[tuple[str, str, str]]]]:
226+
"""The palette of what goes inside a rule: not the event or state that starts it."""
227+
return palette(INSERTABLE)
228+
229+
221230
def fresh(stem: str, taken: Collection[str]) -> str:
222231
"""The first of `stem1`, `stem2`, ... not in `taken`."""
223232
return next(f"{stem}{k}" for k in itertools.count(1) if f"{stem}{k}" not in taken)
@@ -489,3 +498,208 @@ def scale(m: Manifest) -> None:
489498

490499
doc.edit(scale)
491500
return f"{len(b.blocks) + len(b.moves)} nodes spread out"
501+
502+
503+
# rules: what a trigger's rows edit
504+
505+
#: the gap that puts a new rule's first block below every other block
506+
FRESH = float(behaviour.ROW)
507+
508+
509+
def open_rules(m: Manifest) -> list[Path]:
510+
"""The chains from a source that stop at a block that plays nothing and feeds nothing: rules
511+
that have no move yet, which the brain does not check. First block's place first."""
512+
b = m.behaviour
513+
done = {p.blocks for p in behaviour.paths(m)}
514+
out: list[Path] = []
515+
516+
def walk(trail: tuple[str, ...], during: str | None) -> None:
517+
blk = b.blocks[trail[-1]]
518+
if not blk.play and not blk.next and trail not in done:
519+
out.append(Path(trail, "", during))
520+
for n in blk.next:
521+
if n not in trail:
522+
walk((*trail, n), during)
523+
524+
for i, during in behaviour.sources(m):
525+
walk((i,), during)
526+
return sorted(out, key=lambda p: (b.blocks[p.blocks[0]].at[1], b.blocks[p.blocks[0]].at[0]))
527+
528+
529+
def _values(kind: str, by: str = "") -> dict[str, object]:
530+
"""A new block's params: valid ones, the trigger's `by` value among them."""
531+
params = {p.name: v for p in KINDS[kind].params if (v := start(p)) is not None}
532+
if by and KINDS[kind].by:
533+
params[KINDS[kind].by] = by
534+
return params
535+
536+
537+
def add_rule(doc: Edits, t: Trigger) -> str:
538+
"""A rule for trigger `t`: its first block (the event or state kind with the trigger's
539+
value; for a move's while-playing port a Played for block fed by it), below every other
540+
block, so it is checked last. It plays no move yet."""
541+
m = doc.manifest
542+
b = m.behaviour
543+
if t.section == "move":
544+
if t.name not in b.moves:
545+
raise ManifestError(f"no move {t.name!r}")
546+
kind = "played_for"
547+
else:
548+
kind = t.kind
549+
if kind not in KINDS:
550+
raise ManifestError(f"{kind!r} is not a kind of block")
551+
params = _values(kind, t.name if t.section == "event" else "")
552+
i = behaviour.new_id(b)
553+
y = max((blk.at[1] for blk in b.blocks.values()), default=-FRESH) + FRESH
554+
at = _at((float(behaviour.LEFT), y))
555+
556+
def add(m: Manifest) -> None:
557+
m.behaviour.blocks[i] = Block(kind, at, params)
558+
if t.section == "move":
559+
m.behaviour.moves[t.name].during.append(i)
560+
561+
doc.edit(add)
562+
return f"{KINDS[kind].title}: a rule added, checked last"
563+
564+
565+
def insert_block(doc: Edits, after: str, kind: str, to: str | None, play: str | None) -> str:
566+
"""A block of `kind` in a rule after block `after`. `to` is the block the rule goes on to
567+
from `after`, and `play` the move it ends in when it ends there: the new block takes that
568+
place, so `after` feeds it and it feeds `to` or plays `play`."""
569+
m = doc.manifest
570+
here = _block(m, after)
571+
if kind not in KINDS or KINDS[kind].role not in INSERTABLE:
572+
raise ManifestError(f"{kind!r} is not a block a rule holds")
573+
if to is not None and to not in here.next:
574+
raise ManifestError(f"{_say(m, after)} does not feed {_say(m, to)}")
575+
if play is not None and play not in here.play:
576+
raise ManifestError(f"{_say(m, after)} does not play {play}")
577+
i = behaviour.new_id(m.behaviour)
578+
x = (here.at[0] + _block(m, to).at[0]) / 2 if to is not None else here.at[0] + behaviour.COLUMN
579+
at, params = _at((x, here.at[1])), _values(kind)
580+
581+
def add(m: Manifest) -> None:
582+
blocks = m.behaviour.blocks
583+
a, new = blocks[after], Block(kind, at, params)
584+
if to is not None:
585+
new.next = [to]
586+
a.next = [i if n == to else n for n in a.next]
587+
else:
588+
if play is not None:
589+
new.play = [play]
590+
a.play = [p for p in a.play if p != play]
591+
a.next = [*a.next, i]
592+
blocks[i] = new
593+
594+
doc.edit(add)
595+
return f"{KINDS[kind].title} inserted after {_say(m, after)}"
596+
597+
598+
def drop_block(doc: Edits, block: str) -> str:
599+
"""A block out of every rule that holds it, the chain closing up: what fed it feeds what it
600+
fed, and moves it played are played by what fed it. Not the first block of a rule."""
601+
m = doc.manifest
602+
_block(m, block)
603+
if not any(block in blk.next for blk in m.behaviour.blocks.values()):
604+
raise ManifestError("the first block of a rule goes with the rule")
605+
606+
def close(m: Manifest) -> None:
607+
blocks = m.behaviour.blocks
608+
gone = blocks.pop(block)
609+
for blk in blocks.values():
610+
if block in blk.next:
611+
blk.next = list(
612+
dict.fromkeys(n for x in blk.next for n in (gone.next if x == block else [x]))
613+
)
614+
blk.play = list(dict.fromkeys([*blk.play, *gone.play]))
615+
616+
doc.edit(close)
617+
return f"{_say(m, block)} deleted"
618+
619+
620+
def drop_rule(doc: Edits, blocks: Sequence[str], play: str | None) -> str:
621+
"""A rule out: its move, then each block from its end back that has nothing left to feed or
622+
play. Blocks other rules still run through stay."""
623+
m = doc.manifest
624+
for i in blocks:
625+
_block(m, i)
626+
627+
def drop(m: Manifest) -> None:
628+
b = m.behaviour
629+
if play is not None:
630+
b.blocks[blocks[-1]].play = [p for p in b.blocks[blocks[-1]].play if p != play]
631+
for i in reversed(blocks):
632+
if i not in b.blocks or b.blocks[i].next or b.blocks[i].play:
633+
break
634+
del b.blocks[i]
635+
for other in b.blocks.values():
636+
other.next = [n for n in other.next if n != i]
637+
for node in b.moves.values():
638+
node.during = [n for n in node.during if n != i]
639+
640+
doc.edit(drop)
641+
return "rule deleted"
642+
643+
644+
def set_play(doc: Edits, block: str, old: str | None, new: str | None) -> str:
645+
"""The move `block` plays: `old` becomes `new`; no `old` adds one, no `new` takes it away."""
646+
m = doc.manifest
647+
here = _block(m, block)
648+
if new is not None:
649+
_move(m, new)
650+
if old is not None and old not in here.play:
651+
raise ManifestError(f"{_say(m, block)} no longer plays {old}")
652+
if old == new:
653+
return f"{_say(m, block)}: unchanged"
654+
655+
def change(m: Manifest) -> None:
656+
plays = m.behaviour.blocks[block].play
657+
if old is None and new is not None:
658+
plays.append(new)
659+
elif old is not None and new is None:
660+
plays.remove(old)
661+
elif old is not None and new is not None:
662+
plays[plays.index(old)] = new
663+
664+
doc.edit(change)
665+
return (
666+
f"{_say(m, block)}: plays {new}" if new is not None else f"{_say(m, block)}: plays nothing"
667+
)
668+
669+
670+
def move_rule(doc: Edits, rules: Sequence[Path], moved: int, to: int) -> str:
671+
"""Of one trigger's `rules`, in the order they are checked, the one at `moved` takes place
672+
`to`. The places of the rules' first blocks are handed round among them, so every other rule
673+
keeps its place in the whole order; a rule's own blocks move with its first block, a move
674+
node never. Refused if the rule would keep its place."""
675+
m = doc.manifest
676+
roots = [p.blocks[0] for p in rules]
677+
if len(set(roots)) != len(roots):
678+
raise ManifestError("rules that start from the same block cannot be put in another order")
679+
if moved == to:
680+
return "unchanged"
681+
order = list(range(len(rules)))
682+
order.insert(to, order.pop(moved))
683+
heights = sorted(m.behaviour.blocks[r].at[1] for r in roots)
684+
new_y = {roots[old]: heights[k] for k, old in enumerate(order)}
685+
held = Counter(i for p in [*behaviour.paths(m), *open_rules(m)] for i in set(p.blocks))
686+
key = behaviour.trigger_key(m, rules[0])
687+
want = [roots[old] for old in order]
688+
689+
def shift(m: Manifest) -> None:
690+
blocks = m.behaviour.blocks
691+
for old, root in enumerate(roots):
692+
dy = new_y[root] - blocks[root].at[1]
693+
for i in rules[old].blocks:
694+
if dy and (i == root or held[i] == 1):
695+
x, y = blocks[i].at
696+
blocks[i].at = _at((x, y + dy))
697+
got = next(t for t in behaviour.triggers(m) if t.key == key)
698+
if [p.blocks[0] for _, p in got.rules] != want:
699+
raise ManifestError(
700+
"that would not change the rule's rank: rules at the same height are checked"
701+
" left to right, and rules that play nothing go first"
702+
)
703+
704+
doc.edit(shift)
705+
return f"rule moved to place {to + 1} of its trigger"

0 commit comments

Comments
 (0)