a056d6c4390642ed78e0a3cb013eccdd5c92fb1d
[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 self.direction_frames -= 1
77 if self.direction_frames <= 0:
78 if self.aiming:
79 self.owner.direction += self.owner.aim
80 else:
81 self.owner.direction += self.direction
82
83 if self.accel_frames > 0:
84 self.accel_frames -= 1
85 self.owner.mx += self.mx
86 self.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 self.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, self.rank)
115 child = Action(
116 self.owner, self, actions, params, self.rank, repeat)
117 self.owner.replace(self, child)
118 created.extend(child.step())
119 break
120
121 elif isinstance(action, (parser.ActionDef, parser.ActionRef)):
122 actions, params = action(self.params, self.rank)
123 child = Action(self.owner, self, actions, params, self.rank)
124 self.owner.replace(self, child)
125 created.extend(child.step())
126 break
127
128 elif isinstance(action, (parser.FireDef, parser.FireRef)):
129 direction, speed, actions = action(self.params, self.rank)
130 if direction:
131 direction, type = direction
132 if type == "aim" or type is None:
133 direction += self.owner.aim
134 elif type == "sequence":
135 direction += self.previous_fire_direction
136 elif type == "relative":
137 direction += self.owner.direction
138 else:
139 direction = self.owner.aim
140 self.previous_fire_direction = direction
141
142 if speed:
143 speed, type = speed
144 if type == "sequence":
145 speed += self.previous_fire_speed
146 elif type == "relative":
147 # The reference Noiz implementation uses
148 # prvFireSpeed here, but the standard is
149 # pretty clear -- "0 means that the direction
150 # of this fire and the direction of the bullet
151 # are the same".
152 speed += self.owner.speed
153 else:
154 speed = 1
155 self.previous_fire_speed = speed
156
157 bullet = Bullet(self.owner.x, self.owner.y, direction, speed,
158 self.owner.target, actions, self)
159 created.append(bullet)
160
161 elif isinstance(action, parser.ChangeSpeed):
162 frames, (speed, type) = action(self.params, self.rank)
163 self.speed_frames = frames
164 if type == "sequence":
165 self.speed = speed
166 elif type == "relative":
167 self.speed = speed / frames
168 else:
169 self.speed = (speed - self.owner.speed) / frames
170
171 elif isinstance(action, parser.ChangeDirection):
172 frames, (direction, type) = action(self.params, self.rank)
173 self.direction_frames = frames
174 self.aiming = False
175 if type == "sequence":
176 self.direction = direction
177 else:
178 if type == "absolute":
179 self.direction = (
180 direction - self.owner.direction) % PI_2
181 elif type == "relative":
182 self.direction = direction
183 else:
184 self.aiming = True
185 self.direction = (
186 direction
187 + self.owner.aim
188 - self.owner.direction) % PI_2
189
190 if self.direction > math.pi:
191 self.direction -= PI_2
192 if self.direction < -math.pi:
193 self.direction += PI_2
194 self.direction /= self.direction_frames
195
196 elif isinstance(action, parser.Accel):
197 frames, horizontal, vertical = action(self.params, self.rank)
198 self.accel_frames = frames
199 if horizontal:
200 mx, type = horizontal
201 if type == "sequence":
202 self.mx = mx
203 elif type == "absolute":
204 self.mx = (mx - self.owner.mx) / frames
205 elif type == "relative":
206 self.mx = mx / frames
207 if vertical:
208 my, type = vertical
209 if type == "sequence":
210 self.my = my
211 elif type == "absolute":
212 self.my = (my - self.owner.my) / frames
213 elif type == "relative":
214 self.my = my / frames
215
216 elif isinstance(action, parser.Wait):
217 self.wait_frames = action(self.params, self.rank)
218 break
219
220 elif isinstance(action, parser.Vanish):
221 self.owner.vanish()
222 break
223
224 return created
225
226 class Bullet(object):
227 """Simple bullet implementation."""
228
229 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
230 actions=(), parent=None, rank=None):
231 self.x = self.px = x
232 self.y = self.py = y
233 self.mx = 0
234 self.my = 0
235 self.direction = direction
236 self.speed = speed
237 self.vanished = False
238 self.target = target
239 if rank is None:
240 rank = parent.rank if parent else 0.5
241 # New bullets reset the parent hierarchy.
242 self._actions = [Action(self, None, action, params, rank)
243 for action, params in actions]
244
245 def __repr__(self):
246 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
247 "actions=%r, target=%r, vanished=%r)") % (
248 type(self).__name__, self.x, self.y, (self.mx, self.my),
249 self.direction, self.speed, self._actions, self.target,
250 self.vanished)
251
252 @property
253 def aim(self):
254 """Angle to the target."""
255 if self.target is None:
256 return self.direction
257 else:
258 return math.atan2(self.target.x - self.x, self.y - self.target.y)
259
260 @property
261 def finished(self):
262 """Check if this bullet is finished running.
263
264 If this is true, the bullet should be removed from the screen.
265 (You will probably want to cull it under other circumstances
266 as well).
267 """
268 if not self.vanished:
269 return False
270 for action in self._actions:
271 if not action.finished:
272 return False
273 return True
274
275 def vanish(self):
276 """Vanish this bullet and stop all actions."""
277 self.vanished = True
278 for action in self._actions:
279 action.vanish()
280 self._actions = []
281
282 def replace(self, old, new):
283 """Replace an active action with another."""
284 try:
285 idx = self._actions.index(old)
286 except ValueError:
287 pass
288 else:
289 self._actions[idx] = new
290
291 def step(self):
292 """Advance by one frame.
293
294 This updates the direction, speed, x, y, px, and py members,
295 and may set the vanished flag.
296 """
297 created = []
298
299 for action in self._actions:
300 created.extend(action.step())
301
302 self.px = self.x
303 self.py = self.y
304 self.x += self.mx + math.sin(self.direction) * self.speed
305 self.y += self.my - math.cos(self.direction) * self.speed
306
307 return created
308
309 @classmethod
310 def FromDoc(cls, doc, params=(), x=0, y=0, speed=0, direction=0,
311 target=None, rank=0.5):
312 """Construct a bullet from top-level actions in a document."""
313 actions = [act(params, rank) for act in doc.top]
314 return cls(x, y, direction, speed, target, actions, rank=rank)