Bullet.FromDocument: Abstract weird constructor handling.
[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, actions, offset = 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, y, direction, speed, owner.target, actions, rank)
179 created.append(bullet)
180
181 elif isinstance(action, parser.ChangeSpeed):
182 frames, (speed, type) = action(s_params, rank)
183 self.speed_frames = frames
184 if frames <= 0:
185 if type == "absolute":
186 owner.speed = speed
187 elif type == "relative":
188 owner.speed += speed
189 elif type == "sequence":
190 self.speed = speed
191 elif type == "relative":
192 self.speed = speed / frames
193 else:
194 self.speed = (speed - owner.speed) / frames
195
196 elif isinstance(action, parser.ChangeDirection):
197 frames, (direction, type) = action(s_params, rank)
198 self.direction_frames = frames
199 self.aiming = False
200 if type == "sequence":
201 self.direction = direction
202 else:
203 if type == "absolute":
204 direction -= owner.direction
205 elif type != "relative": # aim or default
206 self.aiming = True
207 direction += owner.aim - owner.direction
208
209 # Normalize to [-pi, pi).
210 direction = (direction + math.pi) % PI_2 - math.pi
211 if frames <= 0:
212 owner.direction += direction
213 else:
214 self.direction = direction / frames
215
216 elif isinstance(action, parser.Accel):
217 frames, horizontal, vertical = action(s_params, rank)
218 self.accel_frames = frames
219 if horizontal:
220 mx, type = horizontal
221 if frames <= 0:
222 if type == "absolute":
223 owner.mx = mx
224 elif type == "relative":
225 owner.mx += mx
226 elif type == "sequence":
227 self.mx = mx
228 elif type == "absolute":
229 self.mx = (mx - owner.mx) / frames
230 elif type == "relative":
231 self.mx = mx / frames
232 if vertical:
233 my, type = vertical
234 if frames <= 0:
235 if type == "absolute":
236 owner.my = my
237 elif type == "relative":
238 owner.my += my
239 elif type == "sequence":
240 self.my = my
241 elif type == "absolute":
242 self.my = (my - owner.my) / frames
243 elif type == "relative":
244 self.my = my / frames
245
246 elif isinstance(action, parser.Tag):
247 owner.tags.add(action.tag)
248
249 elif isinstance(action, parser.Untag):
250 try:
251 owner.tags.remove(action.tag)
252 except KeyError:
253 pass
254
255 elif isinstance(action, parser.Wait):
256 self.wait_frames = action(s_params, rank)
257 break
258
259 elif isinstance(action, parser.Vanish):
260 owner.vanish()
261 break
262
263 else:
264 self.handle(action, owner, created, sin, cos)
265
266 def handle(self, action, owner, created, sin, cos):
267 """Override in subclasses for new action types."""
268 raise NotImplementedError(action.__class__.__name__)
269
270 class Bullet(object):
271 """Simple bullet implementation.
272
273 Attributes:
274 x, y - current X/Y position
275 px, py - X/Y position prior to the last step
276 mx, my - X/Y axis-oriented speed modifier ("acceleration")
277 direction - direction of movement, in radians
278 speed - speed of movement, in units per frame
279 target - object with .x and .y fields for "aim" directions
280 vanished - set to true by a <vanish> action
281 rank - game difficulty, 0 to 1, default 0.5
282 tags - string tags set by the running actions
283
284 Contructor Arguments:
285 x, y, direction, speed, target, rank - same as the attributes
286 actions - internal action list
287 parent - parent of actions, None for manually-created bullets
288
289
290 """
291
292 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
293 actions=(), rank=0.5, Action=Action):
294 self.x = self.px = x
295 self.y = self.py = y
296 self.mx = 0
297 self.my = 0
298 self.direction = direction
299 self.speed = speed
300 self.vanished = False
301 self.target = target
302 self.rank = rank
303 self.tags = set()
304 # New bullets reset the parent hierarchy.
305 self._actions = [Action(self, None, action, params, rank)
306 for action, params in actions]
307
308 @classmethod
309 def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None,
310 params=(), rank=0.5, Action=Action):
311 """Construct a new Bullet from a loaded BulletML document."""
312 actions = [a(params, rank) for a in doc.actions]
313 return cls(x, y, direction, speed, target, actions, rank, Action)
314
315 def __repr__(self):
316 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
317 "actions=%r, target=%r, vanished=%r)") % (
318 type(self).__name__, self.x, self.y, (self.mx, self.my),
319 self.direction, self.speed, self._actions, self.target,
320 self.vanished)
321
322 @property
323 def aim(self):
324 """Angle to the target, in radians.
325
326 If the target does not exist or cannot be found, return 0.
327 """
328 try:
329 target_x = self.target.x
330 target_y = self.target.y
331 except AttributeError:
332 return 0
333 else:
334 return math.atan2(target_x - self.x, self.y - target_y)
335
336 @property
337 def finished(self):
338 """Check if this bullet is finished running.
339
340 A bullet is finished when it has vanished, and all its
341 actions have finished.
342
343 If this is true, the bullet should be removed from the screen.
344 (You will probably want to cull it under other circumstances
345 as well).
346 """
347 if not self.vanished:
348 return False
349 for action in self._actions:
350 if not action.finished:
351 return False
352 return True
353
354 def vanish(self):
355 """Vanish this bullet and stop all actions."""
356 self.vanished = True
357 for action in self._actions:
358 action.vanish()
359 self._actions = []
360
361 def replace(self, old, new):
362 """Replace an active action with another.
363
364 This is mostly used by actions internally to queue children.
365 """
366 try:
367 idx = self._actions.index(old)
368 except ValueError:
369 pass
370 else:
371 self._actions[idx] = new
372
373 def step(self, sin=math.sin, cos=math.cos):
374 """Advance by one frame.
375
376 This updates the position and velocity, and may also set the
377 vanished flag.
378
379 It returns any new bullets this bullet spawned during this step.
380 """
381 created = []
382
383 for action in self._actions:
384 action.step(self, created, sin, cos)
385
386 self.px = self.x
387 self.py = self.y
388 self.x += self.mx + sin(self.direction) * self.speed
389 self.y += self.my - cos(self.direction) * self.speed
390
391 return created