ea4893f42dacfaf91cc1b7bcc0693400d2d27d8e
[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 child = self.Child(action, s_params, rank)
124 owner.replace(self, child)
125 child.step(owner, created)
126 break
127
128 elif action(owner, self, s_params, rank, created):
129 break
130
131 class Bullet(object):
132 """Simple bullet implementation.
133
134 Attributes:
135 x, y - current X/Y position
136 px, py - X/Y position prior to the last step
137 mx, my - X/Y axis-oriented speed modifier ("acceleration")
138 direction - direction of movement, in radians
139 speed - speed of movement, in units per frame
140 target - object with .x and .y fields for "aim" directions
141 vanished - set to true by a <vanish> action
142 rank - game difficulty, 0 to 1, default 0.5
143 tags - string tags set by the running actions
144 appearance - string used to set bullet appearance
145
146 Contructor Arguments:
147 x, y, direction, speed, target, rank, tags, appearance
148 - same as the above attributes
149 actions - internal action list
150 Action - custom Action constructor
151
152 """
153
154 def __init__(self, x=0, y=0, direction=0, speed=0, target=None,
155 actions=(), rank=0.5, tags=(), appearance=None,
156 Action=Action):
157 self.x = self.px = x
158 self.y = self.py = y
159 self.mx = 0
160 self.my = 0
161 self.direction = direction
162 self.speed = speed
163 self.vanished = False
164 self.target = target
165 self.rank = rank
166 self.tags = set(tags)
167 self.appearance = appearance
168 # New bullets reset the parent hierarchy.
169 self.actions = [Action(None, action, params, rank)
170 for action, params in actions]
171
172 @classmethod
173 def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None,
174 params=(), rank=0.5, Action=Action):
175 """Construct a new Bullet from a loaded BulletML document."""
176 actions = [a(params, rank) for a in doc.actions]
177 return cls(x=x, y=y, direction=direction, speed=speed,
178 target=target, actions=actions, rank=rank, Action=Action)
179
180 def __repr__(self):
181 return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, "
182 "actions=%r, target=%r, appearance=vanished=%r)") % (
183 type(self).__name__, self.x, self.y, (self.mx, self.my),
184 self.direction, self.speed, self.actions, self.target,
185 self.appearance, self.vanished)
186
187 @property
188 def aim(self):
189 """Angle to the target, in radians.
190
191 If the target does not exist or cannot be found, return 0.
192 """
193 try:
194 target_x = self.target.x
195 target_y = self.target.y
196 except AttributeError:
197 return 0
198 else:
199 return atan2(target_x - self.x, target_y - self.y)
200
201 @property
202 def finished(self):
203 """Check if this bullet is finished running.
204
205 A bullet is finished when it has vanished, and all its
206 actions have finished.
207
208 If this is true, the bullet should be removed from the screen.
209 (You will probably want to cull it under other circumstances
210 as well).
211 """
212 if not self.vanished:
213 return False
214 for action in self.actions:
215 if not action.finished:
216 return False
217 return True
218
219 def vanish(self):
220 """Vanish this bullet and stop all actions."""
221 self.vanished = True
222 for action in self.actions:
223 action.vanish()
224 self.actions = []
225
226 def replace(self, old, new):
227 """Replace an active action with another.
228
229 This is mostly used by actions internally to queue children.
230 """
231 try:
232 idx = self.actions.index(old)
233 except ValueError:
234 pass
235 else:
236 self.actions[idx] = new
237
238 def step(self):
239 """Advance by one frame.
240
241 This updates the position and velocity, and may also set the
242 vanished flag.
243
244 It returns any new bullets this bullet spawned during this step.
245 """
246 created = []
247
248 for action in self.actions:
249 action.step(self, created)
250
251 self.px = self.x
252 self.py = self.y
253 self.x += self.mx + sin(self.direction) * self.speed
254 self.y += -self.my + cos(self.direction) * self.speed
255
256 return created