nodes/enemy.pyΒΆ
Part of Mr. Rescue.
1"""FireBug: single enemy type (port collapses upstream's 6 to one).
2
3Walks left/right on a floor, spawns fires periodically. Knockback on hit.
4"""
5
6from __future__ import annotations
7
8import random
9
10from simvx.core import Node2D, Signal, Sprite2D, Vec2
11
12from . import textures
13from .tile_grid import TILE_SIZE, TileGrid
14
15EN_RUN = 0
16EN_HIT = 1
17EN_RECOVER = 2
18
19MOVE_SPEED = 60.0
20GRAVITY = 350.0
21MAX_HEALTH = 1.3
22SCORE = 100
23RECOVER_TIME = 0.7
24FIRE_SPAWN_MIN = 8.0
25FIRE_SPAWN_MAX = 22.0
26
27ENEMY_W = 12
28ENEMY_H = 22
29
30
31class FireBug(Node2D):
32 killed = Signal(object) # this FireBug, doused by the water stream
33 fire_spawned = Signal(Vec2) # world position of the flame it just lit
34
35 def __init__(self, *, position: Vec2, **kwargs):
36 super().__init__(position=position, **kwargs)
37 self.alive = True
38 self.health = MAX_HEALTH
39 self.dir = random.choice((-1, 1))
40 self.xspeed = 0.0
41 self.yspeed = 0.0
42 self.state = EN_RUN
43 self._recover_t = 0.0
44 self._next_fire = random.uniform(FIRE_SPAWN_MIN, FIRE_SPAWN_MAX)
45
46 self._frames = textures.get("enemy")
47 self._anim_t = 0.0
48 self._sprite = Sprite2D(
49 texture=self._frames[0],
50 width=16,
51 height=22,
52 filter="nearest",
53 )
54 self.add_child(self._sprite)
55
56 self._grid: TileGrid | None = None
57 self._fires = None
58
59 def set_world(self, *, grid, fires):
60 self._grid = grid
61 self._fires = fires
62
63 def aabb(self):
64 return (self.position.x - ENEMY_W / 2, self.position.y - ENEMY_H, ENEMY_W, ENEMY_H)
65
66 def shoot(self, dt: float, dir_: int) -> bool:
67 """Apply water damage. Returns True if the bug died this hit."""
68 self.health -= dt * 1.5
69 self.dir = -dir_
70 self.state = EN_HIT
71 self._recover_t = 0.15
72 if self.health <= 0:
73 self.alive = False
74 self.killed.emit(self)
75 self.destroy()
76 return True
77 return False
78
79 def on_update(self, dt: float):
80 if not self.alive or dt <= 0:
81 return
82 self._anim_t += dt
83
84 if self.state == EN_RUN:
85 self._update_run(dt)
86 elif self.state == EN_HIT:
87 self._recover_t -= dt
88 self.yspeed += GRAVITY * dt
89 self._move_y(dt)
90 if self._recover_t <= 0:
91 self.state = EN_RECOVER
92 self._recover_t = RECOVER_TIME
93 elif self.state == EN_RECOVER:
94 self._recover_t -= dt
95 self.yspeed += GRAVITY * dt
96 self._move_y(dt)
97 if self._recover_t <= 0:
98 self.state = EN_RUN
99
100 # Fire-spawn timer (only while running).
101 if self.state == EN_RUN and self._fires is not None and self._grid is not None:
102 self._next_fire -= dt
103 if self._next_fire <= 0:
104 cx, cy = self._grid.cell_for(self.position.x, self.position.y - 4)
105 if self._fires.spawn(cx, cy):
106 self.fire_spawned.emit(Vec2(cx * TILE_SIZE + 8, cy * TILE_SIZE + 8))
107 self._next_fire = random.uniform(FIRE_SPAWN_MIN, FIRE_SPAWN_MAX)
108
109 self._update_visual()
110
111 def _update_run(self, dt: float):
112 self.xspeed = self.dir * MOVE_SPEED
113 self.yspeed += GRAVITY * dt
114 if self._grid is None:
115 return
116 # X
117 nx = self.position.x + self.xspeed * dt
118 old = self.position
119 self.position = Vec2(nx, self.position.y)
120 x, y, w, h = self.aabb()
121 if self._grid.collides_box(x, y, w, h):
122 self.position = old
123 self.dir *= -1
124 # Y
125 self._move_y(dt)
126 # Edge sense: flip if no floor 1 tile ahead.
127 ahead_x = self.position.x + self.dir * (ENEMY_W / 2 + 2)
128 cx_ahead = int(ahead_x // TILE_SIZE)
129 cy_below = int((self.position.y + 2) // TILE_SIZE)
130 if not self._grid.is_solid(cx_ahead, cy_below):
131 self.dir *= -1
132
133 def _move_y(self, dt: float):
134 self.position = Vec2(self.position.x, self.position.y + self.yspeed * dt)
135 x, y, w, h = self.aabb()
136 if self._grid is not None and self._grid.collides_box(x, y, w, h):
137 if self.yspeed > 0:
138 cy = int((self.position.y - 1) // TILE_SIZE)
139 self.position = Vec2(self.position.x, cy * TILE_SIZE)
140 self.yspeed = 0
141
142 def _update_visual(self):
143 idx = int(self._anim_t * 6) % 4
144 self._sprite.texture = self._frames[idx]
145 self._sprite.scale = Vec2(self.dir, 1)
146 if self.state == EN_HIT:
147 self._sprite.colour = (1.0, 1.0, 1.0, 0.6)
148 else:
149 self._sprite.colour = (1.0, 1.0, 1.0, 1.0)