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
 12
 13from typing_extensions import override
 14import bascenev1 as bs
 15
 16from bascenev1lib.actor.playerspaz import PlayerSpaz
 17from bascenev1lib.actor.scoreboard import Scoreboard
 18from bascenev1lib.actor.flag import (
 19    FlagFactory,
 20    Flag,
 21    FlagPickedUpMessage,
 22    FlagDroppedMessage,
 23    FlagDiedMessage,
 24)
 25
 26if TYPE_CHECKING:
 27    from typing import Any, Sequence
 28
 29
 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
 63
 64
 65class Player(bs.Player['Team']):
 66    """Our player type for this game."""
 67
 68    def __init__(self) -> None:
 69        self.touching_own_flag = 0
 70
 71
 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
 98
 99
100# ba_meta export bascenev1.GameActivity
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.exception('CTF flag_return_touches < 0')
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        # For each "point" our player has touched theflag (Could be multiple),
540        # deduct one from both our player and
541        # the flag's return touches variable.
542        for _ in range(player.touching_own_flag):
543            # Deduct
544            player.touching_own_flag -= 1
545            team.flag_return_touches -= 1
546            # Update our flag's timer accordingly
547            # (Prevents immediate resets in case
548            # there might be more people touching it).
549            if team.flag_return_touches == 0:
550                team.touch_return_timer = None
551                team.touch_return_timer_ticking = None
552            # Safety check, just to be sure!
553            if team.flag_return_touches < 0:
554                logging.exception('CTF flag_return_touches < 0')
555
556    def _flash_base(self, team: Team, length: float = 2.0) -> None:
557        light = bs.newnode(
558            'light',
559            attrs={
560                'position': team.base_pos,
561                'height_attenuated': False,
562                'radius': 0.3,
563                'color': team.color,
564            },
565        )
566        bs.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
567        bs.timer(length, light.delete)
568
569    @override
570    def spawn_player_spaz(
571        self,
572        player: Player,
573        position: Sequence[float] | None = None,
574        angle: float | None = None,
575    ) -> PlayerSpaz:
576        """Intercept new spazzes and add our team material for them."""
577        spaz = super().spawn_player_spaz(player, position, angle)
578        player = spaz.getplayer(Player, True)
579        team: Team = player.team
580        player.touching_own_flag = 0
581        no_physical_mats: list[bs.Material] = [
582            team.spaz_material_no_flag_physical
583        ]
584        no_collide_mats: list[bs.Material] = [
585            team.spaz_material_no_flag_collide
586        ]
587
588        # Our normal parts should still collide; just not physically
589        # (so we can calc restores).
590        assert spaz.node
591        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
592        spaz.node.roller_materials = (
593            list(spaz.node.roller_materials) + no_physical_mats
594        )
595
596        # Pickups and punches shouldn't hit at all though.
597        spaz.node.punch_materials = (
598            list(spaz.node.punch_materials) + no_collide_mats
599        )
600        spaz.node.pickup_materials = (
601            list(spaz.node.pickup_materials) + no_collide_mats
602        )
603        spaz.node.extras_material = (
604            list(spaz.node.extras_material) + no_collide_mats
605        )
606        return spaz
607
608    def _update_scoreboard(self) -> None:
609        for team in self.teams:
610            self._scoreboard.set_team_value(
611                team, team.score, self._score_to_win
612            )
613
614    @override
615    def handlemessage(self, msg: Any) -> Any:
616        if isinstance(msg, bs.PlayerDiedMessage):
617            super().handlemessage(msg)  # Augment standard behavior.
618            self._handle_death_flag_capture(msg.getplayer(Player))
619            self.respawn_player(msg.getplayer(Player))
620
621        elif isinstance(msg, FlagDiedMessage):
622            assert isinstance(msg.flag, CTFFlag)
623            bs.timer(0.1, bs.Call(self._spawn_flag_for_team, msg.flag.team))
624
625        elif isinstance(msg, FlagPickedUpMessage):
626            # Store the last player to hold the flag for scoring purposes.
627            assert isinstance(msg.flag, CTFFlag)
628            try:
629                msg.flag.last_player_to_hold = msg.node.getdelegate(
630                    PlayerSpaz, True
631                ).getplayer(Player, True)
632            except bs.NotFoundError:
633                pass
634
635            msg.flag.held_count += 1
636            msg.flag.reset_return_times()
637
638        elif isinstance(msg, FlagDroppedMessage):
639            # Store the last player to hold the flag for scoring purposes.
640            assert isinstance(msg.flag, CTFFlag)
641            msg.flag.held_count -= 1
642
643        else:
644            super().handlemessage(msg)
645
646    @override
647    def on_player_leave(self, player: Player) -> None:
648        """Prevents leaving players from capturing their flag."""
649        self._handle_death_flag_capture(player)
class CTFFlag(bascenev1lib.actor.flag.Flag):
31class CTFFlag(Flag):
32    """Special flag type for CTF games."""
33
34    activity: CaptureTheFlagGame
35
36    def __init__(self, team: Team):
37        assert team.flagmaterial is not None
38        super().__init__(
39            materials=[team.flagmaterial],
40            position=team.base_pos,
41            color=team.color,
42        )
43        self._team = team
44        self.held_count = 0
45        self.counter = bs.newnode(
46            'text',
47            owner=self.node,
48            attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
49        )
50        self.reset_return_times()
51        self.last_player_to_hold: Player | None = None
52        self.time_out_respawn_time: int | None = None
53        self.touch_return_time: float | None = None
54
55    def reset_return_times(self) -> None:
56        """Clear flag related times in the activity."""
57        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
58        self.touch_return_time = float(self.activity.flag_touch_return_time)
59
60    @property
61    def team(self) -> Team:
62        """The flag's team."""
63        return self._team

Special flag type for CTF games.

CTFFlag(team: Team)
36    def __init__(self, team: Team):
37        assert team.flagmaterial is not None
38        super().__init__(
39            materials=[team.flagmaterial],
40            position=team.base_pos,
41            color=team.color,
42        )
43        self._team = team
44        self.held_count = 0
45        self.counter = bs.newnode(
46            'text',
47            owner=self.node,
48            attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
49        )
50        self.reset_return_times()
51        self.last_player_to_hold: Player | None = None
52        self.time_out_respawn_time: int | None = None
53        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:
55    def reset_return_times(self) -> None:
56        """Clear flag related times in the activity."""
57        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
58        self.touch_return_time = float(self.activity.flag_touch_return_time)

Clear flag related times in the activity.

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

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

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

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:
147    @override
148    @classmethod
149    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
150        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]:
152    @override
153    @classmethod
154    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
155        assert bs.app.classic is not None
156        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]:
181    @override
182    def get_instance_description(self) -> str | Sequence:
183        if self._score_to_win == 1:
184            return 'Steal the enemy flag.'
185        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]:
187    @override
188    def get_instance_description_short(self) -> str | Sequence:
189        if self._score_to_win == 1:
190            return 'return 1 flag'
191        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:
193    @override
194    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
195        # Create our team instance and its initial values.
196
197        base_pos = self.map.get_flag_position(sessionteam.id)
198        Flag.project_stand(base_pos)
199
200        bs.newnode(
201            'light',
202            attrs={
203                'position': base_pos,
204                'intensity': 0.6,
205                'height_attenuated': False,
206                'volume_intensity_scale': 0.1,
207                'radius': 0.1,
208                'color': sessionteam.color,
209            },
210        )
211
212        base_region_mat = bs.Material()
213        pos = base_pos
214        base_region = bs.newnode(
215            'region',
216            attrs={
217                'position': (pos[0], pos[1] + 0.75, pos[2]),
218                'scale': (0.5, 0.5, 0.5),
219                'type': 'sphere',
220                'materials': [base_region_mat, self._all_bases_material],
221            },
222        )
223
224        spaz_mat_no_flag_physical = bs.Material()
225        spaz_mat_no_flag_collide = bs.Material()
226        flagmat = bs.Material()
227
228        team = Team(
229            base_pos=base_pos,
230            base_region_material=base_region_mat,
231            base_region=base_region,
232            spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
233            spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
234            flagmaterial=flagmat,
235        )
236
237        # Some parts of our spazzes don't collide physically with our
238        # flags but generate callbacks.
239        spaz_mat_no_flag_physical.add_actions(
240            conditions=('they_have_material', flagmat),
241            actions=(
242                ('modify_part_collision', 'physical', False),
243                (
244                    'call',
245                    'at_connect',
246                    lambda: self._handle_touching_own_flag(team, True),
247                ),
248                (
249                    'call',
250                    'at_disconnect',
251                    lambda: self._handle_touching_own_flag(team, False),
252                ),
253            ),
254        )
255
256        # Other parts of our spazzes don't collide with our flags at all.
257        spaz_mat_no_flag_collide.add_actions(
258            conditions=('they_have_material', flagmat),
259            actions=('modify_part_collision', 'collide', False),
260        )
261
262        # We wanna know when *any* flag enters/leaves our base.
263        base_region_mat.add_actions(
264            conditions=('they_have_material', FlagFactory.get().flagmaterial),
265            actions=(
266                ('modify_part_collision', 'collide', True),
267                ('modify_part_collision', 'physical', False),
268                (
269                    'call',
270                    'at_connect',
271                    lambda: self._handle_flag_entered_base(team),
272                ),
273                (
274                    'call',
275                    'at_disconnect',
276                    lambda: self._handle_flag_left_base(team),
277                ),
278            ),
279        )
280
281        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:
283    @override
284    def on_team_join(self, team: Team) -> None:
285        # Can't do this in create_team because the team's color/etc. have
286        # not been wired up yet at that point.
287        self._spawn_flag_for_team(team)
288        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
290    @override
291    def on_begin(self) -> None:
292        super().on_begin()
293        self.setup_standard_time_limit(self._time_limit)
294        self.setup_standard_powerup_drops()
295        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:
419    @override
420    def end_game(self) -> None:
421        results = bs.GameResults()
422        for team in self.teams:
423            results.set_team_score(team, team.score)
424        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:
570    @override
571    def spawn_player_spaz(
572        self,
573        player: Player,
574        position: Sequence[float] | None = None,
575        angle: float | None = None,
576    ) -> PlayerSpaz:
577        """Intercept new spazzes and add our team material for them."""
578        spaz = super().spawn_player_spaz(player, position, angle)
579        player = spaz.getplayer(Player, True)
580        team: Team = player.team
581        player.touching_own_flag = 0
582        no_physical_mats: list[bs.Material] = [
583            team.spaz_material_no_flag_physical
584        ]
585        no_collide_mats: list[bs.Material] = [
586            team.spaz_material_no_flag_collide
587        ]
588
589        # Our normal parts should still collide; just not physically
590        # (so we can calc restores).
591        assert spaz.node
592        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
593        spaz.node.roller_materials = (
594            list(spaz.node.roller_materials) + no_physical_mats
595        )
596
597        # Pickups and punches shouldn't hit at all though.
598        spaz.node.punch_materials = (
599            list(spaz.node.punch_materials) + no_collide_mats
600        )
601        spaz.node.pickup_materials = (
602            list(spaz.node.pickup_materials) + no_collide_mats
603        )
604        spaz.node.extras_material = (
605            list(spaz.node.extras_material) + no_collide_mats
606        )
607        return spaz

Intercept new spazzes and add our team material for them.

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

General message handling; can be passed any message object.

@override
def on_player_leave(self, player: Player) -> None:
647    @override
648    def on_player_leave(self, player: Player) -> None:
649        """Prevents leaving players from capturing their flag."""
650        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