nodes/death_screen.pyΒΆ

Part of Dungeon Explorer.

  1"""Death / respawn overlay: red vignette, stat summary, pulsing respawn prompt."""
  2
  3import math
  4
  5from simvx.core import Control, Property
  6from simvx.core.input import MouseButton
  7from simvx.core.ui.enums import AnchorPreset
  8
  9
 10class DeathScreen(Control):
 11    """Full-screen death overlay with red vignette and stats.
 12
 13    No back button. Esc / outside click is suppressed (player must press
 14    Space or click anywhere after the 2 second reveal). The respawn action
 15    is delivered via the ``on_respawn`` callback supplied at construction.
 16    """
 17
 18    visible = Property(
 19        False,
 20        coerce=bool,
 21        hint="Whether this node and its subtree are drawn",
 22        on_change="_on_visible_changed",
 23    )
 24
 25    # on_draw animates every frame off self._timer (fade-in alpha, staggered
 26    # reveals, pulsing respawn prompt) -> retained 2D must re-run it each frame.
 27    dynamic = True
 28
 29    def __init__(self, on_respawn=None, **kwargs):
 30        super().__init__(name="DeathScreen", **kwargs)
 31        self.set_anchor_preset(AnchorPreset.FULL_RECT)
 32
 33        self._on_respawn = on_respawn
 34        self._timer = 0.0
 35        self._player = None
 36        self._dungeon_level = 1
 37        self._gold_lost = 0
 38
 39    # -- Public API --
 40
 41    def set_context(self, player=None, dungeon_level: int = 1) -> None:
 42        self._player = player
 43        self._dungeon_level = dungeon_level
 44
 45    def show(self):
 46        self._timer = 0.0
 47        if self._player:
 48            self._gold_lost = max(1, self._player.gold // 10)
 49        self.show_overlay("blocking", dismiss=False)
 50
 51    # -- Frame update / input --
 52
 53    def on_update(self, dt: float):
 54        self._timer += dt
 55
 56    def _on_gui_input(self, event):
 57        if not self.visible:
 58            return
 59        # Space / Enter / mouse-press after 2s reveal = respawn.
 60        if self._timer < 2.0:
 61            return
 62        if event.button == MouseButton.LEFT and event.pressed:
 63            self._respawn()
 64            event.handled = True
 65        elif event.key and event.pressed and event.key in ("space", "enter", "return", "e"):
 66            self._respawn()
 67            event.handled = True
 68
 69    def _respawn(self):
 70        self.cancel_requested.emit()
 71        self.close_overlay()
 72        if self._on_respawn:
 73            self._on_respawn()
 74
 75    # -- Rendering --
 76
 77    def _screen_size(self) -> tuple[float, float]:
 78        if self.tree is not None:
 79            sw, sh = self.tree.screen_size
 80            return float(sw), float(sh)
 81        return 1280.0, 720.0
 82
 83    def on_draw(self, renderer):
 84        if not self.visible:
 85            return
 86        sw, sh = self._screen_size()
 87        alpha = min(1.0, self._timer / 1.0)
 88
 89        # Dark red background
 90        renderer.draw_rect((0, 0), (sw, sh), colour=(0.12, 0.0, 0.0, 0.85 * alpha), filled=True)
 91
 92        # Red vignette edges
 93        vig_a = 0.5 * alpha
 94        vig_w = 80.0
 95        renderer.draw_rect((0, 0), (vig_w, sh), colour=(0.3, 0.0, 0.0, vig_a), filled=True)
 96        renderer.draw_rect((sw - vig_w, 0), (vig_w, sh), colour=(0.3, 0.0, 0.0, vig_a), filled=True)
 97        renderer.draw_rect((0, 0), (sw, vig_w), colour=(0.3, 0.0, 0.0, vig_a), filled=True)
 98        renderer.draw_rect((0, sh - vig_w), (sw, vig_w), colour=(0.3, 0.0, 0.0, vig_a), filled=True)
 99
100        cx = sw / 2
101
102        if self._timer > 0.5:
103            text_a = min(1.0, (self._timer - 0.5) * 2.0)
104            renderer.draw_text("YOU DIED", (cx - 60 + 2, 202), scale=3.0, colour=(0.0, 0.0, 0.0, text_a * 0.5))
105            renderer.draw_text("YOU DIED", (cx - 60, 200), scale=3.0, colour=(0.8, 0.1, 0.1, text_a))
106
107        if self._timer > 1.5:
108            stat_a = min(1.0, (self._timer - 1.5) * 2.0)
109            sy = 300
110            renderer.draw_text("-- Final Stats --", (cx - 60, sy), scale=1.1, colour=(0.7, 0.5, 0.5, stat_a))
111            sy += 28
112            if self._player:
113                stats = [
114                    (f"Level: {self._player.level}", (0.8, 0.8, 0.8, stat_a)),
115                    (f"Dungeon Floor: {self._dungeon_level}", (0.8, 0.8, 0.8, stat_a)),
116                    (f"Gold: {self._player.gold}", (1.0, 0.85, 0.2, stat_a)),
117                    (f"Enemies Slain: {getattr(self._player, '_total_kills', 0)}", (0.8, 0.5, 0.5, stat_a)),
118                ]
119                for text, colour in stats:
120                    renderer.draw_text(text, (cx - 70, sy), scale=1.0, colour=colour)
121                    sy += 24
122
123        if self._timer > 2.0:
124            pulse = 0.5 + 0.5 * math.sin(self._timer * 3.0)
125            renderer.draw_text(
126                "Press SPACE to respawn at last save point",
127                (cx - 150, sh - 120),
128                scale=1.2,
129                colour=(0.7, 0.7, 0.7, pulse),
130            )