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