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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
crimson/game.py CHANGED
@@ -51,6 +51,19 @@ from .demo_trial import (
51
51
  )
52
52
  from .frontend.boot import BootView
53
53
  from .frontend.assets import MenuAssets, _ensure_texture_cache, load_menu_assets
54
+ from .frontend.high_scores_layout import (
55
+ HS_BACK_BUTTON_X,
56
+ HS_BACK_BUTTON_Y,
57
+ HS_BUTTON_STEP_Y,
58
+ HS_BUTTON_X,
59
+ HS_BUTTON_Y0,
60
+ HS_LEFT_PANEL_HEIGHT,
61
+ HS_LEFT_PANEL_POS_X,
62
+ HS_LEFT_PANEL_POS_Y,
63
+ HS_RIGHT_PANEL_HEIGHT,
64
+ HS_RIGHT_PANEL_POS_X,
65
+ HS_RIGHT_PANEL_POS_Y,
66
+ )
54
67
  from .frontend.menu import (
55
68
  MENU_PANEL_HEIGHT,
56
69
  MENU_PANEL_OFFSET_X,
@@ -72,6 +85,8 @@ from .frontend.menu import (
72
85
  )
73
86
  from .frontend.panels.base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
74
87
  from .frontend.panels.controls import ControlsMenuView
88
+ from .frontend.panels.credits import CreditsView
89
+ from .frontend.panels.databases import UnlockedPerksDatabaseView, UnlockedWeaponsDatabaseView
75
90
  from .frontend.panels.mods import ModsMenuView
76
91
  from .frontend.panels.options import OptionsMenuView
77
92
  from .frontend.panels.play_game import PlayGameMenuView
@@ -80,7 +95,8 @@ from .frontend.pause_menu import PauseMenuView
80
95
  from .frontend.transitions import _draw_screen_fade, _update_screen_fade
81
96
  from .persistence.save_status import GameStatus, ensure_game_status
82
97
  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
98
+ from .ui.menu_panel import draw_classic_menu_panel
99
+ from .ui.perk_menu import UiButtonState, UiButtonTextureSet, button_draw, button_update, button_width
84
100
  from .paths import default_runtime_dir
85
101
  from .assets_fetch import download_missing_paqs
86
102
 
@@ -144,6 +160,7 @@ AUTOEXEC_NAME = "autoexec.txt"
144
160
 
145
161
  QUEST_MENU_BASE_X = -5.0
146
162
  QUEST_MENU_BASE_Y = 185.0
163
+ QUEST_MENU_PANEL_OFFSET_X = -63.0
147
164
 
148
165
  QUEST_TITLE_X_OFFSET = 219.0 # 300 + 64 - 145
149
166
  QUEST_TITLE_Y_OFFSET = 44.0 # 40 + 4
@@ -171,7 +188,7 @@ QUEST_HARDCORE_LIST_Y_SHIFT = 10.0
171
188
 
172
189
  QUEST_BACK_BUTTON_X_OFFSET = 138.0
173
190
  QUEST_BACK_BUTTON_Y_OFFSET = 212.0
174
- QUEST_PANEL_HEIGHT = 379.0
191
+ QUEST_PANEL_HEIGHT = 378.0
175
192
 
176
193
 
177
194
  class QuestsMenuView:
@@ -719,10 +736,10 @@ class QuestsMenuView:
719
736
  if panel is None:
720
737
  return
721
738
  fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
722
- draw_menu_panel(
739
+ draw_classic_menu_panel(
723
740
  panel,
724
741
  dst=rl.Rectangle(
725
- float(QUEST_MENU_BASE_X + MENU_PANEL_OFFSET_X),
742
+ float(QUEST_MENU_BASE_X + QUEST_MENU_PANEL_OFFSET_X),
726
743
  float(QUEST_MENU_BASE_Y + MENU_PANEL_OFFSET_Y + self._widescreen_y_shift),
727
744
  float(MENU_PANEL_WIDTH),
728
745
  float(QUEST_PANEL_HEIGHT),
@@ -1531,14 +1548,19 @@ class QuestFailedView:
1531
1548
  class HighScoresView:
1532
1549
  def __init__(self, state: GameState) -> None:
1533
1550
  self._state = state
1551
+ self._assets: MenuAssets | None = None
1534
1552
  self._ground: GroundRenderer | None = None
1535
1553
  self._action: str | None = None
1536
1554
  self._cursor_pulse_time = 0.0
1555
+ self._widescreen_y_shift = 0.0
1556
+ self._timeline_ms = 0
1557
+ self._timeline_max_ms = PANEL_TIMELINE_START_MS
1537
1558
  self._small_font: SmallFontData | None = None
1538
1559
  self._button_tex: rl.Texture2D | None = None
1539
1560
  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)
1561
+ self._update_button = UiButtonState("Update scores", force_wide=True)
1562
+ self._play_button = UiButtonState("Play a game", force_wide=True)
1563
+ self._back_button = UiButtonState("Back", force_wide=False)
1542
1564
 
1543
1565
  self._request: HighScoresRequest | None = None
1544
1566
  self._records: list = []
@@ -1547,14 +1569,20 @@ class HighScoresView:
1547
1569
  def open(self) -> None:
1548
1570
  from .persistence.highscores import read_highscore_table, scores_path_for_mode
1549
1571
 
1572
+ layout_w = float(self._state.config.screen_width)
1573
+ self._widescreen_y_shift = MenuView._menu_widescreen_y_shift(layout_w)
1550
1574
  self._action = None
1575
+ self._assets = load_menu_assets(self._state)
1551
1576
  self._ground = None if self._state.pause_background is not None else ensure_menu_ground(self._state)
1552
1577
  self._cursor_pulse_time = 0.0
1578
+ self._timeline_ms = 0
1579
+ self._timeline_max_ms = PANEL_TIMELINE_START_MS
1553
1580
  self._small_font = None
1554
1581
  self._scroll_index = 0
1555
1582
  self._button_textures = None
1556
- self._back_button = UiButtonState("Back", force_wide=True)
1557
- self._main_menu_button = UiButtonState("Main menu", force_wide=True)
1583
+ self._update_button = UiButtonState("Update scores", force_wide=True)
1584
+ self._play_button = UiButtonState("Play a game", force_wide=True)
1585
+ self._back_button = UiButtonState("Back", force_wide=False)
1558
1586
 
1559
1587
  cache = _ensure_texture_cache(self._state)
1560
1588
  self._button_tex = cache.get_or_load("ui_buttonMd", "ui/ui_button_128x32.jaz").texture
@@ -1595,12 +1623,18 @@ class HighScoresView:
1595
1623
  if self._small_font is not None:
1596
1624
  rl.unload_texture(self._small_font.texture)
1597
1625
  self._small_font = None
1626
+ self._assets = None
1598
1627
  self._button_tex = None
1599
1628
  self._button_textures = None
1600
1629
  self._request = None
1601
1630
  self._records = []
1602
1631
  self._scroll_index = 0
1603
1632
 
1633
+ def _panel_top_left(self, *, pos_x: float, pos_y: float, scale: float) -> tuple[float, float]:
1634
+ x0 = float(pos_x + MENU_PANEL_OFFSET_X * scale)
1635
+ y0 = float(pos_y + self._widescreen_y_shift + MENU_PANEL_OFFSET_Y * scale)
1636
+ return x0, y0
1637
+
1604
1638
  def update(self, dt: float) -> None:
1605
1639
  if self._state.audio is not None:
1606
1640
  update_audio(self._state.audio, dt)
@@ -1608,62 +1642,83 @@ class HighScoresView:
1608
1642
  self._ground.process_pending()
1609
1643
  self._cursor_pulse_time += min(dt, 0.1) * 1.1
1610
1644
 
1611
- if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
1645
+ dt_ms = int(min(float(dt), 0.1) * 1000.0)
1646
+ if dt_ms > 0:
1647
+ self._timeline_ms = min(self._timeline_max_ms, int(self._timeline_ms + dt_ms))
1648
+
1649
+ enabled = self._timeline_ms >= self._timeline_max_ms
1650
+
1651
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE) and enabled:
1612
1652
  if self._state.audio is not None:
1613
1653
  play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1614
1654
  self._action = "back_to_previous"
1615
1655
  return
1616
1656
 
1617
1657
  textures = self._button_textures
1618
- if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
1658
+ if enabled and textures is not None and (textures.button_sm is not None or textures.button_md is not None):
1619
1659
  scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
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
1622
- gap_x = 18.0 * scale
1623
- x0 = 32.0
1624
- y0 = float(rl.get_screen_height()) - button_h - 52.0 * scale
1660
+ panel_x0, panel_y0 = self._panel_top_left(pos_x=HS_LEFT_PANEL_POS_X, pos_y=HS_LEFT_PANEL_POS_Y, scale=scale)
1661
+
1662
+ x0 = panel_x0 + HS_BUTTON_X * scale
1663
+ y0 = panel_y0 + HS_BUTTON_Y0 * scale
1625
1664
  mouse = rl.get_mouse_position()
1626
1665
  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):
1666
+ w = button_width(None, self._update_button.label, scale=scale, force_wide=self._update_button.force_wide)
1667
+ if button_update(self._update_button, x=x0, y=y0, width=w, dt_ms=dt_ms, mouse=mouse, click=click):
1668
+ # Reload scores from disk (no view transition).
1629
1669
  if self._state.audio is not None:
1630
1670
  play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1631
- self._action = "back_to_previous"
1671
+ self.open()
1632
1672
  return
1673
+ w = button_width(None, self._play_button.label, scale=scale, force_wide=self._play_button.force_wide)
1633
1674
  if button_update(
1634
- self._main_menu_button,
1635
- x=x0 + button_w + gap_x,
1636
- y=y0,
1637
- width=button_w,
1675
+ self._play_button,
1676
+ x=x0,
1677
+ y=y0 + HS_BUTTON_STEP_Y * scale,
1678
+ width=w,
1638
1679
  dt_ms=dt_ms,
1639
1680
  mouse=mouse,
1640
1681
  click=click,
1641
1682
  ):
1642
1683
  if self._state.audio is not None:
1643
1684
  play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1644
- self._action = "back_to_menu"
1685
+ self._action = "open_play_game"
1686
+ return
1687
+ back_w = button_width(None, self._back_button.label, scale=scale, force_wide=self._back_button.force_wide)
1688
+ if button_update(
1689
+ self._back_button,
1690
+ x=panel_x0 + HS_BACK_BUTTON_X * scale,
1691
+ y=panel_y0 + HS_BACK_BUTTON_Y * scale,
1692
+ width=back_w,
1693
+ dt_ms=dt_ms,
1694
+ mouse=mouse,
1695
+ click=click,
1696
+ ):
1697
+ if self._state.audio is not None:
1698
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
1699
+ self._action = "back_to_previous"
1645
1700
  return
1646
1701
 
1647
- font = self._ensure_small_font()
1648
- rows = self._visible_rows(font)
1702
+ rows = 10
1649
1703
  max_scroll = max(0, len(self._records) - rows)
1650
1704
 
1651
- wheel = int(rl.get_mouse_wheel_move())
1652
- if wheel:
1653
- self._scroll_index = max(0, min(max_scroll, int(self._scroll_index) - wheel))
1654
-
1655
- if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
1656
- self._scroll_index = max(0, int(self._scroll_index) - 1)
1657
- if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
1658
- self._scroll_index = min(max_scroll, int(self._scroll_index) + 1)
1659
- if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
1660
- self._scroll_index = max(0, int(self._scroll_index) - rows)
1661
- if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
1662
- self._scroll_index = min(max_scroll, int(self._scroll_index) + rows)
1663
- if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
1664
- self._scroll_index = 0
1665
- if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
1666
- self._scroll_index = max_scroll
1705
+ if enabled:
1706
+ wheel = int(rl.get_mouse_wheel_move())
1707
+ if wheel:
1708
+ self._scroll_index = max(0, min(max_scroll, int(self._scroll_index) - wheel))
1709
+
1710
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
1711
+ self._scroll_index = max(0, int(self._scroll_index) - 1)
1712
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
1713
+ self._scroll_index = min(max_scroll, int(self._scroll_index) + 1)
1714
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_UP):
1715
+ self._scroll_index = max(0, int(self._scroll_index) - rows)
1716
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_PAGE_DOWN):
1717
+ self._scroll_index = min(max_scroll, int(self._scroll_index) + rows)
1718
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_HOME):
1719
+ self._scroll_index = 0
1720
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_END):
1721
+ self._scroll_index = max_scroll
1667
1722
 
1668
1723
  def draw(self) -> None:
1669
1724
  rl.clear_background(rl.BLACK)
@@ -1674,6 +1729,10 @@ class HighScoresView:
1674
1729
  self._ground.draw(0.0, 0.0)
1675
1730
  _draw_screen_fade(self._state)
1676
1731
 
1732
+ assets = self._assets
1733
+ if assets is None or assets.panel is None:
1734
+ return
1735
+
1677
1736
  font = self._ensure_small_font()
1678
1737
  request = self._request
1679
1738
  mode_id = int(request.game_mode_id) if request is not None else int(self._state.config.data.get("game_mode", 1) or 1)
@@ -1681,26 +1740,64 @@ class HighScoresView:
1681
1740
  quest_minor = int(request.quest_stage_minor) if request is not None else 0
1682
1741
  highlight_rank = request.highlight_rank if request is not None else None
1683
1742
 
1684
- title = "High scores"
1685
- subtitle = self._mode_label(mode_id, quest_major, quest_minor)
1686
- draw_small_text(font, title, 32.0, 120.0, 1.2, rl.Color(235, 235, 235, 255))
1687
- draw_small_text(font, subtitle, 32.0, 152.0, 1.0, rl.Color(190, 190, 200, 255))
1743
+ scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
1744
+ fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
1745
+ panel_w = MENU_PANEL_WIDTH * scale
1746
+ _angle_rad, left_slide_x = MenuView._ui_element_anim(
1747
+ self,
1748
+ index=1,
1749
+ start_ms=PANEL_TIMELINE_START_MS,
1750
+ end_ms=PANEL_TIMELINE_END_MS,
1751
+ width=panel_w,
1752
+ direction_flag=0,
1753
+ )
1754
+ _angle_rad, right_slide_x = MenuView._ui_element_anim(
1755
+ self,
1756
+ index=2,
1757
+ start_ms=PANEL_TIMELINE_START_MS,
1758
+ end_ms=PANEL_TIMELINE_END_MS,
1759
+ width=panel_w,
1760
+ direction_flag=1,
1761
+ )
1762
+
1763
+ left_x0, left_y0 = self._panel_top_left(pos_x=HS_LEFT_PANEL_POS_X, pos_y=HS_LEFT_PANEL_POS_Y, scale=scale)
1764
+ right_x0, right_y0 = self._panel_top_left(pos_x=HS_RIGHT_PANEL_POS_X, pos_y=HS_RIGHT_PANEL_POS_Y, scale=scale)
1765
+ left_x0 += float(left_slide_x)
1766
+ right_x0 += float(right_slide_x)
1767
+
1768
+ draw_classic_menu_panel(
1769
+ assets.panel,
1770
+ dst=rl.Rectangle(left_x0, left_y0, panel_w, HS_LEFT_PANEL_HEIGHT * scale),
1771
+ tint=rl.WHITE,
1772
+ shadow=fx_detail,
1773
+ )
1774
+ draw_classic_menu_panel(
1775
+ assets.panel,
1776
+ dst=rl.Rectangle(right_x0, right_y0, panel_w, HS_RIGHT_PANEL_HEIGHT * scale),
1777
+ tint=rl.WHITE,
1778
+ shadow=fx_detail,
1779
+ )
1780
+
1781
+ title = "High scores - Quests" if int(mode_id) == 3 else f"High scores - {self._mode_label(mode_id, quest_major, quest_minor)}"
1782
+ draw_small_text(font, title, left_x0 + 269.0 * scale, left_y0 + 41.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
1783
+ if int(mode_id) == 3:
1784
+ quest_label = f"{int(quest_major)}.{int(quest_minor)}: {self._quest_title(quest_major, quest_minor)}"
1785
+ draw_small_text(font, quest_label, left_x0 + 236.0 * scale, left_y0 + 63.0 * scale, 1.0 * scale, rl.Color(255, 255, 255, 255))
1688
1786
 
1689
1787
  header_color = rl.Color(255, 255, 255, int(255 * 0.85))
1690
- row_y0 = 188.0
1691
- draw_small_text(font, "Rank", 32.0, row_y0, 1.0, header_color)
1692
- draw_small_text(font, "Name", 96.0, row_y0, 1.0, header_color)
1693
- score_label = "Score" if mode_id not in (2, 3) else "Time"
1694
- draw_small_text(font, score_label, 320.0, row_y0, 1.0, header_color)
1788
+ row_y0 = left_y0 + 84.0 * scale
1789
+ draw_small_text(font, "Rank", left_x0 + 211.0 * scale, row_y0, 1.0 * scale, header_color)
1790
+ draw_small_text(font, "Score", left_x0 + 246.0 * scale, row_y0, 1.0 * scale, header_color)
1791
+ draw_small_text(font, "Player", left_x0 + 302.0 * scale, row_y0, 1.0 * scale, header_color)
1695
1792
 
1696
- row_step = float(font.cell_size)
1697
- rows = self._visible_rows(font)
1793
+ row_step = 16.0 * scale
1794
+ rows = 10
1698
1795
  start = max(0, int(self._scroll_index))
1699
1796
  end = min(len(self._records), start + rows)
1700
- y = row_y0 + row_step
1797
+ y = left_y0 + 103.0 * scale
1701
1798
 
1702
1799
  if start >= end:
1703
- draw_small_text(font, "No scores yet.", 32.0, y + 8.0, 1.0, rl.Color(190, 190, 200, 255))
1800
+ draw_small_text(font, "No scores yet.", left_x0 + 211.0 * scale, y + 8.0 * scale, 1.0 * scale, rl.Color(190, 190, 200, 255))
1704
1801
  else:
1705
1802
  for idx in range(start, end):
1706
1803
  entry = self._records[idx]
@@ -1714,44 +1811,82 @@ class HighScoresView:
1714
1811
  if len(name) > 16:
1715
1812
  name = name[:16]
1716
1813
 
1717
- value = ""
1718
- if mode_id in (2, 3):
1719
- seconds = float(int(getattr(entry, "survival_elapsed_ms", 0))) * 0.001
1720
- value = f"{seconds:7.2f}s"
1721
- else:
1722
- value = f"{int(getattr(entry, 'score_xp', 0)):7d}"
1814
+ value = f"{int(getattr(entry, 'score_xp', 0))}"
1723
1815
 
1724
1816
  color = rl.Color(255, 255, 255, int(255 * 0.7))
1725
1817
  if highlight_rank is not None and int(highlight_rank) == idx:
1726
1818
  color = rl.Color(255, 255, 255, 255)
1727
1819
 
1728
- draw_small_text(font, f"{idx + 1:>3}", 32.0, y, 1.0, color)
1729
- draw_small_text(font, name, 96.0, y, 1.0, color)
1730
- draw_small_text(font, value, 320.0, y, 1.0, color)
1820
+ draw_small_text(font, f"{idx + 1}", left_x0 + 216.0 * scale, y, 1.0 * scale, color)
1821
+ draw_small_text(font, value, left_x0 + 246.0 * scale, y, 1.0 * scale, color)
1822
+ draw_small_text(font, name, left_x0 + 304.0 * scale, y, 1.0 * scale, color)
1731
1823
  y += row_step
1732
1824
 
1733
1825
  textures = self._button_textures
1734
1826
  if textures is not None and (textures.button_sm is not None or textures.button_md is not None):
1735
- scale = 0.9 if float(self._state.config.screen_width) < 641.0 else 1.0
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
1738
- gap_x = 18.0 * scale
1739
- x0 = 32.0
1740
- y0 = float(rl.get_screen_height()) - button_h - 52.0 * scale
1741
- x1 = x0 + button_w + gap_x
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)
1827
+ button_x = left_x0 + HS_BUTTON_X * scale
1828
+ button_y0 = left_y0 + HS_BUTTON_Y0 * scale
1829
+ w = button_width(None, self._update_button.label, scale=scale, force_wide=self._update_button.force_wide)
1830
+ button_draw(textures, font, self._update_button, x=button_x, y=button_y0, width=w, scale=scale)
1831
+ w = button_width(None, self._play_button.label, scale=scale, force_wide=self._play_button.force_wide)
1832
+ button_draw(textures, font, self._play_button, x=button_x, y=button_y0 + HS_BUTTON_STEP_Y * scale, width=w, scale=scale)
1833
+ w = button_width(None, self._back_button.label, scale=scale, force_wide=self._back_button.force_wide)
1834
+ button_draw(
1835
+ textures,
1836
+ font,
1837
+ self._back_button,
1838
+ x=left_x0 + HS_BACK_BUTTON_X * scale,
1839
+ y=left_y0 + HS_BACK_BUTTON_Y * scale,
1840
+ width=w,
1841
+ scale=scale,
1842
+ )
1744
1843
 
1745
- draw_small_text(
1746
- font,
1747
- "UP/DOWN: Scroll PGUP/PGDN: Page ESC: Back",
1748
- 32.0,
1749
- float(rl.get_screen_height()) - 28.0,
1750
- 1.0,
1751
- rl.Color(190, 190, 200, 255),
1752
- )
1844
+ self._draw_sign(assets)
1753
1845
  _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
1754
1846
 
1847
+ def _draw_sign(self, assets: MenuAssets) -> None:
1848
+ if assets.sign is None:
1849
+ return
1850
+ sign = assets.sign
1851
+ screen_w = float(self._state.config.screen_width)
1852
+ sign_scale, shift_x = MenuView._sign_layout_scale(int(screen_w))
1853
+ pos_x = screen_w + MENU_SIGN_POS_X_PAD
1854
+ pos_y = MENU_SIGN_POS_Y if screen_w > MENU_SCALE_SMALL_THRESHOLD else MENU_SIGN_POS_Y_SMALL
1855
+ sign_w = MENU_SIGN_WIDTH * sign_scale
1856
+ sign_h = MENU_SIGN_HEIGHT * sign_scale
1857
+ offset_x = MENU_SIGN_OFFSET_X * sign_scale + shift_x
1858
+ offset_y = MENU_SIGN_OFFSET_Y * sign_scale
1859
+ rotation_deg = 0.0
1860
+ fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
1861
+ if fx_detail:
1862
+ MenuView._draw_ui_quad_shadow(
1863
+ texture=sign,
1864
+ src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
1865
+ dst=rl.Rectangle(pos_x + UI_SHADOW_OFFSET, pos_y + UI_SHADOW_OFFSET, sign_w, sign_h),
1866
+ origin=rl.Vector2(-offset_x, -offset_y),
1867
+ rotation_deg=rotation_deg,
1868
+ )
1869
+ MenuView._draw_ui_quad(
1870
+ texture=sign,
1871
+ src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
1872
+ dst=rl.Rectangle(pos_x, pos_y, sign_w, sign_h),
1873
+ origin=rl.Vector2(-offset_x, -offset_y),
1874
+ rotation_deg=rotation_deg,
1875
+ tint=rl.WHITE,
1876
+ )
1877
+
1878
+ @staticmethod
1879
+ def _quest_title(major: int, minor: int) -> str:
1880
+ try:
1881
+ from .quests import quest_by_level
1882
+
1883
+ q = quest_by_level(f"{int(major)}.{int(minor)}")
1884
+ if q is not None and q.title:
1885
+ return str(q.title)
1886
+ except Exception:
1887
+ pass
1888
+ return "???"
1889
+
1755
1890
  def take_action(self) -> str | None:
1756
1891
  action = self._action
1757
1892
  self._action = None
@@ -1817,6 +1952,9 @@ class GameLoopView:
1817
1952
  "open_options": OptionsMenuView(state),
1818
1953
  "open_controls": ControlsMenuView(state),
1819
1954
  "open_statistics": StatisticsMenuView(state),
1955
+ "open_weapon_database": UnlockedWeaponsDatabaseView(state),
1956
+ "open_perk_database": UnlockedPerksDatabaseView(state),
1957
+ "open_credits": CreditsView(state),
1820
1958
  "open_mods": ModsMenuView(state),
1821
1959
  "open_other_games": PanelMenuView(
1822
1960
  state,
@@ -1936,7 +2074,7 @@ class GameLoopView:
1936
2074
  if action is not None:
1937
2075
  view = self._front_views.get(action)
1938
2076
  if view is not None:
1939
- if action == "open_high_scores":
2077
+ if action in {"open_high_scores", "open_weapon_database", "open_perk_database", "open_credits"}:
1940
2078
  if (self._front_active in self._gameplay_views) and (self._state.pause_background is None):
1941
2079
  self._state.pause_background = self._front_active
1942
2080
  self._front_stack.append(self._front_active)
@@ -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:
@@ -440,8 +440,7 @@ class QuestMode(BaseGameplayMode):
440
440
  screen_h = float(rl.get_screen_height())
441
441
  scale = ui_scale(screen_w, screen_h)
442
442
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
443
- menu_t = _clamp(self._perk_menu_timeline_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
444
- 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)
445
444
 
446
445
  mouse = self._ui_mouse_pos()
447
446
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
@@ -451,12 +450,13 @@ class QuestMode(BaseGameplayMode):
451
450
  computed = perk_menu_compute_layout(
452
451
  self._perk_ui_layout,
453
452
  screen_w=screen_w,
454
- origin_x=origin_x + slide_x,
453
+ origin_x=origin_x,
455
454
  origin_y=origin_y,
456
455
  scale=scale,
457
456
  choice_count=len(choices),
458
457
  expert_owned=expert_owned,
459
458
  master_owned=master_owned,
459
+ panel_slide_x=slide_x,
460
460
  )
461
461
 
462
462
  fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
@@ -634,25 +634,26 @@ class QuestMode(BaseGameplayMode):
634
634
  screen_h = float(rl.get_screen_height())
635
635
  scale = ui_scale(screen_w, screen_h)
636
636
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
637
- 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)
638
638
 
639
639
  master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
640
640
  expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
641
641
  computed = perk_menu_compute_layout(
642
642
  self._perk_ui_layout,
643
643
  screen_w=screen_w,
644
- origin_x=origin_x + slide_x,
644
+ origin_x=origin_x,
645
645
  origin_y=origin_y,
646
646
  scale=scale,
647
647
  choice_count=len(choices),
648
648
  expert_owned=expert_owned,
649
649
  master_owned=master_owned,
650
+ panel_slide_x=slide_x,
650
651
  )
651
652
 
652
653
  panel_tex = self._perk_menu_assets.menu_panel
653
654
  if panel_tex is not None:
654
655
  fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
655
- draw_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
656
+ draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
656
657
 
657
658
  title_tex = self._perk_menu_assets.title_pick_perk
658
659
  if title_tex is not None:
@@ -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:
@@ -358,8 +358,7 @@ class SurvivalMode(BaseGameplayMode):
358
358
  screen_h = float(rl.get_screen_height())
359
359
  scale = ui_scale(screen_w, screen_h)
360
360
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
361
- menu_t = _clamp(self._perk_menu_timeline_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
362
- 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)
363
362
 
364
363
  mouse = self._ui_mouse_pos()
365
364
  click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
@@ -369,12 +368,13 @@ class SurvivalMode(BaseGameplayMode):
369
368
  computed = perk_menu_compute_layout(
370
369
  self._perk_ui_layout,
371
370
  screen_w=screen_w,
372
- origin_x=origin_x + slide_x,
371
+ origin_x=origin_x,
373
372
  origin_y=origin_y,
374
373
  scale=scale,
375
374
  choice_count=len(choices),
376
375
  expert_owned=expert_owned,
377
376
  master_owned=master_owned,
377
+ panel_slide_x=slide_x,
378
378
  )
379
379
 
380
380
  fx_toggle = int(self._config.data.get("fx_toggle", 0) or 0) if self._config is not None else 0
@@ -654,25 +654,26 @@ class SurvivalMode(BaseGameplayMode):
654
654
  screen_h = float(rl.get_screen_height())
655
655
  scale = ui_scale(screen_w, screen_h)
656
656
  origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
657
- 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)
658
658
 
659
659
  master_owned = int(self._player.perk_counts[int(PerkId.PERK_MASTER)]) > 0
660
660
  expert_owned = int(self._player.perk_counts[int(PerkId.PERK_EXPERT)]) > 0
661
661
  computed = perk_menu_compute_layout(
662
662
  self._perk_ui_layout,
663
663
  screen_w=screen_w,
664
- origin_x=origin_x + slide_x,
664
+ origin_x=origin_x,
665
665
  origin_y=origin_y,
666
666
  scale=scale,
667
667
  choice_count=len(choices),
668
668
  expert_owned=expert_owned,
669
669
  master_owned=master_owned,
670
+ panel_slide_x=slide_x,
670
671
  )
671
672
 
672
673
  panel_tex = self._perk_menu_assets.menu_panel
673
674
  if panel_tex is not None:
674
675
  fx_detail = bool(int(self._config.data.get("fx_detail_0", 0) or 0)) if self._config is not None else False
675
- draw_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
676
+ draw_classic_menu_panel(panel_tex, dst=computed.panel, shadow=fx_detail)
676
677
 
677
678
  title_tex = self._perk_menu_assets.title_pick_perk
678
679
  if title_tex is not None: