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

Special flag type for CTF games.

CTFFlag(team: bastd.game.capturetheflag.Team)
28    def __init__(self, team: Team):
29        assert team.flagmaterial is not None
30        super().__init__(materials=[team.flagmaterial],
31                         position=team.base_pos,
32                         color=team.color)
33        self._team = team
34        self.held_count = 0
35        self.counter = ba.newnode('text',
36                                  owner=self.node,
37                                  attrs={
38                                      'in_world': True,
39                                      'scale': 0.02,
40                                      'h_align': 'center'
41                                  })
42        self.reset_return_times()
43        self.last_player_to_hold: Player | None = None
44        self.time_out_respawn_time: int | None = None
45        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:
47    def reset_return_times(self) -> None:
48        """Clear flag related times in the activity."""
49        self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
50        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')]):
58class Player(ba.Player['Team']):
59    """Our player type for this game."""
60
61    def __init__(self) -> None:
62        self.touching_own_flag = 0

Our player type for this game.

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

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

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

Instantiate the Activity.

name: str | None = 'Capture the Flag'
description: str | None = 'Return the enemy flag to score.'
available_settings: list[ba._settings.Setting] | None = [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)]
@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
135    @classmethod
136    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
137        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]:
139    @classmethod
140    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
141        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.

def get_instance_description(self) -> Union[str, Sequence]:
165    def get_instance_description(self) -> str | Sequence:
166        if self._score_to_win == 1:
167            return 'Steal the enemy flag.'
168        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]:
170    def get_instance_description_short(self) -> str | Sequence:
171        if self._score_to_win == 1:
172            return 'return 1 flag'
173        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:
175    def create_team(self, sessionteam: ba.SessionTeam) -> Team:
176
177        # Create our team instance and its initial values.
178
179        base_pos = self.map.get_flag_position(sessionteam.id)
180        Flag.project_stand(base_pos)
181
182        ba.newnode('light',
183                   attrs={
184                       'position': base_pos,
185                       'intensity': 0.6,
186                       'height_attenuated': False,
187                       'volume_intensity_scale': 0.1,
188                       'radius': 0.1,
189                       'color': sessionteam.color
190                   })
191
192        base_region_mat = ba.Material()
193        pos = base_pos
194        base_region = ba.newnode(
195            'region',
196            attrs={
197                'position': (pos[0], pos[1] + 0.75, pos[2]),
198                'scale': (0.5, 0.5, 0.5),
199                'type': 'sphere',
200                'materials': [base_region_mat, self._all_bases_material]
201            })
202
203        spaz_mat_no_flag_physical = ba.Material()
204        spaz_mat_no_flag_collide = ba.Material()
205        flagmat = ba.Material()
206
207        team = Team(base_pos=base_pos,
208                    base_region_material=base_region_mat,
209                    base_region=base_region,
210                    spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
211                    spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
212                    flagmaterial=flagmat)
213
214        # Some parts of our spazzes don't collide physically with our
215        # flags but generate callbacks.
216        spaz_mat_no_flag_physical.add_actions(
217            conditions=('they_have_material', flagmat),
218            actions=(
219                ('modify_part_collision', 'physical', False),
220                ('call', 'at_connect',
221                 lambda: self._handle_touching_own_flag(team, True)),
222                ('call', 'at_disconnect',
223                 lambda: self._handle_touching_own_flag(team, False)),
224            ))
225
226        # Other parts of our spazzes don't collide with our flags at all.
227        spaz_mat_no_flag_collide.add_actions(
228            conditions=('they_have_material', flagmat),
229            actions=('modify_part_collision', 'collide', False),
230        )
231
232        # We wanna know when *any* flag enters/leaves our base.
233        base_region_mat.add_actions(
234            conditions=('they_have_material', FlagFactory.get().flagmaterial),
235            actions=(
236                ('modify_part_collision', 'collide', True),
237                ('modify_part_collision', 'physical', False),
238                ('call', 'at_connect',
239                 lambda: self._handle_flag_entered_base(team)),
240                ('call', 'at_disconnect',
241                 lambda: self._handle_flag_left_base(team)),
242            ))
243
244        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:
246    def on_team_join(self, team: Team) -> None:
247        # Can't do this in create_team because the team's color/etc. have
248        # not been wired up yet at that point.
249        self._spawn_flag_for_team(team)
250        self._update_scoreboard()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def on_begin(self) -> None:
252    def on_begin(self) -> None:
253        super().on_begin()
254        self.setup_standard_time_limit(self._time_limit)
255        self.setup_standard_powerup_drops()
256        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:
375    def end_game(self) -> None:
376        results = ba.GameResults()
377        for team in self.teams:
378            results.set_team_score(team, team.score)
379        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:
494    def spawn_player_spaz(self,
495                          player: Player,
496                          position: Sequence[float] | None = None,
497                          angle: float | None = None) -> PlayerSpaz:
498        """Intercept new spazzes and add our team material for them."""
499        spaz = super().spawn_player_spaz(player, position, angle)
500        player = spaz.getplayer(Player, True)
501        team: Team = player.team
502        player.touching_own_flag = 0
503        no_physical_mats: list[ba.Material] = [
504            team.spaz_material_no_flag_physical
505        ]
506        no_collide_mats: list[ba.Material] = [
507            team.spaz_material_no_flag_collide
508        ]
509
510        # Our normal parts should still collide; just not physically
511        # (so we can calc restores).
512        assert spaz.node
513        spaz.node.materials = list(spaz.node.materials) + no_physical_mats
514        spaz.node.roller_materials = list(
515            spaz.node.roller_materials) + no_physical_mats
516
517        # Pickups and punches shouldn't hit at all though.
518        spaz.node.punch_materials = list(
519            spaz.node.punch_materials) + no_collide_mats
520        spaz.node.pickup_materials = list(
521            spaz.node.pickup_materials) + no_collide_mats
522        spaz.node.extras_material = list(
523            spaz.node.extras_material) + no_collide_mats
524        return spaz

Intercept new spazzes and add our team material for them.

def handlemessage(self, msg: Any) -> Any:
531    def handlemessage(self, msg: Any) -> Any:
532
533        if isinstance(msg, ba.PlayerDiedMessage):
534            super().handlemessage(msg)  # Augment standard behavior.
535            self.respawn_player(msg.getplayer(Player))
536
537        elif isinstance(msg, FlagDiedMessage):
538            assert isinstance(msg.flag, CTFFlag)
539            ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))
540
541        elif isinstance(msg, FlagPickedUpMessage):
542
543            # Store the last player to hold the flag for scoring purposes.
544            assert isinstance(msg.flag, CTFFlag)
545            try:
546                msg.flag.last_player_to_hold = msg.node.getdelegate(
547                    PlayerSpaz, True).getplayer(Player, True)
548            except ba.NotFoundError:
549                pass
550
551            msg.flag.held_count += 1
552            msg.flag.reset_return_times()
553
554        elif isinstance(msg, FlagDroppedMessage):
555            # Store the last player to hold the flag for scoring purposes.
556            assert isinstance(msg.flag, CTFFlag)
557            msg.flag.held_count -= 1
558
559        else:
560            super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
ba._activity.Activity
slow_motion
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._gameactivity.GameActivity
default_music
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
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._teamgame.TeamGameActivity
on_transition_in
end
ba._dependency.DependencyComponent
dep_is_present
get_dynamic_deps