bastd.game.football

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

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

Custom flag class for football games.

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

Inherited Members
bastd.actor.flag.Flag
set_score_text
handlemessage
project_stand
ba._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Player(ba._player.Player[ForwardRef('Team')]):
76class Player(ba.Player['Team']):
77    """Our player type for this game."""
78
79    def __init__(self) -> None:
80        self.respawn_timer: ba.Timer | None = None
81        self.respawn_icon: RespawnIcon | None = None

Our player type for this game.

Player()
79    def __init__(self) -> None:
80        self.respawn_timer: ba.Timer | None = None
81        self.respawn_icon: RespawnIcon | None = None
Inherited Members
ba._player.Player
actor
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(ba._team.Team[bastd.game.football.Player]):
84class Team(ba.Team[Player]):
85    """Our team type for this game."""
86
87    def __init__(self) -> None:
88        self.score = 0

Our team type for this game.

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

Football game for teams mode.

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

Instantiate the Activity.

@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
130    @classmethod
131    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
132        # We only support two-team play.
133        return issubclass(sessiontype, ba.DualTeamSession)

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

@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
135    @classmethod
136    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
137        return ba.getmaps('football')

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

slow_motion = False

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

def get_instance_description(self) -> Union[str, Sequence]:
171    def get_instance_description(self) -> str | Sequence:
172        touchdowns = self._score_to_win / 7
173
174        # NOTE: if use just touchdowns = self._score_to_win // 7
175        # and we will need to score, for example, 27 points,
176        # we will be required to score 3 (not 4) goals ..
177        touchdowns = math.ceil(touchdowns)
178        if touchdowns > 1:
179            return 'Score ${ARG1} touchdowns.', touchdowns
180        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.

def get_instance_description_short(self) -> Union[str, Sequence]:
182    def get_instance_description_short(self) -> str | Sequence:
183        touchdowns = self._score_to_win / 7
184        touchdowns = math.ceil(touchdowns)
185        if touchdowns > 1:
186            return 'score ${ARG1} touchdowns', touchdowns
187        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.

def on_begin(self) -> None:
189    def on_begin(self) -> None:
190        super().on_begin()
191        self.setup_standard_time_limit(self._time_limit)
192        self.setup_standard_powerup_drops()
193        self._flag_spawn_pos = self.map.get_flag_position(None)
194        self._spawn_flag()
195        defs = self.map.defs
196        self._score_regions.append(
197            ba.NodeActor(
198                ba.newnode(
199                    'region',
200                    attrs={
201                        'position': defs.boxes['goal1'][0:3],
202                        'scale': defs.boxes['goal1'][6:9],
203                        'type': 'box',
204                        'materials': (self._score_region_material,),
205                    },
206                )
207            )
208        )
209        self._score_regions.append(
210            ba.NodeActor(
211                ba.newnode(
212                    'region',
213                    attrs={
214                        'position': defs.boxes['goal2'][0:3],
215                        'scale': defs.boxes['goal2'][6:9],
216                        'type': 'box',
217                        'materials': (self._score_region_material,),
218                    },
219                )
220            )
221        )
222        self._update_scoreboard()
223        ba.playsound(self._chant_sound)

Called once the previous ba.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.

def on_team_join(self, team: bastd.game.football.Team) -> None:
225    def on_team_join(self, team: Team) -> None:
226        self._update_scoreboard()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def end_game(self) -> None:
286    def end_game(self) -> None:
287        results = ba.GameResults()
288        for team in self.teams:
289            results.set_team_score(team, team.score)
290        self.end(results=results, announce_delay=0.8)

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

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 (ba.GameActivity.setup_standard_time_limit()) will work with the game.

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

General message handling; can be passed any message object.

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

Co-op variant of football.

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

Instantiate the Activity.

default_music = <MusicType.FOOTBALL: 'Football'>
def get_score_type(self) -> str:
380    def get_score_type(self) -> str:
381        return 'time'

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

def get_instance_description(self) -> Union[str, Sequence]:
383    def get_instance_description(self) -> str | Sequence:
384        touchdowns = self._score_to_win / 7
385        touchdowns = math.ceil(touchdowns)
386        if touchdowns > 1:
387            return 'Score ${ARG1} touchdowns.', touchdowns
388        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.

def get_instance_description_short(self) -> Union[str, Sequence]:
390    def get_instance_description_short(self) -> str | Sequence:
391        touchdowns = self._score_to_win / 7
392        touchdowns = math.ceil(touchdowns)
393        if touchdowns > 1:
394            return 'score ${ARG1} touchdowns', touchdowns
395        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.

def on_transition_in(self) -> None:
445    def on_transition_in(self) -> None:
446        super().on_transition_in()
447        self._scoreboard = Scoreboard()
448        self._flag_spawn_pos = self.map.get_flag_position(None)
449        self._spawn_flag()
450
451        # Set up the two score regions.
452        defs = self.map.defs
453        self._score_regions.append(
454            ba.NodeActor(
455                ba.newnode(
456                    'region',
457                    attrs={
458                        'position': defs.boxes['goal1'][0:3],
459                        'scale': defs.boxes['goal1'][6:9],
460                        'type': 'box',
461                        'materials': [self._score_region_material],
462                    },
463                )
464            )
465        )
466        self._score_regions.append(
467            ba.NodeActor(
468                ba.newnode(
469                    'region',
470                    attrs={
471                        'position': defs.boxes['goal2'][0:3],
472                        'scale': defs.boxes['goal2'][6:9],
473                        'type': 'box',
474                        'materials': [self._score_region_material],
475                    },
476                )
477            )
478        )
479        ba.playsound(self._chant_sound)

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 ba.Activity.on_begin() is called.

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

Called once the previous ba.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.

def end_game(self) -> None:
796    def end_game(self) -> None:
797        ba.setmusic(None)
798        self._bots.final_celebrate()
799        ba.timer(0.001, ba.Call(self.do_end, 'defeat'))

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

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 (ba.GameActivity.setup_standard_time_limit()) will work with the game.

def on_continue(self) -> None:
801    def on_continue(self) -> None:
802        # Subtract one touchdown from the bots and get them moving again.
803        assert self._bot_team is not None
804        self._bot_team.score -= 7
805        self._bots.start_moving()
806        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:
808    def update_scores(self) -> None:
809        """update scoreboard and check for winners"""
810        # FIXME: tidy this up
811        # pylint: disable=too-many-nested-blocks
812        have_scoring_team = False
813        win_score = self._score_to_win
814        for team in [self.teams[0], self._bot_team]:
815            assert team is not None
816            assert self._scoreboard is not None
817            self._scoreboard.set_team_value(team, team.score, win_score)
818            if team.score >= win_score:
819                if not have_scoring_team:
820                    self._scoring_team = team
821                    if team is self._bot_team:
822                        self.continue_or_end_game()
823                    else:
824                        ba.setmusic(ba.MusicType.VICTORY)
825
826                        # Completion achievements.
827                        assert self._bot_team is not None
828                        if self._preset in ['rookie', 'rookie_easy']:
829                            self._award_achievement(
830                                'Rookie Football Victory', sound=False
831                            )
832                            if self._bot_team.score == 0:
833                                self._award_achievement(
834                                    'Rookie Football Shutout', sound=False
835                                )
836                        elif self._preset in ['pro', 'pro_easy']:
837                            self._award_achievement(
838                                'Pro Football Victory', sound=False
839                            )
840                            if self._bot_team.score == 0:
841                                self._award_achievement(
842                                    'Pro Football Shutout', sound=False
843                                )
844                        elif self._preset in ['uber', 'uber_easy']:
845                            self._award_achievement(
846                                'Uber Football Victory', sound=False
847                            )
848                            if self._bot_team.score == 0:
849                                self._award_achievement(
850                                    'Uber Football Shutout', sound=False
851                                )
852                            if (
853                                not self._player_has_dropped_bomb
854                                and not self._player_has_punched
855                            ):
856                                self._award_achievement(
857                                    'Got the Moves', sound=False
858                                )
859                        self._bots.stop_moving()
860                        self.show_zoom_message(
861                            ba.Lstr(resource='victoryText'),
862                            scale=1.0,
863                            duration=4.0,
864                        )
865                        self.celebrate(10.0)
866                        assert self._starttime_ms is not None
867                        self._final_time_ms = int(
868                            ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
869                            - self._starttime_ms
870                        )
871                        self._time_text_timer = None
872                        assert (
873                            self._time_text_input is not None
874                            and self._time_text_input.node
875                        )
876                        self._time_text_input.node.timemax = self._final_time_ms
877
878                        # FIXME: Does this still need to be deferred?
879                        ba.pushcall(ba.Call(self.do_end, 'victory'))

update scoreboard and check for winners

def do_end(self, outcome: str) -> None:
881    def do_end(self, outcome: str) -> None:
882        """End the game with the specified outcome."""
883        if outcome == 'defeat':
884            self.fade_to_red()
885        assert self._final_time_ms is not None
886        scoreval = (
887            None if outcome == 'defeat' else int(self._final_time_ms // 10)
888        )
889        self.end(
890            delay=3.0,
891            results={
892                'outcome': outcome,
893                'score': scoreval,
894                'score_order': 'decreasing',
895                'playerinfos': self.initialplayerinfos,
896            },
897        )

End the game with the specified outcome.

def handlemessage(self, msg: Any) -> Any:
899    def handlemessage(self, msg: Any) -> Any:
900        """handle high-level game messages"""
901        if isinstance(msg, ba.PlayerDiedMessage):
902            # Augment standard behavior.
903            super().handlemessage(msg)
904
905            # Respawn them shortly.
906            player = msg.getplayer(Player)
907            assert self.initialplayerinfos is not None
908            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
909            player.respawn_timer = ba.Timer(
910                respawn_time, ba.Call(self.spawn_player_if_exists, player)
911            )
912            player.respawn_icon = RespawnIcon(player, respawn_time)
913
914        elif isinstance(msg, SpazBotDiedMessage):
915
916            # Every time a bad guy dies, spawn a new one.
917            ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot))))
918
919        elif isinstance(msg, SpazBotPunchedMessage):
920            if self._preset in ['rookie', 'rookie_easy']:
921                if msg.damage >= 500:
922                    self._award_achievement('Super Punch')
923            elif self._preset in ['pro', 'pro_easy']:
924                if msg.damage >= 1000:
925                    self._award_achievement('Super Mega Punch')
926
927        # Respawn dead flags.
928        elif isinstance(msg, FlagDiedMessage):
929            assert isinstance(msg.flag, FootballFlag)
930            msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag)
931            self._flag_respawn_light = ba.NodeActor(
932                ba.newnode(
933                    'light',
934                    attrs={
935                        'position': self._flag_spawn_pos,
936                        'height_attenuated': False,
937                        'radius': 0.15,
938                        'color': (1.0, 1.0, 0.3),
939                    },
940                )
941            )
942            assert self._flag_respawn_light.node
943            ba.animate(
944                self._flag_respawn_light.node,
945                'intensity',
946                {0: 0, 0.25: 0.15, 0.5: 0},
947                loop=True,
948            )
949            ba.timer(3.0, self._flag_respawn_light.node.delete)
950        else:
951            return super().handlemessage(msg)
952        return None

handle high-level game messages

def spawn_player(self, player: bastd.game.football.Player) -> ba._actor.Actor:
962    def spawn_player(self, player: Player) -> ba.Actor:
963        spaz = self.spawn_player_spaz(
964            player, position=self.map.get_start_position(player.team.id)
965        )
966        if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
967            spaz.impact_scale = 0.25
968        spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)
969        spaz.punch_callback = self._handle_player_punched
970        return spaz

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

Inherited Members
ba._coopgame.CoopGameActivity
session
supports_session_type
celebrate
spawn_player_spaz
fade_to_red
setup_low_life_warning_sound
ba._gameactivity.GameActivity
allow_pausing
allow_kick_idle_players
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
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
ba._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
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
ba._dependency.DependencyComponent
dep_is_present
get_dynamic_deps