Source code for bascenev1lib.activity.coopscore

# Released under the MIT License. See LICENSE for details.
#
"""Provides a score screen for coop games."""
# pylint: disable=too-many-lines

from __future__ import annotations

import random
import logging
from typing import TYPE_CHECKING, override

from bacommon.login import LoginType
import bascenev1 as bs
import bauiv1 as bui

from bascenev1lib.actor.text import Text
from bascenev1lib.actor.zoomtext import ZoomText

if TYPE_CHECKING:
    from typing import Any, Sequence

    from bauiv1lib.store.button import StoreButton
    from bauiv1lib.league.rankbutton import LeagueRankButton


[docs] class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]): """Score screen showing the results of a cooperative game.""" def __init__(self, settings: dict): # pylint: disable=too-many-statements super().__init__(settings) plus = bs.app.plus assert plus is not None # Keep prev activity alive while we fade in self.transition_time = 0.5 self.inherits_tint = True self.inherits_vr_camera_offset = True self.inherits_music = True self.use_fixed_vr_overlay = True self._do_new_rating: bool = self.session.tournament_id is not None self._score_display_sound = bs.getsound('scoreHit01') self._score_display_sound_small = bs.getsound('scoreHit02') self.drum_roll_sound = bs.getsound('drumRoll') self.cymbal_sound = bs.getsound('cymbal') self._replay_icon_texture = bui.gettexture('replayIcon') self._menu_icon_texture = bui.gettexture('menuIcon') self._next_level_icon_texture = bui.gettexture('nextLevelIcon') self._campaign: bs.Campaign = settings['campaign'] self._have_achievements = ( bs.app.classic is not None and bs.app.classic.ach.achievements_for_coop_level( self._campaign.name + ':' + settings['level'] ) ) self._game_service_icon_color: Sequence[float] | None self._game_service_achievements_texture: bui.Texture | None self._game_service_leaderboards_texture: bui.Texture | None # Tie in to specific game services if they are active. adapter = plus.accounts.login_adapters.get(LoginType.GPGS) gpgs_active = adapter is not None and adapter.is_back_end_active() adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER) game_center_active = ( adapter is not None and adapter.is_back_end_active() ) if game_center_active: self._game_service_icon_color = (1.0, 1.0, 1.0) icon = bui.gettexture('gameCenterIcon') self._game_service_achievements_texture = icon self._game_service_leaderboards_texture = icon self._account_has_achievements = True elif gpgs_active: self._game_service_icon_color = (0.8, 1.0, 0.6) self._game_service_achievements_texture = bui.gettexture( 'googlePlayAchievementsIcon' ) self._game_service_leaderboards_texture = bui.gettexture( 'googlePlayLeaderboardsIcon' ) self._account_has_achievements = True else: self._game_service_icon_color = None self._game_service_achievements_texture = None self._game_service_leaderboards_texture = None self._account_has_achievements = False self._cashregistersound = bs.getsound('cashRegister') self._gun_cocking_sound = bs.getsound('gunCocking') self._dingsound = bs.getsound('ding') self._score_link: str | None = None self._root_ui: bui.Widget | None = None self._background: bs.Actor | None = None self._old_best_rank = 0.0 self._game_name_str: str | None = None self._game_config_str: str | None = None # Ui bits. self._corner_button_offs: tuple[float, float] | None = None self._league_rank_button: LeagueRankButton | None = None self._store_button_instance: StoreButton | None = None self._restart_button: bui.Widget | None = None self._update_corner_button_positions_timer: bui.AppTimer | None = None self._next_level_error: bs.Actor | None = None # Score/gameplay bits. self._was_complete: bool | None = None self._is_complete: bool | None = None self._newly_complete: bool | None = None self._is_more_levels: bool | None = None self._next_level_name: str | None = None self._show_info: dict[str, Any] | None = None self._name_str: str | None = None self._friends_loading_status: bs.Actor | None = None self._score_loading_status: bs.Actor | None = None self._tournament_time_remaining: float | None = None self._tournament_time_remaining_text: Text | None = None self._tournament_time_remaining_text_timer: bs.BaseTimer | None = None self._submit_score = self.session.submit_score # Stuff for activity skip by pressing button self._birth_time = bs.time() self._min_view_time = 5.0 self._allow_server_transition = False self._server_transitioning: bool | None = None self._playerinfos: list[bs.PlayerInfo] = settings['playerinfos'] assert isinstance(self._playerinfos, list) assert all(isinstance(i, bs.PlayerInfo) for i in self._playerinfos) self._score: int | None = settings['score'] assert isinstance(self._score, (int, type(None))) self._fail_message: bs.Lstr | None = settings['fail_message'] assert isinstance(self._fail_message, (bs.Lstr, type(None))) self._begin_time: float | None = None self._score_order: str if 'score_order' in settings: if not settings['score_order'] in ['increasing', 'decreasing']: raise ValueError( 'Invalid score order: ' + settings['score_order'] ) self._score_order = settings['score_order'] else: self._score_order = 'increasing' assert isinstance(self._score_order, str) self._score_type: str if 'score_type' in settings: if not settings['score_type'] in ['points', 'time']: raise ValueError( 'Invalid score type: ' + settings['score_type'] ) self._score_type = settings['score_type'] else: self._score_type = 'points' assert isinstance(self._score_type, str) self._level_name: str = settings['level'] assert isinstance(self._level_name, str) self._game_name_str = self._campaign.name + ':' + self._level_name self._game_config_str = ( str(len(self._playerinfos)) + 'p' + self._campaign.getlevel(self._level_name) .get_score_version_string() .replace(' ', '_') ) try: self._old_best_rank = self._campaign.getlevel( self._level_name ).rating except Exception: self._old_best_rank = 0.0 self._victory: bool = settings['outcome'] == 'victory' @override def __del__(self) -> None: super().__del__() # If our UI is still up, kill it. if self._root_ui and not self._root_ui.transitioning_out: with bui.ContextRef.empty(): bui.containerwidget(edit=self._root_ui, transition='out_left')
[docs] @override def on_transition_in(self) -> None: from bascenev1lib.actor import background # FIXME NO BSSTD bs.set_analytics_screen('Coop Score Screen') super().on_transition_in() self._background = background.Background( fade_time=0.45, start_faded=False, show_logo=True )
def _ui_menu(self) -> None: from bauiv1lib import specialoffer if specialoffer.show_offer(): return bui.containerwidget(edit=self._root_ui, transition='out_left') with self.context: bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end))) def _ui_restart(self) -> None: from bauiv1lib.tournamententry import TournamentEntryWindow from bauiv1lib import specialoffer if specialoffer.show_offer(): return # If we're in a tournament and it looks like there's no time left, # disallow. if self.session.tournament_id is not None: if self._tournament_time_remaining is None: bui.screenmessage( bui.Lstr(resource='tournamentCheckingStateText'), color=(1, 0, 0), ) bui.getsound('error').play() return if self._tournament_time_remaining <= 0: bui.screenmessage( bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0) ) bui.getsound('error').play() return # If there are currently fewer players than our session min, # don't allow. if len(self.players) < self.session.min_players: bui.screenmessage( bui.Lstr(resource='notEnoughPlayersRemainingText'), color=(1, 0, 0), ) bui.getsound('error').play() return self._campaign.set_selected_level(self._level_name) # If this is a tournament, go back to the tournament-entry UI # otherwise just hop back in. tournament_id = self.session.tournament_id if tournament_id is not None: assert self._restart_button is not None TournamentEntryWindow( tournament_id=tournament_id, tournament_activity=self, position=self._restart_button.get_screen_space_center(), ) else: bui.containerwidget(edit=self._root_ui, transition='out_left') self.can_show_ad_on_death = True with self.context: self.end({'outcome': 'restart'}) def _ui_next(self) -> None: from bauiv1lib.specialoffer import show_offer if show_offer(): return # If we didn't just complete this level but are choosing to play the # next one, set it as current (this won't happen otherwise). if ( self._is_complete and self._is_more_levels and not self._newly_complete ): assert self._next_level_name is not None self._campaign.set_selected_level(self._next_level_name) bui.containerwidget(edit=self._root_ui, transition='out_left') with self.context: self.end({'outcome': 'next_level'}) def _ui_gc(self) -> None: if bs.app.plus is not None: bs.app.plus.show_game_service_ui( 'leaderboard', game=self._game_name_str, game_version=self._game_config_str, ) else: logging.warning('show_game_service_ui requires plus feature-set') def _ui_show_achievements(self) -> None: if bs.app.plus is not None: bs.app.plus.show_game_service_ui('achievements') else: logging.warning('show_game_service_ui requires plus feature-set') def _ui_worlds_best(self) -> None: if self._score_link is None: bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource='scoreListUnavailableText'), color=(1, 0.5, 0) ) else: bui.open_url(self._score_link) def _ui_error(self) -> None: with self.context: self._next_level_error = Text( bs.Lstr(resource='completeThisLevelToProceedText'), flash=True, maxwidth=360, scale=0.54, h_align=Text.HAlign.CENTER, color=(0.5, 0.7, 0.5, 1), position=(300, -235), ) bui.getsound('error').play() bs.timer( 2.0, bs.WeakCall( self._next_level_error.handlemessage, bs.DieMessage() ), ) def _should_show_worlds_best_button(self) -> bool: # Link is too complicated to display with no browser. return bui.is_browser_likely_available()
[docs] def request_ui(self) -> None: """Set up a callback to show our UI at the next opportune time.""" assert bui.app.classic is not None # We don't want to just show our UI in case the user already has the # main menu up, so instead we add a callback for when the menu # closes; if we're still alive, we'll come up then. # If there's no main menu this gets called immediately. bui.app.ui_v1.add_main_menu_close_callback(bui.WeakCall(self.show_ui))
[docs] def show_ui(self) -> None: """Show the UI for restarting, playing the next Level, etc.""" # pylint: disable=too-many-locals from bauiv1lib.store.button import StoreButton from bauiv1lib.league.rankbutton import LeagueRankButton assert bui.app.classic is not None env = bui.app.env delay = 0.7 if (self._score is not None) else 0.0 # If there's no players left in the game, lets not show the UI # (that would allow restarting the game with zero players, etc). if not self.players: return rootc = self._root_ui = bui.containerwidget( size=(0, 0), transition='in_right' ) h_offs = 7.0 v_offs = -280.0 # We wanna prevent controllers users from popping up browsers # or game-center widgets in cases where they can't easily get back # to the game (like on mac). can_select_extra_buttons = bui.app.classic.platform == 'android' bui.set_ui_input_device(None) # Menu is up for grabs. if self._have_achievements and self._account_has_achievements: bui.buttonwidget( parent=rootc, color=(0.45, 0.4, 0.5), position=(h_offs - 520, v_offs + 450 - 235 + 40), size=(300, 60), label=bui.Lstr(resource='achievementsText'), on_activate_call=bui.WeakCall(self._ui_show_achievements), transition_delay=delay + 1.5, icon=self._game_service_achievements_texture, icon_color=self._game_service_icon_color, autoselect=True, selectable=can_select_extra_buttons, ) if self._should_show_worlds_best_button(): bui.buttonwidget( parent=rootc, color=(0.45, 0.4, 0.5), position=(160, v_offs + 480), size=(350, 62), label=( bui.Lstr(resource='tournamentStandingsText') if self.session.tournament_id is not None else ( bui.Lstr(resource='worldsBestScoresText') if self._score_type == 'points' else bui.Lstr(resource='worldsBestTimesText') ) ), autoselect=True, on_activate_call=bui.WeakCall(self._ui_worlds_best), transition_delay=delay + 1.9, selectable=can_select_extra_buttons, ) else: pass show_next_button = self._is_more_levels and not (env.demo or env.arcade) if not show_next_button: h_offs += 70 menu_button = bui.buttonwidget( parent=rootc, autoselect=True, position=(h_offs - 130 - 60, v_offs), size=(110, 85), label='', on_activate_call=bui.WeakCall(self._ui_menu), ) bui.imagewidget( parent=rootc, draw_controller=menu_button, position=(h_offs - 130 - 60 + 22, v_offs + 14), size=(60, 60), texture=self._menu_icon_texture, opacity=0.8, ) self._restart_button = restart_button = bui.buttonwidget( parent=rootc, autoselect=True, position=(h_offs - 60, v_offs), size=(110, 85), label='', on_activate_call=bui.WeakCall(self._ui_restart), ) bui.imagewidget( parent=rootc, draw_controller=restart_button, position=(h_offs - 60 + 19, v_offs + 7), size=(70, 70), texture=self._replay_icon_texture, opacity=0.8, ) next_button: bui.Widget | None = None # Our 'next' button is disabled if we haven't unlocked the next # level yet and invisible if there is none. if show_next_button: if self._is_complete: call = bui.WeakCall(self._ui_next) button_sound = True image_opacity = 0.8 color = None else: call = bui.WeakCall(self._ui_error) button_sound = False image_opacity = 0.2 color = (0.3, 0.3, 0.3) next_button = bui.buttonwidget( parent=rootc, autoselect=True, position=(h_offs + 130 - 60, v_offs), size=(110, 85), label='', on_activate_call=call, color=color, enable_sound=button_sound, ) bui.imagewidget( parent=rootc, draw_controller=next_button, position=(h_offs + 130 - 60 + 12, v_offs + 5), size=(80, 80), texture=self._next_level_icon_texture, opacity=image_opacity, ) x_offs_extra = 0 if show_next_button else -100 self._corner_button_offs = ( h_offs + 300.0 + 100.0 + x_offs_extra, v_offs + 560.0, ) if env.demo or env.arcade: self._league_rank_button = None self._store_button_instance = None else: self._league_rank_button = LeagueRankButton( parent=rootc, position=(h_offs + 300 + 100 + x_offs_extra, v_offs + 560), size=(100, 60), scale=0.9, color=(0.4, 0.4, 0.9), textcolor=(0.9, 0.9, 2.0), transition_delay=0.0, smooth_update_delay=5.0, ) self._store_button_instance = StoreButton( parent=rootc, position=(h_offs + 400 + 100 + x_offs_extra, v_offs + 560), show_tickets=True, sale_scale=0.85, size=(100, 60), scale=0.9, button_type='square', color=(0.35, 0.25, 0.45), textcolor=(0.9, 0.7, 1.0), transition_delay=0.0, ) bui.containerwidget( edit=rootc, selected_child=( next_button if (self._newly_complete and self._victory and show_next_button) else restart_button ), on_cancel_call=menu_button.activate, ) self._update_corner_button_positions() self._update_corner_button_positions_timer = bui.AppTimer( 1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True )
def _update_corner_button_positions(self) -> None: offs = -55 if bui.is_party_icon_visible() else 0 assert self._corner_button_offs is not None pos_x = self._corner_button_offs[0] + offs pos_y = self._corner_button_offs[1] if self._league_rank_button is not None: self._league_rank_button.set_position((pos_x, pos_y)) if self._store_button_instance is not None: self._store_button_instance.set_position((pos_x + 100, pos_y)) def _player_press(self) -> None: # (Only for headless builds). # If this activity is a good 'end point', ask server-mode just once if # it wants to do anything special like switch sessions or kill the app. if ( self._allow_server_transition and bs.app.classic is not None and bs.app.classic.server is not None and self._server_transitioning is None ): self._server_transitioning = ( bs.app.classic.server.handle_transition() ) assert isinstance(self._server_transitioning, bool) # If server-mode is handling this, don't do anything ourself. if self._server_transitioning is True: return # Otherwise restart current level. self._campaign.set_selected_level(self._level_name) with self.context: self.end({'outcome': 'restart'}) def _safe_assign(self, player: bs.Player) -> None: # (Only for headless builds). # Just to be extra careful, don't assign if we're transitioning out. # (though theoretically that should be ok). if not self.is_transitioning_out() and player: player.assigninput( ( bs.InputType.JUMP_PRESS, bs.InputType.PUNCH_PRESS, bs.InputType.BOMB_PRESS, bs.InputType.PICK_UP_PRESS, ), self._player_press, )
[docs] @override def on_player_join(self, player: bs.Player) -> None: super().on_player_join(player) if bs.app.classic is not None and bs.app.classic.server is not None: # Host can't press retry button, so anyone can do it instead. time_till_assign = max( 0, self._birth_time + self._min_view_time - bs.time() ) bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player))
[docs] @override def on_begin(self) -> None: # FIXME: Clean this up. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals super().on_begin() app = bs.app env = app.env plus = app.plus assert plus is not None self._begin_time = bs.time() # Calc whether the level is complete and other stuff. levels = self._campaign.levels level = self._campaign.getlevel(self._level_name) self._was_complete = level.complete self._is_complete = self._was_complete or self._victory self._newly_complete = self._is_complete and not self._was_complete self._is_more_levels = ( level.index < len(levels) - 1 ) and self._campaign.sequential # Any time we complete a level, set the next one as unlocked. if self._is_complete and self._is_more_levels: plus.add_v1_account_transaction( { 'type': 'COMPLETE_LEVEL', 'campaign': self._campaign.name, 'level': self._level_name, } ) self._next_level_name = levels[level.index + 1].name # If this is the first time we completed it, set the next one # as current. if self._newly_complete: cfg = app.config cfg['Selected Coop Game'] = ( self._campaign.name + ':' + self._next_level_name ) cfg.commit() self._campaign.set_selected_level(self._next_level_name) bs.timer(1.0, bs.WeakCall(self.request_ui)) if ( self._is_complete and self._victory and self._is_more_levels and not (env.demo or env.arcade) ): Text( ( bs.Lstr( value='${A}:\n', subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))], ) if self._newly_complete else bs.Lstr( value='${A}:\n', subs=[('${A}', bs.Lstr(resource='nextLevelText'))], ) ), transition=Text.Transition.IN_RIGHT, transition_delay=5.2, flash=self._newly_complete, scale=0.54, h_align=Text.HAlign.CENTER, maxwidth=270, color=(0.5, 0.7, 0.5, 1), position=(270, -235), ).autoretain() assert self._next_level_name is not None Text( bs.Lstr(translate=('coopLevelNames', self._next_level_name)), transition=Text.Transition.IN_RIGHT, transition_delay=5.2, flash=self._newly_complete, scale=0.7, h_align=Text.HAlign.CENTER, maxwidth=205, color=(0.5, 0.7, 0.5, 1), position=(270, -255), ).autoretain() if self._newly_complete: bs.timer(5.2, self._cashregistersound.play) bs.timer(5.2, self._dingsound.play) offs_x = -195 if len(self._playerinfos) > 1: pstr = bs.Lstr( value='- ${A} -', subs=[ ( '${A}', bs.Lstr( resource='multiPlayerCountText', subs=[('${COUNT}', str(len(self._playerinfos)))], ), ) ], ) else: pstr = bs.Lstr( value='- ${A} -', subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))], ) ZoomText( self._campaign.getlevel(self._level_name).displayname, maxwidth=800, flash=False, trail=False, color=(0.5, 1, 0.5, 1), h_align='center', scale=0.4, position=(0, 292), jitter=1.0, ).autoretain() Text( pstr, maxwidth=300, transition=Text.Transition.FADE_IN, scale=0.7, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=(0.5, 0.7, 0.5, 1), position=(0, 230), ).autoretain() if app.classic is not None and app.classic.server is None: # If we're running in normal non-headless build, show this text # because only host can continue the game. adisp = plus.get_v1_account_display_string() txt = Text( bs.Lstr( resource='waitingForHostText', subs=[('${HOST}', adisp)] ), maxwidth=300, transition=Text.Transition.FADE_IN, transition_delay=8.0, scale=0.85, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, color=(1, 1, 0, 1), position=(0, -230), ).autoretain() assert txt.node txt.node.client_only = True else: # In headless build, anyone can continue the game. sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') Text( sval, v_attach=Text.VAttach.BOTTOM, h_align=Text.HAlign.CENTER, flash=True, vr_depth=50, position=(0, 60), scale=0.8, color=(0.5, 0.7, 0.5, 0.5), transition=Text.Transition.IN_BOTTOM_SLOW, transition_delay=self._min_view_time, ).autoretain() if self._score is not None: bs.timer(0.35, self._score_display_sound_small.play) # Vestigial remain; this stuff should just be instance vars. self._show_info = {} if self._score is not None: bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x)) else: bs.pushcall(bs.WeakCall(self._show_fail)) self._name_str = name_str = ', '.join( [p.name for p in self._playerinfos] ) self._score_loading_status = Text( bs.Lstr( value='${A}...', subs=[('${A}', bs.Lstr(resource='loadingText'))], ), position=(280, 150 + 30), color=(1, 1, 1, 0.4), transition=Text.Transition.FADE_IN, scale=0.7, transition_delay=2.0, ) if self._score is not None and self._submit_score: bs.timer(0.4, bs.WeakCall(self._play_drumroll)) # Add us to high scores, filter, and store. our_high_scores_all = self._campaign.getlevel( self._level_name ).get_high_scores() our_high_scores = our_high_scores_all.setdefault( str(len(self._playerinfos)) + ' Player', [] ) if self._score is not None: our_score: list | None = [ self._score, { 'players': [ {'name': p.name, 'character': p.character} for p in self._playerinfos ] }, ] our_high_scores.append(our_score) else: our_score = None try: our_high_scores.sort( reverse=self._score_order == 'increasing', key=lambda x: x[0] ) except Exception: logging.exception('Error sorting scores.') print(f'our_high_scores: {our_high_scores}') del our_high_scores[10:] if self._score is not None: sver = self._campaign.getlevel( self._level_name ).get_score_version_string() plus.add_v1_account_transaction( { 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', 'campaign': self._campaign.name, 'level': self._level_name, 'scoreVersion': sver, 'scores': our_high_scores_all, } ) if plus.get_v1_account_state() != 'signed_in': # We expect this only in kiosk mode; complain otherwise. if not (env.demo or env.arcade): logging.error('got not-signed-in at score-submit; unexpected') bs.pushcall(bs.WeakCall(self._got_score_results, None)) else: assert self._game_name_str is not None assert self._game_config_str is not None plus.submit_score( self._game_name_str, self._game_config_str, name_str, self._score, bs.WeakCall(self._got_score_results), order=self._score_order, tournament_id=self.session.tournament_id, score_type=self._score_type, campaign=self._campaign.name, level=self._level_name, ) # Apply the transactions we've been adding locally. plus.run_v1_account_transactions() # If we're not doing the world's-best button, just show a title # instead. ts_height = 300 ts_h_offs = 210 v_offs = 40 txt = Text( ( bs.Lstr(resource='tournamentStandingsText') if self.session.tournament_id is not None else ( bs.Lstr(resource='worldsBestScoresText') if self._score_type == 'points' else bs.Lstr(resource='worldsBestTimesText') ) ), maxwidth=210, position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), transition=Text.Transition.IN_LEFT, v_align=Text.VAlign.CENTER, scale=1.2, transition_delay=2.2, ).autoretain() # If we've got a button on the server, only show this on clients. if self._should_show_worlds_best_button(): assert txt.node txt.node.client_only = True ts_height = 300 ts_h_offs = -480 v_offs = 40 Text( ( bs.Lstr(resource='yourBestScoresText') if self._score_type == 'points' else bs.Lstr(resource='yourBestTimesText') ), maxwidth=210, position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), transition=Text.Transition.IN_RIGHT, v_align=Text.VAlign.CENTER, scale=1.2, transition_delay=1.8, ).autoretain() display_scores = list(our_high_scores) display_count = 5 while len(display_scores) < display_count: display_scores.append((0, None)) showed_ours = False h_offs_extra = 85 if self._score_type == 'points' else 130 v_offs_extra = 20 v_offs_names = 0 scale = 1.0 p_count = len(self._playerinfos) h_offs_extra -= 75 if p_count > 1: h_offs_extra -= 20 if p_count == 2: scale = 0.9 elif p_count == 3: scale = 0.65 elif p_count == 4: scale = 0.5 times: list[tuple[float, float]] = [] for i in range(display_count): times.insert( random.randrange(0, len(times) + 1), (1.9 + i * 0.05, 2.3 + i * 0.05), ) for i in range(display_count): try: if display_scores[i][1] is None: name_str = '-' else: # noinspection PyUnresolvedReferences name_str = ', '.join( [p['name'] for p in display_scores[i][1]['players']] ) except Exception: logging.exception( 'Error calcing name_str for %s.', display_scores ) name_str = '-' if display_scores[i] == our_score and not showed_ours: flash = True color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.6, 0.6, 0.6, 1.0) tdelay1 = 3.7 tdelay2 = 3.7 showed_ours = True else: flash = False color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.6, 0.6, 0.6, 1.0) tdelay1 = times[i][0] tdelay2 = times[i][1] Text( ( str(display_scores[i][0]) if self._score_type == 'points' else bs.timestring((display_scores[i][0] * 10) / 1000.0) ), position=( ts_h_offs + 20 + h_offs_extra, v_offs_extra + ts_height / 2 + -ts_height * (i + 1) / 10 + v_offs + 11.0, ), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, color=color0, flash=flash, transition=Text.Transition.IN_RIGHT, transition_delay=tdelay1, ).autoretain() Text( bs.Lstr(value=name_str), position=( ts_h_offs + 35 + h_offs_extra, v_offs_extra + ts_height / 2 + -ts_height * (i + 1) / 10 + v_offs_names + v_offs + 11.0, ), maxwidth=80.0 + 100.0 * len(self._playerinfos), v_align=Text.VAlign.CENTER, color=color1, flash=flash, scale=scale, transition=Text.Transition.IN_RIGHT, transition_delay=tdelay2, ).autoretain() # Show achievements for this level. ts_height = -150 ts_h_offs = -480 v_offs = 40 # Only make this if we don't have the button # (never want clients to see it so no need for client-only # version, etc). if self._have_achievements: if not self._account_has_achievements: Text( bs.Lstr(resource='achievementsText'), position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3), maxwidth=210, host_only=True, transition=Text.Transition.IN_RIGHT, v_align=Text.VAlign.CENTER, scale=1.2, transition_delay=2.8, ).autoretain() assert self._game_name_str is not None assert bs.app.classic is not None achievements = bs.app.classic.ach.achievements_for_coop_level( self._game_name_str ) hval = -455 vval = -100 tdelay = 0.0 for ach in achievements: ach.create_display(hval, vval + v_offs, 3.0 + tdelay) vval -= 55 tdelay += 0.250 bs.timer(5.0, bs.WeakCall(self._show_tips))
def _play_drumroll(self) -> None: bs.NodeActor( bs.newnode( 'sound', attrs={ 'sound': self.drum_roll_sound, 'positional': False, 'loop': False, }, ) ).autoretain() def _got_friend_score_results(self, results: list[Any] | None) -> None: # FIXME: tidy this up # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements from efro.util import asserttype # delay a bit if results come in too fast assert self._begin_time is not None base_delay = max(0, 1.9 - (bs.time() - self._begin_time)) ts_height = 300 ts_h_offs = -550 v_offs = 30 # Report in case of error. if results is None: self._friends_loading_status = Text( bs.Lstr(resource='friendScoresUnavailableText'), maxwidth=330, position=(-475, 150 + v_offs), color=(1, 1, 1, 0.4), transition=Text.Transition.FADE_IN, transition_delay=base_delay + 0.8, scale=0.7, ) return self._friends_loading_status = None # Ok, it looks like we aren't able to reliably get a just-submitted # result returned in the score list, so we need to look for our score # in this list and replace it if ours is better or add ours otherwise. if self._score is not None: our_score_entry = [self._score, 'Me', True] for score in results: if score[2]: if self._score_order == 'increasing': our_score_entry[0] = max(score[0], self._score) else: our_score_entry[0] = min(score[0], self._score) results.remove(score) break results.append(our_score_entry) results.sort( reverse=self._score_order == 'increasing', key=lambda x: asserttype(x[0], int), ) # If we're not submitting our own score, we still want to change the # name of our own score to 'Me'. else: for score in results: if score[2]: score[1] = 'Me' break h_offs_extra = 80 if self._score_type == 'points' else 130 v_offs_extra = 20 v_offs_names = 0 scale = 1.0 # Make sure there's at least 5. while len(results) < 5: results.append([0, '-', False]) results = results[:5] times: list[tuple[float, float]] = [] for i in range(len(results)): times.insert( random.randrange(0, len(times) + 1), (base_delay + i * 0.05, base_delay + 0.3 + i * 0.05), ) for i, tval in enumerate(results): score = int(tval[0]) name_str = tval[1] is_me = tval[2] if is_me and score == self._score: flash = True color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.6, 0.6, 0.6, 1.0) tdelay1 = base_delay + 1.0 tdelay2 = base_delay + 1.0 else: flash = False if is_me: color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.9, 1.0, 0.9, 1.0) else: color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.6, 0.6, 0.6, 1.0) tdelay1 = times[i][0] tdelay2 = times[i][1] if name_str != '-': Text( ( str(score) if self._score_type == 'points' else bs.timestring((score * 10) / 1000.0) ), position=( ts_h_offs + 20 + h_offs_extra, v_offs_extra + ts_height / 2 + -ts_height * (i + 1) / 10 + v_offs + 11.0, ), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, color=color0, flash=flash, transition=Text.Transition.IN_RIGHT, transition_delay=tdelay1, ).autoretain() else: if is_me: print('Error: got empty name_str on score result:', tval) Text( bs.Lstr(value=name_str), position=( ts_h_offs + 35 + h_offs_extra, v_offs_extra + ts_height / 2 + -ts_height * (i + 1) / 10 + v_offs_names + v_offs + 11.0, ), color=color1, maxwidth=160.0, v_align=Text.VAlign.CENTER, flash=flash, scale=scale, transition=Text.Transition.IN_RIGHT, transition_delay=tdelay2, ).autoretain() def _got_score_results(self, results: dict[str, Any] | None) -> None: # FIXME: tidy this up # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements plus = bs.app.plus assert plus is not None # We need to manually run this in the context of our activity # and only if we aren't shutting down. # (really should make the submit_score call handle that stuff itself) if self.expired: return with self.context: # Delay a bit if results come in too fast. assert self._begin_time is not None base_delay = max(0, 2.7 - (bs.time() - self._begin_time)) v_offs = 20 if results is None: self._score_loading_status = Text( bs.Lstr(resource='worldScoresUnavailableText'), position=(230, 150 + v_offs), color=(1, 1, 1, 0.4), transition=Text.Transition.FADE_IN, transition_delay=base_delay + 0.3, scale=0.7, ) else: self._score_link = results['link'] assert self._score_link is not None # Prepend our master-server addr if its a relative addr. if not self._score_link.startswith( 'http://' ) and not self._score_link.startswith('https://'): self._score_link = ( plus.get_master_server_address() + '/' + self._score_link ) self._score_loading_status = None if 'tournamentSecondsRemaining' in results: secs_remaining = results['tournamentSecondsRemaining'] assert isinstance(secs_remaining, int) self._tournament_time_remaining = secs_remaining self._tournament_time_remaining_text_timer = bs.BaseTimer( 1.0, bs.WeakCall( self._update_tournament_time_remaining_text ), repeat=True, ) assert self._show_info is not None self._show_info['results'] = results if results is not None: if results['tops'] != '': self._show_info['tops'] = results['tops'] else: self._show_info['tops'] = [] offs_x = -195 available = self._show_info['results'] is not None if self._score is not None: bs.basetimer( (1.5 + base_delay), bs.WeakCall(self._show_world_rank, offs_x), ) ts_h_offs = 200 ts_height = 300 # Show world tops. if available: # Show the number of games represented by this # list (except for in tournaments). if self.session.tournament_id is None: Text( bs.Lstr( resource='lastGamesText', subs=[ ( '${COUNT}', str(self._show_info['results']['total']), ) ], ), position=( ts_h_offs - 35 + 95, ts_height / 2 + 6 + v_offs, ), color=(0.4, 0.4, 0.4, 1.0), scale=0.7, transition=Text.Transition.IN_RIGHT, transition_delay=base_delay + 0.3, ).autoretain() else: v_offs += 20 h_offs_extra = 0 v_offs_names = 0 scale = 1.0 p_count = len(self._playerinfos) if p_count > 1: h_offs_extra -= 40 if self._score_type != 'points': h_offs_extra += 60 if p_count == 2: scale = 0.9 elif p_count == 3: scale = 0.65 elif p_count == 4: scale = 0.5 # Make sure there's at least 10. while len(self._show_info['tops']) < 10: self._show_info['tops'].append([0, '-']) times: list[tuple[float, float]] = [] for i in range(len(self._show_info['tops'])): times.insert( random.randrange(0, len(times) + 1), (base_delay + i * 0.05, base_delay + 0.4 + i * 0.05), ) for i, tval in enumerate(self._show_info['tops']): score = int(tval[0]) name_str = tval[1] if self._name_str == name_str and self._score == score: flash = True color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.6, 0.6, 0.6, 1.0) tdelay1 = base_delay + 1.0 tdelay2 = base_delay + 1.0 else: flash = False if self._name_str == name_str: color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.9, 1.0, 0.9, 1.0) else: color0 = (0.6, 0.4, 0.1, 1.0) color1 = (0.6, 0.6, 0.6, 1.0) tdelay1 = times[i][0] tdelay2 = times[i][1] if name_str != '-': Text( ( str(score) if self._score_type == 'points' else bs.timestring((score * 10) / 1000.0) ), position=( ts_h_offs + 20 + h_offs_extra, ts_height / 2 + -ts_height * (i + 1) / 10 + v_offs + 11.0, ), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, color=color0, flash=flash, transition=Text.Transition.IN_LEFT, transition_delay=tdelay1, ).autoretain() Text( bs.Lstr(value=name_str), position=( ts_h_offs + 35 + h_offs_extra, ts_height / 2 + -ts_height * (i + 1) / 10 + v_offs_names + v_offs + 11.0, ), maxwidth=80.0 + 100.0 * len(self._playerinfos), v_align=Text.VAlign.CENTER, color=color1, flash=flash, scale=scale, transition=Text.Transition.IN_LEFT, transition_delay=tdelay2, ).autoretain() def _show_tips(self) -> None: from bascenev1lib.actor.tipstext import TipsText TipsText(offs_y=30).autoretain() def _update_tournament_time_remaining_text(self) -> None: if self._tournament_time_remaining is None: return self._tournament_time_remaining = max( 0, self._tournament_time_remaining - 1 ) if self._tournament_time_remaining_text is not None: val = bs.timestring( self._tournament_time_remaining, centi=False, ) self._tournament_time_remaining_text.node.text = val def _show_world_rank(self, offs_x: float) -> None: # FIXME: Tidy this up. # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements assert bs.app.classic is not None assert self._show_info is not None available = self._show_info['results'] is not None if available and self._submit_score: error = ( self._show_info['results']['error'] if 'error' in self._show_info['results'] else None ) rank = self._show_info['results']['rank'] total = self._show_info['results']['total'] rating = ( 10.0 if total == 1 else 10.0 * (1.0 - (float(rank - 1) / (total - 1))) ) player_rank = self._show_info['results']['playerRank'] best_player_rank = self._show_info['results']['bestPlayerRank'] else: error = False rating = None player_rank = None best_player_rank = None # If we've got tournament-seconds-remaining, show it. if self._tournament_time_remaining is not None: Text( bs.Lstr(resource='coopSelectWindow.timeRemainingText'), position=(-360, -70 - 100), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.8, maxwidth=300, transition_delay=2.0, ).autoretain() self._tournament_time_remaining_text = Text( '', position=(-360, -110 - 100), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=1.6, maxwidth=150, transition_delay=2.0, ) # If we're a tournament, show prizes. try: assert bs.app.classic is not None tournament_id = self.session.tournament_id if tournament_id is not None: if tournament_id in bs.app.classic.accounts.tournament_info: tourney_info = bs.app.classic.accounts.tournament_info[ tournament_id ] # pylint: disable=useless-suppression # pylint: disable=unbalanced-tuple-unpacking ( pr1, pv1, pr2, pv2, pr3, pv3, ) = bs.app.classic.get_tournament_prize_strings( tourney_info ) # pylint: enable=unbalanced-tuple-unpacking # pylint: enable=useless-suppression Text( bs.Lstr(resource='coopSelectWindow.prizesText'), position=(-360, -70 + 77), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=1.0, maxwidth=300, transition_delay=2.0, ).autoretain() vval = -107 + 70 for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)): Text( rng, position=(-410 + 10, vval), color=(1, 1, 1, 0.7), h_align=Text.HAlign.RIGHT, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.6, maxwidth=300, transition_delay=2.0, ).autoretain() Text( val, position=(-390 + 10, vval), color=(0.7, 0.7, 0.7, 1.0), h_align=Text.HAlign.LEFT, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.8, maxwidth=300, transition_delay=2.0, ).autoretain() vval -= 35 except Exception: logging.exception('Error showing prize ranges.') if self._do_new_rating: if error: ZoomText( bs.Lstr(resource='failText'), flash=True, trail=True, scale=1.0 if available else 0.333, tilt_translate=0.11, h_align='center', position=(190 + offs_x, -60), maxwidth=200, jitter=1.0, ).autoretain() Text( bs.Lstr(translate=('serverResponses', error)), position=(0, -140), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.9, maxwidth=400, transition_delay=1.0, ).autoretain() elif self._submit_score: ZoomText( ( ('#' + str(player_rank)) if player_rank is not None else bs.Lstr(resource='unavailableText') ), flash=True, trail=True, scale=1.0 if available else 0.333, tilt_translate=0.11, h_align='center', position=(190 + offs_x, -60), maxwidth=200, jitter=1.0, ).autoretain() Text( bs.Lstr( value='${A}:', subs=[('${A}', bs.Lstr(resource='rankText'))], ), position=(0, 36), maxwidth=300, transition=Text.Transition.FADE_IN, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition_delay=0, ).autoretain() if best_player_rank is not None: Text( bs.Lstr( resource='currentStandingText', fallback_resource='bestRankText', subs=[('${RANK}', str(best_player_rank))], ), position=(0, -155), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.7, transition_delay=1.0, ).autoretain() else: ZoomText( ( f'{rating:.1f}' if available else bs.Lstr(resource='unavailableText') ), flash=True, trail=True, scale=0.6 if available else 0.333, tilt_translate=0.11, h_align='center', position=(190 + offs_x, -94), maxwidth=200, jitter=1.0, ).autoretain() if available: if rating >= 9.5: stars = 3 elif rating >= 7.5: stars = 2 elif rating > 0.0: stars = 1 else: stars = 0 star_tex = bs.gettexture('star') star_x = 135 + offs_x for _i in range(stars): img = bs.NodeActor( bs.newnode( 'image', attrs={ 'texture': star_tex, 'position': (star_x, -16), 'scale': (62, 62), 'opacity': 1.0, 'color': (2.2, 1.2, 0.3), 'absolute_scale': True, }, ) ).autoretain() assert img.node bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) star_x += 60 for _i in range(3 - stars): img = bs.NodeActor( bs.newnode( 'image', attrs={ 'texture': star_tex, 'position': (star_x, -16), 'scale': (62, 62), 'opacity': 1.0, 'color': (0.3, 0.3, 0.3), 'absolute_scale': True, }, ) ).autoretain() assert img.node bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) star_x += 60 def dostar( count: int, xval: float, offs_y: float, score: str ) -> None: Text( score + ' =', position=(xval, -64 + offs_y), color=(0.6, 0.6, 0.6, 0.6), h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.4, transition_delay=1.0, ).autoretain() stx = xval + 20 for _i2 in range(count): img2 = bs.NodeActor( bs.newnode( 'image', attrs={ 'texture': star_tex, 'position': (stx, -64 + offs_y), 'scale': (12, 12), 'opacity': 0.7, 'color': (2.2, 1.2, 0.3), 'absolute_scale': True, }, ) ).autoretain() assert img2.node bs.animate(img2.node, 'opacity', {1.0: 0.0, 1.5: 0.5}) stx += 13.0 dostar(1, -44 - 30, -112, '0.0') dostar(2, 10 - 30, -112, '7.5') dostar(3, 77 - 30, -112, '9.5') try: best_rank = self._campaign.getlevel(self._level_name).rating except Exception: best_rank = 0.0 if available: Text( bs.Lstr( resource='outOfText', subs=[ ( '${RANK}', str(int(self._show_info['results']['rank'])), ), ( '${ALL}', str(self._show_info['results']['total']), ), ], ), position=(0, -155 if self._newly_complete else -145), color=(1, 1, 1, 0.7), h_align=Text.HAlign.CENTER, transition=Text.Transition.FADE_IN, scale=0.55, transition_delay=1.0, ).autoretain() new_best = best_rank > self._old_best_rank and best_rank > 0.0 was_string = bs.Lstr( value=' ${A}', subs=[ ('${A}', bs.Lstr(resource='scoreWasText')), ('${COUNT}', str(self._old_best_rank)), ], ) if not self._newly_complete: Text( ( bs.Lstr( value='${A}${B}', subs=[ ( '${A}', bs.Lstr(resource='newPersonalBestText'), ), ('${B}', was_string), ], ) if new_best else bs.Lstr( resource='bestRatingText', subs=[('${RATING}', str(best_rank))], ) ), position=(0, -165), color=(1, 1, 1, 0.7), flash=new_best, h_align=Text.HAlign.CENTER, transition=( Text.Transition.IN_RIGHT if new_best else Text.Transition.FADE_IN ), scale=0.5, transition_delay=1.0, ).autoretain() Text( bs.Lstr( value='${A}:', subs=[('${A}', bs.Lstr(resource='ratingText'))], ), position=(0, 36), maxwidth=300, transition=Text.Transition.FADE_IN, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition_delay=0, ).autoretain() if self._submit_score: bs.timer(0.35, self._score_display_sound.play) if not error: bs.timer(0.35, self.cymbal_sound.play) def _show_fail(self) -> None: ZoomText( bs.Lstr(resource='failText'), maxwidth=300, flash=False, trail=True, h_align='center', tilt_translate=0.11, position=(0, 40), jitter=1.0, ).autoretain() if self._fail_message is not None: Text( self._fail_message, h_align=Text.HAlign.CENTER, position=(0, -130), maxwidth=300, color=(1, 1, 1, 0.5), transition=Text.Transition.FADE_IN, transition_delay=1.0, ).autoretain() bs.timer(0.35, self._score_display_sound.play) def _show_score_val(self, offs_x: float) -> None: assert self._score_type is not None assert self._score is not None ZoomText( ( str(self._score) if self._score_type == 'points' else bs.timestring((self._score * 10) / 1000.0) ), maxwidth=300, flash=True, trail=True, scale=1.0 if self._score_type == 'points' else 0.6, h_align='center', tilt_translate=0.11, position=(190 + offs_x, 115), jitter=1.0, ).autoretain() Text( ( bs.Lstr( value='${A}:', subs=[('${A}', bs.Lstr(resource='finalScoreText'))], ) if self._score_type == 'points' else bs.Lstr( value='${A}:', subs=[('${A}', bs.Lstr(resource='finalTimeText'))], ) ), maxwidth=300, position=(0, 200), transition=Text.Transition.FADE_IN, h_align=Text.HAlign.CENTER, v_align=Text.VAlign.CENTER, transition_delay=0, ).autoretain() bs.timer(0.35, self._score_display_sound.play)