bascenev1lib.game.football

Implements football games (both co-op and teams varieties).

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

Custom flag class for football games.

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

Instantiate a flag.

If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.

'materials can be a list of extra bs.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')]):
80class Player(bs.Player['Team']):
81    """Our player type for this game."""
82
83    def __init__(self) -> None:
84        self.respawn_timer: bs.Timer | None = None
85        self.respawn_icon: RespawnIcon | None = None

Our player type for this game.

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]):
88class Team(bs.Team[Player]):
89    """Our team type for this game."""
90
91    def __init__(self) -> None:
92        self.score = 0

Our team type for this game.

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

Football game for teams mode.

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

Instantiate the Activity.

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:
134    @override
135    @classmethod
136    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
137        # We only support two-team play.
138        return issubclass(sessiontype, bs.DualTeamSession)

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

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
140    @override
141    @classmethod
142    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
143        assert bs.app.classic is not None
144        return bs.app.classic.getmaps('football')

Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.

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

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

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

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

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

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

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

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def end_game(self) -> None:
297    @override
298    def end_game(self) -> None:
299        results = bs.GameResults()
300        for team in self.teams:
301            results.set_team_score(team, team.score)
302        self.end(results=results, announce_delay=0.8)

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.

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

General message handling; can be passed any message object.

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

Co-op variant of football.

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

Instantiate the Activity.

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:
393    @override
394    def get_score_type(self) -> str:
395        return 'time'

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

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

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

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

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

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

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

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

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

@override
def end_game(self) -> None:
814    @override
815    def end_game(self) -> None:
816        bs.setmusic(None)
817        self._bots.final_celebrate()
818        bs.timer(0.001, bs.Call(self.do_end, 'defeat'))

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.

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

This is called if a game supports and offers a continue and the player accepts. In this case the player should be given an extra life or whatever is relevant to keep the game going.

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

update scoreboard and check for winners

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

End the game with the specified outcome.

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

handle high-level game messages

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

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

Inherited Members
bascenev1._coopgame.CoopGameActivity
session
supports_session_type
celebrate
spawn_player_spaz
fade_to_red
setup_low_life_warning_sound
bascenev1._gameactivity.GameActivity
description
available_settings
allow_pausing
allow_kick_idle_players
show_kill_points
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_available_settings
get_supported_maps
get_settings_display_string
initialplayerinfos
map
get_instance_display_string
get_instance_scoreboard_display_string
is_waiting_for_continue
continue_or_end_game
on_player_join
end
respawn_player
spawn_player_if_exists
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
bascenev1._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
use_fixed_vr_overlay
slow_motion
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
allow_mid_activity_joins
transition_time
can_show_ad_on_death
paused_text
preloads
lobby
context
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
on_player_leave
on_team_join
on_team_leave
on_transition_out
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
create_player
create_team
bascenev1._dependency.DependencyComponent
dep_is_present
get_dynamic_deps