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