"""BulletML implementation.""" from __future__ import division import math from bulletml import parser PI_2 = math.pi * 2 __all__ = ["Action", "Bullet"] class Action(object): """Running action implementation. To implement new actions, - Add a new element/class pair to parser.ActionDef.CONSTRUCTORS. It should support FromXML, __getstate__, and __setstate__. - Subclass impl.Action and override the 'handle' method to handle your custom action type. - Pass the impl.Bullet constructor your Action subclass when creating your root Bullet. """ def __init__(self, owner, parent, actions, params, rank, repeat=1): self.actions = actions self.parent = parent self.repeat = repeat self.wait_frames = 0 self.speed = 0 self.speed_frames = 0 self.direction = 0 self.direction_frames = 0 self.aiming = False self.mx = 0 self.my = 0 self.accel_frames = 0 self.previous_fire_direction = 0 self.previous_fire_speed = 0 self.params = params self.pc = -1 self.finished = False if parent: self.copy_state(parent) def __repr__(self): return "%s(pc=%r, actions=%r)" % ( type(self).__name__, self.pc, self.actions) def vanish(self): """End this action and its parents.""" if self.parent: self.parent.vanish() self.pc = None self.finished = True def copy_state(self, other): """Copy fire/movement state from other to self.""" self.direction_frames = other.direction_frames self.direction = other.direction self.aiming = other.aiming self.speed_frames = other.speed_frames self.speed = other.speed self.accel_frames = other.accel_frames self.mx = other.mx self.my = other.my self.previous_fire_direction = other.previous_fire_direction self.previous_fire_speed = other.previous_fire_speed def step(self, owner, created, sin=math.sin, cos=math.cos): """Advance by one frame.""" s_params = self.params rank = owner.rank if self.speed_frames > 0: self.speed_frames -= 1 owner.speed += self.speed if self.direction_frames > 0: # The Noiz implementation was a little weird here, I think # there was a bug in it that prevented it from working if # the frame count was 1. I'm still not sure what the aim # check is supposed to do, exactly. self.direction_frames -= 1 if self.aiming and self.direction_frames <= 0: owner.direction += owner.aim else: owner.direction += self.direction if self.accel_frames > 0: self.accel_frames -= 1 owner.mx += self.mx owner.my += self.my if self.pc is None: return if self.wait_frames > 0: self.wait_frames -= 1 return while True: self.pc += 1 try: action = self.actions[self.pc] except IndexError: self.repeat -= 1 if self.repeat <= 0: self.pc = None self.finished = True if self.parent is not None: self.parent.copy_state(self) owner.replace(self, self.parent) break else: self.pc = 0 action = self.actions[self.pc] if isinstance(action, parser.Repeat): repeat, (actions, params) = action(s_params, rank) child = self.__class__( owner, self, actions, params, rank, repeat) owner.replace(self, child) child.step(owner, created, sin, cos) break elif isinstance(action, (parser.ActionDef, parser.ActionRef)): actions, params = action(s_params, rank) child = self.__class__(owner, self, actions, params, rank) owner.replace(self, child) child.step(owner, created, sin, cos) break elif isinstance(action, (parser.FireDef, parser.FireRef)): direction, speed, actions, offset = action(s_params, rank) if direction: direction, type = direction if type == "aim" or type is None: direction += owner.aim elif type == "sequence": direction += self.previous_fire_direction elif type == "relative": direction += owner.direction else: direction = owner.aim self.previous_fire_direction = direction if speed: speed, type = speed if type == "sequence": speed += self.previous_fire_speed elif type == "relative": # The reference Noiz implementation uses # prvFireSpeed here, but the standard is # pretty clear -- "In case of the type is # "relative", ... the speed is relative to the # speed of this bullet." speed += owner.speed else: speed = 1 self.previous_fire_speed = speed x, y = owner.x, owner.y if offset: off_x, off_y = offset(s_params, rank) if offset.type == "relative": s = sin(direction) c = cos(direction) x += c * off_x + s * off_y y += s * off_x - c * off_y else: x += off_x y += off_y bullet = owner.__class__( x, y, direction, speed, owner.target, actions, rank) created.append(bullet) elif isinstance(action, parser.ChangeSpeed): frames, (speed, type) = action(s_params, rank) self.speed_frames = frames if frames <= 0: if type == "absolute": owner.speed = speed elif type == "relative": owner.speed += speed elif type == "sequence": self.speed = speed elif type == "relative": self.speed = speed / frames else: self.speed = (speed - owner.speed) / frames elif isinstance(action, parser.ChangeDirection): frames, (direction, type) = action(s_params, rank) self.direction_frames = frames self.aiming = False if type == "sequence": self.direction = direction else: if type == "absolute": direction -= owner.direction elif type != "relative": # aim or default self.aiming = True direction += owner.aim - owner.direction # Normalize to [-pi, pi). direction = (direction + math.pi) % PI_2 - math.pi if frames <= 0: owner.direction += direction else: self.direction = direction / frames elif isinstance(action, parser.Accel): frames, horizontal, vertical = action(s_params, rank) self.accel_frames = frames if horizontal: mx, type = horizontal if frames <= 0: if type == "absolute": owner.mx = mx elif type == "relative": owner.mx += mx elif type == "sequence": self.mx = mx elif type == "absolute": self.mx = (mx - owner.mx) / frames elif type == "relative": self.mx = mx / frames if vertical: my, type = vertical if frames <= 0: if type == "absolute": owner.my = my elif type == "relative": owner.my += my elif type == "sequence": self.my = my elif type == "absolute": self.my = (my - owner.my) / frames elif type == "relative": self.my = my / frames elif isinstance(action, parser.Tag): owner.tags.add(action.tag) elif isinstance(action, parser.Untag): try: owner.tags.remove(action.tag) except KeyError: pass elif isinstance(action, parser.Wait): self.wait_frames = action(s_params, rank) break elif isinstance(action, parser.Vanish): owner.vanish() break else: self.handle(action, owner, created, sin, cos) def handle(self, action, owner, created, sin, cos): """Override in subclasses for new action types.""" raise NotImplementedError(action.__class__.__name__) class Bullet(object): """Simple bullet implementation. Attributes: x, y - current X/Y position px, py - X/Y position prior to the last step mx, my - X/Y axis-oriented speed modifier ("acceleration") direction - direction of movement, in radians speed - speed of movement, in units per frame target - object with .x and .y fields for "aim" directions vanished - set to true by a action rank - game difficulty, 0 to 1, default 0.5 tags - string tags set by the running actions Contructor Arguments: x, y, direction, speed, target, rank - same as the attributes actions - internal action list Action - custom Action constructor """ def __init__(self, x=0, y=0, direction=0, speed=0, target=None, actions=(), rank=0.5, Action=Action): self.x = self.px = x self.y = self.py = y self.mx = 0 self.my = 0 self.direction = direction self.speed = speed self.vanished = False self.target = target self.rank = rank self.tags = set() # New bullets reset the parent hierarchy. self._actions = [Action(self, None, action, params, rank) for action, params in actions] @classmethod def FromDocument(cls, doc, x=0, y=0, direction=0, speed=0, target=None, params=(), rank=0.5, Action=Action): """Construct a new Bullet from a loaded BulletML document.""" actions = [a(params, rank) for a in doc.actions] return cls(x, y, direction, speed, target, actions, rank, Action) def __repr__(self): return ("%s(%r, %r, accel=%r, direction=%r, speed=%r, " "actions=%r, target=%r, vanished=%r)") % ( type(self).__name__, self.x, self.y, (self.mx, self.my), self.direction, self.speed, self._actions, self.target, self.vanished) @property def aim(self): """Angle to the target, in radians. If the target does not exist or cannot be found, return 0. """ try: target_x = self.target.x target_y = self.target.y except AttributeError: return 0 else: return math.atan2(target_x - self.x, self.y - target_y) @property def finished(self): """Check if this bullet is finished running. A bullet is finished when it has vanished, and all its actions have finished. If this is true, the bullet should be removed from the screen. (You will probably want to cull it under other circumstances as well). """ if not self.vanished: return False for action in self._actions: if not action.finished: return False return True def vanish(self): """Vanish this bullet and stop all actions.""" self.vanished = True for action in self._actions: action.vanish() self._actions = [] def replace(self, old, new): """Replace an active action with another. This is mostly used by actions internally to queue children. """ try: idx = self._actions.index(old) except ValueError: pass else: self._actions[idx] = new def step(self, sin=math.sin, cos=math.cos): """Advance by one frame. This updates the position and velocity, and may also set the vanished flag. It returns any new bullets this bullet spawned during this step. """ created = [] for action in self._actions: action.step(self, created, sin, cos) self.px = self.x self.py = self.y self.x += self.mx + sin(self.direction) * self.speed self.y += self.my - cos(self.direction) * self.speed return created