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