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 + 480), 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 + 100.0 + x_offs_extra, 493 v_offs + 560.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 + 100 + x_offs_extra, v_offs + 560), 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 + 100 + x_offs_extra, v_offs + 560), 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 offs = -55 540 assert self._corner_button_offs is not None 541 pos_x = self._corner_button_offs[0] + offs 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, 292), 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, 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 + 11.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 + 11.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 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 + 480), 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 + 100.0 + x_offs_extra, 494 v_offs + 560.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 + 100 + x_offs_extra, v_offs + 560), 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 + 100 + x_offs_extra, v_offs + 560), 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 offs = -55 541 assert self._corner_button_offs is not None 542 pos_x = self._corner_button_offs[0] + offs 543 pos_y = self._corner_button_offs[1] 544 if self._league_rank_button is not None: 545 self._league_rank_button.set_position((pos_x, pos_y)) 546 if self._store_button_instance is not None: 547 self._store_button_instance.set_position((pos_x + 100, pos_y)) 548 549 def _player_press(self) -> None: 550 # (Only for headless builds). 551 552 # If this activity is a good 'end point', ask server-mode just once if 553 # it wants to do anything special like switch sessions or kill the app. 554 if ( 555 self._allow_server_transition 556 and bs.app.classic is not None 557 and bs.app.classic.server is not None 558 and self._server_transitioning is None 559 ): 560 self._server_transitioning = ( 561 bs.app.classic.server.handle_transition() 562 ) 563 assert isinstance(self._server_transitioning, bool) 564 565 # If server-mode is handling this, don't do anything ourself. 566 if self._server_transitioning is True: 567 return 568 569 # Otherwise restart current level. 570 self._campaign.set_selected_level(self._level_name) 571 with self.context: 572 self.end({'outcome': 'restart'}) 573 574 def _safe_assign(self, player: bs.Player) -> None: 575 # (Only for headless builds). 576 577 # Just to be extra careful, don't assign if we're transitioning out. 578 # (though theoretically that should be ok). 579 if not self.is_transitioning_out() and player: 580 player.assigninput( 581 ( 582 bs.InputType.JUMP_PRESS, 583 bs.InputType.PUNCH_PRESS, 584 bs.InputType.BOMB_PRESS, 585 bs.InputType.PICK_UP_PRESS, 586 ), 587 self._player_press, 588 ) 589 590 @override 591 def on_player_join(self, player: bs.Player) -> None: 592 super().on_player_join(player) 593 594 if bs.app.classic is not None and bs.app.classic.server is not None: 595 # Host can't press retry button, so anyone can do it instead. 596 time_till_assign = max( 597 0, self._birth_time + self._min_view_time - bs.time() 598 ) 599 600 bs.timer(time_till_assign, bs.WeakCall(self._safe_assign, player)) 601 602 @override 603 def on_begin(self) -> None: 604 # FIXME: Clean this up. 605 # pylint: disable=too-many-statements 606 # pylint: disable=too-many-branches 607 # pylint: disable=too-many-locals 608 super().on_begin() 609 610 app = bs.app 611 env = app.env 612 plus = app.plus 613 assert plus is not None 614 615 self._begin_time = bs.time() 616 617 # Calc whether the level is complete and other stuff. 618 levels = self._campaign.levels 619 level = self._campaign.getlevel(self._level_name) 620 self._was_complete = level.complete 621 self._is_complete = self._was_complete or self._victory 622 self._newly_complete = self._is_complete and not self._was_complete 623 self._is_more_levels = ( 624 level.index < len(levels) - 1 625 ) and self._campaign.sequential 626 627 # Any time we complete a level, set the next one as unlocked. 628 if self._is_complete and self._is_more_levels: 629 plus.add_v1_account_transaction( 630 { 631 'type': 'COMPLETE_LEVEL', 632 'campaign': self._campaign.name, 633 'level': self._level_name, 634 } 635 ) 636 self._next_level_name = levels[level.index + 1].name 637 638 # If this is the first time we completed it, set the next one 639 # as current. 640 if self._newly_complete: 641 cfg = app.config 642 cfg['Selected Coop Game'] = ( 643 self._campaign.name + ':' + self._next_level_name 644 ) 645 cfg.commit() 646 self._campaign.set_selected_level(self._next_level_name) 647 648 bs.timer(1.0, bs.WeakCall(self.request_ui)) 649 650 if ( 651 self._is_complete 652 and self._victory 653 and self._is_more_levels 654 and not (env.demo or env.arcade) 655 ): 656 Text( 657 ( 658 bs.Lstr( 659 value='${A}:\n', 660 subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))], 661 ) 662 if self._newly_complete 663 else bs.Lstr( 664 value='${A}:\n', 665 subs=[('${A}', bs.Lstr(resource='nextLevelText'))], 666 ) 667 ), 668 transition=Text.Transition.IN_RIGHT, 669 transition_delay=5.2, 670 flash=self._newly_complete, 671 scale=0.54, 672 h_align=Text.HAlign.CENTER, 673 maxwidth=270, 674 color=(0.5, 0.7, 0.5, 1), 675 position=(270, -235), 676 ).autoretain() 677 assert self._next_level_name is not None 678 Text( 679 bs.Lstr(translate=('coopLevelNames', self._next_level_name)), 680 transition=Text.Transition.IN_RIGHT, 681 transition_delay=5.2, 682 flash=self._newly_complete, 683 scale=0.7, 684 h_align=Text.HAlign.CENTER, 685 maxwidth=205, 686 color=(0.5, 0.7, 0.5, 1), 687 position=(270, -255), 688 ).autoretain() 689 if self._newly_complete: 690 bs.timer(5.2, self._cashregistersound.play) 691 bs.timer(5.2, self._dingsound.play) 692 693 offs_x = -195 694 if len(self._playerinfos) > 1: 695 pstr = bs.Lstr( 696 value='- ${A} -', 697 subs=[ 698 ( 699 '${A}', 700 bs.Lstr( 701 resource='multiPlayerCountText', 702 subs=[('${COUNT}', str(len(self._playerinfos)))], 703 ), 704 ) 705 ], 706 ) 707 else: 708 pstr = bs.Lstr( 709 value='- ${A} -', 710 subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))], 711 ) 712 ZoomText( 713 self._campaign.getlevel(self._level_name).displayname, 714 maxwidth=800, 715 flash=False, 716 trail=False, 717 color=(0.5, 1, 0.5, 1), 718 h_align='center', 719 scale=0.4, 720 position=(0, 292), 721 jitter=1.0, 722 ).autoretain() 723 Text( 724 pstr, 725 maxwidth=300, 726 transition=Text.Transition.FADE_IN, 727 scale=0.7, 728 h_align=Text.HAlign.CENTER, 729 v_align=Text.VAlign.CENTER, 730 color=(0.5, 0.7, 0.5, 1), 731 position=(0, 230), 732 ).autoretain() 733 734 if app.classic is not None and app.classic.server is None: 735 # If we're running in normal non-headless build, show this text 736 # because only host can continue the game. 737 adisp = plus.get_v1_account_display_string() 738 txt = Text( 739 bs.Lstr( 740 resource='waitingForHostText', subs=[('${HOST}', adisp)] 741 ), 742 maxwidth=300, 743 transition=Text.Transition.FADE_IN, 744 transition_delay=8.0, 745 scale=0.85, 746 h_align=Text.HAlign.CENTER, 747 v_align=Text.VAlign.CENTER, 748 color=(1, 1, 0, 1), 749 position=(0, -230), 750 ).autoretain() 751 assert txt.node 752 txt.node.client_only = True 753 else: 754 # In headless build, anyone can continue the game. 755 sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') 756 Text( 757 sval, 758 v_attach=Text.VAttach.BOTTOM, 759 h_align=Text.HAlign.CENTER, 760 flash=True, 761 vr_depth=50, 762 position=(0, 60), 763 scale=0.8, 764 color=(0.5, 0.7, 0.5, 0.5), 765 transition=Text.Transition.IN_BOTTOM_SLOW, 766 transition_delay=self._min_view_time, 767 ).autoretain() 768 769 if self._score is not None: 770 bs.timer(0.35, self._score_display_sound_small.play) 771 772 # Vestigial remain; this stuff should just be instance vars. 773 self._show_info = {} 774 775 if self._score is not None: 776 bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x)) 777 else: 778 bs.pushcall(bs.WeakCall(self._show_fail)) 779 780 self._name_str = name_str = ', '.join( 781 [p.name for p in self._playerinfos] 782 ) 783 784 self._score_loading_status = Text( 785 bs.Lstr( 786 value='${A}...', 787 subs=[('${A}', bs.Lstr(resource='loadingText'))], 788 ), 789 position=(280, 150 + 30), 790 color=(1, 1, 1, 0.4), 791 transition=Text.Transition.FADE_IN, 792 scale=0.7, 793 transition_delay=2.0, 794 ) 795 796 if self._score is not None and self._submit_score: 797 bs.timer(0.4, bs.WeakCall(self._play_drumroll)) 798 799 # Add us to high scores, filter, and store. 800 our_high_scores_all = self._campaign.getlevel( 801 self._level_name 802 ).get_high_scores() 803 804 our_high_scores = our_high_scores_all.setdefault( 805 str(len(self._playerinfos)) + ' Player', [] 806 ) 807 808 if self._score is not None: 809 our_score: list | None = [ 810 self._score, 811 { 812 'players': [ 813 {'name': p.name, 'character': p.character} 814 for p in self._playerinfos 815 ] 816 }, 817 ] 818 our_high_scores.append(our_score) 819 else: 820 our_score = None 821 822 try: 823 our_high_scores.sort( 824 reverse=self._score_order == 'increasing', key=lambda x: x[0] 825 ) 826 except Exception: 827 logging.exception('Error sorting scores.') 828 print(f'our_high_scores: {our_high_scores}') 829 830 del our_high_scores[10:] 831 832 if self._score is not None: 833 sver = self._campaign.getlevel( 834 self._level_name 835 ).get_score_version_string() 836 plus.add_v1_account_transaction( 837 { 838 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', 839 'campaign': self._campaign.name, 840 'level': self._level_name, 841 'scoreVersion': sver, 842 'scores': our_high_scores_all, 843 } 844 ) 845 if plus.get_v1_account_state() != 'signed_in': 846 # We expect this only in kiosk mode; complain otherwise. 847 if not (env.demo or env.arcade): 848 logging.error('got not-signed-in at score-submit; unexpected') 849 bs.pushcall(bs.WeakCall(self._got_score_results, None)) 850 else: 851 assert self._game_name_str is not None 852 assert self._game_config_str is not None 853 plus.submit_score( 854 self._game_name_str, 855 self._game_config_str, 856 name_str, 857 self._score, 858 bs.WeakCall(self._got_score_results), 859 order=self._score_order, 860 tournament_id=self.session.tournament_id, 861 score_type=self._score_type, 862 campaign=self._campaign.name, 863 level=self._level_name, 864 ) 865 866 # Apply the transactions we've been adding locally. 867 plus.run_v1_account_transactions() 868 869 # If we're not doing the world's-best button, just show a title 870 # instead. 871 ts_height = 300 872 ts_h_offs = 210 873 v_offs = 40 874 txt = Text( 875 ( 876 bs.Lstr(resource='tournamentStandingsText') 877 if self.session.tournament_id is not None 878 else ( 879 bs.Lstr(resource='worldsBestScoresText') 880 if self._score_type == 'points' 881 else bs.Lstr(resource='worldsBestTimesText') 882 ) 883 ), 884 maxwidth=210, 885 position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), 886 transition=Text.Transition.IN_LEFT, 887 v_align=Text.VAlign.CENTER, 888 scale=1.2, 889 transition_delay=2.2, 890 ).autoretain() 891 892 # If we've got a button on the server, only show this on clients. 893 if self._should_show_worlds_best_button(): 894 assert txt.node 895 txt.node.client_only = True 896 897 ts_height = 300 898 ts_h_offs = -480 899 v_offs = 40 900 Text( 901 ( 902 bs.Lstr(resource='yourBestScoresText') 903 if self._score_type == 'points' 904 else bs.Lstr(resource='yourBestTimesText') 905 ), 906 maxwidth=210, 907 position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), 908 transition=Text.Transition.IN_RIGHT, 909 v_align=Text.VAlign.CENTER, 910 scale=1.2, 911 transition_delay=1.8, 912 ).autoretain() 913 914 display_scores = list(our_high_scores) 915 display_count = 5 916 917 while len(display_scores) < display_count: 918 display_scores.append((0, None)) 919 920 showed_ours = False 921 h_offs_extra = 85 if self._score_type == 'points' else 130 922 v_offs_extra = 20 923 v_offs_names = 0 924 scale = 1.0 925 p_count = len(self._playerinfos) 926 h_offs_extra -= 75 927 if p_count > 1: 928 h_offs_extra -= 20 929 if p_count == 2: 930 scale = 0.9 931 elif p_count == 3: 932 scale = 0.65 933 elif p_count == 4: 934 scale = 0.5 935 times: list[tuple[float, float]] = [] 936 for i in range(display_count): 937 times.insert( 938 random.randrange(0, len(times) + 1), 939 (1.9 + i * 0.05, 2.3 + i * 0.05), 940 ) 941 for i in range(display_count): 942 try: 943 if display_scores[i][1] is None: 944 name_str = '-' 945 else: 946 # noinspection PyUnresolvedReferences 947 name_str = ', '.join( 948 [p['name'] for p in display_scores[i][1]['players']] 949 ) 950 except Exception: 951 logging.exception( 952 'Error calcing name_str for %s.', display_scores 953 ) 954 name_str = '-' 955 if display_scores[i] == our_score and not showed_ours: 956 flash = True 957 color0 = (0.6, 0.4, 0.1, 1.0) 958 color1 = (0.6, 0.6, 0.6, 1.0) 959 tdelay1 = 3.7 960 tdelay2 = 3.7 961 showed_ours = True 962 else: 963 flash = False 964 color0 = (0.6, 0.4, 0.1, 1.0) 965 color1 = (0.6, 0.6, 0.6, 1.0) 966 tdelay1 = times[i][0] 967 tdelay2 = times[i][1] 968 Text( 969 ( 970 str(display_scores[i][0]) 971 if self._score_type == 'points' 972 else bs.timestring((display_scores[i][0] * 10) / 1000.0) 973 ), 974 position=( 975 ts_h_offs + 20 + h_offs_extra, 976 v_offs_extra 977 + ts_height / 2 978 + -ts_height * (i + 1) / 10 979 + v_offs 980 + 11.0, 981 ), 982 h_align=Text.HAlign.RIGHT, 983 v_align=Text.VAlign.CENTER, 984 color=color0, 985 flash=flash, 986 transition=Text.Transition.IN_RIGHT, 987 transition_delay=tdelay1, 988 ).autoretain() 989 990 Text( 991 bs.Lstr(value=name_str), 992 position=( 993 ts_h_offs + 35 + h_offs_extra, 994 v_offs_extra 995 + ts_height / 2 996 + -ts_height * (i + 1) / 10 997 + v_offs_names 998 + v_offs 999 + 11.0, 1000 ), 1001 maxwidth=80.0 + 100.0 * len(self._playerinfos), 1002 v_align=Text.VAlign.CENTER, 1003 color=color1, 1004 flash=flash, 1005 scale=scale, 1006 transition=Text.Transition.IN_RIGHT, 1007 transition_delay=tdelay2, 1008 ).autoretain() 1009 1010 # Show achievements for this level. 1011 ts_height = -150 1012 ts_h_offs = -480 1013 v_offs = 40 1014 1015 # Only make this if we don't have the button 1016 # (never want clients to see it so no need for client-only 1017 # version, etc). 1018 if self._have_achievements: 1019 if not self._account_has_achievements: 1020 Text( 1021 bs.Lstr(resource='achievementsText'), 1022 position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3), 1023 maxwidth=210, 1024 host_only=True, 1025 transition=Text.Transition.IN_RIGHT, 1026 v_align=Text.VAlign.CENTER, 1027 scale=1.2, 1028 transition_delay=2.8, 1029 ).autoretain() 1030 1031 assert self._game_name_str is not None 1032 assert bs.app.classic is not None 1033 achievements = bs.app.classic.ach.achievements_for_coop_level( 1034 self._game_name_str 1035 ) 1036 hval = -455 1037 vval = -100 1038 tdelay = 0.0 1039 for ach in achievements: 1040 ach.create_display(hval, vval + v_offs, 3.0 + tdelay) 1041 vval -= 55 1042 tdelay += 0.250 1043 1044 bs.timer(5.0, bs.WeakCall(self._show_tips)) 1045 1046 def _play_drumroll(self) -> None: 1047 bs.NodeActor( 1048 bs.newnode( 1049 'sound', 1050 attrs={ 1051 'sound': self.drum_roll_sound, 1052 'positional': False, 1053 'loop': False, 1054 }, 1055 ) 1056 ).autoretain() 1057 1058 def _got_friend_score_results(self, results: list[Any] | None) -> None: 1059 # FIXME: tidy this up 1060 # pylint: disable=too-many-locals 1061 # pylint: disable=too-many-branches 1062 # pylint: disable=too-many-statements 1063 from efro.util import asserttype 1064 1065 # delay a bit if results come in too fast 1066 assert self._begin_time is not None 1067 base_delay = max(0, 1.9 - (bs.time() - self._begin_time)) 1068 ts_height = 300 1069 ts_h_offs = -550 1070 v_offs = 30 1071 1072 # Report in case of error. 1073 if results is None: 1074 self._friends_loading_status = Text( 1075 bs.Lstr(resource='friendScoresUnavailableText'), 1076 maxwidth=330, 1077 position=(-475, 150 + v_offs), 1078 color=(1, 1, 1, 0.4), 1079 transition=Text.Transition.FADE_IN, 1080 transition_delay=base_delay + 0.8, 1081 scale=0.7, 1082 ) 1083 return 1084 1085 self._friends_loading_status = None 1086 1087 # Ok, it looks like we aren't able to reliably get a just-submitted 1088 # result returned in the score list, so we need to look for our score 1089 # in this list and replace it if ours is better or add ours otherwise. 1090 if self._score is not None: 1091 our_score_entry = [self._score, 'Me', True] 1092 for score in results: 1093 if score[2]: 1094 if self._score_order == 'increasing': 1095 our_score_entry[0] = max(score[0], self._score) 1096 else: 1097 our_score_entry[0] = min(score[0], self._score) 1098 results.remove(score) 1099 break 1100 results.append(our_score_entry) 1101 results.sort( 1102 reverse=self._score_order == 'increasing', 1103 key=lambda x: asserttype(x[0], int), 1104 ) 1105 1106 # If we're not submitting our own score, we still want to change the 1107 # name of our own score to 'Me'. 1108 else: 1109 for score in results: 1110 if score[2]: 1111 score[1] = 'Me' 1112 break 1113 h_offs_extra = 80 if self._score_type == 'points' else 130 1114 v_offs_extra = 20 1115 v_offs_names = 0 1116 scale = 1.0 1117 1118 # Make sure there's at least 5. 1119 while len(results) < 5: 1120 results.append([0, '-', False]) 1121 results = results[:5] 1122 times: list[tuple[float, float]] = [] 1123 for i in range(len(results)): 1124 times.insert( 1125 random.randrange(0, len(times) + 1), 1126 (base_delay + i * 0.05, base_delay + 0.3 + i * 0.05), 1127 ) 1128 for i, tval in enumerate(results): 1129 score = int(tval[0]) 1130 name_str = tval[1] 1131 is_me = tval[2] 1132 if is_me and score == self._score: 1133 flash = True 1134 color0 = (0.6, 0.4, 0.1, 1.0) 1135 color1 = (0.6, 0.6, 0.6, 1.0) 1136 tdelay1 = base_delay + 1.0 1137 tdelay2 = base_delay + 1.0 1138 else: 1139 flash = False 1140 if is_me: 1141 color0 = (0.6, 0.4, 0.1, 1.0) 1142 color1 = (0.9, 1.0, 0.9, 1.0) 1143 else: 1144 color0 = (0.6, 0.4, 0.1, 1.0) 1145 color1 = (0.6, 0.6, 0.6, 1.0) 1146 tdelay1 = times[i][0] 1147 tdelay2 = times[i][1] 1148 if name_str != '-': 1149 Text( 1150 ( 1151 str(score) 1152 if self._score_type == 'points' 1153 else bs.timestring((score * 10) / 1000.0) 1154 ), 1155 position=( 1156 ts_h_offs + 20 + h_offs_extra, 1157 v_offs_extra 1158 + ts_height / 2 1159 + -ts_height * (i + 1) / 10 1160 + v_offs 1161 + 11.0, 1162 ), 1163 h_align=Text.HAlign.RIGHT, 1164 v_align=Text.VAlign.CENTER, 1165 color=color0, 1166 flash=flash, 1167 transition=Text.Transition.IN_RIGHT, 1168 transition_delay=tdelay1, 1169 ).autoretain() 1170 else: 1171 if is_me: 1172 print('Error: got empty name_str on score result:', tval) 1173 1174 Text( 1175 bs.Lstr(value=name_str), 1176 position=( 1177 ts_h_offs + 35 + h_offs_extra, 1178 v_offs_extra 1179 + ts_height / 2 1180 + -ts_height * (i + 1) / 10 1181 + v_offs_names 1182 + v_offs 1183 + 11.0, 1184 ), 1185 color=color1, 1186 maxwidth=160.0, 1187 v_align=Text.VAlign.CENTER, 1188 flash=flash, 1189 scale=scale, 1190 transition=Text.Transition.IN_RIGHT, 1191 transition_delay=tdelay2, 1192 ).autoretain() 1193 1194 def _got_score_results(self, results: dict[str, Any] | None) -> None: 1195 # FIXME: tidy this up 1196 # pylint: disable=too-many-locals 1197 # pylint: disable=too-many-branches 1198 # pylint: disable=too-many-statements 1199 1200 plus = bs.app.plus 1201 assert plus is not None 1202 1203 # We need to manually run this in the context of our activity 1204 # and only if we aren't shutting down. 1205 # (really should make the submit_score call handle that stuff itself) 1206 if self.expired: 1207 return 1208 with self.context: 1209 # Delay a bit if results come in too fast. 1210 assert self._begin_time is not None 1211 base_delay = max(0, 2.7 - (bs.time() - self._begin_time)) 1212 v_offs = 20 1213 if results is None: 1214 self._score_loading_status = Text( 1215 bs.Lstr(resource='worldScoresUnavailableText'), 1216 position=(230, 150 + v_offs), 1217 color=(1, 1, 1, 0.4), 1218 transition=Text.Transition.FADE_IN, 1219 transition_delay=base_delay + 0.3, 1220 scale=0.7, 1221 ) 1222 else: 1223 self._score_link = results['link'] 1224 assert self._score_link is not None 1225 # Prepend our master-server addr if its a relative addr. 1226 if not self._score_link.startswith( 1227 'http://' 1228 ) and not self._score_link.startswith('https://'): 1229 self._score_link = ( 1230 plus.get_master_server_address() 1231 + '/' 1232 + self._score_link 1233 ) 1234 self._score_loading_status = None 1235 if 'tournamentSecondsRemaining' in results: 1236 secs_remaining = results['tournamentSecondsRemaining'] 1237 assert isinstance(secs_remaining, int) 1238 self._tournament_time_remaining = secs_remaining 1239 self._tournament_time_remaining_text_timer = bs.BaseTimer( 1240 1.0, 1241 bs.WeakCall( 1242 self._update_tournament_time_remaining_text 1243 ), 1244 repeat=True, 1245 ) 1246 1247 assert self._show_info is not None 1248 self._show_info['results'] = results 1249 if results is not None: 1250 if results['tops'] != '': 1251 self._show_info['tops'] = results['tops'] 1252 else: 1253 self._show_info['tops'] = [] 1254 offs_x = -195 1255 available = self._show_info['results'] is not None 1256 if self._score is not None: 1257 bs.basetimer( 1258 (1.5 + base_delay), 1259 bs.WeakCall(self._show_world_rank, offs_x), 1260 ) 1261 ts_h_offs = 200 1262 ts_height = 300 1263 1264 # Show world tops. 1265 if available: 1266 # Show the number of games represented by this 1267 # list (except for in tournaments). 1268 if self.session.tournament_id is None: 1269 Text( 1270 bs.Lstr( 1271 resource='lastGamesText', 1272 subs=[ 1273 ( 1274 '${COUNT}', 1275 str(self._show_info['results']['total']), 1276 ) 1277 ], 1278 ), 1279 position=( 1280 ts_h_offs - 35 + 95, 1281 ts_height / 2 + 6 + v_offs, 1282 ), 1283 color=(0.4, 0.4, 0.4, 1.0), 1284 scale=0.7, 1285 transition=Text.Transition.IN_RIGHT, 1286 transition_delay=base_delay + 0.3, 1287 ).autoretain() 1288 else: 1289 v_offs += 20 1290 1291 h_offs_extra = 0 1292 v_offs_names = 0 1293 scale = 1.0 1294 p_count = len(self._playerinfos) 1295 if p_count > 1: 1296 h_offs_extra -= 40 1297 if self._score_type != 'points': 1298 h_offs_extra += 60 1299 if p_count == 2: 1300 scale = 0.9 1301 elif p_count == 3: 1302 scale = 0.65 1303 elif p_count == 4: 1304 scale = 0.5 1305 1306 # Make sure there's at least 10. 1307 while len(self._show_info['tops']) < 10: 1308 self._show_info['tops'].append([0, '-']) 1309 1310 times: list[tuple[float, float]] = [] 1311 for i in range(len(self._show_info['tops'])): 1312 times.insert( 1313 random.randrange(0, len(times) + 1), 1314 (base_delay + i * 0.05, base_delay + 0.4 + i * 0.05), 1315 ) 1316 for i, tval in enumerate(self._show_info['tops']): 1317 score = int(tval[0]) 1318 name_str = tval[1] 1319 if self._name_str == name_str and self._score == score: 1320 flash = True 1321 color0 = (0.6, 0.4, 0.1, 1.0) 1322 color1 = (0.6, 0.6, 0.6, 1.0) 1323 tdelay1 = base_delay + 1.0 1324 tdelay2 = base_delay + 1.0 1325 else: 1326 flash = False 1327 if self._name_str == name_str: 1328 color0 = (0.6, 0.4, 0.1, 1.0) 1329 color1 = (0.9, 1.0, 0.9, 1.0) 1330 else: 1331 color0 = (0.6, 0.4, 0.1, 1.0) 1332 color1 = (0.6, 0.6, 0.6, 1.0) 1333 tdelay1 = times[i][0] 1334 tdelay2 = times[i][1] 1335 1336 if name_str != '-': 1337 Text( 1338 ( 1339 str(score) 1340 if self._score_type == 'points' 1341 else bs.timestring((score * 10) / 1000.0) 1342 ), 1343 position=( 1344 ts_h_offs + 20 + h_offs_extra, 1345 ts_height / 2 1346 + -ts_height * (i + 1) / 10 1347 + v_offs 1348 + 11.0, 1349 ), 1350 h_align=Text.HAlign.RIGHT, 1351 v_align=Text.VAlign.CENTER, 1352 color=color0, 1353 flash=flash, 1354 transition=Text.Transition.IN_LEFT, 1355 transition_delay=tdelay1, 1356 ).autoretain() 1357 Text( 1358 bs.Lstr(value=name_str), 1359 position=( 1360 ts_h_offs + 35 + h_offs_extra, 1361 ts_height / 2 1362 + -ts_height * (i + 1) / 10 1363 + v_offs_names 1364 + v_offs 1365 + 11.0, 1366 ), 1367 maxwidth=80.0 + 100.0 * len(self._playerinfos), 1368 v_align=Text.VAlign.CENTER, 1369 color=color1, 1370 flash=flash, 1371 scale=scale, 1372 transition=Text.Transition.IN_LEFT, 1373 transition_delay=tdelay2, 1374 ).autoretain() 1375 1376 def _show_tips(self) -> None: 1377 from bascenev1lib.actor.tipstext import TipsText 1378 1379 TipsText(offs_y=30).autoretain() 1380 1381 def _update_tournament_time_remaining_text(self) -> None: 1382 if self._tournament_time_remaining is None: 1383 return 1384 self._tournament_time_remaining = max( 1385 0, self._tournament_time_remaining - 1 1386 ) 1387 if self._tournament_time_remaining_text is not None: 1388 val = bs.timestring( 1389 self._tournament_time_remaining, 1390 centi=False, 1391 ) 1392 self._tournament_time_remaining_text.node.text = val 1393 1394 def _show_world_rank(self, offs_x: float) -> None: 1395 # FIXME: Tidy this up. 1396 # pylint: disable=too-many-locals 1397 # pylint: disable=too-many-branches 1398 # pylint: disable=too-many-statements 1399 assert bs.app.classic is not None 1400 assert self._show_info is not None 1401 available = self._show_info['results'] is not None 1402 1403 if available and self._submit_score: 1404 error = ( 1405 self._show_info['results']['error'] 1406 if 'error' in self._show_info['results'] 1407 else None 1408 ) 1409 rank = self._show_info['results']['rank'] 1410 total = self._show_info['results']['total'] 1411 rating = ( 1412 10.0 1413 if total == 1 1414 else 10.0 * (1.0 - (float(rank - 1) / (total - 1))) 1415 ) 1416 player_rank = self._show_info['results']['playerRank'] 1417 best_player_rank = self._show_info['results']['bestPlayerRank'] 1418 else: 1419 error = False 1420 rating = None 1421 player_rank = None 1422 best_player_rank = None 1423 1424 # If we've got tournament-seconds-remaining, show it. 1425 if self._tournament_time_remaining is not None: 1426 Text( 1427 bs.Lstr(resource='coopSelectWindow.timeRemainingText'), 1428 position=(-360, -70 - 100), 1429 color=(1, 1, 1, 0.7), 1430 h_align=Text.HAlign.CENTER, 1431 v_align=Text.VAlign.CENTER, 1432 transition=Text.Transition.FADE_IN, 1433 scale=0.8, 1434 maxwidth=300, 1435 transition_delay=2.0, 1436 ).autoretain() 1437 self._tournament_time_remaining_text = Text( 1438 '', 1439 position=(-360, -110 - 100), 1440 color=(1, 1, 1, 0.7), 1441 h_align=Text.HAlign.CENTER, 1442 v_align=Text.VAlign.CENTER, 1443 transition=Text.Transition.FADE_IN, 1444 scale=1.6, 1445 maxwidth=150, 1446 transition_delay=2.0, 1447 ) 1448 1449 # If we're a tournament, show prizes. 1450 try: 1451 assert bs.app.classic is not None 1452 tournament_id = self.session.tournament_id 1453 if tournament_id is not None: 1454 if tournament_id in bs.app.classic.accounts.tournament_info: 1455 tourney_info = bs.app.classic.accounts.tournament_info[ 1456 tournament_id 1457 ] 1458 # pylint: disable=useless-suppression 1459 # pylint: disable=unbalanced-tuple-unpacking 1460 ( 1461 pr1, 1462 pv1, 1463 pr2, 1464 pv2, 1465 pr3, 1466 pv3, 1467 ) = bs.app.classic.get_tournament_prize_strings( 1468 tourney_info 1469 ) 1470 # pylint: enable=unbalanced-tuple-unpacking 1471 # pylint: enable=useless-suppression 1472 1473 Text( 1474 bs.Lstr(resource='coopSelectWindow.prizesText'), 1475 position=(-360, -70 + 77), 1476 color=(1, 1, 1, 0.7), 1477 h_align=Text.HAlign.CENTER, 1478 v_align=Text.VAlign.CENTER, 1479 transition=Text.Transition.FADE_IN, 1480 scale=1.0, 1481 maxwidth=300, 1482 transition_delay=2.0, 1483 ).autoretain() 1484 vval = -107 + 70 1485 for rng, val in ((pr1, pv1), (pr2, pv2), (pr3, pv3)): 1486 Text( 1487 rng, 1488 position=(-410 + 10, vval), 1489 color=(1, 1, 1, 0.7), 1490 h_align=Text.HAlign.RIGHT, 1491 v_align=Text.VAlign.CENTER, 1492 transition=Text.Transition.FADE_IN, 1493 scale=0.6, 1494 maxwidth=300, 1495 transition_delay=2.0, 1496 ).autoretain() 1497 Text( 1498 val, 1499 position=(-390 + 10, vval), 1500 color=(0.7, 0.7, 0.7, 1.0), 1501 h_align=Text.HAlign.LEFT, 1502 v_align=Text.VAlign.CENTER, 1503 transition=Text.Transition.FADE_IN, 1504 scale=0.8, 1505 maxwidth=300, 1506 transition_delay=2.0, 1507 ).autoretain() 1508 vval -= 35 1509 except Exception: 1510 logging.exception('Error showing prize ranges.') 1511 1512 if self._do_new_rating: 1513 if error: 1514 ZoomText( 1515 bs.Lstr(resource='failText'), 1516 flash=True, 1517 trail=True, 1518 scale=1.0 if available else 0.333, 1519 tilt_translate=0.11, 1520 h_align='center', 1521 position=(190 + offs_x, -60), 1522 maxwidth=200, 1523 jitter=1.0, 1524 ).autoretain() 1525 Text( 1526 bs.Lstr(translate=('serverResponses', error)), 1527 position=(0, -140), 1528 color=(1, 1, 1, 0.7), 1529 h_align=Text.HAlign.CENTER, 1530 v_align=Text.VAlign.CENTER, 1531 transition=Text.Transition.FADE_IN, 1532 scale=0.9, 1533 maxwidth=400, 1534 transition_delay=1.0, 1535 ).autoretain() 1536 elif self._submit_score: 1537 ZoomText( 1538 ( 1539 ('#' + str(player_rank)) 1540 if player_rank is not None 1541 else bs.Lstr(resource='unavailableText') 1542 ), 1543 flash=True, 1544 trail=True, 1545 scale=1.0 if available else 0.333, 1546 tilt_translate=0.11, 1547 h_align='center', 1548 position=(190 + offs_x, -60), 1549 maxwidth=200, 1550 jitter=1.0, 1551 ).autoretain() 1552 1553 Text( 1554 bs.Lstr( 1555 value='${A}:', 1556 subs=[('${A}', bs.Lstr(resource='rankText'))], 1557 ), 1558 position=(0, 36), 1559 maxwidth=300, 1560 transition=Text.Transition.FADE_IN, 1561 h_align=Text.HAlign.CENTER, 1562 v_align=Text.VAlign.CENTER, 1563 transition_delay=0, 1564 ).autoretain() 1565 if best_player_rank is not None: 1566 Text( 1567 bs.Lstr( 1568 resource='currentStandingText', 1569 fallback_resource='bestRankText', 1570 subs=[('${RANK}', str(best_player_rank))], 1571 ), 1572 position=(0, -155), 1573 color=(1, 1, 1, 0.7), 1574 h_align=Text.HAlign.CENTER, 1575 transition=Text.Transition.FADE_IN, 1576 scale=0.7, 1577 transition_delay=1.0, 1578 ).autoretain() 1579 else: 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 + 480), 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 + 100.0 + x_offs_extra, 494 v_offs + 560.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 + 100 + x_offs_extra, v_offs + 560), 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 + 100 + x_offs_extra, v_offs + 560), 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.
590 @override 591 def on_player_join(self, player: bs.Player) -> None: 592 super().on_player_join(player) 593 594 if bs.app.classic is not None and bs.app.classic.server is not None: 595 # Host can't press retry button, so anyone can do it instead. 596 time_till_assign = max( 597 0, self._birth_time + self._min_view_time - bs.time() 598 ) 599 600 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)
602 @override 603 def on_begin(self) -> None: 604 # FIXME: Clean this up. 605 # pylint: disable=too-many-statements 606 # pylint: disable=too-many-branches 607 # pylint: disable=too-many-locals 608 super().on_begin() 609 610 app = bs.app 611 env = app.env 612 plus = app.plus 613 assert plus is not None 614 615 self._begin_time = bs.time() 616 617 # Calc whether the level is complete and other stuff. 618 levels = self._campaign.levels 619 level = self._campaign.getlevel(self._level_name) 620 self._was_complete = level.complete 621 self._is_complete = self._was_complete or self._victory 622 self._newly_complete = self._is_complete and not self._was_complete 623 self._is_more_levels = ( 624 level.index < len(levels) - 1 625 ) and self._campaign.sequential 626 627 # Any time we complete a level, set the next one as unlocked. 628 if self._is_complete and self._is_more_levels: 629 plus.add_v1_account_transaction( 630 { 631 'type': 'COMPLETE_LEVEL', 632 'campaign': self._campaign.name, 633 'level': self._level_name, 634 } 635 ) 636 self._next_level_name = levels[level.index + 1].name 637 638 # If this is the first time we completed it, set the next one 639 # as current. 640 if self._newly_complete: 641 cfg = app.config 642 cfg['Selected Coop Game'] = ( 643 self._campaign.name + ':' + self._next_level_name 644 ) 645 cfg.commit() 646 self._campaign.set_selected_level(self._next_level_name) 647 648 bs.timer(1.0, bs.WeakCall(self.request_ui)) 649 650 if ( 651 self._is_complete 652 and self._victory 653 and self._is_more_levels 654 and not (env.demo or env.arcade) 655 ): 656 Text( 657 ( 658 bs.Lstr( 659 value='${A}:\n', 660 subs=[('${A}', bs.Lstr(resource='levelUnlockedText'))], 661 ) 662 if self._newly_complete 663 else bs.Lstr( 664 value='${A}:\n', 665 subs=[('${A}', bs.Lstr(resource='nextLevelText'))], 666 ) 667 ), 668 transition=Text.Transition.IN_RIGHT, 669 transition_delay=5.2, 670 flash=self._newly_complete, 671 scale=0.54, 672 h_align=Text.HAlign.CENTER, 673 maxwidth=270, 674 color=(0.5, 0.7, 0.5, 1), 675 position=(270, -235), 676 ).autoretain() 677 assert self._next_level_name is not None 678 Text( 679 bs.Lstr(translate=('coopLevelNames', self._next_level_name)), 680 transition=Text.Transition.IN_RIGHT, 681 transition_delay=5.2, 682 flash=self._newly_complete, 683 scale=0.7, 684 h_align=Text.HAlign.CENTER, 685 maxwidth=205, 686 color=(0.5, 0.7, 0.5, 1), 687 position=(270, -255), 688 ).autoretain() 689 if self._newly_complete: 690 bs.timer(5.2, self._cashregistersound.play) 691 bs.timer(5.2, self._dingsound.play) 692 693 offs_x = -195 694 if len(self._playerinfos) > 1: 695 pstr = bs.Lstr( 696 value='- ${A} -', 697 subs=[ 698 ( 699 '${A}', 700 bs.Lstr( 701 resource='multiPlayerCountText', 702 subs=[('${COUNT}', str(len(self._playerinfos)))], 703 ), 704 ) 705 ], 706 ) 707 else: 708 pstr = bs.Lstr( 709 value='- ${A} -', 710 subs=[('${A}', bs.Lstr(resource='singlePlayerCountText'))], 711 ) 712 ZoomText( 713 self._campaign.getlevel(self._level_name).displayname, 714 maxwidth=800, 715 flash=False, 716 trail=False, 717 color=(0.5, 1, 0.5, 1), 718 h_align='center', 719 scale=0.4, 720 position=(0, 292), 721 jitter=1.0, 722 ).autoretain() 723 Text( 724 pstr, 725 maxwidth=300, 726 transition=Text.Transition.FADE_IN, 727 scale=0.7, 728 h_align=Text.HAlign.CENTER, 729 v_align=Text.VAlign.CENTER, 730 color=(0.5, 0.7, 0.5, 1), 731 position=(0, 230), 732 ).autoretain() 733 734 if app.classic is not None and app.classic.server is None: 735 # If we're running in normal non-headless build, show this text 736 # because only host can continue the game. 737 adisp = plus.get_v1_account_display_string() 738 txt = Text( 739 bs.Lstr( 740 resource='waitingForHostText', subs=[('${HOST}', adisp)] 741 ), 742 maxwidth=300, 743 transition=Text.Transition.FADE_IN, 744 transition_delay=8.0, 745 scale=0.85, 746 h_align=Text.HAlign.CENTER, 747 v_align=Text.VAlign.CENTER, 748 color=(1, 1, 0, 1), 749 position=(0, -230), 750 ).autoretain() 751 assert txt.node 752 txt.node.client_only = True 753 else: 754 # In headless build, anyone can continue the game. 755 sval = bs.Lstr(resource='pressAnyButtonPlayAgainText') 756 Text( 757 sval, 758 v_attach=Text.VAttach.BOTTOM, 759 h_align=Text.HAlign.CENTER, 760 flash=True, 761 vr_depth=50, 762 position=(0, 60), 763 scale=0.8, 764 color=(0.5, 0.7, 0.5, 0.5), 765 transition=Text.Transition.IN_BOTTOM_SLOW, 766 transition_delay=self._min_view_time, 767 ).autoretain() 768 769 if self._score is not None: 770 bs.timer(0.35, self._score_display_sound_small.play) 771 772 # Vestigial remain; this stuff should just be instance vars. 773 self._show_info = {} 774 775 if self._score is not None: 776 bs.timer(0.8, bs.WeakCall(self._show_score_val, offs_x)) 777 else: 778 bs.pushcall(bs.WeakCall(self._show_fail)) 779 780 self._name_str = name_str = ', '.join( 781 [p.name for p in self._playerinfos] 782 ) 783 784 self._score_loading_status = Text( 785 bs.Lstr( 786 value='${A}...', 787 subs=[('${A}', bs.Lstr(resource='loadingText'))], 788 ), 789 position=(280, 150 + 30), 790 color=(1, 1, 1, 0.4), 791 transition=Text.Transition.FADE_IN, 792 scale=0.7, 793 transition_delay=2.0, 794 ) 795 796 if self._score is not None and self._submit_score: 797 bs.timer(0.4, bs.WeakCall(self._play_drumroll)) 798 799 # Add us to high scores, filter, and store. 800 our_high_scores_all = self._campaign.getlevel( 801 self._level_name 802 ).get_high_scores() 803 804 our_high_scores = our_high_scores_all.setdefault( 805 str(len(self._playerinfos)) + ' Player', [] 806 ) 807 808 if self._score is not None: 809 our_score: list | None = [ 810 self._score, 811 { 812 'players': [ 813 {'name': p.name, 'character': p.character} 814 for p in self._playerinfos 815 ] 816 }, 817 ] 818 our_high_scores.append(our_score) 819 else: 820 our_score = None 821 822 try: 823 our_high_scores.sort( 824 reverse=self._score_order == 'increasing', key=lambda x: x[0] 825 ) 826 except Exception: 827 logging.exception('Error sorting scores.') 828 print(f'our_high_scores: {our_high_scores}') 829 830 del our_high_scores[10:] 831 832 if self._score is not None: 833 sver = self._campaign.getlevel( 834 self._level_name 835 ).get_score_version_string() 836 plus.add_v1_account_transaction( 837 { 838 'type': 'SET_LEVEL_LOCAL_HIGH_SCORES', 839 'campaign': self._campaign.name, 840 'level': self._level_name, 841 'scoreVersion': sver, 842 'scores': our_high_scores_all, 843 } 844 ) 845 if plus.get_v1_account_state() != 'signed_in': 846 # We expect this only in kiosk mode; complain otherwise. 847 if not (env.demo or env.arcade): 848 logging.error('got not-signed-in at score-submit; unexpected') 849 bs.pushcall(bs.WeakCall(self._got_score_results, None)) 850 else: 851 assert self._game_name_str is not None 852 assert self._game_config_str is not None 853 plus.submit_score( 854 self._game_name_str, 855 self._game_config_str, 856 name_str, 857 self._score, 858 bs.WeakCall(self._got_score_results), 859 order=self._score_order, 860 tournament_id=self.session.tournament_id, 861 score_type=self._score_type, 862 campaign=self._campaign.name, 863 level=self._level_name, 864 ) 865 866 # Apply the transactions we've been adding locally. 867 plus.run_v1_account_transactions() 868 869 # If we're not doing the world's-best button, just show a title 870 # instead. 871 ts_height = 300 872 ts_h_offs = 210 873 v_offs = 40 874 txt = Text( 875 ( 876 bs.Lstr(resource='tournamentStandingsText') 877 if self.session.tournament_id is not None 878 else ( 879 bs.Lstr(resource='worldsBestScoresText') 880 if self._score_type == 'points' 881 else bs.Lstr(resource='worldsBestTimesText') 882 ) 883 ), 884 maxwidth=210, 885 position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), 886 transition=Text.Transition.IN_LEFT, 887 v_align=Text.VAlign.CENTER, 888 scale=1.2, 889 transition_delay=2.2, 890 ).autoretain() 891 892 # If we've got a button on the server, only show this on clients. 893 if self._should_show_worlds_best_button(): 894 assert txt.node 895 txt.node.client_only = True 896 897 ts_height = 300 898 ts_h_offs = -480 899 v_offs = 40 900 Text( 901 ( 902 bs.Lstr(resource='yourBestScoresText') 903 if self._score_type == 'points' 904 else bs.Lstr(resource='yourBestTimesText') 905 ), 906 maxwidth=210, 907 position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20), 908 transition=Text.Transition.IN_RIGHT, 909 v_align=Text.VAlign.CENTER, 910 scale=1.2, 911 transition_delay=1.8, 912 ).autoretain() 913 914 display_scores = list(our_high_scores) 915 display_count = 5 916 917 while len(display_scores) < display_count: 918 display_scores.append((0, None)) 919 920 showed_ours = False 921 h_offs_extra = 85 if self._score_type == 'points' else 130 922 v_offs_extra = 20 923 v_offs_names = 0 924 scale = 1.0 925 p_count = len(self._playerinfos) 926 h_offs_extra -= 75 927 if p_count > 1: 928 h_offs_extra -= 20 929 if p_count == 2: 930 scale = 0.9 931 elif p_count == 3: 932 scale = 0.65 933 elif p_count == 4: 934 scale = 0.5 935 times: list[tuple[float, float]] = [] 936 for i in range(display_count): 937 times.insert( 938 random.randrange(0, len(times) + 1), 939 (1.9 + i * 0.05, 2.3 + i * 0.05), 940 ) 941 for i in range(display_count): 942 try: 943 if display_scores[i][1] is None: 944 name_str = '-' 945 else: 946 # noinspection PyUnresolvedReferences 947 name_str = ', '.join( 948 [p['name'] for p in display_scores[i][1]['players']] 949 ) 950 except Exception: 951 logging.exception( 952 'Error calcing name_str for %s.', display_scores 953 ) 954 name_str = '-' 955 if display_scores[i] == our_score and not showed_ours: 956 flash = True 957 color0 = (0.6, 0.4, 0.1, 1.0) 958 color1 = (0.6, 0.6, 0.6, 1.0) 959 tdelay1 = 3.7 960 tdelay2 = 3.7 961 showed_ours = True 962 else: 963 flash = False 964 color0 = (0.6, 0.4, 0.1, 1.0) 965 color1 = (0.6, 0.6, 0.6, 1.0) 966 tdelay1 = times[i][0] 967 tdelay2 = times[i][1] 968 Text( 969 ( 970 str(display_scores[i][0]) 971 if self._score_type == 'points' 972 else bs.timestring((display_scores[i][0] * 10) / 1000.0) 973 ), 974 position=( 975 ts_h_offs + 20 + h_offs_extra, 976 v_offs_extra 977 + ts_height / 2 978 + -ts_height * (i + 1) / 10 979 + v_offs 980 + 11.0, 981 ), 982 h_align=Text.HAlign.RIGHT, 983 v_align=Text.VAlign.CENTER, 984 color=color0, 985 flash=flash, 986 transition=Text.Transition.IN_RIGHT, 987 transition_delay=tdelay1, 988 ).autoretain() 989 990 Text( 991 bs.Lstr(value=name_str), 992 position=( 993 ts_h_offs + 35 + h_offs_extra, 994 v_offs_extra 995 + ts_height / 2 996 + -ts_height * (i + 1) / 10 997 + v_offs_names 998 + v_offs 999 + 11.0, 1000 ), 1001 maxwidth=80.0 + 100.0 * len(self._playerinfos), 1002 v_align=Text.VAlign.CENTER, 1003 color=color1, 1004 flash=flash, 1005 scale=scale, 1006 transition=Text.Transition.IN_RIGHT, 1007 transition_delay=tdelay2, 1008 ).autoretain() 1009 1010 # Show achievements for this level. 1011 ts_height = -150 1012 ts_h_offs = -480 1013 v_offs = 40 1014 1015 # Only make this if we don't have the button 1016 # (never want clients to see it so no need for client-only 1017 # version, etc). 1018 if self._have_achievements: 1019 if not self._account_has_achievements: 1020 Text( 1021 bs.Lstr(resource='achievementsText'), 1022 position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 3), 1023 maxwidth=210, 1024 host_only=True, 1025 transition=Text.Transition.IN_RIGHT, 1026 v_align=Text.VAlign.CENTER, 1027 scale=1.2, 1028 transition_delay=2.8, 1029 ).autoretain() 1030 1031 assert self._game_name_str is not None 1032 assert bs.app.classic is not None 1033 achievements = bs.app.classic.ach.achievements_for_coop_level( 1034 self._game_name_str 1035 ) 1036 hval = -455 1037 vval = -100 1038 tdelay = 0.0 1039 for ach in achievements: 1040 ach.create_display(hval, vval + v_offs, 3.0 + tdelay) 1041 vval -= 55 1042 tdelay += 0.250 1043 1044 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.
Inherited Members
- bascenev1._activity.Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- allow_pausing
- allow_kick_idle_players
- slow_motion
- inherits_slow_motion
- inherits_vr_overlay_center
- allow_mid_activity_joins
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- handlemessage
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- end
- create_player
- create_team
- bascenev1._dependency.DependencyComponent
- dep_is_present
- get_dynamic_deps