def get_available_settings( cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: settings = [ ba.IntSetting('Laps', min_value=1, default=3, increment=1), ba.IntChoiceSetting( 'Time Limit', default=0, choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], ), ba.IntChoiceSetting( 'Mine Spawning', default=4000, choices=[ ('No Mines', 0), ('8 Seconds', 8000), ('4 Seconds', 4000), ('2 Seconds', 2000), ], ), ba.IntChoiceSetting( 'Bomb Spawning', choices=[ ('None', 0), ('8 Seconds', 8000), ('4 Seconds', 4000), ('2 Seconds', 2000), ('1 Second', 1000), ], default=2000, ), ba.BoolSetting('Epic Mode', default=False), ] # We have some specific settings in teams mode. if issubclass(sessiontype, ba.DualTeamSession): settings.append( ba.BoolSetting('Entire Team Must Finish', default=False)) return settings
def get_available_settings( cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: settings = [ ba.IntSetting( 'Kills to Win Per Player', min_value=1, default=5, increment=1, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] # In teams mode, a suicide gives a point to the other team, but in # free-for-all it subtracts from your own score. By default we clamp # this at zero to benefit new players, but pro players might like to # be able to go negative. (to avoid a strategy of just # suiciding until you get a good drop) if issubclass(sessiontype, ba.FreeForAllSession): settings.append( ba.BoolSetting('Allow Negative Scores', default=False)) return settings
def get_available_settings( cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: settings = [ ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.BoolSetting('Epic Mode', default=False), ] return settings
def get_available_settings( cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]: settings = [ ba.IntSetting( 'Lives Per Player', default=1, min_value=1, max_value=10, increment=1, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] if issubclass(sessiontype, ba.DualTeamSession): settings.append(ba.BoolSetting('Solo Mode', default=False)) settings.append( ba.BoolSetting('Balance Total Lives', default=False)) return settings
class HockeyGame(ba.TeamGameActivity[Player, Team]): """Ice hockey game.""" name = 'Hockey' description = 'Score some goals.' available_settings = [ ba.IntSetting( 'Score to Win', min_value=1, default=1, increment=1, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ] default_music = ba.MusicType.HOCKEY @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.DualTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('hockey') def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() self._cheer_sound = ba.getsound('cheer') self._chant_sound = ba.getsound('crowdChant') self._foghorn_sound = ba.getsound('foghorn') self._swipsound = ba.getsound('swip') self._whistle_sound = ba.getsound('refWhistle') self.puck_model = ba.getmodel('puck') self.puck_tex = ba.gettexture('puckColor') self._puck_sound = ba.getsound('metalHit') self.puck_material = ba.Material() self.puck_material.add_actions(actions=(('modify_part_collision', 'friction', 0.5))) self.puck_material.add_actions(conditions=('they_have_material', shared.pickup_material), actions=('modify_part_collision', 'collide', False)) self.puck_material.add_actions( conditions=( ('we_are_younger_than', 100), 'and', ('they_have_material', shared.object_material), ), actions=('modify_node_collision', 'collide', False), ) self.puck_material.add_actions(conditions=('they_have_material', shared.footing_material), actions=('impact_sound', self._puck_sound, 0.2, 5)) # Keep track of which player last touched the puck self.puck_material.add_actions( conditions=('they_have_material', shared.player_material), actions=(('call', 'at_connect', self._handle_puck_player_collide), )) # We want the puck to kill powerups; not get stopped by them self.puck_material.add_actions( conditions=('they_have_material', PowerupBoxFactory.get().powerup_material), actions=(('modify_part_collision', 'physical', False), ('message', 'their_node', 'at_connect', ba.DieMessage()))) self._score_region_material = ba.Material() self._score_region_material.add_actions( conditions=('they_have_material', self.puck_material), actions=(('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score))) self._puck_spawn_pos: Optional[Sequence[float]] = None self._score_regions: Optional[List[ba.NodeActor]] = None self._puck: Optional[Puck] = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) def get_instance_description(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'Score a goal.' return 'Score ${ARG1} goals.', self._score_to_win def get_instance_description_short(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'score a goal' return 'score ${ARG1} goals', self._score_to_win def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._puck_spawn_pos = self.map.get_flag_position(None) self._spawn_puck() # Set up the two score regions. defs = self.map.defs self._score_regions = [] 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] }))) self._update_scoreboard() ba.playsound(self._chant_sound) def on_team_join(self, team: Team) -> None: self._update_scoreboard() def _handle_puck_player_collide(self) -> None: collision = ba.getcollision() try: puck = collision.sourcenode.getdelegate(Puck, True) player = collision.opposingnode.getdelegate(PlayerSpaz, True).getplayer( Player, True) except ba.NotFoundError: return puck.last_players_to_touch[player.team.id] = player def _kill_puck(self) -> None: self._puck = None def _handle_score(self) -> None: """A point has been scored.""" assert self._puck is not None assert self._score_regions is not None # Our puck might stick around for a second or two # we don't want it to be able to score again. if self._puck.scored: return region = ba.getcollision().sourcenode index = 0 for index, score_region in enumerate(self._score_regions): if region == score_region.node: break for team in self.teams: if team.id == index: scoring_team = team team.score += 1 # Tell all players to celebrate. for player in team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage(2.0)) # If we've got the player from the scoring team that last # touched us, give them points. if (scoring_team.id in self._puck.last_players_to_touch and self._puck.last_players_to_touch[scoring_team.id]): self.stats.player_scored( self._puck.last_players_to_touch[scoring_team.id], 100, big_message=True) # End game if we won. if team.score >= self._score_to_win: self.end_game() ba.playsound(self._foghorn_sound) ba.playsound(self._cheer_sound) self._puck.scored = True # Kill the puck (it'll respawn itself shortly). ba.timer(1.0, self._kill_puck) 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) ba.cameraflash(duration=10.0) self._update_scoreboard() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results) def _update_scoreboard(self) -> None: winscore = self._score_to_win for team in self.teams: self._scoreboard.set_team_value(team, team.score, winscore) def handlemessage(self, msg: Any) -> Any: # Respawn dead players if they're still in the game. if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior... super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) # Respawn dead pucks. elif isinstance(msg, PuckDiedMessage): if not self.has_ended(): ba.timer(3.0, self._spawn_puck) else: super().handlemessage(msg) def _flash_puck_spawn(self) -> None: light = ba.newnode('light', attrs={ 'position': self._puck_spawn_pos, 'height_attenuated': False, 'color': (1, 0, 0) }) ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True) ba.timer(1.0, light.delete) def _spawn_puck(self) -> None: ba.playsound(self._swipsound) ba.playsound(self._whistle_sound) self._flash_puck_spawn() assert self._puck_spawn_pos is not None self._puck = Puck(position=self._puck_spawn_pos)
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.') available_settings = [ ba.IntSetting( 'Chosen One Time', min_value=10, default=30, increment=10, ), ba.BoolSetting('Chosen One Gets Gloves', default=True), ba.BoolSetting('Chosen One Gets Shield', default=False), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] scoreconfig = ba.ScoreConfig(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): 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() shared = SharedObjects.get() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) Flag.project_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', shared.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]: # Should never return invalid references; return None in that case. 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 # Attempt to get a Player controlling a Spaz that we hit. try: player = ba.getcollision().opposingnode.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: return if 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.GameResults() 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: existing = self._get_chosen_one_player() if existing: existing.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 = 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 FootballTeamGame(ba.TeamGameActivity[Player, Team]): """Football game for teams mode.""" name = 'Football' description = 'Get the flag to the enemy end zone.' available_settings = [ ba.IntSetting( 'Score to Win', min_value=7, default=21, increment=7, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ] default_music = ba.MusicType.FOOTBALL @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: # We only support two-team play. return issubclass(sessiontype, ba.DualTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('football') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard: Optional[Scoreboard] = Scoreboard() # Load some media we need. self._cheer_sound = ba.getsound('cheer') 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_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._flag_spawn_pos: Optional[Sequence[float]] = None self._score_regions: List[ba.NodeActor] = [] self._flag: Optional[FootballFlag] = None self._flag_respawn_timer: Optional[ba.Timer] = None self._flag_respawn_light: Optional[ba.NodeActor] = None self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) def get_instance_description(self) -> Union[str, Sequence]: touchdowns = self._score_to_win / 7 # NOTE: if use just touchdowns = self._score_to_win // 7 # and we will need to score, for example, 27 points, # we will be required to score 3 (not 4) goals .. 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 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() 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, ) }))) self._update_scoreboard() ba.playsound(self._chant_sound) def on_team_join(self, team: Team) -> None: self._update_scoreboard() def _kill_flag(self) -> None: self._flag = None def _handle_score(self) -> None: """A point has been scored.""" # Our flag might stick around for a second or two # make sure it doesn't score again. assert self._flag is not None if self._flag.scored: return 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: if team.id == i: team.score += 7 # Tell all players to celebrate. for player in team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage(2.0)) # If someone on this team was last to touch it, # give them points. assert self._flag is not None if (self._flag.last_holding_player and team == self._flag.last_holding_player.team): self.stats.player_scored(self._flag.last_holding_player, 50, big_message=True) # End the game if we won. if team.score >= self._score_to_win: self.end_game() ba.playsound(self._score_sound) ba.playsound(self._cheer_sound) assert self._flag self._flag.scored = True # Kill the flag (it'll respawn shortly). ba.timer(1.0, self._kill_flag) light = ba.newnode('light', attrs={ 'position': ba.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0) }) ba.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) ba.timer(1.0, light.delete) ba.cameraflash(duration=10.0) self._update_scoreboard() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results, announce_delay=0.8) def _update_scoreboard(self) -> None: assert self._scoreboard is not None for team in self.teams: self._scoreboard.set_team_value(team, team.score, self._score_to_win) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, FlagPickedUpMessage): assert isinstance(msg.flag, FootballFlag) try: msg.flag.last_holding_player = msg.node.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: pass msg.flag.held_count += 1 elif isinstance(msg, FlagDroppedMessage): assert isinstance(msg.flag, FootballFlag) msg.flag.held_count -= 1 # Respawn dead players if they're still in the game. elif isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.getplayer(Player)) # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): if not self.has_ended(): self._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, 0.25: 0.15, 0.5: 0 }, loop=True) ba.timer(3.0, self._flag_respawn_light.node.delete) else: # Augment standard behavior. super().handlemessage(msg) 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 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.' available_settings = [ ba.IntSetting( 'Hold Time', min_value=10, default=30, increment=10, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ] scoreconfig = ba.ScoreConfig(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): 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.GameResults() 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 ConquestGame(ba.TeamGameActivity[Player, Team]): """A game where teams try to claim all flags on the map.""" name = 'Conquest' description = 'Secure all flags on the map to win.' available_settings = [ ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.DualTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('conquest') def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._extraflagmat = ba.Material() self._flags: List[ConquestFlag] = [] self._epic_mode = bool(settings['Epic Mode']) self._time_limit = float(settings['Time Limit']) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP) # We want flags to tell us they've been hit but not react physically. self._extraflagmat.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('call', 'at_connect', self._handle_flag_player_collide), )) def get_instance_description(self) -> Union[str, Sequence]: return 'Secure all ${ARG1} flags.', len(self.map.flag_points) def get_instance_description_short(self) -> Union[str, Sequence]: return 'secure all ${ARG1} flags', len(self.map.flag_points) def on_team_join(self, team: Team) -> None: if self.has_begun(): self._update_scores() def on_player_join(self, player: Player) -> None: player.respawn_timer = None # Only spawn if this player's team has a flag currently. if player.team.flags_held > 0: self.spawn_player(player) def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() # Set up flags with marker lights. for i in range(len(self.map.flag_points)): point = self.map.flag_points[i] flag = ConquestFlag(position=point, touchable=False, materials=[self._extraflagmat]) self._flags.append(flag) Flag.project_stand(point) flag.light = ba.newnode('light', owner=flag.node, attrs={ 'position': point, 'intensity': 0.25, 'height_attenuated': False, 'radius': 0.3, 'color': (1, 1, 1) }) # Give teams a flag to start with. for i in range(len(self.teams)): self._flags[i].team = self.teams[i] light = self._flags[i].light assert light node = self._flags[i].node assert node light.color = self.teams[i].color node.color = self.teams[i].color self._update_scores() # Initial joiners didn't spawn due to no flags being owned yet; # spawn them now. for player in self.players: self.spawn_player(player) def _update_scores(self) -> None: for team in self.teams: team.flags_held = 0 for flag in self._flags: if flag.team is not None: flag.team.flags_held += 1 for team in self.teams: # If a team finds themselves with no flags, cancel all # outstanding spawn-timers. if team.flags_held == 0: for player in team.players: player.respawn_timer = None player.respawn_icon = None if team.flags_held == len(self._flags): self.end_game() self._scoreboard.set_team_value(team, team.flags_held, len(self._flags)) def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.flags_held) self.end(results=results) def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None: assert flag.node assert flag.light light = ba.newnode('light', attrs={ 'position': flag.node.position, 'height_attenuated': False, 'color': flag.light.color }) ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) ba.timer(length, light.delete) def _handle_flag_player_collide(self) -> None: collision = ba.getcollision() try: flag = collision.sourcenode.getdelegate(ConquestFlag, True) player = collision.opposingnode.getdelegate(PlayerSpaz, True).getplayer( Player, True) except ba.NotFoundError: return assert flag.light if flag.team is not player.team: flag.team = player.team flag.light.color = player.team.color flag.node.color = player.team.color self.stats.player_scored(player, 10, screenmessage=False) ba.playsound(self._swipsound) self._flash_flag(flag) self._update_scores() # Respawn any players on this team that were in limbo due to the # lack of a flag for their team. for otherplayer in self.players: if (otherplayer.team is flag.team and otherplayer.actor is not None and not otherplayer.is_alive() and otherplayer.respawn_timer is None): self.spawn_player(otherplayer) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn only if this team has a flag. player = msg.getplayer(Player) if player.team.flags_held > 0: self.respawn_player(player) else: player.respawn_timer = None else: super().handlemessage(msg) def spawn_player(self, player: Player) -> ba.Actor: # We spawn players at different places based on what flags are held. return self.spawn_player_spaz(player, self._get_player_spawn_position(player)) def _get_player_spawn_position(self, player: Player) -> Sequence[float]: # Iterate until we find a spawn owned by this team. spawn_count = len(self.map.spawn_by_flag_points) # Get all spawns owned by this team. spawns = [ i for i in range(spawn_count) if self._flags[i].team is player.team ] closest_spawn = 0 closest_distance = 9999.0 # Now find the spawn that's closest to a spawn not owned by us; # we'll use that one. for spawn in spawns: spt = self.map.spawn_by_flag_points[spawn] our_pt = ba.Vec3(spt[0], spt[1], spt[2]) for otherspawn in [ i for i in range(spawn_count) if self._flags[i].team is not player.team ]: spt = self.map.spawn_by_flag_points[otherspawn] their_pt = ba.Vec3(spt[0], spt[1], spt[2]) dist = (their_pt - our_pt).length() if dist < closest_distance: closest_distance = dist closest_spawn = spawn pos = self.map.spawn_by_flag_points[closest_spawn] x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3]) z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5]) pos = (pos[0] + random.uniform(*x_range), pos[1], pos[2] + random.uniform(*z_range)) return pos
class AssaultGame(ba.TeamGameActivity[Player, Team]): """Game where you score by touching the other team's flag.""" name = 'Assault' description = 'Reach the enemy flag to score.' available_settings = [ ba.IntSetting( 'Score to Win', min_value=1, default=3, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.DualTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('team_flag') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._last_score_time = 0.0 self._score_sound = ba.getsound('score') self._base_region_materials: Dict[int, ba.Material] = {} self._epic_mode = bool(settings['Epic Mode']) self._score_to_win = int(settings['Score to Win']) self._time_limit = float(settings['Time Limit']) # Base class overrides self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH) def get_instance_description(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'Touch the enemy flag.' return 'Touch the enemy flag ${ARG1} times.', self._score_to_win def get_instance_description_short(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'touch 1 flag' return 'touch ${ARG1} flags', self._score_to_win def create_team(self, sessionteam: ba.SessionTeam) -> Team: shared = SharedObjects.get() base_pos = self.map.get_flag_position(sessionteam.id) ba.newnode('light', attrs={ 'position': base_pos, 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': sessionteam.color }) Flag.project_stand(base_pos) flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) team = Team(base_pos=base_pos, flag=flag) mat = self._base_region_materials[sessionteam.id] = 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', ba.Call(self._handle_base_collide, team)), ), ) ba.newnode('region', owner=flag.node, attrs={ 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [self._base_region_materials[sessionteam.id]] }) return team def on_team_join(self, team: Team) -> None: # Can't do this in create_team because the team's color/etc. have # not been wired up yet at that point. self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment standard. self.respawn_player(msg.getplayer(Player)) else: super().handlemessage(msg) def _flash_base(self, team: Team, length: float = 2.0) -> None: light = ba.newnode('light', attrs={ 'position': team.base_pos, 'height_attenuated': False, 'radius': 0.3, 'color': team.color }) ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) ba.timer(length, light.delete) def _handle_base_collide(self, team: Team) -> None: try: player = ba.getcollision().opposingnode.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: return if not player.is_alive(): return # If its another team's player, they scored. player_team = player.team if player_team is not team: # Prevent multiple simultaneous scores. if ba.time() != self._last_score_time: self._last_score_time = ba.time() self.stats.player_scored(player, 50, big_message=True) ba.playsound(self._score_sound) self._flash_base(team) # Move all players on the scoring team back to their start # and add flashes of light so its noticeable. for player in player_team.players: if player.is_alive(): pos = player.node.position light = ba.newnode('light', attrs={ 'position': pos, 'color': player_team.color, 'height_attenuated': False, 'radius': 0.4 }) ba.timer(0.5, light.delete) ba.animate(light, 'intensity', { 0: 0, 0.1: 1.0, 0.5: 0 }) new_pos = (self.map.get_start_position(player_team.id)) light = ba.newnode('light', attrs={ 'position': new_pos, 'color': player_team.color, 'radius': 0.4, 'height_attenuated': False }) ba.timer(0.5, light.delete) ba.animate(light, 'intensity', { 0: 0, 0.1: 1.0, 0.5: 0 }) if player.actor: player.actor.handlemessage( ba.StandMessage(new_pos, random.uniform(0, 360))) # Have teammates celebrate. for player in player_team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage(2.0)) player_team.score += 1 self._update_scoreboard() if player_team.score >= self._score_to_win: self.end_game() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.score, self._score_to_win)
class StickyStormCTFGame(ba.TeamGameActivity[Player, Team]): """Game of stealing other team's flag and returning it to your base.""" name = 'Sticky Storm CTF' description = 'Return the enemy flag to score in a sticky rain' available_settings = [ ba.IntSetting('Score to Win', min_value=1, default=3), ba.IntSetting( 'Flag Touch Return Time', min_value=0, default=0, increment=1, ), ba.IntSetting( 'Flag Idle Return Time', min_value=5, default=30, increment=5, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.DualTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('team_flag') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._alarmsound = ba.getsound('alarm') self._ticking_sound = ba.getsound('ticking') self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._last_score_time = 0 self._all_bases_material = ba.Material() self._last_home_flag_notice_print_time = 0.0 self._score_to_win = int(settings['Score to Win']) self._epic_mode = bool(settings['Epic Mode']) self._time_limit = float(settings['Time Limit']) self.flag_touch_return_time = float(settings['Flag Touch Return Time']) self.flag_idle_return_time = float(settings['Flag Idle Return Time']) self._meteor_time = 2.0 # Base class overrides. self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER) def get_instance_description(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'Steal the enemy flag.' return 'Steal the enemy flag ${ARG1} times.', self._score_to_win def get_instance_description_short(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'return 1 flag' return 'return ${ARG1} flags', self._score_to_win def create_team(self, sessionteam: ba.SessionTeam) -> Team: # Create our team instance and its initial values. base_pos = self.map.get_flag_position(sessionteam.id) Flag.project_stand(base_pos) ba.newnode('light', attrs={ 'position': base_pos, 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': sessionteam.color }) base_region_mat = ba.Material() pos = base_pos base_region = ba.newnode( 'region', attrs={ 'position': (pos[0], pos[1] + 0.75, pos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [base_region_mat, self._all_bases_material] }) spaz_mat_no_flag_physical = ba.Material() spaz_mat_no_flag_collide = ba.Material() flagmat = ba.Material() team = Team(base_pos=base_pos, base_region_material=base_region_mat, base_region=base_region, spaz_material_no_flag_physical=spaz_mat_no_flag_physical, spaz_material_no_flag_collide=spaz_mat_no_flag_collide, flagmaterial=flagmat) # Some parts of our spazzes don't collide physically with our # flags but generate callbacks. spaz_mat_no_flag_physical.add_actions( conditions=('they_have_material', flagmat), actions=( ('modify_part_collision', 'physical', False), ('call', 'at_connect', lambda: self._handle_touching_own_flag(team, True)), ('call', 'at_disconnect', lambda: self._handle_touching_own_flag(team, False)), )) # Other parts of our spazzes don't collide with our flags at all. spaz_mat_no_flag_collide.add_actions( conditions=('they_have_material', flagmat), actions=('modify_part_collision', 'collide', False), ) # We wanna know when *any* flag enters/leaves our base. base_region_mat.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', lambda: self._handle_flag_entered_base(team)), ('call', 'at_disconnect', lambda: self._handle_flag_left_base(team)), )) return team def on_team_join(self, team: Team) -> None: # Can't do this in create_team because the team's color/etc. have # not been wired up yet at that point. self._spawn_flag_for_team(team) self._update_scoreboard() 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.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() ba.timer(1.0, call=self._tick, repeat=True) def _spawn_flag_for_team(self, team: Team) -> None: team.flag = StickyStormCTFFlag(team) team.flag_return_touches = 0 self._flash_base(team, length=1.0) assert team.flag.node ba.playsound(self._swipsound, position=team.flag.node.position) def _handle_flag_entered_base(self, team: Team) -> None: try: flag = ba.getcollision().opposingnode.getdelegate( StickyStormCTFFlag, True) except ba.NotFoundError: # Don't think this should logically ever happen. print( 'Error getting StickyStormCTFFlag in entering-base callback.') return if flag.team is team: team.home_flag_at_base = True # If the enemy flag is already here, score! if team.enemy_flag_at_base: self._score(team) else: team.enemy_flag_at_base = True if team.home_flag_at_base: # Award points to whoever was carrying the enemy flag. player = flag.last_player_to_hold if player and player.team is team: assert self.stats self.stats.player_scored(player, 50, big_message=True) # Update score and reset flags. self._score(team) # If the home-team flag isn't here, print a message to that effect. else: # Don't want slo-mo affecting this curtime = ba.time(ba.TimeType.BASE) if curtime - self._last_home_flag_notice_print_time > 5.0: self._last_home_flag_notice_print_time = curtime bpos = team.base_pos tval = ba.Lstr(resource='ownFlagAtYourBaseWarning') tnode = ba.newnode('text', attrs={ 'text': tval, 'in_world': True, 'scale': 0.013, 'color': (1, 1, 0, 1), 'h_align': 'center', 'position': (bpos[0], bpos[1] + 3.2, bpos[2]) }) ba.timer(5.1, tnode.delete) ba.animate(tnode, 'scale', { 0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0 }) def _tick(self) -> None: # If either flag is away from base and not being held, tick down its # respawn timer. for team in self.teams: flag = team.flag assert flag is not None if not team.home_flag_at_base and flag.held_count == 0: time_out_counting_down = True if flag.time_out_respawn_time is None: flag.reset_return_times() assert flag.time_out_respawn_time is not None flag.time_out_respawn_time -= 1 if flag.time_out_respawn_time <= 0: flag.handlemessage(ba.DieMessage()) else: time_out_counting_down = False if flag.node and flag.counter: pos = flag.node.position flag.counter.position = (pos[0], pos[1] + 1.3, pos[2]) # If there's no self-touches on this flag, set its text # to show its auto-return counter. (if there's self-touches # its showing that time). if team.flag_return_touches == 0: flag.counter.text = (str(flag.time_out_respawn_time) if ( time_out_counting_down and flag.time_out_respawn_time is not None and flag.time_out_respawn_time <= 10) else '') flag.counter.color = (1, 1, 1, 0.5) flag.counter.scale = 0.014 def _score(self, team: Team) -> None: team.score += 1 ba.playsound(self._score_sound) self._flash_base(team) self._update_scoreboard() # Have teammates celebrate. for player in team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage(2.0)) # Reset all flags/state. for reset_team in self.teams: if not reset_team.home_flag_at_base: assert reset_team.flag is not None reset_team.flag.handlemessage(ba.DieMessage()) reset_team.enemy_flag_at_base = False if team.score >= self._score_to_win: self.end_game() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results, announce_delay=0.8) def _handle_flag_left_base(self, team: Team) -> None: cur_time = ba.time() try: flag = ba.getcollision().opposingnode.getdelegate( StickyStormCTFFlag, True) except ba.NotFoundError: # This can happen if the flag stops touching us due to being # deleted; that's ok. return if flag.team is team: # Check times here to prevent too much flashing. if (team.last_flag_leave_time is None or cur_time - team.last_flag_leave_time > 3.0): ba.playsound(self._alarmsound, position=team.base_pos) self._flash_base(team) team.last_flag_leave_time = cur_time team.home_flag_at_base = False else: team.enemy_flag_at_base = False def _touch_return_update(self, team: Team) -> None: # Count down only while its away from base and not being held. assert team.flag is not None if team.home_flag_at_base or team.flag.held_count > 0: team.touch_return_timer_ticking = None return # No need to return when its at home. if team.touch_return_timer_ticking is None: team.touch_return_timer_ticking = ba.NodeActor( ba.newnode('sound', attrs={ 'sound': self._ticking_sound, 'positional': False, 'loop': True })) flag = team.flag if flag.touch_return_time is not None: flag.touch_return_time -= 0.1 if flag.counter: flag.counter.text = f'{flag.touch_return_time:.1f}' flag.counter.color = (1, 1, 0, 1) flag.counter.scale = 0.02 if flag.touch_return_time <= 0.0: self._award_players_touching_own_flag(team) flag.handlemessage(ba.DieMessage()) def _award_players_touching_own_flag(self, team: Team) -> None: for player in team.players: if player.touching_own_flag > 0: return_score = 10 + 5 * int(self.flag_touch_return_time) self.stats.player_scored(player, return_score, screenmessage=False) def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None: """Called when a player touches or stops touching their own team flag. We keep track of when each player is touching their own flag so we can award points when returned. """ player: Optional[Player] try: player = ba.getcollision().sourcenode.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: # This can happen if the player leaves but his corpse touches/etc. player = None if player: player.touching_own_flag += (1 if connecting else -1) # If return-time is zero, just kill it immediately.. otherwise keep # track of touches and count down. if float(self.flag_touch_return_time) <= 0.0: assert team.flag is not None if (connecting and not team.home_flag_at_base and team.flag.held_count == 0): self._award_players_touching_own_flag(team) ba.getcollision().opposingnode.handlemessage(ba.DieMessage()) # Takes a non-zero amount of time to return. else: if connecting: team.flag_return_touches += 1 if team.flag_return_touches == 1: team.touch_return_timer = ba.Timer( 0.1, call=ba.Call(self._touch_return_update, team), repeat=True) team.touch_return_timer_ticking = None else: team.flag_return_touches -= 1 if team.flag_return_touches == 0: team.touch_return_timer = None team.touch_return_timer_ticking = None if team.flag_return_touches < 0: ba.print_error('CTF flag_return_touches < 0') def _flash_base(self, team: Team, length: float = 2.0) -> None: light = ba.newnode('light', attrs={ 'position': team.base_pos, 'height_attenuated': False, 'radius': 0.3, 'color': team.color }) ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) ba.timer(length, light.delete) def spawn_player_spaz(self, player: Player, position: Sequence[float] = None, angle: float = None) -> PlayerSpaz: """Intercept new spazzes and add our team material for them.""" spaz = super().spawn_player_spaz(player, position, angle) player = spaz.getplayer(Player, True) team: Team = player.team player.touching_own_flag = 0 no_physical_mats: List[ba.Material] = [ team.spaz_material_no_flag_physical ] no_collide_mats: List[ba.Material] = [ team.spaz_material_no_flag_collide ] # Our normal parts should still collide; just not physically # (so we can calc restores). assert spaz.node spaz.node.materials = list(spaz.node.materials) + no_physical_mats spaz.node.roller_materials = list( spaz.node.roller_materials) + no_physical_mats # Pickups and punches shouldn't hit at all though. spaz.node.punch_materials = list( spaz.node.punch_materials) + no_collide_mats spaz.node.pickup_materials = list( spaz.node.pickup_materials) + no_collide_mats spaz.node.extras_material = list( spaz.node.extras_material) + no_collide_mats return spaz def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.score, self._score_to_win) 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, bomb_type='sticky').autoretain() def _decrement_meteor_time(self) -> None: self._meteor_time = max(0.01, self._meteor_time * 0.9) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, StickyStormCTFFlag) ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) elif isinstance(msg, FlagPickedUpMessage): # Store the last player to hold the flag for scoring purposes. assert isinstance(msg.flag, StickyStormCTFFlag) try: msg.flag.last_player_to_hold = msg.node.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: pass msg.flag.held_count += 1 msg.flag.reset_return_times() elif isinstance(msg, FlagDroppedMessage): # Store the last player to hold the flag for scoring purposes. assert isinstance(msg.flag, StickyStormCTFFlag) msg.flag.held_count -= 1 else: super().handlemessage(msg) # Copyright (c) Lifetime Benefit-Zebra # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this mod without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the mod. # # THE MOD IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE MOD OR THE USE OR OTHER DEALINGS IN THE # MOD. # -------------------------------------- #By Benefit-Zebra #https://github.com/Benefit-Zebra
class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]): """Game of stealing other team's flag and returning it to your base.""" name = 'Capture the Flag' description = 'Return the enemy flag to score.' available_settings = [ ba.IntSetting('Score to Win', min_value=1, default=3), ba.IntSetting( 'Flag Touch Return Time', min_value=0, default=0, increment=1, ), ba.IntSetting( 'Flag Idle Return Time', min_value=5, default=30, increment=5, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ba.BoolSetting('Epic Mode', default=False), ] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.DualTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('team_flag') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._alarmsound = ba.getsound('alarm') self._ticking_sound = ba.getsound('ticking') self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._last_score_time = 0 self._all_bases_material = ba.Material() self._last_home_flag_notice_print_time = 0.0 self._score_to_win = int(settings['Score to Win']) self._epic_mode = bool(settings['Epic Mode']) self._time_limit = float(settings['Time Limit']) self.flag_touch_return_time = float(settings['Flag Touch Return Time']) self.flag_idle_return_time = float(settings['Flag Idle Return Time']) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER) def get_instance_description(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'Steal the enemy flag.' return 'Steal the enemy flag ${ARG1} times.', self._score_to_win def get_instance_description_short(self) -> Union[str, Sequence]: if self._score_to_win == 1: return 'return 1 flag' return 'return ${ARG1} flags', self._score_to_win def create_team(self, sessionteam: ba.SessionTeam) -> Team: # Create our team instance and its initial values. base_pos = self.map.get_flag_position(sessionteam.id) Flag.project_stand(base_pos) ba.newnode('light', attrs={ 'position': base_pos, 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': sessionteam.color }) base_region_mat = ba.Material() pos = base_pos base_region = ba.newnode( 'region', attrs={ 'position': (pos[0], pos[1] + 0.75, pos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [base_region_mat, self._all_bases_material] }) spaz_mat_no_flag_physical = ba.Material() spaz_mat_no_flag_collide = ba.Material() flagmat = ba.Material() team = Team(base_pos=base_pos, base_region_material=base_region_mat, base_region=base_region, spaz_material_no_flag_physical=spaz_mat_no_flag_physical, spaz_material_no_flag_collide=spaz_mat_no_flag_collide, flagmaterial=flagmat) # Some parts of our spazzes don't collide physically with our # flags but generate callbacks. spaz_mat_no_flag_physical.add_actions( conditions=('they_have_material', flagmat), actions=( ('modify_part_collision', 'physical', False), ('call', 'at_connect', lambda: self._handle_touching_own_flag(team, True)), ('call', 'at_disconnect', lambda: self._handle_touching_own_flag(team, False)), )) # Other parts of our spazzes don't collide with our flags at all. spaz_mat_no_flag_collide.add_actions( conditions=('they_have_material', flagmat), actions=('modify_part_collision', 'collide', False), ) # We wanna know when *any* flag enters/leaves our base. base_region_mat.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', lambda: self._handle_flag_entered_base(team)), ('call', 'at_disconnect', lambda: self._handle_flag_left_base(team)), )) return team def on_team_join(self, team: Team) -> None: # Can't do this in create_team because the team's color/etc. have # not been wired up yet at that point. self._spawn_flag_for_team(team) self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() ba.timer(1.0, call=self._tick, repeat=True) def _spawn_flag_for_team(self, team: Team) -> None: team.flag = CTFFlag(team) team.flag_return_touches = 0 self._flash_base(team, length=1.0) assert team.flag.node ba.playsound(self._swipsound, position=team.flag.node.position) def _handle_flag_entered_base(self, team: Team) -> None: try: flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) except ba.NotFoundError: # Don't think this should logically ever happen. print('Error getting CTFFlag in entering-base callback.') return if flag.team is team: team.home_flag_at_base = True # If the enemy flag is already here, score! if team.enemy_flag_at_base: self._score(team) else: team.enemy_flag_at_base = True if team.home_flag_at_base: # Award points to whoever was carrying the enemy flag. player = flag.last_player_to_hold if player and player.team is team: assert self.stats self.stats.player_scored(player, 50, big_message=True) # Update score and reset flags. self._score(team) # If the home-team flag isn't here, print a message to that effect. else: # Don't want slo-mo affecting this curtime = ba.time(ba.TimeType.BASE) if curtime - self._last_home_flag_notice_print_time > 5.0: self._last_home_flag_notice_print_time = curtime bpos = team.base_pos tval = ba.Lstr(resource='ownFlagAtYourBaseWarning') tnode = ba.newnode('text', attrs={ 'text': tval, 'in_world': True, 'scale': 0.013, 'color': (1, 1, 0, 1), 'h_align': 'center', 'position': (bpos[0], bpos[1] + 3.2, bpos[2]) }) ba.timer(5.1, tnode.delete) ba.animate(tnode, 'scale', { 0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0 }) def _tick(self) -> None: # If either flag is away from base and not being held, tick down its # respawn timer. for team in self.teams: flag = team.flag assert flag is not None if not team.home_flag_at_base and flag.held_count == 0: time_out_counting_down = True if flag.time_out_respawn_time is None: flag.reset_return_times() assert flag.time_out_respawn_time is not None flag.time_out_respawn_time -= 1 if flag.time_out_respawn_time <= 0: flag.handlemessage(ba.DieMessage()) else: time_out_counting_down = False if flag.node and flag.counter: pos = flag.node.position flag.counter.position = (pos[0], pos[1] + 1.3, pos[2]) # If there's no self-touches on this flag, set its text # to show its auto-return counter. (if there's self-touches # its showing that time). if team.flag_return_touches == 0: flag.counter.text = (str(flag.time_out_respawn_time) if ( time_out_counting_down and flag.time_out_respawn_time is not None and flag.time_out_respawn_time <= 10) else '') flag.counter.color = (1, 1, 1, 0.5) flag.counter.scale = 0.014 def _score(self, team: Team) -> None: team.score += 1 ba.playsound(self._score_sound) self._flash_base(team) self._update_scoreboard() # Have teammates celebrate. for player in team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage(2.0)) # Reset all flags/state. for reset_team in self.teams: if not reset_team.home_flag_at_base: assert reset_team.flag is not None reset_team.flag.handlemessage(ba.DieMessage()) reset_team.enemy_flag_at_base = False if team.score >= self._score_to_win: self.end_game() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results, announce_delay=0.8) def _handle_flag_left_base(self, team: Team) -> None: cur_time = ba.time() try: flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True) except ba.NotFoundError: # This can happen if the flag stops touching us due to being # deleted; that's ok. return if flag.team is team: # Check times here to prevent too much flashing. if (team.last_flag_leave_time is None or cur_time - team.last_flag_leave_time > 3.0): ba.playsound(self._alarmsound, position=team.base_pos) self._flash_base(team) team.last_flag_leave_time = cur_time team.home_flag_at_base = False else: team.enemy_flag_at_base = False def _touch_return_update(self, team: Team) -> None: # Count down only while its away from base and not being held. assert team.flag is not None if team.home_flag_at_base or team.flag.held_count > 0: team.touch_return_timer_ticking = None return # No need to return when its at home. if team.touch_return_timer_ticking is None: team.touch_return_timer_ticking = ba.NodeActor( ba.newnode('sound', attrs={ 'sound': self._ticking_sound, 'positional': False, 'loop': True })) flag = team.flag if flag.touch_return_time is not None: flag.touch_return_time -= 0.1 if flag.counter: flag.counter.text = f'{flag.touch_return_time:.1f}' flag.counter.color = (1, 1, 0, 1) flag.counter.scale = 0.02 if flag.touch_return_time <= 0.0: self._award_players_touching_own_flag(team) flag.handlemessage(ba.DieMessage()) def _award_players_touching_own_flag(self, team: Team) -> None: for player in team.players: if player.touching_own_flag > 0: return_score = 10 + 5 * int(self.flag_touch_return_time) self.stats.player_scored(player, return_score, screenmessage=False) def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None: """Called when a player touches or stops touching their own team flag. We keep track of when each player is touching their own flag so we can award points when returned. """ player: Optional[Player] try: player = ba.getcollision().sourcenode.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: # This can happen if the player leaves but his corpse touches/etc. player = None if player: player.touching_own_flag += (1 if connecting else -1) # If return-time is zero, just kill it immediately.. otherwise keep # track of touches and count down. if float(self.flag_touch_return_time) <= 0.0: assert team.flag is not None if (connecting and not team.home_flag_at_base and team.flag.held_count == 0): self._award_players_touching_own_flag(team) ba.getcollision().opposingnode.handlemessage(ba.DieMessage()) # Takes a non-zero amount of time to return. else: if connecting: team.flag_return_touches += 1 if team.flag_return_touches == 1: team.touch_return_timer = ba.Timer( 0.1, call=ba.Call(self._touch_return_update, team), repeat=True) team.touch_return_timer_ticking = None else: team.flag_return_touches -= 1 if team.flag_return_touches == 0: team.touch_return_timer = None team.touch_return_timer_ticking = None if team.flag_return_touches < 0: ba.print_error('CTF flag_return_touches < 0') def _flash_base(self, team: Team, length: float = 2.0) -> None: light = ba.newnode('light', attrs={ 'position': team.base_pos, 'height_attenuated': False, 'radius': 0.3, 'color': team.color }) ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True) ba.timer(length, light.delete) def spawn_player_spaz(self, player: Player, position: Sequence[float] = None, angle: float = None) -> PlayerSpaz: """Intercept new spazzes and add our team material for them.""" spaz = super().spawn_player_spaz(player, position, angle) player = spaz.getplayer(Player, True) team: Team = player.team player.touching_own_flag = 0 no_physical_mats: List[ba.Material] = [ team.spaz_material_no_flag_physical ] no_collide_mats: List[ba.Material] = [ team.spaz_material_no_flag_collide ] # Our normal parts should still collide; just not physically # (so we can calc restores). assert spaz.node spaz.node.materials = list(spaz.node.materials) + no_physical_mats spaz.node.roller_materials = list( spaz.node.roller_materials) + no_physical_mats # Pickups and punches shouldn't hit at all though. spaz.node.punch_materials = list( spaz.node.punch_materials) + no_collide_mats spaz.node.pickup_materials = list( spaz.node.pickup_materials) + no_collide_mats spaz.node.extras_material = list( spaz.node.extras_material) + no_collide_mats return spaz def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.score, self._score_to_win) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, CTFFlag) ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) elif isinstance(msg, FlagPickedUpMessage): # Store the last player to hold the flag for scoring purposes. assert isinstance(msg.flag, CTFFlag) try: msg.flag.last_player_to_hold = msg.node.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: pass msg.flag.held_count += 1 msg.flag.reset_return_times() elif isinstance(msg, FlagDroppedMessage): # Store the last player to hold the flag for scoring purposes. assert isinstance(msg.flag, CTFFlag) msg.flag.held_count -= 1 else: super().handlemessage(msg)
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.' available_settings = [ ba.IntSetting( 'Hold Time', min_value=10, default=30, increment=10, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ] scoreconfig = ba.ScoreConfig(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): 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. numsound = self._countdownsounds.get(scoring_team.time_remaining) if numsound is not None: ba.playsound(numsound) # winner if scoring_team.time_remaining <= 0: self.end_game() def end_game(self) -> None: results = ba.GameResults() 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 QuakeGame(ba.TeamGameActivity[Player, Team]): """Quake Team Game Activity""" name = 'Quake' description = 'Kill a set number of enemies to win.' available_settings = [ ba.IntSetting( 'Kills to Win Per Player', default=15, min_value=1, increment=1, ), ba.IntChoiceSetting( 'Time Limit', choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], default=1.0, ), ba.BoolSetting( 'Speed', default=True, ), ba.BoolSetting( 'Enable Jump', default=True, ), ba.BoolSetting( 'Enable Pickup', default=True, ), ba.BoolSetting( 'Enable Bomb', default=False, ), ba.BoolSetting( 'Obstacles', default=True, ), ba.IntChoiceSetting( 'Obstacles Form', choices=[('Cube', ObstaclesForm.CUBE.value), ('Sphere', ObstaclesForm.SPHERE.value), ('Random', ObstaclesForm.RANDOM.value)], default=0, ), ba.IntChoiceSetting( 'Weapon Type', choices=[('Rocket', WeaponType.ROCKET.value), ('Railgun', WeaponType.RAILGUN.value)], default=WeaponType.ROCKET.value, ), ba.BoolSetting( 'Obstacles Mirror Shots', default=False, ), ba.IntSetting( 'Obstacles Count', default=16, min_value=0, increment=2, ), ba.BoolSetting( 'Random Obstacles Color', default=True, ), ba.BoolSetting( 'Epic Mode', default=False, ), ] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.MultiTeamSession) or issubclass( sessiontype, ba.FreeForAllSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: # TODO add more maps return ['Football Stadium', 'Monkey Face', 'Doom Shroom'] def __init__(self, settings) -> None: super().__init__(settings) self._epic_mode = self.settings_raw['Epic Mode'] self._score_to_win = self.settings_raw['Kills to Win Per Player'] self._time_limit = self.settings_raw['Time Limit'] self._obstacles_enabled = self.settings_raw['Obstacles'] self._obstacles_count = self.settings_raw['Obstacles Count'] self._speed_enabled = self.settings_raw['Speed'] self._bomb_enabled = self.settings_raw['Enable Bomb'] self._pickup_enabled = self.settings_raw['Enable Pickup'] self._jump_enabled = self.settings_raw['Enable Jump'] self._weapon_type = WeaponType(self.settings_raw['Weapon Type']) self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP) self.slow_motion = self._epic_mode self.announce_player_deaths = True self._scoreboard = Scoreboard() self._ding_sound = ba.getsound('dingSmall') self._shield_dropper: Optional[ba.Timer] = None def get_instance_description(self) -> Union[str, Sequence]: return 'Kill ${ARG1} enemies.', self._score_to_win def on_team_join(self, team: Team) -> None: team.score = 0 if self.has_begun(): self._update_scoreboard() def on_begin(self) -> None: ba.TeamGameActivity.on_begin(self) self.drop_shield() self._shield_dropper = ba.Timer(8, ba.WeakCall(self.drop_shield), repeat=True) self.setup_standard_time_limit(self._time_limit) if self._obstacles_enabled: count = self._obstacles_count gamemap = self.map.getname() for i in range(count): # TODO: tidy up around here if gamemap == 'Football Stadium': radius = (random.uniform(-10, 1), 6, random.uniform(-4.5, 4.5)) \ if i > count / 2 else ( random.uniform(10, 1), 6, random.uniform(-4.5, 4.5)) else: radius = (random.uniform(-10, 1), 6, random.uniform(-8, 8)) \ if i > count / 2 else ( random.uniform(10, 1), 6, random.uniform(-8, 8)) Obstacle( position=radius, mirror=self.settings_raw['Obstacles mirror shots'], form=self.settings_raw['Obstacles form']).autoretain() self._update_scoreboard() def drop_shield(self) -> None: """Drop a shield powerup in random place""" # FIXME: should use map defs shield = PowerupBox(poweruptype='shield', position=(random.uniform(-10, 10), 6, random.uniform(-5, 5))).autoretain() ba.playsound(self._ding_sound) p_light = ba.newnode('light', owner=shield.node, attrs={ 'position': (0, 0, 0), 'color': (0.3, 0.0, 0.4), 'radius': 0.3, 'intensity': 2, 'volume_intensity_scale': 10.0 }) shield.node.connectattr('position', p_light, 'position') ba.animate(p_light, 'intensity', {0: 2, 8: 0}) def spawn_player(self, player: Player) -> None: spaz = self.spawn_player_spaz(player) if self._weapon_type == WeaponType.ROCKET: quake.rocket.RocketLauncher().give(spaz) elif self._weapon_type == WeaponType.RAILGUN: quake.railgun.Railgun().give(spaz) spaz.connect_controls_to_player(enable_jump=self._jump_enabled, enable_pickup=self._pickup_enabled, enable_bomb=self._bomb_enabled, enable_fly=False) spaz.node.hockey = self._speed_enabled spaz.spaz_light = ba.newnode('light', owner=spaz.node, attrs={ 'position': (0, 0, 0), 'color': spaz.node.color, 'radius': 0.12, 'intensity': 1, 'volume_intensity_scale': 10.0 }) spaz.node.connectattr('position', spaz.spaz_light, 'position') def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): ba.TeamGameActivity.handlemessage(self, msg) player = msg.getplayer(Player) self.respawn_player(player) killer = msg.getkillerplayer(Player) if killer is None: return # handle team-kills if killer.team is player.team: # in free-for-all, killing yourself loses you a point if isinstance(self.session, ba.FreeForAllSession): new_score = player.team.score - 1 new_score = max(0, new_score) player.team.score = new_score # in teams-mode it gives a point to the other team else: ba.playsound(self._ding_sound) for team in self.teams: if team is not killer.team: team.score += 1 # killing someone on another team nets a kill else: killer.team.score += 1 ba.playsound(self._ding_sound) # in FFA show our score since its hard to find on # the scoreboard assert killer.actor is not None # noinspection PyUnresolvedReferences killer.actor.set_score_text(str(killer.team.score) + '/' + str(self._score_to_win), color=killer.team.color, flash=True) self._update_scoreboard() # if someone has won, set a timer to end shortly # (allows the dust to clear and draws to occur if # deaths are close enough) if any(team.score >= self._score_to_win for team in self.teams): ba.timer(0.5, self.end_game) else: ba.TeamGameActivity.handlemessage(self, msg) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.score, self._score_to_win) def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results=results)
class kth(ba.TeamGameActivity[Player, Team]): """Game where a team wins by holding a 'hill' for a set amount of time.""" name = 'King of the Hillx' description = 'Secure the flag for a set length of time.' available_settings = [ ba.IntSetting( 'Hold Time', min_value=10, default=30, increment=10, ), ba.IntChoiceSetting( 'Time Limit', choices=[ ('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200), ], default=0, ), ba.FloatChoiceSetting( 'Respawn Times', choices=[ ('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0), ], default=1.0, ), ] scoreconfig = ba.ScoreConfig(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): super().__init__(settings) 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 on_begin(self) -> None: super().on_begin() shared = SharedObjects.get() def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.time_remaining) self.end(results=results, announce_delay=0) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment default.