crimsonland 0.1.0.dev15__py3-none-any.whl → 0.1.0.dev16__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.
Files changed (75) hide show
  1. crimson/cli.py +61 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/demo.py +38 -45
  6. crimson/effects.py +7 -13
  7. crimson/frontend/high_scores_layout.py +81 -0
  8. crimson/frontend/panels/base.py +4 -1
  9. crimson/frontend/panels/controls.py +0 -15
  10. crimson/frontend/panels/databases.py +291 -3
  11. crimson/frontend/panels/mods.py +0 -15
  12. crimson/frontend/panels/play_game.py +0 -16
  13. crimson/game.py +441 -1
  14. crimson/gameplay.py +905 -569
  15. crimson/modes/base_gameplay_mode.py +33 -12
  16. crimson/modes/components/__init__.py +2 -0
  17. crimson/modes/components/highscore_record_builder.py +58 -0
  18. crimson/modes/components/perk_menu_controller.py +325 -0
  19. crimson/modes/quest_mode.py +58 -273
  20. crimson/modes/rush_mode.py +12 -43
  21. crimson/modes/survival_mode.py +71 -328
  22. crimson/modes/tutorial_mode.py +46 -247
  23. crimson/modes/typo_mode.py +11 -38
  24. crimson/oracle.py +396 -0
  25. crimson/perks.py +5 -2
  26. crimson/player_damage.py +94 -37
  27. crimson/projectiles.py +539 -320
  28. crimson/render/projectile_draw_registry.py +637 -0
  29. crimson/render/projectile_render_registry.py +110 -0
  30. crimson/render/secondary_projectile_draw_registry.py +206 -0
  31. crimson/render/world_renderer.py +58 -707
  32. crimson/sim/world_state.py +118 -61
  33. crimson/typo/spawns.py +5 -12
  34. crimson/ui/demo_trial_overlay.py +3 -11
  35. crimson/ui/formatting.py +24 -0
  36. crimson/ui/game_over.py +12 -58
  37. crimson/ui/hud.py +72 -39
  38. crimson/ui/layout.py +20 -0
  39. crimson/ui/perk_menu.py +9 -34
  40. crimson/ui/quest_results.py +12 -64
  41. crimson/ui/text_input.py +20 -0
  42. crimson/views/_ui_helpers.py +27 -0
  43. crimson/views/aim_debug.py +15 -32
  44. crimson/views/animations.py +18 -28
  45. crimson/views/arsenal_debug.py +22 -32
  46. crimson/views/bonuses.py +23 -36
  47. crimson/views/camera_debug.py +16 -29
  48. crimson/views/camera_shake.py +9 -33
  49. crimson/views/corpse_stamp_debug.py +13 -21
  50. crimson/views/decals_debug.py +36 -23
  51. crimson/views/fonts.py +8 -25
  52. crimson/views/ground.py +4 -21
  53. crimson/views/lighting_debug.py +42 -45
  54. crimson/views/particles.py +33 -42
  55. crimson/views/perk_menu_debug.py +3 -10
  56. crimson/views/player.py +50 -44
  57. crimson/views/player_sprite_debug.py +24 -31
  58. crimson/views/projectile_fx.py +57 -52
  59. crimson/views/projectile_render_debug.py +24 -33
  60. crimson/views/projectiles.py +24 -37
  61. crimson/views/spawn_plan.py +13 -29
  62. crimson/views/sprites.py +14 -29
  63. crimson/views/terrain.py +6 -23
  64. crimson/views/ui.py +7 -24
  65. crimson/views/wicons.py +28 -33
  66. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  67. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +72 -64
  68. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +2 -2
  69. grim/config.py +29 -1
  70. grim/console.py +7 -10
  71. grim/math.py +12 -0
  72. crimson/.DS_Store +0 -0
  73. crimson/creatures/.DS_Store +0 -0
  74. grim/.DS_Store +0 -0
  75. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
@@ -10,14 +10,14 @@ from grim.audio import AudioState
10
10
  from grim.console import ConsoleState
11
11
  from grim.config import CrimsonConfig
12
12
  from grim.fonts.grim_mono import GrimMonoFont, load_grim_mono_font
13
+ from grim.math import clamp
13
14
  from grim.view import ViewContext
14
15
 
15
16
  from ..debug import debug_enabled
16
17
  from ..game_modes import GameMode
17
- from ..gameplay import most_used_weapon_id_for_player, perk_selection_current_choices, perk_selection_pick, weapon_assign_player
18
+ from ..gameplay import most_used_weapon_id_for_player, weapon_assign_player
18
19
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
19
20
  from ..persistence.save_status import GameStatus
20
- from ..perks import PerkId, perk_display_description, perk_display_name
21
21
  from ..quests import quest_by_level
22
22
  from ..quests.runtime import build_quest_spawn_table, tick_quest_completion_transition
23
23
  from ..quests.timeline import quest_spawn_table_empty, tick_quest_mode_spawns
@@ -25,28 +25,11 @@ from ..quests.types import QuestContext, QuestDefinition, SpawnEntry
25
25
  from ..terrain_assets import terrain_texture_by_id
26
26
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
27
27
  from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
28
- from ..ui.menu_panel import draw_classic_menu_panel
29
- from ..ui.perk_menu import (
30
- PERK_MENU_TRANSITION_MS,
31
- PerkMenuAssets,
32
- PerkMenuLayout,
33
- UiButtonState,
34
- button_draw,
35
- button_update,
36
- button_width,
37
- draw_menu_item,
38
- draw_ui_text,
39
- load_perk_menu_assets,
40
- menu_item_hit_rect,
41
- perk_menu_panel_slide_x,
42
- perk_menu_compute_layout,
43
- ui_origin,
44
- ui_scale,
45
- wrap_ui_text,
46
- )
28
+ from ..ui.perk_menu import PerkMenuAssets, draw_ui_text, load_perk_menu_assets
47
29
  from ..views.quest_title_overlay import draw_quest_title_overlay
48
30
  from ..weapons import WEAPON_BY_ID
49
- from .base_gameplay_mode import BaseGameplayMode, _clamp
31
+ from .base_gameplay_mode import BaseGameplayMode
32
+ from .components.perk_menu_controller import PerkMenuContext, PerkMenuController
50
33
 
51
34
  WORLD_SIZE = 1024.0
52
35
  QUEST_TITLE_FADE_IN_MS = 500.0
@@ -177,11 +160,7 @@ class QuestMode(BaseGameplayMode):
177
160
  self._perk_prompt_timer_ms = 0.0
178
161
  self._perk_prompt_hover = False
179
162
  self._perk_prompt_pulse = 0.0
180
- self._perk_menu_open = False
181
- self._perk_menu_selected = 0
182
- self._perk_menu_timeline_ms = 0.0
183
- self._perk_ui_layout = PerkMenuLayout()
184
- self._perk_cancel_button = UiButtonState("Cancel")
163
+ self._perk_menu = PerkMenuController(on_close=self._reset_perk_prompt)
185
164
 
186
165
  def open(self) -> None:
187
166
  super().open()
@@ -198,11 +177,7 @@ class QuestMode(BaseGameplayMode):
198
177
  self._perk_prompt_timer_ms = 0.0
199
178
  self._perk_prompt_hover = False
200
179
  self._perk_prompt_pulse = 0.0
201
- self._perk_menu_open = False
202
- self._perk_menu_selected = 0
203
- self._perk_menu_timeline_ms = 0.0
204
- self._perk_ui_layout = PerkMenuLayout()
205
- self._perk_cancel_button = UiButtonState("Cancel")
180
+ self._perk_menu.reset()
206
181
 
207
182
  def close(self) -> None:
208
183
  if self._grim_mono is not None:
@@ -211,6 +186,33 @@ class QuestMode(BaseGameplayMode):
211
186
  self._perk_menu_assets = None
212
187
  super().close()
213
188
 
189
+ def _reset_perk_prompt(self) -> None:
190
+ if int(self._state.perk_selection.pending_count) > 0:
191
+ # Reset the prompt swing so each pending perk replays the intro.
192
+ self._perk_prompt_timer_ms = 0.0
193
+ self._perk_prompt_hover = False
194
+ self._perk_prompt_pulse = 0.0
195
+
196
+ def _perk_menu_context(self) -> PerkMenuContext:
197
+ fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
198
+ fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
199
+ players = self._world.players
200
+ return PerkMenuContext(
201
+ state=self._state,
202
+ perk_state=self._state.perk_selection,
203
+ players=players,
204
+ creatures=self._creatures.entries,
205
+ player=self._player,
206
+ game_mode=int(GameMode.QUESTS),
207
+ player_count=len(players),
208
+ fx_toggle=fx_toggle,
209
+ fx_detail=fx_detail,
210
+ font=self._small,
211
+ assets=self._perk_menu_assets,
212
+ mouse=self._ui_mouse_pos(),
213
+ play_sfx=self._world.audio_router.play_sfx,
214
+ )
215
+
214
216
  def select_level(self, level: str | None) -> None:
215
217
  self._selected_level = level
216
218
 
@@ -301,15 +303,15 @@ class QuestMode(BaseGameplayMode):
301
303
  status.increment_quest_play_count(idx)
302
304
 
303
305
  def _handle_input(self) -> None:
304
- if self._perk_menu_open and rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
306
+ if self._perk_menu.open and rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
305
307
  self._world.audio_router.play_sfx("sfx_ui_buttonclick")
306
- self._close_perk_menu()
308
+ self._perk_menu.close()
307
309
  return
308
310
 
309
311
  if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
310
312
  self._paused = not self._paused
311
313
 
312
- if debug_enabled() and (not self._perk_menu_open):
314
+ if debug_enabled() and (not self._perk_menu.open):
313
315
  if rl.is_key_pressed(rl.KeyboardKey.KEY_F2):
314
316
  self._state.debug_god_mode = not bool(self._state.debug_god_mode)
315
317
  self._world.audio_router.play_sfx("sfx_ui_buttonclick")
@@ -414,139 +416,6 @@ class QuestMode(BaseGameplayMode):
414
416
  y = margin
415
417
  return rl.Rectangle(x, y, text_w, text_h)
416
418
 
417
- def _open_perk_menu(self) -> None:
418
- if self._perk_menu_open:
419
- return
420
- players = self._world.players
421
- choices = perk_selection_current_choices(
422
- self._state,
423
- players,
424
- self._state.perk_selection,
425
- game_mode=int(GameMode.QUESTS),
426
- player_count=len(players),
427
- )
428
- if not choices:
429
- self._perk_menu_open = False
430
- return
431
- self._world.audio_router.play_sfx("sfx_ui_panelclick")
432
- self._perk_menu_open = True
433
- self._perk_menu_selected = 0
434
-
435
- def _close_perk_menu(self) -> None:
436
- self._perk_menu_open = False
437
- if int(self._state.perk_selection.pending_count) > 0:
438
- # Reset the prompt swing so each pending perk replays the intro.
439
- self._perk_prompt_timer_ms = 0.0
440
- self._perk_prompt_hover = False
441
- self._perk_prompt_pulse = 0.0
442
-
443
- def _perk_menu_handle_input(self, dt_frame: float, dt_ms: float) -> None:
444
- if self._perk_menu_assets is None:
445
- self._close_perk_menu()
446
- return
447
-
448
- perk_state = self._state.perk_selection
449
- players = self._world.players
450
- choices = perk_selection_current_choices(
451
- self._state,
452
- players,
453
- perk_state,
454
- game_mode=int(GameMode.QUESTS),
455
- player_count=len(players),
456
- )
457
- if not choices:
458
- self._close_perk_menu()
459
- return
460
- if self._perk_menu_selected >= len(choices):
461
- self._perk_menu_selected = 0
462
-
463
- if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
464
- self._perk_menu_selected = (self._perk_menu_selected + 1) % len(choices)
465
- if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
466
- self._perk_menu_selected = (self._perk_menu_selected - 1) % len(choices)
467
-
468
- screen_w = float(rl.get_screen_width())
469
- screen_h = float(rl.get_screen_height())
470
- scale = ui_scale(screen_w, screen_h)
471
- origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
472
- slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
473
-
474
- mouse = self._ui_mouse_pos()
475
- click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
476
-
477
- master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
478
- expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
479
- computed = perk_menu_compute_layout(
480
- self._perk_ui_layout,
481
- screen_w=screen_w,
482
- origin_x=origin_x,
483
- origin_y=origin_y,
484
- scale=scale,
485
- choice_count=len(choices),
486
- expert_owned=expert_owned,
487
- master_owned=master_owned,
488
- panel_slide_x=slide_x,
489
- )
490
-
491
- fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
492
- for idx, perk_id in enumerate(choices):
493
- label = perk_display_name(int(perk_id), fx_toggle=fx_toggle)
494
- item_x = computed.list_x
495
- item_y = computed.list_y + float(idx) * computed.list_step_y
496
- rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
497
- if rl.check_collision_point_rec(mouse, rect):
498
- self._perk_menu_selected = idx
499
- if click:
500
- self._world.audio_router.play_sfx("sfx_ui_buttonclick")
501
- picked = perk_selection_pick(
502
- self._state,
503
- players,
504
- perk_state,
505
- idx,
506
- game_mode=int(GameMode.QUESTS),
507
- player_count=len(players),
508
- dt=dt_frame,
509
- creatures=self._creatures.entries,
510
- )
511
- if picked is not None:
512
- self._world.audio_router.play_sfx("sfx_ui_bonus")
513
- self._close_perk_menu()
514
- return
515
- break
516
-
517
- cancel_w = button_width(self._small, self._perk_cancel_button.label, scale=scale, force_wide=self._perk_cancel_button.force_wide)
518
- cancel_x = computed.cancel_x
519
- button_y = computed.cancel_y
520
-
521
- if button_update(
522
- self._perk_cancel_button,
523
- x=cancel_x,
524
- y=button_y,
525
- width=cancel_w,
526
- dt_ms=dt_ms,
527
- mouse=mouse,
528
- click=click,
529
- ):
530
- self._world.audio_router.play_sfx("sfx_ui_buttonclick")
531
- self._close_perk_menu()
532
- return
533
-
534
- if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) or rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
535
- self._world.audio_router.play_sfx("sfx_ui_buttonclick")
536
- picked = perk_selection_pick(
537
- self._state,
538
- players,
539
- perk_state,
540
- self._perk_menu_selected,
541
- game_mode=int(GameMode.QUESTS),
542
- player_count=len(players),
543
- dt=dt_frame,
544
- creatures=self._creatures.entries,
545
- )
546
- if picked is not None:
547
- self._world.audio_router.play_sfx("sfx_ui_bonus")
548
- self._close_perk_menu()
549
-
550
419
  def _close_failed_run(self) -> None:
551
420
  if self._outcome is None:
552
421
  fired = 0
@@ -584,7 +453,7 @@ class QuestMode(BaseGameplayMode):
584
453
  self.close_requested = True
585
454
 
586
455
  def _draw_perk_prompt(self) -> None:
587
- if self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3:
456
+ if self._perk_menu.active:
588
457
  return
589
458
  if not any(player.health > 0.0 for player in self._world.players):
590
459
  return
@@ -640,94 +509,6 @@ class QuestMode(BaseGameplayMode):
640
509
  rl.draw_texture_pro(tex, src, dst, origin, rot_deg, pulse_tint)
641
510
  rl.end_blend_mode()
642
511
 
643
- def _draw_perk_menu(self) -> None:
644
- menu_t = _clamp(self._perk_menu_timeline_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
645
- if menu_t <= 1e-3:
646
- return
647
- if self._perk_menu_assets is None:
648
- return
649
-
650
- perk_state = self._state.perk_selection
651
- players = self._world.players
652
- choices = perk_selection_current_choices(
653
- self._state,
654
- players,
655
- perk_state,
656
- game_mode=int(GameMode.QUESTS),
657
- player_count=len(players),
658
- )
659
- if not choices:
660
- return
661
-
662
- screen_w = float(rl.get_screen_width())
663
- screen_h = float(rl.get_screen_height())
664
- scale = ui_scale(screen_w, screen_h)
665
- origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
666
- slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
667
-
668
- master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
669
- expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
670
- computed = perk_menu_compute_layout(
671
- self._perk_ui_layout,
672
- screen_w=screen_w,
673
- origin_x=origin_x,
674
- origin_y=origin_y,
675
- scale=scale,
676
- choice_count=len(choices),
677
- expert_owned=expert_owned,
678
- master_owned=master_owned,
679
- panel_slide_x=slide_x,
680
- )
681
-
682
- panel_tex = self._perk_menu_assets.menu_panel
683
- if panel_tex is not None:
684
- fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
685
- draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
686
-
687
- title_tex = self._perk_menu_assets.title_pick_perk
688
- if title_tex is not None:
689
- src = rl.Rectangle(0.0, 0.0, float(title_tex.width), float(title_tex.height))
690
- rl.draw_texture_pro(title_tex, src, computed.title, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
691
-
692
- sponsor = None
693
- if master_owned:
694
- sponsor = "extra perks sponsored by the Perk Master"
695
- elif expert_owned:
696
- sponsor = "extra perk sponsored by the Perk Expert"
697
- if sponsor:
698
- draw_ui_text(self._small, sponsor, computed.sponsor_x, computed.sponsor_y, scale=scale, color=UI_SPONSOR_COLOR)
699
-
700
- mouse = self._ui_mouse_pos()
701
- fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
702
- for idx, perk_id in enumerate(choices):
703
- label = perk_display_name(int(perk_id), fx_toggle=fx_toggle)
704
- item_x = computed.list_x
705
- item_y = computed.list_y + float(idx) * computed.list_step_y
706
- rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
707
- hovered = rl.check_collision_point_rec(mouse, rect) or (idx == self._perk_menu_selected)
708
- draw_menu_item(self._small, label, x=item_x, y=item_y, scale=scale, hovered=hovered)
709
-
710
- selected = choices[self._perk_menu_selected]
711
- desc = perk_display_description(int(selected), fx_toggle=fx_toggle)
712
- desc_x = float(computed.desc.x)
713
- desc_y = float(computed.desc.y)
714
- desc_w = float(computed.desc.width)
715
- desc_h = float(computed.desc.height)
716
- desc_scale = scale * 0.85
717
- desc_lines = wrap_ui_text(self._small, desc, max_width=desc_w, scale=desc_scale)
718
- line_h = float(self._small.cell_size * desc_scale) if self._small is not None else float(20 * desc_scale)
719
- y = desc_y
720
- for line in desc_lines:
721
- if y + line_h > desc_y + desc_h:
722
- break
723
- draw_ui_text(self._small, line, desc_x, y, scale=desc_scale, color=UI_TEXT_COLOR)
724
- y += line_h
725
-
726
- cancel_w = button_width(self._small, self._perk_cancel_button.label, scale=scale, force_wide=self._perk_cancel_button.force_wide)
727
- cancel_x = computed.cancel_x
728
- button_y = computed.cancel_y
729
- button_draw(self._perk_menu_assets, self._small, self._perk_cancel_button, x=cancel_x, y=button_y, width=cancel_w, scale=scale)
730
-
731
512
  def update(self, dt: float) -> None:
732
513
  self._update_audio(dt)
733
514
 
@@ -743,10 +524,11 @@ class QuestMode(BaseGameplayMode):
743
524
  perk_pending = int(self._state.perk_selection.pending_count) > 0 and any_alive
744
525
 
745
526
  self._perk_prompt_hover = False
746
- if self._perk_menu_open:
747
- self._perk_menu_handle_input(dt_frame, dt_ui_ms)
527
+ perk_ctx = self._perk_menu_context()
528
+ if self._perk_menu.open:
529
+ self._perk_menu.handle_input(perk_ctx, dt_frame=dt_frame, dt_ui_ms=dt_ui_ms)
748
530
 
749
- perk_menu_active = self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3
531
+ perk_menu_active = self._perk_menu.active
750
532
 
751
533
  if (not perk_menu_active) and perk_pending and (not self._paused):
752
534
  label = self._perk_prompt_label()
@@ -765,29 +547,26 @@ class QuestMode(BaseGameplayMode):
765
547
 
766
548
  if input_code_is_pressed(pick_key) and (not input_code_is_down(fire_key)):
767
549
  self._perk_prompt_pulse = 1000.0
768
- self._open_perk_menu()
550
+ self._perk_menu.open_if_available(perk_ctx)
769
551
  elif self._perk_prompt_hover and input_code_is_pressed(fire_key):
770
552
  self._perk_prompt_pulse = 1000.0
771
- self._open_perk_menu()
553
+ self._perk_menu.open_if_available(perk_ctx)
772
554
 
773
- perk_menu_active = self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3
555
+ perk_menu_active = self._perk_menu.active
774
556
 
775
557
  if not self._paused:
776
558
  pulse_delta = dt_ui_ms * (6.0 if self._perk_prompt_hover else -2.0)
777
- self._perk_prompt_pulse = _clamp(self._perk_prompt_pulse + pulse_delta, 0.0, 1000.0)
559
+ self._perk_prompt_pulse = clamp(self._perk_prompt_pulse + pulse_delta, 0.0, 1000.0)
778
560
 
779
561
  prompt_active = perk_pending and (not perk_menu_active) and (not self._paused)
780
562
  if prompt_active:
781
- self._perk_prompt_timer_ms = _clamp(self._perk_prompt_timer_ms + dt_ui_ms, 0.0, PERK_PROMPT_MAX_TIMER_MS)
563
+ self._perk_prompt_timer_ms = clamp(self._perk_prompt_timer_ms + dt_ui_ms, 0.0, PERK_PROMPT_MAX_TIMER_MS)
782
564
  else:
783
- self._perk_prompt_timer_ms = _clamp(self._perk_prompt_timer_ms - dt_ui_ms, 0.0, PERK_PROMPT_MAX_TIMER_MS)
565
+ self._perk_prompt_timer_ms = clamp(self._perk_prompt_timer_ms - dt_ui_ms, 0.0, PERK_PROMPT_MAX_TIMER_MS)
784
566
 
785
- if self._perk_menu_open:
786
- self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms + dt_ui_ms, 0.0, PERK_MENU_TRANSITION_MS)
787
- else:
788
- self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms - dt_ui_ms, 0.0, PERK_MENU_TRANSITION_MS)
567
+ self._perk_menu.tick_timeline(dt_ui_ms)
789
568
 
790
- dt_world = 0.0 if self._paused or (not any_alive) or perk_menu_active else dt_frame
569
+ dt_world = 0.0 if self._paused or (not any_alive) or self._perk_menu.active else dt_frame
791
570
  if dt_world <= 0.0:
792
571
  if not any(player.health > 0.0 for player in self._world.players):
793
572
  self._close_failed_run()
@@ -823,6 +602,9 @@ class QuestMode(BaseGameplayMode):
823
602
  self._quest.spawn_timeline_ms = float(timeline_ms)
824
603
  self._quest.no_creatures_timer_ms = float(no_creatures_timer_ms)
825
604
 
605
+ detail_preset = 5
606
+ if self._world.config is not None:
607
+ detail_preset = int(self._world.config.data.get("detail_preset", 5) or 5)
826
608
  for call in spawns:
827
609
  self._creatures.spawn_template(
828
610
  int(call.template_id),
@@ -830,6 +612,8 @@ class QuestMode(BaseGameplayMode):
830
612
  float(call.heading),
831
613
  self._state.rng,
832
614
  rand=self._state.rng.rand,
615
+ state=self._state,
616
+ detail_preset=detail_preset,
833
617
  )
834
618
 
835
619
  completion_ms, completed = tick_quest_completion_transition(
@@ -876,7 +660,7 @@ class QuestMode(BaseGameplayMode):
876
660
  self.close_requested = True
877
661
 
878
662
  def draw(self) -> None:
879
- perk_menu_active = self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3
663
+ perk_menu_active = self._perk_menu.active
880
664
  self._world.draw(draw_aim_indicators=not perk_menu_active)
881
665
  self._draw_screen_fade()
882
666
 
@@ -889,6 +673,7 @@ class QuestMode(BaseGameplayMode):
889
673
  self._draw_target_health_bar()
890
674
  hud_bottom = draw_hud_overlay(
891
675
  self._hud_assets,
676
+ state=self._hud_state,
892
677
  player=self._player,
893
678
  players=self._world.players,
894
679
  bonus_hud=self._state.bonus_hud,
@@ -922,7 +707,7 @@ class QuestMode(BaseGameplayMode):
922
707
  self._draw_ui_text(warn, 24.0, warn_y, rl.Color(240, 80, 80, 255), scale=0.8)
923
708
 
924
709
  self._draw_perk_prompt()
925
- self._draw_perk_menu()
710
+ self._perk_menu.draw(self._perk_menu_context())
926
711
 
927
712
  if perk_menu_active:
928
713
  self._draw_game_cursor()
@@ -13,13 +13,13 @@ from grim.view import ViewContext
13
13
 
14
14
  from ..creatures.spawn import tick_rush_mode_spawns
15
15
  from ..game_modes import GameMode
16
- from ..gameplay import PlayerInput, most_used_weapon_id_for_player, weapon_assign_player
16
+ from ..gameplay import PlayerInput, weapon_assign_player
17
17
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
18
- from ..persistence.highscores import HighScoreRecord
19
18
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
20
19
  from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
21
20
  from ..ui.perk_menu import load_perk_menu_assets
22
21
  from .base_gameplay_mode import BaseGameplayMode
22
+ from .components.highscore_record_builder import build_highscore_record_for_game_over
23
23
 
24
24
  WORLD_SIZE = 1024.0
25
25
  RUSH_WEAPON_ID = 2
@@ -146,25 +146,14 @@ class RushMode(BaseGameplayMode):
146
146
  if self._game_over_active:
147
147
  return
148
148
 
149
- record = HighScoreRecord.blank()
150
- record.score_xp = int(self._player.experience)
151
- record.survival_elapsed_ms = int(self._rush.elapsed_ms)
152
- record.creature_kill_count = int(self._creatures.kill_count)
153
- weapon_id = most_used_weapon_id_for_player(self._state, player_index=int(self._player.index), fallback_weapon_id=int(self._player.weapon_id))
154
- record.most_used_weapon_id = int(weapon_id)
155
- fired = 0
156
- hit = 0
157
- try:
158
- fired = int(self._state.shots_fired[int(self._player.index)])
159
- hit = int(self._state.shots_hit[int(self._player.index)])
160
- except Exception:
161
- fired = 0
162
- hit = 0
163
- fired = max(0, int(fired))
164
- hit = max(0, min(int(hit), fired))
165
- record.shots_fired = fired
166
- record.shots_hit = hit
167
- record.game_mode_id = int(self._config.data.get("game_mode", int(GameMode.RUSH))) if self._config is not None else int(GameMode.RUSH)
149
+ game_mode_id = int(self._config.data.get("game_mode", int(GameMode.RUSH))) if self._config is not None else int(GameMode.RUSH)
150
+ record = build_highscore_record_for_game_over(
151
+ state=self._state,
152
+ player=self._player,
153
+ survival_elapsed_ms=int(self._rush.elapsed_ms),
154
+ creature_kill_count=int(self._creatures.kill_count),
155
+ game_mode_id=game_mode_id,
156
+ )
168
157
 
169
158
  self._game_over_record = record
170
159
  self._game_over_ui.open()
@@ -179,28 +168,7 @@ class RushMode(BaseGameplayMode):
179
168
  return
180
169
 
181
170
  if self._game_over_active:
182
- record = self._game_over_record
183
- if record is None:
184
- self._enter_game_over()
185
- record = self._game_over_record
186
- if record is not None:
187
- action = self._game_over_ui.update(
188
- dt,
189
- record=record,
190
- player_name_default=self._player_name_default(),
191
- play_sfx=self._world.audio_router.play_sfx,
192
- rand=self._state.rng.rand,
193
- mouse=self._ui_mouse_pos(),
194
- )
195
- if action == "play_again":
196
- self.open()
197
- return
198
- if action == "high_scores":
199
- self._action = "open_high_scores"
200
- return
201
- if action == "main_menu":
202
- self._action = "back_to_menu"
203
- self.close_requested = True
171
+ self._update_game_over_ui(dt)
204
172
  return
205
173
 
206
174
  any_alive = any(player.health > 0.0 for player in self._world.players)
@@ -265,6 +233,7 @@ class RushMode(BaseGameplayMode):
265
233
  self._draw_target_health_bar()
266
234
  hud_bottom = draw_hud_overlay(
267
235
  self._hud_assets,
236
+ state=self._hud_state,
268
237
  player=self._player,
269
238
  players=self._world.players,
270
239
  bonus_hud=self._state.bonus_hud,