bastd.game.assault
Defines assault minigame.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Defines assault minigame.""" 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.playerspaz import PlayerSpaz 15from bastd.actor.flag import Flag 16from bastd.actor.scoreboard import Scoreboard 17from bastd.gameutils import SharedObjects 18 19if TYPE_CHECKING: 20 from typing import Any, Sequence 21 22 23class Player(ba.Player['Team']): 24 """Our player type for this game.""" 25 26 27class Team(ba.Team[Player]): 28 """Our team type for this game.""" 29 30 def __init__(self, base_pos: Sequence[float], flag: Flag) -> None: 31 self.base_pos = base_pos 32 self.flag = flag 33 self.score = 0 34 35 36# ba_meta export game 37class AssaultGame(ba.TeamGameActivity[Player, Team]): 38 """Game where you score by touching the other team's flag.""" 39 40 name = 'Assault' 41 description = 'Reach the enemy flag to score.' 42 available_settings = [ 43 ba.IntSetting( 44 'Score to Win', 45 min_value=1, 46 default=3, 47 ), 48 ba.IntChoiceSetting( 49 'Time Limit', 50 choices=[ 51 ('None', 0), 52 ('1 Minute', 60), 53 ('2 Minutes', 120), 54 ('5 Minutes', 300), 55 ('10 Minutes', 600), 56 ('20 Minutes', 1200), 57 ], 58 default=0, 59 ), 60 ba.FloatChoiceSetting( 61 'Respawn Times', 62 choices=[ 63 ('Shorter', 0.25), 64 ('Short', 0.5), 65 ('Normal', 1.0), 66 ('Long', 2.0), 67 ('Longer', 4.0), 68 ], 69 default=1.0, 70 ), 71 ba.BoolSetting('Epic Mode', default=False), 72 ] 73 74 @classmethod 75 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 76 return issubclass(sessiontype, ba.DualTeamSession) 77 78 @classmethod 79 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 80 return ba.getmaps('team_flag') 81 82 def __init__(self, settings: dict): 83 super().__init__(settings) 84 self._scoreboard = Scoreboard() 85 self._last_score_time = 0.0 86 self._score_sound = ba.getsound('score') 87 self._base_region_materials: dict[int, ba.Material] = {} 88 self._epic_mode = bool(settings['Epic Mode']) 89 self._score_to_win = int(settings['Score to Win']) 90 self._time_limit = float(settings['Time Limit']) 91 92 # Base class overrides 93 self.slow_motion = self._epic_mode 94 self.default_music = ( 95 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH 96 ) 97 98 def get_instance_description(self) -> str | Sequence: 99 if self._score_to_win == 1: 100 return 'Touch the enemy flag.' 101 return 'Touch the enemy flag ${ARG1} times.', self._score_to_win 102 103 def get_instance_description_short(self) -> str | Sequence: 104 if self._score_to_win == 1: 105 return 'touch 1 flag' 106 return 'touch ${ARG1} flags', self._score_to_win 107 108 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 109 shared = SharedObjects.get() 110 base_pos = self.map.get_flag_position(sessionteam.id) 111 ba.newnode( 112 'light', 113 attrs={ 114 'position': base_pos, 115 'intensity': 0.6, 116 'height_attenuated': False, 117 'volume_intensity_scale': 0.1, 118 'radius': 0.1, 119 'color': sessionteam.color, 120 }, 121 ) 122 Flag.project_stand(base_pos) 123 flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) 124 team = Team(base_pos=base_pos, flag=flag) 125 126 mat = self._base_region_materials[sessionteam.id] = ba.Material() 127 mat.add_actions( 128 conditions=('they_have_material', shared.player_material), 129 actions=( 130 ('modify_part_collision', 'collide', True), 131 ('modify_part_collision', 'physical', False), 132 ( 133 'call', 134 'at_connect', 135 ba.Call(self._handle_base_collide, team), 136 ), 137 ), 138 ) 139 140 ba.newnode( 141 'region', 142 owner=flag.node, 143 attrs={ 144 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 145 'scale': (0.5, 0.5, 0.5), 146 'type': 'sphere', 147 'materials': [self._base_region_materials[sessionteam.id]], 148 }, 149 ) 150 151 return team 152 153 def on_team_join(self, team: Team) -> None: 154 # Can't do this in create_team because the team's color/etc. have 155 # not been wired up yet at that point. 156 self._update_scoreboard() 157 158 def on_begin(self) -> None: 159 super().on_begin() 160 self.setup_standard_time_limit(self._time_limit) 161 self.setup_standard_powerup_drops() 162 163 def handlemessage(self, msg: Any) -> Any: 164 if isinstance(msg, ba.PlayerDiedMessage): 165 super().handlemessage(msg) # Augment standard. 166 self.respawn_player(msg.getplayer(Player)) 167 else: 168 super().handlemessage(msg) 169 170 def _flash_base(self, team: Team, length: float = 2.0) -> None: 171 light = ba.newnode( 172 'light', 173 attrs={ 174 'position': team.base_pos, 175 'height_attenuated': False, 176 'radius': 0.3, 177 'color': team.color, 178 }, 179 ) 180 ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 181 ba.timer(length, light.delete) 182 183 def _handle_base_collide(self, team: Team) -> None: 184 try: 185 spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) 186 except ba.NotFoundError: 187 return 188 189 if not spaz.is_alive(): 190 return 191 192 try: 193 player = spaz.getplayer(Player, True) 194 except ba.NotFoundError: 195 return 196 197 # If its another team's player, they scored. 198 player_team = player.team 199 if player_team is not team: 200 201 # Prevent multiple simultaneous scores. 202 if ba.time() != self._last_score_time: 203 self._last_score_time = ba.time() 204 self.stats.player_scored(player, 50, big_message=True) 205 ba.playsound(self._score_sound) 206 self._flash_base(team) 207 208 # Move all players on the scoring team back to their start 209 # and add flashes of light so its noticeable. 210 for player in player_team.players: 211 if player.is_alive(): 212 pos = player.node.position 213 light = ba.newnode( 214 'light', 215 attrs={ 216 'position': pos, 217 'color': player_team.color, 218 'height_attenuated': False, 219 'radius': 0.4, 220 }, 221 ) 222 ba.timer(0.5, light.delete) 223 ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) 224 225 new_pos = self.map.get_start_position(player_team.id) 226 light = ba.newnode( 227 'light', 228 attrs={ 229 'position': new_pos, 230 'color': player_team.color, 231 'radius': 0.4, 232 'height_attenuated': False, 233 }, 234 ) 235 ba.timer(0.5, light.delete) 236 ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) 237 if player.actor: 238 player.actor.handlemessage( 239 ba.StandMessage(new_pos, random.uniform(0, 360)) 240 ) 241 242 # Have teammates celebrate. 243 for player in player_team.players: 244 if player.actor: 245 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 246 247 player_team.score += 1 248 self._update_scoreboard() 249 if player_team.score >= self._score_to_win: 250 self.end_game() 251 252 def end_game(self) -> None: 253 results = ba.GameResults() 254 for team in self.teams: 255 results.set_team_score(team, team.score) 256 self.end(results=results) 257 258 def _update_scoreboard(self) -> None: 259 for team in self.teams: 260 self._scoreboard.set_team_value( 261 team, team.score, self._score_to_win 262 )
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
28class Team(ba.Team[Player]): 29 """Our team type for this game.""" 30 31 def __init__(self, base_pos: Sequence[float], flag: Flag) -> None: 32 self.base_pos = base_pos 33 self.flag = flag 34 self.score = 0
Our team type for this game.
Inherited Members
- ba._team.Team
- manual_init
- customdata
- on_expire
- sessionteam
38class AssaultGame(ba.TeamGameActivity[Player, Team]): 39 """Game where you score by touching the other team's flag.""" 40 41 name = 'Assault' 42 description = 'Reach the enemy flag to score.' 43 available_settings = [ 44 ba.IntSetting( 45 'Score to Win', 46 min_value=1, 47 default=3, 48 ), 49 ba.IntChoiceSetting( 50 'Time Limit', 51 choices=[ 52 ('None', 0), 53 ('1 Minute', 60), 54 ('2 Minutes', 120), 55 ('5 Minutes', 300), 56 ('10 Minutes', 600), 57 ('20 Minutes', 1200), 58 ], 59 default=0, 60 ), 61 ba.FloatChoiceSetting( 62 'Respawn Times', 63 choices=[ 64 ('Shorter', 0.25), 65 ('Short', 0.5), 66 ('Normal', 1.0), 67 ('Long', 2.0), 68 ('Longer', 4.0), 69 ], 70 default=1.0, 71 ), 72 ba.BoolSetting('Epic Mode', default=False), 73 ] 74 75 @classmethod 76 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 77 return issubclass(sessiontype, ba.DualTeamSession) 78 79 @classmethod 80 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 81 return ba.getmaps('team_flag') 82 83 def __init__(self, settings: dict): 84 super().__init__(settings) 85 self._scoreboard = Scoreboard() 86 self._last_score_time = 0.0 87 self._score_sound = ba.getsound('score') 88 self._base_region_materials: dict[int, ba.Material] = {} 89 self._epic_mode = bool(settings['Epic Mode']) 90 self._score_to_win = int(settings['Score to Win']) 91 self._time_limit = float(settings['Time Limit']) 92 93 # Base class overrides 94 self.slow_motion = self._epic_mode 95 self.default_music = ( 96 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH 97 ) 98 99 def get_instance_description(self) -> str | Sequence: 100 if self._score_to_win == 1: 101 return 'Touch the enemy flag.' 102 return 'Touch the enemy flag ${ARG1} times.', self._score_to_win 103 104 def get_instance_description_short(self) -> str | Sequence: 105 if self._score_to_win == 1: 106 return 'touch 1 flag' 107 return 'touch ${ARG1} flags', self._score_to_win 108 109 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 110 shared = SharedObjects.get() 111 base_pos = self.map.get_flag_position(sessionteam.id) 112 ba.newnode( 113 'light', 114 attrs={ 115 'position': base_pos, 116 'intensity': 0.6, 117 'height_attenuated': False, 118 'volume_intensity_scale': 0.1, 119 'radius': 0.1, 120 'color': sessionteam.color, 121 }, 122 ) 123 Flag.project_stand(base_pos) 124 flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) 125 team = Team(base_pos=base_pos, flag=flag) 126 127 mat = self._base_region_materials[sessionteam.id] = ba.Material() 128 mat.add_actions( 129 conditions=('they_have_material', shared.player_material), 130 actions=( 131 ('modify_part_collision', 'collide', True), 132 ('modify_part_collision', 'physical', False), 133 ( 134 'call', 135 'at_connect', 136 ba.Call(self._handle_base_collide, team), 137 ), 138 ), 139 ) 140 141 ba.newnode( 142 'region', 143 owner=flag.node, 144 attrs={ 145 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 146 'scale': (0.5, 0.5, 0.5), 147 'type': 'sphere', 148 'materials': [self._base_region_materials[sessionteam.id]], 149 }, 150 ) 151 152 return team 153 154 def on_team_join(self, team: Team) -> None: 155 # Can't do this in create_team because the team's color/etc. have 156 # not been wired up yet at that point. 157 self._update_scoreboard() 158 159 def on_begin(self) -> None: 160 super().on_begin() 161 self.setup_standard_time_limit(self._time_limit) 162 self.setup_standard_powerup_drops() 163 164 def handlemessage(self, msg: Any) -> Any: 165 if isinstance(msg, ba.PlayerDiedMessage): 166 super().handlemessage(msg) # Augment standard. 167 self.respawn_player(msg.getplayer(Player)) 168 else: 169 super().handlemessage(msg) 170 171 def _flash_base(self, team: Team, length: float = 2.0) -> None: 172 light = ba.newnode( 173 'light', 174 attrs={ 175 'position': team.base_pos, 176 'height_attenuated': False, 177 'radius': 0.3, 178 'color': team.color, 179 }, 180 ) 181 ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 182 ba.timer(length, light.delete) 183 184 def _handle_base_collide(self, team: Team) -> None: 185 try: 186 spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) 187 except ba.NotFoundError: 188 return 189 190 if not spaz.is_alive(): 191 return 192 193 try: 194 player = spaz.getplayer(Player, True) 195 except ba.NotFoundError: 196 return 197 198 # If its another team's player, they scored. 199 player_team = player.team 200 if player_team is not team: 201 202 # Prevent multiple simultaneous scores. 203 if ba.time() != self._last_score_time: 204 self._last_score_time = ba.time() 205 self.stats.player_scored(player, 50, big_message=True) 206 ba.playsound(self._score_sound) 207 self._flash_base(team) 208 209 # Move all players on the scoring team back to their start 210 # and add flashes of light so its noticeable. 211 for player in player_team.players: 212 if player.is_alive(): 213 pos = player.node.position 214 light = ba.newnode( 215 'light', 216 attrs={ 217 'position': pos, 218 'color': player_team.color, 219 'height_attenuated': False, 220 'radius': 0.4, 221 }, 222 ) 223 ba.timer(0.5, light.delete) 224 ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) 225 226 new_pos = self.map.get_start_position(player_team.id) 227 light = ba.newnode( 228 'light', 229 attrs={ 230 'position': new_pos, 231 'color': player_team.color, 232 'radius': 0.4, 233 'height_attenuated': False, 234 }, 235 ) 236 ba.timer(0.5, light.delete) 237 ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) 238 if player.actor: 239 player.actor.handlemessage( 240 ba.StandMessage(new_pos, random.uniform(0, 360)) 241 ) 242 243 # Have teammates celebrate. 244 for player in player_team.players: 245 if player.actor: 246 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 247 248 player_team.score += 1 249 self._update_scoreboard() 250 if player_team.score >= self._score_to_win: 251 self.end_game() 252 253 def end_game(self) -> None: 254 results = ba.GameResults() 255 for team in self.teams: 256 results.set_team_score(team, team.score) 257 self.end(results=results) 258 259 def _update_scoreboard(self) -> None: 260 for team in self.teams: 261 self._scoreboard.set_team_value( 262 team, team.score, self._score_to_win 263 )
Game where you score by touching the other team's flag.
83 def __init__(self, settings: dict): 84 super().__init__(settings) 85 self._scoreboard = Scoreboard() 86 self._last_score_time = 0.0 87 self._score_sound = ba.getsound('score') 88 self._base_region_materials: dict[int, ba.Material] = {} 89 self._epic_mode = bool(settings['Epic Mode']) 90 self._score_to_win = int(settings['Score to Win']) 91 self._time_limit = float(settings['Time Limit']) 92 93 # Base class overrides 94 self.slow_motion = self._epic_mode 95 self.default_music = ( 96 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH 97 )
Instantiate the Activity.
75 @classmethod 76 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 77 return issubclass(sessiontype, ba.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
79 @classmethod 80 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 81 return ba.getmaps('team_flag')
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.
99 def get_instance_description(self) -> str | Sequence: 100 if self._score_to_win == 1: 101 return 'Touch the enemy flag.' 102 return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
Return a description for this game instance, in English.
This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'Score 3 goals.' in English
and can properly translate to 'Anota 3 goles.' in Spanish.
If we just returned the string 'Score 3 Goals' here, there would
have to be a translation entry for each specific number. ew.
return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
104 def get_instance_description_short(self) -> str | Sequence: 105 if self._score_to_win == 1: 106 return 'touch 1 flag' 107 return 'touch ${ARG1} flags', self._score_to_win
Return a short description for this game instance in English.
This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'score 3 goals' in English
and can properly translate to 'anota 3 goles' in Spanish.
If we just returned the string 'score 3 goals' here, there would
have to be a translation entry for each specific number. ew.
return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
109 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 110 shared = SharedObjects.get() 111 base_pos = self.map.get_flag_position(sessionteam.id) 112 ba.newnode( 113 'light', 114 attrs={ 115 'position': base_pos, 116 'intensity': 0.6, 117 'height_attenuated': False, 118 'volume_intensity_scale': 0.1, 119 'radius': 0.1, 120 'color': sessionteam.color, 121 }, 122 ) 123 Flag.project_stand(base_pos) 124 flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) 125 team = Team(base_pos=base_pos, flag=flag) 126 127 mat = self._base_region_materials[sessionteam.id] = ba.Material() 128 mat.add_actions( 129 conditions=('they_have_material', shared.player_material), 130 actions=( 131 ('modify_part_collision', 'collide', True), 132 ('modify_part_collision', 'physical', False), 133 ( 134 'call', 135 'at_connect', 136 ba.Call(self._handle_base_collide, team), 137 ), 138 ), 139 ) 140 141 ba.newnode( 142 'region', 143 owner=flag.node, 144 attrs={ 145 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 146 'scale': (0.5, 0.5, 0.5), 147 'type': 'sphere', 148 'materials': [self._base_region_materials[sessionteam.id]], 149 }, 150 ) 151 152 return team
Create the Team instance for this Activity.
Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.
154 def on_team_join(self, team: Team) -> None: 155 # Can't do this in create_team because the team's color/etc. have 156 # not been wired up yet at that point. 157 self._update_scoreboard()
Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
159 def on_begin(self) -> None: 160 super().on_begin() 161 self.setup_standard_time_limit(self._time_limit) 162 self.setup_standard_powerup_drops()
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.
164 def handlemessage(self, msg: Any) -> Any: 165 if isinstance(msg, ba.PlayerDiedMessage): 166 super().handlemessage(msg) # Augment standard. 167 self.respawn_player(msg.getplayer(Player)) 168 else: 169 super().handlemessage(msg)
General message handling; can be passed any message object.
253 def end_game(self) -> None: 254 results = ba.GameResults() 255 for team in self.teams: 256 results.set_team_score(team, team.score) 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._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- ba._gameactivity.GameActivity
- allow_pausing
- allow_kick_idle_players
- 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
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- respawn_player
- spawn_player_if_exists
- spawn_player
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- ba._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps