nodes/projectile.pyΒΆ

Part of SNKRX.

  1"""Player projectiles: pierce shots from archers, AOE bursts from mages.
  2
  3Projectiles are short-lived ``Node2D`` instances that walk a velocity vector
  4and emit trail particles. They flag themselves ``alive=False`` when out of
  5range or after collision; the arena destroys them in its post-update sweep.
  6"""
  7
  8from __future__ import annotations
  9
 10from simvx.core import Node2D, Vec2
 11
 12from .colours import BLUE, FG, GREEN
 13
 14
 15class Projectile(Node2D):
 16    def __init__(
 17        self,
 18        *,
 19        kind: str,
 20        position: Vec2,
 21        velocity: Vec2,
 22        damage: float,
 23        max_distance: float,
 24        radius: float = 4.0,
 25        pierce: int = 1,
 26        aoe_radius: float = 0.0,
 27        colour: tuple[float, float, float, float] = FG,
 28        **kwargs,
 29    ):
 30        super().__init__(name="Projectile", position=Vec2(position), **kwargs)
 31        self.kind = kind
 32        self.velocity = Vec2(velocity)
 33        self.damage = damage
 34        self.max_distance = max_distance
 35        self.radius = radius
 36        self.pierce_left = pierce
 37        self.aoe_radius = aoe_radius
 38        self.colour = colour
 39        self._travelled = 0.0
 40        self._hit_set: set[int] = set()
 41        self.alive = True
 42
 43    def update(self, dt: float):
 44        if not self.alive or dt <= 0:
 45            return
 46        step = self.velocity * dt
 47        self.position = self.position + step
 48        self._travelled += step.length()
 49        if self._travelled >= self.max_distance:
 50            self.alive = False
 51
 52    def on_draw(self, renderer):
 53        if not self.alive:
 54            return
 55        # Draw the projectile head
 56        renderer.draw_circle(
 57            (self.position.x, self.position.y),
 58            self.radius + 1.0,
 59            colour=(1.0, 1.0, 1.0, 0.45),
 60            filled=True,
 61            segments=12,
 62        )
 63        renderer.draw_circle(
 64            (self.position.x, self.position.y),
 65            self.radius,
 66            colour=self.colour,
 67            filled=True,
 68            segments=12,
 69        )
 70        if self.aoe_radius > 0:
 71            renderer.draw_circle(
 72                (self.position.x, self.position.y),
 73                self.aoe_radius,
 74                colour=(self.colour[0], self.colour[1], self.colour[2], 0.18),
 75                filled=False,
 76                segments=18,
 77            )
 78
 79
 80def archer_arrow(position: Vec2, direction: Vec2, damage: float) -> Projectile:
 81    return Projectile(
 82        kind="arrow",
 83        position=position,
 84        velocity=direction.normalized() * 520.0,
 85        damage=damage,
 86        max_distance=520.0,
 87        radius=3.5,
 88        pierce=3,
 89        colour=GREEN,
 90    )
 91
 92
 93def mage_bolt(position: Vec2, direction: Vec2, damage: float) -> Projectile:
 94    return Projectile(
 95        kind="bolt",
 96        position=position,
 97        velocity=direction.normalized() * 360.0,
 98        damage=damage,
 99        max_distance=360.0,
100        radius=5.0,
101        pierce=1,
102        aoe_radius=42.0,
103        colour=BLUE,
104    )