bascenev1lib.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 8 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING 12 13from bascenev1lib.actor.playerspaz import PlayerSpaz 14from bascenev1lib.actor.flag import Flag 15from bascenev1lib.actor.scoreboard import Scoreboard 16from bascenev1lib.gameutils import SharedObjects 17import bascenev1 as bs 18 19if TYPE_CHECKING: 20 from typing import Any, Sequence 21 22 23class Player(bs.Player['Team']): 24 """Our player type for this game.""" 25 26 27class Team(bs.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 bascenev1.GameActivity 37class AssaultGame(bs.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 bs.IntSetting( 44 'Score to Win', 45 min_value=1, 46 default=3, 47 ), 48 bs.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 bs.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 bs.BoolSetting('Epic Mode', default=False), 72 ] 73 74 @classmethod 75 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 76 return issubclass(sessiontype, bs.DualTeamSession) 77 78 @classmethod 79 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 80 assert bs.app.classic is not None 81 return bs.app.classic.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 = bs.getsound('score') 88 self._base_region_materials: dict[int, bs.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 bs.MusicType.EPIC if self._epic_mode else bs.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: bs.SessionTeam) -> Team: 110 shared = SharedObjects.get() 111 base_pos = self.map.get_flag_position(sessionteam.id) 112 bs.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] = bs.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 bs.Call(self._handle_base_collide, team), 137 ), 138 ), 139 ) 140 141 bs.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, bs.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 = bs.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 bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 182 bs.timer(length, light.delete) 183 184 def _handle_base_collide(self, team: Team) -> None: 185 try: 186 spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) 187 except bs.NotFoundError: 188 return 189 190 if not spaz.is_alive(): 191 return 192 193 try: 194 player = spaz.getplayer(Player, True) 195 except bs.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 # Prevent multiple simultaneous scores. 202 if bs.time() != self._last_score_time: 203 self._last_score_time = bs.time() 204 self.stats.player_scored(player, 50, big_message=True) 205 self._score_sound.play() 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 = bs.newnode( 214 'light', 215 attrs={ 216 'position': pos, 217 'color': player_team.color, 218 'height_attenuated': False, 219 'radius': 0.4, 220 }, 221 ) 222 bs.timer(0.5, light.delete) 223 bs.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 = bs.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 bs.timer(0.5, light.delete) 236 bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) 237 if player.actor: 238 player.actor.handlemessage( 239 bs.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(bs.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 = bs.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
- bascenev1._player.Player
- character
- actor
- color
- highlight
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
28class Team(bs.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
- bascenev1._team.Team
- players
- id
- name
- color
- manual_init
- customdata
- on_expire
- sessionteam
38class AssaultGame(bs.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 bs.IntSetting( 45 'Score to Win', 46 min_value=1, 47 default=3, 48 ), 49 bs.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 bs.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 bs.BoolSetting('Epic Mode', default=False), 73 ] 74 75 @classmethod 76 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 77 return issubclass(sessiontype, bs.DualTeamSession) 78 79 @classmethod 80 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 81 assert bs.app.classic is not None 82 return bs.app.classic.getmaps('team_flag') 83 84 def __init__(self, settings: dict): 85 super().__init__(settings) 86 self._scoreboard = Scoreboard() 87 self._last_score_time = 0.0 88 self._score_sound = bs.getsound('score') 89 self._base_region_materials: dict[int, bs.Material] = {} 90 self._epic_mode = bool(settings['Epic Mode']) 91 self._score_to_win = int(settings['Score to Win']) 92 self._time_limit = float(settings['Time Limit']) 93 94 # Base class overrides 95 self.slow_motion = self._epic_mode 96 self.default_music = ( 97 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH 98 ) 99 100 def get_instance_description(self) -> str | Sequence: 101 if self._score_to_win == 1: 102 return 'Touch the enemy flag.' 103 return 'Touch the enemy flag ${ARG1} times.', self._score_to_win 104 105 def get_instance_description_short(self) -> str | Sequence: 106 if self._score_to_win == 1: 107 return 'touch 1 flag' 108 return 'touch ${ARG1} flags', self._score_to_win 109 110 def create_team(self, sessionteam: bs.SessionTeam) -> Team: 111 shared = SharedObjects.get() 112 base_pos = self.map.get_flag_position(sessionteam.id) 113 bs.newnode( 114 'light', 115 attrs={ 116 'position': base_pos, 117 'intensity': 0.6, 118 'height_attenuated': False, 119 'volume_intensity_scale': 0.1, 120 'radius': 0.1, 121 'color': sessionteam.color, 122 }, 123 ) 124 Flag.project_stand(base_pos) 125 flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) 126 team = Team(base_pos=base_pos, flag=flag) 127 128 mat = self._base_region_materials[sessionteam.id] = bs.Material() 129 mat.add_actions( 130 conditions=('they_have_material', shared.player_material), 131 actions=( 132 ('modify_part_collision', 'collide', True), 133 ('modify_part_collision', 'physical', False), 134 ( 135 'call', 136 'at_connect', 137 bs.Call(self._handle_base_collide, team), 138 ), 139 ), 140 ) 141 142 bs.newnode( 143 'region', 144 owner=flag.node, 145 attrs={ 146 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 147 'scale': (0.5, 0.5, 0.5), 148 'type': 'sphere', 149 'materials': [self._base_region_materials[sessionteam.id]], 150 }, 151 ) 152 153 return team 154 155 def on_team_join(self, team: Team) -> None: 156 # Can't do this in create_team because the team's color/etc. have 157 # not been wired up yet at that point. 158 self._update_scoreboard() 159 160 def on_begin(self) -> None: 161 super().on_begin() 162 self.setup_standard_time_limit(self._time_limit) 163 self.setup_standard_powerup_drops() 164 165 def handlemessage(self, msg: Any) -> Any: 166 if isinstance(msg, bs.PlayerDiedMessage): 167 super().handlemessage(msg) # Augment standard. 168 self.respawn_player(msg.getplayer(Player)) 169 else: 170 super().handlemessage(msg) 171 172 def _flash_base(self, team: Team, length: float = 2.0) -> None: 173 light = bs.newnode( 174 'light', 175 attrs={ 176 'position': team.base_pos, 177 'height_attenuated': False, 178 'radius': 0.3, 179 'color': team.color, 180 }, 181 ) 182 bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True) 183 bs.timer(length, light.delete) 184 185 def _handle_base_collide(self, team: Team) -> None: 186 try: 187 spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True) 188 except bs.NotFoundError: 189 return 190 191 if not spaz.is_alive(): 192 return 193 194 try: 195 player = spaz.getplayer(Player, True) 196 except bs.NotFoundError: 197 return 198 199 # If its another team's player, they scored. 200 player_team = player.team 201 if player_team is not team: 202 # Prevent multiple simultaneous scores. 203 if bs.time() != self._last_score_time: 204 self._last_score_time = bs.time() 205 self.stats.player_scored(player, 50, big_message=True) 206 self._score_sound.play() 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 = bs.newnode( 215 'light', 216 attrs={ 217 'position': pos, 218 'color': player_team.color, 219 'height_attenuated': False, 220 'radius': 0.4, 221 }, 222 ) 223 bs.timer(0.5, light.delete) 224 bs.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 = bs.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 bs.timer(0.5, light.delete) 237 bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0}) 238 if player.actor: 239 player.actor.handlemessage( 240 bs.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(bs.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 = bs.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.
84 def __init__(self, settings: dict): 85 super().__init__(settings) 86 self._scoreboard = Scoreboard() 87 self._last_score_time = 0.0 88 self._score_sound = bs.getsound('score') 89 self._base_region_materials: dict[int, bs.Material] = {} 90 self._epic_mode = bool(settings['Epic Mode']) 91 self._score_to_win = int(settings['Score to Win']) 92 self._time_limit = float(settings['Time Limit']) 93 94 # Base class overrides 95 self.slow_motion = self._epic_mode 96 self.default_music = ( 97 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH 98 )
Instantiate the Activity.
75 @classmethod 76 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 77 return issubclass(sessiontype, bs.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
79 @classmethod 80 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 81 assert bs.app.classic is not None 82 return bs.app.classic.getmaps('team_flag')
Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.
100 def get_instance_description(self) -> str | Sequence: 101 if self._score_to_win == 1: 102 return 'Touch the enemy flag.' 103 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.
105 def get_instance_description_short(self) -> str | Sequence: 106 if self._score_to_win == 1: 107 return 'touch 1 flag' 108 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.
110 def create_team(self, sessionteam: bs.SessionTeam) -> Team: 111 shared = SharedObjects.get() 112 base_pos = self.map.get_flag_position(sessionteam.id) 113 bs.newnode( 114 'light', 115 attrs={ 116 'position': base_pos, 117 'intensity': 0.6, 118 'height_attenuated': False, 119 'volume_intensity_scale': 0.1, 120 'radius': 0.1, 121 'color': sessionteam.color, 122 }, 123 ) 124 Flag.project_stand(base_pos) 125 flag = Flag(touchable=False, position=base_pos, color=sessionteam.color) 126 team = Team(base_pos=base_pos, flag=flag) 127 128 mat = self._base_region_materials[sessionteam.id] = bs.Material() 129 mat.add_actions( 130 conditions=('they_have_material', shared.player_material), 131 actions=( 132 ('modify_part_collision', 'collide', True), 133 ('modify_part_collision', 'physical', False), 134 ( 135 'call', 136 'at_connect', 137 bs.Call(self._handle_base_collide, team), 138 ), 139 ), 140 ) 141 142 bs.newnode( 143 'region', 144 owner=flag.node, 145 attrs={ 146 'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]), 147 'scale': (0.5, 0.5, 0.5), 148 'type': 'sphere', 149 'materials': [self._base_region_materials[sessionteam.id]], 150 }, 151 ) 152 153 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.
155 def on_team_join(self, team: Team) -> None: 156 # Can't do this in create_team because the team's color/etc. have 157 # not been wired up yet at that point. 158 self._update_scoreboard()
Called when a new bascenev1.Team joins the Activity.
(including the initial set of Teams)
160 def on_begin(self) -> None: 161 super().on_begin() 162 self.setup_standard_time_limit(self._time_limit) 163 self.setup_standard_powerup_drops()
Called once the previous 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.
165 def handlemessage(self, msg: Any) -> Any: 166 if isinstance(msg, bs.PlayerDiedMessage): 167 super().handlemessage(msg) # Augment standard. 168 self.respawn_player(msg.getplayer(Player)) 169 else: 170 super().handlemessage(msg)
General message handling; can be passed any message object.
253 def end_game(self) -> None: 254 results = bs.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 bascenev1.Activity.end().
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 (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.
Inherited Members
- bascenev1._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- bascenev1._gameactivity.GameActivity
- 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
- initialplayerinfos
- 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
- bascenev1._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
- paused_text
- preloads
- lobby
- context
- 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
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps