bascenev1lib.game.elimination
Elimination mini-game.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Elimination mini-game.""" 4 5# ba_meta require api 9 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import logging 11from typing import TYPE_CHECKING, override 12 13import bascenev1 as bs 14 15from bascenev1lib.actor.spazfactory import SpazFactory 16from bascenev1lib.actor.scoreboard import Scoreboard 17 18if TYPE_CHECKING: 19 from typing import Any, Sequence 20 21 22class Icon(bs.Actor): 23 """Creates in in-game icon on screen.""" 24 25 def __init__( 26 self, 27 player: Player, 28 position: tuple[float, float], 29 scale: float, 30 *, 31 show_lives: bool = True, 32 show_death: bool = True, 33 name_scale: float = 1.0, 34 name_maxwidth: float = 115.0, 35 flatness: float = 1.0, 36 shadow: float = 1.0, 37 ): 38 super().__init__() 39 40 self._player = player 41 self._show_lives = show_lives 42 self._show_death = show_death 43 self._name_scale = name_scale 44 self._outline_tex = bs.gettexture('characterIconMask') 45 46 icon = player.get_icon() 47 self.node = bs.newnode( 48 'image', 49 delegate=self, 50 attrs={ 51 'texture': icon['texture'], 52 'tint_texture': icon['tint_texture'], 53 'tint_color': icon['tint_color'], 54 'vr_depth': 400, 55 'tint2_color': icon['tint2_color'], 56 'mask_texture': self._outline_tex, 57 'opacity': 1.0, 58 'absolute_scale': True, 59 'attach': 'bottomCenter', 60 }, 61 ) 62 self._name_text = bs.newnode( 63 'text', 64 owner=self.node, 65 attrs={ 66 'text': bs.Lstr(value=player.getname()), 67 'color': bs.safecolor(player.team.color), 68 'h_align': 'center', 69 'v_align': 'center', 70 'vr_depth': 410, 71 'maxwidth': name_maxwidth, 72 'shadow': shadow, 73 'flatness': flatness, 74 'h_attach': 'center', 75 'v_attach': 'bottom', 76 }, 77 ) 78 if self._show_lives: 79 self._lives_text = bs.newnode( 80 'text', 81 owner=self.node, 82 attrs={ 83 'text': 'x0', 84 'color': (1, 1, 0.5), 85 'h_align': 'left', 86 'vr_depth': 430, 87 'shadow': 1.0, 88 'flatness': 1.0, 89 'h_attach': 'center', 90 'v_attach': 'bottom', 91 }, 92 ) 93 self.set_position_and_scale(position, scale) 94 95 def set_position_and_scale( 96 self, position: tuple[float, float], scale: float 97 ) -> None: 98 """(Re)position the icon.""" 99 assert self.node 100 self.node.position = position 101 self.node.scale = [70.0 * scale] 102 self._name_text.position = (position[0], position[1] + scale * 52.0) 103 self._name_text.scale = 1.0 * scale * self._name_scale 104 if self._show_lives: 105 self._lives_text.position = ( 106 position[0] + scale * 10.0, 107 position[1] - scale * 43.0, 108 ) 109 self._lives_text.scale = 1.0 * scale 110 111 def update_for_lives(self) -> None: 112 """Update for the target player's current lives.""" 113 if self._player: 114 lives = self._player.lives 115 else: 116 lives = 0 117 if self._show_lives: 118 if lives > 0: 119 self._lives_text.text = 'x' + str(lives - 1) 120 else: 121 self._lives_text.text = '' 122 if lives == 0: 123 self._name_text.opacity = 0.2 124 assert self.node 125 self.node.color = (0.7, 0.3, 0.3) 126 self.node.opacity = 0.2 127 128 def handle_player_spawned(self) -> None: 129 """Our player spawned; hooray!""" 130 if not self.node: 131 return 132 self.node.opacity = 1.0 133 self.update_for_lives() 134 135 def handle_player_died(self) -> None: 136 """Well poo; our player died.""" 137 if not self.node: 138 return 139 if self._show_death: 140 bs.animate( 141 self.node, 142 'opacity', 143 { 144 0.00: 1.0, 145 0.05: 0.0, 146 0.10: 1.0, 147 0.15: 0.0, 148 0.20: 1.0, 149 0.25: 0.0, 150 0.30: 1.0, 151 0.35: 0.0, 152 0.40: 1.0, 153 0.45: 0.0, 154 0.50: 1.0, 155 0.55: 0.2, 156 }, 157 ) 158 lives = self._player.lives 159 if lives == 0: 160 bs.timer(0.6, self.update_for_lives) 161 162 @override 163 def handlemessage(self, msg: Any) -> Any: 164 if isinstance(msg, bs.DieMessage): 165 self.node.delete() 166 return None 167 return super().handlemessage(msg) 168 169 170class Player(bs.Player['Team']): 171 """Our player type for this game.""" 172 173 def __init__(self) -> None: 174 self.lives = 0 175 self.icons: list[Icon] = [] 176 177 178class Team(bs.Team[Player]): 179 """Our team type for this game.""" 180 181 def __init__(self) -> None: 182 self.survival_seconds: int | None = None 183 self.spawn_order: list[Player] = [] 184 185 186# ba_meta export bascenev1.GameActivity 187class EliminationGame(bs.TeamGameActivity[Player, Team]): 188 """Game type where last player(s) left alive win.""" 189 190 name = 'Elimination' 191 description = 'Last remaining alive wins.' 192 scoreconfig = bs.ScoreConfig( 193 label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True 194 ) 195 # Show messages when players die since it's meaningful here. 196 announce_player_deaths = True 197 198 allow_mid_activity_joins = False 199 200 @override 201 @classmethod 202 def get_available_settings( 203 cls, sessiontype: type[bs.Session] 204 ) -> list[bs.Setting]: 205 settings = [ 206 bs.IntSetting( 207 'Lives Per Player', 208 default=1, 209 min_value=1, 210 max_value=10, 211 increment=1, 212 ), 213 bs.IntChoiceSetting( 214 'Time Limit', 215 choices=[ 216 ('None', 0), 217 ('1 Minute', 60), 218 ('2 Minutes', 120), 219 ('5 Minutes', 300), 220 ('10 Minutes', 600), 221 ('20 Minutes', 1200), 222 ], 223 default=0, 224 ), 225 bs.FloatChoiceSetting( 226 'Respawn Times', 227 choices=[ 228 ('Shorter', 0.25), 229 ('Short', 0.5), 230 ('Normal', 1.0), 231 ('Long', 2.0), 232 ('Longer', 4.0), 233 ], 234 default=1.0, 235 ), 236 bs.BoolSetting('Epic Mode', default=False), 237 ] 238 if issubclass(sessiontype, bs.DualTeamSession): 239 settings.append(bs.BoolSetting('Solo Mode', default=False)) 240 settings.append( 241 bs.BoolSetting('Balance Total Lives', default=False) 242 ) 243 return settings 244 245 @override 246 @classmethod 247 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 248 return issubclass(sessiontype, bs.DualTeamSession) or issubclass( 249 sessiontype, bs.FreeForAllSession 250 ) 251 252 @override 253 @classmethod 254 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 255 assert bs.app.classic is not None 256 return bs.app.classic.getmaps('melee') 257 258 def __init__(self, settings: dict): 259 super().__init__(settings) 260 self._scoreboard = Scoreboard() 261 self._start_time: float | None = None 262 self._vs_text: bs.Actor | None = None 263 self._round_end_timer: bs.Timer | None = None 264 self._epic_mode = bool(settings['Epic Mode']) 265 self._lives_per_player = int(settings['Lives Per Player']) 266 self._time_limit = float(settings['Time Limit']) 267 self._balance_total_lives = bool( 268 settings.get('Balance Total Lives', False) 269 ) 270 self._solo_mode = bool(settings.get('Solo Mode', False)) 271 272 # Base class overrides: 273 self.slow_motion = self._epic_mode 274 self.default_music = ( 275 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL 276 ) 277 278 @override 279 def get_instance_description(self) -> str | Sequence: 280 return ( 281 'Last team standing wins.' 282 if isinstance(self.session, bs.DualTeamSession) 283 else 'Last one standing wins.' 284 ) 285 286 @override 287 def get_instance_description_short(self) -> str | Sequence: 288 return ( 289 'last team standing wins' 290 if isinstance(self.session, bs.DualTeamSession) 291 else 'last one standing wins' 292 ) 293 294 @override 295 def on_player_join(self, player: Player) -> None: 296 player.lives = self._lives_per_player 297 298 if self._solo_mode: 299 player.team.spawn_order.append(player) 300 self._update_solo_mode() 301 else: 302 # Create our icon and spawn. 303 player.icons = [Icon(player, position=(0, 50), scale=0.8)] 304 if player.lives > 0: 305 self.spawn_player(player) 306 307 # Don't waste time doing this until begin. 308 if self.has_begun(): 309 self._update_icons() 310 311 @override 312 def on_begin(self) -> None: 313 super().on_begin() 314 self._start_time = bs.time() 315 self.setup_standard_time_limit(self._time_limit) 316 self.setup_standard_powerup_drops() 317 if self._solo_mode: 318 self._vs_text = bs.NodeActor( 319 bs.newnode( 320 'text', 321 attrs={ 322 'position': (0, 105), 323 'h_attach': 'center', 324 'h_align': 'center', 325 'maxwidth': 200, 326 'shadow': 0.5, 327 'vr_depth': 390, 328 'scale': 0.6, 329 'v_attach': 'bottom', 330 'color': (0.8, 0.8, 0.3, 1.0), 331 'text': bs.Lstr(resource='vsText'), 332 }, 333 ) 334 ) 335 336 # If balance-team-lives is on, add lives to the smaller team until 337 # total lives match. 338 if ( 339 isinstance(self.session, bs.DualTeamSession) 340 and self._balance_total_lives 341 and self.teams[0].players 342 and self.teams[1].players 343 ): 344 if self._get_total_team_lives( 345 self.teams[0] 346 ) < self._get_total_team_lives(self.teams[1]): 347 lesser_team = self.teams[0] 348 greater_team = self.teams[1] 349 else: 350 lesser_team = self.teams[1] 351 greater_team = self.teams[0] 352 add_index = 0 353 while self._get_total_team_lives( 354 lesser_team 355 ) < self._get_total_team_lives(greater_team): 356 lesser_team.players[add_index].lives += 1 357 add_index = (add_index + 1) % len(lesser_team.players) 358 359 self._update_icons() 360 361 # We could check game-over conditions at explicit trigger points, 362 # but lets just do the simple thing and poll it. 363 bs.timer(1.0, self._update, repeat=True) 364 365 def _update_solo_mode(self) -> None: 366 # For both teams, find the first player on the spawn order list with 367 # lives remaining and spawn them if they're not alive. 368 for team in self.teams: 369 # Prune dead players from the spawn order. 370 team.spawn_order = [p for p in team.spawn_order if p] 371 for player in team.spawn_order: 372 assert isinstance(player, Player) 373 if player.lives > 0: 374 if not player.is_alive(): 375 self.spawn_player(player) 376 break 377 378 def _update_icons(self) -> None: 379 # pylint: disable=too-many-branches 380 381 # In free-for-all mode, everyone is just lined up along the bottom. 382 if isinstance(self.session, bs.FreeForAllSession): 383 count = len(self.teams) 384 x_offs = 85 385 xval = x_offs * (count - 1) * -0.5 386 for team in self.teams: 387 if len(team.players) == 1: 388 player = team.players[0] 389 for icon in player.icons: 390 icon.set_position_and_scale((xval, 30), 0.7) 391 icon.update_for_lives() 392 xval += x_offs 393 394 # In teams mode we split up teams. 395 else: 396 if self._solo_mode: 397 # First off, clear out all icons. 398 for player in self.players: 399 player.icons = [] 400 401 # Now for each team, cycle through our available players 402 # adding icons. 403 for team in self.teams: 404 if team.id == 0: 405 xval = -60 406 x_offs = -78 407 else: 408 xval = 60 409 x_offs = 78 410 is_first = True 411 test_lives = 1 412 while True: 413 players_with_lives = [ 414 p 415 for p in team.spawn_order 416 if p and p.lives >= test_lives 417 ] 418 if not players_with_lives: 419 break 420 for player in players_with_lives: 421 player.icons.append( 422 Icon( 423 player, 424 position=(xval, (40 if is_first else 25)), 425 scale=1.0 if is_first else 0.5, 426 name_maxwidth=130 if is_first else 75, 427 name_scale=0.8 if is_first else 1.0, 428 flatness=0.0 if is_first else 1.0, 429 shadow=0.5 if is_first else 1.0, 430 show_death=is_first, 431 show_lives=False, 432 ) 433 ) 434 xval += x_offs * (0.8 if is_first else 0.56) 435 is_first = False 436 test_lives += 1 437 # Non-solo mode. 438 else: 439 for team in self.teams: 440 if team.id == 0: 441 xval = -50 442 x_offs = -85 443 else: 444 xval = 50 445 x_offs = 85 446 for player in team.players: 447 for icon in player.icons: 448 icon.set_position_and_scale((xval, 30), 0.7) 449 icon.update_for_lives() 450 xval += x_offs 451 452 def _get_spawn_point(self, player: Player) -> bs.Vec3 | None: 453 del player # Unused. 454 455 # In solo-mode, if there's an existing live player on the map, spawn at 456 # whichever spot is farthest from them (keeps the action spread out). 457 if self._solo_mode: 458 living_player = None 459 living_player_pos = None 460 for team in self.teams: 461 for tplayer in team.players: 462 if tplayer.is_alive(): 463 assert tplayer.node 464 ppos = tplayer.node.position 465 living_player = tplayer 466 living_player_pos = ppos 467 break 468 if living_player: 469 assert living_player_pos is not None 470 player_pos = bs.Vec3(living_player_pos) 471 points: list[tuple[float, bs.Vec3]] = [] 472 for team in self.teams: 473 start_pos = bs.Vec3(self.map.get_start_position(team.id)) 474 points.append( 475 ((start_pos - player_pos).length(), start_pos) 476 ) 477 # Hmm.. we need to sort vectors too? 478 points.sort(key=lambda x: x[0]) 479 return points[-1][1] 480 return None 481 482 @override 483 def spawn_player(self, player: Player) -> bs.Actor: 484 actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) 485 if not self._solo_mode: 486 bs.timer(0.3, bs.Call(self._print_lives, player)) 487 488 # If we have any icons, update their state. 489 for icon in player.icons: 490 icon.handle_player_spawned() 491 return actor 492 493 def _print_lives(self, player: Player) -> None: 494 from bascenev1lib.actor import popuptext 495 496 # We get called in a timer so it's possible our player has left/etc. 497 if not player or not player.is_alive() or not player.node: 498 return 499 500 popuptext.PopupText( 501 'x' + str(player.lives - 1), 502 color=(1, 1, 0, 1), 503 offset=(0, -0.8, 0), 504 random_offset=0.0, 505 scale=1.8, 506 position=player.node.position, 507 ).autoretain() 508 509 @override 510 def on_player_leave(self, player: Player) -> None: 511 super().on_player_leave(player) 512 player.icons = [] 513 514 # Remove us from spawn-order. 515 if self._solo_mode: 516 if player in player.team.spawn_order: 517 player.team.spawn_order.remove(player) 518 519 # Update icons in a moment since our team will be gone from the 520 # list then. 521 bs.timer(0, self._update_icons) 522 523 # If the player to leave was the last in spawn order and had 524 # their final turn currently in-progress, mark the survival time 525 # for their team. 526 if self._get_total_team_lives(player.team) == 0: 527 assert self._start_time is not None 528 player.team.survival_seconds = int(bs.time() - self._start_time) 529 530 def _get_total_team_lives(self, team: Team) -> int: 531 return sum(player.lives for player in team.players) 532 533 @override 534 def handlemessage(self, msg: Any) -> Any: 535 if isinstance(msg, bs.PlayerDiedMessage): 536 # Augment standard behavior. 537 super().handlemessage(msg) 538 player: Player = msg.getplayer(Player) 539 540 player.lives -= 1 541 if player.lives < 0: 542 logging.exception( 543 "Got lives < 0 in Elim; this shouldn't happen. solo: %s", 544 self._solo_mode, 545 ) 546 player.lives = 0 547 548 # If we have any icons, update their state. 549 for icon in player.icons: 550 icon.handle_player_died() 551 552 # Play big death sound on our last death 553 # or for every one in solo mode. 554 if self._solo_mode or player.lives == 0: 555 SpazFactory.get().single_player_death_sound.play() 556 557 # If we hit zero lives, we're dead (and our team might be too). 558 if player.lives == 0: 559 # If the whole team is now dead, mark their survival time. 560 if self._get_total_team_lives(player.team) == 0: 561 assert self._start_time is not None 562 player.team.survival_seconds = int( 563 bs.time() - self._start_time 564 ) 565 else: 566 # Otherwise, in regular mode, respawn. 567 if not self._solo_mode: 568 self.respawn_player(player) 569 570 # In solo, put ourself at the back of the spawn order. 571 if self._solo_mode: 572 player.team.spawn_order.remove(player) 573 player.team.spawn_order.append(player) 574 575 def _update(self) -> None: 576 if self._solo_mode: 577 # For both teams, find the first player on the spawn order 578 # list with lives remaining and spawn them if they're not alive. 579 for team in self.teams: 580 # Prune dead players from the spawn order. 581 team.spawn_order = [p for p in team.spawn_order if p] 582 for player in team.spawn_order: 583 assert isinstance(player, Player) 584 if player.lives > 0: 585 if not player.is_alive(): 586 self.spawn_player(player) 587 self._update_icons() 588 break 589 590 # If we're down to 1 or fewer living teams, start a timer to end 591 # the game (allows the dust to settle and draws to occur if deaths 592 # are close enough). 593 if len(self._get_living_teams()) < 2: 594 self._round_end_timer = bs.Timer(0.5, self.end_game) 595 596 def _get_living_teams(self) -> list[Team]: 597 return [ 598 team 599 for team in self.teams 600 if len(team.players) > 0 601 and any(player.lives > 0 for player in team.players) 602 ] 603 604 @override 605 def end_game(self) -> None: 606 if self.has_ended(): 607 return 608 results = bs.GameResults() 609 self._vs_text = None # Kill our 'vs' if its there. 610 for team in self.teams: 611 results.set_team_score(team, team.survival_seconds) 612 self.end(results=results)
23class Icon(bs.Actor): 24 """Creates in in-game icon on screen.""" 25 26 def __init__( 27 self, 28 player: Player, 29 position: tuple[float, float], 30 scale: float, 31 *, 32 show_lives: bool = True, 33 show_death: bool = True, 34 name_scale: float = 1.0, 35 name_maxwidth: float = 115.0, 36 flatness: float = 1.0, 37 shadow: float = 1.0, 38 ): 39 super().__init__() 40 41 self._player = player 42 self._show_lives = show_lives 43 self._show_death = show_death 44 self._name_scale = name_scale 45 self._outline_tex = bs.gettexture('characterIconMask') 46 47 icon = player.get_icon() 48 self.node = bs.newnode( 49 'image', 50 delegate=self, 51 attrs={ 52 'texture': icon['texture'], 53 'tint_texture': icon['tint_texture'], 54 'tint_color': icon['tint_color'], 55 'vr_depth': 400, 56 'tint2_color': icon['tint2_color'], 57 'mask_texture': self._outline_tex, 58 'opacity': 1.0, 59 'absolute_scale': True, 60 'attach': 'bottomCenter', 61 }, 62 ) 63 self._name_text = bs.newnode( 64 'text', 65 owner=self.node, 66 attrs={ 67 'text': bs.Lstr(value=player.getname()), 68 'color': bs.safecolor(player.team.color), 69 'h_align': 'center', 70 'v_align': 'center', 71 'vr_depth': 410, 72 'maxwidth': name_maxwidth, 73 'shadow': shadow, 74 'flatness': flatness, 75 'h_attach': 'center', 76 'v_attach': 'bottom', 77 }, 78 ) 79 if self._show_lives: 80 self._lives_text = bs.newnode( 81 'text', 82 owner=self.node, 83 attrs={ 84 'text': 'x0', 85 'color': (1, 1, 0.5), 86 'h_align': 'left', 87 'vr_depth': 430, 88 'shadow': 1.0, 89 'flatness': 1.0, 90 'h_attach': 'center', 91 'v_attach': 'bottom', 92 }, 93 ) 94 self.set_position_and_scale(position, scale) 95 96 def set_position_and_scale( 97 self, position: tuple[float, float], scale: float 98 ) -> None: 99 """(Re)position the icon.""" 100 assert self.node 101 self.node.position = position 102 self.node.scale = [70.0 * scale] 103 self._name_text.position = (position[0], position[1] + scale * 52.0) 104 self._name_text.scale = 1.0 * scale * self._name_scale 105 if self._show_lives: 106 self._lives_text.position = ( 107 position[0] + scale * 10.0, 108 position[1] - scale * 43.0, 109 ) 110 self._lives_text.scale = 1.0 * scale 111 112 def update_for_lives(self) -> None: 113 """Update for the target player's current lives.""" 114 if self._player: 115 lives = self._player.lives 116 else: 117 lives = 0 118 if self._show_lives: 119 if lives > 0: 120 self._lives_text.text = 'x' + str(lives - 1) 121 else: 122 self._lives_text.text = '' 123 if lives == 0: 124 self._name_text.opacity = 0.2 125 assert self.node 126 self.node.color = (0.7, 0.3, 0.3) 127 self.node.opacity = 0.2 128 129 def handle_player_spawned(self) -> None: 130 """Our player spawned; hooray!""" 131 if not self.node: 132 return 133 self.node.opacity = 1.0 134 self.update_for_lives() 135 136 def handle_player_died(self) -> None: 137 """Well poo; our player died.""" 138 if not self.node: 139 return 140 if self._show_death: 141 bs.animate( 142 self.node, 143 'opacity', 144 { 145 0.00: 1.0, 146 0.05: 0.0, 147 0.10: 1.0, 148 0.15: 0.0, 149 0.20: 1.0, 150 0.25: 0.0, 151 0.30: 1.0, 152 0.35: 0.0, 153 0.40: 1.0, 154 0.45: 0.0, 155 0.50: 1.0, 156 0.55: 0.2, 157 }, 158 ) 159 lives = self._player.lives 160 if lives == 0: 161 bs.timer(0.6, self.update_for_lives) 162 163 @override 164 def handlemessage(self, msg: Any) -> Any: 165 if isinstance(msg, bs.DieMessage): 166 self.node.delete() 167 return None 168 return super().handlemessage(msg)
Creates in in-game icon on screen.
26 def __init__( 27 self, 28 player: Player, 29 position: tuple[float, float], 30 scale: float, 31 *, 32 show_lives: bool = True, 33 show_death: bool = True, 34 name_scale: float = 1.0, 35 name_maxwidth: float = 115.0, 36 flatness: float = 1.0, 37 shadow: float = 1.0, 38 ): 39 super().__init__() 40 41 self._player = player 42 self._show_lives = show_lives 43 self._show_death = show_death 44 self._name_scale = name_scale 45 self._outline_tex = bs.gettexture('characterIconMask') 46 47 icon = player.get_icon() 48 self.node = bs.newnode( 49 'image', 50 delegate=self, 51 attrs={ 52 'texture': icon['texture'], 53 'tint_texture': icon['tint_texture'], 54 'tint_color': icon['tint_color'], 55 'vr_depth': 400, 56 'tint2_color': icon['tint2_color'], 57 'mask_texture': self._outline_tex, 58 'opacity': 1.0, 59 'absolute_scale': True, 60 'attach': 'bottomCenter', 61 }, 62 ) 63 self._name_text = bs.newnode( 64 'text', 65 owner=self.node, 66 attrs={ 67 'text': bs.Lstr(value=player.getname()), 68 'color': bs.safecolor(player.team.color), 69 'h_align': 'center', 70 'v_align': 'center', 71 'vr_depth': 410, 72 'maxwidth': name_maxwidth, 73 'shadow': shadow, 74 'flatness': flatness, 75 'h_attach': 'center', 76 'v_attach': 'bottom', 77 }, 78 ) 79 if self._show_lives: 80 self._lives_text = bs.newnode( 81 'text', 82 owner=self.node, 83 attrs={ 84 'text': 'x0', 85 'color': (1, 1, 0.5), 86 'h_align': 'left', 87 'vr_depth': 430, 88 'shadow': 1.0, 89 'flatness': 1.0, 90 'h_attach': 'center', 91 'v_attach': 'bottom', 92 }, 93 ) 94 self.set_position_and_scale(position, scale)
Instantiates an Actor in the current bascenev1.Activity.
96 def set_position_and_scale( 97 self, position: tuple[float, float], scale: float 98 ) -> None: 99 """(Re)position the icon.""" 100 assert self.node 101 self.node.position = position 102 self.node.scale = [70.0 * scale] 103 self._name_text.position = (position[0], position[1] + scale * 52.0) 104 self._name_text.scale = 1.0 * scale * self._name_scale 105 if self._show_lives: 106 self._lives_text.position = ( 107 position[0] + scale * 10.0, 108 position[1] - scale * 43.0, 109 ) 110 self._lives_text.scale = 1.0 * scale
(Re)position the icon.
112 def update_for_lives(self) -> None: 113 """Update for the target player's current lives.""" 114 if self._player: 115 lives = self._player.lives 116 else: 117 lives = 0 118 if self._show_lives: 119 if lives > 0: 120 self._lives_text.text = 'x' + str(lives - 1) 121 else: 122 self._lives_text.text = '' 123 if lives == 0: 124 self._name_text.opacity = 0.2 125 assert self.node 126 self.node.color = (0.7, 0.3, 0.3) 127 self.node.opacity = 0.2
Update for the target player's current lives.
129 def handle_player_spawned(self) -> None: 130 """Our player spawned; hooray!""" 131 if not self.node: 132 return 133 self.node.opacity = 1.0 134 self.update_for_lives()
Our player spawned; hooray!
136 def handle_player_died(self) -> None: 137 """Well poo; our player died.""" 138 if not self.node: 139 return 140 if self._show_death: 141 bs.animate( 142 self.node, 143 'opacity', 144 { 145 0.00: 1.0, 146 0.05: 0.0, 147 0.10: 1.0, 148 0.15: 0.0, 149 0.20: 1.0, 150 0.25: 0.0, 151 0.30: 1.0, 152 0.35: 0.0, 153 0.40: 1.0, 154 0.45: 0.0, 155 0.50: 1.0, 156 0.55: 0.2, 157 }, 158 ) 159 lives = self._player.lives 160 if lives == 0: 161 bs.timer(0.6, self.update_for_lives)
Well poo; our player died.
171class Player(bs.Player['Team']): 172 """Our player type for this game.""" 173 174 def __init__(self) -> None: 175 self.lives = 0 176 self.icons: list[Icon] = []
Our player type for this game.
179class Team(bs.Team[Player]): 180 """Our team type for this game.""" 181 182 def __init__(self) -> None: 183 self.survival_seconds: int | None = None 184 self.spawn_order: list[Player] = []
Our team type for this game.
188class EliminationGame(bs.TeamGameActivity[Player, Team]): 189 """Game type where last player(s) left alive win.""" 190 191 name = 'Elimination' 192 description = 'Last remaining alive wins.' 193 scoreconfig = bs.ScoreConfig( 194 label='Survived', scoretype=bs.ScoreType.SECONDS, none_is_winner=True 195 ) 196 # Show messages when players die since it's meaningful here. 197 announce_player_deaths = True 198 199 allow_mid_activity_joins = False 200 201 @override 202 @classmethod 203 def get_available_settings( 204 cls, sessiontype: type[bs.Session] 205 ) -> list[bs.Setting]: 206 settings = [ 207 bs.IntSetting( 208 'Lives Per Player', 209 default=1, 210 min_value=1, 211 max_value=10, 212 increment=1, 213 ), 214 bs.IntChoiceSetting( 215 'Time Limit', 216 choices=[ 217 ('None', 0), 218 ('1 Minute', 60), 219 ('2 Minutes', 120), 220 ('5 Minutes', 300), 221 ('10 Minutes', 600), 222 ('20 Minutes', 1200), 223 ], 224 default=0, 225 ), 226 bs.FloatChoiceSetting( 227 'Respawn Times', 228 choices=[ 229 ('Shorter', 0.25), 230 ('Short', 0.5), 231 ('Normal', 1.0), 232 ('Long', 2.0), 233 ('Longer', 4.0), 234 ], 235 default=1.0, 236 ), 237 bs.BoolSetting('Epic Mode', default=False), 238 ] 239 if issubclass(sessiontype, bs.DualTeamSession): 240 settings.append(bs.BoolSetting('Solo Mode', default=False)) 241 settings.append( 242 bs.BoolSetting('Balance Total Lives', default=False) 243 ) 244 return settings 245 246 @override 247 @classmethod 248 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 249 return issubclass(sessiontype, bs.DualTeamSession) or issubclass( 250 sessiontype, bs.FreeForAllSession 251 ) 252 253 @override 254 @classmethod 255 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 256 assert bs.app.classic is not None 257 return bs.app.classic.getmaps('melee') 258 259 def __init__(self, settings: dict): 260 super().__init__(settings) 261 self._scoreboard = Scoreboard() 262 self._start_time: float | None = None 263 self._vs_text: bs.Actor | None = None 264 self._round_end_timer: bs.Timer | None = None 265 self._epic_mode = bool(settings['Epic Mode']) 266 self._lives_per_player = int(settings['Lives Per Player']) 267 self._time_limit = float(settings['Time Limit']) 268 self._balance_total_lives = bool( 269 settings.get('Balance Total Lives', False) 270 ) 271 self._solo_mode = bool(settings.get('Solo Mode', False)) 272 273 # Base class overrides: 274 self.slow_motion = self._epic_mode 275 self.default_music = ( 276 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL 277 ) 278 279 @override 280 def get_instance_description(self) -> str | Sequence: 281 return ( 282 'Last team standing wins.' 283 if isinstance(self.session, bs.DualTeamSession) 284 else 'Last one standing wins.' 285 ) 286 287 @override 288 def get_instance_description_short(self) -> str | Sequence: 289 return ( 290 'last team standing wins' 291 if isinstance(self.session, bs.DualTeamSession) 292 else 'last one standing wins' 293 ) 294 295 @override 296 def on_player_join(self, player: Player) -> None: 297 player.lives = self._lives_per_player 298 299 if self._solo_mode: 300 player.team.spawn_order.append(player) 301 self._update_solo_mode() 302 else: 303 # Create our icon and spawn. 304 player.icons = [Icon(player, position=(0, 50), scale=0.8)] 305 if player.lives > 0: 306 self.spawn_player(player) 307 308 # Don't waste time doing this until begin. 309 if self.has_begun(): 310 self._update_icons() 311 312 @override 313 def on_begin(self) -> None: 314 super().on_begin() 315 self._start_time = bs.time() 316 self.setup_standard_time_limit(self._time_limit) 317 self.setup_standard_powerup_drops() 318 if self._solo_mode: 319 self._vs_text = bs.NodeActor( 320 bs.newnode( 321 'text', 322 attrs={ 323 'position': (0, 105), 324 'h_attach': 'center', 325 'h_align': 'center', 326 'maxwidth': 200, 327 'shadow': 0.5, 328 'vr_depth': 390, 329 'scale': 0.6, 330 'v_attach': 'bottom', 331 'color': (0.8, 0.8, 0.3, 1.0), 332 'text': bs.Lstr(resource='vsText'), 333 }, 334 ) 335 ) 336 337 # If balance-team-lives is on, add lives to the smaller team until 338 # total lives match. 339 if ( 340 isinstance(self.session, bs.DualTeamSession) 341 and self._balance_total_lives 342 and self.teams[0].players 343 and self.teams[1].players 344 ): 345 if self._get_total_team_lives( 346 self.teams[0] 347 ) < self._get_total_team_lives(self.teams[1]): 348 lesser_team = self.teams[0] 349 greater_team = self.teams[1] 350 else: 351 lesser_team = self.teams[1] 352 greater_team = self.teams[0] 353 add_index = 0 354 while self._get_total_team_lives( 355 lesser_team 356 ) < self._get_total_team_lives(greater_team): 357 lesser_team.players[add_index].lives += 1 358 add_index = (add_index + 1) % len(lesser_team.players) 359 360 self._update_icons() 361 362 # We could check game-over conditions at explicit trigger points, 363 # but lets just do the simple thing and poll it. 364 bs.timer(1.0, self._update, repeat=True) 365 366 def _update_solo_mode(self) -> None: 367 # For both teams, find the first player on the spawn order list with 368 # lives remaining and spawn them if they're not alive. 369 for team in self.teams: 370 # Prune dead players from the spawn order. 371 team.spawn_order = [p for p in team.spawn_order if p] 372 for player in team.spawn_order: 373 assert isinstance(player, Player) 374 if player.lives > 0: 375 if not player.is_alive(): 376 self.spawn_player(player) 377 break 378 379 def _update_icons(self) -> None: 380 # pylint: disable=too-many-branches 381 382 # In free-for-all mode, everyone is just lined up along the bottom. 383 if isinstance(self.session, bs.FreeForAllSession): 384 count = len(self.teams) 385 x_offs = 85 386 xval = x_offs * (count - 1) * -0.5 387 for team in self.teams: 388 if len(team.players) == 1: 389 player = team.players[0] 390 for icon in player.icons: 391 icon.set_position_and_scale((xval, 30), 0.7) 392 icon.update_for_lives() 393 xval += x_offs 394 395 # In teams mode we split up teams. 396 else: 397 if self._solo_mode: 398 # First off, clear out all icons. 399 for player in self.players: 400 player.icons = [] 401 402 # Now for each team, cycle through our available players 403 # adding icons. 404 for team in self.teams: 405 if team.id == 0: 406 xval = -60 407 x_offs = -78 408 else: 409 xval = 60 410 x_offs = 78 411 is_first = True 412 test_lives = 1 413 while True: 414 players_with_lives = [ 415 p 416 for p in team.spawn_order 417 if p and p.lives >= test_lives 418 ] 419 if not players_with_lives: 420 break 421 for player in players_with_lives: 422 player.icons.append( 423 Icon( 424 player, 425 position=(xval, (40 if is_first else 25)), 426 scale=1.0 if is_first else 0.5, 427 name_maxwidth=130 if is_first else 75, 428 name_scale=0.8 if is_first else 1.0, 429 flatness=0.0 if is_first else 1.0, 430 shadow=0.5 if is_first else 1.0, 431 show_death=is_first, 432 show_lives=False, 433 ) 434 ) 435 xval += x_offs * (0.8 if is_first else 0.56) 436 is_first = False 437 test_lives += 1 438 # Non-solo mode. 439 else: 440 for team in self.teams: 441 if team.id == 0: 442 xval = -50 443 x_offs = -85 444 else: 445 xval = 50 446 x_offs = 85 447 for player in team.players: 448 for icon in player.icons: 449 icon.set_position_and_scale((xval, 30), 0.7) 450 icon.update_for_lives() 451 xval += x_offs 452 453 def _get_spawn_point(self, player: Player) -> bs.Vec3 | None: 454 del player # Unused. 455 456 # In solo-mode, if there's an existing live player on the map, spawn at 457 # whichever spot is farthest from them (keeps the action spread out). 458 if self._solo_mode: 459 living_player = None 460 living_player_pos = None 461 for team in self.teams: 462 for tplayer in team.players: 463 if tplayer.is_alive(): 464 assert tplayer.node 465 ppos = tplayer.node.position 466 living_player = tplayer 467 living_player_pos = ppos 468 break 469 if living_player: 470 assert living_player_pos is not None 471 player_pos = bs.Vec3(living_player_pos) 472 points: list[tuple[float, bs.Vec3]] = [] 473 for team in self.teams: 474 start_pos = bs.Vec3(self.map.get_start_position(team.id)) 475 points.append( 476 ((start_pos - player_pos).length(), start_pos) 477 ) 478 # Hmm.. we need to sort vectors too? 479 points.sort(key=lambda x: x[0]) 480 return points[-1][1] 481 return None 482 483 @override 484 def spawn_player(self, player: Player) -> bs.Actor: 485 actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) 486 if not self._solo_mode: 487 bs.timer(0.3, bs.Call(self._print_lives, player)) 488 489 # If we have any icons, update their state. 490 for icon in player.icons: 491 icon.handle_player_spawned() 492 return actor 493 494 def _print_lives(self, player: Player) -> None: 495 from bascenev1lib.actor import popuptext 496 497 # We get called in a timer so it's possible our player has left/etc. 498 if not player or not player.is_alive() or not player.node: 499 return 500 501 popuptext.PopupText( 502 'x' + str(player.lives - 1), 503 color=(1, 1, 0, 1), 504 offset=(0, -0.8, 0), 505 random_offset=0.0, 506 scale=1.8, 507 position=player.node.position, 508 ).autoretain() 509 510 @override 511 def on_player_leave(self, player: Player) -> None: 512 super().on_player_leave(player) 513 player.icons = [] 514 515 # Remove us from spawn-order. 516 if self._solo_mode: 517 if player in player.team.spawn_order: 518 player.team.spawn_order.remove(player) 519 520 # Update icons in a moment since our team will be gone from the 521 # list then. 522 bs.timer(0, self._update_icons) 523 524 # If the player to leave was the last in spawn order and had 525 # their final turn currently in-progress, mark the survival time 526 # for their team. 527 if self._get_total_team_lives(player.team) == 0: 528 assert self._start_time is not None 529 player.team.survival_seconds = int(bs.time() - self._start_time) 530 531 def _get_total_team_lives(self, team: Team) -> int: 532 return sum(player.lives for player in team.players) 533 534 @override 535 def handlemessage(self, msg: Any) -> Any: 536 if isinstance(msg, bs.PlayerDiedMessage): 537 # Augment standard behavior. 538 super().handlemessage(msg) 539 player: Player = msg.getplayer(Player) 540 541 player.lives -= 1 542 if player.lives < 0: 543 logging.exception( 544 "Got lives < 0 in Elim; this shouldn't happen. solo: %s", 545 self._solo_mode, 546 ) 547 player.lives = 0 548 549 # If we have any icons, update their state. 550 for icon in player.icons: 551 icon.handle_player_died() 552 553 # Play big death sound on our last death 554 # or for every one in solo mode. 555 if self._solo_mode or player.lives == 0: 556 SpazFactory.get().single_player_death_sound.play() 557 558 # If we hit zero lives, we're dead (and our team might be too). 559 if player.lives == 0: 560 # If the whole team is now dead, mark their survival time. 561 if self._get_total_team_lives(player.team) == 0: 562 assert self._start_time is not None 563 player.team.survival_seconds = int( 564 bs.time() - self._start_time 565 ) 566 else: 567 # Otherwise, in regular mode, respawn. 568 if not self._solo_mode: 569 self.respawn_player(player) 570 571 # In solo, put ourself at the back of the spawn order. 572 if self._solo_mode: 573 player.team.spawn_order.remove(player) 574 player.team.spawn_order.append(player) 575 576 def _update(self) -> None: 577 if self._solo_mode: 578 # For both teams, find the first player on the spawn order 579 # list with lives remaining and spawn them if they're not alive. 580 for team in self.teams: 581 # Prune dead players from the spawn order. 582 team.spawn_order = [p for p in team.spawn_order if p] 583 for player in team.spawn_order: 584 assert isinstance(player, Player) 585 if player.lives > 0: 586 if not player.is_alive(): 587 self.spawn_player(player) 588 self._update_icons() 589 break 590 591 # If we're down to 1 or fewer living teams, start a timer to end 592 # the game (allows the dust to settle and draws to occur if deaths 593 # are close enough). 594 if len(self._get_living_teams()) < 2: 595 self._round_end_timer = bs.Timer(0.5, self.end_game) 596 597 def _get_living_teams(self) -> list[Team]: 598 return [ 599 team 600 for team in self.teams 601 if len(team.players) > 0 602 and any(player.lives > 0 for player in team.players) 603 ] 604 605 @override 606 def end_game(self) -> None: 607 if self.has_ended(): 608 return 609 results = bs.GameResults() 610 self._vs_text = None # Kill our 'vs' if its there. 611 for team in self.teams: 612 results.set_team_score(team, team.survival_seconds) 613 self.end(results=results)
Game type where last player(s) left alive win.
259 def __init__(self, settings: dict): 260 super().__init__(settings) 261 self._scoreboard = Scoreboard() 262 self._start_time: float | None = None 263 self._vs_text: bs.Actor | None = None 264 self._round_end_timer: bs.Timer | None = None 265 self._epic_mode = bool(settings['Epic Mode']) 266 self._lives_per_player = int(settings['Lives Per Player']) 267 self._time_limit = float(settings['Time Limit']) 268 self._balance_total_lives = bool( 269 settings.get('Balance Total Lives', False) 270 ) 271 self._solo_mode = bool(settings.get('Solo Mode', False)) 272 273 # Base class overrides: 274 self.slow_motion = self._epic_mode 275 self.default_music = ( 276 bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL 277 )
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.
Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.
201 @override 202 @classmethod 203 def get_available_settings( 204 cls, sessiontype: type[bs.Session] 205 ) -> list[bs.Setting]: 206 settings = [ 207 bs.IntSetting( 208 'Lives Per Player', 209 default=1, 210 min_value=1, 211 max_value=10, 212 increment=1, 213 ), 214 bs.IntChoiceSetting( 215 'Time Limit', 216 choices=[ 217 ('None', 0), 218 ('1 Minute', 60), 219 ('2 Minutes', 120), 220 ('5 Minutes', 300), 221 ('10 Minutes', 600), 222 ('20 Minutes', 1200), 223 ], 224 default=0, 225 ), 226 bs.FloatChoiceSetting( 227 'Respawn Times', 228 choices=[ 229 ('Shorter', 0.25), 230 ('Short', 0.5), 231 ('Normal', 1.0), 232 ('Long', 2.0), 233 ('Longer', 4.0), 234 ], 235 default=1.0, 236 ), 237 bs.BoolSetting('Epic Mode', default=False), 238 ] 239 if issubclass(sessiontype, bs.DualTeamSession): 240 settings.append(bs.BoolSetting('Solo Mode', default=False)) 241 settings.append( 242 bs.BoolSetting('Balance Total Lives', default=False) 243 ) 244 return settings
Return a list of settings relevant to this game type when running under the provided session type.
246 @override 247 @classmethod 248 def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool: 249 return issubclass(sessiontype, bs.DualTeamSession) or issubclass( 250 sessiontype, bs.FreeForAllSession 251 )
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
253 @override 254 @classmethod 255 def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]: 256 assert bs.app.classic is not None 257 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.
279 @override 280 def get_instance_description(self) -> str | Sequence: 281 return ( 282 'Last team standing wins.' 283 if isinstance(self.session, bs.DualTeamSession) 284 else 'Last one standing wins.' 285 )
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.
287 @override 288 def get_instance_description_short(self) -> str | Sequence: 289 return ( 290 'last team standing wins' 291 if isinstance(self.session, bs.DualTeamSession) 292 else 'last one standing wins' 293 )
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.
295 @override 296 def on_player_join(self, player: Player) -> None: 297 player.lives = self._lives_per_player 298 299 if self._solo_mode: 300 player.team.spawn_order.append(player) 301 self._update_solo_mode() 302 else: 303 # Create our icon and spawn. 304 player.icons = [Icon(player, position=(0, 50), scale=0.8)] 305 if player.lives > 0: 306 self.spawn_player(player) 307 308 # Don't waste time doing this until begin. 309 if self.has_begun(): 310 self._update_icons()
Called when a new bascenev1.Player has joined the Activity.
(including the initial set of Players)
312 @override 313 def on_begin(self) -> None: 314 super().on_begin() 315 self._start_time = bs.time() 316 self.setup_standard_time_limit(self._time_limit) 317 self.setup_standard_powerup_drops() 318 if self._solo_mode: 319 self._vs_text = bs.NodeActor( 320 bs.newnode( 321 'text', 322 attrs={ 323 'position': (0, 105), 324 'h_attach': 'center', 325 'h_align': 'center', 326 'maxwidth': 200, 327 'shadow': 0.5, 328 'vr_depth': 390, 329 'scale': 0.6, 330 'v_attach': 'bottom', 331 'color': (0.8, 0.8, 0.3, 1.0), 332 'text': bs.Lstr(resource='vsText'), 333 }, 334 ) 335 ) 336 337 # If balance-team-lives is on, add lives to the smaller team until 338 # total lives match. 339 if ( 340 isinstance(self.session, bs.DualTeamSession) 341 and self._balance_total_lives 342 and self.teams[0].players 343 and self.teams[1].players 344 ): 345 if self._get_total_team_lives( 346 self.teams[0] 347 ) < self._get_total_team_lives(self.teams[1]): 348 lesser_team = self.teams[0] 349 greater_team = self.teams[1] 350 else: 351 lesser_team = self.teams[1] 352 greater_team = self.teams[0] 353 add_index = 0 354 while self._get_total_team_lives( 355 lesser_team 356 ) < self._get_total_team_lives(greater_team): 357 lesser_team.players[add_index].lives += 1 358 add_index = (add_index + 1) % len(lesser_team.players) 359 360 self._update_icons() 361 362 # We could check game-over conditions at explicit trigger points, 363 # but lets just do the simple thing and poll it. 364 bs.timer(1.0, self._update, repeat=True)
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.
483 @override 484 def spawn_player(self, player: Player) -> bs.Actor: 485 actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) 486 if not self._solo_mode: 487 bs.timer(0.3, bs.Call(self._print_lives, player)) 488 489 # If we have any icons, update their state. 490 for icon in player.icons: 491 icon.handle_player_spawned() 492 return actor
Spawn something for the provided bascenev1.Player.
The default implementation simply calls spawn_player_spaz().
510 @override 511 def on_player_leave(self, player: Player) -> None: 512 super().on_player_leave(player) 513 player.icons = [] 514 515 # Remove us from spawn-order. 516 if self._solo_mode: 517 if player in player.team.spawn_order: 518 player.team.spawn_order.remove(player) 519 520 # Update icons in a moment since our team will be gone from the 521 # list then. 522 bs.timer(0, self._update_icons) 523 524 # If the player to leave was the last in spawn order and had 525 # their final turn currently in-progress, mark the survival time 526 # for their team. 527 if self._get_total_team_lives(player.team) == 0: 528 assert self._start_time is not None 529 player.team.survival_seconds = int(bs.time() - self._start_time)
Called when a bascenev1.Player is leaving the Activity.
534 @override 535 def handlemessage(self, msg: Any) -> Any: 536 if isinstance(msg, bs.PlayerDiedMessage): 537 # Augment standard behavior. 538 super().handlemessage(msg) 539 player: Player = msg.getplayer(Player) 540 541 player.lives -= 1 542 if player.lives < 0: 543 logging.exception( 544 "Got lives < 0 in Elim; this shouldn't happen. solo: %s", 545 self._solo_mode, 546 ) 547 player.lives = 0 548 549 # If we have any icons, update their state. 550 for icon in player.icons: 551 icon.handle_player_died() 552 553 # Play big death sound on our last death 554 # or for every one in solo mode. 555 if self._solo_mode or player.lives == 0: 556 SpazFactory.get().single_player_death_sound.play() 557 558 # If we hit zero lives, we're dead (and our team might be too). 559 if player.lives == 0: 560 # If the whole team is now dead, mark their survival time. 561 if self._get_total_team_lives(player.team) == 0: 562 assert self._start_time is not None 563 player.team.survival_seconds = int( 564 bs.time() - self._start_time 565 ) 566 else: 567 # Otherwise, in regular mode, respawn. 568 if not self._solo_mode: 569 self.respawn_player(player) 570 571 # In solo, put ourself at the back of the spawn order. 572 if self._solo_mode: 573 player.team.spawn_order.remove(player) 574 player.team.spawn_order.append(player)
General message handling; can be passed any message object.
605 @override 606 def end_game(self) -> None: 607 if self.has_ended(): 608 return 609 results = bs.GameResults() 610 self._vs_text = None # Kill our 'vs' if its there. 611 for team in self.teams: 612 results.set_team_score(team, team.survival_seconds) 613 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.