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,502 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import random
5
+
6
+ import pyray as rl
7
+
8
+ from grim.assets import PaqTextureCache
9
+ from grim.audio import AudioState
10
+ from grim.config import CrimsonConfig
11
+ from grim.fonts.grim_mono import GrimMonoFont, load_grim_mono_font
12
+ from grim.view import ViewContext
13
+
14
+ from ..game_modes import GameMode
15
+ from ..gameplay import most_used_weapon_id_for_player, weapon_assign_player
16
+ from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
17
+ from ..persistence.save_status import GameStatus
18
+ from ..quests import quest_by_level
19
+ from ..quests.runtime import build_quest_spawn_table, tick_quest_completion_transition
20
+ from ..quests.timeline import quest_spawn_table_empty, tick_quest_mode_spawns
21
+ from ..quests.types import QuestContext, QuestDefinition, SpawnEntry
22
+ from ..terrain_assets import terrain_texture_by_id
23
+ from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
24
+ from ..ui.hud import draw_hud_overlay, hud_ui_scale
25
+ from ..ui.perk_menu import PerkMenuAssets, load_perk_menu_assets
26
+ from ..views.quest_title_overlay import draw_quest_title_overlay
27
+ from .base_gameplay_mode import BaseGameplayMode
28
+
29
+ WORLD_SIZE = 1024.0
30
+ QUEST_TITLE_FADE_IN_MS = 500.0
31
+ QUEST_TITLE_HOLD_MS = 1000.0
32
+ QUEST_TITLE_FADE_OUT_MS = 500.0
33
+ QUEST_TITLE_TOTAL_MS = QUEST_TITLE_FADE_IN_MS + QUEST_TITLE_HOLD_MS + QUEST_TITLE_FADE_OUT_MS
34
+
35
+
36
+ @dataclass(slots=True)
37
+ class _QuestRunState:
38
+ quest: QuestDefinition | None = None
39
+ level: str = ""
40
+ spawn_entries: tuple[SpawnEntry, ...] = ()
41
+ total_spawn_count: int = 0
42
+ max_trigger_time_ms: int = 0
43
+ spawn_timeline_ms: float = 0.0
44
+ quest_name_timer_ms: float = 0.0
45
+ no_creatures_timer_ms: float = 0.0
46
+ completion_transition_ms: float = -1.0
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class QuestRunOutcome:
51
+ kind: str # "completed" | "failed"
52
+ level: str
53
+ base_time_ms: int
54
+ player_health: float
55
+ player2_health: float | None
56
+ pending_perk_count: int
57
+ experience: int
58
+ kill_count: int
59
+ weapon_id: int
60
+ shots_fired: int
61
+ shots_hit: int
62
+ most_used_weapon_id: int
63
+
64
+
65
+ def _quest_seed(level: str) -> int:
66
+ tier_text, quest_text = level.split(".", 1)
67
+ try:
68
+ return int(tier_text) * 100 + int(quest_text)
69
+ except ValueError:
70
+ return sum(ord(ch) for ch in level)
71
+
72
+
73
+ def _quest_attempt_counter_index(level: str) -> int | None:
74
+ try:
75
+ tier_text, quest_text = level.split(".", 1)
76
+ tier = int(tier_text)
77
+ quest = int(quest_text)
78
+ except ValueError:
79
+ return None
80
+ global_index = (tier - 1) * 10 + (quest - 1)
81
+ if not (0 <= global_index < 40):
82
+ return None
83
+ return global_index + 11
84
+
85
+
86
+ def _quest_level_label(level: str) -> str:
87
+ try:
88
+ tier_text, quest_text = level.split(".", 1)
89
+ return f"{int(tier_text)}-{int(quest_text)}"
90
+ except Exception:
91
+ return level.replace(".", "-", 1)
92
+
93
+
94
+ class QuestMode(BaseGameplayMode):
95
+ def __init__(
96
+ self,
97
+ ctx: ViewContext,
98
+ *,
99
+ demo_mode_active: bool = False,
100
+ texture_cache: PaqTextureCache | None = None,
101
+ config: CrimsonConfig | None = None,
102
+ audio: AudioState | None = None,
103
+ audio_rng: random.Random | None = None,
104
+ ) -> None:
105
+ super().__init__(
106
+ ctx,
107
+ world_size=WORLD_SIZE,
108
+ default_game_mode_id=int(GameMode.QUESTS),
109
+ demo_mode_active=bool(demo_mode_active),
110
+ difficulty_level=0,
111
+ hardcore=False,
112
+ texture_cache=texture_cache,
113
+ config=config,
114
+ audio=audio,
115
+ audio_rng=audio_rng,
116
+ )
117
+ self._quest = _QuestRunState()
118
+ self._selected_level: str | None = None
119
+ self._outcome: QuestRunOutcome | None = None
120
+ self._ui_assets: PerkMenuAssets | None = None
121
+ self._grim_mono: GrimMonoFont | None = None
122
+
123
+ def open(self) -> None:
124
+ super().open()
125
+ self._quest = _QuestRunState()
126
+ self._outcome = None
127
+ self._ui_assets = load_perk_menu_assets(self._assets_root)
128
+ if self._ui_assets.missing:
129
+ self._missing_assets.extend(self._ui_assets.missing)
130
+ try:
131
+ self._grim_mono = load_grim_mono_font(self._assets_root, self._missing_assets)
132
+ except Exception:
133
+ self._grim_mono = None
134
+
135
+ def close(self) -> None:
136
+ if self._grim_mono is not None:
137
+ rl.unload_texture(self._grim_mono.texture)
138
+ self._grim_mono = None
139
+ self._ui_assets = None
140
+ super().close()
141
+
142
+ def select_level(self, level: str | None) -> None:
143
+ self._selected_level = level
144
+
145
+ def consume_outcome(self) -> QuestRunOutcome | None:
146
+ outcome = self._outcome
147
+ self._outcome = None
148
+ return outcome
149
+
150
+ def prepare_new_run(self, level: str, *, status: GameStatus | None) -> None:
151
+ quest = quest_by_level(level)
152
+ if quest is None:
153
+ self._quest = _QuestRunState(level=level)
154
+ return
155
+ self._outcome = None
156
+
157
+ hardcore_flag = False
158
+ if self._config is not None:
159
+ hardcore_flag = bool(int(self._config.data.get("hardcore_flag", 0) or 0))
160
+
161
+ self._world.hardcore = hardcore_flag
162
+ seed = _quest_seed(level)
163
+
164
+ player_count = 1
165
+ config = self._config
166
+ if config is not None:
167
+ try:
168
+ player_count = int(config.data.get("player_count", 1) or 1)
169
+ except Exception:
170
+ player_count = 1
171
+ self._world.reset(seed=seed, player_count=max(1, min(4, player_count)))
172
+ self._bind_world()
173
+ self._state.status = status
174
+ self._state.quest_stage_major, self._state.quest_stage_minor = quest.level_key
175
+
176
+ base_id, overlay_id, detail_id = quest.terrain_ids or (0, 1, 0)
177
+ base = terrain_texture_by_id(int(base_id))
178
+ overlay = terrain_texture_by_id(int(overlay_id))
179
+ detail = terrain_texture_by_id(int(detail_id))
180
+ if base is not None and overlay is not None:
181
+ base_key, base_path = base
182
+ overlay_key, overlay_path = overlay
183
+ detail_key = detail[0] if detail is not None else None
184
+ detail_path = detail[1] if detail is not None else None
185
+ self._world.set_terrain(
186
+ base_key=base_key,
187
+ overlay_key=overlay_key,
188
+ base_path=base_path,
189
+ overlay_path=overlay_path,
190
+ detail_key=detail_key,
191
+ detail_path=detail_path,
192
+ )
193
+
194
+ # Quest metadata already stores native (1-based) weapon ids.
195
+ start_weapon_id = max(1, int(quest.start_weapon_id))
196
+ for player in self._world.players:
197
+ weapon_assign_player(player, start_weapon_id)
198
+
199
+ ctx = QuestContext(
200
+ width=int(self._world.world_size),
201
+ height=int(self._world.world_size),
202
+ player_count=len(self._world.players),
203
+ )
204
+ entries = build_quest_spawn_table(
205
+ quest,
206
+ ctx,
207
+ seed=seed,
208
+ hardcore=hardcore_flag,
209
+ full_version=not self._world.demo_mode_active,
210
+ )
211
+ total_spawn_count = sum(int(entry.count) for entry in entries)
212
+ max_trigger_ms = max((int(entry.trigger_ms) for entry in entries), default=0)
213
+
214
+ self._quest = _QuestRunState(
215
+ quest=quest,
216
+ level=str(level),
217
+ spawn_entries=entries,
218
+ total_spawn_count=int(total_spawn_count),
219
+ max_trigger_time_ms=int(max_trigger_ms),
220
+ spawn_timeline_ms=0.0,
221
+ quest_name_timer_ms=0.0,
222
+ no_creatures_timer_ms=0.0,
223
+ completion_transition_ms=-1.0,
224
+ )
225
+
226
+ if status is not None:
227
+ idx = _quest_attempt_counter_index(level)
228
+ if idx is not None:
229
+ status.increment_quest_play_count(idx)
230
+
231
+ def _handle_input(self) -> None:
232
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
233
+ self._paused = not self._paused
234
+
235
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
236
+ self.close_requested = True
237
+
238
+ def _build_input(self):
239
+ keybinds = config_keybinds(self._config)
240
+ if not keybinds:
241
+ keybinds = (0x11, 0x1F, 0x1E, 0x20, 0x100)
242
+ up_key, down_key, left_key, right_key, fire_key = player_move_fire_binds(keybinds, 0)
243
+
244
+ move_x = 0.0
245
+ move_y = 0.0
246
+ if input_code_is_down(left_key):
247
+ move_x -= 1.0
248
+ if input_code_is_down(right_key):
249
+ move_x += 1.0
250
+ if input_code_is_down(up_key):
251
+ move_y -= 1.0
252
+ if input_code_is_down(down_key):
253
+ move_y += 1.0
254
+
255
+ mouse = self._ui_mouse_pos()
256
+ aim_x, aim_y = self._world.screen_to_world(float(mouse.x), float(mouse.y))
257
+
258
+ fire_down = input_code_is_down(fire_key)
259
+ fire_pressed = input_code_is_pressed(fire_key)
260
+ reload_key = 0x102
261
+ if self._config is not None:
262
+ reload_key = int(self._config.data.get("keybind_reload", reload_key) or reload_key)
263
+ reload_pressed = input_code_is_pressed(reload_key)
264
+
265
+ from ..gameplay import PlayerInput
266
+
267
+ return PlayerInput(
268
+ move_x=move_x,
269
+ move_y=move_y,
270
+ aim_x=float(aim_x),
271
+ aim_y=float(aim_y),
272
+ fire_down=bool(fire_down),
273
+ fire_pressed=bool(fire_pressed),
274
+ reload_pressed=bool(reload_pressed),
275
+ )
276
+
277
+ def update(self, dt: float) -> None:
278
+ self._update_audio(dt)
279
+
280
+ dt_frame = self._tick_frame(dt)[0]
281
+ dt_ms = float(dt_frame * 1000.0)
282
+ self._handle_input()
283
+
284
+ if self.close_requested:
285
+ return
286
+
287
+ any_alive = any(player.health > 0.0 for player in self._world.players)
288
+ dt_world = 0.0 if self._paused or (not any_alive) else dt_frame
289
+ if dt_world <= 0.0:
290
+ return
291
+
292
+ self._quest.quest_name_timer_ms += dt_ms
293
+
294
+ input_state = self._build_input()
295
+ self._world.update(
296
+ dt_world,
297
+ inputs=[input_state for _ in self._world.players],
298
+ auto_pick_perks=False,
299
+ game_mode=int(GameMode.QUESTS),
300
+ perk_progression_enabled=True,
301
+ )
302
+
303
+ any_alive_after = any(player.health > 0.0 for player in self._world.players)
304
+ if not any_alive_after:
305
+ if self._outcome is None:
306
+ fired = 0
307
+ hit = 0
308
+ try:
309
+ fired = int(self._state.shots_fired[int(self._player.index)])
310
+ hit = int(self._state.shots_hit[int(self._player.index)])
311
+ except Exception:
312
+ fired = 0
313
+ hit = 0
314
+ fired = max(0, int(fired))
315
+ hit = max(0, min(int(hit), fired))
316
+ most_used_weapon_id = most_used_weapon_id_for_player(
317
+ self._state,
318
+ player_index=int(self._player.index),
319
+ fallback_weapon_id=int(self._player.weapon_id),
320
+ )
321
+ player2_health = None
322
+ if len(self._world.players) >= 2:
323
+ player2_health = float(self._world.players[1].health)
324
+ self._outcome = QuestRunOutcome(
325
+ kind="failed",
326
+ level=str(self._quest.level),
327
+ base_time_ms=int(self._quest.spawn_timeline_ms),
328
+ player_health=float(self._player.health),
329
+ player2_health=player2_health,
330
+ pending_perk_count=int(self._state.perk_selection.pending_count),
331
+ experience=int(self._player.experience),
332
+ kill_count=int(self._creatures.kill_count),
333
+ weapon_id=int(self._player.weapon_id),
334
+ shots_fired=fired,
335
+ shots_hit=hit,
336
+ most_used_weapon_id=int(most_used_weapon_id),
337
+ )
338
+ self.close_requested = True
339
+ return
340
+
341
+ creatures_none_active = not bool(self._creatures.iter_active())
342
+
343
+ entries, timeline_ms, creatures_none_active, no_creatures_timer_ms, spawns = tick_quest_mode_spawns(
344
+ self._quest.spawn_entries,
345
+ quest_spawn_timeline_ms=float(self._quest.spawn_timeline_ms),
346
+ frame_dt_ms=dt_world * 1000.0,
347
+ terrain_width=float(self._world.world_size),
348
+ creatures_none_active=creatures_none_active,
349
+ no_creatures_timer_ms=float(self._quest.no_creatures_timer_ms),
350
+ )
351
+ self._quest.spawn_entries = entries
352
+ self._quest.spawn_timeline_ms = float(timeline_ms)
353
+ self._quest.no_creatures_timer_ms = float(no_creatures_timer_ms)
354
+
355
+ for call in spawns:
356
+ self._creatures.spawn_template(
357
+ int(call.template_id),
358
+ call.pos,
359
+ float(call.heading),
360
+ self._state.rng,
361
+ rand=self._state.rng.rand,
362
+ )
363
+
364
+ completion_ms, completed = tick_quest_completion_transition(
365
+ float(self._quest.completion_transition_ms),
366
+ frame_dt_ms=dt_world * 1000.0,
367
+ creatures_none_active=bool(creatures_none_active),
368
+ spawn_table_empty=quest_spawn_table_empty(self._quest.spawn_entries),
369
+ )
370
+ self._quest.completion_transition_ms = float(completion_ms)
371
+ if completed:
372
+ if self._outcome is None:
373
+ fired = 0
374
+ hit = 0
375
+ try:
376
+ fired = int(self._state.shots_fired[int(self._player.index)])
377
+ hit = int(self._state.shots_hit[int(self._player.index)])
378
+ except Exception:
379
+ fired = 0
380
+ hit = 0
381
+ fired = max(0, int(fired))
382
+ hit = max(0, min(int(hit), fired))
383
+ most_used_weapon_id = most_used_weapon_id_for_player(
384
+ self._state,
385
+ player_index=int(self._player.index),
386
+ fallback_weapon_id=int(self._player.weapon_id),
387
+ )
388
+ player2_health = None
389
+ if len(self._world.players) >= 2:
390
+ player2_health = float(self._world.players[1].health)
391
+ self._outcome = QuestRunOutcome(
392
+ kind="completed",
393
+ level=str(self._quest.level),
394
+ base_time_ms=int(self._quest.spawn_timeline_ms),
395
+ player_health=float(self._player.health),
396
+ player2_health=player2_health,
397
+ pending_perk_count=int(self._state.perk_selection.pending_count),
398
+ experience=int(self._player.experience),
399
+ kill_count=int(self._creatures.kill_count),
400
+ weapon_id=int(self._player.weapon_id),
401
+ shots_fired=fired,
402
+ shots_hit=hit,
403
+ most_used_weapon_id=int(most_used_weapon_id),
404
+ )
405
+ self.close_requested = True
406
+
407
+ def draw(self) -> None:
408
+ self._world.draw(draw_aim_indicators=True)
409
+ self._draw_screen_fade()
410
+
411
+ hud_bottom = 0.0
412
+ if self._hud_assets is not None:
413
+ hud_bottom = draw_hud_overlay(
414
+ self._hud_assets,
415
+ player=self._player,
416
+ players=self._world.players,
417
+ bonus_hud=self._state.bonus_hud,
418
+ elapsed_ms=float(self._quest.spawn_timeline_ms),
419
+ font=self._small,
420
+ frame_dt_ms=self._last_dt_ms,
421
+ show_xp=False,
422
+ show_time=True,
423
+ )
424
+ total = int(self._quest.total_spawn_count)
425
+ if total > 0:
426
+ kills = int(self._creatures.kill_count)
427
+ ratio = max(0.0, min(1.0, float(kills) / float(total)))
428
+ scale = hud_ui_scale(float(rl.get_screen_width()), float(rl.get_screen_height()))
429
+ bar_x = 255.0 * scale
430
+ bar_y = 30.0 * scale
431
+ bar_w = 120.0 * scale
432
+ bar_h = 6.0 * scale
433
+ bg = rl.Color(40, 40, 48, 200)
434
+ fg = rl.Color(220, 220, 220, 240)
435
+ rl.draw_rectangle(int(bar_x), int(bar_y), int(bar_w), int(bar_h), bg)
436
+ inner_w = max(0.0, bar_w - 2.0 * scale)
437
+ inner_h = max(0.0, bar_h - 2.0 * scale)
438
+ rl.draw_rectangle(
439
+ int(bar_x + scale),
440
+ int(bar_y + scale),
441
+ int(inner_w * ratio),
442
+ int(inner_h),
443
+ fg,
444
+ )
445
+ self._draw_ui_text(f"{kills}/{total}", bar_x + bar_w + 8.0 * scale, bar_y - 3.0 * scale, rl.Color(220, 220, 220, 255), scale=0.8 * scale)
446
+
447
+ self._draw_quest_title()
448
+
449
+ warn_y = float(rl.get_screen_height()) - 28.0
450
+ if self._world.missing_assets:
451
+ warn = "Missing world assets: " + ", ".join(self._world.missing_assets)
452
+ self._draw_ui_text(warn, 24.0, warn_y, rl.Color(240, 80, 80, 255), scale=0.8)
453
+ warn_y -= float(self._ui_line_height(scale=0.8)) + 2.0
454
+ if self._hud_missing:
455
+ warn = "Missing HUD assets: " + ", ".join(self._hud_missing)
456
+ self._draw_ui_text(warn, 24.0, warn_y, rl.Color(240, 80, 80, 255), scale=0.8)
457
+
458
+ self._draw_aim_cursor()
459
+ if self._paused:
460
+ self._draw_game_cursor()
461
+ x = 18.0
462
+ y = max(18.0, hud_bottom + 10.0)
463
+ self._draw_ui_text("paused (TAB)", x, y, rl.Color(140, 140, 140, 255))
464
+
465
+ def _draw_game_cursor(self) -> None:
466
+ assets = self._ui_assets
467
+ cursor_tex = assets.cursor if assets is not None else None
468
+ draw_menu_cursor(
469
+ self._world.particles_texture,
470
+ cursor_tex,
471
+ x=float(self._ui_mouse_x),
472
+ y=float(self._ui_mouse_y),
473
+ pulse_time=float(self._cursor_pulse_time),
474
+ )
475
+
476
+ def _draw_aim_cursor(self) -> None:
477
+ assets = self._ui_assets
478
+ aim_tex = assets.aim if assets is not None else None
479
+ draw_aim_cursor(
480
+ self._world.particles_texture,
481
+ aim_tex,
482
+ x=float(self._ui_mouse_x),
483
+ y=float(self._ui_mouse_y),
484
+ )
485
+
486
+ def _draw_quest_title(self) -> None:
487
+ font = self._grim_mono
488
+ quest = self._quest.quest
489
+ if font is None or quest is None:
490
+ return
491
+ timer_ms = float(self._quest.quest_name_timer_ms)
492
+ if timer_ms <= 0.0 or timer_ms > QUEST_TITLE_TOTAL_MS:
493
+ return
494
+ if timer_ms < QUEST_TITLE_FADE_IN_MS and QUEST_TITLE_FADE_IN_MS > 1e-3:
495
+ alpha = timer_ms / QUEST_TITLE_FADE_IN_MS
496
+ elif timer_ms < (QUEST_TITLE_FADE_IN_MS + QUEST_TITLE_HOLD_MS):
497
+ alpha = 1.0
498
+ else:
499
+ t = timer_ms - (QUEST_TITLE_FADE_IN_MS + QUEST_TITLE_HOLD_MS)
500
+ alpha = max(0.0, 1.0 - (t / max(1e-3, QUEST_TITLE_FADE_OUT_MS)))
501
+
502
+ draw_quest_title_overlay(font, quest.title, _quest_level_label(self._quest.level), alpha=alpha)