Action: Get owner and rank during the step function, from the Bullet. This is faster...
[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):
65 """Advance by one frame."""
66 created = []
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 created
90
91 if self.wait_frames > 0:
92 self.wait_frames -= 1
93 return created
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(self.params, rank)
115 child = Action(owner, self, actions, params, rank, repeat)
116 owner.replace(self, child)
117 created.extend(child.step(owner, rank))
118 break
119
120 elif isinstance(action, (parser.ActionDef, parser.ActionRef)):
121 actions, params = action(self.params, rank)
122 child = Action(owner, self, actions, params, rank)
123 owner.replace(self, child)
124 created.extend(child.step(owner, rank))
125 break
126
127 elif isinstance(action, (parser.FireDef, parser.FireRef)):
128 direction, speed, actions = action(self.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 -- "0 means that the direction
149 # of this fire and the direction of the bullet
150 # are the same".
151 speed += owner.speed
152 else:
153 speed = 1
154 self.previous_fire_speed = speed
155
156 bullet = Bullet(owner.x, owner.y, direction, speed,
157 owner.target, actions, self, rank)
158 created.append(bullet)
159
160 elif isinstance(action, parser.ChangeSpeed):
161 frames, (speed, type) = action(self.params, rank)
162 self.speed_frames = frames
163 if type == "sequence":
164 self.speed = speed
165 elif type == "relative":
166 self.speed = speed / frames
167 else:
168 self.speed = (speed - owner.speed) / frames
169
170 elif isinstance(action, parser.ChangeDirection):
171 frames, (direction, type) = action(self.params, rank)
172 self.direction_frames = frames
173 self.aiming = False
174 if type == "sequence":
175 self.direction = direction
176 else:
177 if type == "absolute":
178 self.direction = (
179 direction - owner.direction) % PI_2
180 elif type == "relative":
181 self.direction = direction
182 else:
183 self.aiming = True
184 self.direction = (
185 direction
186 + owner.aim
187 - owner.direction) % PI_2
188
189 if self.direction > math.pi:
190 self.direction -= PI_2
191 if self.direction < -math.pi:
192 self.direction += PI_2
193 self.direction /= self.direction_frames
194
195 elif isinstance(action, parser.Accel):
196 frames, horizontal, vertical = action(self.params, rank)
197 self.accel_frames = frames
198 if horizontal:
199 mx, type = horizontal
200 if type == "sequence":
201 self.mx = mx
202 elif type == "absolute":
203 self.mx = (mx - owner.mx) / frames
204 elif type == "relative":
205 self.mx = mx / frames
206 if vertical:
207 my, type = vertical
208 if type == "sequence":
209 self.my = my
210 elif type == "absolute":
211 self.my = (my - owner.my) / frames
212 elif type == "relative":
213 self.my = my / frames
214
215 elif isinstance(action, parser.Wait):
216 self.wait_frames = action(self.params, rank)
217 break
218
219 elif isinstance(action, parser.Vanish):
220 owner.vanish()
221 break
222
223 return created
224
225 class Bullet(object):
226 """Simple bullet implementation.
227
228 Attributes:
229 x, y - current X/Y position
230 px, py - X/Y position prior to the last step
231 mx, my - X/Y axis-oriented speed modifier ("acceleration")
232 direction - direction of movement, in radians
233 speed - speed of movement, in units per frame
234 target - object with .x and .y fields for "aim" directions
235 vanished - set to true by a <vanish> action
236 rank - game difficulty, 0 to 1, default 0.5
237
238 Contructor Arguments:
239 x, y, direction, speed, target, rank - same as the attributes
240 actions - internal action list
241 parent - parent of actions, None for manually-created bullets
242
243
244 """
245
246 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
247 actions=(), parent=None, rank=0.5):
248 self.x = self.px = x
249 self.y = self.py = y
250 self.mx = 0
251 self.my = 0
252 self.direction = direction
253 self.speed = speed
254 self.vanished = False
255 self.target = target
256 self.rank = rank
257 # New bullets reset the parent hierarchy.
258 self._actions = [Action(self, None, action, params, rank)
259 for action, params in actions]
260
261 def __repr__(self):
262 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
263 "actions=%r, target=%r, vanished=%r)") % (
264 type(self).__name__, self.x, self.y, (self.mx, self.my),
265 self.direction, self.speed, self._actions, self.target,
266 self.vanished)
267
268 @property
269 def aim(self):
270 """Angle to the target, in radians."""
271 if self.target is None:
272 return self.direction
273 else:
274 return math.atan2(self.target.x - self.x, self.y - self.target.y)
275
276 @property
277 def finished(self):
278 """Check if this bullet is finished running.
279
280 A bullet is finished when it has vanished, and all its
281 actions have finished.
282
283 If this is true, the bullet should be removed from the screen.
284 (You will probably want to cull it under other circumstances
285 as well).
286 """
287 if not self.vanished:
288 return False
289 for action in self._actions:
290 if not action.finished:
291 return False
292 return True
293
294 def vanish(self):
295 """Vanish this bullet and stop all actions."""
296 self.vanished = True
297 for action in self._actions:
298 action.vanish()
299 self._actions = []
300
301 def replace(self, old, new):
302 """Replace an active action with another.
303
304 This is mostly used by actions internally to queue children.
305 """
306 try:
307 idx = self._actions.index(old)
308 except ValueError:
309 pass
310 else:
311 self._actions[idx] = new
312
313 def step(self):
314 """Advance by one frame.
315
316 This updates the position and velocity, and may also set the
317 vanished flag.
318
319 It returns any new bullets this bullet spawned during this step.
320 """
321 created = []
322
323 for action in self._actions:
324 created.extend(action.step(self, self.rank))
325
326 self.px = self.x
327 self.py = self.y
328 self.x += self.mx + math.sin(self.direction) * self.speed
329 self.y += self.my - math.cos(self.direction) * self.speed
330
331 return created