crimsonland 0.1.0.dev5__py3-none-any.whl → 0.1.0.dev11__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.
@@ -7,6 +7,7 @@ import pyray as rl
7
7
 
8
8
  from grim.assets import PaqTextureCache
9
9
  from grim.audio import AudioState
10
+ from grim.console import ConsoleState
10
11
  from grim.config import CrimsonConfig
11
12
  from grim.view import ViewContext
12
13
 
@@ -17,7 +18,7 @@ from ..gameplay import PlayerInput, most_used_weapon_id_for_player, perk_selecti
17
18
  from ..persistence.highscores import HighScoreRecord
18
19
  from ..perks import PerkId, perk_display_description, perk_display_name
19
20
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
20
- from ..ui.hud import draw_hud_overlay
21
+ from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
21
22
  from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pressed, player_move_fire_binds
22
23
  from ..ui.perk_menu import (
23
24
  PerkMenuLayout,
@@ -85,6 +86,7 @@ class SurvivalMode(BaseGameplayMode):
85
86
  *,
86
87
  texture_cache: PaqTextureCache | None = None,
87
88
  config: CrimsonConfig | None = None,
89
+ console: ConsoleState | None = None,
88
90
  audio: AudioState | None = None,
89
91
  audio_rng: random.Random | None = None,
90
92
  ) -> None:
@@ -97,6 +99,7 @@ class SurvivalMode(BaseGameplayMode):
97
99
  hardcore=False,
98
100
  texture_cache=texture_cache,
99
101
  config=config,
102
+ console=console,
100
103
  audio=audio,
101
104
  audio_rng=audio_rng,
102
105
  )
@@ -744,6 +747,8 @@ class SurvivalMode(BaseGameplayMode):
744
747
  hud_bottom = 0.0
745
748
  if (not self._game_over_active) and (not perk_menu_active) and self._hud_assets is not None:
746
749
  hud_alpha = _clamp(self._hud_fade_ms / PERK_MENU_TRANSITION_MS, 0.0, 1.0)
750
+ hud_flags = hud_flags_for_game_mode(self._config_game_mode_id())
751
+ self._draw_target_health_bar(alpha=hud_alpha)
747
752
  hud_bottom = draw_hud_overlay(
748
753
  self._hud_assets,
749
754
  player=self._player,
@@ -754,9 +759,15 @@ class SurvivalMode(BaseGameplayMode):
754
759
  font=self._small,
755
760
  alpha=hud_alpha,
756
761
  frame_dt_ms=self._last_dt_ms,
762
+ show_health=hud_flags.show_health,
763
+ show_weapon=hud_flags.show_weapon,
764
+ show_xp=hud_flags.show_xp,
765
+ show_time=hud_flags.show_time,
766
+ show_quest_hud=hud_flags.show_quest_hud,
767
+ small_indicators=self._hud_small_indicators(),
757
768
  )
758
769
 
759
- if (not self._game_over_active) and (not perk_menu_active):
770
+ if debug_enabled() and (not self._game_over_active) and (not perk_menu_active):
760
771
  # Minimal debug text.
761
772
  x = 18.0
762
773
  y = max(18.0, hud_bottom + 10.0)
@@ -8,6 +8,7 @@ import pyray as rl
8
8
 
9
9
  from grim.assets import PaqTextureCache
10
10
  from grim.audio import AudioState
11
+ from grim.console import ConsoleState
11
12
  from grim.config import CrimsonConfig
12
13
  from grim.view import ViewContext
13
14
 
@@ -19,7 +20,7 @@ from ..input_codes import config_keybinds, input_code_is_down, input_code_is_pre
19
20
  from ..perks import PerkId, perk_display_description, perk_display_name
20
21
  from ..tutorial.timeline import TutorialFrameActions, TutorialState, tick_tutorial_timeline
21
22
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
22
- from ..ui.hud import draw_hud_overlay, hud_ui_scale
23
+ from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode, hud_ui_scale
23
24
  from ..ui.perk_menu import (
24
25
  PerkMenuAssets,
25
26
  PerkMenuLayout,
@@ -69,6 +70,7 @@ class TutorialMode(BaseGameplayMode):
69
70
  demo_mode_active: bool = False,
70
71
  texture_cache: PaqTextureCache | None = None,
71
72
  config: CrimsonConfig | None = None,
73
+ console: ConsoleState | None = None,
72
74
  audio: AudioState | None = None,
73
75
  audio_rng: random.Random | None = None,
74
76
  ) -> None:
@@ -81,6 +83,7 @@ class TutorialMode(BaseGameplayMode):
81
83
  hardcore=False,
82
84
  texture_cache=texture_cache,
83
85
  config=config,
86
+ console=console,
84
87
  audio=audio,
85
88
  audio_rng=audio_rng,
86
89
  )
@@ -453,6 +456,8 @@ class TutorialMode(BaseGameplayMode):
453
456
 
454
457
  hud_bottom = 0.0
455
458
  if (not perk_menu_active) and self._hud_assets is not None:
459
+ hud_flags = hud_flags_for_game_mode(self._config_game_mode_id())
460
+ self._draw_target_health_bar()
456
461
  hud_bottom = draw_hud_overlay(
457
462
  self._hud_assets,
458
463
  player=self._player,
@@ -463,6 +468,12 @@ class TutorialMode(BaseGameplayMode):
463
468
  font=self._small,
464
469
  alpha=1.0,
465
470
  frame_dt_ms=self._last_dt_ms,
471
+ show_health=hud_flags.show_health,
472
+ show_weapon=hud_flags.show_weapon,
473
+ show_xp=hud_flags.show_xp,
474
+ show_time=hud_flags.show_time,
475
+ show_quest_hud=hud_flags.show_quest_hud,
476
+ small_indicators=self._hud_small_indicators(),
466
477
  )
467
478
 
468
479
  self._draw_tutorial_prompts(hud_bottom=hud_bottom)
@@ -8,6 +8,7 @@ import pyray as rl
8
8
 
9
9
  from grim.assets import PaqTextureCache
10
10
  from grim.audio import AudioState
11
+ from grim.console import ConsoleState
11
12
  from grim.config import CrimsonConfig
12
13
  from grim.view import ViewContext
13
14
 
@@ -20,7 +21,7 @@ from ..typo.names import CreatureNameTable, load_typo_dictionary
20
21
  from ..typo.spawns import tick_typo_spawns
21
22
  from ..typo.typing import TypingBuffer
22
23
  from ..ui.cursor import draw_aim_cursor, draw_menu_cursor
23
- from ..ui.hud import draw_hud_overlay
24
+ from ..ui.hud import draw_hud_overlay, hud_flags_for_game_mode
24
25
  from ..ui.perk_menu import load_perk_menu_assets
25
26
  from .base_gameplay_mode import BaseGameplayMode
26
27
 
@@ -56,6 +57,7 @@ class TypoShooterMode(BaseGameplayMode):
56
57
  *,
57
58
  texture_cache: PaqTextureCache | None = None,
58
59
  config: CrimsonConfig | None = None,
60
+ console: ConsoleState | None = None,
59
61
  audio: AudioState | None = None,
60
62
  audio_rng: random.Random | None = None,
61
63
  ) -> None:
@@ -68,6 +70,7 @@ class TypoShooterMode(BaseGameplayMode):
68
70
  hardcore=False,
69
71
  texture_cache=texture_cache,
70
72
  config=config,
73
+ console=console,
71
74
  audio=audio,
72
75
  audio_rng=audio_rng,
73
76
  )
@@ -434,6 +437,8 @@ class TypoShooterMode(BaseGameplayMode):
434
437
  self._draw_name_labels()
435
438
 
436
439
  if show_gameplay_ui and self._hud_assets is not None:
440
+ hud_flags = hud_flags_for_game_mode(self._config_game_mode_id())
441
+ self._draw_target_health_bar()
437
442
  draw_hud_overlay(
438
443
  self._hud_assets,
439
444
  player=self._player,
@@ -442,9 +447,12 @@ class TypoShooterMode(BaseGameplayMode):
442
447
  elapsed_ms=float(self._typo.elapsed_ms),
443
448
  font=self._small,
444
449
  frame_dt_ms=self._last_dt_ms,
445
- show_weapon=False,
446
- show_xp=True,
447
- show_time=True,
450
+ show_health=hud_flags.show_health,
451
+ show_weapon=hud_flags.show_weapon,
452
+ show_xp=hud_flags.show_xp,
453
+ show_time=hud_flags.show_time,
454
+ show_quest_hud=hud_flags.show_quest_hud,
455
+ small_indicators=self._hud_small_indicators(),
448
456
  )
449
457
 
450
458
  if show_gameplay_ui:
crimson/projectiles.py CHANGED
@@ -112,7 +112,7 @@ class SecondaryProjectile:
112
112
  vel_y: float = 0.0
113
113
  type_id: int = 0
114
114
  owner_id: int = -100
115
- lifetime: float = 0.0
115
+ trail_timer: float = 0.0
116
116
  target_id: int = -1
117
117
 
118
118
 
@@ -383,6 +383,86 @@ class ProjectilePool:
383
383
  detail_preset=detail,
384
384
  )
385
385
 
386
+ def _spawn_plasma_cannon_hit_effects(pos_x: float, pos_y: float) -> None:
387
+ """Port of `projectile_update` Plasma Cannon hit extras.
388
+
389
+ Native does:
390
+ - `sfx_play_panned(sfx_explosion_medium)`
391
+ - `sfx_play_panned(sfx_shockwave)`
392
+ - `FUN_0042f330(pos, 1.5, 1.0)`
393
+ - `FUN_0042f330(pos, 1.0, 1.0)`
394
+ """
395
+
396
+ if effects is None or not hasattr(effects, "spawn"):
397
+ return
398
+
399
+ if isinstance(sfx_queue, list):
400
+ sfx_queue.append("sfx_explosion_medium")
401
+ sfx_queue.append("sfx_shockwave")
402
+
403
+ detail = int(detail_preset)
404
+
405
+ def _spawn_ring(*, scale: float) -> None:
406
+ effects.spawn(
407
+ effect_id=1,
408
+ pos_x=float(pos_x),
409
+ pos_y=float(pos_y),
410
+ vel_x=0.0,
411
+ vel_y=0.0,
412
+ rotation=0.0,
413
+ scale=1.0,
414
+ half_width=4.0,
415
+ half_height=4.0,
416
+ age=0.1,
417
+ lifetime=1.0,
418
+ flags=0x19,
419
+ color_r=0.9,
420
+ color_g=0.6,
421
+ color_b=0.3,
422
+ color_a=1.0,
423
+ rotation_step=0.0,
424
+ scale_step=float(scale) * 45.0,
425
+ detail_preset=detail,
426
+ )
427
+
428
+ _spawn_ring(scale=1.5)
429
+ _spawn_ring(scale=1.0)
430
+
431
+ def _spawn_splitter_hit_effects(pos_x: float, pos_y: float) -> None:
432
+ """Port of `FUN_0042f3f0(pos, 26.0, 3)` from the Splitter Gun hit branch."""
433
+
434
+ if effects is None or not hasattr(effects, "spawn"):
435
+ return
436
+
437
+ detail = int(detail_preset)
438
+ for _ in range(3):
439
+ angle = float(int(rng()) & 0x1FF) * (math.tau / 512.0)
440
+ radius = float(int(rng()) % 26)
441
+ jitter_age = -float(int(rng()) & 0xFF) * 0.0012
442
+ lifetime = 0.1 - jitter_age
443
+
444
+ effects.spawn(
445
+ effect_id=0,
446
+ pos_x=float(pos_x) + math.cos(angle) * radius,
447
+ pos_y=float(pos_y) + math.sin(angle) * radius,
448
+ vel_x=0.0,
449
+ vel_y=0.0,
450
+ rotation=0.0,
451
+ scale=1.0,
452
+ half_width=4.0,
453
+ half_height=4.0,
454
+ age=jitter_age,
455
+ lifetime=lifetime,
456
+ flags=0x19,
457
+ color_r=1.0,
458
+ color_g=0.9,
459
+ color_b=0.1,
460
+ color_a=1.0,
461
+ rotation_step=0.0,
462
+ scale_step=55.0,
463
+ detail_preset=detail,
464
+ )
465
+
386
466
  def _apply_damage_to_creature(
387
467
  creature_index: int,
388
468
  damage: float,
@@ -567,54 +647,6 @@ class ProjectilePool:
567
647
  acc_x = 0.0
568
648
  acc_y = 0.0
569
649
 
570
- if proj.hits_players:
571
- hit_player_idx = None
572
- if players is not None:
573
- for idx, player in enumerate(players):
574
- if float(player.health) <= 0.0:
575
- continue
576
- player_radius = _hit_radius_for(player)
577
- hit_r = proj.hit_radius + player_radius
578
- if _distance_sq(proj.pos_x, proj.pos_y, player.pos_x, player.pos_y) <= hit_r * hit_r:
579
- hit_player_idx = idx
580
- break
581
-
582
- if hit_player_idx is None:
583
- step += 3
584
- continue
585
-
586
- type_id = proj.type_id
587
- hit_x = float(proj.pos_x)
588
- hit_y = float(proj.pos_y)
589
- player = players[int(hit_player_idx)] if players is not None else None
590
- target_x = float(getattr(player, "pos_x", hit_x) if player is not None else hit_x)
591
- target_y = float(getattr(player, "pos_y", hit_y) if player is not None else hit_y)
592
- hits.append((type_id, proj.origin_x, proj.origin_y, hit_x, hit_y, target_x, target_y))
593
-
594
- if proj.life_timer != 0.25 and type_id not in (
595
- ProjectileTypeId.FIRE_BULLETS,
596
- ProjectileTypeId.GAUSS_GUN,
597
- ProjectileTypeId.BLADE_GUN,
598
- ):
599
- proj.life_timer = 0.25
600
- jitter = rng() & 3
601
- proj.pos_x += dir_x * float(jitter)
602
- proj.pos_y += dir_y * float(jitter)
603
-
604
- dist = math.hypot(proj.origin_x - proj.pos_x, proj.origin_y - proj.pos_y)
605
- if dist < 50.0:
606
- dist = 50.0
607
-
608
- damage_scale = _damage_scale(type_id)
609
- damage_amount = ((100.0 / dist) * damage_scale * 30.0 + 10.0) * 0.95
610
- if damage_amount > 0.0:
611
- if apply_player_damage is not None:
612
- apply_player_damage(int(hit_player_idx), float(damage_amount))
613
- elif players is not None:
614
- players[int(hit_player_idx)].health -= float(damage_amount)
615
-
616
- break
617
-
618
650
  hit_idx = None
619
651
  for idx, creature in enumerate(creatures):
620
652
  if creature.hp <= 0.0:
@@ -628,6 +660,61 @@ class ProjectilePool:
628
660
  break
629
661
 
630
662
  if hit_idx is None:
663
+ if proj.hits_players:
664
+ hit_player_idx = None
665
+ owner_id = int(proj.owner_id)
666
+ owner_player_index = -1 - owner_id if owner_id < 0 and owner_id != -100 else None
667
+ if players is not None:
668
+ for idx, player in enumerate(players):
669
+ if owner_player_index is not None and idx == owner_player_index:
670
+ continue
671
+ if float(player.health) <= 0.0:
672
+ continue
673
+ player_radius = _hit_radius_for(player)
674
+ hit_r = proj.hit_radius + player_radius
675
+ if (
676
+ _distance_sq(proj.pos_x, proj.pos_y, player.pos_x, player.pos_y)
677
+ <= hit_r * hit_r
678
+ ):
679
+ hit_player_idx = idx
680
+ break
681
+
682
+ if hit_player_idx is None:
683
+ step += 3
684
+ continue
685
+
686
+ type_id = proj.type_id
687
+ hit_x = float(proj.pos_x)
688
+ hit_y = float(proj.pos_y)
689
+ player = players[int(hit_player_idx)] if players is not None else None
690
+ target_x = float(getattr(player, "pos_x", hit_x) if player is not None else hit_x)
691
+ target_y = float(getattr(player, "pos_y", hit_y) if player is not None else hit_y)
692
+ hits.append((type_id, proj.origin_x, proj.origin_y, hit_x, hit_y, target_x, target_y))
693
+
694
+ if proj.life_timer != 0.25 and type_id not in (
695
+ ProjectileTypeId.FIRE_BULLETS,
696
+ ProjectileTypeId.GAUSS_GUN,
697
+ ProjectileTypeId.BLADE_GUN,
698
+ ):
699
+ proj.life_timer = 0.25
700
+ jitter = rng() & 3
701
+ proj.pos_x += dir_x * float(jitter)
702
+ proj.pos_y += dir_y * float(jitter)
703
+
704
+ dist = math.hypot(proj.origin_x - proj.pos_x, proj.origin_y - proj.pos_y)
705
+ if dist < 50.0:
706
+ dist = 50.0
707
+
708
+ damage_scale = _damage_scale(type_id)
709
+ damage_amount = ((100.0 / dist) * damage_scale * 30.0 + 10.0) * 0.95
710
+ if damage_amount > 0.0:
711
+ if apply_player_damage is not None:
712
+ apply_player_damage(int(hit_player_idx), float(damage_amount))
713
+ elif players is not None:
714
+ players[int(hit_player_idx)].health -= float(damage_amount)
715
+
716
+ break
717
+
631
718
  step += 3
632
719
  continue
633
720
 
@@ -639,6 +726,7 @@ class ProjectilePool:
639
726
  creature.flags |= CreatureFlags.SELF_DAMAGE_TICK
640
727
 
641
728
  if type_id == ProjectileTypeId.SPLITTER_GUN:
729
+ _spawn_splitter_hit_effects(proj.pos_x, proj.pos_y)
642
730
  self.spawn(
643
731
  pos_x=proj.pos_x,
644
732
  pos_y=proj.pos_y,
@@ -710,6 +798,7 @@ class ProjectilePool:
710
798
  owner_id=-100,
711
799
  base_damage=plasma_meta,
712
800
  )
801
+ _spawn_plasma_cannon_hit_effects(proj.pos_x, proj.pos_y)
713
802
  elif type_id == ProjectileTypeId.SHRINKIFIER:
714
803
  if hasattr(creature, "size"):
715
804
  new_size = float(getattr(creature, "size", 50.0) or 50.0) * 0.65
@@ -914,12 +1003,13 @@ class SecondaryProjectilePool:
914
1003
  entry.pos_y = float(pos_y)
915
1004
  entry.owner_id = int(owner_id)
916
1005
  entry.target_id = -1
1006
+ entry.trail_timer = 0.0
917
1007
 
918
1008
  if entry.type_id == 3:
1009
+ # Detonation state: `vel_x` becomes the expansion timer and `vel_y` the scale.
919
1010
  entry.vel_x = 0.0
920
- entry.vel_y = 0.0
1011
+ entry.vel_y = float(time_to_live)
921
1012
  entry.speed = float(time_to_live)
922
- entry.lifetime = 0.0
923
1013
  return index
924
1014
 
925
1015
  # Effects.md: vel = cos/sin(angle - PI/2) * 90 (190 for type 2).
@@ -931,7 +1021,6 @@ class SecondaryProjectilePool:
931
1021
  entry.vel_x = vx
932
1022
  entry.vel_y = vy
933
1023
  entry.speed = float(time_to_live)
934
- entry.lifetime = 0.0
935
1024
  return index
936
1025
 
937
1026
  def iter_active(self) -> list[SecondaryProjectile]:
@@ -966,6 +1055,7 @@ class SecondaryProjectilePool:
966
1055
  rand = _rng_zero
967
1056
  freeze_active = False
968
1057
  effects = None
1058
+ sprite_effects = None
969
1059
  sfx_queue = None
970
1060
  if runtime_state is not None:
971
1061
  rng = getattr(runtime_state, "rng", None)
@@ -977,6 +1067,7 @@ class SecondaryProjectilePool:
977
1067
  freeze_active = True
978
1068
 
979
1069
  effects = getattr(runtime_state, "effects", None)
1070
+ sprite_effects = getattr(runtime_state, "sprite_effects", None)
980
1071
  sfx_queue = getattr(runtime_state, "sfx_queue", None)
981
1072
 
982
1073
  for entry in self._entries:
@@ -984,9 +1075,10 @@ class SecondaryProjectilePool:
984
1075
  continue
985
1076
 
986
1077
  if entry.type_id == 3:
987
- entry.lifetime += dt * 3.0
988
- t = entry.lifetime
989
- scale = entry.speed
1078
+ # Detonation: `vel_x` becomes the expansion timer (0..1) and `vel_y` the scale.
1079
+ entry.vel_x += dt * 3.0
1080
+ t = float(entry.vel_x)
1081
+ scale = float(entry.vel_y)
990
1082
  if t > 1.0:
991
1083
  if fx_queue is not None:
992
1084
  fx_queue.add(
@@ -1066,6 +1158,23 @@ class SecondaryProjectilePool:
1066
1158
 
1067
1159
  entry.speed -= dt * 0.5
1068
1160
 
1161
+ # Rocket smoke trail (`trail_timer` in crimsonland.exe).
1162
+ entry.trail_timer -= (abs(float(entry.vel_x)) + abs(float(entry.vel_y))) * dt * 0.01
1163
+ if entry.trail_timer < 0.0:
1164
+ dir_x = math.cos(float(entry.angle) - math.pi / 2.0)
1165
+ dir_y = math.sin(float(entry.angle) - math.pi / 2.0)
1166
+ spawn_x = float(entry.pos_x) - dir_x * 9.0
1167
+ spawn_y = float(entry.pos_y) - dir_y * 9.0
1168
+ vel_x = math.cos(float(entry.angle) + math.pi / 2.0) * 90.0
1169
+ vel_y = math.sin(float(entry.angle) + math.pi / 2.0) * 90.0
1170
+ if sprite_effects is not None and hasattr(sprite_effects, "spawn"):
1171
+ sprite_id = sprite_effects.spawn(pos_x=spawn_x, pos_y=spawn_y, vel_x=vel_x, vel_y=vel_y, scale=14.0)
1172
+ try:
1173
+ sprite_effects.entries[int(sprite_id)].color_a = 0.25
1174
+ except Exception:
1175
+ pass
1176
+ entry.trail_timer = 0.06
1177
+
1069
1178
  # projectile_update uses creature_find_in_radius(..., 8.0, ...)
1070
1179
  hit_idx: int | None = None
1071
1180
  for idx, creature in enumerate(creatures):
@@ -1118,16 +1227,70 @@ class SecondaryProjectilePool:
1118
1227
  rand=rand,
1119
1228
  )
1120
1229
 
1230
+ if entry.type_id == 1 and effects is not None and hasattr(effects, "spawn_explosion_burst") and int(detail_preset) > 2:
1231
+ effects.spawn_explosion_burst(
1232
+ pos_x=float(entry.pos_x),
1233
+ pos_y=float(entry.pos_y),
1234
+ scale=0.4,
1235
+ rand=rand,
1236
+ detail_preset=int(detail_preset),
1237
+ )
1238
+
1121
1239
  entry.type_id = 3
1122
1240
  entry.vel_x = 0.0
1123
- entry.vel_y = 0.0
1124
- entry.speed = det_scale
1125
- entry.lifetime = 0.0
1241
+ entry.vel_y = float(det_scale)
1242
+ entry.trail_timer = 0.0
1243
+
1244
+ # Extra debris and scorch decals on detonation.
1245
+ if not freeze_active:
1246
+ extra_decals = 0
1247
+ extra_radius = 0.0
1248
+ if entry.type_id == 3:
1249
+ # NOTE: entry.type_id is already 3 here; use det_scale based on prior type.
1250
+ if det_scale == 1.0:
1251
+ extra_decals = 0x14
1252
+ extra_radius = 90.0
1253
+ elif det_scale == 0.35:
1254
+ extra_decals = 10
1255
+ extra_radius = 63.0
1256
+ elif det_scale == 0.25:
1257
+ extra_decals = 3
1258
+ extra_radius = 44.0
1259
+ if fx_queue is not None and extra_decals > 0:
1260
+ cx = float(creatures[hit_idx].x)
1261
+ cy = float(creatures[hit_idx].y)
1262
+ for _ in range(int(extra_decals)):
1263
+ angle = float(int(rand()) % 0x274) * 0.01
1264
+ radius = float(int(rand()) % max(1, int(extra_radius)))
1265
+ fx_queue.add_random(
1266
+ pos_x=cx + math.cos(angle) * radius,
1267
+ pos_y=cy + math.sin(angle) * radius,
1268
+ rand=rand,
1269
+ )
1270
+
1271
+ if sprite_effects is not None and hasattr(sprite_effects, "spawn"):
1272
+ step = math.tau / 10.0
1273
+ for idx in range(10):
1274
+ mag = float(int(rand()) % 800) * 0.1
1275
+ ang = float(idx) * step
1276
+ vel_x = math.cos(ang) * mag
1277
+ vel_y = math.sin(ang) * mag
1278
+ sprite_id = sprite_effects.spawn(
1279
+ pos_x=float(entry.pos_x),
1280
+ pos_y=float(entry.pos_y),
1281
+ vel_x=vel_x,
1282
+ vel_y=vel_y,
1283
+ scale=14.0,
1284
+ )
1285
+ try:
1286
+ sprite_effects.entries[int(sprite_id)].color_a = 0.37
1287
+ except Exception:
1288
+ pass
1289
+
1126
1290
  continue
1127
1291
 
1128
1292
  if entry.speed <= 0.0:
1129
1293
  entry.type_id = 3
1130
1294
  entry.vel_x = 0.0
1131
- entry.vel_y = 0.0
1132
- entry.speed = 0.5
1133
- entry.lifetime = 0.0
1295
+ entry.vel_y = 0.5
1296
+ entry.trail_timer = 0.0