bascenev1lib.game.keepaway

Defines a keep-away game type.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Defines a keep-away game type."""
  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 enum import Enum
 12from typing import TYPE_CHECKING, override
 13
 14import bascenev1 as bs
 15
 16from bascenev1lib.actor.playerspaz import PlayerSpaz
 17from bascenev1lib.actor.scoreboard import Scoreboard
 18from bascenev1lib.actor.flag import (
 19    Flag,
 20    FlagDroppedMessage,
 21    FlagDiedMessage,
 22    FlagPickedUpMessage,
 23)
 24
 25if TYPE_CHECKING:
 26    from typing import Any, Sequence
 27
 28
 29class FlagState(Enum):
 30    """States our single flag can be in."""
 31
 32    NEW = 0
 33    UNCONTESTED = 1
 34    CONTESTED = 2
 35    HELD = 3
 36
 37
 38class Player(bs.Player['Team']):
 39    """Our player type for this game."""
 40
 41
 42class Team(bs.Team[Player]):
 43    """Our team type for this game."""
 44
 45    def __init__(self, timeremaining: int) -> None:
 46        self.timeremaining = timeremaining
 47        self.holdingflag = False
 48
 49
 50# ba_meta export bascenev1.GameActivity
 51class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
 52    """Game where you try to keep the flag away from your enemies."""
 53
 54    name = 'Keep Away'
 55    description = 'Carry the flag for a set length of time.'
 56    available_settings = [
 57        bs.IntSetting(
 58            'Hold Time',
 59            min_value=10,
 60            default=30,
 61            increment=10,
 62        ),
 63        bs.IntChoiceSetting(
 64            'Time Limit',
 65            choices=[
 66                ('None', 0),
 67                ('1 Minute', 60),
 68                ('2 Minutes', 120),
 69                ('5 Minutes', 300),
 70                ('10 Minutes', 600),
 71                ('20 Minutes', 1200),
 72            ],
 73            default=0,
 74        ),
 75        bs.FloatChoiceSetting(
 76            'Respawn Times',
 77            choices=[
 78                ('Shorter', 0.25),
 79                ('Short', 0.5),
 80                ('Normal', 1.0),
 81                ('Long', 2.0),
 82                ('Longer', 4.0),
 83            ],
 84            default=1.0,
 85        ),
 86        bs.BoolSetting('Epic Mode', default=False),
 87    ]
 88    scoreconfig = bs.ScoreConfig(label='Time Held')
 89
 90    @override
 91    @classmethod
 92    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 93        return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
 94            sessiontype, bs.FreeForAllSession
 95        )
 96
 97    @override
 98    @classmethod
 99    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
100        assert bs.app.classic is not None
101        return bs.app.classic.getmaps('keep_away')
102
103    def __init__(self, settings: dict):
104        super().__init__(settings)
105        self._scoreboard = Scoreboard()
106        self._swipsound = bs.getsound('swip')
107        self._tick_sound = bs.getsound('tick')
108        self._countdownsounds = {
109            10: bs.getsound('announceTen'),
110            9: bs.getsound('announceNine'),
111            8: bs.getsound('announceEight'),
112            7: bs.getsound('announceSeven'),
113            6: bs.getsound('announceSix'),
114            5: bs.getsound('announceFive'),
115            4: bs.getsound('announceFour'),
116            3: bs.getsound('announceThree'),
117            2: bs.getsound('announceTwo'),
118            1: bs.getsound('announceOne'),
119        }
120        self._flag_spawn_pos: Sequence[float] | None = None
121        self._update_timer: bs.Timer | None = None
122        self._holding_players: list[Player] = []
123        self._flag_state: FlagState | None = None
124        self._flag_light: bs.Node | None = None
125        self._scoring_team: Team | None = None
126        self._flag: Flag | None = None
127        self._hold_time = int(settings['Hold Time'])
128        self._time_limit = float(settings['Time Limit'])
129        self._epic_mode = bool(settings['Epic Mode'])
130        self.slow_motion = self._epic_mode
131        self.default_music = (
132            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.KEEP_AWAY
133        )
134
135    @override
136    def get_instance_description(self) -> str | Sequence:
137        return 'Carry the flag for ${ARG1} seconds.', self._hold_time
138
139    @override
140    def get_instance_description_short(self) -> str | Sequence:
141        return 'carry the flag for ${ARG1} seconds', self._hold_time
142
143    @override
144    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
145        return Team(timeremaining=self._hold_time)
146
147    @override
148    def on_team_join(self, team: Team) -> None:
149        self._update_scoreboard()
150
151    @override
152    def on_begin(self) -> None:
153        super().on_begin()
154        self.setup_standard_time_limit(self._time_limit)
155        self.setup_standard_powerup_drops()
156        self._flag_spawn_pos = self.map.get_flag_position(None)
157        self._spawn_flag()
158        self._update_timer = bs.Timer(1.0, call=self._tick, repeat=True)
159        self._update_flag_state()
160        Flag.project_stand(self._flag_spawn_pos)
161
162    def _tick(self) -> None:
163        self._update_flag_state()
164
165        # Award points to all living players holding the flag.
166        for player in self._holding_players:
167            if player:
168                self.stats.player_scored(
169                    player, 3, screenmessage=False, display=False
170                )
171
172        scoreteam = self._scoring_team
173
174        if scoreteam is not None:
175            if scoreteam.timeremaining > 0:
176                self._tick_sound.play()
177
178            scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1)
179            self._update_scoreboard()
180            if scoreteam.timeremaining > 0:
181                assert self._flag is not None
182                self._flag.set_score_text(str(scoreteam.timeremaining))
183
184            # Announce numbers we have sounds for.
185            if scoreteam.timeremaining in self._countdownsounds:
186                self._countdownsounds[scoreteam.timeremaining].play()
187
188            # Winner.
189            if scoreteam.timeremaining <= 0:
190                self.end_game()
191
192    @override
193    def end_game(self) -> None:
194        results = bs.GameResults()
195        for team in self.teams:
196            results.set_team_score(team, self._hold_time - team.timeremaining)
197        self.end(results=results, announce_delay=0)
198
199    def _update_flag_state(self) -> None:
200        for team in self.teams:
201            team.holdingflag = False
202        self._holding_players = []
203        for player in self.players:
204            holdingflag = False
205            try:
206                assert isinstance(player.actor, (PlayerSpaz, type(None)))
207                if (
208                    player.actor
209                    and player.actor.node
210                    and player.actor.node.hold_node
211                ):
212                    holdingflag = (
213                        player.actor.node.hold_node.getnodetype() == 'flag'
214                    )
215            except Exception:
216                logging.exception('Error checking hold flag.')
217            if holdingflag:
218                self._holding_players.append(player)
219                player.team.holdingflag = True
220
221        holdingteams = set(t for t in self.teams if t.holdingflag)
222        prevstate = self._flag_state
223        assert self._flag is not None
224        assert self._flag_light
225        assert self._flag.node
226        if len(holdingteams) > 1:
227            self._flag_state = FlagState.CONTESTED
228            self._scoring_team = None
229            self._flag_light.color = (0.6, 0.6, 0.1)
230            self._flag.node.color = (1.0, 1.0, 0.4)
231        elif len(holdingteams) == 1:
232            holdingteam = list(holdingteams)[0]
233            self._flag_state = FlagState.HELD
234            self._scoring_team = holdingteam
235            self._flag_light.color = bs.normalized_color(holdingteam.color)
236            self._flag.node.color = holdingteam.color
237        else:
238            self._flag_state = FlagState.UNCONTESTED
239            self._scoring_team = None
240            self._flag_light.color = (0.2, 0.2, 0.2)
241            self._flag.node.color = (1, 1, 1)
242
243        if self._flag_state != prevstate:
244            self._swipsound.play()
245
246    def _spawn_flag(self) -> None:
247        self._swipsound.play()
248        self._flash_flag_spawn()
249        assert self._flag_spawn_pos is not None
250        self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos)
251        self._flag_state = FlagState.NEW
252        self._flag_light = bs.newnode(
253            'light',
254            owner=self._flag.node,
255            attrs={'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2)},
256        )
257        assert self._flag.node
258        self._flag.node.connectattr('position', self._flag_light, 'position')
259        self._update_flag_state()
260
261    def _flash_flag_spawn(self) -> None:
262        light = bs.newnode(
263            'light',
264            attrs={
265                'position': self._flag_spawn_pos,
266                'color': (1, 1, 1),
267                'radius': 0.3,
268                'height_attenuated': False,
269            },
270        )
271        bs.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
272        bs.timer(1.0, light.delete)
273
274    def _update_scoreboard(self) -> None:
275        for team in self.teams:
276            self._scoreboard.set_team_value(
277                team, team.timeremaining, self._hold_time, countdown=True
278            )
279
280    @override
281    def handlemessage(self, msg: Any) -> Any:
282        if isinstance(msg, bs.PlayerDiedMessage):
283            # Augment standard behavior.
284            super().handlemessage(msg)
285            self.respawn_player(msg.getplayer(Player))
286        elif isinstance(msg, FlagDiedMessage):
287            self._spawn_flag()
288        elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
289            self._update_flag_state()
290        else:
291            super().handlemessage(msg)
class FlagState(enum.Enum):
30class FlagState(Enum):
31    """States our single flag can be in."""
32
33    NEW = 0
34    UNCONTESTED = 1
35    CONTESTED = 2
36    HELD = 3

States our single flag can be in.

NEW = <FlagState.NEW: 0>
UNCONTESTED = <FlagState.UNCONTESTED: 1>
CONTESTED = <FlagState.CONTESTED: 2>
HELD = <FlagState.HELD: 3>
class Player(bascenev1._player.Player[ForwardRef('Team')]):
39class Player(bs.Player['Team']):
40    """Our player type for this game."""

Our player type for this game.

class Team(bascenev1._team.Team[bascenev1lib.game.keepaway.Player]):
43class Team(bs.Team[Player]):
44    """Our team type for this game."""
45
46    def __init__(self, timeremaining: int) -> None:
47        self.timeremaining = timeremaining
48        self.holdingflag = False

Our team type for this game.

Team(timeremaining: int)
46    def __init__(self, timeremaining: int) -> None:
47        self.timeremaining = timeremaining
48        self.holdingflag = False
timeremaining
holdingflag
class KeepAwayGame(bascenev1._teamgame.TeamGameActivity[bascenev1lib.game.keepaway.Player, bascenev1lib.game.keepaway.Team]):
 52class KeepAwayGame(bs.TeamGameActivity[Player, Team]):
 53    """Game where you try to keep the flag away from your enemies."""
 54
 55    name = 'Keep Away'
 56    description = 'Carry the flag for a set length of time.'
 57    available_settings = [
 58        bs.IntSetting(
 59            'Hold Time',
 60            min_value=10,
 61            default=30,
 62            increment=10,
 63        ),
 64        bs.IntChoiceSetting(
 65            'Time Limit',
 66            choices=[
 67                ('None', 0),
 68                ('1 Minute', 60),
 69                ('2 Minutes', 120),
 70                ('5 Minutes', 300),
 71                ('10 Minutes', 600),
 72                ('20 Minutes', 1200),
 73            ],
 74            default=0,
 75        ),
 76        bs.FloatChoiceSetting(
 77            'Respawn Times',
 78            choices=[
 79                ('Shorter', 0.25),
 80                ('Short', 0.5),
 81                ('Normal', 1.0),
 82                ('Long', 2.0),
 83                ('Longer', 4.0),
 84            ],
 85            default=1.0,
 86        ),
 87        bs.BoolSetting('Epic Mode', default=False),
 88    ]
 89    scoreconfig = bs.ScoreConfig(label='Time Held')
 90
 91    @override
 92    @classmethod
 93    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
 94        return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
 95            sessiontype, bs.FreeForAllSession
 96        )
 97
 98    @override
 99    @classmethod
100    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
101        assert bs.app.classic is not None
102        return bs.app.classic.getmaps('keep_away')
103
104    def __init__(self, settings: dict):
105        super().__init__(settings)
106        self._scoreboard = Scoreboard()
107        self._swipsound = bs.getsound('swip')
108        self._tick_sound = bs.getsound('tick')
109        self._countdownsounds = {
110            10: bs.getsound('announceTen'),
111            9: bs.getsound('announceNine'),
112            8: bs.getsound('announceEight'),
113            7: bs.getsound('announceSeven'),
114            6: bs.getsound('announceSix'),
115            5: bs.getsound('announceFive'),
116            4: bs.getsound('announceFour'),
117            3: bs.getsound('announceThree'),
118            2: bs.getsound('announceTwo'),
119            1: bs.getsound('announceOne'),
120        }
121        self._flag_spawn_pos: Sequence[float] | None = None
122        self._update_timer: bs.Timer | None = None
123        self._holding_players: list[Player] = []
124        self._flag_state: FlagState | None = None
125        self._flag_light: bs.Node | None = None
126        self._scoring_team: Team | None = None
127        self._flag: Flag | None = None
128        self._hold_time = int(settings['Hold Time'])
129        self._time_limit = float(settings['Time Limit'])
130        self._epic_mode = bool(settings['Epic Mode'])
131        self.slow_motion = self._epic_mode
132        self.default_music = (
133            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.KEEP_AWAY
134        )
135
136    @override
137    def get_instance_description(self) -> str | Sequence:
138        return 'Carry the flag for ${ARG1} seconds.', self._hold_time
139
140    @override
141    def get_instance_description_short(self) -> str | Sequence:
142        return 'carry the flag for ${ARG1} seconds', self._hold_time
143
144    @override
145    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
146        return Team(timeremaining=self._hold_time)
147
148    @override
149    def on_team_join(self, team: Team) -> None:
150        self._update_scoreboard()
151
152    @override
153    def on_begin(self) -> None:
154        super().on_begin()
155        self.setup_standard_time_limit(self._time_limit)
156        self.setup_standard_powerup_drops()
157        self._flag_spawn_pos = self.map.get_flag_position(None)
158        self._spawn_flag()
159        self._update_timer = bs.Timer(1.0, call=self._tick, repeat=True)
160        self._update_flag_state()
161        Flag.project_stand(self._flag_spawn_pos)
162
163    def _tick(self) -> None:
164        self._update_flag_state()
165
166        # Award points to all living players holding the flag.
167        for player in self._holding_players:
168            if player:
169                self.stats.player_scored(
170                    player, 3, screenmessage=False, display=False
171                )
172
173        scoreteam = self._scoring_team
174
175        if scoreteam is not None:
176            if scoreteam.timeremaining > 0:
177                self._tick_sound.play()
178
179            scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1)
180            self._update_scoreboard()
181            if scoreteam.timeremaining > 0:
182                assert self._flag is not None
183                self._flag.set_score_text(str(scoreteam.timeremaining))
184
185            # Announce numbers we have sounds for.
186            if scoreteam.timeremaining in self._countdownsounds:
187                self._countdownsounds[scoreteam.timeremaining].play()
188
189            # Winner.
190            if scoreteam.timeremaining <= 0:
191                self.end_game()
192
193    @override
194    def end_game(self) -> None:
195        results = bs.GameResults()
196        for team in self.teams:
197            results.set_team_score(team, self._hold_time - team.timeremaining)
198        self.end(results=results, announce_delay=0)
199
200    def _update_flag_state(self) -> None:
201        for team in self.teams:
202            team.holdingflag = False
203        self._holding_players = []
204        for player in self.players:
205            holdingflag = False
206            try:
207                assert isinstance(player.actor, (PlayerSpaz, type(None)))
208                if (
209                    player.actor
210                    and player.actor.node
211                    and player.actor.node.hold_node
212                ):
213                    holdingflag = (
214                        player.actor.node.hold_node.getnodetype() == 'flag'
215                    )
216            except Exception:
217                logging.exception('Error checking hold flag.')
218            if holdingflag:
219                self._holding_players.append(player)
220                player.team.holdingflag = True
221
222        holdingteams = set(t for t in self.teams if t.holdingflag)
223        prevstate = self._flag_state
224        assert self._flag is not None
225        assert self._flag_light
226        assert self._flag.node
227        if len(holdingteams) > 1:
228            self._flag_state = FlagState.CONTESTED
229            self._scoring_team = None
230            self._flag_light.color = (0.6, 0.6, 0.1)
231            self._flag.node.color = (1.0, 1.0, 0.4)
232        elif len(holdingteams) == 1:
233            holdingteam = list(holdingteams)[0]
234            self._flag_state = FlagState.HELD
235            self._scoring_team = holdingteam
236            self._flag_light.color = bs.normalized_color(holdingteam.color)
237            self._flag.node.color = holdingteam.color
238        else:
239            self._flag_state = FlagState.UNCONTESTED
240            self._scoring_team = None
241            self._flag_light.color = (0.2, 0.2, 0.2)
242            self._flag.node.color = (1, 1, 1)
243
244        if self._flag_state != prevstate:
245            self._swipsound.play()
246
247    def _spawn_flag(self) -> None:
248        self._swipsound.play()
249        self._flash_flag_spawn()
250        assert self._flag_spawn_pos is not None
251        self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos)
252        self._flag_state = FlagState.NEW
253        self._flag_light = bs.newnode(
254            'light',
255            owner=self._flag.node,
256            attrs={'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2)},
257        )
258        assert self._flag.node
259        self._flag.node.connectattr('position', self._flag_light, 'position')
260        self._update_flag_state()
261
262    def _flash_flag_spawn(self) -> None:
263        light = bs.newnode(
264            'light',
265            attrs={
266                'position': self._flag_spawn_pos,
267                'color': (1, 1, 1),
268                'radius': 0.3,
269                'height_attenuated': False,
270            },
271        )
272        bs.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
273        bs.timer(1.0, light.delete)
274
275    def _update_scoreboard(self) -> None:
276        for team in self.teams:
277            self._scoreboard.set_team_value(
278                team, team.timeremaining, self._hold_time, countdown=True
279            )
280
281    @override
282    def handlemessage(self, msg: Any) -> Any:
283        if isinstance(msg, bs.PlayerDiedMessage):
284            # Augment standard behavior.
285            super().handlemessage(msg)
286            self.respawn_player(msg.getplayer(Player))
287        elif isinstance(msg, FlagDiedMessage):
288            self._spawn_flag()
289        elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
290            self._update_flag_state()
291        else:
292            super().handlemessage(msg)

Game where you try to keep the flag away from your enemies.

KeepAwayGame(settings: dict)
104    def __init__(self, settings: dict):
105        super().__init__(settings)
106        self._scoreboard = Scoreboard()
107        self._swipsound = bs.getsound('swip')
108        self._tick_sound = bs.getsound('tick')
109        self._countdownsounds = {
110            10: bs.getsound('announceTen'),
111            9: bs.getsound('announceNine'),
112            8: bs.getsound('announceEight'),
113            7: bs.getsound('announceSeven'),
114            6: bs.getsound('announceSix'),
115            5: bs.getsound('announceFive'),
116            4: bs.getsound('announceFour'),
117            3: bs.getsound('announceThree'),
118            2: bs.getsound('announceTwo'),
119            1: bs.getsound('announceOne'),
120        }
121        self._flag_spawn_pos: Sequence[float] | None = None
122        self._update_timer: bs.Timer | None = None
123        self._holding_players: list[Player] = []
124        self._flag_state: FlagState | None = None
125        self._flag_light: bs.Node | None = None
126        self._scoring_team: Team | None = None
127        self._flag: Flag | None = None
128        self._hold_time = int(settings['Hold Time'])
129        self._time_limit = float(settings['Time Limit'])
130        self._epic_mode = bool(settings['Epic Mode'])
131        self.slow_motion = self._epic_mode
132        self.default_music = (
133            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.KEEP_AWAY
134        )

Instantiate the Activity.

name = 'Keep Away'
description = 'Carry the flag for a set length of time.'
available_settings = [IntSetting(name='Hold Time', default=30, min_value=10, max_value=9999, increment=10), 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 supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
91    @override
92    @classmethod
93    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
94        return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
95            sessiontype, bs.FreeForAllSession
96        )

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]:
 98    @override
 99    @classmethod
100    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
101        assert bs.app.classic is not None
102        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]:
136    @override
137    def get_instance_description(self) -> str | Sequence:
138        return 'Carry the flag for ${ARG1} seconds.', self._hold_time

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]:
140    @override
141    def get_instance_description_short(self) -> str | Sequence:
142        return 'carry the flag for ${ARG1} seconds', self._hold_time

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:
144    @override
145    def create_team(self, sessionteam: bs.SessionTeam) -> Team:
146        return Team(timeremaining=self._hold_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:
148    @override
149    def on_team_join(self, team: Team) -> None:
150        self._update_scoreboard()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_begin(self) -> None:
152    @override
153    def on_begin(self) -> None:
154        super().on_begin()
155        self.setup_standard_time_limit(self._time_limit)
156        self.setup_standard_powerup_drops()
157        self._flag_spawn_pos = self.map.get_flag_position(None)
158        self._spawn_flag()
159        self._update_timer = bs.Timer(1.0, call=self._tick, repeat=True)
160        self._update_flag_state()
161        Flag.project_stand(self._flag_spawn_pos)

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:
193    @override
194    def end_game(self) -> None:
195        results = bs.GameResults()
196        for team in self.teams:
197            results.set_team_score(team, self._hold_time - team.timeremaining)
198        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:
281    @override
282    def handlemessage(self, msg: Any) -> Any:
283        if isinstance(msg, bs.PlayerDiedMessage):
284            # Augment standard behavior.
285            super().handlemessage(msg)
286            self.respawn_player(msg.getplayer(Player))
287        elif isinstance(msg, FlagDiedMessage):
288            self._spawn_flag()
289        elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
290            self._update_flag_state()
291        else:
292            super().handlemessage(msg)

General message handling; can be passed any message object.