bastd.game.deathmatch
DeathMatch game and support classes.
1# Released under the MIT License. See LICENSE for details. 2# 3"""DeathMatch game and support classes.""" 4 5# ba_meta require api 7 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10from typing import TYPE_CHECKING 11 12import ba 13from bastd.actor.playerspaz import PlayerSpaz 14from bastd.actor.scoreboard import Scoreboard 15 16if TYPE_CHECKING: 17 from typing import Any, Sequence 18 19 20class Player(ba.Player['Team']): 21 """Our player type for this game.""" 22 23 24class Team(ba.Team[Player]): 25 """Our team type for this game.""" 26 27 def __init__(self) -> None: 28 self.score = 0 29 30 31# ba_meta export game 32class DeathMatchGame(ba.TeamGameActivity[Player, Team]): 33 """A game type based on acquiring kills.""" 34 35 name = 'Death Match' 36 description = 'Kill a set number of enemies to win.' 37 38 # Print messages when players die since it matters here. 39 announce_player_deaths = True 40 41 @classmethod 42 def get_available_settings( 43 cls, sessiontype: type[ba.Session] 44 ) -> list[ba.Setting]: 45 settings = [ 46 ba.IntSetting( 47 'Kills to Win Per Player', 48 min_value=1, 49 default=5, 50 increment=1, 51 ), 52 ba.IntChoiceSetting( 53 'Time Limit', 54 choices=[ 55 ('None', 0), 56 ('1 Minute', 60), 57 ('2 Minutes', 120), 58 ('5 Minutes', 300), 59 ('10 Minutes', 600), 60 ('20 Minutes', 1200), 61 ], 62 default=0, 63 ), 64 ba.FloatChoiceSetting( 65 'Respawn Times', 66 choices=[ 67 ('Shorter', 0.25), 68 ('Short', 0.5), 69 ('Normal', 1.0), 70 ('Long', 2.0), 71 ('Longer', 4.0), 72 ], 73 default=1.0, 74 ), 75 ba.BoolSetting('Epic Mode', default=False), 76 ] 77 78 # In teams mode, a suicide gives a point to the other team, but in 79 # free-for-all it subtracts from your own score. By default we clamp 80 # this at zero to benefit new players, but pro players might like to 81 # be able to go negative. (to avoid a strategy of just 82 # suiciding until you get a good drop) 83 if issubclass(sessiontype, ba.FreeForAllSession): 84 settings.append( 85 ba.BoolSetting('Allow Negative Scores', default=False) 86 ) 87 88 return settings 89 90 @classmethod 91 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 92 return issubclass(sessiontype, ba.DualTeamSession) or issubclass( 93 sessiontype, ba.FreeForAllSession 94 ) 95 96 @classmethod 97 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 98 return ba.getmaps('melee') 99 100 def __init__(self, settings: dict): 101 super().__init__(settings) 102 self._scoreboard = Scoreboard() 103 self._score_to_win: int | None = None 104 self._dingsound = ba.getsound('dingSmall') 105 self._epic_mode = bool(settings['Epic Mode']) 106 self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) 107 self._time_limit = float(settings['Time Limit']) 108 self._allow_negative_scores = bool( 109 settings.get('Allow Negative Scores', False) 110 ) 111 112 # Base class overrides. 113 self.slow_motion = self._epic_mode 114 self.default_music = ( 115 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH 116 ) 117 118 def get_instance_description(self) -> str | Sequence: 119 return 'Crush ${ARG1} of your enemies.', self._score_to_win 120 121 def get_instance_description_short(self) -> str | Sequence: 122 return 'kill ${ARG1} enemies', self._score_to_win 123 124 def on_team_join(self, team: Team) -> None: 125 if self.has_begun(): 126 self._update_scoreboard() 127 128 def on_begin(self) -> None: 129 super().on_begin() 130 self.setup_standard_time_limit(self._time_limit) 131 self.setup_standard_powerup_drops() 132 133 # Base kills needed to win on the size of the largest team. 134 self._score_to_win = self._kills_to_win_per_player * max( 135 1, max(len(t.players) for t in self.teams) 136 ) 137 self._update_scoreboard() 138 139 def handlemessage(self, msg: Any) -> Any: 140 141 if isinstance(msg, ba.PlayerDiedMessage): 142 143 # Augment standard behavior. 144 super().handlemessage(msg) 145 146 player = msg.getplayer(Player) 147 self.respawn_player(player) 148 149 killer = msg.getkillerplayer(Player) 150 if killer is None: 151 return None 152 153 # Handle team-kills. 154 if killer.team is player.team: 155 156 # In free-for-all, killing yourself loses you a point. 157 if isinstance(self.session, ba.FreeForAllSession): 158 new_score = player.team.score - 1 159 if not self._allow_negative_scores: 160 new_score = max(0, new_score) 161 player.team.score = new_score 162 163 # In teams-mode it gives a point to the other team. 164 else: 165 ba.playsound(self._dingsound) 166 for team in self.teams: 167 if team is not killer.team: 168 team.score += 1 169 170 # Killing someone on another team nets a kill. 171 else: 172 killer.team.score += 1 173 ba.playsound(self._dingsound) 174 175 # In FFA show scores since its hard to find on the scoreboard. 176 if isinstance(killer.actor, PlayerSpaz) and killer.actor: 177 killer.actor.set_score_text( 178 str(killer.team.score) + '/' + str(self._score_to_win), 179 color=killer.team.color, 180 flash=True, 181 ) 182 183 self._update_scoreboard() 184 185 # If someone has won, set a timer to end shortly. 186 # (allows the dust to clear and draws to occur if deaths are 187 # close enough) 188 assert self._score_to_win is not None 189 if any(team.score >= self._score_to_win for team in self.teams): 190 ba.timer(0.5, self.end_game) 191 192 else: 193 return super().handlemessage(msg) 194 return None 195 196 def _update_scoreboard(self) -> None: 197 for team in self.teams: 198 self._scoreboard.set_team_value( 199 team, team.score, self._score_to_win 200 ) 201 202 def end_game(self) -> None: 203 results = ba.GameResults() 204 for team in self.teams: 205 results.set_team_score(team, team.score) 206 self.end(results=results)
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
25class Team(ba.Team[Player]): 26 """Our team type for this game.""" 27 28 def __init__(self) -> None: 29 self.score = 0
Our team type for this game.
Inherited Members
- ba._team.Team
- manual_init
- customdata
- on_expire
- sessionteam
33class DeathMatchGame(ba.TeamGameActivity[Player, Team]): 34 """A game type based on acquiring kills.""" 35 36 name = 'Death Match' 37 description = 'Kill a set number of enemies to win.' 38 39 # Print messages when players die since it matters here. 40 announce_player_deaths = True 41 42 @classmethod 43 def get_available_settings( 44 cls, sessiontype: type[ba.Session] 45 ) -> list[ba.Setting]: 46 settings = [ 47 ba.IntSetting( 48 'Kills to Win Per Player', 49 min_value=1, 50 default=5, 51 increment=1, 52 ), 53 ba.IntChoiceSetting( 54 'Time Limit', 55 choices=[ 56 ('None', 0), 57 ('1 Minute', 60), 58 ('2 Minutes', 120), 59 ('5 Minutes', 300), 60 ('10 Minutes', 600), 61 ('20 Minutes', 1200), 62 ], 63 default=0, 64 ), 65 ba.FloatChoiceSetting( 66 'Respawn Times', 67 choices=[ 68 ('Shorter', 0.25), 69 ('Short', 0.5), 70 ('Normal', 1.0), 71 ('Long', 2.0), 72 ('Longer', 4.0), 73 ], 74 default=1.0, 75 ), 76 ba.BoolSetting('Epic Mode', default=False), 77 ] 78 79 # In teams mode, a suicide gives a point to the other team, but in 80 # free-for-all it subtracts from your own score. By default we clamp 81 # this at zero to benefit new players, but pro players might like to 82 # be able to go negative. (to avoid a strategy of just 83 # suiciding until you get a good drop) 84 if issubclass(sessiontype, ba.FreeForAllSession): 85 settings.append( 86 ba.BoolSetting('Allow Negative Scores', default=False) 87 ) 88 89 return settings 90 91 @classmethod 92 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 93 return issubclass(sessiontype, ba.DualTeamSession) or issubclass( 94 sessiontype, ba.FreeForAllSession 95 ) 96 97 @classmethod 98 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 99 return ba.getmaps('melee') 100 101 def __init__(self, settings: dict): 102 super().__init__(settings) 103 self._scoreboard = Scoreboard() 104 self._score_to_win: int | None = None 105 self._dingsound = ba.getsound('dingSmall') 106 self._epic_mode = bool(settings['Epic Mode']) 107 self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) 108 self._time_limit = float(settings['Time Limit']) 109 self._allow_negative_scores = bool( 110 settings.get('Allow Negative Scores', False) 111 ) 112 113 # Base class overrides. 114 self.slow_motion = self._epic_mode 115 self.default_music = ( 116 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH 117 ) 118 119 def get_instance_description(self) -> str | Sequence: 120 return 'Crush ${ARG1} of your enemies.', self._score_to_win 121 122 def get_instance_description_short(self) -> str | Sequence: 123 return 'kill ${ARG1} enemies', self._score_to_win 124 125 def on_team_join(self, team: Team) -> None: 126 if self.has_begun(): 127 self._update_scoreboard() 128 129 def on_begin(self) -> None: 130 super().on_begin() 131 self.setup_standard_time_limit(self._time_limit) 132 self.setup_standard_powerup_drops() 133 134 # Base kills needed to win on the size of the largest team. 135 self._score_to_win = self._kills_to_win_per_player * max( 136 1, max(len(t.players) for t in self.teams) 137 ) 138 self._update_scoreboard() 139 140 def handlemessage(self, msg: Any) -> Any: 141 142 if isinstance(msg, ba.PlayerDiedMessage): 143 144 # Augment standard behavior. 145 super().handlemessage(msg) 146 147 player = msg.getplayer(Player) 148 self.respawn_player(player) 149 150 killer = msg.getkillerplayer(Player) 151 if killer is None: 152 return None 153 154 # Handle team-kills. 155 if killer.team is player.team: 156 157 # In free-for-all, killing yourself loses you a point. 158 if isinstance(self.session, ba.FreeForAllSession): 159 new_score = player.team.score - 1 160 if not self._allow_negative_scores: 161 new_score = max(0, new_score) 162 player.team.score = new_score 163 164 # In teams-mode it gives a point to the other team. 165 else: 166 ba.playsound(self._dingsound) 167 for team in self.teams: 168 if team is not killer.team: 169 team.score += 1 170 171 # Killing someone on another team nets a kill. 172 else: 173 killer.team.score += 1 174 ba.playsound(self._dingsound) 175 176 # In FFA show scores since its hard to find on the scoreboard. 177 if isinstance(killer.actor, PlayerSpaz) and killer.actor: 178 killer.actor.set_score_text( 179 str(killer.team.score) + '/' + str(self._score_to_win), 180 color=killer.team.color, 181 flash=True, 182 ) 183 184 self._update_scoreboard() 185 186 # If someone has won, set a timer to end shortly. 187 # (allows the dust to clear and draws to occur if deaths are 188 # close enough) 189 assert self._score_to_win is not None 190 if any(team.score >= self._score_to_win for team in self.teams): 191 ba.timer(0.5, self.end_game) 192 193 else: 194 return super().handlemessage(msg) 195 return None 196 197 def _update_scoreboard(self) -> None: 198 for team in self.teams: 199 self._scoreboard.set_team_value( 200 team, team.score, self._score_to_win 201 ) 202 203 def end_game(self) -> None: 204 results = ba.GameResults() 205 for team in self.teams: 206 results.set_team_score(team, team.score) 207 self.end(results=results)
A game type based on acquiring kills.
101 def __init__(self, settings: dict): 102 super().__init__(settings) 103 self._scoreboard = Scoreboard() 104 self._score_to_win: int | None = None 105 self._dingsound = ba.getsound('dingSmall') 106 self._epic_mode = bool(settings['Epic Mode']) 107 self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) 108 self._time_limit = float(settings['Time Limit']) 109 self._allow_negative_scores = bool( 110 settings.get('Allow Negative Scores', False) 111 ) 112 113 # Base class overrides. 114 self.slow_motion = self._epic_mode 115 self.default_music = ( 116 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH 117 )
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.
42 @classmethod 43 def get_available_settings( 44 cls, sessiontype: type[ba.Session] 45 ) -> list[ba.Setting]: 46 settings = [ 47 ba.IntSetting( 48 'Kills to Win Per Player', 49 min_value=1, 50 default=5, 51 increment=1, 52 ), 53 ba.IntChoiceSetting( 54 'Time Limit', 55 choices=[ 56 ('None', 0), 57 ('1 Minute', 60), 58 ('2 Minutes', 120), 59 ('5 Minutes', 300), 60 ('10 Minutes', 600), 61 ('20 Minutes', 1200), 62 ], 63 default=0, 64 ), 65 ba.FloatChoiceSetting( 66 'Respawn Times', 67 choices=[ 68 ('Shorter', 0.25), 69 ('Short', 0.5), 70 ('Normal', 1.0), 71 ('Long', 2.0), 72 ('Longer', 4.0), 73 ], 74 default=1.0, 75 ), 76 ba.BoolSetting('Epic Mode', default=False), 77 ] 78 79 # In teams mode, a suicide gives a point to the other team, but in 80 # free-for-all it subtracts from your own score. By default we clamp 81 # this at zero to benefit new players, but pro players might like to 82 # be able to go negative. (to avoid a strategy of just 83 # suiciding until you get a good drop) 84 if issubclass(sessiontype, ba.FreeForAllSession): 85 settings.append( 86 ba.BoolSetting('Allow Negative Scores', default=False) 87 ) 88 89 return settings
Return a list of settings relevant to this game type when running under the provided session type.
91 @classmethod 92 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 93 return issubclass(sessiontype, ba.DualTeamSession) or issubclass( 94 sessiontype, ba.FreeForAllSession 95 )
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
97 @classmethod 98 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 99 return ba.getmaps('melee')
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.
119 def get_instance_description(self) -> str | Sequence: 120 return 'Crush ${ARG1} of your enemies.', 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.
122 def get_instance_description_short(self) -> str | Sequence: 123 return 'kill ${ARG1} enemies', 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.
125 def on_team_join(self, team: Team) -> None: 126 if self.has_begun(): 127 self._update_scoreboard()
Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
129 def on_begin(self) -> None: 130 super().on_begin() 131 self.setup_standard_time_limit(self._time_limit) 132 self.setup_standard_powerup_drops() 133 134 # Base kills needed to win on the size of the largest team. 135 self._score_to_win = self._kills_to_win_per_player * max( 136 1, max(len(t.players) for t in self.teams) 137 ) 138 self._update_scoreboard()
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.
140 def handlemessage(self, msg: Any) -> Any: 141 142 if isinstance(msg, ba.PlayerDiedMessage): 143 144 # Augment standard behavior. 145 super().handlemessage(msg) 146 147 player = msg.getplayer(Player) 148 self.respawn_player(player) 149 150 killer = msg.getkillerplayer(Player) 151 if killer is None: 152 return None 153 154 # Handle team-kills. 155 if killer.team is player.team: 156 157 # In free-for-all, killing yourself loses you a point. 158 if isinstance(self.session, ba.FreeForAllSession): 159 new_score = player.team.score - 1 160 if not self._allow_negative_scores: 161 new_score = max(0, new_score) 162 player.team.score = new_score 163 164 # In teams-mode it gives a point to the other team. 165 else: 166 ba.playsound(self._dingsound) 167 for team in self.teams: 168 if team is not killer.team: 169 team.score += 1 170 171 # Killing someone on another team nets a kill. 172 else: 173 killer.team.score += 1 174 ba.playsound(self._dingsound) 175 176 # In FFA show scores since its hard to find on the scoreboard. 177 if isinstance(killer.actor, PlayerSpaz) and killer.actor: 178 killer.actor.set_score_text( 179 str(killer.team.score) + '/' + str(self._score_to_win), 180 color=killer.team.color, 181 flash=True, 182 ) 183 184 self._update_scoreboard() 185 186 # If someone has won, set a timer to end shortly. 187 # (allows the dust to clear and draws to occur if deaths are 188 # close enough) 189 assert self._score_to_win is not None 190 if any(team.score >= self._score_to_win for team in self.teams): 191 ba.timer(0.5, self.end_game) 192 193 else: 194 return super().handlemessage(msg) 195 return None
General message handling; can be passed any message object.
203 def end_game(self) -> None: 204 results = ba.GameResults() 205 for team in self.teams: 206 results.set_team_score(team, team.score) 207 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_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
- 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
- create_team
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps