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
crimson/game.py
CHANGED
|
@@ -51,6 +51,19 @@ from .demo_trial import (
|
|
|
51
51
|
)
|
|
52
52
|
from .frontend.boot import BootView
|
|
53
53
|
from .frontend.assets import MenuAssets, _ensure_texture_cache, load_menu_assets
|
|
54
|
+
from .frontend.high_scores_layout import (
|
|
55
|
+
HS_BACK_BUTTON_X,
|
|
56
|
+
HS_BACK_BUTTON_Y,
|
|
57
|
+
HS_BUTTON_STEP_Y,
|
|
58
|
+
HS_BUTTON_X,
|
|
59
|
+
HS_BUTTON_Y0,
|
|
60
|
+
HS_LEFT_PANEL_HEIGHT,
|
|
61
|
+
HS_LEFT_PANEL_POS_X,
|
|
62
|
+
HS_LEFT_PANEL_POS_Y,
|
|
63
|
+
HS_RIGHT_PANEL_HEIGHT,
|
|
64
|
+
HS_RIGHT_PANEL_POS_X,
|
|
65
|
+
HS_RIGHT_PANEL_POS_Y,
|
|
66
|
+
)
|
|
54
67
|
from .frontend.menu import (
|
|
55
68
|
MENU_PANEL_HEIGHT,
|
|
56
69
|
MENU_PANEL_OFFSET_X,
|
|
@@ -72,13 +85,18 @@ from .frontend.menu import (
|
|
|
72
85
|
)
|
|
73
86
|
from .frontend.panels.base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
|
|
74
87
|
from .frontend.panels.controls import ControlsMenuView
|
|
88
|
+
from .frontend.panels.credits import CreditsView
|
|
89
|
+
from .frontend.panels.databases import UnlockedPerksDatabaseView, UnlockedWeaponsDatabaseView
|
|
75
90
|
from .frontend.panels.mods import ModsMenuView
|
|
76
91
|
from .frontend.panels.options import OptionsMenuView
|
|
77
92
|
from .frontend.panels.play_game import PlayGameMenuView
|
|
78
93
|
from .frontend.panels.stats import StatisticsMenuView
|
|
94
|
+
from .frontend.pause_menu import PauseMenuView
|
|
79
95
|
from .frontend.transitions import _draw_screen_fade, _update_screen_fade
|
|
80
96
|
from .persistence.save_status import GameStatus, ensure_game_status
|
|
81
97
|
from .ui.demo_trial_overlay import DEMO_PURCHASE_URL, DemoTrialOverlayUi
|
|
98
|
+
from .ui.menu_panel import draw_classic_menu_panel
|
|
99
|
+
from .ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width
|
|
82
100
|
from .paths import default_runtime_dir
|
|
83
101
|
from .assets_fetch import download_missing_paqs
|
|
84
102
|
|
|
@@ -124,6 +142,7 @@ class GameState:
|
|
|
124
142
|
snd_freq_adjustment_enabled: bool = False
|
|
125
143
|
menu_ground: GroundRenderer | None = None
|
|
126
144
|
menu_sign_locked: bool = False
|
|
145
|
+
pause_background: PauseBackground | None = None
|
|
127
146
|
pending_quest_level: str | None = None
|
|
128
147
|
pending_high_scores: HighScoresRequest | None = None
|
|
129
148
|
quest_outcome: QuestRunOutcome | None = None
|
|
@@ -141,6 +160,7 @@ AUTOEXEC_NAME = "autoexec.txt"
|
|
|
141
160
|
|
|
142
161
|
QUEST_MENU_BASE_X = -5.0
|
|
143
162
|
QUEST_MENU_BASE_Y = 185.0
|
|
163
|
+
QUEST_MENU_PANEL_OFFSET_X = -63.0
|
|
144
164
|
|
|
145
165
|
QUEST_TITLE_X_OFFSET = 219.0 # 300 + 64 - 145
|
|
146
166
|
QUEST_TITLE_Y_OFFSET = 44.0 # 40 + 4
|
|
@@ -166,8 +186,9 @@ QUEST_HARDCORE_CHECKBOX_X_OFFSET = 132.0
|
|
|
166
186
|
QUEST_HARDCORE_CHECKBOX_Y_OFFSET = -12.0
|
|
167
187
|
QUEST_HARDCORE_LIST_Y_SHIFT = 10.0
|
|
168
188
|
|
|
169
|
-
QUEST_BACK_BUTTON_X_OFFSET =
|
|
189
|
+
QUEST_BACK_BUTTON_X_OFFSET = 138.0
|
|
170
190
|
QUEST_BACK_BUTTON_Y_OFFSET = 212.0
|
|
191
|
+
QUEST_PANEL_HEIGHT = 378.0
|
|
171
192
|
|
|
172
193
|
|
|
173
194
|
class QuestsMenuView:
|
|
@@ -192,6 +213,8 @@ class QuestsMenuView:
|
|
|
192
213
|
self._check_off: rl.Texture2D | None = None
|
|
193
214
|
self._button_sm: rl.Texture2D | None = None
|
|
194
215
|
self._button_md: rl.Texture2D | None = None
|
|
216
|
+
self._button_textures: UiButtonTextureSet | None = None
|
|
217
|
+
self._back_button = UiButtonState("Back")
|
|
195
218
|
|
|
196
219
|
self._menu_screen_width = 0
|
|
197
220
|
self._widescreen_y_shift = 0.0
|
|
@@ -224,11 +247,13 @@ class QuestsMenuView:
|
|
|
224
247
|
self._check_off = cache.get_or_load("ui_checkOff", "ui/ui_checkOff.jaz").texture
|
|
225
248
|
self._button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
226
249
|
self._button_md = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
250
|
+
self._button_textures = UiButtonTextureSet(button_sm=self._button_sm, button_md=self._button_md)
|
|
227
251
|
|
|
228
252
|
self._action = None
|
|
229
253
|
self._dirty = False
|
|
230
254
|
self._stage = max(1, min(5, int(self._stage)))
|
|
231
255
|
self._cursor_pulse_time = 0.0
|
|
256
|
+
self._back_button = UiButtonState("Back")
|
|
232
257
|
|
|
233
258
|
# Ensure the quest registry is populated so titles render.
|
|
234
259
|
# (The package import registers all tier builders.)
|
|
@@ -247,6 +272,7 @@ class QuestsMenuView:
|
|
|
247
272
|
pass
|
|
248
273
|
self._dirty = False
|
|
249
274
|
self._ground = None
|
|
275
|
+
self._button_textures = None
|
|
250
276
|
|
|
251
277
|
def update(self, dt: float) -> None:
|
|
252
278
|
if self._state.audio is not None:
|
|
@@ -283,9 +309,26 @@ class QuestsMenuView:
|
|
|
283
309
|
if self._hardcore_checkbox_clicked(layout):
|
|
284
310
|
return
|
|
285
311
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
312
|
+
textures = self._button_textures
|
|
313
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
314
|
+
back_x = layout["list_x"] + QUEST_BACK_BUTTON_X_OFFSET
|
|
315
|
+
back_y = self._rows_y0(layout) + QUEST_BACK_BUTTON_Y_OFFSET
|
|
316
|
+
dt_ms = min(float(dt), 0.1) * 1000.0
|
|
317
|
+
font = self._ensure_small_font()
|
|
318
|
+
back_w = button_width(font, self._back_button.label, scale=1.0, force_wide=self._back_button.force_wide)
|
|
319
|
+
mouse = rl.get_mouse_position()
|
|
320
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
321
|
+
if button_update(
|
|
322
|
+
self._back_button,
|
|
323
|
+
x=float(back_x),
|
|
324
|
+
y=float(back_y),
|
|
325
|
+
width=float(back_w),
|
|
326
|
+
dt_ms=float(dt_ms),
|
|
327
|
+
mouse=mouse,
|
|
328
|
+
click=bool(click),
|
|
329
|
+
):
|
|
330
|
+
self._action = "open_play_game"
|
|
331
|
+
return
|
|
289
332
|
|
|
290
333
|
# Quick-select row numbers 1..0 (10).
|
|
291
334
|
row_from_key = self._digit_row_pressed()
|
|
@@ -329,10 +372,10 @@ class QuestsMenuView:
|
|
|
329
372
|
|
|
330
373
|
def _layout(self) -> dict[str, float]:
|
|
331
374
|
# `sub_447d40` base sums:
|
|
332
|
-
# x_sum = <ui_element_x> + (
|
|
333
|
-
# y_sum = <ui_element_y> + 185
|
|
334
|
-
x_sum = QUEST_MENU_BASE_X
|
|
335
|
-
y_sum = QUEST_MENU_BASE_Y + self._widescreen_y_shift
|
|
375
|
+
# x_sum = <ui_element_x> + <ui_element_offset_x> (x=-5)
|
|
376
|
+
# y_sum = <ui_element_y> + <ui_element_offset_y> (y=185 + widescreen shift via ui_menu_layout_init)
|
|
377
|
+
x_sum = QUEST_MENU_BASE_X + MENU_PANEL_OFFSET_X
|
|
378
|
+
y_sum = QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift
|
|
336
379
|
|
|
337
380
|
title_x = x_sum + QUEST_TITLE_X_OFFSET
|
|
338
381
|
title_y = y_sum + QUEST_TITLE_Y_OFFSET
|
|
@@ -392,20 +435,6 @@ class QuestsMenuView:
|
|
|
392
435
|
return True
|
|
393
436
|
return False
|
|
394
437
|
|
|
395
|
-
def _back_button_clicked(self, layout: dict[str, float]) -> bool:
|
|
396
|
-
tex = self._button_sm
|
|
397
|
-
if tex is None:
|
|
398
|
-
tex = self._button_md
|
|
399
|
-
if tex is None:
|
|
400
|
-
return False
|
|
401
|
-
x = layout["list_x"] + QUEST_BACK_BUTTON_X_OFFSET
|
|
402
|
-
y = self._rows_y0(layout) + QUEST_BACK_BUTTON_Y_OFFSET
|
|
403
|
-
w = float(tex.width)
|
|
404
|
-
h = float(tex.height)
|
|
405
|
-
mouse = rl.get_mouse_position()
|
|
406
|
-
hovered = x <= mouse.x <= x + w and y <= mouse.y <= y + h
|
|
407
|
-
return hovered and rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
408
|
-
|
|
409
438
|
@staticmethod
|
|
410
439
|
def _digit_row_pressed() -> int | None:
|
|
411
440
|
keys = [
|
|
@@ -634,12 +663,15 @@ class QuestsMenuView:
|
|
|
634
663
|
else:
|
|
635
664
|
title = "???"
|
|
636
665
|
draw_small_text(font, title, list_x + QUEST_LIST_NAME_X_OFFSET, y, 1.0, color)
|
|
666
|
+
title_w = measure_small_text_width(font, title, 1.0) if unlocked else 0.0
|
|
667
|
+
if unlocked:
|
|
668
|
+
line_y = y + 13.0
|
|
669
|
+
rl.draw_line(int(list_x), int(line_y), int(list_x + title_w + 32.0), int(line_y), color)
|
|
637
670
|
|
|
638
671
|
if show_counts and unlocked:
|
|
639
672
|
counts = self._quest_counts(stage=stage, row=row)
|
|
640
673
|
if counts is not None:
|
|
641
674
|
completed, games = counts
|
|
642
|
-
title_w = measure_small_text_width(font, title, 1.0)
|
|
643
675
|
counts_x = list_x + QUEST_LIST_NAME_X_OFFSET + title_w + 12.0
|
|
644
676
|
draw_small_text(font, f"({completed}/{games})", counts_x, y, 1.0, color)
|
|
645
677
|
|
|
@@ -650,28 +682,12 @@ class QuestsMenuView:
|
|
|
650
682
|
draw_small_text(font, "(completed/games)", header_x, header_y, 1.0, base_color)
|
|
651
683
|
|
|
652
684
|
# Back button.
|
|
653
|
-
|
|
654
|
-
if
|
|
685
|
+
textures = self._button_textures
|
|
686
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
655
687
|
back_x = list_x + QUEST_BACK_BUTTON_X_OFFSET
|
|
656
688
|
back_y = y0 + QUEST_BACK_BUTTON_Y_OFFSET
|
|
657
|
-
back_w =
|
|
658
|
-
|
|
659
|
-
mouse = rl.get_mouse_position()
|
|
660
|
-
hovered = back_x <= mouse.x <= back_x + back_w and back_y <= mouse.y <= back_y + back_h
|
|
661
|
-
rl.draw_texture_pro(
|
|
662
|
-
button,
|
|
663
|
-
rl.Rectangle(0.0, 0.0, float(button.width), float(button.height)),
|
|
664
|
-
rl.Rectangle(back_x, back_y, back_w, back_h),
|
|
665
|
-
rl.Vector2(0.0, 0.0),
|
|
666
|
-
0.0,
|
|
667
|
-
rl.WHITE,
|
|
668
|
-
)
|
|
669
|
-
label = "Back"
|
|
670
|
-
label_w = measure_small_text_width(font, label, 1.0)
|
|
671
|
-
text_x = back_x + (back_w - label_w) * 0.5 + 1.0
|
|
672
|
-
text_y = back_y + 10.0
|
|
673
|
-
text_alpha = 255 if hovered else 179
|
|
674
|
-
draw_small_text(font, label, text_x, text_y, 1.0, rl.Color(255, 255, 255, text_alpha))
|
|
689
|
+
back_w = button_width(font, self._back_button.label, scale=1.0, force_wide=self._back_button.force_wide)
|
|
690
|
+
button_draw(textures, font, self._back_button, x=float(back_x), y=float(back_y), width=float(back_w), scale=1.0)
|
|
675
691
|
|
|
676
692
|
def _draw_sign(self) -> None:
|
|
677
693
|
assets = self._assets
|
|
@@ -719,53 +735,19 @@ class QuestsMenuView:
|
|
|
719
735
|
panel = self._panel_tex
|
|
720
736
|
if panel is None:
|
|
721
737
|
return
|
|
722
|
-
panel_scale = 0.9 if self._menu_screen_width < 641 else 1.0
|
|
723
|
-
dst = rl.Rectangle(
|
|
724
|
-
QUEST_MENU_BASE_X,
|
|
725
|
-
QUEST_MENU_BASE_Y + self._widescreen_y_shift,
|
|
726
|
-
MENU_PANEL_WIDTH * panel_scale,
|
|
727
|
-
MENU_PANEL_HEIGHT * panel_scale,
|
|
728
|
-
)
|
|
729
|
-
origin = rl.Vector2(-(MENU_PANEL_OFFSET_X * panel_scale), -(MENU_PANEL_OFFSET_Y * panel_scale))
|
|
730
738
|
fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
)
|
|
739
|
-
|
|
740
|
-
texture=panel,
|
|
741
|
-
src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
|
|
742
|
-
dst=dst,
|
|
743
|
-
origin=origin,
|
|
744
|
-
rotation_deg=0.0,
|
|
745
|
-
tint=rl.WHITE,
|
|
739
|
+
draw_classic_menu_panel(
|
|
740
|
+
panel,
|
|
741
|
+
dst=rl.Rectangle(
|
|
742
|
+
float(QUEST_MENU_BASE_X + QUEST_MENU_PANEL_OFFSET_X),
|
|
743
|
+
float(QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift),
|
|
744
|
+
float(MENU_PANEL_WIDTH),
|
|
745
|
+
float(QUEST_PANEL_HEIGHT),
|
|
746
|
+
),
|
|
747
|
+
shadow=fx_detail,
|
|
746
748
|
)
|
|
747
749
|
|
|
748
750
|
|
|
749
|
-
class QuestStartView(PanelMenuView):
|
|
750
|
-
def __init__(self, state: GameState) -> None:
|
|
751
|
-
super().__init__(
|
|
752
|
-
state,
|
|
753
|
-
title="Quest",
|
|
754
|
-
body="Quest gameplay is not implemented yet.",
|
|
755
|
-
back_action="open_quests",
|
|
756
|
-
)
|
|
757
|
-
|
|
758
|
-
def open(self) -> None:
|
|
759
|
-
level = self._state.pending_quest_level or "unknown"
|
|
760
|
-
self._title = f"Quest {level}"
|
|
761
|
-
self._body_lines = [
|
|
762
|
-
f"Selected quest: {level}",
|
|
763
|
-
"",
|
|
764
|
-
"Quest gameplay is not implemented yet.",
|
|
765
|
-
]
|
|
766
|
-
super().open()
|
|
767
|
-
|
|
768
|
-
|
|
769
751
|
class FrontView(Protocol):
|
|
770
752
|
def open(self) -> None: ...
|
|
771
753
|
|
|
@@ -778,6 +760,10 @@ class FrontView(Protocol):
|
|
|
778
760
|
def take_action(self) -> str | None: ...
|
|
779
761
|
|
|
780
762
|
|
|
763
|
+
class PauseBackground(Protocol):
|
|
764
|
+
def draw_pause_background(self) -> None: ...
|
|
765
|
+
|
|
766
|
+
|
|
781
767
|
class SurvivalGameView:
|
|
782
768
|
"""Gameplay view wrapper that adapts SurvivalMode into `crimson game`."""
|
|
783
769
|
|
|
@@ -817,6 +803,9 @@ class SurvivalGameView:
|
|
|
817
803
|
def update(self, dt: float) -> None:
|
|
818
804
|
self._mode.update(dt)
|
|
819
805
|
mode_action = self._mode.take_action()
|
|
806
|
+
if mode_action == "open_pause_menu":
|
|
807
|
+
self._action = "open_pause_menu"
|
|
808
|
+
return
|
|
820
809
|
if mode_action == "open_high_scores":
|
|
821
810
|
self._state.pending_high_scores = HighScoresRequest(game_mode_id=1)
|
|
822
811
|
self._action = "open_high_scores"
|
|
@@ -832,6 +821,9 @@ class SurvivalGameView:
|
|
|
832
821
|
def draw(self) -> None:
|
|
833
822
|
self._mode.draw()
|
|
834
823
|
|
|
824
|
+
def draw_pause_background(self) -> None:
|
|
825
|
+
self._mode.draw_pause_background()
|
|
826
|
+
|
|
835
827
|
def take_action(self) -> str | None:
|
|
836
828
|
action = self._action
|
|
837
829
|
self._action = None
|
|
@@ -875,6 +867,9 @@ class RushGameView:
|
|
|
875
867
|
def update(self, dt: float) -> None:
|
|
876
868
|
self._mode.update(dt)
|
|
877
869
|
mode_action = self._mode.take_action()
|
|
870
|
+
if mode_action == "open_pause_menu":
|
|
871
|
+
self._action = "open_pause_menu"
|
|
872
|
+
return
|
|
878
873
|
if mode_action == "open_high_scores":
|
|
879
874
|
self._state.pending_high_scores = HighScoresRequest(game_mode_id=2)
|
|
880
875
|
self._action = "open_high_scores"
|
|
@@ -890,6 +885,9 @@ class RushGameView:
|
|
|
890
885
|
def draw(self) -> None:
|
|
891
886
|
self._mode.draw()
|
|
892
887
|
|
|
888
|
+
def draw_pause_background(self) -> None:
|
|
889
|
+
self._mode.draw_pause_background()
|
|
890
|
+
|
|
893
891
|
def take_action(self) -> str | None:
|
|
894
892
|
action = self._action
|
|
895
893
|
self._action = None
|
|
@@ -933,6 +931,9 @@ class TypoShooterGameView:
|
|
|
933
931
|
def update(self, dt: float) -> None:
|
|
934
932
|
self._mode.update(dt)
|
|
935
933
|
mode_action = self._mode.take_action()
|
|
934
|
+
if mode_action == "open_pause_menu":
|
|
935
|
+
self._action = "open_pause_menu"
|
|
936
|
+
return
|
|
936
937
|
if mode_action == "open_high_scores":
|
|
937
938
|
self._state.pending_high_scores = HighScoresRequest(game_mode_id=4)
|
|
938
939
|
self._action = "open_high_scores"
|
|
@@ -948,6 +949,9 @@ class TypoShooterGameView:
|
|
|
948
949
|
def draw(self) -> None:
|
|
949
950
|
self._mode.draw()
|
|
950
951
|
|
|
952
|
+
def draw_pause_background(self) -> None:
|
|
953
|
+
self._mode.draw_pause_background()
|
|
954
|
+
|
|
951
955
|
def take_action(self) -> str | None:
|
|
952
956
|
action = self._action
|
|
953
957
|
self._action = None
|
|
@@ -991,6 +995,10 @@ class TutorialGameView:
|
|
|
991
995
|
|
|
992
996
|
def update(self, dt: float) -> None:
|
|
993
997
|
self._mode.update(dt)
|
|
998
|
+
mode_action = self._mode.take_action()
|
|
999
|
+
if mode_action == "open_pause_menu":
|
|
1000
|
+
self._action = "open_pause_menu"
|
|
1001
|
+
return
|
|
994
1002
|
if getattr(self._mode, "close_requested", False):
|
|
995
1003
|
self._action = "back_to_menu"
|
|
996
1004
|
self._mode.close_requested = False
|
|
@@ -998,6 +1006,9 @@ class TutorialGameView:
|
|
|
998
1006
|
def draw(self) -> None:
|
|
999
1007
|
self._mode.draw()
|
|
1000
1008
|
|
|
1009
|
+
def draw_pause_background(self) -> None:
|
|
1010
|
+
self._mode.draw_pause_background()
|
|
1011
|
+
|
|
1001
1012
|
def take_action(self) -> str | None:
|
|
1002
1013
|
action = self._action
|
|
1003
1014
|
self._action = None
|
|
@@ -1046,6 +1057,10 @@ class QuestGameView:
|
|
|
1046
1057
|
|
|
1047
1058
|
def update(self, dt: float) -> None:
|
|
1048
1059
|
self._mode.update(dt)
|
|
1060
|
+
mode_action = self._mode.take_action()
|
|
1061
|
+
if mode_action == "open_pause_menu":
|
|
1062
|
+
self._action = "open_pause_menu"
|
|
1063
|
+
return
|
|
1049
1064
|
if getattr(self._mode, "close_requested", False):
|
|
1050
1065
|
outcome = self._mode.consume_outcome()
|
|
1051
1066
|
if outcome is not None:
|
|
@@ -1063,6 +1078,9 @@ class QuestGameView:
|
|
|
1063
1078
|
def draw(self) -> None:
|
|
1064
1079
|
self._mode.draw()
|
|
1065
1080
|
|
|
1081
|
+
def draw_pause_background(self) -> None:
|
|
1082
|
+
self._mode.draw_pause_background()
|
|
1083
|
+
|
|
1066
1084
|
def take_action(self) -> str | None:
|
|
1067
1085
|
action = self._action
|
|
1068
1086
|
self._action = None
|
|
@@ -1103,45 +1121,35 @@ class QuestResultsView:
|
|
|
1103
1121
|
def __init__(self, state: GameState) -> None:
|
|
1104
1122
|
self._state = state
|
|
1105
1123
|
self._ground: GroundRenderer | None = None
|
|
1106
|
-
self.
|
|
1124
|
+
self._quest_level: str = ""
|
|
1107
1125
|
self._quest_title: str = ""
|
|
1108
1126
|
self._quest_stage_major = 0
|
|
1109
1127
|
self._quest_stage_minor = 0
|
|
1110
1128
|
self._unlock_weapon_name: str = ""
|
|
1111
1129
|
self._unlock_perk_name: str = ""
|
|
1112
|
-
self.
|
|
1113
|
-
self._breakdown_anim = None
|
|
1114
|
-
self._record = None
|
|
1115
|
-
self._rank_index: int | None = None
|
|
1130
|
+
self._ui = None
|
|
1116
1131
|
self._action: str | None = None
|
|
1117
|
-
self._cursor_pulse_time = 0.0
|
|
1118
|
-
self._small_font: SmallFontData | None = None
|
|
1119
|
-
self._button_tex: rl.Texture2D | None = None
|
|
1120
1132
|
|
|
1121
1133
|
def open(self) -> None:
|
|
1122
|
-
from .
|
|
1123
|
-
from .
|
|
1134
|
+
from .persistence.highscores import HighScoreRecord
|
|
1135
|
+
from .quests.results import compute_quest_final_time
|
|
1136
|
+
from .ui.quest_results import QuestResultsUi
|
|
1124
1137
|
|
|
1125
1138
|
self._action = None
|
|
1126
|
-
self._ground = ensure_menu_ground(self._state)
|
|
1127
|
-
self._cursor_pulse_time = 0.0
|
|
1128
|
-
self._outcome = self._state.quest_outcome
|
|
1129
|
-
self._state.quest_outcome = None
|
|
1130
|
-
outcome = self._outcome
|
|
1139
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
1131
1140
|
self._state.quest_fail_retry_count = 0
|
|
1141
|
+
outcome = self._state.quest_outcome
|
|
1142
|
+
self._state.quest_outcome = None
|
|
1143
|
+
self._quest_level = ""
|
|
1132
1144
|
self._quest_title = ""
|
|
1133
1145
|
self._quest_stage_major = 0
|
|
1134
1146
|
self._quest_stage_minor = 0
|
|
1135
1147
|
self._unlock_weapon_name = ""
|
|
1136
1148
|
self._unlock_perk_name = ""
|
|
1137
|
-
self.
|
|
1138
|
-
self._breakdown_anim = None
|
|
1139
|
-
self._record = None
|
|
1140
|
-
self._rank_index = None
|
|
1141
|
-
self._button_tex = None
|
|
1142
|
-
self._small_font = None
|
|
1149
|
+
self._ui = None
|
|
1143
1150
|
if outcome is None:
|
|
1144
1151
|
return
|
|
1152
|
+
self._quest_level = str(outcome.level or "")
|
|
1145
1153
|
|
|
1146
1154
|
major, minor = 0, 0
|
|
1147
1155
|
try:
|
|
@@ -1199,7 +1207,8 @@ class QuestResultsView:
|
|
|
1199
1207
|
pending_perk_count=int(outcome.pending_perk_count),
|
|
1200
1208
|
)
|
|
1201
1209
|
record.survival_elapsed_ms = int(breakdown.final_time_ms)
|
|
1202
|
-
|
|
1210
|
+
player_name_default = _player_name_default(self._state.config) or "Player"
|
|
1211
|
+
record.set_name(player_name_default)
|
|
1203
1212
|
|
|
1204
1213
|
global_index = (int(major) - 1) * 10 + (int(minor) - 1)
|
|
1205
1214
|
if 0 <= global_index < 40:
|
|
@@ -1228,271 +1237,86 @@ class QuestResultsView:
|
|
|
1228
1237
|
except Exception:
|
|
1229
1238
|
pass
|
|
1230
1239
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
self.
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1240
|
+
self._ui = QuestResultsUi(
|
|
1241
|
+
assets_root=self._state.assets_dir,
|
|
1242
|
+
base_dir=self._state.base_dir,
|
|
1243
|
+
config=self._state.config,
|
|
1244
|
+
)
|
|
1245
|
+
self._ui.open(
|
|
1246
|
+
record=record,
|
|
1247
|
+
breakdown=breakdown,
|
|
1248
|
+
quest_level=str(outcome.level or ""),
|
|
1249
|
+
quest_title=str(self._quest_title or ""),
|
|
1250
|
+
quest_stage_major=int(self._quest_stage_major),
|
|
1251
|
+
quest_stage_minor=int(self._quest_stage_minor),
|
|
1252
|
+
unlock_weapon_name=str(self._unlock_weapon_name or ""),
|
|
1253
|
+
unlock_perk_name=str(self._unlock_perk_name or ""),
|
|
1254
|
+
player_name_default=player_name_default,
|
|
1255
|
+
)
|
|
1243
1256
|
|
|
1244
1257
|
def close(self) -> None:
|
|
1245
|
-
self.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
self.
|
|
1249
|
-
self._breakdown = None
|
|
1250
|
-
self._breakdown_anim = None
|
|
1251
|
-
self._rank_index = None
|
|
1258
|
+
if self._ui is not None:
|
|
1259
|
+
self._ui.close()
|
|
1260
|
+
self._ui = None
|
|
1261
|
+
self._ground = None
|
|
1252
1262
|
self._quest_stage_major = 0
|
|
1253
1263
|
self._quest_stage_minor = 0
|
|
1264
|
+
self._quest_level = ""
|
|
1265
|
+
self._quest_title = ""
|
|
1254
1266
|
self._unlock_weapon_name = ""
|
|
1255
1267
|
self._unlock_perk_name = ""
|
|
1256
1268
|
|
|
1257
1269
|
def update(self, dt: float) -> None:
|
|
1258
|
-
from .quests.results import tick_quest_results_breakdown_anim
|
|
1259
|
-
|
|
1260
1270
|
if self._state.audio is not None:
|
|
1261
1271
|
update_audio(self._state.audio, dt)
|
|
1262
1272
|
if self._ground is not None:
|
|
1263
1273
|
self._ground.process_pending()
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
1267
|
-
self._action = "back_to_menu"
|
|
1274
|
+
ui = self._ui
|
|
1275
|
+
if ui is None:
|
|
1268
1276
|
return
|
|
1277
|
+
audio = self._state.audio
|
|
1278
|
+
rng = self._state.rng
|
|
1269
1279
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
return
|
|
1273
|
-
|
|
1274
|
-
outcome = self._outcome
|
|
1275
|
-
record = self._record
|
|
1276
|
-
breakdown = self._breakdown
|
|
1277
|
-
if record is None or outcome is None or breakdown is None:
|
|
1278
|
-
return
|
|
1279
|
-
|
|
1280
|
-
anim = self._breakdown_anim
|
|
1281
|
-
if anim is not None and not anim.done:
|
|
1282
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE) or rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT):
|
|
1283
|
-
anim.set_final(breakdown)
|
|
1284
|
-
return
|
|
1285
|
-
|
|
1286
|
-
clinks = tick_quest_results_breakdown_anim(
|
|
1287
|
-
anim,
|
|
1288
|
-
frame_dt_ms=int(min(dt, 0.1) * 1000.0),
|
|
1289
|
-
target=breakdown,
|
|
1290
|
-
)
|
|
1291
|
-
if clinks > 0 and self._state.audio is not None:
|
|
1292
|
-
play_sfx(self._state.audio, "sfx_ui_clink_01", rng=self._state.rng)
|
|
1293
|
-
if not anim.done:
|
|
1280
|
+
def _play(name: str) -> None:
|
|
1281
|
+
if audio is None:
|
|
1294
1282
|
return
|
|
1283
|
+
play_sfx(audio, name, rng=rng)
|
|
1295
1284
|
|
|
1296
|
-
if
|
|
1297
|
-
|
|
1285
|
+
action = ui.update(dt, play_sfx=_play if audio is not None else None, rand=lambda: rng.getrandbits(32))
|
|
1286
|
+
if action == "play_again":
|
|
1287
|
+
self._state.pending_quest_level = self._quest_level
|
|
1298
1288
|
self._action = "start_quest"
|
|
1299
1289
|
return
|
|
1300
|
-
if
|
|
1301
|
-
next_level = _next_quest_level(
|
|
1290
|
+
if action == "play_next":
|
|
1291
|
+
next_level = _next_quest_level(self._quest_level)
|
|
1302
1292
|
if next_level is not None:
|
|
1303
1293
|
self._state.pending_quest_level = next_level
|
|
1304
1294
|
self._action = "start_quest"
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
tex = self._button_tex
|
|
1308
|
-
if tex is None:
|
|
1309
|
-
return
|
|
1310
|
-
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1311
|
-
button_w = float(tex.width) * scale
|
|
1312
|
-
button_h = float(tex.height) * scale
|
|
1313
|
-
gap_x = 18.0 * scale
|
|
1314
|
-
gap_y = 12.0 * scale
|
|
1315
|
-
x0 = 32.0
|
|
1316
|
-
y0 = float(rl.get_screen_height()) - (button_h * 2.0 + gap_y) - 52.0 * scale
|
|
1317
|
-
x1 = x0 + button_w + gap_x
|
|
1318
|
-
y1 = y0 + button_h + gap_y
|
|
1319
|
-
|
|
1320
|
-
buttons = [
|
|
1321
|
-
("Play again", rl.Rectangle(x0, y0, button_w, button_h), "play_again"),
|
|
1322
|
-
("Play next", rl.Rectangle(x1, y0, button_w, button_h), "play_next"),
|
|
1323
|
-
("High scores", rl.Rectangle(x0, y1, button_w, button_h), "high_scores"),
|
|
1324
|
-
("Main menu", rl.Rectangle(x1, y1, button_w, button_h), "main_menu"),
|
|
1325
|
-
]
|
|
1326
|
-
mouse = rl.get_mouse_position()
|
|
1327
|
-
clicked = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
1328
|
-
for _label, rect, action in buttons:
|
|
1329
|
-
hovered = rect.x <= mouse.x <= rect.x + rect.width and rect.y <= mouse.y <= rect.y + rect.height
|
|
1330
|
-
if not hovered or not clicked:
|
|
1331
|
-
continue
|
|
1332
|
-
if action == "play_again":
|
|
1333
|
-
self._state.pending_quest_level = outcome.level
|
|
1334
|
-
self._action = "start_quest"
|
|
1335
|
-
return
|
|
1336
|
-
if action == "play_next":
|
|
1337
|
-
next_level = _next_quest_level(outcome.level)
|
|
1338
|
-
if next_level is not None:
|
|
1339
|
-
self._state.pending_quest_level = next_level
|
|
1340
|
-
self._action = "start_quest"
|
|
1341
|
-
return
|
|
1342
|
-
if action == "main_menu":
|
|
1295
|
+
else:
|
|
1343
1296
|
self._action = "back_to_menu"
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1297
|
+
return
|
|
1298
|
+
if action == "high_scores":
|
|
1299
|
+
self._open_high_scores_list()
|
|
1300
|
+
return
|
|
1301
|
+
if action == "main_menu":
|
|
1302
|
+
self._action = "back_to_menu"
|
|
1303
|
+
return
|
|
1348
1304
|
|
|
1349
1305
|
def draw(self) -> None:
|
|
1350
1306
|
rl.clear_background(rl.BLACK)
|
|
1351
|
-
|
|
1307
|
+
pause_background = self._state.pause_background
|
|
1308
|
+
if pause_background is not None:
|
|
1309
|
+
pause_background.draw_pause_background()
|
|
1310
|
+
elif self._ground is not None:
|
|
1352
1311
|
self._ground.draw(0.0, 0.0)
|
|
1353
1312
|
_draw_screen_fade(self._state)
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
breakdown = self._breakdown
|
|
1358
|
-
if record is None or outcome is None or breakdown is None:
|
|
1359
|
-
rl.draw_text("Quest results unavailable.", 32, 140, 28, rl.Color(235, 235, 235, 255))
|
|
1360
|
-
rl.draw_text("Press ESC to return to the menu.", 32, 180, 18, rl.Color(190, 190, 200, 255))
|
|
1313
|
+
ui = self._ui
|
|
1314
|
+
if ui is not None:
|
|
1315
|
+
ui.draw()
|
|
1361
1316
|
return
|
|
1362
1317
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
life_bonus_ms = int(breakdown.life_bonus_ms)
|
|
1366
|
-
perk_bonus_ms = int(breakdown.unpicked_perk_bonus_ms)
|
|
1367
|
-
final_time_ms = int(breakdown.final_time_ms)
|
|
1368
|
-
step = 4
|
|
1369
|
-
highlight_alpha = 1.0
|
|
1370
|
-
if anim is not None and not anim.done:
|
|
1371
|
-
base_time_ms = int(anim.base_time_ms)
|
|
1372
|
-
life_bonus_ms = int(anim.life_bonus_ms)
|
|
1373
|
-
perk_bonus_ms = int(anim.unpicked_perk_bonus_s) * 1000
|
|
1374
|
-
final_time_ms = int(anim.final_time_ms)
|
|
1375
|
-
step = int(anim.step)
|
|
1376
|
-
highlight_alpha = float(anim.highlight_alpha())
|
|
1377
|
-
|
|
1378
|
-
def _fmt_clock(ms: int) -> str:
|
|
1379
|
-
total_seconds = max(0, int(ms) // 1000)
|
|
1380
|
-
minutes = total_seconds // 60
|
|
1381
|
-
seconds = total_seconds % 60
|
|
1382
|
-
return f"{minutes:02d}:{seconds:02d}"
|
|
1383
|
-
|
|
1384
|
-
def _fmt_bonus(ms: int) -> str:
|
|
1385
|
-
return f"-{float(max(0, int(ms))) / 1000.0:.2f}s"
|
|
1386
|
-
|
|
1387
|
-
def _breakdown_color(idx: int, *, final: bool = False) -> rl.Color:
|
|
1388
|
-
if anim is None or anim.done:
|
|
1389
|
-
if final:
|
|
1390
|
-
return rl.Color(255, 255, 255, 255)
|
|
1391
|
-
return rl.Color(255, 255, 255, int(255 * 0.8))
|
|
1392
|
-
|
|
1393
|
-
alpha = 0.2
|
|
1394
|
-
if idx < step:
|
|
1395
|
-
alpha = 0.4
|
|
1396
|
-
elif idx == step:
|
|
1397
|
-
alpha = 1.0
|
|
1398
|
-
if final:
|
|
1399
|
-
alpha *= highlight_alpha
|
|
1400
|
-
rgb = (255, 255, 255)
|
|
1401
|
-
if idx == step:
|
|
1402
|
-
rgb = (25, 200, 25)
|
|
1403
|
-
return rl.Color(rgb[0], rgb[1], rgb[2], int(255 * max(0.0, min(1.0, alpha))))
|
|
1404
|
-
|
|
1405
|
-
title = f"Quest {outcome.level} completed"
|
|
1406
|
-
subtitle = self._quest_title
|
|
1407
|
-
rl.draw_text(title, 32, 120, 28, rl.Color(235, 235, 235, 255))
|
|
1408
|
-
if subtitle:
|
|
1409
|
-
rl.draw_text(subtitle, 32, 154, 18, rl.Color(190, 190, 200, 255))
|
|
1410
|
-
|
|
1411
|
-
font = self._ensure_small_font()
|
|
1412
|
-
text_color = rl.Color(255, 255, 255, int(255 * 0.8))
|
|
1413
|
-
y = 196.0
|
|
1414
|
-
draw_small_text(font, f"Base time: {_fmt_clock(base_time_ms)}", 32.0, y, 1.0, _breakdown_color(0))
|
|
1415
|
-
y += 18.0
|
|
1416
|
-
draw_small_text(font, f"Life bonus: {_fmt_bonus(life_bonus_ms)}", 32.0, y, 1.0, _breakdown_color(1))
|
|
1417
|
-
y += 18.0
|
|
1418
|
-
draw_small_text(font, f"Perk bonus: {_fmt_bonus(perk_bonus_ms)}", 32.0, y, 1.0, _breakdown_color(2))
|
|
1419
|
-
y += 18.0
|
|
1420
|
-
draw_small_text(font, f"Final time: {_fmt_clock(final_time_ms)}", 32.0, y, 1.0, _breakdown_color(3, final=True))
|
|
1421
|
-
y += 26.0
|
|
1422
|
-
draw_small_text(font, f"Kills: {int(record.creature_kill_count)}", 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.8)))
|
|
1423
|
-
y += 18.0
|
|
1424
|
-
draw_small_text(font, f"XP: {int(record.score_xp)}", 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.8)))
|
|
1425
|
-
if self._rank_index is not None and self._rank_index < 100:
|
|
1426
|
-
y += 18.0
|
|
1427
|
-
draw_small_text(font, f"Rank: {int(self._rank_index) + 1}", 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.8)))
|
|
1428
|
-
|
|
1429
|
-
if self._unlock_weapon_name:
|
|
1430
|
-
y += 26.0
|
|
1431
|
-
draw_small_text(font, "Weapon unlocked", 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.7)))
|
|
1432
|
-
y += 16.0
|
|
1433
|
-
draw_small_text(font, self._unlock_weapon_name, 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.9)))
|
|
1434
|
-
|
|
1435
|
-
if self._unlock_perk_name:
|
|
1436
|
-
y += 20.0
|
|
1437
|
-
draw_small_text(font, "Perk unlocked", 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.7)))
|
|
1438
|
-
y += 16.0
|
|
1439
|
-
draw_small_text(font, self._unlock_perk_name, 32.0, y, 1.0, rl.Color(255, 255, 255, int(255 * 0.9)))
|
|
1440
|
-
|
|
1441
|
-
tex = self._button_tex
|
|
1442
|
-
y0 = 0.0
|
|
1443
|
-
if tex is not None:
|
|
1444
|
-
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1445
|
-
button_w = float(tex.width) * scale
|
|
1446
|
-
button_h = float(tex.height) * scale
|
|
1447
|
-
gap_x = 18.0 * scale
|
|
1448
|
-
gap_y = 12.0 * scale
|
|
1449
|
-
x0 = 32.0
|
|
1450
|
-
y0 = float(rl.get_screen_height()) - (button_h * 2.0 + gap_y) - 52.0 * scale
|
|
1451
|
-
x1 = x0 + button_w + gap_x
|
|
1452
|
-
y1 = y0 + button_h + gap_y
|
|
1453
|
-
|
|
1454
|
-
buttons = [
|
|
1455
|
-
("Play again", rl.Rectangle(x0, y0, button_w, button_h)),
|
|
1456
|
-
("Play next", rl.Rectangle(x1, y0, button_w, button_h)),
|
|
1457
|
-
("High scores", rl.Rectangle(x0, y1, button_w, button_h)),
|
|
1458
|
-
("Main menu", rl.Rectangle(x1, y1, button_w, button_h)),
|
|
1459
|
-
]
|
|
1460
|
-
mouse = rl.get_mouse_position()
|
|
1461
|
-
for label, rect in buttons:
|
|
1462
|
-
hovered = rect.x <= mouse.x <= rect.x + rect.width and rect.y <= mouse.y <= rect.y + rect.height
|
|
1463
|
-
alpha = 255 if hovered else 220
|
|
1464
|
-
rl.draw_texture_pro(
|
|
1465
|
-
tex,
|
|
1466
|
-
rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height)),
|
|
1467
|
-
rect,
|
|
1468
|
-
rl.Vector2(0.0, 0.0),
|
|
1469
|
-
0.0,
|
|
1470
|
-
rl.Color(255, 255, 255, alpha),
|
|
1471
|
-
)
|
|
1472
|
-
label_w = measure_small_text_width(font, label, 1.0 * scale)
|
|
1473
|
-
text_x = rect.x + (rect.width - label_w) * 0.5 + 1.0 * scale
|
|
1474
|
-
text_y = rect.y + 10.0 * scale
|
|
1475
|
-
draw_small_text(font, label, text_x, text_y, 1.0 * scale, rl.Color(20, 20, 20, 255))
|
|
1476
|
-
|
|
1477
|
-
if anim is not None and not anim.done:
|
|
1478
|
-
draw_small_text(
|
|
1479
|
-
font,
|
|
1480
|
-
"SPACE / click: skip breakdown",
|
|
1481
|
-
32.0,
|
|
1482
|
-
float(rl.get_screen_height()) - 46.0,
|
|
1483
|
-
0.9,
|
|
1484
|
-
rl.Color(190, 190, 200, 255),
|
|
1485
|
-
)
|
|
1486
|
-
|
|
1487
|
-
draw_small_text(
|
|
1488
|
-
font,
|
|
1489
|
-
"ENTER: Replay N: Next H: High scores ESC: Menu",
|
|
1490
|
-
32.0,
|
|
1491
|
-
float(rl.get_screen_height()) - 28.0,
|
|
1492
|
-
1.0,
|
|
1493
|
-
rl.Color(190, 190, 200, 255),
|
|
1494
|
-
)
|
|
1495
|
-
_draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
|
|
1318
|
+
rl.draw_text("Quest results unavailable.", 32, 140, 28, rl.Color(235, 235, 235, 255))
|
|
1319
|
+
rl.draw_text("Press ESC to return to the menu.", 32, 180, 18, rl.Color(190, 190, 200, 255))
|
|
1496
1320
|
|
|
1497
1321
|
def take_action(self) -> str | None:
|
|
1498
1322
|
action = self._action
|
|
@@ -1500,21 +1324,17 @@ class QuestResultsView:
|
|
|
1500
1324
|
return action
|
|
1501
1325
|
|
|
1502
1326
|
def _open_high_scores_list(self) -> None:
|
|
1327
|
+
highlight_rank = None
|
|
1328
|
+
if self._ui is not None:
|
|
1329
|
+
highlight_rank = self._ui.highlight_rank
|
|
1503
1330
|
self._state.pending_high_scores = HighScoresRequest(
|
|
1504
1331
|
game_mode_id=3,
|
|
1505
1332
|
quest_stage_major=int(self._quest_stage_major),
|
|
1506
1333
|
quest_stage_minor=int(self._quest_stage_minor),
|
|
1507
|
-
highlight_rank=
|
|
1334
|
+
highlight_rank=highlight_rank,
|
|
1508
1335
|
)
|
|
1509
1336
|
self._action = "open_high_scores"
|
|
1510
1337
|
|
|
1511
|
-
def _ensure_small_font(self) -> SmallFontData:
|
|
1512
|
-
if self._small_font is not None:
|
|
1513
|
-
return self._small_font
|
|
1514
|
-
missing_assets: list[str] = []
|
|
1515
|
-
self._small_font = load_small_font(self._state.assets_dir, missing_assets)
|
|
1516
|
-
return self._small_font
|
|
1517
|
-
|
|
1518
1338
|
|
|
1519
1339
|
class QuestFailedView:
|
|
1520
1340
|
def __init__(self, state: GameState) -> None:
|
|
@@ -1529,13 +1349,17 @@ class QuestFailedView:
|
|
|
1529
1349
|
|
|
1530
1350
|
def open(self) -> None:
|
|
1531
1351
|
self._action = None
|
|
1532
|
-
self._ground = ensure_menu_ground(self._state)
|
|
1352
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
1533
1353
|
self._cursor_pulse_time = 0.0
|
|
1534
1354
|
self._outcome = self._state.quest_outcome
|
|
1535
1355
|
self._state.quest_outcome = None
|
|
1536
1356
|
self._quest_title = ""
|
|
1537
1357
|
self._small_font = None
|
|
1538
1358
|
self._button_tex = None
|
|
1359
|
+
self._button_textures = None
|
|
1360
|
+
self._retry_button = UiButtonState("Retry", force_wide=True)
|
|
1361
|
+
self._quest_list_button = UiButtonState("Quest list", force_wide=True)
|
|
1362
|
+
self._main_menu_button = UiButtonState("Main menu", force_wide=True)
|
|
1539
1363
|
outcome = self._outcome
|
|
1540
1364
|
if outcome is not None:
|
|
1541
1365
|
try:
|
|
@@ -1547,7 +1371,9 @@ class QuestFailedView:
|
|
|
1547
1371
|
self._quest_title = ""
|
|
1548
1372
|
|
|
1549
1373
|
cache = _ensure_texture_cache(self._state)
|
|
1550
|
-
self._button_tex = cache.get_or_load("
|
|
1374
|
+
self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
1375
|
+
button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
1376
|
+
self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=self._button_tex)
|
|
1551
1377
|
|
|
1552
1378
|
def close(self) -> None:
|
|
1553
1379
|
self._ground = None
|
|
@@ -1555,6 +1381,7 @@ class QuestFailedView:
|
|
|
1555
1381
|
self._quest_title = ""
|
|
1556
1382
|
self._small_font = None
|
|
1557
1383
|
self._button_tex = None
|
|
1384
|
+
self._button_textures = None
|
|
1558
1385
|
|
|
1559
1386
|
def update(self, dt: float) -> None:
|
|
1560
1387
|
if self._state.audio is not None:
|
|
@@ -1578,44 +1405,56 @@ class QuestFailedView:
|
|
|
1578
1405
|
self._action = "open_quests"
|
|
1579
1406
|
return
|
|
1580
1407
|
|
|
1581
|
-
|
|
1582
|
-
if
|
|
1408
|
+
textures = self._button_textures
|
|
1409
|
+
if outcome is None or textures is None or (textures.button_sm is None and textures.button_md is None):
|
|
1583
1410
|
return
|
|
1584
1411
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1585
|
-
button_w =
|
|
1586
|
-
button_h =
|
|
1412
|
+
button_w = button_width(None, self._retry_button.label, scale=scale, force_wide=self._retry_button.force_wide)
|
|
1413
|
+
button_h = 32.0 * scale
|
|
1587
1414
|
gap_x = 18.0 * scale
|
|
1588
1415
|
x0 = 32.0
|
|
1589
1416
|
y0 = float(rl.get_screen_height()) - button_h - 56.0 * scale
|
|
1590
1417
|
|
|
1591
|
-
buttons = [
|
|
1592
|
-
("Retry", rl.Rectangle(x0, y0, button_w, button_h), "retry"),
|
|
1593
|
-
("Quest list", rl.Rectangle(x0 + button_w + gap_x, y0, button_w, button_h), "quest_list"),
|
|
1594
|
-
("Main menu", rl.Rectangle(x0 + (button_w + gap_x) * 2.0, y0, button_w, button_h), "main_menu"),
|
|
1595
|
-
]
|
|
1596
1418
|
mouse = rl.get_mouse_position()
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1419
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
1420
|
+
dt_ms = min(float(dt), 0.1) * 1000.0
|
|
1421
|
+
|
|
1422
|
+
if button_update(self._retry_button, x=x0, y=y0, width=button_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
1423
|
+
self._state.quest_fail_retry_count = int(self._state.quest_fail_retry_count) + 1
|
|
1424
|
+
self._state.pending_quest_level = outcome.level
|
|
1425
|
+
self._action = "start_quest"
|
|
1426
|
+
return
|
|
1427
|
+
if button_update(
|
|
1428
|
+
self._quest_list_button,
|
|
1429
|
+
x=x0 + button_w + gap_x,
|
|
1430
|
+
y=y0,
|
|
1431
|
+
width=button_w,
|
|
1432
|
+
dt_ms=dt_ms,
|
|
1433
|
+
mouse=mouse,
|
|
1434
|
+
click=click,
|
|
1435
|
+
):
|
|
1436
|
+
self._state.quest_fail_retry_count = 0
|
|
1437
|
+
self._action = "open_quests"
|
|
1438
|
+
return
|
|
1439
|
+
if button_update(
|
|
1440
|
+
self._main_menu_button,
|
|
1441
|
+
x=x0 + (button_w + gap_x) * 2.0,
|
|
1442
|
+
y=y0,
|
|
1443
|
+
width=button_w,
|
|
1444
|
+
dt_ms=dt_ms,
|
|
1445
|
+
mouse=mouse,
|
|
1446
|
+
click=click,
|
|
1447
|
+
):
|
|
1448
|
+
self._state.quest_fail_retry_count = 0
|
|
1449
|
+
self._action = "back_to_menu"
|
|
1450
|
+
return
|
|
1615
1451
|
|
|
1616
1452
|
def draw(self) -> None:
|
|
1617
1453
|
rl.clear_background(rl.BLACK)
|
|
1618
|
-
|
|
1454
|
+
pause_background = self._state.pause_background
|
|
1455
|
+
if pause_background is not None:
|
|
1456
|
+
pause_background.draw_pause_background()
|
|
1457
|
+
elif self._ground is not None:
|
|
1619
1458
|
self._ground.draw(0.0, 0.0)
|
|
1620
1459
|
_draw_screen_fade(self._state)
|
|
1621
1460
|
|
|
@@ -1655,36 +1494,33 @@ class QuestFailedView:
|
|
|
1655
1494
|
y += 18.0
|
|
1656
1495
|
draw_small_text(font, f"XP: {int(outcome.experience)}", 32.0, y, 1.0, text_color)
|
|
1657
1496
|
|
|
1658
|
-
|
|
1659
|
-
if
|
|
1497
|
+
textures = self._button_textures
|
|
1498
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
1660
1499
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1661
|
-
button_w =
|
|
1662
|
-
button_h =
|
|
1500
|
+
button_w = button_width(None, self._retry_button.label, scale=scale, force_wide=self._retry_button.force_wide)
|
|
1501
|
+
button_h = 32.0 * scale
|
|
1663
1502
|
gap_x = 18.0 * scale
|
|
1664
1503
|
x0 = 32.0
|
|
1665
1504
|
y0 = float(rl.get_screen_height()) - button_h - 56.0 * scale
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
text_x = rect.x + (rect.width - label_w) * 0.5 + 1.0 * scale
|
|
1686
|
-
text_y = rect.y + 10.0 * scale
|
|
1687
|
-
draw_small_text(font, label, text_x, text_y, 1.0 * scale, rl.Color(20, 20, 20, 255))
|
|
1505
|
+
button_draw(textures, font, self._retry_button, x=x0, y=y0, width=button_w, scale=scale)
|
|
1506
|
+
button_draw(
|
|
1507
|
+
textures,
|
|
1508
|
+
font,
|
|
1509
|
+
self._quest_list_button,
|
|
1510
|
+
x=x0 + button_w + gap_x,
|
|
1511
|
+
y=y0,
|
|
1512
|
+
width=button_w,
|
|
1513
|
+
scale=scale,
|
|
1514
|
+
)
|
|
1515
|
+
button_draw(
|
|
1516
|
+
textures,
|
|
1517
|
+
font,
|
|
1518
|
+
self._main_menu_button,
|
|
1519
|
+
x=x0 + (button_w + gap_x) * 2.0,
|
|
1520
|
+
y=y0,
|
|
1521
|
+
width=button_w,
|
|
1522
|
+
scale=scale,
|
|
1523
|
+
)
|
|
1688
1524
|
|
|
1689
1525
|
draw_small_text(
|
|
1690
1526
|
font,
|
|
@@ -1712,11 +1548,19 @@ class QuestFailedView:
|
|
|
1712
1548
|
class HighScoresView:
|
|
1713
1549
|
def __init__(self, state: GameState) -> None:
|
|
1714
1550
|
self._state = state
|
|
1551
|
+
self._assets: MenuAssets | None = None
|
|
1715
1552
|
self._ground: GroundRenderer | None = None
|
|
1716
1553
|
self._action: str | None = None
|
|
1717
1554
|
self._cursor_pulse_time = 0.0
|
|
1555
|
+
self._widescreen_y_shift = 0.0
|
|
1556
|
+
self._timeline_ms = 0
|
|
1557
|
+
self._timeline_max_ms = PANEL_TIMELINE_START_MS
|
|
1718
1558
|
self._small_font: SmallFontData | None = None
|
|
1719
1559
|
self._button_tex: rl.Texture2D | None = None
|
|
1560
|
+
self._button_textures: UiButtonTextureSet | None = None
|
|
1561
|
+
self._update_button = UiButtonState("Update scores", force_wide=True)
|
|
1562
|
+
self._play_button = UiButtonState("Play a game", force_wide=True)
|
|
1563
|
+
self._back_button = UiButtonState("Back", force_wide=False)
|
|
1720
1564
|
|
|
1721
1565
|
self._request: HighScoresRequest | None = None
|
|
1722
1566
|
self._records: list = []
|
|
@@ -1725,14 +1569,25 @@ class HighScoresView:
|
|
|
1725
1569
|
def open(self) -> None:
|
|
1726
1570
|
from .persistence.highscores import read_highscore_table, scores_path_for_mode
|
|
1727
1571
|
|
|
1572
|
+
layout_w = float(self._state.config.screen_width)
|
|
1573
|
+
self._widescreen_y_shift = MenuView._menu_widescreen_y_shift(layout_w)
|
|
1728
1574
|
self._action = None
|
|
1729
|
-
self.
|
|
1575
|
+
self._assets = load_menu_assets(self._state)
|
|
1576
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
1730
1577
|
self._cursor_pulse_time = 0.0
|
|
1578
|
+
self._timeline_ms = 0
|
|
1579
|
+
self._timeline_max_ms = PANEL_TIMELINE_START_MS
|
|
1731
1580
|
self._small_font = None
|
|
1732
1581
|
self._scroll_index = 0
|
|
1582
|
+
self._button_textures = None
|
|
1583
|
+
self._update_button = UiButtonState("Update scores", force_wide=True)
|
|
1584
|
+
self._play_button = UiButtonState("Play a game", force_wide=True)
|
|
1585
|
+
self._back_button = UiButtonState("Back", force_wide=False)
|
|
1733
1586
|
|
|
1734
1587
|
cache = _ensure_texture_cache(self._state)
|
|
1735
|
-
self._button_tex = cache.get_or_load("
|
|
1588
|
+
self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
1589
|
+
button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
1590
|
+
self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=self._button_tex)
|
|
1736
1591
|
|
|
1737
1592
|
request = self._state.pending_high_scores
|
|
1738
1593
|
self._state.pending_high_scores = None
|
|
@@ -1768,11 +1623,18 @@ class HighScoresView:
|
|
|
1768
1623
|
if self._small_font is not None:
|
|
1769
1624
|
rl.unload_texture(self._small_font.texture)
|
|
1770
1625
|
self._small_font = None
|
|
1626
|
+
self._assets = None
|
|
1771
1627
|
self._button_tex = None
|
|
1628
|
+
self._button_textures = None
|
|
1772
1629
|
self._request = None
|
|
1773
1630
|
self._records = []
|
|
1774
1631
|
self._scroll_index = 0
|
|
1775
1632
|
|
|
1633
|
+
def _panel_top_left(self, *, pos_x: float, pos_y: float, scale: float) -> tuple[float, float]:
|
|
1634
|
+
x0 = float(pos_x + MENU_PANEL_OFFSET_X * scale)
|
|
1635
|
+
y0 = float(pos_y + self._widescreen_y_shift + MENU_PANEL_OFFSET_Y * scale)
|
|
1636
|
+
return x0, y0
|
|
1637
|
+
|
|
1776
1638
|
def update(self, dt: float) -> None:
|
|
1777
1639
|
if self._state.audio is not None:
|
|
1778
1640
|
update_audio(self._state.audio, dt)
|
|
@@ -1780,62 +1642,97 @@ class HighScoresView:
|
|
|
1780
1642
|
self._ground.process_pending()
|
|
1781
1643
|
self._cursor_pulse_time += min(dt, 0.1) * 1.1
|
|
1782
1644
|
|
|
1783
|
-
|
|
1645
|
+
dt_ms = int(min(float(dt), 0.1) * 1000.0)
|
|
1646
|
+
if dt_ms > 0:
|
|
1647
|
+
self._timeline_ms = min(self._timeline_max_ms, int(self._timeline_ms + dt_ms))
|
|
1648
|
+
|
|
1649
|
+
enabled = self._timeline_ms >= self._timeline_max_ms
|
|
1650
|
+
|
|
1651
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE) and enabled:
|
|
1784
1652
|
if self._state.audio is not None:
|
|
1785
1653
|
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
1786
1654
|
self._action = "back_to_previous"
|
|
1787
1655
|
return
|
|
1788
1656
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
tex = self._button_tex
|
|
1792
|
-
if tex is not None and clicked:
|
|
1657
|
+
textures = self._button_textures
|
|
1658
|
+
if enabled and textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
1793
1659
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
if
|
|
1660
|
+
panel_x0, panel_y0 = self._panel_top_left(pos_x=HS_LEFT_PANEL_POS_X, pos_y=HS_LEFT_PANEL_POS_Y, scale=scale)
|
|
1661
|
+
|
|
1662
|
+
x0 = panel_x0 + HS_BUTTON_X * scale
|
|
1663
|
+
y0 = panel_y0 + HS_BUTTON_Y0 * scale
|
|
1664
|
+
mouse = rl.get_mouse_position()
|
|
1665
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
1666
|
+
w = button_width(None, self._update_button.label, scale=scale, force_wide=self._update_button.force_wide)
|
|
1667
|
+
if button_update(self._update_button, x=x0, y=y0, width=w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
1668
|
+
# Reload scores from disk (no view transition).
|
|
1802
1669
|
if self._state.audio is not None:
|
|
1803
1670
|
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
1804
|
-
self.
|
|
1671
|
+
self.open()
|
|
1805
1672
|
return
|
|
1806
|
-
|
|
1673
|
+
w = button_width(None, self._play_button.label, scale=scale, force_wide=self._play_button.force_wide)
|
|
1674
|
+
if button_update(
|
|
1675
|
+
self._play_button,
|
|
1676
|
+
x=x0,
|
|
1677
|
+
y=y0 + HS_BUTTON_STEP_Y * scale,
|
|
1678
|
+
width=w,
|
|
1679
|
+
dt_ms=dt_ms,
|
|
1680
|
+
mouse=mouse,
|
|
1681
|
+
click=click,
|
|
1682
|
+
):
|
|
1807
1683
|
if self._state.audio is not None:
|
|
1808
1684
|
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
1809
|
-
self._action = "
|
|
1685
|
+
self._action = "open_play_game"
|
|
1686
|
+
return
|
|
1687
|
+
back_w = button_width(None, self._back_button.label, scale=scale, force_wide=self._back_button.force_wide)
|
|
1688
|
+
if button_update(
|
|
1689
|
+
self._back_button,
|
|
1690
|
+
x=panel_x0 + HS_BACK_BUTTON_X * scale,
|
|
1691
|
+
y=panel_y0 + HS_BACK_BUTTON_Y * scale,
|
|
1692
|
+
width=back_w,
|
|
1693
|
+
dt_ms=dt_ms,
|
|
1694
|
+
mouse=mouse,
|
|
1695
|
+
click=click,
|
|
1696
|
+
):
|
|
1697
|
+
if self._state.audio is not None:
|
|
1698
|
+
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
1699
|
+
self._action = "back_to_previous"
|
|
1810
1700
|
return
|
|
1811
1701
|
|
|
1812
|
-
|
|
1813
|
-
rows = self._visible_rows(font)
|
|
1702
|
+
rows = 10
|
|
1814
1703
|
max_scroll = max(0, len(self._records) - rows)
|
|
1815
1704
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1705
|
+
if enabled:
|
|
1706
|
+
wheel = int(rl.get_mouse_wheel_move())
|
|
1707
|
+
if wheel:
|
|
1708
|
+
self._scroll_index = max(0, min(max_scroll, int(self._scroll_index) - wheel))
|
|
1709
|
+
|
|
1710
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
|
|
1711
|
+
self._scroll_index = max(0, int(self._scroll_index) - 1)
|
|
1712
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
|
|
1713
|
+
self._scroll_index = min(max_scroll, int(self._scroll_index) + 1)
|
|
1714
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
|
|
1715
|
+
self._scroll_index = max(0, int(self._scroll_index) - rows)
|
|
1716
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
|
|
1717
|
+
self._scroll_index = min(max_scroll, int(self._scroll_index) + rows)
|
|
1718
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
|
|
1719
|
+
self._scroll_index = 0
|
|
1720
|
+
if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
|
|
1721
|
+
self._scroll_index = max_scroll
|
|
1832
1722
|
|
|
1833
1723
|
def draw(self) -> None:
|
|
1834
1724
|
rl.clear_background(rl.BLACK)
|
|
1835
|
-
|
|
1725
|
+
pause_background = self._state.pause_background
|
|
1726
|
+
if pause_background is not None:
|
|
1727
|
+
pause_background.draw_pause_background()
|
|
1728
|
+
elif self._ground is not None:
|
|
1836
1729
|
self._ground.draw(0.0, 0.0)
|
|
1837
1730
|
_draw_screen_fade(self._state)
|
|
1838
1731
|
|
|
1732
|
+
assets = self._assets
|
|
1733
|
+
if assets is None or assets.panel is None:
|
|
1734
|
+
return
|
|
1735
|
+
|
|
1839
1736
|
font = self._ensure_small_font()
|
|
1840
1737
|
request = self._request
|
|
1841
1738
|
mode_id = int(request.game_mode_id) if request is not None else int(self._state.config.data.get("game_mode", 1) or 1)
|
|
@@ -1843,26 +1740,64 @@ class HighScoresView:
|
|
|
1843
1740
|
quest_minor = int(request.quest_stage_minor) if request is not None else 0
|
|
1844
1741
|
highlight_rank = request.highlight_rank if request is not None else None
|
|
1845
1742
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1743
|
+
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1744
|
+
fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
|
|
1745
|
+
panel_w = MENU_PANEL_WIDTH * scale
|
|
1746
|
+
_angle_rad, left_slide_x = MenuView._ui_element_anim(
|
|
1747
|
+
self,
|
|
1748
|
+
index=1,
|
|
1749
|
+
start_ms=PANEL_TIMELINE_START_MS,
|
|
1750
|
+
end_ms=PANEL_TIMELINE_END_MS,
|
|
1751
|
+
width=panel_w,
|
|
1752
|
+
direction_flag=0,
|
|
1753
|
+
)
|
|
1754
|
+
_angle_rad, right_slide_x = MenuView._ui_element_anim(
|
|
1755
|
+
self,
|
|
1756
|
+
index=2,
|
|
1757
|
+
start_ms=PANEL_TIMELINE_START_MS,
|
|
1758
|
+
end_ms=PANEL_TIMELINE_END_MS,
|
|
1759
|
+
width=panel_w,
|
|
1760
|
+
direction_flag=1,
|
|
1761
|
+
)
|
|
1762
|
+
|
|
1763
|
+
left_x0, left_y0 = self._panel_top_left(pos_x=HS_LEFT_PANEL_POS_X, pos_y=HS_LEFT_PANEL_POS_Y, scale=scale)
|
|
1764
|
+
right_x0, right_y0 = self._panel_top_left(pos_x=HS_RIGHT_PANEL_POS_X, pos_y=HS_RIGHT_PANEL_POS_Y, scale=scale)
|
|
1765
|
+
left_x0 += float(left_slide_x)
|
|
1766
|
+
right_x0 += float(right_slide_x)
|
|
1767
|
+
|
|
1768
|
+
draw_classic_menu_panel(
|
|
1769
|
+
assets.panel,
|
|
1770
|
+
dst=rl.Rectangle(left_x0, left_y0, panel_w, HS_LEFT_PANEL_HEIGHT * scale),
|
|
1771
|
+
tint=rl.WHITE,
|
|
1772
|
+
shadow=fx_detail,
|
|
1773
|
+
)
|
|
1774
|
+
draw_classic_menu_panel(
|
|
1775
|
+
assets.panel,
|
|
1776
|
+
dst=rl.Rectangle(right_x0, right_y0, panel_w, HS_RIGHT_PANEL_HEIGHT * scale),
|
|
1777
|
+
tint=rl.WHITE,
|
|
1778
|
+
shadow=fx_detail,
|
|
1779
|
+
)
|
|
1780
|
+
|
|
1781
|
+
title = "High scores - Quests" if int(mode_id) == 3 else f"High scores - {self._mode_label(mode_id, quest_major, quest_minor)}"
|
|
1782
|
+
draw_small_text(font, title, left_x0 + 269.0 * scale, left_y0 + 41.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
|
|
1783
|
+
if int(mode_id) == 3:
|
|
1784
|
+
quest_label = f"{int(quest_major)}.{int(quest_minor)}: {self._quest_title(quest_major, quest_minor)}"
|
|
1785
|
+
draw_small_text(font, quest_label, left_x0 + 236.0 * scale, left_y0 + 63.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
|
|
1850
1786
|
|
|
1851
1787
|
header_color = rl.Color(255, 255, 255, int(255 * 0.85))
|
|
1852
|
-
row_y0 =
|
|
1853
|
-
draw_small_text(font, "Rank",
|
|
1854
|
-
draw_small_text(font, "
|
|
1855
|
-
|
|
1856
|
-
draw_small_text(font, score_label, 320.0, row_y0, 1.0, header_color)
|
|
1788
|
+
row_y0 = left_y0 + 84.0 * scale
|
|
1789
|
+
draw_small_text(font, "Rank", left_x0 + 211.0 * scale, row_y0, 1.0 * scale, header_color)
|
|
1790
|
+
draw_small_text(font, "Score", left_x0 + 246.0 * scale, row_y0, 1.0 * scale, header_color)
|
|
1791
|
+
draw_small_text(font, "Player", left_x0 + 302.0 * scale, row_y0, 1.0 * scale, header_color)
|
|
1857
1792
|
|
|
1858
|
-
row_step =
|
|
1859
|
-
rows =
|
|
1793
|
+
row_step = 16.0 * scale
|
|
1794
|
+
rows = 10
|
|
1860
1795
|
start = max(0, int(self._scroll_index))
|
|
1861
1796
|
end = min(len(self._records), start + rows)
|
|
1862
|
-
y =
|
|
1797
|
+
y = left_y0 + 103.0 * scale
|
|
1863
1798
|
|
|
1864
1799
|
if start >= end:
|
|
1865
|
-
draw_small_text(font, "No scores yet.",
|
|
1800
|
+
draw_small_text(font, "No scores yet.", left_x0 + 211.0 * scale, y + 8.0 * scale, 1.0 * scale, rl.Color(190, 190, 200, 255))
|
|
1866
1801
|
else:
|
|
1867
1802
|
for idx in range(start, end):
|
|
1868
1803
|
entry = self._records[idx]
|
|
@@ -1876,62 +1811,81 @@ class HighScoresView:
|
|
|
1876
1811
|
if len(name) > 16:
|
|
1877
1812
|
name = name[:16]
|
|
1878
1813
|
|
|
1879
|
-
value = ""
|
|
1880
|
-
if mode_id in (2, 3):
|
|
1881
|
-
seconds = float(int(getattr(entry, "survival_elapsed_ms", 0))) * 0.001
|
|
1882
|
-
value = f"{seconds:7.2f}s"
|
|
1883
|
-
else:
|
|
1884
|
-
value = f"{int(getattr(entry, 'score_xp', 0)):7d}"
|
|
1814
|
+
value = f"{int(getattr(entry, 'score_xp', 0))}"
|
|
1885
1815
|
|
|
1886
1816
|
color = rl.Color(255, 255, 255, int(255 * 0.7))
|
|
1887
1817
|
if highlight_rank is not None and int(highlight_rank) == idx:
|
|
1888
1818
|
color = rl.Color(255, 255, 255, 255)
|
|
1889
1819
|
|
|
1890
|
-
draw_small_text(font, f"{idx + 1
|
|
1891
|
-
draw_small_text(font,
|
|
1892
|
-
draw_small_text(font,
|
|
1820
|
+
draw_small_text(font, f"{idx + 1}", left_x0 + 216.0 * scale, y, 1.0 * scale, color)
|
|
1821
|
+
draw_small_text(font, value, left_x0 + 246.0 * scale, y, 1.0 * scale, color)
|
|
1822
|
+
draw_small_text(font, name, left_x0 + 304.0 * scale, y, 1.0 * scale, color)
|
|
1893
1823
|
y += row_step
|
|
1894
1824
|
|
|
1895
|
-
|
|
1896
|
-
if
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1825
|
+
textures = self._button_textures
|
|
1826
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
1827
|
+
button_x = left_x0 + HS_BUTTON_X * scale
|
|
1828
|
+
button_y0 = left_y0 + HS_BUTTON_Y0 * scale
|
|
1829
|
+
w = button_width(None, self._update_button.label, scale=scale, force_wide=self._update_button.force_wide)
|
|
1830
|
+
button_draw(textures, font, self._update_button, x=button_x, y=button_y0, width=w, scale=scale)
|
|
1831
|
+
w = button_width(None, self._play_button.label, scale=scale, force_wide=self._play_button.force_wide)
|
|
1832
|
+
button_draw(textures, font, self._play_button, x=button_x, y=button_y0 + HS_BUTTON_STEP_Y * scale, width=w, scale=scale)
|
|
1833
|
+
w = button_width(None, self._back_button.label, scale=scale, force_wide=self._back_button.force_wide)
|
|
1834
|
+
button_draw(
|
|
1835
|
+
textures,
|
|
1836
|
+
font,
|
|
1837
|
+
self._back_button,
|
|
1838
|
+
x=left_x0 + HS_BACK_BUTTON_X * scale,
|
|
1839
|
+
y=left_y0 + HS_BACK_BUTTON_Y * scale,
|
|
1840
|
+
width=w,
|
|
1841
|
+
scale=scale,
|
|
1842
|
+
)
|
|
1904
1843
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
("Main menu", rl.Rectangle(x1, y0, button_w, button_h)),
|
|
1908
|
-
]
|
|
1909
|
-
mouse = rl.get_mouse_position()
|
|
1910
|
-
for label, rect in buttons:
|
|
1911
|
-
hovered = rect.x <= mouse.x <= rect.x + rect.width and rect.y <= mouse.y <= rect.y + rect.height
|
|
1912
|
-
alpha = 255 if hovered else 220
|
|
1913
|
-
rl.draw_texture_pro(
|
|
1914
|
-
tex,
|
|
1915
|
-
rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height)),
|
|
1916
|
-
rect,
|
|
1917
|
-
rl.Vector2(0.0, 0.0),
|
|
1918
|
-
0.0,
|
|
1919
|
-
rl.Color(255, 255, 255, alpha),
|
|
1920
|
-
)
|
|
1921
|
-
label_w = measure_small_text_width(font, label, 1.0 * scale)
|
|
1922
|
-
text_x = rect.x + (rect.width - label_w) * 0.5 + 1.0 * scale
|
|
1923
|
-
text_y = rect.y + 10.0 * scale
|
|
1924
|
-
draw_small_text(font, label, text_x, text_y, 1.0 * scale, rl.Color(20, 20, 20, 255))
|
|
1844
|
+
self._draw_sign(assets)
|
|
1845
|
+
_draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
|
|
1925
1846
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1847
|
+
def _draw_sign(self, assets: MenuAssets) -> None:
|
|
1848
|
+
if assets.sign is None:
|
|
1849
|
+
return
|
|
1850
|
+
sign = assets.sign
|
|
1851
|
+
screen_w = float(self._state.config.screen_width)
|
|
1852
|
+
sign_scale, shift_x = MenuView._sign_layout_scale(int(screen_w))
|
|
1853
|
+
pos_x = screen_w + MENU_SIGN_POS_X_PAD
|
|
1854
|
+
pos_y = MENU_SIGN_POS_Y if screen_w > MENU_SCALE_SMALL_THRESHOLD else MENU_SIGN_POS_Y_SMALL
|
|
1855
|
+
sign_w = MENU_SIGN_WIDTH * sign_scale
|
|
1856
|
+
sign_h = MENU_SIGN_HEIGHT * sign_scale
|
|
1857
|
+
offset_x = MENU_SIGN_OFFSET_X * sign_scale + shift_x
|
|
1858
|
+
offset_y = MENU_SIGN_OFFSET_Y * sign_scale
|
|
1859
|
+
rotation_deg = 0.0
|
|
1860
|
+
fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
|
|
1861
|
+
if fx_detail:
|
|
1862
|
+
MenuView._draw_ui_quad_shadow(
|
|
1863
|
+
texture=sign,
|
|
1864
|
+
src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
|
|
1865
|
+
dst=rl.Rectangle(pos_x + UI_SHADOW_OFFSET, pos_y + UI_SHADOW_OFFSET, sign_w, sign_h),
|
|
1866
|
+
origin=rl.Vector2(-offset_x, -offset_y),
|
|
1867
|
+
rotation_deg=rotation_deg,
|
|
1868
|
+
)
|
|
1869
|
+
MenuView._draw_ui_quad(
|
|
1870
|
+
texture=sign,
|
|
1871
|
+
src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
|
|
1872
|
+
dst=rl.Rectangle(pos_x, pos_y, sign_w, sign_h),
|
|
1873
|
+
origin=rl.Vector2(-offset_x, -offset_y),
|
|
1874
|
+
rotation_deg=rotation_deg,
|
|
1875
|
+
tint=rl.WHITE,
|
|
1933
1876
|
)
|
|
1934
|
-
|
|
1877
|
+
|
|
1878
|
+
@staticmethod
|
|
1879
|
+
def _quest_title(major: int, minor: int) -> str:
|
|
1880
|
+
try:
|
|
1881
|
+
from .quests import quest_by_level
|
|
1882
|
+
|
|
1883
|
+
q = quest_by_level(f"{int(major)}.{int(minor)}")
|
|
1884
|
+
if q is not None and q.title:
|
|
1885
|
+
return str(q.title)
|
|
1886
|
+
except Exception:
|
|
1887
|
+
pass
|
|
1888
|
+
return "???"
|
|
1935
1889
|
|
|
1936
1890
|
def take_action(self) -> str | None:
|
|
1937
1891
|
action = self._action
|
|
@@ -1986,6 +1940,7 @@ class GameLoopView:
|
|
|
1986
1940
|
self._front_views: dict[str, FrontView] = {
|
|
1987
1941
|
"open_play_game": PlayGameMenuView(state),
|
|
1988
1942
|
"open_quests": QuestsMenuView(state),
|
|
1943
|
+
"open_pause_menu": PauseMenuView(state),
|
|
1989
1944
|
"start_quest": QuestGameView(state),
|
|
1990
1945
|
"quest_results": QuestResultsView(state),
|
|
1991
1946
|
"quest_failed": QuestFailedView(state),
|
|
@@ -1997,6 +1952,9 @@ class GameLoopView:
|
|
|
1997
1952
|
"open_options": OptionsMenuView(state),
|
|
1998
1953
|
"open_controls": ControlsMenuView(state),
|
|
1999
1954
|
"open_statistics": StatisticsMenuView(state),
|
|
1955
|
+
"open_weapon_database": UnlockedWeaponsDatabaseView(state),
|
|
1956
|
+
"open_perk_database": UnlockedPerksDatabaseView(state),
|
|
1957
|
+
"open_credits": CreditsView(state),
|
|
2000
1958
|
"open_mods": ModsMenuView(state),
|
|
2001
1959
|
"open_other_games": PanelMenuView(
|
|
2002
1960
|
state,
|
|
@@ -2052,6 +2010,7 @@ class GameLoopView:
|
|
|
2052
2010
|
if self._front_active is not None:
|
|
2053
2011
|
action = self._front_active.take_action()
|
|
2054
2012
|
if action == "back_to_menu":
|
|
2013
|
+
self._state.pause_background = None
|
|
2055
2014
|
self._front_active.close()
|
|
2056
2015
|
self._front_active = None
|
|
2057
2016
|
while self._front_stack:
|
|
@@ -2064,14 +2023,44 @@ class GameLoopView:
|
|
|
2064
2023
|
if self._front_stack:
|
|
2065
2024
|
self._front_active.close()
|
|
2066
2025
|
self._front_active = self._front_stack.pop()
|
|
2026
|
+
if self._front_active in self._gameplay_views:
|
|
2027
|
+
self._state.pause_background = None
|
|
2067
2028
|
self._active = self._front_active
|
|
2068
2029
|
return
|
|
2069
2030
|
self._front_active.close()
|
|
2070
2031
|
self._front_active = None
|
|
2032
|
+
self._state.pause_background = None
|
|
2071
2033
|
self._menu.open()
|
|
2072
2034
|
self._active = self._menu
|
|
2073
2035
|
self._menu_active = True
|
|
2074
2036
|
return
|
|
2037
|
+
if action == "open_pause_menu":
|
|
2038
|
+
pause_view = self._front_views.get("open_pause_menu")
|
|
2039
|
+
if pause_view is None:
|
|
2040
|
+
return
|
|
2041
|
+
if self._front_active in self._gameplay_views:
|
|
2042
|
+
self._state.pause_background = self._front_active
|
|
2043
|
+
self._front_stack.append(self._front_active)
|
|
2044
|
+
pause_view.open()
|
|
2045
|
+
self._front_active = pause_view
|
|
2046
|
+
self._active = pause_view
|
|
2047
|
+
return
|
|
2048
|
+
if self._state.pause_background is None:
|
|
2049
|
+
# Options panel uses open_pause_menu as back_action; when no game is
|
|
2050
|
+
# running, treat it like back_to_menu.
|
|
2051
|
+
self._front_active.close()
|
|
2052
|
+
self._front_active = None
|
|
2053
|
+
while self._front_stack:
|
|
2054
|
+
self._front_stack.pop().close()
|
|
2055
|
+
self._menu.open()
|
|
2056
|
+
self._active = self._menu
|
|
2057
|
+
self._menu_active = True
|
|
2058
|
+
return
|
|
2059
|
+
self._front_active.close()
|
|
2060
|
+
pause_view.open()
|
|
2061
|
+
self._front_active = pause_view
|
|
2062
|
+
self._active = pause_view
|
|
2063
|
+
return
|
|
2075
2064
|
if action in {"start_survival", "start_rush", "start_typo"}:
|
|
2076
2065
|
# Temporary: bump the counter on mode start so the Play Game overlay (F1)
|
|
2077
2066
|
# and Statistics screen reflect activity.
|
|
@@ -2085,9 +2074,26 @@ class GameLoopView:
|
|
|
2085
2074
|
if action is not None:
|
|
2086
2075
|
view = self._front_views.get(action)
|
|
2087
2076
|
if view is not None:
|
|
2088
|
-
if action
|
|
2077
|
+
if action in {"open_high_scores", "open_weapon_database", "open_perk_database", "open_credits"}:
|
|
2078
|
+
if (self._front_active in self._gameplay_views) and (self._state.pause_background is None):
|
|
2079
|
+
self._state.pause_background = self._front_active
|
|
2080
|
+
self._front_stack.append(self._front_active)
|
|
2081
|
+
elif action in {"quest_results", "quest_failed"} and (self._front_active in self._gameplay_views):
|
|
2082
|
+
self._state.pause_background = self._front_active
|
|
2089
2083
|
self._front_stack.append(self._front_active)
|
|
2090
2084
|
else:
|
|
2085
|
+
if action in {
|
|
2086
|
+
"start_survival",
|
|
2087
|
+
"start_rush",
|
|
2088
|
+
"start_typo",
|
|
2089
|
+
"start_tutorial",
|
|
2090
|
+
"start_quest",
|
|
2091
|
+
"open_play_game",
|
|
2092
|
+
"open_quests",
|
|
2093
|
+
}:
|
|
2094
|
+
self._state.pause_background = None
|
|
2095
|
+
while self._front_stack:
|
|
2096
|
+
self._front_stack.pop().close()
|
|
2091
2097
|
self._front_active.close()
|
|
2092
2098
|
view.open()
|
|
2093
2099
|
self._front_active = view
|
|
@@ -2524,6 +2530,7 @@ def run_game(config: GameConfig) -> None:
|
|
|
2524
2530
|
title="Crimsonland",
|
|
2525
2531
|
fps=config.fps,
|
|
2526
2532
|
config_flags=config_flags,
|
|
2533
|
+
exit_key=rl.KeyboardKey.KEY_NULL,
|
|
2527
2534
|
)
|
|
2528
2535
|
if state is not None:
|
|
2529
2536
|
state.status.save_if_dirty()
|