crimsonland 0.1.0.dev12__py3-none-any.whl → 0.1.0.dev14__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/frontend/high_scores_layout.py +26 -0
- crimson/frontend/menu.py +22 -20
- crimson/frontend/panels/base.py +14 -26
- crimson/frontend/panels/controls.py +151 -62
- crimson/frontend/panels/credits.py +221 -0
- crimson/frontend/panels/databases.py +307 -0
- crimson/frontend/panels/options.py +4 -3
- crimson/frontend/panels/play_game.py +4 -4
- crimson/frontend/panels/stats.py +255 -296
- crimson/game.py +219 -81
- crimson/modes/quest_mode.py +10 -9
- crimson/modes/survival_mode.py +10 -9
- crimson/modes/tutorial_mode.py +10 -4
- crimson/ui/menu_panel.py +127 -0
- crimson/ui/perk_menu.py +54 -89
- crimson/ui/quest_results.py +24 -18
- crimson/views/perk_menu_debug.py +2 -2
- crimson/views/perks.py +2 -2
- crimsonland-0.1.0.dev14.dist-info/METADATA +197 -0
- {crimsonland-0.1.0.dev12.dist-info → crimsonland-0.1.0.dev14.dist-info}/RECORD +23 -19
- crimsonland-0.1.0.dev12.dist-info/METADATA +0 -9
- {crimsonland-0.1.0.dev12.dist-info → crimsonland-0.1.0.dev14.dist-info}/WHEEL +0 -0
- {crimsonland-0.1.0.dev12.dist-info → crimsonland-0.1.0.dev14.dist-info}/entry_points.txt +0 -0
crimson/frontend/panels/stats.py
CHANGED
|
@@ -4,97 +4,127 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
import pyray as rl
|
|
6
6
|
|
|
7
|
-
from grim.audio import play_music, stop_music
|
|
7
|
+
from grim.audio import play_music, play_sfx, stop_music, update_audio
|
|
8
8
|
from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
|
|
9
9
|
|
|
10
|
+
from ...ui.menu_panel import draw_classic_menu_panel
|
|
11
|
+
from ...ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width
|
|
12
|
+
from ..assets import MenuAssets, _ensure_texture_cache, load_menu_assets
|
|
10
13
|
from ..menu import (
|
|
11
14
|
MENU_LABEL_ROW_HEIGHT,
|
|
12
15
|
MENU_LABEL_ROW_STATISTICS,
|
|
13
|
-
MENU_LABEL_WIDTH,
|
|
14
|
-
MENU_PANEL_HEIGHT,
|
|
15
16
|
MENU_PANEL_OFFSET_X,
|
|
16
17
|
MENU_PANEL_OFFSET_Y,
|
|
17
18
|
MENU_PANEL_WIDTH,
|
|
19
|
+
MENU_SCALE_SMALL_THRESHOLD,
|
|
20
|
+
MENU_SIGN_HEIGHT,
|
|
21
|
+
MENU_SIGN_OFFSET_X,
|
|
22
|
+
MENU_SIGN_OFFSET_Y,
|
|
23
|
+
MENU_SIGN_POS_X_PAD,
|
|
24
|
+
MENU_SIGN_POS_Y,
|
|
25
|
+
MENU_SIGN_POS_Y_SMALL,
|
|
26
|
+
MENU_SIGN_WIDTH,
|
|
27
|
+
UI_SHADOW_OFFSET,
|
|
18
28
|
MenuView,
|
|
19
29
|
_draw_menu_cursor,
|
|
30
|
+
ensure_menu_ground,
|
|
20
31
|
)
|
|
21
32
|
from ..transitions import _draw_screen_fade
|
|
22
|
-
from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
|
|
23
|
-
|
|
24
|
-
from ...persistence.save_status import MODE_COUNT_ORDER
|
|
25
|
-
from ...weapons import WEAPON_BY_ID, WeaponId
|
|
26
33
|
|
|
27
34
|
if TYPE_CHECKING:
|
|
28
35
|
from ...game import GameState
|
|
36
|
+
from grim.terrain_render import GroundRenderer
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Measured from ui_render_trace_oracle_1024x768.json (state_4:played for # hours # minutes, timeline=300).
|
|
40
|
+
STATISTICS_PANEL_POS_X = -89.0
|
|
41
|
+
STATISTICS_PANEL_POS_Y = 185.0
|
|
42
|
+
STATISTICS_PANEL_HEIGHT = 378.0
|
|
43
|
+
|
|
44
|
+
# Child layout inside the panel (relative to panel top-left).
|
|
45
|
+
_TITLE_X = 290.0
|
|
46
|
+
_TITLE_Y = 52.0
|
|
47
|
+
_TITLE_W = 128.0
|
|
48
|
+
_TITLE_H = 32.0
|
|
49
|
+
|
|
50
|
+
_BUTTON_X = 270.0
|
|
51
|
+
_BUTTON_Y0 = 104.0
|
|
52
|
+
_BUTTON_STEP_Y = 34.0
|
|
53
|
+
|
|
54
|
+
_BACK_BUTTON_X = 394.0
|
|
55
|
+
_BACK_BUTTON_Y = 290.0
|
|
56
|
+
|
|
57
|
+
_PLAYTIME_X = 204.0
|
|
58
|
+
_PLAYTIME_Y = 334.0
|
|
59
|
+
|
|
29
60
|
|
|
61
|
+
class StatisticsMenuView:
|
|
62
|
+
"""
|
|
63
|
+
Classic "Statistics" menu (state_id=4).
|
|
30
64
|
|
|
31
|
-
|
|
32
|
-
|
|
65
|
+
This is a small hub panel with buttons for:
|
|
66
|
+
- High scores
|
|
67
|
+
- Weapons / Perks databases
|
|
68
|
+
- Credits
|
|
69
|
+
"""
|
|
33
70
|
|
|
34
71
|
def __init__(self, state: GameState) -> None:
|
|
35
|
-
|
|
72
|
+
self._state = state
|
|
73
|
+
self._assets: MenuAssets | None = None
|
|
74
|
+
self._ground: GroundRenderer | None = None
|
|
36
75
|
self._small_font: SmallFontData | None = None
|
|
37
|
-
self.
|
|
38
|
-
|
|
39
|
-
self.
|
|
76
|
+
self._button_textures: UiButtonTextureSet | None = None
|
|
77
|
+
|
|
78
|
+
self._cursor_pulse_time = 0.0
|
|
79
|
+
self._widescreen_y_shift = 0.0
|
|
80
|
+
|
|
81
|
+
self._action: str | None = None
|
|
82
|
+
|
|
83
|
+
self._btn_high_scores = UiButtonState("High scores", force_wide=True)
|
|
84
|
+
self._btn_weapons = UiButtonState("Weapons", force_wide=True)
|
|
85
|
+
self._btn_perks = UiButtonState("Perks", force_wide=True)
|
|
86
|
+
self._btn_credits = UiButtonState("Credits", force_wide=True)
|
|
87
|
+
self._btn_back = UiButtonState("Back", force_wide=False)
|
|
40
88
|
|
|
41
89
|
def open(self) -> None:
|
|
42
|
-
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
-
self.
|
|
90
|
+
layout_w = float(self._state.config.screen_width)
|
|
91
|
+
self._widescreen_y_shift = MenuView._menu_widescreen_y_shift(layout_w)
|
|
92
|
+
self._assets = load_menu_assets(self._state)
|
|
93
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
94
|
+
self._small_font = None
|
|
95
|
+
self._cursor_pulse_time = 0.0
|
|
96
|
+
self._action = None
|
|
97
|
+
|
|
98
|
+
cache = _ensure_texture_cache(self._state)
|
|
99
|
+
button_md = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
100
|
+
button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
101
|
+
self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=button_md)
|
|
102
|
+
|
|
103
|
+
self._btn_high_scores = UiButtonState("High scores", force_wide=True)
|
|
104
|
+
self._btn_weapons = UiButtonState("Weapons", force_wide=True)
|
|
105
|
+
self._btn_perks = UiButtonState("Perks", force_wide=True)
|
|
106
|
+
self._btn_credits = UiButtonState("Credits", force_wide=True)
|
|
107
|
+
self._btn_back = UiButtonState("Back", force_wide=False)
|
|
108
|
+
|
|
46
109
|
if self._state.audio is not None:
|
|
47
110
|
if self._state.audio.music.active_track != "shortie_monk":
|
|
48
111
|
stop_music(self._state.audio)
|
|
49
112
|
play_music(self._state.audio, "shortie_monk")
|
|
113
|
+
play_sfx(self._state.audio, "sfx_ui_panelclick", rng=self._state.rng)
|
|
50
114
|
|
|
51
|
-
def
|
|
52
|
-
self.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def update(self, dt: float) -> None:
|
|
65
|
-
super().update(dt)
|
|
66
|
-
if self._closing:
|
|
67
|
-
return
|
|
68
|
-
entry = self._entry
|
|
69
|
-
if entry is None or not self._entry_enabled(entry):
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT):
|
|
73
|
-
self._switch_page(-1)
|
|
74
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT):
|
|
75
|
-
self._switch_page(1)
|
|
76
|
-
|
|
77
|
-
font = self._ensure_small_font()
|
|
78
|
-
layout = self._content_layout()
|
|
79
|
-
rows = self._visible_rows(font, layout)
|
|
80
|
-
max_scroll = max(0, len(self._active_page_lines()) - rows)
|
|
81
|
-
|
|
82
|
-
wheel = int(rl.get_mouse_wheel_move())
|
|
83
|
-
if wheel:
|
|
84
|
-
self._scroll_index = max(0, min(max_scroll, int(self._scroll_index) - wheel))
|
|
85
|
-
|
|
86
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
|
|
87
|
-
self._scroll_index = max(0, int(self._scroll_index) - 1)
|
|
88
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
|
|
89
|
-
self._scroll_index = min(max_scroll, int(self._scroll_index) + 1)
|
|
90
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
|
|
91
|
-
self._scroll_index = max(0, int(self._scroll_index) - rows)
|
|
92
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
|
|
93
|
-
self._scroll_index = min(max_scroll, int(self._scroll_index) + rows)
|
|
94
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
|
|
95
|
-
self._scroll_index = 0
|
|
96
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
|
|
97
|
-
self._scroll_index = max_scroll
|
|
115
|
+
def close(self) -> None:
|
|
116
|
+
if self._small_font is not None:
|
|
117
|
+
rl.unload_texture(self._small_font.texture)
|
|
118
|
+
self._small_font = None
|
|
119
|
+
self._button_textures = None
|
|
120
|
+
self._assets = None
|
|
121
|
+
self._ground = None
|
|
122
|
+
self._action = None
|
|
123
|
+
|
|
124
|
+
def take_action(self) -> str | None:
|
|
125
|
+
action = self._action
|
|
126
|
+
self._action = None
|
|
127
|
+
return action
|
|
98
128
|
|
|
99
129
|
def _ensure_small_font(self) -> SmallFontData:
|
|
100
130
|
if self._small_font is not None:
|
|
@@ -103,247 +133,176 @@ class StatisticsMenuView(PanelMenuView):
|
|
|
103
133
|
self._small_font = load_small_font(self._state.assets_dir, missing_assets)
|
|
104
134
|
return self._small_font
|
|
105
135
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
base_x = panel_left + 212.0 * panel_scale
|
|
123
|
-
base_y = panel_top + 32.0 * panel_scale
|
|
124
|
-
label_x = base_x + 8.0 * panel_scale
|
|
125
|
-
return {
|
|
126
|
-
"panel_left": panel_left,
|
|
127
|
-
"panel_top": panel_top,
|
|
128
|
-
"base_x": base_x,
|
|
129
|
-
"base_y": base_y,
|
|
130
|
-
"label_x": label_x,
|
|
131
|
-
"scale": panel_scale,
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
def _switch_page(self, delta: int) -> None:
|
|
135
|
-
if not self._page_lines:
|
|
136
|
+
def _panel_top_left(self, *, scale: float) -> tuple[float, float]:
|
|
137
|
+
x0 = STATISTICS_PANEL_POS_X + MENU_PANEL_OFFSET_X * scale
|
|
138
|
+
y0 = STATISTICS_PANEL_POS_Y + self._widescreen_y_shift + MENU_PANEL_OFFSET_Y * scale
|
|
139
|
+
return float(x0), float(y0)
|
|
140
|
+
|
|
141
|
+
def update(self, dt: float) -> None:
|
|
142
|
+
if self._state.audio is not None:
|
|
143
|
+
update_audio(self._state.audio, dt)
|
|
144
|
+
if self._ground is not None:
|
|
145
|
+
self._ground.process_pending()
|
|
146
|
+
self._cursor_pulse_time += min(float(dt), 0.1) * 1.1
|
|
147
|
+
|
|
148
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
149
|
+
if self._state.audio is not None:
|
|
150
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
151
|
+
self._action = "back_to_menu"
|
|
136
152
|
return
|
|
137
|
-
count = len(self._page_lines)
|
|
138
|
-
self._page_index = (int(self._page_index) + int(delta)) % count
|
|
139
|
-
self._scroll_index = 0
|
|
140
|
-
|
|
141
|
-
def _active_page_lines(self) -> list[str]:
|
|
142
|
-
if not self._page_lines:
|
|
143
|
-
return []
|
|
144
|
-
idx = int(self._page_index)
|
|
145
|
-
if idx < 0:
|
|
146
|
-
idx = 0
|
|
147
|
-
if idx >= len(self._page_lines):
|
|
148
|
-
idx = len(self._page_lines) - 1
|
|
149
|
-
return self._page_lines[idx]
|
|
150
|
-
|
|
151
|
-
def _build_pages(self) -> list[list[str]]:
|
|
152
|
-
return [
|
|
153
|
-
self._build_summary_lines(),
|
|
154
|
-
self._build_weapon_usage_lines(),
|
|
155
|
-
self._build_quest_progress_lines(),
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
def _build_summary_lines(self) -> list[str]:
|
|
159
|
-
status = self._state.status
|
|
160
|
-
mode_counts = {name: status.mode_play_count(name) for name, _offset in MODE_COUNT_ORDER}
|
|
161
|
-
quest_counts = status.data.get("quest_play_counts", [])
|
|
162
|
-
if isinstance(quest_counts, list):
|
|
163
|
-
quest_total = int(sum(int(v) for v in quest_counts[:40]))
|
|
164
|
-
else:
|
|
165
|
-
quest_total = 0
|
|
166
|
-
|
|
167
|
-
checksum_text = "unknown"
|
|
168
|
-
try:
|
|
169
|
-
from ...persistence.save_status import load_status
|
|
170
|
-
|
|
171
|
-
blob = load_status(status.path)
|
|
172
|
-
ok = "ok" if blob.checksum_valid else "BAD"
|
|
173
|
-
checksum_text = f"0x{blob.checksum:08x} ({ok})"
|
|
174
|
-
except Exception as exc:
|
|
175
|
-
checksum_text = f"error: {type(exc).__name__}"
|
|
176
|
-
|
|
177
|
-
playtime_ms = int(status.game_sequence_id)
|
|
178
|
-
seconds = max(0, playtime_ms // 1000)
|
|
179
|
-
minutes = seconds // 60
|
|
180
|
-
hours = minutes // 60
|
|
181
|
-
minutes %= 60
|
|
182
|
-
seconds %= 60
|
|
183
|
-
|
|
184
|
-
lines = [
|
|
185
|
-
f"Played for: {hours}h {minutes:02d}m {seconds:02d}s",
|
|
186
|
-
f"Quest unlock: {status.quest_unlock_index} (full {status.quest_unlock_index_full})",
|
|
187
|
-
f"Quest plays (1-40): {quest_total}",
|
|
188
|
-
f"Mode plays: surv {mode_counts['survival']} rush {mode_counts['rush']}",
|
|
189
|
-
f" typo {mode_counts['typo']} other {mode_counts['other']}",
|
|
190
|
-
f"Playtime ms: {int(status.game_sequence_id)}",
|
|
191
|
-
f"Checksum: {checksum_text}",
|
|
192
|
-
]
|
|
193
|
-
|
|
194
|
-
usage = status.data.get("weapon_usage_counts", [])
|
|
195
|
-
top_weapons: list[tuple[int, int]] = []
|
|
196
|
-
if isinstance(usage, list):
|
|
197
|
-
for idx, count in enumerate(usage):
|
|
198
|
-
count = int(count)
|
|
199
|
-
if count > 0:
|
|
200
|
-
top_weapons.append((idx, count))
|
|
201
|
-
top_weapons.sort(key=lambda item: (-item[1], item[0]))
|
|
202
|
-
top_weapons = top_weapons[:4]
|
|
203
|
-
|
|
204
|
-
if top_weapons:
|
|
205
|
-
lines.append("Top weapons:")
|
|
206
|
-
for idx, count in top_weapons:
|
|
207
|
-
weapon = WEAPON_BY_ID.get(idx)
|
|
208
|
-
name = weapon.name if weapon is not None and weapon.name else f"weapon_{idx}"
|
|
209
|
-
lines.append(f" {name}: {count}")
|
|
210
|
-
else:
|
|
211
|
-
lines.append("Top weapons: none")
|
|
212
|
-
|
|
213
|
-
return lines
|
|
214
|
-
|
|
215
|
-
def _build_weapon_usage_lines(self) -> list[str]:
|
|
216
|
-
status = self._state.status
|
|
217
|
-
usage = status.data.get("weapon_usage_counts", [])
|
|
218
|
-
if not isinstance(usage, list):
|
|
219
|
-
return ["Weapon usage: error (missing weapon_usage_counts)"]
|
|
220
|
-
|
|
221
|
-
items: list[tuple[int, int, str]] = []
|
|
222
|
-
for idx, count in enumerate(usage):
|
|
223
|
-
weapon_id = int(idx)
|
|
224
|
-
if weapon_id == WeaponId.NONE:
|
|
225
|
-
continue
|
|
226
|
-
count = int(count)
|
|
227
|
-
weapon = WEAPON_BY_ID.get(weapon_id)
|
|
228
|
-
name = weapon.name if weapon is not None and weapon.name else f"weapon_{weapon_id}"
|
|
229
|
-
items.append((weapon_id, count, name))
|
|
230
|
-
|
|
231
|
-
items.sort(key=lambda item: (-item[1], item[0]))
|
|
232
|
-
total = sum(count for _weapon_id, count, _name in items)
|
|
233
|
-
max_id_width = max(2, len(str(max((weapon_id for weapon_id, _count, _name in items), default=0))))
|
|
234
|
-
|
|
235
|
-
lines = [
|
|
236
|
-
f"Weapon uses (total {total}):",
|
|
237
|
-
"",
|
|
238
|
-
]
|
|
239
|
-
for weapon_id, count, name in items:
|
|
240
|
-
lines.append(f"{weapon_id:>{max_id_width}} {count:>8} {name}")
|
|
241
|
-
return lines
|
|
242
|
-
|
|
243
|
-
def _build_quest_progress_lines(self) -> list[str]:
|
|
244
|
-
status = self._state.status
|
|
245
|
-
completed_total = 0
|
|
246
|
-
played_total = 0
|
|
247
|
-
|
|
248
|
-
lines = [
|
|
249
|
-
"Quest progress (stages 1-4):",
|
|
250
|
-
"",
|
|
251
|
-
]
|
|
252
|
-
for global_index in range(40):
|
|
253
|
-
stage = (global_index // 10) + 1
|
|
254
|
-
row = global_index % 10
|
|
255
|
-
level = f"{stage}.{row + 1}"
|
|
256
|
-
title = "???"
|
|
257
|
-
try:
|
|
258
|
-
from ...quests import quest_by_level
|
|
259
|
-
|
|
260
|
-
quest = quest_by_level(level)
|
|
261
|
-
if quest is not None:
|
|
262
|
-
title = quest.title
|
|
263
|
-
except Exception:
|
|
264
|
-
title = "???"
|
|
265
|
-
|
|
266
|
-
count_index = global_index + 10
|
|
267
|
-
games_idx = 1 + count_index
|
|
268
|
-
completed_idx = 41 + count_index
|
|
269
|
-
games = int(status.quest_play_count(games_idx))
|
|
270
|
-
completed = int(status.quest_play_count(completed_idx))
|
|
271
|
-
|
|
272
|
-
completed_total += completed
|
|
273
|
-
played_total += games
|
|
274
|
-
lines.append(f"{level:>4} {completed:>3}/{games:<3} {title}")
|
|
275
|
-
|
|
276
|
-
lines.extend(
|
|
277
|
-
[
|
|
278
|
-
"",
|
|
279
|
-
f"Completed: {completed_total}",
|
|
280
|
-
f"Played: {played_total}",
|
|
281
|
-
]
|
|
282
|
-
)
|
|
283
|
-
return lines
|
|
284
153
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
154
|
+
textures = self._button_textures
|
|
155
|
+
if textures is None or (textures.button_md is None and textures.button_sm is None):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
159
|
+
panel_x0, panel_y0 = self._panel_top_left(scale=scale)
|
|
160
|
+
|
|
161
|
+
mouse = rl.get_mouse_position()
|
|
162
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
163
|
+
dt_ms = min(float(dt), 0.1) * 1000.0
|
|
164
|
+
|
|
165
|
+
def _update_button(btn: UiButtonState, *, x: float, y: float) -> bool:
|
|
166
|
+
w = button_width(None, btn.label, scale=scale, force_wide=btn.force_wide)
|
|
167
|
+
return button_update(btn, x=x, y=y, width=w, dt_ms=dt_ms, mouse=mouse, click=click)
|
|
168
|
+
|
|
169
|
+
x = panel_x0 + _BUTTON_X * scale
|
|
170
|
+
y0 = panel_y0 + _BUTTON_Y0 * scale
|
|
171
|
+
if _update_button(self._btn_high_scores, x=x, y=y0 + _BUTTON_STEP_Y * 0.0 * scale):
|
|
172
|
+
if self._state.audio is not None:
|
|
173
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
174
|
+
self._action = "open_high_scores"
|
|
175
|
+
return
|
|
176
|
+
if _update_button(self._btn_weapons, x=x, y=y0 + _BUTTON_STEP_Y * 1.0 * scale):
|
|
177
|
+
if self._state.audio is not None:
|
|
178
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
179
|
+
self._action = "open_weapon_database"
|
|
180
|
+
return
|
|
181
|
+
if _update_button(self._btn_perks, x=x, y=y0 + _BUTTON_STEP_Y * 2.0 * scale):
|
|
182
|
+
if self._state.audio is not None:
|
|
183
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
184
|
+
self._action = "open_perk_database"
|
|
185
|
+
return
|
|
186
|
+
if _update_button(self._btn_credits, x=x, y=y0 + _BUTTON_STEP_Y * 3.0 * scale):
|
|
187
|
+
if self._state.audio is not None:
|
|
188
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
189
|
+
self._action = "open_credits"
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
back_x = panel_x0 + _BACK_BUTTON_X * scale
|
|
193
|
+
back_y = panel_y0 + _BACK_BUTTON_Y * scale
|
|
194
|
+
if _update_button(self._btn_back, x=back_x, y=back_y):
|
|
195
|
+
if self._state.audio is not None:
|
|
196
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
197
|
+
self._action = "back_to_menu"
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
def draw(self) -> None:
|
|
201
|
+
rl.clear_background(rl.BLACK)
|
|
202
|
+
pause_background = self._state.pause_background
|
|
203
|
+
if pause_background is not None:
|
|
204
|
+
pause_background.draw_pause_background()
|
|
205
|
+
elif self._ground is not None:
|
|
206
|
+
self._ground.draw(0.0, 0.0)
|
|
207
|
+
_draw_screen_fade(self._state)
|
|
292
208
|
|
|
293
|
-
def _draw_stats_contents(self) -> None:
|
|
294
209
|
assets = self._assets
|
|
295
|
-
if assets is None:
|
|
210
|
+
if assets is None or assets.panel is None:
|
|
296
211
|
return
|
|
297
|
-
labels_tex = assets.labels
|
|
298
|
-
layout = self._content_layout()
|
|
299
|
-
base_x = layout["base_x"]
|
|
300
|
-
base_y = layout["base_y"]
|
|
301
|
-
label_x = layout["label_x"]
|
|
302
|
-
scale = layout["scale"]
|
|
303
212
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
)
|
|
315
|
-
dst = rl.Rectangle(
|
|
316
|
-
base_x,
|
|
317
|
-
base_y,
|
|
318
|
-
MENU_LABEL_WIDTH * scale,
|
|
319
|
-
MENU_LABEL_ROW_HEIGHT * scale,
|
|
320
|
-
)
|
|
213
|
+
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
214
|
+
panel_x0, panel_y0 = self._panel_top_left(scale=scale)
|
|
215
|
+
dst = rl.Rectangle(panel_x0, panel_y0, MENU_PANEL_WIDTH * scale, STATISTICS_PANEL_HEIGHT * scale)
|
|
216
|
+
fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
|
|
217
|
+
draw_classic_menu_panel(assets.panel, dst=dst, tint=rl.WHITE, shadow=fx_detail)
|
|
218
|
+
|
|
219
|
+
# Title: full-size row from ui_itemTexts.jaz (128x32).
|
|
220
|
+
if assets.labels is not None:
|
|
221
|
+
label_tex = assets.labels
|
|
222
|
+
row_h = float(MENU_LABEL_ROW_HEIGHT)
|
|
223
|
+
src = rl.Rectangle(0.0, float(MENU_LABEL_ROW_STATISTICS) * row_h, float(label_tex.width), row_h)
|
|
321
224
|
MenuView._draw_ui_quad(
|
|
322
|
-
texture=
|
|
225
|
+
texture=label_tex,
|
|
323
226
|
src=src,
|
|
324
|
-
dst=
|
|
227
|
+
dst=rl.Rectangle(panel_x0 + _TITLE_X * scale, panel_y0 + _TITLE_Y * scale, _TITLE_W * scale, _TITLE_H * scale),
|
|
325
228
|
origin=rl.Vector2(0.0, 0.0),
|
|
326
229
|
rotation_deg=0.0,
|
|
327
230
|
tint=rl.WHITE,
|
|
328
231
|
)
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
232
|
+
|
|
233
|
+
# "played for # hours # minutes"
|
|
234
|
+
font = self._ensure_small_font()
|
|
235
|
+
playtime_text = "played for 0 hours 0 minutes"
|
|
236
|
+
try:
|
|
237
|
+
# The classic menu shows a coarse playtime summary; our persisted value is ms.
|
|
238
|
+
ms = max(0, int(self._state.status.game_sequence_id))
|
|
239
|
+
minutes = (ms // 1000) // 60
|
|
240
|
+
hours = minutes // 60
|
|
241
|
+
minutes %= 60
|
|
242
|
+
playtime_text = f"played for {hours} hours {minutes} minutes"
|
|
243
|
+
except Exception:
|
|
244
|
+
playtime_text = "played for ? hours ? minutes"
|
|
245
|
+
|
|
246
|
+
draw_small_text(
|
|
247
|
+
font,
|
|
248
|
+
playtime_text,
|
|
249
|
+
panel_x0 + _PLAYTIME_X * scale,
|
|
250
|
+
panel_y0 + _PLAYTIME_Y * scale,
|
|
251
|
+
1.0 * scale,
|
|
252
|
+
rl.Color(255, 255, 255, int(255 * 0.8)),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Buttons.
|
|
256
|
+
textures = self._button_textures
|
|
257
|
+
if textures is not None and (textures.button_md is not None or textures.button_sm is not None):
|
|
258
|
+
btn_x = panel_x0 + _BUTTON_X * scale
|
|
259
|
+
btn_y0 = panel_y0 + _BUTTON_Y0 * scale
|
|
260
|
+
for i, btn in enumerate((self._btn_high_scores, self._btn_weapons, self._btn_perks, self._btn_credits)):
|
|
261
|
+
w = button_width(None, btn.label, scale=scale, force_wide=btn.force_wide)
|
|
262
|
+
button_draw(textures, font, btn, x=btn_x, y=btn_y0 + _BUTTON_STEP_Y * float(i) * scale, width=w, scale=scale)
|
|
263
|
+
|
|
264
|
+
back_w = button_width(None, self._btn_back.label, scale=scale, force_wide=self._btn_back.force_wide)
|
|
265
|
+
button_draw(
|
|
266
|
+
textures,
|
|
267
|
+
font,
|
|
268
|
+
self._btn_back,
|
|
269
|
+
x=panel_x0 + _BACK_BUTTON_X * scale,
|
|
270
|
+
y=panel_y0 + _BACK_BUTTON_Y * scale,
|
|
271
|
+
width=back_w,
|
|
272
|
+
scale=scale,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
self._draw_sign(scale=scale)
|
|
276
|
+
_draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
|
|
277
|
+
|
|
278
|
+
def _draw_sign(self, *, scale: float) -> None:
|
|
279
|
+
assets = self._assets
|
|
280
|
+
if assets is None or assets.sign is None:
|
|
281
|
+
return
|
|
282
|
+
sign = assets.sign
|
|
283
|
+
screen_w = float(self._state.config.screen_width)
|
|
284
|
+
sign_scale, shift_x = MenuView._sign_layout_scale(int(screen_w))
|
|
285
|
+
pos_x = screen_w + MENU_SIGN_POS_X_PAD
|
|
286
|
+
pos_y = MENU_SIGN_POS_Y if screen_w > MENU_SCALE_SMALL_THRESHOLD else MENU_SIGN_POS_Y_SMALL
|
|
287
|
+
sign_w = MENU_SIGN_WIDTH * sign_scale
|
|
288
|
+
sign_h = MENU_SIGN_HEIGHT * sign_scale
|
|
289
|
+
offset_x = MENU_SIGN_OFFSET_X * sign_scale + shift_x
|
|
290
|
+
offset_y = MENU_SIGN_OFFSET_Y * sign_scale
|
|
291
|
+
rotation_deg = 0.0
|
|
292
|
+
fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
|
|
293
|
+
if fx_detail:
|
|
294
|
+
MenuView._draw_ui_quad_shadow(
|
|
295
|
+
texture=sign,
|
|
296
|
+
src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
|
|
297
|
+
dst=rl.Rectangle(pos_x + UI_SHADOW_OFFSET, pos_y + UI_SHADOW_OFFSET, sign_w, sign_h),
|
|
298
|
+
origin=rl.Vector2(-offset_x, -offset_y),
|
|
299
|
+
rotation_deg=rotation_deg,
|
|
300
|
+
)
|
|
301
|
+
MenuView._draw_ui_quad(
|
|
302
|
+
texture=sign,
|
|
303
|
+
src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
|
|
304
|
+
dst=rl.Rectangle(pos_x, pos_y, sign_w, sign_h),
|
|
305
|
+
origin=rl.Vector2(-offset_x, -offset_y),
|
|
306
|
+
rotation_deg=rotation_deg,
|
|
307
|
+
tint=rl.WHITE,
|
|
308
|
+
)
|