crimsonland 0.1.0.dev11__py3-none-any.whl → 0.1.0.dev13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. crimson/assets_fetch.py +23 -8
  2. crimson/creatures/runtime.py +15 -0
  3. crimson/demo.py +47 -38
  4. crimson/effects.py +46 -1
  5. crimson/frontend/boot.py +2 -1
  6. crimson/frontend/high_scores_layout.py +26 -0
  7. crimson/frontend/menu.py +24 -43
  8. crimson/frontend/panels/base.py +27 -29
  9. crimson/frontend/panels/controls.py +152 -65
  10. crimson/frontend/panels/credits.py +221 -0
  11. crimson/frontend/panels/databases.py +307 -0
  12. crimson/frontend/panels/mods.py +1 -3
  13. crimson/frontend/panels/options.py +36 -42
  14. crimson/frontend/panels/play_game.py +82 -74
  15. crimson/frontend/panels/stats.py +255 -298
  16. crimson/frontend/pause_menu.py +425 -0
  17. crimson/game.py +512 -505
  18. crimson/gameplay.py +35 -6
  19. crimson/modes/base_gameplay_mode.py +3 -0
  20. crimson/modes/quest_mode.py +54 -44
  21. crimson/modes/rush_mode.py +4 -1
  22. crimson/modes/survival_mode.py +15 -10
  23. crimson/modes/tutorial_mode.py +15 -5
  24. crimson/modes/typo_mode.py +4 -1
  25. crimson/persistence/highscores.py +6 -2
  26. crimson/render/world_renderer.py +1 -1
  27. crimson/sim/world_state.py +8 -1
  28. crimson/typo/spawns.py +3 -4
  29. crimson/ui/demo_trial_overlay.py +3 -3
  30. crimson/ui/game_over.py +18 -2
  31. crimson/ui/menu_panel.py +127 -0
  32. crimson/ui/perk_menu.py +101 -44
  33. crimson/ui/quest_results.py +669 -0
  34. crimson/ui/shadow.py +39 -0
  35. crimson/views/particles.py +1 -1
  36. crimson/views/perk_menu_debug.py +2 -2
  37. crimson/views/perks.py +2 -2
  38. crimson/weapons.py +110 -110
  39. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/METADATA +1 -1
  40. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/RECORD +43 -36
  41. grim/app.py +3 -0
  42. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/WHEEL +0 -0
  43. {crimsonland-0.1.0.dev11.dist-info → crimsonland-0.1.0.dev13.dist-info}/entry_points.txt +0 -0
crimson/gameplay.py CHANGED
@@ -585,7 +585,7 @@ def _creature_find_in_radius(creatures: list[_CreatureForPerks], *, pos_x: float
585
585
  continue
586
586
 
587
587
  dist = math.hypot(float(creature.x) - pos_x, float(creature.y) - pos_y) - radius
588
- threshold = float(creature.size) * 0.142857149 + 3.0
588
+ threshold = float(creature.size) * 0.14285715 + 3.0
589
589
  if threshold < dist:
590
590
  continue
591
591
  if float(creature.hitbox_size) < 5.0:
@@ -1511,9 +1511,9 @@ def player_start_reload(player: PlayerState, state: GameplayState) -> None:
1511
1511
  player.reload_active = True
1512
1512
 
1513
1513
  if perk_active(player, PerkId.FASTLOADER):
1514
- reload_time *= 0.69999999
1514
+ reload_time *= 0.7
1515
1515
  if state.bonuses.weapon_power_up > 0.0:
1516
- reload_time *= 0.60000002
1516
+ reload_time *= 0.6
1517
1517
 
1518
1518
  player.reload_timer = max(0.0, reload_time)
1519
1519
  player.reload_timer_max = player.reload_timer
@@ -1618,7 +1618,14 @@ def _perk_update_fire_cough(player: PlayerState, dt: float, state: GameplayState
1618
1618
  state.perk_intervals.fire_cough = float(state.rng.rand() % 4) + 2.0
1619
1619
 
1620
1620
 
1621
- def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float, state: GameplayState) -> None:
1621
+ def player_fire_weapon(
1622
+ player: PlayerState,
1623
+ input_state: PlayerInput,
1624
+ dt: float,
1625
+ state: GameplayState,
1626
+ *,
1627
+ detail_preset: int = 5,
1628
+ ) -> None:
1622
1629
  dt = float(dt)
1623
1630
 
1624
1631
  weapon_id = int(player.weapon_id)
@@ -1689,6 +1696,20 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1689
1696
  aim_y = float(input_state.aim_y)
1690
1697
  dx = aim_x - float(player.pos_x)
1691
1698
  dy = aim_y - float(player.pos_y)
1699
+ aim_heading = math.atan2(dy, dx) + math.pi / 2.0
1700
+
1701
+ muzzle_dir = (aim_heading - math.pi / 2.0) - 0.150915
1702
+ muzzle_shell_x = player.pos_x + math.cos(muzzle_dir) * 16.0
1703
+ muzzle_shell_y = player.pos_y + math.sin(muzzle_dir) * 16.0
1704
+ state.effects.spawn_shell_casing(
1705
+ pos_x=muzzle_shell_x,
1706
+ pos_y=muzzle_shell_y,
1707
+ aim_heading=aim_heading,
1708
+ weapon_flags=int(weapon.flags or 0),
1709
+ rand=state.rng.rand,
1710
+ detail_preset=int(detail_preset),
1711
+ )
1712
+
1692
1713
  dist = math.hypot(dx, dy)
1693
1714
  max_offset = dist * float(player.spread_heat) * 0.5
1694
1715
  dir_angle = float(int(state.rng.rand()) & 0x1FF) * (math.tau / 512.0)
@@ -1885,7 +1906,15 @@ def player_fire_weapon(player: PlayerState, input_state: PlayerInput, dt: float,
1885
1906
  player_start_reload(player, state)
1886
1907
 
1887
1908
 
1888
- def player_update(player: PlayerState, input_state: PlayerInput, dt: float, state: GameplayState, *, world_size: float = 1024.0) -> None:
1909
+ def player_update(
1910
+ player: PlayerState,
1911
+ input_state: PlayerInput,
1912
+ dt: float,
1913
+ state: GameplayState,
1914
+ *,
1915
+ detail_preset: int = 5,
1916
+ world_size: float = 1024.0,
1917
+ ) -> None:
1889
1918
  """Port of `player_update` (0x004136b0) for the rewrite runtime."""
1890
1919
 
1891
1920
  if dt <= 0.0:
@@ -2048,7 +2077,7 @@ def player_update(player: PlayerState, input_state: PlayerInput, dt: float, stat
2048
2077
  elif player.reload_timer == 0.0:
2049
2078
  player_start_reload(player, state)
2050
2079
 
2051
- player_fire_weapon(player, input_state, dt, state)
2080
+ player_fire_weapon(player, input_state, dt, state, detail_preset=int(detail_preset))
2052
2081
 
2053
2082
  while player.move_phase > 14.0:
2054
2083
  player.move_phase -= 14.0
@@ -283,6 +283,9 @@ class BaseGameplayMode:
283
283
  self._action = None
284
284
  return action
285
285
 
286
+ def draw_pause_background(self) -> None:
287
+ self._world.draw(draw_aim_indicators=False)
288
+
286
289
  def _draw_screen_fade(self) -> None:
287
290
  fade_alpha = 0.0
288
291
  if self._screen_fade is not None:
@@ -24,7 +24,9 @@ from ..quests.types import QuestContext, QuestDefinition, SpawnEntry
24
24
  from ..terrain_assets import terrain_texture_by_id
25
25
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
26
26
  from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
27
+ from ..ui.menu_panel import draw_classic_menu_panel
27
28
  from ..ui.perk_menu import (
29
+ PERK_MENU_TRANSITION_MS,
28
30
  PerkMenuAssets,
29
31
  PerkMenuLayout,
30
32
  UiButtonState,
@@ -32,10 +34,10 @@ from ..ui.perk_menu import (
32
34
  button_update,
33
35
  button_width,
34
36
  draw_menu_item,
35
- draw_menu_panel,
36
37
  draw_ui_text,
37
38
  load_perk_menu_assets,
38
39
  menu_item_hit_rect,
40
+ perk_menu_panel_slide_x,
39
41
  perk_menu_compute_layout,
40
42
  ui_origin,
41
43
  ui_scale,
@@ -78,8 +80,6 @@ PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
78
80
  PERK_PROMPT_TEXT_MARGIN_X = 16.0
79
81
  PERK_PROMPT_TEXT_OFFSET_Y = 8.0
80
82
 
81
- PERK_MENU_TRANSITION_MS = 500.0
82
-
83
83
 
84
84
  @dataclass(slots=True)
85
85
  class _QuestRunState:
@@ -306,7 +306,8 @@ class QuestMode(BaseGameplayMode):
306
306
  self._paused = not self._paused
307
307
 
308
308
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
309
- self.close_requested = True
309
+ self._action = "open_pause_menu"
310
+ return
310
311
 
311
312
  def _build_input(self):
312
313
  keybinds = config_keybinds(self._config)
@@ -439,8 +440,7 @@ class QuestMode(BaseGameplayMode):
439
440
  screen_h = float(rl.get_screen_height())
440
441
  scale = ui_scale(screen_w, screen_h)
441
442
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
442
- menu_t = _clamp(self._perk_menu_timeline_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
443
- slide_x = (menu_t - 1.0) * (self._perk_ui_layout.panel_w * scale)
443
+ slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
444
444
 
445
445
  mouse = self._ui_mouse_pos()
446
446
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
@@ -450,12 +450,13 @@ class QuestMode(BaseGameplayMode):
450
450
  computed = perk_menu_compute_layout(
451
451
  self._perk_ui_layout,
452
452
  screen_w=screen_w,
453
- origin_x=origin_x + slide_x,
453
+ origin_x=origin_x,
454
454
  origin_y=origin_y,
455
455
  scale=scale,
456
456
  choice_count=len(choices),
457
457
  expert_owned=expert_owned,
458
458
  master_owned=master_owned,
459
+ panel_slide_x=slide_x,
459
460
  )
460
461
 
461
462
  fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
@@ -517,6 +518,42 @@ class QuestMode(BaseGameplayMode):
517
518
  self._world.audio_router.play_sfx("sfx_ui_bonus")
518
519
  self._close_perk_menu()
519
520
 
521
+ def _close_failed_run(self) -> None:
522
+ if self._outcome is None:
523
+ fired = 0
524
+ hit = 0
525
+ try:
526
+ fired = int(self._state.shots_fired[int(self._player.index)])
527
+ hit = int(self._state.shots_hit[int(self._player.index)])
528
+ except Exception:
529
+ fired = 0
530
+ hit = 0
531
+ fired = max(0, int(fired))
532
+ hit = max(0, min(int(hit), fired))
533
+ most_used_weapon_id = most_used_weapon_id_for_player(
534
+ self._state,
535
+ player_index=int(self._player.index),
536
+ fallback_weapon_id=int(self._player.weapon_id),
537
+ )
538
+ player2_health = None
539
+ if len(self._world.players) >= 2:
540
+ player2_health = float(self._world.players[1].health)
541
+ self._outcome = QuestRunOutcome(
542
+ kind="failed",
543
+ level=str(self._quest.level),
544
+ base_time_ms=int(self._quest.spawn_timeline_ms),
545
+ player_health=float(self._player.health),
546
+ player2_health=player2_health,
547
+ pending_perk_count=int(self._state.perk_selection.pending_count),
548
+ experience=int(self._player.experience),
549
+ kill_count=int(self._creatures.kill_count),
550
+ weapon_id=int(self._player.weapon_id),
551
+ shots_fired=fired,
552
+ shots_hit=hit,
553
+ most_used_weapon_id=int(most_used_weapon_id),
554
+ )
555
+ self.close_requested = True
556
+
520
557
  def _draw_perk_prompt(self) -> None:
521
558
  if self._perk_menu_open or self._perk_menu_timeline_ms > 1e-3:
522
559
  return
@@ -597,24 +634,26 @@ class QuestMode(BaseGameplayMode):
597
634
  screen_h = float(rl.get_screen_height())
598
635
  scale = ui_scale(screen_w, screen_h)
599
636
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
600
- slide_x = (menu_t - 1.0) * (self._perk_ui_layout.panel_w * scale)
637
+ slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
601
638
 
602
639
  master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
603
640
  expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
604
641
  computed = perk_menu_compute_layout(
605
642
  self._perk_ui_layout,
606
643
  screen_w=screen_w,
607
- origin_x=origin_x + slide_x,
644
+ origin_x=origin_x,
608
645
  origin_y=origin_y,
609
646
  scale=scale,
610
647
  choice_count=len(choices),
611
648
  expert_owned=expert_owned,
612
649
  master_owned=master_owned,
650
+ panel_slide_x=slide_x,
613
651
  )
614
652
 
615
653
  panel_tex = self._perk_menu_assets.menu_panel
616
654
  if panel_tex is not None:
617
- draw_menu_panel(panel_tex, dst=computed.panel)
655
+ fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
656
+ draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
618
657
 
619
658
  title_tex = self._perk_menu_assets.title_pick_perk
620
659
  if title_tex is not None:
@@ -665,6 +704,8 @@ class QuestMode(BaseGameplayMode):
665
704
 
666
705
  dt_frame, dt_ui_ms = self._tick_frame(dt)
667
706
  self._handle_input()
707
+ if self._action == "open_pause_menu":
708
+ return
668
709
 
669
710
  if self.close_requested:
670
711
  return
@@ -719,6 +760,8 @@ class QuestMode(BaseGameplayMode):
719
760
 
720
761
  dt_world = 0.0 if self._paused or (not any_alive) or perk_menu_active else dt_frame
721
762
  if dt_world <= 0.0:
763
+ if not any(player.health > 0.0 for player in self._world.players):
764
+ self._close_failed_run()
722
765
  return
723
766
 
724
767
  self._quest.quest_name_timer_ms += dt_world * 1000.0
@@ -734,40 +777,7 @@ class QuestMode(BaseGameplayMode):
734
777
 
735
778
  any_alive_after = any(player.health > 0.0 for player in self._world.players)
736
779
  if not any_alive_after:
737
- if self._outcome is None:
738
- fired = 0
739
- hit = 0
740
- try:
741
- fired = int(self._state.shots_fired[int(self._player.index)])
742
- hit = int(self._state.shots_hit[int(self._player.index)])
743
- except Exception:
744
- fired = 0
745
- hit = 0
746
- fired = max(0, int(fired))
747
- hit = max(0, min(int(hit), fired))
748
- most_used_weapon_id = most_used_weapon_id_for_player(
749
- self._state,
750
- player_index=int(self._player.index),
751
- fallback_weapon_id=int(self._player.weapon_id),
752
- )
753
- player2_health = None
754
- if len(self._world.players) >= 2:
755
- player2_health = float(self._world.players[1].health)
756
- self._outcome = QuestRunOutcome(
757
- kind="failed",
758
- level=str(self._quest.level),
759
- base_time_ms=int(self._quest.spawn_timeline_ms),
760
- player_health=float(self._player.health),
761
- player2_health=player2_health,
762
- pending_perk_count=int(self._state.perk_selection.pending_count),
763
- experience=int(self._player.experience),
764
- kill_count=int(self._creatures.kill_count),
765
- weapon_id=int(self._player.weapon_id),
766
- shots_fired=fired,
767
- shots_hit=hit,
768
- most_used_weapon_id=int(most_used_weapon_id),
769
- )
770
- self.close_requested = True
780
+ self._close_failed_run()
771
781
  return
772
782
 
773
783
  creatures_none_active = not bool(self._creatures.iter_active())
@@ -95,7 +95,8 @@ class RushMode(BaseGameplayMode):
95
95
  self._paused = not self._paused
96
96
 
97
97
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
98
- self.close_requested = True
98
+ self._action = "open_pause_menu"
99
+ return
99
100
 
100
101
  def _build_input(self) -> PlayerInput:
101
102
  keybinds = config_keybinds(self._config)
@@ -174,6 +175,8 @@ class RushMode(BaseGameplayMode):
174
175
 
175
176
  dt_frame = self._tick_frame(dt)[0]
176
177
  self._handle_input()
178
+ if self._action == "open_pause_menu":
179
+ return
177
180
 
178
181
  if self._game_over_active:
179
182
  record = self._game_over_record
@@ -19,18 +19,20 @@ from ..persistence.highscores import HighScoreRecord
19
19
  from ..perks import PerkId, perk_display_description, perk_display_name
20
20
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
21
21
  from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
22
+ from ..ui.menu_panel import draw_classic_menu_panel
22
23
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
23
24
  from ..ui.perk_menu import (
25
+ PERK_MENU_TRANSITION_MS,
24
26
  PerkMenuLayout,
25
27
  UiButtonState,
26
28
  button_draw,
27
29
  button_update,
28
30
  button_width,
29
- draw_menu_panel,
30
31
  draw_menu_item,
31
32
  draw_ui_text,
32
33
  load_perk_menu_assets,
33
34
  menu_item_hit_rect,
35
+ perk_menu_panel_slide_x,
34
36
  perk_menu_compute_layout,
35
37
  ui_origin,
36
38
  ui_scale,
@@ -69,8 +71,6 @@ PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
69
71
  PERK_PROMPT_TEXT_MARGIN_X = 16.0
70
72
  PERK_PROMPT_TEXT_OFFSET_Y = 8.0
71
73
 
72
- PERK_MENU_TRANSITION_MS = 500.0
73
-
74
74
 
75
75
  @dataclass(slots=True)
76
76
  class _SurvivalState:
@@ -186,7 +186,8 @@ class SurvivalMode(BaseGameplayMode):
186
186
  survival_check_level_up(self._player, self._state.perk_selection)
187
187
 
188
188
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
189
- self.close_requested = True
189
+ self._action = "open_pause_menu"
190
+ return
190
191
 
191
192
  def _build_input(self) -> PlayerInput:
192
193
  keybinds = config_keybinds(self._config)
@@ -357,8 +358,7 @@ class SurvivalMode(BaseGameplayMode):
357
358
  screen_h = float(rl.get_screen_height())
358
359
  scale = ui_scale(screen_w, screen_h)
359
360
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
360
- menu_t = _clamp(self._perk_menu_timeline_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
361
- slide_x = (menu_t - 1.0) * (self._perk_ui_layout.panel_w * scale)
361
+ slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
362
362
 
363
363
  mouse = self._ui_mouse_pos()
364
364
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
@@ -368,12 +368,13 @@ class SurvivalMode(BaseGameplayMode):
368
368
  computed = perk_menu_compute_layout(
369
369
  self._perk_ui_layout,
370
370
  screen_w=screen_w,
371
- origin_x=origin_x + slide_x,
371
+ origin_x=origin_x,
372
372
  origin_y=origin_y,
373
373
  scale=scale,
374
374
  choice_count=len(choices),
375
375
  expert_owned=expert_owned,
376
376
  master_owned=master_owned,
377
+ panel_slide_x=slide_x,
377
378
  )
378
379
 
379
380
  fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
@@ -441,6 +442,8 @@ class SurvivalMode(BaseGameplayMode):
441
442
  dt_frame, dt_ui_ms = self._tick_frame(dt)
442
443
  self._cursor_time += dt_frame
443
444
  self._handle_input()
445
+ if self._action == "open_pause_menu":
446
+ return
444
447
 
445
448
  if self._game_over_active:
446
449
  record = self._game_over_record
@@ -651,24 +654,26 @@ class SurvivalMode(BaseGameplayMode):
651
654
  screen_h = float(rl.get_screen_height())
652
655
  scale = ui_scale(screen_w, screen_h)
653
656
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
654
- slide_x = (menu_t - 1.0) * (self._perk_ui_layout.panel_w * scale)
657
+ slide_x = perk_menu_panel_slide_x(self._perk_menu_timeline_ms, width=self._perk_ui_layout.panel_w)
655
658
 
656
659
  master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
657
660
  expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
658
661
  computed = perk_menu_compute_layout(
659
662
  self._perk_ui_layout,
660
663
  screen_w=screen_w,
661
- origin_x=origin_x + slide_x,
664
+ origin_x=origin_x,
662
665
  origin_y=origin_y,
663
666
  scale=scale,
664
667
  choice_count=len(choices),
665
668
  expert_owned=expert_owned,
666
669
  master_owned=master_owned,
670
+ panel_slide_x=slide_x,
667
671
  )
668
672
 
669
673
  panel_tex = self._perk_menu_assets.menu_panel
670
674
  if panel_tex is not None:
671
- draw_menu_panel(panel_tex, dst=computed.panel)
675
+ fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
676
+ draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
672
677
 
673
678
  title_tex = self._perk_menu_assets.title_pick_perk
674
679
  if title_tex is not None:
@@ -21,7 +21,9 @@ from ..perks import PerkId, perk_display_description, perk_display_name
21
21
  from ..tutorial.timeline import TutorialFrameActions, TutorialState, tick_tutorial_timeline
22
22
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
23
23
  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
24
25
  from ..ui.perk_menu import (
26
+ PERK_MENU_TRANSITION_MS,
25
27
  PerkMenuAssets,
26
28
  PerkMenuLayout,
27
29
  UiButtonState,
@@ -29,10 +31,10 @@ from ..ui.perk_menu import (
29
31
  button_update,
30
32
  button_width,
31
33
  draw_menu_item,
32
- draw_menu_panel,
33
34
  draw_ui_text,
34
35
  load_perk_menu_assets,
35
36
  menu_item_hit_rect,
37
+ perk_menu_panel_slide_x,
36
38
  perk_menu_compute_layout,
37
39
  ui_origin,
38
40
  ui_scale,
@@ -143,7 +145,8 @@ class TutorialMode(BaseGameplayMode):
143
145
  self._paused = not self._paused
144
146
 
145
147
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
146
- self.close_requested = True
148
+ self._action = "open_pause_menu"
149
+ return
147
150
 
148
151
  def _build_input(self) -> PlayerInput:
149
152
  keybinds = config_keybinds(self._config)
@@ -281,6 +284,8 @@ class TutorialMode(BaseGameplayMode):
281
284
  screen_h = float(rl.get_screen_height())
282
285
  scale = ui_scale(screen_w, screen_h)
283
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)
284
289
 
285
290
  mouse = self._ui_mouse_pos()
286
291
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
@@ -296,6 +301,7 @@ class TutorialMode(BaseGameplayMode):
296
301
  choice_count=len(choices),
297
302
  expert_owned=expert_owned,
298
303
  master_owned=master_owned,
304
+ panel_slide_x=slide_x,
299
305
  )
300
306
 
301
307
  fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
@@ -346,6 +352,8 @@ class TutorialMode(BaseGameplayMode):
346
352
  dt_frame, dt_ui_ms = self._tick_frame(dt, clamp_cursor_pulse=True)
347
353
 
348
354
  self._handle_input()
355
+ if self._action == "open_pause_menu":
356
+ return
349
357
  if self.close_requested:
350
358
  return
351
359
 
@@ -358,9 +366,9 @@ class TutorialMode(BaseGameplayMode):
358
366
  self._perk_menu_handle_input(dt_frame, dt_ui_ms)
359
367
 
360
368
  if self._perk_menu_open:
361
- self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms + dt_ui_ms, 0.0, 200.0)
369
+ self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms + dt_ui_ms, 0.0, PERK_MENU_TRANSITION_MS)
362
370
  else:
363
- self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms - dt_ui_ms, 0.0, 200.0)
371
+ self._perk_menu_timeline_ms = _clamp(self._perk_menu_timeline_ms - dt_ui_ms, 0.0, PERK_MENU_TRANSITION_MS)
364
372
 
365
373
  dt_world = 0.0 if self._paused or perk_menu_active else dt_frame
366
374
 
@@ -601,11 +609,13 @@ class TutorialMode(BaseGameplayMode):
601
609
  choice_count=len(choices),
602
610
  expert_owned=expert_owned,
603
611
  master_owned=master_owned,
612
+ panel_slide_x=slide_x,
604
613
  )
605
614
 
606
615
  panel_tex = assets.menu_panel
607
616
  if panel_tex is not None:
608
- draw_menu_panel(panel_tex, dst=computed.panel)
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)
609
619
 
610
620
  title_tex = assets.title_pick_perk
611
621
  if title_tex is not None:
@@ -120,7 +120,8 @@ class TypoShooterMode(BaseGameplayMode):
120
120
  self._paused = not self._paused
121
121
 
122
122
  if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
123
- self.close_requested = True
123
+ self._action = "open_pause_menu"
124
+ return
124
125
 
125
126
  def _active_mask(self) -> list[bool]:
126
127
  return [bool(entry.active) for entry in self._creatures.entries]
@@ -257,6 +258,8 @@ class TypoShooterMode(BaseGameplayMode):
257
258
 
258
259
  dt_frame = self._tick_frame(dt)[0]
259
260
  self._handle_input()
261
+ if self._action == "open_pause_menu":
262
+ return
260
263
 
261
264
  if self._game_over_active:
262
265
  self._update_game_over_ui(dt)
@@ -239,7 +239,9 @@ def scores_path_for_mode(
239
239
  # Native `highscore_build_path` uses `questhc*.hi` when hardcore is OFF,
240
240
  # and `quest*.hi` when hardcore is ON.
241
241
  prefix = "quest" if hardcore else "questhc"
242
- return root / f"{prefix}{int(quest_stage_major)}_{int(quest_stage_minor)}.hi"
242
+ major = int(quest_stage_major)
243
+ minor = int(quest_stage_minor)
244
+ return root / f"{prefix}{major}_{minor}.hi"
243
245
  return root / "unknown.hi"
244
246
 
245
247
 
@@ -272,7 +274,9 @@ def scores_path_for_config(base_dir: Path, config: CrimsonConfig, *, quest_stage
272
274
  # Native `highscore_build_path` uses `questhc*.hi` when hardcore is OFF,
273
275
  # and `quest*.hi` when hardcore is ON.
274
276
  prefix = "quest" if hardcore else "questhc"
275
- return root / f"{prefix}{int(quest_stage_major)}_{int(quest_stage_minor)}.hi"
277
+ major = int(quest_stage_major)
278
+ minor = int(quest_stage_minor)
279
+ return root / f"{prefix}{major}_{minor}.hi"
276
280
  return root / "unknown.hi"
277
281
 
278
282
 
@@ -947,7 +947,7 @@ class WorldRenderer:
947
947
  if float(getattr(creature, "hitbox_size", 0.0)) <= 5.0:
948
948
  continue
949
949
  d = math.hypot(float(creature.x) - pos_x, float(creature.y) - pos_y)
950
- threshold = float(creature.size) * 0.142857149 + 3.0
950
+ threshold = float(creature.size) * 0.14285715 + 3.0
951
951
  if d - radius < threshold:
952
952
  targets.append(creature)
953
953
 
@@ -290,7 +290,14 @@ class WorldState:
290
290
 
291
291
  for idx, player in enumerate(self.players):
292
292
  input_state = inputs[idx] if idx < len(inputs) else PlayerInput()
293
- player_update(player, input_state, dt, self.state, world_size=float(world_size))
293
+ player_update(
294
+ player,
295
+ input_state,
296
+ dt,
297
+ self.state,
298
+ detail_preset=int(detail_preset),
299
+ world_size=float(world_size),
300
+ )
294
301
 
295
302
  if dt > 0.0:
296
303
  self._advance_creature_anim(dt)
crimson/typo/spawns.py CHANGED
@@ -47,9 +47,9 @@ def tick_typo_spawns(
47
47
  y = math.cos(t) * 256.0 + float(world_height) * 0.5
48
48
 
49
49
  tint_t = float(elapsed_ms + 1)
50
- tint_r = _clamp(tint_t * 0.0000083333334 + 0.30000001, 0.0, 1.0)
51
- tint_g = _clamp(tint_t * 10000.0 + 0.30000001, 0.0, 1.0)
52
- tint_b = _clamp(math.sin(tint_t * 0.0001) + 0.30000001, 0.0, 1.0)
50
+ tint_r = _clamp(tint_t * 0.0000083333334 + 0.3, 0.0, 1.0)
51
+ tint_g = _clamp(tint_t * 10000.0 + 0.3, 0.0, 1.0)
52
+ tint_b = _clamp(math.sin(tint_t * 0.0001) + 0.3, 0.0, 1.0)
53
53
  tint = (tint_r, tint_g, tint_b, 1.0)
54
54
 
55
55
  spawns.append(
@@ -70,4 +70,3 @@ def tick_typo_spawns(
70
70
  )
71
71
 
72
72
  return cooldown, spawns
73
-
@@ -66,13 +66,14 @@ class DemoTrialOverlayUi:
66
66
 
67
67
  if self._assets is None:
68
68
  cursor = cache.get_or_load("ui_cursor", "ui/ui_cursor.jaz").texture
69
- button_md = cache.get_or_load("ui_button_md", "ui/ui_button_145x32.jaz").texture
69
+ button_sm = cache.get_or_load("ui_buttonSm", "ui/ui_button_64x32.jaz").texture
70
+ button_md = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
70
71
  self._assets = PerkMenuAssets(
71
72
  menu_panel=None,
72
73
  title_pick_perk=None,
73
74
  title_level_up=None,
74
75
  menu_item=None,
75
- button_sm=None,
76
+ button_sm=button_sm,
76
77
  button_md=button_md,
77
78
  cursor=cursor,
78
79
  aim=None,
@@ -232,4 +233,3 @@ class DemoTrialOverlayUi:
232
233
  scale=float(scale),
233
234
  )
234
235
  cursor_draw(assets, mouse=rl.get_mouse_position(), scale=1.0, alpha=1.0)
235
-
crimson/ui/game_over.py CHANGED
@@ -31,6 +31,7 @@ from .perk_menu import (
31
31
  draw_ui_text,
32
32
  load_perk_menu_assets,
33
33
  )
34
+ from .shadow import UI_SHADOW_OFFSET, draw_ui_quad_shadow
34
35
 
35
36
 
36
37
  UI_BASE_WIDTH = 640.0
@@ -568,9 +569,24 @@ class GameOverUi:
568
569
 
569
570
  # Panel background
570
571
  if self.assets.menu_panel is not None:
571
- src = rl.Rectangle(0.0, 0.0, float(self.assets.menu_panel.width), float(self.assets.menu_panel.height))
572
+ panel_tex = self.assets.menu_panel
573
+ src = rl.Rectangle(0.0, 0.0, float(panel_tex.width), float(panel_tex.height))
572
574
  dst = rl.Rectangle(panel.x, panel.y, panel.width, panel.height)
573
- rl.draw_texture_pro(self.assets.menu_panel, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
575
+ fx_detail = bool(int(getattr(self.config, "data", {}).get("fx_detail_0", 0) or 0))
576
+ if fx_detail:
577
+ draw_ui_quad_shadow(
578
+ texture=panel_tex,
579
+ src=src,
580
+ dst=rl.Rectangle(
581
+ float(dst.x + UI_SHADOW_OFFSET),
582
+ float(dst.y + UI_SHADOW_OFFSET),
583
+ float(dst.width),
584
+ float(dst.height),
585
+ ),
586
+ origin=rl.Vector2(0.0, 0.0),
587
+ rotation_deg=0.0,
588
+ )
589
+ rl.draw_texture_pro(panel_tex, src, dst, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
574
590
 
575
591
  # Banner (Reaper / Well done)
576
592
  banner = self.assets.text_reaper if banner_kind == "reaper" else self.assets.text_well_done