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