crimsonland 0.1.0.dev15__py3-none-any.whl → 0.1.0.dev16__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 (75) hide show
  1. crimson/cli.py +61 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/demo.py +38 -45
  6. crimson/effects.py +7 -13
  7. crimson/frontend/high_scores_layout.py +81 -0
  8. crimson/frontend/panels/base.py +4 -1
  9. crimson/frontend/panels/controls.py +0 -15
  10. crimson/frontend/panels/databases.py +291 -3
  11. crimson/frontend/panels/mods.py +0 -15
  12. crimson/frontend/panels/play_game.py +0 -16
  13. crimson/game.py +441 -1
  14. crimson/gameplay.py +905 -569
  15. crimson/modes/base_gameplay_mode.py +33 -12
  16. crimson/modes/components/__init__.py +2 -0
  17. crimson/modes/components/highscore_record_builder.py +58 -0
  18. crimson/modes/components/perk_menu_controller.py +325 -0
  19. crimson/modes/quest_mode.py +58 -273
  20. crimson/modes/rush_mode.py +12 -43
  21. crimson/modes/survival_mode.py +71 -328
  22. crimson/modes/tutorial_mode.py +46 -247
  23. crimson/modes/typo_mode.py +11 -38
  24. crimson/oracle.py +396 -0
  25. crimson/perks.py +5 -2
  26. crimson/player_damage.py +94 -37
  27. crimson/projectiles.py +539 -320
  28. crimson/render/projectile_draw_registry.py +637 -0
  29. crimson/render/projectile_render_registry.py +110 -0
  30. crimson/render/secondary_projectile_draw_registry.py +206 -0
  31. crimson/render/world_renderer.py +58 -707
  32. crimson/sim/world_state.py +118 -61
  33. crimson/typo/spawns.py +5 -12
  34. crimson/ui/demo_trial_overlay.py +3 -11
  35. crimson/ui/formatting.py +24 -0
  36. crimson/ui/game_over.py +12 -58
  37. crimson/ui/hud.py +72 -39
  38. crimson/ui/layout.py +20 -0
  39. crimson/ui/perk_menu.py +9 -34
  40. crimson/ui/quest_results.py +12 -64
  41. crimson/ui/text_input.py +20 -0
  42. crimson/views/_ui_helpers.py +27 -0
  43. crimson/views/aim_debug.py +15 -32
  44. crimson/views/animations.py +18 -28
  45. crimson/views/arsenal_debug.py +22 -32
  46. crimson/views/bonuses.py +23 -36
  47. crimson/views/camera_debug.py +16 -29
  48. crimson/views/camera_shake.py +9 -33
  49. crimson/views/corpse_stamp_debug.py +13 -21
  50. crimson/views/decals_debug.py +36 -23
  51. crimson/views/fonts.py +8 -25
  52. crimson/views/ground.py +4 -21
  53. crimson/views/lighting_debug.py +42 -45
  54. crimson/views/particles.py +33 -42
  55. crimson/views/perk_menu_debug.py +3 -10
  56. crimson/views/player.py +50 -44
  57. crimson/views/player_sprite_debug.py +24 -31
  58. crimson/views/projectile_fx.py +57 -52
  59. crimson/views/projectile_render_debug.py +24 -33
  60. crimson/views/projectiles.py +24 -37
  61. crimson/views/spawn_plan.py +13 -29
  62. crimson/views/sprites.py +14 -29
  63. crimson/views/terrain.py +6 -23
  64. crimson/views/ui.py +7 -24
  65. crimson/views/wicons.py +28 -33
  66. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  67. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +72 -64
  68. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +2 -2
  69. grim/config.py +29 -1
  70. grim/console.py +7 -10
  71. grim/math.py +12 -0
  72. crimson/.DS_Store +0 -0
  73. crimson/creatures/.DS_Store +0 -0
  74. grim/.DS_Store +0 -0
  75. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
@@ -14,6 +14,7 @@ from dataclasses import dataclass, replace
14
14
  import math
15
15
  from typing import Callable, Sequence
16
16
 
17
+ from grim.math import clamp, distance_sq
17
18
  from grim.rand import Crand
18
19
  from ..effects import FxQueue, FxQueueRotated
19
20
  from ..gameplay import GameplayState, PlayerState, award_experience, perk_active
@@ -62,14 +63,6 @@ CREATURE_CORPSE_DESPAWN_HITBOX = -10.0
62
63
  CREATURE_DEATH_SLIDE_SCALE = 9.0
63
64
 
64
65
 
65
- def _clamp(value: float, lo: float, hi: float) -> float:
66
- if value < lo:
67
- return lo
68
- if value > hi:
69
- return hi
70
- return value
71
-
72
-
73
66
  def _wrap_angle(angle: float) -> float:
74
67
  return (angle + math.pi) % math.tau - math.pi
75
68
 
@@ -85,12 +78,6 @@ def _angle_approach(current: float, target: float, rate: float, dt: float) -> fl
85
78
  return _wrap_angle(current)
86
79
 
87
80
 
88
- def _distance_sq(x0: float, y0: float, x1: float, y1: float) -> float:
89
- dx = x1 - x0
90
- dy = y1 - y0
91
- return dx * dx + dy * dy
92
-
93
-
94
81
  def _owner_id_to_player_index(owner_id: int) -> int | None:
95
82
  # Native uses `-1/-2/-3/-4` for player indices and `-100` as a player-owned sentinel.
96
83
  if owner_id == -100:
@@ -136,8 +123,8 @@ class CreatureState:
136
123
  attack_cooldown: float = 0.0
137
124
  reward_value: float = 0.0
138
125
 
139
- # Contact damage gate.
140
- collision_flag: int = 0
126
+ # Plaguebearer infection state (native: `collision_flag` byte).
127
+ plague_infected: bool = False
141
128
  collision_timer: float = CONTACT_DAMAGE_PERIOD
142
129
  hitbox_size: float = CREATURE_HITBOX_ALIVE
143
130
 
@@ -174,6 +161,182 @@ class CreatureUpdateResult:
174
161
  sfx: tuple[str, ...] = ()
175
162
 
176
163
 
164
+ @dataclass(slots=True)
165
+ class _CreatureInteractionCtx:
166
+ pool: CreaturePool
167
+ creature_index: int
168
+ creature: CreatureState
169
+ state: GameplayState
170
+ players: list[PlayerState]
171
+ player: PlayerState
172
+ dt: float
173
+ rand: Callable[[], int]
174
+ detail_preset: int
175
+ world_width: float
176
+ world_height: float
177
+ fx_queue: FxQueue | None
178
+ fx_queue_rotated: FxQueueRotated | None
179
+ deaths: list[CreatureDeath]
180
+ sfx: list[str]
181
+ skip_creature: bool = False
182
+ contact_dist_sq: float = 0.0
183
+
184
+
185
+ _CreatureInteractionStep = Callable[[_CreatureInteractionCtx], None]
186
+
187
+
188
+ def _creature_interaction_plaguebearer_spread(ctx: _CreatureInteractionCtx) -> None:
189
+ if ctx.players and perk_active(ctx.players[0], PerkId.PLAGUEBEARER) and int(ctx.state.plaguebearer_infection_count) < 0x3C:
190
+ ctx.pool._plaguebearer_spread_infection(ctx.creature_index)
191
+
192
+
193
+ def _creature_interaction_energizer_eat(ctx: _CreatureInteractionCtx) -> None:
194
+ creature = ctx.creature
195
+ if float(ctx.state.bonuses.energizer) <= 0.0:
196
+ return
197
+ if float(creature.max_hp) >= 380.0:
198
+ return
199
+ if float(ctx.player.health) <= 0.0:
200
+ return
201
+
202
+ eat_dist_sq = distance_sq(creature.x, creature.y, ctx.player.pos_x, ctx.player.pos_y)
203
+ if eat_dist_sq >= 20.0 * 20.0:
204
+ return
205
+
206
+ creature.x = clamp(creature.x - creature.vel_x * ctx.dt, 0.0, float(ctx.world_width))
207
+ creature.y = clamp(creature.y - creature.vel_y * ctx.dt, 0.0, float(ctx.world_height))
208
+
209
+ ctx.state.effects.spawn_burst(
210
+ pos_x=float(creature.x),
211
+ pos_y=float(creature.y),
212
+ count=6,
213
+ rand=ctx.rand,
214
+ detail_preset=int(ctx.detail_preset),
215
+ )
216
+ ctx.sfx.append("sfx_ui_bonus")
217
+
218
+ prev_guard = bool(ctx.state.bonus_spawn_guard)
219
+ ctx.state.bonus_spawn_guard = True
220
+ creature.last_hit_owner_id = -1 - int(ctx.player.index)
221
+ ctx.deaths.append(
222
+ ctx.pool.handle_death(
223
+ ctx.creature_index,
224
+ state=ctx.state,
225
+ players=ctx.players,
226
+ rand=ctx.rand,
227
+ detail_preset=int(ctx.detail_preset),
228
+ world_width=float(ctx.world_width),
229
+ world_height=float(ctx.world_height),
230
+ fx_queue=ctx.fx_queue,
231
+ keep_corpse=False,
232
+ )
233
+ )
234
+ ctx.state.bonus_spawn_guard = prev_guard
235
+ ctx.skip_creature = True
236
+
237
+
238
+ def _creature_interaction_contact_damage(ctx: _CreatureInteractionCtx) -> None:
239
+ creature = ctx.creature
240
+ if float(ctx.state.bonuses.energizer) > 0.0:
241
+ return
242
+
243
+ ctx.contact_dist_sq = distance_sq(creature.x, creature.y, ctx.player.pos_x, ctx.player.pos_y)
244
+ contact_r = (float(creature.size) + float(ctx.player.size)) * 0.25 + 20.0
245
+ in_contact = ctx.contact_dist_sq <= contact_r * contact_r
246
+ if not in_contact:
247
+ return
248
+
249
+ creature.collision_timer -= ctx.dt
250
+ if creature.collision_timer >= 0.0:
251
+ return
252
+
253
+ creature.collision_timer += CONTACT_DAMAGE_PERIOD
254
+
255
+ mr_melee_killed = False
256
+ mr_melee_death_start_needed = False
257
+ if perk_active(ctx.player, PerkId.MR_MELEE):
258
+ mr_melee_death_start_needed = creature.hp > 0.0 and creature.hitbox_size == CREATURE_HITBOX_ALIVE
259
+
260
+ from .damage import creature_apply_damage
261
+
262
+ mr_melee_killed = creature_apply_damage(
263
+ creature,
264
+ damage_amount=25.0,
265
+ damage_type=2,
266
+ impulse_x=0.0,
267
+ impulse_y=0.0,
268
+ owner_id=-1 - int(ctx.player.index),
269
+ dt=ctx.dt,
270
+ players=ctx.players,
271
+ rand=ctx.rand,
272
+ )
273
+
274
+ if float(ctx.player.shield_timer) <= 0.0:
275
+ if perk_active(ctx.player, PerkId.TOXIC_AVENGER):
276
+ creature.flags |= CreatureFlags.SELF_DAMAGE_TICK | CreatureFlags.SELF_DAMAGE_TICK_STRONG
277
+ elif perk_active(ctx.player, PerkId.VEINS_OF_POISON):
278
+ creature.flags |= CreatureFlags.SELF_DAMAGE_TICK
279
+
280
+ player_take_damage(ctx.state, ctx.player, float(creature.contact_damage), dt=ctx.dt, rand=ctx.rand)
281
+
282
+ if ctx.fx_queue is not None:
283
+ dx = float(ctx.player.pos_x) - float(creature.x)
284
+ dy = float(ctx.player.pos_y) - float(creature.y)
285
+ dist = math.hypot(dx, dy)
286
+ if dist > 1e-9:
287
+ dx /= dist
288
+ dy /= dist
289
+ else:
290
+ dx = 0.0
291
+ dy = 0.0
292
+ ctx.fx_queue.add_random(
293
+ pos_x=float(ctx.player.pos_x) + dx * 3.0,
294
+ pos_y=float(ctx.player.pos_y) + dy * 3.0,
295
+ rand=ctx.rand,
296
+ )
297
+
298
+ if mr_melee_killed and mr_melee_death_start_needed:
299
+ ctx.deaths.append(
300
+ ctx.pool.handle_death(
301
+ ctx.creature_index,
302
+ state=ctx.state,
303
+ players=ctx.players,
304
+ rand=ctx.rand,
305
+ detail_preset=int(ctx.detail_preset),
306
+ world_width=float(ctx.world_width),
307
+ world_height=float(ctx.world_height),
308
+ fx_queue=ctx.fx_queue,
309
+ )
310
+ )
311
+ if creature.active:
312
+ ctx.pool._tick_dead(
313
+ creature,
314
+ dt=ctx.dt,
315
+ world_width=float(ctx.world_width),
316
+ world_height=float(ctx.world_height),
317
+ fx_queue_rotated=ctx.fx_queue_rotated,
318
+ )
319
+ ctx.skip_creature = True
320
+
321
+
322
+ def _creature_interaction_plaguebearer_contact_flag(ctx: _CreatureInteractionCtx) -> None:
323
+ if float(ctx.state.bonuses.energizer) > 0.0:
324
+ return
325
+
326
+ creature = ctx.creature
327
+ if bool(ctx.player.plaguebearer_active) and float(creature.hp) < 150.0 and int(ctx.state.plaguebearer_infection_count) < 0x32:
328
+ if ctx.contact_dist_sq < 30.0 * 30.0:
329
+ creature.plague_infected = True
330
+
331
+
332
+ _CREATURE_INTERACTION_STEPS: tuple[_CreatureInteractionStep, ...] = (
333
+ _creature_interaction_plaguebearer_spread,
334
+ _creature_interaction_energizer_eat,
335
+ _creature_interaction_contact_damage,
336
+ _creature_interaction_plaguebearer_contact_flag,
337
+ )
338
+
339
+
177
340
  class CreaturePool:
178
341
  def __init__(self, *, size: int = CREATURE_POOL_SIZE, env: SpawnEnv | None = None) -> None:
179
342
  self._entries = [CreatureState() for _ in range(int(size))]
@@ -211,10 +374,10 @@ class CreaturePool:
211
374
  continue
212
375
 
213
376
  if math.hypot(float(creature.x) - float(origin.x), float(creature.y) - float(origin.y)) < 45.0:
214
- if creature.collision_flag != 0 and float(origin.hp) < 150.0:
215
- origin.collision_flag = 1
216
- if origin.collision_flag != 0 and float(creature.hp) < 150.0:
217
- creature.collision_flag = 1
377
+ if creature.plague_infected and float(origin.hp) < 150.0:
378
+ origin.plague_infected = True
379
+ if origin.plague_infected and float(creature.hp) < 150.0:
380
+ creature.plague_infected = True
218
381
  return
219
382
 
220
383
  def _alloc_slot(self, *, rand: Callable[[], int] | None = None) -> int:
@@ -403,11 +566,36 @@ class CreaturePool:
403
566
  creature.vel_y = 0.0
404
567
  continue
405
568
 
569
+ poison_killed = False
406
570
  if creature.flags & CreatureFlags.SELF_DAMAGE_TICK_STRONG:
407
- creature.hp -= dt * 180.0
571
+ from .damage import creature_apply_damage
572
+
573
+ poison_killed = creature_apply_damage(
574
+ creature,
575
+ damage_amount=dt * 180.0,
576
+ damage_type=0,
577
+ impulse_x=0.0,
578
+ impulse_y=0.0,
579
+ owner_id=int(creature.last_hit_owner_id),
580
+ dt=dt,
581
+ players=players,
582
+ rand=rand,
583
+ )
408
584
  elif creature.flags & CreatureFlags.SELF_DAMAGE_TICK:
409
- creature.hp -= dt * 60.0
410
- if creature.hp <= 0.0:
585
+ from .damage import creature_apply_damage
586
+
587
+ poison_killed = creature_apply_damage(
588
+ creature,
589
+ damage_amount=dt * 60.0,
590
+ damage_type=0,
591
+ impulse_x=0.0,
592
+ impulse_y=0.0,
593
+ owner_id=int(creature.last_hit_owner_id),
594
+ dt=dt,
595
+ players=players,
596
+ rand=rand,
597
+ )
598
+ if poison_killed:
411
599
  deaths.append(
412
600
  self.handle_death(
413
601
  idx,
@@ -430,7 +618,7 @@ class CreaturePool:
430
618
  )
431
619
  continue
432
620
 
433
- if creature.collision_flag != 0:
621
+ if creature.plague_infected:
434
622
  creature.collision_timer -= float(dt)
435
623
  if creature.collision_timer < 0.0:
436
624
  creature.collision_timer += CONTACT_DAMAGE_PERIOD
@@ -531,7 +719,7 @@ class CreaturePool:
531
719
  )
532
720
  continue
533
721
 
534
- if (float(state.bonuses.energizer) > 0.0 and float(creature.max_hp) < 500.0) or creature.collision_flag != 0:
722
+ if (float(state.bonuses.energizer) > 0.0 and float(creature.max_hp) < 500.0) or creature.plague_infected:
535
723
  creature.target_heading = _wrap_angle(float(creature.target_heading) + math.pi)
536
724
 
537
725
  turn_rate = float(creature.move_speed) * CREATURE_TURN_RATE_SCALE
@@ -547,16 +735,16 @@ class CreaturePool:
547
735
  dir_y = math.sin(creature.heading - math.pi / 2.0)
548
736
  creature.vel_x = dir_x * speed
549
737
  creature.vel_y = dir_y * speed
550
- creature.x = _clamp(creature.x + creature.vel_x * dt, 0.0, float(world_width))
551
- creature.y = _clamp(creature.y + creature.vel_y * dt, 0.0, float(world_height))
738
+ creature.x = clamp(creature.x + creature.vel_x * dt, 0.0, float(world_width))
739
+ creature.y = clamp(creature.y + creature.vel_y * dt, 0.0, float(world_height))
552
740
  else:
553
741
  # Spawner/short-strip creatures clamp to bounds using `size` as a radius; most are stationary
554
742
  # unless ANIM_LONG_STRIP is set (see creature_update_all).
555
743
  radius = max(0.0, float(creature.size))
556
744
  max_x = max(radius, float(world_width) - radius)
557
745
  max_y = max(radius, float(world_height) - radius)
558
- creature.x = _clamp(creature.x, radius, max_x)
559
- creature.y = _clamp(creature.y, radius, max_y)
746
+ creature.x = clamp(creature.x, radius, max_x)
747
+ creature.y = clamp(creature.y, radius, max_y)
560
748
  if (creature.flags & CreatureFlags.ANIM_LONG_STRIP) == 0:
561
749
  creature.vel_x = 0.0
562
750
  creature.vel_y = 0.0
@@ -566,130 +754,32 @@ class CreaturePool:
566
754
  dir_y = math.sin(creature.heading - math.pi / 2.0)
567
755
  creature.vel_x = dir_x * speed
568
756
  creature.vel_y = dir_y * speed
569
- creature.x = _clamp(creature.x + creature.vel_x * dt, radius, max_x)
570
- creature.y = _clamp(creature.y + creature.vel_y * dt, radius, max_y)
571
-
572
- if (
573
- players
574
- and perk_active(players[0], PerkId.PLAGUEBEARER)
575
- and int(state.plaguebearer_infection_count) < 0x3C
576
- ):
577
- self._plaguebearer_spread_infection(idx)
578
-
579
- if float(state.bonuses.energizer) > 0.0 and float(creature.max_hp) < 380.0 and float(player.health) > 0.0:
580
- eat_dist_sq = _distance_sq(creature.x, creature.y, player.pos_x, player.pos_y)
581
- if eat_dist_sq < 20.0 * 20.0:
582
- creature.x = _clamp(creature.x - creature.vel_x * dt, 0.0, float(world_width))
583
- creature.y = _clamp(creature.y - creature.vel_y * dt, 0.0, float(world_height))
584
-
585
- state.effects.spawn_burst(
586
- pos_x=float(creature.x),
587
- pos_y=float(creature.y),
588
- count=6,
589
- rand=rand,
590
- detail_preset=int(detail_preset),
591
- )
592
- sfx.append("sfx_ui_bonus")
593
-
594
- prev_guard = bool(state.bonus_spawn_guard)
595
- state.bonus_spawn_guard = True
596
- creature.last_hit_owner_id = -1 - int(player.index)
597
- deaths.append(
598
- self.handle_death(
599
- idx,
600
- state=state,
601
- players=players,
602
- rand=rand,
603
- detail_preset=int(detail_preset),
604
- world_width=world_width,
605
- world_height=world_height,
606
- fx_queue=fx_queue,
607
- keep_corpse=False,
608
- )
609
- )
610
- state.bonus_spawn_guard = prev_guard
611
- continue
612
-
613
- # Contact damage throttle. While Energizer is active, the native suppresses
614
- # contact/melee interactions for most creatures (and instead allows "eat" kills).
615
- if float(state.bonuses.energizer) <= 0.0:
616
- dist_sq = _distance_sq(creature.x, creature.y, player.pos_x, player.pos_y)
617
- contact_r = (float(creature.size) + float(player.size)) * 0.25 + 20.0
618
- in_contact = dist_sq <= contact_r * contact_r
619
- if in_contact:
620
- creature.collision_timer -= dt
621
- if creature.collision_timer < 0.0:
622
- creature.collision_timer += CONTACT_DAMAGE_PERIOD
623
- if perk_active(player, PerkId.MR_MELEE):
624
- death_start_needed = creature.hp > 0.0 and creature.hitbox_size == CREATURE_HITBOX_ALIVE
625
-
626
- from .damage import creature_apply_damage
627
-
628
- killed = creature_apply_damage(
629
- creature,
630
- damage_amount=25.0,
631
- damage_type=2,
632
- impulse_x=0.0,
633
- impulse_y=0.0,
634
- owner_id=-1 - int(player.index),
635
- dt=dt,
636
- players=players,
637
- rand=rand,
638
- )
639
- if killed and death_start_needed:
640
- deaths.append(
641
- self.handle_death(
642
- idx,
643
- state=state,
644
- players=players,
645
- rand=rand,
646
- detail_preset=int(detail_preset),
647
- world_width=world_width,
648
- world_height=world_height,
649
- fx_queue=fx_queue,
650
- )
651
- )
652
- if creature.active:
653
- self._tick_dead(
654
- creature,
655
- dt=dt,
656
- world_width=world_width,
657
- world_height=world_height,
658
- fx_queue_rotated=fx_queue_rotated,
659
- )
660
- continue
661
-
662
- if float(player.shield_timer) <= 0.0:
663
- if perk_active(player, PerkId.TOXIC_AVENGER):
664
- creature.flags |= (
665
- CreatureFlags.SELF_DAMAGE_TICK | CreatureFlags.SELF_DAMAGE_TICK_STRONG
666
- )
667
- elif perk_active(player, PerkId.VEINS_OF_POISON):
668
- creature.flags |= CreatureFlags.SELF_DAMAGE_TICK
669
- player_take_damage(state, player, float(creature.contact_damage), dt=dt, rand=rand)
670
- if fx_queue is not None:
671
- dx = float(player.pos_x) - float(creature.x)
672
- dy = float(player.pos_y) - float(creature.y)
673
- dist = math.hypot(dx, dy)
674
- if dist > 1e-9:
675
- dx /= dist
676
- dy /= dist
677
- else:
678
- dx = 0.0
679
- dy = 0.0
680
- fx_queue.add_random(
681
- pos_x=float(player.pos_x) + dx * 3.0,
682
- pos_y=float(player.pos_y) + dy * 3.0,
683
- rand=rand,
684
- )
685
-
686
- if (
687
- bool(player.plaguebearer_active)
688
- and float(creature.hp) < 150.0
689
- and int(state.plaguebearer_infection_count) < 0x32
690
- and dist_sq < 30.0 * 30.0
691
- ):
692
- creature.collision_flag = 1
757
+ creature.x = clamp(creature.x + creature.vel_x * dt, radius, max_x)
758
+ creature.y = clamp(creature.y + creature.vel_y * dt, radius, max_y)
759
+
760
+ interaction_ctx = _CreatureInteractionCtx(
761
+ pool=self,
762
+ creature_index=int(idx),
763
+ creature=creature,
764
+ state=state,
765
+ players=players,
766
+ player=player,
767
+ dt=dt,
768
+ rand=rand,
769
+ detail_preset=int(detail_preset),
770
+ world_width=float(world_width),
771
+ world_height=float(world_height),
772
+ fx_queue=fx_queue,
773
+ fx_queue_rotated=fx_queue_rotated,
774
+ deaths=deaths,
775
+ sfx=sfx,
776
+ )
777
+ for step in _CREATURE_INTERACTION_STEPS:
778
+ step(interaction_ctx)
779
+ if interaction_ctx.skip_creature:
780
+ break
781
+ if interaction_ctx.skip_creature:
782
+ continue
693
783
 
694
784
  if (not frozen_by_evil_eyes) and (creature.flags & (CreatureFlags.RANGED_ATTACK_SHOCK | CreatureFlags.RANGED_ATTACK_VARIANT)):
695
785
  # Ported from creature_update_all (see `analysis/ghidra/raw/crimsonland.exe_decompiled.c`
@@ -862,7 +952,7 @@ class CreaturePool:
862
952
  entry.tint_b = float(tint[2])
863
953
  entry.tint_a = float(tint[3])
864
954
 
865
- entry.collision_flag = 0
955
+ entry.plague_infected = False
866
956
  entry.collision_timer = CONTACT_DAMAGE_PERIOD
867
957
  entry.hitbox_size = CREATURE_HITBOX_ALIVE
868
958
 
@@ -909,8 +999,8 @@ class CreaturePool:
909
999
  dir_y = math.sin(creature.heading - math.pi / 2.0)
910
1000
  creature.vel_x = dir_x * new_hitbox * float(dt) * CREATURE_DEATH_SLIDE_SCALE
911
1001
  creature.vel_y = dir_y * new_hitbox * float(dt) * CREATURE_DEATH_SLIDE_SCALE
912
- creature.x = _clamp(creature.x - creature.vel_x, 0.0, float(world_width))
913
- creature.y = _clamp(creature.y - creature.vel_y, 0.0, float(world_height))
1002
+ creature.x = clamp(creature.x - creature.vel_x, 0.0, float(world_width))
1003
+ creature.y = clamp(creature.y - creature.vel_y, 0.0, float(world_height))
914
1004
  else:
915
1005
  creature.vel_x = 0.0
916
1006
  creature.vel_y = 0.0
@@ -27,6 +27,7 @@ __all__ = [
27
27
  "CreatureFlags",
28
28
  "CreatureInit",
29
29
  "CreatureTypeId",
30
+ "RANDOM_HEADING_SENTINEL",
30
31
  "SpawnId",
31
32
  "SPAWN_ID_TO_TEMPLATE",
32
33
  "SPAWN_TEMPLATES",
@@ -54,6 +55,9 @@ __all__ = [
54
55
  Tint = tuple[float | None, float | None, float | None, float | None]
55
56
  TintRGBA = tuple[float, float, float, float]
56
57
 
58
+ # Heading sentinel that forces randomized heading in `creature_spawn_template`.
59
+ RANDOM_HEADING_SENTINEL = -100.0
60
+
57
61
 
58
62
  class CreatureTypeId(IntEnum):
59
63
  ZOMBIE = 0
@@ -1366,7 +1370,7 @@ class CreatureInit:
1366
1370
  pos_y: float
1367
1371
 
1368
1372
  # Headings are in radians. The original seeds a random heading early, then overwrites it
1369
- # at the end with the function argument (or a randomized argument for `-100.0`).
1373
+ # at the end with the function argument (or a randomized argument for `RANDOM_HEADING_SENTINEL`).
1370
1374
  heading: float
1371
1375
 
1372
1376
  phase_seed: float
@@ -1648,9 +1652,9 @@ class PlanBuilder:
1648
1652
  spawn_slots: list[SpawnSlotInit] = []
1649
1653
  effects: list[BurstEffect] = []
1650
1654
 
1651
- # `heading == -100.0` uses a randomized heading.
1655
+ # `heading == RANDOM_HEADING_SENTINEL` uses a randomized heading.
1652
1656
  final_heading = heading
1653
- if final_heading == -100.0:
1657
+ if final_heading == RANDOM_HEADING_SENTINEL:
1654
1658
  final_heading = float(rng.rand() % 628) * 0.01
1655
1659
 
1656
1660
  # Base initialization always consumes one rand() for a transient heading value.