bascenev1lib.game.capturetheflag

Defines a capture-the-flag game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Defines a capture-the-flag game."""
  4
  5# ba_meta require api 8
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import logging
 11from typing import TYPE_CHECKING, override
 12
 13import bascenev1 as bs
 14
 15from bascenev1lib.actor.playerspaz import PlayerSpaz
 16from bascenev1lib.actor.scoreboard import Scoreboard
 17from bascenev1lib.actor.flag import (
 18    FlagFactory,
 19    Flag,
 20    FlagPickedUpMessage,
 21    FlagDroppedMessage,
 22    FlagDiedMessage,
 23)
 24
 25if TYPE_CHECKING:
 26    from typing import Any, Sequence
 27
 28
 29class CTFFlag(Flag):
 30    """Special flag type for CTF games."""
 31
 32    activity: CaptureTheFlagGame
 33
 34    def __init__(self, team: Team):
 35        assert team.flagmaterial is not None
 36        super().__init__(
 37            materials=[team.flagmaterial],
 38            position=team.base_pos,
 39            color=team.color,
 40        )
 41        self._team = team
 42        self.held_count = 0
 43        self.counter = bs.newnode(
 44            'text',
 45            owner=self.node,
 46            attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
 47        )
 48        self.reset_return_times()
 49        self.last_player_to_hold: Player | None = None
 50        self.time_out_respawn_time: int | None = None
 51        self.touch_return_time: float | None = None
 52
 53    def reset_return_times(self) -> None:
 54        """Clear flag related times in the activity."""
 55        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
 56        self.touch_return_time = float(self.activity.flag_touch_return_time)
 57
 58    @property
 59    def team(self) -> Team:
 60        """The flag's team."""
 61        return self._team
 62
 63
 64class Player(bs.Player['Team']):
 65    """Our player type for this game."""
 66
 67    def __init__(self) -> None:
 68        self.touching_own_flag = 0
 69
 70
 71class Team(bs.Team[Player]):
 72    """Our team type for this game."""
 73
 74    def __init__(
 75        self,
 76        base_pos: Sequence[float],
 77        base_region_material: bs.Material,
 78        base_region: bs.Node,
 79        spaz_material_no_flag_physical: bs.Material,
 80        spaz_material_no_flag_collide: bs.Material,
 81        flagmaterial: bs.Material,
 82    ):
 83        self.base_pos = base_pos
 84        self.base_region_material = base_region_material
 85        self.base_region = base_region
 86        self.spaz_material_no_flag_physical = spaz_material_no_flag_physical
 87        self.spaz_material_no_flag_collide = spaz_material_no_flag_collide
 88        self.flagmaterial = flagmaterial
 89        self.score = 0
 90        self.flag_return_touches = 0
 91        self.home_flag_at_base = True
 92        self.touch_return_timer: bs.Timer | None = None
 93        self.enemy_flag_at_base = False
 94        self.flag: CTFFlag | None = None
 95        self.last_flag_leave_time: float | None = None
 96        self.touch_return_timer_ticking: bs.NodeActor | None = None
 97
 98
 99# ba_meta export bascenev1.GameActivity
100class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
101    """Game of stealing other team's flag and returning it to your base."""
102
103    name = 'Capture the Flag'
104    description = 'Return the enemy flag to score.'
105    available_settings = [
106        bs.IntSetting('Score to Win', min_value=1, default=3),
107        bs.IntSetting(
108            'Flag Touch Return Time',
109            min_value=0,
110            default=0,
111            increment=1,
112        ),
113        bs.IntSetting(
114            'Flag Idle Return Time',
115            min_value=5,
116            default=30,
117            increment=5,
118        ),
119        bs.IntChoiceSetting(
120            'Time Limit',
121            choices=[
122                ('None', 0),
123                ('1 Minute', 60),
124                ('2 Minutes', 120),
125                ('5 Minutes', 300),
126                ('10 Minutes', 600),
127                ('20 Minutes', 1200),
128            ],
129            default=0,
130        ),
131        bs.FloatChoiceSetting(
132            'Respawn Times',
133            choices=[
134                ('Shorter', 0.25),
135                ('Short', 0.5),
136                ('Normal', 1.0),
137                ('Long', 2.0),
138                ('Longer', 4.0),
139            ],
140            default=1.0,
141        ),
142        bs.BoolSetting('Epic Mode', default=False),
143    ]
144
145    @override
146    @classmethod
147    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
148        return issubclass(sessiontype, bs.DualTeamSession)
149
150    @override
151    @classmethod
152    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
153        assert bs.app.classic is not None
154        return bs.app.classic.getmaps('team_flag')
155
156    def __init__(self, settings: dict):
157        super().__init__(settings)
158        self._scoreboard = Scoreboard()
159        self._alarmsound = bs.getsound('alarm')
160        self._ticking_sound = bs.getsound('ticking')
161        self._score_sound = bs.getsound('score')
162        self._swipsound = bs.getsound('swip')
163        self._last_score_time = 0
164        self._all_bases_material = bs.Material()
165        self._last_home_flag_notice_print_time = 0.0
166        self._score_to_win = int(settings['Score to Win'])
167        self._epic_mode = bool(settings['Epic Mode'])
168        self._time_limit = float(settings['Time Limit'])
169
170        self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
171        self.flag_idle_return_time = float(settings['Flag Idle Return Time'])
172
173        # Base class overrides.
174        self.slow_motion = self._epic_mode
175        self.default_music = (
176            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FLAG_CATCHER
177        )
178
179    @override
180    def get_instance_description(self) -> str | Sequence:
181        if self._score_to_win == 1:
182            return 'Steal the enemy flag.'
183        return 'Steal the enemy flag ${ARG1} times.', self._score_to_win
184
185    @override
186    def get_instance_description_short(self) -> str | Sequence:
187        if self._score_to_win == 1:
188            return 'return 1 flag'
189        return 'return ${ARG1} flags', self._score_to_win
190
191    @override
192    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
193        # Create our team instance and its initial values.
194
195        base_pos = self.map.get_flag_position(sessionteam.id)
196        Flag.project_stand(base_pos)
197
198        bs.newnode(
199            'light',
200            attrs={
201                'position': base_pos,
202                'intensity': 0.6,
203                'height_attenuated': False,
204                'volume_intensity_scale': 0.1,
205                'radius': 0.1,
206                'color': sessionteam.color,
207            },
208        )
209
210        base_region_mat = bs.Material()
211        pos = base_pos
212        base_region = bs.newnode(
213            'region',
214            attrs={
215                'position': (pos[0], pos[1] + 0.75, pos[2]),
216                'scale': (0.5, 0.5, 0.5),
217                'type': 'sphere',
218                'materials': [base_region_mat, self._all_bases_material],
219            },
220        )
221
222        spaz_mat_no_flag_physical = bs.Material()
223        spaz_mat_no_flag_collide = bs.Material()
224        flagmat = bs.Material()
225
226        team = Team(
227            base_pos=base_pos,
228            base_region_material=base_region_mat,
229            base_region=base_region,
230            spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
231            spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
232            flagmaterial=flagmat,
233        )
234
235        # Some parts of our spazzes don't collide physically with our
236        # flags but generate callbacks.
237        spaz_mat_no_flag_physical.add_actions(
238            conditions=('they_have_material', flagmat),
239            actions=(
240                ('modify_part_collision', 'physical', False),
241                (
242                    'call',
243                    'at_connect',
244                    lambda: self._handle_touching_own_flag(team, True),
245                ),
246                (
247                    'call',
248                    'at_disconnect',
249                    lambda: self._handle_touching_own_flag(team, False),
250                ),
251            ),
252        )
253
254        # Other parts of our spazzes don't collide with our flags at all.
255        spaz_mat_no_flag_collide.add_actions(
256            conditions=('they_have_material', flagmat),
257            actions=('modify_part_collision', 'collide', False),
258        )
259
260        # We wanna know when *any* flag enters/leaves our base.
261        base_region_mat.add_actions(
262            conditions=('they_have_material', FlagFactory.get().flagmaterial),
263            actions=(
264                ('modify_part_collision', 'collide', True),
265                ('modify_part_collision', 'physical', False),
266                (
267                    'call',
268                    'at_connect',
269                    lambda: self._handle_flag_entered_base(team),
270                ),
271                (
272                    'call',
273                    'at_disconnect',
274                    lambda: self._handle_flag_left_base(team),
275                ),
276            ),
277        )
278
279        return team
280
281    @override
282    def on_team_join(self, team: Team) -> None:
283        # Can't do this in create_team because the team's color/etc. have
284        # not been wired up yet at that point.
285        self._spawn_flag_for_team(team)
286        self._update_scoreboard()
287
288    @override
289    def on_begin(self) -> None:
290        super().on_begin()
291        self.setup_standard_time_limit(self._time_limit)
292        self.setup_standard_powerup_drops()
293        bs.timer(1.0, call=self._tick, repeat=True)
294
295    def _spawn_flag_for_team(self, team: Team) -> None:
296        team.flag = CTFFlag(team)
297        team.flag_return_touches = 0
298        self._flash_base(team, length=1.0)
299        assert team.flag.node
300        self._swipsound.play(position=team.flag.node.position)
301
302    def _handle_flag_entered_base(self, team: Team) -> None:
303        try:
304            flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True)
305        except bs.NotFoundError:
306            # Don't think this should logically ever happen.
307            print('Error getting CTFFlag in entering-base callback.')
308            return
309
310        if flag.team is team:
311            team.home_flag_at_base = True
312
313            # If the enemy flag is already here, score!
314            if team.enemy_flag_at_base:
315                # And show team name which scored (but actually we could
316                # show here player who returned enemy flag).
317                self.show_zoom_message(
318                    bs.Lstr(
319                        resource='nameScoresText', subs=[('${NAME}', team.name)]
320                    ),
321                    color=team.color,
322                )
323                self._score(team)
324        else:
325            team.enemy_flag_at_base = True
326            if team.home_flag_at_base:
327                # Award points to whoever was carrying the enemy flag.
328                player = flag.last_player_to_hold
329                if player and player.team is team:
330                    self.stats.player_scored(player, 50, big_message=True)
331
332                # Update score and reset flags.
333                self._score(team)
334
335            # If the home-team flag isn't here, print a message to that effect.
336            else:
337                # Don't want slo-mo affecting this
338                curtime = bs.basetime()
339                if curtime - self._last_home_flag_notice_print_time > 5.0:
340                    self._last_home_flag_notice_print_time = curtime
341                    bpos = team.base_pos
342                    tval = bs.Lstr(resource='ownFlagAtYourBaseWarning')
343                    tnode = bs.newnode(
344                        'text',
345                        attrs={
346                            'text': tval,
347                            'in_world': True,
348                            'scale': 0.013,
349                            'color': (1, 1, 0, 1),
350                            'h_align': 'center',
351                            'position': (bpos[0], bpos[1] + 3.2, bpos[2]),
352                        },
353                    )
354                    bs.timer(5.1, tnode.delete)
355                    bs.animate(
356                        tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0}
357                    )
358
359    def _tick(self) -> None:
360        # If either flag is away from base and not being held, tick down its
361        # respawn timer.
362        for team in self.teams:
363            flag = team.flag
364            assert flag is not None
365
366            if not team.home_flag_at_base and flag.held_count == 0:
367                time_out_counting_down = True
368                if flag.time_out_respawn_time is None:
369                    flag.reset_return_times()
370                assert flag.time_out_respawn_time is not None
371                flag.time_out_respawn_time -= 1
372                if flag.time_out_respawn_time <= 0:
373                    flag.handlemessage(bs.DieMessage())
374            else:
375                time_out_counting_down = False
376
377            if flag.node and flag.counter:
378                pos = flag.node.position
379                flag.counter.position = (pos[0], pos[1] + 1.3, pos[2])
380
381                # If there's no self-touches on this flag, set its text
382                # to show its auto-return counter.  (if there's self-touches
383                # its showing that time).
384                if team.flag_return_touches == 0:
385                    flag.counter.text = (
386                        str(flag.time_out_respawn_time)
387                        if (
388                            time_out_counting_down
389                            and flag.time_out_respawn_time is not None
390                            and flag.time_out_respawn_time <= 10
391                        )
392                        else ''
393                    )
394                    flag.counter.color = (1, 1, 1, 0.5)
395                    flag.counter.scale = 0.014
396
397    def _score(self, team: Team) -> None:
398        team.score += 1
399        self._score_sound.play()
400        self._flash_base(team)
401        self._update_scoreboard()
402
403        # Have teammates celebrate.
404        for player in team.players:
405            if player.actor:
406                player.actor.handlemessage(bs.CelebrateMessage(2.0))
407
408        # Reset all flags/state.
409        for reset_team in self.teams:
410            if not reset_team.home_flag_at_base:
411                assert reset_team.flag is not None
412                reset_team.flag.handlemessage(bs.DieMessage())
413            reset_team.enemy_flag_at_base = False
414        if team.score >= self._score_to_win:
415            self.end_game()
416
417    @override
418    def end_game(self) -> None:
419        results = bs.GameResults()
420        for team in self.teams:
421            results.set_team_score(team, team.score)
422        self.end(results=results, announce_delay=0.8)
423
424    def _handle_flag_left_base(self, team: Team) -> None:
425        cur_time = bs.time()
426        try:
427            flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True)
428        except bs.NotFoundError:
429            # This can happen if the flag stops touching us due to being
430            # deleted; that's ok.
431            return
432
433        if flag.team is team:
434            # Check times here to prevent too much flashing.
435            if (
436                team.last_flag_leave_time is None
437                or cur_time - team.last_flag_leave_time > 3.0
438            ):
439                self._alarmsound.play(position=team.base_pos)
440                self._flash_base(team)
441            team.last_flag_leave_time = cur_time
442            team.home_flag_at_base = False
443        else:
444            team.enemy_flag_at_base = False
445
446    def _touch_return_update(self, team: Team) -> None:
447        # Count down only while its away from base and not being held.
448        assert team.flag is not None
449        if team.home_flag_at_base or team.flag.held_count > 0:
450            team.touch_return_timer_ticking = None
451            return  # No need to return when its at home.
452        if team.touch_return_timer_ticking is None:
453            team.touch_return_timer_ticking = bs.NodeActor(
454                bs.newnode(
455                    'sound',
456                    attrs={
457                        'sound': self._ticking_sound,
458                        'positional': False,
459                        'loop': True,
460                    },
461                )
462            )
463        flag = team.flag
464        if flag.touch_return_time is not None:
465            flag.touch_return_time -= 0.1
466            if flag.counter:
467                flag.counter.text = f'{flag.touch_return_time:.1f}'
468                flag.counter.color = (1, 1, 0, 1)
469                flag.counter.scale = 0.02
470
471            if flag.touch_return_time <= 0.0:
472                self._award_players_touching_own_flag(team)
473                flag.handlemessage(bs.DieMessage())
474
475    def _award_players_touching_own_flag(self, team: Team) -> None:
476        for player in team.players:
477            if player.touching_own_flag > 0:
478                return_score = 10 + 5 * int(self.flag_touch_return_time)
479                self.stats.player_scored(
480                    player, return_score, screenmessage=False
481                )
482
483    def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
484        """Called when a player touches or stops touching their own team flag.
485
486        We keep track of when each player is touching their own flag so we
487        can award points when returned.
488        """
489        player: Player | None
490        try:
491            spaz = bs.getcollision().sourcenode.getdelegate(PlayerSpaz, True)
492        except bs.NotFoundError:
493            return
494
495        player = spaz.getplayer(Player, True)
496
497        if player:
498            player.touching_own_flag += 1 if connecting else -1
499
500        # If return-time is zero, just kill it immediately.. otherwise keep
501        # track of touches and count down.
502        if float(self.flag_touch_return_time) <= 0.0:
503            assert team.flag is not None
504            if (
505                connecting
506                and not team.home_flag_at_base
507                and team.flag.held_count == 0
508            ):
509                self._award_players_touching_own_flag(team)
510                bs.getcollision().opposingnode.handlemessage(bs.DieMessage())
511
512        # Takes a non-zero amount of time to return.
513        else:
514            if connecting:
515                team.flag_return_touches += 1
516                if team.flag_return_touches == 1:
517                    team.touch_return_timer = bs.Timer(
518                        0.1,
519                        call=bs.Call(self._touch_return_update, team),
520                        repeat=True,
521                    )
522                    team.touch_return_timer_ticking = None
523            else:
524                team.flag_return_touches -= 1
525                if team.flag_return_touches == 0:
526                    team.touch_return_timer = None
527                    team.touch_return_timer_ticking = None
528            if team.flag_return_touches < 0:
529                logging.error('CTF flag_return_touches < 0', stack_info=True)
530
531    def _handle_death_flag_capture(self, player: Player) -> None:
532        """Handles flag values when a player dies or leaves the game."""
533        # Don't do anything if the player hasn't touched the flag at all.
534        if not player.touching_own_flag:
535            return
536
537        team = player.team
538
539        # For each "point" our player has touched theflag (Could be
540        # multiple), deduct one from both our player and the flag's
541        # return touches variable.
542        for _ in range(player.touching_own_flag):
543            # Deduct
544            player.touching_own_flag -= 1
545
546            # (This was only incremented if we have non-zero
547            # return-times).
548            if float(self.flag_touch_return_time) > 0.0:
549                team.flag_return_touches -= 1
550                # Update our flag's timer accordingly
551                # (Prevents immediate resets in case
552                # there might be more people touching it).
553                if team.flag_return_touches == 0:
554                    team.touch_return_timer = None
555                    team.touch_return_timer_ticking = None
556                # Safety check, just to be sure!
557                if team.flag_return_touches < 0:
558                    logging.error(
559                        'CTF flag_return_touches < 0', stack_info=True
560                    )
561
562    def _flash_base(self, team: Team, length: float = 2.0) -> None:
563        light = bs.newnode(
564            'light',
565            attrs={
566                'position': team.base_pos,
567                'height_attenuated': False,
568                'radius': 0.3,
569                'color': team.color,
570            },
571        )
572        bs.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
573        bs.timer(length, light.delete)
574
575    @override
576    def spawn_player_spaz(
577        self,
578        player: Player,
579        position: Sequence[float] | None = None,
580        angle: float | None = None,
581    ) -> PlayerSpaz:
582        """Intercept new spazzes and add our team material for them."""
583        spaz = super().spawn_player_spaz(player, position, angle)
584        player = spaz.getplayer(Player, True)
585        team: Team = player.team
586        player.touching_own_flag = 0
587        no_physical_mats: list[bs.Material] = [
588            team.spaz_material_no_flag_physical
589        ]
590        no_collide_mats: list[bs.Material] = [
591            team.spaz_material_no_flag_collide
592        ]
593
594        # Our normal parts should still collide; just not physically
595        # (so we can calc restores).
596        assert spaz.node
597        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
598        spaz.node.roller_materials = (
599            list(spaz.node.roller_materials) + no_physical_mats
600        )
601
602        # Pickups and punches shouldn't hit at all though.
603        spaz.node.punch_materials = (
604            list(spaz.node.punch_materials) + no_collide_mats
605        )
606        spaz.node.pickup_materials = (
607            list(spaz.node.pickup_materials) + no_collide_mats
608        )
609        spaz.node.extras_material = (
610            list(spaz.node.extras_material) + no_collide_mats
611        )
612        return spaz
613
614    def _update_scoreboard(self) -> None:
615        for team in self.teams:
616            self._scoreboard.set_team_value(
617                team, team.score, self._score_to_win
618            )
619
620    @override
621    def handlemessage(self, msg: Any) -> Any:
622        if isinstance(msg, bs.PlayerDiedMessage):
623            super().handlemessage(msg)  # Augment standard behavior.
624            self._handle_death_flag_capture(msg.getplayer(Player))
625            self.respawn_player(msg.getplayer(Player))
626
627        elif isinstance(msg, FlagDiedMessage):
628            assert isinstance(msg.flag, CTFFlag)
629            bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team))
630
631        elif isinstance(msg, FlagPickedUpMessage):
632            # Store the last player to hold the flag for scoring purposes.
633            assert isinstance(msg.flag, CTFFlag)
634            try:
635                msg.flag.last_player_to_hold = msg.node.getdelegate(
636                    PlayerSpaz, True
637                ).getplayer(Player, True)
638            except bs.NotFoundError:
639                pass
640
641            msg.flag.held_count += 1
642            msg.flag.reset_return_times()
643
644        elif isinstance(msg, FlagDroppedMessage):
645            # Store the last player to hold the flag for scoring purposes.
646            assert isinstance(msg.flag, CTFFlag)
647            msg.flag.held_count -= 1
648
649        else:
650            super().handlemessage(msg)
651
652    @override
653    def on_player_leave(self, player: Player) -> None:
654        """Prevents leaving players from capturing their flag."""
655        self._handle_death_flag_capture(player)
class CTFFlag(bascenev1lib.actor.flag.Flag):
30class CTFFlag(Flag):
31    """Special flag type for CTF games."""
32
33    activity: CaptureTheFlagGame
34
35    def __init__(self, team: Team):
36        assert team.flagmaterial is not None
37        super().__init__(
38            materials=[team.flagmaterial],
39            position=team.base_pos,
40            color=team.color,
41        )
42        self._team = team
43        self.held_count = 0
44        self.counter = bs.newnode(
45            'text',
46            owner=self.node,
47            attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
48        )
49        self.reset_return_times()
50        self.last_player_to_hold: Player | None = None
51        self.time_out_respawn_time: int | None = None
52        self.touch_return_time: float | None = None
53
54    def reset_return_times(self) -> None:
55        """Clear flag related times in the activity."""
56        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
57        self.touch_return_time = float(self.activity.flag_touch_return_time)
58
59    @property
60    def team(self) -> Team:
61        """The flag's team."""
62        return self._team

Special flag type for CTF games.

CTFFlag(team: Team)
35    def __init__(self, team: Team):
36        assert team.flagmaterial is not None
37        super().__init__(
38            materials=[team.flagmaterial],
39            position=team.base_pos,
40            color=team.color,
41        )
42        self._team = team
43        self.held_count = 0
44        self.counter = bs.newnode(
45            'text',
46            owner=self.node,
47            attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
48        )
49        self.reset_return_times()
50        self.last_player_to_hold: Player | None = None
51        self.time_out_respawn_time: int | None = None
52        self.touch_return_time: float | None = None

Instantiate a flag.

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

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

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

activity: bascenev1._activity.Activity
187    @property
188    def activity(self) -> bascenev1.Activity:
189        """The Activity this Actor was created in.
190
191        Raises a bascenev1.ActivityNotFoundError if the Activity no longer
192        exists.
193        """
194        activity = self._activity()
195        if activity is None:
196            raise babase.ActivityNotFoundError()
197        return activity

The Activity this Actor was created in.

Raises a bascenev1.ActivityNotFoundError if the Activity no longer exists.

held_count
counter
last_player_to_hold: Player | None
time_out_respawn_time: int | None
touch_return_time: float | None
def reset_return_times(self) -> None:
54    def reset_return_times(self) -> None:
55        """Clear flag related times in the activity."""
56        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
57        self.touch_return_time = float(self.activity.flag_touch_return_time)

Clear flag related times in the activity.

team: Team
59    @property
60    def team(self) -> Team:
61        """The flag's team."""
62        return self._team

The flag's team.

Inherited Members
bascenev1lib.actor.flag.Flag
node
set_score_text
handlemessage
project_stand
bascenev1._actor.Actor
autoretain
on_expire
expired
exists
is_alive
getactivity
class Player(bascenev1._player.Player[ForwardRef('Team')]):
65class Player(bs.Player['Team']):
66    """Our player type for this game."""
67
68    def __init__(self) -> None:
69        self.touching_own_flag = 0

Our player type for this game.

touching_own_flag
Inherited Members
bascenev1._player.Player
character
actor
color
highlight
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(bascenev1._team.Team[bascenev1lib.game.capturetheflag.Player]):
72class Team(bs.Team[Player]):
73    """Our team type for this game."""
74
75    def __init__(
76        self,
77        base_pos: Sequence[float],
78        base_region_material: bs.Material,
79        base_region: bs.Node,
80        spaz_material_no_flag_physical: bs.Material,
81        spaz_material_no_flag_collide: bs.Material,
82        flagmaterial: bs.Material,
83    ):
84        self.base_pos = base_pos
85        self.base_region_material = base_region_material
86        self.base_region = base_region
87        self.spaz_material_no_flag_physical = spaz_material_no_flag_physical
88        self.spaz_material_no_flag_collide = spaz_material_no_flag_collide
89        self.flagmaterial = flagmaterial
90        self.score = 0
91        self.flag_return_touches = 0
92        self.home_flag_at_base = True
93        self.touch_return_timer: bs.Timer | None = None
94        self.enemy_flag_at_base = False
95        self.flag: CTFFlag | None = None
96        self.last_flag_leave_time: float | None = None
97        self.touch_return_timer_ticking: bs.NodeActor | None = None

Our team type for this game.

Team( base_pos: Sequence[float], base_region_material: _bascenev1.Material, base_region: _bascenev1.Node, spaz_material_no_flag_physical: _bascenev1.Material, spaz_material_no_flag_collide: _bascenev1.Material, flagmaterial: _bascenev1.Material)
75    def __init__(
76        self,
77        base_pos: Sequence[float],
78        base_region_material: bs.Material,
79        base_region: bs.Node,
80        spaz_material_no_flag_physical: bs.Material,
81        spaz_material_no_flag_collide: bs.Material,
82        flagmaterial: bs.Material,
83    ):
84        self.base_pos = base_pos
85        self.base_region_material = base_region_material
86        self.base_region = base_region
87        self.spaz_material_no_flag_physical = spaz_material_no_flag_physical
88        self.spaz_material_no_flag_collide = spaz_material_no_flag_collide
89        self.flagmaterial = flagmaterial
90        self.score = 0
91        self.flag_return_touches = 0
92        self.home_flag_at_base = True
93        self.touch_return_timer: bs.Timer | None = None
94        self.enemy_flag_at_base = False
95        self.flag: CTFFlag | None = None
96        self.last_flag_leave_time: float | None = None
97        self.touch_return_timer_ticking: bs.NodeActor | None = None
base_pos
base_region_material
base_region
spaz_material_no_flag_physical
spaz_material_no_flag_collide
flagmaterial
score
flag_return_touches
home_flag_at_base
touch_return_timer: _bascenev1.Timer | None
enemy_flag_at_base
flag: CTFFlag | None
last_flag_leave_time: float | None
touch_return_timer_ticking: bascenev1._nodeactor.NodeActor | None
Inherited Members
bascenev1._team.Team
players
id
name
color
manual_init
customdata
on_expire
sessionteam
class CaptureTheFlagGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.capturetheflag.Player, bascenev1lib.game.capturetheflag.Team]):
101class CaptureTheFlagGame(bs.TeamGameActivity[Player, Team]):
102    """Game of stealing other team's flag and returning it to your base."""
103
104    name = 'Capture the Flag'
105    description = 'Return the enemy flag to score.'
106    available_settings = [
107        bs.IntSetting('Score to Win', min_value=1, default=3),
108        bs.IntSetting(
109            'Flag Touch Return Time',
110            min_value=0,
111            default=0,
112            increment=1,
113        ),
114        bs.IntSetting(
115            'Flag Idle Return Time',
116            min_value=5,
117            default=30,
118            increment=5,
119        ),
120        bs.IntChoiceSetting(
121            'Time Limit',
122            choices=[
123                ('None', 0),
124                ('1 Minute', 60),
125                ('2 Minutes', 120),
126                ('5 Minutes', 300),
127                ('10 Minutes', 600),
128                ('20 Minutes', 1200),
129            ],
130            default=0,
131        ),
132        bs.FloatChoiceSetting(
133            'Respawn Times',
134            choices=[
135                ('Shorter', 0.25),
136                ('Short', 0.5),
137                ('Normal', 1.0),
138                ('Long', 2.0),
139                ('Longer', 4.0),
140            ],
141            default=1.0,
142        ),
143        bs.BoolSetting('Epic Mode', default=False),
144    ]
145
146    @override
147    @classmethod
148    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
149        return issubclass(sessiontype, bs.DualTeamSession)
150
151    @override
152    @classmethod
153    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
154        assert bs.app.classic is not None
155        return bs.app.classic.getmaps('team_flag')
156
157    def __init__(self, settings: dict):
158        super().__init__(settings)
159        self._scoreboard = Scoreboard()
160        self._alarmsound = bs.getsound('alarm')
161        self._ticking_sound = bs.getsound('ticking')
162        self._score_sound = bs.getsound('score')
163        self._swipsound = bs.getsound('swip')
164        self._last_score_time = 0
165        self._all_bases_material = bs.Material()
166        self._last_home_flag_notice_print_time = 0.0
167        self._score_to_win = int(settings['Score to Win'])
168        self._epic_mode = bool(settings['Epic Mode'])
169        self._time_limit = float(settings['Time Limit'])
170
171        self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
172        self.flag_idle_return_time = float(settings['Flag Idle Return Time'])
173
174        # Base class overrides.
175        self.slow_motion = self._epic_mode
176        self.default_music = (
177            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FLAG_CATCHER
178        )
179
180    @override
181    def get_instance_description(self) -> str | Sequence:
182        if self._score_to_win == 1:
183            return 'Steal the enemy flag.'
184        return 'Steal the enemy flag ${ARG1} times.', self._score_to_win
185
186    @override
187    def get_instance_description_short(self) -> str | Sequence:
188        if self._score_to_win == 1:
189            return 'return 1 flag'
190        return 'return ${ARG1} flags', self._score_to_win
191
192    @override
193    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
194        # Create our team instance and its initial values.
195
196        base_pos = self.map.get_flag_position(sessionteam.id)
197        Flag.project_stand(base_pos)
198
199        bs.newnode(
200            'light',
201            attrs={
202                'position': base_pos,
203                'intensity': 0.6,
204                'height_attenuated': False,
205                'volume_intensity_scale': 0.1,
206                'radius': 0.1,
207                'color': sessionteam.color,
208            },
209        )
210
211        base_region_mat = bs.Material()
212        pos = base_pos
213        base_region = bs.newnode(
214            'region',
215            attrs={
216                'position': (pos[0], pos[1] + 0.75, pos[2]),
217                'scale': (0.5, 0.5, 0.5),
218                'type': 'sphere',
219                'materials': [base_region_mat, self._all_bases_material],
220            },
221        )
222
223        spaz_mat_no_flag_physical = bs.Material()
224        spaz_mat_no_flag_collide = bs.Material()
225        flagmat = bs.Material()
226
227        team = Team(
228            base_pos=base_pos,
229            base_region_material=base_region_mat,
230            base_region=base_region,
231            spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
232            spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
233            flagmaterial=flagmat,
234        )
235
236        # Some parts of our spazzes don't collide physically with our
237        # flags but generate callbacks.
238        spaz_mat_no_flag_physical.add_actions(
239            conditions=('they_have_material', flagmat),
240            actions=(
241                ('modify_part_collision', 'physical', False),
242                (
243                    'call',
244                    'at_connect',
245                    lambda: self._handle_touching_own_flag(team, True),
246                ),
247                (
248                    'call',
249                    'at_disconnect',
250                    lambda: self._handle_touching_own_flag(team, False),
251                ),
252            ),
253        )
254
255        # Other parts of our spazzes don't collide with our flags at all.
256        spaz_mat_no_flag_collide.add_actions(
257            conditions=('they_have_material', flagmat),
258            actions=('modify_part_collision', 'collide', False),
259        )
260
261        # We wanna know when *any* flag enters/leaves our base.
262        base_region_mat.add_actions(
263            conditions=('they_have_material', FlagFactory.get().flagmaterial),
264            actions=(
265                ('modify_part_collision', 'collide', True),
266                ('modify_part_collision', 'physical', False),
267                (
268                    'call',
269                    'at_connect',
270                    lambda: self._handle_flag_entered_base(team),
271                ),
272                (
273                    'call',
274                    'at_disconnect',
275                    lambda: self._handle_flag_left_base(team),
276                ),
277            ),
278        )
279
280        return team
281
282    @override
283    def on_team_join(self, team: Team) -> None:
284        # Can't do this in create_team because the team's color/etc. have
285        # not been wired up yet at that point.
286        self._spawn_flag_for_team(team)
287        self._update_scoreboard()
288
289    @override
290    def on_begin(self) -> None:
291        super().on_begin()
292        self.setup_standard_time_limit(self._time_limit)
293        self.setup_standard_powerup_drops()
294        bs.timer(1.0, call=self._tick, repeat=True)
295
296    def _spawn_flag_for_team(self, team: Team) -> None:
297        team.flag = CTFFlag(team)
298        team.flag_return_touches = 0
299        self._flash_base(team, length=1.0)
300        assert team.flag.node
301        self._swipsound.play(position=team.flag.node.position)
302
303    def _handle_flag_entered_base(self, team: Team) -> None:
304        try:
305            flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True)
306        except bs.NotFoundError:
307            # Don't think this should logically ever happen.
308            print('Error getting CTFFlag in entering-base callback.')
309            return
310
311        if flag.team is team:
312            team.home_flag_at_base = True
313
314            # If the enemy flag is already here, score!
315            if team.enemy_flag_at_base:
316                # And show team name which scored (but actually we could
317                # show here player who returned enemy flag).
318                self.show_zoom_message(
319                    bs.Lstr(
320                        resource='nameScoresText', subs=[('${NAME}', team.name)]
321                    ),
322                    color=team.color,
323                )
324                self._score(team)
325        else:
326            team.enemy_flag_at_base = True
327            if team.home_flag_at_base:
328                # Award points to whoever was carrying the enemy flag.
329                player = flag.last_player_to_hold
330                if player and player.team is team:
331                    self.stats.player_scored(player, 50, big_message=True)
332
333                # Update score and reset flags.
334                self._score(team)
335
336            # If the home-team flag isn't here, print a message to that effect.
337            else:
338                # Don't want slo-mo affecting this
339                curtime = bs.basetime()
340                if curtime - self._last_home_flag_notice_print_time > 5.0:
341                    self._last_home_flag_notice_print_time = curtime
342                    bpos = team.base_pos
343                    tval = bs.Lstr(resource='ownFlagAtYourBaseWarning')
344                    tnode = bs.newnode(
345                        'text',
346                        attrs={
347                            'text': tval,
348                            'in_world': True,
349                            'scale': 0.013,
350                            'color': (1, 1, 0, 1),
351                            'h_align': 'center',
352                            'position': (bpos[0], bpos[1] + 3.2, bpos[2]),
353                        },
354                    )
355                    bs.timer(5.1, tnode.delete)
356                    bs.animate(
357                        tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0}
358                    )
359
360    def _tick(self) -> None:
361        # If either flag is away from base and not being held, tick down its
362        # respawn timer.
363        for team in self.teams:
364            flag = team.flag
365            assert flag is not None
366
367            if not team.home_flag_at_base and flag.held_count == 0:
368                time_out_counting_down = True
369                if flag.time_out_respawn_time is None:
370                    flag.reset_return_times()
371                assert flag.time_out_respawn_time is not None
372                flag.time_out_respawn_time -= 1
373                if flag.time_out_respawn_time <= 0:
374                    flag.handlemessage(bs.DieMessage())
375            else:
376                time_out_counting_down = False
377
378            if flag.node and flag.counter:
379                pos = flag.node.position
380                flag.counter.position = (pos[0], pos[1] + 1.3, pos[2])
381
382                # If there's no self-touches on this flag, set its text
383                # to show its auto-return counter.  (if there's self-touches
384                # its showing that time).
385                if team.flag_return_touches == 0:
386                    flag.counter.text = (
387                        str(flag.time_out_respawn_time)
388                        if (
389                            time_out_counting_down
390                            and flag.time_out_respawn_time is not None
391                            and flag.time_out_respawn_time <= 10
392                        )
393                        else ''
394                    )
395                    flag.counter.color = (1, 1, 1, 0.5)
396                    flag.counter.scale = 0.014
397
398    def _score(self, team: Team) -> None:
399        team.score += 1
400        self._score_sound.play()
401        self._flash_base(team)
402        self._update_scoreboard()
403
404        # Have teammates celebrate.
405        for player in team.players:
406            if player.actor:
407                player.actor.handlemessage(bs.CelebrateMessage(2.0))
408
409        # Reset all flags/state.
410        for reset_team in self.teams:
411            if not reset_team.home_flag_at_base:
412                assert reset_team.flag is not None
413                reset_team.flag.handlemessage(bs.DieMessage())
414            reset_team.enemy_flag_at_base = False
415        if team.score >= self._score_to_win:
416            self.end_game()
417
418    @override
419    def end_game(self) -> None:
420        results = bs.GameResults()
421        for team in self.teams:
422            results.set_team_score(team, team.score)
423        self.end(results=results, announce_delay=0.8)
424
425    def _handle_flag_left_base(self, team: Team) -> None:
426        cur_time = bs.time()
427        try:
428            flag = bs.getcollision().opposingnode.getdelegate(CTFFlag, True)
429        except bs.NotFoundError:
430            # This can happen if the flag stops touching us due to being
431            # deleted; that's ok.
432            return
433
434        if flag.team is team:
435            # Check times here to prevent too much flashing.
436            if (
437                team.last_flag_leave_time is None
438                or cur_time - team.last_flag_leave_time > 3.0
439            ):
440                self._alarmsound.play(position=team.base_pos)
441                self._flash_base(team)
442            team.last_flag_leave_time = cur_time
443            team.home_flag_at_base = False
444        else:
445            team.enemy_flag_at_base = False
446
447    def _touch_return_update(self, team: Team) -> None:
448        # Count down only while its away from base and not being held.
449        assert team.flag is not None
450        if team.home_flag_at_base or team.flag.held_count > 0:
451            team.touch_return_timer_ticking = None
452            return  # No need to return when its at home.
453        if team.touch_return_timer_ticking is None:
454            team.touch_return_timer_ticking = bs.NodeActor(
455                bs.newnode(
456                    'sound',
457                    attrs={
458                        'sound': self._ticking_sound,
459                        'positional': False,
460                        'loop': True,
461                    },
462                )
463            )
464        flag = team.flag
465        if flag.touch_return_time is not None:
466            flag.touch_return_time -= 0.1
467            if flag.counter:
468                flag.counter.text = f'{flag.touch_return_time:.1f}'
469                flag.counter.color = (1, 1, 0, 1)
470                flag.counter.scale = 0.02
471
472            if flag.touch_return_time <= 0.0:
473                self._award_players_touching_own_flag(team)
474                flag.handlemessage(bs.DieMessage())
475
476    def _award_players_touching_own_flag(self, team: Team) -> None:
477        for player in team.players:
478            if player.touching_own_flag > 0:
479                return_score = 10 + 5 * int(self.flag_touch_return_time)
480                self.stats.player_scored(
481                    player, return_score, screenmessage=False
482                )
483
484    def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
485        """Called when a player touches or stops touching their own team flag.
486
487        We keep track of when each player is touching their own flag so we
488        can award points when returned.
489        """
490        player: Player | None
491        try:
492            spaz = bs.getcollision().sourcenode.getdelegate(PlayerSpaz, True)
493        except bs.NotFoundError:
494            return
495
496        player = spaz.getplayer(Player, True)
497
498        if player:
499            player.touching_own_flag += 1 if connecting else -1
500
501        # If return-time is zero, just kill it immediately.. otherwise keep
502        # track of touches and count down.
503        if float(self.flag_touch_return_time) <= 0.0:
504            assert team.flag is not None
505            if (
506                connecting
507                and not team.home_flag_at_base
508                and team.flag.held_count == 0
509            ):
510                self._award_players_touching_own_flag(team)
511                bs.getcollision().opposingnode.handlemessage(bs.DieMessage())
512
513        # Takes a non-zero amount of time to return.
514        else:
515            if connecting:
516                team.flag_return_touches += 1
517                if team.flag_return_touches == 1:
518                    team.touch_return_timer = bs.Timer(
519                        0.1,
520                        call=bs.Call(self._touch_return_update, team),
521                        repeat=True,
522                    )
523                    team.touch_return_timer_ticking = None
524            else:
525                team.flag_return_touches -= 1
526                if team.flag_return_touches == 0:
527                    team.touch_return_timer = None
528                    team.touch_return_timer_ticking = None
529            if team.flag_return_touches < 0:
530                logging.error('CTF flag_return_touches < 0', stack_info=True)
531
532    def _handle_death_flag_capture(self, player: Player) -> None:
533        """Handles flag values when a player dies or leaves the game."""
534        # Don't do anything if the player hasn't touched the flag at all.
535        if not player.touching_own_flag:
536            return
537
538        team = player.team
539
540        # For each "point" our player has touched theflag (Could be
541        # multiple), deduct one from both our player and the flag's
542        # return touches variable.
543        for _ in range(player.touching_own_flag):
544            # Deduct
545            player.touching_own_flag -= 1
546
547            # (This was only incremented if we have non-zero
548            # return-times).
549            if float(self.flag_touch_return_time) > 0.0:
550                team.flag_return_touches -= 1
551                # Update our flag's timer accordingly
552                # (Prevents immediate resets in case
553                # there might be more people touching it).
554                if team.flag_return_touches == 0:
555                    team.touch_return_timer = None
556                    team.touch_return_timer_ticking = None
557                # Safety check, just to be sure!
558                if team.flag_return_touches < 0:
559                    logging.error(
560                        'CTF flag_return_touches < 0', stack_info=True
561                    )
562
563    def _flash_base(self, team: Team, length: float = 2.0) -> None:
564        light = bs.newnode(
565            'light',
566            attrs={
567                'position': team.base_pos,
568                'height_attenuated': False,
569                'radius': 0.3,
570                'color': team.color,
571            },
572        )
573        bs.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
574        bs.timer(length, light.delete)
575
576    @override
577    def spawn_player_spaz(
578        self,
579        player: Player,
580        position: Sequence[float] | None = None,
581        angle: float | None = None,
582    ) -> PlayerSpaz:
583        """Intercept new spazzes and add our team material for them."""
584        spaz = super().spawn_player_spaz(player, position, angle)
585        player = spaz.getplayer(Player, True)
586        team: Team = player.team
587        player.touching_own_flag = 0
588        no_physical_mats: list[bs.Material] = [
589            team.spaz_material_no_flag_physical
590        ]
591        no_collide_mats: list[bs.Material] = [
592            team.spaz_material_no_flag_collide
593        ]
594
595        # Our normal parts should still collide; just not physically
596        # (so we can calc restores).
597        assert spaz.node
598        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
599        spaz.node.roller_materials = (
600            list(spaz.node.roller_materials) + no_physical_mats
601        )
602
603        # Pickups and punches shouldn't hit at all though.
604        spaz.node.punch_materials = (
605            list(spaz.node.punch_materials) + no_collide_mats
606        )
607        spaz.node.pickup_materials = (
608            list(spaz.node.pickup_materials) + no_collide_mats
609        )
610        spaz.node.extras_material = (
611            list(spaz.node.extras_material) + no_collide_mats
612        )
613        return spaz
614
615    def _update_scoreboard(self) -> None:
616        for team in self.teams:
617            self._scoreboard.set_team_value(
618                team, team.score, self._score_to_win
619            )
620
621    @override
622    def handlemessage(self, msg: Any) -> Any:
623        if isinstance(msg, bs.PlayerDiedMessage):
624            super().handlemessage(msg)  # Augment standard behavior.
625            self._handle_death_flag_capture(msg.getplayer(Player))
626            self.respawn_player(msg.getplayer(Player))
627
628        elif isinstance(msg, FlagDiedMessage):
629            assert isinstance(msg.flag, CTFFlag)
630            bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team))
631
632        elif isinstance(msg, FlagPickedUpMessage):
633            # Store the last player to hold the flag for scoring purposes.
634            assert isinstance(msg.flag, CTFFlag)
635            try:
636                msg.flag.last_player_to_hold = msg.node.getdelegate(
637                    PlayerSpaz, True
638                ).getplayer(Player, True)
639            except bs.NotFoundError:
640                pass
641
642            msg.flag.held_count += 1
643            msg.flag.reset_return_times()
644
645        elif isinstance(msg, FlagDroppedMessage):
646            # Store the last player to hold the flag for scoring purposes.
647            assert isinstance(msg.flag, CTFFlag)
648            msg.flag.held_count -= 1
649
650        else:
651            super().handlemessage(msg)
652
653    @override
654    def on_player_leave(self, player: Player) -> None:
655        """Prevents leaving players from capturing their flag."""
656        self._handle_death_flag_capture(player)

Game of stealing other team's flag and returning it to your base.

CaptureTheFlagGame(settings: dict)
157    def __init__(self, settings: dict):
158        super().__init__(settings)
159        self._scoreboard = Scoreboard()
160        self._alarmsound = bs.getsound('alarm')
161        self._ticking_sound = bs.getsound('ticking')
162        self._score_sound = bs.getsound('score')
163        self._swipsound = bs.getsound('swip')
164        self._last_score_time = 0
165        self._all_bases_material = bs.Material()
166        self._last_home_flag_notice_print_time = 0.0
167        self._score_to_win = int(settings['Score to Win'])
168        self._epic_mode = bool(settings['Epic Mode'])
169        self._time_limit = float(settings['Time Limit'])
170
171        self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
172        self.flag_idle_return_time = float(settings['Flag Idle Return Time'])
173
174        # Base class overrides.
175        self.slow_motion = self._epic_mode
176        self.default_music = (
177            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FLAG_CATCHER
178        )

Instantiate the Activity.

name = 'Capture the Flag'
description = 'Return the enemy flag to score.'
available_settings = [IntSetting(name='Score to Win', default=3, min_value=1, max_value=9999, increment=1), IntSetting(name='Flag Touch Return Time', default=0, min_value=0, max_value=9999, increment=1), IntSetting(name='Flag Idle Return Time', default=30, min_value=5, max_value=9999, increment=5), IntChoiceSetting(name='Time Limit', default=0, choices=[('None', 0), ('1 Minute', 60), ('2 Minutes', 120), ('5 Minutes', 300), ('10 Minutes', 600), ('20 Minutes', 1200)]), FloatChoiceSetting(name='Respawn Times', default=1.0, choices=[('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), ('Long', 2.0), ('Longer', 4.0)]), BoolSetting(name='Epic Mode', default=False)]
@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1._session.Session]) -> bool:
146    @override
147    @classmethod
148    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
149        return issubclass(sessiontype, bs.DualTeamSession)

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

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
151    @override
152    @classmethod
153    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
154        assert bs.app.classic is not None
155        return bs.app.classic.getmaps('team_flag')

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

flag_touch_return_time
flag_idle_return_time
slow_motion = False

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

default_music = None
@override
def get_instance_description(self) -> Union[str, Sequence]:
180    @override
181    def get_instance_description(self) -> str | Sequence:
182        if self._score_to_win == 1:
183            return 'Steal the enemy flag.'
184        return 'Steal the enemy flag ${ARG1} times.', self._score_to_win

Return a description for this game instance, in English.

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

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

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

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

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

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

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

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

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
186    @override
187    def get_instance_description_short(self) -> str | Sequence:
188        if self._score_to_win == 1:
189            return 'return 1 flag'
190        return 'return ${ARG1} flags', self._score_to_win

Return a short description for this game instance in English.

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

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

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

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

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

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

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

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

@override
def create_team( self, sessionteam: bascenev1._team.SessionTeam) -> Team:
192    @override
193    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
194        # Create our team instance and its initial values.
195
196        base_pos = self.map.get_flag_position(sessionteam.id)
197        Flag.project_stand(base_pos)
198
199        bs.newnode(
200            'light',
201            attrs={
202                'position': base_pos,
203                'intensity': 0.6,
204                'height_attenuated': False,
205                'volume_intensity_scale': 0.1,
206                'radius': 0.1,
207                'color': sessionteam.color,
208            },
209        )
210
211        base_region_mat = bs.Material()
212        pos = base_pos
213        base_region = bs.newnode(
214            'region',
215            attrs={
216                'position': (pos[0], pos[1] + 0.75, pos[2]),
217                'scale': (0.5, 0.5, 0.5),
218                'type': 'sphere',
219                'materials': [base_region_mat, self._all_bases_material],
220            },
221        )
222
223        spaz_mat_no_flag_physical = bs.Material()
224        spaz_mat_no_flag_collide = bs.Material()
225        flagmat = bs.Material()
226
227        team = Team(
228            base_pos=base_pos,
229            base_region_material=base_region_mat,
230            base_region=base_region,
231            spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
232            spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
233            flagmaterial=flagmat,
234        )
235
236        # Some parts of our spazzes don't collide physically with our
237        # flags but generate callbacks.
238        spaz_mat_no_flag_physical.add_actions(
239            conditions=('they_have_material', flagmat),
240            actions=(
241                ('modify_part_collision', 'physical', False),
242                (
243                    'call',
244                    'at_connect',
245                    lambda: self._handle_touching_own_flag(team, True),
246                ),
247                (
248                    'call',
249                    'at_disconnect',
250                    lambda: self._handle_touching_own_flag(team, False),
251                ),
252            ),
253        )
254
255        # Other parts of our spazzes don't collide with our flags at all.
256        spaz_mat_no_flag_collide.add_actions(
257            conditions=('they_have_material', flagmat),
258            actions=('modify_part_collision', 'collide', False),
259        )
260
261        # We wanna know when *any* flag enters/leaves our base.
262        base_region_mat.add_actions(
263            conditions=('they_have_material', FlagFactory.get().flagmaterial),
264            actions=(
265                ('modify_part_collision', 'collide', True),
266                ('modify_part_collision', 'physical', False),
267                (
268                    'call',
269                    'at_connect',
270                    lambda: self._handle_flag_entered_base(team),
271                ),
272                (
273                    'call',
274                    'at_disconnect',
275                    lambda: self._handle_flag_left_base(team),
276                ),
277            ),
278        )
279
280        return team

Create the Team instance for this Activity.

Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.

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

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
289    @override
290    def on_begin(self) -> None:
291        super().on_begin()
292        self.setup_standard_time_limit(self._time_limit)
293        self.setup_standard_powerup_drops()
294        bs.timer(1.0, call=self._tick, repeat=True)

Called once the previous Activity has finished transitioning out.

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

@override
def end_game(self) -> None:
418    @override
419    def end_game(self) -> None:
420        results = bs.GameResults()
421        for team in self.teams:
422            results.set_team_score(team, team.score)
423        self.end(results=results, announce_delay=0.8)

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

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

@override
def spawn_player_spaz( self, player: Player, position: Optional[Sequence[float]] = None, angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
576    @override
577    def spawn_player_spaz(
578        self,
579        player: Player,
580        position: Sequence[float] | None = None,
581        angle: float | None = None,
582    ) -> PlayerSpaz:
583        """Intercept new spazzes and add our team material for them."""
584        spaz = super().spawn_player_spaz(player, position, angle)
585        player = spaz.getplayer(Player, True)
586        team: Team = player.team
587        player.touching_own_flag = 0
588        no_physical_mats: list[bs.Material] = [
589            team.spaz_material_no_flag_physical
590        ]
591        no_collide_mats: list[bs.Material] = [
592            team.spaz_material_no_flag_collide
593        ]
594
595        # Our normal parts should still collide; just not physically
596        # (so we can calc restores).
597        assert spaz.node
598        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
599        spaz.node.roller_materials = (
600            list(spaz.node.roller_materials) + no_physical_mats
601        )
602
603        # Pickups and punches shouldn't hit at all though.
604        spaz.node.punch_materials = (
605            list(spaz.node.punch_materials) + no_collide_mats
606        )
607        spaz.node.pickup_materials = (
608            list(spaz.node.pickup_materials) + no_collide_mats
609        )
610        spaz.node.extras_material = (
611            list(spaz.node.extras_material) + no_collide_mats
612        )
613        return spaz

Intercept new spazzes and add our team material for them.

@override
def handlemessage(self, msg: Any) -> Any:
621    @override
622    def handlemessage(self, msg: Any) -> Any:
623        if isinstance(msg, bs.PlayerDiedMessage):
624            super().handlemessage(msg)  # Augment standard behavior.
625            self._handle_death_flag_capture(msg.getplayer(Player))
626            self.respawn_player(msg.getplayer(Player))
627
628        elif isinstance(msg, FlagDiedMessage):
629            assert isinstance(msg.flag, CTFFlag)
630            bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team))
631
632        elif isinstance(msg, FlagPickedUpMessage):
633            # Store the last player to hold the flag for scoring purposes.
634            assert isinstance(msg.flag, CTFFlag)
635            try:
636                msg.flag.last_player_to_hold = msg.node.getdelegate(
637                    PlayerSpaz, True
638                ).getplayer(Player, True)
639            except bs.NotFoundError:
640                pass
641
642            msg.flag.held_count += 1
643            msg.flag.reset_return_times()
644
645        elif isinstance(msg, FlagDroppedMessage):
646            # Store the last player to hold the flag for scoring purposes.
647            assert isinstance(msg.flag, CTFFlag)
648            msg.flag.held_count -= 1
649
650        else:
651            super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def on_player_leave(self, player: Player) -> None:
653    @override
654    def on_player_leave(self, player: Player) -> None:
655        """Prevents leaving players from capturing their flag."""
656        self._handle_death_flag_capture(player)

Prevents leaving players from capturing their flag.

Inherited Members
bascenev1._teamgame.TeamGameActivity
on_transition_in
end
bascenev1._gameactivity.GameActivity
tips
scoreconfig
allow_pausing
allow_kick_idle_players
show_kill_points
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_available_settings
get_settings_display_string
initialplayerinfos
map
get_instance_display_string
get_instance_scoreboard_display_string
on_continue
is_waiting_for_continue
continue_or_end_game
on_player_join
respawn_player
spawn_player_if_exists
spawn_player
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
bascenev1._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
use_fixed_vr_overlay
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
allow_mid_activity_joins
transition_time
can_show_ad_on_death
paused_text
preloads
lobby
context
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
session
on_team_leave
on_transition_out
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
create_player
bascenev1._dependency.DependencyComponent
dep_is_present
get_dynamic_deps