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

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

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

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:
76    @override
77    @classmethod
78    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
79        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]:
81    @override
82    @classmethod
83    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
84        assert bs.app.classic is not None
85        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]:
103    @override
104    def get_instance_description(self) -> str | Sequence:
105        if self._score_to_win == 1:
106            return 'Touch the enemy flag.'
107        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]:
109    @override
110    def get_instance_description_short(self) -> str | Sequence:
111        if self._score_to_win == 1:
112            return 'touch 1 flag'
113        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:
115    @override
116    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
117        shared = SharedObjects.get()
118        base_pos = self.map.get_flag_position(sessionteam.id)
119        bs.newnode(
120            'light',
121            attrs={
122                'position': base_pos,
123                'intensity': 0.6,
124                'height_attenuated': False,
125                'volume_intensity_scale': 0.1,
126                'radius': 0.1,
127                'color': sessionteam.color,
128            },
129        )
130        Flag.project_stand(base_pos)
131        flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
132        team = Team(base_pos=base_pos, flag=flag)
133
134        mat = self._base_region_materials[sessionteam.id] = bs.Material()
135        mat.add_actions(
136            conditions=('they_have_material', shared.player_material),
137            actions=(
138                ('modify_part_collision', 'collide', True),
139                ('modify_part_collision', 'physical', False),
140                (
141                    'call',
142                    'at_connect',
143                    bs.Call(self._handle_base_collide, team),
144                ),
145            ),
146        )
147
148        bs.newnode(
149            'region',
150            owner=flag.node,
151            attrs={
152                'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
153                'scale': (0.5, 0.5, 0.5),
154                'type': 'sphere',
155                'materials': [self._base_region_materials[sessionteam.id]],
156            },
157        )
158
159        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:
161    @override
162    def on_team_join(self, team: Team) -> None:
163        # Can't do this in create_team because the team's color/etc. have
164        # not been wired up yet at that point.
165        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
167    @override
168    def on_begin(self) -> None:
169        super().on_begin()
170        self.setup_standard_time_limit(self._time_limit)
171        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:
173    @override
174    def handlemessage(self, msg: Any) -> Any:
175        if isinstance(msg, bs.PlayerDiedMessage):
176            super().handlemessage(msg)  # Augment standard.
177            self.respawn_player(msg.getplayer(Player))
178        else:
179            super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
281    @override
282    def end_game(self) -> None:
283        results = bs.GameResults()
284        for team in self.teams:
285            results.set_team_score(team, team.score)
286        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.