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)