bascenev1lib.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 8 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10from typing import TYPE_CHECKING 11 12from bascenev1lib.actor.playerspaz import PlayerSpaz 13from bascenev1lib.actor.scoreboard import Scoreboard 14import bascenev1 as bs 15 16if TYPE_CHECKING: 17 from typing import Any, Sequence 18 19 20class Player(bs.Player['Team']): 21 """Our player type for this game.""" 22 23 24class Team(bs.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 bascenev1.GameActivity 32class DeathMatchGame(bs.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[bs.Session] 44 ) -> list[bs.Setting]: 45 settings = [ 46 bs.IntSetting( 47 'Kills to Win Per Player', 48 min_value=1, 49 default=5, 50 increment=1, 51 ), 52 bs.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 bs.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 bs.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, bs.FreeForAllSession): 84 settings.append( 85 bs.BoolSetting('Allow Negative Scores', default=False) 86 ) 87 88 return settings 89 90 @classmethod 91 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 92 return issubclass(sessiontype, bs.DualTeamSession) or issubclass( 93 sessiontype, bs.FreeForAllSession 94 ) 95 96 @classmethod 97 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 98 assert bs.app.classic is not None 99 return bs.app.classic.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 = bs.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 bs.MusicType.EPIC if self._epic_mode else bs.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), default=0) 137 ) 138 self._update_scoreboard() 139 140 def handlemessage(self, msg: Any) -> Any: 141 if isinstance(msg, bs.PlayerDiedMessage): 142 # Augment standard behavior. 143 super().handlemessage(msg) 144 145 player = msg.getplayer(Player) 146 self.respawn_player(player) 147 148 killer = msg.getkillerplayer(Player) 149 if killer is None: 150 return None 151 152 # Handle team-kills. 153 if killer.team is player.team: 154 # In free-for-all, killing yourself loses you a point. 155 if isinstance(self.session, bs.FreeForAllSession): 156 new_score = player.team.score - 1 157 if not self._allow_negative_scores: 158 new_score = max(0, new_score) 159 player.team.score = new_score 160 161 # In teams-mode it gives a point to the other team. 162 else: 163 self._dingsound.play() 164 for team in self.teams: 165 if team is not killer.team: 166 team.score += 1 167 168 # Killing someone on another team nets a kill. 169 else: 170 killer.team.score += 1 171 self._dingsound.play() 172 173 # In FFA show scores since its hard to find on the scoreboard. 174 if isinstance(killer.actor, PlayerSpaz) and killer.actor: 175 killer.actor.set_score_text( 176 str(killer.team.score) + '/' + str(self._score_to_win), 177 color=killer.team.color, 178 flash=True, 179 ) 180 181 self._update_scoreboard() 182 183 # If someone has won, set a timer to end shortly. 184 # (allows the dust to clear and draws to occur if deaths are 185 # close enough) 186 assert self._score_to_win is not None 187 if any(team.score >= self._score_to_win for team in self.teams): 188 bs.timer(0.5, self.end_game) 189 190 else: 191 return super().handlemessage(msg) 192 return None 193 194 def _update_scoreboard(self) -> None: 195 for team in self.teams: 196 self._scoreboard.set_team_value( 197 team, team.score, self._score_to_win 198 ) 199 200 def end_game(self) -> None: 201 results = bs.GameResults() 202 for team in self.teams: 203 results.set_team_score(team, team.score) 204 self.end(results=results)
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
25class Team(bs.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
- bascenev1._team.Team
- players
- id
- name
- color
- manual_init
- customdata
- on_expire
- sessionteam
33class DeathMatchGame(bs.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[bs.Session] 45 ) -> list[bs.Setting]: 46 settings = [ 47 bs.IntSetting( 48 'Kills to Win Per Player', 49 min_value=1, 50 default=5, 51 increment=1, 52 ), 53 bs.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 bs.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 bs.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, bs.FreeForAllSession): 85 settings.append( 86 bs.BoolSetting('Allow Negative Scores', default=False) 87 ) 88 89 return settings 90 91 @classmethod 92 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 93 return issubclass(sessiontype, bs.DualTeamSession) or issubclass( 94 sessiontype, bs.FreeForAllSession 95 ) 96 97 @classmethod 98 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 99 assert bs.app.classic is not None 100 return bs.app.classic.getmaps('melee') 101 102 def __init__(self, settings: dict): 103 super().__init__(settings) 104 self._scoreboard = Scoreboard() 105 self._score_to_win: int | None = None 106 self._dingsound = bs.getsound('dingSmall') 107 self._epic_mode = bool(settings['Epic Mode']) 108 self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) 109 self._time_limit = float(settings['Time Limit']) 110 self._allow_negative_scores = bool( 111 settings.get('Allow Negative Scores', False) 112 ) 113 114 # Base class overrides. 115 self.slow_motion = self._epic_mode 116 self.default_music = ( 117 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH 118 ) 119 120 def get_instance_description(self) -> str | Sequence: 121 return 'Crush ${ARG1} of your enemies.', self._score_to_win 122 123 def get_instance_description_short(self) -> str | Sequence: 124 return 'kill ${ARG1} enemies', self._score_to_win 125 126 def on_team_join(self, team: Team) -> None: 127 if self.has_begun(): 128 self._update_scoreboard() 129 130 def on_begin(self) -> None: 131 super().on_begin() 132 self.setup_standard_time_limit(self._time_limit) 133 self.setup_standard_powerup_drops() 134 135 # Base kills needed to win on the size of the largest team. 136 self._score_to_win = self._kills_to_win_per_player * max( 137 1, max((len(t.players) for t in self.teams), default=0) 138 ) 139 self._update_scoreboard() 140 141 def handlemessage(self, msg: Any) -> Any: 142 if isinstance(msg, bs.PlayerDiedMessage): 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 # In free-for-all, killing yourself loses you a point. 156 if isinstance(self.session, bs.FreeForAllSession): 157 new_score = player.team.score - 1 158 if not self._allow_negative_scores: 159 new_score = max(0, new_score) 160 player.team.score = new_score 161 162 # In teams-mode it gives a point to the other team. 163 else: 164 self._dingsound.play() 165 for team in self.teams: 166 if team is not killer.team: 167 team.score += 1 168 169 # Killing someone on another team nets a kill. 170 else: 171 killer.team.score += 1 172 self._dingsound.play() 173 174 # In FFA show scores since its hard to find on the scoreboard. 175 if isinstance(killer.actor, PlayerSpaz) and killer.actor: 176 killer.actor.set_score_text( 177 str(killer.team.score) + '/' + str(self._score_to_win), 178 color=killer.team.color, 179 flash=True, 180 ) 181 182 self._update_scoreboard() 183 184 # If someone has won, set a timer to end shortly. 185 # (allows the dust to clear and draws to occur if deaths are 186 # close enough) 187 assert self._score_to_win is not None 188 if any(team.score >= self._score_to_win for team in self.teams): 189 bs.timer(0.5, self.end_game) 190 191 else: 192 return super().handlemessage(msg) 193 return None 194 195 def _update_scoreboard(self) -> None: 196 for team in self.teams: 197 self._scoreboard.set_team_value( 198 team, team.score, self._score_to_win 199 ) 200 201 def end_game(self) -> None: 202 results = bs.GameResults() 203 for team in self.teams: 204 results.set_team_score(team, team.score) 205 self.end(results=results)
A game type based on acquiring kills.
102 def __init__(self, settings: dict): 103 super().__init__(settings) 104 self._scoreboard = Scoreboard() 105 self._score_to_win: int | None = None 106 self._dingsound = bs.getsound('dingSmall') 107 self._epic_mode = bool(settings['Epic Mode']) 108 self._kills_to_win_per_player = int(settings['Kills to Win Per Player']) 109 self._time_limit = float(settings['Time Limit']) 110 self._allow_negative_scores = bool( 111 settings.get('Allow Negative Scores', False) 112 ) 113 114 # Base class overrides. 115 self.slow_motion = self._epic_mode 116 self.default_music = ( 117 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.TO_THE_DEATH 118 )
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[bs.Session] 45 ) -> list[bs.Setting]: 46 settings = [ 47 bs.IntSetting( 48 'Kills to Win Per Player', 49 min_value=1, 50 default=5, 51 increment=1, 52 ), 53 bs.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 bs.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 bs.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, bs.FreeForAllSession): 85 settings.append( 86 bs.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[bs.Session]) -> bool: 93 return issubclass(sessiontype, bs.DualTeamSession) or issubclass( 94 sessiontype, bs.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[bs.Session]) -> list[str]: 99 assert bs.app.classic is not None 100 return bs.app.classic.getmaps('melee')
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.
120 def get_instance_description(self) -> str | Sequence: 121 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.
123 def get_instance_description_short(self) -> str | Sequence: 124 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.
126 def on_team_join(self, team: Team) -> None: 127 if self.has_begun(): 128 self._update_scoreboard()
Called when a new bascenev1.Team joins the Activity.
(including the initial set of Teams)
130 def on_begin(self) -> None: 131 super().on_begin() 132 self.setup_standard_time_limit(self._time_limit) 133 self.setup_standard_powerup_drops() 134 135 # Base kills needed to win on the size of the largest team. 136 self._score_to_win = self._kills_to_win_per_player * max( 137 1, max((len(t.players) for t in self.teams), default=0) 138 ) 139 self._update_scoreboard()
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.
141 def handlemessage(self, msg: Any) -> Any: 142 if isinstance(msg, bs.PlayerDiedMessage): 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 # In free-for-all, killing yourself loses you a point. 156 if isinstance(self.session, bs.FreeForAllSession): 157 new_score = player.team.score - 1 158 if not self._allow_negative_scores: 159 new_score = max(0, new_score) 160 player.team.score = new_score 161 162 # In teams-mode it gives a point to the other team. 163 else: 164 self._dingsound.play() 165 for team in self.teams: 166 if team is not killer.team: 167 team.score += 1 168 169 # Killing someone on another team nets a kill. 170 else: 171 killer.team.score += 1 172 self._dingsound.play() 173 174 # In FFA show scores since its hard to find on the scoreboard. 175 if isinstance(killer.actor, PlayerSpaz) and killer.actor: 176 killer.actor.set_score_text( 177 str(killer.team.score) + '/' + str(self._score_to_win), 178 color=killer.team.color, 179 flash=True, 180 ) 181 182 self._update_scoreboard() 183 184 # If someone has won, set a timer to end shortly. 185 # (allows the dust to clear and draws to occur if deaths are 186 # close enough) 187 assert self._score_to_win is not None 188 if any(team.score >= self._score_to_win for team in self.teams): 189 bs.timer(0.5, self.end_game) 190 191 else: 192 return super().handlemessage(msg) 193 return None
General message handling; can be passed any message object.
201 def end_game(self) -> None: 202 results = bs.GameResults() 203 for team in self.teams: 204 results.set_team_score(team, team.score) 205 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
- 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
- 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
- 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
- create_team
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps