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