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