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