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,388 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+
6
+ import pyray as rl
7
+
8
+ from grim.assets import TextureLoader
9
+ from grim.fonts.small import SmallFontData, draw_small_text, measure_small_text_width
10
+
11
+
12
+ UI_BASE_WIDTH = 640.0
13
+ UI_BASE_HEIGHT = 480.0
14
+
15
+
16
+ MENU_PANEL_SLICE_Y1 = 130.0
17
+ MENU_PANEL_SLICE_Y2 = 150.0
18
+
19
+ # Layout offsets from the classic game (perk selection screen), derived from
20
+ # `perk_selection_screen_update` (see analysis/ghidra + BN).
21
+ MENU_PANEL_ANCHOR_X = 224.0
22
+ MENU_PANEL_ANCHOR_Y = 40.0
23
+ MENU_TITLE_X = 54.0
24
+ MENU_TITLE_Y = 6.0
25
+ MENU_TITLE_W = 128.0
26
+ MENU_TITLE_H = 32.0
27
+ MENU_SPONSOR_Y = -8.0
28
+ MENU_SPONSOR_X_EXPERT = -26.0
29
+ MENU_SPONSOR_X_MASTER = -28.0
30
+ MENU_LIST_Y_NORMAL = 50.0
31
+ MENU_LIST_Y_EXPERT = 40.0
32
+ MENU_LIST_STEP_NORMAL = 19.0
33
+ MENU_LIST_STEP_EXPERT = 18.0
34
+ MENU_DESC_X = -12.0
35
+ MENU_DESC_Y_AFTER_LIST = 32.0
36
+ MENU_DESC_Y_EXTRA_TIGHTEN = 20.0
37
+ MENU_BUTTON_X = 162.0
38
+ MENU_BUTTON_Y = 276.0
39
+ MENU_DESC_RIGHT_X = 480.0
40
+
41
+
42
+ @dataclass(slots=True)
43
+ class PerkMenuLayout:
44
+ # Coordinates live in the original 640x480 UI space.
45
+ # Matches the classic menu panel: pos (-45, 110) + offset (20, -82).
46
+ panel_x: float = -25.0
47
+ panel_y: float = 28.0
48
+ panel_w: float = 512.0
49
+ panel_h: float = 379.0
50
+
51
+
52
+ @dataclass(slots=True)
53
+ class PerkMenuComputedLayout:
54
+ panel: rl.Rectangle
55
+ title: rl.Rectangle
56
+ sponsor_x: float
57
+ sponsor_y: float
58
+ list_x: float
59
+ list_y: float
60
+ list_step_y: float
61
+ desc: rl.Rectangle
62
+ cancel_x: float
63
+ cancel_y: float
64
+
65
+
66
+ def ui_scale(screen_w: float, screen_h: float) -> float:
67
+ # Classic UI renders in backbuffer pixels; keep menu scale fixed.
68
+ return 1.0
69
+
70
+
71
+ def ui_origin(screen_w: float, screen_h: float, scale: float) -> tuple[float, float]:
72
+ return 0.0, 0.0
73
+
74
+
75
+ def _menu_widescreen_y_shift(layout_w: float) -> float:
76
+ # ui_menu_layout_init: pos_y += (screen_width / 640.0) * 150.0 - 150.0
77
+ return (layout_w / UI_BASE_WIDTH) * 150.0 - 150.0
78
+
79
+
80
+ def perk_menu_compute_layout(
81
+ layout: PerkMenuLayout,
82
+ *,
83
+ screen_w: float,
84
+ origin_x: float,
85
+ origin_y: float,
86
+ scale: float,
87
+ choice_count: int,
88
+ expert_owned: bool,
89
+ master_owned: bool,
90
+ panel_slide_x: float = 0.0,
91
+ ) -> PerkMenuComputedLayout:
92
+ layout_w = screen_w / scale if scale else screen_w
93
+ widescreen_shift_y = _menu_widescreen_y_shift(layout_w)
94
+ panel_x = layout.panel_x + panel_slide_x
95
+ panel_y = layout.panel_y + widescreen_shift_y
96
+ panel = rl.Rectangle(
97
+ origin_x + panel_x * scale,
98
+ origin_y + panel_y * scale,
99
+ layout.panel_w * scale,
100
+ layout.panel_h * scale,
101
+ )
102
+ anchor_x = panel.x + MENU_PANEL_ANCHOR_X * scale
103
+ anchor_y = panel.y + MENU_PANEL_ANCHOR_Y * scale
104
+
105
+ title = rl.Rectangle(
106
+ anchor_x + MENU_TITLE_X * scale,
107
+ anchor_y + MENU_TITLE_Y * scale,
108
+ MENU_TITLE_W * scale,
109
+ MENU_TITLE_H * scale,
110
+ )
111
+
112
+ sponsor_x = anchor_x + (MENU_SPONSOR_X_MASTER if master_owned else MENU_SPONSOR_X_EXPERT) * scale
113
+ sponsor_y = anchor_y + MENU_SPONSOR_Y * scale
114
+
115
+ list_step_y = MENU_LIST_STEP_EXPERT if expert_owned else MENU_LIST_STEP_NORMAL
116
+ list_x = anchor_x
117
+ list_y = anchor_y + (MENU_LIST_Y_EXPERT if expert_owned else MENU_LIST_Y_NORMAL) * scale
118
+
119
+ desc_x = anchor_x + MENU_DESC_X * scale
120
+ desc_y = list_y + float(choice_count) * list_step_y * scale + MENU_DESC_Y_AFTER_LIST * scale
121
+ if choice_count > 5:
122
+ desc_y -= MENU_DESC_Y_EXTRA_TIGHTEN * scale
123
+
124
+ # Keep the description within the monitor screen area and above the button.
125
+ desc_right = panel.x + MENU_DESC_RIGHT_X * scale
126
+ cancel_x = anchor_x + MENU_BUTTON_X * scale
127
+ cancel_y = anchor_y + MENU_BUTTON_Y * scale
128
+ desc_w = max(0.0, float(desc_right - desc_x))
129
+ desc_h = max(0.0, float(cancel_y - 12.0 * scale - desc_y))
130
+ desc = rl.Rectangle(float(desc_x), float(desc_y), float(desc_w), float(desc_h))
131
+
132
+ return PerkMenuComputedLayout(
133
+ panel=panel,
134
+ title=title,
135
+ sponsor_x=float(sponsor_x),
136
+ sponsor_y=float(sponsor_y),
137
+ list_x=float(list_x),
138
+ list_y=float(list_y),
139
+ list_step_y=float(list_step_y * scale),
140
+ desc=desc,
141
+ cancel_x=float(cancel_x),
142
+ cancel_y=float(cancel_y),
143
+ )
144
+
145
+
146
+ def draw_menu_panel(texture: rl.Texture, *, dst: rl.Rectangle, tint: rl.Color = rl.WHITE) -> None:
147
+ scale = float(dst.width) / float(texture.width)
148
+ top_h = MENU_PANEL_SLICE_Y1 * scale
149
+ bottom_h = (float(texture.height) - MENU_PANEL_SLICE_Y2) * scale
150
+ mid_h = float(dst.height) - top_h - bottom_h
151
+ if mid_h < 0.0:
152
+ src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
153
+ rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
154
+ return
155
+
156
+ src_w = float(texture.width)
157
+ src_h = float(texture.height)
158
+
159
+ src_top = rl.Rectangle(0.0, 0.0, src_w, MENU_PANEL_SLICE_Y1)
160
+ src_mid = rl.Rectangle(0.0, MENU_PANEL_SLICE_Y1, src_w, MENU_PANEL_SLICE_Y2 - MENU_PANEL_SLICE_Y1)
161
+ src_bot = rl.Rectangle(0.0, MENU_PANEL_SLICE_Y2, src_w, src_h - MENU_PANEL_SLICE_Y2)
162
+
163
+ dst_top = rl.Rectangle(float(dst.x), float(dst.y), float(dst.width), top_h)
164
+ dst_mid = rl.Rectangle(float(dst.x), float(dst.y) + top_h, float(dst.width), mid_h)
165
+ dst_bot = rl.Rectangle(float(dst.x), float(dst.y) + top_h + mid_h, float(dst.width), bottom_h)
166
+
167
+ origin = rl.Vector2(0.0, 0.0)
168
+ rl.draw_texture_pro(texture, src_top, dst_top, origin, 0.0, tint)
169
+ rl.draw_texture_pro(texture, src_mid, dst_mid, origin, 0.0, tint)
170
+ rl.draw_texture_pro(texture, src_bot, dst_bot, origin, 0.0, tint)
171
+
172
+
173
+ @dataclass(slots=True)
174
+ class PerkMenuAssets:
175
+ menu_panel: rl.Texture | None
176
+ title_pick_perk: rl.Texture | None
177
+ title_level_up: rl.Texture | None
178
+ menu_item: rl.Texture | None
179
+ button_sm: rl.Texture | None
180
+ button_md: rl.Texture | None
181
+ cursor: rl.Texture | None
182
+ aim: rl.Texture | None
183
+ missing: list[str] = field(default_factory=list)
184
+
185
+
186
+ def load_perk_menu_assets(assets_root: Path) -> PerkMenuAssets:
187
+ loader = TextureLoader.from_assets_root(assets_root)
188
+ return PerkMenuAssets(
189
+ menu_panel=loader.get(name="ui_menuPanel", paq_rel="ui/ui_menuPanel.jaz", fs_rel="ui/ui_menuPanel.png"),
190
+ title_pick_perk=loader.get(
191
+ name="ui_textPickAPerk",
192
+ paq_rel="ui/ui_textPickAPerk.jaz",
193
+ fs_rel="ui/ui_textPickAPerk.png",
194
+ ),
195
+ title_level_up=loader.get(
196
+ name="ui_textLevelUp",
197
+ paq_rel="ui/ui_textLevelUp.jaz",
198
+ fs_rel="ui/ui_textLevelUp.png",
199
+ ),
200
+ menu_item=loader.get(name="ui_menuItem", paq_rel="ui/ui_menuItem.jaz", fs_rel="ui/ui_menuItem.png"),
201
+ button_sm=loader.get(name="ui_buttonSm", paq_rel="ui/ui_button_82x32.jaz", fs_rel="ui/ui_button_82x32.png"),
202
+ button_md=loader.get(
203
+ name="ui_buttonMd",
204
+ paq_rel="ui/ui_button_145x32.jaz",
205
+ fs_rel="ui/ui_button_145x32.png",
206
+ ),
207
+ cursor=loader.get(name="ui_cursor", paq_rel="ui/ui_cursor.jaz", fs_rel="ui/ui_cursor.png"),
208
+ aim=loader.get(name="ui_aim", paq_rel="ui/ui_aim.jaz", fs_rel="ui/ui_aim.png"),
209
+ missing=loader.missing,
210
+ )
211
+
212
+
213
+ def _ui_text_width(font: SmallFontData | None, text: str, scale: float) -> float:
214
+ if font is None:
215
+ return float(rl.measure_text(text, int(20 * scale)))
216
+ return float(measure_small_text_width(font, text, scale))
217
+
218
+
219
+ def draw_ui_text(
220
+ font: SmallFontData | None,
221
+ text: str,
222
+ x: float,
223
+ y: float,
224
+ *,
225
+ scale: float,
226
+ color: rl.Color,
227
+ ) -> None:
228
+ if font is not None:
229
+ draw_small_text(font, text, x, y, scale, color)
230
+ else:
231
+ rl.draw_text(text, int(x), int(y), int(20 * scale), color)
232
+
233
+
234
+ def wrap_ui_text(font: SmallFontData | None, text: str, *, max_width: float, scale: float) -> list[str]:
235
+ lines: list[str] = []
236
+ for raw in text.splitlines() or [""]:
237
+ para = raw.strip()
238
+ if not para:
239
+ lines.append("")
240
+ continue
241
+ current = ""
242
+ for word in para.split():
243
+ candidate = word if not current else f"{current} {word}"
244
+ if current and _ui_text_width(font, candidate, scale) > max_width:
245
+ lines.append(current)
246
+ current = word
247
+ else:
248
+ current = candidate
249
+ if current:
250
+ lines.append(current)
251
+ return lines
252
+
253
+
254
+ MENU_ITEM_RGB = (0x46, 0xB4, 0xF0) # from ui_menu_item_update: rgb(70, 180, 240)
255
+ MENU_ITEM_ALPHA_IDLE = 0.6
256
+ MENU_ITEM_ALPHA_HOVER = 1.0
257
+
258
+
259
+ def menu_item_hit_rect(font: SmallFontData | None, label: str, *, x: float, y: float, scale: float) -> rl.Rectangle:
260
+ width = _ui_text_width(font, label, scale)
261
+ height = 16.0 * scale
262
+ return rl.Rectangle(float(x), float(y), float(width), float(height))
263
+
264
+
265
+ def draw_menu_item(
266
+ font: SmallFontData | None,
267
+ label: str,
268
+ *,
269
+ x: float,
270
+ y: float,
271
+ scale: float,
272
+ hovered: bool,
273
+ ) -> float:
274
+ alpha = MENU_ITEM_ALPHA_HOVER if hovered else MENU_ITEM_ALPHA_IDLE
275
+ r, g, b = MENU_ITEM_RGB
276
+ color = rl.Color(int(r), int(g), int(b), int(255 * alpha))
277
+ draw_ui_text(font, label, x, y, scale=scale, color=color)
278
+ width = _ui_text_width(font, label, scale)
279
+ line_y = y + 13.0 * scale
280
+ rl.draw_line(int(x), int(line_y), int(x + width), int(line_y), color)
281
+ return float(width)
282
+
283
+
284
+ @dataclass(slots=True)
285
+ class UiButtonState:
286
+ label: str
287
+ enabled: bool = True
288
+ hovered: bool = False
289
+ activated: bool = False
290
+ hover_t: int = 0 # 0..1000
291
+ press_t: int = 0 # 0..1000
292
+ alpha: float = 1.0
293
+ force_wide: bool = False
294
+
295
+
296
+ def button_width(font: SmallFontData | None, label: str, *, scale: float, force_wide: bool) -> float:
297
+ text_w = _ui_text_width(font, label, scale)
298
+ if force_wide:
299
+ return 145.0 * scale
300
+ if text_w < 40.0 * scale:
301
+ return 82.0 * scale
302
+ return 145.0 * scale
303
+
304
+
305
+ def button_hit_rect(*, x: float, y: float, width: float) -> rl.Rectangle:
306
+ # Mirrors ui_button_update: y is offset by +2, hit height is 0x1c (28).
307
+ return rl.Rectangle(float(x), float(y + 2.0), float(width), float(28.0))
308
+
309
+
310
+ def button_update(
311
+ state: UiButtonState,
312
+ *,
313
+ x: float,
314
+ y: float,
315
+ width: float,
316
+ dt_ms: float,
317
+ mouse: rl.Vector2,
318
+ click: bool,
319
+ ) -> bool:
320
+ if not state.enabled:
321
+ state.hovered = False
322
+ else:
323
+ state.hovered = rl.check_collision_point_rec(mouse, button_hit_rect(x=x, y=y, width=width))
324
+
325
+ delta = 6 if (state.enabled and state.hovered) else -4
326
+ state.hover_t = int(_clamp(float(state.hover_t + int(dt_ms) * delta), 0.0, 1000.0))
327
+
328
+ if state.press_t > 0:
329
+ state.press_t = int(_clamp(float(state.press_t - int(dt_ms) * 6), 0.0, 1000.0))
330
+
331
+ state.activated = bool(state.enabled and state.hovered and click)
332
+ if state.activated:
333
+ state.press_t = 1000
334
+ return state.activated
335
+
336
+
337
+ def _clamp(value: float, lo: float, hi: float) -> float:
338
+ if value < lo:
339
+ return lo
340
+ if value > hi:
341
+ return hi
342
+ return value
343
+
344
+
345
+ def button_draw(
346
+ assets: PerkMenuAssets,
347
+ font: SmallFontData | None,
348
+ state: UiButtonState,
349
+ *,
350
+ x: float,
351
+ y: float,
352
+ width: float,
353
+ scale: float,
354
+ ) -> None:
355
+ texture = assets.button_md if width > 120.0 * scale else assets.button_sm
356
+ if texture is None:
357
+ return
358
+
359
+ if state.hover_t > 0:
360
+ alpha = 0.5
361
+ if state.press_t > 0:
362
+ alpha = min(1.0, 0.5 + (float(state.press_t) * 0.0005))
363
+ hl = rl.Color(255, 255, 255, int(255 * alpha * 0.25 * state.alpha))
364
+ rl.draw_rectangle(int(x + 12.0 * scale), int(y + 5.0 * scale), int(width - 24.0 * scale), int(22.0 * scale), hl)
365
+
366
+ tint_a = state.alpha if state.hovered else state.alpha * 0.7
367
+ tint = rl.Color(255, 255, 255, int(255 * _clamp(tint_a, 0.0, 1.0)))
368
+
369
+ src = rl.Rectangle(0.0, 0.0, float(texture.width), float(texture.height))
370
+ dst = rl.Rectangle(float(x), float(y), float(width), float(32.0 * scale))
371
+ rl.draw_texture_pro(texture, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
372
+
373
+ text_w = _ui_text_width(font, state.label, scale)
374
+ text_x = x + width * 0.5 - text_w * 0.5 + 1.0 * scale
375
+ text_y = y + 10.0 * scale
376
+ draw_ui_text(font, state.label, text_x, text_y, scale=scale, color=tint)
377
+
378
+
379
+ def cursor_draw(assets: PerkMenuAssets, *, mouse: rl.Vector2, scale: float, alpha: float = 1.0) -> None:
380
+ tex = assets.cursor
381
+ if tex is None:
382
+ return
383
+ a = int(255 * _clamp(alpha, 0.0, 1.0))
384
+ tint = rl.Color(255, 255, 255, a)
385
+ size = 32.0 * scale
386
+ src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
387
+ dst = rl.Rectangle(float(mouse.x), float(mouse.y), size, size)
388
+ rl.draw_texture_pro(tex, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from .registry import all_views, view_by_name
4
+
5
+
6
+ def _register_builtin_views() -> None:
7
+ from . import empty as _empty # noqa: F401
8
+ from . import fonts as _fonts # noqa: F401
9
+ from . import animations as _animations # noqa: F401
10
+ from . import sprites as _sprites # noqa: F401
11
+ from . import terrain as _terrain # noqa: F401
12
+ from . import ground as _ground # noqa: F401
13
+ from . import projectiles as _projectiles # noqa: F401
14
+ from . import projectile_fx as _projectile_fx # noqa: F401
15
+ from . import bonuses as _bonuses # noqa: F401
16
+ from . import perks as _perks # noqa: F401
17
+ from . import perk_menu_debug as _perk_menu_debug # noqa: F401
18
+ from . import wicons as _wicons # noqa: F401
19
+ from . import ui as _ui # noqa: F401
20
+ from . import particles as _particles # noqa: F401
21
+ from . import spawn_plan as _spawn_plan # noqa: F401
22
+ from . import player as _player # noqa: F401
23
+ from . import survival as _survival # noqa: F401
24
+ from . import rush as _rush # noqa: F401
25
+ from . import game_over as _game_over # noqa: F401
26
+ from . import small_font_debug as _small_font_debug # noqa: F401
27
+ from . import camera_debug as _camera_debug # noqa: F401
28
+ from . import camera_shake as _camera_shake # noqa: F401
29
+ from . import decals_debug as _decals_debug # noqa: F401
30
+ from . import corpse_stamp_debug as _corpse_stamp_debug # noqa: F401
31
+ from . import player_sprite_debug as _player_sprite_debug # noqa: F401
32
+ from . import aim_debug as _aim_debug # noqa: F401
33
+ from . import projectile_render_debug as _projectile_render_debug # noqa: F401
34
+ from . import arsenal_debug as _arsenal_debug # noqa: F401
35
+ from . import lighting_debug as _lighting_debug # noqa: F401
36
+
37
+
38
+ _register_builtin_views()
39
+
40
+ __all__ = ["all_views", "view_by_name"]
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import pyray as rl
6
+
7
+ from grim.config import ensure_crimson_cfg
8
+ from grim.fonts.small import SmallFontData, draw_small_text, load_small_font
9
+ from grim.view import ViewContext
10
+
11
+ from ..game_world import GameWorld
12
+ from ..gameplay import PlayerInput
13
+ from ..paths import default_runtime_dir
14
+ from ..ui.cursor import draw_cursor_glow
15
+ from .registry import register_view
16
+
17
+ WORLD_SIZE = 1024.0
18
+
19
+ UI_TEXT_SCALE = 1.0
20
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
21
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
22
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
23
+
24
+
25
+ def _clamp(value: float, lo: float, hi: float) -> float:
26
+ if value < lo:
27
+ return lo
28
+ if value > hi:
29
+ return hi
30
+ return value
31
+
32
+
33
+ class AimDebugView:
34
+ def __init__(self, ctx: ViewContext) -> None:
35
+ self._assets_root = ctx.assets_dir
36
+ self._missing_assets: list[str] = []
37
+ self._small: SmallFontData | None = None
38
+ self._world = GameWorld(
39
+ assets_dir=ctx.assets_dir,
40
+ world_size=WORLD_SIZE,
41
+ demo_mode_active=False,
42
+ difficulty_level=0,
43
+ hardcore=False,
44
+ )
45
+ self._player = self._world.players[0] if self._world.players else None
46
+
47
+ self.close_requested = False
48
+
49
+ self._ui_mouse_x = 0.0
50
+ self._ui_mouse_y = 0.0
51
+ self._cursor_pulse_time = 0.0
52
+
53
+ self._simulate = False
54
+ self._draw_world = True
55
+ self._draw_world_aim = True
56
+ self._show_cursor_glow = False
57
+ self._draw_expected_overlay = True
58
+ self._draw_test_circle = True
59
+
60
+ self._force_heat = True
61
+ self._forced_heat = 0.18
62
+ self._test_circle_radius = 96.0
63
+
64
+ def _ui_line_height(self, scale: float = UI_TEXT_SCALE) -> int:
65
+ if self._small is not None:
66
+ return int(self._small.cell_size * scale)
67
+ return int(20 * scale)
68
+
69
+ def _draw_ui_text(
70
+ self,
71
+ text: str,
72
+ x: float,
73
+ y: float,
74
+ color: rl.Color,
75
+ scale: float = UI_TEXT_SCALE,
76
+ ) -> None:
77
+ if self._small is not None:
78
+ draw_small_text(self._small, text, x, y, scale, color)
79
+ else:
80
+ rl.draw_text(text, int(x), int(y), int(20 * scale), color)
81
+
82
+ def _update_ui_mouse(self) -> None:
83
+ mouse = rl.get_mouse_position()
84
+ screen_w = float(rl.get_screen_width())
85
+ screen_h = float(rl.get_screen_height())
86
+ self._ui_mouse_x = _clamp(float(mouse.x), 0.0, max(0.0, screen_w - 1.0))
87
+ self._ui_mouse_y = _clamp(float(mouse.y), 0.0, max(0.0, screen_h - 1.0))
88
+
89
+ def _draw_cursor_glow(self, *, x: float, y: float) -> None:
90
+ draw_cursor_glow(self._world.particles_texture, x=x, y=y)
91
+
92
+ def _handle_debug_input(self) -> None:
93
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
94
+ self.close_requested = True
95
+
96
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_SPACE):
97
+ self._simulate = not self._simulate
98
+
99
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ONE):
100
+ self._draw_world = not self._draw_world
101
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_TWO):
102
+ self._draw_world_aim = not self._draw_world_aim
103
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_THREE):
104
+ self._draw_expected_overlay = not self._draw_expected_overlay
105
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_FOUR):
106
+ self._show_cursor_glow = not self._show_cursor_glow
107
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_FIVE):
108
+ self._draw_test_circle = not self._draw_test_circle
109
+
110
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_H):
111
+ self._force_heat = not self._force_heat
112
+
113
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_LEFT_BRACKET):
114
+ self._forced_heat = max(0.0, self._forced_heat - 0.02)
115
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_RIGHT_BRACKET):
116
+ self._forced_heat = min(0.48, self._forced_heat + 0.02)
117
+
118
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_MINUS):
119
+ self._test_circle_radius = max(8.0, self._test_circle_radius - 8.0)
120
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_EQUAL):
121
+ self._test_circle_radius = min(512.0, self._test_circle_radius + 8.0)
122
+
123
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_R):
124
+ self._world.reset(seed=0xBEEF, player_count=1)
125
+ self._player = self._world.players[0] if self._world.players else None
126
+ self._world.update_camera(0.0)
127
+
128
+ def open(self) -> None:
129
+ self._missing_assets.clear()
130
+ try:
131
+ self._small = load_small_font(self._assets_root, self._missing_assets)
132
+ except Exception:
133
+ self._small = None
134
+
135
+ runtime_dir = default_runtime_dir()
136
+ if runtime_dir.is_dir():
137
+ try:
138
+ self._world.config = ensure_crimson_cfg(runtime_dir)
139
+ except Exception:
140
+ self._world.config = None
141
+ else:
142
+ self._world.config = None
143
+
144
+ self._world.reset(seed=0xBEEF, player_count=1)
145
+ self._player = self._world.players[0] if self._world.players else None
146
+ self._world.open()
147
+ self._world.update_camera(0.0)
148
+ self._ui_mouse_x = float(rl.get_screen_width()) * 0.5
149
+ self._ui_mouse_y = float(rl.get_screen_height()) * 0.5
150
+ self._cursor_pulse_time = 0.0
151
+
152
+ def close(self) -> None:
153
+ if self._small is not None:
154
+ rl.unload_texture(self._small.texture)
155
+ self._small = None
156
+ self._world.close()
157
+
158
+ def update(self, dt: float) -> None:
159
+ dt_frame = float(dt)
160
+ self._update_ui_mouse()
161
+ self._handle_debug_input()
162
+ self._cursor_pulse_time += dt_frame * 1.1
163
+
164
+ aim_x, aim_y = self._world.screen_to_world(self._ui_mouse_x, self._ui_mouse_y)
165
+ if self._player is not None:
166
+ self._player.aim_x = float(aim_x)
167
+ self._player.aim_y = float(aim_y)
168
+ if self._force_heat:
169
+ self._player.spread_heat = float(self._forced_heat)
170
+
171
+ move_x = 0.0
172
+ move_y = 0.0
173
+ if rl.is_key_down(rl.KeyboardKey.KEY_A):
174
+ move_x -= 1.0
175
+ if rl.is_key_down(rl.KeyboardKey.KEY_D):
176
+ move_x += 1.0
177
+ if rl.is_key_down(rl.KeyboardKey.KEY_W):
178
+ move_y -= 1.0
179
+ if rl.is_key_down(rl.KeyboardKey.KEY_S):
180
+ move_y += 1.0
181
+
182
+ dt_world = dt_frame if self._simulate else 0.0
183
+ self._world.update(
184
+ dt_world,
185
+ inputs=[
186
+ PlayerInput(
187
+ move_x=move_x,
188
+ move_y=move_y,
189
+ aim_x=float(aim_x),
190
+ aim_y=float(aim_y),
191
+ fire_down=False,
192
+ fire_pressed=False,
193
+ reload_pressed=False,
194
+ )
195
+ ],
196
+ auto_pick_perks=False,
197
+ perk_progression_enabled=False,
198
+ )
199
+
200
+ if self._player is not None and self._force_heat:
201
+ self._player.spread_heat = float(self._forced_heat)
202
+
203
+ def draw(self) -> None:
204
+ if self._draw_world:
205
+ self._world.draw(draw_aim_indicators=self._draw_world_aim)
206
+ else:
207
+ rl.clear_background(rl.Color(10, 10, 12, 255))
208
+
209
+ mouse_x = float(self._ui_mouse_x)
210
+ mouse_y = float(self._ui_mouse_y)
211
+
212
+ if self._draw_test_circle:
213
+ cx = float(rl.get_screen_width()) * 0.5
214
+ cy = float(rl.get_screen_height()) * 0.5
215
+ self._world._draw_aim_circle(x=cx, y=cy, radius=float(self._test_circle_radius))
216
+ rl.draw_circle_lines(int(cx), int(cy), int(max(1.0, self._test_circle_radius)), rl.Color(255, 80, 80, 220))
217
+
218
+ if self._show_cursor_glow:
219
+ self._draw_cursor_glow(x=mouse_x, y=mouse_y)
220
+
221
+ mouse_world_x, mouse_world_y = self._world.screen_to_world(mouse_x, mouse_y)
222
+ mouse_back_x, mouse_back_y = self._world.world_to_screen(float(mouse_world_x), float(mouse_world_y))
223
+
224
+ if self._draw_expected_overlay and self._player is not None:
225
+ dist = math.hypot(float(self._player.aim_x) - float(self._player.pos_x), float(self._player.aim_y) - float(self._player.pos_y))
226
+ radius = max(6.0, dist * float(self._player.spread_heat) * 0.5)
227
+ cam_x, cam_y, scale_x, scale_y = self._world._world_params()
228
+ scale = (scale_x + scale_y) * 0.5
229
+ screen_radius = max(1.0, radius * scale)
230
+ aim_screen_x, aim_screen_y = self._world.world_to_screen(float(self._player.aim_x), float(self._player.aim_y))
231
+
232
+ rl.draw_circle_lines(
233
+ int(aim_screen_x),
234
+ int(aim_screen_y),
235
+ int(max(1.0, screen_radius)),
236
+ rl.Color(80, 220, 120, 240),
237
+ )
238
+ rl.draw_line(
239
+ int(mouse_x),
240
+ int(mouse_y),
241
+ int(aim_screen_x),
242
+ int(aim_screen_y),
243
+ rl.Color(80, 220, 120, 200),
244
+ )
245
+
246
+ lines = [
247
+ "Aim debug view",
248
+ "SPACE simulate world update",
249
+ "1 world 2 aim-indicators 3 expected overlay 4 cursor glow 5 test circle",
250
+ f"H force_heat={self._force_heat} forced_heat={self._forced_heat:.2f} [ ] adjust",
251
+ f"test_circle_radius={self._test_circle_radius:.0f} -/+ adjust",
252
+ (
253
+ f"mouse=({mouse_x:.1f},{mouse_y:.1f}) -> "
254
+ f"world=({mouse_world_x:.1f},{mouse_world_y:.1f}) -> "
255
+ f"screen=({mouse_back_x:.1f},{mouse_back_y:.1f})"
256
+ ),
257
+ f"player_aim_world=({float(self._player.aim_x):.1f},{float(self._player.aim_y):.1f}) "
258
+ f"player_aim_screen=({aim_screen_x:.1f},{aim_screen_y:.1f})",
259
+ f"player=({float(self._player.pos_x):.1f},{float(self._player.pos_y):.1f}) dist={dist:.1f}",
260
+ f"spread_heat={float(self._player.spread_heat):.3f} r_world={radius:.2f} r_screen={screen_radius:.2f}",
261
+ f"cam=({cam_x:.2f},{cam_y:.2f}) scale=({scale_x:.3f},{scale_y:.3f}) demo_mode={self._world.demo_mode_active}",
262
+ f"bulletTrail={'yes' if self._world.bullet_trail_texture is not None else 'no'} "
263
+ f"particles={'yes' if self._world.particles_texture is not None else 'no'}",
264
+ ]
265
+ x0 = 16.0
266
+ y0 = 16.0
267
+ lh = float(self._ui_line_height())
268
+ for idx, line in enumerate(lines):
269
+ self._draw_ui_text(line, x0, y0 + lh * float(idx), UI_TEXT_COLOR if idx < 6 else UI_HINT_COLOR)
270
+ elif self._draw_expected_overlay and self._player is None:
271
+ self._draw_ui_text("Aim debug view: missing player", 16.0, 16.0, UI_ERROR_COLOR)
272
+
273
+
274
+ @register_view("aim-debug", "Aim indicator debug")
275
+ def _create_aim_debug_view(*, ctx: ViewContext) -> AimDebugView:
276
+ return AimDebugView(ctx)