bastd.game.football
Implements football games (both co-op and teams varieties).
1# Released under the MIT License. See LICENSE for details. 2# 3"""Implements football games (both co-op and teams varieties).""" 4 5# ba_meta require api 7 6# (see https://ballistica.net/wiki/meta-tag-system) 7 8from __future__ import annotations 9 10import random 11from typing import TYPE_CHECKING 12import math 13 14import ba 15from bastd.actor.bomb import TNTSpawner 16from bastd.actor.playerspaz import PlayerSpaz 17from bastd.actor.scoreboard import Scoreboard 18from bastd.actor.respawnicon import RespawnIcon 19from bastd.actor.powerupbox import PowerupBoxFactory, PowerupBox 20from bastd.actor.flag import ( 21 FlagFactory, 22 Flag, 23 FlagPickedUpMessage, 24 FlagDroppedMessage, 25 FlagDiedMessage, 26) 27from bastd.actor.spazbot import ( 28 SpazBotDiedMessage, 29 SpazBotPunchedMessage, 30 SpazBotSet, 31 BrawlerBotLite, 32 BrawlerBot, 33 BomberBotLite, 34 BomberBot, 35 TriggerBot, 36 ChargerBot, 37 TriggerBotPro, 38 BrawlerBotPro, 39 StickyBot, 40 ExplodeyBot, 41) 42 43if TYPE_CHECKING: 44 from typing import Any, Sequence 45 from bastd.actor.spaz import Spaz 46 from bastd.actor.spazbot import SpazBot 47 48 49class FootballFlag(Flag): 50 """Custom flag class for football games.""" 51 52 def __init__(self, position: Sequence[float]): 53 super().__init__( 54 position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3) 55 ) 56 assert self.node 57 self.last_holding_player: ba.Player | None = None 58 self.node.is_area_of_interest = True 59 self.respawn_timer: ba.Timer | None = None 60 self.scored = False 61 self.held_count = 0 62 self.light = ba.newnode( 63 'light', 64 owner=self.node, 65 attrs={ 66 'intensity': 0.25, 67 'height_attenuated': False, 68 'radius': 0.2, 69 'color': (0.9, 0.7, 0.0), 70 }, 71 ) 72 self.node.connectattr('position', self.light, 'position') 73 74 75class Player(ba.Player['Team']): 76 """Our player type for this game.""" 77 78 def __init__(self) -> None: 79 self.respawn_timer: ba.Timer | None = None 80 self.respawn_icon: RespawnIcon | None = None 81 82 83class Team(ba.Team[Player]): 84 """Our team type for this game.""" 85 86 def __init__(self) -> None: 87 self.score = 0 88 89 90# ba_meta export game 91class FootballTeamGame(ba.TeamGameActivity[Player, Team]): 92 """Football game for teams mode.""" 93 94 name = 'Football' 95 description = 'Get the flag to the enemy end zone.' 96 available_settings = [ 97 ba.IntSetting( 98 'Score to Win', 99 min_value=7, 100 default=21, 101 increment=7, 102 ), 103 ba.IntChoiceSetting( 104 'Time Limit', 105 choices=[ 106 ('None', 0), 107 ('1 Minute', 60), 108 ('2 Minutes', 120), 109 ('5 Minutes', 300), 110 ('10 Minutes', 600), 111 ('20 Minutes', 1200), 112 ], 113 default=0, 114 ), 115 ba.FloatChoiceSetting( 116 'Respawn Times', 117 choices=[ 118 ('Shorter', 0.25), 119 ('Short', 0.5), 120 ('Normal', 1.0), 121 ('Long', 2.0), 122 ('Longer', 4.0), 123 ], 124 default=1.0, 125 ), 126 ba.BoolSetting('Epic Mode', default=False), 127 ] 128 129 @classmethod 130 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 131 # We only support two-team play. 132 return issubclass(sessiontype, ba.DualTeamSession) 133 134 @classmethod 135 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 136 return ba.getmaps('football') 137 138 def __init__(self, settings: dict): 139 super().__init__(settings) 140 self._scoreboard: Scoreboard | None = Scoreboard() 141 142 # Load some media we need. 143 self._cheer_sound = ba.getsound('cheer') 144 self._chant_sound = ba.getsound('crowdChant') 145 self._score_sound = ba.getsound('score') 146 self._swipsound = ba.getsound('swip') 147 self._whistle_sound = ba.getsound('refWhistle') 148 self._score_region_material = ba.Material() 149 self._score_region_material.add_actions( 150 conditions=('they_have_material', FlagFactory.get().flagmaterial), 151 actions=( 152 ('modify_part_collision', 'collide', True), 153 ('modify_part_collision', 'physical', False), 154 ('call', 'at_connect', self._handle_score), 155 ), 156 ) 157 self._flag_spawn_pos: Sequence[float] | None = None 158 self._score_regions: list[ba.NodeActor] = [] 159 self._flag: FootballFlag | None = None 160 self._flag_respawn_timer: ba.Timer | None = None 161 self._flag_respawn_light: ba.NodeActor | None = None 162 self._score_to_win = int(settings['Score to Win']) 163 self._time_limit = float(settings['Time Limit']) 164 self._epic_mode = bool(settings['Epic Mode']) 165 self.slow_motion = self._epic_mode 166 self.default_music = ( 167 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FOOTBALL 168 ) 169 170 def get_instance_description(self) -> str | Sequence: 171 touchdowns = self._score_to_win / 7 172 173 # NOTE: if use just touchdowns = self._score_to_win // 7 174 # and we will need to score, for example, 27 points, 175 # we will be required to score 3 (not 4) goals .. 176 touchdowns = math.ceil(touchdowns) 177 if touchdowns > 1: 178 return 'Score ${ARG1} touchdowns.', touchdowns 179 return 'Score a touchdown.' 180 181 def get_instance_description_short(self) -> str | Sequence: 182 touchdowns = self._score_to_win / 7 183 touchdowns = math.ceil(touchdowns) 184 if touchdowns > 1: 185 return 'score ${ARG1} touchdowns', touchdowns 186 return 'score a touchdown' 187 188 def on_begin(self) -> None: 189 super().on_begin() 190 self.setup_standard_time_limit(self._time_limit) 191 self.setup_standard_powerup_drops() 192 self._flag_spawn_pos = self.map.get_flag_position(None) 193 self._spawn_flag() 194 defs = self.map.defs 195 self._score_regions.append( 196 ba.NodeActor( 197 ba.newnode( 198 'region', 199 attrs={ 200 'position': defs.boxes['goal1'][0:3], 201 'scale': defs.boxes['goal1'][6:9], 202 'type': 'box', 203 'materials': (self._score_region_material,), 204 }, 205 ) 206 ) 207 ) 208 self._score_regions.append( 209 ba.NodeActor( 210 ba.newnode( 211 'region', 212 attrs={ 213 'position': defs.boxes['goal2'][0:3], 214 'scale': defs.boxes['goal2'][6:9], 215 'type': 'box', 216 'materials': (self._score_region_material,), 217 }, 218 ) 219 ) 220 ) 221 self._update_scoreboard() 222 ba.playsound(self._chant_sound) 223 224 def on_team_join(self, team: Team) -> None: 225 self._update_scoreboard() 226 227 def _kill_flag(self) -> None: 228 self._flag = None 229 230 def _handle_score(self) -> None: 231 """A point has been scored.""" 232 233 # Our flag might stick around for a second or two 234 # make sure it doesn't score again. 235 assert self._flag is not None 236 if self._flag.scored: 237 return 238 region = ba.getcollision().sourcenode 239 i = None 240 for i, score_region in enumerate(self._score_regions): 241 if region == score_region.node: 242 break 243 for team in self.teams: 244 if team.id == i: 245 team.score += 7 246 247 # Tell all players to celebrate. 248 for player in team.players: 249 if player.actor: 250 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 251 252 # If someone on this team was last to touch it, 253 # give them points. 254 assert self._flag is not None 255 if ( 256 self._flag.last_holding_player 257 and team == self._flag.last_holding_player.team 258 ): 259 self.stats.player_scored( 260 self._flag.last_holding_player, 50, big_message=True 261 ) 262 # End the game if we won. 263 if team.score >= self._score_to_win: 264 self.end_game() 265 ba.playsound(self._score_sound) 266 ba.playsound(self._cheer_sound) 267 assert self._flag 268 self._flag.scored = True 269 270 # Kill the flag (it'll respawn shortly). 271 ba.timer(1.0, self._kill_flag) 272 light = ba.newnode( 273 'light', 274 attrs={ 275 'position': ba.getcollision().position, 276 'height_attenuated': False, 277 'color': (1, 0, 0), 278 }, 279 ) 280 ba.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) 281 ba.timer(1.0, light.delete) 282 ba.cameraflash(duration=10.0) 283 self._update_scoreboard() 284 285 def end_game(self) -> None: 286 results = ba.GameResults() 287 for team in self.teams: 288 results.set_team_score(team, team.score) 289 self.end(results=results, announce_delay=0.8) 290 291 def _update_scoreboard(self) -> None: 292 assert self._scoreboard is not None 293 for team in self.teams: 294 self._scoreboard.set_team_value( 295 team, team.score, self._score_to_win 296 ) 297 298 def handlemessage(self, msg: Any) -> Any: 299 if isinstance(msg, FlagPickedUpMessage): 300 assert isinstance(msg.flag, FootballFlag) 301 try: 302 msg.flag.last_holding_player = msg.node.getdelegate( 303 PlayerSpaz, True 304 ).getplayer(Player, True) 305 except ba.NotFoundError: 306 pass 307 msg.flag.held_count += 1 308 309 elif isinstance(msg, FlagDroppedMessage): 310 assert isinstance(msg.flag, FootballFlag) 311 msg.flag.held_count -= 1 312 313 # Respawn dead players if they're still in the game. 314 elif isinstance(msg, ba.PlayerDiedMessage): 315 # Augment standard behavior. 316 super().handlemessage(msg) 317 self.respawn_player(msg.getplayer(Player)) 318 319 # Respawn dead flags. 320 elif isinstance(msg, FlagDiedMessage): 321 if not self.has_ended(): 322 self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag) 323 self._flag_respawn_light = ba.NodeActor( 324 ba.newnode( 325 'light', 326 attrs={ 327 'position': self._flag_spawn_pos, 328 'height_attenuated': False, 329 'radius': 0.15, 330 'color': (1.0, 1.0, 0.3), 331 }, 332 ) 333 ) 334 assert self._flag_respawn_light.node 335 ba.animate( 336 self._flag_respawn_light.node, 337 'intensity', 338 {0.0: 0, 0.25: 0.15, 0.5: 0}, 339 loop=True, 340 ) 341 ba.timer(3.0, self._flag_respawn_light.node.delete) 342 343 else: 344 # Augment standard behavior. 345 super().handlemessage(msg) 346 347 def _flash_flag_spawn(self) -> None: 348 light = ba.newnode( 349 'light', 350 attrs={ 351 'position': self._flag_spawn_pos, 352 'height_attenuated': False, 353 'color': (1, 1, 0), 354 }, 355 ) 356 ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 357 ba.timer(1.0, light.delete) 358 359 def _spawn_flag(self) -> None: 360 ba.playsound(self._swipsound) 361 ba.playsound(self._whistle_sound) 362 self._flash_flag_spawn() 363 assert self._flag_spawn_pos is not None 364 self._flag = FootballFlag(position=self._flag_spawn_pos) 365 366 367class FootballCoopGame(ba.CoopGameActivity[Player, Team]): 368 """Co-op variant of football.""" 369 370 name = 'Football' 371 tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] 372 scoreconfig = ba.ScoreConfig( 373 scoretype=ba.ScoreType.MILLISECONDS, version='B' 374 ) 375 376 default_music = ba.MusicType.FOOTBALL 377 378 # FIXME: Need to update co-op games to use getscoreconfig. 379 def get_score_type(self) -> str: 380 return 'time' 381 382 def get_instance_description(self) -> str | Sequence: 383 touchdowns = self._score_to_win / 7 384 touchdowns = math.ceil(touchdowns) 385 if touchdowns > 1: 386 return 'Score ${ARG1} touchdowns.', touchdowns 387 return 'Score a touchdown.' 388 389 def get_instance_description_short(self) -> str | Sequence: 390 touchdowns = self._score_to_win / 7 391 touchdowns = math.ceil(touchdowns) 392 if touchdowns > 1: 393 return 'score ${ARG1} touchdowns', touchdowns 394 return 'score a touchdown' 395 396 def __init__(self, settings: dict): 397 settings['map'] = 'Football Stadium' 398 super().__init__(settings) 399 self._preset = settings.get('preset', 'rookie') 400 401 # Load some media we need. 402 self._cheer_sound = ba.getsound('cheer') 403 self._boo_sound = ba.getsound('boo') 404 self._chant_sound = ba.getsound('crowdChant') 405 self._score_sound = ba.getsound('score') 406 self._swipsound = ba.getsound('swip') 407 self._whistle_sound = ba.getsound('refWhistle') 408 self._score_to_win = 21 409 self._score_region_material = ba.Material() 410 self._score_region_material.add_actions( 411 conditions=('they_have_material', FlagFactory.get().flagmaterial), 412 actions=( 413 ('modify_part_collision', 'collide', True), 414 ('modify_part_collision', 'physical', False), 415 ('call', 'at_connect', self._handle_score), 416 ), 417 ) 418 self._powerup_center = (0, 2, 0) 419 self._powerup_spread = (10, 5.5) 420 self._player_has_dropped_bomb = False 421 self._player_has_punched = False 422 self._scoreboard: Scoreboard | None = None 423 self._flag_spawn_pos: Sequence[float] | None = None 424 self._score_regions: list[ba.NodeActor] = [] 425 self._exclude_powerups: list[str] = [] 426 self._have_tnt = False 427 self._bot_types_initial: list[type[SpazBot]] | None = None 428 self._bot_types_7: list[type[SpazBot]] | None = None 429 self._bot_types_14: list[type[SpazBot]] | None = None 430 self._bot_team: Team | None = None 431 self._starttime_ms: int | None = None 432 self._time_text: ba.NodeActor | None = None 433 self._time_text_input: ba.NodeActor | None = None 434 self._tntspawner: TNTSpawner | None = None 435 self._bots = SpazBotSet() 436 self._bot_spawn_timer: ba.Timer | None = None 437 self._powerup_drop_timer: ba.Timer | None = None 438 self._scoring_team: Team | None = None 439 self._final_time_ms: int | None = None 440 self._time_text_timer: ba.Timer | None = None 441 self._flag_respawn_light: ba.Actor | None = None 442 self._flag: FootballFlag | None = None 443 444 def on_transition_in(self) -> None: 445 super().on_transition_in() 446 self._scoreboard = Scoreboard() 447 self._flag_spawn_pos = self.map.get_flag_position(None) 448 self._spawn_flag() 449 450 # Set up the two score regions. 451 defs = self.map.defs 452 self._score_regions.append( 453 ba.NodeActor( 454 ba.newnode( 455 'region', 456 attrs={ 457 'position': defs.boxes['goal1'][0:3], 458 'scale': defs.boxes['goal1'][6:9], 459 'type': 'box', 460 'materials': [self._score_region_material], 461 }, 462 ) 463 ) 464 ) 465 self._score_regions.append( 466 ba.NodeActor( 467 ba.newnode( 468 'region', 469 attrs={ 470 'position': defs.boxes['goal2'][0:3], 471 'scale': defs.boxes['goal2'][6:9], 472 'type': 'box', 473 'materials': [self._score_region_material], 474 }, 475 ) 476 ) 477 ) 478 ba.playsound(self._chant_sound) 479 480 def on_begin(self) -> None: 481 # FIXME: Split this up a bit. 482 # pylint: disable=too-many-statements 483 from bastd.actor import controlsguide 484 485 super().on_begin() 486 487 # Show controls help in kiosk mode. 488 if ba.app.demo_mode or ba.app.arcade_mode: 489 controlsguide.ControlsGuide( 490 delay=3.0, lifespan=10.0, bright=True 491 ).autoretain() 492 assert self.initialplayerinfos is not None 493 abot: type[SpazBot] 494 bbot: type[SpazBot] 495 cbot: type[SpazBot] 496 if self._preset in ['rookie', 'rookie_easy']: 497 self._exclude_powerups = ['curse'] 498 self._have_tnt = False 499 abot = ( 500 BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot 501 ) 502 self._bot_types_initial = [abot] * len(self.initialplayerinfos) 503 bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot 504 self._bot_types_7 = [bbot] * ( 505 1 if len(self.initialplayerinfos) < 3 else 2 506 ) 507 cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot 508 self._bot_types_14 = [cbot] * ( 509 1 if len(self.initialplayerinfos) < 3 else 2 510 ) 511 elif self._preset == 'tournament': 512 self._exclude_powerups = [] 513 self._have_tnt = True 514 self._bot_types_initial = [BrawlerBot] * ( 515 1 if len(self.initialplayerinfos) < 2 else 2 516 ) 517 self._bot_types_7 = [TriggerBot] * ( 518 1 if len(self.initialplayerinfos) < 3 else 2 519 ) 520 self._bot_types_14 = [ChargerBot] * ( 521 1 if len(self.initialplayerinfos) < 4 else 2 522 ) 523 elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: 524 self._exclude_powerups = ['curse'] 525 self._have_tnt = True 526 self._bot_types_initial = [ChargerBot] * len( 527 self.initialplayerinfos 528 ) 529 abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite 530 typed_bot_list: list[type[SpazBot]] = [] 531 self._bot_types_7 = ( 532 typed_bot_list 533 + [abot] 534 + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) 535 ) 536 bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot 537 self._bot_types_14 = [bbot] * ( 538 1 if len(self.initialplayerinfos) < 3 else 2 539 ) 540 elif self._preset in ['uber', 'uber_easy']: 541 self._exclude_powerups = [] 542 self._have_tnt = True 543 abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot 544 bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot 545 typed_bot_list_2: list[type[SpazBot]] = [] 546 self._bot_types_initial = ( 547 typed_bot_list_2 548 + [StickyBot] 549 + [abot] * len(self.initialplayerinfos) 550 ) 551 self._bot_types_7 = [bbot] * ( 552 1 if len(self.initialplayerinfos) < 3 else 2 553 ) 554 self._bot_types_14 = [ExplodeyBot] * ( 555 1 if len(self.initialplayerinfos) < 3 else 2 556 ) 557 else: 558 raise Exception() 559 560 self.setup_low_life_warning_sound() 561 562 self._drop_powerups(standard_points=True) 563 ba.timer(4.0, self._start_powerup_drops) 564 565 # Make a bogus team for our bots. 566 bad_team_name = self.get_team_display_string('Bad Guys') 567 self._bot_team = Team() 568 self._bot_team.manual_init( 569 team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) 570 ) 571 572 for team in [self.teams[0], self._bot_team]: 573 team.score = 0 574 575 self.update_scores() 576 577 # Time display. 578 starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) 579 assert isinstance(starttime_ms, int) 580 self._starttime_ms = starttime_ms 581 self._time_text = ba.NodeActor( 582 ba.newnode( 583 'text', 584 attrs={ 585 'v_attach': 'top', 586 'h_attach': 'center', 587 'h_align': 'center', 588 'color': (1, 1, 0.5, 1), 589 'flatness': 0.5, 590 'shadow': 0.5, 591 'position': (0, -50), 592 'scale': 1.3, 593 'text': '', 594 }, 595 ) 596 ) 597 self._time_text_input = ba.NodeActor( 598 ba.newnode('timedisplay', attrs={'showsubseconds': True}) 599 ) 600 self.globalsnode.connectattr( 601 'time', self._time_text_input.node, 'time2' 602 ) 603 assert self._time_text_input.node 604 assert self._time_text.node 605 self._time_text_input.node.connectattr( 606 'output', self._time_text.node, 'text' 607 ) 608 609 # Our TNT spawner (if applicable). 610 if self._have_tnt: 611 self._tntspawner = TNTSpawner(position=(0, 1, -1)) 612 613 self._bots = SpazBotSet() 614 self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) 615 616 for bottype in self._bot_types_initial: 617 self._spawn_bot(bottype) 618 619 def _on_bot_spawn(self, spaz: SpazBot) -> None: 620 # We want to move to the left by default. 621 spaz.target_point_default = ba.Vec3(0, 0, 0) 622 623 def _spawn_bot( 624 self, spaz_type: type[SpazBot], immediate: bool = False 625 ) -> None: 626 assert self._bot_team is not None 627 pos = self.map.get_start_position(self._bot_team.id) 628 self._bots.spawn_bot( 629 spaz_type, 630 pos=pos, 631 spawn_time=0.001 if immediate else 3.0, 632 on_spawn_call=self._on_bot_spawn, 633 ) 634 635 def _update_bots(self) -> None: 636 bots = self._bots.get_living_bots() 637 for bot in bots: 638 bot.target_flag = None 639 640 # If we're waiting on a continue, stop here so they don't keep scoring. 641 if self.is_waiting_for_continue(): 642 self._bots.stop_moving() 643 return 644 645 # If we've got a flag and no player are holding it, find the closest 646 # bot to it, and make them the designated flag-bearer. 647 assert self._flag is not None 648 if self._flag.node: 649 for player in self.players: 650 if player.actor: 651 assert isinstance(player.actor, PlayerSpaz) 652 if ( 653 player.actor.is_alive() 654 and player.actor.node.hold_node == self._flag.node 655 ): 656 return 657 658 flagpos = ba.Vec3(self._flag.node.position) 659 closest_bot: SpazBot | None = None 660 closest_dist = 0.0 # Always gets assigned first time through. 661 for bot in bots: 662 # If a bot is picked up, he should forget about the flag. 663 if bot.held_count > 0: 664 continue 665 assert bot.node 666 botpos = ba.Vec3(bot.node.position) 667 botdist = (botpos - flagpos).length() 668 if closest_bot is None or botdist < closest_dist: 669 closest_bot = bot 670 closest_dist = botdist 671 if closest_bot is not None: 672 closest_bot.target_flag = self._flag 673 674 def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: 675 if poweruptype is None: 676 poweruptype = PowerupBoxFactory.get().get_random_powerup_type( 677 excludetypes=self._exclude_powerups 678 ) 679 PowerupBox( 680 position=self.map.powerup_spawn_points[index], 681 poweruptype=poweruptype, 682 ).autoretain() 683 684 def _start_powerup_drops(self) -> None: 685 self._powerup_drop_timer = ba.Timer( 686 3.0, self._drop_powerups, repeat=True 687 ) 688 689 def _drop_powerups( 690 self, standard_points: bool = False, poweruptype: str | None = None 691 ) -> None: 692 """Generic powerup drop.""" 693 if standard_points: 694 spawnpoints = self.map.powerup_spawn_points 695 for i, _point in enumerate(spawnpoints): 696 ba.timer( 697 1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype) 698 ) 699 else: 700 point = ( 701 self._powerup_center[0] 702 + random.uniform( 703 -1.0 * self._powerup_spread[0], 704 1.0 * self._powerup_spread[0], 705 ), 706 self._powerup_center[1], 707 self._powerup_center[2] 708 + random.uniform( 709 -self._powerup_spread[1], self._powerup_spread[1] 710 ), 711 ) 712 713 # Drop one random one somewhere. 714 PowerupBox( 715 position=point, 716 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 717 excludetypes=self._exclude_powerups 718 ), 719 ).autoretain() 720 721 def _kill_flag(self) -> None: 722 try: 723 assert self._flag is not None 724 self._flag.handlemessage(ba.DieMessage()) 725 except Exception: 726 ba.print_exception('Error in _kill_flag.') 727 728 def _handle_score(self) -> None: 729 """a point has been scored""" 730 # FIXME tidy this up 731 # pylint: disable=too-many-branches 732 733 # Our flag might stick around for a second or two; 734 # we don't want it to be able to score again. 735 assert self._flag is not None 736 if self._flag.scored: 737 return 738 739 # See which score region it was. 740 region = ba.getcollision().sourcenode 741 i = None 742 for i, score_region in enumerate(self._score_regions): 743 if region == score_region.node: 744 break 745 746 for team in [self.teams[0], self._bot_team]: 747 assert team is not None 748 if team.id == i: 749 team.score += 7 750 751 # Tell all players (or bots) to celebrate. 752 if i == 0: 753 for player in team.players: 754 if player.actor: 755 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 756 else: 757 self._bots.celebrate(2.0) 758 759 # If the good guys scored, add more enemies. 760 if i == 0: 761 if self.teams[0].score == 7: 762 assert self._bot_types_7 is not None 763 for bottype in self._bot_types_7: 764 self._spawn_bot(bottype) 765 elif self.teams[0].score == 14: 766 assert self._bot_types_14 is not None 767 for bottype in self._bot_types_14: 768 self._spawn_bot(bottype) 769 770 ba.playsound(self._score_sound) 771 if i == 0: 772 ba.playsound(self._cheer_sound) 773 else: 774 ba.playsound(self._boo_sound) 775 776 # Kill the flag (it'll respawn shortly). 777 self._flag.scored = True 778 779 ba.timer(0.2, self._kill_flag) 780 781 self.update_scores() 782 light = ba.newnode( 783 'light', 784 attrs={ 785 'position': ba.getcollision().position, 786 'height_attenuated': False, 787 'color': (1, 0, 0), 788 }, 789 ) 790 ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 791 ba.timer(1.0, light.delete) 792 if i == 0: 793 ba.cameraflash(duration=10.0) 794 795 def end_game(self) -> None: 796 ba.setmusic(None) 797 self._bots.final_celebrate() 798 ba.timer(0.001, ba.Call(self.do_end, 'defeat')) 799 800 def on_continue(self) -> None: 801 # Subtract one touchdown from the bots and get them moving again. 802 assert self._bot_team is not None 803 self._bot_team.score -= 7 804 self._bots.start_moving() 805 self.update_scores() 806 807 def update_scores(self) -> None: 808 """update scoreboard and check for winners""" 809 # FIXME: tidy this up 810 # pylint: disable=too-many-nested-blocks 811 have_scoring_team = False 812 win_score = self._score_to_win 813 for team in [self.teams[0], self._bot_team]: 814 assert team is not None 815 assert self._scoreboard is not None 816 self._scoreboard.set_team_value(team, team.score, win_score) 817 if team.score >= win_score: 818 if not have_scoring_team: 819 self._scoring_team = team 820 if team is self._bot_team: 821 self.continue_or_end_game() 822 else: 823 ba.setmusic(ba.MusicType.VICTORY) 824 825 # Completion achievements. 826 assert self._bot_team is not None 827 if self._preset in ['rookie', 'rookie_easy']: 828 self._award_achievement( 829 'Rookie Football Victory', sound=False 830 ) 831 if self._bot_team.score == 0: 832 self._award_achievement( 833 'Rookie Football Shutout', sound=False 834 ) 835 elif self._preset in ['pro', 'pro_easy']: 836 self._award_achievement( 837 'Pro Football Victory', sound=False 838 ) 839 if self._bot_team.score == 0: 840 self._award_achievement( 841 'Pro Football Shutout', sound=False 842 ) 843 elif self._preset in ['uber', 'uber_easy']: 844 self._award_achievement( 845 'Uber Football Victory', sound=False 846 ) 847 if self._bot_team.score == 0: 848 self._award_achievement( 849 'Uber Football Shutout', sound=False 850 ) 851 if ( 852 not self._player_has_dropped_bomb 853 and not self._player_has_punched 854 ): 855 self._award_achievement( 856 'Got the Moves', sound=False 857 ) 858 self._bots.stop_moving() 859 self.show_zoom_message( 860 ba.Lstr(resource='victoryText'), 861 scale=1.0, 862 duration=4.0, 863 ) 864 self.celebrate(10.0) 865 assert self._starttime_ms is not None 866 self._final_time_ms = int( 867 ba.time(timeformat=ba.TimeFormat.MILLISECONDS) 868 - self._starttime_ms 869 ) 870 self._time_text_timer = None 871 assert ( 872 self._time_text_input is not None 873 and self._time_text_input.node 874 ) 875 self._time_text_input.node.timemax = self._final_time_ms 876 877 # FIXME: Does this still need to be deferred? 878 ba.pushcall(ba.Call(self.do_end, 'victory')) 879 880 def do_end(self, outcome: str) -> None: 881 """End the game with the specified outcome.""" 882 if outcome == 'defeat': 883 self.fade_to_red() 884 assert self._final_time_ms is not None 885 scoreval = ( 886 None if outcome == 'defeat' else int(self._final_time_ms // 10) 887 ) 888 self.end( 889 delay=3.0, 890 results={ 891 'outcome': outcome, 892 'score': scoreval, 893 'score_order': 'decreasing', 894 'playerinfos': self.initialplayerinfos, 895 }, 896 ) 897 898 def handlemessage(self, msg: Any) -> Any: 899 """handle high-level game messages""" 900 if isinstance(msg, ba.PlayerDiedMessage): 901 # Augment standard behavior. 902 super().handlemessage(msg) 903 904 # Respawn them shortly. 905 player = msg.getplayer(Player) 906 assert self.initialplayerinfos is not None 907 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 908 player.respawn_timer = ba.Timer( 909 respawn_time, ba.Call(self.spawn_player_if_exists, player) 910 ) 911 player.respawn_icon = RespawnIcon(player, respawn_time) 912 913 elif isinstance(msg, SpazBotDiedMessage): 914 915 # Every time a bad guy dies, spawn a new one. 916 ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) 917 918 elif isinstance(msg, SpazBotPunchedMessage): 919 if self._preset in ['rookie', 'rookie_easy']: 920 if msg.damage >= 500: 921 self._award_achievement('Super Punch') 922 elif self._preset in ['pro', 'pro_easy']: 923 if msg.damage >= 1000: 924 self._award_achievement('Super Mega Punch') 925 926 # Respawn dead flags. 927 elif isinstance(msg, FlagDiedMessage): 928 assert isinstance(msg.flag, FootballFlag) 929 msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) 930 self._flag_respawn_light = ba.NodeActor( 931 ba.newnode( 932 'light', 933 attrs={ 934 'position': self._flag_spawn_pos, 935 'height_attenuated': False, 936 'radius': 0.15, 937 'color': (1.0, 1.0, 0.3), 938 }, 939 ) 940 ) 941 assert self._flag_respawn_light.node 942 ba.animate( 943 self._flag_respawn_light.node, 944 'intensity', 945 {0: 0, 0.25: 0.15, 0.5: 0}, 946 loop=True, 947 ) 948 ba.timer(3.0, self._flag_respawn_light.node.delete) 949 else: 950 return super().handlemessage(msg) 951 return None 952 953 def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None: 954 del player, bomb # Unused. 955 self._player_has_dropped_bomb = True 956 957 def _handle_player_punched(self, player: Spaz) -> None: 958 del player # Unused. 959 self._player_has_punched = True 960 961 def spawn_player(self, player: Player) -> ba.Actor: 962 spaz = self.spawn_player_spaz( 963 player, position=self.map.get_start_position(player.team.id) 964 ) 965 if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: 966 spaz.impact_scale = 0.25 967 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 968 spaz.punch_callback = self._handle_player_punched 969 return spaz 970 971 def _flash_flag_spawn(self) -> None: 972 light = ba.newnode( 973 'light', 974 attrs={ 975 'position': self._flag_spawn_pos, 976 'height_attenuated': False, 977 'color': (1, 1, 0), 978 }, 979 ) 980 ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 981 ba.timer(1.0, light.delete) 982 983 def _spawn_flag(self) -> None: 984 ba.playsound(self._swipsound) 985 ba.playsound(self._whistle_sound) 986 self._flash_flag_spawn() 987 assert self._flag_spawn_pos is not None 988 self._flag = FootballFlag(position=self._flag_spawn_pos)
50class FootballFlag(Flag): 51 """Custom flag class for football games.""" 52 53 def __init__(self, position: Sequence[float]): 54 super().__init__( 55 position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3) 56 ) 57 assert self.node 58 self.last_holding_player: ba.Player | None = None 59 self.node.is_area_of_interest = True 60 self.respawn_timer: ba.Timer | None = None 61 self.scored = False 62 self.held_count = 0 63 self.light = ba.newnode( 64 'light', 65 owner=self.node, 66 attrs={ 67 'intensity': 0.25, 68 'height_attenuated': False, 69 'radius': 0.2, 70 'color': (0.9, 0.7, 0.0), 71 }, 72 ) 73 self.node.connectattr('position', self.light, 'position')
Custom flag class for football games.
53 def __init__(self, position: Sequence[float]): 54 super().__init__( 55 position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3) 56 ) 57 assert self.node 58 self.last_holding_player: ba.Player | None = None 59 self.node.is_area_of_interest = True 60 self.respawn_timer: ba.Timer | None = None 61 self.scored = False 62 self.held_count = 0 63 self.light = ba.newnode( 64 'light', 65 owner=self.node, 66 attrs={ 67 'intensity': 0.25, 68 'height_attenuated': False, 69 'radius': 0.2, 70 'color': (0.9, 0.7, 0.0), 71 }, 72 ) 73 self.node.connectattr('position', self.light, 'position')
Instantiate a flag.
If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.
'materials can be a list of extra ba.Material
s to apply to the flag.
If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.
Inherited Members
- ba._actor.Actor
- autoretain
- on_expire
- expired
- exists
- is_alive
- activity
- getactivity
76class Player(ba.Player['Team']): 77 """Our player type for this game.""" 78 79 def __init__(self) -> None: 80 self.respawn_timer: ba.Timer | None = None 81 self.respawn_icon: RespawnIcon | None = None
Our player type for this game.
Inherited Members
- ba._player.Player
- actor
- on_expire
- team
- customdata
- sessionplayer
- node
- position
- exists
- getname
- is_alive
- get_icon
- assigninput
- resetinput
84class Team(ba.Team[Player]): 85 """Our team type for this game.""" 86 87 def __init__(self) -> None: 88 self.score = 0
Our team type for this game.
Inherited Members
- ba._team.Team
- manual_init
- customdata
- on_expire
- sessionteam
92class FootballTeamGame(ba.TeamGameActivity[Player, Team]): 93 """Football game for teams mode.""" 94 95 name = 'Football' 96 description = 'Get the flag to the enemy end zone.' 97 available_settings = [ 98 ba.IntSetting( 99 'Score to Win', 100 min_value=7, 101 default=21, 102 increment=7, 103 ), 104 ba.IntChoiceSetting( 105 'Time Limit', 106 choices=[ 107 ('None', 0), 108 ('1 Minute', 60), 109 ('2 Minutes', 120), 110 ('5 Minutes', 300), 111 ('10 Minutes', 600), 112 ('20 Minutes', 1200), 113 ], 114 default=0, 115 ), 116 ba.FloatChoiceSetting( 117 'Respawn Times', 118 choices=[ 119 ('Shorter', 0.25), 120 ('Short', 0.5), 121 ('Normal', 1.0), 122 ('Long', 2.0), 123 ('Longer', 4.0), 124 ], 125 default=1.0, 126 ), 127 ba.BoolSetting('Epic Mode', default=False), 128 ] 129 130 @classmethod 131 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 132 # We only support two-team play. 133 return issubclass(sessiontype, ba.DualTeamSession) 134 135 @classmethod 136 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 137 return ba.getmaps('football') 138 139 def __init__(self, settings: dict): 140 super().__init__(settings) 141 self._scoreboard: Scoreboard | None = Scoreboard() 142 143 # Load some media we need. 144 self._cheer_sound = ba.getsound('cheer') 145 self._chant_sound = ba.getsound('crowdChant') 146 self._score_sound = ba.getsound('score') 147 self._swipsound = ba.getsound('swip') 148 self._whistle_sound = ba.getsound('refWhistle') 149 self._score_region_material = ba.Material() 150 self._score_region_material.add_actions( 151 conditions=('they_have_material', FlagFactory.get().flagmaterial), 152 actions=( 153 ('modify_part_collision', 'collide', True), 154 ('modify_part_collision', 'physical', False), 155 ('call', 'at_connect', self._handle_score), 156 ), 157 ) 158 self._flag_spawn_pos: Sequence[float] | None = None 159 self._score_regions: list[ba.NodeActor] = [] 160 self._flag: FootballFlag | None = None 161 self._flag_respawn_timer: ba.Timer | None = None 162 self._flag_respawn_light: ba.NodeActor | None = None 163 self._score_to_win = int(settings['Score to Win']) 164 self._time_limit = float(settings['Time Limit']) 165 self._epic_mode = bool(settings['Epic Mode']) 166 self.slow_motion = self._epic_mode 167 self.default_music = ( 168 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FOOTBALL 169 ) 170 171 def get_instance_description(self) -> str | Sequence: 172 touchdowns = self._score_to_win / 7 173 174 # NOTE: if use just touchdowns = self._score_to_win // 7 175 # and we will need to score, for example, 27 points, 176 # we will be required to score 3 (not 4) goals .. 177 touchdowns = math.ceil(touchdowns) 178 if touchdowns > 1: 179 return 'Score ${ARG1} touchdowns.', touchdowns 180 return 'Score a touchdown.' 181 182 def get_instance_description_short(self) -> str | Sequence: 183 touchdowns = self._score_to_win / 7 184 touchdowns = math.ceil(touchdowns) 185 if touchdowns > 1: 186 return 'score ${ARG1} touchdowns', touchdowns 187 return 'score a touchdown' 188 189 def on_begin(self) -> None: 190 super().on_begin() 191 self.setup_standard_time_limit(self._time_limit) 192 self.setup_standard_powerup_drops() 193 self._flag_spawn_pos = self.map.get_flag_position(None) 194 self._spawn_flag() 195 defs = self.map.defs 196 self._score_regions.append( 197 ba.NodeActor( 198 ba.newnode( 199 'region', 200 attrs={ 201 'position': defs.boxes['goal1'][0:3], 202 'scale': defs.boxes['goal1'][6:9], 203 'type': 'box', 204 'materials': (self._score_region_material,), 205 }, 206 ) 207 ) 208 ) 209 self._score_regions.append( 210 ba.NodeActor( 211 ba.newnode( 212 'region', 213 attrs={ 214 'position': defs.boxes['goal2'][0:3], 215 'scale': defs.boxes['goal2'][6:9], 216 'type': 'box', 217 'materials': (self._score_region_material,), 218 }, 219 ) 220 ) 221 ) 222 self._update_scoreboard() 223 ba.playsound(self._chant_sound) 224 225 def on_team_join(self, team: Team) -> None: 226 self._update_scoreboard() 227 228 def _kill_flag(self) -> None: 229 self._flag = None 230 231 def _handle_score(self) -> None: 232 """A point has been scored.""" 233 234 # Our flag might stick around for a second or two 235 # make sure it doesn't score again. 236 assert self._flag is not None 237 if self._flag.scored: 238 return 239 region = ba.getcollision().sourcenode 240 i = None 241 for i, score_region in enumerate(self._score_regions): 242 if region == score_region.node: 243 break 244 for team in self.teams: 245 if team.id == i: 246 team.score += 7 247 248 # Tell all players to celebrate. 249 for player in team.players: 250 if player.actor: 251 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 252 253 # If someone on this team was last to touch it, 254 # give them points. 255 assert self._flag is not None 256 if ( 257 self._flag.last_holding_player 258 and team == self._flag.last_holding_player.team 259 ): 260 self.stats.player_scored( 261 self._flag.last_holding_player, 50, big_message=True 262 ) 263 # End the game if we won. 264 if team.score >= self._score_to_win: 265 self.end_game() 266 ba.playsound(self._score_sound) 267 ba.playsound(self._cheer_sound) 268 assert self._flag 269 self._flag.scored = True 270 271 # Kill the flag (it'll respawn shortly). 272 ba.timer(1.0, self._kill_flag) 273 light = ba.newnode( 274 'light', 275 attrs={ 276 'position': ba.getcollision().position, 277 'height_attenuated': False, 278 'color': (1, 0, 0), 279 }, 280 ) 281 ba.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True) 282 ba.timer(1.0, light.delete) 283 ba.cameraflash(duration=10.0) 284 self._update_scoreboard() 285 286 def end_game(self) -> None: 287 results = ba.GameResults() 288 for team in self.teams: 289 results.set_team_score(team, team.score) 290 self.end(results=results, announce_delay=0.8) 291 292 def _update_scoreboard(self) -> None: 293 assert self._scoreboard is not None 294 for team in self.teams: 295 self._scoreboard.set_team_value( 296 team, team.score, self._score_to_win 297 ) 298 299 def handlemessage(self, msg: Any) -> Any: 300 if isinstance(msg, FlagPickedUpMessage): 301 assert isinstance(msg.flag, FootballFlag) 302 try: 303 msg.flag.last_holding_player = msg.node.getdelegate( 304 PlayerSpaz, True 305 ).getplayer(Player, True) 306 except ba.NotFoundError: 307 pass 308 msg.flag.held_count += 1 309 310 elif isinstance(msg, FlagDroppedMessage): 311 assert isinstance(msg.flag, FootballFlag) 312 msg.flag.held_count -= 1 313 314 # Respawn dead players if they're still in the game. 315 elif isinstance(msg, ba.PlayerDiedMessage): 316 # Augment standard behavior. 317 super().handlemessage(msg) 318 self.respawn_player(msg.getplayer(Player)) 319 320 # Respawn dead flags. 321 elif isinstance(msg, FlagDiedMessage): 322 if not self.has_ended(): 323 self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag) 324 self._flag_respawn_light = ba.NodeActor( 325 ba.newnode( 326 'light', 327 attrs={ 328 'position': self._flag_spawn_pos, 329 'height_attenuated': False, 330 'radius': 0.15, 331 'color': (1.0, 1.0, 0.3), 332 }, 333 ) 334 ) 335 assert self._flag_respawn_light.node 336 ba.animate( 337 self._flag_respawn_light.node, 338 'intensity', 339 {0.0: 0, 0.25: 0.15, 0.5: 0}, 340 loop=True, 341 ) 342 ba.timer(3.0, self._flag_respawn_light.node.delete) 343 344 else: 345 # Augment standard behavior. 346 super().handlemessage(msg) 347 348 def _flash_flag_spawn(self) -> None: 349 light = ba.newnode( 350 'light', 351 attrs={ 352 'position': self._flag_spawn_pos, 353 'height_attenuated': False, 354 'color': (1, 1, 0), 355 }, 356 ) 357 ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 358 ba.timer(1.0, light.delete) 359 360 def _spawn_flag(self) -> None: 361 ba.playsound(self._swipsound) 362 ba.playsound(self._whistle_sound) 363 self._flash_flag_spawn() 364 assert self._flag_spawn_pos is not None 365 self._flag = FootballFlag(position=self._flag_spawn_pos)
Football game for teams mode.
139 def __init__(self, settings: dict): 140 super().__init__(settings) 141 self._scoreboard: Scoreboard | None = Scoreboard() 142 143 # Load some media we need. 144 self._cheer_sound = ba.getsound('cheer') 145 self._chant_sound = ba.getsound('crowdChant') 146 self._score_sound = ba.getsound('score') 147 self._swipsound = ba.getsound('swip') 148 self._whistle_sound = ba.getsound('refWhistle') 149 self._score_region_material = ba.Material() 150 self._score_region_material.add_actions( 151 conditions=('they_have_material', FlagFactory.get().flagmaterial), 152 actions=( 153 ('modify_part_collision', 'collide', True), 154 ('modify_part_collision', 'physical', False), 155 ('call', 'at_connect', self._handle_score), 156 ), 157 ) 158 self._flag_spawn_pos: Sequence[float] | None = None 159 self._score_regions: list[ba.NodeActor] = [] 160 self._flag: FootballFlag | None = None 161 self._flag_respawn_timer: ba.Timer | None = None 162 self._flag_respawn_light: ba.NodeActor | None = None 163 self._score_to_win = int(settings['Score to Win']) 164 self._time_limit = float(settings['Time Limit']) 165 self._epic_mode = bool(settings['Epic Mode']) 166 self.slow_motion = self._epic_mode 167 self.default_music = ( 168 ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FOOTBALL 169 )
Instantiate the Activity.
130 @classmethod 131 def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool: 132 # We only support two-team play. 133 return issubclass(sessiontype, ba.DualTeamSession)
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
135 @classmethod 136 def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]: 137 return ba.getmaps('football')
Called by the default ba.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given ba.Session type.
171 def get_instance_description(self) -> str | Sequence: 172 touchdowns = self._score_to_win / 7 173 174 # NOTE: if use just touchdowns = self._score_to_win // 7 175 # and we will need to score, for example, 27 points, 176 # we will be required to score 3 (not 4) goals .. 177 touchdowns = math.ceil(touchdowns) 178 if touchdowns > 1: 179 return 'Score ${ARG1} touchdowns.', touchdowns 180 return 'Score a touchdown.'
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.
182 def get_instance_description_short(self) -> str | Sequence: 183 touchdowns = self._score_to_win / 7 184 touchdowns = math.ceil(touchdowns) 185 if touchdowns > 1: 186 return 'score ${ARG1} touchdowns', touchdowns 187 return 'score a touchdown'
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.
189 def on_begin(self) -> None: 190 super().on_begin() 191 self.setup_standard_time_limit(self._time_limit) 192 self.setup_standard_powerup_drops() 193 self._flag_spawn_pos = self.map.get_flag_position(None) 194 self._spawn_flag() 195 defs = self.map.defs 196 self._score_regions.append( 197 ba.NodeActor( 198 ba.newnode( 199 'region', 200 attrs={ 201 'position': defs.boxes['goal1'][0:3], 202 'scale': defs.boxes['goal1'][6:9], 203 'type': 'box', 204 'materials': (self._score_region_material,), 205 }, 206 ) 207 ) 208 ) 209 self._score_regions.append( 210 ba.NodeActor( 211 ba.newnode( 212 'region', 213 attrs={ 214 'position': defs.boxes['goal2'][0:3], 215 'scale': defs.boxes['goal2'][6:9], 216 'type': 'box', 217 'materials': (self._score_region_material,), 218 }, 219 ) 220 ) 221 ) 222 self._update_scoreboard() 223 ba.playsound(self._chant_sound)
Called once the previous ba.Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
286 def end_game(self) -> None: 287 results = ba.GameResults() 288 for team in self.teams: 289 results.set_team_score(team, team.score) 290 self.end(results=results, announce_delay=0.8)
Tell the game to wrap up and call ba.Activity.end() immediately.
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (ba.GameActivity.setup_standard_time_limit()) will work with the game.
299 def handlemessage(self, msg: Any) -> Any: 300 if isinstance(msg, FlagPickedUpMessage): 301 assert isinstance(msg.flag, FootballFlag) 302 try: 303 msg.flag.last_holding_player = msg.node.getdelegate( 304 PlayerSpaz, True 305 ).getplayer(Player, True) 306 except ba.NotFoundError: 307 pass 308 msg.flag.held_count += 1 309 310 elif isinstance(msg, FlagDroppedMessage): 311 assert isinstance(msg.flag, FootballFlag) 312 msg.flag.held_count -= 1 313 314 # Respawn dead players if they're still in the game. 315 elif isinstance(msg, ba.PlayerDiedMessage): 316 # Augment standard behavior. 317 super().handlemessage(msg) 318 self.respawn_player(msg.getplayer(Player)) 319 320 # Respawn dead flags. 321 elif isinstance(msg, FlagDiedMessage): 322 if not self.has_ended(): 323 self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag) 324 self._flag_respawn_light = ba.NodeActor( 325 ba.newnode( 326 'light', 327 attrs={ 328 'position': self._flag_spawn_pos, 329 'height_attenuated': False, 330 'radius': 0.15, 331 'color': (1.0, 1.0, 0.3), 332 }, 333 ) 334 ) 335 assert self._flag_respawn_light.node 336 ba.animate( 337 self._flag_respawn_light.node, 338 'intensity', 339 {0.0: 0, 0.25: 0.15, 0.5: 0}, 340 loop=True, 341 ) 342 ba.timer(3.0, self._flag_respawn_light.node.delete) 343 344 else: 345 # Augment standard behavior. 346 super().handlemessage(msg)
General message handling; can be passed any message object.
Inherited Members
- ba._teamgame.TeamGameActivity
- on_transition_in
- spawn_player_spaz
- end
- ba._gameactivity.GameActivity
- allow_pausing
- allow_kick_idle_players
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_settings_display_string
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- respawn_player
- spawn_player_if_exists
- spawn_player
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- ba._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps
368class FootballCoopGame(ba.CoopGameActivity[Player, Team]): 369 """Co-op variant of football.""" 370 371 name = 'Football' 372 tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] 373 scoreconfig = ba.ScoreConfig( 374 scoretype=ba.ScoreType.MILLISECONDS, version='B' 375 ) 376 377 default_music = ba.MusicType.FOOTBALL 378 379 # FIXME: Need to update co-op games to use getscoreconfig. 380 def get_score_type(self) -> str: 381 return 'time' 382 383 def get_instance_description(self) -> str | Sequence: 384 touchdowns = self._score_to_win / 7 385 touchdowns = math.ceil(touchdowns) 386 if touchdowns > 1: 387 return 'Score ${ARG1} touchdowns.', touchdowns 388 return 'Score a touchdown.' 389 390 def get_instance_description_short(self) -> str | Sequence: 391 touchdowns = self._score_to_win / 7 392 touchdowns = math.ceil(touchdowns) 393 if touchdowns > 1: 394 return 'score ${ARG1} touchdowns', touchdowns 395 return 'score a touchdown' 396 397 def __init__(self, settings: dict): 398 settings['map'] = 'Football Stadium' 399 super().__init__(settings) 400 self._preset = settings.get('preset', 'rookie') 401 402 # Load some media we need. 403 self._cheer_sound = ba.getsound('cheer') 404 self._boo_sound = ba.getsound('boo') 405 self._chant_sound = ba.getsound('crowdChant') 406 self._score_sound = ba.getsound('score') 407 self._swipsound = ba.getsound('swip') 408 self._whistle_sound = ba.getsound('refWhistle') 409 self._score_to_win = 21 410 self._score_region_material = ba.Material() 411 self._score_region_material.add_actions( 412 conditions=('they_have_material', FlagFactory.get().flagmaterial), 413 actions=( 414 ('modify_part_collision', 'collide', True), 415 ('modify_part_collision', 'physical', False), 416 ('call', 'at_connect', self._handle_score), 417 ), 418 ) 419 self._powerup_center = (0, 2, 0) 420 self._powerup_spread = (10, 5.5) 421 self._player_has_dropped_bomb = False 422 self._player_has_punched = False 423 self._scoreboard: Scoreboard | None = None 424 self._flag_spawn_pos: Sequence[float] | None = None 425 self._score_regions: list[ba.NodeActor] = [] 426 self._exclude_powerups: list[str] = [] 427 self._have_tnt = False 428 self._bot_types_initial: list[type[SpazBot]] | None = None 429 self._bot_types_7: list[type[SpazBot]] | None = None 430 self._bot_types_14: list[type[SpazBot]] | None = None 431 self._bot_team: Team | None = None 432 self._starttime_ms: int | None = None 433 self._time_text: ba.NodeActor | None = None 434 self._time_text_input: ba.NodeActor | None = None 435 self._tntspawner: TNTSpawner | None = None 436 self._bots = SpazBotSet() 437 self._bot_spawn_timer: ba.Timer | None = None 438 self._powerup_drop_timer: ba.Timer | None = None 439 self._scoring_team: Team | None = None 440 self._final_time_ms: int | None = None 441 self._time_text_timer: ba.Timer | None = None 442 self._flag_respawn_light: ba.Actor | None = None 443 self._flag: FootballFlag | None = None 444 445 def on_transition_in(self) -> None: 446 super().on_transition_in() 447 self._scoreboard = Scoreboard() 448 self._flag_spawn_pos = self.map.get_flag_position(None) 449 self._spawn_flag() 450 451 # Set up the two score regions. 452 defs = self.map.defs 453 self._score_regions.append( 454 ba.NodeActor( 455 ba.newnode( 456 'region', 457 attrs={ 458 'position': defs.boxes['goal1'][0:3], 459 'scale': defs.boxes['goal1'][6:9], 460 'type': 'box', 461 'materials': [self._score_region_material], 462 }, 463 ) 464 ) 465 ) 466 self._score_regions.append( 467 ba.NodeActor( 468 ba.newnode( 469 'region', 470 attrs={ 471 'position': defs.boxes['goal2'][0:3], 472 'scale': defs.boxes['goal2'][6:9], 473 'type': 'box', 474 'materials': [self._score_region_material], 475 }, 476 ) 477 ) 478 ) 479 ba.playsound(self._chant_sound) 480 481 def on_begin(self) -> None: 482 # FIXME: Split this up a bit. 483 # pylint: disable=too-many-statements 484 from bastd.actor import controlsguide 485 486 super().on_begin() 487 488 # Show controls help in kiosk mode. 489 if ba.app.demo_mode or ba.app.arcade_mode: 490 controlsguide.ControlsGuide( 491 delay=3.0, lifespan=10.0, bright=True 492 ).autoretain() 493 assert self.initialplayerinfos is not None 494 abot: type[SpazBot] 495 bbot: type[SpazBot] 496 cbot: type[SpazBot] 497 if self._preset in ['rookie', 'rookie_easy']: 498 self._exclude_powerups = ['curse'] 499 self._have_tnt = False 500 abot = ( 501 BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot 502 ) 503 self._bot_types_initial = [abot] * len(self.initialplayerinfos) 504 bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot 505 self._bot_types_7 = [bbot] * ( 506 1 if len(self.initialplayerinfos) < 3 else 2 507 ) 508 cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot 509 self._bot_types_14 = [cbot] * ( 510 1 if len(self.initialplayerinfos) < 3 else 2 511 ) 512 elif self._preset == 'tournament': 513 self._exclude_powerups = [] 514 self._have_tnt = True 515 self._bot_types_initial = [BrawlerBot] * ( 516 1 if len(self.initialplayerinfos) < 2 else 2 517 ) 518 self._bot_types_7 = [TriggerBot] * ( 519 1 if len(self.initialplayerinfos) < 3 else 2 520 ) 521 self._bot_types_14 = [ChargerBot] * ( 522 1 if len(self.initialplayerinfos) < 4 else 2 523 ) 524 elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: 525 self._exclude_powerups = ['curse'] 526 self._have_tnt = True 527 self._bot_types_initial = [ChargerBot] * len( 528 self.initialplayerinfos 529 ) 530 abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite 531 typed_bot_list: list[type[SpazBot]] = [] 532 self._bot_types_7 = ( 533 typed_bot_list 534 + [abot] 535 + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) 536 ) 537 bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot 538 self._bot_types_14 = [bbot] * ( 539 1 if len(self.initialplayerinfos) < 3 else 2 540 ) 541 elif self._preset in ['uber', 'uber_easy']: 542 self._exclude_powerups = [] 543 self._have_tnt = True 544 abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot 545 bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot 546 typed_bot_list_2: list[type[SpazBot]] = [] 547 self._bot_types_initial = ( 548 typed_bot_list_2 549 + [StickyBot] 550 + [abot] * len(self.initialplayerinfos) 551 ) 552 self._bot_types_7 = [bbot] * ( 553 1 if len(self.initialplayerinfos) < 3 else 2 554 ) 555 self._bot_types_14 = [ExplodeyBot] * ( 556 1 if len(self.initialplayerinfos) < 3 else 2 557 ) 558 else: 559 raise Exception() 560 561 self.setup_low_life_warning_sound() 562 563 self._drop_powerups(standard_points=True) 564 ba.timer(4.0, self._start_powerup_drops) 565 566 # Make a bogus team for our bots. 567 bad_team_name = self.get_team_display_string('Bad Guys') 568 self._bot_team = Team() 569 self._bot_team.manual_init( 570 team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) 571 ) 572 573 for team in [self.teams[0], self._bot_team]: 574 team.score = 0 575 576 self.update_scores() 577 578 # Time display. 579 starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) 580 assert isinstance(starttime_ms, int) 581 self._starttime_ms = starttime_ms 582 self._time_text = ba.NodeActor( 583 ba.newnode( 584 'text', 585 attrs={ 586 'v_attach': 'top', 587 'h_attach': 'center', 588 'h_align': 'center', 589 'color': (1, 1, 0.5, 1), 590 'flatness': 0.5, 591 'shadow': 0.5, 592 'position': (0, -50), 593 'scale': 1.3, 594 'text': '', 595 }, 596 ) 597 ) 598 self._time_text_input = ba.NodeActor( 599 ba.newnode('timedisplay', attrs={'showsubseconds': True}) 600 ) 601 self.globalsnode.connectattr( 602 'time', self._time_text_input.node, 'time2' 603 ) 604 assert self._time_text_input.node 605 assert self._time_text.node 606 self._time_text_input.node.connectattr( 607 'output', self._time_text.node, 'text' 608 ) 609 610 # Our TNT spawner (if applicable). 611 if self._have_tnt: 612 self._tntspawner = TNTSpawner(position=(0, 1, -1)) 613 614 self._bots = SpazBotSet() 615 self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) 616 617 for bottype in self._bot_types_initial: 618 self._spawn_bot(bottype) 619 620 def _on_bot_spawn(self, spaz: SpazBot) -> None: 621 # We want to move to the left by default. 622 spaz.target_point_default = ba.Vec3(0, 0, 0) 623 624 def _spawn_bot( 625 self, spaz_type: type[SpazBot], immediate: bool = False 626 ) -> None: 627 assert self._bot_team is not None 628 pos = self.map.get_start_position(self._bot_team.id) 629 self._bots.spawn_bot( 630 spaz_type, 631 pos=pos, 632 spawn_time=0.001 if immediate else 3.0, 633 on_spawn_call=self._on_bot_spawn, 634 ) 635 636 def _update_bots(self) -> None: 637 bots = self._bots.get_living_bots() 638 for bot in bots: 639 bot.target_flag = None 640 641 # If we're waiting on a continue, stop here so they don't keep scoring. 642 if self.is_waiting_for_continue(): 643 self._bots.stop_moving() 644 return 645 646 # If we've got a flag and no player are holding it, find the closest 647 # bot to it, and make them the designated flag-bearer. 648 assert self._flag is not None 649 if self._flag.node: 650 for player in self.players: 651 if player.actor: 652 assert isinstance(player.actor, PlayerSpaz) 653 if ( 654 player.actor.is_alive() 655 and player.actor.node.hold_node == self._flag.node 656 ): 657 return 658 659 flagpos = ba.Vec3(self._flag.node.position) 660 closest_bot: SpazBot | None = None 661 closest_dist = 0.0 # Always gets assigned first time through. 662 for bot in bots: 663 # If a bot is picked up, he should forget about the flag. 664 if bot.held_count > 0: 665 continue 666 assert bot.node 667 botpos = ba.Vec3(bot.node.position) 668 botdist = (botpos - flagpos).length() 669 if closest_bot is None or botdist < closest_dist: 670 closest_bot = bot 671 closest_dist = botdist 672 if closest_bot is not None: 673 closest_bot.target_flag = self._flag 674 675 def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: 676 if poweruptype is None: 677 poweruptype = PowerupBoxFactory.get().get_random_powerup_type( 678 excludetypes=self._exclude_powerups 679 ) 680 PowerupBox( 681 position=self.map.powerup_spawn_points[index], 682 poweruptype=poweruptype, 683 ).autoretain() 684 685 def _start_powerup_drops(self) -> None: 686 self._powerup_drop_timer = ba.Timer( 687 3.0, self._drop_powerups, repeat=True 688 ) 689 690 def _drop_powerups( 691 self, standard_points: bool = False, poweruptype: str | None = None 692 ) -> None: 693 """Generic powerup drop.""" 694 if standard_points: 695 spawnpoints = self.map.powerup_spawn_points 696 for i, _point in enumerate(spawnpoints): 697 ba.timer( 698 1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype) 699 ) 700 else: 701 point = ( 702 self._powerup_center[0] 703 + random.uniform( 704 -1.0 * self._powerup_spread[0], 705 1.0 * self._powerup_spread[0], 706 ), 707 self._powerup_center[1], 708 self._powerup_center[2] 709 + random.uniform( 710 -self._powerup_spread[1], self._powerup_spread[1] 711 ), 712 ) 713 714 # Drop one random one somewhere. 715 PowerupBox( 716 position=point, 717 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 718 excludetypes=self._exclude_powerups 719 ), 720 ).autoretain() 721 722 def _kill_flag(self) -> None: 723 try: 724 assert self._flag is not None 725 self._flag.handlemessage(ba.DieMessage()) 726 except Exception: 727 ba.print_exception('Error in _kill_flag.') 728 729 def _handle_score(self) -> None: 730 """a point has been scored""" 731 # FIXME tidy this up 732 # pylint: disable=too-many-branches 733 734 # Our flag might stick around for a second or two; 735 # we don't want it to be able to score again. 736 assert self._flag is not None 737 if self._flag.scored: 738 return 739 740 # See which score region it was. 741 region = ba.getcollision().sourcenode 742 i = None 743 for i, score_region in enumerate(self._score_regions): 744 if region == score_region.node: 745 break 746 747 for team in [self.teams[0], self._bot_team]: 748 assert team is not None 749 if team.id == i: 750 team.score += 7 751 752 # Tell all players (or bots) to celebrate. 753 if i == 0: 754 for player in team.players: 755 if player.actor: 756 player.actor.handlemessage(ba.CelebrateMessage(2.0)) 757 else: 758 self._bots.celebrate(2.0) 759 760 # If the good guys scored, add more enemies. 761 if i == 0: 762 if self.teams[0].score == 7: 763 assert self._bot_types_7 is not None 764 for bottype in self._bot_types_7: 765 self._spawn_bot(bottype) 766 elif self.teams[0].score == 14: 767 assert self._bot_types_14 is not None 768 for bottype in self._bot_types_14: 769 self._spawn_bot(bottype) 770 771 ba.playsound(self._score_sound) 772 if i == 0: 773 ba.playsound(self._cheer_sound) 774 else: 775 ba.playsound(self._boo_sound) 776 777 # Kill the flag (it'll respawn shortly). 778 self._flag.scored = True 779 780 ba.timer(0.2, self._kill_flag) 781 782 self.update_scores() 783 light = ba.newnode( 784 'light', 785 attrs={ 786 'position': ba.getcollision().position, 787 'height_attenuated': False, 788 'color': (1, 0, 0), 789 }, 790 ) 791 ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 792 ba.timer(1.0, light.delete) 793 if i == 0: 794 ba.cameraflash(duration=10.0) 795 796 def end_game(self) -> None: 797 ba.setmusic(None) 798 self._bots.final_celebrate() 799 ba.timer(0.001, ba.Call(self.do_end, 'defeat')) 800 801 def on_continue(self) -> None: 802 # Subtract one touchdown from the bots and get them moving again. 803 assert self._bot_team is not None 804 self._bot_team.score -= 7 805 self._bots.start_moving() 806 self.update_scores() 807 808 def update_scores(self) -> None: 809 """update scoreboard and check for winners""" 810 # FIXME: tidy this up 811 # pylint: disable=too-many-nested-blocks 812 have_scoring_team = False 813 win_score = self._score_to_win 814 for team in [self.teams[0], self._bot_team]: 815 assert team is not None 816 assert self._scoreboard is not None 817 self._scoreboard.set_team_value(team, team.score, win_score) 818 if team.score >= win_score: 819 if not have_scoring_team: 820 self._scoring_team = team 821 if team is self._bot_team: 822 self.continue_or_end_game() 823 else: 824 ba.setmusic(ba.MusicType.VICTORY) 825 826 # Completion achievements. 827 assert self._bot_team is not None 828 if self._preset in ['rookie', 'rookie_easy']: 829 self._award_achievement( 830 'Rookie Football Victory', sound=False 831 ) 832 if self._bot_team.score == 0: 833 self._award_achievement( 834 'Rookie Football Shutout', sound=False 835 ) 836 elif self._preset in ['pro', 'pro_easy']: 837 self._award_achievement( 838 'Pro Football Victory', sound=False 839 ) 840 if self._bot_team.score == 0: 841 self._award_achievement( 842 'Pro Football Shutout', sound=False 843 ) 844 elif self._preset in ['uber', 'uber_easy']: 845 self._award_achievement( 846 'Uber Football Victory', sound=False 847 ) 848 if self._bot_team.score == 0: 849 self._award_achievement( 850 'Uber Football Shutout', sound=False 851 ) 852 if ( 853 not self._player_has_dropped_bomb 854 and not self._player_has_punched 855 ): 856 self._award_achievement( 857 'Got the Moves', sound=False 858 ) 859 self._bots.stop_moving() 860 self.show_zoom_message( 861 ba.Lstr(resource='victoryText'), 862 scale=1.0, 863 duration=4.0, 864 ) 865 self.celebrate(10.0) 866 assert self._starttime_ms is not None 867 self._final_time_ms = int( 868 ba.time(timeformat=ba.TimeFormat.MILLISECONDS) 869 - self._starttime_ms 870 ) 871 self._time_text_timer = None 872 assert ( 873 self._time_text_input is not None 874 and self._time_text_input.node 875 ) 876 self._time_text_input.node.timemax = self._final_time_ms 877 878 # FIXME: Does this still need to be deferred? 879 ba.pushcall(ba.Call(self.do_end, 'victory')) 880 881 def do_end(self, outcome: str) -> None: 882 """End the game with the specified outcome.""" 883 if outcome == 'defeat': 884 self.fade_to_red() 885 assert self._final_time_ms is not None 886 scoreval = ( 887 None if outcome == 'defeat' else int(self._final_time_ms // 10) 888 ) 889 self.end( 890 delay=3.0, 891 results={ 892 'outcome': outcome, 893 'score': scoreval, 894 'score_order': 'decreasing', 895 'playerinfos': self.initialplayerinfos, 896 }, 897 ) 898 899 def handlemessage(self, msg: Any) -> Any: 900 """handle high-level game messages""" 901 if isinstance(msg, ba.PlayerDiedMessage): 902 # Augment standard behavior. 903 super().handlemessage(msg) 904 905 # Respawn them shortly. 906 player = msg.getplayer(Player) 907 assert self.initialplayerinfos is not None 908 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 909 player.respawn_timer = ba.Timer( 910 respawn_time, ba.Call(self.spawn_player_if_exists, player) 911 ) 912 player.respawn_icon = RespawnIcon(player, respawn_time) 913 914 elif isinstance(msg, SpazBotDiedMessage): 915 916 # Every time a bad guy dies, spawn a new one. 917 ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) 918 919 elif isinstance(msg, SpazBotPunchedMessage): 920 if self._preset in ['rookie', 'rookie_easy']: 921 if msg.damage >= 500: 922 self._award_achievement('Super Punch') 923 elif self._preset in ['pro', 'pro_easy']: 924 if msg.damage >= 1000: 925 self._award_achievement('Super Mega Punch') 926 927 # Respawn dead flags. 928 elif isinstance(msg, FlagDiedMessage): 929 assert isinstance(msg.flag, FootballFlag) 930 msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) 931 self._flag_respawn_light = ba.NodeActor( 932 ba.newnode( 933 'light', 934 attrs={ 935 'position': self._flag_spawn_pos, 936 'height_attenuated': False, 937 'radius': 0.15, 938 'color': (1.0, 1.0, 0.3), 939 }, 940 ) 941 ) 942 assert self._flag_respawn_light.node 943 ba.animate( 944 self._flag_respawn_light.node, 945 'intensity', 946 {0: 0, 0.25: 0.15, 0.5: 0}, 947 loop=True, 948 ) 949 ba.timer(3.0, self._flag_respawn_light.node.delete) 950 else: 951 return super().handlemessage(msg) 952 return None 953 954 def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None: 955 del player, bomb # Unused. 956 self._player_has_dropped_bomb = True 957 958 def _handle_player_punched(self, player: Spaz) -> None: 959 del player # Unused. 960 self._player_has_punched = True 961 962 def spawn_player(self, player: Player) -> ba.Actor: 963 spaz = self.spawn_player_spaz( 964 player, position=self.map.get_start_position(player.team.id) 965 ) 966 if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: 967 spaz.impact_scale = 0.25 968 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 969 spaz.punch_callback = self._handle_player_punched 970 return spaz 971 972 def _flash_flag_spawn(self) -> None: 973 light = ba.newnode( 974 'light', 975 attrs={ 976 'position': self._flag_spawn_pos, 977 'height_attenuated': False, 978 'color': (1, 1, 0), 979 }, 980 ) 981 ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 982 ba.timer(1.0, light.delete) 983 984 def _spawn_flag(self) -> None: 985 ba.playsound(self._swipsound) 986 ba.playsound(self._whistle_sound) 987 self._flash_flag_spawn() 988 assert self._flag_spawn_pos is not None 989 self._flag = FootballFlag(position=self._flag_spawn_pos)
Co-op variant of football.
397 def __init__(self, settings: dict): 398 settings['map'] = 'Football Stadium' 399 super().__init__(settings) 400 self._preset = settings.get('preset', 'rookie') 401 402 # Load some media we need. 403 self._cheer_sound = ba.getsound('cheer') 404 self._boo_sound = ba.getsound('boo') 405 self._chant_sound = ba.getsound('crowdChant') 406 self._score_sound = ba.getsound('score') 407 self._swipsound = ba.getsound('swip') 408 self._whistle_sound = ba.getsound('refWhistle') 409 self._score_to_win = 21 410 self._score_region_material = ba.Material() 411 self._score_region_material.add_actions( 412 conditions=('they_have_material', FlagFactory.get().flagmaterial), 413 actions=( 414 ('modify_part_collision', 'collide', True), 415 ('modify_part_collision', 'physical', False), 416 ('call', 'at_connect', self._handle_score), 417 ), 418 ) 419 self._powerup_center = (0, 2, 0) 420 self._powerup_spread = (10, 5.5) 421 self._player_has_dropped_bomb = False 422 self._player_has_punched = False 423 self._scoreboard: Scoreboard | None = None 424 self._flag_spawn_pos: Sequence[float] | None = None 425 self._score_regions: list[ba.NodeActor] = [] 426 self._exclude_powerups: list[str] = [] 427 self._have_tnt = False 428 self._bot_types_initial: list[type[SpazBot]] | None = None 429 self._bot_types_7: list[type[SpazBot]] | None = None 430 self._bot_types_14: list[type[SpazBot]] | None = None 431 self._bot_team: Team | None = None 432 self._starttime_ms: int | None = None 433 self._time_text: ba.NodeActor | None = None 434 self._time_text_input: ba.NodeActor | None = None 435 self._tntspawner: TNTSpawner | None = None 436 self._bots = SpazBotSet() 437 self._bot_spawn_timer: ba.Timer | None = None 438 self._powerup_drop_timer: ba.Timer | None = None 439 self._scoring_team: Team | None = None 440 self._final_time_ms: int | None = None 441 self._time_text_timer: ba.Timer | None = None 442 self._flag_respawn_light: ba.Actor | None = None 443 self._flag: FootballFlag | None = None
Instantiate the Activity.
Return the score unit this co-op game uses ('point', 'seconds', etc.)
383 def get_instance_description(self) -> str | Sequence: 384 touchdowns = self._score_to_win / 7 385 touchdowns = math.ceil(touchdowns) 386 if touchdowns > 1: 387 return 'Score ${ARG1} touchdowns.', touchdowns 388 return 'Score a touchdown.'
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.
390 def get_instance_description_short(self) -> str | Sequence: 391 touchdowns = self._score_to_win / 7 392 touchdowns = math.ceil(touchdowns) 393 if touchdowns > 1: 394 return 'score ${ARG1} touchdowns', touchdowns 395 return 'score a touchdown'
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.
445 def on_transition_in(self) -> None: 446 super().on_transition_in() 447 self._scoreboard = Scoreboard() 448 self._flag_spawn_pos = self.map.get_flag_position(None) 449 self._spawn_flag() 450 451 # Set up the two score regions. 452 defs = self.map.defs 453 self._score_regions.append( 454 ba.NodeActor( 455 ba.newnode( 456 'region', 457 attrs={ 458 'position': defs.boxes['goal1'][0:3], 459 'scale': defs.boxes['goal1'][6:9], 460 'type': 'box', 461 'materials': [self._score_region_material], 462 }, 463 ) 464 ) 465 ) 466 self._score_regions.append( 467 ba.NodeActor( 468 ba.newnode( 469 'region', 470 attrs={ 471 'position': defs.boxes['goal2'][0:3], 472 'scale': defs.boxes['goal2'][6:9], 473 'type': 'box', 474 'materials': [self._score_region_material], 475 }, 476 ) 477 ) 478 ) 479 ba.playsound(self._chant_sound)
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until ba.Activity.on_begin() is called.
481 def on_begin(self) -> None: 482 # FIXME: Split this up a bit. 483 # pylint: disable=too-many-statements 484 from bastd.actor import controlsguide 485 486 super().on_begin() 487 488 # Show controls help in kiosk mode. 489 if ba.app.demo_mode or ba.app.arcade_mode: 490 controlsguide.ControlsGuide( 491 delay=3.0, lifespan=10.0, bright=True 492 ).autoretain() 493 assert self.initialplayerinfos is not None 494 abot: type[SpazBot] 495 bbot: type[SpazBot] 496 cbot: type[SpazBot] 497 if self._preset in ['rookie', 'rookie_easy']: 498 self._exclude_powerups = ['curse'] 499 self._have_tnt = False 500 abot = ( 501 BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot 502 ) 503 self._bot_types_initial = [abot] * len(self.initialplayerinfos) 504 bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot 505 self._bot_types_7 = [bbot] * ( 506 1 if len(self.initialplayerinfos) < 3 else 2 507 ) 508 cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot 509 self._bot_types_14 = [cbot] * ( 510 1 if len(self.initialplayerinfos) < 3 else 2 511 ) 512 elif self._preset == 'tournament': 513 self._exclude_powerups = [] 514 self._have_tnt = True 515 self._bot_types_initial = [BrawlerBot] * ( 516 1 if len(self.initialplayerinfos) < 2 else 2 517 ) 518 self._bot_types_7 = [TriggerBot] * ( 519 1 if len(self.initialplayerinfos) < 3 else 2 520 ) 521 self._bot_types_14 = [ChargerBot] * ( 522 1 if len(self.initialplayerinfos) < 4 else 2 523 ) 524 elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: 525 self._exclude_powerups = ['curse'] 526 self._have_tnt = True 527 self._bot_types_initial = [ChargerBot] * len( 528 self.initialplayerinfos 529 ) 530 abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite 531 typed_bot_list: list[type[SpazBot]] = [] 532 self._bot_types_7 = ( 533 typed_bot_list 534 + [abot] 535 + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) 536 ) 537 bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot 538 self._bot_types_14 = [bbot] * ( 539 1 if len(self.initialplayerinfos) < 3 else 2 540 ) 541 elif self._preset in ['uber', 'uber_easy']: 542 self._exclude_powerups = [] 543 self._have_tnt = True 544 abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot 545 bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot 546 typed_bot_list_2: list[type[SpazBot]] = [] 547 self._bot_types_initial = ( 548 typed_bot_list_2 549 + [StickyBot] 550 + [abot] * len(self.initialplayerinfos) 551 ) 552 self._bot_types_7 = [bbot] * ( 553 1 if len(self.initialplayerinfos) < 3 else 2 554 ) 555 self._bot_types_14 = [ExplodeyBot] * ( 556 1 if len(self.initialplayerinfos) < 3 else 2 557 ) 558 else: 559 raise Exception() 560 561 self.setup_low_life_warning_sound() 562 563 self._drop_powerups(standard_points=True) 564 ba.timer(4.0, self._start_powerup_drops) 565 566 # Make a bogus team for our bots. 567 bad_team_name = self.get_team_display_string('Bad Guys') 568 self._bot_team = Team() 569 self._bot_team.manual_init( 570 team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) 571 ) 572 573 for team in [self.teams[0], self._bot_team]: 574 team.score = 0 575 576 self.update_scores() 577 578 # Time display. 579 starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) 580 assert isinstance(starttime_ms, int) 581 self._starttime_ms = starttime_ms 582 self._time_text = ba.NodeActor( 583 ba.newnode( 584 'text', 585 attrs={ 586 'v_attach': 'top', 587 'h_attach': 'center', 588 'h_align': 'center', 589 'color': (1, 1, 0.5, 1), 590 'flatness': 0.5, 591 'shadow': 0.5, 592 'position': (0, -50), 593 'scale': 1.3, 594 'text': '', 595 }, 596 ) 597 ) 598 self._time_text_input = ba.NodeActor( 599 ba.newnode('timedisplay', attrs={'showsubseconds': True}) 600 ) 601 self.globalsnode.connectattr( 602 'time', self._time_text_input.node, 'time2' 603 ) 604 assert self._time_text_input.node 605 assert self._time_text.node 606 self._time_text_input.node.connectattr( 607 'output', self._time_text.node, 'text' 608 ) 609 610 # Our TNT spawner (if applicable). 611 if self._have_tnt: 612 self._tntspawner = TNTSpawner(position=(0, 1, -1)) 613 614 self._bots = SpazBotSet() 615 self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) 616 617 for bottype in self._bot_types_initial: 618 self._spawn_bot(bottype)
Called once the previous ba.Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
796 def end_game(self) -> None: 797 ba.setmusic(None) 798 self._bots.final_celebrate() 799 ba.timer(0.001, ba.Call(self.do_end, 'defeat'))
Tell the game to wrap up and call ba.Activity.end() immediately.
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (ba.GameActivity.setup_standard_time_limit()) will work with the game.
801 def on_continue(self) -> None: 802 # Subtract one touchdown from the bots and get them moving again. 803 assert self._bot_team is not None 804 self._bot_team.score -= 7 805 self._bots.start_moving() 806 self.update_scores()
This is called if a game supports and offers a continue and the player accepts. In this case the player should be given an extra life or whatever is relevant to keep the game going.
808 def update_scores(self) -> None: 809 """update scoreboard and check for winners""" 810 # FIXME: tidy this up 811 # pylint: disable=too-many-nested-blocks 812 have_scoring_team = False 813 win_score = self._score_to_win 814 for team in [self.teams[0], self._bot_team]: 815 assert team is not None 816 assert self._scoreboard is not None 817 self._scoreboard.set_team_value(team, team.score, win_score) 818 if team.score >= win_score: 819 if not have_scoring_team: 820 self._scoring_team = team 821 if team is self._bot_team: 822 self.continue_or_end_game() 823 else: 824 ba.setmusic(ba.MusicType.VICTORY) 825 826 # Completion achievements. 827 assert self._bot_team is not None 828 if self._preset in ['rookie', 'rookie_easy']: 829 self._award_achievement( 830 'Rookie Football Victory', sound=False 831 ) 832 if self._bot_team.score == 0: 833 self._award_achievement( 834 'Rookie Football Shutout', sound=False 835 ) 836 elif self._preset in ['pro', 'pro_easy']: 837 self._award_achievement( 838 'Pro Football Victory', sound=False 839 ) 840 if self._bot_team.score == 0: 841 self._award_achievement( 842 'Pro Football Shutout', sound=False 843 ) 844 elif self._preset in ['uber', 'uber_easy']: 845 self._award_achievement( 846 'Uber Football Victory', sound=False 847 ) 848 if self._bot_team.score == 0: 849 self._award_achievement( 850 'Uber Football Shutout', sound=False 851 ) 852 if ( 853 not self._player_has_dropped_bomb 854 and not self._player_has_punched 855 ): 856 self._award_achievement( 857 'Got the Moves', sound=False 858 ) 859 self._bots.stop_moving() 860 self.show_zoom_message( 861 ba.Lstr(resource='victoryText'), 862 scale=1.0, 863 duration=4.0, 864 ) 865 self.celebrate(10.0) 866 assert self._starttime_ms is not None 867 self._final_time_ms = int( 868 ba.time(timeformat=ba.TimeFormat.MILLISECONDS) 869 - self._starttime_ms 870 ) 871 self._time_text_timer = None 872 assert ( 873 self._time_text_input is not None 874 and self._time_text_input.node 875 ) 876 self._time_text_input.node.timemax = self._final_time_ms 877 878 # FIXME: Does this still need to be deferred? 879 ba.pushcall(ba.Call(self.do_end, 'victory'))
update scoreboard and check for winners
881 def do_end(self, outcome: str) -> None: 882 """End the game with the specified outcome.""" 883 if outcome == 'defeat': 884 self.fade_to_red() 885 assert self._final_time_ms is not None 886 scoreval = ( 887 None if outcome == 'defeat' else int(self._final_time_ms // 10) 888 ) 889 self.end( 890 delay=3.0, 891 results={ 892 'outcome': outcome, 893 'score': scoreval, 894 'score_order': 'decreasing', 895 'playerinfos': self.initialplayerinfos, 896 }, 897 )
End the game with the specified outcome.
899 def handlemessage(self, msg: Any) -> Any: 900 """handle high-level game messages""" 901 if isinstance(msg, ba.PlayerDiedMessage): 902 # Augment standard behavior. 903 super().handlemessage(msg) 904 905 # Respawn them shortly. 906 player = msg.getplayer(Player) 907 assert self.initialplayerinfos is not None 908 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 909 player.respawn_timer = ba.Timer( 910 respawn_time, ba.Call(self.spawn_player_if_exists, player) 911 ) 912 player.respawn_icon = RespawnIcon(player, respawn_time) 913 914 elif isinstance(msg, SpazBotDiedMessage): 915 916 # Every time a bad guy dies, spawn a new one. 917 ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) 918 919 elif isinstance(msg, SpazBotPunchedMessage): 920 if self._preset in ['rookie', 'rookie_easy']: 921 if msg.damage >= 500: 922 self._award_achievement('Super Punch') 923 elif self._preset in ['pro', 'pro_easy']: 924 if msg.damage >= 1000: 925 self._award_achievement('Super Mega Punch') 926 927 # Respawn dead flags. 928 elif isinstance(msg, FlagDiedMessage): 929 assert isinstance(msg.flag, FootballFlag) 930 msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) 931 self._flag_respawn_light = ba.NodeActor( 932 ba.newnode( 933 'light', 934 attrs={ 935 'position': self._flag_spawn_pos, 936 'height_attenuated': False, 937 'radius': 0.15, 938 'color': (1.0, 1.0, 0.3), 939 }, 940 ) 941 ) 942 assert self._flag_respawn_light.node 943 ba.animate( 944 self._flag_respawn_light.node, 945 'intensity', 946 {0: 0, 0.25: 0.15, 0.5: 0}, 947 loop=True, 948 ) 949 ba.timer(3.0, self._flag_respawn_light.node.delete) 950 else: 951 return super().handlemessage(msg) 952 return None
handle high-level game messages
962 def spawn_player(self, player: Player) -> ba.Actor: 963 spaz = self.spawn_player_spaz( 964 player, position=self.map.get_start_position(player.team.id) 965 ) 966 if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: 967 spaz.impact_scale = 0.25 968 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 969 spaz.punch_callback = self._handle_player_punched 970 return spaz
Spawn something for the provided ba.Player.
The default implementation simply calls spawn_player_spaz().
Inherited Members
- ba._coopgame.CoopGameActivity
- session
- supports_session_type
- celebrate
- spawn_player_spaz
- fade_to_red
- setup_low_life_warning_sound
- ba._gameactivity.GameActivity
- allow_pausing
- allow_kick_idle_players
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_supported_maps
- get_settings_display_string
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- end
- respawn_player
- spawn_player_if_exists
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- ba._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
- ba._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps