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 = (
129            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP
130        )
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
141    def get_instance_description(self) -> str | Sequence:
142        return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
143
144    def get_instance_description_short(self) -> str | Sequence:
145        return 'secure all ${ARG1} flags', len(self.map.flag_points)
146
147    def on_team_join(self, team: Team) -> None:
148        if self.has_begun():
149            self._update_scores()
150
151    def on_player_join(self, player: Player) -> None:
152        player.respawn_timer = None
153
154        # Only spawn if this player's team has a flag currently.
155        if player.team.flags_held > 0:
156            self.spawn_player(player)
157
158    def on_begin(self) -> None:
159        super().on_begin()
160        self.setup_standard_time_limit(self._time_limit)
161        self.setup_standard_powerup_drops()
162
163        # Set up flags with marker lights.
164        for i, flag_point in enumerate(self.map.flag_points):
165            point = flag_point
166            flag = ConquestFlag(
167                position=point, touchable=False, materials=[self._extraflagmat]
168            )
169            self._flags.append(flag)
170            Flag.project_stand(point)
171            flag.light = ba.newnode(
172                'light',
173                owner=flag.node,
174                attrs={
175                    'position': point,
176                    'intensity': 0.25,
177                    'height_attenuated': False,
178                    'radius': 0.3,
179                    'color': (1, 1, 1),
180                },
181            )
182
183        # Give teams a flag to start with.
184        for i, team in enumerate(self.teams):
185            self._flags[i].team = team
186            light = self._flags[i].light
187            assert light
188            node = self._flags[i].node
189            assert node
190            light.color = team.color
191            node.color = team.color
192
193        self._update_scores()
194
195        # Initial joiners didn't spawn due to no flags being owned yet;
196        # spawn them now.
197        for player in self.players:
198            self.spawn_player(player)
199
200    def _update_scores(self) -> None:
201        for team in self.teams:
202            team.flags_held = 0
203        for flag in self._flags:
204            if flag.team is not None:
205                flag.team.flags_held += 1
206        for team in self.teams:
207
208            # If a team finds themselves with no flags, cancel all
209            # outstanding spawn-timers.
210            if team.flags_held == 0:
211                for player in team.players:
212                    player.respawn_timer = None
213                    player.respawn_icon = None
214            if team.flags_held == len(self._flags):
215                self.end_game()
216            self._scoreboard.set_team_value(
217                team, team.flags_held, len(self._flags)
218            )
219
220    def end_game(self) -> None:
221        results = ba.GameResults()
222        for team in self.teams:
223            results.set_team_score(team, team.flags_held)
224        self.end(results=results)
225
226    def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
227        assert flag.node
228        assert flag.light
229        light = ba.newnode(
230            'light',
231            attrs={
232                'position': flag.node.position,
233                'height_attenuated': False,
234                'color': flag.light.color,
235            },
236        )
237        ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
238        ba.timer(length, light.delete)
239
240    def _handle_flag_player_collide(self) -> None:
241        collision = ba.getcollision()
242        try:
243            flag = collision.sourcenode.getdelegate(ConquestFlag, True)
244            player = collision.opposingnode.getdelegate(
245                PlayerSpaz, True
246            ).getplayer(Player, True)
247        except ba.NotFoundError:
248            return
249        assert flag.light
250
251        if flag.team is not player.team:
252            flag.team = player.team
253            flag.light.color = player.team.color
254            flag.node.color = player.team.color
255            self.stats.player_scored(player, 10, screenmessage=False)
256            ba.playsound(self._swipsound)
257            self._flash_flag(flag)
258            self._update_scores()
259
260            # Respawn any players on this team that were in limbo due to the
261            # lack of a flag for their team.
262            for otherplayer in self.players:
263                if (
264                    otherplayer.team is flag.team
265                    and otherplayer.actor is not None
266                    and not otherplayer.is_alive()
267                    and otherplayer.respawn_timer is None
268                ):
269                    self.spawn_player(otherplayer)
270
271    def handlemessage(self, msg: Any) -> Any:
272        if isinstance(msg, ba.PlayerDiedMessage):
273            # Augment standard behavior.
274            super().handlemessage(msg)
275
276            # Respawn only if this team has a flag.
277            player = msg.getplayer(Player)
278            if player.team.flags_held > 0:
279                self.respawn_player(player)
280            else:
281                player.respawn_timer = None
282
283        else:
284            super().handlemessage(msg)
285
286    def spawn_player(self, player: Player) -> ba.Actor:
287        # We spawn players at different places based on what flags are held.
288        return self.spawn_player_spaz(
289            player, self._get_player_spawn_position(player)
290        )
291
292    def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
293
294        # Iterate until we find a spawn owned by this team.
295        spawn_count = len(self.map.spawn_by_flag_points)
296
297        # Get all spawns owned by this team.
298        spawns = [
299            i for i in range(spawn_count) if self._flags[i].team is player.team
300        ]
301
302        closest_spawn = 0
303        closest_distance = 9999.0
304
305        # Now find the spawn that's closest to a spawn not owned by us;
306        # we'll use that one.
307        for spawn in spawns:
308            spt = self.map.spawn_by_flag_points[spawn]
309            our_pt = ba.Vec3(spt[0], spt[1], spt[2])
310            for otherspawn in [
311                i
312                for i in range(spawn_count)
313                if self._flags[i].team is not player.team
314            ]:
315                spt = self.map.spawn_by_flag_points[otherspawn]
316                their_pt = ba.Vec3(spt[0], spt[1], spt[2])
317                dist = (their_pt - our_pt).length()
318                if dist < closest_distance:
319                    closest_distance = dist
320                    closest_spawn = spawn
321
322        pos = self.map.spawn_by_flag_points[closest_spawn]
323        x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
324        z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
325        pos = (
326            pos[0] + random.uniform(*x_range),
327            pos[1],
328            pos[2] + random.uniform(*z_range),
329        )
330        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.

Set 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.

Inherited Members
ba._player.Player
actor
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(ba._team.Team[bastd.game.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 = (
130            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP
131        )
132
133        # We want flags to tell us they've been hit but not react physically.
134        self._extraflagmat.add_actions(
135            conditions=('they_have_material', shared.player_material),
136            actions=(
137                ('modify_part_collision', 'collide', True),
138                ('call', 'at_connect', self._handle_flag_player_collide),
139            ),
140        )
141
142    def get_instance_description(self) -> str | Sequence:
143        return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
144
145    def get_instance_description_short(self) -> str | Sequence:
146        return 'secure all ${ARG1} flags', len(self.map.flag_points)
147
148    def on_team_join(self, team: Team) -> None:
149        if self.has_begun():
150            self._update_scores()
151
152    def on_player_join(self, player: Player) -> None:
153        player.respawn_timer = None
154
155        # Only spawn if this player's team has a flag currently.
156        if player.team.flags_held > 0:
157            self.spawn_player(player)
158
159    def on_begin(self) -> None:
160        super().on_begin()
161        self.setup_standard_time_limit(self._time_limit)
162        self.setup_standard_powerup_drops()
163
164        # Set up flags with marker lights.
165        for i, flag_point in enumerate(self.map.flag_points):
166            point = flag_point
167            flag = ConquestFlag(
168                position=point, touchable=False, materials=[self._extraflagmat]
169            )
170            self._flags.append(flag)
171            Flag.project_stand(point)
172            flag.light = ba.newnode(
173                'light',
174                owner=flag.node,
175                attrs={
176                    'position': point,
177                    'intensity': 0.25,
178                    'height_attenuated': False,
179                    'radius': 0.3,
180                    'color': (1, 1, 1),
181                },
182            )
183
184        # Give teams a flag to start with.
185        for i, team in enumerate(self.teams):
186            self._flags[i].team = team
187            light = self._flags[i].light
188            assert light
189            node = self._flags[i].node
190            assert node
191            light.color = team.color
192            node.color = team.color
193
194        self._update_scores()
195
196        # Initial joiners didn't spawn due to no flags being owned yet;
197        # spawn them now.
198        for player in self.players:
199            self.spawn_player(player)
200
201    def _update_scores(self) -> None:
202        for team in self.teams:
203            team.flags_held = 0
204        for flag in self._flags:
205            if flag.team is not None:
206                flag.team.flags_held += 1
207        for team in self.teams:
208
209            # If a team finds themselves with no flags, cancel all
210            # outstanding spawn-timers.
211            if team.flags_held == 0:
212                for player in team.players:
213                    player.respawn_timer = None
214                    player.respawn_icon = None
215            if team.flags_held == len(self._flags):
216                self.end_game()
217            self._scoreboard.set_team_value(
218                team, team.flags_held, len(self._flags)
219            )
220
221    def end_game(self) -> None:
222        results = ba.GameResults()
223        for team in self.teams:
224            results.set_team_score(team, team.flags_held)
225        self.end(results=results)
226
227    def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
228        assert flag.node
229        assert flag.light
230        light = ba.newnode(
231            'light',
232            attrs={
233                'position': flag.node.position,
234                'height_attenuated': False,
235                'color': flag.light.color,
236            },
237        )
238        ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
239        ba.timer(length, light.delete)
240
241    def _handle_flag_player_collide(self) -> None:
242        collision = ba.getcollision()
243        try:
244            flag = collision.sourcenode.getdelegate(ConquestFlag, True)
245            player = collision.opposingnode.getdelegate(
246                PlayerSpaz, True
247            ).getplayer(Player, True)
248        except ba.NotFoundError:
249            return
250        assert flag.light
251
252        if flag.team is not player.team:
253            flag.team = player.team
254            flag.light.color = player.team.color
255            flag.node.color = player.team.color
256            self.stats.player_scored(player, 10, screenmessage=False)
257            ba.playsound(self._swipsound)
258            self._flash_flag(flag)
259            self._update_scores()
260
261            # Respawn any players on this team that were in limbo due to the
262            # lack of a flag for their team.
263            for otherplayer in self.players:
264                if (
265                    otherplayer.team is flag.team
266                    and otherplayer.actor is not None
267                    and not otherplayer.is_alive()
268                    and otherplayer.respawn_timer is None
269                ):
270                    self.spawn_player(otherplayer)
271
272    def handlemessage(self, msg: Any) -> Any:
273        if isinstance(msg, ba.PlayerDiedMessage):
274            # Augment standard behavior.
275            super().handlemessage(msg)
276
277            # Respawn only if this team has a flag.
278            player = msg.getplayer(Player)
279            if player.team.flags_held > 0:
280                self.respawn_player(player)
281            else:
282                player.respawn_timer = None
283
284        else:
285            super().handlemessage(msg)
286
287    def spawn_player(self, player: Player) -> ba.Actor:
288        # We spawn players at different places based on what flags are held.
289        return self.spawn_player_spaz(
290            player, self._get_player_spawn_position(player)
291        )
292
293    def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
294
295        # Iterate until we find a spawn owned by this team.
296        spawn_count = len(self.map.spawn_by_flag_points)
297
298        # Get all spawns owned by this team.
299        spawns = [
300            i for i in range(spawn_count) if self._flags[i].team is player.team
301        ]
302
303        closest_spawn = 0
304        closest_distance = 9999.0
305
306        # Now find the spawn that's closest to a spawn not owned by us;
307        # we'll use that one.
308        for spawn in spawns:
309            spt = self.map.spawn_by_flag_points[spawn]
310            our_pt = ba.Vec3(spt[0], spt[1], spt[2])
311            for otherspawn in [
312                i
313                for i in range(spawn_count)
314                if self._flags[i].team is not player.team
315            ]:
316                spt = self.map.spawn_by_flag_points[otherspawn]
317                their_pt = ba.Vec3(spt[0], spt[1], spt[2])
318                dist = (their_pt - our_pt).length()
319                if dist < closest_distance:
320                    closest_distance = dist
321                    closest_spawn = spawn
322
323        pos = self.map.spawn_by_flag_points[closest_spawn]
324        x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
325        z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
326        pos = (
327            pos[0] + random.uniform(*x_range),
328            pos[1],
329            pos[2] + random.uniform(*z_range),
330        )
331        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 = (
130            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP
131        )
132
133        # We want flags to tell us they've been hit but not react physically.
134        self._extraflagmat.add_actions(
135            conditions=('they_have_material', shared.player_material),
136            actions=(
137                ('modify_part_collision', 'collide', True),
138                ('call', 'at_connect', self._handle_flag_player_collide),
139            ),
140        )

Instantiate the Activity.

@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.

slow_motion = False

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

def get_instance_description(self) -> Union[str, Sequence]:
142    def get_instance_description(self) -> str | Sequence:
143        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]:
145    def get_instance_description_short(self) -> str | Sequence:
146        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:
148    def on_team_join(self, team: Team) -> None:
149        if self.has_begun():
150            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:
152    def on_player_join(self, player: Player) -> None:
153        player.respawn_timer = None
154
155        # Only spawn if this player's team has a flag currently.
156        if player.team.flags_held > 0:
157            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:
159    def on_begin(self) -> None:
160        super().on_begin()
161        self.setup_standard_time_limit(self._time_limit)
162        self.setup_standard_powerup_drops()
163
164        # Set up flags with marker lights.
165        for i, flag_point in enumerate(self.map.flag_points):
166            point = flag_point
167            flag = ConquestFlag(
168                position=point, touchable=False, materials=[self._extraflagmat]
169            )
170            self._flags.append(flag)
171            Flag.project_stand(point)
172            flag.light = ba.newnode(
173                'light',
174                owner=flag.node,
175                attrs={
176                    'position': point,
177                    'intensity': 0.25,
178                    'height_attenuated': False,
179                    'radius': 0.3,
180                    'color': (1, 1, 1),
181                },
182            )
183
184        # Give teams a flag to start with.
185        for i, team in enumerate(self.teams):
186            self._flags[i].team = team
187            light = self._flags[i].light
188            assert light
189            node = self._flags[i].node
190            assert node
191            light.color = team.color
192            node.color = team.color
193
194        self._update_scores()
195
196        # Initial joiners didn't spawn due to no flags being owned yet;
197        # spawn them now.
198        for player in self.players:
199            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:
221    def end_game(self) -> None:
222        results = ba.GameResults()
223        for team in self.teams:
224            results.set_team_score(team, team.flags_held)
225        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:
272    def handlemessage(self, msg: Any) -> Any:
273        if isinstance(msg, ba.PlayerDiedMessage):
274            # Augment standard behavior.
275            super().handlemessage(msg)
276
277            # Respawn only if this team has a flag.
278            player = msg.getplayer(Player)
279            if player.team.flags_held > 0:
280                self.respawn_player(player)
281            else:
282                player.respawn_timer = None
283
284        else:
285            super().handlemessage(msg)

General message handling; can be passed any message object.

def spawn_player(self, player: bastd.game.conquest.Player) -> ba._actor.Actor:
287    def spawn_player(self, player: Player) -> ba.Actor:
288        # We spawn players at different places based on what flags are held.
289        return self.spawn_player_spaz(
290            player, self._get_player_spawn_position(player)
291        )

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

Inherited Members
ba._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
ba._gameactivity.GameActivity
allow_pausing
allow_kick_idle_players
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_available_settings
get_settings_display_string
map
get_instance_display_string
get_instance_scoreboard_display_string
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._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
use_fixed_vr_overlay
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
allow_mid_activity_joins
transition_time
can_show_ad_on_death
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
session
on_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._dependency.DependencyComponent
dep_is_present
get_dynamic_deps