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