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='mostDestroyedPlayerText'), 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()
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum