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