nodes/grass.pyΒΆ
Part of HeartBeast Action RPG.
1"""Grass patches: destructible world decoration.
2
3Mirrors the upstream HeartBeast Grass scene: a tuft with a hurtbox that the
4player's sword can cut. Cutting it emits ``destroyed`` (the World turns that
5into a burst of leaves) and removes the node.
6"""
7
8from __future__ import annotations
9
10from settings import COLOUR_GRASS_BLADE, COLOUR_SHADOW, GRASS_HURTBOX_RADIUS
11
12from simvx.core import Node2D, Signal, Vec2
13
14
15class Grass(Node2D):
16 """Destructible grass tuft."""
17
18 destroyed = Signal() # (position: Vec2)
19
20 def __init__(self, position: Vec2 | None = None, **kwargs):
21 super().__init__(position=position if position is not None else Vec2(0.0, 0.0), **kwargs)
22 self._alive = True
23
24 @property
25 def is_alive(self) -> bool:
26 """False once the tuft has been cut."""
27 return self._alive
28
29 def on_draw(self, renderer):
30 if not self._alive:
31 return
32 px, py = self.position
33 renderer.draw_circle((px, py + 1), 4.0, colour=COLOUR_SHADOW, filled=True)
34 blades = [
35 [(px - 5, py + 1), (px - 3, py - 8), (px - 1, py + 1)],
36 [(px - 1, py + 1), (px + 0.5, py - 11), (px + 2, py + 1)],
37 [(px + 2, py + 1), (px + 4.5, py - 7), (px + 6, py + 1)],
38 ]
39 for blade in blades:
40 renderer.draw_polygon(blade, colour=COLOUR_GRASS_BLADE)
41
42 def take_damage(self) -> bool:
43 """Cut this tuft down. Returns True if it was still standing."""
44 if not self._alive:
45 return False
46 self._alive = False
47 # ``on_draw`` reads a plain attribute, so the retained 2D cache is told
48 # by hand that this node's output changed before the node goes away.
49 self.queue_redraw()
50 self.destroyed.emit(self.position)
51 self.destroy()
52 return True
53
54 def get_hurtbox(self) -> tuple[Vec2, float]:
55 """``(centre, radius)`` of the tuft's hurtbox."""
56 return (self.position, GRASS_HURTBOX_RADIUS)