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

(Re)position the icon.

def update_for_lives(self) -> None:
 99    def update_for_lives(self) -> None:
100        """Update for the target player's current lives."""
101        if self._player:
102            lives = self._player.lives
103        else:
104            lives = 0
105        if self._show_lives:
106            if lives > 0:
107                self._lives_text.text = 'x' + str(lives - 1)
108            else:
109                self._lives_text.text = ''
110        if lives == 0:
111            self._name_text.opacity = 0.2
112            assert self.node
113            self.node.color = (0.7, 0.3, 0.3)
114            self.node.opacity = 0.2

Update for the target player's current lives.

def handle_player_spawned(self) -> None:
116    def handle_player_spawned(self) -> None:
117        """Our player spawned; hooray!"""
118        if not self.node:
119            return
120        self.node.opacity = 1.0
121        self.update_for_lives()

Our player spawned; hooray!

def handle_player_died(self) -> None:
123    def handle_player_died(self) -> None:
124        """Well poo; our player died."""
125        if not self.node:
126            return
127        if self._show_death:
128            ba.animate(
129                self.node, 'opacity', {
130                    0.00: 1.0,
131                    0.05: 0.0,
132                    0.10: 1.0,
133                    0.15: 0.0,
134                    0.20: 1.0,
135                    0.25: 0.0,
136                    0.30: 1.0,
137                    0.35: 0.0,
138                    0.40: 1.0,
139                    0.45: 0.0,
140                    0.50: 1.0,
141                    0.55: 0.2
142                })
143            lives = self._player.lives
144            if lives == 0:
145                ba.timer(0.6, self.update_for_lives)

Well poo; our player died.

def handlemessage(self, msg: Any) -> Any:
147    def handlemessage(self, msg: Any) -> Any:
148        if isinstance(msg, ba.DieMessage):
149            self.node.delete()
150            return None
151        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')]):
154class Player(ba.Player['Team']):
155    """Our player type for this game."""
156
157    def __init__(self) -> None:
158        self.lives = 0
159        self.icons: list[Icon] = []

Our player type for this game.

Player()
157    def __init__(self) -> None:
158        self.lives = 0
159        self.icons: list[Icon] = []
class Team(ba._team.Team[bastd.game.elimination.Player]):
162class Team(ba.Team[Player]):
163    """Our team type for this game."""
164
165    def __init__(self) -> None:
166        self.survival_seconds: int | None = None
167        self.spawn_order: list[Player] = []

Our team type for this game.

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

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

EliminationGame(settings: dict)
235    def __init__(self, settings: dict):
236        super().__init__(settings)
237        self._scoreboard = Scoreboard()
238        self._start_time: float | None = None
239        self._vs_text: ba.Actor | None = None
240        self._round_end_timer: ba.Timer | None = None
241        self._epic_mode = bool(settings['Epic Mode'])
242        self._lives_per_player = int(settings['Lives Per Player'])
243        self._time_limit = float(settings['Time Limit'])
244        self._balance_total_lives = bool(
245            settings.get('Balance Total Lives', False))
246        self._solo_mode = bool(settings.get('Solo Mode', False))
247
248        # Base class overrides:
249        self.slow_motion = self._epic_mode
250        self.default_music = (ba.MusicType.EPIC
251                              if self._epic_mode else ba.MusicType.SURVIVAL)

Instantiate the Activity.

name: str | None = 'Elimination'
description: str | None = 'Last remaining alive wins.'
scoreconfig: ba._score.ScoreConfig | None = ScoreConfig(label='Survived', scoretype=<ScoreType.SECONDS: 's'>, lower_is_better=False, none_is_winner=True, version='')
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: bool = 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]:
184    @classmethod
185    def get_available_settings(
186            cls, sessiontype: type[ba.Session]) -> list[ba.Setting]:
187        settings = [
188            ba.IntSetting(
189                'Lives Per Player',
190                default=1,
191                min_value=1,
192                max_value=10,
193                increment=1,
194            ),
195            ba.IntChoiceSetting(
196                'Time Limit',
197                choices=[
198                    ('None', 0),
199                    ('1 Minute', 60),
200                    ('2 Minutes', 120),
201                    ('5 Minutes', 300),
202                    ('10 Minutes', 600),
203                    ('20 Minutes', 1200),
204                ],
205                default=0,
206            ),
207            ba.FloatChoiceSetting(
208                'Respawn Times',
209                choices=[
210                    ('Shorter', 0.25),
211                    ('Short', 0.5),
212                    ('Normal', 1.0),
213                    ('Long', 2.0),
214                    ('Longer', 4.0),
215                ],
216                default=1.0,
217            ),
218            ba.BoolSetting('Epic Mode', default=False),
219        ]
220        if issubclass(sessiontype, ba.DualTeamSession):
221            settings.append(ba.BoolSetting('Solo Mode', default=False))
222            settings.append(
223                ba.BoolSetting('Balance Total Lives', default=False))
224        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:
226    @classmethod
227    def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
228        return (issubclass(sessiontype, ba.DualTeamSession)
229                or issubclass(sessiontype, ba.FreeForAllSession))

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]:
231    @classmethod
232    def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
233        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.

def get_instance_description(self) -> Union[str, Sequence]:
253    def get_instance_description(self) -> str | Sequence:
254        return 'Last team standing wins.' if isinstance(
255            self.session, ba.DualTeamSession) else 'Last one standing wins.'

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]:
257    def get_instance_description_short(self) -> str | Sequence:
258        return 'last team standing wins' if isinstance(
259            self.session, ba.DualTeamSession) else 'last one standing wins'

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:
261    def on_player_join(self, player: Player) -> None:
262        player.lives = self._lives_per_player
263
264        if self._solo_mode:
265            player.team.spawn_order.append(player)
266            self._update_solo_mode()
267        else:
268            # Create our icon and spawn.
269            player.icons = [Icon(player, position=(0, 50), scale=0.8)]
270            if player.lives > 0:
271                self.spawn_player(player)
272
273        # Don't waste time doing this until begin.
274        if self.has_begun():
275            self._update_icons()

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

(including the initial set of Players)

def on_begin(self) -> None:
277    def on_begin(self) -> None:
278        super().on_begin()
279        self._start_time = ba.time()
280        self.setup_standard_time_limit(self._time_limit)
281        self.setup_standard_powerup_drops()
282        if self._solo_mode:
283            self._vs_text = ba.NodeActor(
284                ba.newnode('text',
285                           attrs={
286                               'position': (0, 105),
287                               'h_attach': 'center',
288                               'h_align': 'center',
289                               'maxwidth': 200,
290                               'shadow': 0.5,
291                               'vr_depth': 390,
292                               'scale': 0.6,
293                               'v_attach': 'bottom',
294                               'color': (0.8, 0.8, 0.3, 1.0),
295                               'text': ba.Lstr(resource='vsText')
296                           }))
297
298        # If balance-team-lives is on, add lives to the smaller team until
299        # total lives match.
300        if (isinstance(self.session, ba.DualTeamSession)
301                and self._balance_total_lives and self.teams[0].players
302                and self.teams[1].players):
303            if self._get_total_team_lives(
304                    self.teams[0]) < self._get_total_team_lives(self.teams[1]):
305                lesser_team = self.teams[0]
306                greater_team = self.teams[1]
307            else:
308                lesser_team = self.teams[1]
309                greater_team = self.teams[0]
310            add_index = 0
311            while (self._get_total_team_lives(lesser_team) <
312                   self._get_total_team_lives(greater_team)):
313                lesser_team.players[add_index].lives += 1
314                add_index = (add_index + 1) % len(lesser_team.players)
315
316        self._update_icons()
317
318        # We could check game-over conditions at explicit trigger points,
319        # but lets just do the simple thing and poll it.
320        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:
434    def spawn_player(self, player: Player) -> ba.Actor:
435        actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
436        if not self._solo_mode:
437            ba.timer(0.3, ba.Call(self._print_lives, player))
438
439        # If we have any icons, update their state.
440        for icon in player.icons:
441            icon.handle_player_spawned()
442        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:
458    def on_player_leave(self, player: Player) -> None:
459        super().on_player_leave(player)
460        player.icons = []
461
462        # Remove us from spawn-order.
463        if self._solo_mode:
464            if player in player.team.spawn_order:
465                player.team.spawn_order.remove(player)
466
467        # Update icons in a moment since our team will be gone from the
468        # list then.
469        ba.timer(0, self._update_icons)
470
471        # If the player to leave was the last in spawn order and had
472        # their final turn currently in-progress, mark the survival time
473        # for their team.
474        if self._get_total_team_lives(player.team) == 0:
475            assert self._start_time is not None
476            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:
481    def handlemessage(self, msg: Any) -> Any:
482        if isinstance(msg, ba.PlayerDiedMessage):
483
484            # Augment standard behavior.
485            super().handlemessage(msg)
486            player: Player = msg.getplayer(Player)
487
488            player.lives -= 1
489            if player.lives < 0:
490                ba.print_error(
491                    "Got lives < 0 in Elim; this shouldn't happen. solo:" +
492                    str(self._solo_mode))
493                player.lives = 0
494
495            # If we have any icons, update their state.
496            for icon in player.icons:
497                icon.handle_player_died()
498
499            # Play big death sound on our last death
500            # or for every one in solo mode.
501            if self._solo_mode or player.lives == 0:
502                ba.playsound(SpazFactory.get().single_player_death_sound)
503
504            # If we hit zero lives, we're dead (and our team might be too).
505            if player.lives == 0:
506                # If the whole team is now dead, mark their survival time.
507                if self._get_total_team_lives(player.team) == 0:
508                    assert self._start_time is not None
509                    player.team.survival_seconds = int(ba.time() -
510                                                       self._start_time)
511            else:
512                # Otherwise, in regular mode, respawn.
513                if not self._solo_mode:
514                    self.respawn_player(player)
515
516            # In solo, put ourself at the back of the spawn order.
517            if self._solo_mode:
518                player.team.spawn_order.remove(player)
519                player.team.spawn_order.append(player)

General message handling; can be passed any message object.

def end_game(self) -> None:
549    def end_game(self) -> None:
550        if self.has_ended():
551            return
552        results = ba.GameResults()
553        self._vs_text = None  # Kill our 'vs' if its there.
554        for team in self.teams:
555            results.set_team_score(team, team.survival_seconds)
556        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._activity.Activity
slow_motion
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._gameactivity.GameActivity
default_music
tips
available_settings
allow_pausing
allow_kick_idle_players
show_kill_points
create_settings_ui
getscoreconfig
getname
get_display_string
get_team_display_string
get_description
get_description_display_string
get_settings_display_string
map
get_instance_display_string
get_instance_scoreboard_display_string
on_continue
is_waiting_for_continue
continue_or_end_game
respawn_player
spawn_player_if_exists
setup_standard_powerup_drops
setup_standard_time_limit
show_zoom_message
ba._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
ba._dependency.DependencyComponent
dep_is_present
get_dynamic_deps