Re-reverse coordinate system to match OpenGL.
[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
292 Contructor Arguments:
293 x, y, direction, speed, target, rank - same as the attributes
294 actions - internal action list
295 Action - custom Action constructor
296
297
298 """
299
300 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
301 actions=(), rank=0.5, tags=(), appearance=None,
302 Action=Action):
303 self.x = self.px = x
304 self.y = self.py = y
305 self.mx = 0
306 self.my = 0
307 self.direction = direction
308 self.speed = speed
309 self.vanished = False
310 self.target = target
311 self.rank = rank
312 self.tags = set(tags)
313 self.appearance = appearance
314 # New bullets reset the parent hierarchy.
315 self._actions = [Action(self, None, action, params, rank)
316 for action, params in actions]
317
318 @classmethod
319 def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None,
320 params=(), rank=0.5, Action=Action):
321 """Construct a new Bullet from a loaded BulletML document."""
322 actions = [a(params, rank) for a in doc.actions]
323 return cls(x=x, y=y, direction=direction, speed=speed,
324 target=target, actions=actions, rank=rank, Action=Action)
325
326 def __repr__(self):
327 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
328 "actions=%r, target=%r, appearance=vanished=%r)") % (
329 type(self).__name__, self.x, self.y, (self.mx, self.my),
330 self.direction, self.speed, self._actions, self.target,
331 self.appearance, self.vanished)
332
333 @property
334 def aim(self):
335 """Angle to the target, in radians.
336
337 If the target does not exist or cannot be found, return 0.
338 """
339 try:
340 target_x = self.target.x
341 target_y = self.target.y
342 except AttributeError:
343 return 0
344 else:
345 return atan2(target_x - self.x, target_y - self.y)
346
347 @property
348 def finished(self):
349 """Check if this bullet is finished running.
350
351 A bullet is finished when it has vanished, and all its
352 actions have finished.
353
354 If this is true, the bullet should be removed from the screen.
355 (You will probably want to cull it under other circumstances
356 as well).
357 """
358 if not self.vanished:
359 return False
360 for action in self._actions:
361 if not action.finished:
362 return False
363 return True
364
365 def vanish(self):
366 """Vanish this bullet and stop all actions."""
367 self.vanished = True
368 for action in self._actions:
369 action.vanish()
370 self._actions = []
371
372 def replace(self, old, new):
373 """Replace an active action with another.
374
375 This is mostly used by actions internally to queue children.
376 """
377 try:
378 idx = self._actions.index(old)
379 except ValueError:
380 pass
381 else:
382 self._actions[idx] = new
383
384 def step(self):
385 """Advance by one frame.
386
387 This updates the position and velocity, and may also set the
388 vanished flag.
389
390 It returns any new bullets this bullet spawned during this step.
391 """
392 created = []
393
394 for action in self._actions:
395 action.step(self, created)
396
397 self.px = self.x
398 self.py = self.y
399 self.x += self.mx + sin(self.direction) * self.speed
400 self.y += -self.my + cos(self.direction) * self.speed
401
402 return created