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

Show the UI for restarting, playing the next Level, etc.

@override
def on_player_join(self, player: bascenev1.Player) -> None:
590    @override
591    def on_player_join(self, player: bs.Player) -> None:
592        super().on_player_join(player)
593
594        if bs.app.classic is not None and bs.app.classic.server is not None:
595            # Host can't press retry button, so anyone can do it instead.
596            time_till_assign = max(
597                0, self._birth_time + self._min_view_time - bs.time()
598            )
599
600            bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player))

Called when a new bascenev1.Player has joined the Activity.

(including the initial set of Players)

@override
def on_begin(self) -> None:
 602    @override
 603    def on_begin(self) -> None:
 604        # FIXME: Clean this up.
 605        # pylint: disable=too-many-statements
 606        # pylint: disable=too-many-branches
 607        # pylint: disable=too-many-locals
 608        super().on_begin()
 609
 610        app = bs.app
 611        env = app.env
 612        plus = app.plus
 613        assert plus is not None
 614
 615        self._begin_time = bs.time()
 616
 617        # Calc whether the level is complete and other stuff.
 618        levels = self._campaign.levels
 619        level = self._campaign.getlevel(self._level_name)
 620        self._was_complete = level.complete
 621        self._is_complete = self._was_complete or self._victory
 622        self._newly_complete = self._is_complete and not self._was_complete
 623        self._is_more_levels = (
 624            level.index < len(levels) - 1
 625        ) and self._campaign.sequential
 626
 627        # Any time we complete a level, set the next one as unlocked.
 628        if self._is_complete and self._is_more_levels:
 629            plus.add_v1_account_transaction(
 630                {
 631                    'type': 'COMPLETE_LEVEL',
 632                    'campaign': self._campaign.name,
 633                    'level': self._level_name,
 634                }
 635            )
 636            self._next_level_name = levels[level.index + 1].name
 637
 638            # If this is the first time we completed it, set the next one
 639            # as current.
 640            if self._newly_complete:
 641                cfg = app.config
 642                cfg['Selected Coop Game'] = (
 643                    self._campaign.name + ':' + self._next_level_name
 644                )
 645                cfg.commit()
 646                self._campaign.set_selected_level(self._next_level_name)
 647
 648        bs.timer(1.0, bs.WeakCall(self.request_ui))
 649
 650        if (
 651            self._is_complete
 652            and self._victory
 653            and self._is_more_levels
 654            and not (env.demo or env.arcade)
 655        ):
 656            Text(
 657                (
 658                    bs.Lstr(
 659                        value='${A}:\n',
 660                        subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))],
 661                    )
 662                    if self._newly_complete
 663                    else bs.Lstr(
 664                        value='${A}:\n',
 665                        subs=[('${A}', bs.Lstr(resource='nextLevelText'))],
 666                    )
 667                ),
 668                transition=Text.Transition.IN_RIGHT,
 669                transition_delay=5.2,
 670                flash=self._newly_complete,
 671                scale=0.54,
 672                h_align=Text.HAlign.CENTER,
 673                maxwidth=270,
 674                color=(0.5, 0.7, 0.5, 1),
 675                position=(270, -235),
 676            ).autoretain()
 677            assert self._next_level_name is not None
 678            Text(
 679                bs.Lstr(translate=('coopLevelNames', self._next_level_name)),
 680                transition=Text.Transition.IN_RIGHT,
 681                transition_delay=5.2,
 682                flash=self._newly_complete,
 683                scale=0.7,
 684                h_align=Text.HAlign.CENTER,
 685                maxwidth=205,
 686                color=(0.5, 0.7, 0.5, 1),
 687                position=(270, -255),
 688            ).autoretain()
 689            if self._newly_complete:
 690                bs.timer(5.2, self._cashregistersound.play)
 691                bs.timer(5.2, self._dingsound.play)
 692
 693        offs_x = -195
 694        if len(self._playerinfos) > 1:
 695            pstr = bs.Lstr(
 696                value='- ${A} -',
 697                subs=[
 698                    (
 699                        '${A}',
 700                        bs.Lstr(
 701                            resource='multiPlayerCountText',
 702                            subs=[('${COUNT}', str(len(self._playerinfos)))],
 703                        ),
 704                    )
 705                ],
 706            )
 707        else:
 708            pstr = bs.Lstr(
 709                value='- ${A} -',
 710                subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))],
 711            )
 712        ZoomText(
 713            self._campaign.getlevel(self._level_name).displayname,
 714            maxwidth=800,
 715            flash=False,
 716            trail=False,
 717            color=(0.5, 1, 0.5, 1),
 718            h_align='center',
 719            scale=0.4,
 720            position=(0, 292),
 721            jitter=1.0,
 722        ).autoretain()
 723        Text(
 724            pstr,
 725            maxwidth=300,
 726            transition=Text.Transition.FADE_IN,
 727            scale=0.7,
 728            h_align=Text.HAlign.CENTER,
 729            v_align=Text.VAlign.CENTER,
 730            color=(0.5, 0.7, 0.5, 1),
 731            position=(0, 230),
 732        ).autoretain()
 733
 734        if app.classic is not None and app.classic.server is None:
 735            # If we're running in normal non-headless build, show this text
 736            # because only host can continue the game.
 737            adisp = plus.get_v1_account_display_string()
 738            txt = Text(
 739                bs.Lstr(
 740                    resource='waitingForHostText', subs=[('${HOST}', adisp)]
 741                ),
 742                maxwidth=300,
 743                transition=Text.Transition.FADE_IN,
 744                transition_delay=8.0,
 745                scale=0.85,
 746                h_align=Text.HAlign.CENTER,
 747                v_align=Text.VAlign.CENTER,
 748                color=(1, 1, 0, 1),
 749                position=(0, -230),
 750            ).autoretain()
 751            assert txt.node
 752            txt.node.client_only = True
 753        else:
 754            # In headless build, anyone can continue the game.
 755            sval = bs.Lstr(resource='pressAnyButtonPlayAgainText')
 756            Text(
 757                sval,
 758                v_attach=Text.VAttach.BOTTOM,
 759                h_align=Text.HAlign.CENTER,
 760                flash=True,
 761                vr_depth=50,
 762                position=(0, 60),
 763                scale=0.8,
 764                color=(0.5, 0.7, 0.5, 0.5),
 765                transition=Text.Transition.IN_BOTTOM_SLOW,
 766                transition_delay=self._min_view_time,
 767            ).autoretain()
 768
 769        if self._score is not None:
 770            bs.timer(0.35, self._score_display_sound_small.play)
 771
 772        # Vestigial remain; this stuff should just be instance vars.
 773        self._show_info = {}
 774
 775        if self._score is not None:
 776            bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x))
 777        else:
 778            bs.pushcall(bs.WeakCall(self._show_fail))
 779
 780        self._name_str = name_str = ', '.join(
 781            [p.name for p in self._playerinfos]
 782        )
 783
 784        self._score_loading_status = Text(
 785            bs.Lstr(
 786                value='${A}...',
 787                subs=[('${A}', bs.Lstr(resource='loadingText'))],
 788            ),
 789            position=(280, 150 + 30),
 790            color=(1, 1, 1, 0.4),
 791            transition=Text.Transition.FADE_IN,
 792            scale=0.7,
 793            transition_delay=2.0,
 794        )
 795
 796        if self._score is not None and self._submit_score:
 797            bs.timer(0.4, bs.WeakCall(self._play_drumroll))
 798
 799        # Add us to high scores, filter, and store.
 800        our_high_scores_all = self._campaign.getlevel(
 801            self._level_name
 802        ).get_high_scores()
 803
 804        our_high_scores = our_high_scores_all.setdefault(
 805            str(len(self._playerinfos)) + ' Player', []
 806        )
 807
 808        if self._score is not None:
 809            our_score: list | None = [
 810                self._score,
 811                {
 812                    'players': [
 813                        {'name': p.name, 'character': p.character}
 814                        for p in self._playerinfos
 815                    ]
 816                },
 817            ]
 818            our_high_scores.append(our_score)
 819        else:
 820            our_score = None
 821
 822        try:
 823            our_high_scores.sort(
 824                reverse=self._score_order == 'increasing', key=lambda x: x[0]
 825            )
 826        except Exception:
 827            logging.exception('Error sorting scores.')
 828            print(f'our_high_scores: {our_high_scores}')
 829
 830        del our_high_scores[10:]
 831
 832        if self._score is not None:
 833            sver = self._campaign.getlevel(
 834                self._level_name
 835            ).get_score_version_string()
 836            plus.add_v1_account_transaction(
 837                {
 838                    'type': 'SET_LEVEL_LOCAL_HIGH_SCORES',
 839                    'campaign': self._campaign.name,
 840                    'level': self._level_name,
 841                    'scoreVersion': sver,
 842                    'scores': our_high_scores_all,
 843                }
 844            )
 845        if plus.get_v1_account_state() != 'signed_in':
 846            # We expect this only in kiosk mode; complain otherwise.
 847            if not (env.demo or env.arcade):
 848                logging.error('got not-signed-in at score-submit; unexpected')
 849            bs.pushcall(bs.WeakCall(self._got_score_results, None))
 850        else:
 851            assert self._game_name_str is not None
 852            assert self._game_config_str is not None
 853            plus.submit_score(
 854                self._game_name_str,
 855                self._game_config_str,
 856                name_str,
 857                self._score,
 858                bs.WeakCall(self._got_score_results),
 859                order=self._score_order,
 860                tournament_id=self.session.tournament_id,
 861                score_type=self._score_type,
 862                campaign=self._campaign.name,
 863                level=self._level_name,
 864            )
 865
 866        # Apply the transactions we've been adding locally.
 867        plus.run_v1_account_transactions()
 868
 869        # If we're not doing the world's-best button, just show a title
 870        # instead.
 871        ts_height = 300
 872        ts_h_offs = 210
 873        v_offs = 40
 874        txt = Text(
 875            (
 876                bs.Lstr(resource='tournamentStandingsText')
 877                if self.session.tournament_id is not None
 878                else (
 879                    bs.Lstr(resource='worldsBestScoresText')
 880                    if self._score_type == 'points'
 881                    else bs.Lstr(resource='worldsBestTimesText')
 882                )
 883            ),
 884            maxwidth=210,
 885            position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
 886            transition=Text.Transition.IN_LEFT,
 887            v_align=Text.VAlign.CENTER,
 888            scale=1.2,
 889            transition_delay=2.2,
 890        ).autoretain()
 891
 892        # If we've got a button on the server, only show this on clients.
 893        if self._should_show_worlds_best_button():
 894            assert txt.node
 895            txt.node.client_only = True
 896
 897        ts_height = 300
 898        ts_h_offs = -480
 899        v_offs = 40
 900        Text(
 901            (
 902                bs.Lstr(resource='yourBestScoresText')
 903                if self._score_type == 'points'
 904                else bs.Lstr(resource='yourBestTimesText')
 905            ),
 906            maxwidth=210,
 907            position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
 908            transition=Text.Transition.IN_RIGHT,
 909            v_align=Text.VAlign.CENTER,
 910            scale=1.2,
 911            transition_delay=1.8,
 912        ).autoretain()
 913
 914        display_scores = list(our_high_scores)
 915        display_count = 5
 916
 917        while len(display_scores) < display_count:
 918            display_scores.append((0, None))
 919
 920        showed_ours = False
 921        h_offs_extra = 85 if self._score_type == 'points' else 130
 922        v_offs_extra = 20
 923        v_offs_names = 0
 924        scale = 1.0
 925        p_count = len(self._playerinfos)
 926        h_offs_extra -= 75
 927        if p_count > 1:
 928            h_offs_extra -= 20
 929        if p_count == 2:
 930            scale = 0.9
 931        elif p_count == 3:
 932            scale = 0.65
 933        elif p_count == 4:
 934            scale = 0.5
 935        times: list[tuple[float, float]] = []
 936        for i in range(display_count):
 937            times.insert(
 938                random.randrange(0, len(times) + 1),
 939                (1.9 + i * 0.05, 2.3 + i * 0.05),
 940            )
 941        for i in range(display_count):
 942            try:
 943                if display_scores[i][1] is None:
 944                    name_str = '-'
 945                else:
 946                    # noinspection PyUnresolvedReferences
 947                    name_str = ', '.join(
 948                        [p['name'] for p in display_scores[i][1]['players']]
 949                    )
 950            except Exception:
 951                logging.exception(
 952                    'Error calcing name_str for %s.', display_scores
 953                )
 954                name_str = '-'
 955            if display_scores[i] == our_score and not showed_ours:
 956                flash = True
 957                color0 = (0.6, 0.4, 0.1, 1.0)
 958                color1 = (0.6, 0.6, 0.6, 1.0)
 959                tdelay1 = 3.7
 960                tdelay2 = 3.7
 961                showed_ours = True
 962            else:
 963                flash = False
 964                color0 = (0.6, 0.4, 0.1, 1.0)
 965                color1 = (0.6, 0.6, 0.6, 1.0)
 966                tdelay1 = times[i][0]
 967                tdelay2 = times[i][1]
 968            Text(
 969                (
 970                    str(display_scores[i][0])
 971                    if self._score_type == 'points'
 972                    else bs.timestring((display_scores[i][0] * 10) / 1000.0)
 973                ),
 974                position=(
 975                    ts_h_offs + 20 + h_offs_extra,
 976                    v_offs_extra
 977                    + ts_height / 2
 978                    + -ts_height * (i + 1) / 10
 979                    + v_offs
 980                    + 11.0,
 981                ),
 982                h_align=Text.HAlign.RIGHT,
 983                v_align=Text.VAlign.CENTER,
 984                color=color0,
 985                flash=flash,
 986                transition=Text.Transition.IN_RIGHT,
 987                transition_delay=tdelay1,
 988            ).autoretain()
 989
 990            Text(
 991                bs.Lstr(value=name_str),
 992                position=(
 993                    ts_h_offs + 35 + h_offs_extra,
 994                    v_offs_extra
 995                    + ts_height / 2
 996                    + -ts_height * (i + 1) / 10
 997                    + v_offs_names
 998                    + v_offs
 999                    + 11.0,
1000                ),
1001                maxwidth=80.0 + 100.0 * len(self._playerinfos),
1002                v_align=Text.VAlign.CENTER,
1003                color=color1,
1004                flash=flash,
1005                scale=scale,
1006                transition=Text.Transition.IN_RIGHT,
1007                transition_delay=tdelay2,
1008            ).autoretain()
1009
1010        # Show achievements for this level.
1011        ts_height = -150
1012        ts_h_offs = -480
1013        v_offs = 40
1014
1015        # Only make this if we don't have the button
1016        # (never want clients to see it so no need for client-only
1017        # version, etc).
1018        if self._have_achievements:
1019            if not self._account_has_achievements:
1020                Text(
1021                    bs.Lstr(resource='achievementsText'),
1022                    position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3),
1023                    maxwidth=210,
1024                    host_only=True,
1025                    transition=Text.Transition.IN_RIGHT,
1026                    v_align=Text.VAlign.CENTER,
1027                    scale=1.2,
1028                    transition_delay=2.8,
1029                ).autoretain()
1030
1031            assert self._game_name_str is not None
1032            assert bs.app.classic is not None
1033            achievements = bs.app.classic.ach.achievements_for_coop_level(
1034                self._game_name_str
1035            )
1036            hval = -455
1037            vval = -100
1038            tdelay = 0.0
1039            for ach in achievements:
1040                ach.create_display(hval, vval + v_offs, 3.0 + tdelay)
1041                vval -= 55
1042                tdelay += 0.250
1043
1044        bs.timer(5.0, bs.WeakCall(self._show_tips))

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

Inherited Members
bascenev1._activity.Activity
settings_raw
teams
players
announce_player_deaths
is_joining_activity
allow_pausing
allow_kick_idle_players
slow_motion
inherits_slow_motion
inherits_vr_overlay_center
allow_mid_activity_joins
can_show_ad_on_death
paused_text
preloads
lobby
context
globalsnode
stats
on_expire
customdata
expired
playertype
teamtype
retain_actor
add_actor_weak_ref
session
on_player_leave
on_team_join
on_team_leave
on_transition_out
handlemessage
has_transitioned_in
has_begun
has_ended
is_transitioning_out
transition_out
end
create_player
create_team
bascenev1._dependency.DependencyComponent
dep_is_present
get_dynamic_deps