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