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 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 TheLastStandGame(ba.CoopGameActivity[Player, Team]): """Slow motion how-long-can-you-last game.""" name = 'The Last Stand' description = 'Final glorious epic slow motion battle to the death.' tips = [ 'This level never ends, but a high score here\n' 'will earn you eternal respect throughout the world.' ] # Show messages when players die since it matters here. announce_player_deaths = True # And of course the most important part. slow_motion = True def __init__(self, settings: Dict[str, Any]): settings['map'] = 'Rampage' super().__init__(settings) self._new_wave_sound = ba.getsound('scoreHit01') self._winsound = ba.getsound('score') self._cashregistersound = ba.getsound('cashRegister') self._spawn_center = (0, 5.5, -4.14) self._tntspawnpos = (0, 5.5, -6) self._powerup_center = (0, 7, -4.14) self._powerup_spread = (7, 2) self._preset = self.settings_raw.get('preset', 'default') self._excludepowerups: List[str] = [] self._scoreboard: Optional[Scoreboard] = None self._score = 0 self._bots = spazbot.BotSet() self._dingsound = ba.getsound('dingSmall') self._dingsoundhigh = ba.getsound('dingSmallHigh') self._tntspawner: Optional[TNTSpawner] = None self._bot_update_interval: Optional[float] = None self._bot_update_timer: Optional[ba.Timer] = None self._powerup_drop_timer = None # For each bot type: [spawn-rate, increase, d_increase] self._bot_spawn_types = { spazbot.BomberBot: [1.00, 0.00, 0.000], spazbot.BomberBotPro: [0.00, 0.05, 0.001], spazbot.BomberBotProShielded: [0.00, 0.02, 0.002], spazbot.BrawlerBot: [1.00, 0.00, 0.000], spazbot.BrawlerBotPro: [0.00, 0.05, 0.001], spazbot.BrawlerBotProShielded: [0.00, 0.02, 0.002], spazbot.TriggerBot: [0.30, 0.00, 0.000], spazbot.TriggerBotPro: [0.00, 0.05, 0.001], spazbot.TriggerBotProShielded: [0.00, 0.02, 0.002], spazbot.ChargerBot: [0.30, 0.05, 0.000], spazbot.StickyBot: [0.10, 0.03, 0.001], spazbot.ExplodeyBot: [0.05, 0.02, 0.002] } # yapf: disable def on_transition_in(self) -> None: from bastd.actor.scoreboard import Scoreboard self.default_music = ba.MusicType.EPIC super().on_transition_in() ba.timer(1.3, ba.Call(ba.playsound, self._new_wave_sound)) self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), score_split=0.5) def on_begin(self) -> None: super().on_begin() # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) ba.timer(2.0, ba.WeakCall(self._start_powerup_drops)) ba.timer(0.001, ba.WeakCall(self._start_bot_updates)) self.setup_low_life_warning_sound() self._update_scores() # Our TNT spawner (if applicable). self._tntspawner = TNTSpawner(position=self._tntspawnpos, respawn_time=10.0) def spawn_player(self, player: Player) -> ba.Actor: pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], self._spawn_center[2] + random.uniform(-1.5, 1.5)) return self.spawn_player_spaz(player, position=pos) def _start_bot_updates(self) -> None: self._bot_update_interval = 3.3 - 0.3 * (len(self.players)) self._update_bots() self._update_bots() if len(self.players) > 2: self._update_bots() if len(self.players) > 3: self._update_bots() self._bot_update_timer = ba.Timer(self._bot_update_interval, ba.WeakCall(self._update_bots)) def _drop_powerup(self, index: int, poweruptype: str = None) -> None: from bastd.actor import powerupbox if poweruptype is None: poweruptype = (powerupbox.get_factory().get_random_powerup_type( excludetypes=self._excludepowerups)) powerupbox.PowerupBox(position=self.map.powerup_spawn_points[index], poweruptype=poweruptype).autoretain() def _start_powerup_drops(self) -> None: self._powerup_drop_timer = ba.Timer(3.0, ba.WeakCall(self._drop_powerups), repeat=True) def _drop_powerups(self, standard_points: bool = False, force_first: str = None) -> None: """Generic powerup drop.""" from bastd.actor import powerupbox if standard_points: pts = self.map.powerup_spawn_points for i in range(len(pts)): ba.timer( 1.0 + i * 0.5, ba.WeakCall(self._drop_powerup, i, force_first if i == 0 else None)) else: drop_pt = (self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1])) # Drop one random one somewhere. powerupbox.PowerupBox( position=drop_pt, poweruptype=powerupbox.get_factory().get_random_powerup_type( excludetypes=self._excludepowerups)).autoretain() def do_end(self, outcome: str) -> None: """End the game.""" if outcome == 'defeat': self.fade_to_red() self.end(delay=2.0, results={ 'outcome': outcome, 'score': self._score, 'player_info': self.initial_player_info }) def _update_bots(self) -> None: assert self._bot_update_interval is not None self._bot_update_interval = max(0.5, self._bot_update_interval * 0.98) self._bot_update_timer = ba.Timer(self._bot_update_interval, ba.WeakCall(self._update_bots)) botspawnpts: List[Sequence[float]] = [[-5.0, 5.5, -4.14], [0.0, 5.5, -4.14], [5.0, 5.5, -4.14]] dists = [0.0, 0.0, 0.0] playerpts: List[Sequence[float]] = [] for player in self.players: try: if player.is_alive(): assert isinstance(player.actor, playerspaz.PlayerSpaz) assert player.actor.node playerpts.append(player.actor.node.position) except Exception as exc: print('ERROR in _update_bots', exc) for i in range(3): for playerpt in playerpts: dists[i] += abs(playerpt[0] - botspawnpts[i][0]) # Little random variation. dists[i] += random.random() * 5.0 if dists[0] > dists[1] and dists[0] > dists[2]: spawnpt = botspawnpts[0] elif dists[1] > dists[2]: spawnpt = botspawnpts[1] else: spawnpt = botspawnpts[2] spawnpt = (spawnpt[0] + 3.0 * (random.random() - 0.5), spawnpt[1], 2.0 * (random.random() - 0.5) + spawnpt[2]) # Normalize our bot type total and find a random number within that. total = 0.0 for spawntype in self._bot_spawn_types.items(): total += spawntype[1][0] randval = random.random() * total # Now go back through and see where this value falls. total = 0 bottype: Optional[Type[spazbot.SpazBot]] = None for spawntype in self._bot_spawn_types.items(): total += spawntype[1][0] if randval <= total: bottype = spawntype[0] break spawn_time = 1.0 assert bottype is not None self._bots.spawn_bot(bottype, pos=spawnpt, spawn_time=spawn_time) # After every spawn we adjust our ratios slightly to get more # difficult. for spawntype in self._bot_spawn_types.items(): spawntype[1][0] += spawntype[1][1] # incr spawn rate spawntype[1][1] += spawntype[1][2] # incr spawn rate incr rate def _update_scores(self) -> None: # Achievements in default preset only. score = self._score if self._preset == 'default': if score >= 250: self._award_achievement('Last Stand Master') if score >= 500: self._award_achievement('Last Stand Wizard') if score >= 1000: self._award_achievement('Last Stand God') assert self._scoreboard is not None self._scoreboard.set_team_value(self.teams[0], score, max_score=None) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): player = msg.getplayer(Player) self.stats.player_was_killed(player) ba.timer(0.1, self._checkroundover) elif isinstance(msg, ba.PlayerScoredMessage): self._score += msg.score self._update_scores() elif isinstance(msg, spazbot.SpazBotDeathMessage): pts, importance = msg.badguy.get_death_points(msg.how) target: Optional[Sequence[float]] if msg.killerplayer: try: assert msg.badguy.node target = msg.badguy.node.position except Exception: ba.print_exception() target = None try: self.stats.player_scored(msg.killerplayer, pts, target=target, kill=True, screenmessage=False, importance=importance) ba.playsound(self._dingsound if importance == 1 else self._dingsoundhigh, volume=0.6) except Exception as exc: print('EXC on last-stand SpazBotDeathMessage', exc) # Normally we pull scores from the score-set, but if there's no # player lets be explicit. else: self._score += pts self._update_scores() else: super().handlemessage(msg) def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: # FIXME: Unify args. self._show_standard_scores_to_beat_ui(scores) def end_game(self) -> None: # Tell our bots to celebrate just to rub it in. self._bots.final_celebrate() ba.setmusic(None) ba.pushcall(ba.WeakCall(self.do_end, 'defeat')) def _checkroundover(self) -> None: """End the round if conditions are met.""" if not any(player.is_alive() for player in self.teams[0].players): self.end_game()
class ConquestGame(ba.TeamGameActivity): """A game where teams try to claim all flags on the map.""" @classmethod def get_name(cls) -> str: return 'Conquest' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Secure all flags on the map to win.' @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.TeamsSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps("conquest") @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [ ("Time Limit", { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ('Epic Mode', {'default': False})] # yapf: disable def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) if self.settings['Epic Mode']: self.slow_motion = True self._scoreboard = Scoreboard() self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._extraflagmat = ba.Material() self._flags: List[ConquestFlag] = [] # We want flags to tell us they've been hit but not react physically. self._extraflagmat.add_actions( conditions=('they_have_material', ba.sharedobj('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_scoreboard_description(self) -> Union[str, Sequence]: return 'secure all ${ARG1} flags', len(self.map.flag_points) def on_transition_in(self) -> None: self.default_music = (ba.MusicType.EPIC if self.settings['Epic Mode'] else ba.MusicType.GRAND_ROMP) super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: if self.has_begun(): self._update_scores() team.gamedata['flags_held'] = 0 def on_player_join(self, player: ba.Player) -> None: player.gamedata['respawn_timer'] = None # Only spawn if this player's team has a flag currently. if player.team.gamedata['flags_held'] > 0: self.spawn_player(player) def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings['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) # FIXME: Move next few lines to the flag class. self.project_flag_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.gamedata['flags_held'] = 0 for flag in self._flags: try: flag.team.gamedata['flags_held'] += 1 except Exception: pass for team in self.teams: # If a team finds themselves with no flags, cancel all # outstanding spawn-timers. if team.gamedata['flags_held'] == 0: for player in team.players: player.gamedata['respawn_timer'] = None player.gamedata['respawn_icon'] = None if team.gamedata['flags_held'] == len(self._flags): self.end_game() self._scoreboard.set_team_value(team, team.gamedata['flags_held'], len(self._flags)) def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['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: flagnode, playernode = ba.get_collision_info("source_node", "opposing_node") try: player = playernode.getdelegate().getplayer() flag = flagnode.getdelegate() except Exception: return # Player may have left and his body hit the flag. assert isinstance(player, ba.Player) assert isinstance(flag, ConquestFlag) 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.gamedata['respawn_timer'] is None): self.spawn_player(otherplayer) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn only if this team has a flag. player = msg.spaz.player if player.team.gamedata['flags_held'] > 0: self.respawn_player(player) else: player.gamedata['respawn_timer'] = None else: super().handlemessage(msg) def spawn_player(self, player: ba.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: ba.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 DeathMatchGame(ba.TeamGameActivity[Player, Team]): """A game type based on acquiring kills.""" name = 'Death Match' description = 'Kill a set number of enemies to win.' # Print messages when players die since it matters here. announce_player_deaths = True @classmethod 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 @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession)) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('melee') def __init__(self, settings: dict): super().__init__(settings) self._scoreboard = Scoreboard() self._score_to_win: Optional[int] = None self._dingsound = ba.getsound('dingSmall') self._epic_mode = bool(settings['Epic Mode']) self._kills_to_win_per_player = int( settings['Kills to Win Per Player']) self._time_limit = float(settings['Time Limit']) self._allow_negative_scores = bool( settings.get('Allow Negative Scores', False)) # Base class overrides. self.slow_motion = self._epic_mode self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH) def get_instance_description(self) -> Union[str, Sequence]: return 'Crush ${ARG1} of your enemies.', self._score_to_win def get_instance_description_short(self) -> Union[str, Sequence]: return 'kill ${ARG1} enemies', self._score_to_win def on_team_join(self, team: Team) -> None: if self.has_begun(): self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() # Base kills needed to win on the size of the largest team. self._score_to_win = (self._kills_to_win_per_player * max(1, max(len(t.players) for t in self.teams))) self._update_scoreboard() def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) player = msg.getplayer(Player) self.respawn_player(player) killer = msg.getkillerplayer(Player) if killer is None: return None # 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 if not self._allow_negative_scores: 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._dingsound) 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._dingsound) # In FFA show scores since its hard to find on the scoreboard. if isinstance(killer.actor, PlayerSpaz) and killer.actor: 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) assert self._score_to_win is not None if any(team.score >= self._score_to_win for team in self.teams): ba.timer(0.5, self.end_game) else: return super().handlemessage(msg) return None 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 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 CaptureTheFlagGame(ba.TeamGameActivity): """Game of stealing other team's flag and returning it to your base.""" @classmethod def get_name(cls) -> str: return 'Capture the Flag' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Return the enemy flag to score.' @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') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [ ('Score to Win', {'min_value': 1, 'default': 3}), ('Flag Touch Return Time', { 'min_value': 0, 'default': 0, 'increment': 1}), ('Flag Idle Return Time', { 'min_value': 5, 'default': 30, 'increment': 5}), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0}), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0}), ('Epic Mode', {'default': False})] # yapf: disable def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) self._scoreboard = Scoreboard() if self.settings_raw['Epic Mode']: self.slow_motion = True self._alarmsound = ba.getsound('alarm') self._ticking_sound = ba.getsound('ticking') self._last_score_time = 0 self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._all_bases_material = ba.Material() self._last_home_flag_notice_print_time = 0.0 def get_instance_description(self) -> Union[str, Sequence]: if self.settings_raw['Score to Win'] == 1: return 'Steal the enemy flag.' return ('Steal the enemy flag ${ARG1} times.', self.settings_raw['Score to Win']) def get_instance_scoreboard_description(self) -> Union[str, Sequence]: if self.settings_raw['Score to Win'] == 1: return 'return 1 flag' return 'return ${ARG1} flags', self.settings_raw['Score to Win'] def on_transition_in(self) -> None: self.default_music = (ba.MusicType.EPIC if self.settings_raw['Epic Mode'] else ba.MusicType.FLAG_CATCHER) super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['score'] = 0 team.gamedata['flag_return_touches'] = 0 team.gamedata['home_flag_at_base'] = True team.gamedata['touch_return_timer'] = None team.gamedata['enemy_flag_at_base'] = False team.gamedata['base_pos'] = (self.map.get_flag_position(team.get_id())) self.project_flag_stand(team.gamedata['base_pos']) ba.newnode('light', attrs={ 'position': team.gamedata['base_pos'], 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': team.color }) base_region_mat = team.gamedata['base_region_material'] = ba.Material() pos = team.gamedata['base_pos'] team.gamedata['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] }) # create some materials for this team spaz_mat_no_flag_physical = team.gamedata[ 'spaz_material_no_flag_physical'] = ba.Material() spaz_mat_no_flag_collide = team.gamedata[ 'spaz_material_no_flag_collide'] = ba.Material() flagmat = team.gamedata['flagmaterial'] = ba.Material() # 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_hit_own_flag(team, 1)), ('call', 'at_disconnect', lambda: self._handle_hit_own_flag(team, 0)))) # 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', stdflag.get_factory().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)))) self._spawn_flag_for_team(team) self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings_raw['Time Limit']) self.setup_standard_powerup_drops() ba.timer(1.0, call=self._tick, repeat=True) def _spawn_flag_for_team(self, team: ba.Team) -> None: flag = team.gamedata['flag'] = CTFFlag(team) team.gamedata['flag_return_touches'] = 0 self._flash_base(team, length=1.0) assert flag.node ba.playsound(self._swipsound, position=flag.node.position) def _handle_flag_entered_base(self, team: ba.Team) -> None: node = ba.get_collision_info('opposing_node') assert isinstance(node, (ba.Node, type(None))) flag = CTFFlag.from_node(node) if not flag: print('Unable to get flag in _handle_flag_entered_base') return if flag.team is team: team.gamedata['home_flag_at_base'] = True # If the enemy flag is already here, score! if team.gamedata['enemy_flag_at_base']: self._score(team) else: team.gamedata['enemy_flag_at_base'] = True if team.gamedata['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.gamedata['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.gamedata['flag'] if (not team.gamedata['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.gamedata['flag_return_touches'] == 0: flag.counter.text = (str(flag.time_out_respawn_time) if (time_out_counting_down 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: ba.Team) -> None: team.gamedata['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.gamedata['home_flag_at_base']: reset_team.gamedata['flag'].handlemessage(ba.DieMessage()) reset_team.gamedata['enemy_flag_at_base'] = False if team.gamedata['score'] >= self.settings_raw['Score to Win']: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['score']) self.end(results=results, announce_delay=0.8) def _handle_flag_left_base(self, team: ba.Team) -> None: cur_time = ba.time() op_node = ba.get_collision_info('opposing_node') assert isinstance(op_node, (ba.Node, type(None))) flag = CTFFlag.from_node(op_node) if not flag: return if flag.team is team: # Check times here to prevent too much flashing. if ('last_flag_leave_time' not in team.gamedata or cur_time - team.gamedata['last_flag_leave_time'] > 3.0): ba.playsound(self._alarmsound, position=team.gamedata['base_pos']) self._flash_base(team) team.gamedata['last_flag_leave_time'] = cur_time team.gamedata['home_flag_at_base'] = False else: team.gamedata['enemy_flag_at_base'] = False def _touch_return_update(self, team: ba.Team) -> None: # Count down only while its away from base and not being held. if (team.gamedata['home_flag_at_base'] or team.gamedata['flag'].held_count > 0): team.gamedata['touch_return_timer_ticking'] = None return # No need to return when its at home. if team.gamedata['touch_return_timer_ticking'] is None: team.gamedata['touch_return_timer_ticking'] = ba.NodeActor( ba.newnode('sound', attrs={ 'sound': self._ticking_sound, 'positional': False, 'loop': True })) flag = team.gamedata['flag'] flag.touch_return_time -= 0.1 if flag.counter: flag.counter.text = '%.1f' % flag.touch_return_time 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: ba.Team) -> None: for player in team.players: if player.gamedata['touching_own_flag'] > 0: return_score = 10 + 5 * int( self.settings_raw['Flag Touch Return Time']) self.stats.player_scored(player, return_score, screenmessage=False) @staticmethod def _player_from_node(node: Optional[ba.Node]) -> Optional[ba.Player]: """Return a player if given a node that is part of one's actor.""" if not node: return None delegate = node.getdelegate() if not isinstance(delegate, PlayerSpaz): return None return delegate.getplayer() def _handle_hit_own_flag(self, team: ba.Team, val: int) -> None: """ keep track of when each player is touching their own flag so we can award points when returned """ srcnode = ba.get_collision_info('source_node') assert isinstance(srcnode, (ba.Node, type(None))) player = self._player_from_node(srcnode) if player: player.gamedata['touching_own_flag'] += (1 if val else -1) # If return-time is zero, just kill it immediately.. otherwise keep # track of touches and count down. if float(self.settings_raw['Flag Touch Return Time']) <= 0.0: if (not team.gamedata['home_flag_at_base'] and team.gamedata['flag'].held_count == 0): # Use a node message to kill the flag instead of just killing # our team's. (avoids redundantly killing new flags if # multiple body parts generate callbacks in one step). node = ba.get_collision_info('opposing_node') if node: self._award_players_touching_own_flag(team) node.handlemessage(ba.DieMessage()) # Takes a non-zero amount of time to return. else: if val: team.gamedata['flag_return_touches'] += 1 if team.gamedata['flag_return_touches'] == 1: team.gamedata['touch_return_timer'] = ba.Timer( 0.1, call=ba.Call(self._touch_return_update, team), repeat=True) team.gamedata['touch_return_timer_ticking'] = None else: team.gamedata['flag_return_touches'] -= 1 if team.gamedata['flag_return_touches'] == 0: team.gamedata['touch_return_timer'] = None team.gamedata['touch_return_timer_ticking'] = None if team.gamedata['flag_return_touches'] < 0: ba.print_error( "CTF: flag_return_touches < 0; this shouldn't happen.") def _flash_base(self, team: ba.Team, length: float = 2.0) -> None: light = ba.newnode('light', attrs={ 'position': team.gamedata['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, *args: Any, **keywds: Any) -> Any: """Intercept new spazzes and add our team material for them.""" # (chill pylint; we're passing our exact args to parent call) # pylint: disable=signature-differs spaz = super().spawn_player_spaz(*args, **keywds) player = spaz.player player.gamedata['touching_own_flag'] = 0 # Ignore false alarm for gamedata member. no_physical_mats = [ player.team.gamedata['spaz_material_no_flag_physical'] ] no_collide_mats = [ player.team.gamedata['spaz_material_no_flag_collide'] ] # pylint: enable=arguments-differ # 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.gamedata['score'], self.settings_raw['Score to Win']) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.spaz.player) elif isinstance(msg, stdflag.FlagDeathMessage): assert isinstance(msg.flag, CTFFlag) ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team)) elif isinstance(msg, stdflag.FlagPickedUpMessage): # Store the last player to hold the flag for scoring purposes. assert isinstance(msg.flag, CTFFlag) msg.flag.last_player_to_hold = msg.node.getdelegate().getplayer() msg.flag.held_count += 1 msg.flag.reset_return_times() elif isinstance(msg, stdflag.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 FootballCoopGame(ba.CoopGameActivity[Player, Team]): """Co-op variant of football.""" name = 'Football' tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] scoreconfig = ba.ScoreConfig(scoretype=ba.ScoreType.MILLISECONDS, version='B') default_music = ba.MusicType.FOOTBALL # FIXME: Need to update co-op games to use getscoreconfig. def get_score_type(self) -> str: return 'time' def get_instance_description(self) -> Union[str, Sequence]: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'Score ${ARG1} touchdowns.', touchdowns return 'Score a touchdown.' def get_instance_description_short(self) -> Union[str, Sequence]: touchdowns = self._score_to_win / 7 touchdowns = math.ceil(touchdowns) if touchdowns > 1: return 'score ${ARG1} touchdowns', touchdowns return 'score a touchdown' def __init__(self, settings: dict): settings['map'] = 'Football Stadium' super().__init__(settings) self._preset = settings.get('preset', 'rookie') # Load some media we need. self._cheer_sound = ba.getsound('cheer') self._boo_sound = ba.getsound('boo') self._chant_sound = ba.getsound('crowdChant') self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._whistle_sound = ba.getsound('refWhistle') self._score_to_win = 21 self._score_region_material = ba.Material() self._score_region_material.add_actions( conditions=('they_have_material', FlagFactory.get().flagmaterial), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_score), )) self._powerup_center = (0, 2, 0) self._powerup_spread = (10, 5.5) self._player_has_dropped_bomb = False self._player_has_punched = False self._scoreboard: Optional[Scoreboard] = None self._flag_spawn_pos: Optional[Sequence[float]] = None self._score_regions: List[ba.NodeActor] = [] self._exclude_powerups: List[str] = [] self._have_tnt = False self._bot_types_initial: Optional[List[Type[SpazBot]]] = None self._bot_types_7: Optional[List[Type[SpazBot]]] = None self._bot_types_14: Optional[List[Type[SpazBot]]] = None self._bot_team: Optional[Team] = None self._starttime_ms: Optional[int] = None self._time_text: Optional[ba.NodeActor] = None self._time_text_input: Optional[ba.NodeActor] = None self._tntspawner: Optional[TNTSpawner] = None self._bots = SpazBotSet() self._bot_spawn_timer: Optional[ba.Timer] = None self._powerup_drop_timer: Optional[ba.Timer] = None self._scoring_team: Optional[Team] = None self._final_time_ms: Optional[int] = None self._time_text_timer: Optional[ba.Timer] = None self._flag_respawn_light: Optional[ba.Actor] = None self._flag: Optional[FootballFlag] = None def on_transition_in(self) -> None: super().on_transition_in() self._scoreboard = Scoreboard() self._flag_spawn_pos = self.map.get_flag_position(None) self._spawn_flag() # Set up the two score regions. defs = self.map.defs self._score_regions.append( ba.NodeActor( ba.newnode('region', attrs={ 'position': defs.boxes['goal1'][0:3], 'scale': defs.boxes['goal1'][6:9], 'type': 'box', 'materials': [self._score_region_material] }))) self._score_regions.append( ba.NodeActor( ba.newnode('region', attrs={ 'position': defs.boxes['goal2'][0:3], 'scale': defs.boxes['goal2'][6:9], 'type': 'box', 'materials': [self._score_region_material] }))) ba.playsound(self._chant_sound) def on_begin(self) -> None: # FIXME: Split this up a bit. # pylint: disable=too-many-statements from bastd.actor import controlsguide super().on_begin() # Show controls help in kiosk mode. if ba.app.demo_mode or ba.app.arcade_mode: controlsguide.ControlsGuide(delay=3.0, lifespan=10.0, bright=True).autoretain() assert self.initialplayerinfos is not None abot: Type[SpazBot] bbot: Type[SpazBot] cbot: Type[SpazBot] if self._preset in ['rookie', 'rookie_easy']: self._exclude_powerups = ['curse'] self._have_tnt = False abot = (BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot) self._bot_types_initial = [abot] * len(self.initialplayerinfos) bbot = (BomberBotLite if self._preset == 'rookie_easy' else BomberBot) self._bot_types_7 = ( [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) cbot = (BomberBot if self._preset == 'rookie_easy' else TriggerBot) self._bot_types_14 = ( [cbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) elif self._preset == 'tournament': self._exclude_powerups = [] self._have_tnt = True self._bot_types_initial = ( [BrawlerBot] * (1 if len(self.initialplayerinfos) < 2 else 2)) self._bot_types_7 = ( [TriggerBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) self._bot_types_14 = ( [ChargerBot] * (1 if len(self.initialplayerinfos) < 4 else 2)) elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: self._exclude_powerups = ['curse'] self._have_tnt = True self._bot_types_initial = [ChargerBot] * len( self.initialplayerinfos) abot = (BrawlerBot if self._preset == 'pro' else BrawlerBotLite) typed_bot_list: List[Type[SpazBot]] = [] self._bot_types_7 = ( typed_bot_list + [abot] + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) bbot = (TriggerBotPro if self._preset == 'pro' else TriggerBot) self._bot_types_14 = ( [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) elif self._preset in ['uber', 'uber_easy']: self._exclude_powerups = [] self._have_tnt = True abot = (BrawlerBotPro if self._preset == 'uber' else BrawlerBot) bbot = (TriggerBotPro if self._preset == 'uber' else TriggerBot) typed_bot_list_2: List[Type[SpazBot]] = [] self._bot_types_initial = (typed_bot_list_2 + [StickyBot] + [abot] * len(self.initialplayerinfos)) self._bot_types_7 = ( [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2)) self._bot_types_14 = ( [ExplodeyBot] * (1 if len(self.initialplayerinfos) < 3 else 2)) else: raise Exception() self.setup_low_life_warning_sound() self._drop_powerups(standard_points=True) ba.timer(4.0, self._start_powerup_drops) # Make a bogus team for our bots. bad_team_name = self.get_team_display_string('Bad Guys') self._bot_team = Team() self._bot_team.manual_init(team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4)) for team in [self.teams[0], self._bot_team]: team.score = 0 self.update_scores() # Time display. starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) assert isinstance(starttime_ms, int) self._starttime_ms = starttime_ms self._time_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 0.5, 1), 'flatness': 0.5, 'shadow': 0.5, 'position': (0, -50), 'scale': 1.3, 'text': '' })) self._time_text_input = ba.NodeActor( ba.newnode('timedisplay', attrs={'showsubseconds': True})) self.globalsnode.connectattr('time', self._time_text_input.node, 'time2') assert self._time_text_input.node assert self._time_text.node self._time_text_input.node.connectattr('output', self._time_text.node, 'text') # Our TNT spawner (if applicable). if self._have_tnt: self._tntspawner = TNTSpawner(position=(0, 1, -1)) self._bots = SpazBotSet() self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) for bottype in self._bot_types_initial: self._spawn_bot(bottype) def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: self._show_standard_scores_to_beat_ui(scores) def _on_bot_spawn(self, spaz: SpazBot) -> None: # We want to move to the left by default. spaz.target_point_default = ba.Vec3(0, 0, 0) def _spawn_bot(self, spaz_type: Type[SpazBot], immediate: bool = False) -> None: assert self._bot_team is not None pos = self.map.get_start_position(self._bot_team.id) self._bots.spawn_bot(spaz_type, pos=pos, spawn_time=0.001 if immediate else 3.0, on_spawn_call=self._on_bot_spawn) def _update_bots(self) -> None: bots = self._bots.get_living_bots() for bot in bots: bot.target_flag = None # If we're waiting on a continue, stop here so they don't keep scoring. if self.is_waiting_for_continue(): self._bots.stop_moving() return # If we've got a flag and no player are holding it, find the closest # bot to it, and make them the designated flag-bearer. assert self._flag is not None if self._flag.node: for player in self.players: if player.actor: assert isinstance(player.actor, PlayerSpaz) if (player.actor.is_alive() and player.actor.node.hold_node == self._flag.node): return flagpos = ba.Vec3(self._flag.node.position) closest_bot: Optional[SpazBot] = None closest_dist = 0.0 # Always gets assigned first time through. for bot in bots: # If a bot is picked up, he should forget about the flag. if bot.held_count > 0: continue assert bot.node botpos = ba.Vec3(bot.node.position) botdist = (botpos - flagpos).length() if closest_bot is None or botdist < closest_dist: closest_bot = bot closest_dist = botdist if closest_bot is not None: closest_bot.target_flag = self._flag def _drop_powerup(self, index: int, poweruptype: str = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)) PowerupBox(position=self.map.powerup_spawn_points[index], poweruptype=poweruptype).autoretain() def _start_powerup_drops(self) -> None: self._powerup_drop_timer = ba.Timer(3.0, self._drop_powerups, repeat=True) def _drop_powerups(self, standard_points: bool = False, poweruptype: str = None) -> None: """Generic powerup drop.""" if standard_points: spawnpoints = self.map.powerup_spawn_points for i, _point in enumerate(spawnpoints): ba.timer(1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype)) else: point = (self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1])) # Drop one random one somewhere. PowerupBox( position=point, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)).autoretain() def _kill_flag(self) -> None: try: assert self._flag is not None self._flag.handlemessage(ba.DieMessage()) except Exception: ba.print_exception('Error in _kill_flag.') def _handle_score(self) -> None: """ a point has been scored """ # FIXME tidy this up # pylint: disable=too-many-branches # Our flag might stick around for a second or two; # we don't want it to be able to score again. assert self._flag is not None if self._flag.scored: return # See which score region it was. region = ba.getcollision().sourcenode i = None for i in range(len(self._score_regions)): if region == self._score_regions[i].node: break for team in [self.teams[0], self._bot_team]: assert team is not None if team.id == i: team.score += 7 # Tell all players (or bots) to celebrate. if i == 0: for player in team.players: if player.actor: player.actor.handlemessage( ba.CelebrateMessage(2.0)) else: self._bots.celebrate(2.0) # If the good guys scored, add more enemies. if i == 0: if self.teams[0].score == 7: assert self._bot_types_7 is not None for bottype in self._bot_types_7: self._spawn_bot(bottype) elif self.teams[0].score == 14: assert self._bot_types_14 is not None for bottype in self._bot_types_14: self._spawn_bot(bottype) ba.playsound(self._score_sound) if i == 0: ba.playsound(self._cheer_sound) else: ba.playsound(self._boo_sound) # Kill the flag (it'll respawn shortly). self._flag.scored = True ba.timer(0.2, self._kill_flag) self.update_scores() light = ba.newnode('light', attrs={ 'position': ba.getcollision().position, 'height_attenuated': False, 'color': (1, 0, 0) }) ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) ba.timer(1.0, light.delete) if i == 0: ba.cameraflash(duration=10.0) def end_game(self) -> None: ba.setmusic(None) self._bots.final_celebrate() ba.timer(0.001, ba.Call(self.do_end, 'defeat')) def on_continue(self) -> None: # Subtract one touchdown from the bots and get them moving again. assert self._bot_team is not None self._bot_team.score -= 7 self._bots.start_moving() self.update_scores() def update_scores(self) -> None: """ update scoreboard and check for winners """ # FIXME: tidy this up # pylint: disable=too-many-nested-blocks have_scoring_team = False win_score = self._score_to_win for team in [self.teams[0], self._bot_team]: assert team is not None assert self._scoreboard is not None self._scoreboard.set_team_value(team, team.score, win_score) if team.score >= win_score: if not have_scoring_team: self._scoring_team = team if team is self._bot_team: self.continue_or_end_game() else: ba.setmusic(ba.MusicType.VICTORY) # Completion achievements. assert self._bot_team is not None if self._preset in ['rookie', 'rookie_easy']: self._award_achievement('Rookie Football Victory', sound=False) if self._bot_team.score == 0: self._award_achievement( 'Rookie Football Shutout', sound=False) elif self._preset in ['pro', 'pro_easy']: self._award_achievement('Pro Football Victory', sound=False) if self._bot_team.score == 0: self._award_achievement('Pro Football Shutout', sound=False) elif self._preset in ['uber', 'uber_easy']: self._award_achievement('Uber Football Victory', sound=False) if self._bot_team.score == 0: self._award_achievement( 'Uber Football Shutout', sound=False) if (not self._player_has_dropped_bomb and not self._player_has_punched): self._award_achievement('Got the Moves', sound=False) self._bots.stop_moving() self.show_zoom_message(ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0) self.celebrate(10.0) assert self._starttime_ms is not None self._final_time_ms = int( ba.time(timeformat=ba.TimeFormat.MILLISECONDS) - self._starttime_ms) self._time_text_timer = None assert (self._time_text_input is not None and self._time_text_input.node) self._time_text_input.node.timemax = ( self._final_time_ms) # FIXME: Does this still need to be deferred? ba.pushcall(ba.Call(self.do_end, 'victory')) def do_end(self, outcome: str) -> None: """End the game with the specified outcome.""" if outcome == 'defeat': self.fade_to_red() assert self._final_time_ms is not None scoreval = (None if outcome == 'defeat' else int(self._final_time_ms // 10)) self.end(delay=3.0, results={ 'outcome': outcome, 'score': scoreval, 'score_order': 'decreasing', 'playerinfos': self.initialplayerinfos }) def handlemessage(self, msg: Any) -> Any: """ handle high-level game messages """ if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): # Every time a bad guy dies, spawn a new one. ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) elif isinstance(msg, SpazBotPunchedMessage): if self._preset in ['rookie', 'rookie_easy']: if msg.damage >= 500: self._award_achievement('Super Punch') elif self._preset in ['pro', 'pro_easy']: if msg.damage >= 1000: self._award_achievement('Super Mega Punch') # Respawn dead flags. elif isinstance(msg, FlagDiedMessage): assert isinstance(msg.flag, FootballFlag) msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) self._flag_respawn_light = ba.NodeActor( ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'radius': 0.15, 'color': (1.0, 1.0, 0.3) })) assert self._flag_respawn_light.node ba.animate(self._flag_respawn_light.node, 'intensity', { 0: 0, 0.25: 0.15, 0.5: 0 }, loop=True) ba.timer(3.0, self._flag_respawn_light.node.delete) else: return super().handlemessage(msg) return None def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None: del player, bomb # Unused. self._player_has_dropped_bomb = True def _handle_player_punched(self, player: Spaz) -> None: del player # Unused. self._player_has_punched = True def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player, position=self.map.get_start_position( player.team.id)) if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: spaz.impact_scale = 0.25 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) spaz.punch_callback = self._handle_player_punched return spaz def _flash_flag_spawn(self) -> None: light = ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'height_attenuated': False, 'color': (1, 1, 0) }) ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) ba.timer(1.0, light.delete) def _spawn_flag(self) -> None: ba.playsound(self._swipsound) ba.playsound(self._whistle_sound) self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = FootballFlag(position=self._flag_spawn_pos)
class 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.' game_settings = [ ('Hold Time', { 'min_value': 10, 'default': 30, 'increment': 10 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ] score_info = ba.ScoreInfo(label='Time Held') @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.MultiTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('king_of_the_hill') def __init__(self, settings: Dict[str, Any]): super().__init__(settings) shared = SharedObjects.get() self._scoreboard = Scoreboard() self._swipsound = ba.getsound('swip') self._tick_sound = ba.getsound('tick') self._countdownsounds = { 10: ba.getsound('announceTen'), 9: ba.getsound('announceNine'), 8: ba.getsound('announceEight'), 7: ba.getsound('announceSeven'), 6: ba.getsound('announceSix'), 5: ba.getsound('announceFive'), 4: ba.getsound('announceFour'), 3: ba.getsound('announceThree'), 2: ba.getsound('announceTwo'), 1: ba.getsound('announceOne') } self._flag_pos: Optional[Sequence[float]] = None self._flag_state: Optional[FlagState] = None self._flag: Optional[Flag] = None self._flag_light: Optional[ba.Node] = None self._scoring_team: Optional[ReferenceType[Team]] = None self._hold_time = int(settings['Hold Time']) self._time_limit = float(settings['Time Limit']) self._flag_region_material = ba.Material() self._flag_region_material.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', ba.Call(self._handle_player_flag_region_collide, True)), ('call', 'at_disconnect', ba.Call(self._handle_player_flag_region_collide, False)), )) # Base class overrides. self.default_music = ba.MusicType.SCARY def get_instance_description(self) -> Union[str, Sequence]: return 'Secure the flag for ${ARG1} seconds.', self._hold_time def get_instance_description_short(self) -> Union[str, Sequence]: return 'secure the flag for ${ARG1} seconds', self._hold_time def create_team(self, sessionteam: ba.SessionTeam) -> Team: return Team(time_remaining=self._hold_time) def on_begin(self) -> None: super().on_begin() shared = SharedObjects.get() self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._flag_pos = self.map.get_flag_position(None) ba.timer(1.0, self._tick, repeat=True) self._flag_state = FlagState.NEW Flag.project_stand(self._flag_pos) self._flag = Flag(position=self._flag_pos, touchable=False, color=(1, 1, 1)) self._flag_light = ba.newnode('light', attrs={ 'position': self._flag_pos, 'intensity': 0.2, 'height_attenuated': False, 'radius': 0.4, 'color': (0.2, 0.2, 0.2) }) # Flag region. flagmats = [self._flag_region_material, shared.region_material] ba.newnode('region', attrs={ 'position': self._flag_pos, 'scale': (1.8, 1.8, 1.8), 'type': 'sphere', 'materials': flagmats }) self._update_flag_state() def _tick(self) -> None: self._update_flag_state() # Give holding players points. for player in self.players: if player.time_at_flag > 0: self.stats.player_scored(player, 3, screenmessage=False, display=False) if self._scoring_team is None: scoring_team = None else: scoring_team = self._scoring_team() if scoring_team: if scoring_team.time_remaining > 0: ba.playsound(self._tick_sound) scoring_team.time_remaining = max(0, scoring_team.time_remaining - 1) self._update_scoreboard() if scoring_team.time_remaining > 0: assert self._flag is not None self._flag.set_score_text(str(scoring_team.time_remaining)) # Announce numbers we have sounds for. try: ba.playsound( self._countdownsounds[scoring_team.time_remaining]) except Exception: pass # winner if scoring_team.time_remaining <= 0: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, self._hold_time - team.time_remaining) self.end(results=results, announce_delay=0) def _update_flag_state(self) -> None: holding_teams = set(player.team for player in self.players if player.time_at_flag) prev_state = self._flag_state assert self._flag_light assert self._flag is not None assert self._flag.node if len(holding_teams) > 1: self._flag_state = FlagState.CONTESTED self._scoring_team = None self._flag_light.color = (0.6, 0.6, 0.1) self._flag.node.color = (1.0, 1.0, 0.4) elif len(holding_teams) == 1: holding_team = list(holding_teams)[0] self._flag_state = FlagState.HELD self._scoring_team = weakref.ref(holding_team) self._flag_light.color = ba.normalized_color(holding_team.color) self._flag.node.color = holding_team.color else: self._flag_state = FlagState.UNCONTESTED self._scoring_team = None self._flag_light.color = (0.2, 0.2, 0.2) self._flag.node.color = (1, 1, 1) if self._flag_state != prev_state: ba.playsound(self._swipsound) def _handle_player_flag_region_collide(self, colliding: bool) -> None: try: player = ba.getcollision().opposingnode.getdelegate( PlayerSpaz, True).getplayer(Player, True) except ba.NotFoundError: return # Different parts of us can collide so a single value isn't enough # also don't count it if we're dead (flying heads shouldn't be able to # win the game :-) if colliding and player.is_alive(): player.time_at_flag += 1 else: player.time_at_flag = max(0, player.time_at_flag - 1) self._update_flag_state() def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.time_remaining, self._hold_time, countdown=True) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment default. # No longer can count as time_at_flag once dead. player = msg.getplayer(Player) player.time_at_flag = 0 self._update_flag_state() self.respawn_player(player)
class 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 DeathMatchGame(ba.TeamGameActivity): """A game type based on acquiring kills.""" @classmethod def get_name(cls) -> str: return 'Death Match' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Kill a set number of enemies to win.' @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession)) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('melee') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: settings: List[Tuple[str, Dict[str, Any]]] = [ ('Kills to Win Per Player', { 'min_value': 1, 'default': 5, 'increment': 1 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ('Epic Mode', { 'default': False }) ] # yapf: disable # 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(('Allow Negative Scores', {'default': False})) return settings def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) if self.settings_raw['Epic Mode']: self.slow_motion = True # Print messages when players die since it matters here. self.announce_player_deaths = True self._scoreboard = Scoreboard() self._score_to_win = None self._dingsound = ba.getsound('dingSmall') def get_instance_description(self) -> Union[str, Sequence]: return 'Crush ${ARG1} of your enemies.', self._score_to_win def get_instance_scoreboard_description(self) -> Union[str, Sequence]: return 'kill ${ARG1} enemies', self._score_to_win def on_transition_in(self) -> None: self.default_music = (ba.MusicType.EPIC if self.settings_raw['Epic Mode'] else ba.MusicType.TO_THE_DEATH) super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['score'] = 0 if self.has_begun(): self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings_raw['Time Limit']) self.setup_standard_powerup_drops() if self.teams: self._score_to_win = ( self.settings_raw['Kills to Win Per Player'] * max(1, max(len(t.players) for t in self.teams))) else: self._score_to_win = self.settings_raw['Kills to Win Per Player'] self._update_scoreboard() def handlemessage(self, msg: Any) -> Any: # pylint: disable=too-many-branches if isinstance(msg, playerspaz.PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) player = msg.spaz.player self.respawn_player(player) killer = msg.killerplayer 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.gamedata['score'] - 1 if not self.settings_raw['Allow Negative Scores']: new_score = max(0, new_score) player.team.gamedata['score'] = new_score # In teams-mode it gives a point to the other team. else: ba.playsound(self._dingsound) for team in self.teams: if team is not killer.team: team.gamedata['score'] += 1 # Killing someone on another team nets a kill. else: killer.team.gamedata['score'] += 1 ba.playsound(self._dingsound) # In FFA show scores since its hard to find on the scoreboard. try: if isinstance(killer.actor, stdspaz.Spaz): killer.actor.set_score_text( str(killer.team.gamedata['score']) + '/' + str(self._score_to_win), color=killer.team.color, flash=True) except Exception: pass 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.gamedata['score'] >= self._score_to_win for team in self.teams): ba.timer(0.5, self.end_game) else: super().handlemessage(msg) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.gamedata['score'], self._score_to_win) def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['score']) self.end(results=results)
class ChosenOneGame(ba.TeamGameActivity): """ 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. """ @classmethod def get_name(cls) -> str: return 'Chosen One' @classmethod def get_score_info(cls) -> Dict[str, Any]: return {'score_name': 'Time Held'} @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return ('Be the chosen one for a length of time to win.\n' 'Kill the chosen one to become it.') @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('keep_away') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [('Chosen One Time', { 'min_value': 10, 'default': 30, 'increment': 10 }), ('Chosen One Gets Gloves', { 'default': True }), ('Chosen One Gets Shield', { 'default': False }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ('Epic Mode', { 'default': False })] def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) if self.settings['Epic Mode']: self.slow_motion = True self._scoreboard = Scoreboard() self._chosen_one_player: Optional[ba.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.Flag] = None self._reset_region: Optional[ba.Node] = None def get_instance_description(self) -> Union[str, Sequence]: return 'There can be only one.' def on_transition_in(self) -> None: self.default_music = (ba.MusicType.EPIC if self.settings['Epic Mode'] else ba.MusicType.CHOSEN_ONE) super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['time_remaining'] = self.settings['Chosen One Time'] self._update_scoreboard() def on_player_leave(self, player: ba.Player) -> None: ba.TeamGameActivity.on_player_leave(self, player) if self._get_chosen_one_player() is player: self._set_chosen_one_player(None) def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings['Time Limit']) self.setup_standard_powerup_drops() self._flag_spawn_pos = self.map.get_flag_position(None) self.project_flag_stand(self._flag_spawn_pos) self._set_chosen_one_player(None) pos = self._flag_spawn_pos ba.timer(1.0, call=self._tick, repeat=True) mat = self._reset_region_material = ba.Material() mat.add_actions(conditions=('they_have_material', ba.sharedobj('player_material')), actions=(('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)))) self._reset_region = ba.newnode('region', attrs={ 'position': (pos[0], pos[1] + 0.75, pos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [mat] }) def _get_chosen_one_player(self) -> Optional[ba.Player]: if self._chosen_one_player: return self._chosen_one_player return None def _handle_reset_collide(self) -> None: # If we have a chosen one, ignore these. if self._get_chosen_one_player() is not None: return try: player = (ba.get_collision_info( 'opposing_node').getdelegate().getplayer()) except Exception: return if player is not None and player.is_alive(): self._set_chosen_one_player(player) def _flash_flag_spawn(self) -> None: light = ba.newnode('light', attrs={ 'position': self._flag_spawn_pos, 'color': (1, 1, 1), 'radius': 0.3, 'height_attenuated': False }) ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True) ba.timer(1.0, light.delete) def _tick(self) -> None: # Give the chosen one points. player = self._get_chosen_one_player() if player is not None: # This shouldn't happen, but just in case. if not player.is_alive(): ba.print_error('got dead player as chosen one in _tick') self._set_chosen_one_player(None) else: scoring_team = player.team assert self.stats self.stats.player_scored(player, 3, screenmessage=False, display=False) scoring_team.gamedata['time_remaining'] = max( 0, scoring_team.gamedata['time_remaining'] - 1) # show the count over their head try: if scoring_team.gamedata['time_remaining'] > 0: if isinstance(player.actor, spaz.Spaz): player.actor.set_score_text( str(scoring_team.gamedata['time_remaining'])) except Exception: pass self._update_scoreboard() # announce numbers we have sounds for try: ba.playsound(self._countdownsounds[ scoring_team.gamedata['time_remaining']]) except Exception: pass # Winner! if scoring_team.gamedata['time_remaining'] <= 0: self.end_game() else: # (player is None) # This shouldn't happen, but just in case. # (Chosen-one player ceasing to exist should # trigger on_player_leave which resets chosen-one) if self._chosen_one_player is not None: ba.print_error('got nonexistent player as chosen one in _tick') self._set_chosen_one_player(None) def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score( team, self.settings['Chosen One Time'] - team.gamedata['time_remaining']) self.end(results=results, announce_delay=0) def _set_chosen_one_player(self, player: Optional[ba.Player]) -> None: try: for p_other in self.players: p_other.gamedata['chosen_light'] = None ba.playsound(self._swipsound) if not player: assert self._flag_spawn_pos is not None self._flag = 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 is not None: self._flag = None self._chosen_one_player = player if player.actor: if self.settings['Chosen One Gets Shield']: player.actor.handlemessage( ba.PowerupMessage('shield')) if self.settings['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.gamedata['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.PlayerSpaz) player.actor.node.connectattr('position', light.node, 'position') except Exception: ba.print_exception('EXC in _set_chosen_one_player') def handlemessage(self, msg: Any) -> Any: if isinstance(msg, playerspaz.PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) player = msg.spaz.player if player is self._get_chosen_one_player(): killerplayer = msg.killerplayer 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.gamedata['time_remaining'], self.settings['Chosen One Time'], countdown=True)
class HockeyGame(ba.TeamGameActivity): """Ice hockey game.""" @classmethod def get_name(cls) -> str: return 'Hockey' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Score some goals.' @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.TeamsSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('hockey') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [ ("Score to Win", { 'min_value': 1, 'default': 1, 'increment': 1 }), ("Time Limit", { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ("Respawn Times", { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 })] # yapf: disable def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard from bastd.actor import powerupbox super().__init__(settings) 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", ba.sharedobj('pickup_material')), actions=("modify_part_collision", "collide", False)) self.puck_material.add_actions( conditions=(("we_are_younger_than", 100), 'and', ("they_have_material", ba.sharedobj('object_material'))), actions=("modify_node_collision", "collide", False)) self.puck_material.add_actions( conditions=("they_have_material", ba.sharedobj('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", ba.sharedobj('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", powerupbox.get_factory().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 def get_instance_description(self) -> Union[str, Sequence]: if self.settings['Score to Win'] == 1: return 'Score a goal.' return 'Score ${ARG1} goals.', self.settings['Score to Win'] def get_instance_scoreboard_description(self) -> Union[str, Sequence]: if self.settings['Score to Win'] == 1: return 'score a goal' return 'score ${ARG1} goals', self.settings['Score to Win'] def on_transition_in(self) -> None: self.default_music = ba.MusicType.HOCKEY super().on_transition_in() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings['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: ba.Team) -> None: team.gamedata['score'] = 0 self._update_scoreboard() def _handle_puck_player_collide(self) -> None: try: pucknode, playernode = ba.get_collision_info( 'source_node', 'opposing_node') puck = pucknode.getdelegate() player = playernode.getdelegate().getplayer() except Exception: player = puck = None if player and puck: puck.last_players_to_touch[player.team.get_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.get_collision_info("source_node") index = 0 for index in range(len(self._score_regions)): if region == self._score_regions[index].node: break for team in self.teams: if team.get_id() == index: scoring_team = team team.gamedata['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.get_id() in self._puck.last_players_to_touch and self._puck.last_players_to_touch[ scoring_team.get_id()]): self.stats.player_scored(self._puck.last_players_to_touch[ scoring_team.get_id()], 100, big_message=True) # End game if we won. if team.gamedata['score'] >= self.settings['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.get_collision_info('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.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['score']) self.end(results=results) def _update_scoreboard(self) -> None: """ update scoreboard and check for winners """ winscore = self.settings['Score to Win'] for team in self.teams: self._scoreboard.set_team_value(team, team.gamedata['score'], winscore) def handlemessage(self, msg: Any) -> Any: # Respawn dead players if they're still in the game. if isinstance(msg, playerspaz.PlayerSpazDeathMessage): # Augment standard behavior... super().handlemessage(msg) self.respawn_player(msg.spaz.player) # Respawn dead pucks. elif isinstance(msg, PuckDeathMessage): 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 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 EasterEggHuntGame(ba.TeamGameActivity): """A game where score is based on collecting eggs.""" @classmethod def get_name(cls) -> str: return 'Easter Egg Hunt' @classmethod def get_score_info(cls) -> Dict[str, Any]: return {'score_name': 'Score', 'score_type': 'points'} @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Gather eggs!' # 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.TeamsSession) or issubclass(sessiontype, ba.FreeForAllSession)) @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [("Pro Mode", {'default': False})] def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) self._last_player_death_time = None self._scoreboard = Scoreboard() self.egg_model = ba.getmodel('egg') self.egg_tex_1 = ba.gettexture('eggTex1') self.egg_tex_2 = ba.gettexture('eggTex2') self.egg_tex_3 = ba.gettexture('eggTex3') self._collect_sound = ba.getsound('powerup01') self._pro_mode = settings.get('Pro Mode', False) self._max_eggs = 1.0 self.egg_material = ba.Material() self.egg_material.add_actions( conditions=("they_have_material", ba.sharedobj('player_material')), actions=(("call", "at_connect", self._on_egg_player_collide), )) self._eggs: List[Egg] = [] self._update_timer: Optional[ba.Timer] = None self._countdown: Optional[OnScreenCountdown] = None self._bots: Optional[spazbot.BotSet] = None # Called when our game is transitioning in but not ready to start. # ..we can go ahead and set our music and whatnot. def on_transition_in(self) -> None: self.default_music = ba.MusicType.FORWARD_MARCH super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['score'] = 0 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 = spazbot.BotSet() # Spawn evil bunny in co-op only. if isinstance(self.session, ba.CoopSession) and self._pro_mode: self._spawn_evil_bunny() # Overriding the default character spawning. def spawn_player(self, player: ba.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(spazbot.BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0) def _on_egg_player_collide(self) -> None: if not self.has_ended(): egg_node, playernode = ba.get_collision_info( 'source_node', 'opposing_node') if egg_node is not None and playernode is not None: egg = egg_node.getdelegate() assert isinstance(egg, Egg) spaz = playernode.getdelegate() assert isinstance(spaz, playerspaz.PlayerSpaz) player = (spaz.getplayer() if hasattr(spaz, 'getplayer') else None) if player and egg: player.team.gamedata['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.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, playerspaz.PlayerSpazDeathMessage): from bastd.actor import respawnicon # Augment standard behavior. super().handlemessage(msg) player = msg.spaz.getplayer() if not player: return self.stats.player_was_killed(player) # Respawn them shortly. assert self.initial_player_info is not None respawn_time = 2.0 + len(self.initial_player_info) * 1.0 player.gamedata['respawn_timer'] = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.gamedata['respawn_icon'] = respawnicon.RespawnIcon( player, respawn_time) # Whenever our evil bunny dies, respawn him and spew some eggs. elif isinstance(msg, spazbot.SpazBotDeathMessage): self._spawn_evil_bunny() assert msg.badguy.node pos = msg.badguy.node.position for _i in range(6): spread = 0.4 self._eggs.append( Egg(position=(pos[0] + random.uniform(-spread, spread), pos[1] + random.uniform(-spread, spread), pos[2] + random.uniform(-spread, spread)))) else: # Default handler. super().handlemessage(msg) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.gamedata['score']) def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['score']) self.end(results)
class AssaultGame(ba.TeamGameActivity): """Game where you score by touching the other team's flag.""" @classmethod def get_name(cls) -> str: return 'Assault' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Reach the enemy flag to score.' @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') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [('Score to Win', {'min_value': 1, 'default': 3}), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0}), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0}), ('Epic Mode', {'default': False})] # yapf: disable def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) self._scoreboard = Scoreboard() if self.settings_raw['Epic Mode']: self.slow_motion = True self._last_score_time = 0.0 self._score_sound = ba.getsound('score') self._base_region_materials: Dict[int, ba.Material] = {} def get_instance_description(self) -> Union[str, Sequence]: if self.settings_raw['Score to Win'] == 1: return 'Touch the enemy flag.' return ('Touch the enemy flag ${ARG1} times.', self.settings_raw['Score to Win']) def get_instance_scoreboard_description(self) -> Union[str, Sequence]: if self.settings_raw['Score to Win'] == 1: return 'touch 1 flag' return 'touch ${ARG1} flags', self.settings_raw['Score to Win'] def on_transition_in(self) -> None: self.default_music = (ba.MusicType.EPIC if self.settings_raw['Epic Mode'] else ba.MusicType.FORWARD_MARCH) super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['score'] = 0 self._update_scoreboard() def on_begin(self) -> None: from bastd.actor.flag import Flag super().on_begin() self.setup_standard_time_limit(self.settings_raw['Time Limit']) self.setup_standard_powerup_drops() for team in self.teams: mat = self._base_region_materials[team.get_id()] = ba.Material() mat.add_actions(conditions=('they_have_material', ba.sharedobj('player_material')), actions=(('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', ba.Call(self._handle_base_collide, team)))) # Create a score region and flag for each team. for team in self.teams: team.gamedata['base_pos'] = self.map.get_flag_position( team.get_id()) ba.newnode('light', attrs={ 'position': team.gamedata['base_pos'], 'intensity': 0.6, 'height_attenuated': False, 'volume_intensity_scale': 0.1, 'radius': 0.1, 'color': team.color }) self.project_flag_stand(team.gamedata['base_pos']) team.gamedata['flag'] = Flag(touchable=False, position=team.gamedata['base_pos'], color=team.color) basepos = team.gamedata['base_pos'] ba.newnode('region', owner=team.gamedata['flag'].node, attrs={ 'position': (basepos[0], basepos[1] + 0.75, basepos[2]), 'scale': (0.5, 0.5, 0.5), 'type': 'sphere', 'materials': [self._base_region_materials[team.get_id()]] }) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, playerspaz.PlayerSpazDeathMessage): super().handlemessage(msg) # Augment standard. self.respawn_player(msg.spaz.player) else: super().handlemessage(msg) def _flash_base(self, team: ba.Team, length: float = 2.0) -> None: light = ba.newnode('light', attrs={ 'position': team.gamedata['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: ba.Team) -> None: # Attempt to pull a living ba.Player from what we hit. cnode = ba.get_collision_info('opposing_node') assert isinstance(cnode, ba.Node) actor = cnode.getdelegate() if not isinstance(actor, playerspaz.PlayerSpaz): return player = actor.getplayer() if not player or 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(): if player.node: 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.get_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.gamedata['score'] += 1 self._update_scoreboard() if (player_team.gamedata['score'] >= self.settings_raw['Score to Win']): self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['score']) self.end(results=results) def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.gamedata['score'], self.settings_raw['Score to Win'])
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 KeepAwayGame(ba.TeamGameActivity): """Game where you try to keep the flag away from your enemies.""" FLAG_NEW = 0 FLAG_UNCONTESTED = 1 FLAG_CONTESTED = 2 FLAG_HELD = 3 @classmethod def get_name(cls) -> str: return 'Keep Away' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Carry the flag for a set length of time.' @classmethod def get_score_info(cls) -> Dict[str, Any]: return {'score_name': 'Time Held'} @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') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [ ('Hold Time', { 'min_value': 10, 'default': 30, 'increment': 10 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }) ] # yapf: disable def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard 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[ba.Player] = [] self._flag_state: Optional[int] = None self._flag_light: Optional[ba.Node] = None self._scoring_team: Optional[ba.Team] = None self._flag: Optional[stdflag.Flag] = None def get_instance_description(self) -> Union[str, Sequence]: return ('Carry the flag for ${ARG1} seconds.', self.settings['Hold Time']) def get_instance_scoreboard_description(self) -> Union[str, Sequence]: return ('carry the flag for ${ARG1} seconds', self.settings['Hold Time']) def on_transition_in(self) -> None: self.default_music = ba.MusicType.KEEP_AWAY super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['time_remaining'] = self.settings['Hold Time'] self._update_scoreboard() def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings['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() self.project_flag_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) scoring_team = self._scoring_team if scoring_team is not None: if scoring_team.gamedata['time_remaining'] > 0: ba.playsound(self._tick_sound) scoring_team.gamedata['time_remaining'] = max( 0, scoring_team.gamedata['time_remaining'] - 1) self._update_scoreboard() if scoring_team.gamedata['time_remaining'] > 0: assert self._flag is not None self._flag.set_score_text( str(scoring_team.gamedata['time_remaining'])) # Announce numbers we have sounds for. try: ba.playsound(self._countdownsounds[ scoring_team.gamedata['time_remaining']]) except Exception: pass # Winner. if scoring_team.gamedata['time_remaining'] <= 0: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score( team, self.settings['Hold Time'] - team.gamedata['time_remaining']) self.end(results=results, announce_delay=0) def _update_flag_state(self) -> None: for team in self.teams: team.gamedata['holding_flag'] = False self._holding_players = [] for player in self.players: holding_flag = False try: assert isinstance(player.actor, playerspaz.PlayerSpaz) if (player.actor.is_alive() and player.actor.node and player.actor.node.hold_node): holding_flag = ( player.actor.node.hold_node.getnodetype() == 'flag') except Exception: ba.print_exception('exception checking hold flag') if holding_flag: self._holding_players.append(player) player.team.gamedata['holding_flag'] = True holding_teams = set(t for t in self.teams if t.gamedata['holding_flag']) prev_state = self._flag_state assert self._flag is not None assert self._flag_light assert self._flag.node if len(holding_teams) > 1: self._flag_state = self.FLAG_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 = self.FLAG_HELD self._scoring_team = holding_team self._flag_light.color = ba.normalized_color(holding_team.color) self._flag.node.color = holding_team.color else: self._flag_state = self.FLAG_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 _spawn_flag(self) -> None: ba.playsound(self._swipsound) self._flash_flag_spawn() assert self._flag_spawn_pos is not None self._flag = stdflag.Flag(dropped_timeout=20, position=self._flag_spawn_pos) self._flag_state = self.FLAG_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.gamedata['time_remaining'], self.settings['Hold Time'], countdown=True) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, playerspaz.PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.spaz.player) elif isinstance(msg, stdflag.FlagDeathMessage): self._spawn_flag() elif isinstance( msg, (stdflag.FlagDroppedMessage, stdflag.FlagPickedUpMessage)): self._update_flag_state() else: super().handlemessage(msg)
class RaceGame(ba.TeamGameActivity): """Game of racing around a track.""" @classmethod def get_name(cls) -> str: return 'Race' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Run real fast!' @classmethod def get_score_info(cls) -> Dict[str, Any]: return { 'score_name': 'Time', 'lower_is_better': True, 'score_type': 'milliseconds' } @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.MultiTeamSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps('race') @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: settings: List[Tuple[str, Dict[str, Any]]] = [ ('Laps', { 'min_value': 1, 'default': 3, 'increment': 1 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Mine Spawning', { 'choices': [('No Mines', 0), ('8 Seconds', 8000), ('4 Seconds', 4000), ('2 Seconds', 2000)], 'default': 4000 }), ('Bomb Spawning', { 'choices': [('None', 0), ('8 Seconds', 8000), ('4 Seconds', 4000), ('2 Seconds', 2000), ('1 Second', 1000)], 'default': 2000 }), ('Epic Mode', { 'default': False })] # yapf: disable if issubclass(sessiontype, ba.DualTeamSession): settings.append(('Entire Team Must Finish', {'default': False})) return settings def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard self._race_started = False super().__init__(settings) self._scoreboard = Scoreboard() if self.settings['Epic Mode']: self.slow_motion = True self._score_sound = ba.getsound('score') self._swipsound = ba.getsound('swip') self._last_team_time: Optional[float] = None self._front_race_region: Optional[int] = None self._nub_tex = ba.gettexture('nub') self._beep_1_sound = ba.getsound('raceBeep1') self._beep_2_sound = ba.getsound('raceBeep2') self.race_region_material: Optional[ba.Material] = None self._regions: List[RaceRegion] = [] self._team_finish_pts: Optional[int] = None self._time_text: Optional[ba.Actor] = None self._timer: Optional[OnScreenTimer] = None self._race_mines: Optional[List[RaceMine]] = None self._race_mine_timer: Optional[ba.Timer] = None self._scoreboard_timer: Optional[ba.Timer] = None self._player_order_update_timer: Optional[ba.Timer] = None self._start_lights: Optional[List[ba.Node]] = None self._bomb_spawn_timer: Optional[ba.Timer] = None def get_instance_description(self) -> Union[str, Sequence]: if isinstance(self.session, ba.DualTeamSession) and self.settings.get( 'Entire Team Must Finish', False): t_str = ' Your entire team has to finish.' else: t_str = '' if self.settings['Laps'] > 1: return 'Run ${ARG1} laps.' + t_str, self.settings['Laps'] return 'Run 1 lap.' + t_str def get_instance_scoreboard_description(self) -> Union[str, Sequence]: if self.settings['Laps'] > 1: return 'run ${ARG1} laps', self.settings['Laps'] return 'run 1 lap' def on_transition_in(self) -> None: self.default_music = (ba.MusicType.EPIC_RACE if self.settings['Epic Mode'] else ba.MusicType.RACE) super().on_transition_in() pts = self.map.get_def_points('race_point') mat = self.race_region_material = ba.Material() mat.add_actions(conditions=('they_have_material', ba.sharedobj('player_material')), actions=(('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_race_point_collide))) for rpt in pts: self._regions.append(RaceRegion(rpt, len(self._regions))) def _flash_player(self, player: ba.Player, scale: float) -> None: assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = player.actor.node.position light = ba.newnode('light', attrs={ 'position': pos, 'color': (1, 1, 0), 'height_attenuated': False, 'radius': 0.4 }) ba.timer(0.5, light.delete) ba.animate(light, 'intensity', {0: 0, 0.1: 1.0 * scale, 0.5: 0}) def _handle_race_point_collide(self) -> None: # FIXME: Tidy this up. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks region_node, playernode = ba.get_collision_info( 'source_node', 'opposing_node') try: player = playernode.getdelegate().getplayer() except Exception: player = None region = region_node.getdelegate() if not player or not region: return assert isinstance(player, ba.Player) assert isinstance(region, RaceRegion) last_region = player.gamedata['last_region'] this_region = region.index if last_region != this_region: # If a player tries to skip regions, smite them. # Allow a one region leeway though (its plausible players can get # blown over a region, etc). if this_region > last_region + 2: if player.is_alive(): assert player.actor player.actor.handlemessage(ba.DieMessage()) ba.screenmessage(ba.Lstr( translate=('statements', 'Killing ${NAME} for' ' skipping part of the track!'), subs=[('${NAME}', player.get_name(full=True))]), color=(1, 0, 0)) else: # If this player is in first, note that this is the # front-most race-point. if player.gamedata['rank'] == 0: self._front_race_region = this_region player.gamedata['last_region'] = this_region if last_region >= len(self._regions) - 2 and this_region == 0: team = player.team player.gamedata['lap'] = min(self.settings['Laps'], player.gamedata['lap'] + 1) # In teams mode with all-must-finish on, the team lap # value is the min of all team players. # Otherwise its the max. if isinstance(self.session, ba.DualTeamSession) and self.settings.get( 'Entire Team Must Finish'): team.gamedata['lap'] = min( [p.gamedata['lap'] for p in team.players]) else: team.gamedata['lap'] = max( [p.gamedata['lap'] for p in team.players]) # A player is finishing. if player.gamedata['lap'] == self.settings['Laps']: # In teams mode, hand out points based on the order # players come in. if isinstance(self.session, ba.DualTeamSession): assert self._team_finish_pts is not None if self._team_finish_pts > 0: self.stats.player_scored(player, self._team_finish_pts, screenmessage=False) self._team_finish_pts -= 25 # Flash where the player is. self._flash_player(player, 1.0) player.gamedata['finished'] = True assert player.actor player.actor.handlemessage( ba.DieMessage(immediate=True)) # Makes sure noone behind them passes them in rank # while finishing. player.gamedata['distance'] = 9999.0 # If the whole team has finished the race. if team.gamedata['lap'] == self.settings['Laps']: ba.playsound(self._score_sound) player.team.gamedata['finished'] = True assert self._timer is not None cur_time = ba.time( timeformat=ba.TimeFormat.MILLISECONDS) start_time = self._timer.getstarttime( timeformat=ba.TimeFormat.MILLISECONDS) self._last_team_time = ( player.team.gamedata['time']) = (cur_time - start_time) self._check_end_game() # Team has yet to finish. else: ba.playsound(self._swipsound) # They've just finished a lap but not the race. else: ba.playsound(self._swipsound) self._flash_player(player, 0.3) # Print their lap number over their head. try: assert isinstance(player.actor, PlayerSpaz) mathnode = ba.newnode('math', owner=player.actor.node, attrs={ 'input1': (0, 1.9, 0), 'operation': 'add' }) player.actor.node.connectattr( 'torso_position', mathnode, 'input2') tstr = ba.Lstr(resource='lapNumberText', subs=[('${CURRENT}', str(player.gamedata['lap'] + 1)), ('${TOTAL}', str(self.settings['Laps']))]) txtnode = ba.newnode('text', owner=mathnode, attrs={ 'text': tstr, 'in_world': True, 'color': (1, 1, 0, 1), 'scale': 0.015, 'h_align': 'center' }) mathnode.connectattr('output', txtnode, 'position') ba.animate(txtnode, 'scale', { 0.0: 0, 0.2: 0.019, 2.0: 0.019, 2.2: 0 }) ba.timer(2.3, mathnode.delete) except Exception as exc: print('Exception printing lap:', exc) def on_team_join(self, team: ba.Team) -> None: team.gamedata['time'] = None team.gamedata['lap'] = 0 team.gamedata['finished'] = False self._update_scoreboard() def on_player_join(self, player: ba.Player) -> None: player.gamedata['last_region'] = 0 player.gamedata['lap'] = 0 player.gamedata['distance'] = 0.0 player.gamedata['finished'] = False player.gamedata['rank'] = None ba.TeamGameActivity.on_player_join(self, player) def on_player_leave(self, player: ba.Player) -> None: ba.TeamGameActivity.on_player_leave(self, player) # A player leaving disqualifies the team if 'Entire Team Must Finish' # is on (otherwise in teams mode everyone could just leave except the # leading player to win). if (isinstance(self.session, ba.DualTeamSession) and self.settings.get('Entire Team Must Finish')): ba.screenmessage(ba.Lstr( translate=('statements', '${TEAM} is disqualified because ${PLAYER} left'), subs=[('${TEAM}', player.team.name), ('${PLAYER}', player.get_name(full=True))]), color=(1, 1, 0)) player.team.gamedata['finished'] = True player.team.gamedata['time'] = None player.team.gamedata['lap'] = 0 ba.playsound(ba.getsound('boo')) for otherplayer in player.team.players: otherplayer.gamedata['lap'] = 0 otherplayer.gamedata['finished'] = True try: if otherplayer.actor is not None: otherplayer.actor.handlemessage(ba.DieMessage()) except Exception: ba.print_exception('Error sending diemessages') # Defer so team/player lists will be updated. ba.pushcall(self._check_end_game) def _update_scoreboard(self) -> None: for team in self.teams: distances = [ player.gamedata['distance'] for player in team.players ] if not distances: teams_dist = 0 else: if (isinstance(self.session, ba.DualTeamSession) and self.settings.get('Entire Team Must Finish')): teams_dist = min(distances) else: teams_dist = max(distances) self._scoreboard.set_team_value( team, teams_dist, self.settings['Laps'], flash=(teams_dist >= float(self.settings['Laps'])), show_value=False) def on_begin(self) -> None: from bastd.actor.onscreentimer import OnScreenTimer super().on_begin() self.setup_standard_time_limit(self.settings['Time Limit']) self.setup_standard_powerup_drops() self._team_finish_pts = 100 # Throw a timer up on-screen. self._time_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 0.5, 1), 'flatness': 0.5, 'shadow': 0.5, 'position': (0, -50), 'scale': 1.4, 'text': '' })) self._timer = OnScreenTimer() if self.settings['Mine Spawning'] != 0: self._race_mines = [ RaceMine(point=p, mine=None) for p in self.map.get_def_points('race_mine') ] if self._race_mines: self._race_mine_timer = ba.Timer( 0.001 * self.settings['Mine Spawning'], self._update_race_mine, repeat=True) self._scoreboard_timer = ba.Timer(0.25, self._update_scoreboard, repeat=True) self._player_order_update_timer = ba.Timer(0.25, self._update_player_order, repeat=True) if self.slow_motion: t_scale = 0.4 light_y = 50 else: t_scale = 1.0 light_y = 150 lstart = 7.1 * t_scale inc = 1.25 * t_scale ba.timer(lstart, self._do_light_1) ba.timer(lstart + inc, self._do_light_2) ba.timer(lstart + 2 * inc, self._do_light_3) ba.timer(lstart + 3 * inc, self._start_race) self._start_lights = [] for i in range(4): lnub = ba.newnode('image', attrs={ 'texture': ba.gettexture('nub'), 'opacity': 1.0, 'absolute_scale': True, 'position': (-75 + i * 50, light_y), 'scale': (50, 50), 'attach': 'center' }) ba.animate( lnub, 'opacity', { 4.0 * t_scale: 0, 5.0 * t_scale: 1.0, 12.0 * t_scale: 1.0, 12.5 * t_scale: 0.0 }) ba.timer(13.0 * t_scale, lnub.delete) self._start_lights.append(lnub) self._start_lights[0].color = (0.2, 0, 0) self._start_lights[1].color = (0.2, 0, 0) self._start_lights[2].color = (0.2, 0.05, 0) self._start_lights[3].color = (0.0, 0.3, 0) def _do_light_1(self) -> None: assert self._start_lights is not None self._start_lights[0].color = (1.0, 0, 0) ba.playsound(self._beep_1_sound) def _do_light_2(self) -> None: assert self._start_lights is not None self._start_lights[1].color = (1.0, 0, 0) ba.playsound(self._beep_1_sound) def _do_light_3(self) -> None: assert self._start_lights is not None self._start_lights[2].color = (1.0, 0.3, 0) ba.playsound(self._beep_1_sound) def _start_race(self) -> None: assert self._start_lights is not None self._start_lights[3].color = (0.0, 1.0, 0) ba.playsound(self._beep_2_sound) for player in self.players: if player.actor is not None: try: assert isinstance(player.actor, PlayerSpaz) player.actor.connect_controls_to_player() except Exception as exc: print('Exception in race player connects:', exc) assert self._timer is not None self._timer.start() if self.settings['Bomb Spawning'] != 0: self._bomb_spawn_timer = ba.Timer(0.001 * self.settings['Bomb Spawning'], self._spawn_bomb, repeat=True) self._race_started = True def _update_player_order(self) -> None: # FIXME: tidy this up # Calc all player distances. for player in self.players: pos: Optional[ba.Vec3] try: assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = ba.Vec3(player.actor.node.position) except Exception: pos = None if pos is not None: r_index = player.gamedata['last_region'] rg1 = self._regions[r_index] r1pt = ba.Vec3(rg1.pos[:3]) rg2 = self._regions[0] if r_index == len( self._regions) - 1 else self._regions[r_index + 1] r2pt = ba.Vec3(rg2.pos[:3]) r2dist = (pos - r2pt).length() amt = 1.0 - (r2dist / (r2pt - r1pt).length()) amt = player.gamedata['lap'] + (r_index + amt) * ( 1.0 / len(self._regions)) player.gamedata['distance'] = amt # Sort players by distance and update their ranks. p_list = [[player.gamedata['distance'], player] for player in self.players] p_list.sort(reverse=True, key=lambda x: x[0]) for i, plr in enumerate(p_list): try: plr[1].gamedata['rank'] = i if plr[1].actor is not None: # noinspection PyUnresolvedReferences node = plr[1].actor.distance_txt if node: node.text = str(i + 1) if plr[1].is_alive() else '' except Exception: ba.print_exception('error updating player orders') def _spawn_bomb(self) -> None: if self._front_race_region is None: return region = (self._front_race_region + 3) % len(self._regions) pos = self._regions[region].pos # Don't use the full region so we're less likely to spawn off a cliff. region_scale = 0.8 x_range = ((-0.5, 0.5) if pos[3] == 0 else (-region_scale * pos[3], region_scale * pos[3])) z_range = ((-0.5, 0.5) if pos[5] == 0 else (-region_scale * pos[5], region_scale * pos[5])) pos = (pos[0] + random.uniform(*x_range), pos[1] + 1.0, pos[2] + random.uniform(*z_range)) ba.timer(random.uniform(0.0, 2.0), ba.WeakCall(self._spawn_bomb_at_pos, pos)) def _spawn_bomb_at_pos(self, pos: Sequence[float]) -> None: if self.has_ended(): return Bomb(position=pos, bomb_type='normal').autoretain() def _make_mine(self, i: int) -> None: assert self._race_mines is not None rmine = self._race_mines[i] rmine.mine = Bomb(position=rmine.point[:3], bomb_type='land_mine') rmine.mine.arm() def _flash_mine(self, i: int) -> None: assert self._race_mines is not None rmine = self._race_mines[i] light = ba.newnode('light', attrs={ 'position': rmine.point[:3], 'color': (1, 0.2, 0.2), 'radius': 0.1, 'height_attenuated': False }) ba.animate(light, 'intensity', {0.0: 0, 0.1: 1.0, 0.2: 0}, loop=True) ba.timer(1.0, light.delete) def _update_race_mine(self) -> None: assert self._race_mines is not None m_index = -1 rmine = None for _i in range(3): m_index = random.randrange(len(self._race_mines)) rmine = self._race_mines[m_index] if not rmine.mine: break assert rmine is not None if not rmine.mine: self._flash_mine(m_index) ba.timer(0.95, ba.Call(self._make_mine, m_index)) def spawn_player(self, player: ba.Player) -> ba.Actor: if player.team.gamedata['finished']: # FIXME: This is not type-safe # (this call is expected to return an Actor). # noinspection PyTypeChecker return None # type: ignore pos = self._regions[player.gamedata['last_region']].pos # Don't use the full region so we're less likely to spawn off a cliff. region_scale = 0.8 x_range = ((-0.5, 0.5) if pos[3] == 0 else (-region_scale * pos[3], region_scale * pos[3])) z_range = ((-0.5, 0.5) if pos[5] == 0 else (-region_scale * pos[5], region_scale * pos[5])) pos = (pos[0] + random.uniform(*x_range), pos[1], pos[2] + random.uniform(*z_range)) spaz = self.spawn_player_spaz( player, position=pos, angle=90 if not self._race_started else None) assert spaz.node # Prevent controlling of characters before the start of the race. if not self._race_started: spaz.disconnect_controls_from_player() mathnode = ba.newnode('math', owner=spaz.node, attrs={ 'input1': (0, 1.4, 0), 'operation': 'add' }) spaz.node.connectattr('torso_position', mathnode, 'input2') distance_txt = ba.newnode('text', owner=spaz.node, attrs={ 'text': '', 'in_world': True, 'color': (1, 1, 0.4), 'scale': 0.02, 'h_align': 'center' }) # FIXME store this in a type-safe way # noinspection PyTypeHints spaz.distance_txt = distance_txt # type: ignore mathnode.connectattr('output', distance_txt, 'position') return spaz def _check_end_game(self) -> None: # If there's no teams left racing, finish. teams_still_in = len( [t for t in self.teams if not t.gamedata['finished']]) if teams_still_in == 0: self.end_game() return # Count the number of teams that have completed the race. teams_completed = len([ t for t in self.teams if t.gamedata['finished'] and t.gamedata['time'] is not None ]) if teams_completed > 0: session = self.session # In teams mode its over as soon as any team finishes the race # FIXME: The get_ffa_point_awards code looks dangerous. if isinstance(session, ba.DualTeamSession): self.end_game() else: # In ffa we keep the race going while there's still any points # to be handed out. Find out how many points we have to award # and how many teams have finished, and once that matches # we're done. assert isinstance(session, ba.FreeForAllSession) points_to_award = len(session.get_ffa_point_awards()) if teams_completed >= points_to_award - teams_completed: self.end_game() return def end_game(self) -> None: # Stop updating our time text, and set it to show the exact last # finish time if we have one. (so users don't get upset if their # final time differs from what they see onscreen by a tiny bit) assert self._timer is not None if self._timer.has_started(): cur_time = self._timer.getstarttime( timeformat=ba.TimeFormat.MILLISECONDS) self._timer.stop( endtime=None if self._last_team_time is None else ( cur_time + self._last_team_time)) results = ba.TeamGameResults() for team in self.teams: if team.gamedata['time'] is not None: results.set_team_score(team, team.gamedata['time']) # If game have ended before we # get any result, use 'fail' screen # We don't announce a winner in ffa mode since its probably been a # while since the first place guy crossed the finish line so it seems # odd to be announcing that now. self.end(results=results, announce_winning_team=isinstance(self.session, ba.DualTeamSession)) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, PlayerSpazDeathMessage): # Augment default behavior. super().handlemessage(msg) player = msg.spaz.getplayer() if not player: ba.print_error('got no player in PlayerSpazDeathMessage') return if not player.gamedata['finished']: self.respawn_player(player, respawn_time=1) else: super().handlemessage(msg)
class TargetPracticeGame(ba.TeamGameActivity): """Game where players try to hit targets with bombs.""" @classmethod def get_name(cls) -> str: return 'Target Practice' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Bomb as many targets as you can.' @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)) @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [('Target Count', { 'min_value': 1, 'default': 3 }), ('Enable Impact Bombs', { 'default': True }), ('Enable Triple Bombs', { 'default': True })] def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) self._scoreboard = Scoreboard() self._targets: List[Target] = [] self._update_timer: Optional[ba.Timer] = None self._countdown: Optional[OnScreenCountdown] = None def on_transition_in(self) -> None: self.default_music = ba.MusicType.FORWARD_MARCH super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['score'] = 0 if self.has_begun(): self.update_scoreboard() def on_begin(self) -> None: from bastd.actor.onscreencountdown import OnScreenCountdown super().on_begin() self.update_scoreboard() # Number of targets is based on player count. num_targets = self.settings['Target Count'] for i in range(num_targets): 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: ba.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.gamedata['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.settings['Enable Impact Bombs']: spaz.bomb_type = 'impact' if self.settings['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((8.0 * xpos, 2.2, -3.5 + 5.0 * ypos)) def get_min_dist_from_target(pnt: Sequence[float]) -> 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. from bastd.actor.bomb import Bomb # 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() if not player: return # could happen if they leave after throwing a bomb.. bullseye = any( target.do_hit_at_position(pos, player) for target in list(self._targets)) if bullseye: player.gamedata['streak'] += 1 else: player.gamedata['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, playerspaz.PlayerSpazDeathMessage): super().handlemessage(msg) # Do standard stuff. player = msg.spaz.getplayer() 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.gamedata['score']) def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score(team, team.gamedata['score']) self.end(results)
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.' game_settings = [ ('Score to Win', { 'min_value': 1, 'default': 3 }), ('Time Limit', { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), ('Epic Mode', { 'default': False }), ] @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[str, Any]): 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.TeamGameResults() 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 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 RunaroundGame(ba.CoopGameActivity[Player, Team]): """Game involving trying to bomb bots as they walk through the map.""" name = 'Runaround' description = 'Prevent enemies from reaching the exit.' tips = [ 'Jump just as you\'re throwing to get bombs up to the highest levels.', 'No, you can\'t get up on the ledge. You have to throw bombs.', 'Whip back and forth to get more distance on your throws..' ] default_music = ba.MusicType.MARCHING # How fast our various bot types walk. _bot_speed_map: Dict[Type[SpazBot], float] = { BomberBot: 0.48, BomberBotPro: 0.48, BomberBotProShielded: 0.48, BrawlerBot: 0.57, BrawlerBotPro: 0.57, BrawlerBotProShielded: 0.57, TriggerBot: 0.73, TriggerBotPro: 0.78, TriggerBotProShielded: 0.78, ChargerBot: 1.0, ChargerBotProShielded: 1.0, ExplodeyBot: 1.0, StickyBot: 0.5 } def __init__(self, settings: dict): settings['map'] = 'Tower D' super().__init__(settings) shared = SharedObjects.get() self._preset = Preset(settings.get('preset', 'pro')) self._player_death_sound = ba.getsound('playerDeath') self._new_wave_sound = ba.getsound('scoreHit01') self._winsound = ba.getsound('score') self._cashregistersound = ba.getsound('cashRegister') self._bad_guy_score_sound = ba.getsound('shieldDown') self._heart_tex = ba.gettexture('heart') self._heart_model_opaque = ba.getmodel('heartOpaque') self._heart_model_transparent = ba.getmodel('heartTransparent') self._a_player_has_been_killed = False self._spawn_center = self._map_type.defs.points['spawn1'][0:3] self._tntspawnpos = self._map_type.defs.points['tnt_loc'][0:3] self._powerup_center = self._map_type.defs.boxes['powerup_region'][0:3] self._powerup_spread = ( self._map_type.defs.boxes['powerup_region'][6] * 0.5, self._map_type.defs.boxes['powerup_region'][8] * 0.5) self._score_region_material = ba.Material() self._score_region_material.add_actions( conditions=('they_have_material', shared.player_material), actions=( ('modify_part_collision', 'collide', True), ('modify_part_collision', 'physical', False), ('call', 'at_connect', self._handle_reached_end), )) self._last_wave_end_time = ba.time() self._player_has_picked_up_powerup = False self._scoreboard: Optional[Scoreboard] = None self._game_over = False self._wavenum = 0 self._can_end_wave = True self._score = 0 self._time_bonus = 0 self._score_region: Optional[ba.Actor] = None self._dingsound = ba.getsound('dingSmall') self._dingsoundhigh = ba.getsound('dingSmallHigh') self._exclude_powerups: Optional[List[str]] = None self._have_tnt: Optional[bool] = None self._waves: Optional[List[Wave]] = None self._bots = SpazBotSet() self._tntspawner: Optional[TNTSpawner] = None self._lives_bg: Optional[ba.NodeActor] = None self._start_lives = 10 self._lives = self._start_lives self._lives_text: Optional[ba.NodeActor] = None self._flawless = True self._time_bonus_timer: Optional[ba.Timer] = None self._time_bonus_text: Optional[ba.NodeActor] = None self._time_bonus_mult: Optional[float] = None self._wave_text: Optional[ba.NodeActor] = None self._flawless_bonus: Optional[int] = None self._wave_update_timer: Optional[ba.Timer] = None def on_transition_in(self) -> None: super().on_transition_in() self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), score_split=0.5) self._score_region = ba.NodeActor( ba.newnode('region', attrs={ 'position': self.map.defs.boxes['score_region'][0:3], 'scale': self.map.defs.boxes['score_region'][6:9], 'type': 'box', 'materials': [self._score_region_material] })) def on_begin(self) -> None: super().on_begin() player_count = len(self.players) hard = self._preset not in {Preset.PRO_EASY, Preset.UBER_EASY} if self._preset in {Preset.PRO, Preset.PRO_EASY, Preset.TOURNAMENT}: self._exclude_powerups = ['curse'] self._have_tnt = True self._waves = [ Wave(entries=[ Spawn(BomberBot, path=3 if hard else 2), Spawn(BomberBot, path=2), Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2) if player_count > 1 else None, Spawn(BomberBot, path=1) if hard else None, Spawn(BomberBot, path=1) if player_count > 2 else None, Spawn(BomberBot, path=1) if player_count > 3 else None, ]), Wave(entries=[ Spawn(BomberBot, path=1) if hard else None, Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2), Spawn(BomberBot, path=2), Spawn(BomberBot, path=2) if player_count > 3 else None, Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3) if hard else None, Spawn(BrawlerBot, path=3) if player_count > 1 else None, Spawn(BrawlerBot, path=3) if player_count > 2 else None, ]), Wave(entries=[ Spawn(ChargerBot, path=2) if hard else None, Spawn(ChargerBot, path=2) if player_count > 2 else None, Spawn(TriggerBot, path=2), Spawn(TriggerBot, path=2) if player_count > 1 else None, Spacing(duration=3.0), Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2) if hard else None, Spawn(BomberBot, path=2), Spawn(BomberBot, path=3) if hard else None, Spawn(BomberBot, path=3), Spawn(BomberBot, path=3), Spawn(BomberBot, path=3) if player_count > 3 else None, ]), Wave(entries=[ Spawn(TriggerBot, path=1) if hard else None, Spacing(duration=1.0) if hard else None, Spawn(TriggerBot, path=2), Spacing(duration=1.0), Spawn(TriggerBot, path=3), Spacing(duration=1.0), Spawn(TriggerBot, path=1) if hard else None, Spacing(duration=1.0) if hard else None, Spawn(TriggerBot, path=2), Spacing(duration=1.0), Spawn(TriggerBot, path=3), Spacing(duration=1.0), Spawn(TriggerBot, path=1) if ( player_count > 1 and hard) else None, Spacing(duration=1.0), Spawn(TriggerBot, path=2) if player_count > 2 else None, Spacing(duration=1.0), Spawn(TriggerBot, path=3) if player_count > 3 else None, Spacing(duration=1.0), ]), Wave(entries=[ Spawn(ChargerBotProShielded if hard else ChargerBot, path=1), Spawn(BrawlerBot, path=2) if hard else None, Spawn(BrawlerBot, path=2), Spawn(BrawlerBot, path=2), Spawn(BrawlerBot, path=3) if hard else None, Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3), Spawn(BrawlerBot, path=3) if player_count > 1 else None, Spawn(BrawlerBot, path=3) if player_count > 2 else None, Spawn(BrawlerBot, path=3) if player_count > 3 else None, ]), Wave(entries=[ Spawn(BomberBotProShielded, path=3), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=2), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=1) if hard else None, Spacing(duration=1.0) if hard else None, Spawn(BomberBotProShielded, path=3), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=2), Spacing(duration=1.5), Spawn(BomberBotProShielded, path=1) if hard else None, Spacing(duration=1.5) if hard else None, Spawn(BomberBotProShielded, path=3 ) if player_count > 1 else None, Spacing(duration=1.5), Spawn(BomberBotProShielded, path=2 ) if player_count > 2 else None, Spacing(duration=1.5), Spawn(BomberBotProShielded, path=1 ) if player_count > 3 else None, ]), ] elif self._preset in { Preset.UBER_EASY, Preset.UBER, Preset.TOURNAMENT_UBER }: self._exclude_powerups = [] self._have_tnt = True self._waves = [ Wave(entries=[ Spawn(TriggerBot, path=1) if hard else None, Spawn(TriggerBot, path=2), Spawn(TriggerBot, path=2), Spawn(TriggerBot, path=3), Spawn(BrawlerBotPro if hard else BrawlerBot, point=Point.BOTTOM_LEFT), Spawn(BrawlerBotPro, point=Point.BOTTOM_RIGHT ) if player_count > 2 else None, ]), Wave(entries=[ Spawn(ChargerBot, path=2), Spawn(ChargerBot, path=3), Spawn(ChargerBot, path=1) if hard else None, Spawn(ChargerBot, path=2), Spawn(ChargerBot, path=3), Spawn(ChargerBot, path=1) if player_count > 2 else None, ]), Wave(entries=[ Spawn(BomberBotProShielded, path=1) if hard else None, Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=3), Spawn(BomberBotProShielded, path=3), Spawn(ChargerBot, point=Point.BOTTOM_RIGHT), Spawn(ChargerBot, point=Point.BOTTOM_LEFT ) if player_count > 2 else None, ]), Wave(entries=[ Spawn(TriggerBotPro, path=1) if hard else None, Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2), Spawn(TriggerBotPro, path=1 if hard else 2 ) if player_count > 1 else None, Spawn(TriggerBotPro, path=1 if hard else 2 ) if player_count > 3 else None, ]), Wave(entries=[ Spawn(TriggerBotProShielded if hard else TriggerBotPro, point=Point.BOTTOM_LEFT), Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT ) if hard else None, Spawn(TriggerBotProShielded, point=Point.BOTTOM_RIGHT ) if player_count > 2 else None, Spawn(BomberBot, path=3), Spawn(BomberBot, path=3), Spacing(duration=5.0), Spawn(BrawlerBot, path=2), Spawn(BrawlerBot, path=2), Spacing(duration=5.0), Spawn(TriggerBot, path=1) if hard else None, Spawn(TriggerBot, path=1) if hard else None, ]), Wave(entries=[ Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2) if hard else None, Spawn(StickyBot, point=Point.BOTTOM_RIGHT), Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2), Spawn(StickyBot, point=Point.BOTTOM_RIGHT ) if player_count > 2 else None, Spawn(BomberBotProShielded, path=2), Spawn(ExplodeyBot, point=Point.BOTTOM_LEFT), Spawn(BomberBotProShielded, path=2), Spawn(BomberBotProShielded, path=2 ) if player_count > 1 else None, Spacing(duration=5.0), Spawn(StickyBot, point=Point.BOTTOM_LEFT), Spacing(duration=2.0), Spawn(ExplodeyBot, point=Point.BOTTOM_RIGHT), ]), ] elif self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: self._exclude_powerups = [] self._have_tnt = True # Spit out a few powerups and start dropping more shortly. self._drop_powerups(standard_points=True) ba.timer(4.0, self._start_powerup_drops) self.setup_low_life_warning_sound() self._update_scores() # Our TNT spawner (if applicable). if self._have_tnt: self._tntspawner = TNTSpawner(position=self._tntspawnpos) # Make sure to stay out of the way of menu/party buttons in the corner. interface_type = ba.app.interface_type l_offs = (-80 if interface_type == 'small' else -40 if interface_type == 'medium' else 0) self._lives_bg = ba.NodeActor( ba.newnode('image', attrs={ 'texture': self._heart_tex, 'model_opaque': self._heart_model_opaque, 'model_transparent': self._heart_model_transparent, 'attach': 'topRight', 'scale': (90, 90), 'position': (-110 + l_offs, -50), 'color': (1, 0.2, 0.2) })) # FIXME; should not set things based on vr mode. # (won't look right to non-vr connected clients, etc) vrmode = ba.app.vr_mode self._lives_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'right', 'h_align': 'center', 'color': (1, 1, 1, 1) if vrmode else (0.8, 0.8, 0.8, 1.0), 'flatness': 1.0 if vrmode else 0.5, 'shadow': 1.0 if vrmode else 0.5, 'vr_depth': 10, 'position': (-113 + l_offs, -69), 'scale': 1.3, 'text': str(self._lives) })) ba.timer(2.0, self._start_updating_waves) def _handle_reached_end(self) -> None: spaz = ba.getcollision().opposingnode.getdelegate(SpazBot, True) if not spaz.is_alive(): return # Ignore bodies flying in. self._flawless = False pos = spaz.node.position ba.playsound(self._bad_guy_score_sound, position=pos) light = ba.newnode('light', attrs={ 'position': pos, 'radius': 0.5, 'color': (1, 0, 0) }) ba.animate(light, 'intensity', {0.0: 0, 0.1: 1, 0.5: 0}, loop=False) ba.timer(1.0, light.delete) spaz.handlemessage( ba.DieMessage(immediate=True, how=ba.DeathType.REACHED_GOAL)) if self._lives > 0: self._lives -= 1 if self._lives == 0: self._bots.stop_moving() self.continue_or_end_game() assert self._lives_text is not None assert self._lives_text.node self._lives_text.node.text = str(self._lives) delay = 0.0 def _safesetattr(node: ba.Node, attr: str, value: Any) -> None: if node: setattr(node, attr, value) for _i in range(4): ba.timer( delay, ba.Call(_safesetattr, self._lives_text.node, 'color', (1, 0, 0, 1.0))) assert self._lives_bg is not None assert self._lives_bg.node ba.timer( delay, ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 0.5)) delay += 0.125 ba.timer( delay, ba.Call(_safesetattr, self._lives_text.node, 'color', (1.0, 1.0, 0.0, 1.0))) ba.timer( delay, ba.Call(_safesetattr, self._lives_bg.node, 'opacity', 1.0)) delay += 0.125 ba.timer( delay, ba.Call(_safesetattr, self._lives_text.node, 'color', (0.8, 0.8, 0.8, 1.0))) def on_continue(self) -> None: self._lives = 3 assert self._lives_text is not None assert self._lives_text.node self._lives_text.node.text = str(self._lives) self._bots.start_moving() def spawn_player(self, player: Player) -> ba.Actor: pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5), self._spawn_center[1], self._spawn_center[2] + random.uniform(-1.5, 1.5)) spaz = self.spawn_player_spaz(player, position=pos) if self._preset in {Preset.PRO_EASY, Preset.UBER_EASY}: spaz.impact_scale = 0.25 # Add the material that causes us to hit the player-wall. spaz.pick_up_powerup_callback = self._on_player_picked_up_powerup return spaz def _on_player_picked_up_powerup(self, player: ba.Actor) -> None: del player # Unused. self._player_has_picked_up_powerup = True def _drop_powerup(self, index: int, poweruptype: str = None) -> None: if poweruptype is None: poweruptype = (PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups)) PowerupBox(position=self.map.powerup_spawn_points[index], poweruptype=poweruptype).autoretain() def _start_powerup_drops(self) -> None: ba.timer(3.0, self._drop_powerups, repeat=True) def _drop_powerups(self, standard_points: bool = False, force_first: str = None) -> None: """Generic powerup drop.""" # If its been a minute since our last wave finished emerging, stop # giving out land-mine powerups. (prevents players from waiting # around for them on purpose and filling the map up) if ba.time() - self._last_wave_end_time > 60.0: extra_excludes = ['land_mines'] else: extra_excludes = [] if standard_points: points = self.map.powerup_spawn_points for i in range(len(points)): ba.timer( 1.0 + i * 0.5, ba.Call(self._drop_powerup, i, force_first if i == 0 else None)) else: pos = (self._powerup_center[0] + random.uniform( -1.0 * self._powerup_spread[0], 1.0 * self._powerup_spread[0]), self._powerup_center[1], self._powerup_center[2] + random.uniform( -self._powerup_spread[1], self._powerup_spread[1])) # drop one random one somewhere.. assert self._exclude_powerups is not None PowerupBox( position=pos, poweruptype=PowerupBoxFactory.get().get_random_powerup_type( excludetypes=self._exclude_powerups + extra_excludes)).autoretain() def end_game(self) -> None: ba.pushcall(ba.Call(self.do_end, 'defeat')) ba.setmusic(None) ba.playsound(self._player_death_sound) def do_end(self, outcome: str) -> None: """End the game now with the provided outcome.""" if outcome == 'defeat': delay = 2.0 self.fade_to_red() else: delay = 0 score: Optional[int] if self._wavenum >= 2: score = self._score fail_message = None else: score = None fail_message = 'Reach wave 2 to rank.' self.end(delay=delay, results={ 'outcome': outcome, 'score': score, 'fail_message': fail_message, 'playerinfos': self.initialplayerinfos }) def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: self._show_standard_scores_to_beat_ui(scores) def _update_waves(self) -> None: # pylint: disable=too-many-branches # If we have no living bots, go to the next wave. if (self._can_end_wave and not self._bots.have_living_bots() and not self._game_over and self._lives > 0): self._can_end_wave = False self._time_bonus_timer = None self._time_bonus_text = None if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: won = False else: assert self._waves is not None won = (self._wavenum == len(self._waves)) # Reward time bonus. base_delay = 4.0 if won else 0 if self._time_bonus > 0: ba.timer(0, ba.Call(ba.playsound, self._cashregistersound)) ba.timer(base_delay, ba.Call(self._award_time_bonus, self._time_bonus)) base_delay += 1.0 # Reward flawless bonus. if self._wavenum > 0 and self._flawless: ba.timer(base_delay, self._award_flawless_bonus) base_delay += 1.0 self._flawless = True # reset if won: # Completion achievements: if self._preset in {Preset.PRO, Preset.PRO_EASY}: self._award_achievement('Pro Runaround Victory', sound=False) if self._lives == self._start_lives: self._award_achievement('The Wall', sound=False) if not self._player_has_picked_up_powerup: self._award_achievement('Precision Bombing', sound=False) elif self._preset in {Preset.UBER, Preset.UBER_EASY}: self._award_achievement('Uber Runaround Victory', sound=False) if self._lives == self._start_lives: self._award_achievement('The Great Wall', sound=False) if not self._a_player_has_been_killed: self._award_achievement('Stayin\' Alive', sound=False) # Give remaining players some points and have them celebrate. self.show_zoom_message(ba.Lstr(resource='victoryText'), scale=1.0, duration=4.0) self.celebrate(10.0) ba.timer(base_delay, self._award_lives_bonus) base_delay += 1.0 ba.timer(base_delay, self._award_completion_bonus) base_delay += 0.85 ba.playsound(self._winsound) ba.cameraflash() ba.setmusic(ba.MusicType.VICTORY) self._game_over = True ba.timer(base_delay, ba.Call(self.do_end, 'victory')) return self._wavenum += 1 # Short celebration after waves. if self._wavenum > 1: self.celebrate(0.5) ba.timer(base_delay, self._start_next_wave) def _award_completion_bonus(self) -> None: bonus = 200 ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(bonus)), ('${B}', ba.Lstr(resource='completionBonusText'))]), color=(0.7, 0.7, 1.0, 1), scale=1.6, position=(0, 1.5, -1)).autoretain() self._score += bonus self._update_scores() def _award_lives_bonus(self) -> None: bonus = self._lives * 30 ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(bonus)), ('${B}', ba.Lstr(resource='livesBonusText'))]), color=(0.7, 1.0, 0.3, 1), scale=1.3, position=(0, 1, -1)).autoretain() self._score += bonus self._update_scores() def _award_time_bonus(self, bonus: int) -> None: ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(bonus)), ('${B}', ba.Lstr(resource='timeBonusText'))]), color=(1, 1, 0.5, 1), scale=1.0, position=(0, 3, -1)).autoretain() self._score += self._time_bonus self._update_scores() def _award_flawless_bonus(self) -> None: ba.playsound(self._cashregistersound) PopupText(ba.Lstr(value='+${A} ${B}', subs=[('${A}', str(self._flawless_bonus)), ('${B}', ba.Lstr(resource='perfectWaveText')) ]), color=(1, 1, 0.2, 1), scale=1.2, position=(0, 2, -1)).autoretain() assert self._flawless_bonus is not None self._score += self._flawless_bonus self._update_scores() def _start_time_bonus_timer(self) -> None: self._time_bonus_timer = ba.Timer(1.0, self._update_time_bonus, repeat=True) def _start_next_wave(self) -> None: # FIXME: Need to split this up. # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements self.show_zoom_message(ba.Lstr(value='${A} ${B}', subs=[('${A}', ba.Lstr(resource='waveText')), ('${B}', str(self._wavenum))]), scale=1.0, duration=1.0, trail=True) ba.timer(0.4, ba.Call(ba.playsound, self._new_wave_sound)) t_sec = 0.0 base_delay = 0.5 delay = 0.0 bot_types: List[Union[Spawn, Spacing, None]] = [] if self._preset in {Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT}: level = self._wavenum target_points = (level + 1) * 8.0 group_count = random.randint(1, 3) entries: List[Union[Spawn, Spacing, None]] = [] spaz_types: List[Tuple[Type[SpazBot], float]] = [] if level < 6: spaz_types += [(BomberBot, 5.0)] if level < 10: spaz_types += [(BrawlerBot, 5.0)] if level < 15: spaz_types += [(TriggerBot, 6.0)] if level > 5: spaz_types += [(TriggerBotPro, 7.5)] * (1 + (level - 5) // 7) if level > 2: spaz_types += [(BomberBotProShielded, 8.0) ] * (1 + (level - 2) // 6) if level > 6: spaz_types += [(TriggerBotProShielded, 12.0) ] * (1 + (level - 6) // 5) if level > 1: spaz_types += ([(ChargerBot, 10.0)] * (1 + (level - 1) // 4)) if level > 7: spaz_types += [(ChargerBotProShielded, 15.0) ] * (1 + (level - 7) // 3) # Bot type, their effect on target points. defender_types: List[Tuple[Type[SpazBot], float]] = [ (BomberBot, 0.9), (BrawlerBot, 0.9), (TriggerBot, 0.85), ] if level > 2: defender_types += [(ChargerBot, 0.75)] if level > 4: defender_types += ([(StickyBot, 0.7)] * (1 + (level - 5) // 6)) if level > 6: defender_types += ([(ExplodeyBot, 0.7)] * (1 + (level - 5) // 5)) if level > 8: defender_types += ([(BrawlerBotProShielded, 0.65)] * (1 + (level - 5) // 4)) if level > 10: defender_types += ([(TriggerBotProShielded, 0.6)] * (1 + (level - 6) // 3)) for group in range(group_count): this_target_point_s = target_points / group_count # Adding spacing makes things slightly harder. rval = random.random() if rval < 0.07: spacing = 1.5 this_target_point_s *= 0.85 elif rval < 0.15: spacing = 1.0 this_target_point_s *= 0.9 else: spacing = 0.0 path = random.randint(1, 3) # Don't allow hard paths on early levels. if level < 3: if path == 1: path = 3 # Easy path. if path == 3: pass # Harder path. elif path == 2: this_target_point_s *= 0.8 # Even harder path. elif path == 1: this_target_point_s *= 0.7 # Looping forward. elif path == 4: this_target_point_s *= 0.7 # Looping backward. elif path == 5: this_target_point_s *= 0.7 # Random. elif path == 6: this_target_point_s *= 0.7 def _add_defender(defender_type: Tuple[Type[SpazBot], float], pnt: Point) -> Tuple[float, Spawn]: # This is ok because we call it immediately. # pylint: disable=cell-var-from-loop return this_target_point_s * defender_type[1], Spawn( defender_type[0], point=pnt) # Add defenders. defender_type1 = defender_types[random.randrange( len(defender_types))] defender_type2 = defender_types[random.randrange( len(defender_types))] defender1 = defender2 = None if ((group == 0) or (group == 1 and level > 3) or (group == 2 and level > 5)): if random.random() < min(0.75, (level - 1) * 0.11): this_target_point_s, defender1 = _add_defender( defender_type1, Point.BOTTOM_LEFT) if random.random() < min(0.75, (level - 1) * 0.04): this_target_point_s, defender2 = _add_defender( defender_type2, Point.BOTTOM_RIGHT) spaz_type = spaz_types[random.randrange(len(spaz_types))] member_count = max( 1, int(round(this_target_point_s / spaz_type[1]))) for i, _member in enumerate(range(member_count)): if path == 4: this_path = i % 3 # Looping forward. elif path == 5: this_path = 3 - (i % 3) # Looping backward. elif path == 6: this_path = random.randint(1, 3) # Random. else: this_path = path entries.append(Spawn(spaz_type[0], path=this_path)) if spacing != 0.0: entries.append(Spacing(duration=spacing)) if defender1 is not None: entries.append(defender1) if defender2 is not None: entries.append(defender2) # Some spacing between groups. rval = random.random() if rval < 0.1: spacing = 5.0 elif rval < 0.5: spacing = 1.0 else: spacing = 1.0 entries.append(Spacing(duration=spacing)) wave = Wave(entries=entries) else: assert self._waves is not None wave = self._waves[self._wavenum - 1] bot_types += wave.entries self._time_bonus_mult = 1.0 this_flawless_bonus = 0 non_runner_spawn_time = 1.0 for info in bot_types: if info is None: continue if isinstance(info, Spacing): t_sec += info.duration continue bot_type = info.type path = info.path self._time_bonus_mult += bot_type.points_mult * 0.02 this_flawless_bonus += bot_type.points_mult * 5 # If its got a position, use that. if info.point is not None: point = info.point else: point = Point.START # Space our our slower bots. delay = base_delay delay /= self._get_bot_speed(bot_type) t_sec += delay * 0.5 tcall = ba.Call( self.add_bot_at_point, point, bot_type, path, 0.1 if point is Point.START else non_runner_spawn_time) ba.timer(t_sec, tcall) t_sec += delay * 0.5 # We can end the wave after all the spawning happens. ba.timer(t_sec - delay * 0.5 + non_runner_spawn_time + 0.01, self._set_can_end_wave) # Reset our time bonus. # In this game we use a constant time bonus so it erodes away in # roughly the same time (since the time limit a wave can take is # relatively constant) ..we then post-multiply a modifier to adjust # points. self._time_bonus = 150 self._flawless_bonus = this_flawless_bonus assert self._time_bonus_mult is not None txtval = ba.Lstr( value='${A}: ${B}', subs=[('${A}', ba.Lstr(resource='timeBonusText')), ('${B}', str(int(self._time_bonus * self._time_bonus_mult))) ]) self._time_bonus_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'color': (1, 1, 0.0, 1), 'shadow': 1.0, 'vr_depth': -30, 'flatness': 1.0, 'position': (0, -60), 'scale': 0.8, 'text': txtval })) ba.timer(t_sec, self._start_time_bonus_timer) # Keep track of when this wave finishes emerging. We wanna stop # dropping land-mines powerups at some point (otherwise a crafty # player could fill the whole map with them) self._last_wave_end_time = ba.time() + t_sec totalwaves = str(len(self._waves)) if self._waves is not None else '??' txtval = ba.Lstr(value='${A} ${B}', subs=[('${A}', ba.Lstr(resource='waveText')), ('${B}', str(self._wavenum) + ('' if self._preset in { Preset.ENDLESS, Preset.ENDLESS_TOURNAMENT } else f'/{totalwaves}'))]) self._wave_text = ba.NodeActor( ba.newnode('text', attrs={ 'v_attach': 'top', 'h_attach': 'center', 'h_align': 'center', 'vr_depth': -10, 'color': (1, 1, 1, 1), 'shadow': 1.0, 'flatness': 1.0, 'position': (0, -40), 'scale': 1.3, 'text': txtval })) def _on_bot_spawn(self, path: int, spaz: SpazBot) -> None: # Add our custom update callback and set some info for this bot. spaz_type = type(spaz) assert spaz is not None spaz.update_callback = self._update_bot # Tack some custom attrs onto the spaz. setattr(spaz, 'r_walk_row', path) setattr(spaz, 'r_walk_speed', self._get_bot_speed(spaz_type)) def add_bot_at_point(self, point: Point, spaztype: Type[SpazBot], path: int, spawn_time: float = 0.1) -> None: """Add the given type bot with the given delay (in seconds).""" # Don't add if the game has ended. if self._game_over: return pos = self.map.defs.points[point.value][:3] self._bots.spawn_bot(spaztype, pos=pos, spawn_time=spawn_time, on_spawn_call=ba.Call(self._on_bot_spawn, path)) def _update_time_bonus(self) -> None: self._time_bonus = int(self._time_bonus * 0.91) if self._time_bonus > 0 and self._time_bonus_text is not None: assert self._time_bonus_text.node assert self._time_bonus_mult self._time_bonus_text.node.text = ba.Lstr( value='${A}: ${B}', subs=[('${A}', ba.Lstr(resource='timeBonusText')), ('${B}', str(int(self._time_bonus * self._time_bonus_mult)))]) else: self._time_bonus_text = None def _start_updating_waves(self) -> None: self._wave_update_timer = ba.Timer(2.0, self._update_waves, repeat=True) def _update_scores(self) -> None: score = self._score if self._preset is Preset.ENDLESS: if score >= 500: self._award_achievement('Runaround Master') if score >= 1000: self._award_achievement('Runaround Wizard') if score >= 2000: self._award_achievement('Runaround God') assert self._scoreboard is not None self._scoreboard.set_team_value(self.teams[0], score, max_score=None) def _update_bot(self, bot: SpazBot) -> bool: # Yup; that's a lot of return statements right there. # pylint: disable=too-many-return-statements if not bool(bot): return True assert bot.node # FIXME: Do this in a type safe way. r_walk_speed: float = getattr(bot, 'r_walk_speed') r_walk_row: int = getattr(bot, 'r_walk_row') speed = r_walk_speed pos = bot.node.position boxes = self.map.defs.boxes # Bots in row 1 attempt the high road.. if r_walk_row == 1: if ba.is_point_in_box(pos, boxes['b4']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True # Row 1 and 2 bots attempt the middle road.. if r_walk_row in [1, 2]: if ba.is_point_in_box(pos, boxes['b1']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True # All bots settle for the third row. if ba.is_point_in_box(pos, boxes['b7']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b2']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b3']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b5']): bot.node.move_up_down = -speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ba.is_point_in_box(pos, boxes['b6']): bot.node.move_up_down = speed bot.node.move_left_right = 0 bot.node.run = 0.0 return True if ((ba.is_point_in_box(pos, boxes['b8']) and not ba.is_point_in_box(pos, boxes['b9'])) or pos == (0.0, 0.0, 0.0)): # Default to walking right if we're still in the walking area. bot.node.move_left_right = speed bot.node.move_up_down = 0 bot.node.run = 0.0 return True # Revert to normal bot behavior otherwise.. return False def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerScoredMessage): self._score += msg.score self._update_scores() elif isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) self._a_player_has_been_killed = True # Respawn them shortly. player = msg.getplayer(Player) assert self.initialplayerinfos is not None respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): if msg.how is ba.DeathType.REACHED_GOAL: return None pts, importance = msg.spazbot.get_death_points(msg.how) if msg.killerplayer is not None: target: Optional[Sequence[float]] try: assert msg.spazbot is not None assert msg.spazbot.node target = msg.spazbot.node.position except Exception: ba.print_exception() target = None try: if msg.killerplayer: self.stats.player_scored(msg.killerplayer, pts, target=target, kill=True, screenmessage=False, importance=importance) ba.playsound(self._dingsound if importance == 1 else self._dingsoundhigh, volume=0.6) except Exception: ba.print_exception('Error on SpazBotDiedMessage') # Normally we pull scores from the score-set, but if there's no # player lets be explicit. else: self._score += pts self._update_scores() else: return super().handlemessage(msg) return None def _get_bot_speed(self, bot_type: Type[SpazBot]) -> float: speed = self._bot_speed_map.get(bot_type) if speed is None: raise TypeError('Invalid bot type to _get_bot_speed(): ' + str(bot_type)) return speed def _set_can_end_wave(self) -> None: self._can_end_wave = True
class KingOfTheHillGame(ba.TeamGameActivity): """Game where a team wins by holding a 'hill' for a set amount of time.""" FLAG_NEW = 0 FLAG_UNCONTESTED = 1 FLAG_CONTESTED = 2 FLAG_HELD = 3 @classmethod def get_name(cls) -> str: return 'King of the Hill' @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Secure the flag for a set length of time.' @classmethod def get_score_info(cls) -> Dict[str, Any]: return {'score_name': 'Time Held'} @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return issubclass(sessiontype, ba.TeamBaseSession) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ba.getmaps("king_of_the_hill") @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [("Hold Time", { 'min_value': 10, 'default': 30, 'increment': 10 }), ("Time Limit", { 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)], 'default': 0 }), ("Respawn Times", { 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 })] def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard 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_pos: Optional[Sequence[float]] = None self._flag_state: Optional[int] = None self._flag: Optional[stdflag.Flag] = None self._flag_light: Optional[ba.Node] = None self._scoring_team: Optional[ReferenceType[ba.Team]] = None self._flag_region_material = ba.Material() self._flag_region_material.add_actions( conditions=("they_have_material", ba.sharedobj('player_material')), actions=(("modify_part_collision", "collide", True), ("modify_part_collision", "physical", False), ("call", "at_connect", ba.Call(self._handle_player_flag_region_collide, True)), ("call", "at_disconnect", ba.Call(self._handle_player_flag_region_collide, False)))) def get_instance_description(self) -> Union[str, Sequence]: return ('Secure the flag for ${ARG1} seconds.', self.settings['Hold Time']) def get_instance_scoreboard_description(self) -> Union[str, Sequence]: return ('secure the flag for ${ARG1} seconds', self.settings['Hold Time']) def on_transition_in(self) -> None: self.default_music = ba.MusicType.SCARY super().on_transition_in() def on_team_join(self, team: ba.Team) -> None: team.gamedata['time_remaining'] = self.settings["Hold Time"] self._update_scoreboard() def on_player_join(self, player: ba.Player) -> None: ba.TeamGameActivity.on_player_join(self, player) player.gamedata['at_flag'] = 0 def on_begin(self) -> None: super().on_begin() self.setup_standard_time_limit(self.settings['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 = self.FLAG_NEW self.project_flag_stand(self._flag_pos) self._flag = stdflag.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, ba.sharedobj('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.gamedata['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.gamedata['time_remaining'] > 0: ba.playsound(self._tick_sound) scoring_team.gamedata['time_remaining'] = max( 0, scoring_team.gamedata['time_remaining'] - 1) self._update_scoreboard() if scoring_team.gamedata['time_remaining'] > 0: assert self._flag is not None self._flag.set_score_text( str(scoring_team.gamedata['time_remaining'])) # Announce numbers we have sounds for. try: ba.playsound(self._countdownsounds[ scoring_team.gamedata['time_remaining']]) except Exception: pass # winner if scoring_team.gamedata['time_remaining'] <= 0: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: results.set_team_score( team, self.settings['Hold Time'] - team.gamedata['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.gamedata['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 = self.FLAG_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 = self.FLAG_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 = self.FLAG_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: playernode = ba.get_collision_info("opposing_node") try: player = playernode.getdelegate().getplayer() except Exception: 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.gamedata['at_flag'] += 1 else: player.gamedata['at_flag'] = max(0, player.gamedata['at_flag'] - 1) self._update_flag_state() def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, team.gamedata['time_remaining'], self.settings['Hold Time'], countdown=True) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, playerspaz.PlayerSpazDeathMessage): super().handlemessage(msg) # Augment default. # No longer can count as at_flag once dead. player = msg.spaz.player player.gamedata['at_flag'] = 0 self._update_flag_state() self.respawn_player(player)