source: nagslang/enemies.py @ 307:c2bbb1e70d6f

Last change on this file since 307:c2bbb1e70d6f was 307:c2bbb1e70d6f, checked in by Neil Muller <drnlmuller@…>, 7 years ago

Rename animate to update and pass seconds, for future fun

File size: 7.3 KB
Line 
1import math
2
3import pymunk
4import pymunk.pygame_util
5
6from nagslang import render
7from nagslang.constants import COLLISION_TYPE_ENEMY, ZORDER_MID
8from nagslang.game_object import GameObject, SingleShapePhysicser, make_body
9from nagslang.mutators import FLIP_H
10from nagslang.resources import resources
11
12
13def get_editable_enemies():
14    classes = []
15    for cls_name, cls in globals().iteritems():
16        if isinstance(cls, type) and issubclass(cls, Enemy):
17            if hasattr(cls, 'requires'):
18                classes.append((cls_name, cls))
19    return classes
20
21
22class Enemy(GameObject):
23    """A base class for mobile enemies"""
24
25    def __init__(self, space, world, position):
26        self._setup_physics(space, position)
27        self._setup_renderer()
28        self.health = 42
29
30        super(Enemy, self).__init__(
31            self._physicser, self.renderer)
32        self.zorder = ZORDER_MID
33        self.world = world
34
35    def _get_image(self, name, *transforms):
36        return resources.get_image('creatures', name, transforms=transforms)
37
38    def _setup_physics(self, space, position):
39        raise NotImplementedError
40
41    def _setup_renderer(self):
42        raise NotImplementedError
43
44    def attack(self):
45        raise NotImplementedError
46
47    @classmethod
48    def requires(cls):
49        return [("name", "string"), ("position", "coordinates")]
50
51    def lose_health(self, amount):
52        self.health -= amount
53        if self.health < 0:
54            self.world.kills += 1
55            self.physicser.remove_from_space()
56            self.remove = True
57
58
59class PatrollingAlien(Enemy):
60    is_moving = True  # Always walking.
61
62    def __init__(self, space, world, position, end_position):
63        # An enemy that patrols between the two points
64        super(PatrollingAlien, self).__init__(space, world, position)
65        self._start_pos = position
66        self._end_pos = end_position
67        self._direction = 'away'
68
69    def _setup_physics(self, space, position):
70        self._body = make_body(10, pymunk.inf, position, 0.8)
71
72        self._shape = pymunk.Circle(self._body, 30)
73
74        self._shape.elasticity = 1.0
75        self._shape.friction = 0.05
76        self._shape.collision_type = COLLISION_TYPE_ENEMY
77        self._physicser = SingleShapePhysicser(space, self._shape)
78        self.impulse_factor = 50
79        self.angle = 0
80
81    def _setup_renderer(self):
82        self.renderer = render.FacingSelectionRenderer({
83            'left': render.TimedAnimatedRenderer(
84                [self._get_image('alien_A_1.png'),
85                 self._get_image('alien_A_2.png')], 3),
86            'right': render.TimedAnimatedRenderer(
87                [self._get_image('alien_A_1.png', FLIP_H),
88                 self._get_image('alien_A_2.png', FLIP_H)], 3),
89        })
90
91    def get_render_angle(self):
92        # No image rotation when rendering, please.
93        return 0
94
95    def get_facing_direction(self):
96        # Enemies can face left or right.
97        if - math.pi / 2 < self.angle <= math.pi / 2:
98            return 'right'
99        else:
100            return 'left'
101
102    def _switch_direction(self):
103        if self._direction == 'away':
104            self._direction = 'towards'
105        else:
106            self._direction = 'away'
107
108    def set_direction(self, dx, dy):
109        self.angle = pymunk.Vec2d((dx, dy)).angle
110        self._body.apply_impulse(
111            (dx * self.impulse_factor, dy * self.impulse_factor))
112
113    def update(self, seconds):
114        # Calculate the step every frame
115        if self._direction == 'away':
116            target = self._end_pos
117        else:
118            target = self._start_pos
119        x_step = 0
120        y_step = 0
121        if (target[0] < self._body.position[0]):
122            x_step = max(-1, target[0] - self._body.position[0])
123        elif (target[0] > self._body.position[0]):
124            x_step = min(1, target[0] - self._body.position[0])
125        if abs(x_step) < 0.5:
126            x_step = 0
127        if (target[1] < self._body.position[1]):
128            y_step = max(-1, target[1] - self._body.position[1])
129        elif (target[1] > self._body.position[1]):
130            y_step = min(1, target[1] - self._body.position[1])
131        if abs(y_step) < 0.5:
132            y_step = 0
133        if abs(x_step) < 1 and abs(y_step) < 1:
134            self._switch_direction()
135        self.set_direction(x_step, y_step)
136        super(PatrollingAlien, self).update(seconds)
137
138    def collide_with_protagonist(self, protagonist):
139        protagonist.lose_health(15)
140
141    def hit(self, weapon):
142        self.lose_health(weapon.damage)
143
144    @classmethod
145    def requires(cls):
146        return [("name", "string"), ("position", "coordinates"),
147                ("end_position", "coordinates")]
148
149
150class ChargingAlien(Enemy):
151    # Simplistic charging of the protagonist
152    is_moving = False
153
154    def __init__(self, space, world, position, attack_range=100):
155        super(ChargingAlien, self).__init__(space, world, position)
156        self._range = attack_range
157
158    def _setup_physics(self, space, position):
159        self._body = make_body(100, pymunk.inf, position, 0.8)
160
161        self._shape = pymunk.Circle(self._body, 30)
162
163        self._shape.elasticity = 1.0
164        self._shape.friction = 0.05
165        self._shape.collision_type = COLLISION_TYPE_ENEMY
166        self._physicser = SingleShapePhysicser(space, self._shape)
167        self.impulse_factor = 300
168        self.angle = 0
169
170    def _setup_renderer(self):
171        self.renderer = render.FacingSelectionRenderer({
172            'left': render.TimedAnimatedRenderer(
173                [self._get_image('alien_B_1.png'),
174                 self._get_image('alien_B_2.png')], 3),
175            'right': render.TimedAnimatedRenderer(
176                [self._get_image('alien_B_1.png', FLIP_H),
177                 self._get_image('alien_B_2.png', FLIP_H)], 3),
178        })
179
180    def get_render_angle(self):
181        # No image rotation when rendering, please.
182        return 0
183
184    def get_facing_direction(self):
185        # Enemies can face left or right.
186        if - math.pi / 2 < self.angle <= math.pi / 2:
187            return 'right'
188        else:
189            return 'left'
190
191    def set_direction(self, dx, dy):
192        self.angle = pymunk.Vec2d((dx, dy)).angle
193        self._body.apply_impulse(
194            (dx * self.impulse_factor, dy * self.impulse_factor))
195
196    def update(self, seconds):
197        # Calculate the step every frame
198        # Distance to the protagonist
199        pos = self._body.position
200        target = self.world.protagonist.get_shape().body.position
201        if pos.get_distance(target) > self._range:
202            # stop
203            self.is_moving = False
204            return
205        self.is_moving = True
206        x_step = 0
207        y_step = 0
208        if (target[0] < pos[0]):
209            x_step = max(-1, target[0] - pos[0])
210        elif (target[0] > pos[0]):
211            x_step = min(1, target[0] - pos[0])
212        if abs(x_step) < 0.5:
213            x_step = 0
214        if (target[1] < pos[1]):
215            y_step = max(-1, target[1] - pos[1])
216        elif (target[1] > pos[1]):
217            y_step = min(1, target[1] - pos[1])
218        if abs(y_step) < 0.5:
219            y_step = 0
220        self.set_direction(x_step, y_step)
221        super(ChargingAlien, self).update(seconds)
222
223    def collide_with_protagonist(self, protagonist):
224        protagonist.lose_health(20)
225
226    @classmethod
227    def requires(cls):
228        return [("name", "string"), ("position", "coordinates"),
229                ("attack_range", "distance")]
Note: See TracBrowser for help on using the repository browser.