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
  15
  16from typing_extensions import override
  17import bascenev1 as bs
  18
  19from bascenev1lib.actor.bomb import TNTSpawner
  20from bascenev1lib.actor.playerspaz import PlayerSpaz
  21from bascenev1lib.actor.scoreboard import Scoreboard
  22from bascenev1lib.actor.respawnicon import RespawnIcon
  23from bascenev1lib.actor.powerupbox import PowerupBoxFactory, PowerupBox
  24from bascenev1lib.actor.flag import (
  25    FlagFactory,
  26    Flag,
  27    FlagPickedUpMessage,
  28    FlagDroppedMessage,
  29    FlagDiedMessage,
  30)
  31from bascenev1lib.actor.spazbot import (
  32    SpazBotDiedMessage,
  33    SpazBotPunchedMessage,
  34    SpazBotSet,
  35    BrawlerBotLite,
  36    BrawlerBot,
  37    BomberBotLite,
  38    BomberBot,
  39    TriggerBot,
  40    ChargerBot,
  41    TriggerBotPro,
  42    BrawlerBotPro,
  43    StickyBot,
  44    ExplodeyBot,
  45)
  46
  47if TYPE_CHECKING:
  48    from typing import Any, Sequence
  49
  50    from bascenev1lib.actor.spaz import Spaz
  51    from bascenev1lib.actor.spazbot import SpazBot
  52
  53
  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')
  78
  79
  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
  86
  87
  88class Team(bs.Team[Player]):
  89    """Our team type for this game."""
  90
  91    def __init__(self) -> None:
  92        self.score = 0
  93
  94
  95# ba_meta export bascenev1.GameActivity
  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)
 379
 380
 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)
class FootballFlag(bascenev1lib.actor.flag.Flag):
55class FootballFlag(Flag):
56    """Custom flag class for football games."""
57
58    def __init__(self, position: Sequence[float]):
59        super().__init__(
60            position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3)
61        )
62        assert self.node
63        self.last_holding_player: bs.Player | None = None
64        self.node.is_area_of_interest = True
65        self.respawn_timer: bs.Timer | None = None
66        self.scored = False
67        self.held_count = 0
68        self.light = bs.newnode(
69            'light',
70            owner=self.node,
71            attrs={
72                'intensity': 0.25,
73                'height_attenuated': False,
74                'radius': 0.2,
75                'color': (0.9, 0.7, 0.0),
76            },
77        )
78        self.node.connectattr('position', self.light, 'position')

Custom flag class for football games.

FootballFlag(position: Sequence[float])
58    def __init__(self, position: Sequence[float]):
59        super().__init__(
60            position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3)
61        )
62        assert self.node
63        self.last_holding_player: bs.Player | None = None
64        self.node.is_area_of_interest = True
65        self.respawn_timer: bs.Timer | None = None
66        self.scored = False
67        self.held_count = 0
68        self.light = bs.newnode(
69            'light',
70            owner=self.node,
71            attrs={
72                'intensity': 0.25,
73                'height_attenuated': False,
74                'radius': 0.2,
75                'color': (0.9, 0.7, 0.0),
76            },
77        )
78        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.Materials 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.

last_holding_player: bascenev1._player.Player | None
respawn_timer: _bascenev1.Timer | None
scored
held_count
light
Inherited Members
bascenev1lib.actor.flag.Flag
node
set_score_text
handlemessage
project_stand
bascenev1._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Player(bascenev1._player.Player[ForwardRef('Team')]):
81class Player(bs.Player['Team']):
82    """Our player type for this game."""
83
84    def __init__(self) -> None:
85        self.respawn_timer: bs.Timer | None = None
86        self.respawn_icon: RespawnIcon | None = None

Our player type for this game.

respawn_timer: _bascenev1.Timer | None
Inherited Members
bascenev1._player.Player
character
actor
color
highlight
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(bascenev1._team.Team[bascenev1lib.game.football.Player]):
89class Team(bs.Team[Player]):
90    """Our team type for this game."""
91
92    def __init__(self) -> None:
93        self.score = 0

Our team type for this game.

score
Inherited Members
bascenev1._team.Team
players
id
name
color
manual_init
customdata
on_expire
sessionteam
class FootballTeamGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.football.Player, bascenev1lib.game.football.Team]):
 97class FootballTeamGame(bs.TeamGameActivity[Player, Team]):
 98    """Football game for teams mode."""
 99
100    name = 'Football'
101    description = 'Get the flag to the enemy end zone.'
102    available_settings = [
103        bs.IntSetting(
104            'Score to Win',
105            min_value=7,
106            default=21,
107            increment=7,
108        ),
109        bs.IntChoiceSetting(
110            'Time Limit',
111            choices=[
112                ('None', 0),
113                ('1 Minute', 60),
114                ('2 Minutes', 120),
115                ('5 Minutes', 300),
116                ('10 Minutes', 600),
117                ('20 Minutes', 1200),
118            ],
119            default=0,
120        ),
121        bs.FloatChoiceSetting(
122            'Respawn Times',
123            choices=[
124                ('Shorter', 0.25),
125                ('Short', 0.5),
126                ('Normal', 1.0),
127                ('Long', 2.0),
128                ('Longer', 4.0),
129            ],
130            default=1.0,
131        ),
132        bs.BoolSetting('Epic Mode', default=False),
133    ]
134
135    @override
136    @classmethod
137    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
138        # We only support two-team play.
139        return issubclass(sessiontype, bs.DualTeamSession)
140
141    @override
142    @classmethod
143    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
144        assert bs.app.classic is not None
145        return bs.app.classic.getmaps('football')
146
147    def __init__(self, settings: dict):
148        super().__init__(settings)
149        self._scoreboard: Scoreboard | None = Scoreboard()
150
151        # Load some media we need.
152        self._cheer_sound = bs.getsound('cheer')
153        self._chant_sound = bs.getsound('crowdChant')
154        self._score_sound = bs.getsound('score')
155        self._swipsound = bs.getsound('swip')
156        self._whistle_sound = bs.getsound('refWhistle')
157        self._score_region_material = bs.Material()
158        self._score_region_material.add_actions(
159            conditions=('they_have_material', FlagFactory.get().flagmaterial),
160            actions=(
161                ('modify_part_collision', 'collide', True),
162                ('modify_part_collision', 'physical', False),
163                ('call', 'at_connect', self._handle_score),
164            ),
165        )
166        self._flag_spawn_pos: Sequence[float] | None = None
167        self._score_regions: list[bs.NodeActor] = []
168        self._flag: FootballFlag | None = None
169        self._flag_respawn_timer: bs.Timer | None = None
170        self._flag_respawn_light: bs.NodeActor | None = None
171        self._score_to_win = int(settings['Score to Win'])
172        self._time_limit = float(settings['Time Limit'])
173        self._epic_mode = bool(settings['Epic Mode'])
174        self.slow_motion = self._epic_mode
175        self.default_music = (
176            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FOOTBALL
177        )
178
179    @override
180    def get_instance_description(self) -> str | Sequence:
181        touchdowns = self._score_to_win / 7
182
183        # NOTE: if use just touchdowns = self._score_to_win // 7
184        # and we will need to score, for example, 27 points,
185        # we will be required to score 3 (not 4) goals ..
186        touchdowns = math.ceil(touchdowns)
187        if touchdowns > 1:
188            return 'Score ${ARG1} touchdowns.', touchdowns
189        return 'Score a touchdown.'
190
191    @override
192    def get_instance_description_short(self) -> str | Sequence:
193        touchdowns = self._score_to_win / 7
194        touchdowns = math.ceil(touchdowns)
195        if touchdowns > 1:
196            return 'score ${ARG1} touchdowns', touchdowns
197        return 'score a touchdown'
198
199    @override
200    def on_begin(self) -> None:
201        super().on_begin()
202        self.setup_standard_time_limit(self._time_limit)
203        self.setup_standard_powerup_drops()
204        self._flag_spawn_pos = self.map.get_flag_position(None)
205        self._spawn_flag()
206        defs = self.map.defs
207        self._score_regions.append(
208            bs.NodeActor(
209                bs.newnode(
210                    'region',
211                    attrs={
212                        'position': defs.boxes['goal1'][0:3],
213                        'scale': defs.boxes['goal1'][6:9],
214                        'type': 'box',
215                        'materials': (self._score_region_material,),
216                    },
217                )
218            )
219        )
220        self._score_regions.append(
221            bs.NodeActor(
222                bs.newnode(
223                    'region',
224                    attrs={
225                        'position': defs.boxes['goal2'][0:3],
226                        'scale': defs.boxes['goal2'][6:9],
227                        'type': 'box',
228                        'materials': (self._score_region_material,),
229                    },
230                )
231            )
232        )
233        self._update_scoreboard()
234        self._chant_sound.play()
235
236    @override
237    def on_team_join(self, team: Team) -> None:
238        self._update_scoreboard()
239
240    def _kill_flag(self) -> None:
241        self._flag = None
242
243    def _handle_score(self) -> None:
244        """A point has been scored."""
245
246        # Our flag might stick around for a second or two
247        # make sure it doesn't score again.
248        assert self._flag is not None
249        if self._flag.scored:
250            return
251        region = bs.getcollision().sourcenode
252        i = None
253        for i, score_region in enumerate(self._score_regions):
254            if region == score_region.node:
255                break
256        for team in self.teams:
257            if team.id == i:
258                team.score += 7
259
260                # Tell all players to celebrate.
261                for player in team.players:
262                    if player.actor:
263                        player.actor.handlemessage(bs.CelebrateMessage(2.0))
264
265                # If someone on this team was last to touch it,
266                # give them points.
267                assert self._flag is not None
268                if (
269                    self._flag.last_holding_player
270                    and team == self._flag.last_holding_player.team
271                ):
272                    self.stats.player_scored(
273                        self._flag.last_holding_player, 50, big_message=True
274                    )
275                # End the game if we won.
276                if team.score >= self._score_to_win:
277                    self.end_game()
278        self._score_sound.play()
279        self._cheer_sound.play()
280        assert self._flag
281        self._flag.scored = True
282
283        # Kill the flag (it'll respawn shortly).
284        bs.timer(1.0, self._kill_flag)
285        light = bs.newnode(
286            'light',
287            attrs={
288                'position': bs.getcollision().position,
289                'height_attenuated': False,
290                'color': (1, 0, 0),
291            },
292        )
293        bs.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True)
294        bs.timer(1.0, light.delete)
295        bs.cameraflash(duration=10.0)
296        self._update_scoreboard()
297
298    @override
299    def end_game(self) -> None:
300        results = bs.GameResults()
301        for team in self.teams:
302            results.set_team_score(team, team.score)
303        self.end(results=results, announce_delay=0.8)
304
305    def _update_scoreboard(self) -> None:
306        assert self._scoreboard is not None
307        for team in self.teams:
308            self._scoreboard.set_team_value(
309                team, team.score, self._score_to_win
310            )
311
312    @override
313    def handlemessage(self, msg: Any) -> Any:
314        if isinstance(msg, FlagPickedUpMessage):
315            assert isinstance(msg.flag, FootballFlag)
316            try:
317                msg.flag.last_holding_player = msg.node.getdelegate(
318                    PlayerSpaz, True
319                ).getplayer(Player, True)
320            except bs.NotFoundError:
321                pass
322            msg.flag.held_count += 1
323
324        elif isinstance(msg, FlagDroppedMessage):
325            assert isinstance(msg.flag, FootballFlag)
326            msg.flag.held_count -= 1
327
328        # Respawn dead players if they're still in the game.
329        elif isinstance(msg, bs.PlayerDiedMessage):
330            # Augment standard behavior.
331            super().handlemessage(msg)
332            self.respawn_player(msg.getplayer(Player))
333
334        # Respawn dead flags.
335        elif isinstance(msg, FlagDiedMessage):
336            if not self.has_ended():
337                self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag)
338                self._flag_respawn_light = bs.NodeActor(
339                    bs.newnode(
340                        'light',
341                        attrs={
342                            'position': self._flag_spawn_pos,
343                            'height_attenuated': False,
344                            'radius': 0.15,
345                            'color': (1.0, 1.0, 0.3),
346                        },
347                    )
348                )
349                assert self._flag_respawn_light.node
350                bs.animate(
351                    self._flag_respawn_light.node,
352                    'intensity',
353                    {0.0: 0, 0.25: 0.15, 0.5: 0},
354                    loop=True,
355                )
356                bs.timer(3.0, self._flag_respawn_light.node.delete)
357
358        else:
359            # Augment standard behavior.
360            super().handlemessage(msg)
361
362    def _flash_flag_spawn(self) -> None:
363        light = bs.newnode(
364            'light',
365            attrs={
366                'position': self._flag_spawn_pos,
367                'height_attenuated': False,
368                'color': (1, 1, 0),
369            },
370        )
371        bs.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
372        bs.timer(1.0, light.delete)
373
374    def _spawn_flag(self) -> None:
375        self._swipsound.play()
376        self._whistle_sound.play()
377        self._flash_flag_spawn()
378        assert self._flag_spawn_pos is not None
379        self._flag = FootballFlag(position=self._flag_spawn_pos)

Football game for teams mode.

FootballTeamGame(settings: dict)
147    def __init__(self, settings: dict):
148        super().__init__(settings)
149        self._scoreboard: Scoreboard | None = Scoreboard()
150
151        # Load some media we need.
152        self._cheer_sound = bs.getsound('cheer')
153        self._chant_sound = bs.getsound('crowdChant')
154        self._score_sound = bs.getsound('score')
155        self._swipsound = bs.getsound('swip')
156        self._whistle_sound = bs.getsound('refWhistle')
157        self._score_region_material = bs.Material()
158        self._score_region_material.add_actions(
159            conditions=('they_have_material', FlagFactory.get().flagmaterial),
160            actions=(
161                ('modify_part_collision', 'collide', True),
162                ('modify_part_collision', 'physical', False),
163                ('call', 'at_connect', self._handle_score),
164            ),
165        )
166        self._flag_spawn_pos: Sequence[float] | None = None
167        self._score_regions: list[bs.NodeActor] = []
168        self._flag: FootballFlag | None = None
169        self._flag_respawn_timer: bs.Timer | None = None
170        self._flag_respawn_light: bs.NodeActor | None = None
171        self._score_to_win = int(settings['Score to Win'])
172        self._time_limit = float(settings['Time Limit'])
173        self._epic_mode = bool(settings['Epic Mode'])
174        self.slow_motion = self._epic_mode
175        self.default_music = (
176            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FOOTBALL
177        )

Instantiate the Activity.

name = 'Football'
description = 'Get the flag to the enemy end zone.'
available_settings = [IntSetting(name='Score to Win', default=21, min_value=7, max_value=9999, increment=7), IntChoiceSetting(name='Time Limit', default=0, choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)]), FloatChoiceSetting(name='Respawn Times', default=1.0, choices=[('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)]), BoolSetting(name='Epic Mode', default=False)]
@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1._session.Session]) -> bool:
135    @override
136    @classmethod
137    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
138        # We only support two-team play.
139        return issubclass(sessiontype, bs.DualTeamSession)

Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
141    @override
142    @classmethod
143    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
144        assert bs.app.classic is not None
145        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.

slow_motion = False

If True, runs in slow motion and turns down sound pitch.

default_music = None
@override
def get_instance_description(self) -> Union[str, Sequence]:
179    @override
180    def get_instance_description(self) -> str | Sequence:
181        touchdowns = self._score_to_win / 7
182
183        # NOTE: if use just touchdowns = self._score_to_win // 7
184        # and we will need to score, for example, 27 points,
185        # we will be required to score 3 (not 4) goals ..
186        touchdowns = math.ceil(touchdowns)
187        if touchdowns > 1:
188            return 'Score ${ARG1} touchdowns.', touchdowns
189        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.

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
191    @override
192    def get_instance_description_short(self) -> str | Sequence:
193        touchdowns = self._score_to_win / 7
194        touchdowns = math.ceil(touchdowns)
195        if touchdowns > 1:
196            return 'score ${ARG1} touchdowns', touchdowns
197        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.

@override
def on_begin(self) -> None:
199    @override
200    def on_begin(self) -> None:
201        super().on_begin()
202        self.setup_standard_time_limit(self._time_limit)
203        self.setup_standard_powerup_drops()
204        self._flag_spawn_pos = self.map.get_flag_position(None)
205        self._spawn_flag()
206        defs = self.map.defs
207        self._score_regions.append(
208            bs.NodeActor(
209                bs.newnode(
210                    'region',
211                    attrs={
212                        'position': defs.boxes['goal1'][0:3],
213                        'scale': defs.boxes['goal1'][6:9],
214                        'type': 'box',
215                        'materials': (self._score_region_material,),
216                    },
217                )
218            )
219        )
220        self._score_regions.append(
221            bs.NodeActor(
222                bs.newnode(
223                    'region',
224                    attrs={
225                        'position': defs.boxes['goal2'][0:3],
226                        'scale': defs.boxes['goal2'][6:9],
227                        'type': 'box',
228                        'materials': (self._score_region_material,),
229                    },
230                )
231            )
232        )
233        self._update_scoreboard()
234        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.

@override
def on_team_join(self, team: Team) -> None:
236    @override
237    def on_team_join(self, team: Team) -> None:
238        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def end_game(self) -> None:
298    @override
299    def end_game(self) -> None:
300        results = bs.GameResults()
301        for team in self.teams:
302            results.set_team_score(team, team.score)
303        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.

@override
def handlemessage(self, msg: Any) -> Any:
312    @override
313    def handlemessage(self, msg: Any) -> Any:
314        if isinstance(msg, FlagPickedUpMessage):
315            assert isinstance(msg.flag, FootballFlag)
316            try:
317                msg.flag.last_holding_player = msg.node.getdelegate(
318                    PlayerSpaz, True
319                ).getplayer(Player, True)
320            except bs.NotFoundError:
321                pass
322            msg.flag.held_count += 1
323
324        elif isinstance(msg, FlagDroppedMessage):
325            assert isinstance(msg.flag, FootballFlag)
326            msg.flag.held_count -= 1
327
328        # Respawn dead players if they're still in the game.
329        elif isinstance(msg, bs.PlayerDiedMessage):
330            # Augment standard behavior.
331            super().handlemessage(msg)
332            self.respawn_player(msg.getplayer(Player))
333
334        # Respawn dead flags.
335        elif isinstance(msg, FlagDiedMessage):
336            if not self.has_ended():
337                self._flag_respawn_timer = bs.Timer(3.0, self._spawn_flag)
338                self._flag_respawn_light = bs.NodeActor(
339                    bs.newnode(
340                        'light',
341                        attrs={
342                            'position': self._flag_spawn_pos,
343                            'height_attenuated': False,
344                            'radius': 0.15,
345                            'color': (1.0, 1.0, 0.3),
346                        },
347                    )
348                )
349                assert self._flag_respawn_light.node
350                bs.animate(
351                    self._flag_respawn_light.node,
352                    'intensity',
353                    {0.0: 0, 0.25: 0.15, 0.5: 0},
354                    loop=True,
355                )
356                bs.timer(3.0, self._flag_respawn_light.node.delete)
357
358        else:
359            # Augment standard behavior.
360            super().handlemessage(msg)

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

Co-op variant of football.

FootballCoopGame(settings: dict)
414    def __init__(self, settings: dict):
415        settings['map'] = 'Football Stadium'
416        super().__init__(settings)
417        self._preset = settings.get('preset', 'rookie')
418
419        # Load some media we need.
420        self._cheer_sound = bs.getsound('cheer')
421        self._boo_sound = bs.getsound('boo')
422        self._chant_sound = bs.getsound('crowdChant')
423        self._score_sound = bs.getsound('score')
424        self._swipsound = bs.getsound('swip')
425        self._whistle_sound = bs.getsound('refWhistle')
426        self._score_to_win = 21
427        self._score_region_material = bs.Material()
428        self._score_region_material.add_actions(
429            conditions=('they_have_material', FlagFactory.get().flagmaterial),
430            actions=(
431                ('modify_part_collision', 'collide', True),
432                ('modify_part_collision', 'physical', False),
433                ('call', 'at_connect', self._handle_score),
434            ),
435        )
436        self._powerup_center = (0, 2, 0)
437        self._powerup_spread = (10, 5.5)
438        self._player_has_dropped_bomb = False
439        self._player_has_punched = False
440        self._scoreboard: Scoreboard | None = None
441        self._flag_spawn_pos: Sequence[float] | None = None
442        self._score_regions: list[bs.NodeActor] = []
443        self._exclude_powerups: list[str] = []
444        self._have_tnt = False
445        self._bot_types_initial: list[type[SpazBot]] | None = None
446        self._bot_types_7: list[type[SpazBot]] | None = None
447        self._bot_types_14: list[type[SpazBot]] | None = None
448        self._bot_team: Team | None = None
449        self._starttime_ms: int | None = None
450        self._time_text: bs.NodeActor | None = None
451        self._time_text_input: bs.NodeActor | None = None
452        self._tntspawner: TNTSpawner | None = None
453        self._bots = SpazBotSet()
454        self._bot_spawn_timer: bs.Timer | None = None
455        self._powerup_drop_timer: bs.Timer | None = None
456        self._scoring_team: Team | None = None
457        self._final_time_ms: int | None = None
458        self._time_text_timer: bs.Timer | None = None
459        self._flag_respawn_light: bs.Actor | None = None
460        self._flag: FootballFlag | None = None

Instantiate the Activity.

name = 'Football'
tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
scoreconfig = ScoreConfig(label='Score', scoretype=<ScoreType.MILLISECONDS: 'ms'>, lower_is_better=False, none_is_winner=False, version='B')
default_music = <MusicType.FOOTBALL: 'Football'>
@override
def get_score_type(self) -> str:
394    @override
395    def get_score_type(self) -> str:
396        return 'time'

Return the score unit this co-op game uses ('point', 'seconds', etc.)

@override
def get_instance_description(self) -> Union[str, Sequence]:
398    @override
399    def get_instance_description(self) -> str | Sequence:
400        touchdowns = self._score_to_win / 7
401        touchdowns = math.ceil(touchdowns)
402        if touchdowns > 1:
403            return 'Score ${ARG1} touchdowns.', touchdowns
404        return 'Score a touchdown.'

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.

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
406    @override
407    def get_instance_description_short(self) -> str | Sequence:
408        touchdowns = self._score_to_win / 7
409        touchdowns = math.ceil(touchdowns)
410        if touchdowns > 1:
411            return 'score ${ARG1} touchdowns', touchdowns
412        return 'score a touchdown'

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.

@override
def on_transition_in(self) -> None:
462    @override
463    def on_transition_in(self) -> None:
464        super().on_transition_in()
465        self._scoreboard = Scoreboard()
466        self._flag_spawn_pos = self.map.get_flag_position(None)
467        self._spawn_flag()
468
469        # Set up the two score regions.
470        defs = self.map.defs
471        self._score_regions.append(
472            bs.NodeActor(
473                bs.newnode(
474                    'region',
475                    attrs={
476                        'position': defs.boxes['goal1'][0:3],
477                        'scale': defs.boxes['goal1'][6:9],
478                        'type': 'box',
479                        'materials': [self._score_region_material],
480                    },
481                )
482            )
483        )
484        self._score_regions.append(
485            bs.NodeActor(
486                bs.newnode(
487                    'region',
488                    attrs={
489                        'position': defs.boxes['goal2'][0:3],
490                        'scale': defs.boxes['goal2'][6:9],
491                        'type': 'box',
492                        'materials': [self._score_region_material],
493                    },
494                )
495            )
496        )
497        self._chant_sound.play()

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.

@override
def on_begin(self) -> None:
499    @override
500    def on_begin(self) -> None:
501        # FIXME: Split this up a bit.
502        # pylint: disable=too-many-statements
503        from bascenev1lib.actor import controlsguide
504
505        super().on_begin()
506
507        # Show controls help in demo or arcade mode.
508        if bs.app.env.demo or bs.app.env.arcade:
509            controlsguide.ControlsGuide(
510                delay=3.0, lifespan=10.0, bright=True
511            ).autoretain()
512        assert self.initialplayerinfos is not None
513        abot: type[SpazBot]
514        bbot: type[SpazBot]
515        cbot: type[SpazBot]
516        if self._preset in ['rookie', 'rookie_easy']:
517            self._exclude_powerups = ['curse']
518            self._have_tnt = False
519            abot = (
520                BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot
521            )
522            self._bot_types_initial = [abot] * len(self.initialplayerinfos)
523            bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot
524            self._bot_types_7 = [bbot] * (
525                1 if len(self.initialplayerinfos) < 3 else 2
526            )
527            cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot
528            self._bot_types_14 = [cbot] * (
529                1 if len(self.initialplayerinfos) < 3 else 2
530            )
531        elif self._preset == 'tournament':
532            self._exclude_powerups = []
533            self._have_tnt = True
534            self._bot_types_initial = [BrawlerBot] * (
535                1 if len(self.initialplayerinfos) < 2 else 2
536            )
537            self._bot_types_7 = [TriggerBot] * (
538                1 if len(self.initialplayerinfos) < 3 else 2
539            )
540            self._bot_types_14 = [ChargerBot] * (
541                1 if len(self.initialplayerinfos) < 4 else 2
542            )
543        elif self._preset in ['pro', 'pro_easy', 'tournament_pro']:
544            self._exclude_powerups = ['curse']
545            self._have_tnt = True
546            self._bot_types_initial = [ChargerBot] * len(
547                self.initialplayerinfos
548            )
549            abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite
550            typed_bot_list: list[type[SpazBot]] = []
551            self._bot_types_7 = (
552                typed_bot_list
553                + [abot]
554                + [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2)
555            )
556            bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot
557            self._bot_types_14 = [bbot] * (
558                1 if len(self.initialplayerinfos) < 3 else 2
559            )
560        elif self._preset in ['uber', 'uber_easy']:
561            self._exclude_powerups = []
562            self._have_tnt = True
563            abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot
564            bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot
565            typed_bot_list_2: list[type[SpazBot]] = []
566            self._bot_types_initial = (
567                typed_bot_list_2
568                + [StickyBot]
569                + [abot] * len(self.initialplayerinfos)
570            )
571            self._bot_types_7 = [bbot] * (
572                1 if len(self.initialplayerinfos) < 3 else 2
573            )
574            self._bot_types_14 = [ExplodeyBot] * (
575                1 if len(self.initialplayerinfos) < 3 else 2
576            )
577        else:
578            raise RuntimeError()
579
580        self.setup_low_life_warning_sound()
581
582        self._drop_powerups(standard_points=True)
583        bs.timer(4.0, self._start_powerup_drops)
584
585        # Make a bogus team for our bots.
586        bad_team_name = self.get_team_display_string('Bad Guys')
587        self._bot_team = Team()
588        self._bot_team.manual_init(
589            team_id=1, name=bad_team_name, color=(0.5, 0.4, 0.4)
590        )
591
592        for team in [self.teams[0], self._bot_team]:
593            team.score = 0
594
595        self.update_scores()
596
597        # Time display.
598        starttime_ms = int(bs.time() * 1000.0)
599        assert isinstance(starttime_ms, int)
600        self._starttime_ms = starttime_ms
601        self._time_text = bs.NodeActor(
602            bs.newnode(
603                'text',
604                attrs={
605                    'v_attach': 'top',
606                    'h_attach': 'center',
607                    'h_align': 'center',
608                    'color': (1, 1, 0.5, 1),
609                    'flatness': 0.5,
610                    'shadow': 0.5,
611                    'position': (0, -50),
612                    'scale': 1.3,
613                    'text': '',
614                },
615            )
616        )
617        self._time_text_input = bs.NodeActor(
618            bs.newnode('timedisplay', attrs={'showsubseconds': True})
619        )
620        self.globalsnode.connectattr(
621            'time', self._time_text_input.node, 'time2'
622        )
623        assert self._time_text_input.node
624        assert self._time_text.node
625        self._time_text_input.node.connectattr(
626            'output', self._time_text.node, 'text'
627        )
628
629        # Our TNT spawner (if applicable).
630        if self._have_tnt:
631            self._tntspawner = TNTSpawner(position=(0, 1, -1))
632
633        self._bots = SpazBotSet()
634        self._bot_spawn_timer = bs.Timer(1.0, self._update_bots, repeat=True)
635
636        for bottype in self._bot_types_initial:
637            self._spawn_bot(bottype)

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.

@override
def end_game(self) -> None:
815    @override
816    def end_game(self) -> None:
817        bs.setmusic(None)
818        self._bots.final_celebrate()
819        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.

@override
def on_continue(self) -> None:
821    @override
822    def on_continue(self) -> None:
823        # Subtract one touchdown from the bots and get them moving again.
824        assert self._bot_team is not None
825        self._bot_team.score -= 7
826        self._bots.start_moving()
827        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.

def update_scores(self) -> None:
829    def update_scores(self) -> None:
830        """update scoreboard and check for winners"""
831        # FIXME: tidy this up
832        # pylint: disable=too-many-nested-blocks
833        have_scoring_team = False
834        win_score = self._score_to_win
835        for team in [self.teams[0], self._bot_team]:
836            assert team is not None
837            assert self._scoreboard is not None
838            self._scoreboard.set_team_value(team, team.score, win_score)
839            if team.score >= win_score:
840                if not have_scoring_team:
841                    self._scoring_team = team
842                    if team is self._bot_team:
843                        self.continue_or_end_game()
844                    else:
845                        bs.setmusic(bs.MusicType.VICTORY)
846
847                        # Completion achievements.
848                        assert self._bot_team is not None
849                        if self._preset in ['rookie', 'rookie_easy']:
850                            self._award_achievement(
851                                'Rookie Football Victory', sound=False
852                            )
853                            if self._bot_team.score == 0:
854                                self._award_achievement(
855                                    'Rookie Football Shutout', sound=False
856                                )
857                        elif self._preset in ['pro', 'pro_easy']:
858                            self._award_achievement(
859                                'Pro Football Victory', sound=False
860                            )
861                            if self._bot_team.score == 0:
862                                self._award_achievement(
863                                    'Pro Football Shutout', sound=False
864                                )
865                        elif self._preset in ['uber', 'uber_easy']:
866                            self._award_achievement(
867                                'Uber Football Victory', sound=False
868                            )
869                            if self._bot_team.score == 0:
870                                self._award_achievement(
871                                    'Uber Football Shutout', sound=False
872                                )
873                            if (
874                                not self._player_has_dropped_bomb
875                                and not self._player_has_punched
876                            ):
877                                self._award_achievement(
878                                    'Got the Moves', sound=False
879                                )
880                        self._bots.stop_moving()
881                        self.show_zoom_message(
882                            bs.Lstr(resource='victoryText'),
883                            scale=1.0,
884                            duration=4.0,
885                        )
886                        self.celebrate(10.0)
887                        assert self._starttime_ms is not None
888                        self._final_time_ms = int(
889                            int(bs.time() * 1000.0) - self._starttime_ms
890                        )
891                        self._time_text_timer = None
892                        assert (
893                            self._time_text_input is not None
894                            and self._time_text_input.node
895                        )
896                        self._time_text_input.node.timemax = self._final_time_ms
897
898                        # FIXME: Does this still need to be deferred?
899                        bs.pushcall(bs.Call(self.do_end, 'victory'))

update scoreboard and check for winners

def do_end(self, outcome: str) -> None:
901    def do_end(self, outcome: str) -> None:
902        """End the game with the specified outcome."""
903        if outcome == 'defeat':
904            self.fade_to_red()
905        assert self._final_time_ms is not None
906        scoreval = (
907            None if outcome == 'defeat' else int(self._final_time_ms // 10)
908        )
909        self.end(
910            delay=3.0,
911            results={
912                'outcome': outcome,
913                'score': scoreval,
914                'score_order': 'decreasing',
915                'playerinfos': self.initialplayerinfos,
916            },
917        )

End the game with the specified outcome.

@override
def handlemessage(self, msg: Any) -> Any:
919    @override
920    def handlemessage(self, msg: Any) -> Any:
921        """handle high-level game messages"""
922        if isinstance(msg, bs.PlayerDiedMessage):
923            # Augment standard behavior.
924            super().handlemessage(msg)
925
926            # Respawn them shortly.
927            player = msg.getplayer(Player)
928            assert self.initialplayerinfos is not None
929            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
930            player.respawn_timer = bs.Timer(
931                respawn_time, bs.Call(self.spawn_player_if_exists, player)
932            )
933            player.respawn_icon = RespawnIcon(player, respawn_time)
934
935        elif isinstance(msg, SpazBotDiedMessage):
936            # Every time a bad guy dies, spawn a new one.
937            bs.timer(3.0, bs.Call(self._spawn_bot, (type(msg.spazbot))))
938
939        elif isinstance(msg, SpazBotPunchedMessage):
940            if self._preset in ['rookie', 'rookie_easy']:
941                if msg.damage >= 500:
942                    self._award_achievement('Super Punch')
943            elif self._preset in ['pro', 'pro_easy']:
944                if msg.damage >= 1000:
945                    self._award_achievement('Super Mega Punch')
946
947        # Respawn dead flags.
948        elif isinstance(msg, FlagDiedMessage):
949            assert isinstance(msg.flag, FootballFlag)
950            msg.flag.respawn_timer = bs.Timer(3.0, self._spawn_flag)
951            self._flag_respawn_light = bs.NodeActor(
952                bs.newnode(
953                    'light',
954                    attrs={
955                        'position': self._flag_spawn_pos,
956                        'height_attenuated': False,
957                        'radius': 0.15,
958                        'color': (1.0, 1.0, 0.3),
959                    },
960                )
961            )
962            assert self._flag_respawn_light.node
963            bs.animate(
964                self._flag_respawn_light.node,
965                'intensity',
966                {0: 0, 0.25: 0.15, 0.5: 0},
967                loop=True,
968            )
969            bs.timer(3.0, self._flag_respawn_light.node.delete)
970        else:
971            return super().handlemessage(msg)
972        return None

handle high-level game messages

@override
def spawn_player( self, player: Player) -> bascenev1._actor.Actor:
982    @override
983    def spawn_player(self, player: Player) -> bs.Actor:
984        spaz = self.spawn_player_spaz(
985            player, position=self.map.get_start_position(player.team.id)
986        )
987        if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
988            spaz.impact_scale = 0.25
989        spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)
990        spaz.punch_callback = self._handle_player_punched
991        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