class ms(ba.TeamGameActivity[Player, Team]):
    """Minigame involving dodging falling bombs."""

    name = 'Meteor Showerx'
    description = 'Dodge the falling bombs.'
    available_settings = [ba.BoolSetting('Epic Mode', default=False)]
    scoreconfig = ba.ScoreConfig(label='Survived',
                                 scoretype=ba.ScoreType.MILLISECONDS,
                                 version='B')

    # Print messages when players die (since its meaningful in this game).
    announce_player_deaths = True

    # Don't allow joining after we start
    # (would enable leave/rejoin tomfoolery).
    allow_mid_activity_joins = False

    # We're currently hard-coded for one map.
    @classmethod
    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
        return ['Rampage']

    # We support teams, free-for-all, and co-op sessions.
    @classmethod
    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
        return (issubclass(sessiontype, ba.DualTeamSession)
                or issubclass(sessiontype, ba.FreeForAllSession)
                or issubclass(sessiontype, ba.CoopSession))

    def __init__(self, settings: dict):
        super().__init__(settings)

    def on_begin(self) -> None:
        super().on_begin()
示例#2
0
    def get_available_settings(
            cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]:
        settings = [
            ba.IntSetting(
                'Kills to Win Per Player',
                min_value=1,
                default=5,
                increment=1,
            ),
            ba.IntChoiceSetting(
                'Time Limit',
                choices=[
                    ('None', 0),
                    ('1 Minute', 60),
                    ('2 Minutes', 120),
                    ('5 Minutes', 300),
                    ('10 Minutes', 600),
                    ('20 Minutes', 1200),
                ],
                default=0,
            ),
            ba.FloatChoiceSetting(
                'Respawn Times',
                choices=[
                    ('Shorter', 0.25),
                    ('Short', 0.5),
                    ('Normal', 1.0),
                    ('Long', 2.0),
                    ('Longer', 4.0),
                ],
                default=1.0,
            ),
            ba.BoolSetting('Epic Mode', default=False),
        ]

        # In teams mode, a suicide gives a point to the other team, but in
        # free-for-all it subtracts from your own score. By default we clamp
        # this at zero to benefit new players, but pro players might like to
        # be able to go negative. (to avoid a strategy of just
        # suiciding until you get a good drop)
        if issubclass(sessiontype, ba.FreeForAllSession):
            settings.append(
                ba.BoolSetting('Allow Negative Scores', default=False))

        return settings
示例#3
0
    def get_available_settings(
            cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]:
        settings = [
            ba.IntSetting('Laps', min_value=1, default=3, increment=1),
            ba.IntChoiceSetting(
                'Time Limit',
                default=0,
                choices=[
                    ('None', 0),
                    ('1 Minute', 60),
                    ('2 Minutes', 120),
                    ('5 Minutes', 300),
                    ('10 Minutes', 600),
                    ('20 Minutes', 1200),
                ],
            ),
            ba.IntChoiceSetting(
                'Mine Spawning',
                default=4000,
                choices=[
                    ('No Mines', 0),
                    ('8 Seconds', 8000),
                    ('4 Seconds', 4000),
                    ('2 Seconds', 2000),
                ],
            ),
            ba.IntChoiceSetting(
                'Bomb Spawning',
                choices=[
                    ('None', 0),
                    ('8 Seconds', 8000),
                    ('4 Seconds', 4000),
                    ('2 Seconds', 2000),
                    ('1 Second', 1000),
                ],
                default=2000,
            ),
            ba.BoolSetting('Epic Mode', default=False),
        ]

        # We have some specific settings in teams mode.
        if issubclass(sessiontype, ba.DualTeamSession):
            settings.append(
                ba.BoolSetting('Entire Team Must Finish', default=False))
        return settings
 def get_available_settings(
         cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]:
     settings = [
         ba.IntSetting(
             'Lives Per Player',
             default=1,
             min_value=1,
             max_value=10,
             increment=1,
         ),
         ba.IntChoiceSetting(
             'Time Limit',
             choices=[
                 ('None', 0),
                 ('1 Minute', 60),
                 ('2 Minutes', 120),
                 ('5 Minutes', 300),
                 ('10 Minutes', 600),
                 ('20 Minutes', 1200),
             ],
             default=0,
         ),
         ba.FloatChoiceSetting(
             'Respawn Times',
             choices=[
                 ('Shorter', 0.25),
                 ('Short', 0.5),
                 ('Normal', 1.0),
                 ('Long', 2.0),
                 ('Longer', 4.0),
             ],
             default=1.0,
         ),
         ba.BoolSetting('Epic Mode', default=False),
     ]
     if issubclass(sessiontype, ba.DualTeamSession):
         settings.append(ba.BoolSetting('Solo Mode', default=False))
         settings.append(
             ba.BoolSetting('Balance Total Lives', default=False))
     return settings
 def get_available_settings(
         cls, sessiontype: Type[ba.Session]) -> List[ba.Setting]:
     settings = [
         ba.IntChoiceSetting(
             'Time Limit',
             choices=[
                 ('None', 0),
                 ('1 Minute', 60),
                 ('2 Minutes', 120),
                 ('5 Minutes', 300),
                 ('10 Minutes', 600),
                 ('20 Minutes', 1200),
             ],
             default=0,
         ),
         ba.BoolSetting('Epic Mode', default=False),
     ]
     return settings
class ChosenOneGame(ba.TeamGameActivity[Player, Team]):
    """
    Game involving trying to remain the one 'chosen one'
    for a set length of time while everyone else tries to
    kill you and become the chosen one themselves.
    """

    name = 'Chosen One'
    description = ('Be the chosen one for a length of time to win.\n'
                   'Kill the chosen one to become it.')
    available_settings = [
        ba.IntSetting(
            'Chosen One Time',
            min_value=10,
            default=30,
            increment=10,
        ),
        ba.BoolSetting('Chosen One Gets Gloves', default=True),
        ba.BoolSetting('Chosen One Gets Shield', default=False),
        ba.IntChoiceSetting(
            'Time Limit',
            choices=[
                ('None', 0),
                ('1 Minute', 60),
                ('2 Minutes', 120),
                ('5 Minutes', 300),
                ('10 Minutes', 600),
                ('20 Minutes', 1200),
            ],
            default=0,
        ),
        ba.FloatChoiceSetting(
            'Respawn Times',
            choices=[
                ('Shorter', 0.25),
                ('Short', 0.5),
                ('Normal', 1.0),
                ('Long', 2.0),
                ('Longer', 4.0),
            ],
            default=1.0,
        ),
        ba.BoolSetting('Epic Mode', default=False),
    ]
    scoreconfig = ba.ScoreConfig(label='Time Held')

    @classmethod
    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
        return ba.getmaps('keep_away')

    def __init__(self, settings: dict):
        super().__init__(settings)
        self._scoreboard = Scoreboard()
        self._chosen_one_player: Optional[Player] = None
        self._swipsound = ba.getsound('swip')
        self._countdownsounds: dict[int, ba.Sound] = {
            10: ba.getsound('announceTen'),
            9: ba.getsound('announceNine'),
            8: ba.getsound('announceEight'),
            7: ba.getsound('announceSeven'),
            6: ba.getsound('announceSix'),
            5: ba.getsound('announceFive'),
            4: ba.getsound('announceFour'),
            3: ba.getsound('announceThree'),
            2: ba.getsound('announceTwo'),
            1: ba.getsound('announceOne')
        }
        self._flag_spawn_pos: Optional[Sequence[float]] = None
        self._reset_region_material: Optional[ba.Material] = None
        self._flag: Optional[Flag] = None
        self._reset_region: Optional[ba.Node] = None
        self._epic_mode = bool(settings['Epic Mode'])
        self._chosen_one_time = int(settings['Chosen One Time'])
        self._time_limit = float(settings['Time Limit'])
        self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield'])
        self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves'])

        # Base class overrides
        self.slow_motion = self._epic_mode
        self.default_music = (ba.MusicType.EPIC
                              if self._epic_mode else ba.MusicType.CHOSEN_ONE)

    def get_instance_description(self) -> Union[str, Sequence]:
        return 'There can be only one.'

    def create_team(self, sessionteam: ba.SessionTeam) -> Team:
        return Team(time_remaining=self._chosen_one_time)

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

    def on_player_leave(self, player: Player) -> None:
        super().on_player_leave(player)
        if self._get_chosen_one_player() is player:
            self._set_chosen_one_player(None)

    def on_begin(self) -> None:
        super().on_begin()
        shared = SharedObjects.get()
        self.setup_standard_time_limit(self._time_limit)
        self.setup_standard_powerup_drops()
        self._flag_spawn_pos = self.map.get_flag_position(None)
        Flag.project_stand(self._flag_spawn_pos)
        self._set_chosen_one_player(None)

        pos = self._flag_spawn_pos
        ba.timer(1.0, call=self._tick, repeat=True)

        mat = self._reset_region_material = ba.Material()
        mat.add_actions(
            conditions=(
                'they_have_material',
                shared.player_material,
            ),
            actions=(
                ('modify_part_collision', 'collide', True),
                ('modify_part_collision', 'physical', False),
                ('call', 'at_connect',
                 ba.WeakCall(self._handle_reset_collide)),
            ),
        )

        self._reset_region = ba.newnode('region',
                                        attrs={
                                            'position':
                                            (pos[0], pos[1] + 0.75, pos[2]),
                                            'scale': (0.5, 0.5, 0.5),
                                            'type':
                                            'sphere',
                                            'materials': [mat]
                                        })

    def _get_chosen_one_player(self) -> Optional[Player]:
        # Should never return invalid references; return None in that case.
        if self._chosen_one_player:
            return self._chosen_one_player
        return None

    def _handle_reset_collide(self) -> None:
        # If we have a chosen one, ignore these.
        if self._get_chosen_one_player() is not None:
            return

        # Attempt to get a Player controlling a Spaz that we hit.
        try:
            player = ba.getcollision().opposingnode.getdelegate(
                PlayerSpaz, True).getplayer(Player, True)
        except ba.NotFoundError:
            return

        if player.is_alive():
            self._set_chosen_one_player(player)

    def _flash_flag_spawn(self) -> None:
        light = ba.newnode('light',
                           attrs={
                               'position': self._flag_spawn_pos,
                               'color': (1, 1, 1),
                               'radius': 0.3,
                               'height_attenuated': False
                           })
        ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
        ba.timer(1.0, light.delete)

    def _tick(self) -> None:

        # Give the chosen one points.
        player = self._get_chosen_one_player()
        if player is not None:

            # This shouldn't happen, but just in case.
            if not player.is_alive():
                ba.print_error('got dead player as chosen one in _tick')
                self._set_chosen_one_player(None)
            else:
                scoring_team = player.team
                assert self.stats
                self.stats.player_scored(player,
                                         3,
                                         screenmessage=False,
                                         display=False)

                scoring_team.time_remaining = max(
                    0, scoring_team.time_remaining - 1)

                # Show the count over their head
                if scoring_team.time_remaining > 0:
                    if isinstance(player.actor, PlayerSpaz) and player.actor:
                        player.actor.set_score_text(
                            str(scoring_team.time_remaining))

                self._update_scoreboard()

                # announce numbers we have sounds for
                if scoring_team.time_remaining in self._countdownsounds:
                    ba.playsound(
                        self._countdownsounds[scoring_team.time_remaining])

                # Winner!
                if scoring_team.time_remaining <= 0:
                    self.end_game()

        else:
            # (player is None)
            # This shouldn't happen, but just in case.
            # (Chosen-one player ceasing to exist should
            # trigger on_player_leave which resets chosen-one)
            if self._chosen_one_player is not None:
                ba.print_error('got nonexistent player as chosen one in _tick')
                self._set_chosen_one_player(None)

    def end_game(self) -> None:
        results = ba.GameResults()
        for team in self.teams:
            results.set_team_score(team,
                                   self._chosen_one_time - team.time_remaining)
        self.end(results=results, announce_delay=0)

    def _set_chosen_one_player(self, player: Optional[Player]) -> None:
        existing = self._get_chosen_one_player()
        if existing:
            existing.chosen_light = None
        ba.playsound(self._swipsound)
        if not player:
            assert self._flag_spawn_pos is not None
            self._flag = Flag(color=(1, 0.9, 0.2),
                              position=self._flag_spawn_pos,
                              touchable=False)
            self._chosen_one_player = None

            # Create a light to highlight the flag;
            # this will go away when the flag dies.
            ba.newnode('light',
                       owner=self._flag.node,
                       attrs={
                           'position': self._flag_spawn_pos,
                           'intensity': 0.6,
                           'height_attenuated': False,
                           'volume_intensity_scale': 0.1,
                           'radius': 0.1,
                           'color': (1.2, 1.2, 0.4)
                       })

            # Also an extra momentary flash.
            self._flash_flag_spawn()
        else:
            if player.actor:
                self._flag = None
                self._chosen_one_player = player

                if self._chosen_one_gets_shield:
                    player.actor.handlemessage(ba.PowerupMessage('shield'))
                if self._chosen_one_gets_gloves:
                    player.actor.handlemessage(ba.PowerupMessage('punch'))

                # Use a color that's partway between their team color
                # and white.
                color = [
                    0.3 + c * 0.7
                    for c in ba.normalized_color(player.team.color)
                ]
                light = player.chosen_light = ba.NodeActor(
                    ba.newnode('light',
                               attrs={
                                   'intensity': 0.6,
                                   'height_attenuated': False,
                                   'volume_intensity_scale': 0.1,
                                   'radius': 0.13,
                                   'color': color
                               }))

                assert light.node
                ba.animate(light.node,
                           'intensity', {
                               0: 1.0,
                               0.2: 0.4,
                               0.4: 1.0
                           },
                           loop=True)
                assert isinstance(player.actor, PlayerSpaz)
                player.actor.node.connectattr('position', light.node,
                                              'position')

    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ba.PlayerDiedMessage):
            # Augment standard behavior.
            super().handlemessage(msg)
            player = msg.getplayer(Player)
            if player is self._get_chosen_one_player():
                killerplayer = msg.getkillerplayer(Player)
                self._set_chosen_one_player(None if (
                    killerplayer is None or killerplayer is player
                    or not killerplayer.is_alive()) else killerplayer)
            self.respawn_player(player)
        else:
            super().handlemessage(msg)

    def _update_scoreboard(self) -> None:
        for team in self.teams:
            self._scoreboard.set_team_value(team,
                                            team.time_remaining,
                                            self._chosen_one_time,
                                            countdown=True)
示例#7
0
class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
    """A game where score is based on collecting eggs."""

    name = 'Easter Egg Hunt'
    description = 'Gather eggs!'
    available_settings = [ba.BoolSetting('Pro Mode', default=False)]
    scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS)

    # We're currently hard-coded for one map.
    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        return ['Tower D']

    # We support teams, free-for-all, and co-op sessions.
    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return (issubclass(sessiontype, ba.CoopSession)
                or issubclass(sessiontype, ba.DualTeamSession)
                or issubclass(sessiontype, ba.FreeForAllSession))

    def __init__(self, settings: dict):
        super().__init__(settings)
        shared = SharedObjects.get()
        self._last_player_death_time = None
        self._scoreboard = Scoreboard()
        self.egg_model = ba.getmodel('egg')
        self.egg_tex_1 = ba.gettexture('eggTex1')
        self.egg_tex_2 = ba.gettexture('eggTex2')
        self.egg_tex_3 = ba.gettexture('eggTex3')
        self._collect_sound = ba.getsound('powerup01')
        self._pro_mode = settings.get('Pro Mode', False)
        self._max_eggs = 1.0
        self.egg_material = ba.Material()
        self.egg_material.add_actions(
            conditions=('they_have_material', shared.player_material),
            actions=(('call', 'at_connect', self._on_egg_player_collide), ))
        self._eggs: List[Egg] = []
        self._update_timer: Optional[ba.Timer] = None
        self._countdown: Optional[OnScreenCountdown] = None
        self._bots: Optional[SpazBotSet] = None

        # Base class overrides
        self.default_music = ba.MusicType.FORWARD_MARCH

    def on_team_join(self, team: Team) -> None:
        if self.has_begun():
            self._update_scoreboard()

    # Called when our game actually starts.
    def on_begin(self) -> None:
        from bastd.maps import TowerD

        # There's a player-wall on the tower-d level to prevent
        # players from getting up on the stairs.. we wanna kill that.
        gamemap = self.map
        assert isinstance(gamemap, TowerD)
        gamemap.player_wall.delete()
        super().on_begin()
        self._update_scoreboard()
        self._update_timer = ba.Timer(0.25, self._update, repeat=True)
        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
        ba.timer(4.0, self._countdown.start)
        self._bots = SpazBotSet()

        # Spawn evil bunny in co-op only.
        if isinstance(self.session, ba.CoopSession) and self._pro_mode:
            self._spawn_evil_bunny()

    # Overriding the default character spawning.
    def spawn_player(self, player: Player) -> ba.Actor:
        spaz = self.spawn_player_spaz(player)
        spaz.connect_controls_to_player()
        return spaz

    def _spawn_evil_bunny(self) -> None:
        assert self._bots is not None
        self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)

    def _on_egg_player_collide(self) -> None:
        if self.has_ended():
            return
        collision = ba.getcollision()

        # Be defensive here; we could be hitting the corpse of a player
        # who just left/etc.
        try:
            egg = collision.sourcenode.getdelegate(Egg, True)
            player = collision.opposingnode.getdelegate(PlayerSpaz,
                                                        True).getplayer(
                                                            Player, True)
        except ba.NotFoundError:
            return

        player.team.score += 1

        # Displays a +1 (and adds to individual player score in
        # teams mode).
        self.stats.player_scored(player, 1, screenmessage=False)
        if self._max_eggs < 5:
            self._max_eggs += 1.0
        elif self._max_eggs < 10:
            self._max_eggs += 0.5
        elif self._max_eggs < 30:
            self._max_eggs += 0.3
        self._update_scoreboard()
        ba.playsound(self._collect_sound, 0.5, position=egg.node.position)

        # Create a flash.
        light = ba.newnode('light',
                           attrs={
                               'position': egg.node.position,
                               'height_attenuated': False,
                               'radius': 0.1,
                               'color': (1, 1, 0)
                           })
        ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False)
        ba.timer(0.200, light.delete)
        egg.handlemessage(ba.DieMessage())

    def _update(self) -> None:
        # Misc. periodic updating.
        xpos = random.uniform(-7.1, 6.0)
        ypos = random.uniform(3.5, 3.5)
        zpos = random.uniform(-8.2, 3.7)

        # Prune dead eggs from our list.
        self._eggs = [e for e in self._eggs if e]

        # Spawn more eggs if we've got space.
        if len(self._eggs) < int(self._max_eggs):

            # Occasionally spawn a land-mine in addition.
            if self._pro_mode and random.random() < 0.25:
                mine = Bomb(position=(xpos, ypos, zpos),
                            bomb_type='land_mine').autoretain()
                mine.arm()
            else:
                self._eggs.append(Egg(position=(xpos, ypos, zpos)))

    # Various high-level game events come through this method.
    def handlemessage(self, msg: Any) -> Any:

        # Respawn dead players.
        if isinstance(msg, ba.PlayerDiedMessage):
            # Augment standard behavior.
            super().handlemessage(msg)

            # Respawn them shortly.
            player = msg.getplayer(Player)
            assert self.initialplayerinfos is not None
            respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
            player.respawn_timer = ba.Timer(
                respawn_time, ba.Call(self.spawn_player_if_exists, player))
            player.respawn_icon = RespawnIcon(player, respawn_time)

        # Whenever our evil bunny dies, respawn him and spew some eggs.
        elif isinstance(msg, SpazBotDiedMessage):
            self._spawn_evil_bunny()
            assert msg.spazbot.node
            pos = msg.spazbot.node.position
            for _i in range(6):
                spread = 0.4
                self._eggs.append(
                    Egg(position=(pos[0] + random.uniform(-spread, spread),
                                  pos[1] + random.uniform(-spread, spread),
                                  pos[2] + random.uniform(-spread, spread))))
        else:
            # Default handler.
            return super().handlemessage(msg)
        return None

    def _update_scoreboard(self) -> None:
        for team in self.teams:
            self._scoreboard.set_team_value(team, team.score)

    def end_game(self) -> None:
        results = ba.GameResults()
        for team in self.teams:
            results.set_team_score(team, team.score)
        self.end(results)
示例#8
0
class ConquestGame(ba.TeamGameActivity[Player, Team]):
    """A game where teams try to claim all flags on the map."""

    name = 'Conquest'
    description = 'Secure all flags on the map to win.'
    available_settings = [
        ba.IntChoiceSetting(
            'Time Limit',
            choices=[
                ('None', 0),
                ('1 Minute', 60),
                ('2 Minutes', 120),
                ('5 Minutes', 300),
                ('10 Minutes', 600),
                ('20 Minutes', 1200),
            ],
            default=0,
        ),
        ba.FloatChoiceSetting(
            'Respawn Times',
            choices=[
                ('Shorter', 0.25),
                ('Short', 0.5),
                ('Normal', 1.0),
                ('Long', 2.0),
                ('Longer', 4.0),
            ],
            default=1.0,
        ),
        ba.BoolSetting('Epic Mode', default=False),
    ]

    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return issubclass(sessiontype, ba.DualTeamSession)

    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        return ba.getmaps('conquest')

    def __init__(self, settings: dict):
        super().__init__(settings)
        shared = SharedObjects.get()
        self._scoreboard = Scoreboard()
        self._score_sound = ba.getsound('score')
        self._swipsound = ba.getsound('swip')
        self._extraflagmat = ba.Material()
        self._flags: List[ConquestFlag] = []
        self._epic_mode = bool(settings['Epic Mode'])
        self._time_limit = float(settings['Time Limit'])

        # Base class overrides.
        self.slow_motion = self._epic_mode
        self.default_music = (ba.MusicType.EPIC
                              if self._epic_mode else ba.MusicType.GRAND_ROMP)

        # We want flags to tell us they've been hit but not react physically.
        self._extraflagmat.add_actions(
            conditions=('they_have_material', shared.player_material),
            actions=(
                ('modify_part_collision', 'collide', True),
                ('call', 'at_connect', self._handle_flag_player_collide),
            ))

    def get_instance_description(self) -> Union[str, Sequence]:
        return 'Secure all ${ARG1} flags.', len(self.map.flag_points)

    def get_instance_description_short(self) -> Union[str, Sequence]:
        return 'secure all ${ARG1} flags', len(self.map.flag_points)

    def on_team_join(self, team: Team) -> None:
        if self.has_begun():
            self._update_scores()

    def on_player_join(self, player: Player) -> None:
        player.respawn_timer = None

        # Only spawn if this player's team has a flag currently.
        if player.team.flags_held > 0:
            self.spawn_player(player)

    def on_begin(self) -> None:
        super().on_begin()
        self.setup_standard_time_limit(self._time_limit)
        self.setup_standard_powerup_drops()

        # Set up flags with marker lights.
        for i in range(len(self.map.flag_points)):
            point = self.map.flag_points[i]
            flag = ConquestFlag(position=point,
                                touchable=False,
                                materials=[self._extraflagmat])
            self._flags.append(flag)
            Flag.project_stand(point)
            flag.light = ba.newnode('light',
                                    owner=flag.node,
                                    attrs={
                                        'position': point,
                                        'intensity': 0.25,
                                        'height_attenuated': False,
                                        'radius': 0.3,
                                        'color': (1, 1, 1)
                                    })

        # Give teams a flag to start with.
        for i in range(len(self.teams)):
            self._flags[i].team = self.teams[i]
            light = self._flags[i].light
            assert light
            node = self._flags[i].node
            assert node
            light.color = self.teams[i].color
            node.color = self.teams[i].color

        self._update_scores()

        # Initial joiners didn't spawn due to no flags being owned yet;
        # spawn them now.
        for player in self.players:
            self.spawn_player(player)

    def _update_scores(self) -> None:
        for team in self.teams:
            team.flags_held = 0
        for flag in self._flags:
            if flag.team is not None:
                flag.team.flags_held += 1
        for team in self.teams:

            # If a team finds themselves with no flags, cancel all
            # outstanding spawn-timers.
            if team.flags_held == 0:
                for player in team.players:
                    player.respawn_timer = None
                    player.respawn_icon = None
            if team.flags_held == len(self._flags):
                self.end_game()
            self._scoreboard.set_team_value(team, team.flags_held,
                                            len(self._flags))

    def end_game(self) -> None:
        results = ba.GameResults()
        for team in self.teams:
            results.set_team_score(team, team.flags_held)
        self.end(results=results)

    def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
        assert flag.node
        assert flag.light
        light = ba.newnode('light',
                           attrs={
                               'position': flag.node.position,
                               'height_attenuated': False,
                               'color': flag.light.color
                           })
        ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
        ba.timer(length, light.delete)

    def _handle_flag_player_collide(self) -> None:
        collision = ba.getcollision()
        try:
            flag = collision.sourcenode.getdelegate(ConquestFlag, True)
            player = collision.opposingnode.getdelegate(PlayerSpaz,
                                                        True).getplayer(
                                                            Player, True)
        except ba.NotFoundError:
            return
        assert flag.light

        if flag.team is not player.team:
            flag.team = player.team
            flag.light.color = player.team.color
            flag.node.color = player.team.color
            self.stats.player_scored(player, 10, screenmessage=False)
            ba.playsound(self._swipsound)
            self._flash_flag(flag)
            self._update_scores()

            # Respawn any players on this team that were in limbo due to the
            # lack of a flag for their team.
            for otherplayer in self.players:
                if (otherplayer.team is flag.team
                        and otherplayer.actor is not None
                        and not otherplayer.is_alive()
                        and otherplayer.respawn_timer is None):
                    self.spawn_player(otherplayer)

    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ba.PlayerDiedMessage):
            # Augment standard behavior.
            super().handlemessage(msg)

            # Respawn only if this team has a flag.
            player = msg.getplayer(Player)
            if player.team.flags_held > 0:
                self.respawn_player(player)
            else:
                player.respawn_timer = None

        else:
            super().handlemessage(msg)

    def spawn_player(self, player: Player) -> ba.Actor:
        # We spawn players at different places based on what flags are held.
        return self.spawn_player_spaz(player,
                                      self._get_player_spawn_position(player))

    def _get_player_spawn_position(self, player: Player) -> Sequence[float]:

        # Iterate until we find a spawn owned by this team.
        spawn_count = len(self.map.spawn_by_flag_points)

        # Get all spawns owned by this team.
        spawns = [
            i for i in range(spawn_count) if self._flags[i].team is player.team
        ]

        closest_spawn = 0
        closest_distance = 9999.0

        # Now find the spawn that's closest to a spawn not owned by us;
        # we'll use that one.
        for spawn in spawns:
            spt = self.map.spawn_by_flag_points[spawn]
            our_pt = ba.Vec3(spt[0], spt[1], spt[2])
            for otherspawn in [
                    i for i in range(spawn_count)
                    if self._flags[i].team is not player.team
            ]:
                spt = self.map.spawn_by_flag_points[otherspawn]
                their_pt = ba.Vec3(spt[0], spt[1], spt[2])
                dist = (their_pt - our_pt).length()
                if dist < closest_distance:
                    closest_distance = dist
                    closest_spawn = spawn

        pos = self.map.spawn_by_flag_points[closest_spawn]
        x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
        z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
        pos = (pos[0] + random.uniform(*x_range), pos[1],
               pos[2] + random.uniform(*z_range))
        return pos
示例#9
0
class AssaultGame(ba.TeamGameActivity[Player, Team]):
    """Game where you score by touching the other team's flag."""

    name = 'Assault'
    description = 'Reach the enemy flag to score.'
    available_settings = [
        ba.IntSetting(
            'Score to Win',
            min_value=1,
            default=3,
        ),
        ba.IntChoiceSetting(
            'Time Limit',
            choices=[
                ('None', 0),
                ('1 Minute', 60),
                ('2 Minutes', 120),
                ('5 Minutes', 300),
                ('10 Minutes', 600),
                ('20 Minutes', 1200),
            ],
            default=0,
        ),
        ba.FloatChoiceSetting(
            'Respawn Times',
            choices=[
                ('Shorter', 0.25),
                ('Short', 0.5),
                ('Normal', 1.0),
                ('Long', 2.0),
                ('Longer', 4.0),
            ],
            default=1.0,
        ),
        ba.BoolSetting('Epic Mode', default=False),
    ]

    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return issubclass(sessiontype, ba.DualTeamSession)

    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        return ba.getmaps('team_flag')

    def __init__(self, settings: dict):
        super().__init__(settings)
        self._scoreboard = Scoreboard()
        self._last_score_time = 0.0
        self._score_sound = ba.getsound('score')
        self._base_region_materials: Dict[int, ba.Material] = {}
        self._epic_mode = bool(settings['Epic Mode'])
        self._score_to_win = int(settings['Score to Win'])
        self._time_limit = float(settings['Time Limit'])

        # Base class overrides
        self.slow_motion = self._epic_mode
        self.default_music = (ba.MusicType.EPIC if self._epic_mode else
                              ba.MusicType.FORWARD_MARCH)

    def get_instance_description(self) -> Union[str, Sequence]:
        if self._score_to_win == 1:
            return 'Touch the enemy flag.'
        return 'Touch the enemy flag ${ARG1} times.', self._score_to_win

    def get_instance_description_short(self) -> Union[str, Sequence]:
        if self._score_to_win == 1:
            return 'touch 1 flag'
        return 'touch ${ARG1} flags', self._score_to_win

    def create_team(self, sessionteam: ba.SessionTeam) -> Team:
        shared = SharedObjects.get()
        base_pos = self.map.get_flag_position(sessionteam.id)
        ba.newnode('light',
                   attrs={
                       'position': base_pos,
                       'intensity': 0.6,
                       'height_attenuated': False,
                       'volume_intensity_scale': 0.1,
                       'radius': 0.1,
                       'color': sessionteam.color
                   })
        Flag.project_stand(base_pos)
        flag = Flag(touchable=False,
                    position=base_pos,
                    color=sessionteam.color)
        team = Team(base_pos=base_pos, flag=flag)

        mat = self._base_region_materials[sessionteam.id] = ba.Material()
        mat.add_actions(
            conditions=('they_have_material', shared.player_material),
            actions=(
                ('modify_part_collision', 'collide', True),
                ('modify_part_collision', 'physical', False),
                ('call', 'at_connect', ba.Call(self._handle_base_collide,
                                               team)),
            ),
        )

        ba.newnode('region',
                   owner=flag.node,
                   attrs={
                       'position':
                       (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
                       'scale': (0.5, 0.5, 0.5),
                       'type': 'sphere',
                       'materials':
                       [self._base_region_materials[sessionteam.id]]
                   })

        return team

    def on_team_join(self, team: Team) -> None:
        # Can't do this in create_team because the team's color/etc. have
        # not been wired up yet at that point.
        self._update_scoreboard()

    def on_begin(self) -> None:
        super().on_begin()
        self.setup_standard_time_limit(self._time_limit)
        self.setup_standard_powerup_drops()

    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ba.PlayerDiedMessage):
            super().handlemessage(msg)  # Augment standard.
            self.respawn_player(msg.getplayer(Player))
        else:
            super().handlemessage(msg)

    def _flash_base(self, team: Team, length: float = 2.0) -> None:
        light = ba.newnode('light',
                           attrs={
                               'position': team.base_pos,
                               'height_attenuated': False,
                               'radius': 0.3,
                               'color': team.color
                           })
        ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
        ba.timer(length, light.delete)

    def _handle_base_collide(self, team: Team) -> None:
        try:
            player = ba.getcollision().opposingnode.getdelegate(
                PlayerSpaz, True).getplayer(Player, True)
        except ba.NotFoundError:
            return

        if not player.is_alive():
            return

        # If its another team's player, they scored.
        player_team = player.team
        if player_team is not team:

            # Prevent multiple simultaneous scores.
            if ba.time() != self._last_score_time:
                self._last_score_time = ba.time()
                self.stats.player_scored(player, 50, big_message=True)
                ba.playsound(self._score_sound)
                self._flash_base(team)

                # Move all players on the scoring team back to their start
                # and add flashes of light so its noticeable.
                for player in player_team.players:
                    if player.is_alive():
                        pos = player.node.position
                        light = ba.newnode('light',
                                           attrs={
                                               'position': pos,
                                               'color': player_team.color,
                                               'height_attenuated': False,
                                               'radius': 0.4
                                           })
                        ba.timer(0.5, light.delete)
                        ba.animate(light, 'intensity', {
                            0: 0,
                            0.1: 1.0,
                            0.5: 0
                        })

                        new_pos = (self.map.get_start_position(player_team.id))
                        light = ba.newnode('light',
                                           attrs={
                                               'position': new_pos,
                                               'color': player_team.color,
                                               'radius': 0.4,
                                               'height_attenuated': False
                                           })
                        ba.timer(0.5, light.delete)
                        ba.animate(light, 'intensity', {
                            0: 0,
                            0.1: 1.0,
                            0.5: 0
                        })
                        if player.actor:
                            player.actor.handlemessage(
                                ba.StandMessage(new_pos,
                                                random.uniform(0, 360)))

                # Have teammates celebrate.
                for player in player_team.players:
                    if player.actor:
                        player.actor.handlemessage(ba.CelebrateMessage(2.0))

                player_team.score += 1
                self._update_scoreboard()
                if player_team.score >= self._score_to_win:
                    self.end_game()

    def end_game(self) -> None:
        results = ba.GameResults()
        for team in self.teams:
            results.set_team_score(team, team.score)
        self.end(results=results)

    def _update_scoreboard(self) -> None:
        for team in self.teams:
            self._scoreboard.set_team_value(team, team.score,
                                            self._score_to_win)
class StickyStormCTFGame(ba.TeamGameActivity[Player, Team]):
    """Game of stealing other team's flag and returning it to your base."""

    name = 'Sticky Storm CTF'
    description = 'Return the enemy flag to score in a sticky rain'
    available_settings = [
        ba.IntSetting('Score to Win', min_value=1, default=3),
        ba.IntSetting(
            'Flag Touch Return Time',
            min_value=0,
            default=0,
            increment=1,
        ),
        ba.IntSetting(
            'Flag Idle Return Time',
            min_value=5,
            default=30,
            increment=5,
        ),
        ba.IntChoiceSetting(
            'Time Limit',
            choices=[
                ('None', 0),
                ('1 Minute', 60),
                ('2 Minutes', 120),
                ('5 Minutes', 300),
                ('10 Minutes', 600),
                ('20 Minutes', 1200),
            ],
            default=0,
        ),
        ba.FloatChoiceSetting(
            'Respawn Times',
            choices=[
                ('Shorter', 0.25),
                ('Short', 0.5),
                ('Normal', 1.0),
                ('Long', 2.0),
                ('Longer', 4.0),
            ],
            default=1.0,
        ),
        ba.BoolSetting('Epic Mode', default=False),
    ]

    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return issubclass(sessiontype, ba.DualTeamSession)

    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        return ba.getmaps('team_flag')

    def __init__(self, settings: dict):
        super().__init__(settings)
        self._scoreboard = Scoreboard()
        self._alarmsound = ba.getsound('alarm')
        self._ticking_sound = ba.getsound('ticking')
        self._score_sound = ba.getsound('score')
        self._swipsound = ba.getsound('swip')
        self._last_score_time = 0
        self._all_bases_material = ba.Material()
        self._last_home_flag_notice_print_time = 0.0
        self._score_to_win = int(settings['Score to Win'])
        self._epic_mode = bool(settings['Epic Mode'])
        self._time_limit = float(settings['Time Limit'])

        self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
        self.flag_idle_return_time = float(settings['Flag Idle Return Time'])
        self._meteor_time = 2.0

        # Base class overrides.
        self.slow_motion = self._epic_mode
        self.default_music = (ba.MusicType.EPIC if self._epic_mode else
                              ba.MusicType.FLAG_CATCHER)

    def get_instance_description(self) -> Union[str, Sequence]:
        if self._score_to_win == 1:
            return 'Steal the enemy flag.'
        return 'Steal the enemy flag ${ARG1} times.', self._score_to_win

    def get_instance_description_short(self) -> Union[str, Sequence]:
        if self._score_to_win == 1:
            return 'return 1 flag'
        return 'return ${ARG1} flags', self._score_to_win

    def create_team(self, sessionteam: ba.SessionTeam) -> Team:

        # Create our team instance and its initial values.

        base_pos = self.map.get_flag_position(sessionteam.id)
        Flag.project_stand(base_pos)

        ba.newnode('light',
                   attrs={
                       'position': base_pos,
                       'intensity': 0.6,
                       'height_attenuated': False,
                       'volume_intensity_scale': 0.1,
                       'radius': 0.1,
                       'color': sessionteam.color
                   })

        base_region_mat = ba.Material()
        pos = base_pos
        base_region = ba.newnode(
            'region',
            attrs={
                'position': (pos[0], pos[1] + 0.75, pos[2]),
                'scale': (0.5, 0.5, 0.5),
                'type': 'sphere',
                'materials': [base_region_mat, self._all_bases_material]
            })

        spaz_mat_no_flag_physical = ba.Material()
        spaz_mat_no_flag_collide = ba.Material()
        flagmat = ba.Material()

        team = Team(base_pos=base_pos,
                    base_region_material=base_region_mat,
                    base_region=base_region,
                    spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
                    spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
                    flagmaterial=flagmat)

        # Some parts of our spazzes don't collide physically with our
        # flags but generate callbacks.
        spaz_mat_no_flag_physical.add_actions(
            conditions=('they_have_material', flagmat),
            actions=(
                ('modify_part_collision', 'physical', False),
                ('call', 'at_connect',
                 lambda: self._handle_touching_own_flag(team, True)),
                ('call', 'at_disconnect',
                 lambda: self._handle_touching_own_flag(team, False)),
            ))

        # Other parts of our spazzes don't collide with our flags at all.
        spaz_mat_no_flag_collide.add_actions(
            conditions=('they_have_material', flagmat),
            actions=('modify_part_collision', 'collide', False),
        )

        # We wanna know when *any* flag enters/leaves our base.
        base_region_mat.add_actions(
            conditions=('they_have_material', FlagFactory.get().flagmaterial),
            actions=(
                ('modify_part_collision', 'collide', True),
                ('modify_part_collision', 'physical', False),
                ('call', 'at_connect',
                 lambda: self._handle_flag_entered_base(team)),
                ('call', 'at_disconnect',
                 lambda: self._handle_flag_left_base(team)),
            ))

        return team

    def on_team_join(self, team: Team) -> None:
        # Can't do this in create_team because the team's color/etc. have
        # not been wired up yet at that point.
        self._spawn_flag_for_team(team)
        self._update_scoreboard()

    def on_begin(self) -> None:
        super().on_begin()

        # Drop a wave every few seconds.. and every so often drop the time
        # between waves ..lets have things increase faster if we have fewer
        # players.
        delay = 5.0 if len(self.players) > 2 else 2.5
        if self._epic_mode:
            delay *= 0.25
        ba.timer(delay, self._decrement_meteor_time, repeat=True)

        # Kick off the first wave in a few seconds.
        delay = 3.0
        if self._epic_mode:
            delay *= 0.25
        ba.timer(delay, self._set_meteor_timer)

        self.setup_standard_time_limit(self._time_limit)
        self.setup_standard_powerup_drops()
        ba.timer(1.0, call=self._tick, repeat=True)

    def _spawn_flag_for_team(self, team: Team) -> None:
        team.flag = StickyStormCTFFlag(team)
        team.flag_return_touches = 0
        self._flash_base(team, length=1.0)
        assert team.flag.node
        ba.playsound(self._swipsound, position=team.flag.node.position)

    def _handle_flag_entered_base(self, team: Team) -> None:
        try:
            flag = ba.getcollision().opposingnode.getdelegate(
                StickyStormCTFFlag, True)
        except ba.NotFoundError:
            # Don't think this should logically ever happen.
            print(
                'Error getting StickyStormCTFFlag in entering-base callback.')
            return

        if flag.team is team:
            team.home_flag_at_base = True

            # If the enemy flag is already here, score!
            if team.enemy_flag_at_base:
                self._score(team)
        else:
            team.enemy_flag_at_base = True
            if team.home_flag_at_base:
                # Award points to whoever was carrying the enemy flag.
                player = flag.last_player_to_hold
                if player and player.team is team:
                    assert self.stats
                    self.stats.player_scored(player, 50, big_message=True)

                # Update score and reset flags.
                self._score(team)

            # If the home-team flag isn't here, print a message to that effect.
            else:
                # Don't want slo-mo affecting this
                curtime = ba.time(ba.TimeType.BASE)
                if curtime - self._last_home_flag_notice_print_time > 5.0:
                    self._last_home_flag_notice_print_time = curtime
                    bpos = team.base_pos
                    tval = ba.Lstr(resource='ownFlagAtYourBaseWarning')
                    tnode = ba.newnode('text',
                                       attrs={
                                           'text':
                                           tval,
                                           'in_world':
                                           True,
                                           'scale':
                                           0.013,
                                           'color': (1, 1, 0, 1),
                                           'h_align':
                                           'center',
                                           'position':
                                           (bpos[0], bpos[1] + 3.2, bpos[2])
                                       })
                    ba.timer(5.1, tnode.delete)
                    ba.animate(tnode, 'scale', {
                        0.0: 0,
                        0.2: 0.013,
                        4.8: 0.013,
                        5.0: 0
                    })

    def _tick(self) -> None:
        # If either flag is away from base and not being held, tick down its
        # respawn timer.
        for team in self.teams:
            flag = team.flag
            assert flag is not None

            if not team.home_flag_at_base and flag.held_count == 0:
                time_out_counting_down = True
                if flag.time_out_respawn_time is None:
                    flag.reset_return_times()
                assert flag.time_out_respawn_time is not None
                flag.time_out_respawn_time -= 1
                if flag.time_out_respawn_time <= 0:
                    flag.handlemessage(ba.DieMessage())
            else:
                time_out_counting_down = False

            if flag.node and flag.counter:
                pos = flag.node.position
                flag.counter.position = (pos[0], pos[1] + 1.3, pos[2])

                # If there's no self-touches on this flag, set its text
                # to show its auto-return counter.  (if there's self-touches
                # its showing that time).
                if team.flag_return_touches == 0:
                    flag.counter.text = (str(flag.time_out_respawn_time) if (
                        time_out_counting_down
                        and flag.time_out_respawn_time is not None
                        and flag.time_out_respawn_time <= 10) else '')
                    flag.counter.color = (1, 1, 1, 0.5)
                    flag.counter.scale = 0.014

    def _score(self, team: Team) -> None:
        team.score += 1
        ba.playsound(self._score_sound)
        self._flash_base(team)
        self._update_scoreboard()

        # Have teammates celebrate.
        for player in team.players:
            if player.actor:
                player.actor.handlemessage(ba.CelebrateMessage(2.0))

        # Reset all flags/state.
        for reset_team in self.teams:
            if not reset_team.home_flag_at_base:
                assert reset_team.flag is not None
                reset_team.flag.handlemessage(ba.DieMessage())
            reset_team.enemy_flag_at_base = False
        if team.score >= self._score_to_win:
            self.end_game()

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

    def _handle_flag_left_base(self, team: Team) -> None:
        cur_time = ba.time()
        try:
            flag = ba.getcollision().opposingnode.getdelegate(
                StickyStormCTFFlag, True)
        except ba.NotFoundError:
            # This can happen if the flag stops touching us due to being
            # deleted; that's ok.
            return

        if flag.team is team:

            # Check times here to prevent too much flashing.
            if (team.last_flag_leave_time is None
                    or cur_time - team.last_flag_leave_time > 3.0):
                ba.playsound(self._alarmsound, position=team.base_pos)
                self._flash_base(team)
            team.last_flag_leave_time = cur_time
            team.home_flag_at_base = False
        else:
            team.enemy_flag_at_base = False

    def _touch_return_update(self, team: Team) -> None:
        # Count down only while its away from base and not being held.
        assert team.flag is not None
        if team.home_flag_at_base or team.flag.held_count > 0:
            team.touch_return_timer_ticking = None
            return  # No need to return when its at home.
        if team.touch_return_timer_ticking is None:
            team.touch_return_timer_ticking = ba.NodeActor(
                ba.newnode('sound',
                           attrs={
                               'sound': self._ticking_sound,
                               'positional': False,
                               'loop': True
                           }))
        flag = team.flag
        if flag.touch_return_time is not None:
            flag.touch_return_time -= 0.1
            if flag.counter:
                flag.counter.text = f'{flag.touch_return_time:.1f}'
                flag.counter.color = (1, 1, 0, 1)
                flag.counter.scale = 0.02

            if flag.touch_return_time <= 0.0:
                self._award_players_touching_own_flag(team)
                flag.handlemessage(ba.DieMessage())

    def _award_players_touching_own_flag(self, team: Team) -> None:
        for player in team.players:
            if player.touching_own_flag > 0:
                return_score = 10 + 5 * int(self.flag_touch_return_time)
                self.stats.player_scored(player,
                                         return_score,
                                         screenmessage=False)

    def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
        """Called when a player touches or stops touching their own team flag.

        We keep track of when each player is touching their own flag so we
        can award points when returned.
        """
        player: Optional[Player]
        try:
            player = ba.getcollision().sourcenode.getdelegate(
                PlayerSpaz, True).getplayer(Player, True)
        except ba.NotFoundError:
            # This can happen if the player leaves but his corpse touches/etc.
            player = None

        if player:
            player.touching_own_flag += (1 if connecting else -1)

        # If return-time is zero, just kill it immediately.. otherwise keep
        # track of touches and count down.
        if float(self.flag_touch_return_time) <= 0.0:
            assert team.flag is not None
            if (connecting and not team.home_flag_at_base
                    and team.flag.held_count == 0):
                self._award_players_touching_own_flag(team)
                ba.getcollision().opposingnode.handlemessage(ba.DieMessage())

        # Takes a non-zero amount of time to return.
        else:
            if connecting:
                team.flag_return_touches += 1
                if team.flag_return_touches == 1:
                    team.touch_return_timer = ba.Timer(
                        0.1,
                        call=ba.Call(self._touch_return_update, team),
                        repeat=True)
                    team.touch_return_timer_ticking = None
            else:
                team.flag_return_touches -= 1
                if team.flag_return_touches == 0:
                    team.touch_return_timer = None
                    team.touch_return_timer_ticking = None
            if team.flag_return_touches < 0:
                ba.print_error('CTF flag_return_touches < 0')

    def _flash_base(self, team: Team, length: float = 2.0) -> None:
        light = ba.newnode('light',
                           attrs={
                               'position': team.base_pos,
                               'height_attenuated': False,
                               'radius': 0.3,
                               'color': team.color
                           })
        ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
        ba.timer(length, light.delete)

    def spawn_player_spaz(self,
                          player: Player,
                          position: Sequence[float] = None,
                          angle: float = None) -> PlayerSpaz:
        """Intercept new spazzes and add our team material for them."""
        spaz = super().spawn_player_spaz(player, position, angle)
        player = spaz.getplayer(Player, True)
        team: Team = player.team
        player.touching_own_flag = 0
        no_physical_mats: List[ba.Material] = [
            team.spaz_material_no_flag_physical
        ]
        no_collide_mats: List[ba.Material] = [
            team.spaz_material_no_flag_collide
        ]

        # Our normal parts should still collide; just not physically
        # (so we can calc restores).
        assert spaz.node
        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
        spaz.node.roller_materials = list(
            spaz.node.roller_materials) + no_physical_mats

        # Pickups and punches shouldn't hit at all though.
        spaz.node.punch_materials = list(
            spaz.node.punch_materials) + no_collide_mats
        spaz.node.pickup_materials = list(
            spaz.node.pickup_materials) + no_collide_mats
        spaz.node.extras_material = list(
            spaz.node.extras_material) + no_collide_mats
        return spaz

    def _update_scoreboard(self) -> None:
        for team in self.teams:
            self._scoreboard.set_team_value(team, team.score,
                                            self._score_to_win)

    def _set_meteor_timer(self) -> None:
        ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time,
                 self._drop_bomb_cluster)

    def _drop_bomb_cluster(self) -> None:

        # Random note: code like this is a handy way to plot out extents
        # and debug things.
        loc_test = False
        if loc_test:
            ba.newnode('locator', attrs={'position': (8, 6, -5.5)})
            ba.newnode('locator', attrs={'position': (8, 6, -2.3)})
            ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)})
            ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)})

        # Drop several bombs in series.
        delay = 0.0
        for _i in range(random.randrange(1, 3)):
            # Drop them somewhere within our bounds with velocity pointing
            # toward the opposite side.
            pos = (-7.3 + 15.3 * random.random(), 11,
                   -5.5 + 2.1 * random.random())
            dropdir = (-1.0 if pos[0] > 0 else 1.0)
            vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0)
            ba.timer(delay, ba.Call(self._drop_bomb, pos, vel))
            delay += 0.1
        self._set_meteor_timer()

    def _drop_bomb(self, position: Sequence[float],
                   velocity: Sequence[float]) -> None:
        Bomb(position=position, velocity=velocity,
             bomb_type='sticky').autoretain()

    def _decrement_meteor_time(self) -> None:
        self._meteor_time = max(0.01, self._meteor_time * 0.9)

    def handlemessage(self, msg: Any) -> Any:

        if isinstance(msg, ba.PlayerDiedMessage):
            super().handlemessage(msg)  # Augment standard behavior.
            self.respawn_player(msg.getplayer(Player))

        elif isinstance(msg, FlagDiedMessage):
            assert isinstance(msg.flag, StickyStormCTFFlag)
            ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))

        elif isinstance(msg, FlagPickedUpMessage):

            # Store the last player to hold the flag for scoring purposes.
            assert isinstance(msg.flag, StickyStormCTFFlag)
            try:
                msg.flag.last_player_to_hold = msg.node.getdelegate(
                    PlayerSpaz, True).getplayer(Player, True)
            except ba.NotFoundError:
                pass

            msg.flag.held_count += 1
            msg.flag.reset_return_times()

        elif isinstance(msg, FlagDroppedMessage):
            # Store the last player to hold the flag for scoring purposes.
            assert isinstance(msg.flag, StickyStormCTFFlag)
            msg.flag.held_count -= 1

        else:
            super().handlemessage(msg)


# Copyright (c) Lifetime Benefit-Zebra
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this mod without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the mod.
#
# THE MOD IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE MOD OR THE USE OR OTHER DEALINGS IN THE
# MOD.
# --------------------------------------

#By Benefit-Zebra
#https://github.com/Benefit-Zebra
示例#11
0
class StickyStormGame(ba.TeamGameActivity[Player, Team]):
    """Minigame involving dodging falling ice bombs."""

    name = 'Sticky Storm'
    description = 'Dodge the falling sticky bombs.'
    available_settings = [ba.BoolSetting('Epic Mode', default=False)]
    scoreconfig = ba.ScoreConfig(label='Survived',
                                 scoretype=ba.ScoreType.MILLISECONDS,
                                 version='B')

    # Print messages when players die (since its meaningful in this game).
    announce_player_deaths = True

    # we're currently hard-coded for one map..
    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        return ['Rampage']

    # We support teams, free-for-all, and co-op sessions.
    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return (issubclass(sessiontype, ba.DualTeamSession)
                or issubclass(sessiontype, ba.FreeForAllSession)
                or issubclass(sessiontype, ba.CoopSession))

    def __init__(self, settings: dict):
        super().__init__(settings)

        self._epic_mode = settings.get('Epic Mode', False)
        self._last_player_death_time: Optional[float] = None
        self._meteor_time = 2.0
        self._timer: Optional[OnScreenTimer] = None

        # Some base class overrides:
        self.default_music = (ba.MusicType.EPIC
                              if self._epic_mode else ba.MusicType.SURVIVAL)
        if self._epic_mode:
            self.slow_motion = True

    def on_begin(self) -> None:
        super().on_begin()

        # Drop a wave every few seconds.. and every so often drop the time
        # between waves ..lets have things increase faster if we have fewer
        # players.
        delay = 5.0 if len(self.players) > 2 else 2.5
        if self._epic_mode:
            delay *= 0.25
        ba.timer(delay, self._decrement_meteor_time, repeat=True)

        # Kick off the first wave in a few seconds.
        delay = 3.0
        if self._epic_mode:
            delay *= 0.25
        ba.timer(delay, self._set_meteor_timer)

        self._timer = OnScreenTimer()
        self._timer.start()

        # Check for immediate end (if we've only got 1 player, etc).
        ba.timer(5.0, self._check_end_game)

    def on_player_join(self, player: Player) -> None:
        # Don't allow joining after we start
        # (would enable leave/rejoin tomfoolery).
        if self.has_begun():
            ba.screenmessage(
                ba.Lstr(resource='playerDelayedJoinText',
                        subs=[('${PLAYER}', player.getname(full=True))]),
                color=(0, 1, 0),
            )
            # For score purposes, mark them as having died right as the
            # game started.
            assert self._timer is not None
            player.death_time = self._timer.getstarttime()
            return
        self.spawn_player(player)

    def on_player_leave(self, player: Player) -> None:
        # Augment default behavior.
        super().on_player_leave(player)

        # A departing player may trigger game-over.
        self._check_end_game()

    # overriding the default character spawning..
    def spawn_player(self, player: Player) -> ba.Actor:
        spaz = self.spawn_player_spaz(player)

        # Let's reconnect this player's controls to this
        # spaz but *without* the ability to attack or pick stuff up.
        spaz.connect_controls_to_player(enable_punch=False,
                                        enable_bomb=False,
                                        enable_pickup=False)

        # Also lets have them make some noise when they die.
        spaz.play_big_death_sound = True
        return spaz

    # Various high-level game events come through this method.
    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ba.PlayerDiedMessage):

            # Augment standard behavior.
            super().handlemessage(msg)

            curtime = ba.time()

            # Record the player's moment of death.
            # assert isinstance(msg.spaz.player
            msg.getplayer(Player).death_time = curtime

            # In co-op mode, end the game the instant everyone dies
            # (more accurate looking).
            # In teams/ffa, allow a one-second fudge-factor so we can
            # get more draws if players die basically at the same time.
            if isinstance(self.session, ba.CoopSession):
                # Teams will still show up if we check now.. check in
                # the next cycle.
                ba.pushcall(self._check_end_game)

                # Also record this for a final setting of the clock.
                self._last_player_death_time = curtime
            else:
                ba.timer(1.0, self._check_end_game)

        else:
            # Default handler:
            return super().handlemessage(msg)
        return None

    def _check_end_game(self) -> None:
        living_team_count = 0
        for team in self.teams:
            for player in team.players:
                if player.is_alive():
                    living_team_count += 1
                    break

        # In co-op, we go till everyone is dead.. otherwise we go
        # until one team remains.
        if isinstance(self.session, ba.CoopSession):
            if living_team_count <= 0:
                self.end_game()
        else:
            if living_team_count <= 1:
                self.end_game()

    def _set_meteor_timer(self) -> None:
        ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time,
                 self._drop_bomb_cluster)

    def _drop_bomb_cluster(self) -> None:

        # Random note: code like this is a handy way to plot out extents
        # and debug things.
        loc_test = False
        if loc_test:
            ba.newnode('locator', attrs={'position': (8, 6, -5.5)})
            ba.newnode('locator', attrs={'position': (8, 6, -2.3)})
            ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)})
            ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)})

        # Drop several bombs in series.
        delay = 0.0
        for _i in range(random.randrange(1, 3)):
            # Drop them somewhere within our bounds with velocity pointing
            # toward the opposite side.
            pos = (-7.3 + 15.3 * random.random(), 11,
                   -5.5 + 2.1 * random.random())
            dropdir = (-1.0 if pos[0] > 0 else 1.0)
            vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0)
            ba.timer(delay, ba.Call(self._drop_bomb, pos, vel))
            delay += 0.1
        self._set_meteor_timer()

    def _drop_bomb(self, position: Sequence[float],
                   velocity: Sequence[float]) -> None:
        Bomb(position=position, velocity=velocity, bomb_type = 'sticky').autoretain()

    def _decrement_meteor_time(self) -> None:
        self._meteor_time = max(0.01, self._meteor_time * 0.9)

    def end_game(self) -> None:
        cur_time = ba.time()
        assert self._timer is not None
        start_time = self._timer.getstarttime()

        # Mark death-time as now for any still-living players
        # and award players points for how long they lasted.
        # (these per-player scores are only meaningful in team-games)
        for team in self.teams:
            for player in team.players:
                survived = False

                # Throw an extra fudge factor in so teams that
                # didn't die come out ahead of teams that did.
                if player.death_time is None:
                    survived = True
                    player.death_time = cur_time + 1

                # Award a per-player score depending on how many seconds
                # they lasted (per-player scores only affect teams mode;
                # everywhere else just looks at the per-team score).
                score = int(player.death_time - self._timer.getstarttime())
                if survived:
                    score += 50  # A bit extra for survivors.
                self.stats.player_scored(player, score, screenmessage=False)

        # Stop updating our time text, and set the final time to match
        # exactly when our last guy died.
        self._timer.stop(endtime=self._last_player_death_time)

        # Ok now calc game results: set a score for each team and then tell
        # the game to end.
        results = ba.GameResults()

        # Remember that 'free-for-all' mode is simply a special form
        # of 'teams' mode where each player gets their own team, so we can
        # just always deal in teams and have all cases covered.
        for team in self.teams:

            # Set the team score to the max time survived by any player on
            # that team.
            longest_life = 0.0
            for player in team.players:
                assert player.death_time is not None
                longest_life = max(longest_life,
                                   player.death_time - start_time)

            # Submit the score value in milliseconds.
            results.set_team_score(team, int(1000.0 * longest_life))

        self.end(results=results)


# Copyright (c) Lifetime Benefit-Zebra
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this mod without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the mod.
#
# THE MOD IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE MOD OR THE USE OR OTHER DEALINGS IN THE
# MOD.
# --------------------------------------

#By Benefit-Zebra
#https://github.com/Benefit-Zebra
示例#12
0
class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
    """Game of stealing other team's flag and returning it to your base."""

    name = 'Capture the Flag'
    description = 'Return the enemy flag to score.'
    available_settings = [
        ba.IntSetting('Score to Win', min_value=1, default=3),
        ba.IntSetting(
            'Flag Touch Return Time',
            min_value=0,
            default=0,
            increment=1,
        ),
        ba.IntSetting(
            'Flag Idle Return Time',
            min_value=5,
            default=30,
            increment=5,
        ),
        ba.IntChoiceSetting(
            'Time Limit',
            choices=[
                ('None', 0),
                ('1 Minute', 60),
                ('2 Minutes', 120),
                ('5 Minutes', 300),
                ('10 Minutes', 600),
                ('20 Minutes', 1200),
            ],
            default=0,
        ),
        ba.FloatChoiceSetting(
            'Respawn Times',
            choices=[
                ('Shorter', 0.25),
                ('Short', 0.5),
                ('Normal', 1.0),
                ('Long', 2.0),
                ('Longer', 4.0),
            ],
            default=1.0,
        ),
        ba.BoolSetting('Epic Mode', default=False),
    ]

    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return issubclass(sessiontype, ba.DualTeamSession)

    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        return ba.getmaps('team_flag')

    def __init__(self, settings: dict):
        super().__init__(settings)
        self._scoreboard = Scoreboard()
        self._alarmsound = ba.getsound('alarm')
        self._ticking_sound = ba.getsound('ticking')
        self._score_sound = ba.getsound('score')
        self._swipsound = ba.getsound('swip')
        self._last_score_time = 0
        self._all_bases_material = ba.Material()
        self._last_home_flag_notice_print_time = 0.0
        self._score_to_win = int(settings['Score to Win'])
        self._epic_mode = bool(settings['Epic Mode'])
        self._time_limit = float(settings['Time Limit'])

        self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
        self.flag_idle_return_time = float(settings['Flag Idle Return Time'])

        # Base class overrides.
        self.slow_motion = self._epic_mode
        self.default_music = (ba.MusicType.EPIC if self._epic_mode else
                              ba.MusicType.FLAG_CATCHER)

    def get_instance_description(self) -> Union[str, Sequence]:
        if self._score_to_win == 1:
            return 'Steal the enemy flag.'
        return 'Steal the enemy flag ${ARG1} times.', self._score_to_win

    def get_instance_description_short(self) -> Union[str, Sequence]:
        if self._score_to_win == 1:
            return 'return 1 flag'
        return 'return ${ARG1} flags', self._score_to_win

    def create_team(self, sessionteam: ba.SessionTeam) -> Team:

        # Create our team instance and its initial values.

        base_pos = self.map.get_flag_position(sessionteam.id)
        Flag.project_stand(base_pos)

        ba.newnode('light',
                   attrs={
                       'position': base_pos,
                       'intensity': 0.6,
                       'height_attenuated': False,
                       'volume_intensity_scale': 0.1,
                       'radius': 0.1,
                       'color': sessionteam.color
                   })

        base_region_mat = ba.Material()
        pos = base_pos
        base_region = ba.newnode(
            'region',
            attrs={
                'position': (pos[0], pos[1] + 0.75, pos[2]),
                'scale': (0.5, 0.5, 0.5),
                'type': 'sphere',
                'materials': [base_region_mat, self._all_bases_material]
            })

        spaz_mat_no_flag_physical = ba.Material()
        spaz_mat_no_flag_collide = ba.Material()
        flagmat = ba.Material()

        team = Team(base_pos=base_pos,
                    base_region_material=base_region_mat,
                    base_region=base_region,
                    spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
                    spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
                    flagmaterial=flagmat)

        # Some parts of our spazzes don't collide physically with our
        # flags but generate callbacks.
        spaz_mat_no_flag_physical.add_actions(
            conditions=('they_have_material', flagmat),
            actions=(
                ('modify_part_collision', 'physical', False),
                ('call', 'at_connect',
                 lambda: self._handle_touching_own_flag(team, True)),
                ('call', 'at_disconnect',
                 lambda: self._handle_touching_own_flag(team, False)),
            ))

        # Other parts of our spazzes don't collide with our flags at all.
        spaz_mat_no_flag_collide.add_actions(
            conditions=('they_have_material', flagmat),
            actions=('modify_part_collision', 'collide', False),
        )

        # We wanna know when *any* flag enters/leaves our base.
        base_region_mat.add_actions(
            conditions=('they_have_material', FlagFactory.get().flagmaterial),
            actions=(
                ('modify_part_collision', 'collide', True),
                ('modify_part_collision', 'physical', False),
                ('call', 'at_connect',
                 lambda: self._handle_flag_entered_base(team)),
                ('call', 'at_disconnect',
                 lambda: self._handle_flag_left_base(team)),
            ))

        return team

    def on_team_join(self, team: Team) -> None:
        # Can't do this in create_team because the team's color/etc. have
        # not been wired up yet at that point.
        self._spawn_flag_for_team(team)
        self._update_scoreboard()

    def on_begin(self) -> None:
        super().on_begin()
        self.setup_standard_time_limit(self._time_limit)
        self.setup_standard_powerup_drops()
        ba.timer(1.0, call=self._tick, repeat=True)

    def _spawn_flag_for_team(self, team: Team) -> None:
        team.flag = CTFFlag(team)
        team.flag_return_touches = 0
        self._flash_base(team, length=1.0)
        assert team.flag.node
        ba.playsound(self._swipsound, position=team.flag.node.position)

    def _handle_flag_entered_base(self, team: Team) -> None:
        try:
            flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True)
        except ba.NotFoundError:
            # Don't think this should logically ever happen.
            print('Error getting CTFFlag in entering-base callback.')
            return

        if flag.team is team:
            team.home_flag_at_base = True

            # If the enemy flag is already here, score!
            if team.enemy_flag_at_base:
                self._score(team)
        else:
            team.enemy_flag_at_base = True
            if team.home_flag_at_base:
                # Award points to whoever was carrying the enemy flag.
                player = flag.last_player_to_hold
                if player and player.team is team:
                    assert self.stats
                    self.stats.player_scored(player, 50, big_message=True)

                # Update score and reset flags.
                self._score(team)

            # If the home-team flag isn't here, print a message to that effect.
            else:
                # Don't want slo-mo affecting this
                curtime = ba.time(ba.TimeType.BASE)
                if curtime - self._last_home_flag_notice_print_time > 5.0:
                    self._last_home_flag_notice_print_time = curtime
                    bpos = team.base_pos
                    tval = ba.Lstr(resource='ownFlagAtYourBaseWarning')
                    tnode = ba.newnode('text',
                                       attrs={
                                           'text':
                                           tval,
                                           'in_world':
                                           True,
                                           'scale':
                                           0.013,
                                           'color': (1, 1, 0, 1),
                                           'h_align':
                                           'center',
                                           'position':
                                           (bpos[0], bpos[1] + 3.2, bpos[2])
                                       })
                    ba.timer(5.1, tnode.delete)
                    ba.animate(tnode, 'scale', {
                        0.0: 0,
                        0.2: 0.013,
                        4.8: 0.013,
                        5.0: 0
                    })

    def _tick(self) -> None:
        # If either flag is away from base and not being held, tick down its
        # respawn timer.
        for team in self.teams:
            flag = team.flag
            assert flag is not None

            if not team.home_flag_at_base and flag.held_count == 0:
                time_out_counting_down = True
                if flag.time_out_respawn_time is None:
                    flag.reset_return_times()
                assert flag.time_out_respawn_time is not None
                flag.time_out_respawn_time -= 1
                if flag.time_out_respawn_time <= 0:
                    flag.handlemessage(ba.DieMessage())
            else:
                time_out_counting_down = False

            if flag.node and flag.counter:
                pos = flag.node.position
                flag.counter.position = (pos[0], pos[1] + 1.3, pos[2])

                # If there's no self-touches on this flag, set its text
                # to show its auto-return counter.  (if there's self-touches
                # its showing that time).
                if team.flag_return_touches == 0:
                    flag.counter.text = (str(flag.time_out_respawn_time) if (
                        time_out_counting_down
                        and flag.time_out_respawn_time is not None
                        and flag.time_out_respawn_time <= 10) else '')
                    flag.counter.color = (1, 1, 1, 0.5)
                    flag.counter.scale = 0.014

    def _score(self, team: Team) -> None:
        team.score += 1
        ba.playsound(self._score_sound)
        self._flash_base(team)
        self._update_scoreboard()

        # Have teammates celebrate.
        for player in team.players:
            if player.actor:
                player.actor.handlemessage(ba.CelebrateMessage(2.0))

        # Reset all flags/state.
        for reset_team in self.teams:
            if not reset_team.home_flag_at_base:
                assert reset_team.flag is not None
                reset_team.flag.handlemessage(ba.DieMessage())
            reset_team.enemy_flag_at_base = False
        if team.score >= self._score_to_win:
            self.end_game()

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

    def _handle_flag_left_base(self, team: Team) -> None:
        cur_time = ba.time()
        try:
            flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True)
        except ba.NotFoundError:
            # This can happen if the flag stops touching us due to being
            # deleted; that's ok.
            return

        if flag.team is team:

            # Check times here to prevent too much flashing.
            if (team.last_flag_leave_time is None
                    or cur_time - team.last_flag_leave_time > 3.0):
                ba.playsound(self._alarmsound, position=team.base_pos)
                self._flash_base(team)
            team.last_flag_leave_time = cur_time
            team.home_flag_at_base = False
        else:
            team.enemy_flag_at_base = False

    def _touch_return_update(self, team: Team) -> None:
        # Count down only while its away from base and not being held.
        assert team.flag is not None
        if team.home_flag_at_base or team.flag.held_count > 0:
            team.touch_return_timer_ticking = None
            return  # No need to return when its at home.
        if team.touch_return_timer_ticking is None:
            team.touch_return_timer_ticking = ba.NodeActor(
                ba.newnode('sound',
                           attrs={
                               'sound': self._ticking_sound,
                               'positional': False,
                               'loop': True
                           }))
        flag = team.flag
        if flag.touch_return_time is not None:
            flag.touch_return_time -= 0.1
            if flag.counter:
                flag.counter.text = f'{flag.touch_return_time:.1f}'
                flag.counter.color = (1, 1, 0, 1)
                flag.counter.scale = 0.02

            if flag.touch_return_time <= 0.0:
                self._award_players_touching_own_flag(team)
                flag.handlemessage(ba.DieMessage())

    def _award_players_touching_own_flag(self, team: Team) -> None:
        for player in team.players:
            if player.touching_own_flag > 0:
                return_score = 10 + 5 * int(self.flag_touch_return_time)
                self.stats.player_scored(player,
                                         return_score,
                                         screenmessage=False)

    def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
        """Called when a player touches or stops touching their own team flag.

        We keep track of when each player is touching their own flag so we
        can award points when returned.
        """
        player: Optional[Player]
        try:
            player = ba.getcollision().sourcenode.getdelegate(
                PlayerSpaz, True).getplayer(Player, True)
        except ba.NotFoundError:
            # This can happen if the player leaves but his corpse touches/etc.
            player = None

        if player:
            player.touching_own_flag += (1 if connecting else -1)

        # If return-time is zero, just kill it immediately.. otherwise keep
        # track of touches and count down.
        if float(self.flag_touch_return_time) <= 0.0:
            assert team.flag is not None
            if (connecting and not team.home_flag_at_base
                    and team.flag.held_count == 0):
                self._award_players_touching_own_flag(team)
                ba.getcollision().opposingnode.handlemessage(ba.DieMessage())

        # Takes a non-zero amount of time to return.
        else:
            if connecting:
                team.flag_return_touches += 1
                if team.flag_return_touches == 1:
                    team.touch_return_timer = ba.Timer(
                        0.1,
                        call=ba.Call(self._touch_return_update, team),
                        repeat=True)
                    team.touch_return_timer_ticking = None
            else:
                team.flag_return_touches -= 1
                if team.flag_return_touches == 0:
                    team.touch_return_timer = None
                    team.touch_return_timer_ticking = None
            if team.flag_return_touches < 0:
                ba.print_error('CTF flag_return_touches < 0')

    def _flash_base(self, team: Team, length: float = 2.0) -> None:
        light = ba.newnode('light',
                           attrs={
                               'position': team.base_pos,
                               'height_attenuated': False,
                               'radius': 0.3,
                               'color': team.color
                           })
        ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
        ba.timer(length, light.delete)

    def spawn_player_spaz(self,
                          player: Player,
                          position: Sequence[float] = None,
                          angle: float = None) -> PlayerSpaz:
        """Intercept new spazzes and add our team material for them."""
        spaz = super().spawn_player_spaz(player, position, angle)
        player = spaz.getplayer(Player, True)
        team: Team = player.team
        player.touching_own_flag = 0
        no_physical_mats: List[ba.Material] = [
            team.spaz_material_no_flag_physical
        ]
        no_collide_mats: List[ba.Material] = [
            team.spaz_material_no_flag_collide
        ]

        # Our normal parts should still collide; just not physically
        # (so we can calc restores).
        assert spaz.node
        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
        spaz.node.roller_materials = list(
            spaz.node.roller_materials) + no_physical_mats

        # Pickups and punches shouldn't hit at all though.
        spaz.node.punch_materials = list(
            spaz.node.punch_materials) + no_collide_mats
        spaz.node.pickup_materials = list(
            spaz.node.pickup_materials) + no_collide_mats
        spaz.node.extras_material = list(
            spaz.node.extras_material) + no_collide_mats
        return spaz

    def _update_scoreboard(self) -> None:
        for team in self.teams:
            self._scoreboard.set_team_value(team, team.score,
                                            self._score_to_win)

    def handlemessage(self, msg: Any) -> Any:

        if isinstance(msg, ba.PlayerDiedMessage):
            super().handlemessage(msg)  # Augment standard behavior.
            self.respawn_player(msg.getplayer(Player))

        elif isinstance(msg, FlagDiedMessage):
            assert isinstance(msg.flag, CTFFlag)
            ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))

        elif isinstance(msg, FlagPickedUpMessage):

            # Store the last player to hold the flag for scoring purposes.
            assert isinstance(msg.flag, CTFFlag)
            try:
                msg.flag.last_player_to_hold = msg.node.getdelegate(
                    PlayerSpaz, True).getplayer(Player, True)
            except ba.NotFoundError:
                pass

            msg.flag.held_count += 1
            msg.flag.reset_return_times()

        elif isinstance(msg, FlagDroppedMessage):
            # Store the last player to hold the flag for scoring purposes.
            assert isinstance(msg.flag, CTFFlag)
            msg.flag.held_count -= 1

        else:
            super().handlemessage(msg)
class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
    """Minigame involving dodging falling bombs."""

    name = 'Meteor Shower'
    description = 'Dodge the falling bombs.'
    available_settings = [ba.BoolSetting('Epic Mode', default=False)]
    scoreconfig = ba.ScoreConfig(label='Survived',
                                 scoretype=ba.ScoreType.MILLISECONDS,
                                 version='B')

    # Print messages when players die (since its meaningful in this game).
    announce_player_deaths = True

    # Don't allow joining after we start
    # (would enable leave/rejoin tomfoolery).
    allow_mid_activity_joins = False

    # We're currently hard-coded for one map.
    @classmethod
    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
        return ['Rampage']

    # We support teams, free-for-all, and co-op sessions.
    @classmethod
    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
        return (issubclass(sessiontype, ba.DualTeamSession)
                or issubclass(sessiontype, ba.FreeForAllSession)
                or issubclass(sessiontype, ba.CoopSession))

    def __init__(self, settings: dict):
        super().__init__(settings)

        self._epic_mode = settings.get('Epic Mode', False)
        self._last_player_death_time: Optional[float] = None
        self._meteor_time = 2.0
        self._timer: Optional[OnScreenTimer] = None

        # Some base class overrides:
        self.default_music = (ba.MusicType.EPIC
                              if self._epic_mode else ba.MusicType.SURVIVAL)
        if self._epic_mode:
            self.slow_motion = True

    def on_begin(self) -> None:
        super().on_begin()

        # Drop a wave every few seconds.. and every so often drop the time
        # between waves ..lets have things increase faster if we have fewer
        # players.
        delay = 5.0 if len(self.players) > 2 else 2.5
        if self._epic_mode:
            delay *= 0.25
        ba.timer(delay, self._decrement_meteor_time, repeat=True)

        # Kick off the first wave in a few seconds.
        delay = 3.0
        if self._epic_mode:
            delay *= 0.25
        ba.timer(delay, self._set_meteor_timer)

        self._timer = OnScreenTimer()
        self._timer.start()

        # Check for immediate end (if we've only got 1 player, etc).
        ba.timer(5.0, self._check_end_game)

    def on_player_leave(self, player: Player) -> None:
        # Augment default behavior.
        super().on_player_leave(player)

        # A departing player may trigger game-over.
        self._check_end_game()

    # overriding the default character spawning..
    def spawn_player(self, player: Player) -> ba.Actor:
        spaz = self.spawn_player_spaz(player)

        # Let's reconnect this player's controls to this
        # spaz but *without* the ability to attack or pick stuff up.
        spaz.connect_controls_to_player(enable_punch=False,
                                        enable_bomb=False,
                                        enable_pickup=False)

        # Also lets have them make some noise when they die.
        spaz.play_big_death_sound = True
        return spaz

    # Various high-level game events come through this method.
    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ba.PlayerDiedMessage):

            # Augment standard behavior.
            super().handlemessage(msg)

            curtime = ba.time()

            # Record the player's moment of death.
            # assert isinstance(msg.spaz.player
            msg.getplayer(Player).death_time = curtime

            # In co-op mode, end the game the instant everyone dies
            # (more accurate looking).
            # In teams/ffa, allow a one-second fudge-factor so we can
            # get more draws if players die basically at the same time.
            if isinstance(self.session, ba.CoopSession):
                # Teams will still show up if we check now.. check in
                # the next cycle.
                ba.pushcall(self._check_end_game)

                # Also record this for a final setting of the clock.
                self._last_player_death_time = curtime
            else:
                ba.timer(1.0, self._check_end_game)

        else:
            # Default handler:
            return super().handlemessage(msg)
        return None

    def _check_end_game(self) -> None:
        living_team_count = 0
        for team in self.teams:
            for player in team.players:
                if player.is_alive():
                    living_team_count += 1
                    break

        # In co-op, we go till everyone is dead.. otherwise we go
        # until one team remains.
        if isinstance(self.session, ba.CoopSession):
            if living_team_count <= 0:
                self.end_game()
        else:
            if living_team_count <= 1:
                self.end_game()

    def _set_meteor_timer(self) -> None:
        ba.timer((1.0 + 0.2 * random.random()) * self._meteor_time,
                 self._drop_bomb_cluster)

    def _drop_bomb_cluster(self) -> None:

        # Random note: code like this is a handy way to plot out extents
        # and debug things.
        loc_test = False
        if loc_test:
            ba.newnode('locator', attrs={'position': (8, 6, -5.5)})
            ba.newnode('locator', attrs={'position': (8, 6, -2.3)})
            ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)})
            ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)})

        # Drop several bombs in series.
        delay = 0.0
        for _i in range(random.randrange(1, 3)):
            # Drop them somewhere within our bounds with velocity pointing
            # toward the opposite side.
            pos = (-7.3 + 15.3 * random.random(), 11,
                   -5.5 + 2.1 * random.random())
            dropdir = (-1.0 if pos[0] > 0 else 1.0)
            vel = ((-5.0 + random.random() * 30.0) * dropdir, -4.0, 0)
            ba.timer(delay, ba.Call(self._drop_bomb, pos, vel))
            delay += 0.1
        self._set_meteor_timer()

    def _drop_bomb(self, position: Sequence[float],
                   velocity: Sequence[float]) -> None:
        Bomb(position=position, velocity=velocity).autoretain()

    def _decrement_meteor_time(self) -> None:
        self._meteor_time = max(0.01, self._meteor_time * 0.9)

    def end_game(self) -> None:
        cur_time = ba.time()
        assert self._timer is not None
        start_time = self._timer.getstarttime()

        # Mark death-time as now for any still-living players
        # and award players points for how long they lasted.
        # (these per-player scores are only meaningful in team-games)
        for team in self.teams:
            for player in team.players:
                survived = False

                # Throw an extra fudge factor in so teams that
                # didn't die come out ahead of teams that did.
                if player.death_time is None:
                    survived = True
                    player.death_time = cur_time + 1

                # Award a per-player score depending on how many seconds
                # they lasted (per-player scores only affect teams mode;
                # everywhere else just looks at the per-team score).
                score = int(player.death_time - self._timer.getstarttime())
                if survived:
                    score += 50  # A bit extra for survivors.
                self.stats.player_scored(player, score, screenmessage=False)

        # Stop updating our time text, and set the final time to match
        # exactly when our last guy died.
        self._timer.stop(endtime=self._last_player_death_time)

        # Ok now calc game results: set a score for each team and then tell
        # the game to end.
        results = ba.GameResults()

        # Remember that 'free-for-all' mode is simply a special form
        # of 'teams' mode where each player gets their own team, so we can
        # just always deal in teams and have all cases covered.
        for team in self.teams:

            # Set the team score to the max time survived by any player on
            # that team.
            longest_life = 0.0
            for player in team.players:
                assert player.death_time is not None
                longest_life = max(longest_life,
                                   player.death_time - start_time)

            # Submit the score value in milliseconds.
            results.set_team_score(team, int(1000.0 * longest_life))

        self.end(results=results)
示例#14
0
class QuakeGame(ba.TeamGameActivity[Player, Team]):
    """Quake Team Game Activity"""
    name = 'Quake'
    description = 'Kill a set number of enemies to win.'
    available_settings = [
        ba.IntSetting(
            'Kills to Win Per Player',
            default=15,
            min_value=1,
            increment=1,
        ),
        ba.IntChoiceSetting(
            'Time Limit',
            choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120),
                     ('5 Minutes', 300), ('10 Minutes', 600),
                     ('20 Minutes', 1200)],
            default=0,
        ),
        ba.FloatChoiceSetting(
            'Respawn Times',
            choices=[('At once', 0.0), ('Shorter', 0.25), ('Short', 0.5),
                     ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)],
            default=1.0,
        ),
        ba.BoolSetting(
            'Speed',
            default=True,
        ),
        ba.BoolSetting(
            'Enable Jump',
            default=True,
        ),
        ba.BoolSetting(
            'Enable Pickup',
            default=True,
        ),
        ba.BoolSetting(
            'Enable Bomb',
            default=False,
        ),
        ba.BoolSetting(
            'Obstacles',
            default=True,
        ),
        ba.IntChoiceSetting(
            'Obstacles Form',
            choices=[('Cube', ObstaclesForm.CUBE.value),
                     ('Sphere', ObstaclesForm.SPHERE.value),
                     ('Random', ObstaclesForm.RANDOM.value)],
            default=0,
        ),
        ba.IntChoiceSetting(
            'Weapon Type',
            choices=[('Rocket', WeaponType.ROCKET.value),
                     ('Railgun', WeaponType.RAILGUN.value)],
            default=WeaponType.ROCKET.value,
        ),
        ba.BoolSetting(
            'Obstacles Mirror Shots',
            default=False,
        ),
        ba.IntSetting(
            'Obstacles Count',
            default=16,
            min_value=0,
            increment=2,
        ),
        ba.BoolSetting(
            'Random Obstacles Color',
            default=True,
        ),
        ba.BoolSetting(
            'Epic Mode',
            default=False,
        ),
    ]

    @classmethod
    def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool:
        return issubclass(sessiontype, ba.MultiTeamSession) or issubclass(
            sessiontype, ba.FreeForAllSession)

    @classmethod
    def get_supported_maps(cls, sessiontype: Type[ba.Session]) -> List[str]:
        # TODO add more maps
        return ['Football Stadium', 'Monkey Face', 'Doom Shroom']

    def __init__(self, settings) -> None:
        super().__init__(settings)
        self._epic_mode = self.settings_raw['Epic Mode']
        self._score_to_win = self.settings_raw['Kills to Win Per Player']
        self._time_limit = self.settings_raw['Time Limit']
        self._obstacles_enabled = self.settings_raw['Obstacles']
        self._obstacles_count = self.settings_raw['Obstacles Count']
        self._speed_enabled = self.settings_raw['Speed']
        self._bomb_enabled = self.settings_raw['Enable Bomb']
        self._pickup_enabled = self.settings_raw['Enable Pickup']
        self._jump_enabled = self.settings_raw['Enable Jump']
        self._weapon_type = WeaponType(self.settings_raw['Weapon Type'])
        self.default_music = (ba.MusicType.EPIC
                              if self._epic_mode else ba.MusicType.GRAND_ROMP)
        self.slow_motion = self._epic_mode

        self.announce_player_deaths = True
        self._scoreboard = Scoreboard()
        self._ding_sound = ba.getsound('dingSmall')

        self._shield_dropper: Optional[ba.Timer] = None

    def get_instance_description(self) -> Union[str, Sequence]:
        return 'Kill ${ARG1} enemies.', self._score_to_win

    def on_team_join(self, team: Team) -> None:
        team.score = 0
        if self.has_begun():
            self._update_scoreboard()

    def on_begin(self) -> None:
        ba.TeamGameActivity.on_begin(self)
        self.drop_shield()
        self._shield_dropper = ba.Timer(8,
                                        ba.WeakCall(self.drop_shield),
                                        repeat=True)
        self.setup_standard_time_limit(self._time_limit)
        if self._obstacles_enabled:
            count = self._obstacles_count
            gamemap = self.map.getname()
            for i in range(count):  # TODO: tidy up around here
                if gamemap == 'Football Stadium':
                    radius = (random.uniform(-10, 1),
                              6,
                              random.uniform(-4.5, 4.5)) \
                        if i > count / 2 else (
                        random.uniform(10, 1), 6, random.uniform(-4.5, 4.5))
                else:
                    radius = (random.uniform(-10, 1),
                              6,
                              random.uniform(-8, 8)) \
                        if i > count / 2 else (
                        random.uniform(10, 1), 6, random.uniform(-8, 8))

                Obstacle(
                    position=radius,
                    mirror=self.settings_raw['Obstacles mirror shots'],
                    form=self.settings_raw['Obstacles form']).autoretain()

        self._update_scoreboard()

    def drop_shield(self) -> None:
        """Drop a shield powerup in random place"""
        # FIXME: should use map defs
        shield = PowerupBox(poweruptype='shield',
                            position=(random.uniform(-10, 10), 6,
                                      random.uniform(-5, 5))).autoretain()

        ba.playsound(self._ding_sound)

        p_light = ba.newnode('light',
                             owner=shield.node,
                             attrs={
                                 'position': (0, 0, 0),
                                 'color': (0.3, 0.0, 0.4),
                                 'radius': 0.3,
                                 'intensity': 2,
                                 'volume_intensity_scale': 10.0
                             })

        shield.node.connectattr('position', p_light, 'position')

        ba.animate(p_light, 'intensity', {0: 2, 8: 0})

    def spawn_player(self, player: Player) -> None:
        spaz = self.spawn_player_spaz(player)
        if self._weapon_type == WeaponType.ROCKET:
            quake.rocket.RocketLauncher().give(spaz)
        elif self._weapon_type == WeaponType.RAILGUN:
            quake.railgun.Railgun().give(spaz)
        spaz.connect_controls_to_player(enable_jump=self._jump_enabled,
                                        enable_pickup=self._pickup_enabled,
                                        enable_bomb=self._bomb_enabled,
                                        enable_fly=False)

        spaz.node.hockey = self._speed_enabled
        spaz.spaz_light = ba.newnode('light',
                                     owner=spaz.node,
                                     attrs={
                                         'position': (0, 0, 0),
                                         'color': spaz.node.color,
                                         'radius': 0.12,
                                         'intensity': 1,
                                         'volume_intensity_scale': 10.0
                                     })

        spaz.node.connectattr('position', spaz.spaz_light, 'position')

    def handlemessage(self, msg: Any) -> Any:
        if isinstance(msg, ba.PlayerDiedMessage):
            ba.TeamGameActivity.handlemessage(self, msg)
            player = msg.getplayer(Player)
            self.respawn_player(player)
            killer = msg.getkillerplayer(Player)
            if killer is None:
                return

            # handle team-kills
            if killer.team is player.team:
                # in free-for-all, killing yourself loses you a point
                if isinstance(self.session, ba.FreeForAllSession):
                    new_score = player.team.score - 1
                    new_score = max(0, new_score)
                    player.team.score = new_score
                # in teams-mode it gives a point to the other team
                else:
                    ba.playsound(self._ding_sound)
                    for team in self.teams:
                        if team is not killer.team:
                            team.score += 1
            # killing someone on another team nets a kill
            else:
                killer.team.score += 1
                ba.playsound(self._ding_sound)
                # in FFA show our score since its hard to find on
                # the scoreboard
                assert killer.actor is not None
                # noinspection PyUnresolvedReferences
                killer.actor.set_score_text(str(killer.team.score) + '/' +
                                            str(self._score_to_win),
                                            color=killer.team.color,
                                            flash=True)

            self._update_scoreboard()

            # if someone has won, set a timer to end shortly
            # (allows the dust to clear and draws to occur if
            # deaths are close enough)
            if any(team.score >= self._score_to_win for team in self.teams):
                ba.timer(0.5, self.end_game)

        else:
            ba.TeamGameActivity.handlemessage(self, msg)

    def _update_scoreboard(self) -> None:
        for team in self.teams:
            self._scoreboard.set_team_value(team, team.score,
                                            self._score_to_win)

    def end_game(self) -> None:
        results = ba.GameResults()
        for team in self.teams:
            results.set_team_score(team, team.score)

        self.end(results=results)
class TargetPracticeGame(ba.TeamGameActivity[Player, Team]):
    """Game where players try to hit targets with bombs."""

    name = 'Target Practice'
    description = 'Bomb as many targets as you can.'
    available_settings = [
        ba.IntSetting('Target Count', min_value=1, default=3),
        ba.BoolSetting('Enable Impact Bombs', default=True),
        ba.BoolSetting('Enable Triple Bombs', default=True)
    ]
    default_music = ba.MusicType.FORWARD_MARCH

    @classmethod
    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
        return ['Doom Shroom']

    @classmethod
    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
        # We support any teams or versus sessions.
        return (issubclass(sessiontype, ba.CoopSession)
                or issubclass(sessiontype, ba.MultiTeamSession))

    def __init__(self, settings: dict):
        super().__init__(settings)
        self._scoreboard = Scoreboard()
        self._targets: list[Target] = []
        self._update_timer: Optional[ba.Timer] = None
        self._countdown: Optional[OnScreenCountdown] = None
        self._target_count = int(settings['Target Count'])
        self._enable_impact_bombs = bool(settings['Enable Impact Bombs'])
        self._enable_triple_bombs = bool(settings['Enable Triple Bombs'])

    def on_team_join(self, team: Team) -> None:
        if self.has_begun():
            self.update_scoreboard()

    def on_begin(self) -> None:
        super().on_begin()
        self.update_scoreboard()

        # Number of targets is based on player count.
        for i in range(self._target_count):
            ba.timer(5.0 + i * 1.0, self._spawn_target)

        self._update_timer = ba.Timer(1.0, self._update, repeat=True)
        self._countdown = OnScreenCountdown(60, endcall=self.end_game)
        ba.timer(4.0, self._countdown.start)

    def spawn_player(self, player: Player) -> ba.Actor:
        spawn_center = (0, 3, -5)
        pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1],
               spawn_center[2] + random.uniform(-1.5, 1.5))

        # Reset their streak.
        player.streak = 0
        spaz = self.spawn_player_spaz(player, position=pos)

        # Give players permanent triple impact bombs and wire them up
        # to tell us when they drop a bomb.
        if self._enable_impact_bombs:
            spaz.bomb_type = 'impact'
        if self._enable_triple_bombs:
            spaz.set_bomb_count(3)
        spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
        return spaz

    def _spawn_target(self) -> None:

        # Generate a few random points; we'll use whichever one is farthest
        # from our existing targets (don't want overlapping targets).
        points = []

        for _i in range(4):
            # Calc a random point within a circle.
            while True:
                xpos = random.uniform(-1.0, 1.0)
                ypos = random.uniform(-1.0, 1.0)
                if xpos * xpos + ypos * ypos < 1.0:
                    break
            points.append(ba.Vec3(8.0 * xpos, 2.2, -3.5 + 5.0 * ypos))

        def get_min_dist_from_target(pnt: ba.Vec3) -> float:
            return min((t.get_dist_from_point(pnt) for t in self._targets))

        # If we have existing targets, use the point with the highest
        # min-distance-from-targets.
        if self._targets:
            point = max(points, key=get_min_dist_from_target)
        else:
            point = points[0]

        self._targets.append(Target(position=point))

    def _on_spaz_dropped_bomb(self, spaz: ba.Actor, bomb: ba.Actor) -> None:
        del spaz  # Unused.

        # Wire up this bomb to inform us when it blows up.
        assert isinstance(bomb, Bomb)
        bomb.add_explode_callback(self._on_bomb_exploded)

    def _on_bomb_exploded(self, bomb: Bomb, blast: Blast) -> None:
        assert blast.node
        pos = blast.node.position

        # Debugging: throw a locator down where we landed.
        # ba.newnode('locator', attrs={'position':blast.node.position})

        # Feed the explosion point to all our targets and get points in return.
        # Note: we operate on a copy of self._targets since the list may change
        # under us if we hit stuff (don't wanna get points for new targets).
        player = bomb.get_source_player(Player)
        if not player:
            # It's possible the player left after throwing the bomb.
            return

        bullseye = any(
            target.do_hit_at_position(pos, player)
            for target in list(self._targets))
        if bullseye:
            player.streak += 1
        else:
            player.streak = 0

    def _update(self) -> None:
        """Misc. periodic updating."""
        # Clear out targets that have died.
        self._targets = [t for t in self._targets if t]

    def handlemessage(self, msg: Any) -> Any:
        # When players die, respawn them.
        if isinstance(msg, ba.PlayerDiedMessage):
            super().handlemessage(msg)  # Do standard stuff.
            player = msg.getplayer(Player)
            assert player is not None
            self.respawn_player(player)  # Kick off a respawn.
        elif isinstance(msg, Target.TargetHitMessage):
            # A target is telling us it was hit and will die soon..
            # ..so make another one.
            self._spawn_target()
        else:
            super().handlemessage(msg)

    def update_scoreboard(self) -> None:
        """Update the game scoreboard with current team values."""
        for team in self.teams:
            self._scoreboard.set_team_value(team, team.score)

    def end_game(self) -> None:
        results = ba.GameResults()
        for team in self.teams:
            results.set_team_score(team, team.score)
        self.end(results)