bastd.game.elimination

Elimination mini-game.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Elimination mini-game."""
  4
  5# ba_meta require api 7
  6# (see https://ballistica.net/wiki/meta-tag-system)
  7
  8from __future__ import annotations
  9
 10from typing import TYPE_CHECKING
 11
 12import ba
 13from bastd.actor.spazfactory import SpazFactory
 14from bastd.actor.scoreboard import Scoreboard
 15
 16if TYPE_CHECKING:
 17    from typing import Any, Sequence
 18
 19
 20class Icon(ba.Actor):
 21    """Creates in in-game icon on screen."""
 22
 23    def __init__(
 24        self,
 25        player: Player,
 26        position: tuple[float, float],
 27        scale: float,
 28        show_lives: bool = True,
 29        show_death: bool = True,
 30        name_scale: float = 1.0,
 31        name_maxwidth: float = 115.0,
 32        flatness: float = 1.0,
 33        shadow: float = 1.0,
 34    ):
 35        super().__init__()
 36
 37        self._player = player
 38        self._show_lives = show_lives
 39        self._show_death = show_death
 40        self._name_scale = name_scale
 41        self._outline_tex = ba.gettexture('characterIconMask')
 42
 43        icon = player.get_icon()
 44        self.node = ba.newnode(
 45            'image',
 46            delegate=self,
 47            attrs={
 48                'texture': icon['texture'],
 49                'tint_texture': icon['tint_texture'],
 50                'tint_color': icon['tint_color'],
 51                'vr_depth': 400,
 52                'tint2_color': icon['tint2_color'],
 53                'mask_texture': self._outline_tex,
 54                'opacity': 1.0,
 55                'absolute_scale': True,
 56                'attach': 'bottomCenter',
 57            },
 58        )
 59        self._name_text = ba.newnode(
 60            'text',
 61            owner=self.node,
 62            attrs={
 63                'text': ba.Lstr(value=player.getname()),
 64                'color': ba.safecolor(player.team.color),
 65                'h_align': 'center',
 66                'v_align': 'center',
 67                'vr_depth': 410,
 68                'maxwidth': name_maxwidth,
 69                'shadow': shadow,
 70                'flatness': flatness,
 71                'h_attach': 'center',
 72                'v_attach': 'bottom',
 73            },
 74        )
 75        if self._show_lives:
 76            self._lives_text = ba.newnode(
 77                'text',
 78                owner=self.node,
 79                attrs={
 80                    'text': 'x0',
 81                    'color': (1, 1, 0.5),
 82                    'h_align': 'left',
 83                    'vr_depth': 430,
 84                    'shadow': 1.0,
 85                    'flatness': 1.0,
 86                    'h_attach': 'center',
 87                    'v_attach': 'bottom',
 88                },
 89            )
 90        self.set_position_and_scale(position, scale)
 91
 92    def set_position_and_scale(
 93        self, position: tuple[float, float], scale: float
 94    ) -> None:
 95        """(Re)position the icon."""
 96        assert self.node
 97        self.node.position = position
 98        self.node.scale = [70.0 * scale]
 99        self._name_text.position = (position[0], position[1] + scale * 52.0)
100        self._name_text.scale = 1.0 * scale * self._name_scale
101        if self._show_lives:
102            self._lives_text.position = (
103                position[0] + scale * 10.0,
104                position[1] - scale * 43.0,
105            )
106            self._lives_text.scale = 1.0 * scale
107
108    def update_for_lives(self) -> None:
109        """Update for the target player's current lives."""
110        if self._player:
111            lives = self._player.lives
112        else:
113            lives = 0
114        if self._show_lives:
115            if lives > 0:
116                self._lives_text.text = 'x' + str(lives - 1)
117            else:
118                self._lives_text.text = ''
119        if lives == 0:
120            self._name_text.opacity = 0.2
121            assert self.node
122            self.node.color = (0.7, 0.3, 0.3)
123            self.node.opacity = 0.2
124
125    def handle_player_spawned(self) -> None:
126        """Our player spawned; hooray!"""
127        if not self.node:
128            return
129        self.node.opacity = 1.0
130        self.update_for_lives()
131
132    def handle_player_died(self) -> None:
133        """Well poo; our player died."""
134        if not self.node:
135            return
136        if self._show_death:
137            ba.animate(
138                self.node,
139                'opacity',
140                {
141                    0.00: 1.0,
142                    0.05: 0.0,
143                    0.10: 1.0,
144                    0.15: 0.0,
145                    0.20: 1.0,
146                    0.25: 0.0,
147                    0.30: 1.0,
148                    0.35: 0.0,
149                    0.40: 1.0,
150                    0.45: 0.0,
151                    0.50: 1.0,
152                    0.55: 0.2,
153                },
154            )
155            lives = self._player.lives
156            if lives == 0:
157                ba.timer(0.6, self.update_for_lives)
158
159    def handlemessage(self, msg: Any) -> Any:
160        if isinstance(msg, ba.DieMessage):
161            self.node.delete()
162            return None
163        return super().handlemessage(msg)
164
165
166class Player(ba.Player['Team']):
167    """Our player type for this game."""
168
169    def __init__(self) -> None:
170        self.lives = 0
171        self.icons: list[Icon] = []
172
173
174class Team(ba.Team[Player]):
175    """Our team type for this game."""
176
177    def __init__(self) -> None:
178        self.survival_seconds: int | None = None
179        self.spawn_order: list[Player] = []
180
181
182# ba_meta export game
183class EliminationGame(ba.TeamGameActivity[Player, Team]):
184    """Game type where last player(s) left alive win."""
185
186    name = 'Elimination'
187    description = 'Last remaining alive wins.'
188    scoreconfig = ba.ScoreConfig(
189        label='Survived', scoretype=ba.ScoreType.SECONDS, none_is_winner=True
190    )
191    # Show messages when players die since it's meaningful here.
192    announce_player_deaths = True
193
194    allow_mid_activity_joins = False
195
196    @classmethod
197    def get_available_settings(
198        cls, sessiontype: type[ba.Session]
199    ) -> list[ba.Setting]:
200        settings = [
201            ba.IntSetting(
202                'Lives Per Player',
203                default=1,
204                min_value=1,
205                max_value=10,
206                increment=1,
207            ),
208            ba.IntChoiceSetting(
209                'Time Limit',
210                choices=[
211                    ('None', 0),
212                    ('1 Minute', 60),
213                    ('2 Minutes', 120),
214                    ('5 Minutes', 300),
215                    ('10 Minutes', 600),
216                    ('20 Minutes', 1200),
217                ],
218                default=0,
219            ),
220            ba.FloatChoiceSetting(
221                'Respawn Times',
222                choices=[
223                    ('Shorter', 0.25),
224                    ('Short', 0.5),
225                    ('Normal', 1.0),
226                    ('Long', 2.0),
227                    ('Longer', 4.0),
228                ],
229                default=1.0,
230            ),
231            ba.BoolSetting('Epic Mode', default=False),
232        ]
233        if issubclass(sessiontype, ba.DualTeamSession):
234            settings.append(ba.BoolSetting('Solo Mode', default=False))
235            settings.append(
236                ba.BoolSetting('Balance Total Lives', default=False)
237            )
238        return settings
239
240    @classmethod
241    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
242        return issubclass(sessiontype, ba.DualTeamSession) or issubclass(
243            sessiontype, ba.FreeForAllSession
244        )
245
246    @classmethod
247    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
248        return ba.getmaps('melee')
249
250    def __init__(self, settings: dict):
251        super().__init__(settings)
252        self._scoreboard = Scoreboard()
253        self._start_time: float | None = None
254        self._vs_text: ba.Actor | None = None
255        self._round_end_timer: ba.Timer | None = None
256        self._epic_mode = bool(settings['Epic Mode'])
257        self._lives_per_player = int(settings['Lives Per Player'])
258        self._time_limit = float(settings['Time Limit'])
259        self._balance_total_lives = bool(
260            settings.get('Balance Total Lives', False)
261        )
262        self._solo_mode = bool(settings.get('Solo Mode', False))
263
264        # Base class overrides:
265        self.slow_motion = self._epic_mode
266        self.default_music = (
267            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL
268        )
269
270    def get_instance_description(self) -> str | Sequence:
271        return (
272            'Last team standing wins.'
273            if isinstance(self.session, ba.DualTeamSession)
274            else 'Last one standing wins.'
275        )
276
277    def get_instance_description_short(self) -> str | Sequence:
278        return (
279            'last team standing wins'
280            if isinstance(self.session, ba.DualTeamSession)
281            else 'last one standing wins'
282        )
283
284    def on_player_join(self, player: Player) -> None:
285        player.lives = self._lives_per_player
286
287        if self._solo_mode:
288            player.team.spawn_order.append(player)
289            self._update_solo_mode()
290        else:
291            # Create our icon and spawn.
292            player.icons = [Icon(player, position=(0, 50), scale=0.8)]
293            if player.lives > 0:
294                self.spawn_player(player)
295
296        # Don't waste time doing this until begin.
297        if self.has_begun():
298            self._update_icons()
299
300    def on_begin(self) -> None:
301        super().on_begin()
302        self._start_time = ba.time()
303        self.setup_standard_time_limit(self._time_limit)
304        self.setup_standard_powerup_drops()
305        if self._solo_mode:
306            self._vs_text = ba.NodeActor(
307                ba.newnode(
308                    'text',
309                    attrs={
310                        'position': (0, 105),
311                        'h_attach': 'center',
312                        'h_align': 'center',
313                        'maxwidth': 200,
314                        'shadow': 0.5,
315                        'vr_depth': 390,
316                        'scale': 0.6,
317                        'v_attach': 'bottom',
318                        'color': (0.8, 0.8, 0.3, 1.0),
319                        'text': ba.Lstr(resource='vsText'),
320                    },
321                )
322            )
323
324        # If balance-team-lives is on, add lives to the smaller team until
325        # total lives match.
326        if (
327            isinstance(self.session, ba.DualTeamSession)
328            and self._balance_total_lives
329            and self.teams[0].players
330            and self.teams[1].players
331        ):
332            if self._get_total_team_lives(
333                self.teams[0]
334            ) < self._get_total_team_lives(self.teams[1]):
335                lesser_team = self.teams[0]
336                greater_team = self.teams[1]
337            else:
338                lesser_team = self.teams[1]
339                greater_team = self.teams[0]
340            add_index = 0
341            while self._get_total_team_lives(
342                lesser_team
343            ) < self._get_total_team_lives(greater_team):
344                lesser_team.players[add_index].lives += 1
345                add_index = (add_index + 1) % len(lesser_team.players)
346
347        self._update_icons()
348
349        # We could check game-over conditions at explicit trigger points,
350        # but lets just do the simple thing and poll it.
351        ba.timer(1.0, self._update, repeat=True)
352
353    def _update_solo_mode(self) -> None:
354        # For both teams, find the first player on the spawn order list with
355        # lives remaining and spawn them if they're not alive.
356        for team in self.teams:
357            # Prune dead players from the spawn order.
358            team.spawn_order = [p for p in team.spawn_order if p]
359            for player in team.spawn_order:
360                assert isinstance(player, Player)
361                if player.lives > 0:
362                    if not player.is_alive():
363                        self.spawn_player(player)
364                    break
365
366    def _update_icons(self) -> None:
367        # pylint: disable=too-many-branches
368
369        # In free-for-all mode, everyone is just lined up along the bottom.
370        if isinstance(self.session, ba.FreeForAllSession):
371            count = len(self.teams)
372            x_offs = 85
373            xval = x_offs * (count - 1) * -0.5
374            for team in self.teams:
375                if len(team.players) == 1:
376                    player = team.players[0]
377                    for icon in player.icons:
378                        icon.set_position_and_scale((xval, 30), 0.7)
379                        icon.update_for_lives()
380                    xval += x_offs
381
382        # In teams mode we split up teams.
383        else:
384            if self._solo_mode:
385                # First off, clear out all icons.
386                for player in self.players:
387                    player.icons = []
388
389                # Now for each team, cycle through our available players
390                # adding icons.
391                for team in self.teams:
392                    if team.id == 0:
393                        xval = -60
394                        x_offs = -78
395                    else:
396                        xval = 60
397                        x_offs = 78
398                    is_first = True
399                    test_lives = 1
400                    while True:
401                        players_with_lives = [
402                            p
403                            for p in team.spawn_order
404                            if p and p.lives >= test_lives
405                        ]
406                        if not players_with_lives:
407                            break
408                        for player in players_with_lives:
409                            player.icons.append(
410                                Icon(
411                                    player,
412                                    position=(xval, (40 if is_first else 25)),
413                                    scale=1.0 if is_first else 0.5,
414                                    name_maxwidth=130 if is_first else 75,
415                                    name_scale=0.8 if is_first else 1.0,
416                                    flatness=0.0 if is_first else 1.0,
417                                    shadow=0.5 if is_first else 1.0,
418                                    show_death=is_first,
419                                    show_lives=False,
420                                )
421                            )
422                            xval += x_offs * (0.8 if is_first else 0.56)
423                            is_first = False
424                        test_lives += 1
425            # Non-solo mode.
426            else:
427                for team in self.teams:
428                    if team.id == 0:
429                        xval = -50
430                        x_offs = -85
431                    else:
432                        xval = 50
433                        x_offs = 85
434                    for player in team.players:
435                        for icon in player.icons:
436                            icon.set_position_and_scale((xval, 30), 0.7)
437                            icon.update_for_lives()
438                        xval += x_offs
439
440    def _get_spawn_point(self, player: Player) -> ba.Vec3 | None:
441        del player  # Unused.
442
443        # In solo-mode, if there's an existing live player on the map, spawn at
444        # whichever spot is farthest from them (keeps the action spread out).
445        if self._solo_mode:
446            living_player = None
447            living_player_pos = None
448            for team in self.teams:
449                for tplayer in team.players:
450                    if tplayer.is_alive():
451                        assert tplayer.node
452                        ppos = tplayer.node.position
453                        living_player = tplayer
454                        living_player_pos = ppos
455                        break
456            if living_player:
457                assert living_player_pos is not None
458                player_pos = ba.Vec3(living_player_pos)
459                points: list[tuple[float, ba.Vec3]] = []
460                for team in self.teams:
461                    start_pos = ba.Vec3(self.map.get_start_position(team.id))
462                    points.append(
463                        ((start_pos - player_pos).length(), start_pos)
464                    )
465                # Hmm.. we need to sorting vectors too?
466                points.sort(key=lambda x: x[0])
467                return points[-1][1]
468        return None
469
470    def spawn_player(self, player: Player) -> ba.Actor:
471        actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
472        if not self._solo_mode:
473            ba.timer(0.3, ba.Call(self._print_lives, player))
474
475        # If we have any icons, update their state.
476        for icon in player.icons:
477            icon.handle_player_spawned()
478        return actor
479
480    def _print_lives(self, player: Player) -> None:
481        from bastd.actor import popuptext
482
483        # We get called in a timer so it's possible our player has left/etc.
484        if not player or not player.is_alive() or not player.node:
485            return
486
487        popuptext.PopupText(
488            'x' + str(player.lives - 1),
489            color=(1, 1, 0, 1),
490            offset=(0, -0.8, 0),
491            random_offset=0.0,
492            scale=1.8,
493            position=player.node.position,
494        ).autoretain()
495
496    def on_player_leave(self, player: Player) -> None:
497        super().on_player_leave(player)
498        player.icons = []
499
500        # Remove us from spawn-order.
501        if self._solo_mode:
502            if player in player.team.spawn_order:
503                player.team.spawn_order.remove(player)
504
505        # Update icons in a moment since our team will be gone from the
506        # list then.
507        ba.timer(0, self._update_icons)
508
509        # If the player to leave was the last in spawn order and had
510        # their final turn currently in-progress, mark the survival time
511        # for their team.
512        if self._get_total_team_lives(player.team) == 0:
513            assert self._start_time is not None
514            player.team.survival_seconds = int(ba.time() - self._start_time)
515
516    def _get_total_team_lives(self, team: Team) -> int:
517        return sum(player.lives for player in team.players)
518
519    def handlemessage(self, msg: Any) -> Any:
520        if isinstance(msg, ba.PlayerDiedMessage):
521
522            # Augment standard behavior.
523            super().handlemessage(msg)
524            player: Player = msg.getplayer(Player)
525
526            player.lives -= 1
527            if player.lives < 0:
528                ba.print_error(
529                    "Got lives < 0 in Elim; this shouldn't happen. solo:"
530                    + str(self._solo_mode)
531                )
532                player.lives = 0
533
534            # If we have any icons, update their state.
535            for icon in player.icons:
536                icon.handle_player_died()
537
538            # Play big death sound on our last death
539            # or for every one in solo mode.
540            if self._solo_mode or player.lives == 0:
541                ba.playsound(SpazFactory.get().single_player_death_sound)
542
543            # If we hit zero lives, we're dead (and our team might be too).
544            if player.lives == 0:
545                # If the whole team is now dead, mark their survival time.
546                if self._get_total_team_lives(player.team) == 0:
547                    assert self._start_time is not None
548                    player.team.survival_seconds = int(
549                        ba.time() - self._start_time
550                    )
551            else:
552                # Otherwise, in regular mode, respawn.
553                if not self._solo_mode:
554                    self.respawn_player(player)
555
556            # In solo, put ourself at the back of the spawn order.
557            if self._solo_mode:
558                player.team.spawn_order.remove(player)
559                player.team.spawn_order.append(player)
560
561    def _update(self) -> None:
562        if self._solo_mode:
563            # For both teams, find the first player on the spawn order
564            # list with lives remaining and spawn them if they're not alive.
565            for team in self.teams:
566                # Prune dead players from the spawn order.
567                team.spawn_order = [p for p in team.spawn_order if p]
568                for player in team.spawn_order:
569                    assert isinstance(player, Player)
570                    if player.lives > 0:
571                        if not player.is_alive():
572                            self.spawn_player(player)
573                            self._update_icons()
574                        break
575
576        # If we're down to 1 or fewer living teams, start a timer to end
577        # the game (allows the dust to settle and draws to occur if deaths
578        # are close enough).
579        if len(self._get_living_teams()) < 2:
580            self._round_end_timer = ba.Timer(0.5, self.end_game)
581
582    def _get_living_teams(self) -> list[Team]:
583        return [
584            team
585            for team in self.teams
586            if len(team.players) > 0
587            and any(player.lives > 0 for player in team.players)
588        ]
589
590    def end_game(self) -> None:
591        if self.has_ended():
592            return
593        results = ba.GameResults()
594        self._vs_text = None  # Kill our 'vs' if its there.
595        for team in self.teams:
596            results.set_team_score(team, team.survival_seconds)
597        self.end(results=results)
class Icon(ba._actor.Actor):
 21class Icon(ba.Actor):
 22    """Creates in in-game icon on screen."""
 23
 24    def __init__(
 25        self,
 26        player: Player,
 27        position: tuple[float, float],
 28        scale: float,
 29        show_lives: bool = True,
 30        show_death: bool = True,
 31        name_scale: float = 1.0,
 32        name_maxwidth: float = 115.0,
 33        flatness: float = 1.0,
 34        shadow: float = 1.0,
 35    ):
 36        super().__init__()
 37
 38        self._player = player
 39        self._show_lives = show_lives
 40        self._show_death = show_death
 41        self._name_scale = name_scale
 42        self._outline_tex = ba.gettexture('characterIconMask')
 43
 44        icon = player.get_icon()
 45        self.node = ba.newnode(
 46            'image',
 47            delegate=self,
 48            attrs={
 49                'texture': icon['texture'],
 50                'tint_texture': icon['tint_texture'],
 51                'tint_color': icon['tint_color'],
 52                'vr_depth': 400,
 53                'tint2_color': icon['tint2_color'],
 54                'mask_texture': self._outline_tex,
 55                'opacity': 1.0,
 56                'absolute_scale': True,
 57                'attach': 'bottomCenter',
 58            },
 59        )
 60        self._name_text = ba.newnode(
 61            'text',
 62            owner=self.node,
 63            attrs={
 64                'text': ba.Lstr(value=player.getname()),
 65                'color': ba.safecolor(player.team.color),
 66                'h_align': 'center',
 67                'v_align': 'center',
 68                'vr_depth': 410,
 69                'maxwidth': name_maxwidth,
 70                'shadow': shadow,
 71                'flatness': flatness,
 72                'h_attach': 'center',
 73                'v_attach': 'bottom',
 74            },
 75        )
 76        if self._show_lives:
 77            self._lives_text = ba.newnode(
 78                'text',
 79                owner=self.node,
 80                attrs={
 81                    'text': 'x0',
 82                    'color': (1, 1, 0.5),
 83                    'h_align': 'left',
 84                    'vr_depth': 430,
 85                    'shadow': 1.0,
 86                    'flatness': 1.0,
 87                    'h_attach': 'center',
 88                    'v_attach': 'bottom',
 89                },
 90            )
 91        self.set_position_and_scale(position, scale)
 92
 93    def set_position_and_scale(
 94        self, position: tuple[float, float], scale: float
 95    ) -> None:
 96        """(Re)position the icon."""
 97        assert self.node
 98        self.node.position = position
 99        self.node.scale = [70.0 * scale]
100        self._name_text.position = (position[0], position[1] + scale * 52.0)
101        self._name_text.scale = 1.0 * scale * self._name_scale
102        if self._show_lives:
103            self._lives_text.position = (
104                position[0] + scale * 10.0,
105                position[1] - scale * 43.0,
106            )
107            self._lives_text.scale = 1.0 * scale
108
109    def update_for_lives(self) -> None:
110        """Update for the target player's current lives."""
111        if self._player:
112            lives = self._player.lives
113        else:
114            lives = 0
115        if self._show_lives:
116            if lives > 0:
117                self._lives_text.text = 'x' + str(lives - 1)
118            else:
119                self._lives_text.text = ''
120        if lives == 0:
121            self._name_text.opacity = 0.2
122            assert self.node
123            self.node.color = (0.7, 0.3, 0.3)
124            self.node.opacity = 0.2
125
126    def handle_player_spawned(self) -> None:
127        """Our player spawned; hooray!"""
128        if not self.node:
129            return
130        self.node.opacity = 1.0
131        self.update_for_lives()
132
133    def handle_player_died(self) -> None:
134        """Well poo; our player died."""
135        if not self.node:
136            return
137        if self._show_death:
138            ba.animate(
139                self.node,
140                'opacity',
141                {
142                    0.00: 1.0,
143                    0.05: 0.0,
144                    0.10: 1.0,
145                    0.15: 0.0,
146                    0.20: 1.0,
147                    0.25: 0.0,
148                    0.30: 1.0,
149                    0.35: 0.0,
150                    0.40: 1.0,
151                    0.45: 0.0,
152                    0.50: 1.0,
153                    0.55: 0.2,
154                },
155            )
156            lives = self._player.lives
157            if lives == 0:
158                ba.timer(0.6, self.update_for_lives)
159
160    def handlemessage(self, msg: Any) -> Any:
161        if isinstance(msg, ba.DieMessage):
162            self.node.delete()
163            return None
164        return super().handlemessage(msg)

Creates in in-game icon on screen.

Icon( player: bastd.game.elimination.Player, position: tuple[float, float], scale: float, show_lives: bool = True, show_death: bool = True, name_scale: float = 1.0, name_maxwidth: float = 115.0, flatness: float = 1.0, shadow: float = 1.0)
24    def __init__(
25        self,
26        player: Player,
27        position: tuple[float, float],
28        scale: float,
29        show_lives: bool = True,
30        show_death: bool = True,
31        name_scale: float = 1.0,
32        name_maxwidth: float = 115.0,
33        flatness: float = 1.0,
34        shadow: float = 1.0,
35    ):
36        super().__init__()
37
38        self._player = player
39        self._show_lives = show_lives
40        self._show_death = show_death
41        self._name_scale = name_scale
42        self._outline_tex = ba.gettexture('characterIconMask')
43
44        icon = player.get_icon()
45        self.node = ba.newnode(
46            'image',
47            delegate=self,
48            attrs={
49                'texture': icon['texture'],
50                'tint_texture': icon['tint_texture'],
51                'tint_color': icon['tint_color'],
52                'vr_depth': 400,
53                'tint2_color': icon['tint2_color'],
54                'mask_texture': self._outline_tex,
55                'opacity': 1.0,
56                'absolute_scale': True,
57                'attach': 'bottomCenter',
58            },
59        )
60        self._name_text = ba.newnode(
61            'text',
62            owner=self.node,
63            attrs={
64                'text': ba.Lstr(value=player.getname()),
65                'color': ba.safecolor(player.team.color),
66                'h_align': 'center',
67                'v_align': 'center',
68                'vr_depth': 410,
69                'maxwidth': name_maxwidth,
70                'shadow': shadow,
71                'flatness': flatness,
72                'h_attach': 'center',
73                'v_attach': 'bottom',
74            },
75        )
76        if self._show_lives:
77            self._lives_text = ba.newnode(
78                'text',
79                owner=self.node,
80                attrs={
81                    'text': 'x0',
82                    'color': (1, 1, 0.5),
83                    'h_align': 'left',
84                    'vr_depth': 430,
85                    'shadow': 1.0,
86                    'flatness': 1.0,
87                    'h_attach': 'center',
88                    'v_attach': 'bottom',
89                },
90            )
91        self.set_position_and_scale(position, scale)

Instantiates an Actor in the current ba.Activity.

def set_position_and_scale(self, position: tuple[float, float], scale: float) -> None:
 93    def set_position_and_scale(
 94        self, position: tuple[float, float], scale: float
 95    ) -> None:
 96        """(Re)position the icon."""
 97        assert self.node
 98        self.node.position = position
 99        self.node.scale = [70.0 * scale]
100        self._name_text.position = (position[0], position[1] + scale * 52.0)
101        self._name_text.scale = 1.0 * scale * self._name_scale
102        if self._show_lives:
103            self._lives_text.position = (
104                position[0] + scale * 10.0,
105                position[1] - scale * 43.0,
106            )
107            self._lives_text.scale = 1.0 * scale

(Re)position the icon.

def update_for_lives(self) -> None:
109    def update_for_lives(self) -> None:
110        """Update for the target player's current lives."""
111        if self._player:
112            lives = self._player.lives
113        else:
114            lives = 0
115        if self._show_lives:
116            if lives > 0:
117                self._lives_text.text = 'x' + str(lives - 1)
118            else:
119                self._lives_text.text = ''
120        if lives == 0:
121            self._name_text.opacity = 0.2
122            assert self.node
123            self.node.color = (0.7, 0.3, 0.3)
124            self.node.opacity = 0.2

Update for the target player's current lives.

def handle_player_spawned(self) -> None:
126    def handle_player_spawned(self) -> None:
127        """Our player spawned; hooray!"""
128        if not self.node:
129            return
130        self.node.opacity = 1.0
131        self.update_for_lives()

Our player spawned; hooray!

def handle_player_died(self) -> None:
133    def handle_player_died(self) -> None:
134        """Well poo; our player died."""
135        if not self.node:
136            return
137        if self._show_death:
138            ba.animate(
139                self.node,
140                'opacity',
141                {
142                    0.00: 1.0,
143                    0.05: 0.0,
144                    0.10: 1.0,
145                    0.15: 0.0,
146                    0.20: 1.0,
147                    0.25: 0.0,
148                    0.30: 1.0,
149                    0.35: 0.0,
150                    0.40: 1.0,
151                    0.45: 0.0,
152                    0.50: 1.0,
153                    0.55: 0.2,
154                },
155            )
156            lives = self._player.lives
157            if lives == 0:
158                ba.timer(0.6, self.update_for_lives)

Well poo; our player died.

def handlemessage(self, msg: Any) -> Any:
160    def handlemessage(self, msg: Any) -> Any:
161        if isinstance(msg, ba.DieMessage):
162            self.node.delete()
163            return None
164        return super().handlemessage(msg)

General message handling; can be passed any message object.

Inherited Members
ba._actor.Actor
autoretain
on_expire
expired
exists
is_alive
activity
getactivity
class Player(ba._player.Player[ForwardRef('Team')]):
167class Player(ba.Player['Team']):
168    """Our player type for this game."""
169
170    def __init__(self) -> None:
171        self.lives = 0
172        self.icons: list[Icon] = []

Our player type for this game.

Player()
170    def __init__(self) -> None:
171        self.lives = 0
172        self.icons: list[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.elimination.Player]):
175class Team(ba.Team[Player]):
176    """Our team type for this game."""
177
178    def __init__(self) -> None:
179        self.survival_seconds: int | None = None
180        self.spawn_order: list[Player] = []

Our team type for this game.

Team()
178    def __init__(self) -> None:
179        self.survival_seconds: int | None = None
180        self.spawn_order: list[Player] = []
Inherited Members
ba._team.Team
manual_init
customdata
on_expire
sessionteam
class EliminationGame(ba._teamgame.TeamGameActivity[bastd.game.elimination.Player, bastd.game.elimination.Team]):
184class EliminationGame(ba.TeamGameActivity[Player, Team]):
185    """Game type where last player(s) left alive win."""
186
187    name = 'Elimination'
188    description = 'Last remaining alive wins.'
189    scoreconfig = ba.ScoreConfig(
190        label='Survived', scoretype=ba.ScoreType.SECONDS, none_is_winner=True
191    )
192    # Show messages when players die since it's meaningful here.
193    announce_player_deaths = True
194
195    allow_mid_activity_joins = False
196
197    @classmethod
198    def get_available_settings(
199        cls, sessiontype: type[ba.Session]
200    ) -> list[ba.Setting]:
201        settings = [
202            ba.IntSetting(
203                'Lives Per Player',
204                default=1,
205                min_value=1,
206                max_value=10,
207                increment=1,
208            ),
209            ba.IntChoiceSetting(
210                'Time Limit',
211                choices=[
212                    ('None', 0),
213                    ('1 Minute', 60),
214                    ('2 Minutes', 120),
215                    ('5 Minutes', 300),
216                    ('10 Minutes', 600),
217                    ('20 Minutes', 1200),
218                ],
219                default=0,
220            ),
221            ba.FloatChoiceSetting(
222                'Respawn Times',
223                choices=[
224                    ('Shorter', 0.25),
225                    ('Short', 0.5),
226                    ('Normal', 1.0),
227                    ('Long', 2.0),
228                    ('Longer', 4.0),
229                ],
230                default=1.0,
231            ),
232            ba.BoolSetting('Epic Mode', default=False),
233        ]
234        if issubclass(sessiontype, ba.DualTeamSession):
235            settings.append(ba.BoolSetting('Solo Mode', default=False))
236            settings.append(
237                ba.BoolSetting('Balance Total Lives', default=False)
238            )
239        return settings
240
241    @classmethod
242    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
243        return issubclass(sessiontype, ba.DualTeamSession) or issubclass(
244            sessiontype, ba.FreeForAllSession
245        )
246
247    @classmethod
248    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
249        return ba.getmaps('melee')
250
251    def __init__(self, settings: dict):
252        super().__init__(settings)
253        self._scoreboard = Scoreboard()
254        self._start_time: float | None = None
255        self._vs_text: ba.Actor | None = None
256        self._round_end_timer: ba.Timer | None = None
257        self._epic_mode = bool(settings['Epic Mode'])
258        self._lives_per_player = int(settings['Lives Per Player'])
259        self._time_limit = float(settings['Time Limit'])
260        self._balance_total_lives = bool(
261            settings.get('Balance Total Lives', False)
262        )
263        self._solo_mode = bool(settings.get('Solo Mode', False))
264
265        # Base class overrides:
266        self.slow_motion = self._epic_mode
267        self.default_music = (
268            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL
269        )
270
271    def get_instance_description(self) -> str | Sequence:
272        return (
273            'Last team standing wins.'
274            if isinstance(self.session, ba.DualTeamSession)
275            else 'Last one standing wins.'
276        )
277
278    def get_instance_description_short(self) -> str | Sequence:
279        return (
280            'last team standing wins'
281            if isinstance(self.session, ba.DualTeamSession)
282            else 'last one standing wins'
283        )
284
285    def on_player_join(self, player: Player) -> None:
286        player.lives = self._lives_per_player
287
288        if self._solo_mode:
289            player.team.spawn_order.append(player)
290            self._update_solo_mode()
291        else:
292            # Create our icon and spawn.
293            player.icons = [Icon(player, position=(0, 50), scale=0.8)]
294            if player.lives > 0:
295                self.spawn_player(player)
296
297        # Don't waste time doing this until begin.
298        if self.has_begun():
299            self._update_icons()
300
301    def on_begin(self) -> None:
302        super().on_begin()
303        self._start_time = ba.time()
304        self.setup_standard_time_limit(self._time_limit)
305        self.setup_standard_powerup_drops()
306        if self._solo_mode:
307            self._vs_text = ba.NodeActor(
308                ba.newnode(
309                    'text',
310                    attrs={
311                        'position': (0, 105),
312                        'h_attach': 'center',
313                        'h_align': 'center',
314                        'maxwidth': 200,
315                        'shadow': 0.5,
316                        'vr_depth': 390,
317                        'scale': 0.6,
318                        'v_attach': 'bottom',
319                        'color': (0.8, 0.8, 0.3, 1.0),
320                        'text': ba.Lstr(resource='vsText'),
321                    },
322                )
323            )
324
325        # If balance-team-lives is on, add lives to the smaller team until
326        # total lives match.
327        if (
328            isinstance(self.session, ba.DualTeamSession)
329            and self._balance_total_lives
330            and self.teams[0].players
331            and self.teams[1].players
332        ):
333            if self._get_total_team_lives(
334                self.teams[0]
335            ) < self._get_total_team_lives(self.teams[1]):
336                lesser_team = self.teams[0]
337                greater_team = self.teams[1]
338            else:
339                lesser_team = self.teams[1]
340                greater_team = self.teams[0]
341            add_index = 0
342            while self._get_total_team_lives(
343                lesser_team
344            ) < self._get_total_team_lives(greater_team):
345                lesser_team.players[add_index].lives += 1
346                add_index = (add_index + 1) % len(lesser_team.players)
347
348        self._update_icons()
349
350        # We could check game-over conditions at explicit trigger points,
351        # but lets just do the simple thing and poll it.
352        ba.timer(1.0, self._update, repeat=True)
353
354    def _update_solo_mode(self) -> None:
355        # For both teams, find the first player on the spawn order list with
356        # lives remaining and spawn them if they're not alive.
357        for team in self.teams:
358            # Prune dead players from the spawn order.
359            team.spawn_order = [p for p in team.spawn_order if p]
360            for player in team.spawn_order:
361                assert isinstance(player, Player)
362                if player.lives > 0:
363                    if not player.is_alive():
364                        self.spawn_player(player)
365                    break
366
367    def _update_icons(self) -> None:
368        # pylint: disable=too-many-branches
369
370        # In free-for-all mode, everyone is just lined up along the bottom.
371        if isinstance(self.session, ba.FreeForAllSession):
372            count = len(self.teams)
373            x_offs = 85
374            xval = x_offs * (count - 1) * -0.5
375            for team in self.teams:
376                if len(team.players) == 1:
377                    player = team.players[0]
378                    for icon in player.icons:
379                        icon.set_position_and_scale((xval, 30), 0.7)
380                        icon.update_for_lives()
381                    xval += x_offs
382
383        # In teams mode we split up teams.
384        else:
385            if self._solo_mode:
386                # First off, clear out all icons.
387                for player in self.players:
388                    player.icons = []
389
390                # Now for each team, cycle through our available players
391                # adding icons.
392                for team in self.teams:
393                    if team.id == 0:
394                        xval = -60
395                        x_offs = -78
396                    else:
397                        xval = 60
398                        x_offs = 78
399                    is_first = True
400                    test_lives = 1
401                    while True:
402                        players_with_lives = [
403                            p
404                            for p in team.spawn_order
405                            if p and p.lives >= test_lives
406                        ]
407                        if not players_with_lives:
408                            break
409                        for player in players_with_lives:
410                            player.icons.append(
411                                Icon(
412                                    player,
413                                    position=(xval, (40 if is_first else 25)),
414                                    scale=1.0 if is_first else 0.5,
415                                    name_maxwidth=130 if is_first else 75,
416                                    name_scale=0.8 if is_first else 1.0,
417                                    flatness=0.0 if is_first else 1.0,
418                                    shadow=0.5 if is_first else 1.0,
419                                    show_death=is_first,
420                                    show_lives=False,
421                                )
422                            )
423                            xval += x_offs * (0.8 if is_first else 0.56)
424                            is_first = False
425                        test_lives += 1
426            # Non-solo mode.
427            else:
428                for team in self.teams:
429                    if team.id == 0:
430                        xval = -50
431                        x_offs = -85
432                    else:
433                        xval = 50
434                        x_offs = 85
435                    for player in team.players:
436                        for icon in player.icons:
437                            icon.set_position_and_scale((xval, 30), 0.7)
438                            icon.update_for_lives()
439                        xval += x_offs
440
441    def _get_spawn_point(self, player: Player) -> ba.Vec3 | None:
442        del player  # Unused.
443
444        # In solo-mode, if there's an existing live player on the map, spawn at
445        # whichever spot is farthest from them (keeps the action spread out).
446        if self._solo_mode:
447            living_player = None
448            living_player_pos = None
449            for team in self.teams:
450                for tplayer in team.players:
451                    if tplayer.is_alive():
452                        assert tplayer.node
453                        ppos = tplayer.node.position
454                        living_player = tplayer
455                        living_player_pos = ppos
456                        break
457            if living_player:
458                assert living_player_pos is not None
459                player_pos = ba.Vec3(living_player_pos)
460                points: list[tuple[float, ba.Vec3]] = []
461                for team in self.teams:
462                    start_pos = ba.Vec3(self.map.get_start_position(team.id))
463                    points.append(
464                        ((start_pos - player_pos).length(), start_pos)
465                    )
466                # Hmm.. we need to sorting vectors too?
467                points.sort(key=lambda x: x[0])
468                return points[-1][1]
469        return None
470
471    def spawn_player(self, player: Player) -> ba.Actor:
472        actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
473        if not self._solo_mode:
474            ba.timer(0.3, ba.Call(self._print_lives, player))
475
476        # If we have any icons, update their state.
477        for icon in player.icons:
478            icon.handle_player_spawned()
479        return actor
480
481    def _print_lives(self, player: Player) -> None:
482        from bastd.actor import popuptext
483
484        # We get called in a timer so it's possible our player has left/etc.
485        if not player or not player.is_alive() or not player.node:
486            return
487
488        popuptext.PopupText(
489            'x' + str(player.lives - 1),
490            color=(1, 1, 0, 1),
491            offset=(0, -0.8, 0),
492            random_offset=0.0,
493            scale=1.8,
494            position=player.node.position,
495        ).autoretain()
496
497    def on_player_leave(self, player: Player) -> None:
498        super().on_player_leave(player)
499        player.icons = []
500
501        # Remove us from spawn-order.
502        if self._solo_mode:
503            if player in player.team.spawn_order:
504                player.team.spawn_order.remove(player)
505
506        # Update icons in a moment since our team will be gone from the
507        # list then.
508        ba.timer(0, self._update_icons)
509
510        # If the player to leave was the last in spawn order and had
511        # their final turn currently in-progress, mark the survival time
512        # for their team.
513        if self._get_total_team_lives(player.team) == 0:
514            assert self._start_time is not None
515            player.team.survival_seconds = int(ba.time() - self._start_time)
516
517    def _get_total_team_lives(self, team: Team) -> int:
518        return sum(player.lives for player in team.players)
519
520    def handlemessage(self, msg: Any) -> Any:
521        if isinstance(msg, ba.PlayerDiedMessage):
522
523            # Augment standard behavior.
524            super().handlemessage(msg)
525            player: Player = msg.getplayer(Player)
526
527            player.lives -= 1
528            if player.lives < 0:
529                ba.print_error(
530                    "Got lives < 0 in Elim; this shouldn't happen. solo:"
531                    + str(self._solo_mode)
532                )
533                player.lives = 0
534
535            # If we have any icons, update their state.
536            for icon in player.icons:
537                icon.handle_player_died()
538
539            # Play big death sound on our last death
540            # or for every one in solo mode.
541            if self._solo_mode or player.lives == 0:
542                ba.playsound(SpazFactory.get().single_player_death_sound)
543
544            # If we hit zero lives, we're dead (and our team might be too).
545            if player.lives == 0:
546                # If the whole team is now dead, mark their survival time.
547                if self._get_total_team_lives(player.team) == 0:
548                    assert self._start_time is not None
549                    player.team.survival_seconds = int(
550                        ba.time() - self._start_time
551                    )
552            else:
553                # Otherwise, in regular mode, respawn.
554                if not self._solo_mode:
555                    self.respawn_player(player)
556
557            # In solo, put ourself at the back of the spawn order.
558            if self._solo_mode:
559                player.team.spawn_order.remove(player)
560                player.team.spawn_order.append(player)
561
562    def _update(self) -> None:
563        if self._solo_mode:
564            # For both teams, find the first player on the spawn order
565            # list with lives remaining and spawn them if they're not alive.
566            for team in self.teams:
567                # Prune dead players from the spawn order.
568                team.spawn_order = [p for p in team.spawn_order if p]
569                for player in team.spawn_order:
570                    assert isinstance(player, Player)
571                    if player.lives > 0:
572                        if not player.is_alive():
573                            self.spawn_player(player)
574                            self._update_icons()
575                        break
576
577        # If we're down to 1 or fewer living teams, start a timer to end
578        # the game (allows the dust to settle and draws to occur if deaths
579        # are close enough).
580        if len(self._get_living_teams()) < 2:
581            self._round_end_timer = ba.Timer(0.5, self.end_game)
582
583    def _get_living_teams(self) -> list[Team]:
584        return [
585            team
586            for team in self.teams
587            if len(team.players) > 0
588            and any(player.lives > 0 for player in team.players)
589        ]
590
591    def end_game(self) -> None:
592        if self.has_ended():
593            return
594        results = ba.GameResults()
595        self._vs_text = None  # Kill our 'vs' if its there.
596        for team in self.teams:
597            results.set_team_score(team, team.survival_seconds)
598        self.end(results=results)

Game type where last player(s) left alive win.

EliminationGame(settings: dict)
251    def __init__(self, settings: dict):
252        super().__init__(settings)
253        self._scoreboard = Scoreboard()
254        self._start_time: float | None = None
255        self._vs_text: ba.Actor | None = None
256        self._round_end_timer: ba.Timer | None = None
257        self._epic_mode = bool(settings['Epic Mode'])
258        self._lives_per_player = int(settings['Lives Per Player'])
259        self._time_limit = float(settings['Time Limit'])
260        self._balance_total_lives = bool(
261            settings.get('Balance Total Lives', False)
262        )
263        self._solo_mode = bool(settings.get('Solo Mode', False))
264
265        # Base class overrides:
266        self.slow_motion = self._epic_mode
267        self.default_music = (
268            ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL
269        )

Instantiate the Activity.

announce_player_deaths = True

Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.

allow_mid_activity_joins = False

Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.

@classmethod
def get_available_settings( cls, sessiontype: type[ba._session.Session]) -> list[ba._settings.Setting]:
197    @classmethod
198    def get_available_settings(
199        cls, sessiontype: type[ba.Session]
200    ) -> list[ba.Setting]:
201        settings = [
202            ba.IntSetting(
203                'Lives Per Player',
204                default=1,
205                min_value=1,
206                max_value=10,
207                increment=1,
208            ),
209            ba.IntChoiceSetting(
210                'Time Limit',
211                choices=[
212                    ('None', 0),
213                    ('1 Minute', 60),
214                    ('2 Minutes', 120),
215                    ('5 Minutes', 300),
216                    ('10 Minutes', 600),
217                    ('20 Minutes', 1200),
218                ],
219                default=0,
220            ),
221            ba.FloatChoiceSetting(
222                'Respawn Times',
223                choices=[
224                    ('Shorter', 0.25),
225                    ('Short', 0.5),
226                    ('Normal', 1.0),
227                    ('Long', 2.0),
228                    ('Longer', 4.0),
229                ],
230                default=1.0,
231            ),
232            ba.BoolSetting('Epic Mode', default=False),
233        ]
234        if issubclass(sessiontype, ba.DualTeamSession):
235            settings.append(ba.BoolSetting('Solo Mode', default=False))
236            settings.append(
237                ba.BoolSetting('Balance Total Lives', default=False)
238            )
239        return settings

Return a list of settings relevant to this game type when running under the provided session type.

@classmethod
def supports_session_type(cls, sessiontype: type[ba._session.Session]) -> bool:
241    @classmethod
242    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
243        return issubclass(sessiontype, ba.DualTeamSession) or issubclass(
244            sessiontype, ba.FreeForAllSession
245        )

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]:
247    @classmethod
248    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
249        return ba.getmaps('melee')

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]:
271    def get_instance_description(self) -> str | Sequence:
272        return (
273            'Last team standing wins.'
274            if isinstance(self.session, ba.DualTeamSession)
275            else 'Last one standing wins.'
276        )

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]:
278    def get_instance_description_short(self) -> str | Sequence:
279        return (
280            'last team standing wins'
281            if isinstance(self.session, ba.DualTeamSession)
282            else 'last one standing wins'
283        )

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_player_join(self, player: bastd.game.elimination.Player) -> None:
285    def on_player_join(self, player: Player) -> None:
286        player.lives = self._lives_per_player
287
288        if self._solo_mode:
289            player.team.spawn_order.append(player)
290            self._update_solo_mode()
291        else:
292            # Create our icon and spawn.
293            player.icons = [Icon(player, position=(0, 50), scale=0.8)]
294            if player.lives > 0:
295                self.spawn_player(player)
296
297        # Don't waste time doing this until begin.
298        if self.has_begun():
299            self._update_icons()

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

(including the initial set of Players)

def on_begin(self) -> None:
301    def on_begin(self) -> None:
302        super().on_begin()
303        self._start_time = ba.time()
304        self.setup_standard_time_limit(self._time_limit)
305        self.setup_standard_powerup_drops()
306        if self._solo_mode:
307            self._vs_text = ba.NodeActor(
308                ba.newnode(
309                    'text',
310                    attrs={
311                        'position': (0, 105),
312                        'h_attach': 'center',
313                        'h_align': 'center',
314                        'maxwidth': 200,
315                        'shadow': 0.5,
316                        'vr_depth': 390,
317                        'scale': 0.6,
318                        'v_attach': 'bottom',
319                        'color': (0.8, 0.8, 0.3, 1.0),
320                        'text': ba.Lstr(resource='vsText'),
321                    },
322                )
323            )
324
325        # If balance-team-lives is on, add lives to the smaller team until
326        # total lives match.
327        if (
328            isinstance(self.session, ba.DualTeamSession)
329            and self._balance_total_lives
330            and self.teams[0].players
331            and self.teams[1].players
332        ):
333            if self._get_total_team_lives(
334                self.teams[0]
335            ) < self._get_total_team_lives(self.teams[1]):
336                lesser_team = self.teams[0]
337                greater_team = self.teams[1]
338            else:
339                lesser_team = self.teams[1]
340                greater_team = self.teams[0]
341            add_index = 0
342            while self._get_total_team_lives(
343                lesser_team
344            ) < self._get_total_team_lives(greater_team):
345                lesser_team.players[add_index].lives += 1
346                add_index = (add_index + 1) % len(lesser_team.players)
347
348        self._update_icons()
349
350        # We could check game-over conditions at explicit trigger points,
351        # but lets just do the simple thing and poll it.
352        ba.timer(1.0, self._update, repeat=True)

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 spawn_player(self, player: bastd.game.elimination.Player) -> ba._actor.Actor:
471    def spawn_player(self, player: Player) -> ba.Actor:
472        actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
473        if not self._solo_mode:
474            ba.timer(0.3, ba.Call(self._print_lives, player))
475
476        # If we have any icons, update their state.
477        for icon in player.icons:
478            icon.handle_player_spawned()
479        return actor

Spawn something for the provided ba.Player.

The default implementation simply calls spawn_player_spaz().

def on_player_leave(self, player: bastd.game.elimination.Player) -> None:
497    def on_player_leave(self, player: Player) -> None:
498        super().on_player_leave(player)
499        player.icons = []
500
501        # Remove us from spawn-order.
502        if self._solo_mode:
503            if player in player.team.spawn_order:
504                player.team.spawn_order.remove(player)
505
506        # Update icons in a moment since our team will be gone from the
507        # list then.
508        ba.timer(0, self._update_icons)
509
510        # If the player to leave was the last in spawn order and had
511        # their final turn currently in-progress, mark the survival time
512        # for their team.
513        if self._get_total_team_lives(player.team) == 0:
514            assert self._start_time is not None
515            player.team.survival_seconds = int(ba.time() - self._start_time)

Called when a ba.Player is leaving the Activity.

def handlemessage(self, msg: Any) -> Any:
520    def handlemessage(self, msg: Any) -> Any:
521        if isinstance(msg, ba.PlayerDiedMessage):
522
523            # Augment standard behavior.
524            super().handlemessage(msg)
525            player: Player = msg.getplayer(Player)
526
527            player.lives -= 1
528            if player.lives < 0:
529                ba.print_error(
530                    "Got lives < 0 in Elim; this shouldn't happen. solo:"
531                    + str(self._solo_mode)
532                )
533                player.lives = 0
534
535            # If we have any icons, update their state.
536            for icon in player.icons:
537                icon.handle_player_died()
538
539            # Play big death sound on our last death
540            # or for every one in solo mode.
541            if self._solo_mode or player.lives == 0:
542                ba.playsound(SpazFactory.get().single_player_death_sound)
543
544            # If we hit zero lives, we're dead (and our team might be too).
545            if player.lives == 0:
546                # If the whole team is now dead, mark their survival time.
547                if self._get_total_team_lives(player.team) == 0:
548                    assert self._start_time is not None
549                    player.team.survival_seconds = int(
550                        ba.time() - self._start_time
551                    )
552            else:
553                # Otherwise, in regular mode, respawn.
554                if not self._solo_mode:
555                    self.respawn_player(player)
556
557            # In solo, put ourself at the back of the spawn order.
558            if self._solo_mode:
559                player.team.spawn_order.remove(player)
560                player.team.spawn_order.append(player)

General message handling; can be passed any message object.

def end_game(self) -> None:
591    def end_game(self) -> None:
592        if self.has_ended():
593            return
594        results = ba.GameResults()
595        self._vs_text = None  # Kill our 'vs' if its there.
596        for team in self.teams:
597            results.set_team_score(team, team.survival_seconds)
598        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.

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_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
is_joining_activity
use_fixed_vr_overlay
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
transition_time
can_show_ad_on_death
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
session
on_team_join
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