nodes/credits_screen.py¶
Part of Dungeon Explorer.
1"""Animated credits screen with scrolling text."""
2
3from simvx.core import Control, Property
4from simvx.core.input import MouseButton
5from simvx.core.ui.enums import AnchorPreset
6
7from ._back_button import back_button_hit, draw_back_button
8
9
10class CreditsScreen(Control):
11 """Animated credits scroll. Skippable with Space/Escape/click."""
12
13 visible = Property(
14 False,
15 coerce=bool,
16 hint="Whether this node and its subtree are drawn",
17 on_change="_on_visible_changed",
18 )
19
20 # on_draw renders text at self._scroll_y, which drifts every frame in
21 # on_update -> retained 2D must re-run it each frame.
22 dynamic = True
23
24 CREDITS = [
25 ("DUNGEON EXPLORER", "title"),
26 ("", ""),
27 ("Design & Programming", "heading"),
28 ("Built with SimVX Engine", "body"),
29 ("", ""),
30 ("Engine", "heading"),
31 ("SimVX - Pure Python Game Engine", "body"),
32 ("Vulkan GPU-Driven Rendering", "body"),
33 ("Node-Based Scene Hierarchy", "body"),
34 ("", ""),
35 ("Art Direction", "heading"),
36 ("Procedural Draw API Visuals", "body"),
37 ("No External Assets Required", "body"),
38 ("", ""),
39 ("Audio", "heading"),
40 ("Procedural Waveform Synthesis", "body"),
41 ("", ""),
42 ("Special Thanks", "heading"),
43 ("The roguelike community", "body"),
44 ("Classic dungeon crawlers", "body"),
45 ("Open-source game development", "body"),
46 ("", ""),
47 ("", ""),
48 ("Thank you for playing!", "title"),
49 ]
50
51 def __init__(self, on_done=None, **kwargs):
52 super().__init__(name="CreditsScreen", **kwargs)
53 self.set_anchor_preset(AnchorPreset.FULL_RECT)
54
55 self._on_done = on_done
56 self._scroll_y = 720.0
57 self._timer = 0.0
58
59 self.cancel_requested.connect(self._fire_done)
60
61 # -- Public API --
62
63 def show(self):
64 self._scroll_y = 720.0
65 self._timer = 0.0
66 self.show_overlay("blocking", dismiss=True)
67
68 def _fire_done(self):
69 if self._on_done:
70 self._on_done()
71
72 # -- Frame update --
73
74 def on_update(self, dt: float):
75 self._timer += dt
76 self._scroll_y -= dt * 40.0
77 # Auto-close when scrolled past all content.
78 if self._scroll_y < -len(self.CREDITS) * 40 and self.visible:
79 self.cancel_requested.emit()
80 self.close_overlay()
81
82 # -- Input --
83
84 def _on_gui_input(self, event):
85 if not self.visible:
86 return
87 if event.button == MouseButton.LEFT and event.pressed:
88 mx, my = float(event.position[0]), float(event.position[1])
89 if back_button_hit(mx, my):
90 self.cancel_requested.emit()
91 self.close_overlay()
92 event.handled = True
93 return
94 # Any click skips the credits.
95 self.cancel_requested.emit()
96 self.close_overlay()
97 event.handled = True
98 elif event.key and event.pressed and event.key in ("space", "enter", "return"):
99 self.cancel_requested.emit()
100 self.close_overlay()
101 event.handled = True
102 # Esc handled by router → cancel_requested.
103
104 # -- Rendering --
105
106 def _screen_size(self) -> tuple[float, float]:
107 if self.tree is not None:
108 sw, sh = self.tree.screen_size
109 return float(sw), float(sh)
110 return 1280.0, 720.0
111
112 def on_draw(self, renderer):
113 if not self.visible:
114 return
115 sw, sh = self._screen_size()
116 renderer.draw_rect((0, 0), (sw, sh), colour=(0.03, 0.02, 0.05, 1.0), filled=True)
117
118 cx = sw / 2
119 y = self._scroll_y
120 # Reserve the bottom strip for the "Press Space to skip" footer so it
121 # never collides with the scrolling credits text.
122 footer_band = 50
123 scroll_bottom = sh - footer_band
124
125 for text, style in self.CREDITS:
126 if 0 < y < scroll_bottom and text:
127 if style == "title":
128 renderer.draw_text(text, (cx - len(text) * 7, y), scale=2.0, colour=(1.0, 0.85, 0.2, 1.0))
129 elif style == "heading":
130 renderer.draw_text(text, (cx - len(text) * 5, y), scale=1.3, colour=(0.9, 0.9, 0.9, 1.0))
131 else:
132 renderer.draw_text(text, (cx - len(text) * 4, y), scale=1.0, colour=(0.6, 0.6, 0.6, 1.0))
133 y += 40
134
135 # Background strip behind the footer to mask the tail of any line
136 # passing through the band as it scrolls upward.
137 renderer.draw_rect((0, sh - footer_band), (sw, footer_band), colour=(0.03, 0.02, 0.05, 1.0), filled=True)
138 renderer.draw_text("Press Space to skip", (cx - 60, sh - 30), scale=0.8, colour=(0.3, 0.3, 0.3, 1.0))
139 draw_back_button(renderer)