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