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