nodes/dialogue.pyΒΆ
Part of GDQuest Open RPG.
1"""Dialogue box: bottom panel with typewriter text + advance-on-key.
2
3An immediate-mode Node2D overlay: the panel is painted straight into the 2D
4draw stream each frame from the live viewport size, so it needs no widget tree
5and follows the window (or browser canvas) when it resizes.
6"""
7
8from __future__ import annotations
9
10from simvx.core import Node2D
11from simvx.core.input.state import Input
12from simvx.core.signals import Signal
13
14from .settings import (
15 PANEL,
16 PANEL_BORDER,
17 TEXT,
18 TEXT_DIM,
19)
20
21
22class DialogueBox(Node2D):
23 """Bottom-anchored dialogue panel with typewriter effect."""
24
25 PANEL_HEIGHT = 130
26 SIDE_MARGIN = 32
27 BOTTOM_MARGIN = 36 # clears the controls strip
28 CHARS_PER_SEC = 70.0
29
30 def __init__(self) -> None:
31 super().__init__()
32 self.z_index = 5000
33
34 self.finished = Signal() # fires when dialogue sequence ends
35
36 self._lines: list[tuple[str, str]] = []
37 self._index = 0
38 self._reveal_idx = 0.0
39 self._waiting_for_advance = False
40 self._just_advanced_cooldown = 0.0
41 self._showing = False
42
43 # ------------------------------------------------------------------
44 # Public API
45 # ------------------------------------------------------------------
46 def show_lines(self, lines: list[tuple[str, str]]) -> None:
47 if not lines:
48 self.finished.emit()
49 return
50 self._lines = list(lines)
51 self._index = 0
52 self._reveal_idx = 0.0
53 self._waiting_for_advance = False
54 self._just_advanced_cooldown = 0.20
55 self._showing = True
56
57 @property
58 def is_showing(self) -> bool:
59 return self._showing
60
61 # ------------------------------------------------------------------
62 # Process: typewriter + advance
63 # ------------------------------------------------------------------
64 def on_update(self, dt: float) -> None:
65 if not self._showing or not self._lines:
66 return
67 # Persistent overlay added once: the typewriter reveal (`_reveal_idx`) and the
68 # advance arrow animate every frame from non-Property state, so dirty it each
69 # showing frame (it emits 0 ops while hidden, so `dynamic` could miss the first
70 # 0->N upload without a structure change).
71 self.queue_redraw()
72 if self._just_advanced_cooldown > 0:
73 self._just_advanced_cooldown -= dt
74 speaker, text = self._lines[self._index]
75 full_len = len(text)
76 if not self._waiting_for_advance:
77 self._reveal_idx += dt * self.CHARS_PER_SEC
78 if self._reveal_idx >= full_len:
79 self._reveal_idx = float(full_len)
80 self._waiting_for_advance = True
81
82 # Advance on a key, a face button or a tap. While typewriting, the
83 # first press fast-forwards to the full line.
84 if self._just_advanced_cooldown <= 0 and Input.just_pressed("advance"):
85 if not self._waiting_for_advance:
86 self._reveal_idx = float(full_len)
87 self._waiting_for_advance = True
88 else:
89 self._index += 1
90 self._reveal_idx = 0.0
91 self._waiting_for_advance = False
92 self._just_advanced_cooldown = 0.10
93 if self._index >= len(self._lines):
94 self._lines = []
95 self._showing = False
96 self.finished.emit()
97
98 # ------------------------------------------------------------------
99 # Draw the dialogue panel
100 # ------------------------------------------------------------------
101 def on_draw(self, renderer) -> None:
102 if not self._showing or not self._lines:
103 return
104 ww, wh = self.viewport_size
105 x = self.SIDE_MARGIN
106 y = wh - self.BOTTOM_MARGIN - self.PANEL_HEIGHT
107 w = ww - 2 * self.SIDE_MARGIN
108 h = self.PANEL_HEIGHT
109
110 # Drop shadow
111 renderer.rect((x + 4, y + 4), (w, h), fill=(0, 0, 0, 0.40))
112 # Panel body
113 renderer.rect((x, y), (w, h), fill=PANEL)
114 # Border (3 line widths)
115 for k in range(3):
116 renderer.rect((x - k, y - k), (w + 2 * k, h + 2 * k), stroke=PANEL_BORDER)
117
118 speaker, text = self._lines[self._index]
119 # Speaker label tab: top-left
120 if speaker:
121 tab_w = max(80, int(renderer.text_width(speaker, 1.4)) + 24)
122 renderer.rect((x + 12, y - 18), (tab_w, 24), fill=PANEL_BORDER)
123 renderer.rect((x + 12, y - 18), (tab_w, 24), stroke=(0, 0, 0, 1))
124 renderer.text(speaker, (x + 24, y - 14), fill=(0.10, 0.10, 0.16, 1.0), scale=1.4)
125
126 # Body text: typewriter reveal
127 revealed = text[: int(self._reveal_idx)]
128 renderer.text(revealed, (x + 24, y + 24), fill=TEXT, scale=1.4)
129
130 # Advance indicator
131 if self._waiting_for_advance:
132 arrow_x = x + w - 32
133 arrow_y = y + h - 28
134 renderer.text("v", (arrow_x, arrow_y), fill=TEXT_DIM, scale=1.4)