Action: Don't need the owner in the constructor. Factory function to create a child.
[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, actions, params, rank, repeat=1):
52 return type(self)(self, actions, params, rank, repeat)
53
54 def vanish(self):
55 """End this action and its parents."""
56 if self.parent:
57 self.parent.vanish()
58 self.pc = None
59 self.finished = True
60
61 def copy_state(self, other):
62 """Copy fire/movement state from other to self."""
63 self.direction_frames = other.direction_frames
64 self.direction = other.direction
65 self.aiming = other.aiming
66 self.speed_frames = other.speed_frames
67 self.speed = other.speed
68 self.accel_frames = other.accel_frames
69 self.mx = other.mx
70 self.my = other.my
71 self.previous_fire_direction = other.previous_fire_direction
72 self.previous_fire_speed = other.previous_fire_speed
73
74 def step(self, owner, created):
75 """Advance by one frame."""
76 s_params = self.params
77 rank = owner.rank
78
79 if self.speed_frames > 0:
80 self.speed_frames -= 1
81 owner.speed += self.speed
82
83 if self.direction_frames > 0:
84 # I'm still not sure what the aim check is supposed to do.
85 self.direction_frames -= 1
86 if self.aiming and self.direction_frames <= 0:
87 owner.direction += owner.aim
88 else:
89 owner.direction += self.direction
90
91 if self.accel_frames > 0:
92 self.accel_frames -= 1
93 owner.mx += self.mx
94 owner.my += self.my
95
96 if self.pc is None:
97 return
98
99 if self.wait_frames > 0:
100 self.wait_frames -= 1
101 return
102
103 while True:
104 self.pc += 1
105
106 try:
107 action = self.actions[self.pc]
108 except IndexError:
109 self.repeat -= 1
110 if self.repeat <= 0:
111 self.pc = None
112 self.finished = True
113 if self.parent is not None:
114 self.parent.copy_state(self)
115 owner.replace(self, self.parent)
116 break
117 else:
118 self.pc = 0
119 action = self.actions[self.pc]
120
121 if isinstance(action, (parser.ActionDef, parser.ActionRef)):
122 actions, params = action(s_params, rank)
123 child = self.__class__(self, actions, 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