26b95804230b9f76877f5d41bfdf5ed841b1443f
[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, offset = 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 x, y = owner.x, owner.y
157 if offset:
158 off_x, off_y = offset(self.params, rank)
159 if offset.type == "relative":
160 sin = math.sin(direction)
161 cos = math.cos(direction)
162 x += cos * off_x + sin * off_y
163 y += sin * off_x - cos * 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(self.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(self.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 self.direction = (
191 direction - owner.direction) % PI_2
192 elif type == "relative":
193 self.direction = direction
194 else:
195 self.aiming = True
196 self.direction = (
197 direction
198 + owner.aim
199 - owner.direction) % PI_2
200
201 if self.direction > math.pi:
202 self.direction -= PI_2
203 if self.direction < -math.pi:
204 self.direction += PI_2
205 self.direction /= self.direction_frames
206
207 elif isinstance(action, parser.Accel):
208 frames, horizontal, vertical = action(self.params, rank)
209 self.accel_frames = frames
210 if horizontal:
211 mx, type = horizontal
212 if type == "sequence":
213 self.mx = mx
214 elif type == "absolute":
215 self.mx = (mx - owner.mx) / frames
216 elif type == "relative":
217 self.mx = mx / frames
218 if vertical:
219 my, type = vertical
220 if type == "sequence":
221 self.my = my
222 elif type == "absolute":
223 self.my = (my - owner.my) / frames
224 elif type == "relative":
225 self.my = my / frames
226
227 elif isinstance(action, parser.Wait):
228 self.wait_frames = action(self.params, rank)
229 break
230
231 elif isinstance(action, parser.Vanish):
232 owner.vanish()
233 break
234
235 return created
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
250 Contructor Arguments:
251 x, y, direction, speed, target, rank - same as the attributes
252 actions - internal action list
253 parent - parent of actions, None for manually-created bullets
254
255
256 """
257
258 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
259 actions=(), parent=None, rank=0.5):
260 self.x = self.px = x
261 self.y = self.py = y
262 self.mx = 0
263 self.my = 0
264 self.direction = direction
265 self.speed = speed
266 self.vanished = False
267 self.target = target
268 self.rank = rank
269 # New bullets reset the parent hierarchy.
270 self._actions = [Action(self, None, action, params, rank)
271 for action, params in actions]
272
273 def __repr__(self):
274 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
275 "actions=%r, target=%r, vanished=%r)") % (
276 type(self).__name__, self.x, self.y, (self.mx, self.my),
277 self.direction, self.speed, self._actions, self.target,
278 self.vanished)
279
280 @property
281 def aim(self):
282 """Angle to the target, in radians."""
283 if self.target is None:
284 return self.direction
285 else:
286 return math.atan2(self.target.x - self.x, self.y - self.target.y)
287
288 @property
289 def finished(self):
290 """Check if this bullet is finished running.
291
292 A bullet is finished when it has vanished, and all its
293 actions have finished.
294
295 If this is true, the bullet should be removed from the screen.
296 (You will probably want to cull it under other circumstances
297 as well).
298 """
299 if not self.vanished:
300 return False
301 for action in self._actions:
302 if not action.finished:
303 return False
304 return True
305
306 def vanish(self):
307 """Vanish this bullet and stop all actions."""
308 self.vanished = True
309 for action in self._actions:
310 action.vanish()
311 self._actions = []
312
313 def replace(self, old, new):
314 """Replace an active action with another.
315
316 This is mostly used by actions internally to queue children.
317 """
318 try:
319 idx = self._actions.index(old)
320 except ValueError:
321 pass
322 else:
323 self._actions[idx] = new
324
325 def step(self):
326 """Advance by one frame.
327
328 This updates the position and velocity, and may also set the
329 vanished flag.
330
331 It returns any new bullets this bullet spawned during this step.
332 """
333 created = []
334
335 for action in self._actions:
336 created.extend(action.step(self, self.rank))
337
338 self.px = self.x
339 self.py = self.y
340 self.x += self.mx + math.sin(self.direction) * self.speed
341 self.y += self.my - math.cos(self.direction) * self.speed
342
343 return created