nodes/effects.pyΒΆ
Part of HeartBeast Action RPG.
1"""Short-lived visual effects: sword hit, bat death, cut grass.
2
3Each effect is a Node2D that draws a fading burst and calls ``destroy()`` once
4its timer runs out. They animate from a plain timer rather than from Property
5state, so each sets ``dynamic`` to keep the retained 2D cache re-collecting it
6every frame (without that, an effect would render one frozen frame).
7"""
8
9from __future__ import annotations
10
11import math
12import random
13
14from settings import DEATH_EFFECT_DURATION, GRASS_EFFECT_DURATION, HIT_EFFECT_DURATION
15
16from simvx.core import Node2D, Vec2
17
18
19def _burst(count: int, min_speed: float, max_speed: float) -> list[tuple[float, float]]:
20 """``count`` velocities pointing evenly-ish outwards, in px/sec."""
21 velocities = []
22 for _ in range(count):
23 angle = random.uniform(0, math.tau)
24 speed = random.uniform(min_speed, max_speed)
25 velocities.append((math.cos(angle) * speed, math.sin(angle) * speed))
26 return velocities
27
28
29class _TimedEffect(Node2D):
30 """Base for the fading bursts: owns the lifetime and the self-destruct."""
31
32 dynamic = True
33 duration = 0.3
34
35 def __init__(self, position: Vec2, **kwargs):
36 super().__init__(position=position, **kwargs)
37 self._timer = 0.0
38
39 def on_update(self, dt: float):
40 self._timer += dt
41 if self._timer >= self.duration:
42 self.destroy()
43
44 @property
45 def progress(self) -> float:
46 """Lifetime elapsed, 0 at spawn to 1 at expiry."""
47 return min(1.0, self._timer / self.duration)
48
49
50class HitEffect(_TimedEffect):
51 """White flash and expanding ring where the sword landed."""
52
53 duration = HIT_EFFECT_DURATION
54
55 def on_draw(self, renderer):
56 t = self.progress
57 alpha = 1.0 - t
58 size = 5.0 + t * 8.0
59 renderer.draw_circle(self.position, size, colour=(1.0, 1.0, 1.0, alpha * 0.8), filled=True)
60 renderer.draw_circle(self.position, size * 1.6, colour=(1.0, 0.9, 0.7, alpha * 0.5), filled=False)
61
62
63class EnemyDeathEffect(_TimedEffect):
64 """Outward spray of embers where a bat died."""
65
66 duration = DEATH_EFFECT_DURATION
67
68 def __init__(self, position: Vec2, **kwargs):
69 super().__init__(position, **kwargs)
70 self._particles = _burst(8, 30, 80)
71
72 def on_draw(self, renderer):
73 t = self.progress
74 for dx, dy in self._particles:
75 centre = Vec2(self.position[0] + dx * self._timer, self.position[1] + dy * self._timer)
76 renderer.draw_circle(centre, 2.0 * (1.0 - t * 0.5), colour=(0.9, 0.5, 0.3, 1.0 - t), filled=True)
77
78
79class GrassEffect(_TimedEffect):
80 """Small scatter of leaves where a tuft was cut."""
81
82 duration = GRASS_EFFECT_DURATION
83
84 def __init__(self, position: Vec2, **kwargs):
85 super().__init__(position, **kwargs)
86 self._particles = _burst(5, 15, 40)
87
88 def on_draw(self, renderer):
89 t = self.progress
90 for dx, dy in self._particles:
91 centre = Vec2(self.position[0] + dx * self._timer, self.position[1] + dy * self._timer)
92 renderer.draw_circle(centre, 1.6 * (1.0 - t * 0.5), colour=(0.4, 0.7, 0.2, 1.0 - t), filled=True)