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