crimsonland 0.1.0.dev2__tar.gz → 0.1.0.dev3__tar.gz

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 (137) hide show
  1. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/PKG-INFO +1 -1
  2. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/pyproject.toml +1 -1
  3. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/audio_router.py +3 -1
  4. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/projectiles.py +1 -0
  5. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/render/world_renderer.py +368 -31
  6. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/sim/world_defs.py +16 -5
  7. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/__init__.py +0 -0
  8. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/assets_fetch.py +0 -0
  9. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/atlas.py +0 -0
  10. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/bonuses.py +0 -0
  11. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/camera.py +0 -0
  12. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/cli.py +0 -0
  13. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/creatures/__init__.py +0 -0
  14. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/creatures/ai.py +0 -0
  15. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/creatures/anim.py +0 -0
  16. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/creatures/damage.py +0 -0
  17. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/creatures/runtime.py +0 -0
  18. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/creatures/spawn.py +0 -0
  19. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/debug.py +0 -0
  20. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/demo.py +0 -0
  21. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/demo_trial.py +0 -0
  22. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/effects.py +0 -0
  23. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/effects_atlas.py +0 -0
  24. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/__init__.py +0 -0
  25. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/assets.py +0 -0
  26. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/boot.py +0 -0
  27. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/menu.py +0 -0
  28. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/__init__.py +0 -0
  29. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/base.py +0 -0
  30. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/controls.py +0 -0
  31. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/mods.py +0 -0
  32. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/options.py +0 -0
  33. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/play_game.py +0 -0
  34. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/panels/stats.py +0 -0
  35. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/frontend/transitions.py +0 -0
  36. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/game.py +0 -0
  37. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/game_modes.py +0 -0
  38. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/game_world.py +0 -0
  39. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/gameplay.py +0 -0
  40. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/input_codes.py +0 -0
  41. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/__init__.py +0 -0
  42. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/base_gameplay_mode.py +0 -0
  43. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/quest_mode.py +0 -0
  44. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/rush_mode.py +0 -0
  45. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/survival_mode.py +0 -0
  46. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/tutorial_mode.py +0 -0
  47. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/modes/typo_mode.py +0 -0
  48. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/paths.py +0 -0
  49. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/perks.py +0 -0
  50. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/persistence/__init__.py +0 -0
  51. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/persistence/highscores.py +0 -0
  52. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/persistence/save_status.py +0 -0
  53. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/player_damage.py +0 -0
  54. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/__init__.py +0 -0
  55. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/helpers.py +0 -0
  56. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/registry.py +0 -0
  57. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/results.py +0 -0
  58. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/runtime.py +0 -0
  59. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/tier1.py +0 -0
  60. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/tier2.py +0 -0
  61. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/tier3.py +0 -0
  62. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/tier4.py +0 -0
  63. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/tier5.py +0 -0
  64. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/timeline.py +0 -0
  65. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/quests/types.py +0 -0
  66. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/render/__init__.py +0 -0
  67. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/render/terrain_fx.py +0 -0
  68. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/sim/__init__.py +0 -0
  69. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/sim/world_state.py +0 -0
  70. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/terrain_assets.py +0 -0
  71. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/tutorial/__init__.py +0 -0
  72. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/tutorial/timeline.py +0 -0
  73. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/typo/__init__.py +0 -0
  74. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/typo/names.py +0 -0
  75. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/typo/player.py +0 -0
  76. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/typo/spawns.py +0 -0
  77. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/typo/typing.py +0 -0
  78. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/ui/__init__.py +0 -0
  79. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/ui/cursor.py +0 -0
  80. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/ui/demo_trial_overlay.py +0 -0
  81. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/ui/game_over.py +0 -0
  82. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/ui/hud.py +0 -0
  83. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/ui/perk_menu.py +0 -0
  84. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/__init__.py +0 -0
  85. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/aim_debug.py +0 -0
  86. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/animations.py +0 -0
  87. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/arsenal_debug.py +0 -0
  88. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/audio_bootstrap.py +0 -0
  89. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/bonuses.py +0 -0
  90. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/camera_debug.py +0 -0
  91. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/camera_shake.py +0 -0
  92. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/corpse_stamp_debug.py +0 -0
  93. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/decals_debug.py +0 -0
  94. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/empty.py +0 -0
  95. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/fonts.py +0 -0
  96. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/game_over.py +0 -0
  97. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/ground.py +0 -0
  98. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/lighting_debug.py +0 -0
  99. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/particles.py +0 -0
  100. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/perk_menu_debug.py +0 -0
  101. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/perks.py +0 -0
  102. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/player.py +0 -0
  103. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/player_sprite_debug.py +0 -0
  104. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/projectile_fx.py +0 -0
  105. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/projectile_render_debug.py +0 -0
  106. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/projectiles.py +0 -0
  107. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/quest_title_overlay.py +0 -0
  108. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/registry.py +0 -0
  109. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/rush.py +0 -0
  110. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/small_font_debug.py +0 -0
  111. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/spawn_plan.py +0 -0
  112. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/sprites.py +0 -0
  113. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/survival.py +0 -0
  114. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/terrain.py +0 -0
  115. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/ui.py +0 -0
  116. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/views/wicons.py +0 -0
  117. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/weapon_sfx.py +0 -0
  118. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/crimson/weapons.py +0 -0
  119. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/__init__.py +0 -0
  120. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/app.py +0 -0
  121. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/assets.py +0 -0
  122. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/audio.py +0 -0
  123. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/config.py +0 -0
  124. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/console.py +0 -0
  125. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/fonts/__init__.py +0 -0
  126. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/fonts/grim_mono.py +0 -0
  127. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/fonts/small.py +0 -0
  128. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/input.py +0 -0
  129. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/jaz.py +0 -0
  130. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/math.py +0 -0
  131. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/music.py +0 -0
  132. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/paq.py +0 -0
  133. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/rand.py +0 -0
  134. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/sfx.py +0 -0
  135. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/sfx_map.py +0 -0
  136. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/terrain_render.py +0 -0
  137. {crimsonland-0.1.0.dev2 → crimsonland-0.1.0.dev3}/src/grim/view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: crimsonland
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev3
4
4
  Requires-Dist: construct>=2.10.70
5
5
  Requires-Dist: pillow>=12.1.0
6
6
  Requires-Dist: platformdirs>=4.5.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "crimsonland"
3
- version = "0.1.0.dev2"
3
+ version = "0.1.0.dev3"
4
4
  requires-python = ">=3.13"
5
5
  dependencies = [
6
6
  "construct>=2.10.70",
@@ -111,7 +111,9 @@ class AudioRouter:
111
111
  beam_types: frozenset[int],
112
112
  rand: Callable[[], int],
113
113
  ) -> str | None:
114
- if type_id in beam_types:
114
+ weapon = WEAPON_BY_ID.get(int(type_id))
115
+ ammo_class = weapon.ammo_class if weapon is not None else None
116
+ if ammo_class == 4:
115
117
  return "sfx_shock_hit_01"
116
118
  return self._rand_choice(rand, _BULLET_HIT_SFX)
117
119
 
@@ -65,6 +65,7 @@ class ProjectileTypeId(IntEnum):
65
65
  ION_CANNON = 0x17
66
66
  SHRINKIFIER = 0x18
67
67
  BLADE_GUN = 0x19
68
+ SPIDER_PLASMA = 0x1A
68
69
  PLASMA_CANNON = 0x1C
69
70
  SPLITTER_GUN = 0x1D
70
71
  PLAGUE_SPREADER = 0x29
@@ -17,7 +17,14 @@ from ..effects_atlas import EFFECT_ID_ATLAS_TABLE_BY_ID, SIZE_CODE_GRID
17
17
  from ..gameplay import bonus_find_aim_hover_entry, perk_active
18
18
  from ..perks import PerkId
19
19
  from ..projectiles import ProjectileTypeId
20
- from ..sim.world_defs import BEAM_TYPES, CREATURE_ANIM, CREATURE_ASSET, KNOWN_PROJ_FRAMES
20
+ from ..sim.world_defs import (
21
+ BEAM_TYPES,
22
+ CREATURE_ANIM,
23
+ CREATURE_ASSET,
24
+ ION_TYPES,
25
+ KNOWN_PROJ_FRAMES,
26
+ PLASMA_PARTICLE_TYPES,
27
+ )
21
28
  from ..weapons import WEAPON_BY_ID
22
29
 
23
30
  if TYPE_CHECKING:
@@ -683,6 +690,366 @@ class WorldRenderer:
683
690
  if drawn:
684
691
  return
685
692
 
693
+ if type_id in PLASMA_PARTICLE_TYPES and self.particles_texture is not None:
694
+ particles_texture = self.particles_texture
695
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
696
+ if atlas is not None:
697
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
698
+ if grid:
699
+ cell_w = float(particles_texture.width) / float(grid)
700
+ cell_h = float(particles_texture.height) / float(grid)
701
+ frame = int(atlas.frame)
702
+ col = frame % grid
703
+ row = frame // grid
704
+ src = rl.Rectangle(
705
+ cell_w * float(col),
706
+ cell_h * float(row),
707
+ max(0.0, cell_w - 2.0),
708
+ max(0.0, cell_h - 2.0),
709
+ )
710
+
711
+ speed_scale = float(getattr(proj, "speed_scale", 1.0))
712
+ fx_detail_1 = bool(self.config.data.get("fx_detail_1", 0)) if self.config is not None else True
713
+
714
+ rgb = (1.0, 1.0, 1.0)
715
+ spacing = 2.1
716
+ seg_limit = 3
717
+ tail_size = 12.0
718
+ head_size = 16.0
719
+ head_alpha_mul = 0.45
720
+ aura_rgb = rgb
721
+ aura_size = 120.0
722
+ aura_alpha_mul = 0.15
723
+
724
+ if type_id == int(ProjectileTypeId.PLASMA_RIFLE):
725
+ spacing = 2.5
726
+ seg_limit = 8
727
+ tail_size = 22.0
728
+ head_size = 56.0
729
+ aura_size = 256.0
730
+ aura_alpha_mul = 0.3
731
+ elif type_id == int(ProjectileTypeId.PLASMA_MINIGUN):
732
+ spacing = 2.1
733
+ seg_limit = 3
734
+ tail_size = 12.0
735
+ head_size = 16.0
736
+ aura_size = 120.0
737
+ aura_alpha_mul = 0.15
738
+ elif type_id == int(ProjectileTypeId.PLASMA_CANNON):
739
+ spacing = 2.6
740
+ seg_limit = 18
741
+ tail_size = 44.0
742
+ head_size = 84.0
743
+ aura_size = 256.0
744
+ # In the decompile, cannon reuses the tail alpha for the aura (0.4).
745
+ aura_alpha_mul = 0.4
746
+ elif type_id == int(ProjectileTypeId.SPIDER_PLASMA):
747
+ rgb = (0.3, 1.0, 0.3)
748
+ aura_rgb = rgb
749
+ elif type_id == int(ProjectileTypeId.SHRINKIFIER):
750
+ rgb = (0.3, 0.3, 1.0)
751
+ aura_rgb = rgb
752
+
753
+ if life >= 0.4:
754
+ # Reconstruct the tail length heuristic used by the native render path.
755
+ seg_count = int(float(getattr(proj, "base_damage", 0.0)))
756
+ if seg_count < 0:
757
+ seg_count = 0
758
+ seg_count //= 5
759
+ if seg_count > seg_limit:
760
+ seg_count = seg_limit
761
+
762
+ # The stored projectile angle is rotated by +pi/2 vs travel direction.
763
+ dir_x = math.cos(angle + math.pi / 2.0) * speed_scale
764
+ dir_y = math.sin(angle + math.pi / 2.0) * speed_scale
765
+
766
+ tail_tint = self._color_from_rgba((rgb[0], rgb[1], rgb[2], alpha * 0.4))
767
+ head_tint = self._color_from_rgba((rgb[0], rgb[1], rgb[2], alpha * head_alpha_mul))
768
+ aura_tint = self._color_from_rgba((aura_rgb[0], aura_rgb[1], aura_rgb[2], alpha * aura_alpha_mul))
769
+
770
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
771
+
772
+ if seg_count > 0:
773
+ size = tail_size * scale
774
+ origin = rl.Vector2(size * 0.5, size * 0.5)
775
+ step_x = dir_x * spacing
776
+ step_y = dir_y * spacing
777
+ for idx in range(seg_count):
778
+ px = pos_x + float(idx) * step_x
779
+ py = pos_y + float(idx) * step_y
780
+ psx, psy = self.world_to_screen(px, py)
781
+ dst = rl.Rectangle(float(psx), float(psy), float(size), float(size))
782
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, tail_tint)
783
+
784
+ size = head_size * scale
785
+ origin = rl.Vector2(size * 0.5, size * 0.5)
786
+ dst = rl.Rectangle(float(sx), float(sy), float(size), float(size))
787
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, head_tint)
788
+
789
+ if fx_detail_1:
790
+ size = aura_size * scale
791
+ origin = rl.Vector2(size * 0.5, size * 0.5)
792
+ dst = rl.Rectangle(float(sx), float(sy), float(size), float(size))
793
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, aura_tint)
794
+
795
+ rl.end_blend_mode()
796
+ return
797
+
798
+ fade = clamp(life * 2.5, 0.0, 1.0)
799
+ fade_alpha = fade * alpha
800
+ if fade_alpha > 1e-3:
801
+ tint = self._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
802
+ size = 56.0 * scale
803
+ dst = rl.Rectangle(float(sx), float(sy), float(size), float(size))
804
+ origin = rl.Vector2(size * 0.5, size * 0.5)
805
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
806
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, tint)
807
+ rl.end_blend_mode()
808
+ return
809
+
810
+ if type_id in BEAM_TYPES and texture is not None:
811
+ # Ion weapons and Fire Bullets use the projs.png streak effect (and Ion adds chain arcs on impact).
812
+ grid = 4
813
+ frame = 2
814
+
815
+ is_fire_bullets = type_id == int(ProjectileTypeId.FIRE_BULLETS)
816
+ is_ion = type_id in ION_TYPES
817
+
818
+ ox = float(getattr(proj, "origin_x", pos_x))
819
+ oy = float(getattr(proj, "origin_y", pos_y))
820
+ dx = pos_x - ox
821
+ dy = pos_y - oy
822
+ dist = math.hypot(dx, dy)
823
+ if dist <= 1e-6:
824
+ return
825
+
826
+ dir_x = dx / dist
827
+ dir_y = dy / dist
828
+
829
+ # In the native renderer, Ion Gun Master increases the chain effect thickness and reach.
830
+ perk_scale = 1.0
831
+ if any(perk_active(player, PerkId.ION_GUN_MASTER) for player in self.players):
832
+ perk_scale = 1.2
833
+
834
+ if life >= 0.4:
835
+ base_alpha = alpha
836
+ effect_scale = 1.0
837
+ else:
838
+ fade = clamp(life * 2.5, 0.0, 1.0)
839
+ base_alpha = fade * alpha
840
+ if type_id == int(ProjectileTypeId.ION_MINIGUN):
841
+ effect_scale = 1.05
842
+ elif type_id == int(ProjectileTypeId.ION_RIFLE):
843
+ effect_scale = 2.2
844
+ elif type_id == int(ProjectileTypeId.ION_CANNON):
845
+ effect_scale = 3.5
846
+ else:
847
+ effect_scale = 0.8
848
+
849
+ if base_alpha <= 1e-3:
850
+ return
851
+
852
+ streak_rgb = (1.0, 0.6, 0.1) if is_fire_bullets else (0.5, 0.6, 1.0)
853
+ head_rgb = (1.0, 1.0, 0.7)
854
+
855
+ # Only draw the last 256 units of the path.
856
+ start = 0.0
857
+ span = dist
858
+ if dist > 256.0:
859
+ start = dist - 256.0
860
+ span = 256.0
861
+
862
+ step = min(effect_scale * 3.1, 9.0)
863
+ sprite_scale = effect_scale * scale
864
+
865
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
866
+
867
+ s = start
868
+ while s < dist:
869
+ t = (s - start) / span if span > 1e-6 else 1.0
870
+ seg_alpha = t * base_alpha
871
+ if seg_alpha > 1e-3:
872
+ px = ox + dir_x * s
873
+ py = oy + dir_y * s
874
+ psx, psy = self.world_to_screen(px, py)
875
+ tint = self._color_from_rgba((streak_rgb[0], streak_rgb[1], streak_rgb[2], seg_alpha))
876
+ self._draw_atlas_sprite(
877
+ texture,
878
+ grid=grid,
879
+ frame=frame,
880
+ x=psx,
881
+ y=psy,
882
+ scale=sprite_scale,
883
+ rotation_rad=angle,
884
+ tint=tint,
885
+ )
886
+ s += step
887
+
888
+ head_tint = self._color_from_rgba((head_rgb[0], head_rgb[1], head_rgb[2], base_alpha))
889
+ self._draw_atlas_sprite(
890
+ texture,
891
+ grid=grid,
892
+ frame=frame,
893
+ x=sx,
894
+ y=sy,
895
+ scale=sprite_scale,
896
+ rotation_rad=angle,
897
+ tint=head_tint,
898
+ )
899
+
900
+ # Ion-only: impact core + chain arcs. (Fire Bullets renders an extra particles.png overlay in a later pass.)
901
+ if is_fire_bullets and life >= 0.4 and self.particles_texture is not None:
902
+ particles_texture = self.particles_texture
903
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
904
+ if atlas is not None:
905
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
906
+ if grid:
907
+ cell_w = float(particles_texture.width) / float(grid)
908
+ cell_h = float(particles_texture.height) / float(grid)
909
+ frame = int(atlas.frame)
910
+ col = frame % grid
911
+ row = frame // grid
912
+ src = rl.Rectangle(
913
+ cell_w * float(col),
914
+ cell_h * float(row),
915
+ max(0.0, cell_w - 2.0),
916
+ max(0.0, cell_h - 2.0),
917
+ )
918
+ tint = self._color_from_rgba((1.0, 1.0, 1.0, alpha))
919
+ size = 64.0 * scale
920
+ dst = rl.Rectangle(float(sx), float(sy), float(size), float(size))
921
+ origin = rl.Vector2(size * 0.5, size * 0.5)
922
+ rl.draw_texture_pro(particles_texture, src, dst, origin, float(angle * _RAD_TO_DEG), tint)
923
+
924
+ if is_ion and life < 0.4:
925
+ core_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
926
+ self._draw_atlas_sprite(
927
+ texture,
928
+ grid=grid,
929
+ frame=frame,
930
+ x=sx,
931
+ y=sy,
932
+ scale=1.0 * scale,
933
+ rotation_rad=angle,
934
+ tint=core_tint,
935
+ )
936
+
937
+ if type_id == int(ProjectileTypeId.ION_RIFLE):
938
+ radius = 88.0
939
+ elif type_id == int(ProjectileTypeId.ION_MINIGUN):
940
+ radius = 60.0
941
+ else:
942
+ radius = 128.0
943
+ radius *= perk_scale
944
+
945
+ # Pick a stable set of targets so the arc visuals don't flicker.
946
+ candidates: list[tuple[float, object]] = []
947
+ for creature in self.creatures.entries:
948
+ if not creature.active or float(creature.hp) <= 0.0:
949
+ continue
950
+ if float(getattr(creature, "hitbox_size", 0.0)) < 5.0:
951
+ continue
952
+ d = math.hypot(float(creature.x) - pos_x, float(creature.y) - pos_y)
953
+ threshold = float(creature.size) * 0.142857149 + 3.0
954
+ if d > radius + threshold:
955
+ continue
956
+ candidates.append((d, creature))
957
+
958
+ candidates.sort(key=lambda item: item[0])
959
+ targets = [creature for _d, creature in candidates[:8]]
960
+
961
+ inner = 10.0 * perk_scale * scale
962
+ outer = 14.0 * perk_scale * scale
963
+ u = 0.625
964
+ v0 = 0.0
965
+ v1 = 0.25
966
+
967
+ glow_targets: list[object] = []
968
+ rl.rl_set_texture(texture.id)
969
+ rl.rl_begin(rl.RL_QUADS)
970
+
971
+ for creature in targets:
972
+ tx, ty = self.world_to_screen(float(creature.x), float(creature.y))
973
+ ddx = tx - sx
974
+ ddy = ty - sy
975
+ dlen = math.hypot(ddx, ddy)
976
+ if dlen <= 1e-3:
977
+ continue
978
+ glow_targets.append(creature)
979
+ inv = 1.0 / dlen
980
+ nx = ddx * inv
981
+ ny = ddy * inv
982
+ px = -ny
983
+ py = nx
984
+
985
+ # Outer strip (softer).
986
+ half = outer * 0.5
987
+ off_x = px * half
988
+ off_y = py * half
989
+ x0 = sx - off_x
990
+ y0 = sy - off_y
991
+ x1 = sx + off_x
992
+ y1 = sy + off_y
993
+ x2 = tx + off_x
994
+ y2 = ty + off_y
995
+ x3 = tx - off_x
996
+ y3 = ty - off_y
997
+
998
+ outer_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha * 0.5))
999
+ rl.rl_color4ub(outer_tint.r, outer_tint.g, outer_tint.b, outer_tint.a)
1000
+ rl.rl_tex_coord2f(u, v0)
1001
+ rl.rl_vertex2f(x0, y0)
1002
+ rl.rl_tex_coord2f(u, v1)
1003
+ rl.rl_vertex2f(x1, y1)
1004
+ rl.rl_tex_coord2f(u, v1)
1005
+ rl.rl_vertex2f(x2, y2)
1006
+ rl.rl_tex_coord2f(u, v0)
1007
+ rl.rl_vertex2f(x3, y3)
1008
+
1009
+ # Inner strip (brighter).
1010
+ half = inner * 0.5
1011
+ off_x = px * half
1012
+ off_y = py * half
1013
+ x0 = sx - off_x
1014
+ y0 = sy - off_y
1015
+ x1 = sx + off_x
1016
+ y1 = sy + off_y
1017
+ x2 = tx + off_x
1018
+ y2 = ty + off_y
1019
+ x3 = tx - off_x
1020
+ y3 = ty - off_y
1021
+
1022
+ inner_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
1023
+ rl.rl_color4ub(inner_tint.r, inner_tint.g, inner_tint.b, inner_tint.a)
1024
+ rl.rl_tex_coord2f(u, v0)
1025
+ rl.rl_vertex2f(x0, y0)
1026
+ rl.rl_tex_coord2f(u, v1)
1027
+ rl.rl_vertex2f(x1, y1)
1028
+ rl.rl_tex_coord2f(u, v1)
1029
+ rl.rl_vertex2f(x2, y2)
1030
+ rl.rl_tex_coord2f(u, v0)
1031
+ rl.rl_vertex2f(x3, y3)
1032
+
1033
+ rl.rl_end()
1034
+ rl.rl_set_texture(0)
1035
+
1036
+ for creature in glow_targets:
1037
+ tx, ty = self.world_to_screen(float(creature.x), float(creature.y))
1038
+ target_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
1039
+ self._draw_atlas_sprite(
1040
+ texture,
1041
+ grid=grid,
1042
+ frame=frame,
1043
+ x=tx,
1044
+ y=ty,
1045
+ scale=sprite_scale,
1046
+ rotation_rad=0.0,
1047
+ tint=target_tint,
1048
+ )
1049
+
1050
+ rl.end_blend_mode()
1051
+ return
1052
+
686
1053
  mapping = KNOWN_PROJ_FRAMES.get(type_id)
687
1054
  if texture is None or mapping is None:
688
1055
  rl.draw_circle(int(sx), int(sy), max(1.0, 3.0 * scale), rl.Color(240, 220, 160, int(255 * alpha + 0.5)))
@@ -699,36 +1066,6 @@ class WorldRenderer:
699
1066
  elif type_id == ProjectileTypeId.BLADE_GUN:
700
1067
  color = rl.Color(240, 120, 255, 255)
701
1068
 
702
- if type_id in BEAM_TYPES and life >= 0.4:
703
- ox = float(getattr(proj, "origin_x", 0.0))
704
- oy = float(getattr(proj, "origin_y", 0.0))
705
- dx = float(getattr(proj, "pos_x", 0.0)) - ox
706
- dy = float(getattr(proj, "pos_y", 0.0)) - oy
707
- dist = math.hypot(dx, dy)
708
- if dist > 1e-6:
709
- step = 14.0
710
- seg_count = max(1, int(dist // step) + 1)
711
- dir_x = dx / dist
712
- dir_y = dy / dist
713
- for idx in range(seg_count):
714
- t = float(idx) / float(max(1, seg_count - 1))
715
- px = ox + dir_x * dist * t
716
- py = oy + dir_y * dist * t
717
- seg_alpha = int(clamp(220.0 * (1.0 - t * 0.75) * alpha, 0.0, 255.0) + 0.5)
718
- tint = rl.Color(color.r, color.g, color.b, seg_alpha)
719
- psx, psy = self.world_to_screen(px, py)
720
- self._draw_atlas_sprite(
721
- texture,
722
- grid=grid,
723
- frame=frame,
724
- x=psx,
725
- y=psy,
726
- scale=0.55 * scale,
727
- rotation_rad=angle,
728
- tint=tint,
729
- )
730
- return
731
-
732
1069
  alpha_byte = int(clamp(clamp(life / 0.4, 0.0, 1.0) * 255.0 * alpha, 0.0, 255.0) + 0.5)
733
1070
  tint = rl.Color(color.r, color.g, color.b, alpha_byte)
734
1071
  self._draw_atlas_sprite(
@@ -43,14 +43,25 @@ KNOWN_PROJ_FRAMES: dict[int, tuple[int, int]] = {
43
43
  ProjectileTypeId.ION_RIFLE: (4, 2),
44
44
  }
45
45
 
46
- BEAM_TYPES = frozenset(
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(
47
57
  {
48
58
  ProjectileTypeId.ION_RIFLE,
49
59
  ProjectileTypeId.ION_MINIGUN,
50
60
  ProjectileTypeId.ION_CANNON,
51
- ProjectileTypeId.SHRINKIFIER,
52
- ProjectileTypeId.FIRE_BULLETS,
53
- ProjectileTypeId.BLADE_GUN,
54
- ProjectileTypeId.SPLITTER_GUN,
55
61
  }
56
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