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 @@
1
+ from __future__ import annotations
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..creatures.spawn import CreatureTypeId
6
+ from ..projectiles import ProjectileTypeId
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class CreatureAnimInfo:
11
+ base: int
12
+ anim_rate: float
13
+ mirror: bool
14
+
15
+
16
+ CREATURE_ANIM: dict[CreatureTypeId, CreatureAnimInfo] = {
17
+ CreatureTypeId.ZOMBIE: CreatureAnimInfo(base=0x20, anim_rate=1.2, mirror=False),
18
+ CreatureTypeId.LIZARD: CreatureAnimInfo(base=0x10, anim_rate=1.6, mirror=True),
19
+ CreatureTypeId.ALIEN: CreatureAnimInfo(base=0x20, anim_rate=1.35, mirror=False),
20
+ CreatureTypeId.SPIDER_SP1: CreatureAnimInfo(base=0x10, anim_rate=1.5, mirror=True),
21
+ CreatureTypeId.SPIDER_SP2: CreatureAnimInfo(base=0x10, anim_rate=1.5, mirror=True),
22
+ CreatureTypeId.TROOPER: CreatureAnimInfo(base=0x00, anim_rate=1.0, mirror=False),
23
+ }
24
+
25
+ CREATURE_ASSET: dict[CreatureTypeId, str] = {
26
+ CreatureTypeId.ZOMBIE: "zombie",
27
+ CreatureTypeId.LIZARD: "lizard",
28
+ CreatureTypeId.ALIEN: "alien",
29
+ CreatureTypeId.SPIDER_SP1: "spider_sp1",
30
+ CreatureTypeId.SPIDER_SP2: "spider_sp2",
31
+ CreatureTypeId.TROOPER: "trooper",
32
+ }
33
+
34
+ KNOWN_PROJ_FRAMES: dict[int, tuple[int, int]] = {
35
+ # Based on docs/atlas.md (projectile `type_id` values index the weapon table).
36
+ ProjectileTypeId.PULSE_GUN: (2, 0),
37
+ ProjectileTypeId.SPLITTER_GUN: (4, 3),
38
+ ProjectileTypeId.BLADE_GUN: (4, 6),
39
+ ProjectileTypeId.ION_MINIGUN: (4, 2),
40
+ ProjectileTypeId.ION_CANNON: (4, 2),
41
+ ProjectileTypeId.SHRINKIFIER: (4, 2),
42
+ ProjectileTypeId.FIRE_BULLETS: (4, 2),
43
+ ProjectileTypeId.ION_RIFLE: (4, 2),
44
+ }
45
+
46
+ PLASMA_PARTICLE_TYPES = frozenset(
47
+ {
48
+ ProjectileTypeId.PLASMA_RIFLE,
49
+ ProjectileTypeId.PLASMA_MINIGUN,
50
+ ProjectileTypeId.PLASMA_CANNON,
51
+ ProjectileTypeId.SPIDER_PLASMA,
52
+ ProjectileTypeId.SHRINKIFIER,
53
+ }
54
+ )
55
+
56
+ ION_TYPES = frozenset(
57
+ {
58
+ ProjectileTypeId.ION_RIFLE,
59
+ ProjectileTypeId.ION_MINIGUN,
60
+ ProjectileTypeId.ION_CANNON,
61
+ }
62
+ )
63
+
64
+ FIRE_BULLETS_TYPES = frozenset({ProjectileTypeId.FIRE_BULLETS})
65
+
66
+ # "Beam" in the original renderer is really the Ion/Fire streak + chain UV family.
67
+ BEAM_TYPES = ION_TYPES | FIRE_BULLETS_TYPES
@@ -0,0 +1,422 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+
6
+ from ..bonuses import BonusId
7
+ from ..creatures.damage import creature_apply_damage
8
+ from ..creatures.runtime import CreaturePool
9
+ from ..creatures.runtime import CREATURE_HITBOX_ALIVE
10
+ from ..creatures.anim import creature_anim_advance_phase
11
+ from ..creatures.spawn import CreatureFlags, CreatureTypeId, SpawnEnv
12
+ from ..effects import FxQueue, FxQueueRotated
13
+ from ..gameplay import (
14
+ GameplayState,
15
+ PlayerInput,
16
+ PlayerState,
17
+ bonus_update,
18
+ perk_active,
19
+ perks_update_effects,
20
+ player_update,
21
+ survival_progression_update,
22
+ )
23
+ from ..perks import PerkId
24
+ from ..player_damage import player_take_damage
25
+ from .world_defs import CREATURE_ANIM
26
+
27
+ ProjectileHit = tuple[int, float, float, float, float, float, float]
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class WorldEvents:
32
+ hits: list[ProjectileHit]
33
+ deaths: tuple[object, ...]
34
+ pickups: list[object]
35
+ sfx: list[str]
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class WorldState:
40
+ spawn_env: SpawnEnv
41
+ state: GameplayState
42
+ players: list[PlayerState]
43
+ creatures: CreaturePool
44
+
45
+ @classmethod
46
+ def build(
47
+ cls,
48
+ *,
49
+ world_size: float,
50
+ demo_mode_active: bool,
51
+ hardcore: bool,
52
+ difficulty_level: int,
53
+ ) -> WorldState:
54
+ spawn_env = SpawnEnv(
55
+ terrain_width=float(world_size),
56
+ terrain_height=float(world_size),
57
+ demo_mode_active=bool(demo_mode_active),
58
+ hardcore=bool(hardcore),
59
+ difficulty_level=int(difficulty_level),
60
+ )
61
+ state = GameplayState()
62
+ state.demo_mode_active = bool(demo_mode_active)
63
+ state.hardcore = bool(hardcore)
64
+ players: list[PlayerState] = []
65
+ creatures = CreaturePool(env=spawn_env)
66
+ return cls(
67
+ spawn_env=spawn_env,
68
+ state=state,
69
+ players=players,
70
+ creatures=creatures,
71
+ )
72
+
73
+ def step(
74
+ self,
75
+ dt: float,
76
+ *,
77
+ inputs: list[PlayerInput] | None,
78
+ world_size: float,
79
+ damage_scale_by_type: dict[int, float],
80
+ detail_preset: int,
81
+ fx_queue: FxQueue,
82
+ fx_queue_rotated: FxQueueRotated,
83
+ auto_pick_perks: bool,
84
+ game_mode: int,
85
+ perk_progression_enabled: bool,
86
+ ) -> WorldEvents:
87
+ dt = float(dt)
88
+ if dt > 0.0 and self.players and perk_active(self.players[0], PerkId.REFLEX_BOOSTED):
89
+ dt *= 0.9
90
+
91
+ if inputs is None:
92
+ inputs = [PlayerInput() for _ in self.players]
93
+
94
+ prev_positions = [(player.pos_x, player.pos_y) for player in self.players]
95
+ prev_health = [float(player.health) for player in self.players]
96
+
97
+ # Native runs `perks_update_effects` early in the frame loop and relies on the current aim position
98
+ # (`player_state_table.aim_x/aim_y`). Our aim is otherwise updated inside `player_update`, so stage it here.
99
+ for idx, player in enumerate(self.players):
100
+ input_state = inputs[idx] if idx < len(inputs) else PlayerInput()
101
+ player.aim_x = float(input_state.aim_x)
102
+ player.aim_y = float(input_state.aim_y)
103
+
104
+ perks_update_effects(self.state, self.players, dt, creatures=self.creatures.entries, fx_queue=fx_queue)
105
+
106
+ # `effects_update` runs early in the native frame loop, before creature/projectile updates.
107
+ self.state.effects.update(dt, fx_queue=fx_queue)
108
+
109
+ def _apply_projectile_damage_to_player(player_index: int, damage: float) -> None:
110
+ idx = int(player_index)
111
+ if not (0 <= idx < len(self.players)):
112
+ return
113
+ player_take_damage(self.state, self.players[idx], float(damage), dt=dt, rand=self.state.rng.rand)
114
+
115
+ creature_result = self.creatures.update(
116
+ dt,
117
+ state=self.state,
118
+ players=self.players,
119
+ detail_preset=detail_preset,
120
+ world_width=float(world_size),
121
+ world_height=float(world_size),
122
+ fx_queue=fx_queue,
123
+ fx_queue_rotated=fx_queue_rotated,
124
+ )
125
+
126
+ deaths = list(creature_result.deaths)
127
+
128
+ def _apply_projectile_damage_to_creature(
129
+ creature_index: int,
130
+ damage: float,
131
+ damage_type: int,
132
+ impulse_x: float,
133
+ impulse_y: float,
134
+ owner_id: int,
135
+ ) -> None:
136
+ idx = int(creature_index)
137
+ if not (0 <= idx < len(self.creatures.entries)):
138
+ return
139
+ creature = self.creatures.entries[idx]
140
+ if not creature.active:
141
+ return
142
+
143
+ death_start_needed = creature.hp > 0.0 and creature.hitbox_size == CREATURE_HITBOX_ALIVE
144
+ killed = creature_apply_damage(
145
+ creature,
146
+ damage_amount=float(damage),
147
+ damage_type=int(damage_type),
148
+ impulse_x=float(impulse_x),
149
+ impulse_y=float(impulse_y),
150
+ owner_id=int(owner_id),
151
+ dt=float(dt),
152
+ players=self.players,
153
+ rand=self.state.rng.rand,
154
+ )
155
+ if killed and death_start_needed:
156
+ deaths.append(
157
+ self.creatures.handle_death(
158
+ idx,
159
+ state=self.state,
160
+ players=self.players,
161
+ rand=self.state.rng.rand,
162
+ detail_preset=int(detail_preset),
163
+ world_width=float(world_size),
164
+ world_height=float(world_size),
165
+ fx_queue=fx_queue,
166
+ )
167
+ )
168
+
169
+ hits = self.state.projectiles.update(
170
+ dt,
171
+ self.creatures.entries,
172
+ world_size=float(world_size),
173
+ damage_scale_by_type=damage_scale_by_type,
174
+ detail_preset=int(detail_preset),
175
+ rng=self.state.rng.rand,
176
+ runtime_state=self.state,
177
+ players=self.players,
178
+ apply_player_damage=_apply_projectile_damage_to_player,
179
+ apply_creature_damage=_apply_projectile_damage_to_creature,
180
+ )
181
+ self.state.secondary_projectiles.update_pulse_gun(
182
+ dt,
183
+ self.creatures.entries,
184
+ apply_creature_damage=_apply_projectile_damage_to_creature,
185
+ runtime_state=self.state,
186
+ fx_queue=fx_queue,
187
+ detail_preset=int(detail_preset),
188
+ )
189
+
190
+ for idx, player in enumerate(self.players):
191
+ if idx >= len(prev_health):
192
+ continue
193
+ if float(prev_health[idx]) < 0.0:
194
+ continue
195
+ if float(player.health) >= 0.0:
196
+ continue
197
+ if not perk_active(player, PerkId.FINAL_REVENGE):
198
+ continue
199
+
200
+ px = float(player.pos_x)
201
+ py = float(player.pos_y)
202
+ rand = self.state.rng.rand
203
+ self.state.effects.spawn_explosion_burst(
204
+ pos_x=px,
205
+ pos_y=py,
206
+ scale=1.8,
207
+ rand=rand,
208
+ detail_preset=int(detail_preset),
209
+ )
210
+
211
+ prev_guard = bool(self.state.bonus_spawn_guard)
212
+ self.state.bonus_spawn_guard = True
213
+ for creature_idx, creature in enumerate(self.creatures.entries):
214
+ if not creature.active:
215
+ continue
216
+ if float(creature.hp) <= 0.0:
217
+ continue
218
+
219
+ dx = float(creature.x) - px
220
+ dy = float(creature.y) - py
221
+ if abs(dx) > 512.0 or abs(dy) > 512.0:
222
+ continue
223
+
224
+ remaining = 512.0 - math.hypot(dx, dy)
225
+ if remaining <= 0.0:
226
+ continue
227
+
228
+ damage = remaining * 5.0
229
+ death_start_needed = float(creature.hp) > 0.0 and float(creature.hitbox_size) == CREATURE_HITBOX_ALIVE
230
+ killed = creature_apply_damage(
231
+ creature,
232
+ damage_amount=damage,
233
+ damage_type=3,
234
+ impulse_x=0.0,
235
+ impulse_y=0.0,
236
+ owner_id=-1 - int(player.index),
237
+ dt=float(dt),
238
+ players=self.players,
239
+ rand=rand,
240
+ )
241
+ if killed and death_start_needed:
242
+ deaths.append(
243
+ self.creatures.handle_death(
244
+ int(creature_idx),
245
+ state=self.state,
246
+ players=self.players,
247
+ rand=rand,
248
+ detail_preset=int(detail_preset),
249
+ world_width=float(world_size),
250
+ world_height=float(world_size),
251
+ fx_queue=fx_queue,
252
+ )
253
+ )
254
+ self.state.bonus_spawn_guard = prev_guard
255
+ self.state.sfx_queue.append("sfx_explosion_large")
256
+ self.state.sfx_queue.append("sfx_shockwave")
257
+
258
+ def _kill_creature_no_corpse(creature_index: int, owner_id: int) -> None:
259
+ idx = int(creature_index)
260
+ if not (0 <= idx < len(self.creatures.entries)):
261
+ return
262
+ creature = self.creatures.entries[idx]
263
+ if not creature.active:
264
+ return
265
+ if float(creature.hp) <= 0.0:
266
+ return
267
+
268
+ creature.last_hit_owner_id = int(owner_id)
269
+ deaths.append(
270
+ self.creatures.handle_death(
271
+ idx,
272
+ state=self.state,
273
+ players=self.players,
274
+ rand=self.state.rng.rand,
275
+ detail_preset=int(detail_preset),
276
+ world_width=float(world_size),
277
+ world_height=float(world_size),
278
+ fx_queue=fx_queue,
279
+ keep_corpse=False,
280
+ )
281
+ )
282
+
283
+ self.state.particles.update(
284
+ dt,
285
+ creatures=self.creatures.entries,
286
+ apply_creature_damage=_apply_projectile_damage_to_creature,
287
+ kill_creature=_kill_creature_no_corpse,
288
+ )
289
+ self.state.sprite_effects.update(dt)
290
+
291
+ for idx, player in enumerate(self.players):
292
+ input_state = inputs[idx] if idx < len(inputs) else PlayerInput()
293
+ player_update(player, input_state, dt, self.state, world_size=float(world_size))
294
+
295
+ if dt > 0.0:
296
+ self._advance_creature_anim(dt)
297
+ self._advance_player_anim(dt, prev_positions)
298
+
299
+ pickups = bonus_update(
300
+ self.state,
301
+ self.players,
302
+ dt,
303
+ creatures=self.creatures.entries,
304
+ update_hud=True,
305
+ apply_creature_damage=_apply_projectile_damage_to_creature,
306
+ detail_preset=int(detail_preset),
307
+ )
308
+ if pickups:
309
+ for pickup in pickups:
310
+ if pickup.bonus_id != int(BonusId.NUKE):
311
+ self.state.effects.spawn_burst(
312
+ pos_x=float(pickup.pos_x),
313
+ pos_y=float(pickup.pos_y),
314
+ count=12,
315
+ rand=self.state.rng.rand,
316
+ detail_preset=detail_preset,
317
+ lifetime=0.4,
318
+ scale_step=0.1,
319
+ color_r=0.4,
320
+ color_g=0.5,
321
+ color_b=1.0,
322
+ color_a=0.5,
323
+ )
324
+ if pickup.bonus_id == int(BonusId.REFLEX_BOOST):
325
+ self.state.effects.spawn_ring(
326
+ pos_x=float(pickup.pos_x),
327
+ pos_y=float(pickup.pos_y),
328
+ detail_preset=detail_preset,
329
+ color_r=0.6,
330
+ color_g=0.6,
331
+ color_b=1.0,
332
+ color_a=1.0,
333
+ )
334
+ elif pickup.bonus_id == int(BonusId.FREEZE):
335
+ self.state.effects.spawn_ring(
336
+ pos_x=float(pickup.pos_x),
337
+ pos_y=float(pickup.pos_y),
338
+ detail_preset=detail_preset,
339
+ color_r=0.3,
340
+ color_g=0.5,
341
+ color_b=0.8,
342
+ color_a=1.0,
343
+ )
344
+
345
+ if perk_progression_enabled:
346
+ survival_progression_update(
347
+ self.state,
348
+ self.players,
349
+ game_mode=game_mode,
350
+ auto_pick=auto_pick_perks,
351
+ dt=dt,
352
+ creatures=self.creatures.entries,
353
+ )
354
+
355
+ sfx = list(creature_result.sfx)
356
+ if self.state.sfx_queue:
357
+ sfx.extend(self.state.sfx_queue)
358
+ self.state.sfx_queue.clear()
359
+ pain_sfx = ("sfx_trooper_inpain_01", "sfx_trooper_inpain_02", "sfx_trooper_inpain_03")
360
+ death_sfx = ("sfx_trooper_die_01", "sfx_trooper_die_02")
361
+ rand = self.state.rng.rand
362
+ for idx, player in enumerate(self.players):
363
+ if idx >= len(prev_health):
364
+ continue
365
+ before = float(prev_health[idx])
366
+ after = float(player.health)
367
+ if after >= before - 1e-6:
368
+ continue
369
+ if before <= 0.0:
370
+ continue
371
+ if after <= 0.0:
372
+ # Prioritize death VO even if there are many other SFX this frame.
373
+ sfx.insert(0, death_sfx[int(rand()) & 1])
374
+ else:
375
+ sfx.append(pain_sfx[int(rand()) % len(pain_sfx)])
376
+
377
+ return WorldEvents(hits=hits, deaths=tuple(deaths), pickups=pickups, sfx=sfx)
378
+
379
+ def _advance_creature_anim(self, dt: float) -> None:
380
+ if float(self.state.bonuses.freeze) > 0.0:
381
+ return
382
+ for creature in self.creatures.entries:
383
+ if not (creature.active and creature.hp > 0.0):
384
+ continue
385
+ try:
386
+ type_id = CreatureTypeId(int(creature.type_id))
387
+ except ValueError:
388
+ continue
389
+ info = CREATURE_ANIM.get(type_id)
390
+ if info is None:
391
+ continue
392
+ creature.anim_phase, _ = creature_anim_advance_phase(
393
+ creature.anim_phase,
394
+ anim_rate=info.anim_rate,
395
+ move_speed=float(creature.move_speed),
396
+ dt=dt,
397
+ size=float(creature.size),
398
+ local_scale=float(getattr(creature, "move_scale", 1.0)),
399
+ flags=creature.flags,
400
+ ai_mode=int(creature.ai_mode),
401
+ )
402
+
403
+ def _advance_player_anim(self, dt: float, prev_positions: list[tuple[float, float]]) -> None:
404
+ info = CREATURE_ANIM.get(CreatureTypeId.TROOPER)
405
+ if info is None:
406
+ return
407
+ for idx, player in enumerate(self.players):
408
+ if idx >= len(prev_positions):
409
+ continue
410
+ prev_x, prev_y = prev_positions[idx]
411
+ speed = math.hypot(player.pos_x - prev_x, player.pos_y - prev_y)
412
+ move_speed = speed / dt / 120.0 if dt > 0.0 else 0.0
413
+ player.move_phase, _ = creature_anim_advance_phase(
414
+ player.move_phase,
415
+ anim_rate=info.anim_rate,
416
+ move_speed=move_speed,
417
+ dt=dt,
418
+ size=float(player.size),
419
+ local_scale=1.0,
420
+ flags=CreatureFlags(0),
421
+ ai_mode=0,
422
+ )
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ _TERRAIN_TEXTURES: dict[int, tuple[str, str]] = {
5
+ 0: ("ter_q1_base", "ter/ter_q1_base.jaz"),
6
+ 1: ("ter_q1_tex1", "ter/ter_q1_tex1.jaz"),
7
+ 2: ("ter_q2_base", "ter/ter_q2_base.jaz"),
8
+ 3: ("ter_q2_tex1", "ter/ter_q2_tex1.jaz"),
9
+ 4: ("ter_q3_base", "ter/ter_q3_base.jaz"),
10
+ 5: ("ter_q3_tex1", "ter/ter_q3_tex1.jaz"),
11
+ 6: ("ter_q4_base", "ter/ter_q4_base.jaz"),
12
+ 7: ("ter_q4_tex1", "ter/ter_q4_tex1.jaz"),
13
+ }
14
+
15
+
16
+ def terrain_texture_by_id(terrain_id: int) -> tuple[str, str] | None:
17
+ """Return (texture_cache_key, paq_relative_path) for a terrain texture ID."""
18
+ return _TERRAIN_TEXTURES.get(int(terrain_id))
19
+
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .timeline import BonusSpawnCall, TutorialFrameActions, TutorialState, tick_tutorial_timeline, tutorial_stage5_bonus_carrier_config
4
+
5
+ __all__ = [
6
+ "BonusSpawnCall",
7
+ "TutorialFrameActions",
8
+ "TutorialState",
9
+ "tick_tutorial_timeline",
10
+ "tutorial_stage5_bonus_carrier_config",
11
+ ]
12
+