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,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ __all__ = [
6
+ "EFFECT_ID_ATLAS_TABLE",
7
+ "EFFECT_ID_ATLAS_TABLE_BY_ID",
8
+ "EffectAtlasEntry",
9
+ "SIZE_CODE_GRID",
10
+ "effect_src_rect",
11
+ ]
12
+
13
+
14
+ SIZE_CODE_GRID: dict[int, int] = {
15
+ 0x10: 16,
16
+ 0x20: 8,
17
+ 0x40: 4,
18
+ 0x80: 2,
19
+ }
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class EffectAtlasEntry:
24
+ effect_id: int
25
+ size_code: int
26
+ frame: int
27
+
28
+ @property
29
+ def grid(self) -> int:
30
+ return SIZE_CODE_GRID[self.size_code]
31
+
32
+
33
+ # Extracted from `effect_id_table` (`size_code`, `frame`) (see `docs/structs/effects.md`).
34
+ EFFECT_ID_ATLAS_TABLE: tuple[EffectAtlasEntry, ...] = (
35
+ EffectAtlasEntry(0x00, 0x80, 0x02),
36
+ EffectAtlasEntry(0x01, 0x80, 0x03),
37
+ EffectAtlasEntry(0x02, 0x20, 0x00),
38
+ EffectAtlasEntry(0x03, 0x20, 0x01),
39
+ EffectAtlasEntry(0x04, 0x20, 0x02),
40
+ EffectAtlasEntry(0x05, 0x20, 0x03),
41
+ EffectAtlasEntry(0x06, 0x20, 0x04),
42
+ EffectAtlasEntry(0x07, 0x20, 0x05),
43
+ EffectAtlasEntry(0x08, 0x20, 0x08),
44
+ EffectAtlasEntry(0x09, 0x20, 0x09),
45
+ EffectAtlasEntry(0x0A, 0x20, 0x0A),
46
+ EffectAtlasEntry(0x0B, 0x20, 0x0B),
47
+ EffectAtlasEntry(0x0C, 0x40, 0x05),
48
+ EffectAtlasEntry(0x0D, 0x40, 0x03),
49
+ EffectAtlasEntry(0x0E, 0x40, 0x04),
50
+ EffectAtlasEntry(0x0F, 0x40, 0x05),
51
+ EffectAtlasEntry(0x10, 0x40, 0x06),
52
+ EffectAtlasEntry(0x11, 0x40, 0x07),
53
+ EffectAtlasEntry(0x12, 0x10, 0x26),
54
+ )
55
+
56
+ EFFECT_ID_ATLAS_TABLE_BY_ID: dict[int, EffectAtlasEntry] = {entry.effect_id: entry for entry in EFFECT_ID_ATLAS_TABLE}
57
+
58
+
59
+ def effect_src_rect(effect_id: int, *, texture_width: float, texture_height: float) -> tuple[float, float, float, float] | None:
60
+ entry = EFFECT_ID_ATLAS_TABLE_BY_ID.get(int(effect_id))
61
+ if entry is None:
62
+ return None
63
+
64
+ grid = SIZE_CODE_GRID.get(entry.size_code)
65
+ if not grid:
66
+ return None
67
+
68
+ frame = int(entry.frame)
69
+ col = frame % grid
70
+ row = frame // grid
71
+ cell_w = float(texture_width) / float(grid)
72
+ cell_h = float(texture_height) / float(grid)
73
+ return cell_w * float(col), cell_h * float(row), cell_w, cell_h
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+ import pyray as rl
7
+
8
+ from grim.assets import PaqTextureCache, TextureLoader, load_paq_entries_from_path
9
+
10
+ if TYPE_CHECKING:
11
+ from ..game import GameState
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class MenuAssets:
16
+ sign: rl.Texture2D | None
17
+ item: rl.Texture2D | None
18
+ panel: rl.Texture2D | None
19
+ labels: rl.Texture2D | None
20
+
21
+
22
+ def _load_resource_entries(state: GameState) -> dict[str, bytes]:
23
+ return load_paq_entries_from_path(state.resource_paq)
24
+
25
+
26
+ def _ensure_texture_cache(state: GameState) -> PaqTextureCache:
27
+ cache = state.texture_cache
28
+ if cache is None:
29
+ entries = _load_resource_entries(state)
30
+ cache = PaqTextureCache(entries=entries, textures={})
31
+ state.texture_cache = cache
32
+ return cache
33
+
34
+
35
+ def load_menu_assets(state: GameState) -> MenuAssets:
36
+ cache = _ensure_texture_cache(state)
37
+ loader = TextureLoader(assets_root=state.assets_dir, cache=cache)
38
+ return MenuAssets(
39
+ sign=loader.get(name="ui_signCrimson", paq_rel="ui/ui_signCrimson.jaz"),
40
+ item=loader.get(name="ui_menuItem", paq_rel="ui/ui_menuItem.jaz"),
41
+ panel=loader.get(name="ui_menuPanel", paq_rel="ui/ui_menuPanel.jaz"),
42
+ labels=loader.get(name="ui_itemTexts", paq_rel="ui/ui_itemTexts.jaz"),
43
+ )
@@ -0,0 +1,424 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ import os
5
+
6
+ import pyray as rl
7
+
8
+ from grim.audio import init_audio_state, play_music, stop_music, update_audio, shutdown_audio
9
+ from grim.assets import LogoAssets, PaqTextureCache, load_logo_assets
10
+
11
+ from .assets import _load_resource_entries
12
+
13
+ if TYPE_CHECKING:
14
+ from ..game import GameState
15
+
16
+
17
+ TEXTURE_LOAD_STAGES: dict[int, tuple[tuple[str, str], ...]] = {
18
+ 0: (
19
+ ("GRIM_Font2", "load/smallWhite.tga"),
20
+ ("trooper", "game/trooper.jaz"),
21
+ ("zombie", "game/zombie.jaz"),
22
+ ("spider_sp1", "game/spider_sp1.jaz"),
23
+ ("spider_sp2", "game/spider_sp2.jaz"),
24
+ ("alien", "game/alien.jaz"),
25
+ ("lizard", "game/lizard.jaz"),
26
+ ),
27
+ 1: (
28
+ ("arrow", "load/arrow.tga"),
29
+ ("bullet_i", "load/bullet16.tga"),
30
+ ("bulletTrail", "load/bulletTrail.tga"),
31
+ ("bodyset", "game/bodyset.jaz"),
32
+ ("projs", "game/projs.jaz"),
33
+ ),
34
+ 2: (
35
+ ("ui_iconAim", "ui/ui_iconAim.jaz"),
36
+ ("ui_buttonSm", "ui/ui_button_64x32.jaz"),
37
+ ("ui_buttonMd", "ui/ui_button_128x32.jaz"),
38
+ ("ui_checkOn", "ui/ui_checkOn.jaz"),
39
+ ("ui_checkOff", "ui/ui_checkOff.jaz"),
40
+ ("ui_rectOff", "ui/ui_rectOff.jaz"),
41
+ ("ui_rectOn", "ui/ui_rectOn.jaz"),
42
+ ("bonuses", "game/bonuses.jaz"),
43
+ ),
44
+ 3: (
45
+ ("ui_indBullet", "ui/ui_indBullet.jaz"),
46
+ ("ui_indRocket", "ui/ui_indRocket.jaz"),
47
+ ("ui_indElectric", "ui/ui_indElectric.jaz"),
48
+ ("ui_indFire", "ui/ui_indFire.jaz"),
49
+ ("particles", "game/particles.jaz"),
50
+ ),
51
+ 4: (
52
+ ("ui_indLife", "ui/ui_indLife.jaz"),
53
+ ("ui_indPanel", "ui/ui_indPanel.jaz"),
54
+ ("ui_arrow", "ui/ui_arrow.jaz"),
55
+ ("ui_cursor", "ui/ui_cursor.jaz"),
56
+ ("ui_aim", "ui/ui_aim.jaz"),
57
+ ),
58
+ 5: (
59
+ ("ter_q1_base", "ter/ter_q1_base.jaz"),
60
+ ("ter_q1_tex1", "ter/ter_q1_tex1.jaz"),
61
+ ("ter_q2_base", "ter/ter_q2_base.jaz"),
62
+ ("ter_q2_tex1", "ter/ter_q2_tex1.jaz"),
63
+ ("ter_q3_base", "ter/ter_q3_base.jaz"),
64
+ ("ter_q3_tex1", "ter/ter_q3_tex1.jaz"),
65
+ ("ter_q4_base", "ter/ter_q4_base.jaz"),
66
+ ("ter_q4_tex1", "ter/ter_q4_tex1.jaz"),
67
+ ),
68
+ 6: (
69
+ ("ui_textLevComp", "ui/ui_textLevComp.jaz"),
70
+ ("ui_textQuest", "ui/ui_textQuest.jaz"),
71
+ ("ui_num1", "ui/ui_num1.jaz"),
72
+ ("ui_num2", "ui/ui_num2.jaz"),
73
+ ("ui_num3", "ui/ui_num3.jaz"),
74
+ ("ui_num4", "ui/ui_num4.jaz"),
75
+ ("ui_num5", "ui/ui_num5.jaz"),
76
+ ),
77
+ 7: (
78
+ ("ui_wicons", "ui/ui_wicons.jaz"),
79
+ ("iGameUI", "ui/ui_gameTop.jaz"),
80
+ ("iHeart", "ui/ui_lifeHeart.jaz"),
81
+ ("ui_clockTable", "ui/ui_clockTable.jaz"),
82
+ ("ui_clockPointer", "ui/ui_clockPointer.jaz"),
83
+ ),
84
+ 8: (
85
+ ("game\\muzzleFlash.jaz", "game/muzzleFlash.jaz"),
86
+ ("ui_dropOn", "ui/ui_dropDownOn.jaz"),
87
+ ("ui_dropOff", "ui/ui_dropDownOff.jaz"),
88
+ ),
89
+ 9: (),
90
+ }
91
+
92
+ COMPANY_LOGOS: dict[str, str] = {
93
+ "splash10tons": "load/splash10tons.jaz",
94
+ "splashReflexive": "load/splashReflexive.jpg",
95
+ }
96
+ SPLASH_ALPHA_SCALE = 2.0
97
+ LOGO_TIME_SCALE = 1.1
98
+ LOGO_TIME_OFFSET = 2.0
99
+ LOGO_SKIP_ACCEL = 4.0
100
+ LOGO_SKIP_JUMP = 16.0
101
+ LOGO_THEME_TRIGGER = 14.0
102
+ LOGO_10_IN_START = 1.0
103
+ LOGO_10_IN_END = 2.0
104
+ LOGO_10_HOLD_END = 4.0
105
+ LOGO_10_OUT_END = 5.0
106
+ LOGO_REF_IN_START = 7.0
107
+ LOGO_REF_IN_END = 8.0
108
+ LOGO_REF_HOLD_END = 10.0
109
+ LOGO_REF_OUT_END = 11.0
110
+ DEBUG_LOADING_HOLD_ENV = "CRIMSON_DEBUG_LOADING_HOLD_SECONDS"
111
+
112
+ MENU_PREP_TEXTURES: tuple[tuple[str, str], ...] = (
113
+ ("ui_signCrimson", "ui/ui_signCrimson.jaz"),
114
+ ("ui_menuItem", "ui/ui_menuItem.jaz"),
115
+ ("ui_menuPanel", "ui/ui_menuPanel.jaz"),
116
+ ("ui_itemTexts", "ui/ui_itemTexts.jaz"),
117
+ ("ui_checkOn", "ui/ui_checkOn.jaz"),
118
+ ("ui_checkOff", "ui/ui_checkOff.jaz"),
119
+ ("ui_rectOff", "ui/ui_rectOff.jaz"),
120
+ ("ui_rectOn", "ui/ui_rectOn.jaz"),
121
+ ("ui_button_md", "ui/ui_button_145x32.jaz"),
122
+ )
123
+
124
+
125
+ def _debug_loading_hold_seconds() -> float:
126
+ raw = os.getenv(DEBUG_LOADING_HOLD_ENV, "").strip()
127
+ if not raw:
128
+ return 0.0
129
+ try:
130
+ return max(0.0, float(raw))
131
+ except ValueError:
132
+ return 0.0
133
+
134
+
135
+ class BootView:
136
+ def __init__(self, state: GameState) -> None:
137
+ self._state = state
138
+ self._texture_stage = 0
139
+ self._textures_done = False
140
+ self._boot_time = 0.0
141
+ self._fade_out_ready = False
142
+ self._fade_out_done = False
143
+ self._logo_delay_ticks = 0
144
+ self._logo_skip = False
145
+ self._logo_active = False
146
+ self._intro_started = False
147
+ self._theme_started = False
148
+ self._company_logos_loaded = False
149
+ self._menu_prepped = False
150
+ self._loading_hold_remaining = _debug_loading_hold_seconds()
151
+
152
+ def _load_texture_stage(self, stage: int) -> None:
153
+ cache = self._state.texture_cache
154
+ if cache is None:
155
+ return
156
+ stage_defs = TEXTURE_LOAD_STAGES.get(stage)
157
+ if not stage_defs:
158
+ return
159
+ for name, rel_path in stage_defs:
160
+ cache.get_or_load(name, rel_path)
161
+
162
+ def _load_company_logos(self) -> None:
163
+ if self._company_logos_loaded:
164
+ return
165
+ cache = self._state.texture_cache
166
+ if cache is None:
167
+ return
168
+ for name, rel_path in COMPANY_LOGOS.items():
169
+ cache.get_or_load(name, rel_path)
170
+ loaded = sum(1 for name in COMPANY_LOGOS if cache.get(name) and cache.get(name).texture is not None)
171
+ if COMPANY_LOGOS:
172
+ self._state.console.log.log(f"company logos loaded: {loaded}/{len(COMPANY_LOGOS)}")
173
+ self._state.console.log.flush()
174
+ self._company_logos_loaded = True
175
+
176
+ def _prepare_menu_assets(self) -> None:
177
+ if self._menu_prepped:
178
+ return
179
+ cache = self._state.texture_cache
180
+ if cache is None:
181
+ return
182
+ for name, rel_path in MENU_PREP_TEXTURES:
183
+ cache.get_or_load(name, rel_path)
184
+ loaded = sum(1 for name, _rel in MENU_PREP_TEXTURES if cache.get(name) and cache.get(name).texture is not None)
185
+ if MENU_PREP_TEXTURES:
186
+ self._state.console.log.log(f"menu textures loaded: {loaded}/{len(MENU_PREP_TEXTURES)}")
187
+ self._state.console.log.flush()
188
+ self._menu_prepped = True
189
+
190
+ def open(self) -> None:
191
+ if self._state.logos is None:
192
+ entries = _load_resource_entries(self._state)
193
+ logos = load_logo_assets(self._state.assets_dir, entries=entries)
194
+ self._state.console.log.log(f"logo assets: {logos.loaded_count()}/{len(logos.all())} loaded")
195
+ self._state.console.log.flush()
196
+ self._state.logos = logos
197
+ self._state.texture_cache = PaqTextureCache(entries=entries, textures={})
198
+ if self._state.audio is None:
199
+ self._state.audio = init_audio_state(self._state.config, self._state.assets_dir, self._state.console)
200
+ self._state.console.exec_line("exec music/game_tunes.txt")
201
+
202
+ def update(self, dt: float) -> None:
203
+ frame_dt = min(dt, 0.1)
204
+ if self._state.audio is not None:
205
+ update_audio(self._state.audio, frame_dt)
206
+ if self._theme_started:
207
+ return
208
+ if not self._textures_done:
209
+ self._boot_time += frame_dt
210
+ if self._texture_stage in TEXTURE_LOAD_STAGES:
211
+ self._load_texture_stage(self._texture_stage)
212
+ self._texture_stage += 1
213
+ if self._texture_stage >= len(TEXTURE_LOAD_STAGES):
214
+ self._textures_done = True
215
+ if self._state.texture_cache is not None:
216
+ loaded = self._state.texture_cache.loaded_count()
217
+ total = len(self._state.texture_cache.textures)
218
+ self._state.console.log.log(f"boot textures loaded: {loaded}/{total}")
219
+ self._state.console.log.flush()
220
+ self._load_company_logos()
221
+ self._prepare_menu_assets()
222
+ self._fade_out_ready = True
223
+ self._loading_hold_remaining = _debug_loading_hold_seconds()
224
+ if self._boot_time > 0.5:
225
+ self._boot_time = 0.5
226
+ return
227
+
228
+ if self._fade_out_ready and not self._fade_out_done:
229
+ if self._loading_hold_remaining > 0.0:
230
+ if self._boot_time < 0.5:
231
+ self._boot_time = min(0.5, self._boot_time + frame_dt)
232
+ return
233
+ self._loading_hold_remaining = max(0.0, self._loading_hold_remaining - frame_dt)
234
+ return
235
+ self._boot_time -= frame_dt
236
+ if self._boot_time <= 0.0:
237
+ self._boot_time = 0.0
238
+ self._fade_out_done = True
239
+ return
240
+
241
+ if not self._fade_out_done:
242
+ self._boot_time += frame_dt
243
+ return
244
+
245
+ if self._state.skip_intro:
246
+ self._start_theme()
247
+ return
248
+
249
+ if self._logo_delay_ticks < 5:
250
+ self._logo_delay_ticks += 1
251
+ return
252
+
253
+ self._logo_active = True
254
+ if self._boot_time > LOGO_THEME_TRIGGER:
255
+ self._start_theme()
256
+ return
257
+ if (not self._state.skip_intro) and (not self._intro_started) and self._state.audio is not None:
258
+ play_music(self._state.audio, "intro")
259
+ self._intro_started = True
260
+ if not self._logo_skip and self._skip_triggered():
261
+ self._logo_skip = True
262
+ self._boot_time += frame_dt * LOGO_TIME_SCALE
263
+ t = self._boot_time - LOGO_TIME_OFFSET
264
+ if self._logo_skip:
265
+ if t < LOGO_10_IN_START or (LOGO_10_OUT_END <= t and (t < LOGO_REF_IN_START or LOGO_REF_OUT_END <= t)):
266
+ t = LOGO_SKIP_JUMP
267
+ else:
268
+ t += frame_dt * LOGO_SKIP_ACCEL
269
+ self._boot_time = t + LOGO_TIME_OFFSET
270
+
271
+ def draw(self) -> None:
272
+ rl.clear_background(rl.BLACK)
273
+ if not self._fade_out_ready or not self._fade_out_done:
274
+ logos = self._state.logos
275
+ if logos is not None:
276
+ self._draw_splash(logos, self._splash_alpha())
277
+ return
278
+ if self._logo_active and not self._theme_started:
279
+ self._draw_company_logo_sequence()
280
+
281
+ def close(self) -> None:
282
+ if self._state.audio is not None:
283
+ shutdown_audio(self._state.audio)
284
+
285
+ def _start_theme(self) -> None:
286
+ if self._theme_started:
287
+ return
288
+ if self._state.audio is not None:
289
+ stop_music(self._state.audio)
290
+ theme = "crimsonquest" if self._state.demo_enabled else "crimson_theme"
291
+ play_music(self._state.audio, theme)
292
+ self._theme_started = True
293
+
294
+ def is_theme_started(self) -> bool:
295
+ return self._theme_started
296
+
297
+ def _skip_triggered(self) -> bool:
298
+ if rl.get_key_pressed() != 0:
299
+ return True
300
+ if rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_LEFT):
301
+ return True
302
+ if rl.is_mouse_button_pressed(rl.MOUSE_BUTTON_RIGHT):
303
+ return True
304
+ return False
305
+
306
+ def _logo_state(self, t: float) -> tuple[str, float] | None:
307
+ if LOGO_10_IN_START <= t < LOGO_10_OUT_END:
308
+ if t < LOGO_10_IN_END:
309
+ alpha = t - LOGO_10_IN_START
310
+ elif t < LOGO_10_HOLD_END:
311
+ alpha = 1.0
312
+ else:
313
+ alpha = 1.0 - (t - LOGO_10_HOLD_END)
314
+ return ("splash10tons", self._clamp01(alpha))
315
+ if LOGO_REF_IN_START <= t < LOGO_REF_OUT_END:
316
+ if t < LOGO_REF_IN_END:
317
+ alpha = t - LOGO_REF_IN_START
318
+ elif t < LOGO_REF_HOLD_END:
319
+ alpha = 1.0
320
+ else:
321
+ alpha = 1.0 - (t - LOGO_REF_HOLD_END)
322
+ return ("splashReflexive", self._clamp01(alpha))
323
+ return None
324
+
325
+ def _draw_company_logo_sequence(self) -> None:
326
+ cache = self._state.texture_cache
327
+ if cache is None:
328
+ return
329
+ t = self._boot_time - LOGO_TIME_OFFSET
330
+ state = self._logo_state(t)
331
+ if state is None:
332
+ return
333
+ name, alpha = state
334
+ rel_path = COMPANY_LOGOS.get(name)
335
+ if rel_path is None:
336
+ return
337
+ asset = cache.get_or_load(name, rel_path)
338
+ if asset.texture is None:
339
+ return
340
+ tex = asset.texture
341
+ tex_w = float(tex.width)
342
+ tex_h = float(tex.height)
343
+ x = (rl.get_screen_width() - tex_w) * 0.5
344
+ y = (rl.get_screen_height() - tex_h) * 0.5
345
+ tint = rl.Color(255, 255, 255, int(round(alpha * 255.0)))
346
+ rl.draw_texture_v(tex, rl.Vector2(x, y), tint)
347
+
348
+ def _splash_alpha(self) -> float:
349
+ return self._clamp01(self._boot_time * SPLASH_ALPHA_SCALE)
350
+
351
+ @staticmethod
352
+ def _clamp01(value: float) -> float:
353
+ if value < 0.0:
354
+ return 0.0
355
+ if value > 1.0:
356
+ return 1.0
357
+ return value
358
+
359
+ def _draw_splash(self, logos: LogoAssets, alpha: float) -> None:
360
+ screen_w = float(rl.get_screen_width())
361
+ screen_h = float(rl.get_screen_height())
362
+ if alpha <= 0.0:
363
+ return
364
+
365
+ logo = logos.cl_logo.texture
366
+ logo_h = float(logo.height) if logo is not None else 64.0
367
+ band_height = logo_h * 2.0
368
+ band_top = (screen_h - band_height) * 0.5 - 4.0
369
+ band_bottom = band_top + band_height
370
+ band_left = -4.0
371
+ band_right = screen_w + 4.0
372
+
373
+ line_alpha = self._clamp01(alpha * 0.7)
374
+ line_color = rl.Color(149, 175, 198, int(round(line_alpha * 255.0)))
375
+ rl.draw_rectangle(
376
+ int(round(band_left)),
377
+ int(round(band_top)),
378
+ int(round(band_right - band_left)),
379
+ 1,
380
+ line_color,
381
+ )
382
+ rl.draw_rectangle(
383
+ int(round(band_left)),
384
+ int(round(band_bottom)),
385
+ int(round(band_right - band_left)),
386
+ 1,
387
+ line_color,
388
+ )
389
+ rl.draw_rectangle(
390
+ int(round(band_left)),
391
+ int(round(band_top)),
392
+ 1,
393
+ int(round(band_height)),
394
+ line_color,
395
+ )
396
+ rl.draw_rectangle(
397
+ int(round(band_right)),
398
+ int(round(band_top)),
399
+ 1,
400
+ int(round(band_height)),
401
+ line_color,
402
+ )
403
+
404
+ tint = rl.Color(255, 255, 255, int(round(alpha * 255.0)))
405
+
406
+ if logo is not None:
407
+ logo_w = float(logo.width)
408
+ logo_h = float(logo.height)
409
+ logo_x = (screen_w - logo_w) * 0.5
410
+ logo_y = (screen_h - logo_h) * 0.5
411
+ rl.draw_texture_v(logo, rl.Vector2(logo_x, logo_y), tint)
412
+ loading = logos.loading.texture
413
+ if loading is not None:
414
+ loading_x = screen_w * 0.5 + 128.0
415
+ loading_y = screen_h * 0.5 + 16.0
416
+ rl.draw_texture_v(loading, rl.Vector2(loading_x, loading_y), tint)
417
+
418
+ esrb = logos.logo_esrb.texture
419
+ if esrb is not None:
420
+ esrb_w = float(esrb.width)
421
+ esrb_h = float(esrb.height)
422
+ esrb_x = screen_w - esrb_w - 1.0
423
+ esrb_y = screen_h - esrb_h - 1.0
424
+ rl.draw_texture_v(esrb, rl.Vector2(esrb_x, esrb_y), tint)