crimsonland 0.1.0.dev11__py3-none-any.whl → 0.1.0.dev12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crimson/creatures/runtime.py +15 -0
- crimson/demo.py +47 -38
- crimson/effects.py +46 -1
- crimson/frontend/boot.py +2 -1
- crimson/frontend/menu.py +6 -27
- crimson/frontend/panels/base.py +13 -3
- crimson/frontend/panels/controls.py +1 -3
- crimson/frontend/panels/mods.py +1 -3
- crimson/frontend/panels/options.py +35 -42
- crimson/frontend/panels/play_game.py +78 -70
- crimson/frontend/panels/stats.py +1 -3
- crimson/frontend/pause_menu.py +425 -0
- crimson/game.py +315 -446
- crimson/gameplay.py +35 -6
- crimson/modes/base_gameplay_mode.py +3 -0
- crimson/modes/quest_mode.py +45 -36
- crimson/modes/rush_mode.py +4 -1
- crimson/modes/survival_mode.py +6 -2
- crimson/modes/tutorial_mode.py +6 -2
- crimson/modes/typo_mode.py +4 -1
- crimson/persistence/highscores.py +6 -2
- crimson/render/world_renderer.py +1 -1
- crimson/sim/world_state.py +8 -1
- crimson/typo/spawns.py +3 -4
- crimson/ui/demo_trial_overlay.py +3 -3
- crimson/ui/game_over.py +18 -2
- crimson/ui/perk_menu.py +106 -14
- crimson/ui/quest_results.py +663 -0
- crimson/ui/shadow.py +39 -0
- crimson/views/particles.py +1 -1
- crimson/weapons.py +110 -110
- {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev12.dist-info}/METADATA +1 -1
- {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev12.dist-info}/RECORD +36 -33
- grim/app.py +3 -0
- {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev12.dist-info}/WHEEL +0 -0
- {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev12.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import math
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pyray as rl
|
|
9
|
+
|
|
10
|
+
from grim.assets import TextureLoader
|
|
11
|
+
from grim.config import CrimsonConfig
|
|
12
|
+
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font, measure_small_text_width
|
|
13
|
+
|
|
14
|
+
from ..persistence.highscores import (
|
|
15
|
+
NAME_MAX_EDIT,
|
|
16
|
+
TABLE_MAX,
|
|
17
|
+
HighScoreRecord,
|
|
18
|
+
rank_index,
|
|
19
|
+
read_highscore_table,
|
|
20
|
+
scores_path_for_mode,
|
|
21
|
+
upsert_highscore_record,
|
|
22
|
+
)
|
|
23
|
+
from ..quests.results import QuestFinalTime, QuestResultsBreakdownAnim, tick_quest_results_breakdown_anim
|
|
24
|
+
from .perk_menu import (
|
|
25
|
+
PerkMenuAssets,
|
|
26
|
+
UiButtonState,
|
|
27
|
+
button_draw,
|
|
28
|
+
button_update,
|
|
29
|
+
button_width,
|
|
30
|
+
cursor_draw,
|
|
31
|
+
draw_menu_panel,
|
|
32
|
+
draw_ui_text,
|
|
33
|
+
load_perk_menu_assets,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
UI_BASE_WIDTH = 640.0
|
|
38
|
+
UI_BASE_HEIGHT = 480.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ui_scale(screen_w: float, screen_h: float) -> float:
|
|
42
|
+
# Classic UI-space: draw in backbuffer pixels.
|
|
43
|
+
return 1.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ui_origin(screen_w: float, screen_h: float, scale: float) -> tuple[float, float]:
|
|
47
|
+
return 0.0, 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _menu_widescreen_y_shift(layout_w: float) -> float:
|
|
51
|
+
# ui_menu_layout_init: pos_y += (screen_width / 640.0) * 150.0 - 150.0
|
|
52
|
+
return (layout_w / UI_BASE_WIDTH) * 150.0 - 150.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# `quest_results_screen_update` base layout (Crimsonland classic UI panel).
|
|
56
|
+
# Values are derived from `ui_menu_assets_init` + `ui_menu_layout_init` and how
|
|
57
|
+
# the quest results screen composes `ui_menuPanel` geometry:
|
|
58
|
+
# panel_left = geom_x0 + pos_x + 180 + slide_x
|
|
59
|
+
# panel_top = geom_y0 + pos_y
|
|
60
|
+
#
|
|
61
|
+
# Where:
|
|
62
|
+
# - pos_x/pos_y are `ui_element_t` position fields set to (-45, 110)
|
|
63
|
+
# - geom_x0/geom_y0 are the first vertex coordinates of the `ui_menuPanel` geo,
|
|
64
|
+
# after `ui_menu_assets_init` transforms it into an 8-vertex 3-slice panel.
|
|
65
|
+
QUEST_RESULTS_PANEL_POS_X = -45.0
|
|
66
|
+
QUEST_RESULTS_PANEL_POS_Y = 110.0
|
|
67
|
+
QUEST_RESULTS_PANEL_GEOM_X0 = -63.0
|
|
68
|
+
QUEST_RESULTS_PANEL_GEOM_Y0 = -81.0
|
|
69
|
+
QUEST_RESULTS_PANEL_BASE_X = 180.0
|
|
70
|
+
|
|
71
|
+
QUEST_RESULTS_PANEL_W = 512.0
|
|
72
|
+
QUEST_RESULTS_PANEL_H = 379.0
|
|
73
|
+
|
|
74
|
+
TEXTURE_TOP_BANNER_W = 256.0
|
|
75
|
+
TEXTURE_TOP_BANNER_H = 64.0
|
|
76
|
+
|
|
77
|
+
INPUT_BOX_W = 166.0
|
|
78
|
+
INPUT_BOX_H = 18.0
|
|
79
|
+
|
|
80
|
+
PANEL_SLIDE_DURATION_MS = 250.0
|
|
81
|
+
|
|
82
|
+
COLOR_TEXT = rl.Color(255, 255, 255, 255)
|
|
83
|
+
COLOR_TEXT_MUTED = rl.Color(255, 255, 255, int(255 * 0.8))
|
|
84
|
+
COLOR_TEXT_SUBTLE = rl.Color(255, 255, 255, int(255 * 0.7))
|
|
85
|
+
COLOR_GREEN = rl.Color(25, 200, 25, 255)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _poll_text_input(max_len: int, *, allow_space: bool = True) -> str:
|
|
89
|
+
out = ""
|
|
90
|
+
while True:
|
|
91
|
+
value = rl.get_char_pressed()
|
|
92
|
+
if value == 0:
|
|
93
|
+
break
|
|
94
|
+
if value < 0x20 or value > 0xFF:
|
|
95
|
+
continue
|
|
96
|
+
if not allow_space and value == 0x20:
|
|
97
|
+
continue
|
|
98
|
+
if len(out) >= max_len:
|
|
99
|
+
continue
|
|
100
|
+
out += chr(int(value))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _ease_out_cubic(t: float) -> float:
|
|
105
|
+
t = max(0.0, min(1.0, float(t)))
|
|
106
|
+
return 1.0 - (1.0 - t) ** 3
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _format_ordinal(value_1_based: int) -> str:
|
|
110
|
+
value = int(value_1_based)
|
|
111
|
+
if value % 100 in (11, 12, 13):
|
|
112
|
+
suffix = "th"
|
|
113
|
+
elif value % 10 == 1:
|
|
114
|
+
suffix = "st"
|
|
115
|
+
elif value % 10 == 2:
|
|
116
|
+
suffix = "nd"
|
|
117
|
+
elif value % 10 == 3:
|
|
118
|
+
suffix = "rd"
|
|
119
|
+
else:
|
|
120
|
+
suffix = "th"
|
|
121
|
+
return f"{value}{suffix}"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _format_time_mm_ss(ms: int) -> str:
|
|
125
|
+
total_s = max(0, int(ms)) // 1000
|
|
126
|
+
minutes = total_s // 60
|
|
127
|
+
seconds = total_s % 60
|
|
128
|
+
return f"{minutes}:{seconds:02d}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(slots=True)
|
|
132
|
+
class QuestResultsAssets:
|
|
133
|
+
menu_panel: rl.Texture | None
|
|
134
|
+
text_well_done: rl.Texture | None
|
|
135
|
+
perk_menu_assets: PerkMenuAssets
|
|
136
|
+
missing: list[str]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def load_quest_results_assets(assets_root: Path) -> QuestResultsAssets:
|
|
140
|
+
perk_menu_assets = load_perk_menu_assets(assets_root)
|
|
141
|
+
loader = TextureLoader.from_assets_root(assets_root)
|
|
142
|
+
text_well_done = loader.get(
|
|
143
|
+
name="ui_textWellDone",
|
|
144
|
+
paq_rel="ui/ui_textWellDone.jaz",
|
|
145
|
+
fs_rel="ui/ui_textWellDone.png",
|
|
146
|
+
)
|
|
147
|
+
missing: list[str] = list(perk_menu_assets.missing)
|
|
148
|
+
missing.extend(loader.missing)
|
|
149
|
+
return QuestResultsAssets(
|
|
150
|
+
menu_panel=perk_menu_assets.menu_panel,
|
|
151
|
+
text_well_done=text_well_done,
|
|
152
|
+
perk_menu_assets=perk_menu_assets,
|
|
153
|
+
missing=missing,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass(slots=True)
|
|
158
|
+
class QuestResultsUi:
|
|
159
|
+
assets_root: Path
|
|
160
|
+
base_dir: Path
|
|
161
|
+
config: CrimsonConfig
|
|
162
|
+
|
|
163
|
+
assets: QuestResultsAssets | None = None
|
|
164
|
+
font: SmallFontData | None = None
|
|
165
|
+
missing_assets: list[str] = None # type: ignore[assignment]
|
|
166
|
+
|
|
167
|
+
phase: int = -1 # -1 init, 0 breakdown, 1 name entry (if qualifies), 2 results/buttons
|
|
168
|
+
rank: int = TABLE_MAX
|
|
169
|
+
highlight_rank: int | None = None
|
|
170
|
+
|
|
171
|
+
quest_level: str = ""
|
|
172
|
+
quest_title: str = ""
|
|
173
|
+
quest_stage_major: int = 0
|
|
174
|
+
quest_stage_minor: int = 0
|
|
175
|
+
unlock_weapon_name: str = ""
|
|
176
|
+
unlock_perk_name: str = ""
|
|
177
|
+
|
|
178
|
+
record: HighScoreRecord | None = None
|
|
179
|
+
breakdown: QuestFinalTime | None = None
|
|
180
|
+
_breakdown_anim: QuestResultsBreakdownAnim | None = None
|
|
181
|
+
_scores_path: Path | None = None
|
|
182
|
+
|
|
183
|
+
input_text: str = ""
|
|
184
|
+
input_caret: int = 0
|
|
185
|
+
_saved: bool = False
|
|
186
|
+
|
|
187
|
+
_intro_ms: float = 0.0
|
|
188
|
+
_panel_open_sfx_played: bool = False
|
|
189
|
+
_closing: bool = False
|
|
190
|
+
_close_action: str | None = None
|
|
191
|
+
_consume_enter: bool = False
|
|
192
|
+
|
|
193
|
+
_ok_button: UiButtonState = field(default_factory=lambda: UiButtonState("OK", force_wide=False))
|
|
194
|
+
_play_next_button: UiButtonState = field(default_factory=lambda: UiButtonState("Play Next", force_wide=True))
|
|
195
|
+
_play_again_button: UiButtonState = field(default_factory=lambda: UiButtonState("Play Again", force_wide=True))
|
|
196
|
+
_high_scores_button: UiButtonState = field(default_factory=lambda: UiButtonState("High scores", force_wide=True))
|
|
197
|
+
_main_menu_button: UiButtonState = field(default_factory=lambda: UiButtonState("Main Menu", force_wide=True))
|
|
198
|
+
|
|
199
|
+
def open(
|
|
200
|
+
self,
|
|
201
|
+
*,
|
|
202
|
+
record: HighScoreRecord,
|
|
203
|
+
breakdown: QuestFinalTime,
|
|
204
|
+
quest_level: str,
|
|
205
|
+
quest_title: str,
|
|
206
|
+
quest_stage_major: int,
|
|
207
|
+
quest_stage_minor: int,
|
|
208
|
+
unlock_weapon_name: str,
|
|
209
|
+
unlock_perk_name: str,
|
|
210
|
+
player_name_default: str,
|
|
211
|
+
) -> None:
|
|
212
|
+
self.close()
|
|
213
|
+
self.missing_assets = []
|
|
214
|
+
try:
|
|
215
|
+
self.font = load_small_font(self.assets_root, self.missing_assets)
|
|
216
|
+
except Exception:
|
|
217
|
+
self.font = None
|
|
218
|
+
self.assets = load_quest_results_assets(self.assets_root)
|
|
219
|
+
if self.assets.missing:
|
|
220
|
+
self.missing_assets.extend(self.assets.missing)
|
|
221
|
+
|
|
222
|
+
self.phase = -1
|
|
223
|
+
self.rank = TABLE_MAX
|
|
224
|
+
self.highlight_rank = None
|
|
225
|
+
self.quest_level = str(quest_level or "")
|
|
226
|
+
self.quest_title = str(quest_title or "")
|
|
227
|
+
self.quest_stage_major = int(quest_stage_major)
|
|
228
|
+
self.quest_stage_minor = int(quest_stage_minor)
|
|
229
|
+
self.unlock_weapon_name = str(unlock_weapon_name or "")
|
|
230
|
+
self.unlock_perk_name = str(unlock_perk_name or "")
|
|
231
|
+
self.record = record.copy()
|
|
232
|
+
self.breakdown = breakdown
|
|
233
|
+
self._breakdown_anim = QuestResultsBreakdownAnim.start()
|
|
234
|
+
self._saved = False
|
|
235
|
+
|
|
236
|
+
# Native behavior: the final quest replaces "Play Next" with "Show End Note".
|
|
237
|
+
if int(self.quest_stage_major) == 5 and int(self.quest_stage_minor) == 10:
|
|
238
|
+
self._play_next_button.label = "Show End Note"
|
|
239
|
+
else:
|
|
240
|
+
self._play_next_button.label = "Play Next"
|
|
241
|
+
|
|
242
|
+
hardcore = bool(int(self.config.data.get("hardcore_flag", 0) or 0))
|
|
243
|
+
self._scores_path = scores_path_for_mode(
|
|
244
|
+
self.base_dir,
|
|
245
|
+
3,
|
|
246
|
+
hardcore=hardcore,
|
|
247
|
+
quest_stage_major=int(self.quest_stage_major),
|
|
248
|
+
quest_stage_minor=int(self.quest_stage_minor),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
records = read_highscore_table(self._scores_path, game_mode_id=3)
|
|
253
|
+
self.rank = int(rank_index(records, self.record))
|
|
254
|
+
except Exception:
|
|
255
|
+
self.rank = TABLE_MAX
|
|
256
|
+
|
|
257
|
+
self.input_text = str(player_name_default or "")[:NAME_MAX_EDIT]
|
|
258
|
+
self.input_caret = len(self.input_text)
|
|
259
|
+
|
|
260
|
+
self._intro_ms = 0.0
|
|
261
|
+
self._panel_open_sfx_played = False
|
|
262
|
+
self._closing = False
|
|
263
|
+
self._close_action = None
|
|
264
|
+
self._consume_enter = True
|
|
265
|
+
self.phase = 0
|
|
266
|
+
|
|
267
|
+
def close(self) -> None:
|
|
268
|
+
if self.assets is not None:
|
|
269
|
+
self.assets = None
|
|
270
|
+
if self.font is not None:
|
|
271
|
+
rl.unload_texture(self.font.texture)
|
|
272
|
+
self.font = None
|
|
273
|
+
|
|
274
|
+
def _begin_close_transition(self, action: str) -> None:
|
|
275
|
+
if self._closing:
|
|
276
|
+
return
|
|
277
|
+
self._closing = True
|
|
278
|
+
self._close_action = action
|
|
279
|
+
|
|
280
|
+
def _text_width(self, text: str, scale: float) -> float:
|
|
281
|
+
if self.font is None:
|
|
282
|
+
return float(rl.measure_text(text, int(20 * scale)))
|
|
283
|
+
return float(measure_small_text_width(self.font, text, scale))
|
|
284
|
+
|
|
285
|
+
def _draw_small(self, text: str, x: float, y: float, scale: float, color: rl.Color) -> None:
|
|
286
|
+
if self.font is not None:
|
|
287
|
+
draw_small_text(self.font, text, x, y, scale, color)
|
|
288
|
+
else:
|
|
289
|
+
rl.draw_text(text, int(x), int(y), int(20 * scale), color)
|
|
290
|
+
|
|
291
|
+
def _panel_layout(self, *, screen_w: float, scale: float) -> tuple[rl.Rectangle, float, float]:
|
|
292
|
+
t = self._intro_ms / PANEL_SLIDE_DURATION_MS if PANEL_SLIDE_DURATION_MS > 1e-6 else 1.0
|
|
293
|
+
eased = _ease_out_cubic(t)
|
|
294
|
+
panel_slide_x = -QUEST_RESULTS_PANEL_W * (1.0 - eased)
|
|
295
|
+
|
|
296
|
+
left = (QUEST_RESULTS_PANEL_GEOM_X0 + QUEST_RESULTS_PANEL_POS_X + QUEST_RESULTS_PANEL_BASE_X + panel_slide_x) * scale
|
|
297
|
+
layout_w = screen_w / scale if scale else screen_w
|
|
298
|
+
widescreen_shift_y = _menu_widescreen_y_shift(layout_w)
|
|
299
|
+
top = (QUEST_RESULTS_PANEL_GEOM_Y0 + QUEST_RESULTS_PANEL_POS_Y + widescreen_shift_y) * scale
|
|
300
|
+
panel = rl.Rectangle(float(left), float(top), QUEST_RESULTS_PANEL_W * scale, QUEST_RESULTS_PANEL_H * scale)
|
|
301
|
+
return panel, left, top
|
|
302
|
+
|
|
303
|
+
def update(
|
|
304
|
+
self,
|
|
305
|
+
dt: float,
|
|
306
|
+
*,
|
|
307
|
+
play_sfx: Callable[[str], None] | None = None,
|
|
308
|
+
rand: Callable[[], int] | None = None,
|
|
309
|
+
mouse: rl.Vector2 | None = None,
|
|
310
|
+
) -> str | None:
|
|
311
|
+
dt_s = float(min(dt, 0.1))
|
|
312
|
+
dt_ms = dt_s * 1000.0
|
|
313
|
+
if mouse is None:
|
|
314
|
+
mouse = rl.get_mouse_position()
|
|
315
|
+
if rand is None:
|
|
316
|
+
def rand() -> int:
|
|
317
|
+
return 0
|
|
318
|
+
|
|
319
|
+
if self.assets is None or self.record is None or self.breakdown is None:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
if self._closing:
|
|
323
|
+
self._intro_ms = max(0.0, float(self._intro_ms) - dt_ms)
|
|
324
|
+
if self._intro_ms <= 1e-3 and self._close_action is not None:
|
|
325
|
+
action = self._close_action
|
|
326
|
+
self._close_action = None
|
|
327
|
+
self._closing = False
|
|
328
|
+
return action
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
self._intro_ms = min(PANEL_SLIDE_DURATION_MS, self._intro_ms + dt_ms)
|
|
332
|
+
if (not self._panel_open_sfx_played) and play_sfx is not None and self._intro_ms >= PANEL_SLIDE_DURATION_MS - 1e-3:
|
|
333
|
+
play_sfx("sfx_ui_panelclick")
|
|
334
|
+
self._panel_open_sfx_played = True
|
|
335
|
+
if self._consume_enter:
|
|
336
|
+
self._consume_enter = False
|
|
337
|
+
rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER)
|
|
338
|
+
|
|
339
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
340
|
+
if play_sfx is not None:
|
|
341
|
+
play_sfx("sfx_ui_buttonclick")
|
|
342
|
+
self._begin_close_transition("main_menu")
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
qualifies = int(self.rank) < TABLE_MAX
|
|
346
|
+
|
|
347
|
+
if self.phase == 0:
|
|
348
|
+
anim = self._breakdown_anim
|
|
349
|
+
if anim is None:
|
|
350
|
+
self._breakdown_anim = QuestResultsBreakdownAnim.start()
|
|
351
|
+
anim = self._breakdown_anim
|
|
352
|
+
|
|
353
|
+
click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
354
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE) or click:
|
|
355
|
+
anim.set_final(self.breakdown)
|
|
356
|
+
self.phase = 1 if qualifies else 2
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
clinks = tick_quest_results_breakdown_anim(
|
|
360
|
+
anim,
|
|
361
|
+
frame_dt_ms=int(dt_s * 1000.0),
|
|
362
|
+
target=self.breakdown,
|
|
363
|
+
)
|
|
364
|
+
if clinks > 0 and play_sfx is not None:
|
|
365
|
+
play_sfx("sfx_ui_clink_01")
|
|
366
|
+
if anim.done:
|
|
367
|
+
self.phase = 1 if qualifies else 2
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
if self.phase == 1:
|
|
371
|
+
click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
372
|
+
typed = _poll_text_input(NAME_MAX_EDIT - len(self.input_text), allow_space=True)
|
|
373
|
+
if typed:
|
|
374
|
+
self.input_text = (self.input_text[: self.input_caret] + typed + self.input_text[self.input_caret :])[:NAME_MAX_EDIT]
|
|
375
|
+
self.input_caret = min(len(self.input_text), self.input_caret + len(typed))
|
|
376
|
+
if play_sfx is not None:
|
|
377
|
+
play_sfx("sfx_ui_typeclick_01" if (int(rand()) & 1) == 0 else "sfx_ui_typeclick_02")
|
|
378
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
|
|
379
|
+
if self.input_caret > 0:
|
|
380
|
+
self.input_text = self.input_text[: self.input_caret - 1] + self.input_text[self.input_caret :]
|
|
381
|
+
self.input_caret -= 1
|
|
382
|
+
if play_sfx is not None:
|
|
383
|
+
play_sfx("sfx_ui_typeclick_01" if (int(rand()) & 1) == 0 else "sfx_ui_typeclick_02")
|
|
384
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
385
|
+
self.input_caret = max(0, self.input_caret - 1)
|
|
386
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
387
|
+
self.input_caret = min(len(self.input_text), self.input_caret + 1)
|
|
388
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
|
|
389
|
+
self.input_caret = 0
|
|
390
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
|
|
391
|
+
self.input_caret = len(self.input_text)
|
|
392
|
+
|
|
393
|
+
screen_w = float(rl.get_screen_width())
|
|
394
|
+
screen_h = float(rl.get_screen_height())
|
|
395
|
+
scale = ui_scale(screen_w, screen_h)
|
|
396
|
+
_panel, panel_left, panel_top = self._panel_layout(screen_w=screen_w, scale=scale)
|
|
397
|
+
anchor_x = panel_left + 40.0 * scale
|
|
398
|
+
input_y = panel_top + 150.0 * scale
|
|
399
|
+
ok_x = anchor_x + 170.0 * scale
|
|
400
|
+
ok_y = input_y - 8.0 * scale
|
|
401
|
+
ok_w = button_width(self.font, self._ok_button.label, scale=scale, force_wide=self._ok_button.force_wide)
|
|
402
|
+
ok_clicked = button_update(self._ok_button, x=ok_x, y=ok_y, width=ok_w, dt_ms=dt_ms, mouse=mouse, click=click)
|
|
403
|
+
|
|
404
|
+
if ok_clicked or rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
|
|
405
|
+
if self.input_text.strip():
|
|
406
|
+
if play_sfx is not None:
|
|
407
|
+
play_sfx("sfx_ui_typeenter")
|
|
408
|
+
if (not self._saved) and self._scores_path is not None:
|
|
409
|
+
candidate = self.record.copy()
|
|
410
|
+
candidate.set_name(self.input_text)
|
|
411
|
+
try:
|
|
412
|
+
_table, idx = upsert_highscore_record(self._scores_path, candidate)
|
|
413
|
+
self.highlight_rank = int(idx) if int(idx) < TABLE_MAX else None
|
|
414
|
+
if int(idx) < TABLE_MAX:
|
|
415
|
+
self.rank = int(idx)
|
|
416
|
+
except Exception:
|
|
417
|
+
self.highlight_rank = None
|
|
418
|
+
self._saved = True
|
|
419
|
+
self.phase = 2
|
|
420
|
+
return None
|
|
421
|
+
if play_sfx is not None:
|
|
422
|
+
play_sfx("sfx_shock_hit_01")
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
if self.phase == 2:
|
|
426
|
+
click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
|
427
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER):
|
|
428
|
+
if play_sfx is not None:
|
|
429
|
+
play_sfx("sfx_ui_buttonclick")
|
|
430
|
+
self._begin_close_transition("play_again")
|
|
431
|
+
return None
|
|
432
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_N):
|
|
433
|
+
if play_sfx is not None:
|
|
434
|
+
play_sfx("sfx_ui_buttonclick")
|
|
435
|
+
self._begin_close_transition("play_next")
|
|
436
|
+
return None
|
|
437
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
|
|
438
|
+
if play_sfx is not None:
|
|
439
|
+
play_sfx("sfx_ui_buttonclick")
|
|
440
|
+
self._begin_close_transition("high_scores")
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
screen_w = float(rl.get_screen_width())
|
|
444
|
+
screen_h = float(rl.get_screen_height())
|
|
445
|
+
scale = ui_scale(screen_w, screen_h)
|
|
446
|
+
_origin_x, _origin_y = ui_origin(screen_w, screen_h, scale)
|
|
447
|
+
_panel, left, top = self._panel_layout(screen_w=screen_w, scale=scale)
|
|
448
|
+
qualifies = int(self.rank) < TABLE_MAX
|
|
449
|
+
score_card_x = left + 70.0 * scale
|
|
450
|
+
|
|
451
|
+
var_c_12 = top + (96.0 if qualifies else 108.0) * scale
|
|
452
|
+
var_c_14 = var_c_12 + 84.0 * scale
|
|
453
|
+
if self.unlock_weapon_name:
|
|
454
|
+
var_c_14 += 30.0 * scale
|
|
455
|
+
if self.unlock_perk_name:
|
|
456
|
+
var_c_14 += 30.0 * scale
|
|
457
|
+
|
|
458
|
+
button_x = score_card_x + 20.0 * scale
|
|
459
|
+
button_y = var_c_14 + 6.0 * scale
|
|
460
|
+
|
|
461
|
+
play_next_w = button_width(self.font, self._play_next_button.label, scale=scale, force_wide=self._play_next_button.force_wide)
|
|
462
|
+
if button_update(self._play_next_button, x=button_x, y=button_y, width=play_next_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
463
|
+
if play_sfx is not None:
|
|
464
|
+
play_sfx("sfx_ui_buttonclick")
|
|
465
|
+
self._begin_close_transition("play_next")
|
|
466
|
+
return None
|
|
467
|
+
button_y += 32.0 * scale
|
|
468
|
+
|
|
469
|
+
play_again_w = button_width(self.font, self._play_again_button.label, scale=scale, force_wide=self._play_again_button.force_wide)
|
|
470
|
+
if button_update(self._play_again_button, x=button_x, y=button_y, width=play_again_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
471
|
+
if play_sfx is not None:
|
|
472
|
+
play_sfx("sfx_ui_buttonclick")
|
|
473
|
+
self._begin_close_transition("play_again")
|
|
474
|
+
return None
|
|
475
|
+
button_y += 32.0 * scale
|
|
476
|
+
|
|
477
|
+
high_scores_w = button_width(self.font, self._high_scores_button.label, scale=scale, force_wide=self._high_scores_button.force_wide)
|
|
478
|
+
if button_update(self._high_scores_button, x=button_x, y=button_y, width=high_scores_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
479
|
+
if play_sfx is not None:
|
|
480
|
+
play_sfx("sfx_ui_buttonclick")
|
|
481
|
+
self._begin_close_transition("high_scores")
|
|
482
|
+
return None
|
|
483
|
+
button_y += 32.0 * scale
|
|
484
|
+
|
|
485
|
+
main_menu_w = button_width(self.font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide)
|
|
486
|
+
if button_update(self._main_menu_button, x=button_x, y=button_y, width=main_menu_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
487
|
+
if play_sfx is not None:
|
|
488
|
+
play_sfx("sfx_ui_buttonclick")
|
|
489
|
+
self._begin_close_transition("main_menu")
|
|
490
|
+
return None
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
def draw(self, *, mouse: rl.Vector2 | None = None) -> None:
|
|
496
|
+
if self.assets is None or self.record is None or self.breakdown is None:
|
|
497
|
+
return
|
|
498
|
+
if mouse is None:
|
|
499
|
+
mouse = rl.get_mouse_position()
|
|
500
|
+
|
|
501
|
+
screen_w = float(rl.get_screen_width())
|
|
502
|
+
screen_h = float(rl.get_screen_height())
|
|
503
|
+
scale = ui_scale(screen_w, screen_h)
|
|
504
|
+
_origin_x, _origin_y = ui_origin(screen_w, screen_h, scale)
|
|
505
|
+
_ = _origin_x, _origin_y
|
|
506
|
+
|
|
507
|
+
panel, left, top = self._panel_layout(screen_w=screen_w, scale=scale)
|
|
508
|
+
|
|
509
|
+
if self.assets.menu_panel is not None:
|
|
510
|
+
draw_menu_panel(self.assets.menu_panel, dst=panel, tint=rl.WHITE)
|
|
511
|
+
|
|
512
|
+
banner_x = left + 22.0 * scale
|
|
513
|
+
banner_y = top + 36.0 * scale
|
|
514
|
+
if self.assets.text_well_done is not None:
|
|
515
|
+
src = rl.Rectangle(0.0, 0.0, float(self.assets.text_well_done.width), float(self.assets.text_well_done.height))
|
|
516
|
+
dst = rl.Rectangle(banner_x, banner_y, TEXTURE_TOP_BANNER_W * scale, TEXTURE_TOP_BANNER_H * scale)
|
|
517
|
+
rl.draw_texture_pro(self.assets.text_well_done, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
|
|
518
|
+
|
|
519
|
+
qualifies = int(self.rank) < TABLE_MAX
|
|
520
|
+
|
|
521
|
+
if self.phase == 0:
|
|
522
|
+
anchor_x = left + 40.0 * scale
|
|
523
|
+
label_x = anchor_x + 32.0 * scale
|
|
524
|
+
value_x = label_x + 132.0 * scale
|
|
525
|
+
|
|
526
|
+
anim = self._breakdown_anim
|
|
527
|
+
step = 4
|
|
528
|
+
highlight_alpha = 1.0
|
|
529
|
+
base_time_ms = int(self.breakdown.base_time_ms)
|
|
530
|
+
life_bonus_ms = int(self.breakdown.life_bonus_ms)
|
|
531
|
+
perk_bonus_ms = int(self.breakdown.unpicked_perk_bonus_ms)
|
|
532
|
+
final_time_ms = int(self.breakdown.final_time_ms)
|
|
533
|
+
if anim is not None and not anim.done:
|
|
534
|
+
step = int(anim.step)
|
|
535
|
+
highlight_alpha = float(anim.highlight_alpha())
|
|
536
|
+
base_time_ms = int(anim.base_time_ms)
|
|
537
|
+
life_bonus_ms = int(anim.life_bonus_ms)
|
|
538
|
+
perk_bonus_ms = int(anim.unpicked_perk_bonus_s) * 1000
|
|
539
|
+
final_time_ms = int(anim.final_time_ms)
|
|
540
|
+
|
|
541
|
+
def _row_color(idx: int, *, final: bool = False) -> rl.Color:
|
|
542
|
+
if anim is None or anim.done:
|
|
543
|
+
return COLOR_TEXT
|
|
544
|
+
alpha = 0.2
|
|
545
|
+
if idx < step:
|
|
546
|
+
alpha = 0.4
|
|
547
|
+
elif idx == step:
|
|
548
|
+
alpha = 1.0
|
|
549
|
+
if final:
|
|
550
|
+
alpha *= highlight_alpha
|
|
551
|
+
rgb = (255, 255, 255)
|
|
552
|
+
if idx == step:
|
|
553
|
+
rgb = (COLOR_GREEN.r, COLOR_GREEN.g, COLOR_GREEN.b)
|
|
554
|
+
return rl.Color(rgb[0], rgb[1], rgb[2], int(255 * max(0.0, min(1.0, alpha))))
|
|
555
|
+
|
|
556
|
+
y = top + 156.0 * scale
|
|
557
|
+
base_value = _format_time_mm_ss(base_time_ms)
|
|
558
|
+
life_value = _format_time_mm_ss(life_bonus_ms)
|
|
559
|
+
perk_value = _format_time_mm_ss(perk_bonus_ms)
|
|
560
|
+
final_value = _format_time_mm_ss(final_time_ms)
|
|
561
|
+
|
|
562
|
+
self._draw_small("Base Time:", label_x, y, 1.0 * scale, _row_color(0))
|
|
563
|
+
self._draw_small(base_value, value_x, y, 1.0 * scale, _row_color(0))
|
|
564
|
+
y += 20.0 * scale
|
|
565
|
+
|
|
566
|
+
self._draw_small("Life Bonus:", label_x, y, 1.0 * scale, _row_color(1))
|
|
567
|
+
self._draw_small(life_value, value_x, y, 1.0 * scale, _row_color(1))
|
|
568
|
+
y += 20.0 * scale
|
|
569
|
+
|
|
570
|
+
self._draw_small("Unpicked Perk Bonus:", label_x, y, 1.0 * scale, _row_color(2))
|
|
571
|
+
self._draw_small(perk_value, value_x, y, 1.0 * scale, _row_color(2))
|
|
572
|
+
y += 20.0 * scale
|
|
573
|
+
|
|
574
|
+
# Final time underline + row (matches the extra quad draw in native).
|
|
575
|
+
line_y = y + 1.0 * scale
|
|
576
|
+
line_color = rl.Color(255, 255, 255, _row_color(3, final=True).a)
|
|
577
|
+
rl.draw_rectangle(int(label_x - 4.0 * scale), int(line_y), int(168.0 * scale), int(1.0 * scale), line_color)
|
|
578
|
+
|
|
579
|
+
y += 8.0 * scale
|
|
580
|
+
self._draw_small("Final Time:", label_x, y, 1.0 * scale, _row_color(3, final=True))
|
|
581
|
+
self._draw_small(final_value, value_x, y, 1.0 * scale, _row_color(3, final=True))
|
|
582
|
+
|
|
583
|
+
elif self.phase == 1:
|
|
584
|
+
anchor_x = left + 40.0 * scale
|
|
585
|
+
text_y = top + 118.0 * scale
|
|
586
|
+
self._draw_small("State your name trooper!", anchor_x + 42.0 * scale, text_y, 1.0 * scale, COLOR_TEXT)
|
|
587
|
+
|
|
588
|
+
input_x = anchor_x
|
|
589
|
+
input_y = top + 150.0 * scale
|
|
590
|
+
rl.draw_rectangle_lines(int(input_x), int(input_y), int(INPUT_BOX_W * scale), int(INPUT_BOX_H * scale), rl.WHITE)
|
|
591
|
+
rl.draw_rectangle(
|
|
592
|
+
int(input_x + 1.0 * scale),
|
|
593
|
+
int(input_y + 1.0 * scale),
|
|
594
|
+
int((INPUT_BOX_W - 2.0) * scale),
|
|
595
|
+
int((INPUT_BOX_H - 2.0) * scale),
|
|
596
|
+
rl.Color(0, 0, 0, 255),
|
|
597
|
+
)
|
|
598
|
+
draw_ui_text(self.font, self.input_text, input_x + 4.0 * scale, input_y + 2.0 * scale, scale=1.0 * scale, color=COLOR_TEXT_MUTED)
|
|
599
|
+
caret_alpha = 1.0
|
|
600
|
+
if math.sin(float(rl.get_time()) * 4.0) > 0.0:
|
|
601
|
+
caret_alpha = 0.4
|
|
602
|
+
caret_color = rl.Color(255, 255, 255, int(255 * caret_alpha))
|
|
603
|
+
caret_x = input_x + 4.0 * scale + self._text_width(self.input_text[: self.input_caret], 1.0 * scale)
|
|
604
|
+
rl.draw_rectangle(int(caret_x), int(input_y + 2.0 * scale), int(1.0 * scale), int(14.0 * scale), caret_color)
|
|
605
|
+
|
|
606
|
+
ok_x = anchor_x + 170.0 * scale
|
|
607
|
+
ok_y = input_y - 8.0 * scale
|
|
608
|
+
ok_w = button_width(self.font, self._ok_button.label, scale=scale, force_wide=self._ok_button.force_wide)
|
|
609
|
+
button_draw(self.assets.perk_menu_assets, self.font, self._ok_button, x=ok_x, y=ok_y, width=ok_w, scale=scale)
|
|
610
|
+
|
|
611
|
+
else:
|
|
612
|
+
score_card_x = left + 70.0 * scale
|
|
613
|
+
var_c_12 = top + (96.0 if qualifies else 108.0) * scale
|
|
614
|
+
if (not qualifies) and self.font is not None:
|
|
615
|
+
self._draw_small(
|
|
616
|
+
"Score too low for top100.",
|
|
617
|
+
score_card_x + 8.0 * scale,
|
|
618
|
+
top + 102.0 * scale,
|
|
619
|
+
1.0 * scale,
|
|
620
|
+
rl.Color(200, 200, 200, 255),
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
seconds = float(int(self.record.survival_elapsed_ms)) * 0.001
|
|
624
|
+
score_value = f"{seconds:.2f} secs"
|
|
625
|
+
xp_value = f"{int(self.record.score_xp)}"
|
|
626
|
+
rank_text = _format_ordinal(int(self.rank) + 1) if qualifies else "--"
|
|
627
|
+
|
|
628
|
+
col_label = rl.Color(230, 230, 230, 255)
|
|
629
|
+
col_value = rl.Color(230, 230, 255, 255)
|
|
630
|
+
card_y = var_c_12 + 16.0 * scale
|
|
631
|
+
self._draw_small("Score", score_card_x + 32.0 * scale, card_y, 1.0 * scale, col_label)
|
|
632
|
+
self._draw_small(score_value, score_card_x + 32.0 * scale, card_y + 15.0 * scale, 1.0 * scale, col_value)
|
|
633
|
+
self._draw_small(f"Rank: {rank_text}", score_card_x + 32.0 * scale, card_y + 30.0 * scale, 1.0 * scale, col_label)
|
|
634
|
+
self._draw_small("Experience", score_card_x + 140.0 * scale, card_y, 1.0 * scale, col_label)
|
|
635
|
+
self._draw_small(xp_value, score_card_x + 140.0 * scale, card_y + 15.0 * scale, 1.0 * scale, col_value)
|
|
636
|
+
|
|
637
|
+
# Unlock lines (their presence shifts the buttons down in native).
|
|
638
|
+
var_c_14 = var_c_12 + 84.0 * scale
|
|
639
|
+
if self.unlock_weapon_name:
|
|
640
|
+
self._draw_small("Weapon unlocked:", score_card_x, var_c_14 + 1.0 * scale, 1.0 * scale, COLOR_TEXT_SUBTLE)
|
|
641
|
+
self._draw_small(self.unlock_weapon_name, score_card_x, var_c_14 + 14.0 * scale, 1.0 * scale, COLOR_TEXT)
|
|
642
|
+
var_c_14 += 30.0 * scale
|
|
643
|
+
if self.unlock_perk_name:
|
|
644
|
+
self._draw_small("Perk unlocked:", score_card_x, var_c_14 + 1.0 * scale, 1.0 * scale, COLOR_TEXT_SUBTLE)
|
|
645
|
+
self._draw_small(self.unlock_perk_name, score_card_x, var_c_14 + 14.0 * scale, 1.0 * scale, COLOR_TEXT)
|
|
646
|
+
var_c_14 += 30.0 * scale
|
|
647
|
+
|
|
648
|
+
# Buttons
|
|
649
|
+
button_x = score_card_x + 20.0 * scale
|
|
650
|
+
button_y = var_c_14 + 6.0 * scale
|
|
651
|
+
play_next_w = button_width(self.font, self._play_next_button.label, scale=scale, force_wide=self._play_next_button.force_wide)
|
|
652
|
+
button_draw(self.assets.perk_menu_assets, self.font, self._play_next_button, x=button_x, y=button_y, width=play_next_w, scale=scale)
|
|
653
|
+
button_y += 32.0 * scale
|
|
654
|
+
play_again_w = button_width(self.font, self._play_again_button.label, scale=scale, force_wide=self._play_again_button.force_wide)
|
|
655
|
+
button_draw(self.assets.perk_menu_assets, self.font, self._play_again_button, x=button_x, y=button_y, width=play_again_w, scale=scale)
|
|
656
|
+
button_y += 32.0 * scale
|
|
657
|
+
high_scores_w = button_width(self.font, self._high_scores_button.label, scale=scale, force_wide=self._high_scores_button.force_wide)
|
|
658
|
+
button_draw(self.assets.perk_menu_assets, self.font, self._high_scores_button, x=button_x, y=button_y, width=high_scores_w, scale=scale)
|
|
659
|
+
button_y += 32.0 * scale
|
|
660
|
+
main_menu_w = button_width(self.font, self._main_menu_button.label, scale=scale, force_wide=self._main_menu_button.force_wide)
|
|
661
|
+
button_draw(self.assets.perk_menu_assets, self.font, self._main_menu_button, x=button_x, y=button_y, width=main_menu_w, scale=scale)
|
|
662
|
+
|
|
663
|
+
cursor_draw(self.assets.perk_menu_assets, mouse=mouse, scale=scale)
|