d6753e3aafb78a3c778868fe1fecd72156138970
[python-bulletml.git] / bulletml / impl.py
1 """BulletML implementation."""
2
3 from __future__ import division
4
5 from math import atan2, sin, cos
6
7 from bulletml import parser
8
9 __all__ = ["Action", "Bullet"]
10
11 class Action(object):
12 """Running action implementation.
13
14 To implement new actions, add a new element/class pair to
15 parser.ActionDef.CONSTRUCTORS. It should support FromXML,
16 __getstate__, and __setstate__, and 5-ary __call__:
17
18 def __call__(self, owner, action, params, rank, created)
19
20 Which will be called to execute it. This function should modify
21 owner, action, and created in-place, and return true if action
22 execution should stop for this bullet this frame.
23
24 """
25
26 def __init__(self, parent, actions, params, rank, repeat=1):
27 self.actions = actions
28 self.parent = parent
29 self.repeat = repeat
30 self.wait_frames = 0
31 self.speed = 0
32 self.speed_frames = 0
33 self.direction = 0
34 self.direction_frames = 0
35 self.aiming = False
36 self.mx = 0
37 self.my = 0
38 self.accel_frames = 0
39 self.previous_fire_direction = 0
40 self.previous_fire_speed = 0
41 self.params = params
42 self.pc = -1
43 self.finished = False
44 if parent:
45 self.copy_state(parent)
46
47 def __repr__(self):
48 return "%s(pc=%r, actions=%r)" % (
49 type(self).__name__, self.pc, self.actions)
50
51 def Child(self, action, params, rank, repeat=1):
52 actions, params = action(params, rank)
53 return type(self)(self, actions, params, rank, repeat)
54
55 def vanish(self):
56 """End this action and its parents."""
57 if self.parent:
58 self.parent.vanish()
59 self.pc = None
60 self.finished = True
61
62 def copy_state(self, other):
63 """Copy fire/movement state from other to self."""
64 self.direction_frames = other.direction_frames
65 self.direction = other.direction
66 self.aiming = other.aiming
67 self.speed_frames = other.speed_frames
68 self.speed = other.speed
69 self.accel_frames = other.accel_frames
70 self.mx = other.mx
71 self.my = other.my
72 self.previous_fire_direction = other.previous_fire_direction
73 self.previous_fire_speed = other.previous_fire_speed
74
75 def step(self, owner, created):
76 """Advance by one frame."""
77 s_params = self.params
78 rank = owner.rank
79
80 if self.speed_frames > 0:
81 self.speed_frames -= 1
82 owner.speed += self.speed
83
84 if self.direction_frames > 0:
85 # I'm still not sure what the aim check is supposed to do.
86 self.direction_frames -= 1
87 if self.aiming and self.direction_frames <= 0:
88 owner.direction += owner.aim
89 else:
90 owner.direction += self.direction
91
92 if self.accel_frames > 0:
93 self.accel_frames -= 1
94 owner.mx += self.mx
95 owner.my += self.my
96
97 if self.pc is None:
98 return
99
100 if self.wait_frames > 0:
101 self.wait_frames -= 1
102 return
103
104 while True:
105 self.pc += 1
106
107 try:
108 action = self.actions[self.pc]
109 except IndexError:
110 self.repeat -= 1
111 if self.repeat <= 0:
112 self.pc = None
113 self.finished = True
114 if self.parent is not None:
115 self.parent.copy_state(self)
116 owner.replace(self, self.parent)
117 break
118 else:
119 self.pc = 0
120 action = self.actions[self.pc]
121
122 if isinstance(action, (parser.ActionDef, parser.ActionRef)):
123 actions, params = action(s_params, rank)
124 child = self.__class__(self, actions, params, rank)
125 owner.replace(self, child)
126 child.step(owner, created)
127 break
128
129 elif action(owner, self, s_params, rank, created):
130 break
131
132 class Bullet(object):
133 """Simple bullet implementation.
134
135 Attributes:
136 x, y - current X/Y position
137 px, py - X/Y position prior to the last step
138 mx, my - X/Y axis-oriented speed modifier ("acceleration")
139 direction - direction of movement, in radians
140 speed - speed of movement, in units per frame
141 target - object with .x and .y fields for "aim" directions
142 vanished - set to true by a <vanish> action
143 rank - game difficulty, 0 to 1, default 0.5
144 tags - string tags set by the running actions
145 appearance - string used to set bullet appearance
146
147 Contructor Arguments:
148 x, y, direction, speed, target, rank, tags, appearance
149 - same as the above attributes
150 actions - internal action list
151 Action - custom Action constructor
152
153 """
154
155 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
156 actions=(), rank=0.5, tags=(), appearance=None,
157 Action=Action):
158 self.x = self.px = x
159 self.y = self.py = y
160 self.mx = 0
161 self.my = 0
162 self.direction = direction
163 self.speed = speed
164 self.vanished = False
165 self.target = target
166 self.rank = rank
167 self.tags = set(tags)
168 self.appearance = appearance
169 # New bullets reset the parent hierarchy.
170 self.actions = [Action(None, action, params, rank)
171 for action, params in actions]
172
173 @classmethod
174 def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None,
175 params=(), rank=0.5, Action=Action):
176 """Construct a new Bullet from a loaded BulletML document."""
177 actions = [a(params, rank) for a in doc.actions]
178 return cls(x=x, y=y, direction=direction, speed=speed,
179 target=target, actions=actions, rank=rank, Action=Action)
180
181 def __repr__(self):
182 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
183 "actions=%r, target=%r, appearance=vanished=%r)") % (
184 type(self).__name__, self.x, self.y, (self.mx, self.my),
185 self.direction, self.speed, self.actions, self.target,
186 self.appearance, self.vanished)
187
188 @property
189 def aim(self):
190 """Angle to the target, in radians.
191
192 If the target does not exist or cannot be found, return 0.
193 """
194 try:
195 target_x = self.target.x
196 target_y = self.target.y
197 except AttributeError:
198 return 0
199 else:
200 return atan2(target_x - self.x, target_y - self.y)
201
202 @property
203 def finished(self):
204 """Check if this bullet is finished running.
205
206 A bullet is finished when it has vanished, and all its
207 actions have finished.
208
209 If this is true, the bullet should be removed from the screen.
210 (You will probably want to cull it under other circumstances
211 as well).
212 """
213 if not self.vanished:
214 return False
215 for action in self.actions:
216 if not action.finished:
217 return False
218 return True
219
220 def vanish(self):
221 """Vanish this bullet and stop all actions."""
222 self.vanished = True
223 for action in self.actions:
224 action.vanish()
225 self.actions = []
226
227 def replace(self, old, new):
228 """Replace an active action with another.
229
230 This is mostly used by actions internally to queue children.
231 """
232 try:
233 idx = self.actions.index(old)
234 except ValueError:
235 pass
236 else:
237 self.actions[idx] = new
238
239 def step(self):
240 """Advance by one frame.
241
242 This updates the position and velocity, and may also set the
243 vanished flag.
244
245 It returns any new bullets this bullet spawned during this step.
246 """
247 created = []
248
249 for action in self.actions:
250 action.step(self, created)
251
252 self.px = self.x
253 self.py = self.y
254 self.x += self.mx + sin(self.direction) * self.speed
255 self.y += -self.my + cos(self.direction) * self.speed
256
257 return created