nodes/loading_screen.pyΒΆ

Part of Dungeon Explorer.

  1"""Loading screen: 'Generating Dungeon...' with progress indicator."""
  2
  3import math
  4
  5from simvx.core import Anchor, Control, Property
  6
  7
  8class LoadingScreen(Control):
  9    """Full-screen loading overlay shown during dungeon generation.
 10
 11    No back button: caller dismisses via :meth:`mark_complete` once the
 12    backing work is finished. Outside-click dismissal is disabled because
 13    the screen is meant to be a hard wait gate.
 14    """
 15
 16    visible = Property(
 17        False,
 18        coerce=bool,
 19        hint="Whether this node and its subtree are drawn",
 20        on_change="_on_visible_changed",
 21    )
 22
 23    # on_draw animates every frame off self._timer (animated dots, spinner,
 24    # progress fill, rotating tips) -> retained 2D must re-run it each frame.
 25    dynamic = True
 26
 27    def __init__(self, **kwargs):
 28        super().__init__(name="LoadingScreen", anchor=Anchor.FULL_RECT, **kwargs)
 29
 30        self._timer = 0.0
 31        self._message = "Generating Dungeon..."
 32        self._progress = 0.0
 33        self._complete = False
 34
 35    # -- Public API --
 36
 37    def set_message(self, msg: str) -> None:
 38        self._message = msg
 39
 40    def set_progress(self, progress: float) -> None:
 41        self._progress = max(0.0, min(1.0, progress))
 42
 43    def mark_complete(self) -> None:
 44        """Mark loading as complete; the next on_update() call will dismiss."""
 45        self._complete = True
 46
 47    def show(self):
 48        self._timer = 0.0
 49        self._progress = 0.0
 50        self._complete = False
 51        self.show_overlay("blocking", dismiss=False)  # hard wait gate
 52
 53    # -- Frame update --
 54
 55    def on_update(self, dt: float):
 56        self._timer += dt
 57        if self._progress < 0.01:
 58            self._progress = min(0.9, self._timer / 2.0)
 59        if self._complete and self.visible:
 60            self.cancel_requested.emit()
 61            self.close_overlay()
 62
 63    # -- Rendering --
 64
 65    def _screen_size(self) -> tuple[float, float]:
 66        if self.tree is not None:
 67            sw, sh = self.tree.screen_size
 68            return float(sw), float(sh)
 69        return 1280.0, 720.0
 70
 71    def on_draw(self, renderer):
 72        if not self.visible:
 73            return
 74        sw, sh = self._screen_size()
 75
 76        # Dark background
 77        renderer.rect((0, 0), (sw, sh), fill=(0.04, 0.03, 0.05, 1.0))
 78
 79        cx, cy = sw / 2, sh / 2
 80
 81        renderer.text(self._message, (cx - 100, cy - 30), fill=(0.8, 0.75, 0.5, 1.0), scale=1.8)
 82
 83        # Progress bar
 84        bar_w, bar_h = 300.0, 12.0
 85        bar_x = cx - bar_w / 2
 86        bar_y = cy + 20
 87        renderer.rect((bar_x, bar_y), (bar_w, bar_h), fill=(0.15, 0.15, 0.18, 0.9))
 88        fill_w = int(bar_w * self._progress)
 89        if fill_w > 0:
 90            renderer.rect((bar_x, bar_y), (fill_w, bar_h), fill=(0.4, 0.6, 0.9, 0.9))
 91            renderer.rect((bar_x, bar_y), (fill_w, 2), fill=(0.6, 0.8, 1.0, 0.4))
 92
 93        # Animated dots
 94        dots = "." * (int(self._timer * 2) % 4)
 95        renderer.text(dots, (cx + 90, cy - 30), fill=(0.8, 0.75, 0.5, 1.0), scale=1.8)
 96
 97        # Spinner
 98        angle = self._timer * 4.0
 99        for i in range(8):
100            a = angle + i * math.pi / 4
101            dx = math.cos(a) * 20
102            dy = math.sin(a) * 20
103            alpha = 0.2 + 0.6 * ((i / 8.0 + self._timer) % 1.0)
104            renderer.circle((cx + dx, cy + 70 + dy), 3, fill=(0.5, 0.5, 0.6, alpha))
105
106        # Tip text
107        tips = [
108            "Deeper floors have stronger enemies and better loot.",
109            "Use dodge roll (Shift) to avoid attacks.",
110            "Visit the town every 10 floors for shopping and quests.",
111            "Equipment with higher rarity gives better stats.",
112            "Combo kills give bonus XP!",
113        ]
114        tip_idx = int(self._timer / 3) % len(tips)
115        renderer.text(tips[tip_idx], (cx - 180, cy + 120), fill=(0.45, 0.45, 0.5, 0.8), scale=0.9)