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