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

Our player type for this game.

chosen_light: bascenev1.NodeActor | None
class Team(bascenev1._team.Team[bascenev1lib.game.chosenone.Player]):
32class Team(bs.Team[Player]):
33    """Our team type for this game."""
34
35    def __init__(self, time_remaining: int) -> None:
36        self.time_remaining = time_remaining

Our team type for this game.

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

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

Instantiate the Activity.

name = 'Chosen One'
description = 'Be the chosen one for a length of time to win.\nKill the chosen one to become it.'
available_settings = [IntSetting(name='Chosen One Time', default=30, min_value=10, max_value=9999, increment=10), BoolSetting(name='Chosen One Gets Gloves', default=True), BoolSetting(name='Chosen One Gets Shield', default=False), 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)]
scoreconfig = ScoreConfig(label='Time Held', scoretype=<ScoreType.POINTS: 'p'>, lower_is_better=False, none_is_winner=False, version='')
@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
88    @override
89    @classmethod
90    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
91        assert bs.app.classic is not None
92        return bs.app.classic.getmaps('keep_away')

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]:
127    @override
128    def get_instance_description(self) -> str | Sequence:
129        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.

@override
def create_team( self, sessionteam: bascenev1.SessionTeam) -> Team:
131    @override
132    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
133        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.

@override
def on_team_join(self, team: Team) -> None:
135    @override
136    def on_team_join(self, team: Team) -> None:
137        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_player_leave(self, player: Player) -> None:
139    @override
140    def on_player_leave(self, player: Player) -> None:
141        super().on_player_leave(player)
142        if self._get_chosen_one_player() is player:
143            self._set_chosen_one_player(None)

Called when a bascenev1.Player is leaving the Activity.

@override
def on_begin(self) -> None:
145    @override
146    def on_begin(self) -> None:
147        super().on_begin()
148        shared = SharedObjects.get()
149        self.setup_standard_time_limit(self._time_limit)
150        self.setup_standard_powerup_drops()
151        self._flag_spawn_pos = self.map.get_flag_position(None)
152        Flag.project_stand(self._flag_spawn_pos)
153        bs.timer(1.0, call=self._tick, repeat=True)
154
155        mat = self._reset_region_material = bs.Material()
156        mat.add_actions(
157            conditions=(
158                'they_have_material',
159                shared.player_material,
160            ),
161            actions=(
162                ('modify_part_collision', 'collide', True),
163                ('modify_part_collision', 'physical', False),
164                ('call', 'at_connect', bs.WeakCall(self._handle_reset_collide)),
165            ),
166        )
167
168        self._set_chosen_one_player(None)

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 end_game(self) -> None:
262    @override
263    def end_game(self) -> None:
264        results = bs.GameResults()
265        for team in self.teams:
266            results.set_team_score(
267                team, self._chosen_one_time - team.time_remaining
268            )
269        self.end(results=results, announce_delay=0)

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.

@override
def handlemessage(self, msg: Any) -> Any:
347    @override
348    def handlemessage(self, msg: Any) -> Any:
349        if isinstance(msg, bs.PlayerDiedMessage):
350            # Augment standard behavior.
351            super().handlemessage(msg)
352            player = msg.getplayer(Player)
353            if player is self._get_chosen_one_player():
354                killerplayer = msg.getkillerplayer(Player)
355                self._set_chosen_one_player(
356                    None
357                    if (
358                        killerplayer is None
359                        or killerplayer is player
360                        or not killerplayer.is_alive()
361                    )
362                    else killerplayer
363                )
364            self.respawn_player(player)
365        else:
366            super().handlemessage(msg)

General message handling; can be passed any message object.