crimsonland 0.1.0.dev5__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 (139) hide show
  1. crimson/__init__.py +24 -0
  2. crimson/assets_fetch.py +60 -0
  3. crimson/atlas.py +92 -0
  4. crimson/audio_router.py +155 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +380 -0
  8. crimson/creatures/__init__.py +8 -0
  9. crimson/creatures/ai.py +186 -0
  10. crimson/creatures/anim.py +173 -0
  11. crimson/creatures/damage.py +103 -0
  12. crimson/creatures/runtime.py +1019 -0
  13. crimson/creatures/spawn.py +2871 -0
  14. crimson/debug.py +7 -0
  15. crimson/demo.py +1360 -0
  16. crimson/demo_trial.py +140 -0
  17. crimson/effects.py +1086 -0
  18. crimson/effects_atlas.py +73 -0
  19. crimson/frontend/__init__.py +1 -0
  20. crimson/frontend/assets.py +43 -0
  21. crimson/frontend/boot.py +424 -0
  22. crimson/frontend/menu.py +700 -0
  23. crimson/frontend/panels/__init__.py +1 -0
  24. crimson/frontend/panels/base.py +410 -0
  25. crimson/frontend/panels/controls.py +132 -0
  26. crimson/frontend/panels/mods.py +128 -0
  27. crimson/frontend/panels/options.py +409 -0
  28. crimson/frontend/panels/play_game.py +627 -0
  29. crimson/frontend/panels/stats.py +351 -0
  30. crimson/frontend/transitions.py +31 -0
  31. crimson/game.py +2533 -0
  32. crimson/game_modes.py +15 -0
  33. crimson/game_world.py +652 -0
  34. crimson/gameplay.py +2467 -0
  35. crimson/input_codes.py +176 -0
  36. crimson/modes/__init__.py +1 -0
  37. crimson/modes/base_gameplay_mode.py +219 -0
  38. crimson/modes/quest_mode.py +502 -0
  39. crimson/modes/rush_mode.py +300 -0
  40. crimson/modes/survival_mode.py +792 -0
  41. crimson/modes/tutorial_mode.py +648 -0
  42. crimson/modes/typo_mode.py +472 -0
  43. crimson/paths.py +23 -0
  44. crimson/perks.py +828 -0
  45. crimson/persistence/__init__.py +1 -0
  46. crimson/persistence/highscores.py +385 -0
  47. crimson/persistence/save_status.py +245 -0
  48. crimson/player_damage.py +77 -0
  49. crimson/projectiles.py +1133 -0
  50. crimson/quests/__init__.py +18 -0
  51. crimson/quests/helpers.py +147 -0
  52. crimson/quests/registry.py +49 -0
  53. crimson/quests/results.py +164 -0
  54. crimson/quests/runtime.py +91 -0
  55. crimson/quests/tier1.py +620 -0
  56. crimson/quests/tier2.py +652 -0
  57. crimson/quests/tier3.py +579 -0
  58. crimson/quests/tier4.py +721 -0
  59. crimson/quests/tier5.py +886 -0
  60. crimson/quests/timeline.py +115 -0
  61. crimson/quests/types.py +70 -0
  62. crimson/render/__init__.py +1 -0
  63. crimson/render/terrain_fx.py +88 -0
  64. crimson/render/world_renderer.py +1941 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +67 -0
  67. crimson/sim/world_state.py +422 -0
  68. crimson/terrain_assets.py +19 -0
  69. crimson/tutorial/__init__.py +12 -0
  70. crimson/tutorial/timeline.py +291 -0
  71. crimson/typo/__init__.py +2 -0
  72. crimson/typo/names.py +233 -0
  73. crimson/typo/player.py +43 -0
  74. crimson/typo/spawns.py +73 -0
  75. crimson/typo/typing.py +52 -0
  76. crimson/ui/__init__.py +3 -0
  77. crimson/ui/cursor.py +95 -0
  78. crimson/ui/demo_trial_overlay.py +235 -0
  79. crimson/ui/game_over.py +660 -0
  80. crimson/ui/hud.py +601 -0
  81. crimson/ui/perk_menu.py +388 -0
  82. crimson/views/__init__.py +40 -0
  83. crimson/views/aim_debug.py +276 -0
  84. crimson/views/animations.py +274 -0
  85. crimson/views/arsenal_debug.py +404 -0
  86. crimson/views/audio_bootstrap.py +47 -0
  87. crimson/views/bonuses.py +201 -0
  88. crimson/views/camera_debug.py +359 -0
  89. crimson/views/camera_shake.py +229 -0
  90. crimson/views/corpse_stamp_debug.py +324 -0
  91. crimson/views/decals_debug.py +739 -0
  92. crimson/views/empty.py +19 -0
  93. crimson/views/fonts.py +114 -0
  94. crimson/views/game_over.py +117 -0
  95. crimson/views/ground.py +259 -0
  96. crimson/views/lighting_debug.py +1166 -0
  97. crimson/views/particles.py +293 -0
  98. crimson/views/perk_menu_debug.py +430 -0
  99. crimson/views/perks.py +398 -0
  100. crimson/views/player.py +434 -0
  101. crimson/views/player_sprite_debug.py +314 -0
  102. crimson/views/projectile_fx.py +609 -0
  103. crimson/views/projectile_render_debug.py +393 -0
  104. crimson/views/projectiles.py +221 -0
  105. crimson/views/quest_title_overlay.py +108 -0
  106. crimson/views/registry.py +34 -0
  107. crimson/views/rush.py +16 -0
  108. crimson/views/small_font_debug.py +204 -0
  109. crimson/views/spawn_plan.py +363 -0
  110. crimson/views/sprites.py +214 -0
  111. crimson/views/survival.py +15 -0
  112. crimson/views/terrain.py +132 -0
  113. crimson/views/ui.py +123 -0
  114. crimson/views/wicons.py +166 -0
  115. crimson/weapon_sfx.py +63 -0
  116. crimson/weapons.py +860 -0
  117. crimsonland-0.1.0.dev5.dist-info/METADATA +9 -0
  118. crimsonland-0.1.0.dev5.dist-info/RECORD +139 -0
  119. crimsonland-0.1.0.dev5.dist-info/WHEEL +4 -0
  120. crimsonland-0.1.0.dev5.dist-info/entry_points.txt +4 -0
  121. grim/__init__.py +20 -0
  122. grim/app.py +92 -0
  123. grim/assets.py +231 -0
  124. grim/audio.py +106 -0
  125. grim/config.py +294 -0
  126. grim/console.py +737 -0
  127. grim/fonts/__init__.py +7 -0
  128. grim/fonts/grim_mono.py +111 -0
  129. grim/fonts/small.py +120 -0
  130. grim/input.py +44 -0
  131. grim/jaz.py +103 -0
  132. grim/math.py +17 -0
  133. grim/music.py +403 -0
  134. grim/paq.py +76 -0
  135. grim/rand.py +37 -0
  136. grim/sfx.py +276 -0
  137. grim/sfx_map.py +103 -0
  138. grim/terrain_render.py +840 -0
  139. grim/view.py +16 -0
@@ -0,0 +1,430 @@
1
+ from __future__ import annotations
2
+
3
+ import pyray as rl
4
+
5
+ from grim.fonts.small import SmallFontData, load_small_font, measure_small_text_width
6
+ from grim.view import View, ViewContext
7
+
8
+ from ..perks import PERK_BY_ID, PerkId, perk_display_description, perk_display_name
9
+ from ..ui.perk_menu import (
10
+ PerkMenuAssets,
11
+ PerkMenuLayout,
12
+ UiButtonState,
13
+ button_draw,
14
+ button_update,
15
+ button_width,
16
+ cursor_draw,
17
+ draw_menu_panel,
18
+ draw_menu_item,
19
+ draw_ui_text,
20
+ load_perk_menu_assets,
21
+ menu_item_hit_rect,
22
+ perk_menu_compute_layout,
23
+ ui_origin,
24
+ ui_scale,
25
+ wrap_ui_text,
26
+ )
27
+ from .registry import register_view
28
+
29
+
30
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
31
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
32
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
33
+ UI_SPONSOR_COLOR = rl.Color(255, 255, 255, int(255 * 0.5))
34
+
35
+ PERK_PROMPT_OUTSET_X = 50.0
36
+ PERK_PROMPT_BAR_SCALE = 0.75
37
+ PERK_PROMPT_BAR_BASE_OFFSET_X = -72.0
38
+ PERK_PROMPT_BAR_BASE_OFFSET_Y = -60.0
39
+ PERK_PROMPT_BAR_SHIFT_X = -300.0
40
+
41
+ PERK_PROMPT_LEVEL_UP_SCALE = 0.85
42
+ PERK_PROMPT_LEVEL_UP_BASE_OFFSET_X = -230.0
43
+ PERK_PROMPT_LEVEL_UP_BASE_OFFSET_Y = -27.0
44
+ PERK_PROMPT_LEVEL_UP_BASE_W = 75.0
45
+ PERK_PROMPT_LEVEL_UP_BASE_H = 25.0
46
+ PERK_PROMPT_LEVEL_UP_SHIFT_X = -46.0
47
+ PERK_PROMPT_LEVEL_UP_SHIFT_Y = -4.0
48
+
49
+ PERK_PROMPT_TEXT_MARGIN_X = 16.0
50
+ PERK_PROMPT_TEXT_OFFSET_Y = 8.0
51
+
52
+
53
+ class PerkMenuDebugView:
54
+ def __init__(self, ctx: ViewContext) -> None:
55
+ self._assets_root = ctx.assets_dir
56
+ self._missing_assets: list[str] = []
57
+ self._small: SmallFontData | None = None
58
+ self._assets: PerkMenuAssets | None = None
59
+ self._layout = PerkMenuLayout()
60
+
61
+ self._perk_ids = [
62
+ perk_id for perk_id in sorted(PERK_BY_ID.keys()) if perk_id != int(PerkId.ANTIPERK)
63
+ ]
64
+ self._choice_count = 6
65
+ self._selected = 0
66
+ self._expert_owned = False
67
+ self._master_owned = False
68
+ self._show_menu = True
69
+ self._show_prompt = True
70
+ self._panel_slide_x = 0.0
71
+ self._prompt_alpha = 1.0
72
+ self._prompt_pulse = 0.0
73
+ self._prompt_hover = False
74
+ self._prompt_rect: rl.Rectangle | None = None
75
+ self._cancel_button = UiButtonState("Cancel")
76
+ self._debug_overlay = True
77
+ self._show_prompt_rect = False
78
+
79
+ def open(self) -> None:
80
+ self._missing_assets.clear()
81
+ try:
82
+ self._small = load_small_font(self._assets_root, self._missing_assets)
83
+ except Exception:
84
+ self._small = None
85
+ self._assets = load_perk_menu_assets(self._assets_root)
86
+ if self._assets.missing:
87
+ self._missing_assets.extend(self._assets.missing)
88
+ rl.hide_cursor()
89
+
90
+ def close(self) -> None:
91
+ rl.show_cursor()
92
+ if self._assets is not None:
93
+ self._assets.unload()
94
+ self._assets = None
95
+ if self._small is not None:
96
+ rl.unload_texture(self._small.texture)
97
+ self._small = None
98
+
99
+ def _choices(self) -> list[int]:
100
+ if not self._perk_ids:
101
+ return []
102
+ count = max(1, min(self._choice_count, len(self._perk_ids)))
103
+ return self._perk_ids[:count]
104
+
105
+ def _prompt_label(self) -> str:
106
+ pending = max(1, int(self._choice_count))
107
+ suffix = f" ({pending})" if pending > 1 else ""
108
+ return f"Press Mouse2 to pick a perk{suffix}"
109
+
110
+ @staticmethod
111
+ def _perk_prompt_hinge() -> tuple[float, float]:
112
+ screen_w = float(rl.get_screen_width())
113
+ hinge_x = screen_w + PERK_PROMPT_OUTSET_X
114
+ hinge_y = 80.0 if int(screen_w) == 640 else 40.0
115
+ return hinge_x, hinge_y
116
+
117
+ def _perk_prompt_rect(self, label: str) -> rl.Rectangle:
118
+ hinge_x, hinge_y = self._perk_prompt_hinge()
119
+ if self._assets is not None and self._assets.menu_item is not None:
120
+ tex = self._assets.menu_item
121
+ bar_w = float(tex.width) * PERK_PROMPT_BAR_SCALE
122
+ bar_h = float(tex.height) * PERK_PROMPT_BAR_SCALE
123
+ local_x = (PERK_PROMPT_BAR_BASE_OFFSET_X + PERK_PROMPT_BAR_SHIFT_X) * PERK_PROMPT_BAR_SCALE
124
+ local_y = PERK_PROMPT_BAR_BASE_OFFSET_Y * PERK_PROMPT_BAR_SCALE
125
+ return rl.Rectangle(
126
+ float(hinge_x + local_x),
127
+ float(hinge_y + local_y),
128
+ float(bar_w),
129
+ float(bar_h),
130
+ )
131
+
132
+ text_w = float(_ui_text_width(self._small, label, 1.0))
133
+ text_h = 20.0
134
+ x = float(rl.get_screen_width()) - PERK_PROMPT_TEXT_MARGIN_X - text_w
135
+ y = hinge_y + PERK_PROMPT_TEXT_OFFSET_Y
136
+ return rl.Rectangle(x, y, text_w, text_h)
137
+
138
+ def _draw_perk_prompt(self) -> None:
139
+ if not self._show_prompt:
140
+ return
141
+ if self._assets is None:
142
+ return
143
+ label = self._prompt_label()
144
+ if not label:
145
+ return
146
+ alpha = max(0.0, min(self._prompt_alpha, 1.0))
147
+ if alpha <= 1e-3:
148
+ return
149
+
150
+ hinge_x, hinge_y = self._perk_prompt_hinge()
151
+ rot_deg = (1.0 - alpha) * 90.0
152
+ tint = rl.Color(255, 255, 255, int(255 * alpha))
153
+
154
+ text_w = float(_ui_text_width(self._small, label, 1.0))
155
+ x = float(rl.get_screen_width()) - PERK_PROMPT_TEXT_MARGIN_X - text_w
156
+ y = hinge_y + PERK_PROMPT_TEXT_OFFSET_Y
157
+ color = rl.Color(UI_TEXT_COLOR.r, UI_TEXT_COLOR.g, UI_TEXT_COLOR.b, int(255 * alpha))
158
+ draw_ui_text(self._small, label, x, y, scale=1.0, color=color)
159
+
160
+ if self._assets.menu_item is not None:
161
+ tex = self._assets.menu_item
162
+ bar_w = float(tex.width) * PERK_PROMPT_BAR_SCALE
163
+ bar_h = float(tex.height) * PERK_PROMPT_BAR_SCALE
164
+ local_x = (PERK_PROMPT_BAR_BASE_OFFSET_X + PERK_PROMPT_BAR_SHIFT_X) * PERK_PROMPT_BAR_SCALE
165
+ local_y = PERK_PROMPT_BAR_BASE_OFFSET_Y * PERK_PROMPT_BAR_SCALE
166
+ src = rl.Rectangle(float(tex.width), 0.0, -float(tex.width), float(tex.height))
167
+ dst = rl.Rectangle(float(hinge_x), float(hinge_y), float(bar_w), float(bar_h))
168
+ origin = rl.Vector2(float(-local_x), float(-local_y))
169
+ rl.draw_texture_pro(tex, src, dst, origin, rot_deg, tint)
170
+
171
+ if self._assets.title_level_up is not None:
172
+ tex = self._assets.title_level_up
173
+ local_x = PERK_PROMPT_LEVEL_UP_BASE_OFFSET_X * PERK_PROMPT_LEVEL_UP_SCALE + PERK_PROMPT_LEVEL_UP_SHIFT_X
174
+ local_y = PERK_PROMPT_LEVEL_UP_BASE_OFFSET_Y * PERK_PROMPT_LEVEL_UP_SCALE + PERK_PROMPT_LEVEL_UP_SHIFT_Y
175
+ w = PERK_PROMPT_LEVEL_UP_BASE_W * PERK_PROMPT_LEVEL_UP_SCALE
176
+ h = PERK_PROMPT_LEVEL_UP_BASE_H * PERK_PROMPT_LEVEL_UP_SCALE
177
+ pulse_alpha = (100.0 + float(int(self._prompt_pulse * 155.0 / 1000.0))) / 255.0
178
+ pulse_alpha = max(0.0, min(1.0, pulse_alpha))
179
+ label_alpha = max(0.0, min(1.0, alpha * pulse_alpha))
180
+ pulse_tint = rl.Color(255, 255, 255, int(255 * label_alpha))
181
+ src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
182
+ dst = rl.Rectangle(float(hinge_x), float(hinge_y), float(w), float(h))
183
+ origin = rl.Vector2(float(-local_x), float(-local_y))
184
+ rl.draw_texture_pro(tex, src, dst, origin, rot_deg, pulse_tint)
185
+ if label_alpha > 0.0:
186
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
187
+ rl.draw_texture_pro(tex, src, dst, origin, rot_deg, pulse_tint)
188
+ rl.end_blend_mode()
189
+
190
+ def update(self, dt: float) -> None:
191
+ dt_ms = float(min(dt, 0.1) * 1000.0)
192
+
193
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_F1):
194
+ self._debug_overlay = not self._debug_overlay
195
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_E):
196
+ self._expert_owned = not self._expert_owned
197
+ if not self._expert_owned:
198
+ self._master_owned = False
199
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_M):
200
+ self._master_owned = not self._master_owned
201
+ if self._master_owned:
202
+ self._expert_owned = True
203
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_O):
204
+ self._show_menu = not self._show_menu
205
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_P):
206
+ self._show_prompt = not self._show_prompt
207
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
208
+ self._show_prompt_rect = not self._show_prompt_rect
209
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
210
+ self._panel_slide_x = 0.0
211
+ self._choice_count = 6
212
+ self._selected = 0
213
+ self._expert_owned = False
214
+ self._master_owned = False
215
+ self._show_menu = True
216
+ self._show_prompt = True
217
+ self._prompt_pulse = 0.0
218
+ self._prompt_hover = False
219
+
220
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
221
+ self._choice_count = max(1, self._choice_count - 1)
222
+ self._selected = min(self._selected, self._choice_count - 1)
223
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
224
+ self._choice_count = min(len(self._perk_ids), self._choice_count + 1)
225
+
226
+ if self._show_menu and self._choice_count > 0:
227
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_DOWN):
228
+ self._selected = (self._selected + 1) % self._choice_count
229
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_UP):
230
+ self._selected = (self._selected - 1) % self._choice_count
231
+
232
+ step = 10.0
233
+ if rl.is_key_down(rl.KeyboardKey.KEY_LEFT_SHIFT) or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_SHIFT):
234
+ step = 40.0
235
+ if rl.is_key_down(rl.KeyboardKey.KEY_LEFT):
236
+ self._panel_slide_x -= step
237
+ if rl.is_key_down(rl.KeyboardKey.KEY_RIGHT):
238
+ self._panel_slide_x += step
239
+
240
+ self._panel_slide_x = _clamp(self._panel_slide_x, -self._layout.panel_w, 0.0)
241
+
242
+ self._prompt_hover = False
243
+ self._prompt_rect = None
244
+ if self._show_prompt:
245
+ label = self._prompt_label()
246
+ if label:
247
+ rect = self._perk_prompt_rect(label)
248
+ self._prompt_rect = rect
249
+ mouse = rl.get_mouse_position()
250
+ self._prompt_hover = rl.check_collision_point_rec(mouse, rect)
251
+
252
+ pulse_delta = dt_ms * (6.0 if self._prompt_hover else -2.0)
253
+ self._prompt_pulse = _clamp(self._prompt_pulse + pulse_delta, 0.0, 1000.0)
254
+
255
+ if not self._show_menu or self._assets is None:
256
+ return
257
+
258
+ choices = self._choices()
259
+ if not choices:
260
+ return
261
+ screen_w = float(rl.get_screen_width())
262
+ screen_h = float(rl.get_screen_height())
263
+ scale = ui_scale(screen_w, screen_h)
264
+ origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
265
+ computed = perk_menu_compute_layout(
266
+ self._layout,
267
+ screen_w=screen_w,
268
+ origin_x=origin_x,
269
+ origin_y=origin_y,
270
+ scale=scale,
271
+ choice_count=len(choices),
272
+ expert_owned=self._expert_owned,
273
+ master_owned=self._master_owned,
274
+ panel_slide_x=self._panel_slide_x,
275
+ )
276
+
277
+ mouse = rl.get_mouse_position()
278
+ click = rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT)
279
+ for idx, perk_id in enumerate(choices):
280
+ label = perk_display_name(int(perk_id))
281
+ item_x = computed.list_x
282
+ item_y = computed.list_y + float(idx) * computed.list_step_y
283
+ rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
284
+ if rl.check_collision_point_rec(mouse, rect):
285
+ self._selected = idx
286
+ break
287
+
288
+ cancel_w = button_width(self._small, self._cancel_button.label, scale=scale, force_wide=self._cancel_button.force_wide)
289
+ cancel_x = computed.cancel_x
290
+ button_y = computed.cancel_y
291
+ if button_update(
292
+ self._cancel_button,
293
+ x=cancel_x,
294
+ y=button_y,
295
+ width=cancel_w,
296
+ dt_ms=dt_ms,
297
+ mouse=mouse,
298
+ click=click,
299
+ ):
300
+ self._show_menu = False
301
+
302
+ def draw(self) -> None:
303
+ rl.clear_background(rl.Color(0, 0, 0, 255))
304
+ if self._missing_assets and self._debug_overlay:
305
+ draw_ui_text(
306
+ self._small,
307
+ "Missing assets: " + ", ".join(self._missing_assets),
308
+ 24.0,
309
+ 24.0,
310
+ scale=1.0,
311
+ color=UI_ERROR_COLOR,
312
+ )
313
+ return
314
+
315
+ self._draw_perk_prompt()
316
+ if self._show_prompt_rect and self._prompt_rect is not None:
317
+ rl.draw_rectangle_lines_ex(self._prompt_rect, 1.0, rl.Color(255, 0, 255, 255))
318
+
319
+ if self._show_menu and self._assets is not None:
320
+ choices = self._choices()
321
+ if choices:
322
+ screen_w = float(rl.get_screen_width())
323
+ screen_h = float(rl.get_screen_height())
324
+ scale = ui_scale(screen_w, screen_h)
325
+ origin_x, origin_y = ui_origin(screen_w, screen_h, scale)
326
+ computed = perk_menu_compute_layout(
327
+ self._layout,
328
+ screen_w=screen_w,
329
+ origin_x=origin_x,
330
+ origin_y=origin_y,
331
+ scale=scale,
332
+ choice_count=len(choices),
333
+ expert_owned=self._expert_owned,
334
+ master_owned=self._master_owned,
335
+ panel_slide_x=self._panel_slide_x,
336
+ )
337
+
338
+ if self._assets.menu_panel is not None:
339
+ draw_menu_panel(self._assets.menu_panel, dst=computed.panel)
340
+
341
+ if self._assets.title_pick_perk is not None:
342
+ tex = self._assets.title_pick_perk
343
+ src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
344
+ rl.draw_texture_pro(tex, src, computed.title, rl.Vector2(0.0, 0.0), 0.0, rl.WHITE)
345
+
346
+ sponsor = None
347
+ if self._master_owned:
348
+ sponsor = "extra perks sponsored by the Perk Master"
349
+ elif self._expert_owned:
350
+ sponsor = "extra perk sponsored by the Perk Expert"
351
+ if sponsor:
352
+ draw_ui_text(
353
+ self._small,
354
+ sponsor,
355
+ computed.sponsor_x,
356
+ computed.sponsor_y,
357
+ scale=scale,
358
+ color=UI_SPONSOR_COLOR,
359
+ )
360
+
361
+ mouse = rl.get_mouse_position()
362
+ for idx, perk_id in enumerate(choices):
363
+ label = perk_display_name(int(perk_id))
364
+ item_x = computed.list_x
365
+ item_y = computed.list_y + float(idx) * computed.list_step_y
366
+ rect = menu_item_hit_rect(self._small, label, x=item_x, y=item_y, scale=scale)
367
+ hovered = rl.check_collision_point_rec(mouse, rect) or (idx == self._selected)
368
+ draw_menu_item(self._small, label, x=item_x, y=item_y, scale=scale, hovered=hovered)
369
+
370
+ selected_id = choices[self._selected]
371
+ desc = perk_display_description(int(selected_id))
372
+ desc_x = float(computed.desc.x)
373
+ desc_y = float(computed.desc.y)
374
+ desc_w = float(computed.desc.width)
375
+ desc_h = float(computed.desc.height)
376
+ desc_scale = scale * 0.85
377
+ desc_lines = wrap_ui_text(self._small, desc, max_width=desc_w, scale=desc_scale)
378
+ line_h = float(self._small.cell_size * desc_scale) if self._small is not None else float(20 * desc_scale)
379
+ y = desc_y
380
+ for line in desc_lines:
381
+ if y + line_h > desc_y + desc_h:
382
+ break
383
+ draw_ui_text(self._small, line, desc_x, y, scale=desc_scale, color=UI_TEXT_COLOR)
384
+ y += line_h
385
+
386
+ cancel_w = button_width(self._small, self._cancel_button.label, scale=scale, force_wide=self._cancel_button.force_wide)
387
+ button_draw(self._assets, self._small, self._cancel_button, x=computed.cancel_x, y=computed.cancel_y, width=cancel_w, scale=scale)
388
+
389
+ screen_w = float(rl.get_screen_width())
390
+ screen_h = float(rl.get_screen_height())
391
+ scale = ui_scale(screen_w, screen_h)
392
+ if self._assets is not None:
393
+ cursor_draw(self._assets, mouse=rl.get_mouse_position(), scale=scale)
394
+
395
+ if self._debug_overlay:
396
+ self._draw_overlay()
397
+
398
+ def _draw_overlay(self) -> None:
399
+ x = 24.0
400
+ y = 24.0
401
+ scale = 0.9
402
+ line_h = float(self._small.cell_size * scale) if self._small is not None else float(20 * scale)
403
+ lines = [
404
+ "Perk menu render debug (F1 hide)",
405
+ "O toggle menu P toggle prompt H hover rect E/M toggle Expert/Master",
406
+ "Left/Right slide_x (hold Shift for bigger) [/] choices Up/Down selection R reset",
407
+ f"slide_x={self._panel_slide_x:.1f} choices={self._choice_count} selected={self._selected}",
408
+ ]
409
+ for line in lines:
410
+ draw_ui_text(self._small, line, x, y, scale=scale, color=UI_HINT_COLOR)
411
+ y += line_h
412
+
413
+
414
+ def _clamp(value: float, lo: float, hi: float) -> float:
415
+ if value < lo:
416
+ return lo
417
+ if value > hi:
418
+ return hi
419
+ return value
420
+
421
+
422
+ def _ui_text_width(font: SmallFontData | None, text: str, scale: float) -> float:
423
+ if font is None:
424
+ return float(rl.measure_text(text, int(20 * scale)))
425
+ return float(measure_small_text_width(font, text, scale))
426
+
427
+
428
+ @register_view("perk-menu-debug", "Perk menu render debug")
429
+ def _create_perk_menu_debug_view(*, ctx: ViewContext) -> View:
430
+ return PerkMenuDebugView(ctx)