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