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