nodes/loading_screen.pyΒΆ

Part of Dungeon Explorer.

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