crimsonland 0.1.0.dev14__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 (73) hide show
  1. crimson/cli.py +63 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/debug.py +9 -0
  6. crimson/demo.py +38 -45
  7. crimson/effects.py +7 -13
  8. crimson/frontend/high_scores_layout.py +81 -0
  9. crimson/frontend/panels/base.py +4 -1
  10. crimson/frontend/panels/controls.py +0 -15
  11. crimson/frontend/panels/databases.py +291 -3
  12. crimson/frontend/panels/mods.py +0 -15
  13. crimson/frontend/panels/play_game.py +0 -16
  14. crimson/game.py +689 -3
  15. crimson/gameplay.py +921 -569
  16. crimson/modes/base_gameplay_mode.py +33 -12
  17. crimson/modes/components/__init__.py +2 -0
  18. crimson/modes/components/highscore_record_builder.py +58 -0
  19. crimson/modes/components/perk_menu_controller.py +325 -0
  20. crimson/modes/quest_mode.py +94 -272
  21. crimson/modes/rush_mode.py +12 -43
  22. crimson/modes/survival_mode.py +109 -330
  23. crimson/modes/tutorial_mode.py +46 -247
  24. crimson/modes/typo_mode.py +11 -38
  25. crimson/oracle.py +396 -0
  26. crimson/perks.py +5 -2
  27. crimson/player_damage.py +95 -36
  28. crimson/projectiles.py +539 -320
  29. crimson/render/projectile_draw_registry.py +637 -0
  30. crimson/render/projectile_render_registry.py +110 -0
  31. crimson/render/secondary_projectile_draw_registry.py +206 -0
  32. crimson/render/world_renderer.py +58 -707
  33. crimson/sim/world_state.py +118 -61
  34. crimson/typo/spawns.py +5 -12
  35. crimson/ui/demo_trial_overlay.py +3 -11
  36. crimson/ui/formatting.py +24 -0
  37. crimson/ui/game_over.py +12 -58
  38. crimson/ui/hud.py +72 -39
  39. crimson/ui/layout.py +20 -0
  40. crimson/ui/perk_menu.py +9 -34
  41. crimson/ui/quest_results.py +28 -70
  42. crimson/ui/text_input.py +20 -0
  43. crimson/views/_ui_helpers.py +27 -0
  44. crimson/views/aim_debug.py +15 -32
  45. crimson/views/animations.py +18 -28
  46. crimson/views/arsenal_debug.py +22 -32
  47. crimson/views/bonuses.py +23 -36
  48. crimson/views/camera_debug.py +16 -29
  49. crimson/views/camera_shake.py +9 -33
  50. crimson/views/corpse_stamp_debug.py +13 -21
  51. crimson/views/decals_debug.py +36 -23
  52. crimson/views/fonts.py +8 -25
  53. crimson/views/ground.py +4 -21
  54. crimson/views/lighting_debug.py +42 -45
  55. crimson/views/particles.py +33 -42
  56. crimson/views/perk_menu_debug.py +3 -10
  57. crimson/views/player.py +50 -44
  58. crimson/views/player_sprite_debug.py +24 -31
  59. crimson/views/projectile_fx.py +57 -52
  60. crimson/views/projectile_render_debug.py +24 -33
  61. crimson/views/projectiles.py +24 -37
  62. crimson/views/spawn_plan.py +13 -29
  63. crimson/views/sprites.py +14 -29
  64. crimson/views/terrain.py +6 -23
  65. crimson/views/ui.py +7 -24
  66. crimson/views/wicons.py +28 -33
  67. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  68. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +73 -62
  69. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +1 -1
  70. grim/config.py +29 -1
  71. grim/console.py +7 -10
  72. grim/math.py +12 -0
  73. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
@@ -10,45 +10,26 @@ from grim.assets import PaqTextureCache
10
10
  from grim.audio import AudioState
11
11
  from grim.console import ConsoleState
12
12
  from grim.config import CrimsonConfig
13
+ from grim.math import clamp
13
14
  from grim.view import ViewContext
14
15
 
15
- from ..bonuses import BonusId
16
16
  from ..creatures.runtime import CreatureFlags
17
17
  from ..game_modes import GameMode
18
- from ..gameplay import PlayerInput, perk_selection_current_choices, perk_selection_pick, survival_check_level_up, weapon_assign_player
18
+ from ..gameplay import PlayerInput, survival_check_level_up, weapon_assign_player
19
19
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
20
- from ..perks import PerkId, perk_display_description, perk_display_name
21
20
  from ..tutorial.timeline import TutorialFrameActions, TutorialState, tick_tutorial_timeline
22
21
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
23
22
  from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode, hud_ui_scale
24
- from ..ui.menu_panel import draw_classic_menu_panel
25
23
  from ..ui.perk_menu import (
26
- PERK_MENU_TRANSITION_MS,
27
24
  PerkMenuAssets,
28
- PerkMenuLayout,
29
25
  UiButtonState,
30
26
  button_draw,
31
27
  button_update,
32
28
  button_width,
33
- draw_menu_item,
34
- draw_ui_text,
35
29
  load_perk_menu_assets,
36
- menu_item_hit_rect,
37
- perk_menu_panel_slide_x,
38
- perk_menu_compute_layout,
39
- ui_origin,
40
- ui_scale,
41
- wrap_ui_text,
42
30
  )
43
31
  from .base_gameplay_mode import BaseGameplayMode
44
-
45
-
46
- def _clamp(value: float, lo: float, hi: float) -> float:
47
- if value < lo:
48
- return lo
49
- if value > hi:
50
- return hi
51
- return value
32
+ from .components.perk_menu_controller import PerkMenuContext, PerkMenuController
52
33
 
53
34
 
54
35
  UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
@@ -95,11 +76,7 @@ class TutorialMode(BaseGameplayMode):
95
76
  self._ui_assets: PerkMenuAssets | None = None
96
77
  self._ui_layout = _TutorialUiLayout()
97
78
 
98
- self._perk_ui_layout = PerkMenuLayout()
99
- self._perk_cancel_button = UiButtonState("Cancel")
100
- self._perk_menu_open = False
101
- self._perk_menu_selected = 0
102
- self._perk_menu_timeline_ms = 0.0
79
+ self._perk_menu = PerkMenuController()
103
80
 
104
81
  self._skip_button = UiButtonState("Skip tutorial", force_wide=True)
105
82
  self._play_button = UiButtonState("Play a game", force_wide=True)
@@ -111,11 +88,7 @@ class TutorialMode(BaseGameplayMode):
111
88
  if self._ui_assets.missing:
112
89
  self._missing_assets.extend(self._ui_assets.missing)
113
90
 
114
- self._perk_ui_layout = PerkMenuLayout()
115
- self._perk_cancel_button = UiButtonState("Cancel")
116
- self._perk_menu_open = False
117
- self._perk_menu_selected = 0
118
- self._perk_menu_timeline_ms = 0.0
91
+ self._perk_menu.reset()
119
92
 
120
93
  self._skip_button = UiButtonState("Skip tutorial", force_wide=True)
121
94
  self._play_button = UiButtonState("Play a game", force_wide=True)
@@ -136,9 +109,28 @@ class TutorialMode(BaseGameplayMode):
136
109
  self._ui_assets = None
137
110
  super().close()
138
111
 
112
+ def _perk_menu_context(self) -> PerkMenuContext:
113
+ fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
114
+ fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
115
+ return PerkMenuContext(
116
+ state=self._state,
117
+ perk_state=self._state.perk_selection,
118
+ players=[self._player],
119
+ creatures=self._creatures.entries,
120
+ player=self._player,
121
+ game_mode=int(GameMode.TUTORIAL),
122
+ player_count=1,
123
+ fx_toggle=fx_toggle,
124
+ fx_detail=fx_detail,
125
+ font=self._small,
126
+ assets=self._ui_assets,
127
+ mouse=self._ui_mouse_pos(),
128
+ play_sfx=None,
129
+ )
130
+
139
131
  def _handle_input(self) -> None:
140
- if self._perk_menu_open and rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
141
- self._perk_menu_open = False
132
+ if self._perk_menu.open and rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
133
+ self._perk_menu.close()
142
134
  return
143
135
 
144
136
  if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
@@ -214,7 +206,7 @@ class TutorialMode(BaseGameplayMode):
214
206
  self._play_button.enabled = prompt_alpha > 1e-3
215
207
  self._repeat_button.enabled = prompt_alpha > 1e-3
216
208
  else:
217
- skip_alpha = _clamp(float(self._tutorial.stage_timer_ms - 1000) * 0.001, 0.0, 1.0)
209
+ skip_alpha = clamp(float(self._tutorial.stage_timer_ms - 1000) * 0.001, 0.0, 1.0)
218
210
  self._skip_button.alpha = skip_alpha
219
211
  self._skip_button.enabled = skip_alpha > 1e-3
220
212
 
@@ -240,113 +232,6 @@ class TutorialMode(BaseGameplayMode):
240
232
  if button_update(self._skip_button, x=10.0, y=y, width=w, dt_ms=dt_ms, mouse=mouse, click=click):
241
233
  self.close_requested = True
242
234
 
243
- def _open_perk_menu(self) -> None:
244
- if self._ui_assets is None:
245
- return
246
- choices = perk_selection_current_choices(
247
- self._state,
248
- [self._player],
249
- self._state.perk_selection,
250
- game_mode=int(GameMode.TUTORIAL),
251
- player_count=1,
252
- )
253
- if not choices:
254
- self._perk_menu_open = False
255
- return
256
- self._perk_menu_open = True
257
- self._perk_menu_selected = 0
258
-
259
- def _perk_menu_handle_input(self, dt_frame: float, dt_ms: float) -> None:
260
- if self._ui_assets is None:
261
- self._perk_menu_open = False
262
- return
263
-
264
- perk_state = self._state.perk_selection
265
- choices = perk_selection_current_choices(
266
- self._state,
267
- [self._player],
268
- perk_state,
269
- game_mode=int(GameMode.TUTORIAL),
270
- player_count=1,
271
- )
272
- if not choices:
273
- self._perk_menu_open = False
274
- return
275
- if self._perk_menu_selected >= len(choices):
276
- self._perk_menu_selected = 0
277
-
278
- if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
279
- self._perk_menu_selected = (self._perk_menu_selected + 1) % len(choices)
280
- if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
281
- self._perk_menu_selected = (self._perk_menu_selected - 1) % len(choices)
282
-
283
- screen_w = float(rl.get_screen_width())
284
- screen_h = float(rl.get_screen_height())
285
- scale = ui_scale(screen_w, screen_h)
286
- origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
287
- slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
288
- slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
289
-
290
- mouse = self._ui_mouse_pos()
291
- click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
292
-
293
- master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
294
- expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
295
- computed = perk_menu_compute_layout(
296
- self._perk_ui_layout,
297
- screen_w=screen_w,
298
- origin_x=origin_x,
299
- origin_y=origin_y,
300
- scale=scale,
301
- choice_count=len(choices),
302
- expert_owned=expert_owned,
303
- master_owned=master_owned,
304
- panel_slide_x=slide_x,
305
- )
306
-
307
- fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
308
- for idx, perk_id in enumerate(choices):
309
- label = perk_display_name(int(perk_id), fx_toggle=fx_toggle)
310
- item_x = computed.list_x
311
- item_y = computed.list_y + float(idx) * computed.list_step_y
312
- rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
313
- if rl.check_collision_point_rec(mouse, rect):
314
- self._perk_menu_selected = idx
315
- if click:
316
- perk_selection_pick(
317
- self._state,
318
- [self._player],
319
- perk_state,
320
- idx,
321
- game_mode=int(GameMode.TUTORIAL),
322
- player_count=1,
323
- dt=dt_frame,
324
- creatures=self._creatures.entries,
325
- )
326
- self._perk_menu_open = False
327
- return
328
- break
329
-
330
- cancel_w = button_width(self._small, self._perk_cancel_button.label, scale=scale, force_wide=self._perk_cancel_button.force_wide)
331
- cancel_x = computed.cancel_x
332
- cancel_y = computed.cancel_y
333
- if button_update(self._perk_cancel_button, x=cancel_x, y=cancel_y, width=cancel_w, dt_ms=dt_ms, mouse=mouse, click=click):
334
- self._perk_menu_open = False
335
- return
336
-
337
- if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) or rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
338
- perk_selection_pick(
339
- self._state,
340
- [self._player],
341
- perk_state,
342
- self._perk_menu_selected,
343
- game_mode=int(GameMode.TUTORIAL),
344
- player_count=1,
345
- dt=dt_frame,
346
- creatures=self._creatures.entries,
347
- )
348
- self._perk_menu_open = False
349
-
350
235
  def update(self, dt: float) -> None:
351
236
  self._update_audio(dt)
352
237
  dt_frame, dt_ui_ms = self._tick_frame(dt, clamp_cursor_pulse=True)
@@ -357,18 +242,15 @@ class TutorialMode(BaseGameplayMode):
357
242
  if self.close_requested:
358
243
  return
359
244
 
245
+ perk_ctx = self._perk_menu_context()
360
246
  perk_pending = int(self._state.perk_selection.pending_count) > 0 and self._player.health > 0.0
361
- if int(self._tutorial.stage_index) == 6 and perk_pending and not self._perk_menu_open:
362
- self._open_perk_menu()
363
-
364
- perk_menu_active = self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3
365
- if self._perk_menu_open:
366
- self._perk_menu_handle_input(dt_frame, dt_ui_ms)
247
+ if int(self._tutorial.stage_index) == 6 and perk_pending and not self._perk_menu.open:
248
+ self._perk_menu.open_if_available(perk_ctx)
367
249
 
368
- if self._perk_menu_open:
369
- self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms + dt_ui_ms, 0.0, PERK_MENU_TRANSITION_MS)
370
- else:
371
- self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms - dt_ui_ms, 0.0, PERK_MENU_TRANSITION_MS)
250
+ perk_menu_active = self._perk_menu.active
251
+ if self._perk_menu.open:
252
+ self._perk_menu.handle_input(perk_ctx, dt_frame=dt_frame, dt_ui_ms=dt_ui_ms)
253
+ self._perk_menu.tick_timeline(dt_ui_ms)
372
254
 
373
255
  dt_world = 0.0 if self._paused or perk_menu_active else dt_frame
374
256
 
@@ -418,6 +300,10 @@ class TutorialMode(BaseGameplayMode):
418
300
  self._player.experience = int(actions.force_player_experience)
419
301
  survival_check_level_up(self._player, self._state.perk_selection)
420
302
 
303
+ detail_preset = 5
304
+ if self._world.config is not None:
305
+ detail_preset = int(self._world.config.data.get("detail_preset", 5) or 5)
306
+
421
307
  for call in actions.spawn_bonuses:
422
308
  spawned = self._state.bonus_pool.spawn_at(
423
309
  float(call.pos[0]),
@@ -433,7 +319,7 @@ class TutorialMode(BaseGameplayMode):
433
319
  pos_y=float(spawned.pos_y),
434
320
  count=12,
435
321
  rand=self._state.rng.rand,
436
- detail_preset=5,
322
+ detail_preset=detail_preset,
437
323
  )
438
324
 
439
325
  for call in actions.spawn_templates:
@@ -443,6 +329,8 @@ class TutorialMode(BaseGameplayMode):
443
329
  float(call.heading),
444
330
  self._state.rng,
445
331
  rand=self._state.rng.rand,
332
+ state=self._state,
333
+ detail_preset=detail_preset,
446
334
  )
447
335
  if int(call.template_id) == 0x27 and primary is not None and actions.stage5_bonus_carrier_drop is not None:
448
336
  drop_id, drop_amount = actions.stage5_bonus_carrier_drop
@@ -458,7 +346,7 @@ class TutorialMode(BaseGameplayMode):
458
346
  self._update_prompt_buttons(dt_ms=dt_ui_ms, mouse=mouse, click=click)
459
347
 
460
348
  def draw(self) -> None:
461
- perk_menu_active = self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3
349
+ perk_menu_active = self._perk_menu.active
462
350
  self._world.draw(draw_aim_indicators=not perk_menu_active)
463
351
  self._draw_screen_fade()
464
352
 
@@ -468,6 +356,7 @@ class TutorialMode(BaseGameplayMode):
468
356
  self._draw_target_health_bar()
469
357
  hud_bottom = draw_hud_overlay(
470
358
  self._hud_assets,
359
+ state=self._hud_state,
471
360
  player=self._player,
472
361
  players=self._world.players,
473
362
  bonus_hud=self._state.bonus_hud,
@@ -496,7 +385,7 @@ class TutorialMode(BaseGameplayMode):
496
385
  self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
497
386
 
498
387
  if perk_menu_active:
499
- self._draw_perk_menu()
388
+ self._perk_menu.draw(self._perk_menu_context())
500
389
  self._draw_menu_cursor()
501
390
  else:
502
391
  self._draw_aim_cursor()
@@ -538,14 +427,14 @@ class TutorialMode(BaseGameplayMode):
538
427
  self._draw_ui_text("paused (TAB)", x, y, UI_HINT_COLOR)
539
428
 
540
429
  def _draw_prompt_panel(self, text: str, *, alpha: float, y: float) -> None:
541
- alpha = _clamp(float(alpha), 0.0, 1.0)
430
+ alpha = clamp(float(alpha), 0.0, 1.0)
542
431
  rect, lines, line_h = self._prompt_panel_rect(text, y=float(y), scale=1.0)
543
432
  fill = rl.Color(0, 0, 0, int(255 * alpha * 0.8))
544
433
  border = rl.Color(255, 255, 255, int(255 * alpha))
545
434
  rl.draw_rectangle(int(rect.x), int(rect.y), int(rect.width), int(rect.height), fill)
546
435
  rl.draw_rectangle_lines(int(rect.x), int(rect.y), int(rect.width), int(rect.height), border)
547
436
 
548
- text_alpha = int(255 * _clamp(alpha * 0.9, 0.0, 1.0))
437
+ text_alpha = int(255 * clamp(alpha * 0.9, 0.0, 1.0))
549
438
  color = rl.Color(255, 255, 255, text_alpha)
550
439
  x = float(rect.x + self._ui_layout.panel_pad_x)
551
440
  line_y = float(rect.y + self._ui_layout.panel_pad_y)
@@ -577,93 +466,3 @@ class TutorialMode(BaseGameplayMode):
577
466
  x=float(self._ui_mouse_x),
578
467
  y=float(self._ui_mouse_y),
579
468
  )
580
-
581
- def _draw_perk_menu(self) -> None:
582
- assets = self._ui_assets
583
- if assets is None:
584
- return
585
- perk_state = self._state.perk_selection
586
- choices = perk_selection_current_choices(
587
- self._state,
588
- [self._player],
589
- perk_state,
590
- game_mode=int(GameMode.TUTORIAL),
591
- player_count=1,
592
- )
593
- if not choices:
594
- return
595
-
596
- screen_w = float(rl.get_screen_width())
597
- screen_h = float(rl.get_screen_height())
598
- scale = ui_scale(screen_w, screen_h)
599
- origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
600
-
601
- master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
602
- expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
603
- computed = perk_menu_compute_layout(
604
- self._perk_ui_layout,
605
- screen_w=screen_w,
606
- origin_x=origin_x,
607
- origin_y=origin_y,
608
- scale=scale,
609
- choice_count=len(choices),
610
- expert_owned=expert_owned,
611
- master_owned=master_owned,
612
- panel_slide_x=slide_x,
613
- )
614
-
615
- panel_tex = assets.menu_panel
616
- if panel_tex is not None:
617
- fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
618
- draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
619
-
620
- title_tex = assets.title_pick_perk
621
- if title_tex is not None:
622
- src = rl.Rectangle(0.0, 0.0, float(title_tex.width), float(title_tex.height))
623
- rl.draw_texture_pro(title_tex, src, computed.title, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
624
-
625
- sponsor = None
626
- if master_owned:
627
- sponsor = "extra perks sponsored by the Perk Master"
628
- elif expert_owned:
629
- sponsor = "extra perk sponsored by the Perk Expert"
630
- if sponsor:
631
- draw_ui_text(
632
- self._small,
633
- sponsor,
634
- computed.sponsor_x,
635
- computed.sponsor_y,
636
- scale=scale,
637
- color=UI_SPONSOR_COLOR,
638
- )
639
-
640
- mouse = self._ui_mouse_pos()
641
- fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
642
- for idx, perk_id in enumerate(choices):
643
- label = perk_display_name(int(perk_id), fx_toggle=fx_toggle)
644
- item_x = computed.list_x
645
- item_y = computed.list_y + float(idx) * computed.list_step_y
646
- rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
647
- hovered = rl.check_collision_point_rec(mouse, rect) or (idx == self._perk_menu_selected)
648
- draw_menu_item(self._small, label, x=item_x, y=item_y, scale=scale, hovered=hovered)
649
-
650
- selected = choices[self._perk_menu_selected]
651
- desc = perk_display_description(int(selected), fx_toggle=fx_toggle)
652
- desc_x = float(computed.desc.x)
653
- desc_y = float(computed.desc.y)
654
- desc_w = float(computed.desc.width)
655
- desc_h = float(computed.desc.height)
656
- desc_scale = scale * 0.85
657
- desc_lines = wrap_ui_text(self._small, desc, max_width=desc_w, scale=desc_scale)
658
- line_h = float(self._small.cell_size * desc_scale) if self._small is not None else float(20 * desc_scale)
659
- y = desc_y
660
- for line in desc_lines:
661
- if y + line_h > desc_y + desc_h:
662
- break
663
- draw_ui_text(self._small, line, desc_x, y, scale=desc_scale, color=UI_TEXT_COLOR)
664
- y += line_h
665
-
666
- cancel_w = button_width(self._small, self._perk_cancel_button.label, scale=scale, force_wide=self._perk_cancel_button.force_wide)
667
- cancel_x = computed.cancel_x
668
- cancel_y = computed.cancel_y
669
- button_draw(assets, self._small, self._perk_cancel_button, x=cancel_x, y=cancel_y, width=cancel_w, scale=scale)
@@ -14,9 +14,7 @@ from grim.view import ViewContext
14
14
 
15
15
  from ..creatures.spawn import CreatureFlags, CreatureInit, CreatureTypeId
16
16
  from ..game_modes import GameMode
17
- from ..gameplay import most_used_weapon_id_for_player
18
17
  from ..typo.player import build_typo_player_input, enforce_typo_player_frame
19
- from ..persistence.highscores import HighScoreRecord
20
18
  from ..typo.names import CreatureNameTable, load_typo_dictionary
21
19
  from ..typo.spawns import tick_typo_spawns
22
20
  from ..typo.typing import TypingBuffer
@@ -24,6 +22,7 @@ from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
24
22
  from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
25
23
  from ..ui.perk_menu import load_perk_menu_assets
26
24
  from .base_gameplay_mode import BaseGameplayMode
25
+ from .components.highscore_record_builder import build_highscore_record_for_game_over
27
26
 
28
27
  WORLD_SIZE = 1024.0
29
28
 
@@ -211,48 +210,21 @@ class TypoShooterMode(BaseGameplayMode):
211
210
  if self._game_over_active:
212
211
  return
213
212
 
214
- record = HighScoreRecord.blank()
215
- record.score_xp = int(self._player.experience)
216
- record.survival_elapsed_ms = int(self._typo.elapsed_ms)
217
- record.creature_kill_count = int(self._creatures.kill_count)
218
- weapon_id = most_used_weapon_id_for_player(
219
- self._state, player_index=int(self._player.index), fallback_weapon_id=int(self._player.weapon_id)
213
+ record = build_highscore_record_for_game_over(
214
+ state=self._state,
215
+ player=self._player,
216
+ survival_elapsed_ms=int(self._typo.elapsed_ms),
217
+ creature_kill_count=int(self._creatures.kill_count),
218
+ game_mode_id=int(GameMode.TYPO),
219
+ shots_fired=int(self._typing.shots_fired),
220
+ shots_hit=int(self._typing.shots_hit),
221
+ clamp_shots_hit=False,
220
222
  )
221
- record.most_used_weapon_id = int(weapon_id)
222
- record.shots_fired = int(self._typing.shots_fired)
223
- record.shots_hit = int(self._typing.shots_hit)
224
- record.game_mode_id = int(GameMode.TYPO)
225
223
 
226
224
  self._game_over_record = record
227
225
  self._game_over_ui.open()
228
226
  self._game_over_active = True
229
227
 
230
- def _update_game_over_ui(self, dt: float) -> None:
231
- record = self._game_over_record
232
- if record is None:
233
- self._enter_game_over()
234
- record = self._game_over_record
235
- if record is None:
236
- return
237
-
238
- action = self._game_over_ui.update(
239
- dt,
240
- record=record,
241
- player_name_default=self._player_name_default(),
242
- play_sfx=self._world.audio_router.play_sfx,
243
- rand=self._state.rng.rand,
244
- mouse=self._ui_mouse_pos(),
245
- )
246
- if action == "play_again":
247
- self.open()
248
- return
249
- if action == "high_scores":
250
- self._action = "open_high_scores"
251
- return
252
- if action == "main_menu":
253
- self._action = "back_to_menu"
254
- self.close_requested = True
255
-
256
228
  def update(self, dt: float) -> None:
257
229
  self._update_audio(dt)
258
230
 
@@ -444,6 +416,7 @@ class TypoShooterMode(BaseGameplayMode):
444
416
  self._draw_target_health_bar()
445
417
  draw_hud_overlay(
446
418
  self._hud_assets,
419
+ state=self._hud_state,
447
420
  player=self._player,
448
421
  players=self._world.players,
449
422
  bonus_hud=self._state.bonus_hud,