bastd.game.chosenone

Provides the chosen-one mini-game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides the chosen-one mini-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.flag import Flag
 14from bastd.actor.playerspaz import PlayerSpaz
 15from bastd.actor.scoreboard import Scoreboard
 16from bastd.gameutils import SharedObjects
 17
 18if TYPE_CHECKING:
 19    from typing import Any, Sequence
 20
 21
 22class Player(ba.Player['Team']):
 23    """Our player type for this game."""
 24
 25    def __init__(self) -> None:
 26        self.chosen_light: ba.NodeActor | None = None
 27
 28
 29class Team(ba.Team[Player]):
 30    """Our team type for this game."""
 31
 32    def __init__(self, time_remaining: int) -> None:
 33        self.time_remaining = time_remaining
 34
 35
 36# ba_meta export game
 37class ChosenOneGame(ba.TeamGameActivity[Player, Team]):
 38    """
 39    Game involving trying to remain the one 'chosen one'
 40    for a set length of time while everyone else tries to
 41    kill you and become the chosen one themselves.
 42    """
 43
 44    name = 'Chosen One'
 45    description = (
 46        'Be the chosen one for a length of time to win.\n'
 47        'Kill the chosen one to become it.'
 48    )
 49    available_settings = [
 50        ba.IntSetting(
 51            'Chosen One Time',
 52            min_value=10,
 53            default=30,
 54            increment=10,
 55        ),
 56        ba.BoolSetting('Chosen One Gets Gloves', default=True),
 57        ba.BoolSetting('Chosen One Gets Shield', default=False),
 58        ba.IntChoiceSetting(
 59            'Time Limit',
 60            choices=[
 61                ('None', 0),
 62                ('1 Minute', 60),
 63                ('2 Minutes', 120),
 64                ('5 Minutes', 300),
 65                ('10 Minutes', 600),
 66                ('20 Minutes', 1200),
 67            ],
 68            default=0,
 69        ),
 70        ba.FloatChoiceSetting(
 71            'Respawn Times',
 72            choices=[
 73                ('Shorter', 0.25),
 74                ('Short', 0.5),
 75                ('Normal', 1.0),
 76                ('Long', 2.0),
 77                ('Longer', 4.0),
 78            ],
 79            default=1.0,
 80        ),
 81        ba.BoolSetting('Epic Mode', default=False),
 82    ]
 83    scoreconfig = ba.ScoreConfig(label='Time Held')
 84
 85    @classmethod
 86    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
 87        return ba.getmaps('keep_away')
 88
 89    def __init__(self, settings: dict):
 90        super().__init__(settings)
 91        self._scoreboard = Scoreboard()
 92        self._chosen_one_player: Player | None = None
 93        self._swipsound = ba.getsound('swip')
 94        self._countdownsounds: dict[int, ba.Sound] = {
 95            10: ba.getsound('announceTen'),
 96            9: ba.getsound('announceNine'),
 97            8: ba.getsound('announceEight'),
 98            7: ba.getsound('announceSeven'),
 99            6: ba.getsound('announceSix'),
100            5: ba.getsound('announceFive'),
101            4: ba.getsound('announceFour'),
102            3: ba.getsound('announceThree'),
103            2: ba.getsound('announceTwo'),
104            1: ba.getsound('announceOne'),
105        }
106        self._flag_spawn_pos: Sequence[float] | None = None
107        self._reset_region_material: ba.Material | None = None
108        self._flag: Flag | None = None
109        self._reset_region: ba.Node | None = None
110        self._epic_mode = bool(settings['Epic Mode'])
111        self._chosen_one_time = int(settings['Chosen One Time'])
112        self._time_limit = float(settings['Time Limit'])
113        self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield'])
114        self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves'])
115
116        # Base class overrides
117        self.slow_motion = self._epic_mode
118        self.default_music = (
119            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.CHOSEN_ONE
120        )
121
122    def get_instance_description(self) -> str | Sequence:
123        return 'There can be only one.'
124
125    def create_team(self, sessionteam: ba.SessionTeam) -> Team:
126        return Team(time_remaining=self._chosen_one_time)
127
128    def on_team_join(self, team: Team) -> None:
129        self._update_scoreboard()
130
131    def on_player_leave(self, player: Player) -> None:
132        super().on_player_leave(player)
133        if self._get_chosen_one_player() is player:
134            self._set_chosen_one_player(None)
135
136    def on_begin(self) -> None:
137        super().on_begin()
138        shared = SharedObjects.get()
139        self.setup_standard_time_limit(self._time_limit)
140        self.setup_standard_powerup_drops()
141        self._flag_spawn_pos = self.map.get_flag_position(None)
142        Flag.project_stand(self._flag_spawn_pos)
143        ba.timer(1.0, call=self._tick, repeat=True)
144
145        mat = self._reset_region_material = ba.Material()
146        mat.add_actions(
147            conditions=(
148                'they_have_material',
149                shared.player_material,
150            ),
151            actions=(
152                ('modify_part_collision', 'collide', True),
153                ('modify_part_collision', 'physical', False),
154                ('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)),
155            ),
156        )
157
158        self._set_chosen_one_player(None)
159
160    def _create_reset_region(self) -> None:
161        assert self._reset_region_material is not None
162        assert self._flag_spawn_pos is not None
163        pos = self._flag_spawn_pos
164        self._reset_region = ba.newnode(
165            'region',
166            attrs={
167                'position': (pos[0], pos[1] + 0.75, pos[2]),
168                'scale': (0.5, 0.5, 0.5),
169                'type': 'sphere',
170                'materials': [self._reset_region_material],
171            },
172        )
173
174    def _get_chosen_one_player(self) -> Player | None:
175        # Should never return invalid references; return None in that case.
176        if self._chosen_one_player:
177            return self._chosen_one_player
178        return None
179
180    def _handle_reset_collide(self) -> None:
181        # If we have a chosen one, ignore these.
182        if self._get_chosen_one_player() is not None:
183            return
184
185        # Attempt to get a Actor that we hit.
186        try:
187            spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
188            player = spaz.getplayer(Player, True)
189        except ba.NotFoundError:
190            return
191
192        if spaz.is_alive():
193            self._set_chosen_one_player(player)
194
195    def _flash_flag_spawn(self) -> None:
196        light = ba.newnode(
197            'light',
198            attrs={
199                'position': self._flag_spawn_pos,
200                'color': (1, 1, 1),
201                'radius': 0.3,
202                'height_attenuated': False,
203            },
204        )
205        ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
206        ba.timer(1.0, light.delete)
207
208    def _tick(self) -> None:
209
210        # Give the chosen one points.
211        player = self._get_chosen_one_player()
212        if player is not None:
213
214            # This shouldn't happen, but just in case.
215            if not player.is_alive():
216                ba.print_error('got dead player as chosen one in _tick')
217                self._set_chosen_one_player(None)
218            else:
219                scoring_team = player.team
220                assert self.stats
221                self.stats.player_scored(
222                    player, 3, screenmessage=False, display=False
223                )
224
225                scoring_team.time_remaining = max(
226                    0, scoring_team.time_remaining - 1
227                )
228
229                # Show the count over their head
230                if scoring_team.time_remaining > 0:
231                    if isinstance(player.actor, PlayerSpaz) and player.actor:
232                        player.actor.set_score_text(
233                            str(scoring_team.time_remaining)
234                        )
235
236                self._update_scoreboard()
237
238                # announce numbers we have sounds for
239                if scoring_team.time_remaining in self._countdownsounds:
240                    ba.playsound(
241                        self._countdownsounds[scoring_team.time_remaining]
242                    )
243
244                # Winner!
245                if scoring_team.time_remaining <= 0:
246                    self.end_game()
247
248        else:
249            # (player is None)
250            # This shouldn't happen, but just in case.
251            # (Chosen-one player ceasing to exist should
252            # trigger on_player_leave which resets chosen-one)
253            if self._chosen_one_player is not None:
254                ba.print_error('got nonexistent player as chosen one in _tick')
255                self._set_chosen_one_player(None)
256
257    def end_game(self) -> None:
258        results = ba.GameResults()
259        for team in self.teams:
260            results.set_team_score(
261                team, self._chosen_one_time - team.time_remaining
262            )
263        self.end(results=results, announce_delay=0)
264
265    def _set_chosen_one_player(self, player: Player | None) -> None:
266        existing = self._get_chosen_one_player()
267        if existing:
268            existing.chosen_light = None
269        ba.playsound(self._swipsound)
270        if not player:
271            assert self._flag_spawn_pos is not None
272            self._flag = Flag(
273                color=(1, 0.9, 0.2),
274                position=self._flag_spawn_pos,
275                touchable=False,
276            )
277            self._chosen_one_player = None
278
279            # Create a light to highlight the flag;
280            # this will go away when the flag dies.
281            ba.newnode(
282                'light',
283                owner=self._flag.node,
284                attrs={
285                    'position': self._flag_spawn_pos,
286                    'intensity': 0.6,
287                    'height_attenuated': False,
288                    'volume_intensity_scale': 0.1,
289                    'radius': 0.1,
290                    'color': (1.2, 1.2, 0.4),
291                },
292            )
293
294            # Also an extra momentary flash.
295            self._flash_flag_spawn()
296
297            # Re-create our flag region in case if someone is waiting for
298            # flag right there:
299            self._create_reset_region()
300        else:
301            if player.actor:
302                self._flag = None
303                self._chosen_one_player = player
304
305                if self._chosen_one_gets_shield:
306                    player.actor.handlemessage(ba.PowerupMessage('shield'))
307                if self._chosen_one_gets_gloves:
308                    player.actor.handlemessage(ba.PowerupMessage('punch'))
309
310                # Use a color that's partway between their team color
311                # and white.
312                color = [
313                    0.3 + c * 0.7
314                    for c in ba.normalized_color(player.team.color)
315                ]
316                light = player.chosen_light = ba.NodeActor(
317                    ba.newnode(
318                        'light',
319                        attrs={
320                            'intensity': 0.6,
321                            'height_attenuated': False,
322                            'volume_intensity_scale': 0.1,
323                            'radius': 0.13,
324                            'color': color,
325                        },
326                    )
327                )
328
329                assert light.node
330                ba.animate(
331                    light.node,
332                    'intensity',
333                    {0: 1.0, 0.2: 0.4, 0.4: 1.0},
334                    loop=True,
335                )
336                assert isinstance(player.actor, PlayerSpaz)
337                player.actor.node.connectattr(
338                    'position', light.node, 'position'
339                )
340
341    def handlemessage(self, msg: Any) -> Any:
342        if isinstance(msg, ba.PlayerDiedMessage):
343            # Augment standard behavior.
344            super().handlemessage(msg)
345            player = msg.getplayer(Player)
346            if player is self._get_chosen_one_player():
347                killerplayer = msg.getkillerplayer(Player)
348                self._set_chosen_one_player(
349                    None
350                    if (
351                        killerplayer is None
352                        or killerplayer is player
353                        or not killerplayer.is_alive()
354                    )
355                    else killerplayer
356                )
357            self.respawn_player(player)
358        else:
359            super().handlemessage(msg)
360
361    def _update_scoreboard(self) -> None:
362        for team in self.teams:
363            self._scoreboard.set_team_value(
364                team, team.time_remaining, self._chosen_one_time, countdown=True
365            )
class Player(ba._player.Player[ForwardRef('Team')]):
23class Player(ba.Player['Team']):
24    """Our player type for this game."""
25
26    def __init__(self) -> None:
27        self.chosen_light: ba.NodeActor | None = None

Our player type for this game.

Player()
26    def __init__(self) -> None:
27        self.chosen_light: ba.NodeActor | None = None
Inherited Members
ba._player.Player
actor
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(ba._team.Team[bastd.game.chosenone.Player]):
30class Team(ba.Team[Player]):
31    """Our team type for this game."""
32
33    def __init__(self, time_remaining: int) -> None:
34        self.time_remaining = time_remaining

Our team type for this game.

Team(time_remaining: int)
33    def __init__(self, time_remaining: int) -> None:
34        self.time_remaining = time_remaining
Inherited Members
ba._team.Team
manual_init
customdata
on_expire
sessionteam
class ChosenOneGame(ba._teamgame.TeamGameActivity[bastd.game.chosenone.Player, bastd.game.chosenone.Team]):
 38class ChosenOneGame(ba.TeamGameActivity[Player, Team]):
 39    """
 40    Game involving trying to remain the one 'chosen one'
 41    for a set length of time while everyone else tries to
 42    kill you and become the chosen one themselves.
 43    """
 44
 45    name = 'Chosen One'
 46    description = (
 47        'Be the chosen one for a length of time to win.\n'
 48        'Kill the chosen one to become it.'
 49    )
 50    available_settings = [
 51        ba.IntSetting(
 52            'Chosen One Time',
 53            min_value=10,
 54            default=30,
 55            increment=10,
 56        ),
 57        ba.BoolSetting('Chosen One Gets Gloves', default=True),
 58        ba.BoolSetting('Chosen One Gets Shield', default=False),
 59        ba.IntChoiceSetting(
 60            'Time Limit',
 61            choices=[
 62                ('None', 0),
 63                ('1 Minute', 60),
 64                ('2 Minutes', 120),
 65                ('5 Minutes', 300),
 66                ('10 Minutes', 600),
 67                ('20 Minutes', 1200),
 68            ],
 69            default=0,
 70        ),
 71        ba.FloatChoiceSetting(
 72            'Respawn Times',
 73            choices=[
 74                ('Shorter', 0.25),
 75                ('Short', 0.5),
 76                ('Normal', 1.0),
 77                ('Long', 2.0),
 78                ('Longer', 4.0),
 79            ],
 80            default=1.0,
 81        ),
 82        ba.BoolSetting('Epic Mode', default=False),
 83    ]
 84    scoreconfig = ba.ScoreConfig(label='Time Held')
 85
 86    @classmethod
 87    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
 88        return ba.getmaps('keep_away')
 89
 90    def __init__(self, settings: dict):
 91        super().__init__(settings)
 92        self._scoreboard = Scoreboard()
 93        self._chosen_one_player: Player | None = None
 94        self._swipsound = ba.getsound('swip')
 95        self._countdownsounds: dict[int, ba.Sound] = {
 96            10: ba.getsound('announceTen'),
 97            9: ba.getsound('announceNine'),
 98            8: ba.getsound('announceEight'),
 99            7: ba.getsound('announceSeven'),
100            6: ba.getsound('announceSix'),
101            5: ba.getsound('announceFive'),
102            4: ba.getsound('announceFour'),
103            3: ba.getsound('announceThree'),
104            2: ba.getsound('announceTwo'),
105            1: ba.getsound('announceOne'),
106        }
107        self._flag_spawn_pos: Sequence[float] | None = None
108        self._reset_region_material: ba.Material | None = None
109        self._flag: Flag | None = None
110        self._reset_region: ba.Node | None = None
111        self._epic_mode = bool(settings['Epic Mode'])
112        self._chosen_one_time = int(settings['Chosen One Time'])
113        self._time_limit = float(settings['Time Limit'])
114        self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield'])
115        self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves'])
116
117        # Base class overrides
118        self.slow_motion = self._epic_mode
119        self.default_music = (
120            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.CHOSEN_ONE
121        )
122
123    def get_instance_description(self) -> str | Sequence:
124        return 'There can be only one.'
125
126    def create_team(self, sessionteam: ba.SessionTeam) -> Team:
127        return Team(time_remaining=self._chosen_one_time)
128
129    def on_team_join(self, team: Team) -> None:
130        self._update_scoreboard()
131
132    def on_player_leave(self, player: Player) -> None:
133        super().on_player_leave(player)
134        if self._get_chosen_one_player() is player:
135            self._set_chosen_one_player(None)
136
137    def on_begin(self) -> None:
138        super().on_begin()
139        shared = SharedObjects.get()
140        self.setup_standard_time_limit(self._time_limit)
141        self.setup_standard_powerup_drops()
142        self._flag_spawn_pos = self.map.get_flag_position(None)
143        Flag.project_stand(self._flag_spawn_pos)
144        ba.timer(1.0, call=self._tick, repeat=True)
145
146        mat = self._reset_region_material = ba.Material()
147        mat.add_actions(
148            conditions=(
149                'they_have_material',
150                shared.player_material,
151            ),
152            actions=(
153                ('modify_part_collision', 'collide', True),
154                ('modify_part_collision', 'physical', False),
155                ('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)),
156            ),
157        )
158
159        self._set_chosen_one_player(None)
160
161    def _create_reset_region(self) -> None:
162        assert self._reset_region_material is not None
163        assert self._flag_spawn_pos is not None
164        pos = self._flag_spawn_pos
165        self._reset_region = ba.newnode(
166            'region',
167            attrs={
168                'position': (pos[0], pos[1] + 0.75, pos[2]),
169                'scale': (0.5, 0.5, 0.5),
170                'type': 'sphere',
171                'materials': [self._reset_region_material],
172            },
173        )
174
175    def _get_chosen_one_player(self) -> Player | None:
176        # Should never return invalid references; return None in that case.
177        if self._chosen_one_player:
178            return self._chosen_one_player
179        return None
180
181    def _handle_reset_collide(self) -> None:
182        # If we have a chosen one, ignore these.
183        if self._get_chosen_one_player() is not None:
184            return
185
186        # Attempt to get a Actor that we hit.
187        try:
188            spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
189            player = spaz.getplayer(Player, True)
190        except ba.NotFoundError:
191            return
192
193        if spaz.is_alive():
194            self._set_chosen_one_player(player)
195
196    def _flash_flag_spawn(self) -> None:
197        light = ba.newnode(
198            'light',
199            attrs={
200                'position': self._flag_spawn_pos,
201                'color': (1, 1, 1),
202                'radius': 0.3,
203                'height_attenuated': False,
204            },
205        )
206        ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
207        ba.timer(1.0, light.delete)
208
209    def _tick(self) -> None:
210
211        # Give the chosen one points.
212        player = self._get_chosen_one_player()
213        if player is not None:
214
215            # This shouldn't happen, but just in case.
216            if not player.is_alive():
217                ba.print_error('got dead player as chosen one in _tick')
218                self._set_chosen_one_player(None)
219            else:
220                scoring_team = player.team
221                assert self.stats
222                self.stats.player_scored(
223                    player, 3, screenmessage=False, display=False
224                )
225
226                scoring_team.time_remaining = max(
227                    0, scoring_team.time_remaining - 1
228                )
229
230                # Show the count over their head
231                if scoring_team.time_remaining > 0:
232                    if isinstance(player.actor, PlayerSpaz) and player.actor:
233                        player.actor.set_score_text(
234                            str(scoring_team.time_remaining)
235                        )
236
237                self._update_scoreboard()
238
239                # announce numbers we have sounds for
240                if scoring_team.time_remaining in self._countdownsounds:
241                    ba.playsound(
242                        self._countdownsounds[scoring_team.time_remaining]
243                    )
244
245                # Winner!
246                if scoring_team.time_remaining <= 0:
247                    self.end_game()
248
249        else:
250            # (player is None)
251            # This shouldn't happen, but just in case.
252            # (Chosen-one player ceasing to exist should
253            # trigger on_player_leave which resets chosen-one)
254            if self._chosen_one_player is not None:
255                ba.print_error('got nonexistent player as chosen one in _tick')
256                self._set_chosen_one_player(None)
257
258    def end_game(self) -> None:
259        results = ba.GameResults()
260        for team in self.teams:
261            results.set_team_score(
262                team, self._chosen_one_time - team.time_remaining
263            )
264        self.end(results=results, announce_delay=0)
265
266    def _set_chosen_one_player(self, player: Player | None) -> None:
267        existing = self._get_chosen_one_player()
268        if existing:
269            existing.chosen_light = None
270        ba.playsound(self._swipsound)
271        if not player:
272            assert self._flag_spawn_pos is not None
273            self._flag = Flag(
274                color=(1, 0.9, 0.2),
275                position=self._flag_spawn_pos,
276                touchable=False,
277            )
278            self._chosen_one_player = None
279
280            # Create a light to highlight the flag;
281            # this will go away when the flag dies.
282            ba.newnode(
283                'light',
284                owner=self._flag.node,
285                attrs={
286                    'position': self._flag_spawn_pos,
287                    'intensity': 0.6,
288                    'height_attenuated': False,
289                    'volume_intensity_scale': 0.1,
290                    'radius': 0.1,
291                    'color': (1.2, 1.2, 0.4),
292                },
293            )
294
295            # Also an extra momentary flash.
296            self._flash_flag_spawn()
297
298            # Re-create our flag region in case if someone is waiting for
299            # flag right there:
300            self._create_reset_region()
301        else:
302            if player.actor:
303                self._flag = None
304                self._chosen_one_player = player
305
306                if self._chosen_one_gets_shield:
307                    player.actor.handlemessage(ba.PowerupMessage('shield'))
308                if self._chosen_one_gets_gloves:
309                    player.actor.handlemessage(ba.PowerupMessage('punch'))
310
311                # Use a color that's partway between their team color
312                # and white.
313                color = [
314                    0.3 + c * 0.7
315                    for c in ba.normalized_color(player.team.color)
316                ]
317                light = player.chosen_light = ba.NodeActor(
318                    ba.newnode(
319                        'light',
320                        attrs={
321                            'intensity': 0.6,
322                            'height_attenuated': False,
323                            'volume_intensity_scale': 0.1,
324                            'radius': 0.13,
325                            'color': color,
326                        },
327                    )
328                )
329
330                assert light.node
331                ba.animate(
332                    light.node,
333                    'intensity',
334                    {0: 1.0, 0.2: 0.4, 0.4: 1.0},
335                    loop=True,
336                )
337                assert isinstance(player.actor, PlayerSpaz)
338                player.actor.node.connectattr(
339                    'position', light.node, 'position'
340                )
341
342    def handlemessage(self, msg: Any) -> Any:
343        if isinstance(msg, ba.PlayerDiedMessage):
344            # Augment standard behavior.
345            super().handlemessage(msg)
346            player = msg.getplayer(Player)
347            if player is self._get_chosen_one_player():
348                killerplayer = msg.getkillerplayer(Player)
349                self._set_chosen_one_player(
350                    None
351                    if (
352                        killerplayer is None
353                        or killerplayer is player
354                        or not killerplayer.is_alive()
355                    )
356                    else killerplayer
357                )
358            self.respawn_player(player)
359        else:
360            super().handlemessage(msg)
361
362    def _update_scoreboard(self) -> None:
363        for team in self.teams:
364            self._scoreboard.set_team_value(
365                team, team.time_remaining, self._chosen_one_time, countdown=True
366            )

Game involving trying to remain the one 'chosen one' for a set length of time while everyone else tries to kill you and become the chosen one themselves.

ChosenOneGame(settings: dict)
 90    def __init__(self, settings: dict):
 91        super().__init__(settings)
 92        self._scoreboard = Scoreboard()
 93        self._chosen_one_player: Player | None = None
 94        self._swipsound = ba.getsound('swip')
 95        self._countdownsounds: dict[int, ba.Sound] = {
 96            10: ba.getsound('announceTen'),
 97            9: ba.getsound('announceNine'),
 98            8: ba.getsound('announceEight'),
 99            7: ba.getsound('announceSeven'),
100            6: ba.getsound('announceSix'),
101            5: ba.getsound('announceFive'),
102            4: ba.getsound('announceFour'),
103            3: ba.getsound('announceThree'),
104            2: ba.getsound('announceTwo'),
105            1: ba.getsound('announceOne'),
106        }
107        self._flag_spawn_pos: Sequence[float] | None = None
108        self._reset_region_material: ba.Material | None = None
109        self._flag: Flag | None = None
110        self._reset_region: ba.Node | None = None
111        self._epic_mode = bool(settings['Epic Mode'])
112        self._chosen_one_time = int(settings['Chosen One Time'])
113        self._time_limit = float(settings['Time Limit'])
114        self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield'])
115        self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves'])
116
117        # Base class overrides
118        self.slow_motion = self._epic_mode
119        self.default_music = (
120            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.CHOSEN_ONE
121        )

Instantiate the Activity.

@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
86    @classmethod
87    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
88        return ba.getmaps('keep_away')

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

slow_motion = False

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

def get_instance_description(self) -> Union[str, Sequence]:
123    def get_instance_description(self) -> str | Sequence:
124        return 'There can be only one.'

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 create_team(self, sessionteam: ba._team.SessionTeam) -> bastd.game.chosenone.Team:
126    def create_team(self, sessionteam: ba.SessionTeam) -> Team:
127        return Team(time_remaining=self._chosen_one_time)

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.chosenone.Team) -> None:
129    def on_team_join(self, team: Team) -> None:
130        self._update_scoreboard()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def on_player_leave(self, player: bastd.game.chosenone.Player) -> None:
132    def on_player_leave(self, player: Player) -> None:
133        super().on_player_leave(player)
134        if self._get_chosen_one_player() is player:
135            self._set_chosen_one_player(None)

Called when a ba.Player is leaving the Activity.

def on_begin(self) -> None:
137    def on_begin(self) -> None:
138        super().on_begin()
139        shared = SharedObjects.get()
140        self.setup_standard_time_limit(self._time_limit)
141        self.setup_standard_powerup_drops()
142        self._flag_spawn_pos = self.map.get_flag_position(None)
143        Flag.project_stand(self._flag_spawn_pos)
144        ba.timer(1.0, call=self._tick, repeat=True)
145
146        mat = self._reset_region_material = ba.Material()
147        mat.add_actions(
148            conditions=(
149                'they_have_material',
150                shared.player_material,
151            ),
152            actions=(
153                ('modify_part_collision', 'collide', True),
154                ('modify_part_collision', 'physical', False),
155                ('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)),
156            ),
157        )
158
159        self._set_chosen_one_player(None)

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:
258    def end_game(self) -> None:
259        results = ba.GameResults()
260        for team in self.teams:
261            results.set_team_score(
262                team, self._chosen_one_time - team.time_remaining
263            )
264        self.end(results=results, announce_delay=0)

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 handlemessage(self, msg: Any) -> Any:
342    def handlemessage(self, msg: Any) -> Any:
343        if isinstance(msg, ba.PlayerDiedMessage):
344            # Augment standard behavior.
345            super().handlemessage(msg)
346            player = msg.getplayer(Player)
347            if player is self._get_chosen_one_player():
348                killerplayer = msg.getkillerplayer(Player)
349                self._set_chosen_one_player(
350                    None
351                    if (
352                        killerplayer is None
353                        or killerplayer is player
354                        or not killerplayer.is_alive()
355                    )
356                    else killerplayer
357                )
358            self.respawn_player(player)
359        else:
360            super().handlemessage(msg)

General message handling; can be passed any message object.

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