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

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        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)

Instantiates an Actor in the current bascenev1.Activity.

node
def set_position_and_scale(self, position: tuple[float, float], scale: float) -> None:
 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

(Re)position the icon.

def update_for_lives(self) -> None:
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

Update for the target player's current lives.

def handle_player_spawned(self) -> None:
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()

Our player spawned; hooray!

def handle_player_died(self) -> None:
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)

Well poo; our player died.

@override
def handlemessage(self, msg: Any) -> Any:
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)

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')]):
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] = []

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]):
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] = []

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]):
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)

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

EliminationGame(settings: dict)
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        )

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.Session]) -> list[bascenev1._settings.Setting]:
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

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.Session]) -> bool:
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        )

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

@override
@classmethod
def get_supported_maps(cls, sessiontype: type[bascenev1._session.Session]) -> list[str]:
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')

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]:
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        )

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]:
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        )

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:
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()

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

(including the initial set of Players)

@override
def on_begin(self) -> None:
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)

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.Actor:
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

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

@override
def on_player_leave(self, player: Player) -> None:
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)

Called when a bascenev1.Player is leaving the Activity.

@override
def handlemessage(self, msg: Any) -> Any:
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)

General message handling; can be passed any message object.

@override
def end_game(self) -> None:
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)

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