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