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