class RunaroundGame(ba.CoopGameActivity[Player, Team]): """Game involving trying to bomb bots as they walk through the map.""" name = 'Runaround' description = 'Prevent enemies from reaching the exit.' tips = [ 'Jump just as you\'re throwing to get bombs up to the highest levels.', 'No, you can\'t get up on the ledge. You have to throw bombs.', 'Whip back and forth to get more distance on your throws..' ] default_music = ba.MusicType.MARCHING # How fast our various bot types walk. _bot_speed_map: Dict[Type[SpazBot], float] = { BomberBot: 0.48, BomberBotPro: 0.48, BomberBotProShielded: 0.48, BrawlerBot: 0.57, BrawlerBotPro: 0.57, BrawlerBotProShielded: 0.57, TriggerBot: 0.73, TriggerBotPro: 0.78, TriggerBotProShielded: 0.78, ChargerBot: 1.0, ChargerBotProShielded: 1.0, ExplodeyBot: 1.0, StickyBot: 0.5 } def __init__(self, settings: dict): settings['map'] = 'Tower D' super().__init__(settings) shared = SharedObjects.get() self._preset = Preset(settings.get('preset', 'pro')) self._player_death_sound = ba.getsound('playerDeath') self._new_wave_sound = ba.getsound('scoreHit01') self._winsound = ba.getsound('score') self._cashregistersound = ba.getsound('cashRegister') self._bad_guy_score_sound = ba.getsound('shieldDown') self._heart_tex = ba.gettexture('heart') self._heart_model_opaque = ba.getmodel('heartOpaque') self._heart_model_transparent = ba.getmodel('heartTransparent') self._a_player_has_been_killed = False self._spawn_center = self._map_type.defs.points['spawn1'][0:3] self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3] self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3] self._powerup_spread = ( self._map_type.defs.boxes['powerup_region'][6] * 0.5, self._map_type.defs.boxes['powerup_region'][8] * 0.5) self._score_region_material = ba.Material() self._score_region_material.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_reached_end), )) self._last_wave_end_time = ba.time() self._player_has_picked_up_powerup = False self._scoreboard: Optional[Scoreboard] = None self._game_over = False self._wavenum = 0 self._can_end_wave = True self._score = 0 self._time_bonus = 0 self._score_region: Optional[ba.Actor] = None self._dingsound = ba.getsound('dingSmall') self._dingsoundhigh = ba.getsound('dingSmallHigh') self._exclude_powerups: Optional[List[str]] = None self._have_tnt: Optional[bool] = None self._waves: Optional[List[Wave]] = None self._bots = SpazBotSet() self._tntspawner: Optional[TNTSpawner] = None self._lives_bg: Optional[ba.NodeActor] = None self._start_lives = 10 self._lives = self._start_lives self._lives_text: Optional[ba.NodeActor] = None self._flawless = True self._time_bonus_timer: Optional[ba.Timer] = None self._time_bonus_text: Optional[ba.NodeActor] = None self._time_bonus_mult: Optional[float] = None self._wave_text: Optional[ba.NodeActor] = None self._flawless_bonus: Optional[int] = None self._wave_update_timer: Optional[ba.Timer] = None def on_transition_in(self) -> None: super().on_transition_in() self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), score_split=0.5) self._score_region = ba.NodeActor( ba.newnode('region', attrs={ 'position': self.map.defs.boxes['score_region'][0:3], 'scale': self.map.defs.boxes['score_region'][6:9], 'type': 'box', 'materials': [self._score_region_material] })) def on_begin(self) -> None: super().on_begin() player_count = len(self.players) hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY} if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}: self._exclude_powerups = ['curse'] self._have_tnt = True self._waves = [ Wave(entries=[ Spawn(BomberBot, path=3 if hard else 2), Spawn(BomberBot, path=2), Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2) if player_count > 1 else None, Spawn(BomberBot, path=1) if hard else None, Spawn(BomberBot, path=1) if player_count > 2 else None, Spawn(BomberBot, path=1) if player_count > 3 else None, ]), Wave(entries=[ Spawn(BomberBot, path=1) if hard else None, Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2), Spawn(BomberBot, path=2), Spawn(BomberBot, path=2) if player_count > 3 else None, Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3) if hard else None, Spawn(BrawlerBot, path=3) if player_count > 1 else None, Spawn(BrawlerBot, path=3) if player_count > 2 else None, ]), Wave(entries=[ Spawn(ChargerBot, path=2) if hard else None, Spawn(ChargerBot, path=2) if player_count > 2 else None, Spawn(TriggerBot, path=2), Spawn(TriggerBot, path=2) if player_count > 1 else None, Spacing(duration=3.0), Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2), Spawn(BomberBot, path=3) if hard else None, Spawn(BomberBot, path=3), Spawn(BomberBot, path=3), Spawn(BomberBot, path=3) if player_count > 3 else None, ]), Wave(entries=[ Spawn(TriggerBot, path=1) if hard else None, Spacing(duration=1.0) if hard else None, Spawn(TriggerBot, path=2), Spacing(duration=1.0), Spawn(TriggerBot, path=3), Spacing(duration=1.0), Spawn(TriggerBot, path=1) if hard else None, Spacing(duration=1.0) if hard else None, Spawn(TriggerBot, path=2), Spacing(duration=1.0), Spawn(TriggerBot, path=3), Spacing(duration=1.0), Spawn(TriggerBot, path=1) if ( player_count > 1 and hard) else None, Spacing(duration=1.0), Spawn(TriggerBot, path=2) if player_count > 2 else None, Spacing(duration=1.0), Spawn(TriggerBot, path=3) if player_count > 3 else None, Spacing(duration=1.0), ]), Wave(entries=[ Spawn(ChargerBotProShielded if hard else ChargerBot, path=1), Spawn(BrawlerBot, path=2) if hard else None, Spawn(BrawlerBot, path=2), Spawn(BrawlerBot, path=2), Spawn(BrawlerBot, path=3) if hard else None, Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3) if player_count > 1 else None, Spawn(BrawlerBot, path=3) if player_count > 2 else None, Spawn(BrawlerBot, path=3) if player_count > 3 else None, ]), Wave(entries=[ Spawn(BomberBotProShielded, path=3), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=2), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=1) if hard else None, Spacing(duration=1.0) if hard else None, Spawn(BomberBotProShielded, path=3), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=2), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=1) if hard else None, Spacing(duration=1.5) if hard else None, Spawn(BomberBotProShielded, path=3 ) if player_count > 1 else None, Spacing(duration=1.5), Spawn(BomberBotProShielded, path=2 ) if player_count > 2 else None, Spacing(duration=1.5), Spawn(BomberBotProShielded, path=1 ) if player_count > 3 else None, ]), ] elif self._preset in { Preset.UBER_EASY, Preset.UBER, Preset.TOURNAMENT_UBER }: self._exclude_powerups = [] self._have_tnt = True self._waves = [ Wave(entries=[ Spawn(TriggerBot, path=1) if hard else None, Spawn(TriggerBot, path=2), Spawn(TriggerBot, path=2), Spawn(TriggerBot, path=3), Spawn(BrawlerBotPro if hard else BrawlerBot, point=Point.BOTTOM_LEFT), Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT ) if player_count > 2 else None, ]), Wave(entries=[ Spawn(ChargerBot, path=2), Spawn(ChargerBot, path=3), Spawn(ChargerBot, path=1) if hard else None, Spawn(ChargerBot, path=2), Spawn(ChargerBot, path=3), Spawn(ChargerBot, path=1) if player_count > 2 else None, ]), Wave(entries=[ Spawn(BomberBotProShielded, path=1) if hard else None, Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=3), Spawn(BomberBotProShielded, path=3), Spawn(ChargerBot, point=Point.BOTTOM_RIGHT), Spawn(ChargerBot, point=Point.BOTTOM_LEFT ) if player_count > 2 else None, ]), Wave(entries=[ Spawn(TriggerBotPro, path=1) if hard else None, Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2 ) if player_count > 1 else None, Spawn(TriggerBotPro, path=1 if hard else 2 ) if player_count > 3 else None, ]), Wave(entries=[ Spawn(TriggerBotProShielded if hard else TriggerBotPro, point=Point.BOTTOM_LEFT), Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT ) if hard else None, Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT ) if player_count > 2 else None, Spawn(BomberBot, path=3), Spawn(BomberBot, path=3), Spacing(duration=5.0), Spawn(BrawlerBot, path=2), Spawn(BrawlerBot, path=2), Spacing(duration=5.0), Spawn(TriggerBot, path=1) if hard else None, Spawn(TriggerBot, path=1) if hard else None, ]), Wave(entries=[ Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2) if hard else None, Spawn(StickyBot, point=Point.BOTTOM_RIGHT), Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2), Spawn(StickyBot, point=Point.BOTTOM_RIGHT ) if player_count > 2 else None, Spawn(BomberBotProShielded, path=2), Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT), Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2 ) if player_count > 1 else None, Spacing(duration=5.0), Spawn(StickyBot, point=Point.BOTTOM_LEFT), Spacing(duration=2.0), Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT), ]), ] elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: self._exclude_powerups = [] self._have_tnt = True # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) ba.timer(4.0, self._start_powerup_drops) self.setup_low_life_warning_sound() self._update_scores() # Our TNT spawner (if applicable). if self._have_tnt: self._tntspawner = TNTSpawner(position=self._tntspawnpos) # Make sure to stay out of the way of menu/party buttons in the corner. interface_type = ba.app.interface_type l_offs = (-80 if interface_type == 'small' else -40 if interface_type == 'medium' else 0) self._lives_bg = ba.NodeActor( ba.newnode('image', attrs={ 'texture': self._heart_tex, 'model_opaque': self._heart_model_opaque, 'model_transparent': self._heart_model_transparent, 'attach': 'topRight', 'scale': (90, 90), 'position': (-110 + l_offs, -50), 'color': (1, 0.2, 0.2) })) # FIXME; should not set things based on vr mode. # (won't look right to non-vr connected clients, etc) vrmode = ba.app.vr_mode self._lives_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'right', 'h_align': 'center', 'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0), 'flatness': 1.0 if vrmode else 0.5, 'shadow': 1.0 if vrmode else 0.5, 'vr_depth': 10, 'position': (-113 + l_offs, -69), 'scale': 1.3, 'text': str(self._lives) })) ba.timer(2.0, self._start_updating_waves) def _handle_reached_end(self) -> None: spaz = ba.getcollision().opposingnode.getdelegate(SpazBot, True) if not spaz.is_alive(): return # Ignore bodies flying in. self._flawless = False pos = spaz.node.position ba.playsound(self._bad_guy_score_sound, position=pos) light = ba.newnode('light', attrs={ 'position': pos, 'radius': 0.5, 'color': (1, 0, 0) }) ba.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) ba.timer(1.0, light.delete) spaz.handlemessage( ba.DieMessage(immediate=True, how=ba.DeathType.REACHED_GOAL)) if self._lives > 0: self._lives -= 1 if self._lives == 0: self._bots.stop_moving() self.continue_or_end_game() assert self._lives_text is not None assert self._lives_text.node self._lives_text.node.text = str(self._lives) delay = 0.0 def _safesetattr(node: ba.Node, attr: str, value: Any) -> None: if node: setattr(node, attr, value) for _i in range(4): ba.timer( delay, ba.Call(_safesetattr, self._lives_text.node, 'color', (1, 0, 0, 1.0))) assert self._lives_bg is not None assert self._lives_bg.node ba.timer( delay, ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5)) delay += 0.125 ba.timer( delay, ba.Call(_safesetattr, self._lives_text.node, 'color', (1.0, 1.0, 0.0, 1.0))) ba.timer( delay, ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0)) delay += 0.125 ba.timer( delay, ba.Call(_safesetattr, self._lives_text.node, 'color', (0.8, 0.8, 0.8, 1.0))) def on_continue(self) -> None: self._lives = 3 assert self._lives_text is not None assert self._lives_text.node self._lives_text.node.text = str(self._lives) self._bots.start_moving() def spawn_player(self, player: Player) -> ba.Actor: pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], self._spawn_center[2] + random.uniform(-1.5, 1.5)) spaz = self.spawn_player_spaz(player, position=pos) if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}: spaz.impact_scale = 0.25 # Add the material that causes us to hit the player-wall. spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup return spaz def _on_player_picked_up_powerup(self, player: ba.Actor) -> None: del player # Unused. self._player_has_picked_up_powerup = True def _drop_powerup(self, index: int, poweruptype: str = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)) PowerupBox(position=self.map.powerup_spawn_points[index], poweruptype=poweruptype).autoretain() def _start_powerup_drops(self) -> None: ba.timer(3.0, self._drop_powerups, repeat=True) def _drop_powerups(self, standard_points: bool = False, force_first: str = None) -> None: """Generic powerup drop.""" # If its been a minute since our last wave finished emerging, stop # giving out land-mine powerups. (prevents players from waiting # around for them on purpose and filling the map up) if ba.time() - self._last_wave_end_time > 60.0: extra_excludes = ['land_mines'] else: extra_excludes = [] if standard_points: points = self.map.powerup_spawn_points for i in range(len(points)): ba.timer( 1.0 + i * 0.5, ba.Call(self._drop_powerup, i, force_first if i == 0 else None)) else: pos = (self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1])) # drop one random one somewhere.. assert self._exclude_powerups is not None PowerupBox( position=pos, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups + extra_excludes)).autoretain() def end_game(self) -> None: ba.pushcall(ba.Call(self.do_end, 'defeat')) ba.setmusic(None) ba.playsound(self._player_death_sound) def do_end(self, outcome: str) -> None: """End the game now with the provided outcome.""" if outcome == 'defeat': delay = 2.0 self.fade_to_red() else: delay = 0 score: Optional[int] if self._wavenum >= 2: score = self._score fail_message = None else: score = None fail_message = 'Reach wave 2 to rank.' self.end(delay=delay, results={ 'outcome': outcome, 'score': score, 'fail_message': fail_message, 'playerinfos': self.initialplayerinfos }) def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: self._show_standard_scores_to_beat_ui(scores) def _update_waves(self) -> None: # pylint: disable=too-many-branches # If we have no living bots, go to the next wave. if (self._can_end_wave and not self._bots.have_living_bots() and not self._game_over and self._lives > 0): self._can_end_wave = False self._time_bonus_timer = None self._time_bonus_text = None if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: won = False else: assert self._waves is not None won = (self._wavenum == len(self._waves)) # Reward time bonus. base_delay = 4.0 if won else 0 if self._time_bonus > 0: ba.timer(0, ba.Call(ba.playsound, self._cashregistersound)) ba.timer(base_delay, ba.Call(self._award_time_bonus, self._time_bonus)) base_delay += 1.0 # Reward flawless bonus. if self._wavenum > 0 and self._flawless: ba.timer(base_delay, self._award_flawless_bonus) base_delay += 1.0 self._flawless = True # reset if won: # Completion achievements: if self._preset in {Preset.PRO, Preset.PRO_EASY}: self._award_achievement('Pro Runaround Victory', sound=False) if self._lives == self._start_lives: self._award_achievement('The Wall', sound=False) if not self._player_has_picked_up_powerup: self._award_achievement('Precision Bombing', sound=False) elif self._preset in {Preset.UBER, Preset.UBER_EASY}: self._award_achievement('Uber Runaround Victory', sound=False) if self._lives == self._start_lives: self._award_achievement('The Great Wall', sound=False) if not self._a_player_has_been_killed: self._award_achievement('Stayin\' Alive', sound=False) # Give remaining players some points and have them celebrate. self.show_zoom_message(ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0) self.celebrate(10.0) ba.timer(base_delay, self._award_lives_bonus) base_delay += 1.0 ba.timer(base_delay, self._award_completion_bonus) base_delay += 0.85 ba.playsound(self._winsound) ba.cameraflash() ba.setmusic(ba.MusicType.VICTORY) self._game_over = True ba.timer(base_delay, ba.Call(self.do_end, 'victory')) return self._wavenum += 1 # Short celebration after waves. if self._wavenum > 1: self.celebrate(0.5) ba.timer(base_delay, self._start_next_wave) def _award_completion_bonus(self) -> None: bonus = 200 ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(bonus)), ('${B}', ba.Lstr(resource='completionBonusText'))]), color=(0.7, 0.7, 1.0, 1), scale=1.6, position=(0, 1.5, -1)).autoretain() self._score += bonus self._update_scores() def _award_lives_bonus(self) -> None: bonus = self._lives * 30 ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(bonus)), ('${B}', ba.Lstr(resource='livesBonusText'))]), color=(0.7, 1.0, 0.3, 1), scale=1.3, position=(0, 1, -1)).autoretain() self._score += bonus self._update_scores() def _award_time_bonus(self, bonus: int) -> None: ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(bonus)), ('${B}', ba.Lstr(resource='timeBonusText'))]), color=(1, 1, 0.5, 1), scale=1.0, position=(0, 3, -1)).autoretain() self._score += self._time_bonus self._update_scores() def _award_flawless_bonus(self) -> None: ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(self._flawless_bonus)), ('${B}', ba.Lstr(resource='perfectWaveText')) ]), color=(1, 1, 0.2, 1), scale=1.2, position=(0, 2, -1)).autoretain() assert self._flawless_bonus is not None self._score += self._flawless_bonus self._update_scores() def _start_time_bonus_timer(self) -> None: self._time_bonus_timer = ba.Timer(1.0, self._update_time_bonus, repeat=True) def _start_next_wave(self) -> None: # FIXME: Need to split this up. # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements self.show_zoom_message(ba.Lstr(value='${A} ${B}', subs=[('${A}', ba.Lstr(resource='waveText')), ('${B}', str(self._wavenum))]), scale=1.0, duration=1.0, trail=True) ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) t_sec = 0.0 base_delay = 0.5 delay = 0.0 bot_types: List[Union[Spawn, Spacing, None]] = [] if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: level = self._wavenum target_points = (level + 1) * 8.0 group_count = random.randint(1, 3) entries: List[Union[Spawn, Spacing, None]] = [] spaz_types: List[Tuple[Type[SpazBot], float]] = [] if level < 6: spaz_types += [(BomberBot, 5.0)] if level < 10: spaz_types += [(BrawlerBot, 5.0)] if level < 15: spaz_types += [(TriggerBot, 6.0)] if level > 5: spaz_types += [(TriggerBotPro, 7.5)] * (1 + (level - 5) // 7) if level > 2: spaz_types += [(BomberBotProShielded, 8.0) ] * (1 + (level - 2) // 6) if level > 6: spaz_types += [(TriggerBotProShielded, 12.0) ] * (1 + (level - 6) // 5) if level > 1: spaz_types += ([(ChargerBot, 10.0)] * (1 + (level - 1) // 4)) if level > 7: spaz_types += [(ChargerBotProShielded, 15.0) ] * (1 + (level - 7) // 3) # Bot type, their effect on target points. defender_types: List[Tuple[Type[SpazBot], float]] = [ (BomberBot, 0.9), (BrawlerBot, 0.9), (TriggerBot, 0.85), ] if level > 2: defender_types += [(ChargerBot, 0.75)] if level > 4: defender_types += ([(StickyBot, 0.7)] * (1 + (level - 5) // 6)) if level > 6: defender_types += ([(ExplodeyBot, 0.7)] * (1 + (level - 5) // 5)) if level > 8: defender_types += ([(BrawlerBotProShielded, 0.65)] * (1 + (level - 5) // 4)) if level > 10: defender_types += ([(TriggerBotProShielded, 0.6)] * (1 + (level - 6) // 3)) for group in range(group_count): this_target_point_s = target_points / group_count # Adding spacing makes things slightly harder. rval = random.random() if rval < 0.07: spacing = 1.5 this_target_point_s *= 0.85 elif rval < 0.15: spacing = 1.0 this_target_point_s *= 0.9 else: spacing = 0.0 path = random.randint(1, 3) # Don't allow hard paths on early levels. if level < 3: if path == 1: path = 3 # Easy path. if path == 3: pass # Harder path. elif path == 2: this_target_point_s *= 0.8 # Even harder path. elif path == 1: this_target_point_s *= 0.7 # Looping forward. elif path == 4: this_target_point_s *= 0.7 # Looping backward. elif path == 5: this_target_point_s *= 0.7 # Random. elif path == 6: this_target_point_s *= 0.7 def _add_defender(defender_type: Tuple[Type[SpazBot], float], pnt: Point) -> Tuple[float, Spawn]: # This is ok because we call it immediately. # pylint: disable=cell-var-from-loop return this_target_point_s * defender_type[1], Spawn( defender_type[0], point=pnt) # Add defenders. defender_type1 = defender_types[random.randrange( len(defender_types))] defender_type2 = defender_types[random.randrange( len(defender_types))] defender1 = defender2 = None if ((group == 0) or (group == 1 and level > 3) or (group == 2 and level > 5)): if random.random() < min(0.75, (level - 1) * 0.11): this_target_point_s, defender1 = _add_defender( defender_type1, Point.BOTTOM_LEFT) if random.random() < min(0.75, (level - 1) * 0.04): this_target_point_s, defender2 = _add_defender( defender_type2, Point.BOTTOM_RIGHT) spaz_type = spaz_types[random.randrange(len(spaz_types))] member_count = max( 1, int(round(this_target_point_s / spaz_type[1]))) for i, _member in enumerate(range(member_count)): if path == 4: this_path = i % 3 # Looping forward. elif path == 5: this_path = 3 - (i % 3) # Looping backward. elif path == 6: this_path = random.randint(1, 3) # Random. else: this_path = path entries.append(Spawn(spaz_type[0], path=this_path)) if spacing != 0.0: entries.append(Spacing(duration=spacing)) if defender1 is not None: entries.append(defender1) if defender2 is not None: entries.append(defender2) # Some spacing between groups. rval = random.random() if rval < 0.1: spacing = 5.0 elif rval < 0.5: spacing = 1.0 else: spacing = 1.0 entries.append(Spacing(duration=spacing)) wave = Wave(entries=entries) else: assert self._waves is not None wave = self._waves[self._wavenum - 1] bot_types += wave.entries self._time_bonus_mult = 1.0 this_flawless_bonus = 0 non_runner_spawn_time = 1.0 for info in bot_types: if info is None: continue if isinstance(info, Spacing): t_sec += info.duration continue bot_type = info.type path = info.path self._time_bonus_mult += bot_type.points_mult * 0.02 this_flawless_bonus += bot_type.points_mult * 5 # If its got a position, use that. if info.point is not None: point = info.point else: point = Point.START # Space our our slower bots. delay = base_delay delay /= self._get_bot_speed(bot_type) t_sec += delay * 0.5 tcall = ba.Call( self.add_bot_at_point, point, bot_type, path, 0.1 if point is Point.START else non_runner_spawn_time) ba.timer(t_sec, tcall) t_sec += delay * 0.5 # We can end the wave after all the spawning happens. ba.timer(t_sec - delay * 0.5 + non_runner_spawn_time + 0.01, self._set_can_end_wave) # Reset our time bonus. # In this game we use a constant time bonus so it erodes away in # roughly the same time (since the time limit a wave can take is # relatively constant) ..we then post-multiply a modifier to adjust # points. self._time_bonus = 150 self._flawless_bonus = this_flawless_bonus assert self._time_bonus_mult is not None txtval = ba.Lstr( value='${A}: ${B}', subs=[('${A}', ba.Lstr(resource='timeBonusText')), ('${B}', str(int(self._time_bonus * self._time_bonus_mult))) ]) self._time_bonus_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 0.0, 1), 'shadow': 1.0, 'vr_depth': -30, 'flatness': 1.0, 'position': (0, -60), 'scale': 0.8, 'text': txtval })) ba.timer(t_sec, self._start_time_bonus_timer) # Keep track of when this wave finishes emerging. We wanna stop # dropping land-mines powerups at some point (otherwise a crafty # player could fill the whole map with them) self._last_wave_end_time = ba.time() + t_sec totalwaves = str(len(self._waves)) if self._waves is not None else '??' txtval = ba.Lstr(value='${A} ${B}', subs=[('${A}', ba.Lstr(resource='waveText')), ('${B}', str(self._wavenum) + ('' if self._preset in { Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT } else f'/{totalwaves}'))]) self._wave_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'vr_depth': -10, 'color': (1, 1, 1, 1), 'shadow': 1.0, 'flatness': 1.0, 'position': (0, -40), 'scale': 1.3, 'text': txtval })) def _on_bot_spawn(self, path: int, spaz: SpazBot) -> None: # Add our custom update callback and set some info for this bot. spaz_type = type(spaz) assert spaz is not None spaz.update_callback = self._update_bot # Tack some custom attrs onto the spaz. setattr(spaz, 'r_walk_row', path) setattr(spaz, 'r_walk_speed', self._get_bot_speed(spaz_type)) def add_bot_at_point(self, point: Point, spaztype: Type[SpazBot], path: int, spawn_time: float = 0.1) -> None: """Add the given type bot with the given delay (in seconds).""" # Don't add if the game has ended. if self._game_over: return pos = self.map.defs.points[point.value][:3] self._bots.spawn_bot(spaztype, pos=pos, spawn_time=spawn_time, on_spawn_call=ba.Call(self._on_bot_spawn, path)) def _update_time_bonus(self) -> None: self._time_bonus = int(self._time_bonus * 0.91) if self._time_bonus > 0 and self._time_bonus_text is not None: assert self._time_bonus_text.node assert self._time_bonus_mult self._time_bonus_text.node.text = ba.Lstr( value='${A}: ${B}', subs=[('${A}', ba.Lstr(resource='timeBonusText')), ('${B}', str(int(self._time_bonus * self._time_bonus_mult)))]) else: self._time_bonus_text = None def _start_updating_waves(self) -> None: self._wave_update_timer = ba.Timer(2.0, self._update_waves, repeat=True) def _update_scores(self) -> None: score = self._score if self._preset is Preset.ENDLESS: if score >= 500: self._award_achievement('Runaround Master') if score >= 1000: self._award_achievement('Runaround Wizard') if score >= 2000: self._award_achievement('Runaround God') assert self._scoreboard is not None self._scoreboard.set_team_value(self.teams[0], score, max_score=None) def _update_bot(self, bot: SpazBot) -> bool: # Yup; that's a lot of return statements right there. # pylint: disable=too-many-return-statements if not bool(bot): return True assert bot.node # FIXME: Do this in a type safe way. r_walk_speed: float = getattr(bot, 'r_walk_speed') r_walk_row: int = getattr(bot, 'r_walk_row') speed = r_walk_speed pos = bot.node.position boxes = self.map.defs.boxes # Bots in row 1 attempt the high road.. if r_walk_row == 1: if ba.is_point_in_box(pos, boxes['b4']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True # Row 1 and 2 bots attempt the middle road.. if r_walk_row in [1, 2]: if ba.is_point_in_box(pos, boxes['b1']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True # All bots settle for the third row. if ba.is_point_in_box(pos, boxes['b7']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b2']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b3']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b5']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b6']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ((ba.is_point_in_box(pos, boxes['b8']) and not ba.is_point_in_box(pos, boxes['b9'])) or pos == (0.0, 0.0, 0.0)): # Default to walking right if we're still in the walking area. bot.node.move_left_right = speed bot.node.move_up_down = 0 bot.node.run = 0.0 return True # Revert to normal bot behavior otherwise.. return False def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerScoredMessage): self._score += msg.score self._update_scores() elif isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self._a_player_has_been_killed = True # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): if msg.how is ba.DeathType.REACHED_GOAL: return None pts, importance = msg.spazbot.get_death_points(msg.how) if msg.killerplayer is not None: target: Optional[Sequence[float]] try: assert msg.spazbot is not None assert msg.spazbot.node target = msg.spazbot.node.position except Exception: ba.print_exception() target = None try: if msg.killerplayer: self.stats.player_scored(msg.killerplayer, pts, target=target, kill=True, screenmessage=False, importance=importance) ba.playsound(self._dingsound if importance == 1 else self._dingsoundhigh, volume=0.6) except Exception: ba.print_exception('Error on SpazBotDiedMessage') # Normally we pull scores from the score-set, but if there's no # player lets be explicit. else: self._score += pts self._update_scores() else: return super().handlemessage(msg) return None def _get_bot_speed(self, bot_type: Type[SpazBot]) -> float: speed = self._bot_speed_map.get(bot_type) if speed is None: raise TypeError('Invalid bot type to _get_bot_speed(): ' + str(bot_type)) return speed def _set_can_end_wave(self) -> None: self._can_end_wave = True
class TheLastStandGame(ba.CoopGameActivity[Player, Team]): """Slow motion how-long-can-you-last game.""" name = 'The Last Stand' description = 'Final glorious epic slow motion battle to the death.' tips = [ 'This level never ends, but a high score here\n' 'will earn you eternal respect throughout the world.' ] # Show messages when players die since it matters here. announce_player_deaths = True # And of course the most important part. slow_motion = True default_music = ba.MusicType.EPIC def __init__(self, settings: dict): settings['map'] = 'Rampage' super().__init__(settings) self._new_wave_sound = ba.getsound('scoreHit01') self._winsound = ba.getsound('score') self._cashregistersound = ba.getsound('cashRegister') self._spawn_center = (0, 5.5, -4.14) self._tntspawnpos = (0, 5.5, -6) self._powerup_center = (0, 7, -4.14) self._powerup_spread = (7, 2) self._preset = str(settings.get('preset', 'default')) self._excludepowerups: list[str] = [] self._scoreboard: Optional[Scoreboard] = None self._score = 0 self._bots = SpazBotSet() self._dingsound = ba.getsound('dingSmall') self._dingsoundhigh = ba.getsound('dingSmallHigh') self._tntspawner: Optional[TNTSpawner] = None self._bot_update_interval: Optional[float] = None self._bot_update_timer: Optional[ba.Timer] = None self._powerup_drop_timer = None # For each bot type: [spawnrate, increase, d_increase] self._bot_spawn_types = { BomberBot: SpawnInfo(1.00, 0.00, 0.000), BomberBotPro: SpawnInfo(0.00, 0.05, 0.001), BomberBotProShielded: SpawnInfo(0.00, 0.02, 0.002), BrawlerBot: SpawnInfo(1.00, 0.00, 0.000), BrawlerBotPro: SpawnInfo(0.00, 0.05, 0.001), BrawlerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), TriggerBot: SpawnInfo(0.30, 0.00, 0.000), TriggerBotPro: SpawnInfo(0.00, 0.05, 0.001), TriggerBotProShielded: SpawnInfo(0.00, 0.02, 0.002), ChargerBot: SpawnInfo(0.30, 0.05, 0.000), StickyBot: SpawnInfo(0.10, 0.03, 0.001), ExplodeyBot: SpawnInfo(0.05, 0.02, 0.002) } # yapf: disable def on_transition_in(self) -> None: super().on_transition_in() ba.timer(1.3, ba.Call(ba.playsound, self._new_wave_sound)) self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), score_split=0.5) def on_begin(self) -> None: super().on_begin() # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) ba.timer(2.0, ba.WeakCall(self._start_powerup_drops)) ba.timer(0.001, ba.WeakCall(self._start_bot_updates)) self.setup_low_life_warning_sound() self._update_scores() self._tntspawner = TNTSpawner(position=self._tntspawnpos, respawn_time=10.0) def spawn_player(self, player: Player) -> ba.Actor: pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], self._spawn_center[2] + random.uniform(-1.5, 1.5)) return self.spawn_player_spaz(player, position=pos) def _start_bot_updates(self) -> None: self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) self._update_bots() self._update_bots() if len(self.players) > 2: self._update_bots() if len(self.players) > 3: self._update_bots() self._bot_update_timer = ba.Timer(self._bot_update_interval, ba.WeakCall(self._update_bots)) def _drop_powerup(self, index: int, poweruptype: str = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._excludepowerups)) PowerupBox(position=self.map.powerup_spawn_points[index], poweruptype=poweruptype).autoretain() def _start_powerup_drops(self) -> None: self._powerup_drop_timer = ba.Timer(3.0, ba.WeakCall(self._drop_powerups), repeat=True) def _drop_powerups(self, standard_points: bool = False, force_first: str = None) -> None: """Generic powerup drop.""" from bastd.actor import powerupbox if standard_points: pts = self.map.powerup_spawn_points for i in range(len(pts)): ba.timer( 1.0 + i * 0.5, ba.WeakCall(self._drop_powerup, i, force_first if i == 0 else None)) else: drop_pt = (self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1])) # Drop one random one somewhere. powerupbox.PowerupBox( position=drop_pt, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._excludepowerups)).autoretain() def do_end(self, outcome: str) -> None: """End the game.""" if outcome == 'defeat': self.fade_to_red() self.end(delay=2.0, results={ 'outcome': outcome, 'score': self._score, 'playerinfos': self.initialplayerinfos }) def _update_bots(self) -> None: assert self._bot_update_interval is not None self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) self._bot_update_timer = ba.Timer(self._bot_update_interval, ba.WeakCall(self._update_bots)) botspawnpts: list[Sequence[float]] = [[-5.0, 5.5, -4.14], [0.0, 5.5, -4.14], [5.0, 5.5, -4.14]] dists = [0.0, 0.0, 0.0] playerpts: list[Sequence[float]] = [] for player in self.players: try: if player.is_alive(): assert isinstance(player.actor, PlayerSpaz) assert player.actor.node playerpts.append(player.actor.node.position) except Exception: ba.print_exception('Error updating bots.') for i in range(3): for playerpt in playerpts: dists[i] += abs(playerpt[0] - botspawnpts[i][0]) dists[i] += random.random() * 5.0 # Minor random variation. if dists[0] > dists[1] and dists[0] > dists[2]: spawnpt = botspawnpts[0] elif dists[1] > dists[2]: spawnpt = botspawnpts[1] else: spawnpt = botspawnpts[2] spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], 2.0 * (random.random() - 0.5) + spawnpt[2]) # Normalize our bot type total and find a random number within that. total = 0.0 for spawninfo in self._bot_spawn_types.values(): total += spawninfo.spawnrate randval = random.random() * total # Now go back through and see where this value falls. total = 0 bottype: Optional[type[SpazBot]] = None for spawntype, spawninfo in self._bot_spawn_types.items(): total += spawninfo.spawnrate if randval <= total: bottype = spawntype break spawn_time = 1.0 assert bottype is not None self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) # After every spawn we adjust our ratios slightly to get more # difficult. for spawninfo in self._bot_spawn_types.values(): spawninfo.spawnrate += spawninfo.increase spawninfo.increase += spawninfo.dincrease def _update_scores(self) -> None: score = self._score # Achievements apply to the default preset only. if self._preset == 'default': if score >= 250: self._award_achievement('Last Stand Master') if score >= 500: self._award_achievement('Last Stand Wizard') if score >= 1000: self._award_achievement('Last Stand God') assert self._scoreboard is not None self._scoreboard.set_team_value(self.teams[0], score, max_score=None) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): player = msg.getplayer(Player) self.stats.player_was_killed(player) ba.timer(0.1, self._checkroundover) elif isinstance(msg, ba.PlayerScoredMessage): self._score += msg.score self._update_scores() elif isinstance(msg, SpazBotDiedMessage): pts, importance = msg.spazbot.get_death_points(msg.how) target: Optional[Sequence[float]] if msg.killerplayer: assert msg.spazbot.node target = msg.spazbot.node.position self.stats.player_scored(msg.killerplayer, pts, target=target, kill=True, screenmessage=False, importance=importance) ba.playsound(self._dingsound if importance == 1 else self._dingsoundhigh, volume=0.6) # Normally we pull scores from the score-set, but if there's no # player lets be explicit. else: self._score += pts self._update_scores() else: super().handlemessage(msg) def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None: self._show_standard_scores_to_beat_ui(scores) def end_game(self) -> None: # Tell our bots to celebrate just to rub it in. self._bots.final_celebrate() ba.setmusic(None) ba.pushcall(ba.WeakCall(self.do_end, 'defeat')) def _checkroundover(self) -> None: """End the round if conditions are met.""" if not any(player.is_alive() for player in self.teams[0].players): self.end_game()
class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): """A game where score is based on collecting eggs.""" name = 'Easter Egg Hunt' description = 'Gather eggs!' available_settings = [ba.BoolSetting('Pro Mode', default=False)] scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS) # We're currently hard-coded for one map. @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ['Tower D'] # We support teams, free-for-all, and co-op sessions. @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.CoopSession) or issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession)) def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._last_player_death_time = None self._scoreboard = Scoreboard() self.egg_model = ba.getmodel('egg') self.egg_tex_1 = ba.gettexture('eggTex1') self.egg_tex_2 = ba.gettexture('eggTex2') self.egg_tex_3 = ba.gettexture('eggTex3') self._collect_sound = ba.getsound('powerup01') self._pro_mode = settings.get('Pro Mode', False) self._max_eggs = 1.0 self.egg_material = ba.Material() self.egg_material.add_actions( conditions=('they_have_material', shared.player_material), actions=(('call', 'at_connect', self._on_egg_player_collide), )) self._eggs: List[Egg] = [] self._update_timer: Optional[ba.Timer] = None self._countdown: Optional[OnScreenCountdown] = None self._bots: Optional[SpazBotSet] = None # Base class overrides self.default_music = ba.MusicType.FORWARD_MARCH def on_team_join(self, team: Team) -> None: if self.has_begun(): self._update_scoreboard() # Called when our game actually starts. def on_begin(self) -> None: from bastd.maps import TowerD # There's a player-wall on the tower-d level to prevent # players from getting up on the stairs.. we wanna kill that. gamemap = self.map assert isinstance(gamemap, TowerD) gamemap.player_wall.delete() super().on_begin() self._update_scoreboard() self._update_timer = ba.Timer(0.25, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) ba.timer(4.0, self._countdown.start) self._bots = SpazBotSet() # Spawn evil bunny in co-op only. if isinstance(self.session, ba.CoopSession) and self._pro_mode: self._spawn_evil_bunny() # Overriding the default character spawning. def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player) spaz.connect_controls_to_player() return spaz def _spawn_evil_bunny(self) -> None: assert self._bots is not None self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0) def _on_egg_player_collide(self) -> None: if self.has_ended(): return collision = ba.getcollision() # Be defensive here; we could be hitting the corpse of a player # who just left/etc. try: egg = collision.sourcenode.getdelegate(Egg, True) player = collision.opposingnode.getdelegate(PlayerSpaz, True).getplayer( Player, True) except ba.NotFoundError: return player.team.score += 1 # Displays a +1 (and adds to individual player score in # teams mode). self.stats.player_scored(player, 1, screenmessage=False) if self._max_eggs < 5: self._max_eggs += 1.0 elif self._max_eggs < 10: self._max_eggs += 0.5 elif self._max_eggs < 30: self._max_eggs += 0.3 self._update_scoreboard() ba.playsound(self._collect_sound, 0.5, position=egg.node.position) # Create a flash. light = ba.newnode('light', attrs={ 'position': egg.node.position, 'height_attenuated': False, 'radius': 0.1, 'color': (1, 1, 0) }) ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False) ba.timer(0.200, light.delete) egg.handlemessage(ba.DieMessage()) def _update(self) -> None: # Misc. periodic updating. xpos = random.uniform(-7.1, 6.0) ypos = random.uniform(3.5, 3.5) zpos = random.uniform(-8.2, 3.7) # Prune dead eggs from our list. self._eggs = [e for e in self._eggs if e] # Spawn more eggs if we've got space. if len(self._eggs) < int(self._max_eggs): # Occasionally spawn a land-mine in addition. if self._pro_mode and random.random() < 0.25: mine = Bomb(position=(xpos, ypos, zpos), bomb_type='land_mine').autoretain() mine.arm() else: self._eggs.append(Egg(position=(xpos, ypos, zpos))) # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: # Respawn dead players. if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.respawn_icon = RespawnIcon(player, respawn_time) # Whenever our evil bunny dies, respawn him and spew some eggs. elif isinstance(msg, SpazBotDiedMessage): self._spawn_evil_bunny() assert msg.spazbot.node pos = msg.spazbot.node.position for _i in range(6): spread = 0.4 self._eggs.append( Egg(position=(pos[0] + random.uniform(-spread, spread), pos[1] + random.uniform(-spread, spread), pos[2] + random.uniform(-spread, spread)))) else: # Default handler. return super().handlemessage(msg) return None def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.score) def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results)
class FootballCoopGame(ba.CoopGameActivity[Player, Team]): """Co-op variant of football.""" name = 'Football' tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS, version='B') default_music = ba.MusicType.FOOTBALL # FIXME: Need to update co-op games to use getscoreconfig. def get_score_type(self) -> str: return 'time' def get_instance_description(self) -> Union[str, Sequence]: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'Score ${ARG1} touchdowns.', touchdowns return 'Score a touchdown.' def get_instance_description_short(self) -> Union[str, Sequence]: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'score ${ARG1} touchdowns', touchdowns return 'score a touchdown' def __init__(self, settings: dict): settings['map'] = 'Football Stadium' super().__init__(settings) self._preset = settings.get('preset', 'rookie') # Load some media we need. self._cheer_sound = ba.getsound('cheer') self._boo_sound = ba.getsound('boo') self._chant_sound = ba.getsound('crowdChant') self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._whistle_sound = ba.getsound('refWhistle') self._score_to_win = 21 self._score_region_material = ba.Material() self._score_region_material.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score), )) self._powerup_center = (0, 2, 0) self._powerup_spread = (10, 5.5) self._player_has_dropped_bomb = False self._player_has_punched = False self._scoreboard: Optional[Scoreboard] = None self._flag_spawn_pos: Optional[Sequence[float]] = None self._score_regions: List[ba.NodeActor] = [] self._exclude_powerups: List[str] = [] self._have_tnt = False self._bot_types_initial: Optional[List[Type[SpazBot]]] = None self._bot_types_7: Optional[List[Type[SpazBot]]] = None self._bot_types_14: Optional[List[Type[SpazBot]]] = None self._bot_team: Optional[Team] = None self._starttime_ms: Optional[int] = None self._time_text: Optional[ba.NodeActor] = None self._time_text_input: Optional[ba.NodeActor] = None self._tntspawner: Optional[TNTSpawner] = None self._bots = SpazBotSet() self._bot_spawn_timer: Optional[ba.Timer] = None self._powerup_drop_timer: Optional[ba.Timer] = None self._scoring_team: Optional[Team] = None self._final_time_ms: Optional[int] = None self._time_text_timer: Optional[ba.Timer] = None self._flag_respawn_light: Optional[ba.Actor] = None self._flag: Optional[FootballFlag] = None def on_transition_in(self) -> None: super().on_transition_in() self._scoreboard = Scoreboard() self._flag_spawn_pos = self.map.get_flag_position(None) self._spawn_flag() # Set up the two score regions. defs = self.map.defs self._score_regions.append( ba.NodeActor( ba.newnode('region', attrs={ 'position': defs.boxes['goal1'][0:3], 'scale': defs.boxes['goal1'][6:9], 'type': 'box', 'materials': [self._score_region_material] }))) self._score_regions.append( ba.NodeActor( ba.newnode('region', attrs={ 'position': defs.boxes['goal2'][0:3], 'scale': defs.boxes['goal2'][6:9], 'type': 'box', 'materials': [self._score_region_material] }))) ba.playsound(self._chant_sound) def on_begin(self) -> None: # FIXME: Split this up a bit. # pylint: disable=too-many-statements from bastd.actor import controlsguide super().on_begin() # Show controls help in kiosk mode. if ba.app.demo_mode or ba.app.arcade_mode: controlsguide.ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() assert self.initialplayerinfos is not None abot: Type[SpazBot] bbot: Type[SpazBot] cbot: Type[SpazBot] if self._preset in ['rookie', 'rookie_easy']: self._exclude_powerups = ['curse'] self._have_tnt = False abot = (BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot) self._bot_types_initial = [abot] * len(self.initialplayerinfos) bbot = (BomberBotLite if self._preset == 'rookie_easy' else BomberBot) self._bot_types_7 = ( [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) cbot = (BomberBot if self._preset == 'rookie_easy' else TriggerBot) self._bot_types_14 = ( [cbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) elif self._preset == 'tournament': self._exclude_powerups = [] self._have_tnt = True self._bot_types_initial = ( [BrawlerBot] * (1 if len(self.initialplayerinfos) < 2 else 2)) self._bot_types_7 = ( [TriggerBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) self._bot_types_14 = ( [ChargerBot] * (1 if len(self.initialplayerinfos) < 4 else 2)) elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: self._exclude_powerups = ['curse'] self._have_tnt = True self._bot_types_initial = [ChargerBot] * len( self.initialplayerinfos) abot = (BrawlerBot if self._preset == 'pro' else BrawlerBotLite) typed_bot_list: List[Type[SpazBot]] = [] self._bot_types_7 = ( typed_bot_list + [abot] + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) bbot = (TriggerBotPro if self._preset == 'pro' else TriggerBot) self._bot_types_14 = ( [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) elif self._preset in ['uber', 'uber_easy']: self._exclude_powerups = [] self._have_tnt = True abot = (BrawlerBotPro if self._preset == 'uber' else BrawlerBot) bbot = (TriggerBotPro if self._preset == 'uber' else TriggerBot) typed_bot_list_2: List[Type[SpazBot]] = [] self._bot_types_initial = (typed_bot_list_2 + [StickyBot] + [abot] * len(self.initialplayerinfos)) self._bot_types_7 = ( [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) self._bot_types_14 = ( [ExplodeyBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) else: raise Exception() self.setup_low_life_warning_sound() self._drop_powerups(standard_points=True) ba.timer(4.0, self._start_powerup_drops) # Make a bogus team for our bots. bad_team_name = self.get_team_display_string('Bad Guys') self._bot_team = Team() self._bot_team.manual_init(team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4)) for team in [self.teams[0], self._bot_team]: team.score = 0 self.update_scores() # Time display. starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) assert isinstance(starttime_ms, int) self._starttime_ms = starttime_ms self._time_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 0.5, 1), 'flatness': 0.5, 'shadow': 0.5, 'position': (0, -50), 'scale': 1.3, 'text': '' })) self._time_text_input = ba.NodeActor( ba.newnode('timedisplay', attrs={'showsubseconds': True})) self.globalsnode.connectattr('time', self._time_text_input.node, 'time2') assert self._time_text_input.node assert self._time_text.node self._time_text_input.node.connectattr('output', self._time_text.node, 'text') # Our TNT spawner (if applicable). if self._have_tnt: self._tntspawner = TNTSpawner(position=(0, 1, -1)) self._bots = SpazBotSet() self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) for bottype in self._bot_types_initial: self._spawn_bot(bottype) def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: self._show_standard_scores_to_beat_ui(scores) 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 _spawn_bot(self, spaz_type: Type[SpazBot], immediate: bool = False) -> None: assert self._bot_team is not None pos = self.map.get_start_position(self._bot_team.id) self._bots.spawn_bot(spaz_type, pos=pos, spawn_time=0.001 if immediate else 3.0, on_spawn_call=self._on_bot_spawn) 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 _drop_powerup(self, index: int, poweruptype: str = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)) PowerupBox(position=self.map.powerup_spawn_points[index], poweruptype=poweruptype).autoretain() def _start_powerup_drops(self) -> None: self._powerup_drop_timer = ba.Timer(3.0, self._drop_powerups, repeat=True) def _drop_powerups(self, standard_points: bool = False, poweruptype: str = None) -> None: """Generic powerup drop.""" if standard_points: spawnpoints = self.map.powerup_spawn_points for i, _point in enumerate(spawnpoints): ba.timer(1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype)) else: point = (self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1])) # Drop one random one somewhere. PowerupBox( position=point, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)).autoretain() def _kill_flag(self) -> None: try: assert self._flag is not None self._flag.handlemessage(ba.DieMessage()) except Exception: ba.print_exception('Error in _kill_flag.') def _handle_score(self) -> None: """ a point has been scored """ # FIXME tidy this up # pylint: disable=too-many-branches # Our flag might stick around for a second or two; # we don't want it to be able to score again. assert self._flag is not None if self._flag.scored: return # See which score region it was. region = ba.getcollision().sourcenode i = None for i in range(len(self._score_regions)): if region == self._score_regions[i].node: break for team in [self.teams[0], self._bot_team]: assert team is not None if team.id == i: team.score += 7 # Tell all players (or bots) to celebrate. if i == 0: for player in team.players: if player.actor: player.actor.handlemessage( ba.CelebrateMessage(2.0)) else: self._bots.celebrate(2.0) # If the good guys scored, add more enemies. if i == 0: if self.teams[0].score == 7: assert self._bot_types_7 is not None for bottype in self._bot_types_7: self._spawn_bot(bottype) elif self.teams[0].score == 14: assert self._bot_types_14 is not None for bottype in self._bot_types_14: self._spawn_bot(bottype) ba.playsound(self._score_sound) if i == 0: ba.playsound(self._cheer_sound) else: ba.playsound(self._boo_sound) # Kill the flag (it'll respawn shortly). self._flag.scored = True ba.timer(0.2, self._kill_flag) self.update_scores() light = ba.newnode('light', attrs={ 'position': ba.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0) }) ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) ba.timer(1.0, light.delete) if i == 0: ba.cameraflash(duration=10.0) def end_game(self) -> None: ba.setmusic(None) self._bots.final_celebrate() ba.timer(0.001, ba.Call(self.do_end, 'defeat')) def on_continue(self) -> None: # Subtract one touchdown from the bots and get them moving again. assert self._bot_team is not None self._bot_team.score -= 7 self._bots.start_moving() self.update_scores() def update_scores(self) -> None: """ update scoreboard and check for winners """ # FIXME: tidy this up # pylint: disable=too-many-nested-blocks have_scoring_team = False win_score = self._score_to_win for team in [self.teams[0], self._bot_team]: assert team is not None assert self._scoreboard is not None self._scoreboard.set_team_value(team, team.score, win_score) if team.score >= win_score: if not have_scoring_team: self._scoring_team = team if team is self._bot_team: self.continue_or_end_game() else: ba.setmusic(ba.MusicType.VICTORY) # Completion achievements. assert self._bot_team is not None if self._preset in ['rookie', 'rookie_easy']: self._award_achievement('Rookie Football Victory', sound=False) if self._bot_team.score == 0: self._award_achievement( 'Rookie Football Shutout', sound=False) elif self._preset in ['pro', 'pro_easy']: self._award_achievement('Pro Football Victory', sound=False) if self._bot_team.score == 0: self._award_achievement('Pro Football Shutout', sound=False) elif self._preset in ['uber', 'uber_easy']: self._award_achievement('Uber Football Victory', sound=False) if self._bot_team.score == 0: self._award_achievement( 'Uber Football Shutout', sound=False) if (not self._player_has_dropped_bomb and not self._player_has_punched): self._award_achievement('Got the Moves', sound=False) self._bots.stop_moving() self.show_zoom_message(ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0) self.celebrate(10.0) assert self._starttime_ms is not None self._final_time_ms = int( ba.time(timeformat=ba.TimeFormat.MILLISECONDS) - self._starttime_ms) self._time_text_timer = None assert (self._time_text_input is not None and self._time_text_input.node) self._time_text_input.node.timemax = ( self._final_time_ms) # FIXME: Does this still need to be deferred? ba.pushcall(ba.Call(self.do_end, 'victory')) def do_end(self, outcome: str) -> None: """End the game with the specified outcome.""" if outcome == 'defeat': self.fade_to_red() assert self._final_time_ms is not None scoreval = (None if outcome == 'defeat' else int(self._final_time_ms // 10)) self.end(delay=3.0, results={ 'outcome': outcome, 'score': scoreval, 'score_order': 'decreasing', 'playerinfos': self.initialplayerinfos }) def handlemessage(self, msg: Any) -> Any: """ handle high-level game messages """ if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): # Every time a bad guy dies, spawn a new one. ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) elif isinstance(msg, SpazBotPunchedMessage): if self._preset in ['rookie', 'rookie_easy']: if msg.damage >= 500: self._award_achievement('Super Punch') elif self._preset in ['pro', 'pro_easy']: if msg.damage >= 1000: self._award_achievement('Super Mega Punch') # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, FootballFlag) msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) self._flag_respawn_light = ba.NodeActor( ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'radius': 0.15, 'color': (1.0, 1.0, 0.3) })) assert self._flag_respawn_light.node ba.animate(self._flag_respawn_light.node, 'intensity', { 0: 0, 0.25: 0.15, 0.5: 0 }, loop=True) ba.timer(3.0, self._flag_respawn_light.node.delete) else: return super().handlemessage(msg) return None def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None: del player, bomb # Unused. self._player_has_dropped_bomb = True def _handle_player_punched(self, player: Spaz) -> None: del player # Unused. self._player_has_punched = True def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player, position=self.map.get_start_position( player.team.id)) if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: spaz.impact_scale = 0.25 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) spaz.punch_callback = self._handle_player_punched return spaz def _flash_flag_spawn(self) -> None: light = ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'color': (1, 1, 0) }) ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) ba.timer(1.0, light.delete) def _spawn_flag(self) -> None: ba.playsound(self._swipsound) ba.playsound(self._whistle_sound) self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = FootballFlag(position=self._flag_spawn_pos)
class NinjaFightGame(ba.TeamGameActivity[Player, Team]): """ A co-op game where you try to defeat a group of Ninjas as fast as possible """ name = 'Ninja Fight' description = 'How fast can you defeat the ninjas?' score_info = ba.ScoreInfo(label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: # For now we're hard-coding spawn positions and whatnot # so we need to be sure to specify that we only support # a specific map. return ['Courtyard'] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: # We currently support Co-Op only. return issubclass(sessiontype, ba.CoopSession) # In the constructor we should load any media we need/etc. # ...but not actually create anything yet. def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self._winsound = ba.getsound('score') self._won = False self._timer: Optional[OnScreenTimer] = None self._bots = SpazBotSet() self._preset = str(settings['preset']) # Called when our game is transitioning in but not ready to begin; # we can go ahead and start creating stuff, playing music, etc. def on_transition_in(self) -> None: self.default_music = ba.MusicType.TO_THE_DEATH super().on_transition_in() # Called when our game actually begins. def on_begin(self) -> None: super().on_begin() is_pro = self._preset == 'pro' # In pro mode there's no powerups. if not is_pro: self.setup_standard_powerup_drops() # Make our on-screen timer and start it roughly when our bots appear. self._timer = OnScreenTimer() ba.timer(4.0, self._timer.start) # Spawn some baddies. ba.timer( 1.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(3, 3, -2), spawn_time=3.0)) ba.timer( 2.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(-3, 3, -2), spawn_time=3.0)) ba.timer( 3.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(5, 3, -2), spawn_time=3.0)) ba.timer( 4.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(-5, 3, -2), spawn_time=3.0)) # Add some extras for multiplayer or pro mode. assert self.initial_player_info is not None if len(self.initial_player_info) > 2 or is_pro: ba.timer( 5.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(0, 3, -5), spawn_time=3.0)) if len(self.initial_player_info) > 3 or is_pro: ba.timer( 6.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(0, 3, 1), spawn_time=3.0)) # Called for each spawning player. def spawn_player(self, player: Player) -> ba.Actor: # Let's spawn close to the center. spawn_center = (0, 3, -2) pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], spawn_center[2] + random.uniform(-1.5, 1.5)) return self.spawn_player_spaz(player, position=pos) def _check_if_won(self) -> None: # Simply end the game if there's no living bots. # FIXME: Should also make sure all bots have been spawned; # if spawning is spread out enough that we're able to kill # all living bots before the next spawns, it would incorrectly # count as a win. if not self._bots.have_living_bots(): self._won = True self.end_game() # Called for miscellaneous messages. def handlemessage(self, msg: Any) -> Any: # A player has died. if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) # A spaz-bot has died. elif isinstance(msg, SpazBotDiedMessage): # Unfortunately the bot-set will always tell us there are living # bots if we ask here (the currently-dying bot isn't officially # marked dead yet) ..so lets push a call into the event loop to # check once this guy has finished dying. ba.pushcall(self._check_if_won) # Let the base class handle anything we don't. else: return super().handlemessage(msg) return None # When this is called, we should fill out results and end the game # *regardless* of whether is has been won. (this may be called due # to a tournament ending or other external reason). def end_game(self) -> None: # Stop our on-screen timer so players can see what they got. assert self._timer is not None self._timer.stop() results = ba.TeamGameResults() # If we won, set our score to the elapsed time in milliseconds. # (there should just be 1 team here since this is co-op). # ..if we didn't win, leave scores as default (None) which means # we lost. if self._won: elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0) ba.cameraflash() ba.playsound(self._winsound) for team in self.teams: for player in team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage()) results.set_team_score(team, elapsed_time_ms) # Ends the activity. self.end(results)