Basic static/moving circle collisions. (Fixes issue #5)
[python-bulletml.git] / bulletml / impl.py
1 """BulletML implementation."""
2
3 from __future__ import division
4
5 import math
6
7 from bulletml import parser
8
9 PI_2 = math.pi * 2
10
11 __all__ = ["Action", "Bullet"]
12
13 class Action(object):
14 """Running action implementation.
15
16 To implement new actions,
17
18 - Add a new element/class pair to parser.ActionDef.CONSTRUCTORS.
19 It should support FromXML, __getstate__, and __setstate__.
20 - Subclass impl.Action and override the 'handle' method to handle
21 your custom action type.
22 - Pass the impl.Bullet constructor your Action subclass when
23 creating your root Bullet.
24 """
25
26 def __init__(self, owner, parent, actions, params, rank, repeat=1):
27 self.actions = actions
28 self.parent = parent
29 self.repeat = repeat
30 self.wait_frames = 0
31 self.speed = 0
32 self.speed_frames = 0
33 self.direction = 0
34 self.direction_frames = 0
35 self.aiming = False
36 self.mx = 0
37 self.my = 0
38 self.accel_frames = 0
39 self.previous_fire_direction = 0
40 self.previous_fire_speed = 0
41 self.params = params
42 self.pc = -1
43 self.finished = False
44 if parent:
45 self.copy_state(parent)
46
47 def __repr__(self):
48 return "%s(pc=%r, actions=%r)" % (
49 type(self).__name__, self.pc, self.actions)
50
51 def vanish(self):
52 """End this action and its parents."""
53 if self.parent:
54 self.parent.vanish()
55 self.pc = None
56 self.finished = True
57
58 def copy_state(self, other):
59 """Copy fire/movement state from other to self."""
60 self.direction_frames = other.direction_frames
61 self.direction = other.direction
62 self.aiming = other.aiming
63 self.speed_frames = other.speed_frames
64 self.speed = other.speed
65 self.accel_frames = other.accel_frames
66 self.mx = other.mx
67 self.my = other.my
68 self.previous_fire_direction = other.previous_fire_direction
69 self.previous_fire_speed = other.previous_fire_speed
70
71 def step(self, owner, created, sin=math.sin, cos=math.cos):
72 """Advance by one frame."""
73 s_params = self.params
74 rank = owner.rank
75
76 if self.speed_frames > 0:
77 self.speed_frames -= 1
78 owner.speed += self.speed
79
80 if self.direction_frames > 0:
81 # The Noiz implementation was a little weird here, I think
82 # there was a bug in it that prevented it from working if
83 # the frame count was 1. I'm still not sure what the aim
84 # check is supposed to do, exactly.
85 self.direction_frames -= 1
86 if self.aiming and self.direction_frames <= 0:
87 owner.direction += owner.aim
88 else:
89 owner.direction += self.direction
90
91 if self.accel_frames > 0:
92 self.accel_frames -= 1
93 owner.mx += self.mx
94 owner.my += self.my
95
96 if self.pc is None:
97 return
98
99 if self.wait_frames > 0:
100 self.wait_frames -= 1
101 return
102
103 while True:
104 self.pc += 1
105
106 try:
107 action = self.actions[self.pc]
108 except IndexError:
109 self.repeat -= 1
110 if self.repeat <= 0:
111 self.pc = None
112 self.finished = True
113 if self.parent is not None:
114 self.parent.copy_state(self)
115 owner.replace(self, self.parent)
116 break
117 else:
118 self.pc = 0
119 action = self.actions[self.pc]
120
121 if isinstance(action, parser.Repeat):
122 repeat, (actions, params) = action(s_params, rank)
123 child = self.__class__(
124 owner, self, actions, params, rank, repeat)
125 owner.replace(self, child)
126 child.step(owner, created, sin, cos)
127 break
128
129 elif isinstance(action, (parser.ActionDef, parser.ActionRef)):
130 actions, params = action(s_params, rank)
131 child = self.__class__(owner, self, actions, params, rank)
132 owner.replace(self, child)
133 child.step(owner, created, sin, cos)
134 break
135
136 elif isinstance(action, (parser.FireDef, parser.FireRef)):
137 direction, speed, offset, tags, actions = action(s_params, rank)
138 if direction:
139 direction, type = direction
140 if type == "aim" or type is None:
141 direction += owner.aim
142 elif type == "sequence":
143 direction += self.previous_fire_direction
144 elif type == "relative":
145 direction += owner.direction
146 else:
147 direction = owner.aim
148 self.previous_fire_direction = direction
149
150 if speed:
151 speed, type = speed
152 if type == "sequence":
153 speed += self.previous_fire_speed
154 elif type == "relative":
155 # The reference Noiz implementation uses
156 # prvFireSpeed here, but the standard is
157 # pretty clear -- "In case of the type is
158 # "relative", ... the speed is relative to the
159 # speed of this bullet."
160 speed += owner.speed
161 else:
162 speed = 1
163 self.previous_fire_speed = speed
164
165 x, y = owner.x, owner.y
166 if offset:
167 off_x, off_y = offset(s_params, rank)
168 if offset.type == "relative":
169 s = sin(direction)
170 c = cos(direction)
171 x += c * off_x + s * off_y
172 y += s * off_x - c * off_y
173 else:
174 x += off_x
175 y += off_y
176
177 bullet = owner.__class__(
178 x=x, y=y, direction=direction, speed=speed,
179 target=owner.target, actions=actions, rank=rank,
180 tags=tags, Action=self.__class__)
181 created.append(bullet)
182
183 elif isinstance(action, parser.ChangeSpeed):
184 frames, (speed, type) = action(s_params, rank)
185 self.speed_frames = frames
186 if frames <= 0:
187 if type == "absolute":
188 owner.speed = speed
189 elif type == "relative":
190 owner.speed += speed
191 elif type == "sequence":
192 self.speed = speed
193 elif type == "relative":
194 self.speed = speed / frames
195 else:
196 self.speed = (speed - owner.speed) / frames
197
198 elif isinstance(action, parser.ChangeDirection):
199 frames, (direction, type) = action(s_params, rank)
200 self.direction_frames = frames
201 self.aiming = False
202 if type == "sequence":
203 self.direction = direction
204 else:
205 if type == "absolute":
206 direction -= owner.direction
207 elif type != "relative": # aim or default
208 self.aiming = True
209 direction += owner.aim - owner.direction
210
211 # Normalize to [-pi, pi).
212 direction = (direction + math.pi) % PI_2 - math.pi
213 if frames <= 0:
214 owner.direction += direction
215 else:
216 self.direction = direction / frames
217
218 elif isinstance(action, parser.Accel):
219 frames, horizontal, vertical = action(s_params, rank)
220 self.accel_frames = frames
221 if horizontal:
222 mx, type = horizontal
223 if frames <= 0:
224 if type == "absolute":
225 owner.mx = mx
226 elif type == "relative":
227 owner.mx += mx
228 elif type == "sequence":
229 self.mx = mx
230 elif type == "absolute":
231 self.mx = (mx - owner.mx) / frames
232 elif type == "relative":
233 self.mx = mx / frames
234 if vertical:
235 my, type = vertical
236 if frames <= 0:
237 if type == "absolute":
238 owner.my = my
239 elif type == "relative":
240 owner.my += my
241 elif type == "sequence":
242 self.my = my
243 elif type == "absolute":
244 self.my = (my - owner.my) / frames
245 elif type == "relative":
246 self.my = my / frames
247
248 elif isinstance(action, parser.Tag):
249 owner.tags.add(action.tag)
250
251 elif isinstance(action, parser.Untag):
252 try:
253 owner.tags.remove(action.tag)
254 except KeyError:
255 pass
256
257 elif isinstance(action, parser.Wait):
258 self.wait_frames = action(s_params, rank)
259 break
260
261 elif isinstance(action, parser.Vanish):
262 owner.vanish()
263 break
264
265 else:
266 self.handle(action, owner, created, sin, cos)
267
268 def handle(self, action, owner, created, sin, cos):
269 """Override in subclasses for new action types."""
270 raise NotImplementedError(action.__class__.__name__)
271
272 class Bullet(object):
273 """Simple bullet implementation.
274
275 Attributes:
276 x, y - current X/Y position
277 px, py - X/Y position prior to the last step
278 mx, my - X/Y axis-oriented speed modifier ("acceleration")
279 direction - direction of movement, in radians
280 speed - speed of movement, in units per frame
281 target - object with .x and .y fields for "aim" directions
282 vanished - set to true by a <vanish> action
283 rank - game difficulty, 0 to 1, default 0.5
284 tags - string tags set by the running actions
285
286 Contructor Arguments:
287 x, y, direction, speed, target, rank - same as the attributes
288 actions - internal action list
289 Action - custom Action constructor
290
291
292 """
293
294 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
295 actions=(), rank=0.5, tags=(), Action=Action):
296 self.x = self.px = x
297 self.y = self.py = y
298 self.mx = 0
299 self.my = 0
300 self.direction = direction
301 self.speed = speed
302 self.vanished = False
303 self.target = target
304 self.rank = rank
305 self.tags = set(tags)
306 # New bullets reset the parent hierarchy.
307 self._actions = [Action(self, None, action, params, rank)
308 for action, params in actions]
309
310 @classmethod
311 def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None,
312 params=(), rank=0.5, Action=Action):
313 """Construct a new Bullet from a loaded BulletML document."""
314 actions = [a(params, rank) for a in doc.actions]
315 return cls(x=x, y=y, direction=direction, speed=speed,
316 target=target, actions=actions, rank=rank, Action=Action)
317
318 def __repr__(self):
319 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
320 "actions=%r, target=%r, vanished=%r)") % (
321 type(self).__name__, self.x, self.y, (self.mx, self.my),
322 self.direction, self.speed, self._actions, self.target,
323 self.vanished)
324
325 @property
326 def aim(self):
327 """Angle to the target, in radians.
328
329 If the target does not exist or cannot be found, return 0.
330 """
331 try:
332 target_x = self.target.x
333 target_y = self.target.y
334 except AttributeError:
335 return 0
336 else:
337 return math.atan2(target_x - self.x, self.y - target_y)
338
339 @property
340 def finished(self):
341 """Check if this bullet is finished running.
342
343 A bullet is finished when it has vanished, and all its
344 actions have finished.
345
346 If this is true, the bullet should be removed from the screen.
347 (You will probably want to cull it under other circumstances
348 as well).
349 """
350 if not self.vanished:
351 return False
352 for action in self._actions:
353 if not action.finished:
354 return False
355 return True
356
357 def vanish(self):
358 """Vanish this bullet and stop all actions."""
359 self.vanished = True
360 for action in self._actions:
361 action.vanish()
362 self._actions = []
363
364 def replace(self, old, new):
365 """Replace an active action with another.
366
367 This is mostly used by actions internally to queue children.
368 """
369 try:
370 idx = self._actions.index(old)
371 except ValueError:
372 pass
373 else:
374 self._actions[idx] = new
375
376 def step(self, sin=math.sin, cos=math.cos):
377 """Advance by one frame.
378
379 This updates the position and velocity, and may also set the
380 vanished flag.
381
382 It returns any new bullets this bullet spawned during this step.
383 """
384 created = []
385
386 for action in self._actions:
387 action.step(self, created, sin, cos)
388
389 self.px = self.x
390 self.py = self.y
391 self.x += self.mx + sin(self.direction) * self.speed
392 self.y += self.my - cos(self.direction) * self.speed
393
394 return created