crimsonland 0.1.0.dev1__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 (138) 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 +153 -0
  5. crimson/bonuses.py +167 -0
  6. crimson/camera.py +75 -0
  7. crimson/cli.py +377 -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 +663 -0
  34. crimson/gameplay.py +2450 -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 +1039 -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 +1338 -0
  65. crimson/sim/__init__.py +1 -0
  66. crimson/sim/world_defs.py +56 -0
  67. crimson/sim/world_state.py +421 -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 +414 -0
  86. crimson/views/bonuses.py +201 -0
  87. crimson/views/camera_debug.py +359 -0
  88. crimson/views/camera_shake.py +229 -0
  89. crimson/views/corpse_stamp_debug.py +324 -0
  90. crimson/views/decals_debug.py +739 -0
  91. crimson/views/empty.py +19 -0
  92. crimson/views/fonts.py +114 -0
  93. crimson/views/game_over.py +117 -0
  94. crimson/views/ground.py +259 -0
  95. crimson/views/lighting_debug.py +1166 -0
  96. crimson/views/particles.py +293 -0
  97. crimson/views/perk_menu_debug.py +430 -0
  98. crimson/views/perks.py +398 -0
  99. crimson/views/player.py +433 -0
  100. crimson/views/player_sprite_debug.py +314 -0
  101. crimson/views/projectile_fx.py +608 -0
  102. crimson/views/projectile_render_debug.py +407 -0
  103. crimson/views/projectiles.py +221 -0
  104. crimson/views/quest_title_overlay.py +108 -0
  105. crimson/views/registry.py +34 -0
  106. crimson/views/rush.py +16 -0
  107. crimson/views/small_font_debug.py +204 -0
  108. crimson/views/spawn_plan.py +363 -0
  109. crimson/views/sprites.py +214 -0
  110. crimson/views/survival.py +15 -0
  111. crimson/views/terrain.py +132 -0
  112. crimson/views/ui.py +123 -0
  113. crimson/views/wicons.py +166 -0
  114. crimson/weapon_sfx.py +63 -0
  115. crimson/weapons.py +860 -0
  116. crimsonland-0.1.0.dev1.dist-info/METADATA +9 -0
  117. crimsonland-0.1.0.dev1.dist-info/RECORD +138 -0
  118. crimsonland-0.1.0.dev1.dist-info/WHEEL +4 -0
  119. crimsonland-0.1.0.dev1.dist-info/entry_points.txt +4 -0
  120. grim/__init__.py +20 -0
  121. grim/app.py +92 -0
  122. grim/assets.py +231 -0
  123. grim/audio.py +106 -0
  124. grim/config.py +294 -0
  125. grim/console.py +737 -0
  126. grim/fonts/__init__.py +7 -0
  127. grim/fonts/grim_mono.py +111 -0
  128. grim/fonts/small.py +120 -0
  129. grim/input.py +44 -0
  130. grim/jaz.py +103 -0
  131. grim/math.py +17 -0
  132. grim/music.py +403 -0
  133. grim/paq.py +76 -0
  134. grim/rand.py +37 -0
  135. grim/sfx.py +276 -0
  136. grim/sfx_map.py +103 -0
  137. grim/terrain_render.py +840 -0
  138. grim/view.py +16 -0
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,56 @@
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
+ BEAM_TYPES = frozenset(
47
+ {
48
+ ProjectileTypeId.ION_RIFLE,
49
+ ProjectileTypeId.ION_MINIGUN,
50
+ ProjectileTypeId.ION_CANNON,
51
+ ProjectileTypeId.SHRINKIFIER,
52
+ ProjectileTypeId.FIRE_BULLETS,
53
+ ProjectileTypeId.BLADE_GUN,
54
+ ProjectileTypeId.SPLITTER_GUN,
55
+ }
56
+ )
@@ -0,0 +1,421 @@
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
+ rng=self.state.rng.rand,
175
+ runtime_state=self.state,
176
+ players=self.players,
177
+ apply_player_damage=_apply_projectile_damage_to_player,
178
+ apply_creature_damage=_apply_projectile_damage_to_creature,
179
+ )
180
+ self.state.secondary_projectiles.update_pulse_gun(
181
+ dt,
182
+ self.creatures.entries,
183
+ apply_creature_damage=_apply_projectile_damage_to_creature,
184
+ runtime_state=self.state,
185
+ fx_queue=fx_queue,
186
+ detail_preset=int(detail_preset),
187
+ )
188
+
189
+ for idx, player in enumerate(self.players):
190
+ if idx >= len(prev_health):
191
+ continue
192
+ if float(prev_health[idx]) < 0.0:
193
+ continue
194
+ if float(player.health) >= 0.0:
195
+ continue
196
+ if not perk_active(player, PerkId.FINAL_REVENGE):
197
+ continue
198
+
199
+ px = float(player.pos_x)
200
+ py = float(player.pos_y)
201
+ rand = self.state.rng.rand
202
+ self.state.effects.spawn_explosion_burst(
203
+ pos_x=px,
204
+ pos_y=py,
205
+ scale=1.8,
206
+ rand=rand,
207
+ detail_preset=int(detail_preset),
208
+ )
209
+
210
+ prev_guard = bool(self.state.bonus_spawn_guard)
211
+ self.state.bonus_spawn_guard = True
212
+ for creature_idx, creature in enumerate(self.creatures.entries):
213
+ if not creature.active:
214
+ continue
215
+ if float(creature.hp) <= 0.0:
216
+ continue
217
+
218
+ dx = float(creature.x) - px
219
+ dy = float(creature.y) - py
220
+ if abs(dx) > 512.0 or abs(dy) > 512.0:
221
+ continue
222
+
223
+ remaining = 512.0 - math.hypot(dx, dy)
224
+ if remaining <= 0.0:
225
+ continue
226
+
227
+ damage = remaining * 5.0
228
+ death_start_needed = float(creature.hp) > 0.0 and float(creature.hitbox_size) == CREATURE_HITBOX_ALIVE
229
+ killed = creature_apply_damage(
230
+ creature,
231
+ damage_amount=damage,
232
+ damage_type=3,
233
+ impulse_x=0.0,
234
+ impulse_y=0.0,
235
+ owner_id=-1 - int(player.index),
236
+ dt=float(dt),
237
+ players=self.players,
238
+ rand=rand,
239
+ )
240
+ if killed and death_start_needed:
241
+ deaths.append(
242
+ self.creatures.handle_death(
243
+ int(creature_idx),
244
+ state=self.state,
245
+ players=self.players,
246
+ rand=rand,
247
+ detail_preset=int(detail_preset),
248
+ world_width=float(world_size),
249
+ world_height=float(world_size),
250
+ fx_queue=fx_queue,
251
+ )
252
+ )
253
+ self.state.bonus_spawn_guard = prev_guard
254
+ self.state.sfx_queue.append("sfx_explosion_large")
255
+ self.state.sfx_queue.append("sfx_shockwave")
256
+
257
+ def _kill_creature_no_corpse(creature_index: int, owner_id: int) -> None:
258
+ idx = int(creature_index)
259
+ if not (0 <= idx < len(self.creatures.entries)):
260
+ return
261
+ creature = self.creatures.entries[idx]
262
+ if not creature.active:
263
+ return
264
+ if float(creature.hp) <= 0.0:
265
+ return
266
+
267
+ creature.last_hit_owner_id = int(owner_id)
268
+ deaths.append(
269
+ self.creatures.handle_death(
270
+ idx,
271
+ state=self.state,
272
+ players=self.players,
273
+ rand=self.state.rng.rand,
274
+ detail_preset=int(detail_preset),
275
+ world_width=float(world_size),
276
+ world_height=float(world_size),
277
+ fx_queue=fx_queue,
278
+ keep_corpse=False,
279
+ )
280
+ )
281
+
282
+ self.state.particles.update(
283
+ dt,
284
+ creatures=self.creatures.entries,
285
+ apply_creature_damage=_apply_projectile_damage_to_creature,
286
+ kill_creature=_kill_creature_no_corpse,
287
+ )
288
+ self.state.sprite_effects.update(dt)
289
+
290
+ for idx, player in enumerate(self.players):
291
+ input_state = inputs[idx] if idx < len(inputs) else PlayerInput()
292
+ player_update(player, input_state, dt, self.state, world_size=float(world_size))
293
+
294
+ if dt > 0.0:
295
+ self._advance_creature_anim(dt)
296
+ self._advance_player_anim(dt, prev_positions)
297
+
298
+ pickups = bonus_update(
299
+ self.state,
300
+ self.players,
301
+ dt,
302
+ creatures=self.creatures.entries,
303
+ update_hud=True,
304
+ apply_creature_damage=_apply_projectile_damage_to_creature,
305
+ detail_preset=int(detail_preset),
306
+ )
307
+ if pickups:
308
+ for pickup in pickups:
309
+ if pickup.bonus_id != int(BonusId.NUKE):
310
+ self.state.effects.spawn_burst(
311
+ pos_x=float(pickup.pos_x),
312
+ pos_y=float(pickup.pos_y),
313
+ count=12,
314
+ rand=self.state.rng.rand,
315
+ detail_preset=detail_preset,
316
+ lifetime=0.4,
317
+ scale_step=0.1,
318
+ color_r=0.4,
319
+ color_g=0.5,
320
+ color_b=1.0,
321
+ color_a=0.5,
322
+ )
323
+ if pickup.bonus_id == int(BonusId.REFLEX_BOOST):
324
+ self.state.effects.spawn_ring(
325
+ pos_x=float(pickup.pos_x),
326
+ pos_y=float(pickup.pos_y),
327
+ detail_preset=detail_preset,
328
+ color_r=0.6,
329
+ color_g=0.6,
330
+ color_b=1.0,
331
+ color_a=1.0,
332
+ )
333
+ elif pickup.bonus_id == int(BonusId.FREEZE):
334
+ self.state.effects.spawn_ring(
335
+ pos_x=float(pickup.pos_x),
336
+ pos_y=float(pickup.pos_y),
337
+ detail_preset=detail_preset,
338
+ color_r=0.3,
339
+ color_g=0.5,
340
+ color_b=0.8,
341
+ color_a=1.0,
342
+ )
343
+
344
+ if perk_progression_enabled:
345
+ survival_progression_update(
346
+ self.state,
347
+ self.players,
348
+ game_mode=game_mode,
349
+ auto_pick=auto_pick_perks,
350
+ dt=dt,
351
+ creatures=self.creatures.entries,
352
+ )
353
+
354
+ sfx = list(creature_result.sfx)
355
+ if self.state.sfx_queue:
356
+ sfx.extend(self.state.sfx_queue)
357
+ self.state.sfx_queue.clear()
358
+ pain_sfx = ("sfx_trooper_inpain_01", "sfx_trooper_inpain_02", "sfx_trooper_inpain_03")
359
+ death_sfx = ("sfx_trooper_die_01", "sfx_trooper_die_02")
360
+ rand = self.state.rng.rand
361
+ for idx, player in enumerate(self.players):
362
+ if idx >= len(prev_health):
363
+ continue
364
+ before = float(prev_health[idx])
365
+ after = float(player.health)
366
+ if after >= before - 1e-6:
367
+ continue
368
+ if before <= 0.0:
369
+ continue
370
+ if after <= 0.0:
371
+ # Prioritize death VO even if there are many other SFX this frame.
372
+ sfx.insert(0, death_sfx[int(rand()) & 1])
373
+ else:
374
+ sfx.append(pain_sfx[int(rand()) % len(pain_sfx)])
375
+
376
+ return WorldEvents(hits=hits, deaths=tuple(deaths), pickups=pickups, sfx=sfx)
377
+
378
+ def _advance_creature_anim(self, dt: float) -> None:
379
+ if float(self.state.bonuses.freeze) > 0.0:
380
+ return
381
+ for creature in self.creatures.entries:
382
+ if not (creature.active and creature.hp > 0.0):
383
+ continue
384
+ try:
385
+ type_id = CreatureTypeId(int(creature.type_id))
386
+ except ValueError:
387
+ continue
388
+ info = CREATURE_ANIM.get(type_id)
389
+ if info is None:
390
+ continue
391
+ creature.anim_phase, _ = creature_anim_advance_phase(
392
+ creature.anim_phase,
393
+ anim_rate=info.anim_rate,
394
+ move_speed=float(creature.move_speed),
395
+ dt=dt,
396
+ size=float(creature.size),
397
+ local_scale=float(getattr(creature, "move_scale", 1.0)),
398
+ flags=creature.flags,
399
+ ai_mode=int(creature.ai_mode),
400
+ )
401
+
402
+ def _advance_player_anim(self, dt: float, prev_positions: list[tuple[float, float]]) -> None:
403
+ info = CREATURE_ANIM.get(CreatureTypeId.TROOPER)
404
+ if info is None:
405
+ return
406
+ for idx, player in enumerate(self.players):
407
+ if idx >= len(prev_positions):
408
+ continue
409
+ prev_x, prev_y = prev_positions[idx]
410
+ speed = math.hypot(player.pos_x - prev_x, player.pos_y - prev_y)
411
+ move_speed = speed / dt / 120.0 if dt > 0.0 else 0.0
412
+ player.move_phase, _ = creature_anim_advance_phase(
413
+ player.move_phase,
414
+ anim_rate=info.anim_rate,
415
+ move_speed=move_speed,
416
+ dt=dt,
417
+ size=float(player.size),
418
+ local_scale=1.0,
419
+ flags=CreatureFlags(0),
420
+ ai_mode=0,
421
+ )
@@ -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
+