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