def _get_spawn_point(self, player: Player) -> Optional[ba.Vec3]: del player # Unused. # In solo-mode, if there's an existing live player on the map, spawn at # whichever spot is farthest from them (keeps the action spread out). if self._solo_mode: living_player = None living_player_pos = None for team in self.teams: for tplayer in team.players: if tplayer.is_alive(): assert tplayer.node ppos = tplayer.node.position living_player = tplayer living_player_pos = ppos break if living_player: assert living_player_pos is not None player_pos = ba.Vec3(living_player_pos) points: List[Tuple[float, ba.Vec3]] = [] for team in self.teams: start_pos = ba.Vec3(self.map.get_start_position(team.id)) points.append( ((start_pos - player_pos).length(), start_pos)) # Hmm.. we need to sorting vectors too? points.sort(key=lambda x: x[0]) return points[-1][1] return None
def _update(self) -> None: # Update one of our bot lists each time through. # First off, remove dead bots from the list. Note that we check # exists() here via the bool operator instead of dead; we want to # keep them around even if they're just a corpse. try: bot_list = self._bot_lists[self._bot_update_list] = ([ b for b in self._bot_lists[self._bot_update_list] if b ]) except Exception: bot_list = [] ba.print_exception('error updating bot list: ' + str(self._bot_lists[self._bot_update_list])) self._bot_update_list = (self._bot_update_list + 1) % self._bot_list_count # Update our list of player points for the bots to use. player_pts = [] for player in ba.getactivity().players: assert isinstance(player, ba.Player) try: if player.is_alive(): assert isinstance(player.actor, Spaz) assert player.actor.node player_pts.append((ba.Vec3(player.actor.node.position), ba.Vec3(player.actor.node.velocity))) except Exception: ba.print_exception('error on bot-set _update') for bot in bot_list: bot.set_player_points(player_pts) bot.update_ai()
def _get_target_player_pt( self) -> tuple[Optional[ba.Vec3], Optional[ba.Vec3]]: """Returns the position and velocity of our target. Both values will be None in the case of no target. """ assert self.node botpt = ba.Vec3(self.node.position) closest_dist: Optional[float] = None closest_vel: Optional[ba.Vec3] = None closest: Optional[ba.Vec3] = None assert self._player_pts is not None for plpt, plvel in self._player_pts: dist = (plpt - botpt).length() # Ignore player-points that are significantly below the bot # (keeps bots from following players off cliffs). if (closest_dist is None or dist < closest_dist) and (plpt[1] > botpt[1] - 5.0): closest_dist = dist closest_vel = plvel closest = plpt if closest_dist is not None: assert closest_vel is not None assert closest is not None return (ba.Vec3(closest[0], closest[1], closest[2]), ba.Vec3(closest_vel[0], closest_vel[1], closest_vel[2])) return None, None
def _update(self) -> None: # Update one of our bot lists each time through. # First off, remove no-longer-existing bots from the list. try: bot_list = self._bot_lists[self._bot_update_list] = ([ b for b in self._bot_lists[self._bot_update_list] if b ]) except Exception: bot_list = [] ba.print_exception('Error updating bot list: ' + str(self._bot_lists[self._bot_update_list])) self._bot_update_list = (self._bot_update_list + 1) % self._bot_list_count # Update our list of player points for the bots to use. player_pts = [] for player in ba.getactivity().players: assert isinstance(player, ba.Player) try: # TODO: could use abstracted player.position here so we # don't have to assume their actor type, but we have no # abstracted velocity as of yet. if player.is_alive(): assert isinstance(player.actor, Spaz) assert player.actor.node player_pts.append((ba.Vec3(player.actor.node.position), ba.Vec3(player.actor.node.velocity))) except Exception: ba.print_exception('Error on bot-set _update.') for bot in bot_list: bot.set_player_points(player_pts) bot.update_ai()
def _update_player_order(self) -> None: # Calc all player distances. for player in self.players: pos: Optional[ba.Vec3] try: pos = player.position except ba.NotFoundError: pos = None if pos is not None: r_index = player.last_region rg1 = self._regions[r_index] r1pt = ba.Vec3(rg1.pos[:3]) rg2 = self._regions[0] if r_index == len( self._regions) - 1 else self._regions[r_index + 1] r2pt = ba.Vec3(rg2.pos[:3]) r2dist = (pos - r2pt).length() amt = 1.0 - (r2dist / (r2pt - r1pt).length()) amt = player.lap + (r_index + amt) * (1.0 / len(self._regions)) player.distance = amt # Sort players by distance and update their ranks. p_list = [(player.distance, player) for player in self.players] p_list.sort(reverse=True, key=lambda x: x[0]) for i, plr in enumerate(p_list): plr[1].rank = i if plr[1].actor: node = plr[1].distance_txt if node: node.text = str(i + 1) if plr[1].is_alive() else ''
def _get_player_spawn_position(self, player: Player) -> Sequence[float]: # Iterate until we find a spawn owned by this team. spawn_count = len(self.map.spawn_by_flag_points) # Get all spawns owned by this team. spawns = [ i for i in range(spawn_count) if self._flags[i].team is player.team ] closest_spawn = 0 closest_distance = 9999.0 # Now find the spawn that's closest to a spawn not owned by us; # we'll use that one. for spawn in spawns: spt = self.map.spawn_by_flag_points[spawn] our_pt = ba.Vec3(spt[0], spt[1], spt[2]) for otherspawn in [ i for i in range(spawn_count) if self._flags[i].team is not player.team ]: spt = self.map.spawn_by_flag_points[otherspawn] their_pt = ba.Vec3(spt[0], spt[1], spt[2]) dist = (their_pt - our_pt).length() if dist < closest_distance: closest_distance = dist closest_spawn = spawn pos = self.map.spawn_by_flag_points[closest_spawn] x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3]) z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5]) pos = (pos[0] + random.uniform(*x_range), pos[1], pos[2] + random.uniform(*z_range)) return pos
def __init__(self, position=(0, 5, 0), direction=(0, 2, 0), source_player=None, owner=None, color=(1, 1, 1)) -> None: super().__init__() self._color = color self.node = ba.newnode('light', delegate=self, attrs={ 'position': position, 'color': self._color }) ba.animate(self.node, 'radius', {0: 0, 0.1: 0.5, 0.5: 0}) self.source_player = source_player self.owner = owner self._life_timer = ba.Timer( 0.5, ba.WeakCall(self.handlemessage, ba.DieMessage())) pos = position vel = tuple(i / 5 for i in ba.Vec3(direction).normalized()) for _ in range(500): # Optimization :( ba.newnode('explosion', owner=self.node, attrs={ 'position': pos, 'radius': 0.2, 'color': self._color }) pos = (pos[0] + vel[0], pos[1] + vel[1], pos[2] + vel[2]) for node in _ba.getnodes(): if node and node.getnodetype() == 'spaz': # pylint: disable=invalid-name m3 = ba.Vec3(position) a = ba.Vec3(direction[2], direction[1], direction[0]) m1 = ba.Vec3(node.position) # pylint: enable=invalid-name # distance between node and line dist = (a * (m1 - m3)).length() / a.length() if dist < 0.3: if node and node != self.owner and node.getdelegate( PlayerSpaz, True).getplayer( ba.Player, True).team != self.owner.team: node.handlemessage(ba.FreezeMessage()) pos = self.node.position hit_dir = (0, 10, 0) node.handlemessage( ba.HitMessage(pos=pos, magnitude=50, velocity_magnitude=50, radius=0, srcnode=self.node, source_player=self.source_player, force_direction=hit_dir))
def _spawn_target(self) -> None: # Generate a few random points; we'll use whichever one is farthest # from our existing targets (don't want overlapping targets). points = [] for _i in range(4): # Calc a random point within a circle. while True: xpos = random.uniform(-1.0, 1.0) ypos = random.uniform(-1.0, 1.0) if xpos * xpos + ypos * ypos < 1.0: break points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) def get_min_dist_from_target(pnt: ba.Vec3) -> float: return min((t.get_dist_from_point(pnt) for t in self._targets)) # If we have existing targets, use the point with the highest # min-distance-from-targets. if self._targets: point = max(points, key=get_min_dist_from_target) else: point = points[0] self._targets.append(Target(position=point))
def _update_player_order(self) -> None: # FIXME: tidy this up # Calc all player distances. for player in self.players: pos: Optional[ba.Vec3] try: assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = ba.Vec3(player.actor.node.position) except Exception: pos = None if pos is not None: r_index = player.gamedata['last_region'] rg1 = self._regions[r_index] r1pt = ba.Vec3(rg1.pos[:3]) rg2 = self._regions[0] if r_index == len( self._regions) - 1 else self._regions[r_index + 1] r2pt = ba.Vec3(rg2.pos[:3]) r2dist = (pos - r2pt).length() amt = 1.0 - (r2dist / (r2pt - r1pt).length()) amt = player.gamedata['lap'] + (r_index + amt) * ( 1.0 / len(self._regions)) player.gamedata['distance'] = amt # Sort players by distance and update their ranks. p_list = [[player.gamedata['distance'], player] for player in self.players] p_list.sort(reverse=True, key=lambda x: x[0]) for i, plr in enumerate(p_list): try: plr[1].gamedata['rank'] = i if plr[1].actor is not None: # noinspection PyUnresolvedReferences node = plr[1].actor.distance_txt if node: node.text = str(i + 1) if plr[1].is_alive() else '' except Exception: ba.print_exception('error updating player orders')
def _update(self): if not self.target: del self # commit suicide because we have no goal in our existing :( return d = ba.Vec3(self.target.position) - ba.Vec3(self.position) if d.length() < 0.1: self._blast() del self return d = d.normalized() * 0.04 from math import sin, cos self.position = (self.position[0] + d.x + sin(ba.time() * 2) * 0.03, self.position[1] + d.y, self.position[2] + d.z + cos(ba.time() * 2) * 0.03) self._sparkle() ba.timer(0.001, ba.WeakCall(self._update))
def _update_bots(self) -> None: bots = self._bots.get_living_bots() for bot in bots: bot.target_flag = None # If we're waiting on a continue, stop here so they don't keep scoring. if self.is_waiting_for_continue(): self._bots.stop_moving() return # If we've got a flag and no player are holding it, find the closest # bot to it, and make them the designated flag-bearer. assert self._flag is not None if self._flag.node: for player in self.players: if player.actor: assert isinstance(player.actor, PlayerSpaz) if (player.actor.is_alive() and player.actor.node.hold_node == self._flag.node): return flagpos = ba.Vec3(self._flag.node.position) closest_bot: Optional[SpazBot] = None closest_dist = 0.0 # Always gets assigned first time through. for bot in bots: # If a bot is picked up, he should forget about the flag. if bot.held_count > 0: continue assert bot.node botpos = ba.Vec3(bot.node.position) botdist = (botpos - flagpos).length() if closest_bot is None or botdist < closest_dist: closest_bot = bot closest_dist = botdist if closest_bot is not None: closest_bot.target_flag = self._flag
def __init__(self, position: Sequence[float]): self._r1 = 0.45 self._r2 = 1.1 self._r3 = 2.0 self._rfudge = 0.15 super().__init__() self._position = ba.Vec3(position) self._hit = False # It can be handy to test with this on to make sure the projection # isn't too far off from the actual object. show_in_space = False loc1 = ba.newnode('locator', attrs={ 'shape': 'circle', 'position': position, 'color': (0, 1, 0), 'opacity': 0.5, 'draw_beauty': show_in_space, 'additive': True }) loc2 = ba.newnode('locator', attrs={ 'shape': 'circleOutline', 'position': position, 'color': (0, 1, 0), 'opacity': 0.3, 'draw_beauty': False, 'additive': True }) loc3 = ba.newnode('locator', attrs={ 'shape': 'circleOutline', 'position': position, 'color': (0, 1, 0), 'opacity': 0.1, 'draw_beauty': False, 'additive': True }) self._nodes = [loc1, loc2, loc3] ba.animate_array(loc1, 'size', 1, {0: [0.0], 0.2: [self._r1 * 2.0]}) ba.animate_array(loc2, 'size', 1, { 0.05: [0.0], 0.25: [self._r2 * 2.0] }) ba.animate_array(loc3, 'size', 1, {0.1: [0.0], 0.3: [self._r3 * 2.0]}) ba.playsound(ba.getsound('laserReverse'))
def shot(self, spaz: Spaz) -> None: """Release a rocket""" time = ba.time() if time - self.last_shot > 0.6: self.last_shot = time center = spaz.node.position_center forward = spaz.node.position_forward direction = [ center[0] - forward[0], forward[1] - center[1], center[2] - forward[2] ] direction[1] = 0.0 mag = 10.0 / ba.Vec3(*direction).length() vel = [v * mag for v in direction] Rocket(position=spaz.node.position, velocity=vel, owner=spaz.getplayer(ba.Player), source_player=spaz.getplayer(ba.Player), color=spaz.node.color).autoretain()
def update_ai(self) -> None: """Should be called periodically to update the spaz' AI.""" # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals if self.update_callback is not None: if self.update_callback(self): # Bot has been handled. return if not self.node: return pos = self.node.position our_pos = ba.Vec3(pos[0], 0, pos[2]) can_attack = True target_pt_raw: Optional[ba.Vec3] target_vel: Optional[ba.Vec3] # If we're a flag-bearer, we're pretty simple-minded - just walk # towards the flag and try to pick it up. if self.target_flag: if self.node.hold_node: holding_flag = (self.node.hold_node.getnodetype() == 'flag') else: holding_flag = False # If we're holding the flag, just walk left. if holding_flag: # Just walk left. self.node.move_left_right = -1.0 self.node.move_up_down = 0.0 # Otherwise try to go pick it up. elif self.target_flag.node: target_pt_raw = ba.Vec3(*self.target_flag.node.position) diff = (target_pt_raw - our_pos) diff = ba.Vec3(diff[0], 0, diff[2]) # Don't care about y. dist = diff.length() to_target = diff.normalized() # If we're holding some non-flag item, drop it. if self.node.hold_node: self.node.pickup_pressed = True self.node.pickup_pressed = False return # If we're a runner, run only when not super-near the flag. if self.run and dist > 3.0: self._running = True self.node.run = 1.0 else: self._running = False self.node.run = 0.0 self.node.move_left_right = to_target.x self.node.move_up_down = -to_target.z if dist < 1.25: self.node.pickup_pressed = True self.node.pickup_pressed = False return # Not a flag-bearer. If we're holding anything but a bomb, drop it. if self.node.hold_node: holding_bomb = (self.node.hold_node.getnodetype() in ['bomb', 'prop']) if not holding_bomb: self.node.pickup_pressed = True self.node.pickup_pressed = False return target_pt_raw, target_vel = self._get_target_player_pt() if target_pt_raw is None: # Use default target if we've got one. if self.target_point_default is not None: target_pt_raw = self.target_point_default target_vel = ba.Vec3(0, 0, 0) can_attack = False # With no target, we stop moving and drop whatever we're holding. else: self.node.move_left_right = 0 self.node.move_up_down = 0 if self.node.hold_node: self.node.pickup_pressed = True self.node.pickup_pressed = False return # We don't want height to come into play. target_pt_raw[1] = 0.0 assert target_vel is not None target_vel[1] = 0.0 dist_raw = (target_pt_raw - our_pos).length() # Use a point out in front of them as real target. # (more out in front the farther from us they are) target_pt = (target_pt_raw + target_vel * dist_raw * 0.3 * self._lead_amount) diff = (target_pt - our_pos) dist = diff.length() to_target = diff.normalized() if self._mode == 'throw': # We can only throw if alive and well. if not self._dead and not self.node.knockout: assert self._throw_release_time is not None time_till_throw = self._throw_release_time - ba.time() if not self.node.hold_node: # If we haven't thrown yet, whip out the bomb. if not self._have_dropped_throw_bomb: self.drop_bomb() self._have_dropped_throw_bomb = True # Otherwise our lack of held node means we successfully # released our bomb; lets retreat now. else: self._mode = 'flee' # Oh crap, we're holding a bomb; better throw it. elif time_till_throw <= 0.0: # Jump and throw. def _safe_pickup(node: ba.Node) -> None: if node and self.node: self.node.pickup_pressed = True self.node.pickup_pressed = False if dist > 5.0: self.node.jump_pressed = True self.node.jump_pressed = False # Throws: ba.timer(0.1, ba.Call(_safe_pickup, self.node)) else: # Throws: ba.timer(0.1, ba.Call(_safe_pickup, self.node)) if self.static: if time_till_throw < 0.3: speed = 1.0 elif time_till_throw < 0.7 and dist > 3.0: speed = -1.0 # Whiplash for long throws. else: speed = 0.02 else: if time_till_throw < 0.7: # Right before throw charge full speed towards target. speed = 1.0 else: # Earlier we can hold or move backward for a whiplash. speed = 0.0125 self.node.move_left_right = to_target.x * speed self.node.move_up_down = to_target.z * -1.0 * speed elif self._mode == 'charge': if random.random() < 0.3: self._charge_speed = random.uniform(self.charge_speed_min, self.charge_speed_max) # If we're a runner we run during charges *except when near # an edge (otherwise we tend to fly off easily). if self.run and dist_raw > self.run_dist_min: self._lead_amount = 0.3 self._running = True self.node.run = 1.0 else: self._lead_amount = 0.01 self._running = False self.node.run = 0.0 self.node.move_left_right = to_target.x * self._charge_speed self.node.move_up_down = to_target.z * -1.0 * self._charge_speed elif self._mode == 'wait': # Every now and then, aim towards our target. # Other than that, just stand there. if ba.time(timeformat=ba.TimeFormat.MILLISECONDS) % 1234 < 100: self.node.move_left_right = to_target.x * (400.0 / 33000) self.node.move_up_down = to_target.z * (-400.0 / 33000) else: self.node.move_left_right = 0 self.node.move_up_down = 0 elif self._mode == 'flee': # Even if we're a runner, only run till we get away from our # target (if we keep running we tend to run off edges). if self.run and dist < 3.0: self._running = True self.node.run = 1.0 else: self._running = False self.node.run = 0.0 self.node.move_left_right = to_target.x * -1.0 self.node.move_up_down = to_target.z # We might wanna switch states unless we're doing a throw # (in which case that's our sole concern). if self._mode != 'throw': # If we're currently charging, keep track of how far we are # from our target. When this value increases it means our charge # is over (ran by them or something). if self._mode == 'charge': if (self._charge_closing_in and self._last_charge_dist < dist < 3.0): self._charge_closing_in = False self._last_charge_dist = dist # If we have a clean shot, throw! if (self.throw_dist_min <= dist < self.throw_dist_max and random.random() < self.throwiness and can_attack): self._mode = 'throw' self._lead_amount = ((0.4 + random.random() * 0.6) if dist_raw > 4.0 else (0.1 + random.random() * 0.4)) self._have_dropped_throw_bomb = False self._throw_release_time = (ba.time() + (1.0 / self.throw_rate) * (0.8 + 1.3 * random.random())) # If we're static, always charge (which for us means barely move). elif self.static: self._mode = 'wait' # If we're too close to charge (and aren't in the middle of an # existing charge) run away. elif dist < self.charge_dist_min and not self._charge_closing_in: # ..unless we're near an edge, in which case we've got no # choice but to charge. if self.map.is_point_near_edge(our_pos, self._running): if self._mode != 'charge': self._mode = 'charge' self._lead_amount = 0.2 self._charge_closing_in = True self._last_charge_dist = dist else: self._mode = 'flee' # We're within charging distance, backed against an edge, # or farther than our max throw distance.. chaaarge! elif (dist < self.charge_dist_max or dist > self.throw_dist_max or self.map.is_point_near_edge(our_pos, self._running)): if self._mode != 'charge': self._mode = 'charge' self._lead_amount = 0.01 self._charge_closing_in = True self._last_charge_dist = dist # We're too close to throw but too far to charge - either run # away or just chill if we're near an edge. elif dist < self.throw_dist_min: # Charge if either we're within charge range or # cant retreat to throw. self._mode = 'flee' # Do some awesome jumps if we're running. # FIXME: pylint: disable=too-many-boolean-expressions if ((self._running and 1.2 < dist < 2.2 and ba.time() - self._last_jump_time > 1.0) or (self.bouncy and ba.time() - self._last_jump_time > 0.4 and random.random() < 0.5)): self._last_jump_time = ba.time() self.node.jump_pressed = True self.node.jump_pressed = False # Throw punches when real close. if dist < (1.6 if self._running else 1.2) and can_attack: if random.random() < self.punchiness: self.on_punch_press() self.on_punch_release()
def _on_bot_spawn(self, spaz: SpazBot) -> None: # We want to move to the left by default. spaz.target_point_default = ba.Vec3(0, 0, 0)
def get_dist_from_point(self, pos: Sequence[float]) -> float: """Given a point, returns distance squared from it.""" return (ba.Vec3(pos) - self._position).length()
def do_hit_at_position(self, pos: Sequence[float], player: ba.Player) -> bool: """Handle a bomb hit at the given position.""" # pylint: disable=too-many-statements from bastd.actor import popuptext activity = self.activity # Ignore hits if the game is over or if we've already been hit if activity.has_ended() or self._hit or not self._nodes: return False diff = (ba.Vec3(pos) - self._position) # Disregard Y difference. Our target point probably isn't exactly # on the ground anyway. diff[1] = 0.0 dist = diff.length() bullseye = False if dist <= self._r3 + self._rfudge: # Inform our activity that we were hit self._hit = True activity.handlemessage(self.TargetHitMessage()) keys: Dict[float, Sequence[float]] = { 0.0: (1.0, 0.0, 0.0), 0.049: (1.0, 0.0, 0.0), 0.05: (1.0, 1.0, 1.0), 0.1: (0.0, 1.0, 0.0) } cdull = (0.3, 0.3, 0.3) popupcolor: Sequence[float] if dist <= self._r1 + self._rfudge: bullseye = True self._nodes[1].color = cdull self._nodes[2].color = cdull ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) popupscale = 1.8 popupcolor = (1, 1, 0, 1) streak = player.gamedata['streak'] points = 10 + min(20, streak * 2) ba.playsound(ba.getsound('bellHigh')) if streak > 0: ba.playsound( ba.getsound( 'orchestraHit4' if streak > 3 else 'orchestraHit3' if streak > 2 else 'orchestraHit2' if streak > 1 else 'orchestraHit')) elif dist <= self._r2 + self._rfudge: self._nodes[0].color = cdull self._nodes[2].color = cdull ba.animate_array(self._nodes[1], 'color', 3, keys, loop=True) popupscale = 1.25 popupcolor = (1, 0.5, 0.2, 1) points = 4 ba.playsound(ba.getsound('bellMed')) else: self._nodes[0].color = cdull self._nodes[1].color = cdull ba.animate_array(self._nodes[2], 'color', 3, keys, loop=True) popupscale = 1.0 popupcolor = (0.8, 0.3, 0.3, 1) points = 2 ba.playsound(ba.getsound('bellLow')) # Award points/etc.. (technically should probably leave this up # to the activity). popupstr = '+' + str(points) # If there's more than 1 player in the game, include their # names and colors so they know who got the hit. if len(activity.players) > 1: popupcolor = ba.safecolor(player.color, target_intensity=0.75) popupstr += ' ' + player.get_name() popuptext.PopupText(popupstr, position=self._position, color=popupcolor, scale=popupscale).autoretain() # Give this player's team points and update the score-board. player.team.gamedata['score'] += points assert isinstance(activity, TargetPracticeGame) activity.update_scoreboard() # Also give this individual player points # (only applies in teams mode). assert activity.stats is not None activity.stats.player_scored(player, points, showpoints=False, screenmessage=False) ba.animate_array(self._nodes[0], 'size', 1, { 0.8: self._nodes[0].size, 1.0: [0.0] }) ba.animate_array(self._nodes[1], 'size', 1, { 0.85: self._nodes[1].size, 1.05: [0.0] }) ba.animate_array(self._nodes[2], 'size', 1, { 0.9: self._nodes[2].size, 1.1: [0.0] }) ba.timer(1.1, ba.Call(self.handlemessage, ba.DieMessage())) return bullseye