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

Custom flag class for football games.

FootballFlag(position: Sequence[float])
38    def __init__(self, position: Sequence[float]):
39        super().__init__(position=position,
40                         dropped_timeout=20,
41                         color=(1.0, 1.0, 0.3))
42        assert self.node
43        self.last_holding_player: ba.Player | None = None
44        self.node.is_area_of_interest = True
45        self.respawn_timer: ba.Timer | None = None
46        self.scored = False
47        self.held_count = 0
48        self.light = ba.newnode('light',
49                                owner=self.node,
50                                attrs={
51                                    'intensity': 0.25,
52                                    'height_attenuated': False,
53                                    'radius': 0.2,
54                                    'color': (0.9, 0.7, 0.0)
55                                })
56        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')]):
59class Player(ba.Player['Team']):
60    """Our player type for this game."""
61
62    def __init__(self) -> None:
63        self.respawn_timer: ba.Timer | None = None
64        self.respawn_icon: RespawnIcon | None = None

Our player type for this game.

Player()
62    def __init__(self) -> None:
63        self.respawn_timer: ba.Timer | None = None
64        self.respawn_icon: RespawnIcon | None = None
class Team(ba._team.Team[bastd.game.football.Player]):
67class Team(ba.Team[Player]):
68    """Our team type for this game."""
69
70    def __init__(self) -> None:
71        self.score = 0

Our team type for this game.

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

Football game for teams mode.

FootballTeamGame(settings: dict)
122    def __init__(self, settings: dict):
123        super().__init__(settings)
124        self._scoreboard: Scoreboard | None = Scoreboard()
125
126        # Load some media we need.
127        self._cheer_sound = ba.getsound('cheer')
128        self._chant_sound = ba.getsound('crowdChant')
129        self._score_sound = ba.getsound('score')
130        self._swipsound = ba.getsound('swip')
131        self._whistle_sound = ba.getsound('refWhistle')
132        self._score_region_material = ba.Material()
133        self._score_region_material.add_actions(
134            conditions=('they_have_material', FlagFactory.get().flagmaterial),
135            actions=(
136                ('modify_part_collision', 'collide', True),
137                ('modify_part_collision', 'physical', False),
138                ('call', 'at_connect', self._handle_score),
139            ))
140        self._flag_spawn_pos: Sequence[float] | None = None
141        self._score_regions: list[ba.NodeActor] = []
142        self._flag: FootballFlag | None = None
143        self._flag_respawn_timer: ba.Timer | None = None
144        self._flag_respawn_light: ba.NodeActor | None = None
145        self._score_to_win = int(settings['Score to Win'])
146        self._time_limit = float(settings['Time Limit'])

Instantiate the Activity.

name: str | None = 'Football'
description: str | None = 'Get the flag to the enemy end zone.'
available_settings: list[ba._settings.Setting] | None = [IntSetting(name='Score to Win', default=21, min_value=7, max_value=9999, increment=7), IntChoiceSetting(name='Time Limit', default=0, choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)]), FloatChoiceSetting(name='Respawn Times', default=1.0, choices=[('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)])]
default_music: ba._music.MusicType | None = <MusicType.FOOTBALL: 'Football'>
@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
113    @classmethod
114    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
115        # We only support two-team play.
116        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]:
118    @classmethod
119    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
120        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.

def get_instance_description(self) -> Union[str, Sequence]:
148    def get_instance_description(self) -> str | Sequence:
149        touchdowns = self._score_to_win / 7
150
151        # NOTE: if use just touchdowns = self._score_to_win // 7
152        # and we will need to score, for example, 27 points,
153        # we will be required to score 3 (not 4) goals ..
154        touchdowns = math.ceil(touchdowns)
155        if touchdowns > 1:
156            return 'Score ${ARG1} touchdowns.', touchdowns
157        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]:
159    def get_instance_description_short(self) -> str | Sequence:
160        touchdowns = self._score_to_win / 7
161        touchdowns = math.ceil(touchdowns)
162        if touchdowns > 1:
163            return 'score ${ARG1} touchdowns', touchdowns
164        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:
166    def on_begin(self) -> None:
167        super().on_begin()
168        self.setup_standard_time_limit(self._time_limit)
169        self.setup_standard_powerup_drops()
170        self._flag_spawn_pos = (self.map.get_flag_position(None))
171        self._spawn_flag()
172        defs = self.map.defs
173        self._score_regions.append(
174            ba.NodeActor(
175                ba.newnode('region',
176                           attrs={
177                               'position': defs.boxes['goal1'][0:3],
178                               'scale': defs.boxes['goal1'][6:9],
179                               'type': 'box',
180                               'materials': (self._score_region_material, )
181                           })))
182        self._score_regions.append(
183            ba.NodeActor(
184                ba.newnode('region',
185                           attrs={
186                               'position': defs.boxes['goal2'][0:3],
187                               'scale': defs.boxes['goal2'][6:9],
188                               'type': 'box',
189                               'materials': (self._score_region_material, )
190                           })))
191        self._update_scoreboard()
192        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:
194    def on_team_join(self, team: Team) -> None:
195        self._update_scoreboard()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

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

Co-op variant of football.

FootballCoopGame(settings: dict)
354    def __init__(self, settings: dict):
355        settings['map'] = 'Football Stadium'
356        super().__init__(settings)
357        self._preset = settings.get('preset', 'rookie')
358
359        # Load some media we need.
360        self._cheer_sound = ba.getsound('cheer')
361        self._boo_sound = ba.getsound('boo')
362        self._chant_sound = ba.getsound('crowdChant')
363        self._score_sound = ba.getsound('score')
364        self._swipsound = ba.getsound('swip')
365        self._whistle_sound = ba.getsound('refWhistle')
366        self._score_to_win = 21
367        self._score_region_material = ba.Material()
368        self._score_region_material.add_actions(
369            conditions=('they_have_material', FlagFactory.get().flagmaterial),
370            actions=(
371                ('modify_part_collision', 'collide', True),
372                ('modify_part_collision', 'physical', False),
373                ('call', 'at_connect', self._handle_score),
374            ))
375        self._powerup_center = (0, 2, 0)
376        self._powerup_spread = (10, 5.5)
377        self._player_has_dropped_bomb = False
378        self._player_has_punched = False
379        self._scoreboard: Scoreboard | None = None
380        self._flag_spawn_pos: Sequence[float] | None = None
381        self._score_regions: list[ba.NodeActor] = []
382        self._exclude_powerups: list[str] = []
383        self._have_tnt = False
384        self._bot_types_initial: list[type[SpazBot]] | None = None
385        self._bot_types_7: list[type[SpazBot]] | None = None
386        self._bot_types_14: list[type[SpazBot]] | None = None
387        self._bot_team: Team | None = None
388        self._starttime_ms: int | None = None
389        self._time_text: ba.NodeActor | None = None
390        self._time_text_input: ba.NodeActor | None = None
391        self._tntspawner: TNTSpawner | None = None
392        self._bots = SpazBotSet()
393        self._bot_spawn_timer: ba.Timer | None = None
394        self._powerup_drop_timer: ba.Timer | None = None
395        self._scoring_team: Team | None = None
396        self._final_time_ms: int | None = None
397        self._time_text_timer: ba.Timer | None = None
398        self._flag_respawn_light: ba.Actor | None = None
399        self._flag: FootballFlag | None = None

Instantiate the Activity.

name: str | None = 'Football'
tips: list[str | ba._gameutils.GameTip] = ['Use the pick-up button to grab the flag < ${PICKUP} >']
scoreconfig: ba._score.ScoreConfig | None = ScoreConfig(label='Score', scoretype=<ScoreType.MILLISECONDS: 'ms'>, lower_is_better=False, none_is_winner=False, version='B')
default_music: ba._music.MusicType | None = <MusicType.FOOTBALL: 'Football'>
def get_score_type(self) -> str:
337    def get_score_type(self) -> str:
338        return 'time'

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

def get_instance_description(self) -> Union[str, Sequence]:
340    def get_instance_description(self) -> str | Sequence:
341        touchdowns = self._score_to_win / 7
342        touchdowns = math.ceil(touchdowns)
343        if touchdowns > 1:
344            return 'Score ${ARG1} touchdowns.', touchdowns
345        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]:
347    def get_instance_description_short(self) -> str | Sequence:
348        touchdowns = self._score_to_win / 7
349        touchdowns = math.ceil(touchdowns)
350        if touchdowns > 1:
351            return 'score ${ARG1} touchdowns', touchdowns
352        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:
401    def on_transition_in(self) -> None:
402        super().on_transition_in()
403        self._scoreboard = Scoreboard()
404        self._flag_spawn_pos = self.map.get_flag_position(None)
405        self._spawn_flag()
406
407        # Set up the two score regions.
408        defs = self.map.defs
409        self._score_regions.append(
410            ba.NodeActor(
411                ba.newnode('region',
412                           attrs={
413                               'position': defs.boxes['goal1'][0:3],
414                               'scale': defs.boxes['goal1'][6:9],
415                               'type': 'box',
416                               'materials': [self._score_region_material]
417                           })))
418        self._score_regions.append(
419            ba.NodeActor(
420                ba.newnode('region',
421                           attrs={
422                               'position': defs.boxes['goal2'][0:3],
423                               'scale': defs.boxes['goal2'][6:9],
424                               'type': 'box',
425                               'materials': [self._score_region_material]
426                           })))
427        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:
429    def on_begin(self) -> None:
430        # FIXME: Split this up a bit.
431        # pylint: disable=too-many-statements
432        from bastd.actor import controlsguide
433        super().on_begin()
434
435        # Show controls help in kiosk mode.
436        if ba.app.demo_mode or ba.app.arcade_mode:
437            controlsguide.ControlsGuide(delay=3.0, lifespan=10.0,
438                                        bright=True).autoretain()
439        assert self.initialplayerinfos is not None
440        abot: type[SpazBot]
441        bbot: type[SpazBot]
442        cbot: type[SpazBot]
443        if self._preset in ['rookie', 'rookie_easy']:
444            self._exclude_powerups = ['curse']
445            self._have_tnt = False
446            abot = (BrawlerBotLite
447                    if self._preset == 'rookie_easy' else BrawlerBot)
448            self._bot_types_initial = [abot] * len(self.initialplayerinfos)
449            bbot = (BomberBotLite
450                    if self._preset == 'rookie_easy' else BomberBot)
451            self._bot_types_7 = (
452                [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
453            cbot = (BomberBot if self._preset == 'rookie_easy' else TriggerBot)
454            self._bot_types_14 = (
455                [cbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
456        elif self._preset == 'tournament':
457            self._exclude_powerups = []
458            self._have_tnt = True
459            self._bot_types_initial = (
460                [BrawlerBot] * (1 if len(self.initialplayerinfos) < 2 else 2))
461            self._bot_types_7 = (
462                [TriggerBot] * (1 if len(self.initialplayerinfos) < 3 else 2))
463            self._bot_types_14 = (
464                [ChargerBot] * (1 if len(self.initialplayerinfos) < 4 else 2))
465        elif self._preset in ['pro', 'pro_easy', 'tournament_pro']:
466            self._exclude_powerups = ['curse']
467            self._have_tnt = True
468            self._bot_types_initial = [ChargerBot] * len(
469                self.initialplayerinfos)
470            abot = (BrawlerBot if self._preset == 'pro' else BrawlerBotLite)
471            typed_bot_list: list[type[SpazBot]] = []
472            self._bot_types_7 = (
473                typed_bot_list + [abot] + [BomberBot] *
474                (1 if len(self.initialplayerinfos) < 3 else 2))
475            bbot = (TriggerBotPro if self._preset == 'pro' else TriggerBot)
476            self._bot_types_14 = (
477                [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
478        elif self._preset in ['uber', 'uber_easy']:
479            self._exclude_powerups = []
480            self._have_tnt = True
481            abot = (BrawlerBotPro if self._preset == 'uber' else BrawlerBot)
482            bbot = (TriggerBotPro if self._preset == 'uber' else TriggerBot)
483            typed_bot_list_2: list[type[SpazBot]] = []
484            self._bot_types_initial = (typed_bot_list_2 + [StickyBot] +
485                                       [abot] * len(self.initialplayerinfos))
486            self._bot_types_7 = (
487                [bbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
488            self._bot_types_14 = (
489                [ExplodeyBot] * (1 if len(self.initialplayerinfos) < 3 else 2))
490        else:
491            raise Exception()
492
493        self.setup_low_life_warning_sound()
494
495        self._drop_powerups(standard_points=True)
496        ba.timer(4.0, self._start_powerup_drops)
497
498        # Make a bogus team for our bots.
499        bad_team_name = self.get_team_display_string('Bad Guys')
500        self._bot_team = Team()
501        self._bot_team.manual_init(team_id=1,
502                                   name=bad_team_name,
503                                   color=(0.5, 0.4, 0.4))
504
505        for team in [self.teams[0], self._bot_team]:
506            team.score = 0
507
508        self.update_scores()
509
510        # Time display.
511        starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
512        assert isinstance(starttime_ms, int)
513        self._starttime_ms = starttime_ms
514        self._time_text = ba.NodeActor(
515            ba.newnode('text',
516                       attrs={
517                           'v_attach': 'top',
518                           'h_attach': 'center',
519                           'h_align': 'center',
520                           'color': (1, 1, 0.5, 1),
521                           'flatness': 0.5,
522                           'shadow': 0.5,
523                           'position': (0, -50),
524                           'scale': 1.3,
525                           'text': ''
526                       }))
527        self._time_text_input = ba.NodeActor(
528            ba.newnode('timedisplay', attrs={'showsubseconds': True}))
529        self.globalsnode.connectattr('time', self._time_text_input.node,
530                                     'time2')
531        assert self._time_text_input.node
532        assert self._time_text.node
533        self._time_text_input.node.connectattr('output', self._time_text.node,
534                                               'text')
535
536        # Our TNT spawner (if applicable).
537        if self._have_tnt:
538            self._tntspawner = TNTSpawner(position=(0, 1, -1))
539
540        self._bots = SpazBotSet()
541        self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True)
542
543        for bottype in self._bot_types_initial:
544            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:
709    def end_game(self) -> None:
710        ba.setmusic(None)
711        self._bots.final_celebrate()
712        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:
714    def on_continue(self) -> None:
715        # Subtract one touchdown from the bots and get them moving again.
716        assert self._bot_team is not None
717        self._bot_team.score -= 7
718        self._bots.start_moving()
719        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:
721    def update_scores(self) -> None:
722        """ update scoreboard and check for winners """
723        # FIXME: tidy this up
724        # pylint: disable=too-many-nested-blocks
725        have_scoring_team = False
726        win_score = self._score_to_win
727        for team in [self.teams[0], self._bot_team]:
728            assert team is not None
729            assert self._scoreboard is not None
730            self._scoreboard.set_team_value(team, team.score, win_score)
731            if team.score >= win_score:
732                if not have_scoring_team:
733                    self._scoring_team = team
734                    if team is self._bot_team:
735                        self.continue_or_end_game()
736                    else:
737                        ba.setmusic(ba.MusicType.VICTORY)
738
739                        # Completion achievements.
740                        assert self._bot_team is not None
741                        if self._preset in ['rookie', 'rookie_easy']:
742                            self._award_achievement('Rookie Football Victory',
743                                                    sound=False)
744                            if self._bot_team.score == 0:
745                                self._award_achievement(
746                                    'Rookie Football Shutout', sound=False)
747                        elif self._preset in ['pro', 'pro_easy']:
748                            self._award_achievement('Pro Football Victory',
749                                                    sound=False)
750                            if self._bot_team.score == 0:
751                                self._award_achievement('Pro Football Shutout',
752                                                        sound=False)
753                        elif self._preset in ['uber', 'uber_easy']:
754                            self._award_achievement('Uber Football Victory',
755                                                    sound=False)
756                            if self._bot_team.score == 0:
757                                self._award_achievement(
758                                    'Uber Football Shutout', sound=False)
759                            if (not self._player_has_dropped_bomb
760                                    and not self._player_has_punched):
761                                self._award_achievement('Got the Moves',
762                                                        sound=False)
763                        self._bots.stop_moving()
764                        self.show_zoom_message(ba.Lstr(resource='victoryText'),
765                                               scale=1.0,
766                                               duration=4.0)
767                        self.celebrate(10.0)
768                        assert self._starttime_ms is not None
769                        self._final_time_ms = int(
770                            ba.time(timeformat=ba.TimeFormat.MILLISECONDS) -
771                            self._starttime_ms)
772                        self._time_text_timer = None
773                        assert (self._time_text_input is not None
774                                and self._time_text_input.node)
775                        self._time_text_input.node.timemax = (
776                            self._final_time_ms)
777
778                        # FIXME: Does this still need to be deferred?
779                        ba.pushcall(ba.Call(self.do_end, 'victory'))

update scoreboard and check for winners

def do_end(self, outcome: str) -> None:
781    def do_end(self, outcome: str) -> None:
782        """End the game with the specified outcome."""
783        if outcome == 'defeat':
784            self.fade_to_red()
785        assert self._final_time_ms is not None
786        scoreval = (None if outcome == 'defeat' else int(self._final_time_ms //
787                                                         10))
788        self.end(delay=3.0,
789                 results={
790                     'outcome': outcome,
791                     'score': scoreval,
792                     'score_order': 'decreasing',
793                     'playerinfos': self.initialplayerinfos
794                 })

End the game with the specified outcome.

def handlemessage(self, msg: Any) -> Any:
796    def handlemessage(self, msg: Any) -> Any:
797        """ handle high-level game messages """
798        if isinstance(msg, ba.PlayerDiedMessage):
799            # Augment standard behavior.
800            super().handlemessage(msg)
801
802            # Respawn them shortly.
803            player = msg.getplayer(Player)
804            assert self.initialplayerinfos is not None
805            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
806            player.respawn_timer = ba.Timer(
807                respawn_time, ba.Call(self.spawn_player_if_exists, player))
808            player.respawn_icon = RespawnIcon(player, respawn_time)
809
810        elif isinstance(msg, SpazBotDiedMessage):
811
812            # Every time a bad guy dies, spawn a new one.
813            ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot))))
814
815        elif isinstance(msg, SpazBotPunchedMessage):
816            if self._preset in ['rookie', 'rookie_easy']:
817                if msg.damage >= 500:
818                    self._award_achievement('Super Punch')
819            elif self._preset in ['pro', 'pro_easy']:
820                if msg.damage >= 1000:
821                    self._award_achievement('Super Mega Punch')
822
823        # Respawn dead flags.
824        elif isinstance(msg, FlagDiedMessage):
825            assert isinstance(msg.flag, FootballFlag)
826            msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag)
827            self._flag_respawn_light = ba.NodeActor(
828                ba.newnode('light',
829                           attrs={
830                               'position': self._flag_spawn_pos,
831                               'height_attenuated': False,
832                               'radius': 0.15,
833                               'color': (1.0, 1.0, 0.3)
834                           }))
835            assert self._flag_respawn_light.node
836            ba.animate(self._flag_respawn_light.node,
837                       'intensity', {
838                           0: 0,
839                           0.25: 0.15,
840                           0.5: 0
841                       },
842                       loop=True)
843            ba.timer(3.0, self._flag_respawn_light.node.delete)
844        else:
845            return super().handlemessage(msg)
846        return None

handle high-level game messages

def spawn_player(self, player: bastd.game.football.Player) -> ba._actor.Actor:
857    def spawn_player(self, player: Player) -> ba.Actor:
858        spaz = self.spawn_player_spaz(player,
859                                      position=self.map.get_start_position(
860                                          player.team.id))
861        if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
862            spaz.impact_scale = 0.25
863        spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)
864        spaz.punch_callback = self._handle_player_punched
865        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
description
available_settings
allow_pausing
allow_kick_idle_players
show_kill_points
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_available_settings
get_supported_maps
get_settings_display_string
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