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 if msg.self_kill: 335 self._spawn_flag() 336 else: 337 self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag) 338 self._flag_respawn_light = bs.NodeActor( 339 bs.newnode( 340 'light', 341 attrs={ 342 'position': self._flag_spawn_pos, 343 'height_attenuated': False, 344 'radius': 0.15, 345 'color': (1.0, 1.0, 0.3), 346 }, 347 ) 348 ) 349 assert self._flag_respawn_light.node 350 bs.animate( 351 self._flag_respawn_light.node, 352 'intensity', 353 {0.0: 0, 0.25: 0.15, 0.5: 0}, 354 loop=True, 355 ) 356 bs.timer(3.0, self._flag_respawn_light.node.delete) 357 358 else: 359 # Augment standard behavior. 360 super().handlemessage(msg) 361 362 def _flash_flag_spawn(self) -> None: 363 light = bs.newnode( 364 'light', 365 attrs={ 366 'position': self._flag_spawn_pos, 367 'height_attenuated': False, 368 'color': (1, 1, 0), 369 }, 370 ) 371 bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 372 bs.timer(1.0, light.delete) 373 374 def _spawn_flag(self) -> None: 375 self._swipsound.play() 376 self._whistle_sound.play() 377 self._flash_flag_spawn() 378 assert self._flag_spawn_pos is not None 379 self._flag = FootballFlag(position=self._flag_spawn_pos) 380 381 382class FootballCoopGame(bs.CoopGameActivity[Player, Team]): 383 """Co-op variant of football.""" 384 385 name = 'Football' 386 tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] 387 scoreconfig = bs.ScoreConfig( 388 scoretype=bs.ScoreType.MILLISECONDS, version='B' 389 ) 390 391 default_music = bs.MusicType.FOOTBALL 392 393 # FIXME: Need to update co-op games to use getscoreconfig. 394 @override 395 def get_score_type(self) -> str: 396 return 'time' 397 398 @override 399 def get_instance_description(self) -> str | Sequence: 400 touchdowns = self._score_to_win / 7 401 touchdowns = math.ceil(touchdowns) 402 if touchdowns > 1: 403 return 'Score ${ARG1} touchdowns.', touchdowns 404 return 'Score a touchdown.' 405 406 @override 407 def get_instance_description_short(self) -> str | Sequence: 408 touchdowns = self._score_to_win / 7 409 touchdowns = math.ceil(touchdowns) 410 if touchdowns > 1: 411 return 'score ${ARG1} touchdowns', touchdowns 412 return 'score a touchdown' 413 414 def __init__(self, settings: dict): 415 settings['map'] = 'Football Stadium' 416 super().__init__(settings) 417 self._preset = settings.get('preset', 'rookie') 418 419 # Load some media we need. 420 self._cheer_sound = bs.getsound('cheer') 421 self._boo_sound = bs.getsound('boo') 422 self._chant_sound = bs.getsound('crowdChant') 423 self._score_sound = bs.getsound('score') 424 self._swipsound = bs.getsound('swip') 425 self._whistle_sound = bs.getsound('refWhistle') 426 self._score_to_win = 21 427 self._score_region_material = bs.Material() 428 self._score_region_material.add_actions( 429 conditions=('they_have_material', FlagFactory.get().flagmaterial), 430 actions=( 431 ('modify_part_collision', 'collide', True), 432 ('modify_part_collision', 'physical', False), 433 ('call', 'at_connect', self._handle_score), 434 ), 435 ) 436 self._powerup_center = (0, 2, 0) 437 self._powerup_spread = (10, 5.5) 438 self._player_has_dropped_bomb = False 439 self._player_has_punched = False 440 self._scoreboard: Scoreboard | None = None 441 self._flag_spawn_pos: Sequence[float] | None = None 442 self._score_regions: list[bs.NodeActor] = [] 443 self._exclude_powerups: list[str] = [] 444 self._have_tnt = False 445 self._bot_types_initial: list[type[SpazBot]] | None = None 446 self._bot_types_7: list[type[SpazBot]] | None = None 447 self._bot_types_14: list[type[SpazBot]] | None = None 448 self._bot_team: Team | None = None 449 self._starttime_ms: int | None = None 450 self._time_text: bs.NodeActor | None = None 451 self._time_text_input: bs.NodeActor | None = None 452 self._tntspawner: TNTSpawner | None = None 453 self._bots = SpazBotSet() 454 self._bot_spawn_timer: bs.Timer | None = None 455 self._powerup_drop_timer: bs.Timer | None = None 456 self._scoring_team: Team | None = None 457 self._final_time_ms: int | None = None 458 self._time_text_timer: bs.Timer | None = None 459 self._flag_respawn_light: bs.Actor | None = None 460 self._flag: FootballFlag | None = None 461 462 @override 463 def on_transition_in(self) -> None: 464 super().on_transition_in() 465 self._scoreboard = Scoreboard() 466 self._flag_spawn_pos = self.map.get_flag_position(None) 467 self._spawn_flag() 468 469 # Set up the two score regions. 470 defs = self.map.defs 471 self._score_regions.append( 472 bs.NodeActor( 473 bs.newnode( 474 'region', 475 attrs={ 476 'position': defs.boxes['goal1'][0:3], 477 'scale': defs.boxes['goal1'][6:9], 478 'type': 'box', 479 'materials': [self._score_region_material], 480 }, 481 ) 482 ) 483 ) 484 self._score_regions.append( 485 bs.NodeActor( 486 bs.newnode( 487 'region', 488 attrs={ 489 'position': defs.boxes['goal2'][0:3], 490 'scale': defs.boxes['goal2'][6:9], 491 'type': 'box', 492 'materials': [self._score_region_material], 493 }, 494 ) 495 ) 496 ) 497 self._chant_sound.play() 498 499 @override 500 def on_begin(self) -> None: 501 # FIXME: Split this up a bit. 502 # pylint: disable=too-many-statements 503 from bascenev1lib.actor import controlsguide 504 505 super().on_begin() 506 507 # Show controls help in demo or arcade mode. 508 if bs.app.env.demo or bs.app.env.arcade: 509 controlsguide.ControlsGuide( 510 delay=3.0, lifespan=10.0, bright=True 511 ).autoretain() 512 assert self.initialplayerinfos is not None 513 abot: type[SpazBot] 514 bbot: type[SpazBot] 515 cbot: type[SpazBot] 516 if self._preset in ['rookie', 'rookie_easy']: 517 self._exclude_powerups = ['curse'] 518 self._have_tnt = False 519 abot = ( 520 BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot 521 ) 522 self._bot_types_initial = [abot] * len(self.initialplayerinfos) 523 bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot 524 self._bot_types_7 = [bbot] * ( 525 1 if len(self.initialplayerinfos) < 3 else 2 526 ) 527 cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot 528 self._bot_types_14 = [cbot] * ( 529 1 if len(self.initialplayerinfos) < 3 else 2 530 ) 531 elif self._preset == 'tournament': 532 self._exclude_powerups = [] 533 self._have_tnt = True 534 self._bot_types_initial = [BrawlerBot] * ( 535 1 if len(self.initialplayerinfos) < 2 else 2 536 ) 537 self._bot_types_7 = [TriggerBot] * ( 538 1 if len(self.initialplayerinfos) < 3 else 2 539 ) 540 self._bot_types_14 = [ChargerBot] * ( 541 1 if len(self.initialplayerinfos) < 4 else 2 542 ) 543 elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: 544 self._exclude_powerups = ['curse'] 545 self._have_tnt = True 546 self._bot_types_initial = [ChargerBot] * len( 547 self.initialplayerinfos 548 ) 549 abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite 550 typed_bot_list: list[type[SpazBot]] = [] 551 self._bot_types_7 = ( 552 typed_bot_list 553 + [abot] 554 + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) 555 ) 556 bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot 557 self._bot_types_14 = [bbot] * ( 558 1 if len(self.initialplayerinfos) < 3 else 2 559 ) 560 elif self._preset in ['uber', 'uber_easy']: 561 self._exclude_powerups = [] 562 self._have_tnt = True 563 abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot 564 bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot 565 typed_bot_list_2: list[type[SpazBot]] = [] 566 self._bot_types_initial = ( 567 typed_bot_list_2 568 + [StickyBot] 569 + [abot] * len(self.initialplayerinfos) 570 ) 571 self._bot_types_7 = [bbot] * ( 572 1 if len(self.initialplayerinfos) < 3 else 2 573 ) 574 self._bot_types_14 = [ExplodeyBot] * ( 575 1 if len(self.initialplayerinfos) < 3 else 2 576 ) 577 else: 578 raise RuntimeError() 579 580 self.setup_low_life_warning_sound() 581 582 self._drop_powerups(standard_points=True) 583 bs.timer(4.0, self._start_powerup_drops) 584 585 # Make a bogus team for our bots. 586 bad_team_name = self.get_team_display_string('Bad Guys') 587 self._bot_team = Team() 588 self._bot_team.manual_init( 589 team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) 590 ) 591 592 for team in [self.teams[0], self._bot_team]: 593 team.score = 0 594 595 self.update_scores() 596 597 # Time display. 598 starttime_ms = int(bs.time() * 1000.0) 599 assert isinstance(starttime_ms, int) 600 self._starttime_ms = starttime_ms 601 self._time_text = bs.NodeActor( 602 bs.newnode( 603 'text', 604 attrs={ 605 'v_attach': 'top', 606 'h_attach': 'center', 607 'h_align': 'center', 608 'color': (1, 1, 0.5, 1), 609 'flatness': 0.5, 610 'shadow': 0.5, 611 'position': (0, -50), 612 'scale': 1.3, 613 'text': '', 614 }, 615 ) 616 ) 617 self._time_text_input = bs.NodeActor( 618 bs.newnode('timedisplay', attrs={'showsubseconds': True}) 619 ) 620 self.globalsnode.connectattr( 621 'time', self._time_text_input.node, 'time2' 622 ) 623 assert self._time_text_input.node 624 assert self._time_text.node 625 self._time_text_input.node.connectattr( 626 'output', self._time_text.node, 'text' 627 ) 628 629 # Our TNT spawner (if applicable). 630 if self._have_tnt: 631 self._tntspawner = TNTSpawner(position=(0, 1, -1)) 632 633 self._bots = SpazBotSet() 634 self._bot_spawn_timer = bs.Timer(1.0, self._update_bots, repeat=True) 635 636 for bottype in self._bot_types_initial: 637 self._spawn_bot(bottype) 638 639 def _on_bot_spawn(self, spaz: SpazBot) -> None: 640 # We want to move to the left by default. 641 spaz.target_point_default = bs.Vec3(0, 0, 0) 642 643 def _spawn_bot( 644 self, spaz_type: type[SpazBot], immediate: bool = False 645 ) -> None: 646 assert self._bot_team is not None 647 pos = self.map.get_start_position(self._bot_team.id) 648 self._bots.spawn_bot( 649 spaz_type, 650 pos=pos, 651 spawn_time=0.001 if immediate else 3.0, 652 on_spawn_call=self._on_bot_spawn, 653 ) 654 655 def _update_bots(self) -> None: 656 bots = self._bots.get_living_bots() 657 for bot in bots: 658 bot.target_flag = None 659 660 # If we've got a flag and no player are holding it, find the closest 661 # bot to it, and make them the designated flag-bearer. 662 assert self._flag is not None 663 if self._flag.node: 664 for player in self.players: 665 if player.actor: 666 assert isinstance(player.actor, PlayerSpaz) 667 if ( 668 player.actor.is_alive() 669 and player.actor.node.hold_node == self._flag.node 670 ): 671 return 672 673 flagpos = bs.Vec3(self._flag.node.position) 674 closest_bot: SpazBot | None = None 675 closest_dist = 0.0 # Always gets assigned first time through. 676 for bot in bots: 677 # If a bot is picked up, he should forget about the flag. 678 if bot.held_count > 0: 679 continue 680 assert bot.node 681 botpos = bs.Vec3(bot.node.position) 682 botdist = (botpos - flagpos).length() 683 if closest_bot is None or botdist < closest_dist: 684 closest_bot = bot 685 closest_dist = botdist 686 if closest_bot is not None: 687 closest_bot.target_flag = self._flag 688 689 def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: 690 if poweruptype is None: 691 poweruptype = PowerupBoxFactory.get().get_random_powerup_type( 692 excludetypes=self._exclude_powerups 693 ) 694 PowerupBox( 695 position=self.map.powerup_spawn_points[index], 696 poweruptype=poweruptype, 697 ).autoretain() 698 699 def _start_powerup_drops(self) -> None: 700 self._powerup_drop_timer = bs.Timer( 701 3.0, self._drop_powerups, repeat=True 702 ) 703 704 def _drop_powerups( 705 self, standard_points: bool = False, poweruptype: str | None = None 706 ) -> None: 707 """Generic powerup drop.""" 708 if standard_points: 709 spawnpoints = self.map.powerup_spawn_points 710 for i, _point in enumerate(spawnpoints): 711 bs.timer( 712 1.0 + i * 0.5, bs.Call(self._drop_powerup, i, poweruptype) 713 ) 714 else: 715 point = ( 716 self._powerup_center[0] 717 + random.uniform( 718 -1.0 * self._powerup_spread[0], 719 1.0 * self._powerup_spread[0], 720 ), 721 self._powerup_center[1], 722 self._powerup_center[2] 723 + random.uniform( 724 -self._powerup_spread[1], self._powerup_spread[1] 725 ), 726 ) 727 728 # Drop one random one somewhere. 729 PowerupBox( 730 position=point, 731 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 732 excludetypes=self._exclude_powerups 733 ), 734 ).autoretain() 735 736 def _kill_flag(self) -> None: 737 try: 738 assert self._flag is not None 739 self._flag.handlemessage(bs.DieMessage()) 740 except Exception: 741 logging.exception('Error in _kill_flag.') 742 743 def _handle_score(self) -> None: 744 """a point has been scored""" 745 # FIXME tidy this up 746 # pylint: disable=too-many-branches 747 748 # Our flag might stick around for a second or two; 749 # we don't want it to be able to score again. 750 assert self._flag is not None 751 if self._flag.scored: 752 return 753 754 # See which score region it was. 755 region = bs.getcollision().sourcenode 756 i = None 757 for i, score_region in enumerate(self._score_regions): 758 if region == score_region.node: 759 break 760 761 for team in [self.teams[0], self._bot_team]: 762 assert team is not None 763 if team.id == i: 764 team.score += 7 765 766 # Tell all players (or bots) to celebrate. 767 if i == 0: 768 for player in team.players: 769 if player.actor: 770 player.actor.handlemessage(bs.CelebrateMessage(2.0)) 771 else: 772 self._bots.celebrate(2.0) 773 774 # If the good guys scored, add more enemies. 775 if i == 0: 776 if self.teams[0].score == 7: 777 assert self._bot_types_7 is not None 778 for bottype in self._bot_types_7: 779 self._spawn_bot(bottype) 780 elif self.teams[0].score == 14: 781 assert self._bot_types_14 is not None 782 for bottype in self._bot_types_14: 783 self._spawn_bot(bottype) 784 785 self._score_sound.play() 786 if i == 0: 787 self._cheer_sound.play() 788 else: 789 self._boo_sound.play() 790 791 # Kill the flag (it'll respawn shortly). 792 self._flag.scored = True 793 794 bs.timer(0.2, self._kill_flag) 795 796 self.update_scores() 797 light = bs.newnode( 798 'light', 799 attrs={ 800 'position': bs.getcollision().position, 801 'height_attenuated': False, 802 'color': (1, 0, 0), 803 }, 804 ) 805 bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 806 bs.timer(1.0, light.delete) 807 if i == 0: 808 bs.cameraflash(duration=10.0) 809 810 @override 811 def end_game(self) -> None: 812 bs.setmusic(None) 813 self._bots.final_celebrate() 814 bs.timer(0.001, bs.Call(self.do_end, 'defeat')) 815 816 def update_scores(self) -> None: 817 """update scoreboard and check for winners""" 818 # FIXME: tidy this up 819 # pylint: disable=too-many-nested-blocks 820 have_scoring_team = False 821 win_score = self._score_to_win 822 for team in [self.teams[0], self._bot_team]: 823 assert team is not None 824 assert self._scoreboard is not None 825 self._scoreboard.set_team_value(team, team.score, win_score) 826 if team.score >= win_score: 827 if not have_scoring_team: 828 self._scoring_team = team 829 if team is self._bot_team: 830 self.end_game() 831 else: 832 bs.setmusic(bs.MusicType.VICTORY) 833 834 # Completion achievements. 835 assert self._bot_team is not None 836 if self._preset in ['rookie', 'rookie_easy']: 837 self._award_achievement( 838 'Rookie Football Victory', sound=False 839 ) 840 if self._bot_team.score == 0: 841 self._award_achievement( 842 'Rookie Football Shutout', sound=False 843 ) 844 elif self._preset in ['pro', 'pro_easy']: 845 self._award_achievement( 846 'Pro Football Victory', sound=False 847 ) 848 if self._bot_team.score == 0: 849 self._award_achievement( 850 'Pro Football Shutout', sound=False 851 ) 852 elif self._preset in ['uber', 'uber_easy']: 853 self._award_achievement( 854 'Uber Football Victory', sound=False 855 ) 856 if self._bot_team.score == 0: 857 self._award_achievement( 858 'Uber Football Shutout', sound=False 859 ) 860 if ( 861 not self._player_has_dropped_bomb 862 and not self._player_has_punched 863 ): 864 self._award_achievement( 865 'Got the Moves', sound=False 866 ) 867 self._bots.stop_moving() 868 self.show_zoom_message( 869 bs.Lstr(resource='victoryText'), 870 scale=1.0, 871 duration=4.0, 872 ) 873 self.celebrate(10.0) 874 assert self._starttime_ms is not None 875 self._final_time_ms = int( 876 int(bs.time() * 1000.0) - self._starttime_ms 877 ) 878 self._time_text_timer = None 879 assert ( 880 self._time_text_input is not None 881 and self._time_text_input.node 882 ) 883 self._time_text_input.node.timemax = self._final_time_ms 884 885 self.do_end('victory') 886 887 def do_end(self, outcome: str) -> None: 888 """End the game with the specified outcome.""" 889 if outcome == 'defeat': 890 self.fade_to_red() 891 assert self._final_time_ms is not None 892 scoreval = ( 893 None if outcome == 'defeat' else int(self._final_time_ms // 10) 894 ) 895 self.end( 896 delay=3.0, 897 results={ 898 'outcome': outcome, 899 'score': scoreval, 900 'score_order': 'decreasing', 901 'playerinfos': self.initialplayerinfos, 902 }, 903 ) 904 905 @override 906 def handlemessage(self, msg: Any) -> Any: 907 """handle high-level game messages""" 908 if isinstance(msg, bs.PlayerDiedMessage): 909 # Augment standard behavior. 910 super().handlemessage(msg) 911 912 # Respawn them shortly. 913 player = msg.getplayer(Player) 914 assert self.initialplayerinfos is not None 915 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 916 player.respawn_timer = bs.Timer( 917 respawn_time, bs.Call(self.spawn_player_if_exists, player) 918 ) 919 player.respawn_icon = RespawnIcon(player, respawn_time) 920 921 elif isinstance(msg, SpazBotDiedMessage): 922 # Every time a bad guy dies, spawn a new one. 923 bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot)))) 924 925 elif isinstance(msg, SpazBotPunchedMessage): 926 if self._preset in ['rookie', 'rookie_easy']: 927 if msg.damage >= 500: 928 self._award_achievement('Super Punch') 929 elif self._preset in ['pro', 'pro_easy']: 930 if msg.damage >= 1000: 931 self._award_achievement('Super Mega Punch') 932 933 # Respawn dead flags. 934 elif isinstance(msg, FlagDiedMessage): 935 assert isinstance(msg.flag, FootballFlag) 936 if msg.self_kill: 937 self._spawn_flag() 938 else: 939 msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag) 940 self._flag_respawn_light = bs.NodeActor( 941 bs.newnode( 942 'light', 943 attrs={ 944 'position': self._flag_spawn_pos, 945 'height_attenuated': False, 946 'radius': 0.15, 947 'color': (1.0, 1.0, 0.3), 948 }, 949 ) 950 ) 951 assert self._flag_respawn_light.node 952 bs.animate( 953 self._flag_respawn_light.node, 954 'intensity', 955 {0: 0, 0.25: 0.15, 0.5: 0}, 956 loop=(not msg.self_kill), 957 ) 958 bs.timer(3.0, self._flag_respawn_light.node.delete) 959 else: 960 return super().handlemessage(msg) 961 return None 962 963 def _handle_player_dropped_bomb(self, player: Spaz, bomb: bs.Actor) -> None: 964 del player, bomb # Unused. 965 self._player_has_dropped_bomb = True 966 967 def _handle_player_punched(self, player: Spaz) -> None: 968 del player # Unused. 969 self._player_has_punched = True 970 971 @override 972 def spawn_player(self, player: Player) -> bs.Actor: 973 spaz = self.spawn_player_spaz( 974 player, position=self.map.get_start_position(player.team.id) 975 ) 976 if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: 977 spaz.impact_scale = 0.25 978 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 979 spaz.punch_callback = self._handle_player_punched 980 return spaz 981 982 def _flash_flag_spawn(self) -> None: 983 light = bs.newnode( 984 'light', 985 attrs={ 986 'position': self._flag_spawn_pos, 987 'height_attenuated': False, 988 'color': (1, 1, 0), 989 }, 990 ) 991 bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 992 bs.timer(1.0, light.delete) 993 994 def _spawn_flag(self) -> None: 995 self._swipsound.play() 996 self._whistle_sound.play() 997 self._flash_flag_spawn() 998 assert self._flag_spawn_pos is not None 999 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 if msg.self_kill: 336 self._spawn_flag() 337 else: 338 self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag) 339 self._flag_respawn_light = bs.NodeActor( 340 bs.newnode( 341 'light', 342 attrs={ 343 'position': self._flag_spawn_pos, 344 'height_attenuated': False, 345 'radius': 0.15, 346 'color': (1.0, 1.0, 0.3), 347 }, 348 ) 349 ) 350 assert self._flag_respawn_light.node 351 bs.animate( 352 self._flag_respawn_light.node, 353 'intensity', 354 {0.0: 0, 0.25: 0.15, 0.5: 0}, 355 loop=True, 356 ) 357 bs.timer(3.0, self._flag_respawn_light.node.delete) 358 359 else: 360 # Augment standard behavior. 361 super().handlemessage(msg) 362 363 def _flash_flag_spawn(self) -> None: 364 light = bs.newnode( 365 'light', 366 attrs={ 367 'position': self._flag_spawn_pos, 368 'height_attenuated': False, 369 'color': (1, 1, 0), 370 }, 371 ) 372 bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 373 bs.timer(1.0, light.delete) 374 375 def _spawn_flag(self) -> None: 376 self._swipsound.play() 377 self._whistle_sound.play() 378 self._flash_flag_spawn() 379 assert self._flag_spawn_pos is not None 380 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 if msg.self_kill: 336 self._spawn_flag() 337 else: 338 self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag) 339 self._flag_respawn_light = bs.NodeActor( 340 bs.newnode( 341 'light', 342 attrs={ 343 'position': self._flag_spawn_pos, 344 'height_attenuated': False, 345 'radius': 0.15, 346 'color': (1.0, 1.0, 0.3), 347 }, 348 ) 349 ) 350 assert self._flag_respawn_light.node 351 bs.animate( 352 self._flag_respawn_light.node, 353 'intensity', 354 {0.0: 0, 0.25: 0.15, 0.5: 0}, 355 loop=True, 356 ) 357 bs.timer(3.0, self._flag_respawn_light.node.delete) 358 359 else: 360 # Augment standard behavior. 361 super().handlemessage(msg)
General message handling; can be passed any message object.
383class FootballCoopGame(bs.CoopGameActivity[Player, Team]): 384 """Co-op variant of football.""" 385 386 name = 'Football' 387 tips = ['Use the pick-up button to grab the flag < ${PICKUP} >'] 388 scoreconfig = bs.ScoreConfig( 389 scoretype=bs.ScoreType.MILLISECONDS, version='B' 390 ) 391 392 default_music = bs.MusicType.FOOTBALL 393 394 # FIXME: Need to update co-op games to use getscoreconfig. 395 @override 396 def get_score_type(self) -> str: 397 return 'time' 398 399 @override 400 def get_instance_description(self) -> str | Sequence: 401 touchdowns = self._score_to_win / 7 402 touchdowns = math.ceil(touchdowns) 403 if touchdowns > 1: 404 return 'Score ${ARG1} touchdowns.', touchdowns 405 return 'Score a touchdown.' 406 407 @override 408 def get_instance_description_short(self) -> str | Sequence: 409 touchdowns = self._score_to_win / 7 410 touchdowns = math.ceil(touchdowns) 411 if touchdowns > 1: 412 return 'score ${ARG1} touchdowns', touchdowns 413 return 'score a touchdown' 414 415 def __init__(self, settings: dict): 416 settings['map'] = 'Football Stadium' 417 super().__init__(settings) 418 self._preset = settings.get('preset', 'rookie') 419 420 # Load some media we need. 421 self._cheer_sound = bs.getsound('cheer') 422 self._boo_sound = bs.getsound('boo') 423 self._chant_sound = bs.getsound('crowdChant') 424 self._score_sound = bs.getsound('score') 425 self._swipsound = bs.getsound('swip') 426 self._whistle_sound = bs.getsound('refWhistle') 427 self._score_to_win = 21 428 self._score_region_material = bs.Material() 429 self._score_region_material.add_actions( 430 conditions=('they_have_material', FlagFactory.get().flagmaterial), 431 actions=( 432 ('modify_part_collision', 'collide', True), 433 ('modify_part_collision', 'physical', False), 434 ('call', 'at_connect', self._handle_score), 435 ), 436 ) 437 self._powerup_center = (0, 2, 0) 438 self._powerup_spread = (10, 5.5) 439 self._player_has_dropped_bomb = False 440 self._player_has_punched = False 441 self._scoreboard: Scoreboard | None = None 442 self._flag_spawn_pos: Sequence[float] | None = None 443 self._score_regions: list[bs.NodeActor] = [] 444 self._exclude_powerups: list[str] = [] 445 self._have_tnt = False 446 self._bot_types_initial: list[type[SpazBot]] | None = None 447 self._bot_types_7: list[type[SpazBot]] | None = None 448 self._bot_types_14: list[type[SpazBot]] | None = None 449 self._bot_team: Team | None = None 450 self._starttime_ms: int | None = None 451 self._time_text: bs.NodeActor | None = None 452 self._time_text_input: bs.NodeActor | None = None 453 self._tntspawner: TNTSpawner | None = None 454 self._bots = SpazBotSet() 455 self._bot_spawn_timer: bs.Timer | None = None 456 self._powerup_drop_timer: bs.Timer | None = None 457 self._scoring_team: Team | None = None 458 self._final_time_ms: int | None = None 459 self._time_text_timer: bs.Timer | None = None 460 self._flag_respawn_light: bs.Actor | None = None 461 self._flag: FootballFlag | None = None 462 463 @override 464 def on_transition_in(self) -> None: 465 super().on_transition_in() 466 self._scoreboard = Scoreboard() 467 self._flag_spawn_pos = self.map.get_flag_position(None) 468 self._spawn_flag() 469 470 # Set up the two score regions. 471 defs = self.map.defs 472 self._score_regions.append( 473 bs.NodeActor( 474 bs.newnode( 475 'region', 476 attrs={ 477 'position': defs.boxes['goal1'][0:3], 478 'scale': defs.boxes['goal1'][6:9], 479 'type': 'box', 480 'materials': [self._score_region_material], 481 }, 482 ) 483 ) 484 ) 485 self._score_regions.append( 486 bs.NodeActor( 487 bs.newnode( 488 'region', 489 attrs={ 490 'position': defs.boxes['goal2'][0:3], 491 'scale': defs.boxes['goal2'][6:9], 492 'type': 'box', 493 'materials': [self._score_region_material], 494 }, 495 ) 496 ) 497 ) 498 self._chant_sound.play() 499 500 @override 501 def on_begin(self) -> None: 502 # FIXME: Split this up a bit. 503 # pylint: disable=too-many-statements 504 from bascenev1lib.actor import controlsguide 505 506 super().on_begin() 507 508 # Show controls help in demo or arcade mode. 509 if bs.app.env.demo or bs.app.env.arcade: 510 controlsguide.ControlsGuide( 511 delay=3.0, lifespan=10.0, bright=True 512 ).autoretain() 513 assert self.initialplayerinfos is not None 514 abot: type[SpazBot] 515 bbot: type[SpazBot] 516 cbot: type[SpazBot] 517 if self._preset in ['rookie', 'rookie_easy']: 518 self._exclude_powerups = ['curse'] 519 self._have_tnt = False 520 abot = ( 521 BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot 522 ) 523 self._bot_types_initial = [abot] * len(self.initialplayerinfos) 524 bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot 525 self._bot_types_7 = [bbot] * ( 526 1 if len(self.initialplayerinfos) < 3 else 2 527 ) 528 cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot 529 self._bot_types_14 = [cbot] * ( 530 1 if len(self.initialplayerinfos) < 3 else 2 531 ) 532 elif self._preset == 'tournament': 533 self._exclude_powerups = [] 534 self._have_tnt = True 535 self._bot_types_initial = [BrawlerBot] * ( 536 1 if len(self.initialplayerinfos) < 2 else 2 537 ) 538 self._bot_types_7 = [TriggerBot] * ( 539 1 if len(self.initialplayerinfos) < 3 else 2 540 ) 541 self._bot_types_14 = [ChargerBot] * ( 542 1 if len(self.initialplayerinfos) < 4 else 2 543 ) 544 elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: 545 self._exclude_powerups = ['curse'] 546 self._have_tnt = True 547 self._bot_types_initial = [ChargerBot] * len( 548 self.initialplayerinfos 549 ) 550 abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite 551 typed_bot_list: list[type[SpazBot]] = [] 552 self._bot_types_7 = ( 553 typed_bot_list 554 + [abot] 555 + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) 556 ) 557 bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot 558 self._bot_types_14 = [bbot] * ( 559 1 if len(self.initialplayerinfos) < 3 else 2 560 ) 561 elif self._preset in ['uber', 'uber_easy']: 562 self._exclude_powerups = [] 563 self._have_tnt = True 564 abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot 565 bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot 566 typed_bot_list_2: list[type[SpazBot]] = [] 567 self._bot_types_initial = ( 568 typed_bot_list_2 569 + [StickyBot] 570 + [abot] * len(self.initialplayerinfos) 571 ) 572 self._bot_types_7 = [bbot] * ( 573 1 if len(self.initialplayerinfos) < 3 else 2 574 ) 575 self._bot_types_14 = [ExplodeyBot] * ( 576 1 if len(self.initialplayerinfos) < 3 else 2 577 ) 578 else: 579 raise RuntimeError() 580 581 self.setup_low_life_warning_sound() 582 583 self._drop_powerups(standard_points=True) 584 bs.timer(4.0, self._start_powerup_drops) 585 586 # Make a bogus team for our bots. 587 bad_team_name = self.get_team_display_string('Bad Guys') 588 self._bot_team = Team() 589 self._bot_team.manual_init( 590 team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) 591 ) 592 593 for team in [self.teams[0], self._bot_team]: 594 team.score = 0 595 596 self.update_scores() 597 598 # Time display. 599 starttime_ms = int(bs.time() * 1000.0) 600 assert isinstance(starttime_ms, int) 601 self._starttime_ms = starttime_ms 602 self._time_text = bs.NodeActor( 603 bs.newnode( 604 'text', 605 attrs={ 606 'v_attach': 'top', 607 'h_attach': 'center', 608 'h_align': 'center', 609 'color': (1, 1, 0.5, 1), 610 'flatness': 0.5, 611 'shadow': 0.5, 612 'position': (0, -50), 613 'scale': 1.3, 614 'text': '', 615 }, 616 ) 617 ) 618 self._time_text_input = bs.NodeActor( 619 bs.newnode('timedisplay', attrs={'showsubseconds': True}) 620 ) 621 self.globalsnode.connectattr( 622 'time', self._time_text_input.node, 'time2' 623 ) 624 assert self._time_text_input.node 625 assert self._time_text.node 626 self._time_text_input.node.connectattr( 627 'output', self._time_text.node, 'text' 628 ) 629 630 # Our TNT spawner (if applicable). 631 if self._have_tnt: 632 self._tntspawner = TNTSpawner(position=(0, 1, -1)) 633 634 self._bots = SpazBotSet() 635 self._bot_spawn_timer = bs.Timer(1.0, self._update_bots, repeat=True) 636 637 for bottype in self._bot_types_initial: 638 self._spawn_bot(bottype) 639 640 def _on_bot_spawn(self, spaz: SpazBot) -> None: 641 # We want to move to the left by default. 642 spaz.target_point_default = bs.Vec3(0, 0, 0) 643 644 def _spawn_bot( 645 self, spaz_type: type[SpazBot], immediate: bool = False 646 ) -> None: 647 assert self._bot_team is not None 648 pos = self.map.get_start_position(self._bot_team.id) 649 self._bots.spawn_bot( 650 spaz_type, 651 pos=pos, 652 spawn_time=0.001 if immediate else 3.0, 653 on_spawn_call=self._on_bot_spawn, 654 ) 655 656 def _update_bots(self) -> None: 657 bots = self._bots.get_living_bots() 658 for bot in bots: 659 bot.target_flag = None 660 661 # If we've got a flag and no player are holding it, find the closest 662 # bot to it, and make them the designated flag-bearer. 663 assert self._flag is not None 664 if self._flag.node: 665 for player in self.players: 666 if player.actor: 667 assert isinstance(player.actor, PlayerSpaz) 668 if ( 669 player.actor.is_alive() 670 and player.actor.node.hold_node == self._flag.node 671 ): 672 return 673 674 flagpos = bs.Vec3(self._flag.node.position) 675 closest_bot: SpazBot | None = None 676 closest_dist = 0.0 # Always gets assigned first time through. 677 for bot in bots: 678 # If a bot is picked up, he should forget about the flag. 679 if bot.held_count > 0: 680 continue 681 assert bot.node 682 botpos = bs.Vec3(bot.node.position) 683 botdist = (botpos - flagpos).length() 684 if closest_bot is None or botdist < closest_dist: 685 closest_bot = bot 686 closest_dist = botdist 687 if closest_bot is not None: 688 closest_bot.target_flag = self._flag 689 690 def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None: 691 if poweruptype is None: 692 poweruptype = PowerupBoxFactory.get().get_random_powerup_type( 693 excludetypes=self._exclude_powerups 694 ) 695 PowerupBox( 696 position=self.map.powerup_spawn_points[index], 697 poweruptype=poweruptype, 698 ).autoretain() 699 700 def _start_powerup_drops(self) -> None: 701 self._powerup_drop_timer = bs.Timer( 702 3.0, self._drop_powerups, repeat=True 703 ) 704 705 def _drop_powerups( 706 self, standard_points: bool = False, poweruptype: str | None = None 707 ) -> None: 708 """Generic powerup drop.""" 709 if standard_points: 710 spawnpoints = self.map.powerup_spawn_points 711 for i, _point in enumerate(spawnpoints): 712 bs.timer( 713 1.0 + i * 0.5, bs.Call(self._drop_powerup, i, poweruptype) 714 ) 715 else: 716 point = ( 717 self._powerup_center[0] 718 + random.uniform( 719 -1.0 * self._powerup_spread[0], 720 1.0 * self._powerup_spread[0], 721 ), 722 self._powerup_center[1], 723 self._powerup_center[2] 724 + random.uniform( 725 -self._powerup_spread[1], self._powerup_spread[1] 726 ), 727 ) 728 729 # Drop one random one somewhere. 730 PowerupBox( 731 position=point, 732 poweruptype=PowerupBoxFactory.get().get_random_powerup_type( 733 excludetypes=self._exclude_powerups 734 ), 735 ).autoretain() 736 737 def _kill_flag(self) -> None: 738 try: 739 assert self._flag is not None 740 self._flag.handlemessage(bs.DieMessage()) 741 except Exception: 742 logging.exception('Error in _kill_flag.') 743 744 def _handle_score(self) -> None: 745 """a point has been scored""" 746 # FIXME tidy this up 747 # pylint: disable=too-many-branches 748 749 # Our flag might stick around for a second or two; 750 # we don't want it to be able to score again. 751 assert self._flag is not None 752 if self._flag.scored: 753 return 754 755 # See which score region it was. 756 region = bs.getcollision().sourcenode 757 i = None 758 for i, score_region in enumerate(self._score_regions): 759 if region == score_region.node: 760 break 761 762 for team in [self.teams[0], self._bot_team]: 763 assert team is not None 764 if team.id == i: 765 team.score += 7 766 767 # Tell all players (or bots) to celebrate. 768 if i == 0: 769 for player in team.players: 770 if player.actor: 771 player.actor.handlemessage(bs.CelebrateMessage(2.0)) 772 else: 773 self._bots.celebrate(2.0) 774 775 # If the good guys scored, add more enemies. 776 if i == 0: 777 if self.teams[0].score == 7: 778 assert self._bot_types_7 is not None 779 for bottype in self._bot_types_7: 780 self._spawn_bot(bottype) 781 elif self.teams[0].score == 14: 782 assert self._bot_types_14 is not None 783 for bottype in self._bot_types_14: 784 self._spawn_bot(bottype) 785 786 self._score_sound.play() 787 if i == 0: 788 self._cheer_sound.play() 789 else: 790 self._boo_sound.play() 791 792 # Kill the flag (it'll respawn shortly). 793 self._flag.scored = True 794 795 bs.timer(0.2, self._kill_flag) 796 797 self.update_scores() 798 light = bs.newnode( 799 'light', 800 attrs={ 801 'position': bs.getcollision().position, 802 'height_attenuated': False, 803 'color': (1, 0, 0), 804 }, 805 ) 806 bs.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True) 807 bs.timer(1.0, light.delete) 808 if i == 0: 809 bs.cameraflash(duration=10.0) 810 811 @override 812 def end_game(self) -> None: 813 bs.setmusic(None) 814 self._bots.final_celebrate() 815 bs.timer(0.001, bs.Call(self.do_end, 'defeat')) 816 817 def update_scores(self) -> None: 818 """update scoreboard and check for winners""" 819 # FIXME: tidy this up 820 # pylint: disable=too-many-nested-blocks 821 have_scoring_team = False 822 win_score = self._score_to_win 823 for team in [self.teams[0], self._bot_team]: 824 assert team is not None 825 assert self._scoreboard is not None 826 self._scoreboard.set_team_value(team, team.score, win_score) 827 if team.score >= win_score: 828 if not have_scoring_team: 829 self._scoring_team = team 830 if team is self._bot_team: 831 self.end_game() 832 else: 833 bs.setmusic(bs.MusicType.VICTORY) 834 835 # Completion achievements. 836 assert self._bot_team is not None 837 if self._preset in ['rookie', 'rookie_easy']: 838 self._award_achievement( 839 'Rookie Football Victory', sound=False 840 ) 841 if self._bot_team.score == 0: 842 self._award_achievement( 843 'Rookie Football Shutout', sound=False 844 ) 845 elif self._preset in ['pro', 'pro_easy']: 846 self._award_achievement( 847 'Pro Football Victory', sound=False 848 ) 849 if self._bot_team.score == 0: 850 self._award_achievement( 851 'Pro Football Shutout', sound=False 852 ) 853 elif self._preset in ['uber', 'uber_easy']: 854 self._award_achievement( 855 'Uber Football Victory', sound=False 856 ) 857 if self._bot_team.score == 0: 858 self._award_achievement( 859 'Uber Football Shutout', sound=False 860 ) 861 if ( 862 not self._player_has_dropped_bomb 863 and not self._player_has_punched 864 ): 865 self._award_achievement( 866 'Got the Moves', sound=False 867 ) 868 self._bots.stop_moving() 869 self.show_zoom_message( 870 bs.Lstr(resource='victoryText'), 871 scale=1.0, 872 duration=4.0, 873 ) 874 self.celebrate(10.0) 875 assert self._starttime_ms is not None 876 self._final_time_ms = int( 877 int(bs.time() * 1000.0) - self._starttime_ms 878 ) 879 self._time_text_timer = None 880 assert ( 881 self._time_text_input is not None 882 and self._time_text_input.node 883 ) 884 self._time_text_input.node.timemax = self._final_time_ms 885 886 self.do_end('victory') 887 888 def do_end(self, outcome: str) -> None: 889 """End the game with the specified outcome.""" 890 if outcome == 'defeat': 891 self.fade_to_red() 892 assert self._final_time_ms is not None 893 scoreval = ( 894 None if outcome == 'defeat' else int(self._final_time_ms // 10) 895 ) 896 self.end( 897 delay=3.0, 898 results={ 899 'outcome': outcome, 900 'score': scoreval, 901 'score_order': 'decreasing', 902 'playerinfos': self.initialplayerinfos, 903 }, 904 ) 905 906 @override 907 def handlemessage(self, msg: Any) -> Any: 908 """handle high-level game messages""" 909 if isinstance(msg, bs.PlayerDiedMessage): 910 # Augment standard behavior. 911 super().handlemessage(msg) 912 913 # Respawn them shortly. 914 player = msg.getplayer(Player) 915 assert self.initialplayerinfos is not None 916 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 917 player.respawn_timer = bs.Timer( 918 respawn_time, bs.Call(self.spawn_player_if_exists, player) 919 ) 920 player.respawn_icon = RespawnIcon(player, respawn_time) 921 922 elif isinstance(msg, SpazBotDiedMessage): 923 # Every time a bad guy dies, spawn a new one. 924 bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot)))) 925 926 elif isinstance(msg, SpazBotPunchedMessage): 927 if self._preset in ['rookie', 'rookie_easy']: 928 if msg.damage >= 500: 929 self._award_achievement('Super Punch') 930 elif self._preset in ['pro', 'pro_easy']: 931 if msg.damage >= 1000: 932 self._award_achievement('Super Mega Punch') 933 934 # Respawn dead flags. 935 elif isinstance(msg, FlagDiedMessage): 936 assert isinstance(msg.flag, FootballFlag) 937 if msg.self_kill: 938 self._spawn_flag() 939 else: 940 msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag) 941 self._flag_respawn_light = bs.NodeActor( 942 bs.newnode( 943 'light', 944 attrs={ 945 'position': self._flag_spawn_pos, 946 'height_attenuated': False, 947 'radius': 0.15, 948 'color': (1.0, 1.0, 0.3), 949 }, 950 ) 951 ) 952 assert self._flag_respawn_light.node 953 bs.animate( 954 self._flag_respawn_light.node, 955 'intensity', 956 {0: 0, 0.25: 0.15, 0.5: 0}, 957 loop=(not msg.self_kill), 958 ) 959 bs.timer(3.0, self._flag_respawn_light.node.delete) 960 else: 961 return super().handlemessage(msg) 962 return None 963 964 def _handle_player_dropped_bomb(self, player: Spaz, bomb: bs.Actor) -> None: 965 del player, bomb # Unused. 966 self._player_has_dropped_bomb = True 967 968 def _handle_player_punched(self, player: Spaz) -> None: 969 del player # Unused. 970 self._player_has_punched = True 971 972 @override 973 def spawn_player(self, player: Player) -> bs.Actor: 974 spaz = self.spawn_player_spaz( 975 player, position=self.map.get_start_position(player.team.id) 976 ) 977 if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: 978 spaz.impact_scale = 0.25 979 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 980 spaz.punch_callback = self._handle_player_punched 981 return spaz 982 983 def _flash_flag_spawn(self) -> None: 984 light = bs.newnode( 985 'light', 986 attrs={ 987 'position': self._flag_spawn_pos, 988 'height_attenuated': False, 989 'color': (1, 1, 0), 990 }, 991 ) 992 bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True) 993 bs.timer(1.0, light.delete) 994 995 def _spawn_flag(self) -> None: 996 self._swipsound.play() 997 self._whistle_sound.play() 998 self._flash_flag_spawn() 999 assert self._flag_spawn_pos is not None 1000 self._flag = FootballFlag(position=self._flag_spawn_pos)
Co-op variant of football.
415 def __init__(self, settings: dict): 416 settings['map'] = 'Football Stadium' 417 super().__init__(settings) 418 self._preset = settings.get('preset', 'rookie') 419 420 # Load some media we need. 421 self._cheer_sound = bs.getsound('cheer') 422 self._boo_sound = bs.getsound('boo') 423 self._chant_sound = bs.getsound('crowdChant') 424 self._score_sound = bs.getsound('score') 425 self._swipsound = bs.getsound('swip') 426 self._whistle_sound = bs.getsound('refWhistle') 427 self._score_to_win = 21 428 self._score_region_material = bs.Material() 429 self._score_region_material.add_actions( 430 conditions=('they_have_material', FlagFactory.get().flagmaterial), 431 actions=( 432 ('modify_part_collision', 'collide', True), 433 ('modify_part_collision', 'physical', False), 434 ('call', 'at_connect', self._handle_score), 435 ), 436 ) 437 self._powerup_center = (0, 2, 0) 438 self._powerup_spread = (10, 5.5) 439 self._player_has_dropped_bomb = False 440 self._player_has_punched = False 441 self._scoreboard: Scoreboard | None = None 442 self._flag_spawn_pos: Sequence[float] | None = None 443 self._score_regions: list[bs.NodeActor] = [] 444 self._exclude_powerups: list[str] = [] 445 self._have_tnt = False 446 self._bot_types_initial: list[type[SpazBot]] | None = None 447 self._bot_types_7: list[type[SpazBot]] | None = None 448 self._bot_types_14: list[type[SpazBot]] | None = None 449 self._bot_team: Team | None = None 450 self._starttime_ms: int | None = None 451 self._time_text: bs.NodeActor | None = None 452 self._time_text_input: bs.NodeActor | None = None 453 self._tntspawner: TNTSpawner | None = None 454 self._bots = SpazBotSet() 455 self._bot_spawn_timer: bs.Timer | None = None 456 self._powerup_drop_timer: bs.Timer | None = None 457 self._scoring_team: Team | None = None 458 self._final_time_ms: int | None = None 459 self._time_text_timer: bs.Timer | None = None 460 self._flag_respawn_light: bs.Actor | None = None 461 self._flag: FootballFlag | None = None
Instantiate the Activity.
Return the score unit this co-op game uses ('point', 'seconds', etc.)
399 @override 400 def get_instance_description(self) -> str | Sequence: 401 touchdowns = self._score_to_win / 7 402 touchdowns = math.ceil(touchdowns) 403 if touchdowns > 1: 404 return 'Score ${ARG1} touchdowns.', touchdowns 405 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.
407 @override 408 def get_instance_description_short(self) -> str | Sequence: 409 touchdowns = self._score_to_win / 7 410 touchdowns = math.ceil(touchdowns) 411 if touchdowns > 1: 412 return 'score ${ARG1} touchdowns', touchdowns 413 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.
463 @override 464 def on_transition_in(self) -> None: 465 super().on_transition_in() 466 self._scoreboard = Scoreboard() 467 self._flag_spawn_pos = self.map.get_flag_position(None) 468 self._spawn_flag() 469 470 # Set up the two score regions. 471 defs = self.map.defs 472 self._score_regions.append( 473 bs.NodeActor( 474 bs.newnode( 475 'region', 476 attrs={ 477 'position': defs.boxes['goal1'][0:3], 478 'scale': defs.boxes['goal1'][6:9], 479 'type': 'box', 480 'materials': [self._score_region_material], 481 }, 482 ) 483 ) 484 ) 485 self._score_regions.append( 486 bs.NodeActor( 487 bs.newnode( 488 'region', 489 attrs={ 490 'position': defs.boxes['goal2'][0:3], 491 'scale': defs.boxes['goal2'][6:9], 492 'type': 'box', 493 'materials': [self._score_region_material], 494 }, 495 ) 496 ) 497 ) 498 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.
500 @override 501 def on_begin(self) -> None: 502 # FIXME: Split this up a bit. 503 # pylint: disable=too-many-statements 504 from bascenev1lib.actor import controlsguide 505 506 super().on_begin() 507 508 # Show controls help in demo or arcade mode. 509 if bs.app.env.demo or bs.app.env.arcade: 510 controlsguide.ControlsGuide( 511 delay=3.0, lifespan=10.0, bright=True 512 ).autoretain() 513 assert self.initialplayerinfos is not None 514 abot: type[SpazBot] 515 bbot: type[SpazBot] 516 cbot: type[SpazBot] 517 if self._preset in ['rookie', 'rookie_easy']: 518 self._exclude_powerups = ['curse'] 519 self._have_tnt = False 520 abot = ( 521 BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot 522 ) 523 self._bot_types_initial = [abot] * len(self.initialplayerinfos) 524 bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot 525 self._bot_types_7 = [bbot] * ( 526 1 if len(self.initialplayerinfos) < 3 else 2 527 ) 528 cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot 529 self._bot_types_14 = [cbot] * ( 530 1 if len(self.initialplayerinfos) < 3 else 2 531 ) 532 elif self._preset == 'tournament': 533 self._exclude_powerups = [] 534 self._have_tnt = True 535 self._bot_types_initial = [BrawlerBot] * ( 536 1 if len(self.initialplayerinfos) < 2 else 2 537 ) 538 self._bot_types_7 = [TriggerBot] * ( 539 1 if len(self.initialplayerinfos) < 3 else 2 540 ) 541 self._bot_types_14 = [ChargerBot] * ( 542 1 if len(self.initialplayerinfos) < 4 else 2 543 ) 544 elif self._preset in ['pro', 'pro_easy', 'tournament_pro']: 545 self._exclude_powerups = ['curse'] 546 self._have_tnt = True 547 self._bot_types_initial = [ChargerBot] * len( 548 self.initialplayerinfos 549 ) 550 abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite 551 typed_bot_list: list[type[SpazBot]] = [] 552 self._bot_types_7 = ( 553 typed_bot_list 554 + [abot] 555 + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2) 556 ) 557 bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot 558 self._bot_types_14 = [bbot] * ( 559 1 if len(self.initialplayerinfos) < 3 else 2 560 ) 561 elif self._preset in ['uber', 'uber_easy']: 562 self._exclude_powerups = [] 563 self._have_tnt = True 564 abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot 565 bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot 566 typed_bot_list_2: list[type[SpazBot]] = [] 567 self._bot_types_initial = ( 568 typed_bot_list_2 569 + [StickyBot] 570 + [abot] * len(self.initialplayerinfos) 571 ) 572 self._bot_types_7 = [bbot] * ( 573 1 if len(self.initialplayerinfos) < 3 else 2 574 ) 575 self._bot_types_14 = [ExplodeyBot] * ( 576 1 if len(self.initialplayerinfos) < 3 else 2 577 ) 578 else: 579 raise RuntimeError() 580 581 self.setup_low_life_warning_sound() 582 583 self._drop_powerups(standard_points=True) 584 bs.timer(4.0, self._start_powerup_drops) 585 586 # Make a bogus team for our bots. 587 bad_team_name = self.get_team_display_string('Bad Guys') 588 self._bot_team = Team() 589 self._bot_team.manual_init( 590 team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4) 591 ) 592 593 for team in [self.teams[0], self._bot_team]: 594 team.score = 0 595 596 self.update_scores() 597 598 # Time display. 599 starttime_ms = int(bs.time() * 1000.0) 600 assert isinstance(starttime_ms, int) 601 self._starttime_ms = starttime_ms 602 self._time_text = bs.NodeActor( 603 bs.newnode( 604 'text', 605 attrs={ 606 'v_attach': 'top', 607 'h_attach': 'center', 608 'h_align': 'center', 609 'color': (1, 1, 0.5, 1), 610 'flatness': 0.5, 611 'shadow': 0.5, 612 'position': (0, -50), 613 'scale': 1.3, 614 'text': '', 615 }, 616 ) 617 ) 618 self._time_text_input = bs.NodeActor( 619 bs.newnode('timedisplay', attrs={'showsubseconds': True}) 620 ) 621 self.globalsnode.connectattr( 622 'time', self._time_text_input.node, 'time2' 623 ) 624 assert self._time_text_input.node 625 assert self._time_text.node 626 self._time_text_input.node.connectattr( 627 'output', self._time_text.node, 'text' 628 ) 629 630 # Our TNT spawner (if applicable). 631 if self._have_tnt: 632 self._tntspawner = TNTSpawner(position=(0, 1, -1)) 633 634 self._bots = SpazBotSet() 635 self._bot_spawn_timer = bs.Timer(1.0, self._update_bots, repeat=True) 636 637 for bottype in self._bot_types_initial: 638 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.
811 @override 812 def end_game(self) -> None: 813 bs.setmusic(None) 814 self._bots.final_celebrate() 815 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.
817 def update_scores(self) -> None: 818 """update scoreboard and check for winners""" 819 # FIXME: tidy this up 820 # pylint: disable=too-many-nested-blocks 821 have_scoring_team = False 822 win_score = self._score_to_win 823 for team in [self.teams[0], self._bot_team]: 824 assert team is not None 825 assert self._scoreboard is not None 826 self._scoreboard.set_team_value(team, team.score, win_score) 827 if team.score >= win_score: 828 if not have_scoring_team: 829 self._scoring_team = team 830 if team is self._bot_team: 831 self.end_game() 832 else: 833 bs.setmusic(bs.MusicType.VICTORY) 834 835 # Completion achievements. 836 assert self._bot_team is not None 837 if self._preset in ['rookie', 'rookie_easy']: 838 self._award_achievement( 839 'Rookie Football Victory', sound=False 840 ) 841 if self._bot_team.score == 0: 842 self._award_achievement( 843 'Rookie Football Shutout', sound=False 844 ) 845 elif self._preset in ['pro', 'pro_easy']: 846 self._award_achievement( 847 'Pro Football Victory', sound=False 848 ) 849 if self._bot_team.score == 0: 850 self._award_achievement( 851 'Pro Football Shutout', sound=False 852 ) 853 elif self._preset in ['uber', 'uber_easy']: 854 self._award_achievement( 855 'Uber Football Victory', sound=False 856 ) 857 if self._bot_team.score == 0: 858 self._award_achievement( 859 'Uber Football Shutout', sound=False 860 ) 861 if ( 862 not self._player_has_dropped_bomb 863 and not self._player_has_punched 864 ): 865 self._award_achievement( 866 'Got the Moves', sound=False 867 ) 868 self._bots.stop_moving() 869 self.show_zoom_message( 870 bs.Lstr(resource='victoryText'), 871 scale=1.0, 872 duration=4.0, 873 ) 874 self.celebrate(10.0) 875 assert self._starttime_ms is not None 876 self._final_time_ms = int( 877 int(bs.time() * 1000.0) - self._starttime_ms 878 ) 879 self._time_text_timer = None 880 assert ( 881 self._time_text_input is not None 882 and self._time_text_input.node 883 ) 884 self._time_text_input.node.timemax = self._final_time_ms 885 886 self.do_end('victory')
update scoreboard and check for winners
888 def do_end(self, outcome: str) -> None: 889 """End the game with the specified outcome.""" 890 if outcome == 'defeat': 891 self.fade_to_red() 892 assert self._final_time_ms is not None 893 scoreval = ( 894 None if outcome == 'defeat' else int(self._final_time_ms // 10) 895 ) 896 self.end( 897 delay=3.0, 898 results={ 899 'outcome': outcome, 900 'score': scoreval, 901 'score_order': 'decreasing', 902 'playerinfos': self.initialplayerinfos, 903 }, 904 )
End the game with the specified outcome.
906 @override 907 def handlemessage(self, msg: Any) -> Any: 908 """handle high-level game messages""" 909 if isinstance(msg, bs.PlayerDiedMessage): 910 # Augment standard behavior. 911 super().handlemessage(msg) 912 913 # Respawn them shortly. 914 player = msg.getplayer(Player) 915 assert self.initialplayerinfos is not None 916 respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0 917 player.respawn_timer = bs.Timer( 918 respawn_time, bs.Call(self.spawn_player_if_exists, player) 919 ) 920 player.respawn_icon = RespawnIcon(player, respawn_time) 921 922 elif isinstance(msg, SpazBotDiedMessage): 923 # Every time a bad guy dies, spawn a new one. 924 bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot)))) 925 926 elif isinstance(msg, SpazBotPunchedMessage): 927 if self._preset in ['rookie', 'rookie_easy']: 928 if msg.damage >= 500: 929 self._award_achievement('Super Punch') 930 elif self._preset in ['pro', 'pro_easy']: 931 if msg.damage >= 1000: 932 self._award_achievement('Super Mega Punch') 933 934 # Respawn dead flags. 935 elif isinstance(msg, FlagDiedMessage): 936 assert isinstance(msg.flag, FootballFlag) 937 if msg.self_kill: 938 self._spawn_flag() 939 else: 940 msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag) 941 self._flag_respawn_light = bs.NodeActor( 942 bs.newnode( 943 'light', 944 attrs={ 945 'position': self._flag_spawn_pos, 946 'height_attenuated': False, 947 'radius': 0.15, 948 'color': (1.0, 1.0, 0.3), 949 }, 950 ) 951 ) 952 assert self._flag_respawn_light.node 953 bs.animate( 954 self._flag_respawn_light.node, 955 'intensity', 956 {0: 0, 0.25: 0.15, 0.5: 0}, 957 loop=(not msg.self_kill), 958 ) 959 bs.timer(3.0, self._flag_respawn_light.node.delete) 960 else: 961 return super().handlemessage(msg) 962 return None
handle high-level game messages
972 @override 973 def spawn_player(self, player: Player) -> bs.Actor: 974 spaz = self.spawn_player_spaz( 975 player, position=self.map.get_start_position(player.team.id) 976 ) 977 if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']: 978 spaz.impact_scale = 0.25 979 spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb) 980 spaz.punch_callback = self._handle_player_punched 981 return spaz
Spawn something for the provided bascenev1.Player.
The default implementation simply calls spawn_player_spaz().