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