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 MeteorShowerGame(ba.TeamGameActivity): """Minigame involving dodging falling bombs.""" @classmethod def get_name(cls) -> str: return 'Meteor Shower' @classmethod def get_score_info(cls) -> ba.ScoreInfo: return ba.ScoreInfo(label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B') @classmethod def get_description(cls, sessiontype: Type[ba.Session]) -> str: return 'Dodge the falling bombs.' # we're currently hard-coded for one map.. @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ['Rampage'] @classmethod def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: return [('Epic Mode', {'default': False})] # We support teams, free-for-all, and co-op sessions. @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession) or issubclass(sessiontype, ba.CoopSession)) def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self._epic_mode = settings.get('Epic Mode', False) self._last_player_death_time: Optional[float] = None self._meteor_time = 2.0 self._timer: Optional[OnScreenTimer] = None # Some base class overrides: self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL) if self._epic_mode: self.slow_motion = True # Print messages when players die (since its meaningful in this game). self.announce_player_deaths = True def on_begin(self) -> None: from bastd.actor.onscreentimer import OnScreenTimer super().on_begin() # Drop a wave every few seconds.. and every so often drop the time # between waves ..lets have things increase faster if we have fewer # players. delay = 5.0 if len(self.players) > 2 else 2.5 if self._epic_mode: delay *= 0.25 ba.timer(delay, self._decrement_meteor_time, repeat=True) # Kick off the first wave in a few seconds. delay = 3.0 if self._epic_mode: delay *= 0.25 ba.timer(delay, self._set_meteor_timer) self._timer = OnScreenTimer() self._timer.start() # Check for immediate end (if we've only got 1 player, etc). ba.timer(5.0, self._check_end_game) def on_player_join(self, player: ba.Player) -> None: # Don't allow joining after we start # (would enable leave/rejoin tomfoolery). if self.has_begun(): ba.screenmessage(ba.Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', player.get_name(full=True))]), color=(0, 1, 0)) # For score purposes, mark them as having died right as the # game started. assert self._timer is not None PlayerData.get(player).death_time = self._timer.getstarttime() return self.spawn_player(player) def on_player_leave(self, player: ba.Player) -> None: # Augment default behavior. super().on_player_leave(player) # A departing player may trigger game-over. self._check_end_game() # overriding the default character spawning.. def spawn_player(self, player: ba.Player) -> ba.Actor: spaz = self.spawn_player_spaz(player) # Let's reconnect this player's controls to this # spaz but *without* the ability to attack or pick stuff up. spaz.connect_controls_to_player(enable_punch=False, enable_bomb=False, enable_pickup=False) # Also lets have them make some noise when they die. spaz.play_big_death_sound = True return spaz # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: if isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) curtime = ba.time() # Record the player's moment of death. PlayerData.get(msg.spaz.player).death_time = curtime # In co-op mode, end the game the instant everyone dies # (more accurate looking). # In teams/ffa, allow a one-second fudge-factor so we can # get more draws if players die basically at the same time. if isinstance(self.session, ba.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. ba.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = curtime else: ba.timer(1.0, self._check_end_game) else: # Default handler: super().handlemessage(msg) def _check_end_game(self) -> None: living_team_count = 0 for team in self.teams: for player in team.players: if player.is_alive(): living_team_count += 1 break # In co-op, we go till everyone is dead.. otherwise we go # until one team remains. if isinstance(self.session, ba.CoopSession): if living_team_count <= 0: self.end_game() else: if living_team_count <= 1: self.end_game() def _set_meteor_timer(self) -> None: ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster) def _drop_bomb_cluster(self) -> None: # Random note: code like this is a handy way to plot out extents # and debug things. loc_test = False if loc_test: ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) # Drop several bombs in series. delay = 0.0 for _i in range(random.randrange(1, 3)): # Drop them somewhere within our bounds with velocity pointing # toward the opposite side. pos = (-7.3 + 15.3 * random.random(), 11, -5.5 + 2.1 * random.random()) dropdir = (-1.0 if pos[0] > 0 else 1.0) vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) delay += 0.1 self._set_meteor_timer() def _drop_bomb(self, position: Sequence[float], velocity: Sequence[float]) -> None: Bomb(position=position, velocity=velocity).autoretain() def _decrement_meteor_time(self) -> None: self._meteor_time = max(0.01, self._meteor_time * 0.9) def end_game(self) -> None: cur_time = ba.time() assert self._timer is not None start_time = self._timer.getstarttime() # Mark death-time as now for any still-living players # and award players points for how long they lasted. # (these per-player scores are only meaningful in team-games) for team in self.teams: for player in team.players: playerdata = PlayerData.get(player) survived = False # Throw an extra fudge factor in so teams that # didn't die come out ahead of teams that did. if playerdata.death_time is None: survived = True playerdata.death_time = cur_time + 1 # Award a per-player score depending on how many seconds # they lasted (per-player scores only affect teams mode; # everywhere else just looks at the per-team score). score = int(playerdata.death_time - self._timer.getstarttime()) if survived: score += 50 # A bit extra for survivors. self.stats.player_scored(player, score, screenmessage=False) # Stop updating our time text, and set the final time to match # exactly when our last guy died. self._timer.stop(endtime=self._last_player_death_time) # Ok now calc game results: set a score for each team and then tell # the game to end. results = ba.TeamGameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: # Set the team score to the max time survived by any player on # that team. longest_life = 0.0 for player in team.players: playerdata = PlayerData.get(player) assert playerdata.death_time is not None longest_life = max(longest_life, playerdata.death_time - start_time) # Submit the score value in milliseconds. results.set_team_score(team, int(1000.0 * longest_life)) self.end(results=results)
class NinjaFightGame(ba.TeamGameActivity[Player, Team]): """ A co-op game where you try to defeat a group of Ninjas as fast as possible """ name = 'Ninja Fight' description = 'How fast can you defeat the ninjas?' score_info = ba.ScoreInfo(label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True) @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: # For now we're hard-coding spawn positions and whatnot # so we need to be sure to specify that we only support # a specific map. return ['Courtyard'] @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: # We currently support Co-Op only. return issubclass(sessiontype, ba.CoopSession) # In the constructor we should load any media we need/etc. # ...but not actually create anything yet. def __init__(self, settings: Dict[str, Any]): super().__init__(settings) self._winsound = ba.getsound('score') self._won = False self._timer: Optional[OnScreenTimer] = None self._bots = SpazBotSet() self._preset = str(settings['preset']) # Called when our game is transitioning in but not ready to begin; # we can go ahead and start creating stuff, playing music, etc. def on_transition_in(self) -> None: self.default_music = ba.MusicType.TO_THE_DEATH super().on_transition_in() # Called when our game actually begins. def on_begin(self) -> None: super().on_begin() is_pro = self._preset == 'pro' # In pro mode there's no powerups. if not is_pro: self.setup_standard_powerup_drops() # Make our on-screen timer and start it roughly when our bots appear. self._timer = OnScreenTimer() ba.timer(4.0, self._timer.start) # Spawn some baddies. ba.timer( 1.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(3, 3, -2), spawn_time=3.0)) ba.timer( 2.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(-3, 3, -2), spawn_time=3.0)) ba.timer( 3.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(5, 3, -2), spawn_time=3.0)) ba.timer( 4.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(-5, 3, -2), spawn_time=3.0)) # Add some extras for multiplayer or pro mode. assert self.initial_player_info is not None if len(self.initial_player_info) > 2 or is_pro: ba.timer( 5.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(0, 3, -5), spawn_time=3.0)) if len(self.initial_player_info) > 3 or is_pro: ba.timer( 6.0, lambda: self._bots.spawn_bot( ChargerBot, pos=(0, 3, 1), spawn_time=3.0)) # Called for each spawning player. def spawn_player(self, player: Player) -> ba.Actor: # Let's spawn close to the center. spawn_center = (0, 3, -2) pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], spawn_center[2] + random.uniform(-1.5, 1.5)) return self.spawn_player_spaz(player, position=pos) def _check_if_won(self) -> None: # Simply end the game if there's no living bots. # FIXME: Should also make sure all bots have been spawned; # if spawning is spread out enough that we're able to kill # all living bots before the next spawns, it would incorrectly # count as a win. if not self._bots.have_living_bots(): self._won = True self.end_game() # Called for miscellaneous messages. def handlemessage(self, msg: Any) -> Any: # A player has died. if isinstance(msg, ba.PlayerDiedMessage): super().handlemessage(msg) # Augment standard behavior. self.respawn_player(msg.getplayer(Player)) # A spaz-bot has died. elif isinstance(msg, SpazBotDiedMessage): # Unfortunately the bot-set will always tell us there are living # bots if we ask here (the currently-dying bot isn't officially # marked dead yet) ..so lets push a call into the event loop to # check once this guy has finished dying. ba.pushcall(self._check_if_won) # Let the base class handle anything we don't. else: return super().handlemessage(msg) return None # When this is called, we should fill out results and end the game # *regardless* of whether is has been won. (this may be called due # to a tournament ending or other external reason). def end_game(self) -> None: # Stop our on-screen timer so players can see what they got. assert self._timer is not None self._timer.stop() results = ba.TeamGameResults() # If we won, set our score to the elapsed time in milliseconds. # (there should just be 1 team here since this is co-op). # ..if we didn't win, leave scores as default (None) which means # we lost. if self._won: elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0) ba.cameraflash() ba.playsound(self._winsound) for team in self.teams: for player in team.players: if player.actor: player.actor.handlemessage(ba.CelebrateMessage()) results.set_team_score(team, elapsed_time_ms) # Ends the activity. self.end(results)
class StickyStormGame(ba.TeamGameActivity[Player, Team]): """Minigame involving dodging falling ice bombs.""" name = 'Sticky Storm' description = 'Dodge the falling sticky bombs.' available_settings = [ba.BoolSetting('Epic Mode', default=False)] scoreconfig = ba.ScoreConfig(label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B') # Print messages when players die (since its meaningful in this game). announce_player_deaths = True # we're currently hard-coded for one map.. @classmethod def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]: return ['Rampage'] # We support teams, free-for-all, and co-op sessions. @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: return (issubclass(sessiontype, ba.DualTeamSession) or issubclass(sessiontype, ba.FreeForAllSession) or issubclass(sessiontype, ba.CoopSession)) def __init__(self, settings: dict): super().__init__(settings) self._epic_mode = settings.get('Epic Mode', False) self._last_player_death_time: Optional[float] = None self._meteor_time = 2.0 self._timer: Optional[OnScreenTimer] = None # Some base class overrides: self.default_music = (ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL) if self._epic_mode: self.slow_motion = True def on_begin(self) -> None: super().on_begin() # Drop a wave every few seconds.. and every so often drop the time # between waves ..lets have things increase faster if we have fewer # players. delay = 5.0 if len(self.players) > 2 else 2.5 if self._epic_mode: delay *= 0.25 ba.timer(delay, self._decrement_meteor_time, repeat=True) # Kick off the first wave in a few seconds. delay = 3.0 if self._epic_mode: delay *= 0.25 ba.timer(delay, self._set_meteor_timer) self._timer = OnScreenTimer() self._timer.start() # Check for immediate end (if we've only got 1 player, etc). ba.timer(5.0, self._check_end_game) def on_player_join(self, player: Player) -> None: # Don't allow joining after we start # (would enable leave/rejoin tomfoolery). if self.has_begun(): ba.screenmessage( ba.Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', player.getname(full=True))]), color=(0, 1, 0), ) # For score purposes, mark them as having died right as the # game started. assert self._timer is not None player.death_time = self._timer.getstarttime() return self.spawn_player(player) def on_player_leave(self, player: Player) -> None: # Augment default behavior. super().on_player_leave(player) # A departing player may trigger game-over. self._check_end_game() # overriding the default character spawning.. def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player) # Let's reconnect this player's controls to this # spaz but *without* the ability to attack or pick stuff up. spaz.connect_controls_to_player(enable_punch=False, enable_bomb=False, enable_pickup=False) # Also lets have them make some noise when they die. spaz.play_big_death_sound = True return spaz # Various high-level game events come through this method. def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): # Augment standard behavior. super().handlemessage(msg) curtime = ba.time() # Record the player's moment of death. # assert isinstance(msg.spaz.player msg.getplayer(Player).death_time = curtime # In co-op mode, end the game the instant everyone dies # (more accurate looking). # In teams/ffa, allow a one-second fudge-factor so we can # get more draws if players die basically at the same time. if isinstance(self.session, ba.CoopSession): # Teams will still show up if we check now.. check in # the next cycle. ba.pushcall(self._check_end_game) # Also record this for a final setting of the clock. self._last_player_death_time = curtime else: ba.timer(1.0, self._check_end_game) else: # Default handler: return super().handlemessage(msg) return None def _check_end_game(self) -> None: living_team_count = 0 for team in self.teams: for player in team.players: if player.is_alive(): living_team_count += 1 break # In co-op, we go till everyone is dead.. otherwise we go # until one team remains. if isinstance(self.session, ba.CoopSession): if living_team_count <= 0: self.end_game() else: if living_team_count <= 1: self.end_game() def _set_meteor_timer(self) -> None: ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time, self._drop_bomb_cluster) def _drop_bomb_cluster(self) -> None: # Random note: code like this is a handy way to plot out extents # and debug things. loc_test = False if loc_test: ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) # Drop several bombs in series. delay = 0.0 for _i in range(random.randrange(1, 3)): # Drop them somewhere within our bounds with velocity pointing # toward the opposite side. pos = (-7.3 + 15.3 * random.random(), 11, -5.5 + 2.1 * random.random()) dropdir = (-1.0 if pos[0] > 0 else 1.0) vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0) ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) delay += 0.1 self._set_meteor_timer() def _drop_bomb(self, position: Sequence[float], velocity: Sequence[float]) -> None: Bomb(position=position, velocity=velocity, bomb_type = 'sticky').autoretain() def _decrement_meteor_time(self) -> None: self._meteor_time = max(0.01, self._meteor_time * 0.9) def end_game(self) -> None: cur_time = ba.time() assert self._timer is not None start_time = self._timer.getstarttime() # Mark death-time as now for any still-living players # and award players points for how long they lasted. # (these per-player scores are only meaningful in team-games) for team in self.teams: for player in team.players: survived = False # Throw an extra fudge factor in so teams that # didn't die come out ahead of teams that did. if player.death_time is None: survived = True player.death_time = cur_time + 1 # Award a per-player score depending on how many seconds # they lasted (per-player scores only affect teams mode; # everywhere else just looks at the per-team score). score = int(player.death_time - self._timer.getstarttime()) if survived: score += 50 # A bit extra for survivors. self.stats.player_scored(player, score, screenmessage=False) # Stop updating our time text, and set the final time to match # exactly when our last guy died. self._timer.stop(endtime=self._last_player_death_time) # Ok now calc game results: set a score for each team and then tell # the game to end. results = ba.GameResults() # Remember that 'free-for-all' mode is simply a special form # of 'teams' mode where each player gets their own team, so we can # just always deal in teams and have all cases covered. for team in self.teams: # Set the team score to the max time survived by any player on # that team. longest_life = 0.0 for player in team.players: assert player.death_time is not None longest_life = max(longest_life, player.death_time - start_time) # Submit the score value in milliseconds. results.set_team_score(team, int(1000.0 * longest_life)) self.end(results=results) # Copyright (c) Lifetime Benefit-Zebra # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this mod without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the mod. # # THE MOD IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE MOD OR THE USE OR OTHER DEALINGS IN THE # MOD. # -------------------------------------- #By Benefit-Zebra #https://github.com/Benefit-Zebra