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

Final score screen for a team series.

TeamSeriesVictoryScoreScreenActivity(settings: dict)
18    def __init__(self, settings: dict):
19        super().__init__(settings=settings)
20        self._min_view_time = 15.0
21        self._is_ffa = isinstance(self.session, bs.FreeForAllSession)
22        self._allow_server_transition = True
23        self._tips_text = None
24        self._default_show_tips = False

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

Inherited Members
bascenev1lib.activity.multiteamscore.MultiTeamScoreScreenActivity
show_player_scores
bascenev1._activitytypes.ScoreScreenActivity
transition_time
inherits_tint
inherits_vr_camera_offset
use_fixed_vr_overlay
on_player_join
on_transition_in
bascenev1._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
allow_pausing
allow_kick_idle_players
slow_motion
inherits_slow_motion
inherits_music
inherits_vr_overlay_center
allow_mid_activity_joins
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_player_leave
on_team_join
on_team_leave
on_transition_out
handlemessage
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
end
create_player
create_team
bascenev1._dependency.DependencyComponent
dep_is_present
get_dynamic_deps