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