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