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,410 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import pyray as rl
6
+
7
+ from grim.assets import PaqTextureCache
8
+ from grim.audio import play_sfx, update_audio
9
+ from grim.terrain_render import GroundRenderer
10
+
11
+ from ..assets import MenuAssets, _ensure_texture_cache, load_menu_assets
12
+ from ..menu import (
13
+ MENU_ITEM_OFFSET_X,
14
+ MENU_ITEM_OFFSET_Y,
15
+ MENU_LABEL_HEIGHT,
16
+ MENU_LABEL_OFFSET_X,
17
+ MENU_LABEL_OFFSET_Y,
18
+ MENU_LABEL_ROW_BACK,
19
+ MENU_LABEL_ROW_HEIGHT,
20
+ MENU_LABEL_WIDTH,
21
+ MENU_PANEL_HEIGHT,
22
+ MENU_PANEL_OFFSET_X,
23
+ MENU_PANEL_OFFSET_Y,
24
+ MENU_PANEL_WIDTH,
25
+ MENU_SCALE_SMALL_THRESHOLD,
26
+ MENU_SIGN_HEIGHT,
27
+ MENU_SIGN_OFFSET_X,
28
+ MENU_SIGN_OFFSET_Y,
29
+ MENU_SIGN_POS_X_PAD,
30
+ MENU_SIGN_POS_Y,
31
+ MENU_SIGN_POS_Y_SMALL,
32
+ MENU_SIGN_WIDTH,
33
+ UI_SHADOW_OFFSET,
34
+ UI_SHADOW_TINT,
35
+ MenuEntry,
36
+ MenuView,
37
+ _draw_menu_cursor,
38
+ ensure_menu_ground,
39
+ )
40
+ from ..transitions import _draw_screen_fade
41
+
42
+ if TYPE_CHECKING:
43
+ from ...game import GameState
44
+
45
+
46
+ PANEL_POS_X = -45.0
47
+ PANEL_POS_Y = 210.0
48
+ PANEL_BACK_POS_X = -55.0
49
+ PANEL_BACK_POS_Y = 430.0
50
+ PANEL_TIMELINE_START_MS = 300
51
+ PANEL_TIMELINE_END_MS = 0
52
+
53
+ FADE_TO_GAME_ACTIONS = frozenset(
54
+ {
55
+ "start_survival",
56
+ "start_rush",
57
+ "start_typo",
58
+ "start_tutorial",
59
+ "start_quest",
60
+ }
61
+ )
62
+
63
+
64
+ class PanelMenuView:
65
+ def __init__(
66
+ self,
67
+ state: GameState,
68
+ *,
69
+ title: str,
70
+ body: str | None = None,
71
+ panel_pos_x: float = PANEL_POS_X,
72
+ panel_pos_y: float = PANEL_POS_Y,
73
+ back_pos_x: float = PANEL_BACK_POS_X,
74
+ back_pos_y: float = PANEL_BACK_POS_Y,
75
+ back_action: str = "back_to_menu",
76
+ ) -> None:
77
+ self._state = state
78
+ self._title = title
79
+ self._body_lines = (body or "").splitlines()
80
+ self._panel_pos_x = panel_pos_x
81
+ self._panel_pos_y = panel_pos_y
82
+ self._back_pos_x = back_pos_x
83
+ self._back_pos_y = back_pos_y
84
+ self._back_action = back_action
85
+ self._assets: MenuAssets | None = None
86
+ self._ground: GroundRenderer | None = None
87
+ self._entry: MenuEntry | None = None
88
+ self._hovered = False
89
+ self._menu_screen_width = 0
90
+ self._widescreen_y_shift = 0.0
91
+ self._timeline_ms = 0
92
+ self._timeline_max_ms = 0
93
+ self._cursor_pulse_time = 0.0
94
+ self._closing = False
95
+ self._close_action: str | None = None
96
+ self._pending_action: str | None = None
97
+ self._panel_open_sfx_played = False
98
+
99
+ def open(self) -> None:
100
+ layout_w = float(self._state.config.screen_width)
101
+ self._menu_screen_width = int(layout_w)
102
+ self._widescreen_y_shift = MenuView._menu_widescreen_y_shift(layout_w)
103
+ self._assets = load_menu_assets(self._state)
104
+ self._entry = MenuEntry(slot=0, row=MENU_LABEL_ROW_BACK, y=self._back_pos_y)
105
+ self._hovered = False
106
+ self._timeline_ms = 0
107
+ self._timeline_max_ms = PANEL_TIMELINE_START_MS
108
+ self._cursor_pulse_time = 0.0
109
+ self._closing = False
110
+ self._close_action = None
111
+ self._pending_action = None
112
+ self._panel_open_sfx_played = False
113
+ self._init_ground()
114
+
115
+ def close(self) -> None:
116
+ self._ground = None
117
+
118
+ def update(self, dt: float) -> None:
119
+ if self._state.audio is not None:
120
+ update_audio(self._state.audio, dt)
121
+ if self._ground is not None:
122
+ self._ground.process_pending()
123
+ self._cursor_pulse_time += min(dt, 0.1) * 1.1
124
+ dt_ms = int(min(dt, 0.1) * 1000.0)
125
+ if self._closing:
126
+ if dt_ms > 0 and self._pending_action is None:
127
+ self._timeline_ms -= dt_ms
128
+ if self._timeline_ms < 0 and self._close_action is not None:
129
+ self._pending_action = self._close_action
130
+ self._close_action = None
131
+ return
132
+
133
+ if dt_ms > 0:
134
+ self._timeline_ms = min(self._timeline_max_ms, self._timeline_ms + dt_ms)
135
+ if self._timeline_ms >= self._timeline_max_ms:
136
+ self._state.menu_sign_locked = True
137
+ if (not self._panel_open_sfx_played) and (self._state.audio is not None):
138
+ play_sfx(self._state.audio, "sfx_ui_panelclick", rng=self._state.rng)
139
+ self._panel_open_sfx_played = True
140
+
141
+ entry = self._entry
142
+ if entry is None:
143
+ return
144
+
145
+ enabled = self._entry_enabled(entry)
146
+ hovered = enabled and self._hovered_entry(entry)
147
+ self._hovered = hovered
148
+
149
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE) and enabled:
150
+ self._begin_close_transition(self._back_action)
151
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) and enabled:
152
+ self._begin_close_transition(self._back_action)
153
+ if enabled and hovered and rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT):
154
+ self._begin_close_transition(self._back_action)
155
+
156
+ if hovered:
157
+ entry.hover_amount += dt_ms * 6
158
+ else:
159
+ entry.hover_amount -= dt_ms * 2
160
+ entry.hover_amount = max(0, min(1000, entry.hover_amount))
161
+
162
+ if entry.ready_timer_ms < 0x100:
163
+ entry.ready_timer_ms = min(0x100, entry.ready_timer_ms + dt_ms)
164
+
165
+ def draw(self) -> None:
166
+ rl.clear_background(rl.BLACK)
167
+ if self._ground is not None:
168
+ self._ground.draw(0.0, 0.0)
169
+ _draw_screen_fade(self._state)
170
+ assets = self._assets
171
+ entry = self._entry
172
+ if assets is None or entry is None:
173
+ return
174
+ self._draw_panel()
175
+ self._draw_entry(entry)
176
+ self._draw_sign()
177
+ self._draw_title_text()
178
+ _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
179
+
180
+ def take_action(self) -> str | None:
181
+ action = self._pending_action
182
+ self._pending_action = None
183
+ return action
184
+
185
+ def _draw_title_text(self) -> None:
186
+ x = 32
187
+ y = 140
188
+ rl.draw_text(self._title, x, y, 28, rl.Color(235, 235, 235, 255))
189
+ y += 34
190
+ for line in self._body_lines:
191
+ rl.draw_text(line, x, y, 18, rl.Color(190, 190, 200, 255))
192
+ y += 22
193
+
194
+ def _begin_close_transition(self, action: str) -> None:
195
+ if self._closing:
196
+ return
197
+ if action in FADE_TO_GAME_ACTIONS:
198
+ self._state.screen_fade_alpha = 0.0
199
+ self._state.screen_fade_ramp = True
200
+ if self._state.audio is not None:
201
+ play_sfx(self._state.audio, "sfx_ui_buttonclick", rng=self._state.rng)
202
+ self._closing = True
203
+ self._close_action = action
204
+
205
+ def _ensure_cache(self) -> PaqTextureCache:
206
+ return _ensure_texture_cache(self._state)
207
+
208
+ def _init_ground(self) -> None:
209
+ self._ground = ensure_menu_ground(self._state)
210
+
211
+ def _draw_panel(self) -> None:
212
+ assets = self._assets
213
+ if assets is None or assets.panel is None:
214
+ return
215
+ panel = assets.panel
216
+ panel_w = MENU_PANEL_WIDTH
217
+ panel_h = MENU_PANEL_HEIGHT
218
+ _angle_rad, slide_x = MenuView._ui_element_anim(
219
+ self,
220
+ index=1,
221
+ start_ms=PANEL_TIMELINE_START_MS,
222
+ end_ms=PANEL_TIMELINE_END_MS,
223
+ width=panel_w * self._menu_item_scale(0)[0],
224
+ )
225
+ item_scale, _local_y_shift = self._menu_item_scale(0)
226
+ dst = rl.Rectangle(
227
+ self._panel_pos_x + slide_x,
228
+ self._panel_pos_y + self._widescreen_y_shift,
229
+ panel_w * item_scale,
230
+ panel_h * item_scale,
231
+ )
232
+ origin = rl.Vector2(-(MENU_PANEL_OFFSET_X * item_scale), -(MENU_PANEL_OFFSET_Y * item_scale))
233
+ fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
234
+ if fx_detail:
235
+ MenuView._draw_ui_quad_shadow(
236
+ texture=panel,
237
+ src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
238
+ dst=rl.Rectangle(dst.x + UI_SHADOW_OFFSET, dst.y + UI_SHADOW_OFFSET, dst.width, dst.height),
239
+ origin=origin,
240
+ rotation_deg=0.0,
241
+ )
242
+ MenuView._draw_ui_quad(
243
+ texture=panel,
244
+ src=rl.Rectangle(0.0, 0.0, float(panel.width), float(panel.height)),
245
+ dst=dst,
246
+ origin=origin,
247
+ rotation_deg=0.0,
248
+ tint=rl.WHITE,
249
+ )
250
+
251
+ def _draw_entry(self, entry: MenuEntry) -> None:
252
+ assets = self._assets
253
+ if assets is None or assets.labels is None:
254
+ return
255
+ item = assets.item
256
+ if item is None:
257
+ return
258
+ label_tex = assets.labels
259
+ item_w = float(item.width)
260
+ item_h = float(item.height)
261
+ pos_x = self._back_pos_x
262
+ pos_y = entry.y + self._widescreen_y_shift
263
+ _angle_rad, slide_x = MenuView._ui_element_anim(
264
+ self,
265
+ index=2,
266
+ start_ms=PANEL_TIMELINE_START_MS,
267
+ end_ms=PANEL_TIMELINE_END_MS,
268
+ width=item_w * self._menu_item_scale(entry.slot)[0],
269
+ )
270
+ pos_x += slide_x
271
+ item_scale, local_y_shift = self._menu_item_scale(entry.slot)
272
+ offset_x = MENU_ITEM_OFFSET_X * item_scale
273
+ offset_y = MENU_ITEM_OFFSET_Y * item_scale - local_y_shift
274
+ dst = rl.Rectangle(
275
+ pos_x,
276
+ pos_y,
277
+ item_w * item_scale,
278
+ item_h * item_scale,
279
+ )
280
+ origin = rl.Vector2(-offset_x, -offset_y)
281
+ fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
282
+ if fx_detail:
283
+ MenuView._draw_ui_quad_shadow(
284
+ texture=item,
285
+ src=rl.Rectangle(0.0, 0.0, item_w, item_h),
286
+ dst=rl.Rectangle(dst.x + UI_SHADOW_OFFSET, dst.y + UI_SHADOW_OFFSET, dst.width, dst.height),
287
+ origin=origin,
288
+ rotation_deg=0.0,
289
+ )
290
+ MenuView._draw_ui_quad(
291
+ texture=item,
292
+ src=rl.Rectangle(0.0, 0.0, item_w, item_h),
293
+ dst=dst,
294
+ origin=origin,
295
+ rotation_deg=0.0,
296
+ tint=rl.WHITE,
297
+ )
298
+ alpha = MenuView._label_alpha(entry.hover_amount)
299
+ tint = rl.Color(255, 255, 255, alpha)
300
+ src = rl.Rectangle(
301
+ 0.0,
302
+ float(entry.row) * MENU_LABEL_ROW_HEIGHT,
303
+ MENU_LABEL_WIDTH,
304
+ MENU_LABEL_ROW_HEIGHT,
305
+ )
306
+ label_offset_x = MENU_LABEL_OFFSET_X * item_scale
307
+ label_offset_y = MENU_LABEL_OFFSET_Y * item_scale - local_y_shift
308
+ label_dst = rl.Rectangle(
309
+ pos_x,
310
+ pos_y,
311
+ MENU_LABEL_WIDTH * item_scale,
312
+ MENU_LABEL_HEIGHT * item_scale,
313
+ )
314
+ label_origin = rl.Vector2(-label_offset_x, -label_offset_y)
315
+ MenuView._draw_ui_quad(
316
+ texture=label_tex,
317
+ src=src,
318
+ dst=label_dst,
319
+ origin=label_origin,
320
+ rotation_deg=0.0,
321
+ tint=tint,
322
+ )
323
+ if self._entry_enabled(entry):
324
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
325
+ MenuView._draw_ui_quad(
326
+ texture=label_tex,
327
+ src=src,
328
+ dst=label_dst,
329
+ origin=label_origin,
330
+ rotation_deg=0.0,
331
+ tint=rl.Color(255, 255, 255, alpha),
332
+ )
333
+ rl.end_blend_mode()
334
+
335
+ def _draw_sign(self) -> None:
336
+ assets = self._assets
337
+ if assets is None or assets.sign is None:
338
+ return
339
+ screen_w = float(self._state.config.screen_width)
340
+ scale, shift_x = MenuView._sign_layout_scale(int(screen_w))
341
+ pos_x = screen_w + MENU_SIGN_POS_X_PAD
342
+ pos_y = MENU_SIGN_POS_Y if screen_w > MENU_SCALE_SMALL_THRESHOLD else MENU_SIGN_POS_Y_SMALL
343
+ sign_w = MENU_SIGN_WIDTH * scale
344
+ sign_h = MENU_SIGN_HEIGHT * scale
345
+ offset_x = MENU_SIGN_OFFSET_X * scale + shift_x
346
+ offset_y = MENU_SIGN_OFFSET_Y * scale
347
+ # Quest screen is only reachable after the Play Game panel is fully visible,
348
+ # so the sign is already locked in place. Keep it static here.
349
+ rotation_deg = 0.0
350
+ sign = assets.sign
351
+ fx_detail = bool(self._state.config.data.get("fx_detail_0", 0))
352
+ if fx_detail:
353
+ MenuView._draw_ui_quad_shadow(
354
+ texture=sign,
355
+ src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
356
+ dst=rl.Rectangle(pos_x + UI_SHADOW_OFFSET, pos_y + UI_SHADOW_OFFSET, sign_w, sign_h),
357
+ origin=rl.Vector2(-offset_x, -offset_y),
358
+ rotation_deg=rotation_deg,
359
+ )
360
+ MenuView._draw_ui_quad(
361
+ texture=sign,
362
+ src=rl.Rectangle(0.0, 0.0, float(sign.width), float(sign.height)),
363
+ dst=rl.Rectangle(pos_x, pos_y, sign_w, sign_h),
364
+ origin=rl.Vector2(-offset_x, -offset_y),
365
+ rotation_deg=rotation_deg,
366
+ tint=rl.WHITE,
367
+ )
368
+
369
+ def _entry_enabled(self, entry: MenuEntry) -> bool:
370
+ return self._timeline_ms >= PANEL_TIMELINE_START_MS
371
+
372
+ def _hovered_entry(self, entry: MenuEntry) -> bool:
373
+ left, top, right, bottom = self._menu_item_bounds(entry)
374
+ mouse = rl.get_mouse_position()
375
+ mouse_x = float(mouse.x)
376
+ mouse_y = float(mouse.y)
377
+ return left <= mouse_x <= right and top <= mouse_y <= bottom
378
+
379
+ def _menu_item_scale(self, slot: int) -> tuple[float, float]:
380
+ if self._menu_screen_width < 641:
381
+ return 0.9, float(slot) * 11.0
382
+ return 1.0, 0.0
383
+
384
+ def _menu_item_bounds(self, entry: MenuEntry) -> tuple[float, float, float, float]:
385
+ assets = self._assets
386
+ if assets is None or assets.item is None:
387
+ return (0.0, 0.0, 0.0, 0.0)
388
+ item_w = float(assets.item.width)
389
+ item_h = float(assets.item.height)
390
+ item_scale, local_y_shift = self._menu_item_scale(entry.slot)
391
+ x0 = MENU_ITEM_OFFSET_X * item_scale
392
+ y0 = MENU_ITEM_OFFSET_Y * item_scale - local_y_shift
393
+ x2 = (MENU_ITEM_OFFSET_X + item_w) * item_scale
394
+ y2 = (MENU_ITEM_OFFSET_Y + item_h) * item_scale - local_y_shift
395
+ w = x2 - x0
396
+ h = y2 - y0
397
+ _angle_rad, slide_x = MenuView._ui_element_anim(
398
+ self,
399
+ index=2,
400
+ start_ms=PANEL_TIMELINE_START_MS,
401
+ end_ms=PANEL_TIMELINE_END_MS,
402
+ width=item_w * item_scale,
403
+ )
404
+ pos_x = self._back_pos_x + slide_x
405
+ pos_y = entry.y + self._widescreen_y_shift
406
+ left = pos_x + x0 + w * 0.54
407
+ top = pos_y + y0 + h * 0.28
408
+ right = pos_x + x2 - w * 0.05
409
+ bottom = pos_y + y2 - h * 0.10
410
+ return left, top, right, bottom
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import pyray as rl
6
+
7
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
8
+
9
+ from ..menu import (
10
+ MENU_PANEL_OFFSET_X,
11
+ MENU_PANEL_OFFSET_Y,
12
+ MENU_PANEL_WIDTH,
13
+ MenuView,
14
+ _draw_menu_cursor,
15
+ )
16
+ from ..transitions import _draw_screen_fade
17
+ from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
18
+ from ...input_codes import config_keybinds, input_code_name, player_move_fire_binds
19
+
20
+ if TYPE_CHECKING:
21
+ from ...game import GameState
22
+
23
+
24
+ class ControlsMenuView(PanelMenuView):
25
+ def __init__(self, state: GameState) -> None:
26
+ super().__init__(state, title="Controls", back_action="open_options")
27
+ self._small_font: SmallFontData | None = None
28
+ self._lines: list[str] = []
29
+
30
+ def open(self) -> None:
31
+ super().open()
32
+ self._lines = self._build_lines()
33
+
34
+ def draw(self) -> None:
35
+ rl.clear_background(rl.BLACK)
36
+ if self._ground is not None:
37
+ self._ground.draw(0.0, 0.0)
38
+ _draw_screen_fade(self._state)
39
+ assets = self._assets
40
+ entry = self._entry
41
+ if assets is None or entry is None:
42
+ return
43
+ self._draw_panel()
44
+ self._draw_entry(entry)
45
+ self._draw_sign()
46
+ self._draw_contents()
47
+ _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
48
+
49
+ def _ensure_small_font(self) -> SmallFontData:
50
+ if self._small_font is not None:
51
+ return self._small_font
52
+ missing_assets: list[str] = []
53
+ self._small_font = load_small_font(self._state.assets_dir, missing_assets)
54
+ return self._small_font
55
+
56
+ def _content_layout(self) -> dict[str, float]:
57
+ panel_scale, _local_shift = self._menu_item_scale(0)
58
+ panel_w = MENU_PANEL_WIDTH * panel_scale
59
+ _angle_rad, slide_x = MenuView._ui_element_anim(
60
+ self,
61
+ index=1,
62
+ start_ms=PANEL_TIMELINE_START_MS,
63
+ end_ms=PANEL_TIMELINE_END_MS,
64
+ width=panel_w,
65
+ )
66
+ panel_x = self._panel_pos_x + slide_x
67
+ panel_y = self._panel_pos_y + self._widescreen_y_shift
68
+ origin_x = -(MENU_PANEL_OFFSET_X * panel_scale)
69
+ origin_y = -(MENU_PANEL_OFFSET_Y * panel_scale)
70
+ panel_left = panel_x - origin_x
71
+ panel_top = panel_y - origin_y
72
+ base_x = panel_left + 212.0 * panel_scale
73
+ base_y = panel_top + 32.0 * panel_scale
74
+ label_x = base_x + 8.0 * panel_scale
75
+ return {
76
+ "base_x": base_x,
77
+ "base_y": base_y,
78
+ "label_x": label_x,
79
+ "scale": panel_scale,
80
+ }
81
+
82
+ def _build_lines(self) -> list[str]:
83
+ config = self._state.config
84
+ pick_perk_key = int(config.data.get("keybind_pick_perk", 0x101) or 0x101)
85
+ reload_key = int(config.data.get("keybind_reload", 0x102) or 0x102)
86
+
87
+ keybinds = config_keybinds(config)
88
+ if not keybinds:
89
+ keybinds = (0x11, 0x1F, 0x1E, 0x20, 0x100) + (0x17E,) * 11 + (0xC8, 0xD0, 0xCB, 0xCD, 0x9D)
90
+
91
+ p1_up, p1_down, p1_left, p1_right, p1_fire = player_move_fire_binds(keybinds, 0)
92
+ p2_up, p2_down, p2_left, p2_right, p2_fire = player_move_fire_binds(keybinds, 1)
93
+
94
+ return [
95
+ f"Level up: {input_code_name(pick_perk_key)} / Space / KeyPad+",
96
+ f"Reload: {input_code_name(reload_key)}",
97
+ "",
98
+ "Player 1:",
99
+ f" Up: {input_code_name(p1_up)}",
100
+ f" Down: {input_code_name(p1_down)}",
101
+ f" Left: {input_code_name(p1_left)}",
102
+ f" Right: {input_code_name(p1_right)}",
103
+ f" Fire: {input_code_name(p1_fire)}",
104
+ "",
105
+ "Player 2:",
106
+ f" Up: {input_code_name(p2_up)}",
107
+ f" Down: {input_code_name(p2_down)}",
108
+ f" Left: {input_code_name(p2_left)}",
109
+ f" Right: {input_code_name(p2_right)}",
110
+ f" Fire: {input_code_name(p2_fire)}",
111
+ ]
112
+
113
+ def _draw_contents(self) -> None:
114
+ layout = self._content_layout()
115
+ base_x = layout["base_x"]
116
+ base_y = layout["base_y"]
117
+ label_x = layout["label_x"]
118
+ scale = layout["scale"]
119
+
120
+ font = self._ensure_small_font()
121
+ title_scale = 1.2 * scale
122
+ text_scale = 1.0 * scale
123
+
124
+ title_color = rl.Color(255, 255, 255, 255)
125
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
126
+
127
+ draw_small_text(font, "CONTROLS", base_x, base_y, title_scale, title_color)
128
+ line_y = base_y + 44.0 * scale
129
+ line_step = (font.cell_size + 4.0) * scale
130
+ for line in self._lines:
131
+ draw_small_text(font, line, label_x, line_y, text_scale, text_color)
132
+ line_y += line_step
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ import pyray as rl
7
+
8
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
9
+
10
+ from ..menu import (
11
+ MENU_PANEL_OFFSET_X,
12
+ MENU_PANEL_OFFSET_Y,
13
+ MENU_PANEL_WIDTH,
14
+ MenuView,
15
+ _draw_menu_cursor,
16
+ )
17
+ from ..transitions import _draw_screen_fade
18
+ from .base import PANEL_TIMELINE_END_MS, PANEL_TIMELINE_START_MS, PanelMenuView
19
+
20
+ if TYPE_CHECKING:
21
+ from ...game import GameState
22
+
23
+
24
+ class ModsMenuView(PanelMenuView):
25
+ def __init__(self, state: GameState) -> None:
26
+ super().__init__(state, title="Mods")
27
+ self._small_font: SmallFontData | None = None
28
+ self._lines: list[str] = []
29
+
30
+ def open(self) -> None:
31
+ super().open()
32
+ self._lines = self._build_lines()
33
+
34
+ def draw(self) -> None:
35
+ rl.clear_background(rl.BLACK)
36
+ if self._ground is not None:
37
+ self._ground.draw(0.0, 0.0)
38
+ _draw_screen_fade(self._state)
39
+ assets = self._assets
40
+ entry = self._entry
41
+ if assets is None or entry is None:
42
+ return
43
+ self._draw_panel()
44
+ self._draw_entry(entry)
45
+ self._draw_sign()
46
+ self._draw_contents()
47
+ _draw_menu_cursor(self._state, pulse_time=self._cursor_pulse_time)
48
+
49
+ def _ensure_small_font(self) -> SmallFontData:
50
+ if self._small_font is not None:
51
+ return self._small_font
52
+ missing_assets: list[str] = []
53
+ self._small_font = load_small_font(self._state.assets_dir, missing_assets)
54
+ return self._small_font
55
+
56
+ def _content_layout(self) -> dict[str, float]:
57
+ panel_scale, _local_shift = self._menu_item_scale(0)
58
+ panel_w = MENU_PANEL_WIDTH * panel_scale
59
+ _angle_rad, slide_x = MenuView._ui_element_anim(
60
+ self,
61
+ index=1,
62
+ start_ms=PANEL_TIMELINE_START_MS,
63
+ end_ms=PANEL_TIMELINE_END_MS,
64
+ width=panel_w,
65
+ )
66
+ panel_x = self._panel_pos_x + slide_x
67
+ panel_y = self._panel_pos_y + self._widescreen_y_shift
68
+ origin_x = -(MENU_PANEL_OFFSET_X * panel_scale)
69
+ origin_y = -(MENU_PANEL_OFFSET_Y * panel_scale)
70
+ panel_left = panel_x - origin_x
71
+ panel_top = panel_y - origin_y
72
+ base_x = panel_left + 212.0 * panel_scale
73
+ base_y = panel_top + 32.0 * panel_scale
74
+ label_x = base_x + 8.0 * panel_scale
75
+ return {
76
+ "base_x": base_x,
77
+ "base_y": base_y,
78
+ "label_x": label_x,
79
+ "scale": panel_scale,
80
+ }
81
+
82
+ def _build_lines(self) -> list[str]:
83
+ mods_dir = self._state.base_dir / "mods"
84
+ dlls: list[Path] = []
85
+ try:
86
+ dlls = sorted(mods_dir.glob("*.dll"))
87
+ except Exception:
88
+ dlls = []
89
+
90
+ if not dlls:
91
+ return [
92
+ "No mod DLLs found.",
93
+ "",
94
+ "Expected location:",
95
+ f" {mods_dir}",
96
+ "",
97
+ "Mod loading is not implemented yet.",
98
+ ]
99
+
100
+ lines = [f"Found {len(dlls)} mod DLL(s):", ""]
101
+ for path in dlls[:10]:
102
+ lines.append(f" {path.name}")
103
+ if len(dlls) > 10:
104
+ lines.append(f" ... ({len(dlls) - 10} more)")
105
+ lines.append("")
106
+ lines.append("Mod loading is not implemented yet.")
107
+ return lines
108
+
109
+ def _draw_contents(self) -> None:
110
+ layout = self._content_layout()
111
+ base_x = layout["base_x"]
112
+ base_y = layout["base_y"]
113
+ label_x = layout["label_x"]
114
+ scale = layout["scale"]
115
+
116
+ font = self._ensure_small_font()
117
+ title_scale = 1.2 * scale
118
+ text_scale = 1.0 * scale
119
+
120
+ title_color = rl.Color(255, 255, 255, 255)
121
+ text_color = rl.Color(255, 255, 255, int(255 * 0.8))
122
+
123
+ draw_small_text(font, "MODS", base_x, base_y, title_scale, title_color)
124
+ line_y = base_y + 44.0 * scale
125
+ line_step = (font.cell_size + 4.0) * scale
126
+ for line in self._lines:
127
+ draw_small_text(font, line, label_x, line_y, text_scale, text_color)
128
+ line_y += line_step