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 = (ba.MusicType.EPIC if self._epic_mode else 95 ba.MusicType.FORWARD_MARCH) 96 97 def get_instance_description(self) -> str | Sequence: 98 if self._score_to_win == 1: 99 return 'Touch the enemy flag.' 100 return 'Touch the enemy flag ${ARG1} times.', self._score_to_win 101 102 def get_instance_description_short(self) -> str | Sequence: 103 if self._score_to_win == 1: 104 return 'touch 1 flag' 105 return 'touch ${ARG1} flags', self._score_to_win 106 107 def create_team(self, sessionteam: ba.SessionTeam) -> Team: 108 shared = SharedObjects.get() 109 base_pos = self.map.get_flag_position(sessionteam.id) 110 ba.newnode('light', 111 attrs={ 112 'position': base_pos, 113 'intensity': 0.6, 114 'height_attenuated': False, 115 'volume_intensity_scale': 0.1, 116 'radius': 0.1, 117 'color': sessionteam.color 118 }) 119 Flag.project_stand(base_pos) 120 flag = Flag(touchable=False, 121 position=base_pos, 122 color=sessionteam.color) 123 team = Team(base_pos=base_pos, flag=flag) 124 125 mat = self._base_region_materials[sessionteam.id] = ba.Material() 126 mat.add_actions( 127 conditions=('they_have_material', shared.player_material), 128 actions=( 129 ('modify_part_collision', 'collide', True), 130 ('modify_part_collision', 'physical', False), 131 ('call', 'at_connect', ba.Call(self._handle_base_collide, 132 team)), 133 ), 134 ) 135 136 ba.newnode( 137 'region', 138 owner=flag.node, 139 attrs={ 140 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 141 'scale': (0.5, 0.5, 0.5), 142 'type': 'sphere', 143 'materials': [self._base_region_materials[sessionteam.id]] 144 }) 145 146 return team 147 148 def on_team_join(self, team: Team) -> None: 149 # Can't do this in create_team because the team's color/etc. have 150 # not been wired up yet at that point. 151 self._update_scoreboard() 152 153 def on_begin(self) -> None: 154 super().on_begin() 155 self.setup_standard_time_limit(self._time_limit) 156 self.setup_standard_powerup_drops() 157 158 def handlemessage(self, msg: Any) -> Any: 159 if isinstance(msg, ba.PlayerDiedMessage): 160 super().handlemessage(msg) # Augment standard. 161 self.respawn_player(msg.getplayer(Player)) 162 else: 163 super().handlemessage(msg) 164 165 def _flash_base(self, team: Team, length: float = 2.0) -> None: 166 light = ba.newnode('light', 167 attrs={ 168 'position': team.base_pos, 169 'height_attenuated': False, 170 'radius': 0.3, 171 'color': team.color 172 }) 173 ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 174 ba.timer(length, light.delete) 175 176 def _handle_base_collide(self, team: Team) -> None: 177 try: 178 spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) 179 except ba.NotFoundError: 180 return 181 182 if not spaz.is_alive(): 183 return 184 185 try: 186 player = spaz.getplayer(Player, True) 187 except ba.NotFoundError: 188 return 189 190 # If its another team's player, they scored. 191 player_team = player.team 192 if player_team is not team: 193 194 # Prevent multiple simultaneous scores. 195 if ba.time() != self._last_score_time: 196 self._last_score_time = ba.time() 197 self.stats.player_scored(player, 50, big_message=True) 198 ba.playsound(self._score_sound) 199 self._flash_base(team) 200 201 # Move all players on the scoring team back to their start 202 # and add flashes of light so its noticeable. 203 for player in player_team.players: 204 if player.is_alive(): 205 pos = player.node.position 206 light = ba.newnode('light', 207 attrs={ 208 'position': pos, 209 'color': player_team.color, 210 'height_attenuated': False, 211 'radius': 0.4 212 }) 213 ba.timer(0.5, light.delete) 214 ba.animate(light, 'intensity', { 215 0: 0, 216 0.1: 1.0, 217 0.5: 0 218 }) 219 220 new_pos = (self.map.get_start_position(player_team.id)) 221 light = ba.newnode('light', 222 attrs={ 223 'position': new_pos, 224 'color': player_team.color, 225 'radius': 0.4, 226 'height_attenuated': False 227 }) 228 ba.timer(0.5, light.delete) 229 ba.animate(light, 'intensity', { 230 0: 0, 231 0.1: 1.0, 232 0.5: 0 233 }) 234 if player.actor: 235 player.actor.handlemessage( 236 ba.StandMessage(new_pos, 237 random.uniform(0, 360))) 238 239 # Have teammates celebrate. 240 for player in player_team.players: 241 if player.actor: 242 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 243 244 player_team.score += 1 245 self._update_scoreboard() 246 if player_team.score >= self._score_to_win: 247 self.end_game() 248 249 def end_game(self) -> None: 250 results = ba.GameResults() 251 for team in self.teams: 252 results.set_team_score(team, team.score) 253 self.end(results=results) 254 255 def _update_scoreboard(self) -> None: 256 for team in self.teams: 257 self._scoreboard.set_team_value(team, team.score, 258 self._score_to_win)
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
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 = (ba.MusicType.EPIC if self._epic_mode else 96 ba.MusicType.FORWARD_MARCH) 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('light', 112 attrs={ 113 'position': base_pos, 114 'intensity': 0.6, 115 'height_attenuated': False, 116 'volume_intensity_scale': 0.1, 117 'radius': 0.1, 118 'color': sessionteam.color 119 }) 120 Flag.project_stand(base_pos) 121 flag = Flag(touchable=False, 122 position=base_pos, 123 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 ('call', 'at_connect', ba.Call(self._handle_base_collide, 133 team)), 134 ), 135 ) 136 137 ba.newnode( 138 'region', 139 owner=flag.node, 140 attrs={ 141 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 142 'scale': (0.5, 0.5, 0.5), 143 'type': 'sphere', 144 'materials': [self._base_region_materials[sessionteam.id]] 145 }) 146 147 return team 148 149 def on_team_join(self, team: Team) -> None: 150 # Can't do this in create_team because the team's color/etc. have 151 # not been wired up yet at that point. 152 self._update_scoreboard() 153 154 def on_begin(self) -> None: 155 super().on_begin() 156 self.setup_standard_time_limit(self._time_limit) 157 self.setup_standard_powerup_drops() 158 159 def handlemessage(self, msg: Any) -> Any: 160 if isinstance(msg, ba.PlayerDiedMessage): 161 super().handlemessage(msg) # Augment standard. 162 self.respawn_player(msg.getplayer(Player)) 163 else: 164 super().handlemessage(msg) 165 166 def _flash_base(self, team: Team, length: float = 2.0) -> None: 167 light = ba.newnode('light', 168 attrs={ 169 'position': team.base_pos, 170 'height_attenuated': False, 171 'radius': 0.3, 172 'color': team.color 173 }) 174 ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 175 ba.timer(length, light.delete) 176 177 def _handle_base_collide(self, team: Team) -> None: 178 try: 179 spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True) 180 except ba.NotFoundError: 181 return 182 183 if not spaz.is_alive(): 184 return 185 186 try: 187 player = spaz.getplayer(Player, True) 188 except ba.NotFoundError: 189 return 190 191 # If its another team's player, they scored. 192 player_team = player.team 193 if player_team is not team: 194 195 # Prevent multiple simultaneous scores. 196 if ba.time() != self._last_score_time: 197 self._last_score_time = ba.time() 198 self.stats.player_scored(player, 50, big_message=True) 199 ba.playsound(self._score_sound) 200 self._flash_base(team) 201 202 # Move all players on the scoring team back to their start 203 # and add flashes of light so its noticeable. 204 for player in player_team.players: 205 if player.is_alive(): 206 pos = player.node.position 207 light = ba.newnode('light', 208 attrs={ 209 'position': pos, 210 'color': player_team.color, 211 'height_attenuated': False, 212 'radius': 0.4 213 }) 214 ba.timer(0.5, light.delete) 215 ba.animate(light, 'intensity', { 216 0: 0, 217 0.1: 1.0, 218 0.5: 0 219 }) 220 221 new_pos = (self.map.get_start_position(player_team.id)) 222 light = ba.newnode('light', 223 attrs={ 224 'position': new_pos, 225 'color': player_team.color, 226 'radius': 0.4, 227 'height_attenuated': False 228 }) 229 ba.timer(0.5, light.delete) 230 ba.animate(light, 'intensity', { 231 0: 0, 232 0.1: 1.0, 233 0.5: 0 234 }) 235 if player.actor: 236 player.actor.handlemessage( 237 ba.StandMessage(new_pos, 238 random.uniform(0, 360))) 239 240 # Have teammates celebrate. 241 for player in player_team.players: 242 if player.actor: 243 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 244 245 player_team.score += 1 246 self._update_scoreboard() 247 if player_team.score >= self._score_to_win: 248 self.end_game() 249 250 def end_game(self) -> None: 251 results = ba.GameResults() 252 for team in self.teams: 253 results.set_team_score(team, team.score) 254 self.end(results=results) 255 256 def _update_scoreboard(self) -> None: 257 for team in self.teams: 258 self._scoreboard.set_team_value(team, team.score, 259 self._score_to_win)
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 = (ba.MusicType.EPIC if self._epic_mode else 96 ba.MusicType.FORWARD_MARCH)
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.
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
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.
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
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.
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('light', 112 attrs={ 113 'position': base_pos, 114 'intensity': 0.6, 115 'height_attenuated': False, 116 'volume_intensity_scale': 0.1, 117 'radius': 0.1, 118 'color': sessionteam.color 119 }) 120 Flag.project_stand(base_pos) 121 flag = Flag(touchable=False, 122 position=base_pos, 123 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 ('call', 'at_connect', ba.Call(self._handle_base_collide, 133 team)), 134 ), 135 ) 136 137 ba.newnode( 138 'region', 139 owner=flag.node, 140 attrs={ 141 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 142 'scale': (0.5, 0.5, 0.5), 143 'type': 'sphere', 144 'materials': [self._base_region_materials[sessionteam.id]] 145 }) 146 147 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.
149 def on_team_join(self, team: Team) -> None: 150 # Can't do this in create_team because the team's color/etc. have 151 # not been wired up yet at that point. 152 self._update_scoreboard()
Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
154 def on_begin(self) -> None: 155 super().on_begin() 156 self.setup_standard_time_limit(self._time_limit) 157 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.
159 def handlemessage(self, msg: Any) -> Any: 160 if isinstance(msg, ba.PlayerDiedMessage): 161 super().handlemessage(msg) # Augment standard. 162 self.respawn_player(msg.getplayer(Player)) 163 else: 164 super().handlemessage(msg)
General message handling; can be passed any message object.
250 def end_game(self) -> None: 251 results = ba.GameResults() 252 for team in self.teams: 253 results.set_team_score(team, team.score) 254 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._activity.Activity
- slow_motion
- 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._gameactivity.GameActivity
- default_music
- tips
- scoreconfig
- 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
- 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._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps