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
crimson/ui/hud.py ADDED
@@ -0,0 +1,601 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ import math
6
+
7
+ import pyray as rl
8
+
9
+ from grim.assets import TextureLoader
10
+ from grim.fonts.small import SmallFontData, draw_small_text
11
+ from ..gameplay import BonusHudState, PlayerState, survival_level_threshold
12
+ from ..weapons import WEAPON_BY_ID
13
+
14
+ HUD_TEXT_COLOR = rl.Color(220, 220, 220, 255)
15
+ HUD_HINT_COLOR = rl.Color(170, 170, 180, 255)
16
+ HUD_ACCENT_COLOR = rl.Color(240, 200, 80, 255)
17
+
18
+ HUD_BASE_WIDTH = 1024.0
19
+ HUD_BASE_HEIGHT = 768.0
20
+
21
+ HUD_TOP_BAR_ALPHA = 0.7
22
+ HUD_ICON_ALPHA = 0.8
23
+ HUD_PANEL_ALPHA = 0.9
24
+ HUD_HEALTH_BG_ALPHA = 0.5
25
+ HUD_AMMO_DIM_ALPHA = 0.3
26
+
27
+ HUD_TOP_BAR_POS = (0.0, 0.0)
28
+ HUD_TOP_BAR_SIZE = (512.0, 64.0)
29
+ HUD_HEART_CENTER = (27.0, 21.0)
30
+ HUD_HEALTH_BAR_POS = (64.0, 16.0)
31
+ HUD_HEALTH_BAR_SIZE = (120.0, 9.0)
32
+ HUD_WEAPON_ICON_POS = (220.0, 2.0)
33
+ HUD_WEAPON_ICON_SIZE = (64.0, 32.0)
34
+ HUD_CLOCK_POS = (220.0, 2.0)
35
+ HUD_CLOCK_SIZE = (32.0, 32.0)
36
+ HUD_CLOCK_ALPHA = 0.9
37
+ HUD_AMMO_BASE_POS = (300.0, 10.0)
38
+ HUD_AMMO_BAR_SIZE = (6.0, 16.0)
39
+ HUD_AMMO_BAR_STEP = 6.0
40
+ HUD_AMMO_BAR_LIMIT = 30
41
+ HUD_AMMO_BAR_CLAMP = 20
42
+ HUD_AMMO_TEXT_OFFSET = (8.0, 1.0)
43
+ HUD_SURV_PANEL_POS = (-68.0, 60.0)
44
+ HUD_SURV_PANEL_SIZE = (182.0, 53.0)
45
+ HUD_SURV_XP_LABEL_POS = (4.0, 78.0)
46
+ HUD_SURV_XP_VALUE_POS = (26.0, 74.0)
47
+ HUD_SURV_LVL_VALUE_POS = (85.0, 79.0)
48
+ HUD_SURV_PROGRESS_POS = (26.0, 91.0)
49
+ HUD_SURV_PROGRESS_WIDTH = 54.0
50
+ HUD_BONUS_BASE_Y = 121.0
51
+ HUD_BONUS_ICON_SIZE = 32.0
52
+ HUD_BONUS_TEXT_OFFSET = (36.0, 6.0)
53
+ HUD_BONUS_SPACING = 52.0
54
+ HUD_BONUS_PANEL_OFFSET_Y = -11.0
55
+ HUD_XP_BAR_RGBA = (0.1, 0.3, 0.6, 1.0)
56
+
57
+ _SURVIVAL_XP_SMOOTHED = 0
58
+
59
+
60
+ @dataclass(slots=True)
61
+ class HudAssets:
62
+ game_top: rl.Texture | None
63
+ life_heart: rl.Texture | None
64
+ ind_life: rl.Texture | None
65
+ ind_panel: rl.Texture | None
66
+ ind_bullet: rl.Texture | None
67
+ ind_fire: rl.Texture | None
68
+ ind_rocket: rl.Texture | None
69
+ ind_electric: rl.Texture | None
70
+ wicons: rl.Texture | None
71
+ clock_table: rl.Texture | None
72
+ clock_pointer: rl.Texture | None
73
+ bonuses: rl.Texture | None
74
+ missing: list[str] = field(default_factory=list)
75
+
76
+
77
+ def hud_ui_scale(screen_w: float, screen_h: float) -> float:
78
+ scale = min(screen_w / HUD_BASE_WIDTH, screen_h / HUD_BASE_HEIGHT)
79
+ if scale < 0.75:
80
+ return 0.75
81
+ if scale > 1.5:
82
+ return 1.5
83
+ return float(scale)
84
+
85
+
86
+ def load_hud_assets(assets_root: Path) -> HudAssets:
87
+ loader = TextureLoader.from_assets_root(assets_root)
88
+ return HudAssets(
89
+ game_top=loader.get(name="iGameUI", paq_rel="ui/ui_gameTop.jaz", fs_rel="ui/ui_gameTop.png"),
90
+ life_heart=loader.get(name="iHeart", paq_rel="ui/ui_lifeHeart.jaz", fs_rel="ui/ui_lifeHeart.png"),
91
+ ind_life=loader.get(name="ui_indLife", paq_rel="ui/ui_indLife.jaz", fs_rel="ui/ui_indLife.png"),
92
+ ind_panel=loader.get(name="ui_indPanel", paq_rel="ui/ui_indPanel.jaz", fs_rel="ui/ui_indPanel.png"),
93
+ ind_bullet=loader.get(name="ui_indBullet", paq_rel="ui/ui_indBullet.jaz", fs_rel="ui/ui_indBullet.png"),
94
+ ind_fire=loader.get(name="ui_indFire", paq_rel="ui/ui_indFire.jaz", fs_rel="ui/ui_indFire.png"),
95
+ ind_rocket=loader.get(name="ui_indRocket", paq_rel="ui/ui_indRocket.jaz", fs_rel="ui/ui_indRocket.png"),
96
+ ind_electric=loader.get(
97
+ name="ui_indElectric",
98
+ paq_rel="ui/ui_indElectric.jaz",
99
+ fs_rel="ui/ui_indElectric.png",
100
+ ),
101
+ wicons=loader.get(name="ui_wicons", paq_rel="ui/ui_wicons.jaz", fs_rel="ui/ui_wicons.png"),
102
+ clock_table=loader.get(name="ui_clockTable", paq_rel="ui/ui_clockTable.jaz", fs_rel="ui/ui_clockTable.png"),
103
+ clock_pointer=loader.get(
104
+ name="ui_clockPointer",
105
+ paq_rel="ui/ui_clockPointer.jaz",
106
+ fs_rel="ui/ui_clockPointer.png",
107
+ ),
108
+ bonuses=loader.get(name="bonuses", paq_rel="game/bonuses.jaz", fs_rel="game/bonuses.png"),
109
+ missing=loader.missing,
110
+ )
111
+
112
+
113
+ def _draw_text(font: SmallFontData | None, text: str, x: float, y: float, scale: float, color: rl.Color) -> None:
114
+ if font is not None:
115
+ draw_small_text(font, text, x, y, scale, color)
116
+ else:
117
+ rl.draw_text(text, int(x), int(y), int(18 * scale), color)
118
+
119
+
120
+ def _with_alpha(color: rl.Color, alpha: float) -> rl.Color:
121
+ alpha = max(0.0, min(1.0, float(alpha)))
122
+ return rl.Color(color.r, color.g, color.b, int(color.a * alpha))
123
+
124
+
125
+ def _smooth_xp(target: int, frame_dt_ms: float) -> int:
126
+ global _SURVIVAL_XP_SMOOTHED
127
+ target = int(target)
128
+ if target <= 0:
129
+ _SURVIVAL_XP_SMOOTHED = 0
130
+ return 0
131
+ smoothed = int(_SURVIVAL_XP_SMOOTHED)
132
+ if smoothed == target:
133
+ return smoothed
134
+ step = max(1, int(frame_dt_ms) // 2)
135
+ diff = abs(smoothed - target)
136
+ if diff > 1000:
137
+ step *= diff // 100
138
+ if smoothed < target:
139
+ smoothed += step
140
+ if smoothed > target:
141
+ smoothed = target
142
+ else:
143
+ smoothed -= step
144
+ if smoothed < target:
145
+ smoothed = target
146
+ _SURVIVAL_XP_SMOOTHED = smoothed
147
+ return smoothed
148
+
149
+
150
+ def _draw_progress_bar(x: float, y: float, width: float, ratio: float, rgba: tuple[float, float, float, float], scale: float) -> None:
151
+ ratio = max(0.0, min(1.0, float(ratio)))
152
+ width = max(0.0, float(width))
153
+ if width <= 0.0:
154
+ return
155
+ bar_h = 4.0 * scale
156
+ inner_h = 2.0 * scale
157
+ bg_color = rl.Color(
158
+ int(255 * rgba[0] * 0.6),
159
+ int(255 * rgba[1] * 0.6),
160
+ int(255 * rgba[2] * 0.6),
161
+ int(255 * rgba[3] * 0.4),
162
+ )
163
+ fg_color = rl.Color(
164
+ int(255 * rgba[0]),
165
+ int(255 * rgba[1]),
166
+ int(255 * rgba[2]),
167
+ int(255 * rgba[3]),
168
+ )
169
+ rl.draw_rectangle(int(x), int(y), int(width), int(bar_h), bg_color)
170
+ inner_w = max(0.0, (width - 2.0 * scale) * ratio)
171
+ rl.draw_rectangle(int(x + scale), int(y + scale), int(inner_w), int(inner_h), fg_color)
172
+
173
+
174
+ def _weapon_icon_index(weapon_id: int) -> int | None:
175
+ entry = WEAPON_BY_ID.get(int(weapon_id))
176
+ icon_index = entry.icon_index if entry is not None else None
177
+ if icon_index is None or icon_index < 0 or icon_index > 31:
178
+ return None
179
+ return int(icon_index)
180
+
181
+
182
+ def _weapon_ammo_class(weapon_id: int) -> int:
183
+ entry = WEAPON_BY_ID.get(int(weapon_id))
184
+ value = entry.ammo_class if entry is not None else None
185
+ return int(value) if value is not None else 0
186
+
187
+
188
+ def _weapon_icon_src(texture: rl.Texture, icon_index: int) -> rl.Rectangle:
189
+ grid = 8
190
+ cell_w = float(texture.width) / grid
191
+ cell_h = float(texture.height) / grid
192
+ frame = int(icon_index) * 2
193
+ col = frame % grid
194
+ row = frame // grid
195
+ return rl.Rectangle(float(col * cell_w), float(row * cell_h), float(cell_w * 2), float(cell_h))
196
+
197
+
198
+ def _bonus_icon_src(texture: rl.Texture, icon_id: int) -> rl.Rectangle:
199
+ grid = 4
200
+ cell_w = float(texture.width) / grid
201
+ cell_h = float(texture.height) / grid
202
+ col = int(icon_id) % grid
203
+ row = int(icon_id) // grid
204
+ return rl.Rectangle(float(col * cell_w), float(row * cell_h), float(cell_w), float(cell_h))
205
+
206
+
207
+ def draw_hud_overlay(
208
+ assets: HudAssets,
209
+ *,
210
+ player: PlayerState,
211
+ players: list[PlayerState] | None = None,
212
+ bonus_hud: BonusHudState | None = None,
213
+ elapsed_ms: float = 0.0,
214
+ score: int | None = None,
215
+ font: SmallFontData | None = None,
216
+ alpha: float = 1.0,
217
+ frame_dt_ms: float | None = None,
218
+ show_weapon: bool = True,
219
+ show_xp: bool = True,
220
+ show_time: bool = False,
221
+ ) -> float:
222
+ if frame_dt_ms is None:
223
+ frame_dt_ms = max(0.0, float(rl.get_frame_time()) * 1000.0)
224
+ hud_players = list(players) if players is not None else [player]
225
+ if not hud_players:
226
+ hud_players = [player]
227
+ player_count = len(hud_players)
228
+
229
+ screen_w = float(rl.get_screen_width())
230
+ screen_h = float(rl.get_screen_height())
231
+ scale = hud_ui_scale(screen_w, screen_h)
232
+ text_scale = 1.0 * scale
233
+ line_h = float(font.cell_size) * text_scale if font is not None else 18.0 * text_scale
234
+
235
+ def sx(value: float) -> float:
236
+ return value * scale
237
+
238
+ def sy(value: float) -> float:
239
+ return value * scale
240
+
241
+ max_y = 0.0
242
+ alpha = max(0.0, min(1.0, float(alpha)))
243
+ text_color = _with_alpha(HUD_TEXT_COLOR, alpha)
244
+ accent_color = _with_alpha(HUD_ACCENT_COLOR, alpha)
245
+ panel_text_color = _with_alpha(HUD_TEXT_COLOR, alpha * HUD_PANEL_ALPHA)
246
+
247
+ # Top bar background.
248
+ if assets.game_top is not None:
249
+ src = rl.Rectangle(0.0, 0.0, float(assets.game_top.width), float(assets.game_top.height))
250
+ dst = rl.Rectangle(
251
+ sx(HUD_TOP_BAR_POS[0]),
252
+ sy(HUD_TOP_BAR_POS[1]),
253
+ sx(HUD_TOP_BAR_SIZE[0]),
254
+ sy(HUD_TOP_BAR_SIZE[1]),
255
+ )
256
+ top_alpha = alpha * HUD_TOP_BAR_ALPHA
257
+ rl.draw_texture_pro(
258
+ assets.game_top,
259
+ src,
260
+ dst,
261
+ rl.Vector2(0.0, 0.0),
262
+ 0.0,
263
+ rl.Color(255, 255, 255, int(255 * top_alpha)),
264
+ )
265
+ max_y = max(max_y, dst.y + dst.height)
266
+
267
+ # Pulsing heart.
268
+ if assets.life_heart is not None:
269
+ t = max(0.0, elapsed_ms) / 1000.0
270
+ src = rl.Rectangle(0.0, 0.0, float(assets.life_heart.width), float(assets.life_heart.height))
271
+ if player_count == 1:
272
+ base_center_x, base_center_y = HUD_HEART_CENTER
273
+ heart_step_y = 0.0
274
+ heart_scale = 1.0
275
+ else:
276
+ base_center_x = 27.0
277
+ base_center_y = 12.0
278
+ heart_step_y = 15.0
279
+ heart_scale = 0.5
280
+
281
+ for idx, hud_player in enumerate(hud_players):
282
+ pulse_speed = 5.0 if hud_player.health < 30.0 else 2.0
283
+ phase = float(idx) * (math.pi * 0.5)
284
+ pulse = ((math.sin(t * pulse_speed + phase) ** 4) * 4.0 + 14.0) * heart_scale
285
+ size = pulse * 2.0
286
+ center_y = base_center_y + float(idx) * heart_step_y
287
+ dst = rl.Rectangle(
288
+ sx(base_center_x - pulse),
289
+ sy(center_y - pulse),
290
+ sx(size),
291
+ sy(size),
292
+ )
293
+ rl.draw_texture_pro(
294
+ assets.life_heart,
295
+ src,
296
+ dst,
297
+ rl.Vector2(0.0, 0.0),
298
+ 0.0,
299
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_ICON_ALPHA)),
300
+ )
301
+ max_y = max(max_y, dst.y + dst.height)
302
+
303
+ # Health bar.
304
+ if assets.ind_life is not None:
305
+ bar_x, bar_y = HUD_HEALTH_BAR_POS
306
+ bar_w, bar_h = HUD_HEALTH_BAR_SIZE
307
+ bg_src = rl.Rectangle(0.0, 0.0, float(assets.ind_life.width), float(assets.ind_life.height))
308
+ if player_count > 1:
309
+ bar_y = 6.0
310
+
311
+ for idx, hud_player in enumerate(hud_players):
312
+ bar_y_offset = float(idx) * 16.0 if player_count > 1 else 0.0
313
+ bg_dst = rl.Rectangle(sx(bar_x), sy(bar_y + bar_y_offset), sx(bar_w), sy(bar_h))
314
+ rl.draw_texture_pro(
315
+ assets.ind_life,
316
+ bg_src,
317
+ bg_dst,
318
+ rl.Vector2(0.0, 0.0),
319
+ 0.0,
320
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_HEALTH_BG_ALPHA)),
321
+ )
322
+ health_ratio = max(0.0, min(1.0, hud_player.health / 100.0))
323
+ if health_ratio > 0.0:
324
+ fill_w = bar_w * health_ratio
325
+ fill_dst = rl.Rectangle(sx(bar_x), sy(bar_y + bar_y_offset), sx(fill_w), sy(bar_h))
326
+ fill_src = rl.Rectangle(
327
+ 0.0,
328
+ 0.0,
329
+ float(assets.ind_life.width) * health_ratio,
330
+ float(assets.ind_life.height),
331
+ )
332
+ rl.draw_texture_pro(
333
+ assets.ind_life,
334
+ fill_src,
335
+ fill_dst,
336
+ rl.Vector2(0.0, 0.0),
337
+ 0.0,
338
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_ICON_ALPHA)),
339
+ )
340
+ max_y = max(max_y, bg_dst.y + bg_dst.height)
341
+
342
+ # Weapon icon.
343
+ if show_weapon and assets.wicons is not None:
344
+ if player_count == 1:
345
+ base_x, base_y = HUD_WEAPON_ICON_POS
346
+ icon_w, icon_h = HUD_WEAPON_ICON_SIZE
347
+ icon_step_y = 0.0
348
+ else:
349
+ base_x = 220.0
350
+ base_y = 4.0
351
+ icon_w = 32.0
352
+ icon_h = 16.0
353
+ icon_step_y = 16.0
354
+
355
+ for idx, hud_player in enumerate(hud_players):
356
+ icon_index = _weapon_icon_index(hud_player.weapon_id)
357
+ if icon_index is None:
358
+ continue
359
+ src = _weapon_icon_src(assets.wicons, icon_index)
360
+ dst = rl.Rectangle(
361
+ sx(base_x),
362
+ sy(base_y + float(idx) * icon_step_y),
363
+ sx(icon_w),
364
+ sy(icon_h),
365
+ )
366
+ rl.draw_texture_pro(
367
+ assets.wicons,
368
+ src,
369
+ dst,
370
+ rl.Vector2(0.0, 0.0),
371
+ 0.0,
372
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_ICON_ALPHA)),
373
+ )
374
+ max_y = max(max_y, dst.y + dst.height)
375
+
376
+ # Ammo bars.
377
+ if show_weapon:
378
+ if player_count == 1:
379
+ ammo_base_x, ammo_base_y = HUD_AMMO_BASE_POS
380
+ ammo_step_y = 0.0
381
+ else:
382
+ ammo_base_x = 290.0
383
+ ammo_base_y = 4.0
384
+ ammo_step_y = 14.0
385
+
386
+ base_alpha = alpha * HUD_ICON_ALPHA
387
+ for player_idx, hud_player in enumerate(hud_players):
388
+ ammo_tex = None
389
+ ammo_class = _weapon_ammo_class(hud_player.weapon_id)
390
+ if ammo_class == 1:
391
+ ammo_tex = assets.ind_fire
392
+ elif ammo_class == 2:
393
+ ammo_tex = assets.ind_rocket
394
+ elif ammo_class == 0:
395
+ ammo_tex = assets.ind_bullet
396
+ else:
397
+ ammo_tex = assets.ind_electric
398
+ if ammo_tex is None:
399
+ continue
400
+
401
+ base_y = ammo_base_y + float(player_idx) * ammo_step_y
402
+ bars = max(0, int(hud_player.clip_size))
403
+ if bars > HUD_AMMO_BAR_LIMIT:
404
+ bars = HUD_AMMO_BAR_CLAMP
405
+ ammo_count = max(0, int(hud_player.ammo))
406
+ for idx in range(bars):
407
+ bar_alpha = base_alpha if idx < ammo_count else base_alpha * HUD_AMMO_DIM_ALPHA
408
+ dst = rl.Rectangle(
409
+ sx(ammo_base_x + idx * HUD_AMMO_BAR_STEP),
410
+ sy(base_y),
411
+ sx(HUD_AMMO_BAR_SIZE[0]),
412
+ sy(HUD_AMMO_BAR_SIZE[1]),
413
+ )
414
+ src = rl.Rectangle(0.0, 0.0, float(ammo_tex.width), float(ammo_tex.height))
415
+ rl.draw_texture_pro(
416
+ ammo_tex,
417
+ src,
418
+ dst,
419
+ rl.Vector2(0.0, 0.0),
420
+ 0.0,
421
+ rl.Color(255, 255, 255, int(255 * bar_alpha)),
422
+ )
423
+ max_y = max(max_y, dst.y + dst.height)
424
+ if ammo_count > bars:
425
+ extra = ammo_count - bars
426
+ text_x = ammo_base_x + bars * HUD_AMMO_BAR_STEP + HUD_AMMO_TEXT_OFFSET[0]
427
+ text_y = base_y + HUD_AMMO_TEXT_OFFSET[1]
428
+ _draw_text(font, f"+ {extra}", sx(text_x), sy(text_y), text_scale, text_color)
429
+
430
+ # Survival XP panel.
431
+ xp_target = int(player.experience if score is None else score)
432
+ xp_display = _smooth_xp(xp_target, frame_dt_ms) if show_xp else xp_target
433
+ if show_xp and assets.ind_panel is not None:
434
+ panel_x, panel_y = HUD_SURV_PANEL_POS
435
+ panel_w, panel_h = HUD_SURV_PANEL_SIZE
436
+ dst = rl.Rectangle(sx(panel_x), sy(panel_y), sx(panel_w), sy(panel_h))
437
+ src = rl.Rectangle(0.0, 0.0, float(assets.ind_panel.width), float(assets.ind_panel.height))
438
+ rl.draw_texture_pro(
439
+ assets.ind_panel,
440
+ src,
441
+ dst,
442
+ rl.Vector2(0.0, 0.0),
443
+ 0.0,
444
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_PANEL_ALPHA)),
445
+ )
446
+ max_y = max(max_y, dst.y + dst.height)
447
+
448
+ if show_xp:
449
+ _draw_text(
450
+ font,
451
+ "Xp",
452
+ sx(HUD_SURV_XP_LABEL_POS[0]),
453
+ sy(HUD_SURV_XP_LABEL_POS[1]),
454
+ text_scale,
455
+ panel_text_color,
456
+ )
457
+ _draw_text(
458
+ font,
459
+ f"{xp_display}",
460
+ sx(HUD_SURV_XP_VALUE_POS[0]),
461
+ sy(HUD_SURV_XP_VALUE_POS[1]),
462
+ text_scale,
463
+ panel_text_color,
464
+ )
465
+ _draw_text(
466
+ font,
467
+ f"{int(player.level)}",
468
+ sx(HUD_SURV_LVL_VALUE_POS[0]),
469
+ sy(HUD_SURV_LVL_VALUE_POS[1]),
470
+ text_scale,
471
+ panel_text_color,
472
+ )
473
+
474
+ level = max(1, int(player.level))
475
+ prev_threshold = 0 if level <= 1 else survival_level_threshold(level - 1)
476
+ next_threshold = survival_level_threshold(level)
477
+ progress_ratio = 0.0
478
+ if next_threshold > prev_threshold:
479
+ progress_ratio = (xp_target - prev_threshold) / float(next_threshold - prev_threshold)
480
+ bar_x, bar_y = HUD_SURV_PROGRESS_POS
481
+ bar_w = HUD_SURV_PROGRESS_WIDTH
482
+ bar_rgba = (HUD_XP_BAR_RGBA[0], HUD_XP_BAR_RGBA[1], HUD_XP_BAR_RGBA[2], HUD_XP_BAR_RGBA[3] * alpha)
483
+ _draw_progress_bar(sx(bar_x), sy(bar_y), sx(bar_w), progress_ratio, bar_rgba, scale)
484
+ max_y = max(max_y, sy(bar_y + 4.0))
485
+
486
+ # Mode time clock/text (rush/typo-style HUD).
487
+ if show_time:
488
+ time_ms = max(0.0, float(elapsed_ms))
489
+ if assets.clock_table is not None:
490
+ dst = rl.Rectangle(
491
+ sx(HUD_CLOCK_POS[0]),
492
+ sy(HUD_CLOCK_POS[1]),
493
+ sx(HUD_CLOCK_SIZE[0]),
494
+ sy(HUD_CLOCK_SIZE[1]),
495
+ )
496
+ src = rl.Rectangle(0.0, 0.0, float(assets.clock_table.width), float(assets.clock_table.height))
497
+ rl.draw_texture_pro(
498
+ assets.clock_table,
499
+ src,
500
+ dst,
501
+ rl.Vector2(0.0, 0.0),
502
+ 0.0,
503
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_CLOCK_ALPHA)),
504
+ )
505
+ max_y = max(max_y, dst.y + dst.height)
506
+ if assets.clock_pointer is not None:
507
+ # NOTE: Raylib's draw_texture_pro uses dst.x/y as the rotation origin position;
508
+ # offset by half-size so the 32x32 quad stays aligned with the table.
509
+ dst = rl.Rectangle(
510
+ sx(HUD_CLOCK_POS[0] + HUD_CLOCK_SIZE[0] * 0.5),
511
+ sy(HUD_CLOCK_POS[1] + HUD_CLOCK_SIZE[1] * 0.5),
512
+ sx(HUD_CLOCK_SIZE[0]),
513
+ sy(HUD_CLOCK_SIZE[1]),
514
+ )
515
+ src = rl.Rectangle(0.0, 0.0, float(assets.clock_pointer.width), float(assets.clock_pointer.height))
516
+ rotation = time_ms / 1000.0 * 6.0
517
+ origin = rl.Vector2(sx(HUD_CLOCK_SIZE[0] * 0.5), sy(HUD_CLOCK_SIZE[1] * 0.5))
518
+ rl.draw_texture_pro(
519
+ assets.clock_pointer,
520
+ src,
521
+ dst,
522
+ origin,
523
+ rotation,
524
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_CLOCK_ALPHA)),
525
+ )
526
+ total_seconds = max(0, int(time_ms) // 1000)
527
+ time_text = f"{total_seconds} seconds"
528
+ _draw_text(font, time_text, sx(255.0), sy(10.0), text_scale, text_color)
529
+ max_y = max(max_y, sy(10.0 + line_h))
530
+
531
+ # Bonus HUD slots (text + icons), anchored below the survival panel.
532
+ slots = [slot for slot in bonus_hud.slots if slot.active] if bonus_hud is not None else []
533
+ if slots:
534
+ bonus_x = sx(4.0)
535
+ bonus_y = sy(HUD_BONUS_BASE_Y)
536
+ for slot in slots[:16]:
537
+ if assets.ind_panel is not None:
538
+ panel_x, panel_y = HUD_SURV_PANEL_POS
539
+ dst = rl.Rectangle(
540
+ sx(panel_x),
541
+ bonus_y + sy(HUD_BONUS_PANEL_OFFSET_Y),
542
+ sx(HUD_SURV_PANEL_SIZE[0]),
543
+ sy(HUD_SURV_PANEL_SIZE[1]),
544
+ )
545
+ src = rl.Rectangle(0.0, 0.0, float(assets.ind_panel.width), float(assets.ind_panel.height))
546
+ rl.draw_texture_pro(
547
+ assets.ind_panel,
548
+ src,
549
+ dst,
550
+ rl.Vector2(0.0, 0.0),
551
+ 0.0,
552
+ rl.Color(255, 255, 255, int(255 * alpha * HUD_TOP_BAR_ALPHA)),
553
+ )
554
+ max_y = max(max_y, dst.y + dst.height)
555
+
556
+ icon_drawn = False
557
+ if assets.bonuses is not None and slot.icon_id >= 0:
558
+ src = _bonus_icon_src(assets.bonuses, slot.icon_id)
559
+ dst = rl.Rectangle(bonus_x, bonus_y, sx(HUD_BONUS_ICON_SIZE), sy(HUD_BONUS_ICON_SIZE))
560
+ rl.draw_texture_pro(
561
+ assets.bonuses,
562
+ src,
563
+ dst,
564
+ rl.Vector2(0.0, 0.0),
565
+ 0.0,
566
+ rl.Color(255, 255, 255, int(255 * alpha)),
567
+ )
568
+ label_x = bonus_x + sx(HUD_BONUS_TEXT_OFFSET[0])
569
+ icon_drawn = True
570
+ max_y = max(max_y, dst.y + dst.height)
571
+ else:
572
+ label_x = bonus_x
573
+
574
+ if not icon_drawn and assets.wicons is not None:
575
+ alt_icon_index = _weapon_icon_index(player.weapon_id)
576
+ if alt_icon_index is not None:
577
+ src = _weapon_icon_src(assets.wicons, alt_icon_index)
578
+ dst = rl.Rectangle(bonus_x, bonus_y, sx(HUD_BONUS_ICON_SIZE), sy(HUD_BONUS_ICON_SIZE))
579
+ rl.draw_texture_pro(
580
+ assets.wicons,
581
+ src,
582
+ dst,
583
+ rl.Vector2(0.0, 0.0),
584
+ 0.0,
585
+ rl.Color(255, 255, 255, int(255 * alpha)),
586
+ )
587
+ label_x = bonus_x + sx(HUD_BONUS_TEXT_OFFSET[0])
588
+ max_y = max(max_y, dst.y + dst.height)
589
+
590
+ _draw_text(
591
+ font,
592
+ slot.label,
593
+ label_x,
594
+ bonus_y + sy(HUD_BONUS_TEXT_OFFSET[1]),
595
+ text_scale,
596
+ accent_color,
597
+ )
598
+ bonus_y += sy(HUD_BONUS_SPACING)
599
+ max_y = max(max_y, bonus_y)
600
+
601
+ return max_y