class ms(ba.TeamGameActivity[Player, Team]): """Minigame involving dodging falling bombs.""" name = 'Meteor Showerx' description = 'Dodge the falling bombs.' available_settings = [ba.BoolSetting('Epic Mode', default=False)] scoreconfig = ba.ScoreConfig(label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B') # Print messages when players die (since its meaningful in this game). announce_player_deaths = True # Don't allow joining after we start # (would enable leave/rejoin tomfoolery). allow_mid_activity_joins = False # 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): super().__init__(settings) def on_begin(self) -> None: super().on_begin()
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.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( '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
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
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 EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): """A game where score is based on collecting eggs.""" name = 'Easter Egg Hunt' description = 'Gather eggs!' available_settings = [ba.BoolSetting('Pro Mode', default=False)] scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS) # We're currently hard-coded for one map. @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ['Tower D'] # We support teams, free-for-all, and co-op sessions. @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.CoopSession) or issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession)) def __init__(self, settings: dict): super().__init__(settings) shared = SharedObjects.get() self._last_player_death_time = None self._scoreboard = Scoreboard() self.egg_model = ba.getmodel('egg') self.egg_tex_1 = ba.gettexture('eggTex1') self.egg_tex_2 = ba.gettexture('eggTex2') self.egg_tex_3 = ba.gettexture('eggTex3') self._collect_sound = ba.getsound('powerup01') self._pro_mode = settings.get('Pro Mode', False) self._max_eggs = 1.0 self.egg_material = ba.Material() self.egg_material.add_actions( conditions=('they_have_material', shared.player_material), actions=(('call', 'at_connect', self._on_egg_player_collide), )) self._eggs: List[Egg] = [] self._update_timer: Optional[ba.Timer] = None self._countdown: Optional[OnScreenCountdown] = None self._bots: Optional[SpazBotSet] = None # Base class overrides self.default_music = ba.MusicType.FORWARD_MARCH def on_team_join(self, team: Team) -> None: if self.has_begun(): self._update_scoreboard() # Called when our game actually starts. def on_begin(self) -> None: from bastd.maps import TowerD # There's a player-wall on the tower-d level to prevent # players from getting up on the stairs.. we wanna kill that. gamemap = self.map assert isinstance(gamemap, TowerD) gamemap.player_wall.delete() super().on_begin() self._update_scoreboard() self._update_timer = ba.Timer(0.25, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) ba.timer(4.0, self._countdown.start) self._bots = SpazBotSet() # Spawn evil bunny in co-op only. if isinstance(self.session, ba.CoopSession) and self._pro_mode: self._spawn_evil_bunny() # Overriding the default character spawning. def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player) spaz.connect_controls_to_player() return spaz def _spawn_evil_bunny(self) -> None: assert self._bots is not None self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0) def _on_egg_player_collide(self) -> None: if self.has_ended(): return collision = ba.getcollision() # Be defensive here; we could be hitting the corpse of a player # who just left/etc. try: egg = collision.sourcenode.getdelegate(Egg, True) player = collision.opposingnode.getdelegate(PlayerSpaz, True).getplayer( Player, True) except ba.NotFoundError: return player.team.score += 1 # Displays a +1 (and adds to individual player score in # teams mode). self.stats.player_scored(player, 1, screenmessage=False) if self._max_eggs < 5: self._max_eggs += 1.0 elif self._max_eggs < 10: self._max_eggs += 0.5 elif self._max_eggs < 30: self._max_eggs += 0.3 self._update_scoreboard() ba.playsound(self._collect_sound, 0.5, position=egg.node.position) # Create a flash. light = ba.newnode('light', attrs={ 'position': egg.node.position, 'height_attenuated': False, 'radius': 0.1, 'color': (1, 1, 0) }) ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False) ba.timer(0.200, light.delete) egg.handlemessage(ba.DieMessage()) def _update(self) -> None: # Misc. periodic updating. xpos = random.uniform(-7.1, 6.0) ypos = random.uniform(3.5, 3.5) zpos = random.uniform(-8.2, 3.7) # Prune dead eggs from our list. self._eggs = [e for e in self._eggs if e] # Spawn more eggs if we've got space. if len(self._eggs) < int(self._max_eggs): # Occasionally spawn a land-mine in addition. if self._pro_mode and random.random() < 0.25: mine = Bomb(position=(xpos, ypos, zpos), bomb_type='land_mine').autoretain() mine.arm() else: self._eggs.append(Egg(position=(xpos, ypos, zpos))) # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: # Respawn dead players. if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.respawn_icon = RespawnIcon(player, respawn_time) # Whenever our evil bunny dies, respawn him and spew some eggs. elif isinstance(msg, SpazBotDiedMessage): self._spawn_evil_bunny() assert msg.spazbot.node pos = msg.spazbot.node.position for _i in range(6): spread = 0.4 self._eggs.append( Egg(position=(pos[0] + random.uniform(-spread, spread), pos[1] + random.uniform(-spread, spread), pos[2] + random.uniform(-spread, spread)))) else: # Default handler. return super().handlemessage(msg) return None def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.score) def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results)
class 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 StickyStormGame(ba.TeamGameActivity[Player, Team]): """Minigame involving dodging falling ice bombs.""" name = 'Sticky Storm' description = 'Dodge the falling sticky bombs.' available_settings = [ba.BoolSetting('Epic Mode', default=False)] scoreconfig = ba.ScoreConfig(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): 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.getname(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: return super().handlemessage(msg) return None 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, bomb_type = 'sticky').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.GameResults() # 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) # 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 MeteorShowerGame(ba.TeamGameActivity[Player, Team]): """Minigame involving dodging falling bombs.""" name = 'Meteor Shower' description = 'Dodge the falling bombs.' available_settings = [ba.BoolSetting('Epic Mode', default=False)] scoreconfig = ba.ScoreConfig(label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B') # Print messages when players die (since its meaningful in this game). announce_player_deaths = True # Don't allow joining after we start # (would enable leave/rejoin tomfoolery). allow_mid_activity_joins = False # 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): 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_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: return super().handlemessage(msg) return None 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.GameResults() # 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 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 TargetPracticeGame(ba.TeamGameActivity[Player, Team]): """Game where players try to hit targets with bombs.""" name = 'Target Practice' description = 'Bomb as many targets as you can.' available_settings = [ ba.IntSetting('Target Count', min_value=1, default=3), ba.BoolSetting('Enable Impact Bombs', default=True), ba.BoolSetting('Enable Triple Bombs', default=True) ] default_music = ba.MusicType.FORWARD_MARCH @classmethod def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: return ['Doom Shroom'] @classmethod def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: # We support any teams or versus sessions. return (issubclass(sessiontype, ba.CoopSession) or issubclass(sessiontype, ba.MultiTeamSession)) def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._targets: list[Target] = [] self._update_timer: Optional[ba.Timer] = None self._countdown: Optional[OnScreenCountdown] = None self._target_count = int(settings['Target Count']) self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) self._enable_triple_bombs = bool(settings['Enable Triple Bombs']) def on_team_join(self, team: Team) -> None: if self.has_begun(): self.update_scoreboard() def on_begin(self) -> None: super().on_begin() self.update_scoreboard() # Number of targets is based on player count. for i in range(self._target_count): ba.timer(5.0 + i * 1.0, self._spawn_target) self._update_timer = ba.Timer(1.0, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) ba.timer(4.0, self._countdown.start) def spawn_player(self, player: Player) -> ba.Actor: spawn_center = (0, 3, -5) pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], spawn_center[2] + random.uniform(-1.5, 1.5)) # Reset their streak. player.streak = 0 spaz = self.spawn_player_spaz(player, position=pos) # Give players permanent triple impact bombs and wire them up # to tell us when they drop a bomb. if self._enable_impact_bombs: spaz.bomb_type = 'impact' if self._enable_triple_bombs: spaz.set_bomb_count(3) spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) return spaz def _spawn_target(self) -> None: # Generate a few random points; we'll use whichever one is farthest # from our existing targets (don't want overlapping targets). points = [] for _i in range(4): # Calc a random point within a circle. while True: xpos = random.uniform(-1.0, 1.0) ypos = random.uniform(-1.0, 1.0) if xpos * xpos + ypos * ypos < 1.0: break points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) def get_min_dist_from_target(pnt: ba.Vec3) -> float: return min((t.get_dist_from_point(pnt) for t in self._targets)) # If we have existing targets, use the point with the highest # min-distance-from-targets. if self._targets: point = max(points, key=get_min_dist_from_target) else: point = points[0] self._targets.append(Target(position=point)) def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None: del spaz # Unused. # Wire up this bomb to inform us when it blows up. assert isinstance(bomb, Bomb) bomb.add_explode_callback(self._on_bomb_exploded) def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None: assert blast.node pos = blast.node.position # Debugging: throw a locator down where we landed. # ba.newnode('locator', attrs={'position':blast.node.position}) # Feed the explosion point to all our targets and get points in return. # Note: we operate on a copy of self._targets since the list may change # under us if we hit stuff (don't wanna get points for new targets). player = bomb.get_source_player(Player) if not player: # It's possible the player left after throwing the bomb. return bullseye = any( target.do_hit_at_position(pos, player) for target in list(self._targets)) if bullseye: player.streak += 1 else: player.streak = 0 def _update(self) -> None: """Misc. periodic updating.""" # Clear out targets that have died. self._targets = [t for t in self._targets if t] def handlemessage(self, msg: Any) -> Any: # When players die, respawn them. if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Do standard stuff. player = msg.getplayer(Player) assert player is not None self.respawn_player(player) # Kick off a respawn. elif isinstance(msg, Target.TargetHitMessage): # A target is telling us it was hit and will die soon.. # ..so make another one. self._spawn_target() else: super().handlemessage(msg) def update_scoreboard(self) -> None: """Update the game scoreboard with current team values.""" for team in self.teams: self._scoreboard.set_team_value(team, team.score) def end_game(self) -> None: results = ba.GameResults() for team in self.teams: results.set_team_score(team, team.score) self.end(results)