crimsonland 0.1.0.dev15__py3-none-any.whl → 0.1.0.dev16__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. crimson/cli.py +61 -0
  2. crimson/creatures/damage.py +111 -36
  3. crimson/creatures/runtime.py +246 -156
  4. crimson/creatures/spawn.py +7 -3
  5. crimson/demo.py +38 -45
  6. crimson/effects.py +7 -13
  7. crimson/frontend/high_scores_layout.py +81 -0
  8. crimson/frontend/panels/base.py +4 -1
  9. crimson/frontend/panels/controls.py +0 -15
  10. crimson/frontend/panels/databases.py +291 -3
  11. crimson/frontend/panels/mods.py +0 -15
  12. crimson/frontend/panels/play_game.py +0 -16
  13. crimson/game.py +441 -1
  14. crimson/gameplay.py +905 -569
  15. crimson/modes/base_gameplay_mode.py +33 -12
  16. crimson/modes/components/__init__.py +2 -0
  17. crimson/modes/components/highscore_record_builder.py +58 -0
  18. crimson/modes/components/perk_menu_controller.py +325 -0
  19. crimson/modes/quest_mode.py +58 -273
  20. crimson/modes/rush_mode.py +12 -43
  21. crimson/modes/survival_mode.py +71 -328
  22. crimson/modes/tutorial_mode.py +46 -247
  23. crimson/modes/typo_mode.py +11 -38
  24. crimson/oracle.py +396 -0
  25. crimson/perks.py +5 -2
  26. crimson/player_damage.py +94 -37
  27. crimson/projectiles.py +539 -320
  28. crimson/render/projectile_draw_registry.py +637 -0
  29. crimson/render/projectile_render_registry.py +110 -0
  30. crimson/render/secondary_projectile_draw_registry.py +206 -0
  31. crimson/render/world_renderer.py +58 -707
  32. crimson/sim/world_state.py +118 -61
  33. crimson/typo/spawns.py +5 -12
  34. crimson/ui/demo_trial_overlay.py +3 -11
  35. crimson/ui/formatting.py +24 -0
  36. crimson/ui/game_over.py +12 -58
  37. crimson/ui/hud.py +72 -39
  38. crimson/ui/layout.py +20 -0
  39. crimson/ui/perk_menu.py +9 -34
  40. crimson/ui/quest_results.py +12 -64
  41. crimson/ui/text_input.py +20 -0
  42. crimson/views/_ui_helpers.py +27 -0
  43. crimson/views/aim_debug.py +15 -32
  44. crimson/views/animations.py +18 -28
  45. crimson/views/arsenal_debug.py +22 -32
  46. crimson/views/bonuses.py +23 -36
  47. crimson/views/camera_debug.py +16 -29
  48. crimson/views/camera_shake.py +9 -33
  49. crimson/views/corpse_stamp_debug.py +13 -21
  50. crimson/views/decals_debug.py +36 -23
  51. crimson/views/fonts.py +8 -25
  52. crimson/views/ground.py +4 -21
  53. crimson/views/lighting_debug.py +42 -45
  54. crimson/views/particles.py +33 -42
  55. crimson/views/perk_menu_debug.py +3 -10
  56. crimson/views/player.py +50 -44
  57. crimson/views/player_sprite_debug.py +24 -31
  58. crimson/views/projectile_fx.py +57 -52
  59. crimson/views/projectile_render_debug.py +24 -33
  60. crimson/views/projectiles.py +24 -37
  61. crimson/views/spawn_plan.py +13 -29
  62. crimson/views/sprites.py +14 -29
  63. crimson/views/terrain.py +6 -23
  64. crimson/views/ui.py +7 -24
  65. crimson/views/wicons.py +28 -33
  66. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/METADATA +1 -1
  67. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/RECORD +72 -64
  68. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/WHEEL +2 -2
  69. grim/config.py +29 -1
  70. grim/console.py +7 -10
  71. grim/math.py +12 -0
  72. crimson/.DS_Store +0 -0
  73. crimson/creatures/.DS_Store +0 -0
  74. grim/.DS_Store +0 -0
  75. {crimsonland-0.1.0.dev15.dist-info → crimsonland-0.1.0.dev16.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,637 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import math
5
+ from typing import TYPE_CHECKING
6
+
7
+ import pyray as rl
8
+
9
+ from grim.math import clamp
10
+
11
+ from ..effects_atlas import EFFECT_ID_ATLAS_TABLE_BY_ID, SIZE_CODE_GRID
12
+ from ..gameplay import perk_active
13
+ from ..perks import PerkId
14
+ from ..projectiles import ProjectileTypeId
15
+ from ..sim.world_defs import BEAM_TYPES, ION_TYPES, KNOWN_PROJ_FRAMES, PLASMA_PARTICLE_TYPES
16
+ from .projectile_render_registry import beam_effect_scale, plasma_projectile_render_config
17
+
18
+ if TYPE_CHECKING:
19
+ from .world_renderer import WorldRenderer
20
+
21
+ _RAD_TO_DEG = 57.29577951308232
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class ProjectileDrawCtx:
26
+ renderer: WorldRenderer
27
+ proj: object
28
+ proj_index: int
29
+ texture: rl.Texture | None
30
+ type_id: int
31
+ pos_x: float
32
+ pos_y: float
33
+ sx: float
34
+ sy: float
35
+ life: float
36
+ angle: float
37
+ scale: float
38
+ alpha: float
39
+
40
+
41
+ def _draw_bullet_trail(ctx: ProjectileDrawCtx) -> bool:
42
+ renderer = ctx.renderer
43
+ type_id = int(ctx.type_id)
44
+ if not renderer._is_bullet_trail_type(type_id):
45
+ return False
46
+
47
+ life_alpha = int(clamp(float(ctx.life), 0.0, 1.0) * 255.0)
48
+ alpha_byte = int(clamp(float(life_alpha) * float(ctx.alpha), 0.0, 255.0) + 0.5)
49
+ drawn = False
50
+
51
+ if renderer.bullet_trail_texture is not None:
52
+ ox = float(getattr(ctx.proj, "origin_x", ctx.pos_x))
53
+ oy = float(getattr(ctx.proj, "origin_y", ctx.pos_y))
54
+ sx0, sy0 = renderer.world_to_screen(ox, oy)
55
+ drawn = renderer._draw_bullet_trail(sx0, sy0, ctx.sx, ctx.sy, type_id=type_id, alpha=alpha_byte, scale=ctx.scale)
56
+
57
+ if renderer.bullet_texture is not None and float(ctx.life) >= 0.39:
58
+ size = renderer._bullet_sprite_size(type_id, scale=ctx.scale)
59
+ src = rl.Rectangle(0.0, 0.0, float(renderer.bullet_texture.width), float(renderer.bullet_texture.height))
60
+ dst = rl.Rectangle(float(ctx.sx), float(ctx.sy), float(size), float(size))
61
+ origin = rl.Vector2(float(size) * 0.5, float(size) * 0.5)
62
+ tint = rl.Color(220, 220, 220, int(alpha_byte))
63
+ rl.draw_texture_pro(renderer.bullet_texture, src, dst, origin, float(ctx.angle) * _RAD_TO_DEG, tint)
64
+ drawn = True
65
+
66
+ return bool(drawn)
67
+
68
+
69
+ def _draw_plasma_particles(ctx: ProjectileDrawCtx) -> bool:
70
+ renderer = ctx.renderer
71
+ type_id = int(ctx.type_id)
72
+ if type_id not in PLASMA_PARTICLE_TYPES:
73
+ return False
74
+ if renderer.particles_texture is None:
75
+ return False
76
+
77
+ particles_texture = renderer.particles_texture
78
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
79
+ if atlas is None:
80
+ return False
81
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
82
+ if not grid:
83
+ return False
84
+
85
+ cell_w = float(particles_texture.width) / float(grid)
86
+ cell_h = float(particles_texture.height) / float(grid)
87
+ frame = int(atlas.frame)
88
+ col = frame % grid
89
+ row = frame // grid
90
+ src = rl.Rectangle(
91
+ cell_w * float(col),
92
+ cell_h * float(row),
93
+ max(0.0, cell_w - 2.0),
94
+ max(0.0, cell_h - 2.0),
95
+ )
96
+
97
+ speed_scale = float(getattr(ctx.proj, "speed_scale", 1.0))
98
+ fx_detail_1 = bool(renderer.config.data.get("fx_detail_1", 0)) if renderer.config is not None else True
99
+
100
+ plasma_cfg = plasma_projectile_render_config(type_id)
101
+ rgb = plasma_cfg.rgb
102
+ spacing = plasma_cfg.spacing
103
+ seg_limit = plasma_cfg.seg_limit
104
+ tail_size = plasma_cfg.tail_size
105
+ head_size = plasma_cfg.head_size
106
+ head_alpha_mul = plasma_cfg.head_alpha_mul
107
+ aura_rgb = plasma_cfg.aura_rgb
108
+ aura_size = plasma_cfg.aura_size
109
+ aura_alpha_mul = plasma_cfg.aura_alpha_mul
110
+
111
+ if float(ctx.life) >= 0.4:
112
+ # Reconstruct the tail length heuristic used by the native render path.
113
+ seg_count = int(float(getattr(ctx.proj, "base_damage", 0.0)))
114
+ if seg_count < 0:
115
+ seg_count = 0
116
+ seg_count //= 5
117
+ if seg_count > int(seg_limit):
118
+ seg_count = int(seg_limit)
119
+
120
+ # The stored projectile angle is rotated by +pi/2 vs travel direction.
121
+ dir_x = math.cos(float(ctx.angle) + math.pi / 2.0) * speed_scale
122
+ dir_y = math.sin(float(ctx.angle) + math.pi / 2.0) * speed_scale
123
+
124
+ alpha = float(ctx.alpha)
125
+ tail_tint = renderer._color_from_rgba((rgb[0], rgb[1], rgb[2], alpha * 0.4))
126
+ head_tint = renderer._color_from_rgba((rgb[0], rgb[1], rgb[2], alpha * head_alpha_mul))
127
+ aura_tint = renderer._color_from_rgba((aura_rgb[0], aura_rgb[1], aura_rgb[2], alpha * aura_alpha_mul))
128
+
129
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
130
+
131
+ if seg_count > 0:
132
+ size = float(tail_size) * float(ctx.scale)
133
+ origin = rl.Vector2(size * 0.5, size * 0.5)
134
+ step_x = dir_x * float(spacing)
135
+ step_y = dir_y * float(spacing)
136
+ for idx in range(int(seg_count)):
137
+ px = float(ctx.pos_x) + float(idx) * step_x
138
+ py = float(ctx.pos_y) + float(idx) * step_y
139
+ psx, psy = renderer.world_to_screen(px, py)
140
+ dst = rl.Rectangle(float(psx), float(psy), float(size), float(size))
141
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, tail_tint)
142
+
143
+ size = float(head_size) * float(ctx.scale)
144
+ origin = rl.Vector2(size * 0.5, size * 0.5)
145
+ dst = rl.Rectangle(float(ctx.sx), float(ctx.sy), float(size), float(size))
146
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, head_tint)
147
+
148
+ if fx_detail_1:
149
+ size = float(aura_size) * float(ctx.scale)
150
+ origin = rl.Vector2(size * 0.5, size * 0.5)
151
+ dst = rl.Rectangle(float(ctx.sx), float(ctx.sy), float(size), float(size))
152
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, aura_tint)
153
+
154
+ rl.end_blend_mode()
155
+ return True
156
+
157
+ fade = clamp(float(ctx.life) * 2.5, 0.0, 1.0)
158
+ fade_alpha = fade * float(ctx.alpha)
159
+ if fade_alpha > 1e-3:
160
+ tint = renderer._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
161
+ size = 56.0 * float(ctx.scale)
162
+ dst = rl.Rectangle(float(ctx.sx), float(ctx.sy), float(size), float(size))
163
+ origin = rl.Vector2(size * 0.5, size * 0.5)
164
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
165
+ rl.draw_texture_pro(particles_texture, src, dst, origin, 0.0, tint)
166
+ rl.end_blend_mode()
167
+
168
+ return True
169
+
170
+
171
+ def _draw_beam_effect(ctx: ProjectileDrawCtx) -> bool:
172
+ renderer = ctx.renderer
173
+ type_id = int(ctx.type_id)
174
+ texture = ctx.texture
175
+ if type_id not in BEAM_TYPES:
176
+ return False
177
+ if texture is None:
178
+ return False
179
+
180
+ # Ion weapons and Fire Bullets use the projs.png streak effect (and Ion adds chain arcs on impact).
181
+ grid = 4
182
+ frame = 2
183
+
184
+ is_fire_bullets = type_id == int(ProjectileTypeId.FIRE_BULLETS)
185
+ is_ion = type_id in ION_TYPES
186
+
187
+ ox = float(getattr(ctx.proj, "origin_x", ctx.pos_x))
188
+ oy = float(getattr(ctx.proj, "origin_y", ctx.pos_y))
189
+ dx = float(ctx.pos_x) - ox
190
+ dy = float(ctx.pos_y) - oy
191
+ dist = math.hypot(dx, dy)
192
+ if dist <= 1e-6:
193
+ return True
194
+
195
+ dir_x = dx / dist
196
+ dir_y = dy / dist
197
+
198
+ # In the native renderer, Ion Gun Master increases the chain effect thickness and reach.
199
+ perk_scale = 1.0
200
+ if any(perk_active(player, PerkId.ION_GUN_MASTER) for player in renderer.players):
201
+ perk_scale = 1.2
202
+
203
+ effect_scale = beam_effect_scale(type_id)
204
+
205
+ alpha = float(ctx.alpha)
206
+ life = float(ctx.life)
207
+ if life >= 0.4:
208
+ base_alpha = alpha
209
+ else:
210
+ fade = clamp(life * 2.5, 0.0, 1.0)
211
+ base_alpha = fade * alpha
212
+
213
+ if base_alpha <= 1e-3:
214
+ return True
215
+
216
+ streak_rgb = (1.0, 0.6, 0.1) if is_fire_bullets else (0.5, 0.6, 1.0)
217
+ head_rgb = (1.0, 1.0, 0.7)
218
+
219
+ # Only draw the last 256 units of the path.
220
+ start = 0.0
221
+ span = dist
222
+ if dist > 256.0:
223
+ start = dist - 256.0
224
+ span = 256.0
225
+
226
+ step = min(effect_scale * 3.1, 9.0)
227
+ sprite_scale = effect_scale * float(ctx.scale)
228
+
229
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
230
+
231
+ s = start
232
+ while s < dist:
233
+ t = (s - start) / span if span > 1e-6 else 1.0
234
+ seg_alpha = t * base_alpha
235
+ if seg_alpha > 1e-3:
236
+ px = ox + dir_x * s
237
+ py = oy + dir_y * s
238
+ psx, psy = renderer.world_to_screen(px, py)
239
+ tint = renderer._color_from_rgba((streak_rgb[0], streak_rgb[1], streak_rgb[2], seg_alpha))
240
+ renderer._draw_atlas_sprite(
241
+ texture,
242
+ grid=grid,
243
+ frame=frame,
244
+ x=psx,
245
+ y=psy,
246
+ scale=sprite_scale,
247
+ rotation_rad=float(ctx.angle),
248
+ tint=tint,
249
+ )
250
+ s += step
251
+
252
+ if life >= 0.4:
253
+ head_tint = renderer._color_from_rgba((head_rgb[0], head_rgb[1], head_rgb[2], base_alpha))
254
+ renderer._draw_atlas_sprite(
255
+ texture,
256
+ grid=grid,
257
+ frame=frame,
258
+ x=float(ctx.sx),
259
+ y=float(ctx.sy),
260
+ scale=sprite_scale,
261
+ rotation_rad=float(ctx.angle),
262
+ tint=head_tint,
263
+ )
264
+
265
+ # Fire Bullets renders an extra particles.png overlay in a later pass.
266
+ if is_fire_bullets and renderer.particles_texture is not None:
267
+ particles_texture = renderer.particles_texture
268
+ atlas = EFFECT_ID_ATLAS_TABLE_BY_ID.get(0x0D)
269
+ if atlas is not None:
270
+ grid = SIZE_CODE_GRID.get(int(atlas.size_code))
271
+ if grid:
272
+ cell_w = float(particles_texture.width) / float(grid)
273
+ cell_h = float(particles_texture.height) / float(grid)
274
+ frame = int(atlas.frame)
275
+ col = frame % grid
276
+ row = frame // grid
277
+ src = rl.Rectangle(
278
+ cell_w * float(col),
279
+ cell_h * float(row),
280
+ max(0.0, cell_w - 2.0),
281
+ max(0.0, cell_h - 2.0),
282
+ )
283
+ tint = renderer._color_from_rgba((1.0, 1.0, 1.0, alpha))
284
+ size = 64.0 * float(ctx.scale)
285
+ dst = rl.Rectangle(float(ctx.sx), float(ctx.sy), float(size), float(size))
286
+ origin = rl.Vector2(size * 0.5, size * 0.5)
287
+ rl.draw_texture_pro(particles_texture, src, dst, origin, float(ctx.angle) * _RAD_TO_DEG, tint)
288
+ else:
289
+ # Native draws a small blue "core" at the head during the fade stage (life_timer < 0.4).
290
+ core_tint = renderer._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
291
+ renderer._draw_atlas_sprite(
292
+ texture,
293
+ grid=grid,
294
+ frame=frame,
295
+ x=float(ctx.sx),
296
+ y=float(ctx.sy),
297
+ scale=1.0 * float(ctx.scale),
298
+ rotation_rad=float(ctx.angle),
299
+ tint=core_tint,
300
+ )
301
+
302
+ if is_ion:
303
+ # Native: chain reach is derived from the streak scale (`fVar29 * perk_scale * 40.0`).
304
+ radius = effect_scale * perk_scale * 40.0
305
+
306
+ # Native iterates via creature_find_in_radius(pos, radius, start_index) in pool order.
307
+ targets: list[object] = []
308
+ for creature in renderer.creatures.entries[1:]:
309
+ if not creature.active:
310
+ continue
311
+ if float(getattr(creature, "hitbox_size", 0.0)) <= 5.0:
312
+ continue
313
+ d = math.hypot(float(creature.x) - float(ctx.pos_x), float(creature.y) - float(ctx.pos_y))
314
+ threshold = float(creature.size) * 0.14285715 + 3.0
315
+ if d - radius < threshold:
316
+ targets.append(creature)
317
+
318
+ inner_half = 10.0 * perk_scale * float(ctx.scale)
319
+ outer_half = 14.0 * perk_scale * float(ctx.scale)
320
+ u = 0.625
321
+ v0 = 0.0
322
+ v1 = 0.25
323
+
324
+ glow_targets: list[object] = []
325
+ rl.rl_set_texture(texture.id)
326
+ rl.rl_begin(rl.RL_QUADS)
327
+
328
+ for creature in targets:
329
+ tx, ty = renderer.world_to_screen(float(creature.x), float(creature.y))
330
+ ddx = float(tx) - float(ctx.sx)
331
+ ddy = float(ty) - float(ctx.sy)
332
+ dlen = math.hypot(ddx, ddy)
333
+ if dlen <= 1e-3:
334
+ continue
335
+ glow_targets.append(creature)
336
+ inv = 1.0 / dlen
337
+ nx = ddx * inv
338
+ ny = ddy * inv
339
+ px = -ny
340
+ py = nx
341
+
342
+ # Outer strip (softer).
343
+ half = outer_half
344
+ off_x = px * half
345
+ off_y = py * half
346
+ x0 = float(ctx.sx) - off_x
347
+ y0 = float(ctx.sy) - off_y
348
+ x1 = float(ctx.sx) + off_x
349
+ y1 = float(ctx.sy) + off_y
350
+ x2 = float(tx) + off_x
351
+ y2 = float(ty) + off_y
352
+ x3 = float(tx) - off_x
353
+ y3 = float(ty) - off_y
354
+
355
+ outer_tint = renderer._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
356
+ rl.rl_color4ub(outer_tint.r, outer_tint.g, outer_tint.b, outer_tint.a)
357
+ rl.rl_tex_coord2f(u, v0)
358
+ rl.rl_vertex2f(x0, y0)
359
+ rl.rl_tex_coord2f(u, v1)
360
+ rl.rl_vertex2f(x1, y1)
361
+ rl.rl_tex_coord2f(u, v1)
362
+ rl.rl_vertex2f(x2, y2)
363
+ rl.rl_tex_coord2f(u, v0)
364
+ rl.rl_vertex2f(x3, y3)
365
+
366
+ # Inner strip (brighter).
367
+ half = inner_half
368
+ off_x = px * half
369
+ off_y = py * half
370
+ x0 = float(ctx.sx) - off_x
371
+ y0 = float(ctx.sy) - off_y
372
+ x1 = float(ctx.sx) + off_x
373
+ y1 = float(ctx.sy) + off_y
374
+ x2 = float(tx) + off_x
375
+ y2 = float(ty) + off_y
376
+ x3 = float(tx) - off_x
377
+ y3 = float(ty) - off_y
378
+
379
+ inner_tint = renderer._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
380
+ rl.rl_color4ub(inner_tint.r, inner_tint.g, inner_tint.b, inner_tint.a)
381
+ rl.rl_tex_coord2f(u, v0)
382
+ rl.rl_vertex2f(x0, y0)
383
+ rl.rl_tex_coord2f(u, v1)
384
+ rl.rl_vertex2f(x1, y1)
385
+ rl.rl_tex_coord2f(u, v1)
386
+ rl.rl_vertex2f(x2, y2)
387
+ rl.rl_tex_coord2f(u, v0)
388
+ rl.rl_vertex2f(x3, y3)
389
+
390
+ rl.rl_end()
391
+ rl.rl_set_texture(0)
392
+
393
+ for creature in glow_targets:
394
+ tx, ty = renderer.world_to_screen(float(creature.x), float(creature.y))
395
+ target_tint = renderer._color_from_rgba((0.5, 0.6, 1.0, base_alpha))
396
+ renderer._draw_atlas_sprite(
397
+ texture,
398
+ grid=grid,
399
+ frame=frame,
400
+ x=float(tx),
401
+ y=float(ty),
402
+ scale=sprite_scale,
403
+ rotation_rad=0.0,
404
+ tint=target_tint,
405
+ )
406
+
407
+ rl.end_blend_mode()
408
+ return True
409
+
410
+
411
+ def _draw_pulse_gun(ctx: ProjectileDrawCtx) -> bool:
412
+ renderer = ctx.renderer
413
+ if int(ctx.type_id) != int(ProjectileTypeId.PULSE_GUN):
414
+ return False
415
+ if ctx.texture is None:
416
+ return False
417
+
418
+ mapping = KNOWN_PROJ_FRAMES.get(int(ctx.type_id))
419
+ if mapping is None:
420
+ return True
421
+ grid, frame = mapping
422
+ cell_w = float(ctx.texture.width) / float(grid)
423
+
424
+ alpha = float(ctx.alpha)
425
+ life = float(ctx.life)
426
+ if life >= 0.4:
427
+ ox = float(getattr(ctx.proj, "origin_x", ctx.pos_x))
428
+ oy = float(getattr(ctx.proj, "origin_y", ctx.pos_y))
429
+ dist = math.hypot(float(ctx.pos_x) - ox, float(ctx.pos_y) - oy)
430
+
431
+ desired_size = dist * 0.16 * float(ctx.scale)
432
+ if desired_size <= 1e-3:
433
+ return True
434
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
435
+ if sprite_scale <= 1e-6:
436
+ return True
437
+
438
+ tint = renderer._color_from_rgba((0.1, 0.6, 0.2, alpha * 0.7))
439
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
440
+ renderer._draw_atlas_sprite(
441
+ ctx.texture,
442
+ grid=grid,
443
+ frame=frame,
444
+ x=float(ctx.sx),
445
+ y=float(ctx.sy),
446
+ scale=sprite_scale,
447
+ rotation_rad=float(ctx.angle),
448
+ tint=tint,
449
+ )
450
+ rl.end_blend_mode()
451
+ return True
452
+
453
+ fade = clamp(life * 2.5, 0.0, 1.0)
454
+ fade_alpha = fade * alpha
455
+ if fade_alpha <= 1e-3:
456
+ return True
457
+
458
+ desired_size = 56.0 * float(ctx.scale)
459
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
460
+ if sprite_scale <= 1e-6:
461
+ return True
462
+
463
+ tint = renderer._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
464
+ rl.begin_blend_mode(rl.BLEND_ADDITIVE)
465
+ renderer._draw_atlas_sprite(
466
+ ctx.texture,
467
+ grid=grid,
468
+ frame=frame,
469
+ x=float(ctx.sx),
470
+ y=float(ctx.sy),
471
+ scale=sprite_scale,
472
+ rotation_rad=float(ctx.angle),
473
+ tint=tint,
474
+ )
475
+ rl.end_blend_mode()
476
+ return True
477
+
478
+
479
+ def _draw_splitter_or_blade(ctx: ProjectileDrawCtx) -> bool:
480
+ renderer = ctx.renderer
481
+ type_id = int(ctx.type_id)
482
+ if type_id not in (int(ProjectileTypeId.SPLITTER_GUN), int(ProjectileTypeId.BLADE_GUN)):
483
+ return False
484
+ if ctx.texture is None:
485
+ return False
486
+
487
+ mapping = KNOWN_PROJ_FRAMES.get(type_id)
488
+ if mapping is None:
489
+ return True
490
+ grid, frame = mapping
491
+ cell_w = float(ctx.texture.width) / float(grid)
492
+
493
+ if float(ctx.life) < 0.4:
494
+ return True
495
+
496
+ ox = float(getattr(ctx.proj, "origin_x", ctx.pos_x))
497
+ oy = float(getattr(ctx.proj, "origin_y", ctx.pos_y))
498
+ dist = math.hypot(float(ctx.pos_x) - ox, float(ctx.pos_y) - oy)
499
+
500
+ desired_size = min(dist, 20.0) * float(ctx.scale)
501
+ if desired_size <= 1e-3:
502
+ return True
503
+
504
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
505
+ if sprite_scale <= 1e-6:
506
+ return True
507
+
508
+ rotation_rad = float(ctx.angle)
509
+ rgb = (1.0, 1.0, 1.0)
510
+ if type_id == int(ProjectileTypeId.BLADE_GUN):
511
+ rotation_rad = float(int(ctx.proj_index)) * 0.1 - float(renderer._elapsed_ms) * 0.1
512
+ rgb = (0.8, 0.8, 0.8)
513
+
514
+ tint = renderer._color_from_rgba((rgb[0], rgb[1], rgb[2], float(ctx.alpha)))
515
+ renderer._draw_atlas_sprite(
516
+ ctx.texture,
517
+ grid=grid,
518
+ frame=frame,
519
+ x=float(ctx.sx),
520
+ y=float(ctx.sy),
521
+ scale=sprite_scale,
522
+ rotation_rad=rotation_rad,
523
+ tint=tint,
524
+ )
525
+ return True
526
+
527
+
528
+ def _draw_plague_spreader(ctx: ProjectileDrawCtx) -> bool:
529
+ renderer = ctx.renderer
530
+ if int(ctx.type_id) != int(ProjectileTypeId.PLAGUE_SPREADER):
531
+ return False
532
+ if ctx.texture is None:
533
+ return False
534
+
535
+ grid = 4
536
+ frame = 2
537
+ cell_w = float(ctx.texture.width) / float(grid)
538
+
539
+ alpha = float(ctx.alpha)
540
+ life = float(ctx.life)
541
+ if life >= 0.4:
542
+ tint = renderer._color_from_rgba((1.0, 1.0, 1.0, alpha))
543
+
544
+ def draw_plague_quad(*, px: float, py: float, size: float) -> None:
545
+ size = float(size)
546
+ if size <= 1e-3:
547
+ return
548
+ desired_size = size * float(ctx.scale)
549
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
550
+ if sprite_scale <= 1e-6:
551
+ return
552
+ psx, psy = renderer.world_to_screen(float(px), float(py))
553
+ renderer._draw_atlas_sprite(
554
+ ctx.texture,
555
+ grid=grid,
556
+ frame=frame,
557
+ x=float(psx),
558
+ y=float(psy),
559
+ scale=sprite_scale,
560
+ rotation_rad=0.0,
561
+ tint=tint,
562
+ )
563
+
564
+ draw_plague_quad(px=float(ctx.pos_x), py=float(ctx.pos_y), size=60.0)
565
+
566
+ offset_angle = float(ctx.angle) + math.pi / 2.0
567
+ draw_plague_quad(
568
+ px=float(ctx.pos_x) + math.cos(offset_angle) * 15.0,
569
+ py=float(ctx.pos_y) + math.sin(offset_angle) * 15.0,
570
+ size=60.0,
571
+ )
572
+
573
+ phase = float(int(ctx.proj_index)) + float(renderer._elapsed_ms) * 0.01
574
+ cos_phase = math.cos(phase)
575
+ sin_phase = math.sin(phase)
576
+ draw_plague_quad(
577
+ px=float(ctx.pos_x) + cos_phase * cos_phase - 5.0,
578
+ py=float(ctx.pos_y) + sin_phase * 11.0 - 5.0,
579
+ size=52.0,
580
+ )
581
+
582
+ phase_120 = phase + 2.0943952
583
+ sin_phase_120 = math.sin(phase_120)
584
+ draw_plague_quad(
585
+ px=float(ctx.pos_x) + math.cos(phase_120) * 10.0,
586
+ py=float(ctx.pos_y) + sin_phase_120 * 10.0,
587
+ size=62.0,
588
+ )
589
+
590
+ phase_240 = phase + 4.1887903
591
+ draw_plague_quad(
592
+ px=float(ctx.pos_x) + math.cos(phase_240) * 10.0,
593
+ py=float(ctx.pos_y) + math.sin(phase_240) * sin_phase_120,
594
+ size=62.0,
595
+ )
596
+ return True
597
+
598
+ fade = clamp(life * 2.5, 0.0, 1.0)
599
+ fade_alpha = fade * alpha
600
+ if fade_alpha <= 1e-3:
601
+ return True
602
+
603
+ desired_size = (fade * 40.0 + 32.0) * float(ctx.scale)
604
+ sprite_scale = desired_size / cell_w if cell_w > 1e-6 else 0.0
605
+ if sprite_scale <= 1e-6:
606
+ return True
607
+
608
+ tint = renderer._color_from_rgba((1.0, 1.0, 1.0, fade_alpha))
609
+ renderer._draw_atlas_sprite(
610
+ ctx.texture,
611
+ grid=grid,
612
+ frame=frame,
613
+ x=float(ctx.sx),
614
+ y=float(ctx.sy),
615
+ scale=sprite_scale,
616
+ rotation_rad=0.0,
617
+ tint=tint,
618
+ )
619
+ return True
620
+
621
+
622
+ PROJECTILE_DRAW_HANDLERS = (
623
+ _draw_bullet_trail,
624
+ _draw_plasma_particles,
625
+ _draw_beam_effect,
626
+ _draw_pulse_gun,
627
+ _draw_splitter_or_blade,
628
+ _draw_plague_spreader,
629
+ )
630
+
631
+
632
+ def draw_projectile_from_registry(ctx: ProjectileDrawCtx) -> bool:
633
+ for handler in PROJECTILE_DRAW_HANDLERS:
634
+ if handler(ctx):
635
+ return True
636
+ return False
637
+