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

Special flag type for CTF games.

CTFFlag(team: bastd.game.capturetheflag.Team)
33    def __init__(self, team: Team):
34        assert team.flagmaterial is not None
35        super().__init__(
36            materials=[team.flagmaterial],
37            position=team.base_pos,
38            color=team.color,
39        )
40        self._team = team
41        self.held_count = 0
42        self.counter = ba.newnode(
43            'text',
44            owner=self.node,
45            attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
46        )
47        self.reset_return_times()
48        self.last_player_to_hold: Player | None = None
49        self.time_out_respawn_time: int | None = None
50        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 ba.Materials to apply to the flag.

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

activity: ba._activity.Activity

The Activity this Actor was created in.

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

def reset_return_times(self) -> None:
52    def reset_return_times(self) -> None:
53        """Clear flag related times in the activity."""
54        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
55        self.touch_return_time = float(self.activity.flag_touch_return_time)

Clear flag related times in the activity.

The flag's team.

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

Our player type for this game.

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

Our team type for this game.

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

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

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

Instantiate the Activity.

@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
144    @classmethod
145    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
146        return issubclass(sessiontype, ba.DualTeamSession)

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

@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
148    @classmethod
149    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
150        return ba.getmaps('team_flag')

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

slow_motion = False

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

def get_instance_description(self) -> Union[str, Sequence]:
175    def get_instance_description(self) -> str | Sequence:
176        if self._score_to_win == 1:
177            return 'Steal the enemy flag.'
178        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.

def get_instance_description_short(self) -> Union[str, Sequence]:
180    def get_instance_description_short(self) -> str | Sequence:
181        if self._score_to_win == 1:
182            return 'return 1 flag'
183        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.

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

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

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

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

Called once the previous ba.Activity has finished transitioning out.

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

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

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

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

def spawn_player_spaz( self, player: bastd.game.capturetheflag.Player, position: Optional[Sequence[float]] = None, angle: float | None = None) -> bastd.actor.playerspaz.PlayerSpaz:
540    def spawn_player_spaz(
541        self,
542        player: Player,
543        position: Sequence[float] | None = None,
544        angle: float | None = None,
545    ) -> PlayerSpaz:
546        """Intercept new spazzes and add our team material for them."""
547        spaz = super().spawn_player_spaz(player, position, angle)
548        player = spaz.getplayer(Player, True)
549        team: Team = player.team
550        player.touching_own_flag = 0
551        no_physical_mats: list[ba.Material] = [
552            team.spaz_material_no_flag_physical
553        ]
554        no_collide_mats: list[ba.Material] = [
555            team.spaz_material_no_flag_collide
556        ]
557
558        # Our normal parts should still collide; just not physically
559        # (so we can calc restores).
560        assert spaz.node
561        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
562        spaz.node.roller_materials = (
563            list(spaz.node.roller_materials) + no_physical_mats
564        )
565
566        # Pickups and punches shouldn't hit at all though.
567        spaz.node.punch_materials = (
568            list(spaz.node.punch_materials) + no_collide_mats
569        )
570        spaz.node.pickup_materials = (
571            list(spaz.node.pickup_materials) + no_collide_mats
572        )
573        spaz.node.extras_material = (
574            list(spaz.node.extras_material) + no_collide_mats
575        )
576        return spaz

Intercept new spazzes and add our team material for them.

def handlemessage(self, msg: Any) -> Any:
584    def handlemessage(self, msg: Any) -> Any:
585
586        if isinstance(msg, ba.PlayerDiedMessage):
587            super().handlemessage(msg)  # Augment standard behavior.
588            self.respawn_player(msg.getplayer(Player))
589
590        elif isinstance(msg, FlagDiedMessage):
591            assert isinstance(msg.flag, CTFFlag)
592            ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))
593
594        elif isinstance(msg, FlagPickedUpMessage):
595
596            # Store the last player to hold the flag for scoring purposes.
597            assert isinstance(msg.flag, CTFFlag)
598            try:
599                msg.flag.last_player_to_hold = msg.node.getdelegate(
600                    PlayerSpaz, True
601                ).getplayer(Player, True)
602            except ba.NotFoundError:
603                pass
604
605            msg.flag.held_count += 1
606            msg.flag.reset_return_times()
607
608        elif isinstance(msg, FlagDroppedMessage):
609            # Store the last player to hold the flag for scoring purposes.
610            assert isinstance(msg.flag, CTFFlag)
611            msg.flag.held_count -= 1
612
613        else:
614            super().handlemessage(msg)

General message handling; can be passed any message object.

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