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