Source code for bascenev1lib.activity.multiteamvictory

# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the final screen in multi-teams sessions."""

from __future__ import annotations

from typing import override, TYPE_CHECKING

import bascenev1 as bs

from bascenev1lib.activity.multiteamscore import MultiTeamScoreScreenActivity

if TYPE_CHECKING:
    from typing import Any


[docs] class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): """Final score screen for a team series.""" # Dont' play music by default; (we do manually after a delay). default_music = None def __init__(self, settings: dict): super().__init__(settings=settings) self._min_view_time = 15.0 self._is_ffa = isinstance(self.session, bs.FreeForAllSession) self._allow_server_transition = True self._tips_text = None self._default_show_tips = False self._ffa_top_player_info: list[Any] | None = None self._ffa_top_player_rec: bs.PlayerRecord | None = None
[docs] @override def on_begin(self) -> None: # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements from bascenev1lib.actor.text import Text from bascenev1lib.actor.image import Image bs.set_analytics_screen( 'FreeForAll Series Victory Screen' if self._is_ffa else 'Teams Series Victory Screen' ) assert bs.app.classic is not None if bs.app.ui_v1.uiscale is bs.UIScale.LARGE: sval = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText') else: sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') self._show_up_next = False self._custom_continue_message = sval super().on_begin() winning_sessionteam = self.settings_raw['winner'] # Pause a moment before playing victory music. bs.timer(0.6, bs.WeakCall(self._play_victory_music)) bs.timer( 4.4, bs.WeakCall(self._show_winner, self.settings_raw['winner']) ) bs.timer(4.6, self._score_display_sound.play) # Score / Name / Player-record. player_entries: list[tuple[int, str, bs.PlayerRecord]] = [] # Note: for ffa, exclude players who haven't entered the game yet. if self._is_ffa: for _pkey, prec in self.stats.get_records().items(): if prec.player.in_game: player_entries.append( ( prec.player.sessionteam.customdata['score'], prec.getname(full=True), prec, ) ) player_entries.sort(reverse=True, key=lambda x: x[0]) if len(player_entries) > 0: # Store some info for the top ffa player so we can # show winner info even if they leave. self._ffa_top_player_info = list(player_entries[0]) self._ffa_top_player_info[1] = self._ffa_top_player_info[ 2 ].getname() self._ffa_top_player_info[2] = self._ffa_top_player_info[ 2 ].get_icon() else: for _pkey, prec in self.stats.get_records().items(): player_entries.append((prec.score, prec.name_full, prec)) player_entries.sort(reverse=True, key=lambda x: x[0]) ts_height = 300.0 ts_h_offs = -390.0 tval = 6.4 t_incr = 0.12 always_use_first_to = bs.app.lang.get_resource( 'bestOfUseFirstToInstead' ) session = self.session if self._is_ffa: assert isinstance(session, bs.FreeForAllSession) txt = bs.Lstr( value='${A}:', subs=[ ( '${A}', bs.Lstr( resource='firstToFinalText', subs=[ ( '${COUNT}', str(session.get_ffa_series_length()), ) ], ), ) ], ) else: assert isinstance(session, bs.MultiTeamSession) # Some languages may prefer to always show 'first to X' instead of # 'best of X'. # FIXME: This will affect all clients connected to us even if # they're not using this language. Should try to come up # with a wording that works everywhere. if always_use_first_to: txt = bs.Lstr( value='${A}:', subs=[ ( '${A}', bs.Lstr( resource='firstToFinalText', subs=[ ( '${COUNT}', str( session.get_series_length() / 2 + 1 ), ) ], ), ) ], ) else: txt = bs.Lstr( value='${A}:', subs=[ ( '${A}', bs.Lstr( resource='bestOfFinalText', subs=[ ( '${COUNT}', str(session.get_series_length()), ) ], ), ) ], ) Text( txt, v_align=Text.VAlign.CENTER, maxwidth=300, color=(0.5, 0.5, 0.5, 1.0), position=(0, 220), scale=1.2, transition=Text.Transition.IN_TOP_SLOW, h_align=Text.HAlign.CENTER, transition_delay=t_incr * 4, ).autoretain() win_score = (session.get_series_length() - 1) // 2 + 1 lose_score = 0 for team in self.teams: if team.sessionteam.customdata['score'] != win_score: lose_score = team.sessionteam.customdata['score'] if not self._is_ffa: Text( bs.Lstr( resource='gamesToText', subs=[ ('${WINCOUNT}', str(win_score)), ('${LOSECOUNT}', str(lose_score)), ], ), color=(0.5, 0.5, 0.5, 1.0), maxwidth=160, v_align=Text.VAlign.CENTER, position=(0, -215), scale=1.8, transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.CENTER, transition_delay=4.8 + t_incr * 4, ).autoretain() if self._is_ffa: v_extra = 120 else: v_extra = 0 mvp: bs.PlayerRecord | None = None mvp_name: str | None = None # Show game MVP. if not self._is_ffa: mvp, mvp_name = None, None for entry in player_entries: if entry[2].team == winning_sessionteam: mvp = entry[2] mvp_name = entry[1] break if mvp is not None: Text( bs.Lstr(resource='mostValuablePlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, position=(180, ts_height / 2 + 15), transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.LEFT, transition_delay=tval, ).autoretain() tval += 4 * t_incr Image( mvp.get_icon(), position=(230, ts_height / 2 - 55 + 14 - 5), scale=(70, 70), transition=Image.Transition.IN_LEFT, transition_delay=tval, ).autoretain() assert mvp_name is not None Text( bs.Lstr(value=mvp_name), position=(280, ts_height / 2 - 55 + 15 - 5), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=170, scale=1.3, color=bs.safecolor(mvp.team.color + (1,)), transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() tval += 4 * t_incr # Most violent. most_kills = 0 for entry in player_entries: if entry[2].kill_count >= most_kills: mvp = entry[2] mvp_name = entry[1] most_kills = entry[2].kill_count if mvp is not None: Text( bs.Lstr(resource='mostViolentPlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, position=(180, ts_height / 2 - 150 + v_extra + 15), transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.LEFT, transition_delay=tval, ).autoretain() Text( bs.Lstr( value='(${A})', subs=[ ( '${A}', bs.Lstr( resource='killsTallyText', subs=[('${COUNT}', str(most_kills))], ), ) ], ), position=(260, ts_height / 2 - 150 - 15 + v_extra), color=(0.3, 0.3, 0.3, 1.0), scale=0.6, h_align=Text.HAlign.LEFT, transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() tval += 4 * t_incr Image( mvp.get_icon(), position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra), scale=(50, 50), transition=Image.Transition.IN_LEFT, transition_delay=tval, ).autoretain() assert mvp_name is not None Text( bs.Lstr(value=mvp_name), position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=180, color=bs.safecolor(mvp.team.color + (1,)), transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() tval += 4 * t_incr # Most killed. most_killed = 0 mkp, mkp_name = None, None for entry in player_entries: if entry[2].killed_count >= most_killed: mkp = entry[2] mkp_name = entry[1] most_killed = entry[2].killed_count if mkp is not None: Text( bs.Lstr(resource='mostViolatedPlayerText'), color=(0.5, 0.5, 0.5, 1.0), v_align=Text.VAlign.CENTER, maxwidth=300, position=(180, ts_height / 2 - 300 + v_extra + 15), transition=Text.Transition.IN_LEFT, h_align=Text.HAlign.LEFT, transition_delay=tval, ).autoretain() Text( bs.Lstr( value='(${A})', subs=[ ( '${A}', bs.Lstr( resource='deathsTallyText', subs=[('${COUNT}', str(most_killed))], ), ) ], ), position=(260, ts_height / 2 - 300 - 15 + v_extra), h_align=Text.HAlign.LEFT, scale=0.6, color=(0.3, 0.3, 0.3, 1.0), transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() tval += 4 * t_incr Image( mkp.get_icon(), position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra), scale=(50, 50), transition=Image.Transition.IN_LEFT, transition_delay=tval, ).autoretain() assert mkp_name is not None Text( bs.Lstr(value=mkp_name), position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, color=bs.safecolor(mkp.team.color + (1,)), maxwidth=180, transition=Text.Transition.IN_LEFT, transition_delay=tval, ).autoretain() tval += 4 * t_incr # Now show individual scores. tdelay = tval Text( bs.Lstr(resource='finalScoresText'), color=(0.5, 0.5, 0.5, 1.0), position=(ts_h_offs, ts_height / 2), transition=Text.Transition.IN_RIGHT, transition_delay=tdelay, ).autoretain() tdelay += 4 * t_incr v_offs = 0.0 tdelay += len(player_entries) * 8 * t_incr for _score, name, prec in player_entries: tdelay -= 4 * t_incr v_offs -= 40 Text( ( str(prec.team.customdata['score']) if self._is_ffa else str(prec.score) ), color=(0.5, 0.5, 0.5, 1.0), position=(ts_h_offs + 230, ts_height / 2 + v_offs), h_align=Text.HAlign.RIGHT, transition=Text.Transition.IN_RIGHT, transition_delay=tdelay, ).autoretain() tdelay -= 4 * t_incr Image( prec.get_icon(), position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15), scale=(30, 30), transition=Image.Transition.IN_LEFT, transition_delay=tdelay, ).autoretain() Text( bs.Lstr(value=name), position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, maxwidth=180, color=bs.safecolor(prec.team.color + (1,)), transition=Text.Transition.IN_RIGHT, transition_delay=tdelay, ).autoretain() bs.timer(15.0, bs.WeakCall(self._show_tips))
def _show_tips(self) -> None: from bascenev1lib.actor.tipstext import TipsText self._tips_text = TipsText(offs_y=70) def _play_victory_music(self) -> None: # Make sure we don't stomp on the next activity's music choice. if not self.is_transitioning_out(): bs.setmusic(bs.MusicType.VICTORY) def _show_winner(self, team: bs.SessionTeam) -> None: from bascenev1lib.actor.image import Image from bascenev1lib.actor.zoomtext import ZoomText if not self._is_ffa: offs_v = 0.0 ZoomText( team.name, position=(0, 97), color=team.color, scale=1.15, jitter=1.0, maxwidth=250, ).autoretain() else: offs_v = -80 assert isinstance(self.session, bs.MultiTeamSession) series_length = self.session.get_ffa_series_length() icon: dict | None # Pull live player info if they're still around. if len(team.players) == 1: icon = team.players[0].get_icon() player_name = team.players[0].getname(full=True, icon=False) # Otherwise use the special info we stored when we came in. elif ( self._ffa_top_player_info is not None and self._ffa_top_player_info[0] >= series_length ): icon = self._ffa_top_player_info[2] player_name = self._ffa_top_player_info[1] else: icon = None player_name = 'Player Not Found' if icon is not None: i = Image( icon, position=(0, 143), scale=(100, 100), ).autoretain() assert i.node bs.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0}) ZoomText( bs.Lstr(value=player_name), position=(0, 97 + offs_v + (0 if icon is not None else 60)), color=team.color, scale=1.15, jitter=1.0, maxwidth=250, ).autoretain() s_extra = 1.0 if self._is_ffa else 1.0 # Some languages say "FOO WINS" differently for teams vs players. if isinstance(self.session, bs.FreeForAllSession): wins_resource = 'seriesWinLine1PlayerText' else: wins_resource = 'seriesWinLine1TeamText' wins_text = bs.Lstr(resource=wins_resource) # Temp - if these come up as the english default, fall-back to the # unified old form which is more likely to be translated. ZoomText( wins_text, position=(0, -10 + offs_v), color=team.color, scale=0.65 * s_extra, jitter=1.0, maxwidth=250, ).autoretain() ZoomText( bs.Lstr(resource='seriesWinLine2Text'), position=(0, -110 + offs_v), scale=1.0 * s_extra, color=team.color, jitter=1.0, maxwidth=250, ).autoretain()