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