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