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