bastd.game.meteorshower
Defines a bomb-dodging mini-game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines a bomb-dodging mini-game.""" 4 5# ba_meta require api 7 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING 12 13import ba 14from bastd.actor.bomb import Bomb 15from bastd.actor.onscreentimer import OnScreenTimer 16 17if TYPE_CHECKING: 18 from typing import Any, Sequence 19 20 21class Player(ba.Player['Team']): 22 """Our player type for this game.""" 23 24 def __init__(self) -> None: 25 super().__init__() 26 self.death_time: float | None = None 27 28 29class Team(ba.Team[Player]): 30 """Our team type for this game.""" 31 32 33# ba_meta export game 34class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): 35 """Minigame involving dodging falling bombs.""" 36 37 name = 'Meteor Shower' 38 description = 'Dodge the falling bombs.' 39 available_settings = [ba.BoolSetting('Epic Mode', default=False)] 40 scoreconfig = ba.ScoreConfig(label='Survived', 41 scoretype=ba.ScoreType.MILLISECONDS, 42 version='B') 43 44 # Print messages when players die (since its meaningful in this game). 45 announce_player_deaths = True 46 47 # Don't allow joining after we start 48 # (would enable leave/rejoin tomfoolery). 49 allow_mid_activity_joins = False 50 51 # We're currently hard-coded for one map. 52 @classmethod 53 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 54 return ['Rampage'] 55 56 # We support teams, free-for-all, and co-op sessions. 57 @classmethod 58 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 59 return (issubclass(sessiontype, ba.DualTeamSession) 60 or issubclass(sessiontype, ba.FreeForAllSession) 61 or issubclass(sessiontype, ba.CoopSession)) 62 63 def __init__(self, settings: dict): 64 super().__init__(settings) 65 66 self._epic_mode = settings.get('Epic Mode', False) 67 self._last_player_death_time: float | None = None 68 self._meteor_time = 2.0 69 self._timer: OnScreenTimer | None = None 70 71 # Some base class overrides: 72 self.default_music = (ba.MusicType.EPIC 73 if self._epic_mode else ba.MusicType.SURVIVAL) 74 if self._epic_mode: 75 self.slow_motion = True 76 77 def on_begin(self) -> None: 78 super().on_begin() 79 80 # Drop a wave every few seconds.. and every so often drop the time 81 # between waves ..lets have things increase faster if we have fewer 82 # players. 83 delay = 5.0 if len(self.players) > 2 else 2.5 84 if self._epic_mode: 85 delay *= 0.25 86 ba.timer(delay, self._decrement_meteor_time, repeat=True) 87 88 # Kick off the first wave in a few seconds. 89 delay = 3.0 90 if self._epic_mode: 91 delay *= 0.25 92 ba.timer(delay, self._set_meteor_timer) 93 94 self._timer = OnScreenTimer() 95 self._timer.start() 96 97 # Check for immediate end (if we've only got 1 player, etc). 98 ba.timer(5.0, self._check_end_game) 99 100 def on_player_leave(self, player: Player) -> None: 101 # Augment default behavior. 102 super().on_player_leave(player) 103 104 # A departing player may trigger game-over. 105 self._check_end_game() 106 107 # overriding the default character spawning.. 108 def spawn_player(self, player: Player) -> ba.Actor: 109 spaz = self.spawn_player_spaz(player) 110 111 # Let's reconnect this player's controls to this 112 # spaz but *without* the ability to attack or pick stuff up. 113 spaz.connect_controls_to_player(enable_punch=False, 114 enable_bomb=False, 115 enable_pickup=False) 116 117 # Also lets have them make some noise when they die. 118 spaz.play_big_death_sound = True 119 return spaz 120 121 # Various high-level game events come through this method. 122 def handlemessage(self, msg: Any) -> Any: 123 if isinstance(msg, ba.PlayerDiedMessage): 124 125 # Augment standard behavior. 126 super().handlemessage(msg) 127 128 curtime = ba.time() 129 130 # Record the player's moment of death. 131 # assert isinstance(msg.spaz.player 132 msg.getplayer(Player).death_time = curtime 133 134 # In co-op mode, end the game the instant everyone dies 135 # (more accurate looking). 136 # In teams/ffa, allow a one-second fudge-factor so we can 137 # get more draws if players die basically at the same time. 138 if isinstance(self.session, ba.CoopSession): 139 # Teams will still show up if we check now.. check in 140 # the next cycle. 141 ba.pushcall(self._check_end_game) 142 143 # Also record this for a final setting of the clock. 144 self._last_player_death_time = curtime 145 else: 146 ba.timer(1.0, self._check_end_game) 147 148 else: 149 # Default handler: 150 return super().handlemessage(msg) 151 return None 152 153 def _check_end_game(self) -> None: 154 living_team_count = 0 155 for team in self.teams: 156 for player in team.players: 157 if player.is_alive(): 158 living_team_count += 1 159 break 160 161 # In co-op, we go till everyone is dead.. otherwise we go 162 # until one team remains. 163 if isinstance(self.session, ba.CoopSession): 164 if living_team_count <= 0: 165 self.end_game() 166 else: 167 if living_team_count <= 1: 168 self.end_game() 169 170 def _set_meteor_timer(self) -> None: 171 ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time, 172 self._drop_bomb_cluster) 173 174 def _drop_bomb_cluster(self) -> None: 175 176 # Random note: code like this is a handy way to plot out extents 177 # and debug things. 178 loc_test = False 179 if loc_test: 180 ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) 181 ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) 182 ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) 183 ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) 184 185 # Drop several bombs in series. 186 delay = 0.0 187 for _i in range(random.randrange(1, 3)): 188 # Drop them somewhere within our bounds with velocity pointing 189 # toward the opposite side. 190 pos = (-7.3 + 15.3 * random.random(), 11, 191 -5.57 + 2.1 * random.random()) 192 dropdir = (-1.0 if pos[0] > 0 else 1.0) 193 vel = ((-5.0 + random.random() * 30.0) * dropdir, 194 random.uniform(-3.066, -4.12), 0) 195 ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) 196 delay += 0.1 197 self._set_meteor_timer() 198 199 def _drop_bomb(self, position: Sequence[float], 200 velocity: Sequence[float]) -> None: 201 Bomb(position=position, velocity=velocity).autoretain() 202 203 def _decrement_meteor_time(self) -> None: 204 self._meteor_time = max(0.01, self._meteor_time * 0.9) 205 206 def end_game(self) -> None: 207 cur_time = ba.time() 208 assert self._timer is not None 209 start_time = self._timer.getstarttime() 210 211 # Mark death-time as now for any still-living players 212 # and award players points for how long they lasted. 213 # (these per-player scores are only meaningful in team-games) 214 for team in self.teams: 215 for player in team.players: 216 survived = False 217 218 # Throw an extra fudge factor in so teams that 219 # didn't die come out ahead of teams that did. 220 if player.death_time is None: 221 survived = True 222 player.death_time = cur_time + 1 223 224 # Award a per-player score depending on how many seconds 225 # they lasted (per-player scores only affect teams mode; 226 # everywhere else just looks at the per-team score). 227 score = int(player.death_time - self._timer.getstarttime()) 228 if survived: 229 score += 50 # A bit extra for survivors. 230 self.stats.player_scored(player, score, screenmessage=False) 231 232 # Stop updating our time text, and set the final time to match 233 # exactly when our last guy died. 234 self._timer.stop(endtime=self._last_player_death_time) 235 236 # Ok now calc game results: set a score for each team and then tell 237 # the game to end. 238 results = ba.GameResults() 239 240 # Remember that 'free-for-all' mode is simply a special form 241 # of 'teams' mode where each player gets their own team, so we can 242 # just always deal in teams and have all cases covered. 243 for team in self.teams: 244 245 # Set the team score to the max time survived by any player on 246 # that team. 247 longest_life = 0.0 248 for player in team.players: 249 assert player.death_time is not None 250 longest_life = max(longest_life, 251 player.death_time - start_time) 252 253 # Submit the score value in milliseconds. 254 results.set_team_score(team, int(1000.0 * longest_life)) 255 256 self.end(results=results)
22class Player(ba.Player['Team']): 23 """Our player type for this game.""" 24 25 def __init__(self) -> None: 26 super().__init__() 27 self.death_time: float | None = None
Our player type for this game.
Inherited Members
- ba._player.Player
- actor
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
Our team type for this game.
Inherited Members
35class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): 36 """Minigame involving dodging falling bombs.""" 37 38 name = 'Meteor Shower' 39 description = 'Dodge the falling bombs.' 40 available_settings = [ba.BoolSetting('Epic Mode', default=False)] 41 scoreconfig = ba.ScoreConfig(label='Survived', 42 scoretype=ba.ScoreType.MILLISECONDS, 43 version='B') 44 45 # Print messages when players die (since its meaningful in this game). 46 announce_player_deaths = True 47 48 # Don't allow joining after we start 49 # (would enable leave/rejoin tomfoolery). 50 allow_mid_activity_joins = False 51 52 # We're currently hard-coded for one map. 53 @classmethod 54 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 55 return ['Rampage'] 56 57 # We support teams, free-for-all, and co-op sessions. 58 @classmethod 59 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 60 return (issubclass(sessiontype, ba.DualTeamSession) 61 or issubclass(sessiontype, ba.FreeForAllSession) 62 or issubclass(sessiontype, ba.CoopSession)) 63 64 def __init__(self, settings: dict): 65 super().__init__(settings) 66 67 self._epic_mode = settings.get('Epic Mode', False) 68 self._last_player_death_time: float | None = None 69 self._meteor_time = 2.0 70 self._timer: OnScreenTimer | None = None 71 72 # Some base class overrides: 73 self.default_music = (ba.MusicType.EPIC 74 if self._epic_mode else ba.MusicType.SURVIVAL) 75 if self._epic_mode: 76 self.slow_motion = True 77 78 def on_begin(self) -> None: 79 super().on_begin() 80 81 # Drop a wave every few seconds.. and every so often drop the time 82 # between waves ..lets have things increase faster if we have fewer 83 # players. 84 delay = 5.0 if len(self.players) > 2 else 2.5 85 if self._epic_mode: 86 delay *= 0.25 87 ba.timer(delay, self._decrement_meteor_time, repeat=True) 88 89 # Kick off the first wave in a few seconds. 90 delay = 3.0 91 if self._epic_mode: 92 delay *= 0.25 93 ba.timer(delay, self._set_meteor_timer) 94 95 self._timer = OnScreenTimer() 96 self._timer.start() 97 98 # Check for immediate end (if we've only got 1 player, etc). 99 ba.timer(5.0, self._check_end_game) 100 101 def on_player_leave(self, player: Player) -> None: 102 # Augment default behavior. 103 super().on_player_leave(player) 104 105 # A departing player may trigger game-over. 106 self._check_end_game() 107 108 # overriding the default character spawning.. 109 def spawn_player(self, player: Player) -> ba.Actor: 110 spaz = self.spawn_player_spaz(player) 111 112 # Let's reconnect this player's controls to this 113 # spaz but *without* the ability to attack or pick stuff up. 114 spaz.connect_controls_to_player(enable_punch=False, 115 enable_bomb=False, 116 enable_pickup=False) 117 118 # Also lets have them make some noise when they die. 119 spaz.play_big_death_sound = True 120 return spaz 121 122 # Various high-level game events come through this method. 123 def handlemessage(self, msg: Any) -> Any: 124 if isinstance(msg, ba.PlayerDiedMessage): 125 126 # Augment standard behavior. 127 super().handlemessage(msg) 128 129 curtime = ba.time() 130 131 # Record the player's moment of death. 132 # assert isinstance(msg.spaz.player 133 msg.getplayer(Player).death_time = curtime 134 135 # In co-op mode, end the game the instant everyone dies 136 # (more accurate looking). 137 # In teams/ffa, allow a one-second fudge-factor so we can 138 # get more draws if players die basically at the same time. 139 if isinstance(self.session, ba.CoopSession): 140 # Teams will still show up if we check now.. check in 141 # the next cycle. 142 ba.pushcall(self._check_end_game) 143 144 # Also record this for a final setting of the clock. 145 self._last_player_death_time = curtime 146 else: 147 ba.timer(1.0, self._check_end_game) 148 149 else: 150 # Default handler: 151 return super().handlemessage(msg) 152 return None 153 154 def _check_end_game(self) -> None: 155 living_team_count = 0 156 for team in self.teams: 157 for player in team.players: 158 if player.is_alive(): 159 living_team_count += 1 160 break 161 162 # In co-op, we go till everyone is dead.. otherwise we go 163 # until one team remains. 164 if isinstance(self.session, ba.CoopSession): 165 if living_team_count <= 0: 166 self.end_game() 167 else: 168 if living_team_count <= 1: 169 self.end_game() 170 171 def _set_meteor_timer(self) -> None: 172 ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time, 173 self._drop_bomb_cluster) 174 175 def _drop_bomb_cluster(self) -> None: 176 177 # Random note: code like this is a handy way to plot out extents 178 # and debug things. 179 loc_test = False 180 if loc_test: 181 ba.newnode('locator', attrs={'position': (8, 6, -5.5)}) 182 ba.newnode('locator', attrs={'position': (8, 6, -2.3)}) 183 ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)}) 184 ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)}) 185 186 # Drop several bombs in series. 187 delay = 0.0 188 for _i in range(random.randrange(1, 3)): 189 # Drop them somewhere within our bounds with velocity pointing 190 # toward the opposite side. 191 pos = (-7.3 + 15.3 * random.random(), 11, 192 -5.57 + 2.1 * random.random()) 193 dropdir = (-1.0 if pos[0] > 0 else 1.0) 194 vel = ((-5.0 + random.random() * 30.0) * dropdir, 195 random.uniform(-3.066, -4.12), 0) 196 ba.timer(delay, ba.Call(self._drop_bomb, pos, vel)) 197 delay += 0.1 198 self._set_meteor_timer() 199 200 def _drop_bomb(self, position: Sequence[float], 201 velocity: Sequence[float]) -> None: 202 Bomb(position=position, velocity=velocity).autoretain() 203 204 def _decrement_meteor_time(self) -> None: 205 self._meteor_time = max(0.01, self._meteor_time * 0.9) 206 207 def end_game(self) -> None: 208 cur_time = ba.time() 209 assert self._timer is not None 210 start_time = self._timer.getstarttime() 211 212 # Mark death-time as now for any still-living players 213 # and award players points for how long they lasted. 214 # (these per-player scores are only meaningful in team-games) 215 for team in self.teams: 216 for player in team.players: 217 survived = False 218 219 # Throw an extra fudge factor in so teams that 220 # didn't die come out ahead of teams that did. 221 if player.death_time is None: 222 survived = True 223 player.death_time = cur_time + 1 224 225 # Award a per-player score depending on how many seconds 226 # they lasted (per-player scores only affect teams mode; 227 # everywhere else just looks at the per-team score). 228 score = int(player.death_time - self._timer.getstarttime()) 229 if survived: 230 score += 50 # A bit extra for survivors. 231 self.stats.player_scored(player, score, screenmessage=False) 232 233 # Stop updating our time text, and set the final time to match 234 # exactly when our last guy died. 235 self._timer.stop(endtime=self._last_player_death_time) 236 237 # Ok now calc game results: set a score for each team and then tell 238 # the game to end. 239 results = ba.GameResults() 240 241 # Remember that 'free-for-all' mode is simply a special form 242 # of 'teams' mode where each player gets their own team, so we can 243 # just always deal in teams and have all cases covered. 244 for team in self.teams: 245 246 # Set the team score to the max time survived by any player on 247 # that team. 248 longest_life = 0.0 249 for player in team.players: 250 assert player.death_time is not None 251 longest_life = max(longest_life, 252 player.death_time - start_time) 253 254 # Submit the score value in milliseconds. 255 results.set_team_score(team, int(1000.0 * longest_life)) 256 257 self.end(results=results)
Minigame involving dodging falling bombs.
64 def __init__(self, settings: dict): 65 super().__init__(settings) 66 67 self._epic_mode = settings.get('Epic Mode', False) 68 self._last_player_death_time: float | None = None 69 self._meteor_time = 2.0 70 self._timer: OnScreenTimer | None = None 71 72 # Some base class overrides: 73 self.default_music = (ba.MusicType.EPIC 74 if self._epic_mode else ba.MusicType.SURVIVAL) 75 if self._epic_mode: 76 self.slow_motion = True
Instantiate the Activity.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.
53 @classmethod 54 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 55 return ['Rampage']
Called by the default ba.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given ba.Session type.
58 @classmethod 59 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 60 return (issubclass(sessiontype, ba.DualTeamSession) 61 or issubclass(sessiontype, ba.FreeForAllSession) 62 or issubclass(sessiontype, ba.CoopSession))
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
78 def on_begin(self) -> None: 79 super().on_begin() 80 81 # Drop a wave every few seconds.. and every so often drop the time 82 # between waves ..lets have things increase faster if we have fewer 83 # players. 84 delay = 5.0 if len(self.players) > 2 else 2.5 85 if self._epic_mode: 86 delay *= 0.25 87 ba.timer(delay, self._decrement_meteor_time, repeat=True) 88 89 # Kick off the first wave in a few seconds. 90 delay = 3.0 91 if self._epic_mode: 92 delay *= 0.25 93 ba.timer(delay, self._set_meteor_timer) 94 95 self._timer = OnScreenTimer() 96 self._timer.start() 97 98 # Check for immediate end (if we've only got 1 player, etc). 99 ba.timer(5.0, self._check_end_game)
Called once the previous ba.Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
101 def on_player_leave(self, player: Player) -> None: 102 # Augment default behavior. 103 super().on_player_leave(player) 104 105 # A departing player may trigger game-over. 106 self._check_end_game()
Called when a ba.Player is leaving the Activity.
109 def spawn_player(self, player: Player) -> ba.Actor: 110 spaz = self.spawn_player_spaz(player) 111 112 # Let's reconnect this player's controls to this 113 # spaz but *without* the ability to attack or pick stuff up. 114 spaz.connect_controls_to_player(enable_punch=False, 115 enable_bomb=False, 116 enable_pickup=False) 117 118 # Also lets have them make some noise when they die. 119 spaz.play_big_death_sound = True 120 return spaz
Spawn something for the provided ba.Player.
The default implementation simply calls spawn_player_spaz().
123 def handlemessage(self, msg: Any) -> Any: 124 if isinstance(msg, ba.PlayerDiedMessage): 125 126 # Augment standard behavior. 127 super().handlemessage(msg) 128 129 curtime = ba.time() 130 131 # Record the player's moment of death. 132 # assert isinstance(msg.spaz.player 133 msg.getplayer(Player).death_time = curtime 134 135 # In co-op mode, end the game the instant everyone dies 136 # (more accurate looking). 137 # In teams/ffa, allow a one-second fudge-factor so we can 138 # get more draws if players die basically at the same time. 139 if isinstance(self.session, ba.CoopSession): 140 # Teams will still show up if we check now.. check in 141 # the next cycle. 142 ba.pushcall(self._check_end_game) 143 144 # Also record this for a final setting of the clock. 145 self._last_player_death_time = curtime 146 else: 147 ba.timer(1.0, self._check_end_game) 148 149 else: 150 # Default handler: 151 return super().handlemessage(msg) 152 return None
General message handling; can be passed any message object.
207 def end_game(self) -> None: 208 cur_time = ba.time() 209 assert self._timer is not None 210 start_time = self._timer.getstarttime() 211 212 # Mark death-time as now for any still-living players 213 # and award players points for how long they lasted. 214 # (these per-player scores are only meaningful in team-games) 215 for team in self.teams: 216 for player in team.players: 217 survived = False 218 219 # Throw an extra fudge factor in so teams that 220 # didn't die come out ahead of teams that did. 221 if player.death_time is None: 222 survived = True 223 player.death_time = cur_time + 1 224 225 # Award a per-player score depending on how many seconds 226 # they lasted (per-player scores only affect teams mode; 227 # everywhere else just looks at the per-team score). 228 score = int(player.death_time - self._timer.getstarttime()) 229 if survived: 230 score += 50 # A bit extra for survivors. 231 self.stats.player_scored(player, score, screenmessage=False) 232 233 # Stop updating our time text, and set the final time to match 234 # exactly when our last guy died. 235 self._timer.stop(endtime=self._last_player_death_time) 236 237 # Ok now calc game results: set a score for each team and then tell 238 # the game to end. 239 results = ba.GameResults() 240 241 # Remember that 'free-for-all' mode is simply a special form 242 # of 'teams' mode where each player gets their own team, so we can 243 # just always deal in teams and have all cases covered. 244 for team in self.teams: 245 246 # Set the team score to the max time survived by any player on 247 # that team. 248 longest_life = 0.0 249 for player in team.players: 250 assert player.death_time is not None 251 longest_life = max(longest_life, 252 player.death_time - start_time) 253 254 # Submit the score value in milliseconds. 255 results.set_team_score(team, int(1000.0 * longest_life)) 256 257 self.end(results=results)
Tell the game to wrap up and call ba.Activity.end() immediately.
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (ba.GameActivity.setup_standard_time_limit()) will work with the game.
Inherited Members
- ba._gameactivity.GameActivity
- default_music
- tips
- allow_pausing
- allow_kick_idle_players
- show_kill_points
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_settings_display_string
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- get_instance_description
- get_instance_description_short
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- respawn_player
- spawn_player_if_exists
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- ba._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- ba._activity.Activity
- settings_raw
- teams
- players
- is_joining_activity
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- transition_time
- can_show_ad_on_death
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_team_join
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps