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

A custom flag for use with Conquest games.

ConquestFlag(*args: Any, **keywds: Any)
29    def __init__(self, *args: Any, **keywds: Any):
30        super().__init__(*args, **keywds)
31        self._team: Team | None = None
32        self.light: bs.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 bs.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.

light: _bascenev1.Node | None
team: Team | None
34    @property
35    def team(self) -> Team | None:
36        """The team that owns this flag."""
37        return self._team

The team that owns this flag.

class Player(bascenev1._player.Player[ForwardRef('Team')]):
45class Player(bs.Player['Team']):
46    """Our player type for this game."""
47
48    # FIXME: We shouldn't be using customdata here
49    # (but need to update respawn funcs accordingly first).
50    @property
51    def respawn_timer(self) -> bs.Timer | None:
52        """Type safe access to standard respawn timer."""
53        val = self.customdata.get('respawn_timer', None)
54        assert isinstance(val, (bs.Timer, type(None)))
55        return val
56
57    @respawn_timer.setter
58    def respawn_timer(self, value: bs.Timer | None) -> None:
59        self.customdata['respawn_timer'] = value
60
61    @property
62    def respawn_icon(self) -> RespawnIcon | None:
63        """Type safe access to standard respawn icon."""
64        val = self.customdata.get('respawn_icon', None)
65        assert isinstance(val, (RespawnIcon, type(None)))
66        return val
67
68    @respawn_icon.setter
69    def respawn_icon(self, value: RespawnIcon | None) -> None:
70        self.customdata['respawn_icon'] = value

Our player type for this game.

respawn_timer: _bascenev1.Timer | None
50    @property
51    def respawn_timer(self) -> bs.Timer | None:
52        """Type safe access to standard respawn timer."""
53        val = self.customdata.get('respawn_timer', None)
54        assert isinstance(val, (bs.Timer, type(None)))
55        return val

Type safe access to standard respawn timer.

respawn_icon: bascenev1lib.actor.respawnicon.RespawnIcon | None
61    @property
62    def respawn_icon(self) -> RespawnIcon | None:
63        """Type safe access to standard respawn icon."""
64        val = self.customdata.get('respawn_icon', None)
65        assert isinstance(val, (RespawnIcon, type(None)))
66        return val

Type safe access to standard respawn icon.

class Team(bascenev1._team.Team[bascenev1lib.game.conquest.Player]):
73class Team(bs.Team[Player]):
74    """Our team type for this game."""
75
76    def __init__(self) -> None:
77        self.flags_held = 0

Our team type for this game.

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

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

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

Instantiate the Activity.

name = 'Conquest'
description = 'Secure all flags on the map to win.'
available_settings = [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)]
@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
113    @override
114    @classmethod
115    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
116        return issubclass(sessiontype, bs.DualTeamSession)

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]:
118    @override
119    @classmethod
120    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
121        assert bs.app.classic is not None
122        return bs.app.classic.getmaps('conquest')

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]:
150    @override
151    def get_instance_description(self) -> str | Sequence:
152        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.

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
154    @override
155    def get_instance_description_short(self) -> str | Sequence:
156        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.

@override
def on_team_join(self, team: Team) -> None:
158    @override
159    def on_team_join(self, team: Team) -> None:
160        if self.has_begun():
161            self._update_scores()

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

@override
def on_player_join(self, player: Player) -> None:
163    @override
164    def on_player_join(self, player: Player) -> None:
165        player.respawn_timer = None
166
167        # Only spawn if this player's team has a flag currently.
168        if player.team.flags_held > 0:
169            self.spawn_player(player)

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

(including the initial set of Players)

@override
def on_begin(self) -> None:
171    @override
172    def on_begin(self) -> None:
173        super().on_begin()
174        self.setup_standard_time_limit(self._time_limit)
175        self.setup_standard_powerup_drops()
176
177        # Set up flags with marker lights.
178        for i, flag_point in enumerate(self.map.flag_points):
179            point = flag_point
180            flag = ConquestFlag(
181                position=point, touchable=False, materials=[self._extraflagmat]
182            )
183            self._flags.append(flag)
184            Flag.project_stand(point)
185            flag.light = bs.newnode(
186                'light',
187                owner=flag.node,
188                attrs={
189                    'position': point,
190                    'intensity': 0.25,
191                    'height_attenuated': False,
192                    'radius': 0.3,
193                    'color': (1, 1, 1),
194                },
195            )
196
197        # Give teams a flag to start with.
198        for i, team in enumerate(self.teams):
199            self._flags[i].team = team
200            light = self._flags[i].light
201            assert light
202            node = self._flags[i].node
203            assert node
204            light.color = team.color
205            node.color = team.color
206
207        self._update_scores()
208
209        # Initial joiners didn't spawn due to no flags being owned yet;
210        # spawn them now.
211        for player in self.players:
212            self.spawn_player(player)

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:
233    @override
234    def end_game(self) -> None:
235        results = bs.GameResults()
236        for team in self.teams:
237            results.set_team_score(team, team.flags_held)
238        self.end(results=results)

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:
285    @override
286    def handlemessage(self, msg: Any) -> Any:
287        if isinstance(msg, bs.PlayerDiedMessage):
288            # Augment standard behavior.
289            super().handlemessage(msg)
290
291            # Respawn only if this team has a flag.
292            player = msg.getplayer(Player)
293            if player.team.flags_held > 0:
294                self.respawn_player(player)
295            else:
296                player.respawn_timer = None
297
298        else:
299            super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def spawn_player( self, player: Player) -> bascenev1.Actor:
301    @override
302    def spawn_player(self, player: Player) -> bs.Actor:
303        # We spawn players at different places based on what flags are held.
304        return self.spawn_player_spaz(
305            player, self._get_player_spawn_position(player)
306        )

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().