nodes/magic.pyΒΆ

Part of Clear Code Zelda.

 1"""Heal and flame spells: wrappers that spawn particle effects + damage hits."""
 2
 3from __future__ import annotations
 4
 5from simvx.core import Node2D, Vec2
 6
 7from .particles import ParticleEffect
 8
 9
10class FlameProjectile(Node2D):
11    """Linear-flying flame; damages the first enemy whose AABB it overlaps.
12
13    Self-destructs on hit or after ``lifetime`` seconds.
14    """
15
16    def __init__(self, position: Vec2, direction: Vec2, frames: list[str], strength: float, **kwargs):
17        super().__init__(position=position, **kwargs)
18        self.direction = direction
19        self.speed = 460.0
20        self.strength = strength
21        self.lifetime = 0.9
22        self._frames = frames
23        self._consumed = False
24
25    def on_ready(self):
26        self.add_child(ParticleEffect(self._frames, Vec2(0.0, 0.0), fps=14.0, size=42))
27        # Looping flame: re-spawn the effect when it finishes
28        self._respawn_timer = 0.18
29
30    def on_update(self, dt: float):
31        if self._consumed:
32            return
33        self.lifetime -= dt
34        if self.lifetime <= 0:
35            self.destroy()
36            return
37        self.position += self.direction * (self.speed * dt)
38        # Periodically spawn a new flame puff for the looping effect
39        self._respawn_timer -= dt
40        if self._respawn_timer <= 0:
41            self.add_child(ParticleEffect(self._frames, Vec2(0.0, 0.0), fps=14.0, size=42))
42            self._respawn_timer = 0.18
43
44    def consume(self):
45        self._consumed = True
46        self.destroy()
47
48
49class MagicPlayer:
50    """Helper bound to the level: keeps spell logic out of the level body."""
51
52    def __init__(self, animation_library):
53        self._anims = animation_library
54
55    def heal(self, player, strength: float, cost: int, parent: Node2D) -> None:
56        if player.energy < cost:
57            return
58        if player.hp >= player.max_hp:
59            return
60        player.energy = max(0.0, player.energy - cost)
61        player.hp = min(player.max_hp, player.hp + strength)
62        parent.add_child(
63            ParticleEffect(self._anims.get("heal"), Vec2(player.position.x, player.position.y - 60), fps=14.0, size=72)
64        )
65        parent.add_child(
66            ParticleEffect(self._anims.get("aura"), Vec2(player.position.x, player.position.y), fps=14.0, size=80)
67        )
68
69    def flame(self, player, strength: float, cost: int, parent: Node2D) -> FlameProjectile | None:
70        if player.energy < cost:
71            return None
72        player.energy = max(0.0, player.energy - cost)
73        face = player.facing_vector()
74        if face.x == 0 and face.y == 0:
75            face = Vec2(0.0, 1.0)
76        spawn = Vec2(player.position.x + face.x * 30, player.position.y + face.y * 30)
77        proj = FlameProjectile(spawn, face, self._anims.get("flame"), strength)
78        parent.add_child(proj)
79        return proj