nodes/player_pause_bridge.pyΒΆ
Part of Dungeon Explorer.
1"""PlayerPauseBridge: toggles only the player's update_mode on overlay events.
2
3Passing ``inert=True`` to :meth:`Control.show_overlay` freezes the whole
4SceneTree, which is too broad for Dungeon Explorer: HUD timers, ambient audio,
5music, and screen-space overlays must keep ticking while a popup is open. This
6bridge listens to :attr:`SceneTree.overlay_opened` / :attr:`SceneTree.overlay_closed`
7and switches just the referenced player node between :attr:`UpdateMode.DISABLED`
8and :attr:`UpdateMode.INHERIT` so combat input is silenced without freezing
9the rest of the world.
10
11The bridge also exposes :meth:`open` / :meth:`close` / :attr:`is_open` as a
12small game-side facade over the overlay registry: Dungeon Explorer shows one
13popup at a time, and these helpers let call sites stay terse without
14forcing any "one overlay" policy into the engine itself.
15"""
16
17from simvx.core import Node, Property, UpdateMode
18
19__all__ = ["PlayerPauseBridge"]
20
21
22class PlayerPauseBridge(Node):
23 """Pauses ``self.player`` whenever any overlay Control is active in the tree."""
24
25 # Keep listening while the tree is paused.
26 update_mode = Property(
27 UpdateMode.ALWAYS,
28 hint="Processing behaviour while the tree is paused",
29 on_change="_invalidate_update_mode_cache",
30 )
31
32 player = Property(None) # Node | None: the actor whose update_mode is toggled
33
34 def __init__(self, player=None, **kwargs):
35 super().__init__(name="PlayerPauseBridge", **kwargs)
36 if player is not None:
37 self.player = player
38
39 def on_ready(self):
40 tree = self.tree
41 if tree is None:
42 return
43 tree.overlay_opened.connect(self._on_overlay_opened)
44 tree.overlay_closed.connect(self._on_overlay_closed)
45
46 def on_exit_tree(self):
47 tree = self.tree
48 if tree is not None:
49 tree.overlay_opened.disconnect(self._on_overlay_opened)
50 tree.overlay_closed.disconnect(self._on_overlay_closed)
51
52 def _on_overlay_opened(self, _ctrl):
53 if self.player is not None:
54 self.player.update_mode = UpdateMode.DISABLED
55
56 def _on_overlay_closed(self, _ctrl):
57 # Restore only when the registry is empty: overlay-over-overlay close
58 # events otherwise re-enable the player while another overlay is still up.
59 tree = self.tree
60 if tree is not None and tree.overlays:
61 return
62 if self.player is not None:
63 self.player.update_mode = UpdateMode.INHERIT
64
65 # -- Overlay facade (one popup at a time) --
66
67 @property
68 def is_open(self) -> bool:
69 """Whether any overlay is currently active in the tree."""
70 tree = self.tree
71 return bool(tree and tree.overlays)
72
73 @property
74 def active(self):
75 """The topmost capturing overlay Control, or None."""
76 tree = self.tree
77 if tree is not None:
78 return tree.overlays.topmost_capturing()
79 return None
80
81 def open(self, popup) -> None:
82 """Close any active overlay then show ``popup`` as the new overlay.
83
84 ``popup`` must already be in the tree (typically as a child of the
85 UI ``CanvasLayer``). If ``popup`` defines a custom :meth:`show`
86 helper that resets per-open state, that is preferred over the bare
87 :meth:`show_overlay`.
88 """
89 tree = self.tree
90 if tree is not None:
91 for entry in tree.overlays.draw_set():
92 if entry is popup:
93 continue
94 if hasattr(entry, "close_overlay"):
95 entry.close_overlay()
96
97 if hasattr(popup, "show"):
98 popup.show()
99 else:
100 popup.show_overlay("blocking")
101
102 def close(self) -> None:
103 """Close the topmost active overlay (idempotent)."""
104 active = self.active
105 if active is not None and hasattr(active, "close_overlay"):
106 active.close_overlay()