class FootballCoopGame(ba.CoopGameActivity[Player, Team]): """ Co-op variant of football """ name = 'Football' tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] score_info = ba.ScoreInfo(scoretype=ba.ScoreType.MILLISECONDS, version='B') default_music = ba.MusicType.FOOTBALL # FIXME: Need to update co-op games to use get_score_info. 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[str, Any]): 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.kiosk_mode: controlsguide.ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() assert self.initial_player_info 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.initial_player_info) bbot = (BomberBotLite if self._preset == 'rookie_easy' else BomberBot) self._bot_types_7 = ( [bbot] * (1 if len(self.initial_player_info) < 3 else 2)) cbot = (BomberBot if self._preset == 'rookie_easy' else TriggerBot) self._bot_types_14 = ( [cbot] * (1 if len(self.initial_player_info) < 3 else 2)) elif self._preset == 'tournament': self._exclude_powerups = [] self._have_tnt = True self._bot_types_initial = ( [BrawlerBot] * (1 if len(self.initial_player_info) < 2 else 2)) self._bot_types_7 = ( [TriggerBot] * (1 if len(self.initial_player_info) < 3 else 2)) self._bot_types_14 = ( [ChargerBot] * (1 if len(self.initial_player_info) < 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.initial_player_info) 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.initial_player_info) < 3 else 2)) bbot = (TriggerBotPro if self._preset == 'pro' else TriggerBot) self._bot_types_14 = ( [bbot] * (1 if len(self.initial_player_info) < 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.initial_player_info)) self._bot_types_7 = ( [bbot] * (1 if len(self.initial_player_info) < 3 else 2)) self._bot_types_14 = ( [ExplodeyBot] * (1 if len(self.initial_player_info) < 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', 'player_info': self.initial_player_info }) 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.initial_player_info is not None respawn_time = 2.0 + len(self.initial_player_info) * 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 EliminationGame(ba.TeamGameActivity[Player, Team]): """Game type where last player(s) left alive win.""" name = 'Elimination' description = 'Last remaining alive wins.' score_info = ba.ScoreInfo(label='Survived', scoretype=ba.ScoreType.SECONDS, none_is_winner=True) # Show messages when players die since it's meaningful here. announce_player_deaths = True @classmethod def get_game_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: settings: List[Tuple[str, Dict[str, Any]]] = [ ('Lives Per Player', { 'default': 1, 'min_value': 1, 'max_value': 10, 'increment': 1 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ('Epic Mode', { 'default': False }), ] if issubclass(sessiontype, ba.DualTeamSession): settings.append(('Solo Mode', {'default': False})) settings.append(('Balance Total Lives', {'default': False})) return settings @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession)) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('melee') def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self._scoreboard = Scoreboard() self._start_time: Optional[float] = None self._vs_text: Optional[ba.Actor] = None self._round_end_timer: Optional[ba.Timer] = None self._epic_mode = bool(settings['Epic Mode']) self._lives_per_player = int(settings['Lives Per Player']) self._time_limit = float(settings['Time Limit']) self._balance_total_lives = bool( settings.get('Balance Total Lives', False)) self._solo_mode = bool(settings.get('Solo Mode', False)) # Base class overrides: self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL) def get_instance_description(self) -> Union[str, Sequence]: return 'Last team standing wins.' if isinstance( self.session, ba.DualTeamSession) else 'Last one standing wins.' def get_instance_description_short(self) -> Union[str, Sequence]: return 'last team standing wins' if isinstance( self.session, ba.DualTeamSession) else 'last one standing wins' def on_player_join(self, player: Player) -> None: # No longer allowing mid-game joiners here; too easy to exploit. if self.has_begun(): # Make sure their team has survival seconds set if they're all dead # (otherwise blocked new ffa players are considered 'still alive' # in score tallying). if (self._get_total_team_lives(player.team) == 0 and player.team.survival_seconds is None): player.team.survival_seconds = 0 ba.screenmessage( ba.Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', player.getname(full=True))]), color=(0, 1, 0), ) return player.lives = self._lives_per_player if self._solo_mode: player.team.spawn_order.append(player) self._update_solo_mode() else: # Create our icon and spawn. player.icons = [Icon(player, position=(0, 50), scale=0.8)] if player.lives > 0: self.spawn_player(player) # Don't waste time doing this until begin. if self.has_begun(): self._update_icons() def on_begin(self) -> None: super().on_begin() self._start_time = ba.time() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() if self._solo_mode: self._vs_text = ba.NodeActor( ba.newnode('text', attrs={ 'position': (0, 105), 'h_attach': 'center', 'h_align': 'center', 'maxwidth': 200, 'shadow': 0.5, 'vr_depth': 390, 'scale': 0.6, 'v_attach': 'bottom', 'color': (0.8, 0.8, 0.3, 1.0), 'text': ba.Lstr(resource='vsText') })) # If balance-team-lives is on, add lives to the smaller team until # total lives match. if (isinstance(self.session, ba.DualTeamSession) and self._balance_total_lives and self.teams[0].players and self.teams[1].players): if self._get_total_team_lives( self.teams[0]) < self._get_total_team_lives(self.teams[1]): lesser_team = self.teams[0] greater_team = self.teams[1] else: lesser_team = self.teams[1] greater_team = self.teams[0] add_index = 0 while (self._get_total_team_lives(lesser_team) < self._get_total_team_lives(greater_team)): lesser_team.players[add_index].lives += 1 add_index = (add_index + 1) % len(lesser_team.players) self._update_icons() # We could check game-over conditions at explicit trigger points, # but lets just do the simple thing and poll it. ba.timer(1.0, self._update, repeat=True) def _update_solo_mode(self) -> None: # For both teams, find the first player on the spawn order list with # lives remaining and spawn them if they're not alive. for team in self.teams: # Prune dead players from the spawn order. team.spawn_order = [p for p in team.spawn_order if p] for player in team.spawn_order: assert isinstance(player, Player) if player.lives > 0: if not player.is_alive(): self.spawn_player(player) break def _update_icons(self) -> None: # pylint: disable=too-many-branches # In free-for-all mode, everyone is just lined up along the bottom. if isinstance(self.session, ba.FreeForAllSession): count = len(self.teams) x_offs = 85 xval = x_offs * (count - 1) * -0.5 for team in self.teams: if len(team.players) == 1: player = team.players[0] for icon in player.icons: icon.set_position_and_scale((xval, 30), 0.7) icon.update_for_lives() xval += x_offs # In teams mode we split up teams. else: if self._solo_mode: # First off, clear out all icons. for player in self.players: player.icons = [] # Now for each team, cycle through our available players # adding icons. for team in self.teams: if team.id == 0: xval = -60 x_offs = -78 else: xval = 60 x_offs = 78 is_first = True test_lives = 1 while True: players_with_lives = [ p for p in team.spawn_order if p and p.lives >= test_lives ] if not players_with_lives: break for player in players_with_lives: player.icons.append( Icon(player, position=(xval, (40 if is_first else 25)), scale=1.0 if is_first else 0.5, name_maxwidth=130 if is_first else 75, name_scale=0.8 if is_first else 1.0, flatness=0.0 if is_first else 1.0, shadow=0.5 if is_first else 1.0, show_death=is_first, show_lives=False)) xval += x_offs * (0.8 if is_first else 0.56) is_first = False test_lives += 1 # Non-solo mode. else: for team in self.teams: if team.id == 0: xval = -50 x_offs = -85 else: xval = 50 x_offs = 85 for player in team.players: for icon in player.icons: icon.set_position_and_scale((xval, 30), 0.7) icon.update_for_lives() xval += x_offs 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 spawn_player(self, player: Player) -> ba.Actor: actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) if not self._solo_mode: ba.timer(0.3, ba.Call(self._print_lives, player)) # If we have any icons, update their state. for icon in player.icons: icon.handle_player_spawned() return actor def _print_lives(self, player: Player) -> None: from bastd.actor import popuptext # We get called in a timer so it's possible our player has left/etc. if not player or not player.is_alive() or not player.node: return popuptext.PopupText('x' + str(player.lives - 1), color=(1, 1, 0, 1), offset=(0, -0.8, 0), random_offset=0.0, scale=1.8, position=player.node.position).autoretain() def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) player.icons = [] # Remove us from spawn-order. if self._solo_mode: if player in player.team.spawn_order: player.team.spawn_order.remove(player) # Update icons in a moment since our team will be gone from the # list then. ba.timer(0, self._update_icons) def _get_total_team_lives(self, team: Team) -> int: return sum(player.lives for player in team.players) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) player: Player = msg.getplayer(Player) player.lives -= 1 if player.lives < 0: ba.print_error( "Got lives < 0 in Elim; this shouldn't happen. solo:" + str(self._solo_mode)) player.lives = 0 # If we have any icons, update their state. for icon in player.icons: icon.handle_player_died() # Play big death sound on our last death # or for every one in solo mode. if self._solo_mode or player.lives == 0: ba.playsound(get_factory().single_player_death_sound) # If we hit zero lives, we're dead (and our team might be too). if player.lives == 0: # If the whole team is now dead, mark their survival time. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None player.team.survival_seconds = int(ba.time() - self._start_time) else: # Otherwise, in regular mode, respawn. if not self._solo_mode: self.respawn_player(player) # In solo, put ourself at the back of the spawn order. if self._solo_mode: player.team.spawn_order.remove(player) player.team.spawn_order.append(player) def _update(self) -> None: if self._solo_mode: # For both teams, find the first player on the spawn order # list with lives remaining and spawn them if they're not alive. for team in self.teams: # Prune dead players from the spawn order. team.spawn_order = [p for p in team.spawn_order if p] for player in team.spawn_order: assert isinstance(player, Player) if player.lives > 0: if not player.is_alive(): self.spawn_player(player) self._update_icons() break # If we're down to 1 or fewer living teams, start a timer to end # the game (allows the dust to settle and draws to occur if deaths # are close enough). if len(self._get_living_teams()) < 2: self._round_end_timer = ba.Timer(0.5, self.end_game) def _get_living_teams(self) -> List[Team]: return [ team for team in self.teams if len(team.players) > 0 and any(player.lives > 0 for player in team.players) ] def end_game(self) -> None: if self.has_ended(): return results = ba.TeamGameResults() self._vs_text = None # Kill our 'vs' if its there. for team in self.teams: results.set_team_score(team, team.survival_seconds) self.end(results=results)
def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B')
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)
class ChosenOneGame(ba.TeamGameActivity[Player, Team]): """ Game involving trying to remain the one 'chosen one' for a set length of time while everyone else tries to kill you and become the chosen one themselves. """ name = 'Chosen One' description = ('Be the chosen one for a length of time to win.\n' 'Kill the chosen one to become it.') game_settings = [ ('Chosen One Time', { 'min_value': 10, 'default': 30, 'increment': 10 }), ('Chosen One Gets Gloves', { 'default': True }), ('Chosen One Gets Shield', { 'default': False }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ('Epic Mode', { 'default': False }), ] score_info = ba.ScoreInfo(label='Time Held') @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('keep_away') def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) self._scoreboard = Scoreboard() self._chosen_one_player: Optional[Player] = None self._swipsound = ba.getsound('swip') self._countdownsounds: Dict[int, ba.Sound] = { 10: ba.getsound('announceTen'), 9: ba.getsound('announceNine'), 8: ba.getsound('announceEight'), 7: ba.getsound('announceSeven'), 6: ba.getsound('announceSix'), 5: ba.getsound('announceFive'), 4: ba.getsound('announceFour'), 3: ba.getsound('announceThree'), 2: ba.getsound('announceTwo'), 1: ba.getsound('announceOne') } self._flag_spawn_pos: Optional[Sequence[float]] = None self._reset_region_material: Optional[ba.Material] = None self._flag: Optional[Flag] = None self._reset_region: Optional[ba.Node] = None self._epic_mode = bool(settings['Epic Mode']) self._chosen_one_time = int(settings['Chosen One Time']) self._time_limit = float(settings['Time Limit']) self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield']) self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves']) # Base class overrides self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.CHOSEN_ONE) def get_instance_description(self) -> Union[str, Sequence]: return 'There can be only one.' def create_team(self, sessionteam: ba.SessionTeam) -> Team: return Team(time_remaining=self._chosen_one_time) def on_team_join(self, team: Team) -> None: self._update_scoreboard() def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) if self._get_chosen_one_player() is player: self._set_chosen_one_player(None) def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) self.project_flag_stand(self._flag_spawn_pos) self._set_chosen_one_player(None) pos = self._flag_spawn_pos ba.timer(1.0, call=self._tick, repeat=True) mat = self._reset_region_material = ba.Material() mat.add_actions(conditions=('they_have_material', ba.sharedobj('player_material')), actions=(('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)))) self._reset_region = ba.newnode('region', attrs={ 'position': (pos[0], pos[1] + 0.75, pos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [mat] }) def _get_chosen_one_player(self) -> Optional[Player]: if self._chosen_one_player: return self._chosen_one_player return None def _handle_reset_collide(self) -> None: # If we have a chosen one, ignore these. if self._get_chosen_one_player() is not None: return delegate = ba.get_collision_info('opposing_node').getdelegate() if isinstance(delegate, PlayerSpaz): player = ba.playercast_o(Player, delegate.getplayer()) if player is not None and player.is_alive(): self._set_chosen_one_player(player) def _flash_flag_spawn(self) -> None: light = ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'color': (1, 1, 1), 'radius': 0.3, 'height_attenuated': False }) ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) ba.timer(1.0, light.delete) def _tick(self) -> None: # Give the chosen one points. player = self._get_chosen_one_player() if player is not None: # This shouldn't happen, but just in case. if not player.is_alive(): ba.print_error('got dead player as chosen one in _tick') self._set_chosen_one_player(None) else: scoring_team = player.team assert self.stats self.stats.player_scored(player, 3, screenmessage=False, display=False) scoring_team.time_remaining = max( 0, scoring_team.time_remaining - 1) # Show the count over their head if scoring_team.time_remaining > 0: if isinstance(player.actor, PlayerSpaz) and player.actor: player.actor.set_score_text( str(scoring_team.time_remaining)) self._update_scoreboard() # announce numbers we have sounds for if scoring_team.time_remaining in self._countdownsounds: ba.playsound( self._countdownsounds[scoring_team.time_remaining]) # Winner! if scoring_team.time_remaining <= 0: self.end_game() else: # (player is None) # This shouldn't happen, but just in case. # (Chosen-one player ceasing to exist should # trigger on_player_leave which resets chosen-one) if self._chosen_one_player is not None: ba.print_error('got nonexistent player as chosen one in _tick') self._set_chosen_one_player(None) def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, self._chosen_one_time - team.time_remaining) self.end(results=results, announce_delay=0) def _set_chosen_one_player(self, player: Optional[Player]) -> None: for p_other in self.players: p_other.chosen_light = None ba.playsound(self._swipsound) if not player: assert self._flag_spawn_pos is not None self._flag = Flag(color=(1, 0.9, 0.2), position=self._flag_spawn_pos, touchable=False) self._chosen_one_player = None # Create a light to highlight the flag; # this will go away when the flag dies. ba.newnode('light', owner=self._flag.node, attrs={ 'position': self._flag_spawn_pos, 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': (1.2, 1.2, 0.4) }) # Also an extra momentary flash. self._flash_flag_spawn() else: if player.actor: self._flag = None self._chosen_one_player = player if self._chosen_one_gets_shield: player.actor.handlemessage(ba.PowerupMessage('shield')) if self._chosen_one_gets_gloves: player.actor.handlemessage(ba.PowerupMessage('punch')) # Use a color that's partway between their team color # and white. color = [ 0.3 + c * 0.7 for c in ba.normalized_color(player.team.color) ] light = player.chosen_light = ba.NodeActor( ba.newnode('light', attrs={ 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.13, 'color': color })) assert light.node ba.animate(light.node, 'intensity', { 0: 1.0, 0.2: 0.4, 0.4: 1.0 }, loop=True) assert isinstance(player.actor, PlayerSpaz) player.actor.node.connectattr('position', light.node, 'position') def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) player = msg.getplayer(Player) if player is self._get_chosen_one_player(): killerplayer = ba.playercast_o(Player, msg.getkillerplayer(Player)) self._set_chosen_one_player(None if ( killerplayer is None or killerplayer is player or not killerplayer.is_alive()) else killerplayer) self.respawn_player(player) else: super().handlemessage(msg) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.time_remaining, self._chosen_one_time, countdown=True)
class KeepAwayGame(ba.TeamGameActivity[Player, Team]): """Game where you try to keep the flag away from your enemies.""" name = 'Keep Away' description = 'Carry the flag for a set length of time.' game_settings = [ ('Hold Time', { 'min_value': 10, 'default': 30, 'increment': 10 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ] score_info = ba.ScoreInfo(label='Time Held') default_music = ba.MusicType.KEEP_AWAY @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession)) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('keep_away') def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self._scoreboard = Scoreboard() self._swipsound = ba.getsound('swip') self._tick_sound = ba.getsound('tick') self._countdownsounds = { 10: ba.getsound('announceTen'), 9: ba.getsound('announceNine'), 8: ba.getsound('announceEight'), 7: ba.getsound('announceSeven'), 6: ba.getsound('announceSix'), 5: ba.getsound('announceFive'), 4: ba.getsound('announceFour'), 3: ba.getsound('announceThree'), 2: ba.getsound('announceTwo'), 1: ba.getsound('announceOne') } self._flag_spawn_pos: Optional[Sequence[float]] = None self._update_timer: Optional[ba.Timer] = None self._holding_players: List[Player] = [] self._flag_state: Optional[FlagState] = None self._flag_light: Optional[ba.Node] = None self._scoring_team: Optional[Team] = None self._flag: Optional[Flag] = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) def get_instance_description(self) -> Union[str, Sequence]: return 'Carry the flag for ${ARG1} seconds.', self._hold_time def get_instance_description_short(self) -> Union[str, Sequence]: return 'carry the flag for ${ARG1} seconds', self._hold_time def create_team(self, sessionteam: ba.SessionTeam) -> Team: return Team(timeremaining=self._hold_time) def on_team_join(self, team: Team) -> None: self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) self._spawn_flag() self._update_timer = ba.Timer(1.0, call=self._tick, repeat=True) self._update_flag_state() Flag.project_stand(self._flag_spawn_pos) def _tick(self) -> None: self._update_flag_state() # Award points to all living players holding the flag. for player in self._holding_players: if player: assert self.stats self.stats.player_scored(player, 3, screenmessage=False, display=False) scoreteam = self._scoring_team if scoreteam is not None: if scoreteam.timeremaining > 0: ba.playsound(self._tick_sound) scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1) self._update_scoreboard() if scoreteam.timeremaining > 0: assert self._flag is not None self._flag.set_score_text(str(scoreteam.timeremaining)) # Announce numbers we have sounds for. if scoreteam.timeremaining in self._countdownsounds: ba.playsound(self._countdownsounds[scoreteam.timeremaining]) # Winner. if scoreteam.timeremaining <= 0: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.timeremaining) self.end(results=results, announce_delay=0) def _update_flag_state(self) -> None: for team in self.teams: team.holdingflag = False self._holding_players = [] for player in self.players: holdingflag = False try: assert isinstance(player.actor, (PlayerSpaz, type(None))) if (player.actor and player.actor.node and player.actor.node.hold_node): holdingflag = ( player.actor.node.hold_node.getnodetype() == 'flag') except Exception: ba.print_exception('exception checking hold flag') if holdingflag: self._holding_players.append(player) player.team.holdingflag = True holdingteams = set(t for t in self.teams if t.holdingflag) prevstate = self._flag_state assert self._flag is not None assert self._flag_light assert self._flag.node if len(holdingteams) > 1: self._flag_state = FlagState.CONTESTED self._scoring_team = None self._flag_light.color = (0.6, 0.6, 0.1) self._flag.node.color = (1.0, 1.0, 0.4) elif len(holdingteams) == 1: holdingteam = list(holdingteams)[0] self._flag_state = FlagState.HELD self._scoring_team = holdingteam self._flag_light.color = ba.normalized_color(holdingteam.color) self._flag.node.color = holdingteam.color else: self._flag_state = FlagState.UNCONTESTED self._scoring_team = None self._flag_light.color = (0.2, 0.2, 0.2) self._flag.node.color = (1, 1, 1) if self._flag_state != prevstate: ba.playsound(self._swipsound) def _spawn_flag(self) -> None: ba.playsound(self._swipsound) self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos) self._flag_state = FlagState.NEW self._flag_light = ba.newnode('light', owner=self._flag.node, attrs={ 'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2) }) assert self._flag.node self._flag.node.connectattr('position', self._flag_light, 'position') self._update_flag_state() def _flash_flag_spawn(self) -> None: light = ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'color': (1, 1, 1), 'radius': 0.3, 'height_attenuated': False }) ba.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True) ba.timer(1.0, light.delete) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.timeremaining, self._hold_time, countdown=True) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) elif isinstance(msg, FlagDiedMessage): self._spawn_flag() elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)): self._update_flag_state() else: super().handlemessage(msg)
class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): """Minigame involving dodging falling bombs.""" name = 'Meteor Shower' description = 'Dodge the falling bombs.' game_settings = [('Epic Mode', {'default': False})] score_info = ba.ScoreInfo(label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B') # Print messages when players die (since its meaningful in this game). announce_player_deaths = True # we're currently hard-coded for one map.. @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ['Rampage'] # 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.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession) or issubclass(sessiontype, ba.CoopSession)) def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self._epic_mode = settings.get('Epic Mode', False) self._last_player_death_time: Optional[float] = None self._meteor_time = 2.0 self._timer: Optional[OnScreenTimer] = None # Some base class overrides: self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL) if self._epic_mode: self.slow_motion = True def on_begin(self) -> None: super().on_begin() # Drop a wave every few seconds.. and every so often drop the time # between waves ..lets have things increase faster if we have fewer # players. delay = 5.0 if len(self.players) > 2 else 2.5 if self._epic_mode: delay *= 0.25 ba.timer(delay, self._decrement_meteor_time, repeat=True) # Kick off the first wave in a few seconds. delay = 3.0 if self._epic_mode: delay *= 0.25 ba.timer(delay, self._set_meteor_timer) self._timer = OnScreenTimer() self._timer.start() # Check for immediate end (if we've only got 1 player, etc). ba.timer(5.0, self._check_end_game) def on_player_join(self, player: Player) -> None: # Don't allow joining after we start # (would enable leave/rejoin tomfoolery). if self.has_begun(): ba.screenmessage( ba.Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', player.get_name(full=True))]), color=(0, 1, 0), ) # For score purposes, mark them as having died right as the # game started. assert self._timer is not None player.death_time = self._timer.getstarttime() return self.spawn_player(player) def on_player_leave(self, player: Player) -> None: # Augment default behavior. super().on_player_leave(player) # A departing player may trigger game-over. self._check_end_game() # overriding the default character spawning.. def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player) # Let's reconnect this player's controls to this # spaz but *without* the ability to attack or pick stuff up. spaz.connect_controls_to_player(enable_punch=False, enable_bomb=False, enable_pickup=False) # Also lets have them make some noise when they die. spaz.play_big_death_sound = True return spaz # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) curtime = ba.time() # Record the player's moment of death. # assert isinstance(msg.spaz.player msg.getplayer(Player).death_time = curtime # In co-op mode, end the game the instant everyone dies # (more accurate looking). # In teams/ffa, allow a one-second fudge-factor so we can # get more draws if players die basically at the same time. if isinstance(self.session, ba.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. ba.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = curtime else: ba.timer(1.0, self._check_end_game) else: # Default handler: super().handlemessage(msg) def _check_end_game(self) -> None: living_team_count = 0 for team in self.teams: for player in team.players: if player.is_alive(): living_team_count += 1 break # In co-op, we go till everyone is dead.. otherwise we go # until one team remains. if isinstance(self.session, ba.CoopSession): if living_team_count <= 0: self.end_game() else: if living_team_count <= 1: self.end_game() def _set_meteor_timer(self) -> None: ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster) def _drop_bomb_cluster(self) -> None: # Random note: code like this is a handy way to plot out extents # and debug things. loc_test = False if loc_test: ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) # Drop several bombs in series. delay = 0.0 for _i in range(random.randrange(1, 3)): # Drop them somewhere within our bounds with velocity pointing # toward the opposite side. pos = (-7.3 + 15.3 * random.random(), 11, -5.5 + 2.1 * random.random()) dropdir = (-1.0 if pos[0] > 0 else 1.0) vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) delay += 0.1 self._set_meteor_timer() def _drop_bomb(self, position: Sequence[float], velocity: Sequence[float]) -> None: Bomb(position=position, velocity=velocity).autoretain() def _decrement_meteor_time(self) -> None: self._meteor_time = max(0.01, self._meteor_time * 0.9) def end_game(self) -> None: cur_time = ba.time() assert self._timer is not None start_time = self._timer.getstarttime() # Mark death-time as now for any still-living players # and award players points for how long they lasted. # (these per-player scores are only meaningful in team-games) for team in self.teams: for player in team.players: survived = False # Throw an extra fudge factor in so teams that # didn't die come out ahead of teams that did. if player.death_time is None: survived = True player.death_time = cur_time + 1 # Award a per-player score depending on how many seconds # they lasted (per-player scores only affect teams mode; # everywhere else just looks at the per-team score). score = int(player.death_time - self._timer.getstarttime()) if survived: score += 50 # A bit extra for survivors. self.stats.player_scored(player, score, screenmessage=False) # Stop updating our time text, and set the final time to match # exactly when our last guy died. self._timer.stop(endtime=self._last_player_death_time) # Ok now calc game results: set a score for each team and then tell # the game to end. results = ba.TeamGameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: # Set the team score to the max time survived by any player on # that team. longest_life = 0.0 for player in team.players: assert player.death_time is not None longest_life = max(longest_life, player.death_time - start_time) # Submit the score value in milliseconds. results.set_team_score(team, int(1000.0 * longest_life)) self.end(results=results)
class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): """Game where a team wins by holding a 'hill' for a set amount of time.""" name = 'King of the Hill' description = 'Secure the flag for a set length of time.' game_settings = [ ('Hold Time', { 'min_value': 10, 'default': 30, 'increment': 10 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ] score_info = ba.ScoreInfo(label='Time Held') @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.MultiTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('king_of_the_hill') def __init__(self, settings: Dict[str, Any]): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() self._swipsound = ba.getsound('swip') self._tick_sound = ba.getsound('tick') self._countdownsounds = { 10: ba.getsound('announceTen'), 9: ba.getsound('announceNine'), 8: ba.getsound('announceEight'), 7: ba.getsound('announceSeven'), 6: ba.getsound('announceSix'), 5: ba.getsound('announceFive'), 4: ba.getsound('announceFour'), 3: ba.getsound('announceThree'), 2: ba.getsound('announceTwo'), 1: ba.getsound('announceOne') } self._flag_pos: Optional[Sequence[float]] = None self._flag_state: Optional[FlagState] = None self._flag: Optional[Flag] = None self._flag_light: Optional[ba.Node] = None self._scoring_team: Optional[ReferenceType[Team]] = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) self._flag_region_material = ba.Material() self._flag_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', ba.Call(self._handle_player_flag_region_collide, True)), ('call', 'at_disconnect', ba.Call(self._handle_player_flag_region_collide, False)), )) # Base class overrides. self.default_music = ba.MusicType.SCARY def get_instance_description(self) -> Union[str, Sequence]: return 'Secure the flag for ${ARG1} seconds.', self._hold_time def get_instance_description_short(self) -> Union[str, Sequence]: return 'secure the flag for ${ARG1} seconds', self._hold_time def create_team(self, sessionteam: ba.SessionTeam) -> Team: return Team(time_remaining=self._hold_time) def on_begin(self) -> None: super().on_begin() shared = SharedObjects.get() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_pos = self.map.get_flag_position(None) ba.timer(1.0, self._tick, repeat=True) self._flag_state = FlagState.NEW Flag.project_stand(self._flag_pos) self._flag = Flag(position=self._flag_pos, touchable=False, color=(1, 1, 1)) self._flag_light = ba.newnode('light', attrs={ 'position': self._flag_pos, 'intensity': 0.2, 'height_attenuated': False, 'radius': 0.4, 'color': (0.2, 0.2, 0.2) }) # Flag region. flagmats = [self._flag_region_material, shared.region_material] ba.newnode('region', attrs={ 'position': self._flag_pos, 'scale': (1.8, 1.8, 1.8), 'type': 'sphere', 'materials': flagmats }) self._update_flag_state() def _tick(self) -> None: self._update_flag_state() # Give holding players points. for player in self.players: if player.time_at_flag > 0: self.stats.player_scored(player, 3, screenmessage=False, display=False) if self._scoring_team is None: scoring_team = None else: scoring_team = self._scoring_team() if scoring_team: if scoring_team.time_remaining > 0: ba.playsound(self._tick_sound) scoring_team.time_remaining = max(0, scoring_team.time_remaining - 1) self._update_scoreboard() if scoring_team.time_remaining > 0: assert self._flag is not None self._flag.set_score_text(str(scoring_team.time_remaining)) # Announce numbers we have sounds for. try: ba.playsound( self._countdownsounds[scoring_team.time_remaining]) except Exception: pass # winner if scoring_team.time_remaining <= 0: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.time_remaining) self.end(results=results, announce_delay=0) def _update_flag_state(self) -> None: holding_teams = set(player.team for player in self.players if player.time_at_flag) prev_state = self._flag_state assert self._flag_light assert self._flag is not None assert self._flag.node if len(holding_teams) > 1: self._flag_state = FlagState.CONTESTED self._scoring_team = None self._flag_light.color = (0.6, 0.6, 0.1) self._flag.node.color = (1.0, 1.0, 0.4) elif len(holding_teams) == 1: holding_team = list(holding_teams)[0] self._flag_state = FlagState.HELD self._scoring_team = weakref.ref(holding_team) self._flag_light.color = ba.normalized_color(holding_team.color) self._flag.node.color = holding_team.color else: self._flag_state = FlagState.UNCONTESTED self._scoring_team = None self._flag_light.color = (0.2, 0.2, 0.2) self._flag.node.color = (1, 1, 1) if self._flag_state != prev_state: ba.playsound(self._swipsound) def _handle_player_flag_region_collide(self, colliding: bool) -> None: try: player = ba.getcollision().opposingnode.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: return # Different parts of us can collide so a single value isn't enough # also don't count it if we're dead (flying heads shouldn't be able to # win the game :-) if colliding and player.is_alive(): player.time_at_flag += 1 else: player.time_at_flag = max(0, player.time_at_flag - 1) self._update_flag_state() def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.time_remaining, self._hold_time, countdown=True) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment default. # No longer can count as time_at_flag once dead. player = msg.getplayer(Player) player.time_at_flag = 0 self._update_flag_state() self.respawn_player(player)
class RaceGame(ba.TeamGameActivity[Player, Team]): """Game of racing around a track.""" name = 'Race' description = 'Run real fast!' score_info = ba.ScoreInfo(label='Time', lower_is_better=True, scoretype=ba.ScoreType.MILLISECONDS) @classmethod def get_game_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: settings: List[Tuple[str, Dict[str, Any]]] = [ ('Laps', { 'min_value': 1, 'default': 3, 'increment': 1 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Mine Spawning', { 'choices': [('No Mines', 0), ('8 Seconds', 8000), ('4 Seconds', 4000), ('2 Seconds', 2000)], 'default': 4000 }), ('Bomb Spawning', { 'choices': [('None', 0), ('8 Seconds', 8000), ('4 Seconds', 4000), ('2 Seconds', 2000), ('1 Second', 1000)], 'default': 2000 }), ('Epic Mode', { 'default': False }), ] # We have some specific settings in teams mode. if issubclass(sessiontype, ba.DualTeamSession): settings.append(('Entire Team Must Finish', {'default': False})) return settings @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.MultiTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('race') def __init__(self, settings: Dict[str, Any]): self._race_started = False super().__init__(settings) self._scoreboard = Scoreboard() self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._last_team_time: Optional[float] = None self._front_race_region: Optional[int] = None self._nub_tex = ba.gettexture('nub') self._beep_1_sound = ba.getsound('raceBeep1') self._beep_2_sound = ba.getsound('raceBeep2') self.race_region_material: Optional[ba.Material] = None self._regions: List[RaceRegion] = [] self._team_finish_pts: Optional[int] = None self._time_text: Optional[ba.Actor] = None self._timer: Optional[OnScreenTimer] = None self._race_mines: Optional[List[RaceMine]] = None self._race_mine_timer: Optional[ba.Timer] = None self._scoreboard_timer: Optional[ba.Timer] = None self._player_order_update_timer: Optional[ba.Timer] = None self._start_lights: Optional[List[ba.Node]] = None self._bomb_spawn_timer: Optional[ba.Timer] = None self._laps = int(settings['Laps']) self._entire_team_must_finish = bool( settings.get('Entire Team Must Finish', False)) self._time_limit = float(settings['Time Limit']) self._mine_spawning = int(settings['Mine Spawning']) self._bomb_spawning = int(settings['Bomb Spawning']) self._epic_mode = bool(settings['Epic Mode']) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC_RACE if self._epic_mode else ba.MusicType.RACE) def get_instance_description(self) -> Union[str, Sequence]: if (isinstance(self.session, ba.DualTeamSession) and self._entire_team_must_finish): t_str = ' Your entire team has to finish.' else: t_str = '' if self._laps > 1: return 'Run ${ARG1} laps.' + t_str, self._laps return 'Run 1 lap.' + t_str def get_instance_description_short(self) -> Union[str, Sequence]: if self._laps > 1: return 'run ${ARG1} laps', self._laps return 'run 1 lap' def on_transition_in(self) -> None: super().on_transition_in() shared = SharedObjects.get() pts = self.map.get_def_points('race_point') mat = self.race_region_material = ba.Material() mat.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_race_point_collide), )) for rpt in pts: self._regions.append(RaceRegion(rpt, len(self._regions))) def _flash_player(self, player: Player, scale: float) -> None: assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = player.actor.node.position light = ba.newnode('light', attrs={ 'position': pos, 'color': (1, 1, 0), 'height_attenuated': False, 'radius': 0.4 }) ba.timer(0.5, light.delete) ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) def _handle_race_point_collide(self) -> None: # FIXME: Tidy this up. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks collision = ba.getcollision() try: region = collision.sourcenode.getdelegate(RaceRegion, True) player = collision.opposingnode.getdelegate(PlayerSpaz, True).getplayer( Player, True) except ba.NotFoundError: return last_region = player.last_region this_region = region.index if last_region != this_region: # If a player tries to skip regions, smite them. # Allow a one region leeway though (its plausible players can get # blown over a region, etc). if this_region > last_region + 2: if player.is_alive(): assert player.actor player.actor.handlemessage(ba.DieMessage()) ba.screenmessage(ba.Lstr( translate=('statements', 'Killing ${NAME} for' ' skipping part of the track!'), subs=[('${NAME}', player.getname(full=True))]), color=(1, 0, 0)) else: # If this player is in first, note that this is the # front-most race-point. if player.rank == 0: self._front_race_region = this_region player.last_region = this_region if last_region >= len(self._regions) - 2 and this_region == 0: team = player.team player.lap = min(self._laps, player.lap + 1) # In teams mode with all-must-finish on, the team lap # value is the min of all team players. # Otherwise its the max. if isinstance(self.session, ba.DualTeamSession ) and self._entire_team_must_finish: team.lap = min([p.lap for p in team.players]) else: team.lap = max([p.lap for p in team.players]) # A player is finishing. if player.lap == self._laps: # In teams mode, hand out points based on the order # players come in. if isinstance(self.session, ba.DualTeamSession): assert self._team_finish_pts is not None if self._team_finish_pts > 0: self.stats.player_scored(player, self._team_finish_pts, screenmessage=False) self._team_finish_pts -= 25 # Flash where the player is. self._flash_player(player, 1.0) player.finished = True assert player.actor player.actor.handlemessage( ba.DieMessage(immediate=True)) # Makes sure noone behind them passes them in rank # while finishing. player.distance = 9999.0 # If the whole team has finished the race. if team.lap == self._laps: ba.playsound(self._score_sound) player.team.finished = True assert self._timer is not None elapsed = ba.time() - self._timer.getstarttime() self._last_team_time = player.team.time = elapsed self._check_end_game() # Team has yet to finish. else: ba.playsound(self._swipsound) # They've just finished a lap but not the race. else: ba.playsound(self._swipsound) self._flash_player(player, 0.3) # Print their lap number over their head. try: assert isinstance(player.actor, PlayerSpaz) mathnode = ba.newnode('math', owner=player.actor.node, attrs={ 'input1': (0, 1.9, 0), 'operation': 'add' }) player.actor.node.connectattr( 'torso_position', mathnode, 'input2') tstr = ba.Lstr(resource='lapNumberText', subs=[('${CURRENT}', str(player.lap + 1)), ('${TOTAL}', str(self._laps)) ]) txtnode = ba.newnode('text', owner=mathnode, attrs={ 'text': tstr, 'in_world': True, 'color': (1, 1, 0, 1), 'scale': 0.015, 'h_align': 'center' }) mathnode.connectattr('output', txtnode, 'position') ba.animate(txtnode, 'scale', { 0.0: 0, 0.2: 0.019, 2.0: 0.019, 2.2: 0 }) ba.timer(2.3, mathnode.delete) except Exception as exc: print('Exception printing lap:', exc) def on_team_join(self, team: Team) -> None: self._update_scoreboard() def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) # A player leaving disqualifies the team if 'Entire Team Must Finish' # is on (otherwise in teams mode everyone could just leave except the # leading player to win). if (isinstance(self.session, ba.DualTeamSession) and self._entire_team_must_finish): ba.screenmessage(ba.Lstr( translate=('statements', '${TEAM} is disqualified because ${PLAYER} left'), subs=[('${TEAM}', player.team.name), ('${PLAYER}', player.getname(full=True))]), color=(1, 1, 0)) player.team.finished = True player.team.time = None player.team.lap = 0 ba.playsound(ba.getsound('boo')) for otherplayer in player.team.players: otherplayer.lap = 0 otherplayer.finished = True try: if otherplayer.actor is not None: otherplayer.actor.handlemessage(ba.DieMessage()) except Exception: ba.print_exception('Error sending diemessages') # Defer so team/player lists will be updated. ba.pushcall(self._check_end_game) def _update_scoreboard(self) -> None: for team in self.teams: distances = [player.distance for player in team.players] if not distances: teams_dist = 0.0 else: if (isinstance(self.session, ba.DualTeamSession) and self._entire_team_must_finish): teams_dist = min(distances) else: teams_dist = max(distances) self._scoreboard.set_team_value( team, teams_dist, self._laps, flash=(teams_dist >= float(self._laps)), show_value=False) def on_begin(self) -> None: from bastd.actor.onscreentimer import OnScreenTimer super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._team_finish_pts = 100 # Throw a timer up on-screen. 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.4, 'text': '' })) self._timer = OnScreenTimer() if self._mine_spawning != 0: self._race_mines = [ RaceMine(point=p, mine=None) for p in self.map.get_def_points('race_mine') ] if self._race_mines: self._race_mine_timer = ba.Timer(0.001 * self._mine_spawning, self._update_race_mine, repeat=True) self._scoreboard_timer = ba.Timer(0.25, self._update_scoreboard, repeat=True) self._player_order_update_timer = ba.Timer(0.25, self._update_player_order, repeat=True) if self.slow_motion: t_scale = 0.4 light_y = 50 else: t_scale = 1.0 light_y = 150 lstart = 7.1 * t_scale inc = 1.25 * t_scale ba.timer(lstart, self._do_light_1) ba.timer(lstart + inc, self._do_light_2) ba.timer(lstart + 2 * inc, self._do_light_3) ba.timer(lstart + 3 * inc, self._start_race) self._start_lights = [] for i in range(4): lnub = ba.newnode('image', attrs={ 'texture': ba.gettexture('nub'), 'opacity': 1.0, 'absolute_scale': True, 'position': (-75 + i * 50, light_y), 'scale': (50, 50), 'attach': 'center' }) ba.animate( lnub, 'opacity', { 4.0 * t_scale: 0, 5.0 * t_scale: 1.0, 12.0 * t_scale: 1.0, 12.5 * t_scale: 0.0 }) ba.timer(13.0 * t_scale, lnub.delete) self._start_lights.append(lnub) self._start_lights[0].color = (0.2, 0, 0) self._start_lights[1].color = (0.2, 0, 0) self._start_lights[2].color = (0.2, 0.05, 0) self._start_lights[3].color = (0.0, 0.3, 0) def _do_light_1(self) -> None: assert self._start_lights is not None self._start_lights[0].color = (1.0, 0, 0) ba.playsound(self._beep_1_sound) def _do_light_2(self) -> None: assert self._start_lights is not None self._start_lights[1].color = (1.0, 0, 0) ba.playsound(self._beep_1_sound) def _do_light_3(self) -> None: assert self._start_lights is not None self._start_lights[2].color = (1.0, 0.3, 0) ba.playsound(self._beep_1_sound) def _start_race(self) -> None: assert self._start_lights is not None self._start_lights[3].color = (0.0, 1.0, 0) ba.playsound(self._beep_2_sound) for player in self.players: if player.actor is not None: try: assert isinstance(player.actor, PlayerSpaz) player.actor.connect_controls_to_player() except Exception: ba.print_exception('Error in race player connects') assert self._timer is not None self._timer.start() if self._bomb_spawning != 0: self._bomb_spawn_timer = ba.Timer(0.001 * self._bomb_spawning, self._spawn_bomb, repeat=True) self._race_started = True 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.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): try: 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 '' except Exception: ba.print_exception('error updating player orders') def _spawn_bomb(self) -> None: if self._front_race_region is None: return region = (self._front_race_region + 3) % len(self._regions) pos = self._regions[region].pos # Don't use the full region so we're less likely to spawn off a cliff. region_scale = 0.8 x_range = ((-0.5, 0.5) if pos[3] == 0 else (-region_scale * pos[3], region_scale * pos[3])) z_range = ((-0.5, 0.5) if pos[5] == 0 else (-region_scale * pos[5], region_scale * pos[5])) pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, pos[2] + random.uniform(*z_range)) ba.timer(random.uniform(0.0, 2.0), ba.WeakCall(self._spawn_bomb_at_pos, pos)) def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: if self.has_ended(): return Bomb(position=pos, bomb_type='normal').autoretain() def _make_mine(self, i: int) -> None: assert self._race_mines is not None rmine = self._race_mines[i] rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') rmine.mine.arm() def _flash_mine(self, i: int) -> None: assert self._race_mines is not None rmine = self._race_mines[i] light = ba.newnode('light', attrs={ 'position': rmine.point[:3], 'color': (1, 0.2, 0.2), 'radius': 0.1, 'height_attenuated': False }) ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) ba.timer(1.0, light.delete) def _update_race_mine(self) -> None: assert self._race_mines is not None m_index = -1 rmine = None for _i in range(3): m_index = random.randrange(len(self._race_mines)) rmine = self._race_mines[m_index] if not rmine.mine: break assert rmine is not None if not rmine.mine: self._flash_mine(m_index) ba.timer(0.95, ba.Call(self._make_mine, m_index)) def spawn_player(self, player: Player) -> ba.Actor: if player.team.finished: # FIXME: This is not type-safe! # This call is expected to always return an Actor! # Perhaps we need something like can_spawn_player()... # noinspection PyTypeChecker return None # type: ignore pos = self._regions[player.last_region].pos # Don't use the full region so we're less likely to spawn off a cliff. region_scale = 0.8 x_range = ((-0.5, 0.5) if pos[3] == 0 else (-region_scale * pos[3], region_scale * pos[3])) z_range = ((-0.5, 0.5) if pos[5] == 0 else (-region_scale * pos[5], region_scale * pos[5])) pos = (pos[0] + random.uniform(*x_range), pos[1], pos[2] + random.uniform(*z_range)) spaz = self.spawn_player_spaz( player, position=pos, angle=90 if not self._race_started else None) assert spaz.node # Prevent controlling of characters before the start of the race. if not self._race_started: spaz.disconnect_controls_from_player() mathnode = ba.newnode('math', owner=spaz.node, attrs={ 'input1': (0, 1.4, 0), 'operation': 'add' }) spaz.node.connectattr('torso_position', mathnode, 'input2') distance_txt = ba.newnode('text', owner=spaz.node, attrs={ 'text': '', 'in_world': True, 'color': (1, 1, 0.4), 'scale': 0.02, 'h_align': 'center' }) player.distance_txt = distance_txt mathnode.connectattr('output', distance_txt, 'position') return spaz def _check_end_game(self) -> None: # If there's no teams left racing, finish. teams_still_in = len([t for t in self.teams if not t.finished]) if teams_still_in == 0: self.end_game() return # Count the number of teams that have completed the race. teams_completed = len( [t for t in self.teams if t.finished and t.time is not None]) if teams_completed > 0: session = self.session # In teams mode its over as soon as any team finishes the race # FIXME: The get_ffa_point_awards code looks dangerous. if isinstance(session, ba.DualTeamSession): self.end_game() else: # In ffa we keep the race going while there's still any points # to be handed out. Find out how many points we have to award # and how many teams have finished, and once that matches # we're done. assert isinstance(session, ba.FreeForAllSession) points_to_award = len(session.get_ffa_point_awards()) if teams_completed >= points_to_award - teams_completed: self.end_game() return def end_game(self) -> None: # Stop updating our time text, and set it to show the exact last # finish time if we have one. (so users don't get upset if their # final time differs from what they see onscreen by a tiny amount) assert self._timer is not None if self._timer.has_started(): self._timer.stop( endtime=None if self._last_team_time is None else ( self._timer.getstarttime() + self._last_team_time)) results = ba.TeamGameResults() for team in self.teams: if team.time is not None: # We store time in seconds, but pass a score in milliseconds. results.set_team_score(team, int(team.time * 1000.0)) else: results.set_team_score(team, None) # We don't announce a winner in ffa mode since its probably been a # while since the first place guy crossed the finish line so it seems # odd to be announcing that now. self.end(results=results, announce_winning_team=isinstance(self.session, ba.DualTeamSession)) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment default behavior. player = msg.getplayer(Player) if not player.finished: self.respawn_player(player, respawn_time=1) else: super().handlemessage(msg)
def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(scoretype=ba.ScoreType.MILLISECONDS, version='B')
def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True)
def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(label='Survived', scoretype=ba.ScoreType.SECONDS, none_is_winner=True)
def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(label='Time Held')
class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): """A game where score is based on collecting eggs.""" name = 'Easter Egg Hunt' description = 'Gather eggs!' game_settings = [('Pro Mode', {'default': False})] score_info = ba.ScoreInfo(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[str, Any]): super().__init__(settings) 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', ba.sharedobj('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[BotSet] = 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 = BotSet() # 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 not self.has_ended(): egg_node, playernode = ba.get_collision_info( 'source_node', 'opposing_node') if egg_node is not None and playernode is not None: egg = egg_node.getdelegate() assert isinstance(egg, Egg) spaz = playernode.getdelegate() assert isinstance(spaz, PlayerSpaz) player = (spaz.getplayer() if hasattr(spaz, 'getplayer') else None) if player and egg: 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) player = msg.getplayer(Player) if not player: return self.stats.player_was_killed(player) # Respawn them shortly. assert self.initial_player_info is not None respawn_time = 2.0 + len(self.initial_player_info) * 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, SpazBotDeathMessage): self._spawn_evil_bunny() assert msg.badguy.node pos = msg.badguy.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. super().handlemessage(msg) 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.TeamGameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results)
def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(label='Score', scoretype=ba.ScoreType.POINTS)