bascenev1lib.game.conquest
Provides the Conquest game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides the Conquest game.""" 4 5# ba_meta require api 9 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING, override 12 13import bascenev1 as bs 14 15from bascenev1lib.actor.flag import Flag 16from bascenev1lib.actor.scoreboard import Scoreboard 17from bascenev1lib.actor.playerspaz import PlayerSpaz 18from bascenev1lib.gameutils import SharedObjects 19from bascenev1lib.actor.respawnicon import RespawnIcon 20 21if TYPE_CHECKING: 22 from typing import Any, Sequence 23 24 25class ConquestFlag(Flag): 26 """A custom flag for use with Conquest games.""" 27 28 def __init__(self, *args: Any, **keywds: Any): 29 super().__init__(*args, **keywds) 30 self._team: Team | None = None 31 self.light: bs.Node | None = None 32 33 @property 34 def team(self) -> Team | None: 35 """The team that owns this flag.""" 36 return self._team 37 38 @team.setter 39 def team(self, team: Team) -> None: 40 """Set the team that owns this flag.""" 41 self._team = team 42 43 44class Player(bs.Player['Team']): 45 """Our player type for this game.""" 46 47 # FIXME: We shouldn't be using customdata here 48 # (but need to update respawn funcs accordingly first). 49 @property 50 def respawn_timer(self) -> bs.Timer | None: 51 """Type safe access to standard respawn timer.""" 52 val = self.customdata.get('respawn_timer', None) 53 assert isinstance(val, (bs.Timer, type(None))) 54 return val 55 56 @respawn_timer.setter 57 def respawn_timer(self, value: bs.Timer | None) -> None: 58 self.customdata['respawn_timer'] = value 59 60 @property 61 def respawn_icon(self) -> RespawnIcon | None: 62 """Type safe access to standard respawn icon.""" 63 val = self.customdata.get('respawn_icon', None) 64 assert isinstance(val, (RespawnIcon, type(None))) 65 return val 66 67 @respawn_icon.setter 68 def respawn_icon(self, value: RespawnIcon | None) -> None: 69 self.customdata['respawn_icon'] = value 70 71 72class Team(bs.Team[Player]): 73 """Our team type for this game.""" 74 75 def __init__(self) -> None: 76 self.flags_held = 0 77 78 79# ba_meta export bascenev1.GameActivity 80class ConquestGame(bs.TeamGameActivity[Player, Team]): 81 """A game where teams try to claim all flags on the map.""" 82 83 name = 'Conquest' 84 description = 'Secure all flags on the map to win.' 85 available_settings = [ 86 bs.IntChoiceSetting( 87 'Time Limit', 88 choices=[ 89 ('None', 0), 90 ('1 Minute', 60), 91 ('2 Minutes', 120), 92 ('5 Minutes', 300), 93 ('10 Minutes', 600), 94 ('20 Minutes', 1200), 95 ], 96 default=0, 97 ), 98 bs.FloatChoiceSetting( 99 'Respawn Times', 100 choices=[ 101 ('Shorter', 0.25), 102 ('Short', 0.5), 103 ('Normal', 1.0), 104 ('Long', 2.0), 105 ('Longer', 4.0), 106 ], 107 default=1.0, 108 ), 109 bs.BoolSetting('Epic Mode', default=False), 110 ] 111 112 @override 113 @classmethod 114 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 115 return issubclass(sessiontype, bs.DualTeamSession) 116 117 @override 118 @classmethod 119 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 120 assert bs.app.classic is not None 121 return bs.app.classic.getmaps('conquest') 122 123 def __init__(self, settings: dict): 124 super().__init__(settings) 125 shared = SharedObjects.get() 126 self._scoreboard = Scoreboard() 127 self._score_sound = bs.getsound('score') 128 self._swipsound = bs.getsound('swip') 129 self._extraflagmat = bs.Material() 130 self._flags: list[ConquestFlag] = [] 131 self._epic_mode = bool(settings['Epic Mode']) 132 self._time_limit = float(settings['Time Limit']) 133 134 # Base class overrides. 135 self.slow_motion = self._epic_mode 136 self.default_music = ( 137 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.GRAND_ROMP 138 ) 139 140 # We want flags to tell us they've been hit but not react physically. 141 self._extraflagmat.add_actions( 142 conditions=('they_have_material', shared.player_material), 143 actions=( 144 ('modify_part_collision', 'collide', True), 145 ('call', 'at_connect', self._handle_flag_player_collide), 146 ), 147 ) 148 149 @override 150 def get_instance_description(self) -> str | Sequence: 151 return 'Secure all ${ARG1} flags.', len(self.map.flag_points) 152 153 @override 154 def get_instance_description_short(self) -> str | Sequence: 155 return 'secure all ${ARG1} flags', len(self.map.flag_points) 156 157 @override 158 def on_team_join(self, team: Team) -> None: 159 if self.has_begun(): 160 self._update_scores() 161 162 @override 163 def on_player_join(self, player: Player) -> None: 164 player.respawn_timer = None 165 166 # Only spawn if this player's team has a flag currently. 167 if player.team.flags_held > 0: 168 self.spawn_player(player) 169 170 @override 171 def on_begin(self) -> None: 172 super().on_begin() 173 self.setup_standard_time_limit(self._time_limit) 174 self.setup_standard_powerup_drops() 175 176 # Set up flags with marker lights. 177 for i, flag_point in enumerate(self.map.flag_points): 178 point = flag_point 179 flag = ConquestFlag( 180 position=point, touchable=False, materials=[self._extraflagmat] 181 ) 182 self._flags.append(flag) 183 Flag.project_stand(point) 184 flag.light = bs.newnode( 185 'light', 186 owner=flag.node, 187 attrs={ 188 'position': point, 189 'intensity': 0.25, 190 'height_attenuated': False, 191 'radius': 0.3, 192 'color': (1, 1, 1), 193 }, 194 ) 195 196 # Give teams a flag to start with. 197 for i, team in enumerate(self.teams): 198 self._flags[i].team = team 199 light = self._flags[i].light 200 assert light 201 node = self._flags[i].node 202 assert node 203 light.color = team.color 204 node.color = team.color 205 206 self._update_scores() 207 208 # Initial joiners didn't spawn due to no flags being owned yet; 209 # spawn them now. 210 for player in self.players: 211 self.spawn_player(player) 212 213 def _update_scores(self) -> None: 214 for team in self.teams: 215 team.flags_held = 0 216 for flag in self._flags: 217 if flag.team is not None: 218 flag.team.flags_held += 1 219 for team in self.teams: 220 # If a team finds themselves with no flags, cancel all 221 # outstanding spawn-timers. 222 if team.flags_held == 0: 223 for player in team.players: 224 player.respawn_timer = None 225 player.respawn_icon = None 226 if team.flags_held == len(self._flags): 227 self.end_game() 228 self._scoreboard.set_team_value( 229 team, team.flags_held, len(self._flags) 230 ) 231 232 @override 233 def end_game(self) -> None: 234 results = bs.GameResults() 235 for team in self.teams: 236 results.set_team_score(team, team.flags_held) 237 self.end(results=results) 238 239 def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None: 240 assert flag.node 241 assert flag.light 242 light = bs.newnode( 243 'light', 244 attrs={ 245 'position': flag.node.position, 246 'height_attenuated': False, 247 'color': flag.light.color, 248 }, 249 ) 250 bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) 251 bs.timer(length, light.delete) 252 253 def _handle_flag_player_collide(self) -> None: 254 collision = bs.getcollision() 255 try: 256 flag = collision.sourcenode.getdelegate(ConquestFlag, True) 257 player = collision.opposingnode.getdelegate( 258 PlayerSpaz, True 259 ).getplayer(Player, True) 260 except bs.NotFoundError: 261 return 262 assert flag.light 263 264 if flag.team is not player.team: 265 flag.team = player.team 266 flag.light.color = player.team.color 267 flag.node.color = player.team.color 268 self.stats.player_scored(player, 10, screenmessage=False) 269 self._swipsound.play() 270 self._flash_flag(flag) 271 self._update_scores() 272 273 # Respawn any players on this team that were in limbo due to the 274 # lack of a flag for their team. 275 for otherplayer in self.players: 276 if ( 277 otherplayer.team is flag.team 278 and otherplayer.actor is not None 279 and not otherplayer.is_alive() 280 and otherplayer.respawn_timer is None 281 ): 282 self.spawn_player(otherplayer) 283 284 @override 285 def handlemessage(self, msg: Any) -> Any: 286 if isinstance(msg, bs.PlayerDiedMessage): 287 # Augment standard behavior. 288 super().handlemessage(msg) 289 290 # Respawn only if this team has a flag. 291 player = msg.getplayer(Player) 292 if player.team.flags_held > 0: 293 self.respawn_player(player) 294 else: 295 player.respawn_timer = None 296 297 else: 298 super().handlemessage(msg) 299 300 @override 301 def spawn_player(self, player: Player) -> bs.Actor: 302 # We spawn players at different places based on what flags are held. 303 return self.spawn_player_spaz( 304 player, self._get_player_spawn_position(player) 305 ) 306 307 def _get_player_spawn_position(self, player: Player) -> Sequence[float]: 308 # Iterate until we find a spawn owned by this team. 309 spawn_count = len(self.map.spawn_by_flag_points) 310 311 # Get all spawns owned by this team. 312 spawns = [ 313 i for i in range(spawn_count) if self._flags[i].team is player.team 314 ] 315 316 closest_spawn = 0 317 closest_distance = 9999.0 318 319 # Now find the spawn that's closest to a spawn not owned by us; 320 # we'll use that one. 321 for spawn in spawns: 322 spt = self.map.spawn_by_flag_points[spawn] 323 our_pt = bs.Vec3(spt[0], spt[1], spt[2]) 324 for otherspawn in [ 325 i 326 for i in range(spawn_count) 327 if self._flags[i].team is not player.team 328 ]: 329 spt = self.map.spawn_by_flag_points[otherspawn] 330 their_pt = bs.Vec3(spt[0], spt[1], spt[2]) 331 dist = (their_pt - our_pt).length() 332 if dist < closest_distance: 333 closest_distance = dist 334 closest_spawn = spawn 335 336 pos = self.map.spawn_by_flag_points[closest_spawn] 337 x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3]) 338 z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5]) 339 pos = ( 340 pos[0] + random.uniform(*x_range), 341 pos[1], 342 pos[2] + random.uniform(*z_range), 343 ) 344 return pos
26class ConquestFlag(Flag): 27 """A custom flag for use with Conquest games.""" 28 29 def __init__(self, *args: Any, **keywds: Any): 30 super().__init__(*args, **keywds) 31 self._team: Team | None = None 32 self.light: bs.Node | None = None 33 34 @property 35 def team(self) -> Team | None: 36 """The team that owns this flag.""" 37 return self._team 38 39 @team.setter 40 def team(self, team: Team) -> None: 41 """Set the team that owns this flag.""" 42 self._team = team
A custom flag for use with Conquest games.
29 def __init__(self, *args: Any, **keywds: Any): 30 super().__init__(*args, **keywds) 31 self._team: Team | None = None 32 self.light: bs.Node | None = None
Instantiate a flag.
If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.
'materials can be a list of extra bs.Material
s to apply to the flag.
If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.
34 @property 35 def team(self) -> Team | None: 36 """The team that owns this flag.""" 37 return self._team
The team that owns this flag.
Inherited Members
45class Player(bs.Player['Team']): 46 """Our player type for this game.""" 47 48 # FIXME: We shouldn't be using customdata here 49 # (but need to update respawn funcs accordingly first). 50 @property 51 def respawn_timer(self) -> bs.Timer | None: 52 """Type safe access to standard respawn timer.""" 53 val = self.customdata.get('respawn_timer', None) 54 assert isinstance(val, (bs.Timer, type(None))) 55 return val 56 57 @respawn_timer.setter 58 def respawn_timer(self, value: bs.Timer | None) -> None: 59 self.customdata['respawn_timer'] = value 60 61 @property 62 def respawn_icon(self) -> RespawnIcon | None: 63 """Type safe access to standard respawn icon.""" 64 val = self.customdata.get('respawn_icon', None) 65 assert isinstance(val, (RespawnIcon, type(None))) 66 return val 67 68 @respawn_icon.setter 69 def respawn_icon(self, value: RespawnIcon | None) -> None: 70 self.customdata['respawn_icon'] = value
Our player type for this game.
50 @property 51 def respawn_timer(self) -> bs.Timer | None: 52 """Type safe access to standard respawn timer.""" 53 val = self.customdata.get('respawn_timer', None) 54 assert isinstance(val, (bs.Timer, type(None))) 55 return val
Type safe access to standard respawn timer.
61 @property 62 def respawn_icon(self) -> RespawnIcon | None: 63 """Type safe access to standard respawn icon.""" 64 val = self.customdata.get('respawn_icon', None) 65 assert isinstance(val, (RespawnIcon, type(None))) 66 return val
Type safe access to standard respawn icon.
73class Team(bs.Team[Player]): 74 """Our team type for this game.""" 75 76 def __init__(self) -> None: 77 self.flags_held = 0
Our team type for this game.
81class ConquestGame(bs.TeamGameActivity[Player, Team]): 82 """A game where teams try to claim all flags on the map.""" 83 84 name = 'Conquest' 85 description = 'Secure all flags on the map to win.' 86 available_settings = [ 87 bs.IntChoiceSetting( 88 'Time Limit', 89 choices=[ 90 ('None', 0), 91 ('1 Minute', 60), 92 ('2 Minutes', 120), 93 ('5 Minutes', 300), 94 ('10 Minutes', 600), 95 ('20 Minutes', 1200), 96 ], 97 default=0, 98 ), 99 bs.FloatChoiceSetting( 100 'Respawn Times', 101 choices=[ 102 ('Shorter', 0.25), 103 ('Short', 0.5), 104 ('Normal', 1.0), 105 ('Long', 2.0), 106 ('Longer', 4.0), 107 ], 108 default=1.0, 109 ), 110 bs.BoolSetting('Epic Mode', default=False), 111 ] 112 113 @override 114 @classmethod 115 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 116 return issubclass(sessiontype, bs.DualTeamSession) 117 118 @override 119 @classmethod 120 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 121 assert bs.app.classic is not None 122 return bs.app.classic.getmaps('conquest') 123 124 def __init__(self, settings: dict): 125 super().__init__(settings) 126 shared = SharedObjects.get() 127 self._scoreboard = Scoreboard() 128 self._score_sound = bs.getsound('score') 129 self._swipsound = bs.getsound('swip') 130 self._extraflagmat = bs.Material() 131 self._flags: list[ConquestFlag] = [] 132 self._epic_mode = bool(settings['Epic Mode']) 133 self._time_limit = float(settings['Time Limit']) 134 135 # Base class overrides. 136 self.slow_motion = self._epic_mode 137 self.default_music = ( 138 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.GRAND_ROMP 139 ) 140 141 # We want flags to tell us they've been hit but not react physically. 142 self._extraflagmat.add_actions( 143 conditions=('they_have_material', shared.player_material), 144 actions=( 145 ('modify_part_collision', 'collide', True), 146 ('call', 'at_connect', self._handle_flag_player_collide), 147 ), 148 ) 149 150 @override 151 def get_instance_description(self) -> str | Sequence: 152 return 'Secure all ${ARG1} flags.', len(self.map.flag_points) 153 154 @override 155 def get_instance_description_short(self) -> str | Sequence: 156 return 'secure all ${ARG1} flags', len(self.map.flag_points) 157 158 @override 159 def on_team_join(self, team: Team) -> None: 160 if self.has_begun(): 161 self._update_scores() 162 163 @override 164 def on_player_join(self, player: Player) -> None: 165 player.respawn_timer = None 166 167 # Only spawn if this player's team has a flag currently. 168 if player.team.flags_held > 0: 169 self.spawn_player(player) 170 171 @override 172 def on_begin(self) -> None: 173 super().on_begin() 174 self.setup_standard_time_limit(self._time_limit) 175 self.setup_standard_powerup_drops() 176 177 # Set up flags with marker lights. 178 for i, flag_point in enumerate(self.map.flag_points): 179 point = flag_point 180 flag = ConquestFlag( 181 position=point, touchable=False, materials=[self._extraflagmat] 182 ) 183 self._flags.append(flag) 184 Flag.project_stand(point) 185 flag.light = bs.newnode( 186 'light', 187 owner=flag.node, 188 attrs={ 189 'position': point, 190 'intensity': 0.25, 191 'height_attenuated': False, 192 'radius': 0.3, 193 'color': (1, 1, 1), 194 }, 195 ) 196 197 # Give teams a flag to start with. 198 for i, team in enumerate(self.teams): 199 self._flags[i].team = team 200 light = self._flags[i].light 201 assert light 202 node = self._flags[i].node 203 assert node 204 light.color = team.color 205 node.color = team.color 206 207 self._update_scores() 208 209 # Initial joiners didn't spawn due to no flags being owned yet; 210 # spawn them now. 211 for player in self.players: 212 self.spawn_player(player) 213 214 def _update_scores(self) -> None: 215 for team in self.teams: 216 team.flags_held = 0 217 for flag in self._flags: 218 if flag.team is not None: 219 flag.team.flags_held += 1 220 for team in self.teams: 221 # If a team finds themselves with no flags, cancel all 222 # outstanding spawn-timers. 223 if team.flags_held == 0: 224 for player in team.players: 225 player.respawn_timer = None 226 player.respawn_icon = None 227 if team.flags_held == len(self._flags): 228 self.end_game() 229 self._scoreboard.set_team_value( 230 team, team.flags_held, len(self._flags) 231 ) 232 233 @override 234 def end_game(self) -> None: 235 results = bs.GameResults() 236 for team in self.teams: 237 results.set_team_score(team, team.flags_held) 238 self.end(results=results) 239 240 def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None: 241 assert flag.node 242 assert flag.light 243 light = bs.newnode( 244 'light', 245 attrs={ 246 'position': flag.node.position, 247 'height_attenuated': False, 248 'color': flag.light.color, 249 }, 250 ) 251 bs.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True) 252 bs.timer(length, light.delete) 253 254 def _handle_flag_player_collide(self) -> None: 255 collision = bs.getcollision() 256 try: 257 flag = collision.sourcenode.getdelegate(ConquestFlag, True) 258 player = collision.opposingnode.getdelegate( 259 PlayerSpaz, True 260 ).getplayer(Player, True) 261 except bs.NotFoundError: 262 return 263 assert flag.light 264 265 if flag.team is not player.team: 266 flag.team = player.team 267 flag.light.color = player.team.color 268 flag.node.color = player.team.color 269 self.stats.player_scored(player, 10, screenmessage=False) 270 self._swipsound.play() 271 self._flash_flag(flag) 272 self._update_scores() 273 274 # Respawn any players on this team that were in limbo due to the 275 # lack of a flag for their team. 276 for otherplayer in self.players: 277 if ( 278 otherplayer.team is flag.team 279 and otherplayer.actor is not None 280 and not otherplayer.is_alive() 281 and otherplayer.respawn_timer is None 282 ): 283 self.spawn_player(otherplayer) 284 285 @override 286 def handlemessage(self, msg: Any) -> Any: 287 if isinstance(msg, bs.PlayerDiedMessage): 288 # Augment standard behavior. 289 super().handlemessage(msg) 290 291 # Respawn only if this team has a flag. 292 player = msg.getplayer(Player) 293 if player.team.flags_held > 0: 294 self.respawn_player(player) 295 else: 296 player.respawn_timer = None 297 298 else: 299 super().handlemessage(msg) 300 301 @override 302 def spawn_player(self, player: Player) -> bs.Actor: 303 # We spawn players at different places based on what flags are held. 304 return self.spawn_player_spaz( 305 player, self._get_player_spawn_position(player) 306 ) 307 308 def _get_player_spawn_position(self, player: Player) -> Sequence[float]: 309 # Iterate until we find a spawn owned by this team. 310 spawn_count = len(self.map.spawn_by_flag_points) 311 312 # Get all spawns owned by this team. 313 spawns = [ 314 i for i in range(spawn_count) if self._flags[i].team is player.team 315 ] 316 317 closest_spawn = 0 318 closest_distance = 9999.0 319 320 # Now find the spawn that's closest to a spawn not owned by us; 321 # we'll use that one. 322 for spawn in spawns: 323 spt = self.map.spawn_by_flag_points[spawn] 324 our_pt = bs.Vec3(spt[0], spt[1], spt[2]) 325 for otherspawn in [ 326 i 327 for i in range(spawn_count) 328 if self._flags[i].team is not player.team 329 ]: 330 spt = self.map.spawn_by_flag_points[otherspawn] 331 their_pt = bs.Vec3(spt[0], spt[1], spt[2]) 332 dist = (their_pt - our_pt).length() 333 if dist < closest_distance: 334 closest_distance = dist 335 closest_spawn = spawn 336 337 pos = self.map.spawn_by_flag_points[closest_spawn] 338 x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3]) 339 z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5]) 340 pos = ( 341 pos[0] + random.uniform(*x_range), 342 pos[1], 343 pos[2] + random.uniform(*z_range), 344 ) 345 return pos
A game where teams try to claim all flags on the map.
124 def __init__(self, settings: dict): 125 super().__init__(settings) 126 shared = SharedObjects.get() 127 self._scoreboard = Scoreboard() 128 self._score_sound = bs.getsound('score') 129 self._swipsound = bs.getsound('swip') 130 self._extraflagmat = bs.Material() 131 self._flags: list[ConquestFlag] = [] 132 self._epic_mode = bool(settings['Epic Mode']) 133 self._time_limit = float(settings['Time Limit']) 134 135 # Base class overrides. 136 self.slow_motion = self._epic_mode 137 self.default_music = ( 138 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.GRAND_ROMP 139 ) 140 141 # We want flags to tell us they've been hit but not react physically. 142 self._extraflagmat.add_actions( 143 conditions=('they_have_material', shared.player_material), 144 actions=( 145 ('modify_part_collision', 'collide', True), 146 ('call', 'at_connect', self._handle_flag_player_collide), 147 ), 148 )
Instantiate the Activity.
113 @override 114 @classmethod 115 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 116 return issubclass(sessiontype, bs.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
118 @override 119 @classmethod 120 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 121 assert bs.app.classic is not None 122 return bs.app.classic.getmaps('conquest')
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.
150 @override 151 def get_instance_description(self) -> str | Sequence: 152 return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
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.
154 @override 155 def get_instance_description_short(self) -> str | Sequence: 156 return 'secure all ${ARG1} flags', len(self.map.flag_points)
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.
158 @override 159 def on_team_join(self, team: Team) -> None: 160 if self.has_begun(): 161 self._update_scores()
Called when a new bascenev1.Team joins the Activity.
(including the initial set of Teams)
163 @override 164 def on_player_join(self, player: Player) -> None: 165 player.respawn_timer = None 166 167 # Only spawn if this player's team has a flag currently. 168 if player.team.flags_held > 0: 169 self.spawn_player(player)
Called when a new bascenev1.Player has joined the Activity.
(including the initial set of Players)
171 @override 172 def on_begin(self) -> None: 173 super().on_begin() 174 self.setup_standard_time_limit(self._time_limit) 175 self.setup_standard_powerup_drops() 176 177 # Set up flags with marker lights. 178 for i, flag_point in enumerate(self.map.flag_points): 179 point = flag_point 180 flag = ConquestFlag( 181 position=point, touchable=False, materials=[self._extraflagmat] 182 ) 183 self._flags.append(flag) 184 Flag.project_stand(point) 185 flag.light = bs.newnode( 186 'light', 187 owner=flag.node, 188 attrs={ 189 'position': point, 190 'intensity': 0.25, 191 'height_attenuated': False, 192 'radius': 0.3, 193 'color': (1, 1, 1), 194 }, 195 ) 196 197 # Give teams a flag to start with. 198 for i, team in enumerate(self.teams): 199 self._flags[i].team = team 200 light = self._flags[i].light 201 assert light 202 node = self._flags[i].node 203 assert node 204 light.color = team.color 205 node.color = team.color 206 207 self._update_scores() 208 209 # Initial joiners didn't spawn due to no flags being owned yet; 210 # spawn them now. 211 for player in self.players: 212 self.spawn_player(player)
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.
233 @override 234 def end_game(self) -> None: 235 results = bs.GameResults() 236 for team in self.teams: 237 results.set_team_score(team, team.flags_held) 238 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.
285 @override 286 def handlemessage(self, msg: Any) -> Any: 287 if isinstance(msg, bs.PlayerDiedMessage): 288 # Augment standard behavior. 289 super().handlemessage(msg) 290 291 # Respawn only if this team has a flag. 292 player = msg.getplayer(Player) 293 if player.team.flags_held > 0: 294 self.respawn_player(player) 295 else: 296 player.respawn_timer = None 297 298 else: 299 super().handlemessage(msg)
General message handling; can be passed any message object.
301 @override 302 def spawn_player(self, player: Player) -> bs.Actor: 303 # We spawn players at different places based on what flags are held. 304 return self.spawn_player_spaz( 305 player, self._get_player_spawn_position(player) 306 )
Spawn something for the provided bascenev1.Player.
The default implementation simply calls spawn_player_spaz().