crimsonland 0.1.0.dev14__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 (73) hide show
  1. crimson/cli.py +63 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/debug.py +9 -0
  6. crimson/demo.py +38 -45
  7. crimson/effects.py +7 -13
  8. crimson/frontend/high_scores_layout.py +81 -0
  9. crimson/frontend/panels/base.py +4 -1
  10. crimson/frontend/panels/controls.py +0 -15
  11. crimson/frontend/panels/databases.py +291 -3
  12. crimson/frontend/panels/mods.py +0 -15
  13. crimson/frontend/panels/play_game.py +0 -16
  14. crimson/game.py +689 -3
  15. crimson/gameplay.py +921 -569
  16. crimson/modes/base_gameplay_mode.py +33 -12
  17. crimson/modes/components/__init__.py +2 -0
  18. crimson/modes/components/highscore_record_builder.py +58 -0
  19. crimson/modes/components/perk_menu_controller.py +325 -0
  20. crimson/modes/quest_mode.py +94 -272
  21. crimson/modes/rush_mode.py +12 -43
  22. crimson/modes/survival_mode.py +109 -330
  23. crimson/modes/tutorial_mode.py +46 -247
  24. crimson/modes/typo_mode.py +11 -38
  25. crimson/oracle.py +396 -0
  26. crimson/perks.py +5 -2
  27. crimson/player_damage.py +95 -36
  28. crimson/projectiles.py +539 -320
  29. crimson/render/projectile_draw_registry.py +637 -0
  30. crimson/render/projectile_render_registry.py +110 -0
  31. crimson/render/secondary_projectile_draw_registry.py +206 -0
  32. crimson/render/world_renderer.py +58 -707
  33. crimson/sim/world_state.py +118 -61
  34. crimson/typo/spawns.py +5 -12
  35. crimson/ui/demo_trial_overlay.py +3 -11
  36. crimson/ui/formatting.py +24 -0
  37. crimson/ui/game_over.py +12 -58
  38. crimson/ui/hud.py +72 -39
  39. crimson/ui/layout.py +20 -0
  40. crimson/ui/perk_menu.py +9 -34
  41. crimson/ui/quest_results.py +28 -70
  42. crimson/ui/text_input.py +20 -0
  43. crimson/views/_ui_helpers.py +27 -0
  44. crimson/views/aim_debug.py +15 -32
  45. crimson/views/animations.py +18 -28
  46. crimson/views/arsenal_debug.py +22 -32
  47. crimson/views/bonuses.py +23 -36
  48. crimson/views/camera_debug.py +16 -29
  49. crimson/views/camera_shake.py +9 -33
  50. crimson/views/corpse_stamp_debug.py +13 -21
  51. crimson/views/decals_debug.py +36 -23
  52. crimson/views/fonts.py +8 -25
  53. crimson/views/ground.py +4 -21
  54. crimson/views/lighting_debug.py +42 -45
  55. crimson/views/particles.py +33 -42
  56. crimson/views/perk_menu_debug.py +3 -10
  57. crimson/views/player.py +50 -44
  58. crimson/views/player_sprite_debug.py +24 -31
  59. crimson/views/projectile_fx.py +57 -52
  60. crimson/views/projectile_render_debug.py +24 -33
  61. crimson/views/projectiles.py +24 -37
  62. crimson/views/spawn_plan.py +13 -29
  63. crimson/views/sprites.py +14 -29
  64. crimson/views/terrain.py +6 -23
  65. crimson/views/ui.py +7 -24
  66. crimson/views/wicons.py +28 -33
  67. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  68. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +73 -62
  69. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +1 -1
  70. grim/config.py +29 -1
  71. grim/console.py +7 -10
  72. grim/math.py +12 -0
  73. {crimsonland-0.1.0.dev14.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
@@ -18,14 +18,14 @@ from ..gameplay import bonus_find_aim_hover_entry, perk_active
18
18
  from ..perks import PerkId
19
19
  from ..projectiles import ProjectileTypeId
20
20
  from ..sim.world_defs import (
21
- BEAM_TYPES,
22
21
  CREATURE_ANIM,
23
22
  CREATURE_ASSET,
24
- ION_TYPES,
25
23
  KNOWN_PROJ_FRAMES,
26
- PLASMA_PARTICLE_TYPES,
27
24
  )
28
25
  from ..weapons import WEAPON_BY_ID
26
+ from .projectile_draw_registry import ProjectileDrawCtx, draw_projectile_from_registry
27
+ from .projectile_render_registry import known_proj_rgb
28
+ from .secondary_projectile_draw_registry import SecondaryProjectileDrawCtx, draw_secondary_projectile_from_registry
29
29
 
30
30
  if TYPE_CHECKING:
31
31
  from ..game_world import GameWorld
@@ -662,570 +662,22 @@ class WorldRenderer:
662
662
  life = float(getattr(proj, "life_timer", 0.0))
663
663
  angle = float(getattr(proj, "angle", 0.0))
664
664
 
665
- if self._is_bullet_trail_type(type_id):
666
- life_alpha = int(clamp(life, 0.0, 1.0) * 255)
667
- alpha_byte = int(clamp(float(life_alpha) * alpha, 0.0, 255.0) + 0.5)
668
- drawn = False
669
- if self.bullet_trail_texture is not None:
670
- ox = float(getattr(proj, "origin_x", pos_x))
671
- oy = float(getattr(proj, "origin_y", pos_y))
672
- sx0, sy0 = self.world_to_screen(ox, oy)
673
- sx1, sy1 = sx, sy
674
- drawn = self._draw_bullet_trail(sx0, sy0, sx1, sy1, type_id=type_id, alpha=alpha_byte, scale=scale)
675
-
676
- if self.bullet_texture is not None and life >= 0.39:
677
- size = self._bullet_sprite_size(type_id, scale=scale)
678
- src = rl.Rectangle(
679
- 0.0,
680
- 0.0,
681
- float(self.bullet_texture.width),
682
- float(self.bullet_texture.height),
683
- )
684
- dst = rl.Rectangle(float(sx), float(sy), size, size)
685
- origin = rl.Vector2(size * 0.5, size * 0.5)
686
- tint = rl.Color(220, 220, 220, alpha_byte)
687
- rl.draw_texture_pro(self.bullet_texture, src, dst, origin, float(angle * _RAD_TO_DEG), tint)
688
- drawn = True
689
-
690
- if drawn:
691
- return
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 type_id == int(ProjectileTypeId.ION_MINIGUN):
835
- effect_scale = 1.05
836
- elif type_id == int(ProjectileTypeId.ION_RIFLE):
837
- effect_scale = 2.2
838
- elif type_id == int(ProjectileTypeId.ION_CANNON):
839
- effect_scale = 3.5
840
- else:
841
- effect_scale = 0.8
842
-
843
- if life >= 0.4:
844
- base_alpha = alpha
845
- else:
846
- fade = clamp(life * 2.5, 0.0, 1.0)
847
- base_alpha = fade * alpha
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
- if life >= 0.4:
889
- head_tint = self._color_from_rgba((head_rgb[0], head_rgb[1], head_rgb[2], base_alpha))
890
- self._draw_atlas_sprite(
891
- texture,
892
- grid=grid,
893
- frame=frame,
894
- x=sx,
895
- y=sy,
896
- scale=sprite_scale,
897
- rotation_rad=angle,
898
- tint=head_tint,
899
- )
900
-
901
- # Fire Bullets renders an extra particles.png overlay in a later pass.
902
- if is_fire_bullets and self.particles_texture is not None:
903
- particles_texture = self.particles_texture
904
- atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
905
- if atlas is not None:
906
- grid = SIZE_CODE_GRID.get(int(atlas.size_code))
907
- if grid:
908
- cell_w = float(particles_texture.width) / float(grid)
909
- cell_h = float(particles_texture.height) / float(grid)
910
- frame = int(atlas.frame)
911
- col = frame % grid
912
- row = frame // grid
913
- src = rl.Rectangle(
914
- cell_w * float(col),
915
- cell_h * float(row),
916
- max(0.0, cell_w - 2.0),
917
- max(0.0, cell_h - 2.0),
918
- )
919
- tint = self._color_from_rgba((1.0, 1.0, 1.0, alpha))
920
- size = 64.0 * scale
921
- dst = rl.Rectangle(float(sx), float(sy), float(size), float(size))
922
- origin = rl.Vector2(size * 0.5, size * 0.5)
923
- rl.draw_texture_pro(particles_texture, src, dst, origin, float(angle * _RAD_TO_DEG), tint)
924
- else:
925
- # Native draws a small blue "core" at the head during the fade stage (life_timer < 0.4).
926
- core_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
927
- self._draw_atlas_sprite(
928
- texture,
929
- grid=grid,
930
- frame=frame,
931
- x=sx,
932
- y=sy,
933
- scale=1.0 * scale,
934
- rotation_rad=angle,
935
- tint=core_tint,
936
- )
937
-
938
- if is_ion:
939
- # Native: chain reach is derived from the streak scale (`fVar29 * perk_scale * 40.0`).
940
- radius = effect_scale * perk_scale * 40.0
941
-
942
- # Native iterates via creature_find_in_radius(pos, radius, start_index) in pool order.
943
- targets: list[object] = []
944
- for creature in self.creatures.entries[1:]:
945
- if not creature.active:
946
- continue
947
- if float(getattr(creature, "hitbox_size", 0.0)) <= 5.0:
948
- continue
949
- d = math.hypot(float(creature.x) - pos_x, float(creature.y) - pos_y)
950
- threshold = float(creature.size) * 0.14285715 + 3.0
951
- if d - radius < threshold:
952
- targets.append(creature)
953
-
954
- inner_half = 10.0 * perk_scale * scale
955
- outer_half = 14.0 * perk_scale * scale
956
- u = 0.625
957
- v0 = 0.0
958
- v1 = 0.25
959
-
960
- glow_targets: list[object] = []
961
- rl.rl_set_texture(texture.id)
962
- rl.rl_begin(rl.RL_QUADS)
963
-
964
- for creature in targets:
965
- tx, ty = self.world_to_screen(float(creature.x), float(creature.y))
966
- ddx = tx - sx
967
- ddy = ty - sy
968
- dlen = math.hypot(ddx, ddy)
969
- if dlen <= 1e-3:
970
- continue
971
- glow_targets.append(creature)
972
- inv = 1.0 / dlen
973
- nx = ddx * inv
974
- ny = ddy * inv
975
- px = -ny
976
- py = nx
977
-
978
- # Outer strip (softer).
979
- half = outer_half
980
- off_x = px * half
981
- off_y = py * half
982
- x0 = sx - off_x
983
- y0 = sy - off_y
984
- x1 = sx + off_x
985
- y1 = sy + off_y
986
- x2 = tx + off_x
987
- y2 = ty + off_y
988
- x3 = tx - off_x
989
- y3 = ty - off_y
990
-
991
- outer_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
992
- rl.rl_color4ub(outer_tint.r, outer_tint.g, outer_tint.b, outer_tint.a)
993
- rl.rl_tex_coord2f(u, v0)
994
- rl.rl_vertex2f(x0, y0)
995
- rl.rl_tex_coord2f(u, v1)
996
- rl.rl_vertex2f(x1, y1)
997
- rl.rl_tex_coord2f(u, v1)
998
- rl.rl_vertex2f(x2, y2)
999
- rl.rl_tex_coord2f(u, v0)
1000
- rl.rl_vertex2f(x3, y3)
1001
-
1002
- # Inner strip (brighter).
1003
- half = inner_half
1004
- off_x = px * half
1005
- off_y = py * half
1006
- x0 = sx - off_x
1007
- y0 = sy - off_y
1008
- x1 = sx + off_x
1009
- y1 = sy + off_y
1010
- x2 = tx + off_x
1011
- y2 = ty + off_y
1012
- x3 = tx - off_x
1013
- y3 = ty - off_y
1014
-
1015
- inner_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
1016
- rl.rl_color4ub(inner_tint.r, inner_tint.g, inner_tint.b, inner_tint.a)
1017
- rl.rl_tex_coord2f(u, v0)
1018
- rl.rl_vertex2f(x0, y0)
1019
- rl.rl_tex_coord2f(u, v1)
1020
- rl.rl_vertex2f(x1, y1)
1021
- rl.rl_tex_coord2f(u, v1)
1022
- rl.rl_vertex2f(x2, y2)
1023
- rl.rl_tex_coord2f(u, v0)
1024
- rl.rl_vertex2f(x3, y3)
1025
-
1026
- rl.rl_end()
1027
- rl.rl_set_texture(0)
1028
-
1029
- for creature in glow_targets:
1030
- tx, ty = self.world_to_screen(float(creature.x), float(creature.y))
1031
- target_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
1032
- self._draw_atlas_sprite(
1033
- texture,
1034
- grid=grid,
1035
- frame=frame,
1036
- x=tx,
1037
- y=ty,
1038
- scale=sprite_scale,
1039
- rotation_rad=0.0,
1040
- tint=target_tint,
1041
- )
1042
-
1043
- rl.end_blend_mode()
1044
- return
1045
-
1046
- if type_id == int(ProjectileTypeId.PULSE_GUN) and texture is not None:
1047
- mapping = KNOWN_PROJ_FRAMES.get(type_id)
1048
- if mapping is None:
1049
- return
1050
- grid, frame = mapping
1051
- cell_w = float(texture.width) / float(grid)
1052
-
1053
- if life >= 0.4:
1054
- ox = float(getattr(proj, "origin_x", pos_x))
1055
- oy = float(getattr(proj, "origin_y", pos_y))
1056
- dist = math.hypot(pos_x - ox, pos_y - oy)
1057
-
1058
- desired_size = dist * 0.16 * scale
1059
- if desired_size <= 1e-3:
1060
- return
1061
- sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1062
- if sprite_scale <= 1e-6:
1063
- return
1064
-
1065
- tint = self._color_from_rgba((0.1, 0.6, 0.2, alpha * 0.7))
1066
- rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1067
- self._draw_atlas_sprite(
1068
- texture,
1069
- grid=grid,
1070
- frame=frame,
1071
- x=sx,
1072
- y=sy,
1073
- scale=sprite_scale,
1074
- rotation_rad=angle,
1075
- tint=tint,
1076
- )
1077
- rl.end_blend_mode()
1078
- return
1079
-
1080
- fade = clamp(life * 2.5, 0.0, 1.0)
1081
- fade_alpha = fade * alpha
1082
- if fade_alpha <= 1e-3:
1083
- return
1084
-
1085
- desired_size = 56.0 * scale
1086
- sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1087
- if sprite_scale <= 1e-6:
1088
- return
1089
-
1090
- tint = self._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
1091
- rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1092
- self._draw_atlas_sprite(
1093
- texture,
1094
- grid=grid,
1095
- frame=frame,
1096
- x=sx,
1097
- y=sy,
1098
- scale=sprite_scale,
1099
- rotation_rad=angle,
1100
- tint=tint,
1101
- )
1102
- rl.end_blend_mode()
1103
- return
1104
-
1105
- if type_id in (int(ProjectileTypeId.SPLITTER_GUN), int(ProjectileTypeId.BLADE_GUN)) and texture is not None:
1106
- mapping = KNOWN_PROJ_FRAMES.get(type_id)
1107
- if mapping is None:
1108
- return
1109
- grid, frame = mapping
1110
- cell_w = float(texture.width) / float(grid)
1111
-
1112
- if life < 0.4:
1113
- return
1114
-
1115
- ox = float(getattr(proj, "origin_x", pos_x))
1116
- oy = float(getattr(proj, "origin_y", pos_y))
1117
- dist = math.hypot(pos_x - ox, pos_y - oy)
1118
-
1119
- desired_size = min(dist, 20.0) * scale
1120
- if desired_size <= 1e-3:
1121
- return
1122
-
1123
- sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1124
- if sprite_scale <= 1e-6:
1125
- return
1126
-
1127
- rotation_rad = angle
1128
- rgb = (1.0, 1.0, 1.0)
1129
- if type_id == int(ProjectileTypeId.BLADE_GUN):
1130
- rotation_rad = float(int(proj_index)) * 0.1 - float(self._elapsed_ms) * 0.1
1131
- rgb = (0.8, 0.8, 0.8)
1132
-
1133
- tint = self._color_from_rgba((rgb[0], rgb[1], rgb[2], alpha))
1134
- self._draw_atlas_sprite(
1135
- texture,
1136
- grid=grid,
1137
- frame=frame,
1138
- x=sx,
1139
- y=sy,
1140
- scale=sprite_scale,
1141
- rotation_rad=rotation_rad,
1142
- tint=tint,
1143
- )
1144
- return
1145
-
1146
- if type_id == int(ProjectileTypeId.PLAGUE_SPREADER) and texture is not None:
1147
- grid = 4
1148
- frame = 2
1149
- cell_w = float(texture.width) / float(grid)
1150
-
1151
- if life >= 0.4:
1152
- tint = self._color_from_rgba((1.0, 1.0, 1.0, alpha))
1153
-
1154
- def draw_plague_quad(*, px: float, py: float, size: float) -> None:
1155
- size = float(size)
1156
- if size <= 1e-3:
1157
- return
1158
- desired_size = size * scale
1159
- sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1160
- if sprite_scale <= 1e-6:
1161
- return
1162
- psx, psy = self.world_to_screen(px, py)
1163
- self._draw_atlas_sprite(
1164
- texture,
1165
- grid=grid,
1166
- frame=frame,
1167
- x=psx,
1168
- y=psy,
1169
- scale=sprite_scale,
1170
- rotation_rad=0.0,
1171
- tint=tint,
1172
- )
1173
-
1174
- draw_plague_quad(px=pos_x, py=pos_y, size=60.0)
1175
-
1176
- offset_angle = angle + math.pi / 2.0
1177
- draw_plague_quad(
1178
- px=pos_x + math.cos(offset_angle) * 15.0,
1179
- py=pos_y + math.sin(offset_angle) * 15.0,
1180
- size=60.0,
1181
- )
1182
-
1183
- phase = float(int(proj_index)) + float(self._elapsed_ms) * 0.01
1184
- cos_phase = math.cos(phase)
1185
- sin_phase = math.sin(phase)
1186
- draw_plague_quad(
1187
- px=pos_x + cos_phase * cos_phase - 5.0,
1188
- py=pos_y + sin_phase * 11.0 - 5.0,
1189
- size=52.0,
1190
- )
1191
-
1192
- phase_120 = phase + 2.0943952
1193
- sin_phase_120 = math.sin(phase_120)
1194
- draw_plague_quad(
1195
- px=pos_x + math.cos(phase_120) * 10.0,
1196
- py=pos_y + sin_phase_120 * 10.0,
1197
- size=62.0,
1198
- )
1199
-
1200
- phase_240 = phase + 4.1887903
1201
- draw_plague_quad(
1202
- px=pos_x + math.cos(phase_240) * 10.0,
1203
- py=pos_y + math.sin(phase_240) * sin_phase_120,
1204
- size=62.0,
1205
- )
1206
- return
1207
-
1208
- fade = clamp(life * 2.5, 0.0, 1.0)
1209
- fade_alpha = fade * alpha
1210
- if fade_alpha <= 1e-3:
1211
- return
1212
-
1213
- desired_size = (fade * 40.0 + 32.0) * scale
1214
- sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1215
- if sprite_scale <= 1e-6:
1216
- return
1217
-
1218
- tint = self._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
1219
- self._draw_atlas_sprite(
1220
- texture,
1221
- grid=grid,
1222
- frame=frame,
1223
- x=sx,
1224
- y=sy,
1225
- scale=sprite_scale,
1226
- rotation_rad=0.0,
1227
- tint=tint,
1228
- )
665
+ ctx = ProjectileDrawCtx(
666
+ renderer=self,
667
+ proj=proj,
668
+ proj_index=int(proj_index),
669
+ texture=texture,
670
+ type_id=int(type_id),
671
+ pos_x=float(pos_x),
672
+ pos_y=float(pos_y),
673
+ sx=float(sx),
674
+ sy=float(sy),
675
+ life=float(life),
676
+ angle=float(angle),
677
+ scale=float(scale),
678
+ alpha=float(alpha),
679
+ )
680
+ if draw_projectile_from_registry(ctx):
1229
681
  return
1230
682
 
1231
683
  mapping = KNOWN_PROJ_FRAMES.get(type_id)
@@ -1234,18 +686,9 @@ class WorldRenderer:
1234
686
  return
1235
687
  grid, frame = mapping
1236
688
 
1237
- color = rl.Color(240, 220, 160, 255)
1238
- if type_id in (ProjectileTypeId.ION_RIFLE, ProjectileTypeId.ION_MINIGUN, ProjectileTypeId.ION_CANNON):
1239
- color = rl.Color(120, 200, 255, 255)
1240
- elif type_id == ProjectileTypeId.FIRE_BULLETS:
1241
- color = rl.Color(255, 170, 90, 255)
1242
- elif type_id == ProjectileTypeId.SHRINKIFIER:
1243
- color = rl.Color(160, 255, 170, 255)
1244
- elif type_id == ProjectileTypeId.BLADE_GUN:
1245
- color = rl.Color(240, 120, 255, 255)
1246
-
1247
689
  alpha_byte = int(clamp(clamp(life / 0.4, 0.0, 1.0) * 255.0 * alpha, 0.0, 255.0) + 0.5)
1248
- tint = rl.Color(color.r, color.g, color.b, alpha_byte)
690
+ r, g, b = known_proj_rgb(type_id)
691
+ tint = rl.Color(int(r), int(g), int(b), alpha_byte)
1249
692
  self._draw_atlas_sprite(
1250
693
  texture,
1251
694
  grid=grid,
@@ -1439,135 +882,17 @@ class WorldRenderer:
1439
882
  proj_type = int(getattr(proj, "type_id", 0))
1440
883
  angle = float(getattr(proj, "angle", 0.0))
1441
884
 
1442
- if proj_type in (1, 2, 4) and self.projs_texture is not None:
1443
- texture = self.projs_texture
1444
- cell_w = float(texture.width) / 4.0
1445
- if cell_w <= 1e-6:
1446
- return
1447
-
1448
- base_alpha = clamp(alpha * 0.9, 0.0, 1.0)
1449
- base_tint = self._color_from_rgba((0.8, 0.8, 0.8, base_alpha))
1450
- base_size = 14.0
1451
- if proj_type == 2:
1452
- base_size = 10.0
1453
- elif proj_type == 4:
1454
- base_size = 8.0
1455
- sprite_scale = (base_size * scale) / cell_w
1456
-
1457
- fx_detail_1 = bool(self.config.data.get("fx_detail_1", 0)) if self.config is not None else True
1458
- if fx_detail_1 and self.particles_texture is not None:
1459
- particles_texture = self.particles_texture
1460
- atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
1461
- if atlas is not None:
1462
- grid = SIZE_CODE_GRID.get(int(atlas.size_code))
1463
- if grid:
1464
- particle_cell_w = float(particles_texture.width) / float(grid)
1465
- particle_cell_h = float(particles_texture.height) / float(grid)
1466
- frame = int(atlas.frame)
1467
- col = frame % grid
1468
- row = frame // grid
1469
- src = rl.Rectangle(
1470
- particle_cell_w * float(col),
1471
- particle_cell_h * float(row),
1472
- max(0.0, particle_cell_w - 2.0),
1473
- max(0.0, particle_cell_h - 2.0),
1474
- )
1475
-
1476
- dir_x = math.cos(angle - math.pi / 2.0)
1477
- dir_y = math.sin(angle - math.pi / 2.0)
1478
-
1479
- def _draw_rocket_fx(
1480
- *,
1481
- size: float,
1482
- offset: float,
1483
- rgba: tuple[float, float, float, float],
1484
- ) -> None:
1485
- fx_alpha = rgba[3]
1486
- if fx_alpha <= 1e-3:
1487
- return
1488
- tint = self._color_from_rgba(rgba)
1489
- fx_sx = sx - dir_x * offset * scale
1490
- fx_sy = sy - dir_y * offset * scale
1491
- dst_size = size * scale
1492
- dst = rl.Rectangle(float(fx_sx), float(fx_sy), float(dst_size), float(dst_size))
1493
- origin = rl.Vector2(dst_size * 0.5, dst_size * 0.5)
1494
- rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, tint)
1495
-
1496
- rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1497
- # Large bloom around the rocket (effect_id=0x0D).
1498
- _draw_rocket_fx(size=140.0, offset=5.0, rgba=(1.0, 1.0, 1.0, alpha * 0.48))
1499
-
1500
- if proj_type == 4:
1501
- _draw_rocket_fx(size=30.0, offset=9.0, rgba=(0.7, 0.7, 1.0, alpha * 0.158))
1502
- elif proj_type == 2:
1503
- _draw_rocket_fx(size=40.0, offset=9.0, rgba=(1.0, 1.0, 1.0, alpha * 0.58))
1504
- else:
1505
- _draw_rocket_fx(size=60.0, offset=9.0, rgba=(1.0, 1.0, 1.0, alpha * 0.68))
1506
-
1507
- rl.end_blend_mode()
1508
- self._draw_atlas_sprite(
1509
- texture,
1510
- grid=4,
1511
- frame=3,
1512
- x=sx,
1513
- y=sy,
1514
- scale=sprite_scale,
1515
- rotation_rad=angle,
1516
- tint=base_tint,
1517
- )
1518
- return
1519
-
1520
- if proj_type == 4:
1521
- rl.draw_circle(int(sx), int(sy), max(1.0, 12.0 * scale), rl.Color(200, 120, 255, int(255 * alpha + 0.5)))
1522
- return
1523
- if proj_type == 3:
1524
- # Secondary projectile detonation visuals (secondary_projectile_update + render).
1525
- t = clamp(float(getattr(proj, "vel_x", 0.0)), 0.0, 1.0)
1526
- det_scale = float(getattr(proj, "vel_y", 1.0))
1527
- fade = (1.0 - t) * alpha
1528
- if fade <= 1e-3 or det_scale <= 1e-6:
1529
- return
1530
- if self.particles_texture is None:
1531
- radius = det_scale * t * 80.0
1532
- alpha_byte = int(clamp((1.0 - t) * 180.0 * alpha, 0.0, 255.0) + 0.5)
1533
- color = rl.Color(255, 180, 100, alpha_byte)
1534
- rl.draw_circle_lines(int(sx), int(sy), max(1.0, radius * scale), color)
1535
- return
1536
-
1537
- atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
1538
- if atlas is None:
1539
- return
1540
- grid = SIZE_CODE_GRID.get(int(atlas.size_code))
1541
- if not grid:
1542
- return
1543
- frame = int(atlas.frame)
1544
- col = frame % grid
1545
- row = frame // grid
1546
- cell_w = float(self.particles_texture.width) / float(grid)
1547
- cell_h = float(self.particles_texture.height) / float(grid)
1548
- src = rl.Rectangle(
1549
- cell_w * float(col),
1550
- cell_h * float(row),
1551
- max(0.0, cell_w - 2.0),
1552
- max(0.0, cell_h - 2.0),
1553
- )
1554
-
1555
- def _draw_detonation_quad(*, size: float, alpha_mul: float) -> None:
1556
- a = fade * alpha_mul
1557
- if a <= 1e-3:
1558
- return
1559
- dst_size = size * scale
1560
- if dst_size <= 1e-3:
1561
- return
1562
- tint = self._color_from_rgba((1.0, 0.6, 0.1, a))
1563
- dst = rl.Rectangle(float(sx), float(sy), float(dst_size), float(dst_size))
1564
- origin = rl.Vector2(float(dst_size) * 0.5, float(dst_size) * 0.5)
1565
- rl.draw_texture_pro(self.particles_texture, src, dst, origin, 0.0, tint)
1566
-
1567
- rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1568
- _draw_detonation_quad(size=det_scale * t * 64.0, alpha_mul=1.0)
1569
- _draw_detonation_quad(size=det_scale * t * 200.0, alpha_mul=0.3)
1570
- rl.end_blend_mode()
885
+ ctx = SecondaryProjectileDrawCtx(
886
+ renderer=self,
887
+ proj=proj,
888
+ proj_type=int(proj_type),
889
+ sx=float(sx),
890
+ sy=float(sy),
891
+ angle=float(angle),
892
+ scale=float(scale),
893
+ alpha=float(alpha),
894
+ )
895
+ if draw_secondary_projectile_from_registry(ctx):
1571
896
  return
1572
897
  rl.draw_circle(int(sx), int(sy), max(1.0, 4.0 * scale), rl.Color(200, 200, 220, int(200 * alpha + 0.5)))
1573
898
 
@@ -1865,6 +1190,23 @@ class WorldRenderer:
1865
1190
  max(0.0, cell_w - 2.0),
1866
1191
  max(0.0, cell_h - 2.0),
1867
1192
  )
1193
+ poison_src: rl.Rectangle | None = None
1194
+ if particles_texture is not None:
1195
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x12)
1196
+ if atlas is not None:
1197
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
1198
+ if grid:
1199
+ frame = int(atlas.frame)
1200
+ col = frame % grid
1201
+ row = frame // grid
1202
+ cell_w = float(particles_texture.width) / float(grid)
1203
+ cell_h = float(particles_texture.height) / float(grid)
1204
+ poison_src = rl.Rectangle(
1205
+ cell_w * float(col),
1206
+ cell_h * float(row),
1207
+ max(0.0, cell_w - 2.0),
1208
+ max(0.0, cell_h - 2.0),
1209
+ )
1868
1210
 
1869
1211
  def draw_player(player: object) -> None:
1870
1212
  if trooper_texture is not None:
@@ -1912,6 +1254,15 @@ class WorldRenderer:
1912
1254
  type_id = None
1913
1255
  asset = CREATURE_ASSET.get(type_id) if type_id is not None else None
1914
1256
  texture = self.creature_textures.get(asset) if asset is not None else None
1257
+ if particles_texture is not None and poison_src is not None and (creature.flags & CreatureFlags.SELF_DAMAGE_TICK):
1258
+ fade = monster_vision_fade_alpha(hitbox_size)
1259
+ poison_alpha = fade * entity_alpha
1260
+ if poison_alpha > 1e-3:
1261
+ size = 60.0 * scale
1262
+ dst = rl.Rectangle(float(sx), float(sy), float(size), float(size))
1263
+ origin = rl.Vector2(size * 0.5, size * 0.5)
1264
+ tint = rl.Color(255, 0, 0, int(clamp(poison_alpha, 0.0, 1.0) * 255.0 + 0.5))
1265
+ rl.draw_texture_pro(particles_texture, poison_src, dst, origin, 0.0, tint)
1915
1266
  if monster_vision and particles_texture is not None and monster_vision_src is not None:
1916
1267
  fade = monster_vision_fade_alpha(hitbox_size)
1917
1268
  mv_alpha = fade * entity_alpha