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

Creates in in-game icon on screen.

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

Instantiates an Actor in the current bascenev1.Activity.

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

(Re)position the icon.

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

Update for the target player's current lives.

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

Our player spawned; hooray!

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

Well poo; our player died.

@override
def handlemessage(self, msg: Any) -> Any:
163    @override
164    def handlemessage(self, msg: Any) -> Any:
165        if isinstance(msg, bs.DieMessage):
166            self.node.delete()
167            return None
168        return super().handlemessage(msg)

General message handling; can be passed any message object.

class Player(bascenev1._player.Player[ForwardRef('Team')]):
171class Player(bs.Player['Team']):
172    """Our player type for this game."""
173
174    def __init__(self) -> None:
175        self.lives = 0
176        self.icons: list[Icon] = []

Our player type for this game.

lives
icons: list[Icon]
class Team(bascenev1._team.Team[bascenev1lib.game.elimination.Player]):
179class Team(bs.Team[Player]):
180    """Our team type for this game."""
181
182    def __init__(self) -> None:
183        self.survival_seconds: int | None = None
184        self.spawn_order: list[Player] = []

Our team type for this game.

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

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

EliminationGame(settings: dict)
259    def __init__(self, settings: dict):
260        super().__init__(settings)
261        self._scoreboard = Scoreboard()
262        self._start_time: float | None = None
263        self._vs_text: bs.Actor | None = None
264        self._round_end_timer: bs.Timer | None = None
265        self._epic_mode = bool(settings['Epic Mode'])
266        self._lives_per_player = int(settings['Lives Per Player'])
267        self._time_limit = float(settings['Time Limit'])
268        self._balance_total_lives = bool(
269            settings.get('Balance Total Lives', False)
270        )
271        self._solo_mode = bool(settings.get('Solo Mode', False))
272
273        # Base class overrides:
274        self.slow_motion = self._epic_mode
275        self.default_music = (
276            bs.MusicType.EPIC if self._epic_mode else bs.MusicType.SURVIVAL
277        )

Instantiate the Activity.

name = 'Elimination'
description = 'Last remaining alive wins.'
scoreconfig = 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 = 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.

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

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

@override
@classmethod
def supports_session_type(cls, sessiontype: type[bascenev1.Session]) -> bool:
246    @override
247    @classmethod
248    def supports_session_type(cls, sessiontype: type[bs.Session]) -> bool:
249        return issubclass(sessiontype, bs.DualTeamSession) or issubclass(
250            sessiontype, bs.FreeForAllSession
251        )

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

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1.Session]) -> list[str]:
253    @override
254    @classmethod
255    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
256        assert bs.app.classic is not None
257        return bs.app.classic.getmaps('melee')

Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.

slow_motion = False

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

default_music = None
@override
def get_instance_description(self) -> Union[str, Sequence]:
279    @override
280    def get_instance_description(self) -> str | Sequence:
281        return (
282            'Last team standing wins.'
283            if isinstance(self.session, bs.DualTeamSession)
284            else 'Last one standing wins.'
285        )

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

@override
def get_instance_description_short(self) -> Union[str, Sequence]:
287    @override
288    def get_instance_description_short(self) -> str | Sequence:
289        return (
290            'last team standing wins'
291            if isinstance(self.session, bs.DualTeamSession)
292            else 'last one standing wins'
293        )

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

@override
def on_player_join(self, player: Player) -> None:
295    @override
296    def on_player_join(self, player: Player) -> None:
297        player.lives = self._lives_per_player
298
299        if self._solo_mode:
300            player.team.spawn_order.append(player)
301            self._update_solo_mode()
302        else:
303            # Create our icon and spawn.
304            player.icons = [Icon(player, position=(0, 50), scale=0.8)]
305            if player.lives > 0:
306                self.spawn_player(player)
307
308        # Don't waste time doing this until begin.
309        if self.has_begun():
310            self._update_icons()

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

(including the initial set of Players)

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

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

@override
def spawn_player( self, player: Player) -> bascenev1.Actor:
483    @override
484    def spawn_player(self, player: Player) -> bs.Actor:
485        actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
486        if not self._solo_mode:
487            bs.timer(0.3, bs.Call(self._print_lives, player))
488
489        # If we have any icons, update their state.
490        for icon in player.icons:
491            icon.handle_player_spawned()
492        return actor

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

@override
def on_player_leave(self, player: Player) -> None:
510    @override
511    def on_player_leave(self, player: Player) -> None:
512        super().on_player_leave(player)
513        player.icons = []
514
515        # Remove us from spawn-order.
516        if self._solo_mode:
517            if player in player.team.spawn_order:
518                player.team.spawn_order.remove(player)
519
520        # Update icons in a moment since our team will be gone from the
521        # list then.
522        bs.timer(0, self._update_icons)
523
524        # If the player to leave was the last in spawn order and had
525        # their final turn currently in-progress, mark the survival time
526        # for their team.
527        if self._get_total_team_lives(player.team) == 0:
528            assert self._start_time is not None
529            player.team.survival_seconds = int(bs.time() - self._start_time)

Called when a bascenev1.Player is leaving the Activity.

@override
def handlemessage(self, msg: Any) -> Any:
534    @override
535    def handlemessage(self, msg: Any) -> Any:
536        if isinstance(msg, bs.PlayerDiedMessage):
537            # Augment standard behavior.
538            super().handlemessage(msg)
539            player: Player = msg.getplayer(Player)
540
541            player.lives -= 1
542            if player.lives < 0:
543                logging.exception(
544                    "Got lives < 0 in Elim; this shouldn't happen. solo: %s",
545                    self._solo_mode,
546                )
547                player.lives = 0
548
549            # If we have any icons, update their state.
550            for icon in player.icons:
551                icon.handle_player_died()
552
553            # Play big death sound on our last death
554            # or for every one in solo mode.
555            if self._solo_mode or player.lives == 0:
556                SpazFactory.get().single_player_death_sound.play()
557
558            # If we hit zero lives, we're dead (and our team might be too).
559            if player.lives == 0:
560                # If the whole team is now dead, mark their survival time.
561                if self._get_total_team_lives(player.team) == 0:
562                    assert self._start_time is not None
563                    player.team.survival_seconds = int(
564                        bs.time() - self._start_time
565                    )
566            else:
567                # Otherwise, in regular mode, respawn.
568                if not self._solo_mode:
569                    self.respawn_player(player)
570
571            # In solo, put ourself at the back of the spawn order.
572            if self._solo_mode:
573                player.team.spawn_order.remove(player)
574                player.team.spawn_order.append(player)

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
605    @override
606    def end_game(self) -> None:
607        if self.has_ended():
608            return
609        results = bs.GameResults()
610        self._vs_text = None  # Kill our 'vs' if its there.
611        for team in self.teams:
612            results.set_team_score(team, team.survival_seconds)
613        self.end(results=results)

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.