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