crimsonland 0.1.0.dev10__py3-none-any.whl → 0.1.0.dev12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crimson/creatures/runtime.py +15 -0
- crimson/demo.py +47 -38
- crimson/effects.py +46 -1
- crimson/frontend/boot.py +2 -1
- crimson/frontend/menu.py +6 -27
- crimson/frontend/panels/base.py +13 -3
- crimson/frontend/panels/controls.py +1 -3
- crimson/frontend/panels/mods.py +1 -3
- crimson/frontend/panels/options.py +35 -42
- crimson/frontend/panels/play_game.py +78 -70
- crimson/frontend/panels/stats.py +1 -3
- crimson/frontend/pause_menu.py +425 -0
- crimson/game.py +315 -446
- crimson/gameplay.py +37 -8
- crimson/modes/base_gameplay_mode.py +3 -0
- crimson/modes/quest_mode.py +45 -36
- crimson/modes/rush_mode.py +4 -1
- crimson/modes/survival_mode.py +6 -2
- crimson/modes/tutorial_mode.py +6 -2
- crimson/modes/typo_mode.py +4 -1
- crimson/persistence/highscores.py +6 -2
- crimson/render/world_renderer.py +1 -1
- crimson/sim/world_state.py +8 -1
- crimson/typo/spawns.py +3 -4
- crimson/ui/demo_trial_overlay.py +3 -3
- crimson/ui/game_over.py +18 -2
- crimson/ui/perk_menu.py +106 -14
- crimson/ui/quest_results.py +663 -0
- crimson/ui/shadow.py +39 -0
- crimson/views/particles.py +1 -1
- crimson/weapons.py +110 -110
- {crimsonland-0.1.0.dev10.dist-info → crimsonland-0.1.0.dev12.dist-info}/METADATA +1 -1
- {crimsonland-0.1.0.dev10.dist-info → crimsonland-0.1.0.dev12.dist-info}/RECORD +36 -33
- grim/app.py +3 -0
- {crimsonland-0.1.0.dev10.dist-info → crimsonland-0.1.0.dev12.dist-info}/WHEEL +0 -0
- {crimsonland-0.1.0.dev10.dist-info → crimsonland-0.1.0.dev12.dist-info}/entry_points.txt +0 -0
crimson/game.py
CHANGED
|
@@ -76,9 +76,11 @@ from .frontend.panels.mods import ModsMenuView
|
|
|
76
76
|
from .frontend.panels.options import OptionsMenuView
|
|
77
77
|
from .frontend.panels.play_game import PlayGameMenuView
|
|
78
78
|
from .frontend.panels.stats import StatisticsMenuView
|
|
79
|
+
from .frontend.pause_menu import PauseMenuView
|
|
79
80
|
from .frontend.transitions import _draw_screen_fade, _update_screen_fade
|
|
80
81
|
from .persistence.save_status import GameStatus, ensure_game_status
|
|
81
82
|
from .ui.demo_trial_overlay import DEMO_PURCHASE_URL, DemoTrialOverlayUi
|
|
83
|
+
from .ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width, draw_menu_panel
|
|
82
84
|
from .paths import default_runtime_dir
|
|
83
85
|
from .assets_fetch import download_missing_paqs
|
|
84
86
|
|
|
@@ -124,6 +126,7 @@ class GameState:
|
|
|
124
126
|
snd_freq_adjustment_enabled: bool = False
|
|
125
127
|
menu_ground: GroundRenderer | None = None
|
|
126
128
|
menu_sign_locked: bool = False
|
|
129
|
+
pause_background: PauseBackground | None = None
|
|
127
130
|
pending_quest_level: str | None = None
|
|
128
131
|
pending_high_scores: HighScoresRequest | None = None
|
|
129
132
|
quest_outcome: QuestRunOutcome | None = None
|
|
@@ -166,8 +169,9 @@ QUEST_HARDCORE_CHECKBOX_X_OFFSET = 132.0
|
|
|
166
169
|
QUEST_HARDCORE_CHECKBOX_Y_OFFSET = -12.0
|
|
167
170
|
QUEST_HARDCORE_LIST_Y_SHIFT = 10.0
|
|
168
171
|
|
|
169
|
-
QUEST_BACK_BUTTON_X_OFFSET =
|
|
172
|
+
QUEST_BACK_BUTTON_X_OFFSET = 138.0
|
|
170
173
|
QUEST_BACK_BUTTON_Y_OFFSET = 212.0
|
|
174
|
+
QUEST_PANEL_HEIGHT = 379.0
|
|
171
175
|
|
|
172
176
|
|
|
173
177
|
class QuestsMenuView:
|
|
@@ -192,6 +196,8 @@ class QuestsMenuView:
|
|
|
192
196
|
self._check_off: rl.Texture2D | None = None
|
|
193
197
|
self._button_sm: rl.Texture2D | None = None
|
|
194
198
|
self._button_md: rl.Texture2D | None = None
|
|
199
|
+
self._button_textures: UiButtonTextureSet | None = None
|
|
200
|
+
self._back_button = UiButtonState("Back")
|
|
195
201
|
|
|
196
202
|
self._menu_screen_width = 0
|
|
197
203
|
self._widescreen_y_shift = 0.0
|
|
@@ -224,11 +230,13 @@ class QuestsMenuView:
|
|
|
224
230
|
self._check_off = cache.get_or_load("ui_checkOff", "ui/ui_checkOff.jaz").texture
|
|
225
231
|
self._button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
226
232
|
self._button_md = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
233
|
+
self._button_textures = UiButtonTextureSet(button_sm=self._button_sm, button_md=self._button_md)
|
|
227
234
|
|
|
228
235
|
self._action = None
|
|
229
236
|
self._dirty = False
|
|
230
237
|
self._stage = max(1, min(5, int(self._stage)))
|
|
231
238
|
self._cursor_pulse_time = 0.0
|
|
239
|
+
self._back_button = UiButtonState("Back")
|
|
232
240
|
|
|
233
241
|
# Ensure the quest registry is populated so titles render.
|
|
234
242
|
# (The package import registers all tier builders.)
|
|
@@ -247,6 +255,7 @@ class QuestsMenuView:
|
|
|
247
255
|
pass
|
|
248
256
|
self._dirty = False
|
|
249
257
|
self._ground = None
|
|
258
|
+
self._button_textures = None
|
|
250
259
|
|
|
251
260
|
def update(self, dt: float) -> None:
|
|
252
261
|
if self._state.audio is not None:
|
|
@@ -283,9 +292,26 @@ class QuestsMenuView:
|
|
|
283
292
|
if self._hardcore_checkbox_clicked(layout):
|
|
284
293
|
return
|
|
285
294
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
295
|
+
textures = self._button_textures
|
|
296
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
297
|
+
back_x = layout["list_x"] + QUEST_BACK_BUTTON_X_OFFSET
|
|
298
|
+
back_y = self._rows_y0(layout) + QUEST_BACK_BUTTON_Y_OFFSET
|
|
299
|
+
dt_ms = min(float(dt), 0.1) * 1000.0
|
|
300
|
+
font = self._ensure_small_font()
|
|
301
|
+
back_w = button_width(font, self._back_button.label, scale=1.0, force_wide=self._back_button.force_wide)
|
|
302
|
+
mouse = rl.get_mouse_position()
|
|
303
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
304
|
+
if button_update(
|
|
305
|
+
self._back_button,
|
|
306
|
+
x=float(back_x),
|
|
307
|
+
y=float(back_y),
|
|
308
|
+
width=float(back_w),
|
|
309
|
+
dt_ms=float(dt_ms),
|
|
310
|
+
mouse=mouse,
|
|
311
|
+
click=bool(click),
|
|
312
|
+
):
|
|
313
|
+
self._action = "open_play_game"
|
|
314
|
+
return
|
|
289
315
|
|
|
290
316
|
# Quick-select row numbers 1..0 (10).
|
|
291
317
|
row_from_key = self._digit_row_pressed()
|
|
@@ -329,10 +355,10 @@ class QuestsMenuView:
|
|
|
329
355
|
|
|
330
356
|
def _layout(self) -> dict[str, float]:
|
|
331
357
|
# `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
|
|
358
|
+
# x_sum = <ui_element_x> + <ui_element_offset_x> (x=-5)
|
|
359
|
+
# y_sum = <ui_element_y> + <ui_element_offset_y> (y=185 + widescreen shift via ui_menu_layout_init)
|
|
360
|
+
x_sum = QUEST_MENU_BASE_X + MENU_PANEL_OFFSET_X
|
|
361
|
+
y_sum = QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift
|
|
336
362
|
|
|
337
363
|
title_x = x_sum + QUEST_TITLE_X_OFFSET
|
|
338
364
|
title_y = y_sum + QUEST_TITLE_Y_OFFSET
|
|
@@ -392,20 +418,6 @@ class QuestsMenuView:
|
|
|
392
418
|
return True
|
|
393
419
|
return False
|
|
394
420
|
|
|
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
421
|
@staticmethod
|
|
410
422
|
def _digit_row_pressed() -> int | None:
|
|
411
423
|
keys = [
|
|
@@ -634,12 +646,15 @@ class QuestsMenuView:
|
|
|
634
646
|
else:
|
|
635
647
|
title = "???"
|
|
636
648
|
draw_small_text(font, title, list_x + QUEST_LIST_NAME_X_OFFSET, y, 1.0, color)
|
|
649
|
+
title_w = measure_small_text_width(font, title, 1.0) if unlocked else 0.0
|
|
650
|
+
if unlocked:
|
|
651
|
+
line_y = y + 13.0
|
|
652
|
+
rl.draw_line(int(list_x), int(line_y), int(list_x + title_w + 32.0), int(line_y), color)
|
|
637
653
|
|
|
638
654
|
if show_counts and unlocked:
|
|
639
655
|
counts = self._quest_counts(stage=stage, row=row)
|
|
640
656
|
if counts is not None:
|
|
641
657
|
completed, games = counts
|
|
642
|
-
title_w = measure_small_text_width(font, title, 1.0)
|
|
643
658
|
counts_x = list_x + QUEST_LIST_NAME_X_OFFSET + title_w + 12.0
|
|
644
659
|
draw_small_text(font, f"({completed}/{games})", counts_x, y, 1.0, color)
|
|
645
660
|
|
|
@@ -650,28 +665,12 @@ class QuestsMenuView:
|
|
|
650
665
|
draw_small_text(font, "(completed/games)", header_x, header_y, 1.0, base_color)
|
|
651
666
|
|
|
652
667
|
# Back button.
|
|
653
|
-
|
|
654
|
-
if
|
|
668
|
+
textures = self._button_textures
|
|
669
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
655
670
|
back_x = list_x + QUEST_BACK_BUTTON_X_OFFSET
|
|
656
671
|
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))
|
|
672
|
+
back_w = button_width(font, self._back_button.label, scale=1.0, force_wide=self._back_button.force_wide)
|
|
673
|
+
button_draw(textures, font, self._back_button, x=float(back_x), y=float(back_y), width=float(back_w), scale=1.0)
|
|
675
674
|
|
|
676
675
|
def _draw_sign(self) -> None:
|
|
677
676
|
assets = self._assets
|
|
@@ -719,52 +718,18 @@ class QuestsMenuView:
|
|
|
719
718
|
panel = self._panel_tex
|
|
720
719
|
if panel is None:
|
|
721
720
|
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
721
|
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,
|
|
746
|
-
)
|
|
747
|
-
|
|
748
|
-
|
|
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",
|
|
722
|
+
draw_menu_panel(
|
|
723
|
+
panel,
|
|
724
|
+
dst=rl.Rectangle(
|
|
725
|
+
float(QUEST_MENU_BASE_X + MENU_PANEL_OFFSET_X),
|
|
726
|
+
float(QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift),
|
|
727
|
+
float(MENU_PANEL_WIDTH),
|
|
728
|
+
float(QUEST_PANEL_HEIGHT),
|
|
729
|
+
),
|
|
730
|
+
shadow=fx_detail,
|
|
756
731
|
)
|
|
757
732
|
|
|
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
733
|
|
|
769
734
|
class FrontView(Protocol):
|
|
770
735
|
def open(self) -> None: ...
|
|
@@ -778,6 +743,10 @@ class FrontView(Protocol):
|
|
|
778
743
|
def take_action(self) -> str | None: ...
|
|
779
744
|
|
|
780
745
|
|
|
746
|
+
class PauseBackground(Protocol):
|
|
747
|
+
def draw_pause_background(self) -> None: ...
|
|
748
|
+
|
|
749
|
+
|
|
781
750
|
class SurvivalGameView:
|
|
782
751
|
"""Gameplay view wrapper that adapts SurvivalMode into `crimson game`."""
|
|
783
752
|
|
|
@@ -817,6 +786,9 @@ class SurvivalGameView:
|
|
|
817
786
|
def update(self, dt: float) -> None:
|
|
818
787
|
self._mode.update(dt)
|
|
819
788
|
mode_action = self._mode.take_action()
|
|
789
|
+
if mode_action == "open_pause_menu":
|
|
790
|
+
self._action = "open_pause_menu"
|
|
791
|
+
return
|
|
820
792
|
if mode_action == "open_high_scores":
|
|
821
793
|
self._state.pending_high_scores = HighScoresRequest(game_mode_id=1)
|
|
822
794
|
self._action = "open_high_scores"
|
|
@@ -832,6 +804,9 @@ class SurvivalGameView:
|
|
|
832
804
|
def draw(self) -> None:
|
|
833
805
|
self._mode.draw()
|
|
834
806
|
|
|
807
|
+
def draw_pause_background(self) -> None:
|
|
808
|
+
self._mode.draw_pause_background()
|
|
809
|
+
|
|
835
810
|
def take_action(self) -> str | None:
|
|
836
811
|
action = self._action
|
|
837
812
|
self._action = None
|
|
@@ -875,6 +850,9 @@ class RushGameView:
|
|
|
875
850
|
def update(self, dt: float) -> None:
|
|
876
851
|
self._mode.update(dt)
|
|
877
852
|
mode_action = self._mode.take_action()
|
|
853
|
+
if mode_action == "open_pause_menu":
|
|
854
|
+
self._action = "open_pause_menu"
|
|
855
|
+
return
|
|
878
856
|
if mode_action == "open_high_scores":
|
|
879
857
|
self._state.pending_high_scores = HighScoresRequest(game_mode_id=2)
|
|
880
858
|
self._action = "open_high_scores"
|
|
@@ -890,6 +868,9 @@ class RushGameView:
|
|
|
890
868
|
def draw(self) -> None:
|
|
891
869
|
self._mode.draw()
|
|
892
870
|
|
|
871
|
+
def draw_pause_background(self) -> None:
|
|
872
|
+
self._mode.draw_pause_background()
|
|
873
|
+
|
|
893
874
|
def take_action(self) -> str | None:
|
|
894
875
|
action = self._action
|
|
895
876
|
self._action = None
|
|
@@ -933,6 +914,9 @@ class TypoShooterGameView:
|
|
|
933
914
|
def update(self, dt: float) -> None:
|
|
934
915
|
self._mode.update(dt)
|
|
935
916
|
mode_action = self._mode.take_action()
|
|
917
|
+
if mode_action == "open_pause_menu":
|
|
918
|
+
self._action = "open_pause_menu"
|
|
919
|
+
return
|
|
936
920
|
if mode_action == "open_high_scores":
|
|
937
921
|
self._state.pending_high_scores = HighScoresRequest(game_mode_id=4)
|
|
938
922
|
self._action = "open_high_scores"
|
|
@@ -948,6 +932,9 @@ class TypoShooterGameView:
|
|
|
948
932
|
def draw(self) -> None:
|
|
949
933
|
self._mode.draw()
|
|
950
934
|
|
|
935
|
+
def draw_pause_background(self) -> None:
|
|
936
|
+
self._mode.draw_pause_background()
|
|
937
|
+
|
|
951
938
|
def take_action(self) -> str | None:
|
|
952
939
|
action = self._action
|
|
953
940
|
self._action = None
|
|
@@ -991,6 +978,10 @@ class TutorialGameView:
|
|
|
991
978
|
|
|
992
979
|
def update(self, dt: float) -> None:
|
|
993
980
|
self._mode.update(dt)
|
|
981
|
+
mode_action = self._mode.take_action()
|
|
982
|
+
if mode_action == "open_pause_menu":
|
|
983
|
+
self._action = "open_pause_menu"
|
|
984
|
+
return
|
|
994
985
|
if getattr(self._mode, "close_requested", False):
|
|
995
986
|
self._action = "back_to_menu"
|
|
996
987
|
self._mode.close_requested = False
|
|
@@ -998,6 +989,9 @@ class TutorialGameView:
|
|
|
998
989
|
def draw(self) -> None:
|
|
999
990
|
self._mode.draw()
|
|
1000
991
|
|
|
992
|
+
def draw_pause_background(self) -> None:
|
|
993
|
+
self._mode.draw_pause_background()
|
|
994
|
+
|
|
1001
995
|
def take_action(self) -> str | None:
|
|
1002
996
|
action = self._action
|
|
1003
997
|
self._action = None
|
|
@@ -1046,6 +1040,10 @@ class QuestGameView:
|
|
|
1046
1040
|
|
|
1047
1041
|
def update(self, dt: float) -> None:
|
|
1048
1042
|
self._mode.update(dt)
|
|
1043
|
+
mode_action = self._mode.take_action()
|
|
1044
|
+
if mode_action == "open_pause_menu":
|
|
1045
|
+
self._action = "open_pause_menu"
|
|
1046
|
+
return
|
|
1049
1047
|
if getattr(self._mode, "close_requested", False):
|
|
1050
1048
|
outcome = self._mode.consume_outcome()
|
|
1051
1049
|
if outcome is not None:
|
|
@@ -1063,6 +1061,9 @@ class QuestGameView:
|
|
|
1063
1061
|
def draw(self) -> None:
|
|
1064
1062
|
self._mode.draw()
|
|
1065
1063
|
|
|
1064
|
+
def draw_pause_background(self) -> None:
|
|
1065
|
+
self._mode.draw_pause_background()
|
|
1066
|
+
|
|
1066
1067
|
def take_action(self) -> str | None:
|
|
1067
1068
|
action = self._action
|
|
1068
1069
|
self._action = None
|
|
@@ -1103,45 +1104,35 @@ class QuestResultsView:
|
|
|
1103
1104
|
def __init__(self, state: GameState) -> None:
|
|
1104
1105
|
self._state = state
|
|
1105
1106
|
self._ground: GroundRenderer | None = None
|
|
1106
|
-
self.
|
|
1107
|
+
self._quest_level: str = ""
|
|
1107
1108
|
self._quest_title: str = ""
|
|
1108
1109
|
self._quest_stage_major = 0
|
|
1109
1110
|
self._quest_stage_minor = 0
|
|
1110
1111
|
self._unlock_weapon_name: str = ""
|
|
1111
1112
|
self._unlock_perk_name: str = ""
|
|
1112
|
-
self.
|
|
1113
|
-
self._breakdown_anim = None
|
|
1114
|
-
self._record = None
|
|
1115
|
-
self._rank_index: int | None = None
|
|
1113
|
+
self._ui = None
|
|
1116
1114
|
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
1115
|
|
|
1121
1116
|
def open(self) -> None:
|
|
1122
|
-
from .
|
|
1123
|
-
from .
|
|
1117
|
+
from .persistence.highscores import HighScoreRecord
|
|
1118
|
+
from .quests.results import compute_quest_final_time
|
|
1119
|
+
from .ui.quest_results import QuestResultsUi
|
|
1124
1120
|
|
|
1125
1121
|
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
|
|
1122
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
1131
1123
|
self._state.quest_fail_retry_count = 0
|
|
1124
|
+
outcome = self._state.quest_outcome
|
|
1125
|
+
self._state.quest_outcome = None
|
|
1126
|
+
self._quest_level = ""
|
|
1132
1127
|
self._quest_title = ""
|
|
1133
1128
|
self._quest_stage_major = 0
|
|
1134
1129
|
self._quest_stage_minor = 0
|
|
1135
1130
|
self._unlock_weapon_name = ""
|
|
1136
1131
|
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
|
|
1132
|
+
self._ui = None
|
|
1143
1133
|
if outcome is None:
|
|
1144
1134
|
return
|
|
1135
|
+
self._quest_level = str(outcome.level or "")
|
|
1145
1136
|
|
|
1146
1137
|
major, minor = 0, 0
|
|
1147
1138
|
try:
|
|
@@ -1199,7 +1190,8 @@ class QuestResultsView:
|
|
|
1199
1190
|
pending_perk_count=int(outcome.pending_perk_count),
|
|
1200
1191
|
)
|
|
1201
1192
|
record.survival_elapsed_ms = int(breakdown.final_time_ms)
|
|
1202
|
-
|
|
1193
|
+
player_name_default = _player_name_default(self._state.config) or "Player"
|
|
1194
|
+
record.set_name(player_name_default)
|
|
1203
1195
|
|
|
1204
1196
|
global_index = (int(major) - 1) * 10 + (int(minor) - 1)
|
|
1205
1197
|
if 0 <= global_index < 40:
|
|
@@ -1228,271 +1220,86 @@ class QuestResultsView:
|
|
|
1228
1220
|
except Exception:
|
|
1229
1221
|
pass
|
|
1230
1222
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
self.
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1223
|
+
self._ui = QuestResultsUi(
|
|
1224
|
+
assets_root=self._state.assets_dir,
|
|
1225
|
+
base_dir=self._state.base_dir,
|
|
1226
|
+
config=self._state.config,
|
|
1227
|
+
)
|
|
1228
|
+
self._ui.open(
|
|
1229
|
+
record=record,
|
|
1230
|
+
breakdown=breakdown,
|
|
1231
|
+
quest_level=str(outcome.level or ""),
|
|
1232
|
+
quest_title=str(self._quest_title or ""),
|
|
1233
|
+
quest_stage_major=int(self._quest_stage_major),
|
|
1234
|
+
quest_stage_minor=int(self._quest_stage_minor),
|
|
1235
|
+
unlock_weapon_name=str(self._unlock_weapon_name or ""),
|
|
1236
|
+
unlock_perk_name=str(self._unlock_perk_name or ""),
|
|
1237
|
+
player_name_default=player_name_default,
|
|
1238
|
+
)
|
|
1243
1239
|
|
|
1244
1240
|
def close(self) -> None:
|
|
1245
|
-
self.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
self.
|
|
1249
|
-
self._breakdown = None
|
|
1250
|
-
self._breakdown_anim = None
|
|
1251
|
-
self._rank_index = None
|
|
1241
|
+
if self._ui is not None:
|
|
1242
|
+
self._ui.close()
|
|
1243
|
+
self._ui = None
|
|
1244
|
+
self._ground = None
|
|
1252
1245
|
self._quest_stage_major = 0
|
|
1253
1246
|
self._quest_stage_minor = 0
|
|
1247
|
+
self._quest_level = ""
|
|
1248
|
+
self._quest_title = ""
|
|
1254
1249
|
self._unlock_weapon_name = ""
|
|
1255
1250
|
self._unlock_perk_name = ""
|
|
1256
1251
|
|
|
1257
1252
|
def update(self, dt: float) -> None:
|
|
1258
|
-
from .quests.results import tick_quest_results_breakdown_anim
|
|
1259
|
-
|
|
1260
1253
|
if self._state.audio is not None:
|
|
1261
1254
|
update_audio(self._state.audio, dt)
|
|
1262
1255
|
if self._ground is not None:
|
|
1263
1256
|
self._ground.process_pending()
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
|
|
1267
|
-
self._action = "back_to_menu"
|
|
1268
|
-
return
|
|
1269
|
-
|
|
1270
|
-
if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
|
|
1271
|
-
self._open_high_scores_list()
|
|
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:
|
|
1257
|
+
ui = self._ui
|
|
1258
|
+
if ui is None:
|
|
1278
1259
|
return
|
|
1260
|
+
audio = self._state.audio
|
|
1261
|
+
rng = self._state.rng
|
|
1279
1262
|
|
|
1280
|
-
|
|
1281
|
-
|
|
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:
|
|
1263
|
+
def _play(name: str) -> None:
|
|
1264
|
+
if audio is None:
|
|
1294
1265
|
return
|
|
1266
|
+
play_sfx(audio, name, rng=rng)
|
|
1295
1267
|
|
|
1296
|
-
if
|
|
1297
|
-
|
|
1268
|
+
action = ui.update(dt, play_sfx=_play if audio is not None else None, rand=lambda: rng.getrandbits(32))
|
|
1269
|
+
if action == "play_again":
|
|
1270
|
+
self._state.pending_quest_level = self._quest_level
|
|
1298
1271
|
self._action = "start_quest"
|
|
1299
1272
|
return
|
|
1300
|
-
if
|
|
1301
|
-
next_level = _next_quest_level(
|
|
1273
|
+
if action == "play_next":
|
|
1274
|
+
next_level = _next_quest_level(self._quest_level)
|
|
1302
1275
|
if next_level is not None:
|
|
1303
1276
|
self._state.pending_quest_level = next_level
|
|
1304
1277
|
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":
|
|
1278
|
+
else:
|
|
1343
1279
|
self._action = "back_to_menu"
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1280
|
+
return
|
|
1281
|
+
if action == "high_scores":
|
|
1282
|
+
self._open_high_scores_list()
|
|
1283
|
+
return
|
|
1284
|
+
if action == "main_menu":
|
|
1285
|
+
self._action = "back_to_menu"
|
|
1286
|
+
return
|
|
1348
1287
|
|
|
1349
1288
|
def draw(self) -> None:
|
|
1350
1289
|
rl.clear_background(rl.BLACK)
|
|
1351
|
-
|
|
1290
|
+
pause_background = self._state.pause_background
|
|
1291
|
+
if pause_background is not None:
|
|
1292
|
+
pause_background.draw_pause_background()
|
|
1293
|
+
elif self._ground is not None:
|
|
1352
1294
|
self._ground.draw(0.0, 0.0)
|
|
1353
1295
|
_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))
|
|
1296
|
+
ui = self._ui
|
|
1297
|
+
if ui is not None:
|
|
1298
|
+
ui.draw()
|
|
1361
1299
|
return
|
|
1362
1300
|
|
|
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)
|
|
1301
|
+
rl.draw_text("Quest results unavailable.", 32, 140, 28, rl.Color(235, 235, 235, 255))
|
|
1302
|
+
rl.draw_text("Press ESC to return to the menu.", 32, 180, 18, rl.Color(190, 190, 200, 255))
|
|
1496
1303
|
|
|
1497
1304
|
def take_action(self) -> str | None:
|
|
1498
1305
|
action = self._action
|
|
@@ -1500,21 +1307,17 @@ class QuestResultsView:
|
|
|
1500
1307
|
return action
|
|
1501
1308
|
|
|
1502
1309
|
def _open_high_scores_list(self) -> None:
|
|
1310
|
+
highlight_rank = None
|
|
1311
|
+
if self._ui is not None:
|
|
1312
|
+
highlight_rank = self._ui.highlight_rank
|
|
1503
1313
|
self._state.pending_high_scores = HighScoresRequest(
|
|
1504
1314
|
game_mode_id=3,
|
|
1505
1315
|
quest_stage_major=int(self._quest_stage_major),
|
|
1506
1316
|
quest_stage_minor=int(self._quest_stage_minor),
|
|
1507
|
-
highlight_rank=
|
|
1317
|
+
highlight_rank=highlight_rank,
|
|
1508
1318
|
)
|
|
1509
1319
|
self._action = "open_high_scores"
|
|
1510
1320
|
|
|
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
1321
|
|
|
1519
1322
|
class QuestFailedView:
|
|
1520
1323
|
def __init__(self, state: GameState) -> None:
|
|
@@ -1529,13 +1332,17 @@ class QuestFailedView:
|
|
|
1529
1332
|
|
|
1530
1333
|
def open(self) -> None:
|
|
1531
1334
|
self._action = None
|
|
1532
|
-
self._ground = ensure_menu_ground(self._state)
|
|
1335
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
1533
1336
|
self._cursor_pulse_time = 0.0
|
|
1534
1337
|
self._outcome = self._state.quest_outcome
|
|
1535
1338
|
self._state.quest_outcome = None
|
|
1536
1339
|
self._quest_title = ""
|
|
1537
1340
|
self._small_font = None
|
|
1538
1341
|
self._button_tex = None
|
|
1342
|
+
self._button_textures = None
|
|
1343
|
+
self._retry_button = UiButtonState("Retry", force_wide=True)
|
|
1344
|
+
self._quest_list_button = UiButtonState("Quest list", force_wide=True)
|
|
1345
|
+
self._main_menu_button = UiButtonState("Main menu", force_wide=True)
|
|
1539
1346
|
outcome = self._outcome
|
|
1540
1347
|
if outcome is not None:
|
|
1541
1348
|
try:
|
|
@@ -1547,7 +1354,9 @@ class QuestFailedView:
|
|
|
1547
1354
|
self._quest_title = ""
|
|
1548
1355
|
|
|
1549
1356
|
cache = _ensure_texture_cache(self._state)
|
|
1550
|
-
self._button_tex = cache.get_or_load("
|
|
1357
|
+
self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
1358
|
+
button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
1359
|
+
self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=self._button_tex)
|
|
1551
1360
|
|
|
1552
1361
|
def close(self) -> None:
|
|
1553
1362
|
self._ground = None
|
|
@@ -1555,6 +1364,7 @@ class QuestFailedView:
|
|
|
1555
1364
|
self._quest_title = ""
|
|
1556
1365
|
self._small_font = None
|
|
1557
1366
|
self._button_tex = None
|
|
1367
|
+
self._button_textures = None
|
|
1558
1368
|
|
|
1559
1369
|
def update(self, dt: float) -> None:
|
|
1560
1370
|
if self._state.audio is not None:
|
|
@@ -1578,44 +1388,56 @@ class QuestFailedView:
|
|
|
1578
1388
|
self._action = "open_quests"
|
|
1579
1389
|
return
|
|
1580
1390
|
|
|
1581
|
-
|
|
1582
|
-
if
|
|
1391
|
+
textures = self._button_textures
|
|
1392
|
+
if outcome is None or textures is None or (textures.button_sm is None and textures.button_md is None):
|
|
1583
1393
|
return
|
|
1584
1394
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1585
|
-
button_w =
|
|
1586
|
-
button_h =
|
|
1395
|
+
button_w = button_width(None, self._retry_button.label, scale=scale, force_wide=self._retry_button.force_wide)
|
|
1396
|
+
button_h = 32.0 * scale
|
|
1587
1397
|
gap_x = 18.0 * scale
|
|
1588
1398
|
x0 = 32.0
|
|
1589
1399
|
y0 = float(rl.get_screen_height()) - button_h - 56.0 * scale
|
|
1590
1400
|
|
|
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
1401
|
mouse = rl.get_mouse_position()
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1402
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
1403
|
+
dt_ms = min(float(dt), 0.1) * 1000.0
|
|
1404
|
+
|
|
1405
|
+
if button_update(self._retry_button, x=x0, y=y0, width=button_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
1406
|
+
self._state.quest_fail_retry_count = int(self._state.quest_fail_retry_count) + 1
|
|
1407
|
+
self._state.pending_quest_level = outcome.level
|
|
1408
|
+
self._action = "start_quest"
|
|
1409
|
+
return
|
|
1410
|
+
if button_update(
|
|
1411
|
+
self._quest_list_button,
|
|
1412
|
+
x=x0 + button_w + gap_x,
|
|
1413
|
+
y=y0,
|
|
1414
|
+
width=button_w,
|
|
1415
|
+
dt_ms=dt_ms,
|
|
1416
|
+
mouse=mouse,
|
|
1417
|
+
click=click,
|
|
1418
|
+
):
|
|
1419
|
+
self._state.quest_fail_retry_count = 0
|
|
1420
|
+
self._action = "open_quests"
|
|
1421
|
+
return
|
|
1422
|
+
if button_update(
|
|
1423
|
+
self._main_menu_button,
|
|
1424
|
+
x=x0 + (button_w + gap_x) * 2.0,
|
|
1425
|
+
y=y0,
|
|
1426
|
+
width=button_w,
|
|
1427
|
+
dt_ms=dt_ms,
|
|
1428
|
+
mouse=mouse,
|
|
1429
|
+
click=click,
|
|
1430
|
+
):
|
|
1431
|
+
self._state.quest_fail_retry_count = 0
|
|
1432
|
+
self._action = "back_to_menu"
|
|
1433
|
+
return
|
|
1615
1434
|
|
|
1616
1435
|
def draw(self) -> None:
|
|
1617
1436
|
rl.clear_background(rl.BLACK)
|
|
1618
|
-
|
|
1437
|
+
pause_background = self._state.pause_background
|
|
1438
|
+
if pause_background is not None:
|
|
1439
|
+
pause_background.draw_pause_background()
|
|
1440
|
+
elif self._ground is not None:
|
|
1619
1441
|
self._ground.draw(0.0, 0.0)
|
|
1620
1442
|
_draw_screen_fade(self._state)
|
|
1621
1443
|
|
|
@@ -1655,36 +1477,33 @@ class QuestFailedView:
|
|
|
1655
1477
|
y += 18.0
|
|
1656
1478
|
draw_small_text(font, f"XP: {int(outcome.experience)}", 32.0, y, 1.0, text_color)
|
|
1657
1479
|
|
|
1658
|
-
|
|
1659
|
-
if
|
|
1480
|
+
textures = self._button_textures
|
|
1481
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
1660
1482
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1661
|
-
button_w =
|
|
1662
|
-
button_h =
|
|
1483
|
+
button_w = button_width(None, self._retry_button.label, scale=scale, force_wide=self._retry_button.force_wide)
|
|
1484
|
+
button_h = 32.0 * scale
|
|
1663
1485
|
gap_x = 18.0 * scale
|
|
1664
1486
|
x0 = 32.0
|
|
1665
1487
|
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))
|
|
1488
|
+
button_draw(textures, font, self._retry_button, x=x0, y=y0, width=button_w, scale=scale)
|
|
1489
|
+
button_draw(
|
|
1490
|
+
textures,
|
|
1491
|
+
font,
|
|
1492
|
+
self._quest_list_button,
|
|
1493
|
+
x=x0 + button_w + gap_x,
|
|
1494
|
+
y=y0,
|
|
1495
|
+
width=button_w,
|
|
1496
|
+
scale=scale,
|
|
1497
|
+
)
|
|
1498
|
+
button_draw(
|
|
1499
|
+
textures,
|
|
1500
|
+
font,
|
|
1501
|
+
self._main_menu_button,
|
|
1502
|
+
x=x0 + (button_w + gap_x) * 2.0,
|
|
1503
|
+
y=y0,
|
|
1504
|
+
width=button_w,
|
|
1505
|
+
scale=scale,
|
|
1506
|
+
)
|
|
1688
1507
|
|
|
1689
1508
|
draw_small_text(
|
|
1690
1509
|
font,
|
|
@@ -1717,6 +1536,9 @@ class HighScoresView:
|
|
|
1717
1536
|
self._cursor_pulse_time = 0.0
|
|
1718
1537
|
self._small_font: SmallFontData | None = None
|
|
1719
1538
|
self._button_tex: rl.Texture2D | None = None
|
|
1539
|
+
self._button_textures: UiButtonTextureSet | None = None
|
|
1540
|
+
self._back_button = UiButtonState("Back", force_wide=True)
|
|
1541
|
+
self._main_menu_button = UiButtonState("Main menu", force_wide=True)
|
|
1720
1542
|
|
|
1721
1543
|
self._request: HighScoresRequest | None = None
|
|
1722
1544
|
self._records: list = []
|
|
@@ -1726,13 +1548,18 @@ class HighScoresView:
|
|
|
1726
1548
|
from .persistence.highscores import read_highscore_table, scores_path_for_mode
|
|
1727
1549
|
|
|
1728
1550
|
self._action = None
|
|
1729
|
-
self._ground = ensure_menu_ground(self._state)
|
|
1551
|
+
self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
|
|
1730
1552
|
self._cursor_pulse_time = 0.0
|
|
1731
1553
|
self._small_font = None
|
|
1732
1554
|
self._scroll_index = 0
|
|
1555
|
+
self._button_textures = None
|
|
1556
|
+
self._back_button = UiButtonState("Back", force_wide=True)
|
|
1557
|
+
self._main_menu_button = UiButtonState("Main menu", force_wide=True)
|
|
1733
1558
|
|
|
1734
1559
|
cache = _ensure_texture_cache(self._state)
|
|
1735
|
-
self._button_tex = cache.get_or_load("
|
|
1560
|
+
self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
|
|
1561
|
+
button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
|
|
1562
|
+
self._button_textures = UiButtonTextureSet(button_sm=button_sm, button_md=self._button_tex)
|
|
1736
1563
|
|
|
1737
1564
|
request = self._state.pending_high_scores
|
|
1738
1565
|
self._state.pending_high_scores = None
|
|
@@ -1769,6 +1596,7 @@ class HighScoresView:
|
|
|
1769
1596
|
rl.unload_texture(self._small_font.texture)
|
|
1770
1597
|
self._small_font = None
|
|
1771
1598
|
self._button_tex = None
|
|
1599
|
+
self._button_textures = None
|
|
1772
1600
|
self._request = None
|
|
1773
1601
|
self._records = []
|
|
1774
1602
|
self._scroll_index = 0
|
|
@@ -1786,24 +1614,31 @@ class HighScoresView:
|
|
|
1786
1614
|
self._action = "back_to_previous"
|
|
1787
1615
|
return
|
|
1788
1616
|
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
tex = self._button_tex
|
|
1792
|
-
if tex is not None and clicked:
|
|
1617
|
+
textures = self._button_textures
|
|
1618
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
1793
1619
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1794
|
-
button_w =
|
|
1795
|
-
button_h =
|
|
1620
|
+
button_w = button_width(None, self._back_button.label, scale=scale, force_wide=self._back_button.force_wide)
|
|
1621
|
+
button_h = 32.0 * scale
|
|
1796
1622
|
gap_x = 18.0 * scale
|
|
1797
1623
|
x0 = 32.0
|
|
1798
1624
|
y0 = float(rl.get_screen_height()) - button_h - 52.0 * scale
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1625
|
+
mouse = rl.get_mouse_position()
|
|
1626
|
+
click = rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT)
|
|
1627
|
+
dt_ms = min(float(dt), 0.1) * 1000.0
|
|
1628
|
+
if button_update(self._back_button, x=x0, y=y0, width=button_w, dt_ms=dt_ms, mouse=mouse, click=click):
|
|
1802
1629
|
if self._state.audio is not None:
|
|
1803
1630
|
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
1804
1631
|
self._action = "back_to_previous"
|
|
1805
1632
|
return
|
|
1806
|
-
if
|
|
1633
|
+
if button_update(
|
|
1634
|
+
self._main_menu_button,
|
|
1635
|
+
x=x0 + button_w + gap_x,
|
|
1636
|
+
y=y0,
|
|
1637
|
+
width=button_w,
|
|
1638
|
+
dt_ms=dt_ms,
|
|
1639
|
+
mouse=mouse,
|
|
1640
|
+
click=click,
|
|
1641
|
+
):
|
|
1807
1642
|
if self._state.audio is not None:
|
|
1808
1643
|
play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
|
|
1809
1644
|
self._action = "back_to_menu"
|
|
@@ -1832,7 +1667,10 @@ class HighScoresView:
|
|
|
1832
1667
|
|
|
1833
1668
|
def draw(self) -> None:
|
|
1834
1669
|
rl.clear_background(rl.BLACK)
|
|
1835
|
-
|
|
1670
|
+
pause_background = self._state.pause_background
|
|
1671
|
+
if pause_background is not None:
|
|
1672
|
+
pause_background.draw_pause_background()
|
|
1673
|
+
elif self._ground is not None:
|
|
1836
1674
|
self._ground.draw(0.0, 0.0)
|
|
1837
1675
|
_draw_screen_fade(self._state)
|
|
1838
1676
|
|
|
@@ -1892,36 +1730,17 @@ class HighScoresView:
|
|
|
1892
1730
|
draw_small_text(font, value, 320.0, y, 1.0, color)
|
|
1893
1731
|
y += row_step
|
|
1894
1732
|
|
|
1895
|
-
|
|
1896
|
-
if
|
|
1733
|
+
textures = self._button_textures
|
|
1734
|
+
if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
|
|
1897
1735
|
scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
|
|
1898
|
-
button_w =
|
|
1899
|
-
button_h =
|
|
1736
|
+
button_w = button_width(None, self._back_button.label, scale=scale, force_wide=self._back_button.force_wide)
|
|
1737
|
+
button_h = 32.0 * scale
|
|
1900
1738
|
gap_x = 18.0 * scale
|
|
1901
1739
|
x0 = 32.0
|
|
1902
1740
|
y0 = float(rl.get_screen_height()) - button_h - 52.0 * scale
|
|
1903
1741
|
x1 = x0 + button_w + gap_x
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
("Back", rl.Rectangle(x0, y0, button_w, button_h)),
|
|
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))
|
|
1742
|
+
button_draw(textures, font, self._back_button, x=x0, y=y0, width=button_w, scale=scale)
|
|
1743
|
+
button_draw(textures, font, self._main_menu_button, x=x1, y=y0, width=button_w, scale=scale)
|
|
1925
1744
|
|
|
1926
1745
|
draw_small_text(
|
|
1927
1746
|
font,
|
|
@@ -1986,6 +1805,7 @@ class GameLoopView:
|
|
|
1986
1805
|
self._front_views: dict[str, FrontView] = {
|
|
1987
1806
|
"open_play_game": PlayGameMenuView(state),
|
|
1988
1807
|
"open_quests": QuestsMenuView(state),
|
|
1808
|
+
"open_pause_menu": PauseMenuView(state),
|
|
1989
1809
|
"start_quest": QuestGameView(state),
|
|
1990
1810
|
"quest_results": QuestResultsView(state),
|
|
1991
1811
|
"quest_failed": QuestFailedView(state),
|
|
@@ -2052,6 +1872,7 @@ class GameLoopView:
|
|
|
2052
1872
|
if self._front_active is not None:
|
|
2053
1873
|
action = self._front_active.take_action()
|
|
2054
1874
|
if action == "back_to_menu":
|
|
1875
|
+
self._state.pause_background = None
|
|
2055
1876
|
self._front_active.close()
|
|
2056
1877
|
self._front_active = None
|
|
2057
1878
|
while self._front_stack:
|
|
@@ -2064,14 +1885,44 @@ class GameLoopView:
|
|
|
2064
1885
|
if self._front_stack:
|
|
2065
1886
|
self._front_active.close()
|
|
2066
1887
|
self._front_active = self._front_stack.pop()
|
|
1888
|
+
if self._front_active in self._gameplay_views:
|
|
1889
|
+
self._state.pause_background = None
|
|
2067
1890
|
self._active = self._front_active
|
|
2068
1891
|
return
|
|
2069
1892
|
self._front_active.close()
|
|
2070
1893
|
self._front_active = None
|
|
1894
|
+
self._state.pause_background = None
|
|
2071
1895
|
self._menu.open()
|
|
2072
1896
|
self._active = self._menu
|
|
2073
1897
|
self._menu_active = True
|
|
2074
1898
|
return
|
|
1899
|
+
if action == "open_pause_menu":
|
|
1900
|
+
pause_view = self._front_views.get("open_pause_menu")
|
|
1901
|
+
if pause_view is None:
|
|
1902
|
+
return
|
|
1903
|
+
if self._front_active in self._gameplay_views:
|
|
1904
|
+
self._state.pause_background = self._front_active
|
|
1905
|
+
self._front_stack.append(self._front_active)
|
|
1906
|
+
pause_view.open()
|
|
1907
|
+
self._front_active = pause_view
|
|
1908
|
+
self._active = pause_view
|
|
1909
|
+
return
|
|
1910
|
+
if self._state.pause_background is None:
|
|
1911
|
+
# Options panel uses open_pause_menu as back_action; when no game is
|
|
1912
|
+
# running, treat it like back_to_menu.
|
|
1913
|
+
self._front_active.close()
|
|
1914
|
+
self._front_active = None
|
|
1915
|
+
while self._front_stack:
|
|
1916
|
+
self._front_stack.pop().close()
|
|
1917
|
+
self._menu.open()
|
|
1918
|
+
self._active = self._menu
|
|
1919
|
+
self._menu_active = True
|
|
1920
|
+
return
|
|
1921
|
+
self._front_active.close()
|
|
1922
|
+
pause_view.open()
|
|
1923
|
+
self._front_active = pause_view
|
|
1924
|
+
self._active = pause_view
|
|
1925
|
+
return
|
|
2075
1926
|
if action in {"start_survival", "start_rush", "start_typo"}:
|
|
2076
1927
|
# Temporary: bump the counter on mode start so the Play Game overlay (F1)
|
|
2077
1928
|
# and Statistics screen reflect activity.
|
|
@@ -2086,8 +1937,25 @@ class GameLoopView:
|
|
|
2086
1937
|
view = self._front_views.get(action)
|
|
2087
1938
|
if view is not None:
|
|
2088
1939
|
if action == "open_high_scores":
|
|
1940
|
+
if (self._front_active in self._gameplay_views) and (self._state.pause_background is None):
|
|
1941
|
+
self._state.pause_background = self._front_active
|
|
1942
|
+
self._front_stack.append(self._front_active)
|
|
1943
|
+
elif action in {"quest_results", "quest_failed"} and (self._front_active in self._gameplay_views):
|
|
1944
|
+
self._state.pause_background = self._front_active
|
|
2089
1945
|
self._front_stack.append(self._front_active)
|
|
2090
1946
|
else:
|
|
1947
|
+
if action in {
|
|
1948
|
+
"start_survival",
|
|
1949
|
+
"start_rush",
|
|
1950
|
+
"start_typo",
|
|
1951
|
+
"start_tutorial",
|
|
1952
|
+
"start_quest",
|
|
1953
|
+
"open_play_game",
|
|
1954
|
+
"open_quests",
|
|
1955
|
+
}:
|
|
1956
|
+
self._state.pause_background = None
|
|
1957
|
+
while self._front_stack:
|
|
1958
|
+
self._front_stack.pop().close()
|
|
2091
1959
|
self._front_active.close()
|
|
2092
1960
|
view.open()
|
|
2093
1961
|
self._front_active = view
|
|
@@ -2524,6 +2392,7 @@ def run_game(config: GameConfig) -> None:
|
|
|
2524
2392
|
title="Crimsonland",
|
|
2525
2393
|
fps=config.fps,
|
|
2526
2394
|
config_flags=config_flags,
|
|
2395
|
+
exit_key=rl.KeyboardKey.KEY_NULL,
|
|
2527
2396
|
)
|
|
2528
2397
|
if state is not None:
|
|
2529
2398
|
state.status.save_if_dirty()
|