bascenev1lib.game.assault

Defines assault minigame.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Defines assault minigame."""
  4
  5# ba_meta require api 9
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import random
 11from typing import TYPE_CHECKING, override
 12
 13import bascenev1 as bs
 14
 15from bascenev1lib.actor.playerspaz import PlayerSpaz
 16from bascenev1lib.actor.flag import Flag
 17from bascenev1lib.actor.scoreboard import Scoreboard
 18from bascenev1lib.gameutils import SharedObjects
 19
 20if TYPE_CHECKING:
 21    from typing import Any, Sequence
 22
 23
 24class Player(bs.Player['Team']):
 25    """Our player type for this game."""
 26
 27
 28class Team(bs.Team[Player]):
 29    """Our team type for this game."""
 30
 31    def __init__(self, base_pos: Sequence[float], flag: Flag) -> None:
 32
 33        #: Where our base is.
 34        self.base_pos = base_pos
 35
 36        #: Flag for this team.
 37        self.flag = flag
 38
 39        #: Current score.
 40        self.score = 0
 41
 42
 43# ba_meta export bascenev1.GameActivity
 44class AssaultGame(bs.TeamGameActivity[Player, Team]):
 45    """Game where you score by touching the other team's flag."""
 46
 47    name = 'Assault'
 48    description = 'Reach the enemy flag to score.'
 49    available_settings = [
 50        bs.IntSetting(
 51            'Score to Win',
 52            min_value=1,
 53            default=3,
 54        ),
 55        bs.IntChoiceSetting(
 56            'Time Limit',
 57            choices=[
 58                ('None', 0),
 59                ('1 Minute', 60),
 60                ('2 Minutes', 120),
 61                ('5 Minutes', 300),
 62                ('10 Minutes', 600),
 63                ('20 Minutes', 1200),
 64            ],
 65            default=0,
 66        ),
 67        bs.FloatChoiceSetting(
 68            'Respawn Times',
 69            choices=[
 70                ('Shorter', 0.25),
 71                ('Short', 0.5),
 72                ('Normal', 1.0),
 73                ('Long', 2.0),
 74                ('Longer', 4.0),
 75            ],
 76            default=1.0,
 77        ),
 78        bs.BoolSetting('Epic Mode', default=False),
 79    ]
 80
 81    @override
 82    @classmethod
 83    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 84        return issubclass(sessiontype, bs.DualTeamSession)
 85
 86    @override
 87    @classmethod
 88    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 89        assert bs.app.classic is not None
 90        return bs.app.classic.getmaps('team_flag')
 91
 92    def __init__(self, settings: dict):
 93        super().__init__(settings)
 94        self._scoreboard = Scoreboard()
 95        self._last_score_time = 0.0
 96        self._score_sound = bs.getsound('score')
 97        self._base_region_materials: dict[int, bs.Material] = {}
 98        self._epic_mode = bool(settings['Epic Mode'])
 99        self._score_to_win = int(settings['Score to Win'])
100        self._time_limit = float(settings['Time Limit'])
101
102        # Base class overrides
103        self.slow_motion = self._epic_mode
104        self.default_music = (
105            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
106        )
107
108    @override
109    def get_instance_description(self) -> str | Sequence:
110        if self._score_to_win == 1:
111            return 'Touch the enemy flag.'
112        return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
113
114    @override
115    def get_instance_description_short(self) -> str | Sequence:
116        if self._score_to_win == 1:
117            return 'touch 1 flag'
118        return 'touch ${ARG1} flags', self._score_to_win
119
120    @override
121    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
122        shared = SharedObjects.get()
123        base_pos = self.map.get_flag_position(sessionteam.id)
124        bs.newnode(
125            'light',
126            attrs={
127                'position': base_pos,
128                'intensity': 0.6,
129                'height_attenuated': False,
130                'volume_intensity_scale': 0.1,
131                'radius': 0.1,
132                'color': sessionteam.color,
133            },
134        )
135        Flag.project_stand(base_pos)
136        flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
137        team = Team(base_pos=base_pos, flag=flag)
138
139        mat = self._base_region_materials[sessionteam.id] = bs.Material()
140        mat.add_actions(
141            conditions=('they_have_material', shared.player_material),
142            actions=(
143                ('modify_part_collision', 'collide', True),
144                ('modify_part_collision', 'physical', False),
145                (
146                    'call',
147                    'at_connect',
148                    bs.Call(self._handle_base_collide, team),
149                ),
150            ),
151        )
152
153        bs.newnode(
154            'region',
155            owner=flag.node,
156            attrs={
157                'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
158                'scale': (0.5, 0.5, 0.5),
159                'type': 'sphere',
160                'materials': [self._base_region_materials[sessionteam.id]],
161            },
162        )
163
164        return team
165
166    @override
167    def on_team_join(self, team: Team) -> None:
168        # Can't do this in create_team because the team's color/etc. have
169        # not been wired up yet at that point.
170        self._update_scoreboard()
171
172    @override
173    def on_begin(self) -> None:
174        super().on_begin()
175        self.setup_standard_time_limit(self._time_limit)
176        self.setup_standard_powerup_drops()
177
178    @override
179    def handlemessage(self, msg: Any) -> Any:
180        if isinstance(msg, bs.PlayerDiedMessage):
181            super().handlemessage(msg)  # Augment standard.
182            self.respawn_player(msg.getplayer(Player))
183        else:
184            super().handlemessage(msg)
185
186    def _flash_base(self, team: Team, length: float = 2.0) -> None:
187        light = bs.newnode(
188            'light',
189            attrs={
190                'position': team.base_pos,
191                'height_attenuated': False,
192                'radius': 0.3,
193                'color': team.color,
194            },
195        )
196        bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
197        bs.timer(length, light.delete)
198
199    def _handle_base_collide(self, team: Team) -> None:
200        try:
201            spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
202        except bs.NotFoundError:
203            return
204
205        if not spaz.is_alive():
206            return
207
208        try:
209            player = spaz.getplayer(Player, True)
210        except bs.NotFoundError:
211            return
212
213        # If its another team's player, they scored.
214        player_team = player.team
215        if player_team is not team:
216            # Prevent multiple simultaneous scores.
217            if bs.time() != self._last_score_time:
218                self._last_score_time = bs.time()
219                self.stats.player_scored(player, 50, big_message=True)
220                self._score_sound.play()
221                self._flash_base(team)
222
223                # Move all players on the scoring team back to their start
224                # and add flashes of light so its noticeable.
225                for player in player_team.players:
226                    if player.is_alive():
227                        pos = player.node.position
228                        light = bs.newnode(
229                            'light',
230                            attrs={
231                                'position': pos,
232                                'color': player_team.color,
233                                'height_attenuated': False,
234                                'radius': 0.4,
235                            },
236                        )
237                        bs.timer(0.5, light.delete)
238                        bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
239
240                        new_pos = self.map.get_start_position(player_team.id)
241                        light = bs.newnode(
242                            'light',
243                            attrs={
244                                'position': new_pos,
245                                'color': player_team.color,
246                                'radius': 0.4,
247                                'height_attenuated': False,
248                            },
249                        )
250                        bs.timer(0.5, light.delete)
251                        bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
252                        if player.actor:
253                            random_num = random.uniform(0, 360)
254
255                            # Slightly hacky workaround: normally,
256                            # teleporting back to base with a sticky
257                            # bomb stuck to you gives a crazy whiplash
258                            # rubber-band effect. Running the teleport
259                            # twice in a row seems to suppress that
260                            # though. Would be better to fix this at a
261                            # lower level, but this works for now.
262                            self._teleport(player, new_pos, random_num)
263                            bs.timer(
264                                0.01,
265                                bs.Call(
266                                    self._teleport, player, new_pos, random_num
267                                ),
268                            )
269
270                # Have teammates celebrate.
271                for player in player_team.players:
272                    if player.actor:
273                        player.actor.handlemessage(bs.CelebrateMessage(2.0))
274
275                player_team.score += 1
276                self._update_scoreboard()
277                if player_team.score >= self._score_to_win:
278                    self.end_game()
279
280    def _teleport(
281        self, client: Player, pos: Sequence[float], num: float
282    ) -> None:
283        if client.actor:
284            client.actor.handlemessage(bs.StandMessage(pos, num))
285
286    @override
287    def end_game(self) -> None:
288        results = bs.GameResults()
289        for team in self.teams:
290            results.set_team_score(team, team.score)
291        self.end(results=results)
292
293    def _update_scoreboard(self) -> None:
294        for team in self.teams:
295            self._scoreboard.set_team_value(
296                team, team.score, self._score_to_win
297            )
class Player(bascenev1._player.Player[ForwardRef('Team')]):
25class Player(bs.Player['Team']):
26    """Our player type for this game."""

Our player type for this game.

class Team(bascenev1._team.Team[bascenev1lib.game.assault.Player]):
29class Team(bs.Team[Player]):
30    """Our team type for this game."""
31
32    def __init__(self, base_pos: Sequence[float], flag: Flag) -> None:
33
34        #: Where our base is.
35        self.base_pos = base_pos
36
37        #: Flag for this team.
38        self.flag = flag
39
40        #: Current score.
41        self.score = 0

Our team type for this game.

Team(base_pos: Sequence[float], flag: bascenev1lib.actor.flag.Flag)
32    def __init__(self, base_pos: Sequence[float], flag: Flag) -> None:
33
34        #: Where our base is.
35        self.base_pos = base_pos
36
37        #: Flag for this team.
38        self.flag = flag
39
40        #: Current score.
41        self.score = 0
base_pos
flag
score
class AssaultGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.assault.Player, bascenev1lib.game.assault.Team]):
 45class AssaultGame(bs.TeamGameActivity[Player, Team]):
 46    """Game where you score by touching the other team's flag."""
 47
 48    name = 'Assault'
 49    description = 'Reach the enemy flag to score.'
 50    available_settings = [
 51        bs.IntSetting(
 52            'Score to Win',
 53            min_value=1,
 54            default=3,
 55        ),
 56        bs.IntChoiceSetting(
 57            'Time Limit',
 58            choices=[
 59                ('None', 0),
 60                ('1 Minute', 60),
 61                ('2 Minutes', 120),
 62                ('5 Minutes', 300),
 63                ('10 Minutes', 600),
 64                ('20 Minutes', 1200),
 65            ],
 66            default=0,
 67        ),
 68        bs.FloatChoiceSetting(
 69            'Respawn Times',
 70            choices=[
 71                ('Shorter', 0.25),
 72                ('Short', 0.5),
 73                ('Normal', 1.0),
 74                ('Long', 2.0),
 75                ('Longer', 4.0),
 76            ],
 77            default=1.0,
 78        ),
 79        bs.BoolSetting('Epic Mode', default=False),
 80    ]
 81
 82    @override
 83    @classmethod
 84    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 85        return issubclass(sessiontype, bs.DualTeamSession)
 86
 87    @override
 88    @classmethod
 89    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
 90        assert bs.app.classic is not None
 91        return bs.app.classic.getmaps('team_flag')
 92
 93    def __init__(self, settings: dict):
 94        super().__init__(settings)
 95        self._scoreboard = Scoreboard()
 96        self._last_score_time = 0.0
 97        self._score_sound = bs.getsound('score')
 98        self._base_region_materials: dict[int, bs.Material] = {}
 99        self._epic_mode = bool(settings['Epic Mode'])
100        self._score_to_win = int(settings['Score to Win'])
101        self._time_limit = float(settings['Time Limit'])
102
103        # Base class overrides
104        self.slow_motion = self._epic_mode
105        self.default_music = (
106            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
107        )
108
109    @override
110    def get_instance_description(self) -> str | Sequence:
111        if self._score_to_win == 1:
112            return 'Touch the enemy flag.'
113        return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
114
115    @override
116    def get_instance_description_short(self) -> str | Sequence:
117        if self._score_to_win == 1:
118            return 'touch 1 flag'
119        return 'touch ${ARG1} flags', self._score_to_win
120
121    @override
122    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
123        shared = SharedObjects.get()
124        base_pos = self.map.get_flag_position(sessionteam.id)
125        bs.newnode(
126            'light',
127            attrs={
128                'position': base_pos,
129                'intensity': 0.6,
130                'height_attenuated': False,
131                'volume_intensity_scale': 0.1,
132                'radius': 0.1,
133                'color': sessionteam.color,
134            },
135        )
136        Flag.project_stand(base_pos)
137        flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
138        team = Team(base_pos=base_pos, flag=flag)
139
140        mat = self._base_region_materials[sessionteam.id] = bs.Material()
141        mat.add_actions(
142            conditions=('they_have_material', shared.player_material),
143            actions=(
144                ('modify_part_collision', 'collide', True),
145                ('modify_part_collision', 'physical', False),
146                (
147                    'call',
148                    'at_connect',
149                    bs.Call(self._handle_base_collide, team),
150                ),
151            ),
152        )
153
154        bs.newnode(
155            'region',
156            owner=flag.node,
157            attrs={
158                'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
159                'scale': (0.5, 0.5, 0.5),
160                'type': 'sphere',
161                'materials': [self._base_region_materials[sessionteam.id]],
162            },
163        )
164
165        return team
166
167    @override
168    def on_team_join(self, team: Team) -> None:
169        # Can't do this in create_team because the team's color/etc. have
170        # not been wired up yet at that point.
171        self._update_scoreboard()
172
173    @override
174    def on_begin(self) -> None:
175        super().on_begin()
176        self.setup_standard_time_limit(self._time_limit)
177        self.setup_standard_powerup_drops()
178
179    @override
180    def handlemessage(self, msg: Any) -> Any:
181        if isinstance(msg, bs.PlayerDiedMessage):
182            super().handlemessage(msg)  # Augment standard.
183            self.respawn_player(msg.getplayer(Player))
184        else:
185            super().handlemessage(msg)
186
187    def _flash_base(self, team: Team, length: float = 2.0) -> None:
188        light = bs.newnode(
189            'light',
190            attrs={
191                'position': team.base_pos,
192                'height_attenuated': False,
193                'radius': 0.3,
194                'color': team.color,
195            },
196        )
197        bs.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
198        bs.timer(length, light.delete)
199
200    def _handle_base_collide(self, team: Team) -> None:
201        try:
202            spaz = bs.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
203        except bs.NotFoundError:
204            return
205
206        if not spaz.is_alive():
207            return
208
209        try:
210            player = spaz.getplayer(Player, True)
211        except bs.NotFoundError:
212            return
213
214        # If its another team's player, they scored.
215        player_team = player.team
216        if player_team is not team:
217            # Prevent multiple simultaneous scores.
218            if bs.time() != self._last_score_time:
219                self._last_score_time = bs.time()
220                self.stats.player_scored(player, 50, big_message=True)
221                self._score_sound.play()
222                self._flash_base(team)
223
224                # Move all players on the scoring team back to their start
225                # and add flashes of light so its noticeable.
226                for player in player_team.players:
227                    if player.is_alive():
228                        pos = player.node.position
229                        light = bs.newnode(
230                            'light',
231                            attrs={
232                                'position': pos,
233                                'color': player_team.color,
234                                'height_attenuated': False,
235                                'radius': 0.4,
236                            },
237                        )
238                        bs.timer(0.5, light.delete)
239                        bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
240
241                        new_pos = self.map.get_start_position(player_team.id)
242                        light = bs.newnode(
243                            'light',
244                            attrs={
245                                'position': new_pos,
246                                'color': player_team.color,
247                                'radius': 0.4,
248                                'height_attenuated': False,
249                            },
250                        )
251                        bs.timer(0.5, light.delete)
252                        bs.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
253                        if player.actor:
254                            random_num = random.uniform(0, 360)
255
256                            # Slightly hacky workaround: normally,
257                            # teleporting back to base with a sticky
258                            # bomb stuck to you gives a crazy whiplash
259                            # rubber-band effect. Running the teleport
260                            # twice in a row seems to suppress that
261                            # though. Would be better to fix this at a
262                            # lower level, but this works for now.
263                            self._teleport(player, new_pos, random_num)
264                            bs.timer(
265                                0.01,
266                                bs.Call(
267                                    self._teleport, player, new_pos, random_num
268                                ),
269                            )
270
271                # Have teammates celebrate.
272                for player in player_team.players:
273                    if player.actor:
274                        player.actor.handlemessage(bs.CelebrateMessage(2.0))
275
276                player_team.score += 1
277                self._update_scoreboard()
278                if player_team.score >= self._score_to_win:
279                    self.end_game()
280
281    def _teleport(
282        self, client: Player, pos: Sequence[float], num: float
283    ) -> None:
284        if client.actor:
285            client.actor.handlemessage(bs.StandMessage(pos, num))
286
287    @override
288    def end_game(self) -> None:
289        results = bs.GameResults()
290        for team in self.teams:
291            results.set_team_score(team, team.score)
292        self.end(results=results)
293
294    def _update_scoreboard(self) -> None:
295        for team in self.teams:
296            self._scoreboard.set_team_value(
297                team, team.score, self._score_to_win
298            )

Game where you score by touching the other team's flag.

AssaultGame(settings: dict)
 93    def __init__(self, settings: dict):
 94        super().__init__(settings)
 95        self._scoreboard = Scoreboard()
 96        self._last_score_time = 0.0
 97        self._score_sound = bs.getsound('score')
 98        self._base_region_materials: dict[int, bs.Material] = {}
 99        self._epic_mode = bool(settings['Epic Mode'])
100        self._score_to_win = int(settings['Score to Win'])
101        self._time_limit = float(settings['Time Limit'])
102
103        # Base class overrides
104        self.slow_motion = self._epic_mode
105        self.default_music = (
106            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.FORWARD_MARCH
107        )

Instantiate the Activity.

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

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

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
87    @override
88    @classmethod
89    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
90        assert bs.app.classic is not None
91        return bs.app.classic.getmaps('team_flag')

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

slow_motion = False

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

default_music = None
@override
def get_instance_description(self) -> Union[str, Sequence]:
109    @override
110    def get_instance_description(self) -> str | Sequence:
111        if self._score_to_win == 1:
112            return 'Touch the enemy flag.'
113        return 'Touch the enemy flag ${ARG1} times.', self._score_to_win

Return a description for this game instance, in English.

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

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

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

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

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

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

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

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

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
115    @override
116    def get_instance_description_short(self) -> str | Sequence:
117        if self._score_to_win == 1:
118            return 'touch 1 flag'
119        return 'touch ${ARG1} flags', self._score_to_win

Return a short description for this game instance in English.

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

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

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

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

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

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

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

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

@override
def create_team( self, sessionteam: bascenev1.SessionTeam) -> Team:
121    @override
122    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
123        shared = SharedObjects.get()
124        base_pos = self.map.get_flag_position(sessionteam.id)
125        bs.newnode(
126            'light',
127            attrs={
128                'position': base_pos,
129                'intensity': 0.6,
130                'height_attenuated': False,
131                'volume_intensity_scale': 0.1,
132                'radius': 0.1,
133                'color': sessionteam.color,
134            },
135        )
136        Flag.project_stand(base_pos)
137        flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
138        team = Team(base_pos=base_pos, flag=flag)
139
140        mat = self._base_region_materials[sessionteam.id] = bs.Material()
141        mat.add_actions(
142            conditions=('they_have_material', shared.player_material),
143            actions=(
144                ('modify_part_collision', 'collide', True),
145                ('modify_part_collision', 'physical', False),
146                (
147                    'call',
148                    'at_connect',
149                    bs.Call(self._handle_base_collide, team),
150                ),
151            ),
152        )
153
154        bs.newnode(
155            'region',
156            owner=flag.node,
157            attrs={
158                'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
159                'scale': (0.5, 0.5, 0.5),
160                'type': 'sphere',
161                'materials': [self._base_region_materials[sessionteam.id]],
162            },
163        )
164
165        return team

Create the Team instance for this Activity.

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

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

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
173    @override
174    def on_begin(self) -> None:
175        super().on_begin()
176        self.setup_standard_time_limit(self._time_limit)
177        self.setup_standard_powerup_drops()

Called once the previous Activity has finished transitioning out.

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

@override
def handlemessage(self, msg: Any) -> Any:
179    @override
180    def handlemessage(self, msg: Any) -> Any:
181        if isinstance(msg, bs.PlayerDiedMessage):
182            super().handlemessage(msg)  # Augment standard.
183            self.respawn_player(msg.getplayer(Player))
184        else:
185            super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
287    @override
288    def end_game(self) -> None:
289        results = bs.GameResults()
290        for team in self.teams:
291            results.set_team_score(team, team.score)
292        self.end(results=results)

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

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