crimsonland 0.1.0.dev4__py3-none-any.whl → 0.1.0.dev6__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.
@@ -452,13 +452,13 @@ class WorldRenderer:
452
452
  ):
453
453
  atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x10)
454
454
  if atlas is not None:
455
- grid = SIZE_CODE_GRID.get(int(atlas.size_code))
456
- if grid:
455
+ aura_grid = SIZE_CODE_GRID.get(int(atlas.size_code))
456
+ if aura_grid:
457
457
  frame = int(atlas.frame)
458
- col = frame % grid
459
- row = frame // grid
460
- cell_w = float(self.particles_texture.width) / float(grid)
461
- cell_h = float(self.particles_texture.height) / float(grid)
458
+ col = frame % aura_grid
459
+ row = frame // aura_grid
460
+ cell_w = float(self.particles_texture.width) / float(aura_grid)
461
+ cell_h = float(self.particles_texture.height) / float(aura_grid)
462
462
  src = rl.Rectangle(
463
463
  cell_w * float(col),
464
464
  cell_h * float(row),
@@ -650,7 +650,7 @@ class WorldRenderer:
650
650
  )
651
651
  draw(frame, x=sx, y=sy, scale_mul=1.0, rotation=float(player.aim_heading), color=overlay_tint)
652
652
 
653
- def _draw_projectile(self, proj: object, *, scale: float, alpha: float = 1.0) -> None:
653
+ def _draw_projectile(self, proj: object, *, proj_index: int = 0, scale: float, alpha: float = 1.0) -> None:
654
654
  alpha = clamp(float(alpha), 0.0, 1.0)
655
655
  if alpha <= 1e-3:
656
656
  return
@@ -831,20 +831,20 @@ class WorldRenderer:
831
831
  if any(perk_active(player, PerkId.ION_GUN_MASTER) for player in self.players):
832
832
  perk_scale = 1.2
833
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
+
834
843
  if life >= 0.4:
835
844
  base_alpha = alpha
836
- effect_scale = 1.0
837
845
  else:
838
846
  fade = clamp(life * 2.5, 0.0, 1.0)
839
847
  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
848
 
849
849
  if base_alpha <= 1e-3:
850
850
  return
@@ -885,43 +885,44 @@ class WorldRenderer:
885
885
  )
886
886
  s += step
887
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)
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
+ )
923
900
 
924
- if is_ion and life < 0.4:
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).
925
926
  core_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
926
927
  self._draw_atlas_sprite(
927
928
  texture,
@@ -934,120 +935,301 @@ class WorldRenderer:
934
935
  tint=core_tint,
935
936
  )
936
937
 
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))
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
+ # Pick a stable set of targets so the arc visuals don't flicker.
943
+ candidates: list[tuple[float, object]] = []
944
+ for creature in self.creatures.entries:
945
+ if not creature.active or float(creature.hp) <= 0.0:
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.142857149 + 3.0
951
+ if d > radius + threshold:
952
+ continue
953
+ candidates.append((d, creature))
954
+
955
+ candidates.sort(key=lambda item: item[0])
956
+ targets = [creature for _d, creature in candidates[:8]]
957
+
958
+ inner = 10.0 * perk_scale * scale
959
+ outer = 14.0 * perk_scale * scale
960
+ u = 0.625
961
+ v0 = 0.0
962
+ v1 = 0.25
963
+
964
+ glow_targets: list[object] = []
965
+ rl.rl_set_texture(texture.id)
966
+ rl.rl_begin(rl.RL_QUADS)
967
+
968
+ for creature in targets:
969
+ tx, ty = self.world_to_screen(float(creature.x), float(creature.y))
970
+ ddx = tx - sx
971
+ ddy = ty - sy
972
+ dlen = math.hypot(ddx, ddy)
973
+ if dlen <= 1e-3:
974
+ continue
975
+ glow_targets.append(creature)
976
+ inv = 1.0 / dlen
977
+ nx = ddx * inv
978
+ ny = ddy * inv
979
+ px = -ny
980
+ py = nx
981
+
982
+ # Outer strip (softer).
983
+ half = outer * 0.5
984
+ off_x = px * half
985
+ off_y = py * half
986
+ x0 = sx - off_x
987
+ y0 = sy - off_y
988
+ x1 = sx + off_x
989
+ y1 = sy + off_y
990
+ x2 = tx + off_x
991
+ y2 = ty + off_y
992
+ x3 = tx - off_x
993
+ y3 = ty - off_y
994
+
995
+ outer_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha * 0.5))
996
+ rl.rl_color4ub(outer_tint.r, outer_tint.g, outer_tint.b, outer_tint.a)
997
+ rl.rl_tex_coord2f(u, v0)
998
+ rl.rl_vertex2f(x0, y0)
999
+ rl.rl_tex_coord2f(u, v1)
1000
+ rl.rl_vertex2f(x1, y1)
1001
+ rl.rl_tex_coord2f(u, v1)
1002
+ rl.rl_vertex2f(x2, y2)
1003
+ rl.rl_tex_coord2f(u, v0)
1004
+ rl.rl_vertex2f(x3, y3)
1005
+
1006
+ # Inner strip (brighter).
1007
+ half = inner * 0.5
1008
+ off_x = px * half
1009
+ off_y = py * half
1010
+ x0 = sx - off_x
1011
+ y0 = sy - off_y
1012
+ x1 = sx + off_x
1013
+ y1 = sy + off_y
1014
+ x2 = tx + off_x
1015
+ y2 = ty + off_y
1016
+ x3 = tx - off_x
1017
+ y3 = ty - off_y
1018
+
1019
+ inner_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
1020
+ rl.rl_color4ub(inner_tint.r, inner_tint.g, inner_tint.b, inner_tint.a)
1021
+ rl.rl_tex_coord2f(u, v0)
1022
+ rl.rl_vertex2f(x0, y0)
1023
+ rl.rl_tex_coord2f(u, v1)
1024
+ rl.rl_vertex2f(x1, y1)
1025
+ rl.rl_tex_coord2f(u, v1)
1026
+ rl.rl_vertex2f(x2, y2)
1027
+ rl.rl_tex_coord2f(u, v0)
1028
+ rl.rl_vertex2f(x3, y3)
1029
+
1030
+ rl.rl_end()
1031
+ rl.rl_set_texture(0)
1032
+
1033
+ for creature in glow_targets:
1034
+ tx, ty = self.world_to_screen(float(creature.x), float(creature.y))
1035
+ target_tint = self._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
1036
+ self._draw_atlas_sprite(
1037
+ texture,
1038
+ grid=grid,
1039
+ frame=frame,
1040
+ x=tx,
1041
+ y=ty,
1042
+ scale=sprite_scale,
1043
+ rotation_rad=0.0,
1044
+ tint=target_tint,
1045
+ )
1046
+
1047
+ rl.end_blend_mode()
1048
+ return
1049
+
1050
+ if type_id == int(ProjectileTypeId.PULSE_GUN) and texture is not None:
1051
+ mapping = KNOWN_PROJ_FRAMES.get(type_id)
1052
+ if mapping is None:
1053
+ return
1054
+ grid, frame = mapping
1055
+ cell_w = float(texture.width) / float(grid)
1056
+
1057
+ if life >= 0.4:
1058
+ ox = float(getattr(proj, "origin_x", pos_x))
1059
+ oy = float(getattr(proj, "origin_y", pos_y))
1060
+ dist = math.hypot(pos_x - ox, pos_y - oy)
1061
+
1062
+ desired_size = dist * 0.16 * scale
1063
+ if desired_size <= 1e-3:
1064
+ return
1065
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1066
+ if sprite_scale <= 1e-6:
1067
+ return
1068
+
1069
+ tint = self._color_from_rgba((0.1, 0.6, 0.2, alpha * 0.7))
1070
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1071
+ self._draw_atlas_sprite(
1072
+ texture,
1073
+ grid=grid,
1074
+ frame=frame,
1075
+ x=sx,
1076
+ y=sy,
1077
+ scale=sprite_scale,
1078
+ rotation_rad=angle,
1079
+ tint=tint,
1080
+ )
1081
+ rl.end_blend_mode()
1082
+ return
1083
+
1084
+ fade = clamp(life * 2.5, 0.0, 1.0)
1085
+ fade_alpha = fade * alpha
1086
+ if fade_alpha <= 1e-3:
1087
+ return
1088
+
1089
+ desired_size = 56.0 * scale
1090
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1091
+ if sprite_scale <= 1e-6:
1092
+ return
1093
+
1094
+ tint = self._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
1095
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1096
+ self._draw_atlas_sprite(
1097
+ texture,
1098
+ grid=grid,
1099
+ frame=frame,
1100
+ x=sx,
1101
+ y=sy,
1102
+ scale=sprite_scale,
1103
+ rotation_rad=angle,
1104
+ tint=tint,
1105
+ )
1106
+ rl.end_blend_mode()
1107
+ return
1108
+
1109
+ if type_id in (int(ProjectileTypeId.SPLITTER_GUN), int(ProjectileTypeId.BLADE_GUN)) and texture is not None:
1110
+ mapping = KNOWN_PROJ_FRAMES.get(type_id)
1111
+ if mapping is None:
1112
+ return
1113
+ grid, frame = mapping
1114
+ cell_w = float(texture.width) / float(grid)
1115
+
1116
+ if life < 0.4:
1117
+ return
1118
+
1119
+ ox = float(getattr(proj, "origin_x", pos_x))
1120
+ oy = float(getattr(proj, "origin_y", pos_y))
1121
+ dist = math.hypot(pos_x - ox, pos_y - oy)
1122
+
1123
+ desired_size = min(dist, 20.0) * scale
1124
+ if desired_size <= 1e-3:
1125
+ return
1126
+
1127
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1128
+ if sprite_scale <= 1e-6:
1129
+ return
1130
+
1131
+ rotation_rad = angle
1132
+ rgb = (1.0, 1.0, 1.0)
1133
+ if type_id == int(ProjectileTypeId.BLADE_GUN):
1134
+ rotation_rad = float(int(proj_index)) * 0.1 - float(self._elapsed_ms) * 0.1
1135
+ rgb = (0.8, 0.8, 0.8)
1136
+
1137
+ tint = self._color_from_rgba((rgb[0], rgb[1], rgb[2], alpha))
1138
+ self._draw_atlas_sprite(
1139
+ texture,
1140
+ grid=grid,
1141
+ frame=frame,
1142
+ x=sx,
1143
+ y=sy,
1144
+ scale=sprite_scale,
1145
+ rotation_rad=rotation_rad,
1146
+ tint=tint,
1147
+ )
1148
+ return
1149
+
1150
+ if type_id == int(ProjectileTypeId.PLAGUE_SPREADER) and texture is not None:
1151
+ grid = 4
1152
+ frame = 2
1153
+ cell_w = float(texture.width) / float(grid)
1154
+
1155
+ if life >= 0.4:
1156
+ tint = self._color_from_rgba((1.0, 1.0, 1.0, alpha))
1157
+
1158
+ def draw_plague_quad(*, px: float, py: float, size: float) -> None:
1159
+ size = float(size)
1160
+ if size <= 1e-3:
1161
+ return
1162
+ desired_size = size * scale
1163
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1164
+ if sprite_scale <= 1e-6:
1165
+ return
1166
+ psx, psy = self.world_to_screen(px, py)
1039
1167
  self._draw_atlas_sprite(
1040
1168
  texture,
1041
1169
  grid=grid,
1042
1170
  frame=frame,
1043
- x=tx,
1044
- y=ty,
1171
+ x=psx,
1172
+ y=psy,
1045
1173
  scale=sprite_scale,
1046
1174
  rotation_rad=0.0,
1047
- tint=target_tint,
1175
+ tint=tint,
1048
1176
  )
1049
1177
 
1050
- rl.end_blend_mode()
1178
+ draw_plague_quad(px=pos_x, py=pos_y, size=60.0)
1179
+
1180
+ offset_angle = angle + math.pi / 2.0
1181
+ draw_plague_quad(
1182
+ px=pos_x + math.cos(offset_angle) * 15.0,
1183
+ py=pos_y + math.sin(offset_angle) * 15.0,
1184
+ size=60.0,
1185
+ )
1186
+
1187
+ phase = float(int(proj_index)) + float(self._elapsed_ms) * 0.01
1188
+ cos_phase = math.cos(phase)
1189
+ sin_phase = math.sin(phase)
1190
+ draw_plague_quad(
1191
+ px=pos_x + cos_phase * cos_phase - 5.0,
1192
+ py=pos_y + sin_phase * 11.0 - 5.0,
1193
+ size=52.0,
1194
+ )
1195
+
1196
+ phase_120 = phase + 2.0943952
1197
+ sin_phase_120 = math.sin(phase_120)
1198
+ draw_plague_quad(
1199
+ px=pos_x + math.cos(phase_120) * 10.0,
1200
+ py=pos_y + sin_phase_120 * 10.0,
1201
+ size=62.0,
1202
+ )
1203
+
1204
+ phase_240 = phase + 4.1887903
1205
+ draw_plague_quad(
1206
+ px=pos_x + math.cos(phase_240) * 10.0,
1207
+ py=pos_y + math.sin(phase_240) * sin_phase_120,
1208
+ size=62.0,
1209
+ )
1210
+ return
1211
+
1212
+ fade = clamp(life * 2.5, 0.0, 1.0)
1213
+ fade_alpha = fade * alpha
1214
+ if fade_alpha <= 1e-3:
1215
+ return
1216
+
1217
+ desired_size = (fade * 40.0 + 32.0) * scale
1218
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
1219
+ if sprite_scale <= 1e-6:
1220
+ return
1221
+
1222
+ tint = self._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
1223
+ self._draw_atlas_sprite(
1224
+ texture,
1225
+ grid=grid,
1226
+ frame=frame,
1227
+ x=sx,
1228
+ y=sy,
1229
+ scale=sprite_scale,
1230
+ rotation_rad=0.0,
1231
+ tint=tint,
1232
+ )
1051
1233
  return
1052
1234
 
1053
1235
  mapping = KNOWN_PROJ_FRAMES.get(type_id)
@@ -1160,21 +1342,236 @@ class WorldRenderer:
1160
1342
  rl.end_blend_mode()
1161
1343
  return True
1162
1344
 
1345
+ def _draw_sharpshooter_laser_sight(
1346
+ self,
1347
+ *,
1348
+ cam_x: float,
1349
+ cam_y: float,
1350
+ scale_x: float,
1351
+ scale_y: float,
1352
+ scale: float,
1353
+ alpha: float,
1354
+ ) -> None:
1355
+ """Laser sight overlay for the Sharpshooter perk (`projectile_render` @ 0x00422c70)."""
1356
+
1357
+ alpha = clamp(float(alpha), 0.0, 1.0)
1358
+ if alpha <= 1e-3:
1359
+ return
1360
+ if self.bullet_trail_texture is None:
1361
+ return
1362
+
1363
+ players = self.players
1364
+ if not players:
1365
+ return
1366
+
1367
+ tail_alpha = int(clamp(alpha * 0.5, 0.0, 1.0) * 255.0 + 0.5)
1368
+ head_alpha = int(clamp(alpha * 0.2, 0.0, 1.0) * 255.0 + 0.5)
1369
+ tail = rl.Color(255, 0, 0, tail_alpha)
1370
+ head = rl.Color(255, 0, 0, head_alpha)
1371
+
1372
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1373
+ rl.rl_set_texture(self.bullet_trail_texture.id)
1374
+ rl.rl_begin(rl.RL_QUADS)
1375
+
1376
+ for player in players:
1377
+ if float(getattr(player, "health", 0.0)) <= 0.0:
1378
+ continue
1379
+ if not perk_active(player, PerkId.SHARPSHOOTER):
1380
+ continue
1381
+
1382
+ aim_heading = float(getattr(player, "aim_heading", 0.0))
1383
+ dir_x = math.cos(aim_heading - math.pi / 2.0)
1384
+ dir_y = math.sin(aim_heading - math.pi / 2.0)
1385
+
1386
+ start_x = float(getattr(player, "pos_x", 0.0)) + dir_x * 15.0
1387
+ start_y = float(getattr(player, "pos_y", 0.0)) + dir_y * 15.0
1388
+ end_x = float(getattr(player, "pos_x", 0.0)) + dir_x * 512.0
1389
+ end_y = float(getattr(player, "pos_y", 0.0)) + dir_y * 512.0
1390
+
1391
+ sx0 = (start_x + cam_x) * scale_x
1392
+ sy0 = (start_y + cam_y) * scale_y
1393
+ sx1 = (end_x + cam_x) * scale_x
1394
+ sy1 = (end_y + cam_y) * scale_y
1395
+
1396
+ dx = sx1 - sx0
1397
+ dy = sy1 - sy0
1398
+ dist = math.hypot(dx, dy)
1399
+ if dist <= 1e-3:
1400
+ continue
1401
+
1402
+ thickness = max(1.0, 2.0 * scale)
1403
+ half = thickness * 0.5
1404
+ inv = 1.0 / dist
1405
+ nx = dx * inv
1406
+ ny = dy * inv
1407
+ px = -ny
1408
+ py = nx
1409
+ ox = px * half
1410
+ oy = py * half
1411
+
1412
+ x0 = sx0 - ox
1413
+ y0 = sy0 - oy
1414
+ x1 = sx0 + ox
1415
+ y1 = sy0 + oy
1416
+ x2 = sx1 + ox
1417
+ y2 = sy1 + oy
1418
+ x3 = sx1 - ox
1419
+ y3 = sy1 - oy
1420
+
1421
+ rl.rl_color4ub(tail.r, tail.g, tail.b, tail.a)
1422
+ rl.rl_tex_coord2f(0.0, 0.0)
1423
+ rl.rl_vertex2f(x0, y0)
1424
+ rl.rl_color4ub(tail.r, tail.g, tail.b, tail.a)
1425
+ rl.rl_tex_coord2f(1.0, 0.0)
1426
+ rl.rl_vertex2f(x1, y1)
1427
+ rl.rl_color4ub(head.r, head.g, head.b, head.a)
1428
+ rl.rl_tex_coord2f(1.0, 0.5)
1429
+ rl.rl_vertex2f(x2, y2)
1430
+ rl.rl_color4ub(head.r, head.g, head.b, head.a)
1431
+ rl.rl_tex_coord2f(0.0, 0.5)
1432
+ rl.rl_vertex2f(x3, y3)
1433
+
1434
+ rl.rl_end()
1435
+ rl.rl_set_texture(0)
1436
+ rl.end_blend_mode()
1437
+
1163
1438
  def _draw_secondary_projectile(self, proj: object, *, scale: float, alpha: float = 1.0) -> None:
1164
1439
  alpha = clamp(float(alpha), 0.0, 1.0)
1165
1440
  if alpha <= 1e-3:
1166
1441
  return
1167
1442
  sx, sy = self.world_to_screen(float(getattr(proj, "pos_x", 0.0)), float(getattr(proj, "pos_y", 0.0)))
1168
1443
  proj_type = int(getattr(proj, "type_id", 0))
1444
+ angle = float(getattr(proj, "angle", 0.0))
1445
+
1446
+ if proj_type in (1, 2, 4) and self.projs_texture is not None:
1447
+ texture = self.projs_texture
1448
+ cell_w = float(texture.width) / 4.0
1449
+ if cell_w <= 1e-6:
1450
+ return
1451
+
1452
+ base_alpha = clamp(alpha * 0.9, 0.0, 1.0)
1453
+ base_tint = self._color_from_rgba((0.8, 0.8, 0.8, base_alpha))
1454
+ base_size = 14.0
1455
+ if proj_type == 2:
1456
+ base_size = 10.0
1457
+ elif proj_type == 4:
1458
+ base_size = 8.0
1459
+ sprite_scale = (base_size * scale) / cell_w
1460
+
1461
+ fx_detail_1 = bool(self.config.data.get("fx_detail_1", 0)) if self.config is not None else True
1462
+ if fx_detail_1 and self.particles_texture is not None:
1463
+ particles_texture = self.particles_texture
1464
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
1465
+ if atlas is not None:
1466
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
1467
+ if grid:
1468
+ particle_cell_w = float(particles_texture.width) / float(grid)
1469
+ particle_cell_h = float(particles_texture.height) / float(grid)
1470
+ frame = int(atlas.frame)
1471
+ col = frame % grid
1472
+ row = frame // grid
1473
+ src = rl.Rectangle(
1474
+ particle_cell_w * float(col),
1475
+ particle_cell_h * float(row),
1476
+ max(0.0, particle_cell_w - 2.0),
1477
+ max(0.0, particle_cell_h - 2.0),
1478
+ )
1479
+
1480
+ dir_x = math.cos(angle - math.pi / 2.0)
1481
+ dir_y = math.sin(angle - math.pi / 2.0)
1482
+
1483
+ def _draw_rocket_fx(
1484
+ *,
1485
+ size: float,
1486
+ offset: float,
1487
+ rgba: tuple[float, float, float, float],
1488
+ ) -> None:
1489
+ fx_alpha = rgba[3]
1490
+ if fx_alpha <= 1e-3:
1491
+ return
1492
+ tint = self._color_from_rgba(rgba)
1493
+ fx_sx = sx - dir_x * offset * scale
1494
+ fx_sy = sy - dir_y * offset * scale
1495
+ dst_size = size * scale
1496
+ dst = rl.Rectangle(float(fx_sx), float(fx_sy), float(dst_size), float(dst_size))
1497
+ origin = rl.Vector2(dst_size * 0.5, dst_size * 0.5)
1498
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, tint)
1499
+
1500
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1501
+ # Large bloom around the rocket (effect_id=0x0D).
1502
+ _draw_rocket_fx(size=140.0, offset=5.0, rgba=(1.0, 1.0, 1.0, alpha * 0.48))
1503
+
1504
+ if proj_type == 4:
1505
+ _draw_rocket_fx(size=30.0, offset=9.0, rgba=(0.7, 0.7, 1.0, alpha * 0.158))
1506
+ elif proj_type == 2:
1507
+ _draw_rocket_fx(size=40.0, offset=9.0, rgba=(1.0, 1.0, 1.0, alpha * 0.58))
1508
+ else:
1509
+ _draw_rocket_fx(size=60.0, offset=9.0, rgba=(1.0, 1.0, 1.0, alpha * 0.68))
1510
+
1511
+ rl.end_blend_mode()
1512
+ self._draw_atlas_sprite(
1513
+ texture,
1514
+ grid=4,
1515
+ frame=3,
1516
+ x=sx,
1517
+ y=sy,
1518
+ scale=sprite_scale,
1519
+ rotation_rad=angle,
1520
+ tint=base_tint,
1521
+ )
1522
+ return
1523
+
1169
1524
  if proj_type == 4:
1170
1525
  rl.draw_circle(int(sx), int(sy), max(1.0, 12.0 * scale), rl.Color(200, 120, 255, int(255 * alpha + 0.5)))
1171
1526
  return
1172
1527
  if proj_type == 3:
1173
- t = clamp(float(getattr(proj, "lifetime", 0.0)), 0.0, 1.0)
1174
- radius = float(getattr(proj, "speed", 1.0)) * t * 80.0
1175
- alpha_byte = int(clamp((1.0 - t) * 180.0 * alpha, 0.0, 255.0) + 0.5)
1176
- color = rl.Color(200, 120, 255, alpha_byte)
1177
- rl.draw_circle_lines(int(sx), int(sy), max(1.0, radius * scale), color)
1528
+ # Secondary projectile detonation visuals (secondary_projectile_update + render).
1529
+ t = clamp(float(getattr(proj, "vel_x", 0.0)), 0.0, 1.0)
1530
+ det_scale = float(getattr(proj, "vel_y", 1.0))
1531
+ fade = (1.0 - t) * alpha
1532
+ if fade <= 1e-3 or det_scale <= 1e-6:
1533
+ return
1534
+ if self.particles_texture is None:
1535
+ radius = det_scale * t * 80.0
1536
+ alpha_byte = int(clamp((1.0 - t) * 180.0 * alpha, 0.0, 255.0) + 0.5)
1537
+ color = rl.Color(255, 180, 100, alpha_byte)
1538
+ rl.draw_circle_lines(int(sx), int(sy), max(1.0, radius * scale), color)
1539
+ return
1540
+
1541
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
1542
+ if atlas is None:
1543
+ return
1544
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
1545
+ if not grid:
1546
+ return
1547
+ frame = int(atlas.frame)
1548
+ col = frame % grid
1549
+ row = frame // grid
1550
+ cell_w = float(self.particles_texture.width) / float(grid)
1551
+ cell_h = float(self.particles_texture.height) / float(grid)
1552
+ src = rl.Rectangle(
1553
+ cell_w * float(col),
1554
+ cell_h * float(row),
1555
+ max(0.0, cell_w - 2.0),
1556
+ max(0.0, cell_h - 2.0),
1557
+ )
1558
+
1559
+ def _draw_detonation_quad(*, size: float, alpha_mul: float) -> None:
1560
+ a = fade * alpha_mul
1561
+ if a <= 1e-3:
1562
+ return
1563
+ dst_size = size * scale
1564
+ if dst_size <= 1e-3:
1565
+ return
1566
+ tint = self._color_from_rgba((1.0, 0.6, 0.1, a))
1567
+ dst = rl.Rectangle(float(sx), float(sy), float(dst_size), float(dst_size))
1568
+ origin = rl.Vector2(float(dst_size) * 0.5, float(dst_size) * 0.5)
1569
+ rl.draw_texture_pro(self.particles_texture, src, dst, origin, 0.0, tint)
1570
+
1571
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1572
+ _draw_detonation_quad(size=det_scale * t * 64.0, alpha_mul=1.0)
1573
+ _draw_detonation_quad(size=det_scale * t * 200.0, alpha_mul=0.3)
1574
+ rl.end_blend_mode()
1178
1575
  return
1179
1576
  rl.draw_circle(int(sx), int(sy), max(1.0, 4.0 * scale), rl.Color(200, 200, 220, int(200 * alpha + 0.5)))
1180
1577
 
@@ -1222,7 +1619,7 @@ class WorldRenderer:
1222
1619
  rl.begin_blend_mode(rl.BLEND_ADDITIVE)
1223
1620
 
1224
1621
  if fx_detail_1 and src_large is not None:
1225
- alpha_byte = int(clamp(alpha * 0.04, 0.0, 1.0) * 255.0 + 0.5)
1622
+ alpha_byte = int(clamp(alpha * 0.065, 0.0, 1.0) * 255.0 + 0.5)
1226
1623
  tint = rl.Color(255, 255, 255, alpha_byte)
1227
1624
  for idx, entry in enumerate(particles):
1228
1625
  if not entry.active or (idx % 2) or int(entry.style_id) == 8:
@@ -1627,10 +2024,19 @@ class WorldRenderer:
1627
2024
  if player.health > 0.0:
1628
2025
  draw_player(player)
1629
2026
 
1630
- for proj in self.state.projectiles.entries:
2027
+ self._draw_sharpshooter_laser_sight(
2028
+ cam_x=cam_x,
2029
+ cam_y=cam_y,
2030
+ scale_x=scale_x,
2031
+ scale_y=scale_y,
2032
+ scale=scale,
2033
+ alpha=entity_alpha,
2034
+ )
2035
+
2036
+ for proj_index, proj in enumerate(self.state.projectiles.entries):
1631
2037
  if not proj.active:
1632
2038
  continue
1633
- self._draw_projectile(proj, scale=scale, alpha=entity_alpha)
2039
+ self._draw_projectile(proj, proj_index=proj_index, scale=scale, alpha=entity_alpha)
1634
2040
 
1635
2041
  self._draw_particle_pool(cam_x=cam_x, cam_y=cam_y, scale_x=scale_x, scale_y=scale_y, alpha=entity_alpha)
1636
2042