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()