bascenev1lib.activity.coopscore

Provides a score screen for coop games.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""Provides a score screen for coop games."""
   4# pylint: disable=too-many-lines
   5
   6from __future__ import annotations
   7
   8import random
   9import logging
  10from typing import TYPE_CHECKING, override
  11
  12from bacommon.login import LoginType
  13import bascenev1 as bs
  14import bauiv1 as bui
  15
  16from bascenev1lib.actor.text import Text
  17from bascenev1lib.actor.zoomtext import ZoomText
  18
  19if TYPE_CHECKING:
  20    from typing import Any, Sequence
  21
  22    from bauiv1lib.store.button import StoreButton
  23    from bauiv1lib.league.rankbutton import LeagueRankButton
  24
  25
  26class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
  27    """Score screen showing the results of a cooperative game."""
  28
  29    def __init__(self, settings: dict):
  30        # pylint: disable=too-many-statements
  31        super().__init__(settings)
  32
  33        plus = bs.app.plus
  34        assert plus is not None
  35
  36        # Keep prev activity alive while we fade in
  37        self.transition_time = 0.5
  38        self.inherits_tint = True
  39        self.inherits_vr_camera_offset = True
  40        self.inherits_music = True
  41        self.use_fixed_vr_overlay = True
  42
  43        self._do_new_rating: bool = self.session.tournament_id is not None
  44
  45        self._score_display_sound = bs.getsound('scoreHit01')
  46        self._score_display_sound_small = bs.getsound('scoreHit02')
  47        self.drum_roll_sound = bs.getsound('drumRoll')
  48        self.cymbal_sound = bs.getsound('cymbal')
  49
  50        self._replay_icon_texture = bui.gettexture('replayIcon')
  51        self._menu_icon_texture = bui.gettexture('menuIcon')
  52        self._next_level_icon_texture = bui.gettexture('nextLevelIcon')
  53
  54        self._campaign: bs.Campaign = settings['campaign']
  55
  56        self._have_achievements = (
  57            bs.app.classic is not None
  58            and bs.app.classic.ach.achievements_for_coop_level(
  59                self._campaign.name + ':' + settings['level']
  60            )
  61        )
  62
  63        self._game_service_icon_color: Sequence[float] | None
  64        self._game_service_achievements_texture: bui.Texture | None
  65        self._game_service_leaderboards_texture: bui.Texture | None
  66
  67        # Tie in to specific game services if they are active.
  68        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
  69        gpgs_active = adapter is not None and adapter.is_back_end_active()
  70        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
  71        game_center_active = (
  72            adapter is not None and adapter.is_back_end_active()
  73        )
  74
  75        if game_center_active:
  76            self._game_service_icon_color = (1.0, 1.0, 1.0)
  77            icon = bui.gettexture('gameCenterIcon')
  78            self._game_service_achievements_texture = icon
  79            self._game_service_leaderboards_texture = icon
  80            self._account_has_achievements = True
  81        elif gpgs_active:
  82            self._game_service_icon_color = (0.8, 1.0, 0.6)
  83            self._game_service_achievements_texture = bui.gettexture(
  84                'googlePlayAchievementsIcon'
  85            )
  86            self._game_service_leaderboards_texture = bui.gettexture(
  87                'googlePlayLeaderboardsIcon'
  88            )
  89            self._account_has_achievements = True
  90        else:
  91            self._game_service_icon_color = None
  92            self._game_service_achievements_texture = None
  93            self._game_service_leaderboards_texture = None
  94            self._account_has_achievements = False
  95
  96        self._cashregistersound = bs.getsound('cashRegister')
  97        self._gun_cocking_sound = bs.getsound('gunCocking')
  98        self._dingsound = bs.getsound('ding')
  99        self._score_link: str | None = None
 100        self._root_ui: bui.Widget | None = None
 101        self._background: bs.Actor | None = None
 102        self._old_best_rank = 0.0
 103        self._game_name_str: str | None = None
 104        self._game_config_str: str | None = None
 105
 106        # Ui bits.
 107        self._corner_button_offs: tuple[float, float] | None = None
 108        self._league_rank_button: LeagueRankButton | None = None
 109        self._store_button_instance: StoreButton | None = None
 110        self._restart_button: bui.Widget | None = None
 111        self._update_corner_button_positions_timer: bui.AppTimer | None = None
 112        self._next_level_error: bs.Actor | None = None
 113
 114        # Score/gameplay bits.
 115        self._was_complete: bool | None = None
 116        self._is_complete: bool | None = None
 117        self._newly_complete: bool | None = None
 118        self._is_more_levels: bool | None = None
 119        self._next_level_name: str | None = None
 120        self._show_info: dict[str, Any] | None = None
 121        self._name_str: str | None = None
 122        self._friends_loading_status: bs.Actor | None = None
 123        self._score_loading_status: bs.Actor | None = None
 124        self._tournament_time_remaining: float | None = None
 125        self._tournament_time_remaining_text: Text | None = None
 126        self._tournament_time_remaining_text_timer: bs.BaseTimer | None = None
 127        self._submit_score = self.session.submit_score
 128
 129        # Stuff for activity skip by pressing button
 130        self._birth_time = bs.time()
 131        self._min_view_time = 5.0
 132        self._allow_server_transition = False
 133        self._server_transitioning: bool | None = None
 134
 135        self._playerinfos: list[bs.PlayerInfo] = settings['playerinfos']
 136        assert isinstance(self._playerinfos, list)
 137        assert all(isinstance(i, bs.PlayerInfo) for i in self._playerinfos)
 138
 139        self._score: int | None = settings['score']
 140        assert isinstance(self._score, (int, type(None)))
 141
 142        self._fail_message: bs.Lstr | None = settings['fail_message']
 143        assert isinstance(self._fail_message, (bs.Lstr, type(None)))
 144
 145        self._begin_time: float | None = None
 146
 147        self._score_order: str
 148        if 'score_order' in settings:
 149            if not settings['score_order'] in ['increasing', 'decreasing']:
 150                raise ValueError(
 151                    'Invalid score order: ' + settings['score_order']
 152                )
 153            self._score_order = settings['score_order']
 154        else:
 155            self._score_order = 'increasing'
 156        assert isinstance(self._score_order, str)
 157
 158        self._score_type: str
 159        if 'score_type' in settings:
 160            if not settings['score_type'] in ['points', 'time']:
 161                raise ValueError(
 162                    'Invalid score type: ' + settings['score_type']
 163                )
 164            self._score_type = settings['score_type']
 165        else:
 166            self._score_type = 'points'
 167        assert isinstance(self._score_type, str)
 168
 169        self._level_name: str = settings['level']
 170        assert isinstance(self._level_name, str)
 171
 172        self._game_name_str = self._campaign.name + ':' + self._level_name
 173        self._game_config_str = (
 174            str(len(self._playerinfos))
 175            + 'p'
 176            + self._campaign.getlevel(self._level_name)
 177            .get_score_version_string()
 178            .replace(' ', '_')
 179        )
 180
 181        try:
 182            self._old_best_rank = self._campaign.getlevel(
 183                self._level_name
 184            ).rating
 185        except Exception:
 186            self._old_best_rank = 0.0
 187
 188        self._victory: bool = settings['outcome'] == 'victory'
 189
 190    @override
 191    def __del__(self) -> None:
 192        super().__del__()
 193
 194        # If our UI is still up, kill it.
 195        if self._root_ui and not self._root_ui.transitioning_out:
 196            with bui.ContextRef.empty():
 197                bui.containerwidget(edit=self._root_ui, transition='out_left')
 198
 199    @override
 200    def on_transition_in(self) -> None:
 201        from bascenev1lib.actor import background  # FIXME NO BSSTD
 202
 203        bs.set_analytics_screen('Coop Score Screen')
 204        super().on_transition_in()
 205        self._background = background.Background(
 206            fade_time=0.45, start_faded=False, show_logo=True
 207        )
 208
 209    def _ui_menu(self) -> None:
 210        from bauiv1lib import specialoffer
 211
 212        if specialoffer.show_offer():
 213            return
 214        bui.containerwidget(edit=self._root_ui, transition='out_left')
 215        with self.context:
 216            bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end)))
 217
 218    def _ui_restart(self) -> None:
 219        from bauiv1lib.tournamententry import TournamentEntryWindow
 220        from bauiv1lib import specialoffer
 221
 222        if specialoffer.show_offer():
 223            return
 224
 225        # If we're in a tournament and it looks like there's no time left,
 226        # disallow.
 227        if self.session.tournament_id is not None:
 228            if self._tournament_time_remaining is None:
 229                bui.screenmessage(
 230                    bui.Lstr(resource='tournamentCheckingStateText'),
 231                    color=(1, 0, 0),
 232                )
 233                bui.getsound('error').play()
 234                return
 235            if self._tournament_time_remaining <= 0:
 236                bui.screenmessage(
 237                    bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
 238                )
 239                bui.getsound('error').play()
 240                return
 241
 242        # If there are currently fewer players than our session min,
 243        # don't allow.
 244        if len(self.players) < self.session.min_players:
 245            bui.screenmessage(
 246                bui.Lstr(resource='notEnoughPlayersRemainingText'),
 247                color=(1, 0, 0),
 248            )
 249            bui.getsound('error').play()
 250            return
 251
 252        self._campaign.set_selected_level(self._level_name)
 253
 254        # If this is a tournament, go back to the tournament-entry UI
 255        # otherwise just hop back in.
 256        tournament_id = self.session.tournament_id
 257        if tournament_id is not None:
 258            assert self._restart_button is not None
 259            TournamentEntryWindow(
 260                tournament_id=tournament_id,
 261                tournament_activity=self,
 262                position=self._restart_button.get_screen_space_center(),
 263            )
 264        else:
 265            bui.containerwidget(edit=self._root_ui, transition='out_left')
 266            self.can_show_ad_on_death = True
 267            with self.context:
 268                self.end({'outcome': 'restart'})
 269
 270    def _ui_next(self) -> None:
 271        from bauiv1lib.specialoffer import show_offer
 272
 273        if show_offer():
 274            return
 275
 276        # If we didn't just complete this level but are choosing to play the
 277        # next one, set it as current (this won't happen otherwise).
 278        if (
 279            self._is_complete
 280            and self._is_more_levels
 281            and not self._newly_complete
 282        ):
 283            assert self._next_level_name is not None
 284            self._campaign.set_selected_level(self._next_level_name)
 285        bui.containerwidget(edit=self._root_ui, transition='out_left')
 286        with self.context:
 287            self.end({'outcome': 'next_level'})
 288
 289    def _ui_gc(self) -> None:
 290        if bs.app.plus is not None:
 291            bs.app.plus.show_game_service_ui(
 292                'leaderboard',
 293                game=self._game_name_str,
 294                game_version=self._game_config_str,
 295            )
 296        else:
 297            logging.warning('show_game_service_ui requires plus feature-set')
 298
 299    def _ui_show_achievements(self) -> None:
 300        if bs.app.plus is not None:
 301            bs.app.plus.show_game_service_ui('achievements')
 302        else:
 303            logging.warning('show_game_service_ui requires plus feature-set')
 304
 305    def _ui_worlds_best(self) -> None:
 306        if self._score_link is None:
 307            bui.getsound('error').play()
 308            bui.screenmessage(
 309                bui.Lstr(resource='scoreListUnavailableText'), color=(1, 0.5, 0)
 310            )
 311        else:
 312            bui.open_url(self._score_link)
 313
 314    def _ui_error(self) -> None:
 315        with self.context:
 316            self._next_level_error = Text(
 317                bs.Lstr(resource='completeThisLevelToProceedText'),
 318                flash=True,
 319                maxwidth=360,
 320                scale=0.54,
 321                h_align=Text.HAlign.CENTER,
 322                color=(0.5, 0.7, 0.5, 1),
 323                position=(300, -235),
 324            )
 325            bui.getsound('error').play()
 326            bs.timer(
 327                2.0,
 328                bs.WeakCall(
 329                    self._next_level_error.handlemessage, bs.DieMessage()
 330                ),
 331            )
 332
 333    def _should_show_worlds_best_button(self) -> bool:
 334        # Link is too complicated to display with no browser.
 335        return bui.is_browser_likely_available()
 336
 337    def request_ui(self) -> None:
 338        """Set up a callback to show our UI at the next opportune time."""
 339        assert bui.app.classic is not None
 340        # We don't want to just show our UI in case the user already has the
 341        # main menu up, so instead we add a callback for when the menu
 342        # closes; if we're still alive, we'll come up then.
 343        # If there's no main menu this gets called immediately.
 344        bui.app.ui_v1.add_main_menu_close_callback(bui.WeakCall(self.show_ui))
 345
 346    def show_ui(self) -> None:
 347        """Show the UI for restarting, playing the next Level, etc."""
 348        # pylint: disable=too-many-locals
 349        from bauiv1lib.store.button import StoreButton
 350        from bauiv1lib.league.rankbutton import LeagueRankButton
 351
 352        assert bui.app.classic is not None
 353
 354        env = bui.app.env
 355
 356        delay = 0.7 if (self._score is not None) else 0.0
 357
 358        # If there's no players left in the game, lets not show the UI
 359        # (that would allow restarting the game with zero players, etc).
 360        if not self.players:
 361            return
 362
 363        rootc = self._root_ui = bui.containerwidget(
 364            size=(0, 0), transition='in_right'
 365        )
 366
 367        h_offs = 7.0
 368        v_offs = -280.0
 369
 370        # We wanna prevent controllers users from popping up browsers
 371        # or game-center widgets in cases where they can't easily get back
 372        # to the game (like on mac).
 373        can_select_extra_buttons = bui.app.classic.platform == 'android'
 374
 375        bui.set_ui_input_device(None)  # Menu is up for grabs.
 376
 377        if self._have_achievements and self._account_has_achievements:
 378            bui.buttonwidget(
 379                parent=rootc,
 380                color=(0.45, 0.4, 0.5),
 381                position=(h_offs - 520, v_offs + 450 - 235 + 40),
 382                size=(300, 60),
 383                label=bui.Lstr(resource='achievementsText'),
 384                on_activate_call=bui.WeakCall(self._ui_show_achievements),
 385                transition_delay=delay + 1.5,
 386                icon=self._game_service_achievements_texture,
 387                icon_color=self._game_service_icon_color,
 388                autoselect=True,
 389                selectable=can_select_extra_buttons,
 390            )
 391
 392        if self._should_show_worlds_best_button():
 393            bui.buttonwidget(
 394                parent=rootc,
 395                color=(0.45, 0.4, 0.5),
 396                position=(160, v_offs + 480),
 397                size=(350, 62),
 398                label=(
 399                    bui.Lstr(resource='tournamentStandingsText')
 400                    if self.session.tournament_id is not None
 401                    else (
 402                        bui.Lstr(resource='worldsBestScoresText')
 403                        if self._score_type == 'points'
 404                        else bui.Lstr(resource='worldsBestTimesText')
 405                    )
 406                ),
 407                autoselect=True,
 408                on_activate_call=bui.WeakCall(self._ui_worlds_best),
 409                transition_delay=delay + 1.9,
 410                selectable=can_select_extra_buttons,
 411            )
 412        else:
 413            pass
 414
 415        show_next_button = self._is_more_levels and not (env.demo or env.arcade)
 416
 417        if not show_next_button:
 418            h_offs += 70
 419
 420        menu_button = bui.buttonwidget(
 421            parent=rootc,
 422            autoselect=True,
 423            position=(h_offs - 130 - 60, v_offs),
 424            size=(110, 85),
 425            label='',
 426            on_activate_call=bui.WeakCall(self._ui_menu),
 427        )
 428        bui.imagewidget(
 429            parent=rootc,
 430            draw_controller=menu_button,
 431            position=(h_offs - 130 - 60 + 22, v_offs + 14),
 432            size=(60, 60),
 433            texture=self._menu_icon_texture,
 434            opacity=0.8,
 435        )
 436        self._restart_button = restart_button = bui.buttonwidget(
 437            parent=rootc,
 438            autoselect=True,
 439            position=(h_offs - 60, v_offs),
 440            size=(110, 85),
 441            label='',
 442            on_activate_call=bui.WeakCall(self._ui_restart),
 443        )
 444        bui.imagewidget(
 445            parent=rootc,
 446            draw_controller=restart_button,
 447            position=(h_offs - 60 + 19, v_offs + 7),
 448            size=(70, 70),
 449            texture=self._replay_icon_texture,
 450            opacity=0.8,
 451        )
 452
 453        next_button: bui.Widget | None = None
 454
 455        # Our 'next' button is disabled if we haven't unlocked the next
 456        # level yet and invisible if there is none.
 457        if show_next_button:
 458            if self._is_complete:
 459                call = bui.WeakCall(self._ui_next)
 460                button_sound = True
 461                image_opacity = 0.8
 462                color = None
 463            else:
 464                call = bui.WeakCall(self._ui_error)
 465                button_sound = False
 466                image_opacity = 0.2
 467                color = (0.3, 0.3, 0.3)
 468            next_button = bui.buttonwidget(
 469                parent=rootc,
 470                autoselect=True,
 471                position=(h_offs + 130 - 60, v_offs),
 472                size=(110, 85),
 473                label='',
 474                on_activate_call=call,
 475                color=color,
 476                enable_sound=button_sound,
 477            )
 478            bui.imagewidget(
 479                parent=rootc,
 480                draw_controller=next_button,
 481                position=(h_offs + 130 - 60 + 12, v_offs + 5),
 482                size=(80, 80),
 483                texture=self._next_level_icon_texture,
 484                opacity=image_opacity,
 485            )
 486
 487        x_offs_extra = 0 if show_next_button else -100
 488        self._corner_button_offs = (
 489            h_offs + 300.0 + 100.0 + x_offs_extra,
 490            v_offs + 560.0,
 491        )
 492
 493        if env.demo or env.arcade:
 494            self._league_rank_button = None
 495            self._store_button_instance = None
 496        else:
 497            self._league_rank_button = LeagueRankButton(
 498                parent=rootc,
 499                position=(h_offs + 300 + 100 + x_offs_extra, v_offs + 560),
 500                size=(100, 60),
 501                scale=0.9,
 502                color=(0.4, 0.4, 0.9),
 503                textcolor=(0.9, 0.9, 2.0),
 504                transition_delay=0.0,
 505                smooth_update_delay=5.0,
 506            )
 507            self._store_button_instance = StoreButton(
 508                parent=rootc,
 509                position=(h_offs + 400 + 100 + x_offs_extra, v_offs + 560),
 510                show_tickets=True,
 511                sale_scale=0.85,
 512                size=(100, 60),
 513                scale=0.9,
 514                button_type='square',
 515                color=(0.35, 0.25, 0.45),
 516                textcolor=(0.9, 0.7, 1.0),
 517                transition_delay=0.0,
 518            )
 519
 520        bui.containerwidget(
 521            edit=rootc,
 522            selected_child=(
 523                next_button
 524                if (self._newly_complete and self._victory and show_next_button)
 525                else restart_button
 526            ),
 527            on_cancel_call=menu_button.activate,
 528        )
 529
 530        self._update_corner_button_positions()
 531        self._update_corner_button_positions_timer = bui.AppTimer(
 532            1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True
 533        )
 534
 535    def _update_corner_button_positions(self) -> None:
 536        offs = -55 if bui.is_party_icon_visible() else 0
 537        assert self._corner_button_offs is not None
 538        pos_x = self._corner_button_offs[0] + offs
 539        pos_y = self._corner_button_offs[1]
 540        if self._league_rank_button is not None:
 541            self._league_rank_button.set_position((pos_x, pos_y))
 542        if self._store_button_instance is not None:
 543            self._store_button_instance.set_position((pos_x + 100, pos_y))
 544
 545    def _player_press(self) -> None:
 546        # (Only for headless builds).
 547
 548        # If this activity is a good 'end point', ask server-mode just once if
 549        # it wants to do anything special like switch sessions or kill the app.
 550        if (
 551            self._allow_server_transition
 552            and bs.app.classic is not None
 553            and bs.app.classic.server is not None
 554            and self._server_transitioning is None
 555        ):
 556            self._server_transitioning = (
 557                bs.app.classic.server.handle_transition()
 558            )
 559            assert isinstance(self._server_transitioning, bool)
 560
 561        # If server-mode is handling this, don't do anything ourself.
 562        if self._server_transitioning is True:
 563            return
 564
 565        # Otherwise restart current level.
 566        self._campaign.set_selected_level(self._level_name)
 567        with self.context:
 568            self.end({'outcome': 'restart'})
 569
 570    def _safe_assign(self, player: bs.Player) -> None:
 571        # (Only for headless builds).
 572
 573        # Just to be extra careful, don't assign if we're transitioning out.
 574        # (though theoretically that should be ok).
 575        if not self.is_transitioning_out() and player:
 576            player.assigninput(
 577                (
 578                    bs.InputType.JUMP_PRESS,
 579                    bs.InputType.PUNCH_PRESS,
 580                    bs.InputType.BOMB_PRESS,
 581                    bs.InputType.PICK_UP_PRESS,
 582                ),
 583                self._player_press,
 584            )
 585
 586    @override
 587    def on_player_join(self, player: bs.Player) -> None:
 588        super().on_player_join(player)
 589
 590        if bs.app.classic is not None and bs.app.classic.server is not None:
 591            # Host can't press retry button, so anyone can do it instead.
 592            time_till_assign = max(
 593                0, self._birth_time + self._min_view_time - bs.time()
 594            )
 595
 596            bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player))
 597
 598    @override
 599    def on_begin(self) -> None:
 600        # FIXME: Clean this up.
 601        # pylint: disable=too-many-statements
 602        # pylint: disable=too-many-branches
 603        # pylint: disable=too-many-locals
 604        super().on_begin()
 605
 606        app = bs.app
 607        env = app.env
 608        plus = app.plus
 609        assert plus is not None
 610
 611        self._begin_time = bs.time()
 612
 613        # Calc whether the level is complete and other stuff.
 614        levels = self._campaign.levels
 615        level = self._campaign.getlevel(self._level_name)
 616        self._was_complete = level.complete
 617        self._is_complete = self._was_complete or self._victory
 618        self._newly_complete = self._is_complete and not self._was_complete
 619        self._is_more_levels = (
 620            level.index < len(levels) - 1
 621        ) and self._campaign.sequential
 622
 623        # Any time we complete a level, set the next one as unlocked.
 624        if self._is_complete and self._is_more_levels:
 625            plus.add_v1_account_transaction(
 626                {
 627                    'type': 'COMPLETE_LEVEL',
 628                    'campaign': self._campaign.name,
 629                    'level': self._level_name,
 630                }
 631            )
 632            self._next_level_name = levels[level.index + 1].name
 633
 634            # If this is the first time we completed it, set the next one
 635            # as current.
 636            if self._newly_complete:
 637                cfg = app.config
 638                cfg['Selected Coop Game'] = (
 639                    self._campaign.name + ':' + self._next_level_name
 640                )
 641                cfg.commit()
 642                self._campaign.set_selected_level(self._next_level_name)
 643
 644        bs.timer(1.0, bs.WeakCall(self.request_ui))
 645
 646        if (
 647            self._is_complete
 648            and self._victory
 649            and self._is_more_levels
 650            and not (env.demo or env.arcade)
 651        ):
 652            Text(
 653                (
 654                    bs.Lstr(
 655                        value='${A}:\n',
 656                        subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))],
 657                    )
 658                    if self._newly_complete
 659                    else bs.Lstr(
 660                        value='${A}:\n',
 661                        subs=[('${A}', bs.Lstr(resource='nextLevelText'))],
 662                    )
 663                ),
 664                transition=Text.Transition.IN_RIGHT,
 665                transition_delay=5.2,
 666                flash=self._newly_complete,
 667                scale=0.54,
 668                h_align=Text.HAlign.CENTER,
 669                maxwidth=270,
 670                color=(0.5, 0.7, 0.5, 1),
 671                position=(270, -235),
 672            ).autoretain()
 673            assert self._next_level_name is not None
 674            Text(
 675                bs.Lstr(translate=('coopLevelNames', self._next_level_name)),
 676                transition=Text.Transition.IN_RIGHT,
 677                transition_delay=5.2,
 678                flash=self._newly_complete,
 679                scale=0.7,
 680                h_align=Text.HAlign.CENTER,
 681                maxwidth=205,
 682                color=(0.5, 0.7, 0.5, 1),
 683                position=(270, -255),
 684            ).autoretain()
 685            if self._newly_complete:
 686                bs.timer(5.2, self._cashregistersound.play)
 687                bs.timer(5.2, self._dingsound.play)
 688
 689        offs_x = -195
 690        if len(self._playerinfos) > 1:
 691            pstr = bs.Lstr(
 692                value='- ${A} -',
 693                subs=[
 694                    (
 695                        '${A}',
 696                        bs.Lstr(
 697                            resource='multiPlayerCountText',
 698                            subs=[('${COUNT}', str(len(self._playerinfos)))],
 699                        ),
 700                    )
 701                ],
 702            )
 703        else:
 704            pstr = bs.Lstr(
 705                value='- ${A} -',
 706                subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))],
 707            )
 708        ZoomText(
 709            self._campaign.getlevel(self._level_name).displayname,
 710            maxwidth=800,
 711            flash=False,
 712            trail=False,
 713            color=(0.5, 1, 0.5, 1),
 714            h_align='center',
 715            scale=0.4,
 716            position=(0, 292),
 717            jitter=1.0,
 718        ).autoretain()
 719        Text(
 720            pstr,
 721            maxwidth=300,
 722            transition=Text.Transition.FADE_IN,
 723            scale=0.7,
 724            h_align=Text.HAlign.CENTER,
 725            v_align=Text.VAlign.CENTER,
 726            color=(0.5, 0.7, 0.5, 1),
 727            position=(0, 230),
 728        ).autoretain()
 729
 730        if app.classic is not None and app.classic.server is None:
 731            # If we're running in normal non-headless build, show this text
 732            # because only host can continue the game.
 733            adisp = plus.get_v1_account_display_string()
 734            txt = Text(
 735                bs.Lstr(
 736                    resource='waitingForHostText', subs=[('${HOST}', adisp)]
 737                ),
 738                maxwidth=300,
 739                transition=Text.Transition.FADE_IN,
 740                transition_delay=8.0,
 741                scale=0.85,
 742                h_align=Text.HAlign.CENTER,
 743                v_align=Text.VAlign.CENTER,
 744                color=(1, 1, 0, 1),
 745                position=(0, -230),
 746            ).autoretain()
 747            assert txt.node
 748            txt.node.client_only = True
 749        else:
 750            # In headless build, anyone can continue the game.
 751            sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
 752            Text(
 753                sval,
 754                v_attach=Text.VAttach.BOTTOM,
 755                h_align=Text.HAlign.CENTER,
 756                flash=True,
 757                vr_depth=50,
 758                position=(0, 60),
 759                scale=0.8,
 760                color=(0.5, 0.7, 0.5, 0.5),
 761                transition=Text.Transition.IN_BOTTOM_SLOW,
 762                transition_delay=self._min_view_time,
 763            ).autoretain()
 764
 765        if self._score is not None:
 766            bs.timer(0.35, self._score_display_sound_small.play)
 767
 768        # Vestigial remain; this stuff should just be instance vars.
 769        self._show_info = {}
 770
 771        if self._score is not None:
 772            bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x))
 773        else:
 774            bs.pushcall(bs.WeakCall(self._show_fail))
 775
 776        self._name_str = name_str = ', '.join(
 777            [p.name for p in self._playerinfos]
 778        )
 779
 780        self._score_loading_status = Text(
 781            bs.Lstr(
 782                value='${A}...',
 783                subs=[('${A}', bs.Lstr(resource='loadingText'))],
 784            ),
 785            position=(280, 150 + 30),
 786            color=(1, 1, 1, 0.4),
 787            transition=Text.Transition.FADE_IN,
 788            scale=0.7,
 789            transition_delay=2.0,
 790        )
 791
 792        if self._score is not None and self._submit_score:
 793            bs.timer(0.4, bs.WeakCall(self._play_drumroll))
 794
 795        # Add us to high scores, filter, and store.
 796        our_high_scores_all = self._campaign.getlevel(
 797            self._level_name
 798        ).get_high_scores()
 799
 800        our_high_scores = our_high_scores_all.setdefault(
 801            str(len(self._playerinfos)) + ' Player', []
 802        )
 803
 804        if self._score is not None:
 805            our_score: list | None = [
 806                self._score,
 807                {
 808                    'players': [
 809                        {'name': p.name, 'character': p.character}
 810                        for p in self._playerinfos
 811                    ]
 812                },
 813            ]
 814            our_high_scores.append(our_score)
 815        else:
 816            our_score = None
 817
 818        try:
 819            our_high_scores.sort(
 820                reverse=self._score_order == 'increasing', key=lambda x: x[0]
 821            )
 822        except Exception:
 823            logging.exception('Error sorting scores.')
 824            print(f'our_high_scores: {our_high_scores}')
 825
 826        del our_high_scores[10:]
 827
 828        if self._score is not None:
 829            sver = self._campaign.getlevel(
 830                self._level_name
 831            ).get_score_version_string()
 832            plus.add_v1_account_transaction(
 833                {
 834                    'type': 'SET_LEVEL_LOCAL_HIGH_SCORES',
 835                    'campaign': self._campaign.name,
 836                    'level': self._level_name,
 837                    'scoreVersion': sver,
 838                    'scores': our_high_scores_all,
 839                }
 840            )
 841        if plus.get_v1_account_state() != 'signed_in':
 842            # We expect this only in kiosk mode; complain otherwise.
 843            if not (env.demo or env.arcade):
 844                logging.error('got not-signed-in at score-submit; unexpected')
 845            bs.pushcall(bs.WeakCall(self._got_score_results, None))
 846        else:
 847            assert self._game_name_str is not None
 848            assert self._game_config_str is not None
 849            plus.submit_score(
 850                self._game_name_str,
 851                self._game_config_str,
 852                name_str,
 853                self._score,
 854                bs.WeakCall(self._got_score_results),
 855                order=self._score_order,
 856                tournament_id=self.session.tournament_id,
 857                score_type=self._score_type,
 858                campaign=self._campaign.name,
 859                level=self._level_name,
 860            )
 861
 862        # Apply the transactions we've been adding locally.
 863        plus.run_v1_account_transactions()
 864
 865        # If we're not doing the world's-best button, just show a title
 866        # instead.
 867        ts_height = 300
 868        ts_h_offs = 210
 869        v_offs = 40
 870        txt = Text(
 871            (
 872                bs.Lstr(resource='tournamentStandingsText')
 873                if self.session.tournament_id is not None
 874                else (
 875                    bs.Lstr(resource='worldsBestScoresText')
 876                    if self._score_type == 'points'
 877                    else bs.Lstr(resource='worldsBestTimesText')
 878                )
 879            ),
 880            maxwidth=210,
 881            position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
 882            transition=Text.Transition.IN_LEFT,
 883            v_align=Text.VAlign.CENTER,
 884            scale=1.2,
 885            transition_delay=2.2,
 886        ).autoretain()
 887
 888        # If we've got a button on the server, only show this on clients.
 889        if self._should_show_worlds_best_button():
 890            assert txt.node
 891            txt.node.client_only = True
 892
 893        ts_height = 300
 894        ts_h_offs = -480
 895        v_offs = 40
 896        Text(
 897            (
 898                bs.Lstr(resource='yourBestScoresText')
 899                if self._score_type == 'points'
 900                else bs.Lstr(resource='yourBestTimesText')
 901            ),
 902            maxwidth=210,
 903            position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
 904            transition=Text.Transition.IN_RIGHT,
 905            v_align=Text.VAlign.CENTER,
 906            scale=1.2,
 907            transition_delay=1.8,
 908        ).autoretain()
 909
 910        display_scores = list(our_high_scores)
 911        display_count = 5
 912
 913        while len(display_scores) < display_count:
 914            display_scores.append((0, None))
 915
 916        showed_ours = False
 917        h_offs_extra = 85 if self._score_type == 'points' else 130
 918        v_offs_extra = 20
 919        v_offs_names = 0
 920        scale = 1.0
 921        p_count = len(self._playerinfos)
 922        h_offs_extra -= 75
 923        if p_count > 1:
 924            h_offs_extra -= 20
 925        if p_count == 2:
 926            scale = 0.9
 927        elif p_count == 3:
 928            scale = 0.65
 929        elif p_count == 4:
 930            scale = 0.5
 931        times: list[tuple[float, float]] = []
 932        for i in range(display_count):
 933            times.insert(
 934                random.randrange(0, len(times) + 1),
 935                (1.9 + i * 0.05, 2.3 + i * 0.05),
 936            )
 937        for i in range(display_count):
 938            try:
 939                if display_scores[i][1] is None:
 940                    name_str = '-'
 941                else:
 942                    # noinspection PyUnresolvedReferences
 943                    name_str = ', '.join(
 944                        [p['name'] for p in display_scores[i][1]['players']]
 945                    )
 946            except Exception:
 947                logging.exception(
 948                    'Error calcing name_str for %s.', display_scores
 949                )
 950                name_str = '-'
 951            if display_scores[i] == our_score and not showed_ours:
 952                flash = True
 953                color0 = (0.6, 0.4, 0.1, 1.0)
 954                color1 = (0.6, 0.6, 0.6, 1.0)
 955                tdelay1 = 3.7
 956                tdelay2 = 3.7
 957                showed_ours = True
 958            else:
 959                flash = False
 960                color0 = (0.6, 0.4, 0.1, 1.0)
 961                color1 = (0.6, 0.6, 0.6, 1.0)
 962                tdelay1 = times[i][0]
 963                tdelay2 = times[i][1]
 964            Text(
 965                (
 966                    str(display_scores[i][0])
 967                    if self._score_type == 'points'
 968                    else bs.timestring((display_scores[i][0] * 10) / 1000.0)
 969                ),
 970                position=(
 971                    ts_h_offs + 20 + h_offs_extra,
 972                    v_offs_extra
 973                    + ts_height / 2
 974                    + -ts_height * (i + 1) / 10
 975                    + v_offs
 976                    + 11.0,
 977                ),
 978                h_align=Text.HAlign.RIGHT,
 979                v_align=Text.VAlign.CENTER,
 980                color=color0,
 981                flash=flash,
 982                transition=Text.Transition.IN_RIGHT,
 983                transition_delay=tdelay1,
 984            ).autoretain()
 985
 986            Text(
 987                bs.Lstr(value=name_str),
 988                position=(
 989                    ts_h_offs + 35 + h_offs_extra,
 990                    v_offs_extra
 991                    + ts_height / 2
 992                    + -ts_height * (i + 1) / 10
 993                    + v_offs_names
 994                    + v_offs
 995                    + 11.0,
 996                ),
 997                maxwidth=80.0 + 100.0 * len(self._playerinfos),
 998                v_align=Text.VAlign.CENTER,
 999                color=color1,
1000                flash=flash,
1001                scale=scale,
1002                transition=Text.Transition.IN_RIGHT,
1003                transition_delay=tdelay2,
1004            ).autoretain()
1005
1006        # Show achievements for this level.
1007        ts_height = -150
1008        ts_h_offs = -480
1009        v_offs = 40
1010
1011        # Only make this if we don't have the button
1012        # (never want clients to see it so no need for client-only
1013        # version, etc).
1014        if self._have_achievements:
1015            if not self._account_has_achievements:
1016                Text(
1017                    bs.Lstr(resource='achievementsText'),
1018                    position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3),
1019                    maxwidth=210,
1020                    host_only=True,
1021                    transition=Text.Transition.IN_RIGHT,
1022                    v_align=Text.VAlign.CENTER,
1023                    scale=1.2,
1024                    transition_delay=2.8,
1025                ).autoretain()
1026
1027            assert self._game_name_str is not None
1028            assert bs.app.classic is not None
1029            achievements = bs.app.classic.ach.achievements_for_coop_level(
1030                self._game_name_str
1031            )
1032            hval = -455
1033            vval = -100
1034            tdelay = 0.0
1035            for ach in achievements:
1036                ach.create_display(hval, vval + v_offs, 3.0 + tdelay)
1037                vval -= 55
1038                tdelay += 0.250
1039
1040        bs.timer(5.0, bs.WeakCall(self._show_tips))
1041
1042    def _play_drumroll(self) -> None:
1043        bs.NodeActor(
1044            bs.newnode(
1045                'sound',
1046                attrs={
1047                    'sound': self.drum_roll_sound,
1048                    'positional': False,
1049                    'loop': False,
1050                },
1051            )
1052        ).autoretain()
1053
1054    def _got_friend_score_results(self, results: list[Any] | None) -> None:
1055        # FIXME: tidy this up
1056        # pylint: disable=too-many-locals
1057        # pylint: disable=too-many-branches
1058        # pylint: disable=too-many-statements
1059        from efro.util import asserttype
1060
1061        # delay a bit if results come in too fast
1062        assert self._begin_time is not None
1063        base_delay = max(0, 1.9 - (bs.time() - self._begin_time))
1064        ts_height = 300
1065        ts_h_offs = -550
1066        v_offs = 30
1067
1068        # Report in case of error.
1069        if results is None:
1070            self._friends_loading_status = Text(
1071                bs.Lstr(resource='friendScoresUnavailableText'),
1072                maxwidth=330,
1073                position=(-475, 150 + v_offs),
1074                color=(1, 1, 1, 0.4),
1075                transition=Text.Transition.FADE_IN,
1076                transition_delay=base_delay + 0.8,
1077                scale=0.7,
1078            )
1079            return
1080
1081        self._friends_loading_status = None
1082
1083        # Ok, it looks like we aren't able to reliably get a just-submitted
1084        # result returned in the score list, so we need to look for our score
1085        # in this list and replace it if ours is better or add ours otherwise.
1086        if self._score is not None:
1087            our_score_entry = [self._score, 'Me', True]
1088            for score in results:
1089                if score[2]:
1090                    if self._score_order == 'increasing':
1091                        our_score_entry[0] = max(score[0], self._score)
1092                    else:
1093                        our_score_entry[0] = min(score[0], self._score)
1094                    results.remove(score)
1095                    break
1096            results.append(our_score_entry)
1097            results.sort(
1098                reverse=self._score_order == 'increasing',
1099                key=lambda x: asserttype(x[0], int),
1100            )
1101
1102        # If we're not submitting our own score, we still want to change the
1103        # name of our own score to 'Me'.
1104        else:
1105            for score in results:
1106                if score[2]:
1107                    score[1] = 'Me'
1108                    break
1109        h_offs_extra = 80 if self._score_type == 'points' else 130
1110        v_offs_extra = 20
1111        v_offs_names = 0
1112        scale = 1.0
1113
1114        # Make sure there's at least 5.
1115        while len(results) < 5:
1116            results.append([0, '-', False])
1117        results = results[:5]
1118        times: list[tuple[float, float]] = []
1119        for i in range(len(results)):
1120            times.insert(
1121                random.randrange(0, len(times) + 1),
1122                (base_delay + i * 0.05, base_delay + 0.3 + i * 0.05),
1123            )
1124        for i, tval in enumerate(results):
1125            score = int(tval[0])
1126            name_str = tval[1]
1127            is_me = tval[2]
1128            if is_me and score == self._score:
1129                flash = True
1130                color0 = (0.6, 0.4, 0.1, 1.0)
1131                color1 = (0.6, 0.6, 0.6, 1.0)
1132                tdelay1 = base_delay + 1.0
1133                tdelay2 = base_delay + 1.0
1134            else:
1135                flash = False
1136                if is_me:
1137                    color0 = (0.6, 0.4, 0.1, 1.0)
1138                    color1 = (0.9, 1.0, 0.9, 1.0)
1139                else:
1140                    color0 = (0.6, 0.4, 0.1, 1.0)
1141                    color1 = (0.6, 0.6, 0.6, 1.0)
1142                tdelay1 = times[i][0]
1143                tdelay2 = times[i][1]
1144            if name_str != '-':
1145                Text(
1146                    (
1147                        str(score)
1148                        if self._score_type == 'points'
1149                        else bs.timestring((score * 10) / 1000.0)
1150                    ),
1151                    position=(
1152                        ts_h_offs + 20 + h_offs_extra,
1153                        v_offs_extra
1154                        + ts_height / 2
1155                        + -ts_height * (i + 1) / 10
1156                        + v_offs
1157                        + 11.0,
1158                    ),
1159                    h_align=Text.HAlign.RIGHT,
1160                    v_align=Text.VAlign.CENTER,
1161                    color=color0,
1162                    flash=flash,
1163                    transition=Text.Transition.IN_RIGHT,
1164                    transition_delay=tdelay1,
1165                ).autoretain()
1166            else:
1167                if is_me:
1168                    print('Error: got empty name_str on score result:', tval)
1169
1170            Text(
1171                bs.Lstr(value=name_str),
1172                position=(
1173                    ts_h_offs + 35 + h_offs_extra,
1174                    v_offs_extra
1175                    + ts_height / 2
1176                    + -ts_height * (i + 1) / 10
1177                    + v_offs_names
1178                    + v_offs
1179                    + 11.0,
1180                ),
1181                color=color1,
1182                maxwidth=160.0,
1183                v_align=Text.VAlign.CENTER,
1184                flash=flash,
1185                scale=scale,
1186                transition=Text.Transition.IN_RIGHT,
1187                transition_delay=tdelay2,
1188            ).autoretain()
1189
1190    def _got_score_results(self, results: dict[str, Any] | None) -> None:
1191        # FIXME: tidy this up
1192        # pylint: disable=too-many-locals
1193        # pylint: disable=too-many-branches
1194        # pylint: disable=too-many-statements
1195
1196        plus = bs.app.plus
1197        assert plus is not None
1198
1199        # We need to manually run this in the context of our activity
1200        # and only if we aren't shutting down.
1201        # (really should make the submit_score call handle that stuff itself)
1202        if self.expired:
1203            return
1204        with self.context:
1205            # Delay a bit if results come in too fast.
1206            assert self._begin_time is not None
1207            base_delay = max(0, 2.7 - (bs.time() - self._begin_time))
1208            v_offs = 20
1209            if results is None:
1210                self._score_loading_status = Text(
1211                    bs.Lstr(resource='worldScoresUnavailableText'),
1212                    position=(230, 150 + v_offs),
1213                    color=(1, 1, 1, 0.4),
1214                    transition=Text.Transition.FADE_IN,
1215                    transition_delay=base_delay + 0.3,
1216                    scale=0.7,
1217                )
1218            else:
1219                self._score_link = results['link']
1220                assert self._score_link is not None
1221                # Prepend our master-server addr if its a relative addr.
1222                if not self._score_link.startswith(
1223                    'http://'
1224                ) and not self._score_link.startswith('https://'):
1225                    self._score_link = (
1226                        plus.get_master_server_address()
1227                        + '/'
1228                        + self._score_link
1229                    )
1230                self._score_loading_status = None
1231                if 'tournamentSecondsRemaining' in results:
1232                    secs_remaining = results['tournamentSecondsRemaining']
1233                    assert isinstance(secs_remaining, int)
1234                    self._tournament_time_remaining = secs_remaining
1235                    self._tournament_time_remaining_text_timer = bs.BaseTimer(
1236                        1.0,
1237                        bs.WeakCall(
1238                            self._update_tournament_time_remaining_text
1239                        ),
1240                        repeat=True,
1241                    )
1242
1243            assert self._show_info is not None
1244            self._show_info['results'] = results
1245            if results is not None:
1246                if results['tops'] != '':
1247                    self._show_info['tops'] = results['tops']
1248                else:
1249                    self._show_info['tops'] = []
1250            offs_x = -195
1251            available = self._show_info['results'] is not None
1252            if self._score is not None:
1253                bs.basetimer(
1254                    (1.5 + base_delay),
1255                    bs.WeakCall(self._show_world_rank, offs_x),
1256                )
1257            ts_h_offs = 200
1258            ts_height = 300
1259
1260            # Show world tops.
1261            if available:
1262                # Show the number of games represented by this
1263                # list (except for in tournaments).
1264                if self.session.tournament_id is None:
1265                    Text(
1266                        bs.Lstr(
1267                            resource='lastGamesText',
1268                            subs=[
1269                                (
1270                                    '${COUNT}',
1271                                    str(self._show_info['results']['total']),
1272                                )
1273                            ],
1274                        ),
1275                        position=(
1276                            ts_h_offs - 35 + 95,
1277                            ts_height / 2 + 6 + v_offs,
1278                        ),
1279                        color=(0.4, 0.4, 0.4, 1.0),
1280                        scale=0.7,
1281                        transition=Text.Transition.IN_RIGHT,
1282                        transition_delay=base_delay + 0.3,
1283                    ).autoretain()
1284                else:
1285                    v_offs += 20
1286
1287                h_offs_extra = 0
1288                v_offs_names = 0
1289                scale = 1.0
1290                p_count = len(self._playerinfos)
1291                if p_count > 1:
1292                    h_offs_extra -= 40
1293                if self._score_type != 'points':
1294                    h_offs_extra += 60
1295                if p_count == 2:
1296                    scale = 0.9
1297                elif p_count == 3:
1298                    scale = 0.65
1299                elif p_count == 4:
1300                    scale = 0.5
1301
1302                # Make sure there's at least 10.
1303                while len(self._show_info['tops']) < 10:
1304                    self._show_info['tops'].append([0, '-'])
1305
1306                times: list[tuple[float, float]] = []
1307                for i in range(len(self._show_info['tops'])):
1308                    times.insert(
1309                        random.randrange(0, len(times) + 1),
1310                        (base_delay + i * 0.05, base_delay + 0.4 + i * 0.05),
1311                    )
1312                for i, tval in enumerate(self._show_info['tops']):
1313                    score = int(tval[0])
1314                    name_str = tval[1]
1315                    if self._name_str == name_str and self._score == score:
1316                        flash = True
1317                        color0 = (0.6, 0.4, 0.1, 1.0)
1318                        color1 = (0.6, 0.6, 0.6, 1.0)
1319                        tdelay1 = base_delay + 1.0
1320                        tdelay2 = base_delay + 1.0
1321                    else:
1322                        flash = False
1323                        if self._name_str == name_str:
1324                            color0 = (0.6, 0.4, 0.1, 1.0)
1325                            color1 = (0.9, 1.0, 0.9, 1.0)
1326                        else:
1327                            color0 = (0.6, 0.4, 0.1, 1.0)
1328                            color1 = (0.6, 0.6, 0.6, 1.0)
1329                        tdelay1 = times[i][0]
1330                        tdelay2 = times[i][1]
1331
1332                    if name_str != '-':
1333                        Text(
1334                            (
1335                                str(score)
1336                                if self._score_type == 'points'
1337                                else bs.timestring((score * 10) / 1000.0)
1338                            ),
1339                            position=(
1340                                ts_h_offs + 20 + h_offs_extra,
1341                                ts_height / 2
1342                                + -ts_height * (i + 1) / 10
1343                                + v_offs
1344                                + 11.0,
1345                            ),
1346                            h_align=Text.HAlign.RIGHT,
1347                            v_align=Text.VAlign.CENTER,
1348                            color=color0,
1349                            flash=flash,
1350                            transition=Text.Transition.IN_LEFT,
1351                            transition_delay=tdelay1,
1352                        ).autoretain()
1353                    Text(
1354                        bs.Lstr(value=name_str),
1355                        position=(
1356                            ts_h_offs + 35 + h_offs_extra,
1357                            ts_height / 2
1358                            + -ts_height * (i + 1) / 10
1359                            + v_offs_names
1360                            + v_offs
1361                            + 11.0,
1362                        ),
1363                        maxwidth=80.0 + 100.0 * len(self._playerinfos),
1364                        v_align=Text.VAlign.CENTER,
1365                        color=color1,
1366                        flash=flash,
1367                        scale=scale,
1368                        transition=Text.Transition.IN_LEFT,
1369                        transition_delay=tdelay2,
1370                    ).autoretain()
1371
1372    def _show_tips(self) -> None:
1373        from bascenev1lib.actor.tipstext import TipsText
1374
1375        TipsText(offs_y=30).autoretain()
1376
1377    def _update_tournament_time_remaining_text(self) -> None:
1378        if self._tournament_time_remaining is None:
1379            return
1380        self._tournament_time_remaining = max(
1381            0, self._tournament_time_remaining - 1
1382        )
1383        if self._tournament_time_remaining_text is not None:
1384            val = bs.timestring(
1385                self._tournament_time_remaining,
1386                centi=False,
1387            )
1388            self._tournament_time_remaining_text.node.text = val
1389
1390    def _show_world_rank(self, offs_x: float) -> None:
1391        # FIXME: Tidy this up.
1392        # pylint: disable=too-many-locals
1393        # pylint: disable=too-many-branches
1394        # pylint: disable=too-many-statements
1395        assert bs.app.classic is not None
1396        assert self._show_info is not None
1397        available = self._show_info['results'] is not None
1398
1399        if available and self._submit_score:
1400            error = (
1401                self._show_info['results']['error']
1402                if 'error' in self._show_info['results']
1403                else None
1404            )
1405            rank = self._show_info['results']['rank']
1406            total = self._show_info['results']['total']
1407            rating = (
1408                10.0
1409                if total == 1
1410                else 10.0 * (1.0 - (float(rank - 1) / (total - 1)))
1411            )
1412            player_rank = self._show_info['results']['playerRank']
1413            best_player_rank = self._show_info['results']['bestPlayerRank']
1414        else:
1415            error = False
1416            rating = None
1417            player_rank = None
1418            best_player_rank = None
1419
1420        # If we've got tournament-seconds-remaining, show it.
1421        if self._tournament_time_remaining is not None:
1422            Text(
1423                bs.Lstr(resource='coopSelectWindow.timeRemainingText'),
1424                position=(-360, -70 - 100),
1425                color=(1, 1, 1, 0.7),
1426                h_align=Text.HAlign.CENTER,
1427                v_align=Text.VAlign.CENTER,
1428                transition=Text.Transition.FADE_IN,
1429                scale=0.8,
1430                maxwidth=300,
1431                transition_delay=2.0,
1432            ).autoretain()
1433            self._tournament_time_remaining_text = Text(
1434                '',
1435                position=(-360, -110 - 100),
1436                color=(1, 1, 1, 0.7),
1437                h_align=Text.HAlign.CENTER,
1438                v_align=Text.VAlign.CENTER,
1439                transition=Text.Transition.FADE_IN,
1440                scale=1.6,
1441                maxwidth=150,
1442                transition_delay=2.0,
1443            )
1444
1445        # If we're a tournament, show prizes.
1446        try:
1447            assert bs.app.classic is not None
1448            tournament_id = self.session.tournament_id
1449            if tournament_id is not None:
1450                if tournament_id in bs.app.classic.accounts.tournament_info:
1451                    tourney_info = bs.app.classic.accounts.tournament_info[
1452                        tournament_id
1453                    ]
1454                    # pylint: disable=useless-suppression
1455                    # pylint: disable=unbalanced-tuple-unpacking
1456                    (
1457                        pr1,
1458                        pv1,
1459                        pr2,
1460                        pv2,
1461                        pr3,
1462                        pv3,
1463                    ) = bs.app.classic.get_tournament_prize_strings(
1464                        tourney_info
1465                    )
1466                    # pylint: enable=unbalanced-tuple-unpacking
1467                    # pylint: enable=useless-suppression
1468
1469                    Text(
1470                        bs.Lstr(resource='coopSelectWindow.prizesText'),
1471                        position=(-360, -70 + 77),
1472                        color=(1, 1, 1, 0.7),
1473                        h_align=Text.HAlign.CENTER,
1474                        v_align=Text.VAlign.CENTER,
1475                        transition=Text.Transition.FADE_IN,
1476                        scale=1.0,
1477                        maxwidth=300,
1478                        transition_delay=2.0,
1479                    ).autoretain()
1480                    vval = -107 + 70
1481                    for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)):
1482                        Text(
1483                            rng,
1484                            position=(-410 + 10, vval),
1485                            color=(1, 1, 1, 0.7),
1486                            h_align=Text.HAlign.RIGHT,
1487                            v_align=Text.VAlign.CENTER,
1488                            transition=Text.Transition.FADE_IN,
1489                            scale=0.6,
1490                            maxwidth=300,
1491                            transition_delay=2.0,
1492                        ).autoretain()
1493                        Text(
1494                            val,
1495                            position=(-390 + 10, vval),
1496                            color=(0.7, 0.7, 0.7, 1.0),
1497                            h_align=Text.HAlign.LEFT,
1498                            v_align=Text.VAlign.CENTER,
1499                            transition=Text.Transition.FADE_IN,
1500                            scale=0.8,
1501                            maxwidth=300,
1502                            transition_delay=2.0,
1503                        ).autoretain()
1504                        vval -= 35
1505        except Exception:
1506            logging.exception('Error showing prize ranges.')
1507
1508        if self._do_new_rating:
1509            if error:
1510                ZoomText(
1511                    bs.Lstr(resource='failText'),
1512                    flash=True,
1513                    trail=True,
1514                    scale=1.0 if available else 0.333,
1515                    tilt_translate=0.11,
1516                    h_align='center',
1517                    position=(190 + offs_x, -60),
1518                    maxwidth=200,
1519                    jitter=1.0,
1520                ).autoretain()
1521                Text(
1522                    bs.Lstr(translate=('serverResponses', error)),
1523                    position=(0, -140),
1524                    color=(1, 1, 1, 0.7),
1525                    h_align=Text.HAlign.CENTER,
1526                    v_align=Text.VAlign.CENTER,
1527                    transition=Text.Transition.FADE_IN,
1528                    scale=0.9,
1529                    maxwidth=400,
1530                    transition_delay=1.0,
1531                ).autoretain()
1532            elif self._submit_score:
1533                ZoomText(
1534                    (
1535                        ('#' + str(player_rank))
1536                        if player_rank is not None
1537                        else bs.Lstr(resource='unavailableText')
1538                    ),
1539                    flash=True,
1540                    trail=True,
1541                    scale=1.0 if available else 0.333,
1542                    tilt_translate=0.11,
1543                    h_align='center',
1544                    position=(190 + offs_x, -60),
1545                    maxwidth=200,
1546                    jitter=1.0,
1547                ).autoretain()
1548
1549                Text(
1550                    bs.Lstr(
1551                        value='${A}:',
1552                        subs=[('${A}', bs.Lstr(resource='rankText'))],
1553                    ),
1554                    position=(0, 36),
1555                    maxwidth=300,
1556                    transition=Text.Transition.FADE_IN,
1557                    h_align=Text.HAlign.CENTER,
1558                    v_align=Text.VAlign.CENTER,
1559                    transition_delay=0,
1560                ).autoretain()
1561                if best_player_rank is not None:
1562                    Text(
1563                        bs.Lstr(
1564                            resource='currentStandingText',
1565                            fallback_resource='bestRankText',
1566                            subs=[('${RANK}', str(best_player_rank))],
1567                        ),
1568                        position=(0, -155),
1569                        color=(1, 1, 1, 0.7),
1570                        h_align=Text.HAlign.CENTER,
1571                        transition=Text.Transition.FADE_IN,
1572                        scale=0.7,
1573                        transition_delay=1.0,
1574                    ).autoretain()
1575        else:
1576            ZoomText(
1577                (
1578                    f'{rating:.1f}'
1579                    if available
1580                    else bs.Lstr(resource='unavailableText')
1581                ),
1582                flash=True,
1583                trail=True,
1584                scale=0.6 if available else 0.333,
1585                tilt_translate=0.11,
1586                h_align='center',
1587                position=(190 + offs_x, -94),
1588                maxwidth=200,
1589                jitter=1.0,
1590            ).autoretain()
1591
1592            if available:
1593                if rating >= 9.5:
1594                    stars = 3
1595                elif rating >= 7.5:
1596                    stars = 2
1597                elif rating > 0.0:
1598                    stars = 1
1599                else:
1600                    stars = 0
1601                star_tex = bs.gettexture('star')
1602                star_x = 135 + offs_x
1603                for _i in range(stars):
1604                    img = bs.NodeActor(
1605                        bs.newnode(
1606                            'image',
1607                            attrs={
1608                                'texture': star_tex,
1609                                'position': (star_x, -16),
1610                                'scale': (62, 62),
1611                                'opacity': 1.0,
1612                                'color': (2.2, 1.2, 0.3),
1613                                'absolute_scale': True,
1614                            },
1615                        )
1616                    ).autoretain()
1617
1618                    assert img.node
1619                    bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1})
1620                    star_x += 60
1621                for _i in range(3 - stars):
1622                    img = bs.NodeActor(
1623                        bs.newnode(
1624                            'image',
1625                            attrs={
1626                                'texture': star_tex,
1627                                'position': (star_x, -16),
1628                                'scale': (62, 62),
1629                                'opacity': 1.0,
1630                                'color': (0.3, 0.3, 0.3),
1631                                'absolute_scale': True,
1632                            },
1633                        )
1634                    ).autoretain()
1635                    assert img.node
1636                    bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1})
1637                    star_x += 60
1638
1639                def dostar(
1640                    count: int, xval: float, offs_y: float, score: str
1641                ) -> None:
1642                    Text(
1643                        score + ' =',
1644                        position=(xval, -64 + offs_y),
1645                        color=(0.6, 0.6, 0.6, 0.6),
1646                        h_align=Text.HAlign.CENTER,
1647                        v_align=Text.VAlign.CENTER,
1648                        transition=Text.Transition.FADE_IN,
1649                        scale=0.4,
1650                        transition_delay=1.0,
1651                    ).autoretain()
1652                    stx = xval + 20
1653                    for _i2 in range(count):
1654                        img2 = bs.NodeActor(
1655                            bs.newnode(
1656                                'image',
1657                                attrs={
1658                                    'texture': star_tex,
1659                                    'position': (stx, -64 + offs_y),
1660                                    'scale': (12, 12),
1661                                    'opacity': 0.7,
1662                                    'color': (2.2, 1.2, 0.3),
1663                                    'absolute_scale': True,
1664                                },
1665                            )
1666                        ).autoretain()
1667                        assert img2.node
1668                        bs.animate(img2.node, 'opacity', {1.0: 0.0, 1.5: 0.5})
1669                        stx += 13.0
1670
1671                dostar(1, -44 - 30, -112, '0.0')
1672                dostar(2, 10 - 30, -112, '7.5')
1673                dostar(3, 77 - 30, -112, '9.5')
1674            try:
1675                best_rank = self._campaign.getlevel(self._level_name).rating
1676            except Exception:
1677                best_rank = 0.0
1678
1679            if available:
1680                Text(
1681                    bs.Lstr(
1682                        resource='outOfText',
1683                        subs=[
1684                            (
1685                                '${RANK}',
1686                                str(int(self._show_info['results']['rank'])),
1687                            ),
1688                            (
1689                                '${ALL}',
1690                                str(self._show_info['results']['total']),
1691                            ),
1692                        ],
1693                    ),
1694                    position=(0, -155 if self._newly_complete else -145),
1695                    color=(1, 1, 1, 0.7),
1696                    h_align=Text.HAlign.CENTER,
1697                    transition=Text.Transition.FADE_IN,
1698                    scale=0.55,
1699                    transition_delay=1.0,
1700                ).autoretain()
1701
1702            new_best = best_rank > self._old_best_rank and best_rank > 0.0
1703            was_string = bs.Lstr(
1704                value=' ${A}',
1705                subs=[
1706                    ('${A}', bs.Lstr(resource='scoreWasText')),
1707                    ('${COUNT}', str(self._old_best_rank)),
1708                ],
1709            )
1710            if not self._newly_complete:
1711                Text(
1712                    (
1713                        bs.Lstr(
1714                            value='${A}${B}',
1715                            subs=[
1716                                (
1717                                    '${A}',
1718                                    bs.Lstr(resource='newPersonalBestText'),
1719                                ),
1720                                ('${B}', was_string),
1721                            ],
1722                        )
1723                        if new_best
1724                        else bs.Lstr(
1725                            resource='bestRatingText',
1726                            subs=[('${RATING}', str(best_rank))],
1727                        )
1728                    ),
1729                    position=(0, -165),
1730                    color=(1, 1, 1, 0.7),
1731                    flash=new_best,
1732                    h_align=Text.HAlign.CENTER,
1733                    transition=(
1734                        Text.Transition.IN_RIGHT
1735                        if new_best
1736                        else Text.Transition.FADE_IN
1737                    ),
1738                    scale=0.5,
1739                    transition_delay=1.0,
1740                ).autoretain()
1741
1742            Text(
1743                bs.Lstr(
1744                    value='${A}:',
1745                    subs=[('${A}', bs.Lstr(resource='ratingText'))],
1746                ),
1747                position=(0, 36),
1748                maxwidth=300,
1749                transition=Text.Transition.FADE_IN,
1750                h_align=Text.HAlign.CENTER,
1751                v_align=Text.VAlign.CENTER,
1752                transition_delay=0,
1753            ).autoretain()
1754
1755        if self._submit_score:
1756            bs.timer(0.35, self._score_display_sound.play)
1757            if not error:
1758                bs.timer(0.35, self.cymbal_sound.play)
1759
1760    def _show_fail(self) -> None:
1761        ZoomText(
1762            bs.Lstr(resource='failText'),
1763            maxwidth=300,
1764            flash=False,
1765            trail=True,
1766            h_align='center',
1767            tilt_translate=0.11,
1768            position=(0, 40),
1769            jitter=1.0,
1770        ).autoretain()
1771        if self._fail_message is not None:
1772            Text(
1773                self._fail_message,
1774                h_align=Text.HAlign.CENTER,
1775                position=(0, -130),
1776                maxwidth=300,
1777                color=(1, 1, 1, 0.5),
1778                transition=Text.Transition.FADE_IN,
1779                transition_delay=1.0,
1780            ).autoretain()
1781        bs.timer(0.35, self._score_display_sound.play)
1782
1783    def _show_score_val(self, offs_x: float) -> None:
1784        assert self._score_type is not None
1785        assert self._score is not None
1786        ZoomText(
1787            (
1788                str(self._score)
1789                if self._score_type == 'points'
1790                else bs.timestring((self._score * 10) / 1000.0)
1791            ),
1792            maxwidth=300,
1793            flash=True,
1794            trail=True,
1795            scale=1.0 if self._score_type == 'points' else 0.6,
1796            h_align='center',
1797            tilt_translate=0.11,
1798            position=(190 + offs_x, 115),
1799            jitter=1.0,
1800        ).autoretain()
1801        Text(
1802            (
1803                bs.Lstr(
1804                    value='${A}:',
1805                    subs=[('${A}', bs.Lstr(resource='finalScoreText'))],
1806                )
1807                if self._score_type == 'points'
1808                else bs.Lstr(
1809                    value='${A}:',
1810                    subs=[('${A}', bs.Lstr(resource='finalTimeText'))],
1811                )
1812            ),
1813            maxwidth=300,
1814            position=(0, 200),
1815            transition=Text.Transition.FADE_IN,
1816            h_align=Text.HAlign.CENTER,
1817            v_align=Text.VAlign.CENTER,
1818            transition_delay=0,
1819        ).autoretain()
1820        bs.timer(0.35, self._score_display_sound.play)
class CoopScoreScreen(bascenev1._activity.Activity[bascenev1._player.Player, bascenev1._team.Team]):
  27class CoopScoreScreen(bs.Activity[bs.Player, bs.Team]):
  28    """Score screen showing the results of a cooperative game."""
  29
  30    def __init__(self, settings: dict):
  31        # pylint: disable=too-many-statements
  32        super().__init__(settings)
  33
  34        plus = bs.app.plus
  35        assert plus is not None
  36
  37        # Keep prev activity alive while we fade in
  38        self.transition_time = 0.5
  39        self.inherits_tint = True
  40        self.inherits_vr_camera_offset = True
  41        self.inherits_music = True
  42        self.use_fixed_vr_overlay = True
  43
  44        self._do_new_rating: bool = self.session.tournament_id is not None
  45
  46        self._score_display_sound = bs.getsound('scoreHit01')
  47        self._score_display_sound_small = bs.getsound('scoreHit02')
  48        self.drum_roll_sound = bs.getsound('drumRoll')
  49        self.cymbal_sound = bs.getsound('cymbal')
  50
  51        self._replay_icon_texture = bui.gettexture('replayIcon')
  52        self._menu_icon_texture = bui.gettexture('menuIcon')
  53        self._next_level_icon_texture = bui.gettexture('nextLevelIcon')
  54
  55        self._campaign: bs.Campaign = settings['campaign']
  56
  57        self._have_achievements = (
  58            bs.app.classic is not None
  59            and bs.app.classic.ach.achievements_for_coop_level(
  60                self._campaign.name + ':' + settings['level']
  61            )
  62        )
  63
  64        self._game_service_icon_color: Sequence[float] | None
  65        self._game_service_achievements_texture: bui.Texture | None
  66        self._game_service_leaderboards_texture: bui.Texture | None
  67
  68        # Tie in to specific game services if they are active.
  69        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
  70        gpgs_active = adapter is not None and adapter.is_back_end_active()
  71        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
  72        game_center_active = (
  73            adapter is not None and adapter.is_back_end_active()
  74        )
  75
  76        if game_center_active:
  77            self._game_service_icon_color = (1.0, 1.0, 1.0)
  78            icon = bui.gettexture('gameCenterIcon')
  79            self._game_service_achievements_texture = icon
  80            self._game_service_leaderboards_texture = icon
  81            self._account_has_achievements = True
  82        elif gpgs_active:
  83            self._game_service_icon_color = (0.8, 1.0, 0.6)
  84            self._game_service_achievements_texture = bui.gettexture(
  85                'googlePlayAchievementsIcon'
  86            )
  87            self._game_service_leaderboards_texture = bui.gettexture(
  88                'googlePlayLeaderboardsIcon'
  89            )
  90            self._account_has_achievements = True
  91        else:
  92            self._game_service_icon_color = None
  93            self._game_service_achievements_texture = None
  94            self._game_service_leaderboards_texture = None
  95            self._account_has_achievements = False
  96
  97        self._cashregistersound = bs.getsound('cashRegister')
  98        self._gun_cocking_sound = bs.getsound('gunCocking')
  99        self._dingsound = bs.getsound('ding')
 100        self._score_link: str | None = None
 101        self._root_ui: bui.Widget | None = None
 102        self._background: bs.Actor | None = None
 103        self._old_best_rank = 0.0
 104        self._game_name_str: str | None = None
 105        self._game_config_str: str | None = None
 106
 107        # Ui bits.
 108        self._corner_button_offs: tuple[float, float] | None = None
 109        self._league_rank_button: LeagueRankButton | None = None
 110        self._store_button_instance: StoreButton | None = None
 111        self._restart_button: bui.Widget | None = None
 112        self._update_corner_button_positions_timer: bui.AppTimer | None = None
 113        self._next_level_error: bs.Actor | None = None
 114
 115        # Score/gameplay bits.
 116        self._was_complete: bool | None = None
 117        self._is_complete: bool | None = None
 118        self._newly_complete: bool | None = None
 119        self._is_more_levels: bool | None = None
 120        self._next_level_name: str | None = None
 121        self._show_info: dict[str, Any] | None = None
 122        self._name_str: str | None = None
 123        self._friends_loading_status: bs.Actor | None = None
 124        self._score_loading_status: bs.Actor | None = None
 125        self._tournament_time_remaining: float | None = None
 126        self._tournament_time_remaining_text: Text | None = None
 127        self._tournament_time_remaining_text_timer: bs.BaseTimer | None = None
 128        self._submit_score = self.session.submit_score
 129
 130        # Stuff for activity skip by pressing button
 131        self._birth_time = bs.time()
 132        self._min_view_time = 5.0
 133        self._allow_server_transition = False
 134        self._server_transitioning: bool | None = None
 135
 136        self._playerinfos: list[bs.PlayerInfo] = settings['playerinfos']
 137        assert isinstance(self._playerinfos, list)
 138        assert all(isinstance(i, bs.PlayerInfo) for i in self._playerinfos)
 139
 140        self._score: int | None = settings['score']
 141        assert isinstance(self._score, (int, type(None)))
 142
 143        self._fail_message: bs.Lstr | None = settings['fail_message']
 144        assert isinstance(self._fail_message, (bs.Lstr, type(None)))
 145
 146        self._begin_time: float | None = None
 147
 148        self._score_order: str
 149        if 'score_order' in settings:
 150            if not settings['score_order'] in ['increasing', 'decreasing']:
 151                raise ValueError(
 152                    'Invalid score order: ' + settings['score_order']
 153                )
 154            self._score_order = settings['score_order']
 155        else:
 156            self._score_order = 'increasing'
 157        assert isinstance(self._score_order, str)
 158
 159        self._score_type: str
 160        if 'score_type' in settings:
 161            if not settings['score_type'] in ['points', 'time']:
 162                raise ValueError(
 163                    'Invalid score type: ' + settings['score_type']
 164                )
 165            self._score_type = settings['score_type']
 166        else:
 167            self._score_type = 'points'
 168        assert isinstance(self._score_type, str)
 169
 170        self._level_name: str = settings['level']
 171        assert isinstance(self._level_name, str)
 172
 173        self._game_name_str = self._campaign.name + ':' + self._level_name
 174        self._game_config_str = (
 175            str(len(self._playerinfos))
 176            + 'p'
 177            + self._campaign.getlevel(self._level_name)
 178            .get_score_version_string()
 179            .replace(' ', '_')
 180        )
 181
 182        try:
 183            self._old_best_rank = self._campaign.getlevel(
 184                self._level_name
 185            ).rating
 186        except Exception:
 187            self._old_best_rank = 0.0
 188
 189        self._victory: bool = settings['outcome'] == 'victory'
 190
 191    @override
 192    def __del__(self) -> None:
 193        super().__del__()
 194
 195        # If our UI is still up, kill it.
 196        if self._root_ui and not self._root_ui.transitioning_out:
 197            with bui.ContextRef.empty():
 198                bui.containerwidget(edit=self._root_ui, transition='out_left')
 199
 200    @override
 201    def on_transition_in(self) -> None:
 202        from bascenev1lib.actor import background  # FIXME NO BSSTD
 203
 204        bs.set_analytics_screen('Coop Score Screen')
 205        super().on_transition_in()
 206        self._background = background.Background(
 207            fade_time=0.45, start_faded=False, show_logo=True
 208        )
 209
 210    def _ui_menu(self) -> None:
 211        from bauiv1lib import specialoffer
 212
 213        if specialoffer.show_offer():
 214            return
 215        bui.containerwidget(edit=self._root_ui, transition='out_left')
 216        with self.context:
 217            bs.timer(0.1, bs.Call(bs.WeakCall(self.session.end)))
 218
 219    def _ui_restart(self) -> None:
 220        from bauiv1lib.tournamententry import TournamentEntryWindow
 221        from bauiv1lib import specialoffer
 222
 223        if specialoffer.show_offer():
 224            return
 225
 226        # If we're in a tournament and it looks like there's no time left,
 227        # disallow.
 228        if self.session.tournament_id is not None:
 229            if self._tournament_time_remaining is None:
 230                bui.screenmessage(
 231                    bui.Lstr(resource='tournamentCheckingStateText'),
 232                    color=(1, 0, 0),
 233                )
 234                bui.getsound('error').play()
 235                return
 236            if self._tournament_time_remaining <= 0:
 237                bui.screenmessage(
 238                    bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
 239                )
 240                bui.getsound('error').play()
 241                return
 242
 243        # If there are currently fewer players than our session min,
 244        # don't allow.
 245        if len(self.players) < self.session.min_players:
 246            bui.screenmessage(
 247                bui.Lstr(resource='notEnoughPlayersRemainingText'),
 248                color=(1, 0, 0),
 249            )
 250            bui.getsound('error').play()
 251            return
 252
 253        self._campaign.set_selected_level(self._level_name)
 254
 255        # If this is a tournament, go back to the tournament-entry UI
 256        # otherwise just hop back in.
 257        tournament_id = self.session.tournament_id
 258        if tournament_id is not None:
 259            assert self._restart_button is not None
 260            TournamentEntryWindow(
 261                tournament_id=tournament_id,
 262                tournament_activity=self,
 263                position=self._restart_button.get_screen_space_center(),
 264            )
 265        else:
 266            bui.containerwidget(edit=self._root_ui, transition='out_left')
 267            self.can_show_ad_on_death = True
 268            with self.context:
 269                self.end({'outcome': 'restart'})
 270
 271    def _ui_next(self) -> None:
 272        from bauiv1lib.specialoffer import show_offer
 273
 274        if show_offer():
 275            return
 276
 277        # If we didn't just complete this level but are choosing to play the
 278        # next one, set it as current (this won't happen otherwise).
 279        if (
 280            self._is_complete
 281            and self._is_more_levels
 282            and not self._newly_complete
 283        ):
 284            assert self._next_level_name is not None
 285            self._campaign.set_selected_level(self._next_level_name)
 286        bui.containerwidget(edit=self._root_ui, transition='out_left')
 287        with self.context:
 288            self.end({'outcome': 'next_level'})
 289
 290    def _ui_gc(self) -> None:
 291        if bs.app.plus is not None:
 292            bs.app.plus.show_game_service_ui(
 293                'leaderboard',
 294                game=self._game_name_str,
 295                game_version=self._game_config_str,
 296            )
 297        else:
 298            logging.warning('show_game_service_ui requires plus feature-set')
 299
 300    def _ui_show_achievements(self) -> None:
 301        if bs.app.plus is not None:
 302            bs.app.plus.show_game_service_ui('achievements')
 303        else:
 304            logging.warning('show_game_service_ui requires plus feature-set')
 305
 306    def _ui_worlds_best(self) -> None:
 307        if self._score_link is None:
 308            bui.getsound('error').play()
 309            bui.screenmessage(
 310                bui.Lstr(resource='scoreListUnavailableText'), color=(1, 0.5, 0)
 311            )
 312        else:
 313            bui.open_url(self._score_link)
 314
 315    def _ui_error(self) -> None:
 316        with self.context:
 317            self._next_level_error = Text(
 318                bs.Lstr(resource='completeThisLevelToProceedText'),
 319                flash=True,
 320                maxwidth=360,
 321                scale=0.54,
 322                h_align=Text.HAlign.CENTER,
 323                color=(0.5, 0.7, 0.5, 1),
 324                position=(300, -235),
 325            )
 326            bui.getsound('error').play()
 327            bs.timer(
 328                2.0,
 329                bs.WeakCall(
 330                    self._next_level_error.handlemessage, bs.DieMessage()
 331                ),
 332            )
 333
 334    def _should_show_worlds_best_button(self) -> bool:
 335        # Link is too complicated to display with no browser.
 336        return bui.is_browser_likely_available()
 337
 338    def request_ui(self) -> None:
 339        """Set up a callback to show our UI at the next opportune time."""
 340        assert bui.app.classic is not None
 341        # We don't want to just show our UI in case the user already has the
 342        # main menu up, so instead we add a callback for when the menu
 343        # closes; if we're still alive, we'll come up then.
 344        # If there's no main menu this gets called immediately.
 345        bui.app.ui_v1.add_main_menu_close_callback(bui.WeakCall(self.show_ui))
 346
 347    def show_ui(self) -> None:
 348        """Show the UI for restarting, playing the next Level, etc."""
 349        # pylint: disable=too-many-locals
 350        from bauiv1lib.store.button import StoreButton
 351        from bauiv1lib.league.rankbutton import LeagueRankButton
 352
 353        assert bui.app.classic is not None
 354
 355        env = bui.app.env
 356
 357        delay = 0.7 if (self._score is not None) else 0.0
 358
 359        # If there's no players left in the game, lets not show the UI
 360        # (that would allow restarting the game with zero players, etc).
 361        if not self.players:
 362            return
 363
 364        rootc = self._root_ui = bui.containerwidget(
 365            size=(0, 0), transition='in_right'
 366        )
 367
 368        h_offs = 7.0
 369        v_offs = -280.0
 370
 371        # We wanna prevent controllers users from popping up browsers
 372        # or game-center widgets in cases where they can't easily get back
 373        # to the game (like on mac).
 374        can_select_extra_buttons = bui.app.classic.platform == 'android'
 375
 376        bui.set_ui_input_device(None)  # Menu is up for grabs.
 377
 378        if self._have_achievements and self._account_has_achievements:
 379            bui.buttonwidget(
 380                parent=rootc,
 381                color=(0.45, 0.4, 0.5),
 382                position=(h_offs - 520, v_offs + 450 - 235 + 40),
 383                size=(300, 60),
 384                label=bui.Lstr(resource='achievementsText'),
 385                on_activate_call=bui.WeakCall(self._ui_show_achievements),
 386                transition_delay=delay + 1.5,
 387                icon=self._game_service_achievements_texture,
 388                icon_color=self._game_service_icon_color,
 389                autoselect=True,
 390                selectable=can_select_extra_buttons,
 391            )
 392
 393        if self._should_show_worlds_best_button():
 394            bui.buttonwidget(
 395                parent=rootc,
 396                color=(0.45, 0.4, 0.5),
 397                position=(160, v_offs + 480),
 398                size=(350, 62),
 399                label=(
 400                    bui.Lstr(resource='tournamentStandingsText')
 401                    if self.session.tournament_id is not None
 402                    else (
 403                        bui.Lstr(resource='worldsBestScoresText')
 404                        if self._score_type == 'points'
 405                        else bui.Lstr(resource='worldsBestTimesText')
 406                    )
 407                ),
 408                autoselect=True,
 409                on_activate_call=bui.WeakCall(self._ui_worlds_best),
 410                transition_delay=delay + 1.9,
 411                selectable=can_select_extra_buttons,
 412            )
 413        else:
 414            pass
 415
 416        show_next_button = self._is_more_levels and not (env.demo or env.arcade)
 417
 418        if not show_next_button:
 419            h_offs += 70
 420
 421        menu_button = bui.buttonwidget(
 422            parent=rootc,
 423            autoselect=True,
 424            position=(h_offs - 130 - 60, v_offs),
 425            size=(110, 85),
 426            label='',
 427            on_activate_call=bui.WeakCall(self._ui_menu),
 428        )
 429        bui.imagewidget(
 430            parent=rootc,
 431            draw_controller=menu_button,
 432            position=(h_offs - 130 - 60 + 22, v_offs + 14),
 433            size=(60, 60),
 434            texture=self._menu_icon_texture,
 435            opacity=0.8,
 436        )
 437        self._restart_button = restart_button = bui.buttonwidget(
 438            parent=rootc,
 439            autoselect=True,
 440            position=(h_offs - 60, v_offs),
 441            size=(110, 85),
 442            label='',
 443            on_activate_call=bui.WeakCall(self._ui_restart),
 444        )
 445        bui.imagewidget(
 446            parent=rootc,
 447            draw_controller=restart_button,
 448            position=(h_offs - 60 + 19, v_offs + 7),
 449            size=(70, 70),
 450            texture=self._replay_icon_texture,
 451            opacity=0.8,
 452        )
 453
 454        next_button: bui.Widget | None = None
 455
 456        # Our 'next' button is disabled if we haven't unlocked the next
 457        # level yet and invisible if there is none.
 458        if show_next_button:
 459            if self._is_complete:
 460                call = bui.WeakCall(self._ui_next)
 461                button_sound = True
 462                image_opacity = 0.8
 463                color = None
 464            else:
 465                call = bui.WeakCall(self._ui_error)
 466                button_sound = False
 467                image_opacity = 0.2
 468                color = (0.3, 0.3, 0.3)
 469            next_button = bui.buttonwidget(
 470                parent=rootc,
 471                autoselect=True,
 472                position=(h_offs + 130 - 60, v_offs),
 473                size=(110, 85),
 474                label='',
 475                on_activate_call=call,
 476                color=color,
 477                enable_sound=button_sound,
 478            )
 479            bui.imagewidget(
 480                parent=rootc,
 481                draw_controller=next_button,
 482                position=(h_offs + 130 - 60 + 12, v_offs + 5),
 483                size=(80, 80),
 484                texture=self._next_level_icon_texture,
 485                opacity=image_opacity,
 486            )
 487
 488        x_offs_extra = 0 if show_next_button else -100
 489        self._corner_button_offs = (
 490            h_offs + 300.0 + 100.0 + x_offs_extra,
 491            v_offs + 560.0,
 492        )
 493
 494        if env.demo or env.arcade:
 495            self._league_rank_button = None
 496            self._store_button_instance = None
 497        else:
 498            self._league_rank_button = LeagueRankButton(
 499                parent=rootc,
 500                position=(h_offs + 300 + 100 + x_offs_extra, v_offs + 560),
 501                size=(100, 60),
 502                scale=0.9,
 503                color=(0.4, 0.4, 0.9),
 504                textcolor=(0.9, 0.9, 2.0),
 505                transition_delay=0.0,
 506                smooth_update_delay=5.0,
 507            )
 508            self._store_button_instance = StoreButton(
 509                parent=rootc,
 510                position=(h_offs + 400 + 100 + x_offs_extra, v_offs + 560),
 511                show_tickets=True,
 512                sale_scale=0.85,
 513                size=(100, 60),
 514                scale=0.9,
 515                button_type='square',
 516                color=(0.35, 0.25, 0.45),
 517                textcolor=(0.9, 0.7, 1.0),
 518                transition_delay=0.0,
 519            )
 520
 521        bui.containerwidget(
 522            edit=rootc,
 523            selected_child=(
 524                next_button
 525                if (self._newly_complete and self._victory and show_next_button)
 526                else restart_button
 527            ),
 528            on_cancel_call=menu_button.activate,
 529        )
 530
 531        self._update_corner_button_positions()
 532        self._update_corner_button_positions_timer = bui.AppTimer(
 533            1.0, bui.WeakCall(self._update_corner_button_positions), repeat=True
 534        )
 535
 536    def _update_corner_button_positions(self) -> None:
 537        offs = -55 if bui.is_party_icon_visible() else 0
 538        assert self._corner_button_offs is not None
 539        pos_x = self._corner_button_offs[0] + offs
 540        pos_y = self._corner_button_offs[1]
 541        if self._league_rank_button is not None:
 542            self._league_rank_button.set_position((pos_x, pos_y))
 543        if self._store_button_instance is not None:
 544            self._store_button_instance.set_position((pos_x + 100, pos_y))
 545
 546    def _player_press(self) -> None:
 547        # (Only for headless builds).
 548
 549        # If this activity is a good 'end point', ask server-mode just once if
 550        # it wants to do anything special like switch sessions or kill the app.
 551        if (
 552            self._allow_server_transition
 553            and bs.app.classic is not None
 554            and bs.app.classic.server is not None
 555            and self._server_transitioning is None
 556        ):
 557            self._server_transitioning = (
 558                bs.app.classic.server.handle_transition()
 559            )
 560            assert isinstance(self._server_transitioning, bool)
 561
 562        # If server-mode is handling this, don't do anything ourself.
 563        if self._server_transitioning is True:
 564            return
 565
 566        # Otherwise restart current level.
 567        self._campaign.set_selected_level(self._level_name)
 568        with self.context:
 569            self.end({'outcome': 'restart'})
 570
 571    def _safe_assign(self, player: bs.Player) -> None:
 572        # (Only for headless builds).
 573
 574        # Just to be extra careful, don't assign if we're transitioning out.
 575        # (though theoretically that should be ok).
 576        if not self.is_transitioning_out() and player:
 577            player.assigninput(
 578                (
 579                    bs.InputType.JUMP_PRESS,
 580                    bs.InputType.PUNCH_PRESS,
 581                    bs.InputType.BOMB_PRESS,
 582                    bs.InputType.PICK_UP_PRESS,
 583                ),
 584                self._player_press,
 585            )
 586
 587    @override
 588    def on_player_join(self, player: bs.Player) -> None:
 589        super().on_player_join(player)
 590
 591        if bs.app.classic is not None and bs.app.classic.server is not None:
 592            # Host can't press retry button, so anyone can do it instead.
 593            time_till_assign = max(
 594                0, self._birth_time + self._min_view_time - bs.time()
 595            )
 596
 597            bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player))
 598
 599    @override
 600    def on_begin(self) -> None:
 601        # FIXME: Clean this up.
 602        # pylint: disable=too-many-statements
 603        # pylint: disable=too-many-branches
 604        # pylint: disable=too-many-locals
 605        super().on_begin()
 606
 607        app = bs.app
 608        env = app.env
 609        plus = app.plus
 610        assert plus is not None
 611
 612        self._begin_time = bs.time()
 613
 614        # Calc whether the level is complete and other stuff.
 615        levels = self._campaign.levels
 616        level = self._campaign.getlevel(self._level_name)
 617        self._was_complete = level.complete
 618        self._is_complete = self._was_complete or self._victory
 619        self._newly_complete = self._is_complete and not self._was_complete
 620        self._is_more_levels = (
 621            level.index < len(levels) - 1
 622        ) and self._campaign.sequential
 623
 624        # Any time we complete a level, set the next one as unlocked.
 625        if self._is_complete and self._is_more_levels:
 626            plus.add_v1_account_transaction(
 627                {
 628                    'type': 'COMPLETE_LEVEL',
 629                    'campaign': self._campaign.name,
 630                    'level': self._level_name,
 631                }
 632            )
 633            self._next_level_name = levels[level.index + 1].name
 634
 635            # If this is the first time we completed it, set the next one
 636            # as current.
 637            if self._newly_complete:
 638                cfg = app.config
 639                cfg['Selected Coop Game'] = (
 640                    self._campaign.name + ':' + self._next_level_name
 641                )
 642                cfg.commit()
 643                self._campaign.set_selected_level(self._next_level_name)
 644
 645        bs.timer(1.0, bs.WeakCall(self.request_ui))
 646
 647        if (
 648            self._is_complete
 649            and self._victory
 650            and self._is_more_levels
 651            and not (env.demo or env.arcade)
 652        ):
 653            Text(
 654                (
 655                    bs.Lstr(
 656                        value='${A}:\n',
 657                        subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))],
 658                    )
 659                    if self._newly_complete
 660                    else bs.Lstr(
 661                        value='${A}:\n',
 662                        subs=[('${A}', bs.Lstr(resource='nextLevelText'))],
 663                    )
 664                ),
 665                transition=Text.Transition.IN_RIGHT,
 666                transition_delay=5.2,
 667                flash=self._newly_complete,
 668                scale=0.54,
 669                h_align=Text.HAlign.CENTER,
 670                maxwidth=270,
 671                color=(0.5, 0.7, 0.5, 1),
 672                position=(270, -235),
 673            ).autoretain()
 674            assert self._next_level_name is not None
 675            Text(
 676                bs.Lstr(translate=('coopLevelNames', self._next_level_name)),
 677                transition=Text.Transition.IN_RIGHT,
 678                transition_delay=5.2,
 679                flash=self._newly_complete,
 680                scale=0.7,
 681                h_align=Text.HAlign.CENTER,
 682                maxwidth=205,
 683                color=(0.5, 0.7, 0.5, 1),
 684                position=(270, -255),
 685            ).autoretain()
 686            if self._newly_complete:
 687                bs.timer(5.2, self._cashregistersound.play)
 688                bs.timer(5.2, self._dingsound.play)
 689
 690        offs_x = -195
 691        if len(self._playerinfos) > 1:
 692            pstr = bs.Lstr(
 693                value='- ${A} -',
 694                subs=[
 695                    (
 696                        '${A}',
 697                        bs.Lstr(
 698                            resource='multiPlayerCountText',
 699                            subs=[('${COUNT}', str(len(self._playerinfos)))],
 700                        ),
 701                    )
 702                ],
 703            )
 704        else:
 705            pstr = bs.Lstr(
 706                value='- ${A} -',
 707                subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))],
 708            )
 709        ZoomText(
 710            self._campaign.getlevel(self._level_name).displayname,
 711            maxwidth=800,
 712            flash=False,
 713            trail=False,
 714            color=(0.5, 1, 0.5, 1),
 715            h_align='center',
 716            scale=0.4,
 717            position=(0, 292),
 718            jitter=1.0,
 719        ).autoretain()
 720        Text(
 721            pstr,
 722            maxwidth=300,
 723            transition=Text.Transition.FADE_IN,
 724            scale=0.7,
 725            h_align=Text.HAlign.CENTER,
 726            v_align=Text.VAlign.CENTER,
 727            color=(0.5, 0.7, 0.5, 1),
 728            position=(0, 230),
 729        ).autoretain()
 730
 731        if app.classic is not None and app.classic.server is None:
 732            # If we're running in normal non-headless build, show this text
 733            # because only host can continue the game.
 734            adisp = plus.get_v1_account_display_string()
 735            txt = Text(
 736                bs.Lstr(
 737                    resource='waitingForHostText', subs=[('${HOST}', adisp)]
 738                ),
 739                maxwidth=300,
 740                transition=Text.Transition.FADE_IN,
 741                transition_delay=8.0,
 742                scale=0.85,
 743                h_align=Text.HAlign.CENTER,
 744                v_align=Text.VAlign.CENTER,
 745                color=(1, 1, 0, 1),
 746                position=(0, -230),
 747            ).autoretain()
 748            assert txt.node
 749            txt.node.client_only = True
 750        else:
 751            # In headless build, anyone can continue the game.
 752            sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
 753            Text(
 754                sval,
 755                v_attach=Text.VAttach.BOTTOM,
 756                h_align=Text.HAlign.CENTER,
 757                flash=True,
 758                vr_depth=50,
 759                position=(0, 60),
 760                scale=0.8,
 761                color=(0.5, 0.7, 0.5, 0.5),
 762                transition=Text.Transition.IN_BOTTOM_SLOW,
 763                transition_delay=self._min_view_time,
 764            ).autoretain()
 765
 766        if self._score is not None:
 767            bs.timer(0.35, self._score_display_sound_small.play)
 768
 769        # Vestigial remain; this stuff should just be instance vars.
 770        self._show_info = {}
 771
 772        if self._score is not None:
 773            bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x))
 774        else:
 775            bs.pushcall(bs.WeakCall(self._show_fail))
 776
 777        self._name_str = name_str = ', '.join(
 778            [p.name for p in self._playerinfos]
 779        )
 780
 781        self._score_loading_status = Text(
 782            bs.Lstr(
 783                value='${A}...',
 784                subs=[('${A}', bs.Lstr(resource='loadingText'))],
 785            ),
 786            position=(280, 150 + 30),
 787            color=(1, 1, 1, 0.4),
 788            transition=Text.Transition.FADE_IN,
 789            scale=0.7,
 790            transition_delay=2.0,
 791        )
 792
 793        if self._score is not None and self._submit_score:
 794            bs.timer(0.4, bs.WeakCall(self._play_drumroll))
 795
 796        # Add us to high scores, filter, and store.
 797        our_high_scores_all = self._campaign.getlevel(
 798            self._level_name
 799        ).get_high_scores()
 800
 801        our_high_scores = our_high_scores_all.setdefault(
 802            str(len(self._playerinfos)) + ' Player', []
 803        )
 804
 805        if self._score is not None:
 806            our_score: list | None = [
 807                self._score,
 808                {
 809                    'players': [
 810                        {'name': p.name, 'character': p.character}
 811                        for p in self._playerinfos
 812                    ]
 813                },
 814            ]
 815            our_high_scores.append(our_score)
 816        else:
 817            our_score = None
 818
 819        try:
 820            our_high_scores.sort(
 821                reverse=self._score_order == 'increasing', key=lambda x: x[0]
 822            )
 823        except Exception:
 824            logging.exception('Error sorting scores.')
 825            print(f'our_high_scores: {our_high_scores}')
 826
 827        del our_high_scores[10:]
 828
 829        if self._score is not None:
 830            sver = self._campaign.getlevel(
 831                self._level_name
 832            ).get_score_version_string()
 833            plus.add_v1_account_transaction(
 834                {
 835                    'type': 'SET_LEVEL_LOCAL_HIGH_SCORES',
 836                    'campaign': self._campaign.name,
 837                    'level': self._level_name,
 838                    'scoreVersion': sver,
 839                    'scores': our_high_scores_all,
 840                }
 841            )
 842        if plus.get_v1_account_state() != 'signed_in':
 843            # We expect this only in kiosk mode; complain otherwise.
 844            if not (env.demo or env.arcade):
 845                logging.error('got not-signed-in at score-submit; unexpected')
 846            bs.pushcall(bs.WeakCall(self._got_score_results, None))
 847        else:
 848            assert self._game_name_str is not None
 849            assert self._game_config_str is not None
 850            plus.submit_score(
 851                self._game_name_str,
 852                self._game_config_str,
 853                name_str,
 854                self._score,
 855                bs.WeakCall(self._got_score_results),
 856                order=self._score_order,
 857                tournament_id=self.session.tournament_id,
 858                score_type=self._score_type,
 859                campaign=self._campaign.name,
 860                level=self._level_name,
 861            )
 862
 863        # Apply the transactions we've been adding locally.
 864        plus.run_v1_account_transactions()
 865
 866        # If we're not doing the world's-best button, just show a title
 867        # instead.
 868        ts_height = 300
 869        ts_h_offs = 210
 870        v_offs = 40
 871        txt = Text(
 872            (
 873                bs.Lstr(resource='tournamentStandingsText')
 874                if self.session.tournament_id is not None
 875                else (
 876                    bs.Lstr(resource='worldsBestScoresText')
 877                    if self._score_type == 'points'
 878                    else bs.Lstr(resource='worldsBestTimesText')
 879                )
 880            ),
 881            maxwidth=210,
 882            position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
 883            transition=Text.Transition.IN_LEFT,
 884            v_align=Text.VAlign.CENTER,
 885            scale=1.2,
 886            transition_delay=2.2,
 887        ).autoretain()
 888
 889        # If we've got a button on the server, only show this on clients.
 890        if self._should_show_worlds_best_button():
 891            assert txt.node
 892            txt.node.client_only = True
 893
 894        ts_height = 300
 895        ts_h_offs = -480
 896        v_offs = 40
 897        Text(
 898            (
 899                bs.Lstr(resource='yourBestScoresText')
 900                if self._score_type == 'points'
 901                else bs.Lstr(resource='yourBestTimesText')
 902            ),
 903            maxwidth=210,
 904            position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
 905            transition=Text.Transition.IN_RIGHT,
 906            v_align=Text.VAlign.CENTER,
 907            scale=1.2,
 908            transition_delay=1.8,
 909        ).autoretain()
 910
 911        display_scores = list(our_high_scores)
 912        display_count = 5
 913
 914        while len(display_scores) < display_count:
 915            display_scores.append((0, None))
 916
 917        showed_ours = False
 918        h_offs_extra = 85 if self._score_type == 'points' else 130
 919        v_offs_extra = 20
 920        v_offs_names = 0
 921        scale = 1.0
 922        p_count = len(self._playerinfos)
 923        h_offs_extra -= 75
 924        if p_count > 1:
 925            h_offs_extra -= 20
 926        if p_count == 2:
 927            scale = 0.9
 928        elif p_count == 3:
 929            scale = 0.65
 930        elif p_count == 4:
 931            scale = 0.5
 932        times: list[tuple[float, float]] = []
 933        for i in range(display_count):
 934            times.insert(
 935                random.randrange(0, len(times) + 1),
 936                (1.9 + i * 0.05, 2.3 + i * 0.05),
 937            )
 938        for i in range(display_count):
 939            try:
 940                if display_scores[i][1] is None:
 941                    name_str = '-'
 942                else:
 943                    # noinspection PyUnresolvedReferences
 944                    name_str = ', '.join(
 945                        [p['name'] for p in display_scores[i][1]['players']]
 946                    )
 947            except Exception:
 948                logging.exception(
 949                    'Error calcing name_str for %s.', display_scores
 950                )
 951                name_str = '-'
 952            if display_scores[i] == our_score and not showed_ours:
 953                flash = True
 954                color0 = (0.6, 0.4, 0.1, 1.0)
 955                color1 = (0.6, 0.6, 0.6, 1.0)
 956                tdelay1 = 3.7
 957                tdelay2 = 3.7
 958                showed_ours = True
 959            else:
 960                flash = False
 961                color0 = (0.6, 0.4, 0.1, 1.0)
 962                color1 = (0.6, 0.6, 0.6, 1.0)
 963                tdelay1 = times[i][0]
 964                tdelay2 = times[i][1]
 965            Text(
 966                (
 967                    str(display_scores[i][0])
 968                    if self._score_type == 'points'
 969                    else bs.timestring((display_scores[i][0] * 10) / 1000.0)
 970                ),
 971                position=(
 972                    ts_h_offs + 20 + h_offs_extra,
 973                    v_offs_extra
 974                    + ts_height / 2
 975                    + -ts_height * (i + 1) / 10
 976                    + v_offs
 977                    + 11.0,
 978                ),
 979                h_align=Text.HAlign.RIGHT,
 980                v_align=Text.VAlign.CENTER,
 981                color=color0,
 982                flash=flash,
 983                transition=Text.Transition.IN_RIGHT,
 984                transition_delay=tdelay1,
 985            ).autoretain()
 986
 987            Text(
 988                bs.Lstr(value=name_str),
 989                position=(
 990                    ts_h_offs + 35 + h_offs_extra,
 991                    v_offs_extra
 992                    + ts_height / 2
 993                    + -ts_height * (i + 1) / 10
 994                    + v_offs_names
 995                    + v_offs
 996                    + 11.0,
 997                ),
 998                maxwidth=80.0 + 100.0 * len(self._playerinfos),
 999                v_align=Text.VAlign.CENTER,
1000                color=color1,
1001                flash=flash,
1002                scale=scale,
1003                transition=Text.Transition.IN_RIGHT,
1004                transition_delay=tdelay2,
1005            ).autoretain()
1006
1007        # Show achievements for this level.
1008        ts_height = -150
1009        ts_h_offs = -480
1010        v_offs = 40
1011
1012        # Only make this if we don't have the button
1013        # (never want clients to see it so no need for client-only
1014        # version, etc).
1015        if self._have_achievements:
1016            if not self._account_has_achievements:
1017                Text(
1018                    bs.Lstr(resource='achievementsText'),
1019                    position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3),
1020                    maxwidth=210,
1021                    host_only=True,
1022                    transition=Text.Transition.IN_RIGHT,
1023                    v_align=Text.VAlign.CENTER,
1024                    scale=1.2,
1025                    transition_delay=2.8,
1026                ).autoretain()
1027
1028            assert self._game_name_str is not None
1029            assert bs.app.classic is not None
1030            achievements = bs.app.classic.ach.achievements_for_coop_level(
1031                self._game_name_str
1032            )
1033            hval = -455
1034            vval = -100
1035            tdelay = 0.0
1036            for ach in achievements:
1037                ach.create_display(hval, vval + v_offs, 3.0 + tdelay)
1038                vval -= 55
1039                tdelay += 0.250
1040
1041        bs.timer(5.0, bs.WeakCall(self._show_tips))
1042
1043    def _play_drumroll(self) -> None:
1044        bs.NodeActor(
1045            bs.newnode(
1046                'sound',
1047                attrs={
1048                    'sound': self.drum_roll_sound,
1049                    'positional': False,
1050                    'loop': False,
1051                },
1052            )
1053        ).autoretain()
1054
1055    def _got_friend_score_results(self, results: list[Any] | None) -> None:
1056        # FIXME: tidy this up
1057        # pylint: disable=too-many-locals
1058        # pylint: disable=too-many-branches
1059        # pylint: disable=too-many-statements
1060        from efro.util import asserttype
1061
1062        # delay a bit if results come in too fast
1063        assert self._begin_time is not None
1064        base_delay = max(0, 1.9 - (bs.time() - self._begin_time))
1065        ts_height = 300
1066        ts_h_offs = -550
1067        v_offs = 30
1068
1069        # Report in case of error.
1070        if results is None:
1071            self._friends_loading_status = Text(
1072                bs.Lstr(resource='friendScoresUnavailableText'),
1073                maxwidth=330,
1074                position=(-475, 150 + v_offs),
1075                color=(1, 1, 1, 0.4),
1076                transition=Text.Transition.FADE_IN,
1077                transition_delay=base_delay + 0.8,
1078                scale=0.7,
1079            )
1080            return
1081
1082        self._friends_loading_status = None
1083
1084        # Ok, it looks like we aren't able to reliably get a just-submitted
1085        # result returned in the score list, so we need to look for our score
1086        # in this list and replace it if ours is better or add ours otherwise.
1087        if self._score is not None:
1088            our_score_entry = [self._score, 'Me', True]
1089            for score in results:
1090                if score[2]:
1091                    if self._score_order == 'increasing':
1092                        our_score_entry[0] = max(score[0], self._score)
1093                    else:
1094                        our_score_entry[0] = min(score[0], self._score)
1095                    results.remove(score)
1096                    break
1097            results.append(our_score_entry)
1098            results.sort(
1099                reverse=self._score_order == 'increasing',
1100                key=lambda x: asserttype(x[0], int),
1101            )
1102
1103        # If we're not submitting our own score, we still want to change the
1104        # name of our own score to 'Me'.
1105        else:
1106            for score in results:
1107                if score[2]:
1108                    score[1] = 'Me'
1109                    break
1110        h_offs_extra = 80 if self._score_type == 'points' else 130
1111        v_offs_extra = 20
1112        v_offs_names = 0
1113        scale = 1.0
1114
1115        # Make sure there's at least 5.
1116        while len(results) < 5:
1117            results.append([0, '-', False])
1118        results = results[:5]
1119        times: list[tuple[float, float]] = []
1120        for i in range(len(results)):
1121            times.insert(
1122                random.randrange(0, len(times) + 1),
1123                (base_delay + i * 0.05, base_delay + 0.3 + i * 0.05),
1124            )
1125        for i, tval in enumerate(results):
1126            score = int(tval[0])
1127            name_str = tval[1]
1128            is_me = tval[2]
1129            if is_me and score == self._score:
1130                flash = True
1131                color0 = (0.6, 0.4, 0.1, 1.0)
1132                color1 = (0.6, 0.6, 0.6, 1.0)
1133                tdelay1 = base_delay + 1.0
1134                tdelay2 = base_delay + 1.0
1135            else:
1136                flash = False
1137                if is_me:
1138                    color0 = (0.6, 0.4, 0.1, 1.0)
1139                    color1 = (0.9, 1.0, 0.9, 1.0)
1140                else:
1141                    color0 = (0.6, 0.4, 0.1, 1.0)
1142                    color1 = (0.6, 0.6, 0.6, 1.0)
1143                tdelay1 = times[i][0]
1144                tdelay2 = times[i][1]
1145            if name_str != '-':
1146                Text(
1147                    (
1148                        str(score)
1149                        if self._score_type == 'points'
1150                        else bs.timestring((score * 10) / 1000.0)
1151                    ),
1152                    position=(
1153                        ts_h_offs + 20 + h_offs_extra,
1154                        v_offs_extra
1155                        + ts_height / 2
1156                        + -ts_height * (i + 1) / 10
1157                        + v_offs
1158                        + 11.0,
1159                    ),
1160                    h_align=Text.HAlign.RIGHT,
1161                    v_align=Text.VAlign.CENTER,
1162                    color=color0,
1163                    flash=flash,
1164                    transition=Text.Transition.IN_RIGHT,
1165                    transition_delay=tdelay1,
1166                ).autoretain()
1167            else:
1168                if is_me:
1169                    print('Error: got empty name_str on score result:', tval)
1170
1171            Text(
1172                bs.Lstr(value=name_str),
1173                position=(
1174                    ts_h_offs + 35 + h_offs_extra,
1175                    v_offs_extra
1176                    + ts_height / 2
1177                    + -ts_height * (i + 1) / 10
1178                    + v_offs_names
1179                    + v_offs
1180                    + 11.0,
1181                ),
1182                color=color1,
1183                maxwidth=160.0,
1184                v_align=Text.VAlign.CENTER,
1185                flash=flash,
1186                scale=scale,
1187                transition=Text.Transition.IN_RIGHT,
1188                transition_delay=tdelay2,
1189            ).autoretain()
1190
1191    def _got_score_results(self, results: dict[str, Any] | None) -> None:
1192        # FIXME: tidy this up
1193        # pylint: disable=too-many-locals
1194        # pylint: disable=too-many-branches
1195        # pylint: disable=too-many-statements
1196
1197        plus = bs.app.plus
1198        assert plus is not None
1199
1200        # We need to manually run this in the context of our activity
1201        # and only if we aren't shutting down.
1202        # (really should make the submit_score call handle that stuff itself)
1203        if self.expired:
1204            return
1205        with self.context:
1206            # Delay a bit if results come in too fast.
1207            assert self._begin_time is not None
1208            base_delay = max(0, 2.7 - (bs.time() - self._begin_time))
1209            v_offs = 20
1210            if results is None:
1211                self._score_loading_status = Text(
1212                    bs.Lstr(resource='worldScoresUnavailableText'),
1213                    position=(230, 150 + v_offs),
1214                    color=(1, 1, 1, 0.4),
1215                    transition=Text.Transition.FADE_IN,
1216                    transition_delay=base_delay + 0.3,
1217                    scale=0.7,
1218                )
1219            else:
1220                self._score_link = results['link']
1221                assert self._score_link is not None
1222                # Prepend our master-server addr if its a relative addr.
1223                if not self._score_link.startswith(
1224                    'http://'
1225                ) and not self._score_link.startswith('https://'):
1226                    self._score_link = (
1227                        plus.get_master_server_address()
1228                        + '/'
1229                        + self._score_link
1230                    )
1231                self._score_loading_status = None
1232                if 'tournamentSecondsRemaining' in results:
1233                    secs_remaining = results['tournamentSecondsRemaining']
1234                    assert isinstance(secs_remaining, int)
1235                    self._tournament_time_remaining = secs_remaining
1236                    self._tournament_time_remaining_text_timer = bs.BaseTimer(
1237                        1.0,
1238                        bs.WeakCall(
1239                            self._update_tournament_time_remaining_text
1240                        ),
1241                        repeat=True,
1242                    )
1243
1244            assert self._show_info is not None
1245            self._show_info['results'] = results
1246            if results is not None:
1247                if results['tops'] != '':
1248                    self._show_info['tops'] = results['tops']
1249                else:
1250                    self._show_info['tops'] = []
1251            offs_x = -195
1252            available = self._show_info['results'] is not None
1253            if self._score is not None:
1254                bs.basetimer(
1255                    (1.5 + base_delay),
1256                    bs.WeakCall(self._show_world_rank, offs_x),
1257                )
1258            ts_h_offs = 200
1259            ts_height = 300
1260
1261            # Show world tops.
1262            if available:
1263                # Show the number of games represented by this
1264                # list (except for in tournaments).
1265                if self.session.tournament_id is None:
1266                    Text(
1267                        bs.Lstr(
1268                            resource='lastGamesText',
1269                            subs=[
1270                                (
1271                                    '${COUNT}',
1272                                    str(self._show_info['results']['total']),
1273                                )
1274                            ],
1275                        ),
1276                        position=(
1277                            ts_h_offs - 35 + 95,
1278                            ts_height / 2 + 6 + v_offs,
1279                        ),
1280                        color=(0.4, 0.4, 0.4, 1.0),
1281                        scale=0.7,
1282                        transition=Text.Transition.IN_RIGHT,
1283                        transition_delay=base_delay + 0.3,
1284                    ).autoretain()
1285                else:
1286                    v_offs += 20
1287
1288                h_offs_extra = 0
1289                v_offs_names = 0
1290                scale = 1.0
1291                p_count = len(self._playerinfos)
1292                if p_count > 1:
1293                    h_offs_extra -= 40
1294                if self._score_type != 'points':
1295                    h_offs_extra += 60
1296                if p_count == 2:
1297                    scale = 0.9
1298                elif p_count == 3:
1299                    scale = 0.65
1300                elif p_count == 4:
1301                    scale = 0.5
1302
1303                # Make sure there's at least 10.
1304                while len(self._show_info['tops']) < 10:
1305                    self._show_info['tops'].append([0, '-'])
1306
1307                times: list[tuple[float, float]] = []
1308                for i in range(len(self._show_info['tops'])):
1309                    times.insert(
1310                        random.randrange(0, len(times) + 1),
1311                        (base_delay + i * 0.05, base_delay + 0.4 + i * 0.05),
1312                    )
1313                for i, tval in enumerate(self._show_info['tops']):
1314                    score = int(tval[0])
1315                    name_str = tval[1]
1316                    if self._name_str == name_str and self._score == score:
1317                        flash = True
1318                        color0 = (0.6, 0.4, 0.1, 1.0)
1319                        color1 = (0.6, 0.6, 0.6, 1.0)
1320                        tdelay1 = base_delay + 1.0
1321                        tdelay2 = base_delay + 1.0
1322                    else:
1323                        flash = False
1324                        if self._name_str == name_str:
1325                            color0 = (0.6, 0.4, 0.1, 1.0)
1326                            color1 = (0.9, 1.0, 0.9, 1.0)
1327                        else:
1328                            color0 = (0.6, 0.4, 0.1, 1.0)
1329                            color1 = (0.6, 0.6, 0.6, 1.0)
1330                        tdelay1 = times[i][0]
1331                        tdelay2 = times[i][1]
1332
1333                    if name_str != '-':
1334                        Text(
1335                            (
1336                                str(score)
1337                                if self._score_type == 'points'
1338                                else bs.timestring((score * 10) / 1000.0)
1339                            ),
1340                            position=(
1341                                ts_h_offs + 20 + h_offs_extra,
1342                                ts_height / 2
1343                                + -ts_height * (i + 1) / 10
1344                                + v_offs
1345                                + 11.0,
1346                            ),
1347                            h_align=Text.HAlign.RIGHT,
1348                            v_align=Text.VAlign.CENTER,
1349                            color=color0,
1350                            flash=flash,
1351                            transition=Text.Transition.IN_LEFT,
1352                            transition_delay=tdelay1,
1353                        ).autoretain()
1354                    Text(
1355                        bs.Lstr(value=name_str),
1356                        position=(
1357                            ts_h_offs + 35 + h_offs_extra,
1358                            ts_height / 2
1359                            + -ts_height * (i + 1) / 10
1360                            + v_offs_names
1361                            + v_offs
1362                            + 11.0,
1363                        ),
1364                        maxwidth=80.0 + 100.0 * len(self._playerinfos),
1365                        v_align=Text.VAlign.CENTER,
1366                        color=color1,
1367                        flash=flash,
1368                        scale=scale,
1369                        transition=Text.Transition.IN_LEFT,
1370                        transition_delay=tdelay2,
1371                    ).autoretain()
1372
1373    def _show_tips(self) -> None:
1374        from bascenev1lib.actor.tipstext import TipsText
1375
1376        TipsText(offs_y=30).autoretain()
1377
1378    def _update_tournament_time_remaining_text(self) -> None:
1379        if self._tournament_time_remaining is None:
1380            return
1381        self._tournament_time_remaining = max(
1382            0, self._tournament_time_remaining - 1
1383        )
1384        if self._tournament_time_remaining_text is not None:
1385            val = bs.timestring(
1386                self._tournament_time_remaining,
1387                centi=False,
1388            )
1389            self._tournament_time_remaining_text.node.text = val
1390
1391    def _show_world_rank(self, offs_x: float) -> None:
1392        # FIXME: Tidy this up.
1393        # pylint: disable=too-many-locals
1394        # pylint: disable=too-many-branches
1395        # pylint: disable=too-many-statements
1396        assert bs.app.classic is not None
1397        assert self._show_info is not None
1398        available = self._show_info['results'] is not None
1399
1400        if available and self._submit_score:
1401            error = (
1402                self._show_info['results']['error']
1403                if 'error' in self._show_info['results']
1404                else None
1405            )
1406            rank = self._show_info['results']['rank']
1407            total = self._show_info['results']['total']
1408            rating = (
1409                10.0
1410                if total == 1
1411                else 10.0 * (1.0 - (float(rank - 1) / (total - 1)))
1412            )
1413            player_rank = self._show_info['results']['playerRank']
1414            best_player_rank = self._show_info['results']['bestPlayerRank']
1415        else:
1416            error = False
1417            rating = None
1418            player_rank = None
1419            best_player_rank = None
1420
1421        # If we've got tournament-seconds-remaining, show it.
1422        if self._tournament_time_remaining is not None:
1423            Text(
1424                bs.Lstr(resource='coopSelectWindow.timeRemainingText'),
1425                position=(-360, -70 - 100),
1426                color=(1, 1, 1, 0.7),
1427                h_align=Text.HAlign.CENTER,
1428                v_align=Text.VAlign.CENTER,
1429                transition=Text.Transition.FADE_IN,
1430                scale=0.8,
1431                maxwidth=300,
1432                transition_delay=2.0,
1433            ).autoretain()
1434            self._tournament_time_remaining_text = Text(
1435                '',
1436                position=(-360, -110 - 100),
1437                color=(1, 1, 1, 0.7),
1438                h_align=Text.HAlign.CENTER,
1439                v_align=Text.VAlign.CENTER,
1440                transition=Text.Transition.FADE_IN,
1441                scale=1.6,
1442                maxwidth=150,
1443                transition_delay=2.0,
1444            )
1445
1446        # If we're a tournament, show prizes.
1447        try:
1448            assert bs.app.classic is not None
1449            tournament_id = self.session.tournament_id
1450            if tournament_id is not None:
1451                if tournament_id in bs.app.classic.accounts.tournament_info:
1452                    tourney_info = bs.app.classic.accounts.tournament_info[
1453                        tournament_id
1454                    ]
1455                    # pylint: disable=useless-suppression
1456                    # pylint: disable=unbalanced-tuple-unpacking
1457                    (
1458                        pr1,
1459                        pv1,
1460                        pr2,
1461                        pv2,
1462                        pr3,
1463                        pv3,
1464                    ) = bs.app.classic.get_tournament_prize_strings(
1465                        tourney_info
1466                    )
1467                    # pylint: enable=unbalanced-tuple-unpacking
1468                    # pylint: enable=useless-suppression
1469
1470                    Text(
1471                        bs.Lstr(resource='coopSelectWindow.prizesText'),
1472                        position=(-360, -70 + 77),
1473                        color=(1, 1, 1, 0.7),
1474                        h_align=Text.HAlign.CENTER,
1475                        v_align=Text.VAlign.CENTER,
1476                        transition=Text.Transition.FADE_IN,
1477                        scale=1.0,
1478                        maxwidth=300,
1479                        transition_delay=2.0,
1480                    ).autoretain()
1481                    vval = -107 + 70
1482                    for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)):
1483                        Text(
1484                            rng,
1485                            position=(-410 + 10, vval),
1486                            color=(1, 1, 1, 0.7),
1487                            h_align=Text.HAlign.RIGHT,
1488                            v_align=Text.VAlign.CENTER,
1489                            transition=Text.Transition.FADE_IN,
1490                            scale=0.6,
1491                            maxwidth=300,
1492                            transition_delay=2.0,
1493                        ).autoretain()
1494                        Text(
1495                            val,
1496                            position=(-390 + 10, vval),
1497                            color=(0.7, 0.7, 0.7, 1.0),
1498                            h_align=Text.HAlign.LEFT,
1499                            v_align=Text.VAlign.CENTER,
1500                            transition=Text.Transition.FADE_IN,
1501                            scale=0.8,
1502                            maxwidth=300,
1503                            transition_delay=2.0,
1504                        ).autoretain()
1505                        vval -= 35
1506        except Exception:
1507            logging.exception('Error showing prize ranges.')
1508
1509        if self._do_new_rating:
1510            if error:
1511                ZoomText(
1512                    bs.Lstr(resource='failText'),
1513                    flash=True,
1514                    trail=True,
1515                    scale=1.0 if available else 0.333,
1516                    tilt_translate=0.11,
1517                    h_align='center',
1518                    position=(190 + offs_x, -60),
1519                    maxwidth=200,
1520                    jitter=1.0,
1521                ).autoretain()
1522                Text(
1523                    bs.Lstr(translate=('serverResponses', error)),
1524                    position=(0, -140),
1525                    color=(1, 1, 1, 0.7),
1526                    h_align=Text.HAlign.CENTER,
1527                    v_align=Text.VAlign.CENTER,
1528                    transition=Text.Transition.FADE_IN,
1529                    scale=0.9,
1530                    maxwidth=400,
1531                    transition_delay=1.0,
1532                ).autoretain()
1533            elif self._submit_score:
1534                ZoomText(
1535                    (
1536                        ('#' + str(player_rank))
1537                        if player_rank is not None
1538                        else bs.Lstr(resource='unavailableText')
1539                    ),
1540                    flash=True,
1541                    trail=True,
1542                    scale=1.0 if available else 0.333,
1543                    tilt_translate=0.11,
1544                    h_align='center',
1545                    position=(190 + offs_x, -60),
1546                    maxwidth=200,
1547                    jitter=1.0,
1548                ).autoretain()
1549
1550                Text(
1551                    bs.Lstr(
1552                        value='${A}:',
1553                        subs=[('${A}', bs.Lstr(resource='rankText'))],
1554                    ),
1555                    position=(0, 36),
1556                    maxwidth=300,
1557                    transition=Text.Transition.FADE_IN,
1558                    h_align=Text.HAlign.CENTER,
1559                    v_align=Text.VAlign.CENTER,
1560                    transition_delay=0,
1561                ).autoretain()
1562                if best_player_rank is not None:
1563                    Text(
1564                        bs.Lstr(
1565                            resource='currentStandingText',
1566                            fallback_resource='bestRankText',
1567                            subs=[('${RANK}', str(best_player_rank))],
1568                        ),
1569                        position=(0, -155),
1570                        color=(1, 1, 1, 0.7),
1571                        h_align=Text.HAlign.CENTER,
1572                        transition=Text.Transition.FADE_IN,
1573                        scale=0.7,
1574                        transition_delay=1.0,
1575                    ).autoretain()
1576        else:
1577            ZoomText(
1578                (
1579                    f'{rating:.1f}'
1580                    if available
1581                    else bs.Lstr(resource='unavailableText')
1582                ),
1583                flash=True,
1584                trail=True,
1585                scale=0.6 if available else 0.333,
1586                tilt_translate=0.11,
1587                h_align='center',
1588                position=(190 + offs_x, -94),
1589                maxwidth=200,
1590                jitter=1.0,
1591            ).autoretain()
1592
1593            if available:
1594                if rating >= 9.5:
1595                    stars = 3
1596                elif rating >= 7.5:
1597                    stars = 2
1598                elif rating > 0.0:
1599                    stars = 1
1600                else:
1601                    stars = 0
1602                star_tex = bs.gettexture('star')
1603                star_x = 135 + offs_x
1604                for _i in range(stars):
1605                    img = bs.NodeActor(
1606                        bs.newnode(
1607                            'image',
1608                            attrs={
1609                                'texture': star_tex,
1610                                'position': (star_x, -16),
1611                                'scale': (62, 62),
1612                                'opacity': 1.0,
1613                                'color': (2.2, 1.2, 0.3),
1614                                'absolute_scale': True,
1615                            },
1616                        )
1617                    ).autoretain()
1618
1619                    assert img.node
1620                    bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1})
1621                    star_x += 60
1622                for _i in range(3 - stars):
1623                    img = bs.NodeActor(
1624                        bs.newnode(
1625                            'image',
1626                            attrs={
1627                                'texture': star_tex,
1628                                'position': (star_x, -16),
1629                                'scale': (62, 62),
1630                                'opacity': 1.0,
1631                                'color': (0.3, 0.3, 0.3),
1632                                'absolute_scale': True,
1633                            },
1634                        )
1635                    ).autoretain()
1636                    assert img.node
1637                    bs.animate(img.node, 'opacity', {0.15: 0, 0.4: 1})
1638                    star_x += 60
1639
1640                def dostar(
1641                    count: int, xval: float, offs_y: float, score: str
1642                ) -> None:
1643                    Text(
1644                        score + ' =',
1645                        position=(xval, -64 + offs_y),
1646                        color=(0.6, 0.6, 0.6, 0.6),
1647                        h_align=Text.HAlign.CENTER,
1648                        v_align=Text.VAlign.CENTER,
1649                        transition=Text.Transition.FADE_IN,
1650                        scale=0.4,
1651                        transition_delay=1.0,
1652                    ).autoretain()
1653                    stx = xval + 20
1654                    for _i2 in range(count):
1655                        img2 = bs.NodeActor(
1656                            bs.newnode(
1657                                'image',
1658                                attrs={
1659                                    'texture': star_tex,
1660                                    'position': (stx, -64 + offs_y),
1661                                    'scale': (12, 12),
1662                                    'opacity': 0.7,
1663                                    'color': (2.2, 1.2, 0.3),
1664                                    'absolute_scale': True,
1665                                },
1666                            )
1667                        ).autoretain()
1668                        assert img2.node
1669                        bs.animate(img2.node, 'opacity', {1.0: 0.0, 1.5: 0.5})
1670                        stx += 13.0
1671
1672                dostar(1, -44 - 30, -112, '0.0')
1673                dostar(2, 10 - 30, -112, '7.5')
1674                dostar(3, 77 - 30, -112, '9.5')
1675            try:
1676                best_rank = self._campaign.getlevel(self._level_name).rating
1677            except Exception:
1678                best_rank = 0.0
1679
1680            if available:
1681                Text(
1682                    bs.Lstr(
1683                        resource='outOfText',
1684                        subs=[
1685                            (
1686                                '${RANK}',
1687                                str(int(self._show_info['results']['rank'])),
1688                            ),
1689                            (
1690                                '${ALL}',
1691                                str(self._show_info['results']['total']),
1692                            ),
1693                        ],
1694                    ),
1695                    position=(0, -155 if self._newly_complete else -145),
1696                    color=(1, 1, 1, 0.7),
1697                    h_align=Text.HAlign.CENTER,
1698                    transition=Text.Transition.FADE_IN,
1699                    scale=0.55,
1700                    transition_delay=1.0,
1701                ).autoretain()
1702
1703            new_best = best_rank > self._old_best_rank and best_rank > 0.0
1704            was_string = bs.Lstr(
1705                value=' ${A}',
1706                subs=[
1707                    ('${A}', bs.Lstr(resource='scoreWasText')),
1708                    ('${COUNT}', str(self._old_best_rank)),
1709                ],
1710            )
1711            if not self._newly_complete:
1712                Text(
1713                    (
1714                        bs.Lstr(
1715                            value='${A}${B}',
1716                            subs=[
1717                                (
1718                                    '${A}',
1719                                    bs.Lstr(resource='newPersonalBestText'),
1720                                ),
1721                                ('${B}', was_string),
1722                            ],
1723                        )
1724                        if new_best
1725                        else bs.Lstr(
1726                            resource='bestRatingText',
1727                            subs=[('${RATING}', str(best_rank))],
1728                        )
1729                    ),
1730                    position=(0, -165),
1731                    color=(1, 1, 1, 0.7),
1732                    flash=new_best,
1733                    h_align=Text.HAlign.CENTER,
1734                    transition=(
1735                        Text.Transition.IN_RIGHT
1736                        if new_best
1737                        else Text.Transition.FADE_IN
1738                    ),
1739                    scale=0.5,
1740                    transition_delay=1.0,
1741                ).autoretain()
1742
1743            Text(
1744                bs.Lstr(
1745                    value='${A}:',
1746                    subs=[('${A}', bs.Lstr(resource='ratingText'))],
1747                ),
1748                position=(0, 36),
1749                maxwidth=300,
1750                transition=Text.Transition.FADE_IN,
1751                h_align=Text.HAlign.CENTER,
1752                v_align=Text.VAlign.CENTER,
1753                transition_delay=0,
1754            ).autoretain()
1755
1756        if self._submit_score:
1757            bs.timer(0.35, self._score_display_sound.play)
1758            if not error:
1759                bs.timer(0.35, self.cymbal_sound.play)
1760
1761    def _show_fail(self) -> None:
1762        ZoomText(
1763            bs.Lstr(resource='failText'),
1764            maxwidth=300,
1765            flash=False,
1766            trail=True,
1767            h_align='center',
1768            tilt_translate=0.11,
1769            position=(0, 40),
1770            jitter=1.0,
1771        ).autoretain()
1772        if self._fail_message is not None:
1773            Text(
1774                self._fail_message,
1775                h_align=Text.HAlign.CENTER,
1776                position=(0, -130),
1777                maxwidth=300,
1778                color=(1, 1, 1, 0.5),
1779                transition=Text.Transition.FADE_IN,
1780                transition_delay=1.0,
1781            ).autoretain()
1782        bs.timer(0.35, self._score_display_sound.play)
1783
1784    def _show_score_val(self, offs_x: float) -> None:
1785        assert self._score_type is not None
1786        assert self._score is not None
1787        ZoomText(
1788            (
1789                str(self._score)
1790                if self._score_type == 'points'
1791                else bs.timestring((self._score * 10) / 1000.0)
1792            ),
1793            maxwidth=300,
1794            flash=True,
1795            trail=True,
1796            scale=1.0 if self._score_type == 'points' else 0.6,
1797            h_align='center',
1798            tilt_translate=0.11,
1799            position=(190 + offs_x, 115),
1800            jitter=1.0,
1801        ).autoretain()
1802        Text(
1803            (
1804                bs.Lstr(
1805                    value='${A}:',
1806                    subs=[('${A}', bs.Lstr(resource='finalScoreText'))],
1807                )
1808                if self._score_type == 'points'
1809                else bs.Lstr(
1810                    value='${A}:',
1811                    subs=[('${A}', bs.Lstr(resource='finalTimeText'))],
1812                )
1813            ),
1814            maxwidth=300,
1815            position=(0, 200),
1816            transition=Text.Transition.FADE_IN,
1817            h_align=Text.HAlign.CENTER,
1818            v_align=Text.VAlign.CENTER,
1819            transition_delay=0,
1820        ).autoretain()
1821        bs.timer(0.35, self._score_display_sound.play)

Score screen showing the results of a cooperative game.

CoopScoreScreen(settings: dict)
 30    def __init__(self, settings: dict):
 31        # pylint: disable=too-many-statements
 32        super().__init__(settings)
 33
 34        plus = bs.app.plus
 35        assert plus is not None
 36
 37        # Keep prev activity alive while we fade in
 38        self.transition_time = 0.5
 39        self.inherits_tint = True
 40        self.inherits_vr_camera_offset = True
 41        self.inherits_music = True
 42        self.use_fixed_vr_overlay = True
 43
 44        self._do_new_rating: bool = self.session.tournament_id is not None
 45
 46        self._score_display_sound = bs.getsound('scoreHit01')
 47        self._score_display_sound_small = bs.getsound('scoreHit02')
 48        self.drum_roll_sound = bs.getsound('drumRoll')
 49        self.cymbal_sound = bs.getsound('cymbal')
 50
 51        self._replay_icon_texture = bui.gettexture('replayIcon')
 52        self._menu_icon_texture = bui.gettexture('menuIcon')
 53        self._next_level_icon_texture = bui.gettexture('nextLevelIcon')
 54
 55        self._campaign: bs.Campaign = settings['campaign']
 56
 57        self._have_achievements = (
 58            bs.app.classic is not None
 59            and bs.app.classic.ach.achievements_for_coop_level(
 60                self._campaign.name + ':' + settings['level']
 61            )
 62        )
 63
 64        self._game_service_icon_color: Sequence[float] | None
 65        self._game_service_achievements_texture: bui.Texture | None
 66        self._game_service_leaderboards_texture: bui.Texture | None
 67
 68        # Tie in to specific game services if they are active.
 69        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
 70        gpgs_active = adapter is not None and adapter.is_back_end_active()
 71        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
 72        game_center_active = (
 73            adapter is not None and adapter.is_back_end_active()
 74        )
 75
 76        if game_center_active:
 77            self._game_service_icon_color = (1.0, 1.0, 1.0)
 78            icon = bui.gettexture('gameCenterIcon')
 79            self._game_service_achievements_texture = icon
 80            self._game_service_leaderboards_texture = icon
 81            self._account_has_achievements = True
 82        elif gpgs_active:
 83            self._game_service_icon_color = (0.8, 1.0, 0.6)
 84            self._game_service_achievements_texture = bui.gettexture(
 85                'googlePlayAchievementsIcon'
 86            )
 87            self._game_service_leaderboards_texture = bui.gettexture(
 88                'googlePlayLeaderboardsIcon'
 89            )
 90            self._account_has_achievements = True
 91        else:
 92            self._game_service_icon_color = None
 93            self._game_service_achievements_texture = None
 94            self._game_service_leaderboards_texture = None
 95            self._account_has_achievements = False
 96
 97        self._cashregistersound = bs.getsound('cashRegister')
 98        self._gun_cocking_sound = bs.getsound('gunCocking')
 99        self._dingsound = bs.getsound('ding')
100        self._score_link: str | None = None
101        self._root_ui: bui.Widget | None = None
102        self._background: bs.Actor | None = None
103        self._old_best_rank = 0.0
104        self._game_name_str: str | None = None
105        self._game_config_str: str | None = None
106
107        # Ui bits.
108        self._corner_button_offs: tuple[float, float] | None = None
109        self._league_rank_button: LeagueRankButton | None = None
110        self._store_button_instance: StoreButton | None = None
111        self._restart_button: bui.Widget | None = None
112        self._update_corner_button_positions_timer: bui.AppTimer | None = None
113        self._next_level_error: bs.Actor | None = None
114
115        # Score/gameplay bits.
116        self._was_complete: bool | None = None
117        self._is_complete: bool | None = None
118        self._newly_complete: bool | None = None
119        self._is_more_levels: bool | None = None
120        self._next_level_name: str | None = None
121        self._show_info: dict[str, Any] | None = None
122        self._name_str: str | None = None
123        self._friends_loading_status: bs.Actor | None = None
124        self._score_loading_status: bs.Actor | None = None
125        self._tournament_time_remaining: float | None = None
126        self._tournament_time_remaining_text: Text | None = None
127        self._tournament_time_remaining_text_timer: bs.BaseTimer | None = None
128        self._submit_score = self.session.submit_score
129
130        # Stuff for activity skip by pressing button
131        self._birth_time = bs.time()
132        self._min_view_time = 5.0
133        self._allow_server_transition = False
134        self._server_transitioning: bool | None = None
135
136        self._playerinfos: list[bs.PlayerInfo] = settings['playerinfos']
137        assert isinstance(self._playerinfos, list)
138        assert all(isinstance(i, bs.PlayerInfo) for i in self._playerinfos)
139
140        self._score: int | None = settings['score']
141        assert isinstance(self._score, (int, type(None)))
142
143        self._fail_message: bs.Lstr | None = settings['fail_message']
144        assert isinstance(self._fail_message, (bs.Lstr, type(None)))
145
146        self._begin_time: float | None = None
147
148        self._score_order: str
149        if 'score_order' in settings:
150            if not settings['score_order'] in ['increasing', 'decreasing']:
151                raise ValueError(
152                    'Invalid score order: ' + settings['score_order']
153                )
154            self._score_order = settings['score_order']
155        else:
156            self._score_order = 'increasing'
157        assert isinstance(self._score_order, str)
158
159        self._score_type: str
160        if 'score_type' in settings:
161            if not settings['score_type'] in ['points', 'time']:
162                raise ValueError(
163                    'Invalid score type: ' + settings['score_type']
164                )
165            self._score_type = settings['score_type']
166        else:
167            self._score_type = 'points'
168        assert isinstance(self._score_type, str)
169
170        self._level_name: str = settings['level']
171        assert isinstance(self._level_name, str)
172
173        self._game_name_str = self._campaign.name + ':' + self._level_name
174        self._game_config_str = (
175            str(len(self._playerinfos))
176            + 'p'
177            + self._campaign.getlevel(self._level_name)
178            .get_score_version_string()
179            .replace(' ', '_')
180        )
181
182        try:
183            self._old_best_rank = self._campaign.getlevel(
184                self._level_name
185            ).rating
186        except Exception:
187            self._old_best_rank = 0.0
188
189        self._victory: bool = settings['outcome'] == 'victory'

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.

transition_time = 0.0

If the activity fades or transitions in, it should set the length of time here so that previous activities will be kept alive for that long (avoiding 'holes' in the screen) This value is given in real-time seconds.

inherits_tint = False

Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).

inherits_vr_camera_offset = False

Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).

inherits_music = False

Set this to True to keep playing the music from the previous activity (without even restarting it).

use_fixed_vr_overlay = False

In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.

drum_roll_sound
cymbal_sound
@override
def on_transition_in(self) -> None:
200    @override
201    def on_transition_in(self) -> None:
202        from bascenev1lib.actor import background  # FIXME NO BSSTD
203
204        bs.set_analytics_screen('Coop Score Screen')
205        super().on_transition_in()
206        self._background = background.Background(
207            fade_time=0.45, start_faded=False, show_logo=True
208        )

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

def request_ui(self) -> None:
338    def request_ui(self) -> None:
339        """Set up a callback to show our UI at the next opportune time."""
340        assert bui.app.classic is not None
341        # We don't want to just show our UI in case the user already has the
342        # main menu up, so instead we add a callback for when the menu
343        # closes; if we're still alive, we'll come up then.
344        # If there's no main menu this gets called immediately.
345        bui.app.ui_v1.add_main_menu_close_callback(bui.WeakCall(self.show_ui))

Set up a callback to show our UI at the next opportune time.

def show_ui(self) -> None:
347    def show_ui(self) -> None:
348        """Show the UI for restarting, playing the next Level, etc."""
349        # pylint: disable=too-many-locals
350        from bauiv1lib.store.button import StoreButton
351        from bauiv1lib.league.rankbutton import LeagueRankButton
352
353        assert bui.app.classic is not None
354
355        env = bui.app.env
356
357        delay = 0.7 if (self._score is not None) else 0.0
358
359        # If there's no players left in the game, lets not show the UI
360        # (that would allow restarting the game with zero players, etc).
361        if not self.players:
362            return
363
364        rootc = self._root_ui = bui.containerwidget(
365            size=(0, 0), transition='in_right'
366        )
367
368        h_offs = 7.0
369        v_offs = -280.0
370
371        # We wanna prevent controllers users from popping up browsers
372        # or game-center widgets in cases where they can't easily get back
373        # to the game (like on mac).
374        can_select_extra_buttons = bui.app.classic.platform == 'android'
375
376        bui.set_ui_input_device(None)  # Menu is up for grabs.
377
378        if self._have_achievements and self._account_has_achievements:
379            bui.buttonwidget(
380                parent=rootc,
381                color=(0.45, 0.4, 0.5),
382                position=(h_offs - 520, v_offs + 450 - 235 + 40),
383                size=(300, 60),
384                label=bui.Lstr(resource='achievementsText'),
385                on_activate_call=bui.WeakCall(self._ui_show_achievements),
386                transition_delay=delay + 1.5,
387                icon=self._game_service_achievements_texture,
388                icon_color=self._game_service_icon_color,
389                autoselect=True,
390                selectable=can_select_extra_buttons,
391            )
392
393        if self._should_show_worlds_best_button():
394            bui.buttonwidget(
395                parent=rootc,
396                color=(0.45, 0.4, 0.5),
397                position=(160, v_offs + 480),
398                size=(350, 62),
399                label=(
400                    bui.Lstr(resource='tournamentStandingsText')
401                    if self.session.tournament_id is not None
402                    else (
403                        bui.Lstr(resource='worldsBestScoresText')
404                        if self._score_type == 'points'
405                        else bui.Lstr(resource='worldsBestTimesText')
406                    )
407                ),
408                autoselect=True,
409                on_activate_call=bui.WeakCall(self._ui_worlds_best),
410                transition_delay=delay + 1.9,
411                selectable=can_select_extra_buttons,
412            )
413        else:
414            pass
415
416        show_next_button = self._is_more_levels and not (env.demo or env.arcade)
417
418        if not show_next_button:
419            h_offs += 70
420
421        menu_button = bui.buttonwidget(
422            parent=rootc,
423            autoselect=True,
424            position=(h_offs - 130 - 60, v_offs),
425            size=(110, 85),
426            label='',
427            on_activate_call=bui.WeakCall(self._ui_menu),
428        )
429        bui.imagewidget(
430            parent=rootc,
431            draw_controller=menu_button,
432            position=(h_offs - 130 - 60 + 22, v_offs + 14),
433            size=(60, 60),
434            texture=self._menu_icon_texture,
435            opacity=0.8,
436        )
437        self._restart_button = restart_button = bui.buttonwidget(
438            parent=rootc,
439            autoselect=True,
440            position=(h_offs - 60, v_offs),
441            size=(110, 85),
442            label='',
443            on_activate_call=bui.WeakCall(self._ui_restart),
444        )
445        bui.imagewidget(
446            parent=rootc,
447            draw_controller=restart_button,
448            position=(h_offs - 60 + 19, v_offs + 7),
449            size=(70, 70),
450            texture=self._replay_icon_texture,
451            opacity=0.8,
452        )
453
454        next_button: bui.Widget | None = None
455
456        # Our 'next' button is disabled if we haven't unlocked the next
457        # level yet and invisible if there is none.
458        if show_next_button:
459            if self._is_complete:
460                call = bui.WeakCall(self._ui_next)
461                button_sound = True
462                image_opacity = 0.8
463                color = None
464            else:
465                call = bui.WeakCall(self._ui_error)
466                button_sound = False
467                image_opacity = 0.2
468                color = (0.3, 0.3, 0.3)
469            next_button = bui.buttonwidget(
470                parent=rootc,
471                autoselect=True,
472                position=(h_offs + 130 - 60, v_offs),
473                size=(110, 85),
474                label='',
475                on_activate_call=call,
476                color=color,
477                enable_sound=button_sound,
478            )
479            bui.imagewidget(
480                parent=rootc,
481                draw_controller=next_button,
482                position=(h_offs + 130 - 60 + 12, v_offs + 5),
483                size=(80, 80),
484                texture=self._next_level_icon_texture,
485                opacity=image_opacity,
486            )
487
488        x_offs_extra = 0 if show_next_button else -100
489        self._corner_button_offs = (
490            h_offs + 300.0 + 100.0 + x_offs_extra,
491            v_offs + 560.0,
492        )
493
494        if env.demo or env.arcade:
495            self._league_rank_button = None
496            self._store_button_instance = None
497        else:
498            self._league_rank_button = LeagueRankButton(
499                parent=rootc,
500                position=(h_offs + 300 + 100 + x_offs_extra, v_offs + 560),
501                size=(100, 60),
502                scale=0.9,
503                color=(0.4, 0.4, 0.9),
504                textcolor=(0.9, 0.9, 2.0),
505                transition_delay=0.0,
506                smooth_update_delay=5.0,
507            )