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