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