bastd.game.conquest

Provides the Conquest game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Provides the Conquest game."""
  4
  5# ba_meta require api 7
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10import random
 11from typing import TYPE_CHECKING
 12
 13import ba
 14from bastd.actor.flag import Flag
 15from bastd.actor.scoreboard import Scoreboard
 16from bastd.actor.playerspaz import PlayerSpaz
 17from bastd.gameutils import SharedObjects
 18
 19if TYPE_CHECKING:
 20    from typing import Any, Sequence
 21    from bastd.actor.respawnicon import RespawnIcon
 22
 23
 24class ConquestFlag(Flag):
 25    """A custom flag for use with Conquest games."""
 26
 27    def __init__(self, *args: Any, **keywds: Any):
 28        super().__init__(*args, **keywds)
 29        self._team: Team | None = None
 30        self.light: ba.Node | None = None
 31
 32    @property
 33    def team(self) -> Team | None:
 34        """The team that owns this flag."""
 35        return self._team
 36
 37    @team.setter
 38    def team(self, team: Team) -> None:
 39        """Set the team that owns this flag."""
 40        self._team = team
 41
 42
 43class Player(ba.Player['Team']):
 44    """Our player type for this game."""
 45
 46    # FIXME: We shouldn't be using customdata here
 47    # (but need to update respawn funcs accordingly first).
 48    @property
 49    def respawn_timer(self) -> ba.Timer | None:
 50        """Type safe access to standard respawn timer."""
 51        return self.customdata.get('respawn_timer', None)
 52
 53    @respawn_timer.setter
 54    def respawn_timer(self, value: ba.Timer | None) -> None:
 55        self.customdata['respawn_timer'] = value
 56
 57    @property
 58    def respawn_icon(self) -> RespawnIcon | None:
 59        """Type safe access to standard respawn icon."""
 60        return self.customdata.get('respawn_icon', None)
 61
 62    @respawn_icon.setter
 63    def respawn_icon(self, value: RespawnIcon | None) -> None:
 64        self.customdata['respawn_icon'] = value
 65
 66
 67class Team(ba.Team[Player]):
 68    """Our team type for this game."""
 69
 70    def __init__(self) -> None:
 71        self.flags_held = 0
 72
 73
 74# ba_meta export game
 75class ConquestGame(ba.TeamGameActivity[Player, Team]):
 76    """A game where teams try to claim all flags on the map."""
 77
 78    name = 'Conquest'
 79    description = 'Secure all flags on the map to win.'
 80    available_settings = [
 81        ba.IntChoiceSetting(
 82            'Time Limit',
 83            choices=[
 84                ('None', 0),
 85                ('1 Minute', 60),
 86                ('2 Minutes', 120),
 87                ('5 Minutes', 300),
 88                ('10 Minutes', 600),
 89                ('20 Minutes', 1200),
 90            ],
 91            default=0,
 92        ),
 93        ba.FloatChoiceSetting(
 94            'Respawn Times',
 95            choices=[
 96                ('Shorter', 0.25),
 97                ('Short', 0.5),
 98                ('Normal', 1.0),
 99                ('Long', 2.0),
100                ('Longer', 4.0),
101            ],
102            default=1.0,
103        ),
104        ba.BoolSetting('Epic Mode', default=False),
105    ]
106
107    @classmethod
108    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
109        return issubclass(sessiontype, ba.DualTeamSession)
110
111    @classmethod
112    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
113        return ba.getmaps('conquest')
114
115    def __init__(self, settings: dict):
116        super().__init__(settings)
117        shared = SharedObjects.get()
118        self._scoreboard = Scoreboard()
119        self._score_sound = ba.getsound('score')
120        self._swipsound = ba.getsound('swip')
121        self._extraflagmat = ba.Material()
122        self._flags: list[ConquestFlag] = []
123        self._epic_mode = bool(settings['Epic Mode'])
124        self._time_limit = float(settings['Time Limit'])
125
126        # Base class overrides.
127        self.slow_motion = self._epic_mode
128        self.default_music = (ba.MusicType.EPIC
129                              if self._epic_mode else ba.MusicType.GRAND_ROMP)
130
131        # We want flags to tell us they've been hit but not react physically.
132        self._extraflagmat.add_actions(
133            conditions=('they_have_material', shared.player_material),
134            actions=(
135                ('modify_part_collision', 'collide', True),
136                ('call', 'at_connect', self._handle_flag_player_collide),
137            ))
138
139    def get_instance_description(self) -> str | Sequence:
140        return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
141
142    def get_instance_description_short(self) -> str | Sequence:
143        return 'secure all ${ARG1} flags', len(self.map.flag_points)
144
145    def on_team_join(self, team: Team) -> None:
146        if self.has_begun():
147            self._update_scores()
148
149    def on_player_join(self, player: Player) -> None:
150        player.respawn_timer = None
151
152        # Only spawn if this player's team has a flag currently.
153        if player.team.flags_held > 0:
154            self.spawn_player(player)
155
156    def on_begin(self) -> None:
157        super().on_begin()
158        self.setup_standard_time_limit(self._time_limit)
159        self.setup_standard_powerup_drops()
160
161        # Set up flags with marker lights.
162        for i, flag_point in enumerate(self.map.flag_points):
163            point = flag_point
164            flag = ConquestFlag(position=point,
165                                touchable=False,
166                                materials=[self._extraflagmat])
167            self._flags.append(flag)
168            Flag.project_stand(point)
169            flag.light = ba.newnode('light',
170                                    owner=flag.node,
171                                    attrs={
172                                        'position': point,
173                                        'intensity': 0.25,
174                                        'height_attenuated': False,
175                                        'radius': 0.3,
176                                        'color': (1, 1, 1)
177                                    })
178
179        # Give teams a flag to start with.
180        for i, team in enumerate(self.teams):
181            self._flags[i].team = team
182            light = self._flags[i].light
183            assert light
184            node = self._flags[i].node
185            assert node
186            light.color = team.color
187            node.color = team.color
188
189        self._update_scores()
190
191        # Initial joiners didn't spawn due to no flags being owned yet;
192        # spawn them now.
193        for player in self.players:
194            self.spawn_player(player)
195
196    def _update_scores(self) -> None:
197        for team in self.teams:
198            team.flags_held = 0
199        for flag in self._flags:
200            if flag.team is not None:
201                flag.team.flags_held += 1
202        for team in self.teams:
203
204            # If a team finds themselves with no flags, cancel all
205            # outstanding spawn-timers.
206            if team.flags_held == 0:
207                for player in team.players:
208                    player.respawn_timer = None
209                    player.respawn_icon = None
210            if team.flags_held == len(self._flags):
211                self.end_game()
212            self._scoreboard.set_team_value(team, team.flags_held,
213                                            len(self._flags))
214
215    def end_game(self) -> None:
216        results = ba.GameResults()
217        for team in self.teams:
218            results.set_team_score(team, team.flags_held)
219        self.end(results=results)
220
221    def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
222        assert flag.node
223        assert flag.light
224        light = ba.newnode('light',
225                           attrs={
226                               'position': flag.node.position,
227                               'height_attenuated': False,
228                               'color': flag.light.color
229                           })
230        ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
231        ba.timer(length, light.delete)
232
233    def _handle_flag_player_collide(self) -> None:
234        collision = ba.getcollision()
235        try:
236            flag = collision.sourcenode.getdelegate(ConquestFlag, True)
237            player = collision.opposingnode.getdelegate(PlayerSpaz,
238                                                        True).getplayer(
239                                                            Player, True)
240        except ba.NotFoundError:
241            return
242        assert flag.light
243
244        if flag.team is not player.team:
245            flag.team = player.team
246            flag.light.color = player.team.color
247            flag.node.color = player.team.color
248            self.stats.player_scored(player, 10, screenmessage=False)
249            ba.playsound(self._swipsound)
250            self._flash_flag(flag)
251            self._update_scores()
252
253            # Respawn any players on this team that were in limbo due to the
254            # lack of a flag for their team.
255            for otherplayer in self.players:
256                if (otherplayer.team is flag.team
257                        and otherplayer.actor is not None
258                        and not otherplayer.is_alive()
259                        and otherplayer.respawn_timer is None):
260                    self.spawn_player(otherplayer)
261
262    def handlemessage(self, msg: Any) -> Any:
263        if isinstance(msg, ba.PlayerDiedMessage):
264            # Augment standard behavior.
265            super().handlemessage(msg)
266
267            # Respawn only if this team has a flag.
268            player = msg.getplayer(Player)
269            if player.team.flags_held > 0:
270                self.respawn_player(player)
271            else:
272                player.respawn_timer = None
273
274        else:
275            super().handlemessage(msg)
276
277    def spawn_player(self, player: Player) -> ba.Actor:
278        # We spawn players at different places based on what flags are held.
279        return self.spawn_player_spaz(player,
280                                      self._get_player_spawn_position(player))
281
282    def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
283
284        # Iterate until we find a spawn owned by this team.
285        spawn_count = len(self.map.spawn_by_flag_points)
286
287        # Get all spawns owned by this team.
288        spawns = [
289            i for i in range(spawn_count) if self._flags[i].team is player.team
290        ]
291
292        closest_spawn = 0
293        closest_distance = 9999.0
294
295        # Now find the spawn that's closest to a spawn not owned by us;
296        # we'll use that one.
297        for spawn in spawns:
298            spt = self.map.spawn_by_flag_points[spawn]
299            our_pt = ba.Vec3(spt[0], spt[1], spt[2])
300            for otherspawn in [
301                    i for i in range(spawn_count)
302                    if self._flags[i].team is not player.team
303            ]:
304                spt = self.map.spawn_by_flag_points[otherspawn]
305                their_pt = ba.Vec3(spt[0], spt[1], spt[2])
306                dist = (their_pt - our_pt).length()
307                if dist < closest_distance:
308                    closest_distance = dist
309                    closest_spawn = spawn
310
311        pos = self.map.spawn_by_flag_points[closest_spawn]
312        x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
313        z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
314        pos = (pos[0] + random.uniform(*x_range), pos[1],
315               pos[2] + random.uniform(*z_range))
316        return pos
class ConquestFlag(bastd.actor.flag.Flag):
25class ConquestFlag(Flag):
26    """A custom flag for use with Conquest games."""
27
28    def __init__(self, *args: Any, **keywds: Any):
29        super().__init__(*args, **keywds)
30        self._team: Team | None = None
31        self.light: ba.Node | None = None
32
33    @property
34    def team(self) -> Team | None:
35        """The team that owns this flag."""
36        return self._team
37
38    @team.setter
39    def team(self, team: Team) -> None:
40        """Set the team that owns this flag."""
41        self._team = team

A custom flag for use with Conquest games.

ConquestFlag(*args: Any, **keywds: Any)
28    def __init__(self, *args: Any, **keywds: Any):
29        super().__init__(*args, **keywds)
30        self._team: Team | None = None
31        self.light: ba.Node | None = None

Instantiate a flag.

If 'touchable' is False, the flag will only touch terrain; useful for things like king-of-the-hill where players should not be moving the flag around.

'materials can be a list of extra ba.Materials to apply to the flag.

If 'dropped_timeout' is provided (in seconds), the flag will die after remaining untouched for that long once it has been moved from its initial position.

The team that owns this flag.

Inherited Members
bastd.actor.flag.Flag
set_score_text
handlemessage
project_stand
ba._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Player(ba._player.Player[ForwardRef('Team')]):
44class Player(ba.Player['Team']):
45    """Our player type for this game."""
46
47    # FIXME: We shouldn't be using customdata here
48    # (but need to update respawn funcs accordingly first).
49    @property
50    def respawn_timer(self) -> ba.Timer | None:
51        """Type safe access to standard respawn timer."""
52        return self.customdata.get('respawn_timer', None)
53
54    @respawn_timer.setter
55    def respawn_timer(self, value: ba.Timer | None) -> None:
56        self.customdata['respawn_timer'] = value
57
58    @property
59    def respawn_icon(self) -> RespawnIcon | None:
60        """Type safe access to standard respawn icon."""
61        return self.customdata.get('respawn_icon', None)
62
63    @respawn_icon.setter
64    def respawn_icon(self, value: RespawnIcon | None) -> None:
65        self.customdata['respawn_icon'] = value

Our player type for this game.

Player()
respawn_timer: _ba.Timer | None

Type safe access to standard respawn timer.

Type safe access to standard respawn icon.

class Team(ba._team.Team[bastd.game.conquest.Player]):
68class Team(ba.Team[Player]):
69    """Our team type for this game."""
70
71    def __init__(self) -> None:
72        self.flags_held = 0

Our team type for this game.

Team()
71    def __init__(self) -> None:
72        self.flags_held = 0
Inherited Members
ba._team.Team
manual_init
customdata
on_expire
sessionteam
class ConquestGame(ba._teamgame.TeamGameActivity[bastd.game.conquest.Player, bastd.game.conquest.Team]):
 76class ConquestGame(ba.TeamGameActivity[Player, Team]):
 77    """A game where teams try to claim all flags on the map."""
 78
 79    name = 'Conquest'
 80    description = 'Secure all flags on the map to win.'
 81    available_settings = [
 82        ba.IntChoiceSetting(
 83            'Time Limit',
 84            choices=[
 85                ('None', 0),
 86                ('1 Minute', 60),
 87                ('2 Minutes', 120),
 88                ('5 Minutes', 300),
 89                ('10 Minutes', 600),
 90                ('20 Minutes', 1200),
 91            ],
 92            default=0,
 93        ),
 94        ba.FloatChoiceSetting(
 95            'Respawn Times',
 96            choices=[
 97                ('Shorter', 0.25),
 98                ('Short', 0.5),
 99                ('Normal', 1.0),
100                ('Long', 2.0),
101                ('Longer', 4.0),
102            ],
103            default=1.0,
104        ),
105        ba.BoolSetting('Epic Mode', default=False),
106    ]
107
108    @classmethod
109    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
110        return issubclass(sessiontype, ba.DualTeamSession)
111
112    @classmethod
113    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
114        return ba.getmaps('conquest')
115
116    def __init__(self, settings: dict):
117        super().__init__(settings)
118        shared = SharedObjects.get()
119        self._scoreboard = Scoreboard()
120        self._score_sound = ba.getsound('score')
121        self._swipsound = ba.getsound('swip')
122        self._extraflagmat = ba.Material()
123        self._flags: list[ConquestFlag] = []
124        self._epic_mode = bool(settings['Epic Mode'])
125        self._time_limit = float(settings['Time Limit'])
126
127        # Base class overrides.
128        self.slow_motion = self._epic_mode
129        self.default_music = (ba.MusicType.EPIC
130                              if self._epic_mode else ba.MusicType.GRAND_ROMP)
131
132        # We want flags to tell us they've been hit but not react physically.
133        self._extraflagmat.add_actions(
134            conditions=('they_have_material', shared.player_material),
135            actions=(
136                ('modify_part_collision', 'collide', True),
137                ('call', 'at_connect', self._handle_flag_player_collide),
138            ))
139
140    def get_instance_description(self) -> str | Sequence:
141        return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
142
143    def get_instance_description_short(self) -> str | Sequence:
144        return 'secure all ${ARG1} flags', len(self.map.flag_points)
145
146    def on_team_join(self, team: Team) -> None:
147        if self.has_begun():
148            self._update_scores()
149
150    def on_player_join(self, player: Player) -> None:
151        player.respawn_timer = None
152
153        # Only spawn if this player's team has a flag currently.
154        if player.team.flags_held > 0:
155            self.spawn_player(player)
156
157    def on_begin(self) -> None:
158        super().on_begin()
159        self.setup_standard_time_limit(self._time_limit)
160        self.setup_standard_powerup_drops()
161
162        # Set up flags with marker lights.
163        for i, flag_point in enumerate(self.map.flag_points):
164            point = flag_point
165            flag = ConquestFlag(position=point,
166                                touchable=False,
167                                materials=[self._extraflagmat])
168            self._flags.append(flag)
169            Flag.project_stand(point)
170            flag.light = ba.newnode('light',
171                                    owner=flag.node,
172                                    attrs={
173                                        'position': point,
174                                        'intensity': 0.25,
175                                        'height_attenuated': False,
176                                        'radius': 0.3,
177                                        'color': (1, 1, 1)
178                                    })
179
180        # Give teams a flag to start with.
181        for i, team in enumerate(self.teams):
182            self._flags[i].team = team
183            light = self._flags[i].light
184            assert light
185            node = self._flags[i].node
186            assert node
187            light.color = team.color
188            node.color = team.color
189
190        self._update_scores()
191
192        # Initial joiners didn't spawn due to no flags being owned yet;
193        # spawn them now.
194        for player in self.players:
195            self.spawn_player(player)
196
197    def _update_scores(self) -> None:
198        for team in self.teams:
199            team.flags_held = 0
200        for flag in self._flags:
201            if flag.team is not None:
202                flag.team.flags_held += 1
203        for team in self.teams:
204
205            # If a team finds themselves with no flags, cancel all
206            # outstanding spawn-timers.
207            if team.flags_held == 0:
208                for player in team.players:
209                    player.respawn_timer = None
210                    player.respawn_icon = None
211            if team.flags_held == len(self._flags):
212                self.end_game()
213            self._scoreboard.set_team_value(team, team.flags_held,
214                                            len(self._flags))
215
216    def end_game(self) -> None:
217        results = ba.GameResults()
218        for team in self.teams:
219            results.set_team_score(team, team.flags_held)
220        self.end(results=results)
221
222    def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
223        assert flag.node
224        assert flag.light
225        light = ba.newnode('light',
226                           attrs={
227                               'position': flag.node.position,
228                               'height_attenuated': False,
229                               'color': flag.light.color
230                           })
231        ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
232        ba.timer(length, light.delete)
233
234    def _handle_flag_player_collide(self) -> None:
235        collision = ba.getcollision()
236        try:
237            flag = collision.sourcenode.getdelegate(ConquestFlag, True)
238            player = collision.opposingnode.getdelegate(PlayerSpaz,
239                                                        True).getplayer(
240                                                            Player, True)
241        except ba.NotFoundError:
242            return
243        assert flag.light
244
245        if flag.team is not player.team:
246            flag.team = player.team
247            flag.light.color = player.team.color
248            flag.node.color = player.team.color
249            self.stats.player_scored(player, 10, screenmessage=False)
250            ba.playsound(self._swipsound)
251            self._flash_flag(flag)
252            self._update_scores()
253
254            # Respawn any players on this team that were in limbo due to the
255            # lack of a flag for their team.
256            for otherplayer in self.players:
257                if (otherplayer.team is flag.team
258                        and otherplayer.actor is not None
259                        and not otherplayer.is_alive()
260                        and otherplayer.respawn_timer is None):
261                    self.spawn_player(otherplayer)
262
263    def handlemessage(self, msg: Any) -> Any:
264        if isinstance(msg, ba.PlayerDiedMessage):
265            # Augment standard behavior.
266            super().handlemessage(msg)
267
268            # Respawn only if this team has a flag.
269            player = msg.getplayer(Player)
270            if player.team.flags_held > 0:
271                self.respawn_player(player)
272            else:
273                player.respawn_timer = None
274
275        else:
276            super().handlemessage(msg)
277
278    def spawn_player(self, player: Player) -> ba.Actor:
279        # We spawn players at different places based on what flags are held.
280        return self.spawn_player_spaz(player,
281                                      self._get_player_spawn_position(player))
282
283    def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
284
285        # Iterate until we find a spawn owned by this team.
286        spawn_count = len(self.map.spawn_by_flag_points)
287
288        # Get all spawns owned by this team.
289        spawns = [
290            i for i in range(spawn_count) if self._flags[i].team is player.team
291        ]
292
293        closest_spawn = 0
294        closest_distance = 9999.0
295
296        # Now find the spawn that's closest to a spawn not owned by us;
297        # we'll use that one.
298        for spawn in spawns:
299            spt = self.map.spawn_by_flag_points[spawn]
300            our_pt = ba.Vec3(spt[0], spt[1], spt[2])
301            for otherspawn in [
302                    i for i in range(spawn_count)
303                    if self._flags[i].team is not player.team
304            ]:
305                spt = self.map.spawn_by_flag_points[otherspawn]
306                their_pt = ba.Vec3(spt[0], spt[1], spt[2])
307                dist = (their_pt - our_pt).length()
308                if dist < closest_distance:
309                    closest_distance = dist
310                    closest_spawn = spawn
311
312        pos = self.map.spawn_by_flag_points[closest_spawn]
313        x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
314        z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
315        pos = (pos[0] + random.uniform(*x_range), pos[1],
316               pos[2] + random.uniform(*z_range))
317        return pos

A game where teams try to claim all flags on the map.

ConquestGame(settings: dict)
116    def __init__(self, settings: dict):
117        super().__init__(settings)
118        shared = SharedObjects.get()
119        self._scoreboard = Scoreboard()
120        self._score_sound = ba.getsound('score')
121        self._swipsound = ba.getsound('swip')
122        self._extraflagmat = ba.Material()
123        self._flags: list[ConquestFlag] = []
124        self._epic_mode = bool(settings['Epic Mode'])
125        self._time_limit = float(settings['Time Limit'])
126
127        # Base class overrides.
128        self.slow_motion = self._epic_mode
129        self.default_music = (ba.MusicType.EPIC
130                              if self._epic_mode else ba.MusicType.GRAND_ROMP)
131
132        # We want flags to tell us they've been hit but not react physically.
133        self._extraflagmat.add_actions(
134            conditions=('they_have_material', shared.player_material),
135            actions=(
136                ('modify_part_collision', 'collide', True),
137                ('call', 'at_connect', self._handle_flag_player_collide),
138            ))

Instantiate the Activity.

name: str | None = 'Conquest'
description: str | None = 'Secure all flags on the map to win.'
available_settings: list[ba._settings.Setting] | None = [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)]
@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
108    @classmethod
109    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
110        return issubclass(sessiontype, ba.DualTeamSession)

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

@classmethod
def get_supported_maps(cls, sessiontype: type[ba._session.Session]) -> list[str]:
112    @classmethod
113    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
114        return ba.getmaps('conquest')

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.

def get_instance_description(self) -> Union[str, Sequence]:
140    def get_instance_description(self) -> str | Sequence:
141        return 'Secure all ${ARG1} flags.', len(self.map.flag_points)

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 get_instance_description_short(self) -> Union[str, Sequence]:
143    def get_instance_description_short(self) -> str | Sequence:
144        return 'secure all ${ARG1} flags', len(self.map.flag_points)

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.

def on_team_join(self, team: bastd.game.conquest.Team) -> None:
146    def on_team_join(self, team: Team) -> None:
147        if self.has_begun():
148            self._update_scores()

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def on_player_join(self, player: bastd.game.conquest.Player) -> None:
150    def on_player_join(self, player: Player) -> None:
151        player.respawn_timer = None
152
153        # Only spawn if this player's team has a flag currently.
154        if player.team.flags_held > 0:
155            self.spawn_player(player)

Called when a new ba.Player has joined the Activity.

(including the initial set of Players)

def on_begin(self) -> None:
157    def on_begin(self) -> None:
158        super().on_begin()
159        self.setup_standard_time_limit(self._time_limit)
160        self.setup_standard_powerup_drops()
161
162        # Set up flags with marker lights.
163        for i, flag_point in enumerate(self.map.flag_points):
164            point = flag_point
165            flag = ConquestFlag(position=point,
166                                touchable=False,
167                                materials=[self._extraflagmat])
168            self._flags.append(flag)
169            Flag.project_stand(point)
170            flag.light = ba.newnode('light',
171                                    owner=flag.node,
172                                    attrs={
173                                        'position': point,
174                                        'intensity': 0.25,
175                                        'height_attenuated': False,
176                                        'radius': 0.3,
177                                        'color': (1, 1, 1)
178                                    })
179
180        # Give teams a flag to start with.
181        for i, team in enumerate(self.teams):
182            self._flags[i].team = team
183            light = self._flags[i].light
184            assert light
185            node = self._flags[i].node
186            assert node
187            light.color = team.color
188            node.color = team.color
189
190        self._update_scores()
191
192        # Initial joiners didn't spawn due to no flags being owned yet;
193        # spawn them now.
194        for player in self.players:
195            self.spawn_player(player)

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:
216    def end_game(self) -> None:
217        results = ba.GameResults()
218        for team in self.teams:
219            results.set_team_score(team, team.flags_held)
220        self.end(results=results)

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:
263    def handlemessage(self, msg: Any) -> Any:
264        if isinstance(msg, ba.PlayerDiedMessage):
265            # Augment standard behavior.
266            super().handlemessage(msg)
267
268            # Respawn only if this team has a flag.
269            player = msg.getplayer(Player)
270            if player.team.flags_held > 0:
271                self.respawn_player(player)
272            else:
273                player.respawn_timer = None
274
275        else:
276            super().handlemessage(msg)

General message handling; can be passed any message object.

def spawn_player(self, player: bastd.game.conquest.Player) -> ba._actor.Actor:
278    def spawn_player(self, player: Player) -> ba.Actor:
279        # We spawn players at different places based on what flags are held.
280        return self.spawn_player_spaz(player,
281                                      self._get_player_spawn_position(player))

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

Inherited Members
ba._activity.Activity
slow_motion
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_player_leave
on_team_leave
on_transition_out
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
create_player
create_team
ba._gameactivity.GameActivity
default_music
tips
scoreconfig
allow_pausing
allow_kick_idle_players
show_kill_points
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
on_continue
is_waiting_for_continue
continue_or_end_game
respawn_player
spawn_player_if_exists
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
ba._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
ba._dependency.DependencyComponent
dep_is_present
get_dynamic_deps