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

(Re)position the icon.

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

Update for the target player's current lives.

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

Our player spawned; hooray!

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

Well poo; our player died.

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

General message handling; can be passed any message object.

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

Our player type for this game.

lives
icons: list[Icon]
Inherited Members
bascenev1._player.Player
character
actor
color
highlight
on_expire
team
customdata
sessionplayer
node
position
exists
getname
is_alive
get_icon
assigninput
resetinput
class Team(bascenev1._team.Team[bascenev1lib.game.elimination.Player]):
176class Team(bs.Team[Player]):
177    """Our team type for this game."""
178
179    def __init__(self) -> None:
180        self.survival_seconds: int | None = None
181        self.spawn_order: list[Player] = []

Our team type for this game.

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

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

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

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.

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

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

@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
248    @classmethod
249    def get_supported_maps(cls, sessiontype: type[bs.Session]) -> list[str]:
250        assert bs.app.classic is not None
251        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
def get_instance_description(self) -> Union[str, Sequence]:
273    def get_instance_description(self) -> str | Sequence:
274        return (
275            'Last team standing wins.'
276            if isinstance(self.session, bs.DualTeamSession)
277            else 'Last one standing wins.'
278        )

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

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

(including the initial set of Players)

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

def spawn_player( self, player: Player) -> bascenev1._actor.Actor:
473    def spawn_player(self, player: Player) -> bs.Actor:
474        actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
475        if not self._solo_mode:
476            bs.timer(0.3, bs.Call(self._print_lives, player))
477
478        # If we have any icons, update their state.
479        for icon in player.icons:
480            icon.handle_player_spawned()
481        return actor

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

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

Called when a bascenev1.Player is leaving the Activity.

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

General message handling; can be passed any message object.

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

Inherited Members
bascenev1._teamgame.TeamGameActivity
on_transition_in
spawn_player_spaz
end
bascenev1._gameactivity.GameActivity
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
initialplayerinfos
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
bascenev1._activity.Activity
settings_raw
teams
players
is_joining_activity
use_fixed_vr_overlay
inherits_slow_motion
inherits_music
inherits_vr_camera_offset
inherits_vr_overlay_center
inherits_tint
transition_time
can_show_ad_on_death
paused_text
preloads
lobby
context
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
bascenev1._dependency.DependencyComponent
dep_is_present
get_dynamic_deps