bascenev1lib.activity.multiteamvictory

Functionality related to the final screen in multi-teams sessions.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Functionality related to the final screen in multi-teams sessions."""
  4
  5from __future__ import annotations
  6
  7from typing import override, TYPE_CHECKING
  8
  9import bascenev1 as bs
 10
 11from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity
 12
 13if TYPE_CHECKING:
 14    from typing import Any
 15
 16
 17class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
 18    """Final score screen for a team series."""
 19
 20    # Dont' play music by default; (we do manually after a delay).
 21    default_music = None
 22
 23    def __init__(self, settings: dict):
 24        super().__init__(settings=settings)
 25        self._min_view_time = 15.0
 26        self._is_ffa = isinstance(self.session, bs.FreeForAllSession)
 27        self._allow_server_transition = True
 28        self._tips_text = None
 29        self._default_show_tips = False
 30        self._ffa_top_player_info: list[Any] | None = None
 31        self._ffa_top_player_rec: bs.PlayerRecord | None = None
 32
 33    @override
 34    def on_begin(self) -> None:
 35        # pylint: disable=too-many-branches
 36        # pylint: disable=too-many-locals
 37        # pylint: disable=too-many-statements
 38        from bascenev1lib.actor.text import Text
 39        from bascenev1lib.actor.image import Image
 40
 41        bs.set_analytics_screen(
 42            'FreeForAll Series Victory Screen'
 43            if self._is_ffa
 44            else 'Teams Series Victory Screen'
 45        )
 46        assert bs.app.classic is not None
 47        if bs.app.ui_v1.uiscale is bs.UIScale.LARGE:
 48            sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText')
 49        else:
 50            sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
 51        self._show_up_next = False
 52        self._custom_continue_message = sval
 53        super().on_begin()
 54        winning_sessionteam = self.settings_raw['winner']
 55
 56        # Pause a moment before playing victory music.
 57        bs.timer(0.6, bs.WeakCall(self._play_victory_music))
 58        bs.timer(
 59            4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner'])
 60        )
 61        bs.timer(4.6, self._score_display_sound.play)
 62
 63        # Score / Name / Player-record.
 64        player_entries: list[tuple[int, str, bs.PlayerRecord]] = []
 65
 66        # Note: for ffa, exclude players who haven't entered the game yet.
 67        if self._is_ffa:
 68            for _pkey, prec in self.stats.get_records().items():
 69                if prec.player.in_game:
 70                    player_entries.append(
 71                        (
 72                            prec.player.sessionteam.customdata['score'],
 73                            prec.getname(full=True),
 74                            prec,
 75                        )
 76                    )
 77            player_entries.sort(reverse=True, key=lambda x: x[0])
 78            if len(player_entries) > 0:
 79                # Store some info for the top ffa player so we can
 80                # show winner info even if they leave.
 81                self._ffa_top_player_info = list(player_entries[0])
 82                self._ffa_top_player_info[1] = self._ffa_top_player_info[
 83                    2
 84                ].getname()
 85                self._ffa_top_player_info[2] = self._ffa_top_player_info[
 86                    2
 87                ].get_icon()
 88        else:
 89            for _pkey, prec in self.stats.get_records().items():
 90                player_entries.append((prec.score, prec.name_full, prec))
 91            player_entries.sort(reverse=True, key=lambda x: x[0])
 92
 93        ts_height = 300.0
 94        ts_h_offs = -390.0
 95        tval = 6.4
 96        t_incr = 0.12
 97
 98        always_use_first_to = bs.app.lang.get_resource(
 99            'bestOfUseFirstToInstead'
100        )
101
102        session = self.session
103        if self._is_ffa:
104            assert isinstance(session, bs.FreeForAllSession)
105            txt = bs.Lstr(
106                value='${A}:',
107                subs=[
108                    (
109                        '${A}',
110                        bs.Lstr(
111                            resource='firstToFinalText',
112                            subs=[
113                                (
114                                    '${COUNT}',
115                                    str(session.get_ffa_series_length()),
116                                )
117                            ],
118                        ),
119                    )
120                ],
121            )
122        else:
123            assert isinstance(session, bs.MultiTeamSession)
124
125            # Some languages may prefer to always show 'first to X' instead of
126            # 'best of X'.
127            # FIXME: This will affect all clients connected to us even if
128            #  they're not using this language. Should try to come up
129            #  with a wording that works everywhere.
130            if always_use_first_to:
131                txt = bs.Lstr(
132                    value='${A}:',
133                    subs=[
134                        (
135                            '${A}',
136                            bs.Lstr(
137                                resource='firstToFinalText',
138                                subs=[
139                                    (
140                                        '${COUNT}',
141                                        str(
142                                            session.get_series_length() / 2 + 1
143                                        ),
144                                    )
145                                ],
146                            ),
147                        )
148                    ],
149                )
150            else:
151                txt = bs.Lstr(
152                    value='${A}:',
153                    subs=[
154                        (
155                            '${A}',
156                            bs.Lstr(
157                                resource='bestOfFinalText',
158                                subs=[
159                                    (
160                                        '${COUNT}',
161                                        str(session.get_series_length()),
162                                    )
163                                ],
164                            ),
165                        )
166                    ],
167                )
168
169        Text(
170            txt,
171            v_align=Text.VAlign.CENTER,
172            maxwidth=300,
173            color=(0.5, 0.5, 0.5, 1.0),
174            position=(0, 220),
175            scale=1.2,
176            transition=Text.Transition.IN_TOP_SLOW,
177            h_align=Text.HAlign.CENTER,
178            transition_delay=t_incr * 4,
179        ).autoretain()
180
181        win_score = (session.get_series_length() - 1) // 2 + 1
182        lose_score = 0
183        for team in self.teams:
184            if team.sessionteam.customdata['score'] != win_score:
185                lose_score = team.sessionteam.customdata['score']
186
187        if not self._is_ffa:
188            Text(
189                bs.Lstr(
190                    resource='gamesToText',
191                    subs=[
192                        ('${WINCOUNT}', str(win_score)),
193                        ('${LOSECOUNT}', str(lose_score)),
194                    ],
195                ),
196                color=(0.5, 0.5, 0.5, 1.0),
197                maxwidth=160,
198                v_align=Text.VAlign.CENTER,
199                position=(0, -215),
200                scale=1.8,
201                transition=Text.Transition.IN_LEFT,
202                h_align=Text.HAlign.CENTER,
203                transition_delay=4.8 + t_incr * 4,
204            ).autoretain()
205
206        if self._is_ffa:
207            v_extra = 120
208        else:
209            v_extra = 0
210
211        mvp: bs.PlayerRecord | None = None
212        mvp_name: str | None = None
213
214        # Show game MVP.
215        if not self._is_ffa:
216            mvp, mvp_name = None, None
217            for entry in player_entries:
218                if entry[2].team == winning_sessionteam:
219                    mvp = entry[2]
220                    mvp_name = entry[1]
221                    break
222            if mvp is not None:
223                Text(
224                    bs.Lstr(resource='mostValuablePlayerText'),
225                    color=(0.5, 0.5, 0.5, 1.0),
226                    v_align=Text.VAlign.CENTER,
227                    maxwidth=300,
228                    position=(180, ts_height / 2 + 15),
229                    transition=Text.Transition.IN_LEFT,
230                    h_align=Text.HAlign.LEFT,
231                    transition_delay=tval,
232                ).autoretain()
233                tval += 4 * t_incr
234
235                Image(
236                    mvp.get_icon(),
237                    position=(230, ts_height / 2 - 55 + 14 - 5),
238                    scale=(70, 70),
239                    transition=Image.Transition.IN_LEFT,
240                    transition_delay=tval,
241                ).autoretain()
242                assert mvp_name is not None
243                Text(
244                    bs.Lstr(value=mvp_name),
245                    position=(280, ts_height / 2 - 55 + 15 - 5),
246                    h_align=Text.HAlign.LEFT,
247                    v_align=Text.VAlign.CENTER,
248                    maxwidth=170,
249                    scale=1.3,
250                    color=bs.safecolor(mvp.team.color + (1,)),
251                    transition=Text.Transition.IN_LEFT,
252                    transition_delay=tval,
253                ).autoretain()
254                tval += 4 * t_incr
255
256        # Most violent.
257        most_kills = 0
258        for entry in player_entries:
259            if entry[2].kill_count >= most_kills:
260                mvp = entry[2]
261                mvp_name = entry[1]
262                most_kills = entry[2].kill_count
263        if mvp is not None:
264            Text(
265                bs.Lstr(resource='mostViolentPlayerText'),
266                color=(0.5, 0.5, 0.5, 1.0),
267                v_align=Text.VAlign.CENTER,
268                maxwidth=300,
269                position=(180, ts_height / 2 - 150 + v_extra + 15),
270                transition=Text.Transition.IN_LEFT,
271                h_align=Text.HAlign.LEFT,
272                transition_delay=tval,
273            ).autoretain()
274            Text(
275                bs.Lstr(
276                    value='(${A})',
277                    subs=[
278                        (
279                            '${A}',
280                            bs.Lstr(
281                                resource='killsTallyText',
282                                subs=[('${COUNT}', str(most_kills))],
283                            ),
284                        )
285                    ],
286                ),
287                position=(260, ts_height / 2 - 150 - 15 + v_extra),
288                color=(0.3, 0.3, 0.3, 1.0),
289                scale=0.6,
290                h_align=Text.HAlign.LEFT,
291                transition=Text.Transition.IN_LEFT,
292                transition_delay=tval,
293            ).autoretain()
294            tval += 4 * t_incr
295
296            Image(
297                mvp.get_icon(),
298                position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
299                scale=(50, 50),
300                transition=Image.Transition.IN_LEFT,
301                transition_delay=tval,
302            ).autoretain()
303            assert mvp_name is not None
304            Text(
305                bs.Lstr(value=mvp_name),
306                position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
307                h_align=Text.HAlign.LEFT,
308                v_align=Text.VAlign.CENTER,
309                maxwidth=180,
310                color=bs.safecolor(mvp.team.color + (1,)),
311                transition=Text.Transition.IN_LEFT,
312                transition_delay=tval,
313            ).autoretain()
314            tval += 4 * t_incr
315
316        # Most killed.
317        most_killed = 0
318        mkp, mkp_name = None, None
319        for entry in player_entries:
320            if entry[2].killed_count >= most_killed:
321                mkp = entry[2]
322                mkp_name = entry[1]
323                most_killed = entry[2].killed_count
324        if mkp is not None:
325            Text(
326                bs.Lstr(resource='mostViolatedPlayerText'),
327                color=(0.5, 0.5, 0.5, 1.0),
328                v_align=Text.VAlign.CENTER,
329                maxwidth=300,
330                position=(180, ts_height / 2 - 300 + v_extra + 15),
331                transition=Text.Transition.IN_LEFT,
332                h_align=Text.HAlign.LEFT,
333                transition_delay=tval,
334            ).autoretain()
335            Text(
336                bs.Lstr(
337                    value='(${A})',
338                    subs=[
339                        (
340                            '${A}',
341                            bs.Lstr(
342                                resource='deathsTallyText',
343                                subs=[('${COUNT}', str(most_killed))],
344                            ),
345                        )
346                    ],
347                ),
348                position=(260, ts_height / 2 - 300 - 15 + v_extra),
349                h_align=Text.HAlign.LEFT,
350                scale=0.6,
351                color=(0.3, 0.3, 0.3, 1.0),
352                transition=Text.Transition.IN_LEFT,
353                transition_delay=tval,
354            ).autoretain()
355            tval += 4 * t_incr
356            Image(
357                mkp.get_icon(),
358                position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
359                scale=(50, 50),
360                transition=Image.Transition.IN_LEFT,
361                transition_delay=tval,
362            ).autoretain()
363            assert mkp_name is not None
364            Text(
365                bs.Lstr(value=mkp_name),
366                position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
367                h_align=Text.HAlign.LEFT,
368                v_align=Text.VAlign.CENTER,
369                color=bs.safecolor(mkp.team.color + (1,)),
370                maxwidth=180,
371                transition=Text.Transition.IN_LEFT,
372                transition_delay=tval,
373            ).autoretain()
374            tval += 4 * t_incr
375
376        # Now show individual scores.
377        tdelay = tval
378        Text(
379            bs.Lstr(resource='finalScoresText'),
380            color=(0.5, 0.5, 0.5, 1.0),
381            position=(ts_h_offs, ts_height / 2),
382            transition=Text.Transition.IN_RIGHT,
383            transition_delay=tdelay,
384        ).autoretain()
385        tdelay += 4 * t_incr
386
387        v_offs = 0.0
388        tdelay += len(player_entries) * 8 * t_incr
389        for _score, name, prec in player_entries:
390            tdelay -= 4 * t_incr
391            v_offs -= 40
392            Text(
393                (
394                    str(prec.team.customdata['score'])
395                    if self._is_ffa
396                    else str(prec.score)
397                ),
398                color=(0.5, 0.5, 0.5, 1.0),
399                position=(ts_h_offs + 230, ts_height / 2 + v_offs),
400                h_align=Text.HAlign.RIGHT,
401                transition=Text.Transition.IN_RIGHT,
402                transition_delay=tdelay,
403            ).autoretain()
404            tdelay -= 4 * t_incr
405
406            Image(
407                prec.get_icon(),
408                position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
409                scale=(30, 30),
410                transition=Image.Transition.IN_LEFT,
411                transition_delay=tdelay,
412            ).autoretain()
413            Text(
414                bs.Lstr(value=name),
415                position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
416                h_align=Text.HAlign.LEFT,
417                v_align=Text.VAlign.CENTER,
418                maxwidth=180,
419                color=bs.safecolor(prec.team.color + (1,)),
420                transition=Text.Transition.IN_RIGHT,
421                transition_delay=tdelay,
422            ).autoretain()
423
424        bs.timer(15.0, bs.WeakCall(self._show_tips))
425
426    def _show_tips(self) -> None:
427        from bascenev1lib.actor.tipstext import TipsText
428
429        self._tips_text = TipsText(offs_y=70)
430
431    def _play_victory_music(self) -> None:
432        # Make sure we don't stomp on the next activity's music choice.
433        if not self.is_transitioning_out():
434            bs.setmusic(bs.MusicType.VICTORY)
435
436    def _show_winner(self, team: bs.SessionTeam) -> None:
437        from bascenev1lib.actor.image import Image
438        from bascenev1lib.actor.zoomtext import ZoomText
439
440        if not self._is_ffa:
441            offs_v = 0.0
442            ZoomText(
443                team.name,
444                position=(0, 97),
445                color=team.color,
446                scale=1.15,
447                jitter=1.0,
448                maxwidth=250,
449            ).autoretain()
450        else:
451            offs_v = -80
452            assert isinstance(self.session, bs.MultiTeamSession)
453            series_length = self.session.get_ffa_series_length()
454            icon: dict | None
455            # Pull live player info if they're still around.
456            if len(team.players) == 1:
457                icon = team.players[0].get_icon()
458                player_name = team.players[0].getname(full=True, icon=False)
459            # Otherwise use the special info we stored when we came in.
460            elif (
461                self._ffa_top_player_info is not None
462                and self._ffa_top_player_info[0] >= series_length
463            ):
464                icon = self._ffa_top_player_info[2]
465                player_name = self._ffa_top_player_info[1]
466            else:
467                icon = None
468                player_name = 'Player Not Found'
469
470            if icon is not None:
471                i = Image(
472                    icon,
473                    position=(0, 143),
474                    scale=(100, 100),
475                ).autoretain()
476                assert i.node
477                bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
478
479            ZoomText(
480                bs.Lstr(value=player_name),
481                position=(0, 97 + offs_v + (0 if icon is not None else 60)),
482                color=team.color,
483                scale=1.15,
484                jitter=1.0,
485                maxwidth=250,
486            ).autoretain()
487
488        s_extra = 1.0 if self._is_ffa else 1.0
489
490        # Some languages say "FOO WINS" differently for teams vs players.
491        if isinstance(self.session, bs.FreeForAllSession):
492            wins_resource = 'seriesWinLine1PlayerText'
493        else:
494            wins_resource = 'seriesWinLine1TeamText'
495        wins_text = bs.Lstr(resource=wins_resource)
496
497        # Temp - if these come up as the english default, fall-back to the
498        # unified old form which is more likely to be translated.
499        ZoomText(
500            wins_text,
501            position=(0, -10 + offs_v),
502            color=team.color,
503            scale=0.65 * s_extra,
504            jitter=1.0,
505            maxwidth=250,
506        ).autoretain()
507        ZoomText(
508            bs.Lstr(resource='seriesWinLine2Text'),
509            position=(0, -110 + offs_v),
510            scale=1.0 * s_extra,
511            color=team.color,
512            jitter=1.0,
513            maxwidth=250,
514        ).autoretain()
class TeamSeriesVictoryScoreScreenActivity(bascenev1._activity.Activity[bascenev1._player.EmptyPlayer, bascenev1._team.EmptyTeam]):
 18class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
 19    """Final score screen for a team series."""
 20
 21    # Dont' play music by default; (we do manually after a delay).
 22    default_music = None
 23
 24    def __init__(self, settings: dict):
 25        super().__init__(settings=settings)
 26        self._min_view_time = 15.0
 27        self._is_ffa = isinstance(self.session, bs.FreeForAllSession)
 28        self._allow_server_transition = True
 29        self._tips_text = None
 30        self._default_show_tips = False
 31        self._ffa_top_player_info: list[Any] | None = None
 32        self._ffa_top_player_rec: bs.PlayerRecord | None = None
 33
 34    @override
 35    def on_begin(self) -> None:
 36        # pylint: disable=too-many-branches
 37        # pylint: disable=too-many-locals
 38        # pylint: disable=too-many-statements
 39        from bascenev1lib.actor.text import Text
 40        from bascenev1lib.actor.image import Image
 41
 42        bs.set_analytics_screen(
 43            'FreeForAll Series Victory Screen'
 44            if self._is_ffa
 45            else 'Teams Series Victory Screen'
 46        )
 47        assert bs.app.classic is not None
 48        if bs.app.ui_v1.uiscale is bs.UIScale.LARGE:
 49            sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText')
 50        else:
 51            sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
 52        self._show_up_next = False
 53        self._custom_continue_message = sval
 54        super().on_begin()
 55        winning_sessionteam = self.settings_raw['winner']
 56
 57        # Pause a moment before playing victory music.
 58        bs.timer(0.6, bs.WeakCall(self._play_victory_music))
 59        bs.timer(
 60            4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner'])
 61        )
 62        bs.timer(4.6, self._score_display_sound.play)
 63
 64        # Score / Name / Player-record.
 65        player_entries: list[tuple[int, str, bs.PlayerRecord]] = []
 66
 67        # Note: for ffa, exclude players who haven't entered the game yet.
 68        if self._is_ffa:
 69            for _pkey, prec in self.stats.get_records().items():
 70                if prec.player.in_game:
 71                    player_entries.append(
 72                        (
 73                            prec.player.sessionteam.customdata['score'],
 74                            prec.getname(full=True),
 75                            prec,
 76                        )
 77                    )
 78            player_entries.sort(reverse=True, key=lambda x: x[0])
 79            if len(player_entries) > 0:
 80                # Store some info for the top ffa player so we can
 81                # show winner info even if they leave.
 82                self._ffa_top_player_info = list(player_entries[0])
 83                self._ffa_top_player_info[1] = self._ffa_top_player_info[
 84                    2
 85                ].getname()
 86                self._ffa_top_player_info[2] = self._ffa_top_player_info[
 87                    2
 88                ].get_icon()
 89        else:
 90            for _pkey, prec in self.stats.get_records().items():
 91                player_entries.append((prec.score, prec.name_full, prec))
 92            player_entries.sort(reverse=True, key=lambda x: x[0])
 93
 94        ts_height = 300.0
 95        ts_h_offs = -390.0
 96        tval = 6.4
 97        t_incr = 0.12
 98
 99        always_use_first_to = bs.app.lang.get_resource(
100            'bestOfUseFirstToInstead'
101        )
102
103        session = self.session
104        if self._is_ffa:
105            assert isinstance(session, bs.FreeForAllSession)
106            txt = bs.Lstr(
107                value='${A}:',
108                subs=[
109                    (
110                        '${A}',
111                        bs.Lstr(
112                            resource='firstToFinalText',
113                            subs=[
114                                (
115                                    '${COUNT}',
116                                    str(session.get_ffa_series_length()),
117                                )
118                            ],
119                        ),
120                    )
121                ],
122            )
123        else:
124            assert isinstance(session, bs.MultiTeamSession)
125
126            # Some languages may prefer to always show 'first to X' instead of
127            # 'best of X'.
128            # FIXME: This will affect all clients connected to us even if
129            #  they're not using this language. Should try to come up
130            #  with a wording that works everywhere.
131            if always_use_first_to:
132                txt = bs.Lstr(
133                    value='${A}:',
134                    subs=[
135                        (
136                            '${A}',
137                            bs.Lstr(
138                                resource='firstToFinalText',
139                                subs=[
140                                    (
141                                        '${COUNT}',
142                                        str(
143                                            session.get_series_length() / 2 + 1
144                                        ),
145                                    )
146                                ],
147                            ),
148                        )
149                    ],
150                )
151            else:
152                txt = bs.Lstr(
153                    value='${A}:',
154                    subs=[
155                        (
156                            '${A}',
157                            bs.Lstr(
158                                resource='bestOfFinalText',
159                                subs=[
160                                    (
161                                        '${COUNT}',
162                                        str(session.get_series_length()),
163                                    )
164                                ],
165                            ),
166                        )
167                    ],
168                )
169
170        Text(
171            txt,
172            v_align=Text.VAlign.CENTER,
173            maxwidth=300,
174            color=(0.5, 0.5, 0.5, 1.0),
175            position=(0, 220),
176            scale=1.2,
177            transition=Text.Transition.IN_TOP_SLOW,
178            h_align=Text.HAlign.CENTER,
179            transition_delay=t_incr * 4,
180        ).autoretain()
181
182        win_score = (session.get_series_length() - 1) // 2 + 1
183        lose_score = 0
184        for team in self.teams:
185            if team.sessionteam.customdata['score'] != win_score:
186                lose_score = team.sessionteam.customdata['score']
187
188        if not self._is_ffa:
189            Text(
190                bs.Lstr(
191                    resource='gamesToText',
192                    subs=[
193                        ('${WINCOUNT}', str(win_score)),
194                        ('${LOSECOUNT}', str(lose_score)),
195                    ],
196                ),
197                color=(0.5, 0.5, 0.5, 1.0),
198                maxwidth=160,
199                v_align=Text.VAlign.CENTER,
200                position=(0, -215),
201                scale=1.8,
202                transition=Text.Transition.IN_LEFT,
203                h_align=Text.HAlign.CENTER,
204                transition_delay=4.8 + t_incr * 4,
205            ).autoretain()
206
207        if self._is_ffa:
208            v_extra = 120
209        else:
210            v_extra = 0
211
212        mvp: bs.PlayerRecord | None = None
213        mvp_name: str | None = None
214
215        # Show game MVP.
216        if not self._is_ffa:
217            mvp, mvp_name = None, None
218            for entry in player_entries:
219                if entry[2].team == winning_sessionteam:
220                    mvp = entry[2]
221                    mvp_name = entry[1]
222                    break
223            if mvp is not None:
224                Text(
225                    bs.Lstr(resource='mostValuablePlayerText'),
226                    color=(0.5, 0.5, 0.5, 1.0),
227                    v_align=Text.VAlign.CENTER,
228                    maxwidth=300,
229                    position=(180, ts_height / 2 + 15),
230                    transition=Text.Transition.IN_LEFT,
231                    h_align=Text.HAlign.LEFT,
232                    transition_delay=tval,
233                ).autoretain()
234                tval += 4 * t_incr
235
236                Image(
237                    mvp.get_icon(),
238                    position=(230, ts_height / 2 - 55 + 14 - 5),
239                    scale=(70, 70),
240                    transition=Image.Transition.IN_LEFT,
241                    transition_delay=tval,
242                ).autoretain()
243                assert mvp_name is not None
244                Text(
245                    bs.Lstr(value=mvp_name),
246                    position=(280, ts_height / 2 - 55 + 15 - 5),
247                    h_align=Text.HAlign.LEFT,
248                    v_align=Text.VAlign.CENTER,
249                    maxwidth=170,
250                    scale=1.3,
251                    color=bs.safecolor(mvp.team.color + (1,)),
252                    transition=Text.Transition.IN_LEFT,
253                    transition_delay=tval,
254                ).autoretain()
255                tval += 4 * t_incr
256
257        # Most violent.
258        most_kills = 0
259        for entry in player_entries:
260            if entry[2].kill_count >= most_kills:
261                mvp = entry[2]
262                mvp_name = entry[1]
263                most_kills = entry[2].kill_count
264        if mvp is not None:
265            Text(
266                bs.Lstr(resource='mostViolentPlayerText'),
267                color=(0.5, 0.5, 0.5, 1.0),
268                v_align=Text.VAlign.CENTER,
269                maxwidth=300,
270                position=(180, ts_height / 2 - 150 + v_extra + 15),
271                transition=Text.Transition.IN_LEFT,
272                h_align=Text.HAlign.LEFT,
273                transition_delay=tval,
274            ).autoretain()
275            Text(
276                bs.Lstr(
277                    value='(${A})',
278                    subs=[
279                        (
280                            '${A}',
281                            bs.Lstr(
282                                resource='killsTallyText',
283                                subs=[('${COUNT}', str(most_kills))],
284                            ),
285                        )
286                    ],
287                ),
288                position=(260, ts_height / 2 - 150 - 15 + v_extra),
289                color=(0.3, 0.3, 0.3, 1.0),
290                scale=0.6,
291                h_align=Text.HAlign.LEFT,
292                transition=Text.Transition.IN_LEFT,
293                transition_delay=tval,
294            ).autoretain()
295            tval += 4 * t_incr
296
297            Image(
298                mvp.get_icon(),
299                position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
300                scale=(50, 50),
301                transition=Image.Transition.IN_LEFT,
302                transition_delay=tval,
303            ).autoretain()
304            assert mvp_name is not None
305            Text(
306                bs.Lstr(value=mvp_name),
307                position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
308                h_align=Text.HAlign.LEFT,
309                v_align=Text.VAlign.CENTER,
310                maxwidth=180,
311                color=bs.safecolor(mvp.team.color + (1,)),
312                transition=Text.Transition.IN_LEFT,
313                transition_delay=tval,
314            ).autoretain()
315            tval += 4 * t_incr
316
317        # Most killed.
318        most_killed = 0
319        mkp, mkp_name = None, None
320        for entry in player_entries:
321            if entry[2].killed_count >= most_killed:
322                mkp = entry[2]
323                mkp_name = entry[1]
324                most_killed = entry[2].killed_count
325        if mkp is not None:
326            Text(
327                bs.Lstr(resource='mostViolatedPlayerText'),
328                color=(0.5, 0.5, 0.5, 1.0),
329                v_align=Text.VAlign.CENTER,
330                maxwidth=300,
331                position=(180, ts_height / 2 - 300 + v_extra + 15),
332                transition=Text.Transition.IN_LEFT,
333                h_align=Text.HAlign.LEFT,
334                transition_delay=tval,
335            ).autoretain()
336            Text(
337                bs.Lstr(
338                    value='(${A})',
339                    subs=[
340                        (
341                            '${A}',
342                            bs.Lstr(
343                                resource='deathsTallyText',
344                                subs=[('${COUNT}', str(most_killed))],
345                            ),
346                        )
347                    ],
348                ),
349                position=(260, ts_height / 2 - 300 - 15 + v_extra),
350                h_align=Text.HAlign.LEFT,
351                scale=0.6,
352                color=(0.3, 0.3, 0.3, 1.0),
353                transition=Text.Transition.IN_LEFT,
354                transition_delay=tval,
355            ).autoretain()
356            tval += 4 * t_incr
357            Image(
358                mkp.get_icon(),
359                position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
360                scale=(50, 50),
361                transition=Image.Transition.IN_LEFT,
362                transition_delay=tval,
363            ).autoretain()
364            assert mkp_name is not None
365            Text(
366                bs.Lstr(value=mkp_name),
367                position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
368                h_align=Text.HAlign.LEFT,
369                v_align=Text.VAlign.CENTER,
370                color=bs.safecolor(mkp.team.color + (1,)),
371                maxwidth=180,
372                transition=Text.Transition.IN_LEFT,
373                transition_delay=tval,
374            ).autoretain()
375            tval += 4 * t_incr
376
377        # Now show individual scores.
378        tdelay = tval
379        Text(
380            bs.Lstr(resource='finalScoresText'),
381            color=(0.5, 0.5, 0.5, 1.0),
382            position=(ts_h_offs, ts_height / 2),
383            transition=Text.Transition.IN_RIGHT,
384            transition_delay=tdelay,
385        ).autoretain()
386        tdelay += 4 * t_incr
387
388        v_offs = 0.0
389        tdelay += len(player_entries) * 8 * t_incr
390        for _score, name, prec in player_entries:
391            tdelay -= 4 * t_incr
392            v_offs -= 40
393            Text(
394                (
395                    str(prec.team.customdata['score'])
396                    if self._is_ffa
397                    else str(prec.score)
398                ),
399                color=(0.5, 0.5, 0.5, 1.0),
400                position=(ts_h_offs + 230, ts_height / 2 + v_offs),
401                h_align=Text.HAlign.RIGHT,
402                transition=Text.Transition.IN_RIGHT,
403                transition_delay=tdelay,
404            ).autoretain()
405            tdelay -= 4 * t_incr
406
407            Image(
408                prec.get_icon(),
409                position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
410                scale=(30, 30),
411                transition=Image.Transition.IN_LEFT,
412                transition_delay=tdelay,
413            ).autoretain()
414            Text(
415                bs.Lstr(value=name),
416                position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
417                h_align=Text.HAlign.LEFT,
418                v_align=Text.VAlign.CENTER,
419                maxwidth=180,
420                color=bs.safecolor(prec.team.color + (1,)),
421                transition=Text.Transition.IN_RIGHT,
422                transition_delay=tdelay,
423            ).autoretain()
424
425        bs.timer(15.0, bs.WeakCall(self._show_tips))
426
427    def _show_tips(self) -> None:
428        from bascenev1lib.actor.tipstext import TipsText
429
430        self._tips_text = TipsText(offs_y=70)
431
432    def _play_victory_music(self) -> None:
433        # Make sure we don't stomp on the next activity's music choice.
434        if not self.is_transitioning_out():
435            bs.setmusic(bs.MusicType.VICTORY)
436
437    def _show_winner(self, team: bs.SessionTeam) -> None:
438        from bascenev1lib.actor.image import Image
439        from bascenev1lib.actor.zoomtext import ZoomText
440
441        if not self._is_ffa:
442            offs_v = 0.0
443            ZoomText(
444                team.name,
445                position=(0, 97),
446                color=team.color,
447                scale=1.15,
448                jitter=1.0,
449                maxwidth=250,
450            ).autoretain()
451        else:
452            offs_v = -80
453            assert isinstance(self.session, bs.MultiTeamSession)
454            series_length = self.session.get_ffa_series_length()
455            icon: dict | None
456            # Pull live player info if they're still around.
457            if len(team.players) == 1:
458                icon = team.players[0].get_icon()
459                player_name = team.players[0].getname(full=True, icon=False)
460            # Otherwise use the special info we stored when we came in.
461            elif (
462                self._ffa_top_player_info is not None
463                and self._ffa_top_player_info[0] >= series_length
464            ):
465                icon = self._ffa_top_player_info[2]
466                player_name = self._ffa_top_player_info[1]
467            else:
468                icon = None
469                player_name = 'Player Not Found'
470
471            if icon is not None:
472                i = Image(
473                    icon,
474                    position=(0, 143),
475                    scale=(100, 100),
476                ).autoretain()
477                assert i.node
478                bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
479
480            ZoomText(
481                bs.Lstr(value=player_name),
482                position=(0, 97 + offs_v + (0 if icon is not None else 60)),
483                color=team.color,
484                scale=1.15,
485                jitter=1.0,
486                maxwidth=250,
487            ).autoretain()
488
489        s_extra = 1.0 if self._is_ffa else 1.0
490
491        # Some languages say "FOO WINS" differently for teams vs players.
492        if isinstance(self.session, bs.FreeForAllSession):
493            wins_resource = 'seriesWinLine1PlayerText'
494        else:
495            wins_resource = 'seriesWinLine1TeamText'
496        wins_text = bs.Lstr(resource=wins_resource)
497
498        # Temp - if these come up as the english default, fall-back to the
499        # unified old form which is more likely to be translated.
500        ZoomText(
501            wins_text,
502            position=(0, -10 + offs_v),
503            color=team.color,
504            scale=0.65 * s_extra,
505            jitter=1.0,
506            maxwidth=250,
507        ).autoretain()
508        ZoomText(
509            bs.Lstr(resource='seriesWinLine2Text'),
510            position=(0, -110 + offs_v),
511            scale=1.0 * s_extra,
512            color=team.color,
513            jitter=1.0,
514            maxwidth=250,
515        ).autoretain()

Final score screen for a team series.

TeamSeriesVictoryScoreScreenActivity(settings: dict)
24    def __init__(self, settings: dict):
25        super().__init__(settings=settings)
26        self._min_view_time = 15.0
27        self._is_ffa = isinstance(self.session, bs.FreeForAllSession)
28        self._allow_server_transition = True
29        self._tips_text = None
30        self._default_show_tips = False
31        self._ffa_top_player_info: list[Any] | None = None
32        self._ffa_top_player_rec: bs.PlayerRecord | None = None

Creates an Activity in the current bascenev1.Session.

The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.

Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.

default_music = None
@override
def on_begin(self) -> None:
 34    @override
 35    def on_begin(self) -> None:
 36        # pylint: disable=too-many-branches
 37        # pylint: disable=too-many-locals
 38        # pylint: disable=too-many-statements
 39        from bascenev1lib.actor.text import Text
 40        from bascenev1lib.actor.image import Image
 41
 42        bs.set_analytics_screen(
 43            'FreeForAll Series Victory Screen'
 44            if self._is_ffa
 45            else 'Teams Series Victory Screen'
 46        )
 47        assert bs.app.classic is not None
 48        if bs.app.ui_v1.uiscale is bs.UIScale.LARGE:
 49            sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText')
 50        else:
 51            sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
 52        self._show_up_next = False
 53        self._custom_continue_message = sval
 54        super().on_begin()
 55        winning_sessionteam = self.settings_raw['winner']
 56
 57        # Pause a moment before playing victory music.
 58        bs.timer(0.6, bs.WeakCall(self._play_victory_music))
 59        bs.timer(
 60            4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner'])
 61        )
 62        bs.timer(4.6, self._score_display_sound.play)
 63
 64        # Score / Name / Player-record.
 65        player_entries: list[tuple[int, str, bs.PlayerRecord]] = []
 66
 67        # Note: for ffa, exclude players who haven't entered the game yet.
 68        if self._is_ffa:
 69            for _pkey, prec in self.stats.get_records().items():
 70                if prec.player.in_game:
 71                    player_entries.append(
 72                        (
 73                            prec.player.sessionteam.customdata['score'],
 74                            prec.getname(full=True),
 75                            prec,
 76                        )
 77                    )
 78            player_entries.sort(reverse=True, key=lambda x: x[0])
 79            if len(player_entries) > 0:
 80                # Store some info for the top ffa player so we can
 81                # show winner info even if they leave.
 82                self._ffa_top_player_info = list(player_entries[0])
 83                self._ffa_top_player_info[1] = self._ffa_top_player_info[
 84                    2
 85                ].getname()
 86                self._ffa_top_player_info[2] = self._ffa_top_player_info[
 87                    2
 88                ].get_icon()
 89        else:
 90            for _pkey, prec in self.stats.get_records().items():
 91                player_entries.append((prec.score, prec.name_full, prec))
 92            player_entries.sort(reverse=True, key=lambda x: x[0])
 93
 94        ts_height = 300.0
 95        ts_h_offs = -390.0
 96        tval = 6.4
 97        t_incr = 0.12
 98
 99        always_use_first_to = bs.app.lang.get_resource(
100            'bestOfUseFirstToInstead'
101        )
102
103        session = self.session
104        if self._is_ffa:
105            assert isinstance(session, bs.FreeForAllSession)
106            txt = bs.Lstr(
107                value='${A}:',
108                subs=[
109                    (
110                        '${A}',
111                        bs.Lstr(
112                            resource='firstToFinalText',
113                            subs=[
114                                (
115                                    '${COUNT}',
116                                    str(session.get_ffa_series_length()),
117                                )
118                            ],
119                        ),
120                    )
121                ],
122            )
123        else:
124            assert isinstance(session, bs.MultiTeamSession)
125
126            # Some languages may prefer to always show 'first to X' instead of
127            # 'best of X'.
128            # FIXME: This will affect all clients connected to us even if
129            #  they're not using this language. Should try to come up
130            #  with a wording that works everywhere.
131            if always_use_first_to:
132                txt = bs.Lstr(
133                    value='${A}:',
134                    subs=[
135                        (
136                            '${A}',
137                            bs.Lstr(
138                                resource='firstToFinalText',
139                                subs=[
140                                    (
141                                        '${COUNT}',
142                                        str(
143                                            session.get_series_length() / 2 + 1
144                                        ),
145                                    )
146                                ],
147                            ),
148                        )
149                    ],
150                )
151            else:
152                txt = bs.Lstr(
153                    value='${A}:',
154                    subs=[
155                        (
156                            '${A}',
157                            bs.Lstr(
158                                resource='bestOfFinalText',
159                                subs=[
160                                    (
161                                        '${COUNT}',
162                                        str(session.get_series_length()),
163                                    )
164                                ],
165                            ),
166                        )
167                    ],
168                )
169
170        Text(
171            txt,
172            v_align=Text.VAlign.CENTER,
173            maxwidth=300,
174            color=(0.5, 0.5, 0.5, 1.0),
175            position=(0, 220),
176            scale=1.2,
177            transition=Text.Transition.IN_TOP_SLOW,
178            h_align=Text.HAlign.CENTER,
179            transition_delay=t_incr * 4,
180        ).autoretain()
181
182        win_score = (session.get_series_length() - 1) // 2 + 1
183        lose_score = 0
184        for team in self.teams:
185            if team.sessionteam.customdata['score'] != win_score:
186                lose_score = team.sessionteam.customdata['score']
187
188        if not self._is_ffa:
189            Text(
190                bs.Lstr(
191                    resource='gamesToText',
192                    subs=[
193                        ('${WINCOUNT}', str(win_score)),
194                        ('${LOSECOUNT}', str(lose_score)),
195                    ],
196                ),
197                color=(0.5, 0.5, 0.5, 1.0),
198                maxwidth=160,
199                v_align=Text.VAlign.CENTER,
200                position=(0, -215),
201                scale=1.8,
202                transition=Text.Transition.IN_LEFT,
203                h_align=Text.HAlign.CENTER,
204                transition_delay=4.8 + t_incr * 4,
205            ).autoretain()
206
207        if self._is_ffa:
208            v_extra = 120
209        else:
210            v_extra = 0
211
212        mvp: bs.PlayerRecord | None = None
213        mvp_name: str | None = None
214
215        # Show game MVP.
216        if not self._is_ffa:
217            mvp, mvp_name = None, None
218            for entry in player_entries:
219                if entry[2].team == winning_sessionteam:
220                    mvp = entry[2]
221                    mvp_name = entry[1]
222                    break
223            if mvp is not None:
224                Text(
225                    bs.Lstr(resource='mostValuablePlayerText'),
226                    color=(0.5, 0.5, 0.5, 1.0),
227                    v_align=Text.VAlign.CENTER,
228                    maxwidth=300,
229                    position=(180, ts_height / 2 + 15),
230                    transition=Text.Transition.IN_LEFT,
231                    h_align=Text.HAlign.LEFT,
232                    transition_delay=tval,
233                ).autoretain()
234                tval += 4 * t_incr
235
236                Image(
237                    mvp.get_icon(),
238                    position=(230, ts_height / 2 - 55 + 14 - 5),
239                    scale=(70, 70),
240                    transition=Image.Transition.IN_LEFT,
241                    transition_delay=tval,
242                ).autoretain()
243                assert mvp_name is not None
244                Text(
245                    bs.Lstr(value=mvp_name),
246                    position=(280, ts_height / 2 - 55 + 15 - 5),
247                    h_align=Text.HAlign.LEFT,
248                    v_align=Text.VAlign.CENTER,
249                    maxwidth=170,
250                    scale=1.3,
251                    color=bs.safecolor(mvp.team.color + (1,)),
252                    transition=Text.Transition.IN_LEFT,
253                    transition_delay=tval,
254                ).autoretain()
255                tval += 4 * t_incr
256
257        # Most violent.
258        most_kills = 0
259        for entry in player_entries:
260            if entry[2].kill_count >= most_kills:
261                mvp = entry[2]
262                mvp_name = entry[1]
263                most_kills = entry[2].kill_count
264        if mvp is not None:
265            Text(
266                bs.Lstr(resource='mostViolentPlayerText'),
267                color=(0.5, 0.5, 0.5, 1.0),
268                v_align=Text.VAlign.CENTER,
269                maxwidth=300,
270                position=(180, ts_height / 2 - 150 + v_extra + 15),
271                transition=Text.Transition.IN_LEFT,
272                h_align=Text.HAlign.LEFT,
273                transition_delay=tval,
274            ).autoretain()
275            Text(
276                bs.Lstr(
277                    value='(${A})',
278                    subs=[
279                        (
280                            '${A}',
281                            bs.Lstr(
282                                resource='killsTallyText',
283                                subs=[('${COUNT}', str(most_kills))],
284                            ),
285                        )
286                    ],
287                ),
288                position=(260, ts_height / 2 - 150 - 15 + v_extra),
289                color=(0.3, 0.3, 0.3, 1.0),
290                scale=0.6,
291                h_align=Text.HAlign.LEFT,
292                transition=Text.Transition.IN_LEFT,
293                transition_delay=tval,
294            ).autoretain()
295            tval += 4 * t_incr
296
297            Image(
298                mvp.get_icon(),
299                position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
300                scale=(50, 50),
301                transition=Image.Transition.IN_LEFT,
302                transition_delay=tval,
303            ).autoretain()
304            assert mvp_name is not None
305            Text(
306                bs.Lstr(value=mvp_name),
307                position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
308                h_align=Text.HAlign.LEFT,
309                v_align=Text.VAlign.CENTER,
310                maxwidth=180,
311                color=bs.safecolor(mvp.team.color + (1,)),
312                transition=Text.Transition.IN_LEFT,
313                transition_delay=tval,
314            ).autoretain()
315            tval += 4 * t_incr
316
317        # Most killed.
318        most_killed = 0
319        mkp, mkp_name = None, None
320        for entry in player_entries:
321            if entry[2].killed_count >= most_killed:
322                mkp = entry[2]
323                mkp_name = entry[1]
324                most_killed = entry[2].killed_count
325        if mkp is not None:
326            Text(
327                bs.Lstr(resource='mostViolatedPlayerText'),
328                color=(0.5, 0.5, 0.5, 1.0),
329                v_align=Text.VAlign.CENTER,
330                maxwidth=300,
331                position=(180, ts_height / 2 - 300 + v_extra + 15),
332                transition=Text.Transition.IN_LEFT,
333                h_align=Text.HAlign.LEFT,
334                transition_delay=tval,
335            ).autoretain()
336            Text(
337                bs.Lstr(
338                    value='(${A})',
339                    subs=[
340                        (
341                            '${A}',
342                            bs.Lstr(
343                                resource='deathsTallyText',
344                                subs=[('${COUNT}', str(most_killed))],
345                            ),
346                        )
347                    ],
348                ),
349                position=(260, ts_height / 2 - 300 - 15 + v_extra),
350                h_align=Text.HAlign.LEFT,
351                scale=0.6,
352                color=(0.3, 0.3, 0.3, 1.0),
353                transition=Text.Transition.IN_LEFT,
354                transition_delay=tval,
355            ).autoretain()
356            tval += 4 * t_incr
357            Image(
358                mkp.get_icon(),
359                position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
360                scale=(50, 50),
361                transition=Image.Transition.IN_LEFT,
362                transition_delay=tval,
363            ).autoretain()
364            assert mkp_name is not None
365            Text(
366                bs.Lstr(value=mkp_name),
367                position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
368                h_align=Text.HAlign.LEFT,
369                v_align=Text.VAlign.CENTER,
370                color=bs.safecolor(mkp.team.color + (1,)),
371                maxwidth=180,
372                transition=Text.Transition.IN_LEFT,
373                transition_delay=tval,
374            ).autoretain()
375            tval += 4 * t_incr
376
377        # Now show individual scores.
378        tdelay = tval
379        Text(
380            bs.Lstr(resource='finalScoresText'),
381            color=(0.5, 0.5, 0.5, 1.0),
382            position=(ts_h_offs, ts_height / 2),
383            transition=Text.Transition.IN_RIGHT,
384            transition_delay=tdelay,
385        ).autoretain()
386        tdelay += 4 * t_incr
387
388        v_offs = 0.0
389        tdelay += len(player_entries) * 8 * t_incr
390        for _score, name, prec in player_entries:
391            tdelay -= 4 * t_incr
392            v_offs -= 40
393            Text(
394                (
395                    str(prec.team.customdata['score'])
396                    if self._is_ffa
397                    else str(prec.score)
398                ),
399                color=(0.5, 0.5, 0.5, 1.0),
400                position=(ts_h_offs + 230, ts_height / 2 + v_offs),
401                h_align=Text.HAlign.RIGHT,
402                transition=Text.Transition.IN_RIGHT,
403                transition_delay=tdelay,
404            ).autoretain()
405            tdelay -= 4 * t_incr
406
407            Image(
408                prec.get_icon(),
409                position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
410                scale=(30, 30),
411                transition=Image.Transition.IN_LEFT,
412                transition_delay=tdelay,
413            ).autoretain()
414            Text(
415                bs.Lstr(value=name),
416                position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
417                h_align=Text.HAlign.LEFT,
418                v_align=Text.VAlign.CENTER,
419                maxwidth=180,
420                color=bs.safecolor(prec.team.color + (1,)),
421                transition=Text.Transition.IN_RIGHT,
422                transition_delay=tdelay,
423            ).autoretain()
424
425        bs.timer(15.0, bs.WeakCall(self._show_tips))

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.