"""BulletML implementation. http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html """ from __future__ import division import math from bulletml import parser # TODO(jfw): This is very non-Pythonic, it's pretty much just the # BulletML reference ActionImpl translated to Python. PI_2 = math.pi * 2 class Action(object): """Running action implementation.""" 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.owner = owner self.accel_frames = 0 self.previous_fire_direction = 0 self.previous_fire_speed = 0 self.params = params self.rank = rank 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): """Advance by one frame.""" created = [] if self.speed_frames > 0: self.speed_frames -= 1 self.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: self.owner.direction += self.owner.aim else: self.owner.direction += self.direction if self.accel_frames > 0: self.accel_frames -= 1 self.owner.mx += self.mx self.owner.my += self.my if self.pc is None: return created if self.wait_frames > 0: self.wait_frames -= 1 return created 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) 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(self.params, self.rank) child = Action( self.owner, self, actions, params, self.rank, repeat) self.owner.replace(self, child) created.extend(child.step()) break elif isinstance(action, (parser.ActionDef, parser.ActionRef)): actions, params = action(self.params, self.rank) child = Action(self.owner, self, actions, params, self.rank) self.owner.replace(self, child) created.extend(child.step()) break elif isinstance(action, (parser.FireDef, parser.FireRef)): direction, speed, actions = action(self.params, self.rank) if direction: direction, type = direction if type == "aim" or type is None: direction += self.owner.aim elif type == "sequence": direction += self.previous_fire_direction elif type == "relative": direction += self.owner.direction else: direction = self.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 -- "0 means that the direction # of this fire and the direction of the bullet # are the same". speed += self.owner.speed else: speed = 1 self.previous_fire_speed = speed bullet = Bullet(self.owner.x, self.owner.y, direction, speed, self.owner.target, actions, self) created.append(bullet) elif isinstance(action, parser.ChangeSpeed): frames, (speed, type) = action(self.params, self.rank) self.speed_frames = frames if type == "sequence": self.speed = speed elif type == "relative": self.speed = speed / frames else: self.speed = (speed - self.owner.speed) / frames elif isinstance(action, parser.ChangeDirection): frames, (direction, type) = action(self.params, self.rank) self.direction_frames = frames self.aiming = False if type == "sequence": self.direction = direction else: if type == "absolute": self.direction = ( direction - self.owner.direction) % PI_2 elif type == "relative": self.direction = direction else: self.aiming = True self.direction = ( direction + self.owner.aim - self.owner.direction) % PI_2 if self.direction > math.pi: self.direction -= PI_2 if self.direction < -math.pi: self.direction += PI_2 self.direction /= self.direction_frames elif isinstance(action, parser.Accel): frames, horizontal, vertical = action(self.params, self.rank) self.accel_frames = frames if horizontal: mx, type = horizontal if type == "sequence": self.mx = mx elif type == "absolute": self.mx = (mx - self.owner.mx) / frames elif type == "relative": self.mx = mx / frames if vertical: my, type = vertical if type == "sequence": self.my = my elif type == "absolute": self.my = (my - self.owner.my) / frames elif type == "relative": self.my = my / frames elif isinstance(action, parser.Wait): self.wait_frames = action(self.params, self.rank) break elif isinstance(action, parser.Vanish): self.owner.vanish() break return created class Bullet(object): """Simple bullet implementation.""" def __init__(self, x=0, y=0, direction=0, speed=0, target=None, actions=(), parent=None, rank=None): 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 if rank is None: rank = parent.rank if parent else 0.5 # New bullets reset the parent hierarchy. self._actions = [Action(self, None, action, params, rank) for action, params in actions] 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.""" if self.target is None: return self.direction else: return math.atan2(self.target.x - self.x, self.y - self.target.y) @property def finished(self): """Check if this bullet is finished running. 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.""" try: idx = self._actions.index(old) except ValueError: pass else: self._actions[idx] = new def step(self): """Advance by one frame. This updates the direction, speed, x, y, px, and py members, and may set the vanished flag. """ created = [] for action in self._actions: created.extend(action.step()) self.px = self.x self.py = self.y self.x += self.mx + math.sin(self.direction) * self.speed self.y += self.my - math.cos(self.direction) * self.speed return created