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

Custom flag class for football games.

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

Instantiate a flag.

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

'materials can be a list of extra bs.Materials to apply to the flag.

If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.

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

Our player type for this game.

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

Our team type for this game.

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

Football game for teams mode.

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

Instantiate the Activity.

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

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

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

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

slow_motion = False

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

default_music = None
def get_instance_description(self) -> Union[str, Sequence]:
174    def get_instance_description(self) -> str | Sequence:
175        touchdowns = self._score_to_win / 7
176
177        # NOTE: if use just touchdowns = self._score_to_win // 7
178        # and we will need to score, for example, 27 points,
179        # we will be required to score 3 (not 4) goals ..
180        touchdowns = math.ceil(touchdowns)
181        if touchdowns > 1:
182            return 'Score ${ARG1} touchdowns.', touchdowns
183        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]:
185    def get_instance_description_short(self) -> str | Sequence:
186        touchdowns = self._score_to_win / 7
187        touchdowns = math.ceil(touchdowns)
188        if touchdowns > 1:
189            return 'score ${ARG1} touchdowns', touchdowns
190        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:
192    def on_begin(self) -> None:
193        super().on_begin()
194        self.setup_standard_time_limit(self._time_limit)
195        self.setup_standard_powerup_drops()
196        self._flag_spawn_pos = self.map.get_flag_position(None)
197        self._spawn_flag()
198        defs = self.map.defs
199        self._score_regions.append(
200            bs.NodeActor(
201                bs.newnode(
202                    'region',
203                    attrs={
204                        'position': defs.boxes['goal1'][0:3],
205                        'scale': defs.boxes['goal1'][6:9],
206                        'type': 'box',
207                        'materials': (self._score_region_material,),
208                    },
209                )
210            )
211        )
212        self._score_regions.append(
213            bs.NodeActor(
214                bs.newnode(
215                    'region',
216                    attrs={
217                        'position': defs.boxes['goal2'][0:3],
218                        'scale': defs.boxes['goal2'][6:9],
219                        'type': 'box',
220                        'materials': (self._score_region_material,),
221                    },
222                )
223            )
224        )
225        self._update_scoreboard()
226        self._chant_sound.play()

Called once the previous Activity has finished transitioning out.

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

def on_team_join(self, team: Team) -> None:
228    def on_team_join(self, team: Team) -> None:
229        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

def end_game(self) -> None:
289    def end_game(self) -> None:
290        results = bs.GameResults()
291        for team in self.teams:
292            results.set_team_score(team, team.score)
293        self.end(results=results, announce_delay=0.8)

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

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

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

General message handling; can be passed any message object.

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

Co-op variant of football.

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

Instantiate the Activity.

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

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

def get_instance_description(self) -> Union[str, Sequence]:
386    def get_instance_description(self) -> str | Sequence:
387        touchdowns = self._score_to_win / 7
388        touchdowns = math.ceil(touchdowns)
389        if touchdowns > 1:
390            return 'Score ${ARG1} touchdowns.', touchdowns
391        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]:
393    def get_instance_description_short(self) -> str | Sequence:
394        touchdowns = self._score_to_win / 7
395        touchdowns = math.ceil(touchdowns)
396        if touchdowns > 1:
397            return 'score ${ARG1} touchdowns', touchdowns
398        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:
448    def on_transition_in(self) -> None:
449        super().on_transition_in()
450        self._scoreboard = Scoreboard()
451        self._flag_spawn_pos = self.map.get_flag_position(None)
452        self._spawn_flag()
453
454        # Set up the two score regions.
455        defs = self.map.defs
456        self._score_regions.append(
457            bs.NodeActor(
458                bs.newnode(
459                    'region',
460                    attrs={
461                        'position': defs.boxes['goal1'][0:3],
462                        'scale': defs.boxes['goal1'][6:9],
463                        'type': 'box',
464                        'materials': [self._score_region_material],
465                    },
466                )
467            )
468        )
469        self._score_regions.append(
470            bs.NodeActor(
471                bs.newnode(
472                    'region',
473                    attrs={
474                        'position': defs.boxes['goal2'][0:3],
475                        'scale': defs.boxes['goal2'][6:9],
476                        'type': 'box',
477                        'materials': [self._score_region_material],
478                    },
479                )
480            )
481        )
482        self._chant_sound.play()

Called when the Activity is first becoming visible.

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

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

Called once the previous Activity has finished transitioning out.

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

def end_game(self) -> None:
799    def end_game(self) -> None:
800        bs.setmusic(None)
801        self._bots.final_celebrate()
802        bs.timer(0.001, bs.Call(self.do_end, 'defeat'))

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

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

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

update scoreboard and check for winners

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

End the game with the specified outcome.

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

handle high-level game messages

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

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

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