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,472 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ import random
6
+
7
+ import pyray as rl
8
+
9
+ from grim.assets import PaqTextureCache
10
+ from grim.audio import AudioState
11
+ from grim.config import CrimsonConfig
12
+ from grim.view import ViewContext
13
+
14
+ from ..creatures.spawn import CreatureFlags, CreatureInit, CreatureTypeId
15
+ from ..game_modes import GameMode
16
+ from ..gameplay import most_used_weapon_id_for_player
17
+ from ..typo.player import build_typo_player_input, enforce_typo_player_frame
18
+ from ..persistence.highscores import HighScoreRecord
19
+ from ..typo.names import CreatureNameTable, load_typo_dictionary
20
+ from ..typo.spawns import tick_typo_spawns
21
+ from ..typo.typing import TypingBuffer
22
+ from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
23
+ from ..ui.hud import draw_hud_overlay
24
+ from ..ui.perk_menu import load_perk_menu_assets
25
+ from .base_gameplay_mode import BaseGameplayMode
26
+
27
+ WORLD_SIZE = 1024.0
28
+
29
+ UI_TEXT_COLOR = rl.Color(220, 220, 220, 255)
30
+ UI_HINT_COLOR = rl.Color(140, 140, 140, 255)
31
+ UI_ERROR_COLOR = rl.Color(240, 80, 80, 255)
32
+
33
+ NAME_LABEL_SCALE = 1.0
34
+ NAME_LABEL_BG_ALPHA = 0.67
35
+
36
+ # Original typoshooter input box constants (from 0x004457C0)
37
+ TYPING_PANEL_WIDTH = 182.0
38
+ TYPING_PANEL_HEIGHT = 53.0
39
+ TYPING_PANEL_ALPHA = 0.7
40
+ TYPING_TEXT_X = 6.0
41
+ TYPING_PROMPT = ">"
42
+ TYPING_CURSOR = "_"
43
+ TYPING_CURSOR_X_OFFSET = 14.0
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class _TypoState:
48
+ elapsed_ms: int = 0
49
+ spawn_cooldown_ms: int = 0
50
+
51
+
52
+ class TypoShooterMode(BaseGameplayMode):
53
+ def __init__(
54
+ self,
55
+ ctx: ViewContext,
56
+ *,
57
+ texture_cache: PaqTextureCache | None = None,
58
+ config: CrimsonConfig | None = None,
59
+ audio: AudioState | None = None,
60
+ audio_rng: random.Random | None = None,
61
+ ) -> None:
62
+ super().__init__(
63
+ ctx,
64
+ world_size=WORLD_SIZE,
65
+ default_game_mode_id=int(GameMode.TYPO),
66
+ demo_mode_active=False,
67
+ difficulty_level=0,
68
+ hardcore=False,
69
+ texture_cache=texture_cache,
70
+ config=config,
71
+ audio=audio,
72
+ audio_rng=audio_rng,
73
+ )
74
+ self._typo = _TypoState()
75
+ self._typing = TypingBuffer()
76
+ self._names = CreatureNameTable.sized(0)
77
+ self._aim_target_x = 0.0
78
+ self._aim_target_y = 0.0
79
+ self._unique_words: list[str] | None = None
80
+
81
+ self._ui_assets = None
82
+
83
+ def open(self) -> None:
84
+ super().open()
85
+ self._ui_assets = load_perk_menu_assets(self._assets_root)
86
+ if self._ui_assets.missing:
87
+ self._missing_assets.extend(self._ui_assets.missing)
88
+ self._typo = _TypoState()
89
+ self._typing = TypingBuffer()
90
+ self._names = CreatureNameTable.sized(len(self._creatures.entries))
91
+ self._unique_words = None
92
+
93
+ dictionary_path = self._base_dir / "typo_dictionary.txt"
94
+ if dictionary_path.is_file():
95
+ words = load_typo_dictionary(dictionary_path)
96
+ if words:
97
+ self._unique_words = words
98
+
99
+ self._aim_target_x = float(self._player.pos_x) + 128.0
100
+ self._aim_target_y = float(self._player.pos_y)
101
+
102
+ enforce_typo_player_frame(self._player)
103
+
104
+ def close(self) -> None:
105
+ if self._ui_assets is not None:
106
+ self._ui_assets = None
107
+ super().close()
108
+
109
+ def _handle_input(self) -> None:
110
+ if self._game_over_active:
111
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
112
+ self._action = "back_to_menu"
113
+ self.close_requested = True
114
+ return
115
+
116
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_TAB):
117
+ self._paused = not self._paused
118
+
119
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE):
120
+ self.close_requested = True
121
+
122
+ def _active_mask(self) -> list[bool]:
123
+ return [bool(entry.active) for entry in self._creatures.entries]
124
+
125
+ def _handle_typing_input(self) -> tuple[bool, bool]:
126
+ fire_pressed = False
127
+ reload_pressed = False
128
+
129
+ if rl.is_key_pressed(rl.KeyboardKey.KEY_BACKSPACE):
130
+ self._typing.backspace()
131
+ if self._world.audio_router is not None:
132
+ key = "sfx_ui_typeclick_01" if (self._state.rng.rand() & 1) == 0 else "sfx_ui_typeclick_02"
133
+ self._world.audio_router.play_sfx(key)
134
+
135
+ codepoint = int(rl.get_char_pressed())
136
+ while codepoint > 0:
137
+ if codepoint not in (13, 8) and 0x20 <= codepoint <= 0xFF:
138
+ try:
139
+ ch = chr(codepoint)
140
+ except ValueError:
141
+ ch = ""
142
+ if ch:
143
+ self._typing.push_char(ch)
144
+ if self._world.audio_router is not None:
145
+ key = "sfx_ui_typeclick_01" if (self._state.rng.rand() & 1) == 0 else "sfx_ui_typeclick_02"
146
+ self._world.audio_router.play_sfx(key)
147
+ codepoint = int(rl.get_char_pressed())
148
+
149
+ enter_pressed = rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER) or rl.is_key_pressed(rl.KeyboardKey.KEY_KP_ENTER)
150
+ if enter_pressed:
151
+ had_text = bool(self._typing.text)
152
+ active = self._active_mask()
153
+
154
+ def _find_target(name: str) -> int | None:
155
+ return self._names.find_by_name(name, active_mask=active)
156
+
157
+ result = self._typing.enter(find_target=_find_target)
158
+ if had_text and self._world.audio_router is not None:
159
+ self._world.audio_router.play_sfx("sfx_ui_typeenter")
160
+ if result.fire_requested and result.target_creature_idx is not None:
161
+ target_idx = int(result.target_creature_idx)
162
+ if 0 <= target_idx < len(self._creatures.entries):
163
+ creature = self._creatures.entries[target_idx]
164
+ if creature.active:
165
+ self._aim_target_x = float(creature.x)
166
+ self._aim_target_y = float(creature.y)
167
+ fire_pressed = True
168
+ if result.reload_requested:
169
+ reload_pressed = True
170
+
171
+ return fire_pressed, reload_pressed
172
+
173
+ def _spawn_tinted_creature(
174
+ self, *, type_id: CreatureTypeId, pos_x: float, pos_y: float, tint_rgba: tuple[float, float, float, float]
175
+ ) -> int:
176
+ rand = self._state.rng.rand
177
+ heading = float(int(rand()) % 314) * 0.01
178
+ size = float(int(rand()) % 20 + 47)
179
+
180
+ flags = CreatureFlags(0)
181
+ move_speed = 1.7
182
+ if int(type_id) in (int(CreatureTypeId.SPIDER_SP1), int(CreatureTypeId.SPIDER_SP2)):
183
+ flags |= CreatureFlags.AI7_LINK_TIMER
184
+ move_speed *= 1.2
185
+ size *= 0.8
186
+
187
+ init = CreatureInit(
188
+ origin_template_id=0,
189
+ pos_x=float(pos_x),
190
+ pos_y=float(pos_y),
191
+ heading=float(heading),
192
+ phase_seed=0.0,
193
+ type_id=type_id,
194
+ flags=flags,
195
+ ai_mode=2,
196
+ health=1.0,
197
+ max_health=1.0,
198
+ move_speed=float(move_speed),
199
+ reward_value=1.0,
200
+ size=float(size),
201
+ contact_damage=100.0,
202
+ tint=tint_rgba,
203
+ )
204
+ return self._creatures.spawn_init(init, rand=rand)
205
+
206
+ def _enter_game_over(self) -> None:
207
+ if self._game_over_active:
208
+ return
209
+
210
+ record = HighScoreRecord.blank()
211
+ record.score_xp = int(self._player.experience)
212
+ record.survival_elapsed_ms = int(self._typo.elapsed_ms)
213
+ record.creature_kill_count = int(self._creatures.kill_count)
214
+ weapon_id = most_used_weapon_id_for_player(
215
+ self._state, player_index=int(self._player.index), fallback_weapon_id=int(self._player.weapon_id)
216
+ )
217
+ record.most_used_weapon_id = int(weapon_id)
218
+ record.shots_fired = int(self._typing.shots_fired)
219
+ record.shots_hit = int(self._typing.shots_hit)
220
+ record.game_mode_id = int(GameMode.TYPO)
221
+
222
+ self._game_over_record = record
223
+ self._game_over_ui.open()
224
+ self._game_over_active = True
225
+
226
+ def _update_game_over_ui(self, dt: float) -> None:
227
+ record = self._game_over_record
228
+ if record is None:
229
+ self._enter_game_over()
230
+ record = self._game_over_record
231
+ if record is None:
232
+ return
233
+
234
+ action = self._game_over_ui.update(
235
+ dt,
236
+ record=record,
237
+ player_name_default=self._player_name_default(),
238
+ play_sfx=self._world.audio_router.play_sfx,
239
+ rand=self._state.rng.rand,
240
+ mouse=self._ui_mouse_pos(),
241
+ )
242
+ if action == "play_again":
243
+ self.open()
244
+ return
245
+ if action == "high_scores":
246
+ self._action = "open_high_scores"
247
+ return
248
+ if action == "main_menu":
249
+ self._action = "back_to_menu"
250
+ self.close_requested = True
251
+
252
+ def update(self, dt: float) -> None:
253
+ self._update_audio(dt)
254
+
255
+ dt_frame = self._tick_frame(dt)[0]
256
+ self._handle_input()
257
+
258
+ if self._game_over_active:
259
+ self._update_game_over_ui(dt)
260
+ return
261
+
262
+ dt_world = 0.0 if self._paused else dt_frame
263
+
264
+ # Native: delay game-over transition until the trooper death animation finishes
265
+ # (checks `death_timer < 0.0` in the main gameplay loop).
266
+ if self._player.health <= 0.0:
267
+ if dt_world > 0.0:
268
+ self._player.death_timer -= float(dt_world) * 20.0
269
+ if self._player.death_timer < 0.0:
270
+ self._enter_game_over()
271
+ self._update_game_over_ui(dt)
272
+ return
273
+ return
274
+
275
+ fire_pressed = False
276
+ reload_pressed = False
277
+ if dt_world > 0.0:
278
+ fire_pressed, reload_pressed = self._handle_typing_input()
279
+
280
+ if dt_world <= 0.0:
281
+ return
282
+
283
+ enforce_typo_player_frame(self._player)
284
+ input_state = build_typo_player_input(
285
+ aim_x=float(self._aim_target_x),
286
+ aim_y=float(self._aim_target_y),
287
+ fire_requested=bool(fire_pressed),
288
+ reload_requested=bool(reload_pressed),
289
+ )
290
+ self._world.update(
291
+ dt_world,
292
+ inputs=[input_state],
293
+ auto_pick_perks=False,
294
+ game_mode=int(GameMode.TYPO),
295
+ perk_progression_enabled=False,
296
+ )
297
+ enforce_typo_player_frame(self._player)
298
+
299
+ self._state.bonuses.weapon_power_up = 0.0
300
+ self._state.bonuses.reflex_boost = 0.0
301
+ self._state.bonus_pool.reset()
302
+
303
+ cooldown, spawns = tick_typo_spawns(
304
+ elapsed_ms=int(self._typo.elapsed_ms),
305
+ spawn_cooldown_ms=int(self._typo.spawn_cooldown_ms),
306
+ frame_dt_ms=int(dt_world * 1000.0),
307
+ player_count=1,
308
+ world_width=float(self._world.world_size),
309
+ world_height=float(self._world.world_size),
310
+ )
311
+ self._typo.spawn_cooldown_ms = int(cooldown)
312
+ for call in spawns:
313
+ creature_idx = self._spawn_tinted_creature(
314
+ type_id=call.type_id,
315
+ pos_x=float(call.pos_x),
316
+ pos_y=float(call.pos_y),
317
+ tint_rgba=call.tint_rgba,
318
+ )
319
+ self._names.assign_random(
320
+ creature_idx,
321
+ self._state.rng,
322
+ score_xp=int(self._player.experience),
323
+ active_mask=self._active_mask(),
324
+ unique_words=self._unique_words,
325
+ )
326
+
327
+ self._typo.elapsed_ms += int(dt_world * 1000.0)
328
+ # Death/game-over flow is handled at the start of the next frame so the
329
+ # trooper death animation can play before the UI slides in.
330
+
331
+ def _draw_game_cursor(self) -> None:
332
+ mouse_x = float(self._ui_mouse_x)
333
+ mouse_y = float(self._ui_mouse_y)
334
+ cursor_tex = self._ui_assets.cursor if self._ui_assets is not None else None
335
+ draw_menu_cursor(
336
+ self._world.particles_texture,
337
+ cursor_tex,
338
+ x=mouse_x,
339
+ y=mouse_y,
340
+ pulse_time=float(self._cursor_pulse_time),
341
+ )
342
+
343
+ def _draw_aim_cursor(self) -> None:
344
+ mouse_x = float(self._ui_mouse_x)
345
+ mouse_y = float(self._ui_mouse_y)
346
+ aim_tex = self._ui_assets.aim if self._ui_assets is not None else None
347
+ draw_aim_cursor(self._world.particles_texture, aim_tex, x=mouse_x, y=mouse_y)
348
+
349
+ def _draw_name_labels(self) -> None:
350
+ names = self._names.names
351
+ if not names:
352
+ return
353
+
354
+ for idx, creature in enumerate(self._creatures.entries):
355
+ if not creature.active:
356
+ continue
357
+ if not (0 <= idx < len(names)):
358
+ continue
359
+ text = names[idx]
360
+ if not text:
361
+ continue
362
+
363
+ label_alpha = 1.0
364
+ hitbox = float(creature.hitbox_size)
365
+ if hitbox < 0.0:
366
+ label_alpha = max(0.0, min(1.0, (hitbox + 10.0) * 0.1))
367
+ if label_alpha <= 1e-3:
368
+ continue
369
+
370
+ sx, sy = self._world.world_to_screen(float(creature.x), float(creature.y))
371
+ y = float(sy) - 50.0
372
+ text_w = float(self._ui_text_width(text, scale=NAME_LABEL_SCALE))
373
+ text_h = 15.0
374
+ x = float(sx) - text_w * 0.5
375
+
376
+ bg_alpha = label_alpha * NAME_LABEL_BG_ALPHA
377
+ bg = rl.Color(0, 0, 0, int(255 * bg_alpha))
378
+ fg = rl.Color(255, 255, 255, int(255 * label_alpha))
379
+ rl.draw_rectangle_rec(rl.Rectangle(x - 4.0, y, text_w + 8.0, text_h), bg)
380
+ self._draw_ui_text(text, x, y, fg, scale=NAME_LABEL_SCALE)
381
+
382
+ def _draw_typing_box(self) -> None:
383
+ screen_h = float(rl.get_screen_height())
384
+
385
+ # Original positioning from 0x004457C0:
386
+ # v38 = screen_height - 128.0
387
+ # Panel Y = v38 - 16.0 = screen_height - 144.0
388
+ # Text Y = v38 + 1.0 = screen_height - 127.0
389
+ panel_x = -1.0
390
+ panel_y = screen_h - 144.0 # v38 - 16.0
391
+ text_y = screen_h - 127.0 # v38 + 1.0
392
+
393
+ # Draw panel backdrop using ind_panel texture (original: DAT_0048f7c4)
394
+ if self._hud_assets is not None and self._hud_assets.ind_panel is not None:
395
+ tex = self._hud_assets.ind_panel
396
+ src = rl.Rectangle(0.0, 0.0, float(tex.width), float(tex.height))
397
+ dst = rl.Rectangle(
398
+ panel_x,
399
+ panel_y,
400
+ TYPING_PANEL_WIDTH,
401
+ TYPING_PANEL_HEIGHT,
402
+ )
403
+ tint = rl.Color(255, 255, 255, int(255 * TYPING_PANEL_ALPHA))
404
+ rl.draw_texture_pro(tex, src, dst, rl.Vector2(0.0, 0.0), 0.0, tint)
405
+
406
+ # Draw prompt + typing text
407
+ # Original draws with format string that includes prompt "> "
408
+ text = self._typing.text
409
+ full_text = TYPING_PROMPT + text
410
+ text_color = rl.Color(255, 255, 255, 255)
411
+ self._draw_ui_text(full_text, TYPING_TEXT_X, text_y, text_color, scale=1.0)
412
+
413
+ # Draw cursor (original: alpha = sin(game_time_s * 4.0) > 0.0 ? 0.4 : 1.0)
414
+ cursor_dim = math.sin(float(self._cursor_pulse_time) * 4.0) > 0.0
415
+ cursor_alpha = 0.4 if cursor_dim else 1.0
416
+ cursor_color = rl.Color(255, 255, 255, int(255 * cursor_alpha))
417
+
418
+ # Cursor position: text_width + 14.0 (original)
419
+ text_w = float(self._ui_text_width(text))
420
+ cursor_x = text_w + TYPING_CURSOR_X_OFFSET
421
+ cursor_y = text_y
422
+
423
+ # Draw cursor as "_" character (original: DAT_004712b8 = "_")
424
+ self._draw_ui_text(TYPING_CURSOR, cursor_x, cursor_y, cursor_color, scale=1.0)
425
+
426
+ def draw(self) -> None:
427
+ alive = self._player.health > 0.0
428
+ show_gameplay_ui = alive and (not self._game_over_active)
429
+
430
+ self._world.draw(draw_aim_indicators=show_gameplay_ui)
431
+ self._draw_screen_fade()
432
+
433
+ if show_gameplay_ui:
434
+ self._draw_name_labels()
435
+
436
+ if show_gameplay_ui and self._hud_assets is not None:
437
+ draw_hud_overlay(
438
+ self._hud_assets,
439
+ player=self._player,
440
+ players=self._world.players,
441
+ bonus_hud=self._state.bonus_hud,
442
+ elapsed_ms=float(self._typo.elapsed_ms),
443
+ font=self._small,
444
+ frame_dt_ms=self._last_dt_ms,
445
+ show_weapon=False,
446
+ show_xp=True,
447
+ show_time=True,
448
+ )
449
+
450
+ if show_gameplay_ui:
451
+ self._draw_typing_box()
452
+
453
+ warn_y = float(rl.get_screen_height()) - 28.0
454
+ if self._world.missing_assets:
455
+ warn = "Missing world assets: " + ", ".join(self._world.missing_assets)
456
+ self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
457
+ warn_y -= float(self._ui_line_height(scale=0.8)) + 2.0
458
+ if self._hud_missing:
459
+ warn = "Missing HUD assets: " + ", ".join(self._hud_missing)
460
+ self._draw_ui_text(warn, 24.0, warn_y, UI_ERROR_COLOR, scale=0.8)
461
+
462
+ if show_gameplay_ui:
463
+ self._draw_aim_cursor()
464
+ elif self._game_over_active:
465
+ self._draw_game_cursor()
466
+ if self._game_over_record is not None:
467
+ self._game_over_ui.draw(
468
+ record=self._game_over_record,
469
+ banner_kind=self._game_over_banner,
470
+ hud_assets=self._hud_assets,
471
+ mouse=self._ui_mouse_pos(),
472
+ )
crimson/paths.py ADDED
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ import os
5
+
6
+ from platformdirs import PlatformDirs
7
+
8
+ APP_NAME = "banteg/crimsonland"
9
+
10
+
11
+ def default_runtime_dir() -> Path:
12
+ """Return the default per-user runtime directory.
13
+
14
+ This is intended for saves/config/logs (e.g. `game.cfg`, `crimson.cfg`, highscores).
15
+ Override with `CRIMSON_RUNTIME_DIR` (or legacy `CRIMSON_BASE_DIR`).
16
+ """
17
+
18
+ override = os.environ.get("CRIMSON_RUNTIME_DIR") or os.environ.get("CRIMSON_BASE_DIR")
19
+ if override:
20
+ return Path(override).expanduser()
21
+
22
+ dirs = PlatformDirs(appname=APP_NAME, appauthor=False)
23
+ return Path(dirs.user_data_path)