baclassic
Components for the classic BombSquad experience.
This package/feature-set contains functionality related to the classic BombSquad experience. Note that much legacy BombSquad code is still a bit tangled and thus this feature-set is largely inseperable from scenev1 and uiv1. Future feature-sets will be designed in a more modular way.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Components for the classic BombSquad experience. 4 5This package/feature-set contains functionality related to the classic 6BombSquad experience. Note that much legacy BombSquad code is still a 7bit tangled and thus this feature-set is largely inseperable from 8scenev1 and uiv1. Future feature-sets will be designed in a more modular 9way. 10""" 11 12# ba_meta require api 9 13 14# Note: Code relying on classic should import things from here *only* 15# for type-checking and use the versions in ba*.app.classic at runtime; 16# that way type-checking will cleanly cover the classic-not-present case 17# (ba*.app.classic being None). 18import logging 19 20from efro.util import set_canonical_module_names 21 22from baclassic._appmode import ClassicAppMode 23from baclassic._appsubsystem import ClassicAppSubsystem 24from baclassic._achievement import Achievement, AchievementSubsystem 25from baclassic._chest import ( 26 ChestAppearanceDisplayInfo, 27 CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, 28 CHEST_APPEARANCE_DISPLAY_INFOS, 29) 30from baclassic._displayitem import show_display_item 31 32__all__ = [ 33 'ChestAppearanceDisplayInfo', 34 'CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT', 35 'CHEST_APPEARANCE_DISPLAY_INFOS', 36 'ClassicAppMode', 37 'ClassicAppSubsystem', 38 'Achievement', 39 'AchievementSubsystem', 40 'show_display_item', 41] 42 43# We want stuff here to show up as packagename.Foo instead of 44# packagename._submodule.Foo. 45set_canonical_module_names(globals()) 46 47# Sanity check: we want to keep ballistica's dependencies and 48# bootstrapping order clearly defined; let's check a few particular 49# modules to make sure they never directly or indirectly import us 50# before their own execs complete. 51if __debug__: 52 for _mdl in 'babase', '_babase': 53 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 54 logging.warning( 55 '%s was imported before %s finished importing;' 56 ' should not happen.', 57 __name__, 58 _mdl, 59 )
16@dataclass 17class ChestAppearanceDisplayInfo: 18 """Info about how to locally display chest appearances.""" 19 20 # NOTE TO SELF: Don't rename these attrs; the C++ layer is hard 21 # coded to look for them. 22 23 texclosed: str 24 texclosedtint: str 25 texopen: str 26 texopentint: str 27 color: tuple[float, float, float] 28 tint: tuple[float, float, float] 29 tint2: tuple[float, float, float]
Info about how to locally display chest appearances.
29class ClassicAppMode(babase.AppMode): 30 """AppMode for the classic BombSquad experience.""" 31 32 LEAGUE_VIS_VALS_CONFIG_KEY = 'ClassicLeagueVisVals' 33 34 def __init__(self) -> None: 35 self._on_primary_account_changed_callback: ( 36 CallbackRegistration | None 37 ) = None 38 self._on_connectivity_changed_callback: CallbackRegistration | None = ( 39 None 40 ) 41 self._test_sub: babase.CloudSubscription | None = None 42 self._account_data_sub: babase.CloudSubscription | None = None 43 44 self._have_account_values = False 45 self._have_connectivity = False 46 self._current_account_id: str | None = None 47 self._should_restore_account_league_vis_vals = False 48 49 @override 50 @classmethod 51 def get_app_experience(cls) -> AppExperience: 52 return AppExperience.MELEE 53 54 @override 55 @classmethod 56 def _can_handle_intent(cls, intent: babase.AppIntent) -> bool: 57 # We support default and exec intents currently. 58 return isinstance( 59 intent, babase.AppIntentExec | babase.AppIntentDefault 60 ) 61 62 @override 63 def handle_intent(self, intent: babase.AppIntent) -> None: 64 if isinstance(intent, babase.AppIntentExec): 65 _baclassic.classic_app_mode_handle_app_intent_exec(intent.code) 66 return 67 assert isinstance(intent, babase.AppIntentDefault) 68 _baclassic.classic_app_mode_handle_app_intent_default() 69 70 @override 71 def on_activate(self) -> None: 72 73 # Let the native layer do its thing. 74 _baclassic.classic_app_mode_activate() 75 76 app = babase.app 77 plus = app.plus 78 assert plus is not None 79 80 # Wire up the root ui to do what we want. 81 ui = app.ui_v1 82 ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = ( 83 self._root_ui_account_press 84 ) 85 ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = ( 86 self._root_ui_menu_press 87 ) 88 ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = ( 89 self._root_ui_squad_press 90 ) 91 ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = ( 92 self._root_ui_settings_press 93 ) 94 ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = ( 95 self._root_ui_store_press 96 ) 97 ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = ( 98 self._root_ui_inventory_press 99 ) 100 ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = ( 101 self._root_ui_get_tokens_press 102 ) 103 ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = ( 104 self._root_ui_inbox_press 105 ) 106 ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = ( 107 self._root_ui_tickets_meter_press 108 ) 109 ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = ( 110 self._root_ui_tokens_meter_press 111 ) 112 ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = ( 113 self._root_ui_trophy_meter_press 114 ) 115 ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = ( 116 self._root_ui_level_meter_press 117 ) 118 ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = ( 119 self._root_ui_achievements_press 120 ) 121 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_0] = partial( 122 self._root_ui_chest_slot_pressed, 0 123 ) 124 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial( 125 self._root_ui_chest_slot_pressed, 1 126 ) 127 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial( 128 self._root_ui_chest_slot_pressed, 2 129 ) 130 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial( 131 self._root_ui_chest_slot_pressed, 3 132 ) 133 134 # We want to be informed when connectivity changes. 135 self._on_connectivity_changed_callback = ( 136 plus.cloud.on_connectivity_changed_callbacks.register( 137 self._update_for_connectivity_change 138 ) 139 ) 140 # We want to be informed when primary account changes. 141 self._on_primary_account_changed_callback = ( 142 plus.accounts.on_primary_account_changed_callbacks.register( 143 self._update_for_primary_account 144 ) 145 ) 146 # Establish subscriptions/etc. for any current primary account. 147 self._update_for_primary_account(plus.accounts.primary) 148 self._have_connectivity = plus.cloud.is_connected() 149 self._update_for_connectivity_change(self._have_connectivity) 150 151 @override 152 def on_deactivate(self) -> None: 153 154 classic = babase.app.classic 155 156 # Store latest league vis vals for any active account. 157 self._save_account_league_vis_vals() 158 159 # Stop being informed of account changes. 160 self._on_primary_account_changed_callback = None 161 162 # Remove anything following any current account. 163 self._update_for_primary_account(None) 164 165 # Save where we were in the UI so we return there next time. 166 if classic is not None: 167 classic.save_ui_state() 168 169 # Let the native layer do its thing. 170 _baclassic.classic_app_mode_deactivate() 171 172 @override 173 def on_app_active_changed(self) -> None: 174 if not babase.app.active: 175 # If we've gone inactive, bring up the main menu, which has the 176 # side effect of pausing the action (when possible). 177 babase.invoke_main_menu() 178 179 # Also store any league vis state for the active account. 180 # this may be our last chance to do this on mobile. 181 self._save_account_league_vis_vals() 182 183 def _update_for_primary_account( 184 self, account: babase.AccountV2Handle | None 185 ) -> None: 186 """Update subscriptions/etc. for a new primary account state.""" 187 assert babase.in_logic_thread() 188 plus = babase.app.plus 189 190 assert plus is not None 191 192 classic = babase.app.classic 193 assert classic is not None 194 195 if account is not None: 196 self._current_account_id = account.accountid 197 babase.set_ui_account_state(True, account.tag) 198 self._should_restore_account_league_vis_vals = True 199 else: 200 # If we had an account, save any existing league vis state 201 # so we'll properly animate to new values the next time we 202 # sign in. 203 self._save_account_league_vis_vals() 204 205 self._current_account_id = None 206 babase.set_ui_account_state(False) 207 self._should_restore_account_league_vis_vals = False 208 209 # For testing subscription functionality. 210 if os.environ.get('BA_SUBSCRIPTION_TEST') == '1': 211 if account is None: 212 self._test_sub = None 213 else: 214 with account: 215 self._test_sub = plus.cloud.subscribe_test( 216 self._on_sub_test_update 217 ) 218 else: 219 self._test_sub = None 220 221 if account is None: 222 classic.gold_pass = False 223 classic.chest_dock_full = False 224 classic.remove_ads = False 225 self._account_data_sub = None 226 _baclassic.set_root_ui_account_values( 227 tickets=-1, 228 tokens=-1, 229 league_type='', 230 league_number=-1, 231 league_rank=-1, 232 achievements_percent_text='', 233 level_text='', 234 xp_text='', 235 inbox_count_text='', 236 gold_pass=False, 237 chest_0_appearance='', 238 chest_1_appearance='', 239 chest_2_appearance='', 240 chest_3_appearance='', 241 chest_0_unlock_time=-1.0, 242 chest_1_unlock_time=-1.0, 243 chest_2_unlock_time=-1.0, 244 chest_3_unlock_time=-1.0, 245 chest_0_ad_allow_time=-1.0, 246 chest_1_ad_allow_time=-1.0, 247 chest_2_ad_allow_time=-1.0, 248 chest_3_ad_allow_time=-1.0, 249 ) 250 self._have_account_values = False 251 self._update_ui_live_state() 252 253 else: 254 with account: 255 self._account_data_sub = ( 256 plus.cloud.subscribe_classic_account_data( 257 self._on_classic_account_data_change 258 ) 259 ) 260 261 def _update_for_connectivity_change(self, connected: bool) -> None: 262 """Update when the app's connectivity state changes.""" 263 self._have_connectivity = connected 264 self._update_ui_live_state() 265 266 def _update_ui_live_state(self) -> None: 267 # We want to show ui elements faded if we don't have a live 268 # connection to the master-server OR if we haven't received a 269 # set of account values from them yet. If we just plug in raw 270 # connectivity state here we get UI stuff un-fading a moment or 271 # two before values appear (since the subscriptions have not 272 # sent us any values yet) which looks odd. 273 _baclassic.set_root_ui_have_live_values( 274 self._have_connectivity and self._have_account_values 275 ) 276 277 def _on_sub_test_update(self, val: int | None) -> None: 278 print(f'GOT SUB TEST UPDATE: {val}') 279 280 def _on_classic_account_data_change( 281 self, val: bacommon.bs.ClassicAccountLiveData 282 ) -> None: 283 # print('ACCOUNT CHANGED:', val) 284 achp = round(val.achievements / max(val.achievements_total, 1) * 100.0) 285 ibc = str(val.inbox_count) 286 if val.inbox_count_is_max: 287 ibc += '+' 288 289 chest0 = val.chests.get('0') 290 chest1 = val.chests.get('1') 291 chest2 = val.chests.get('2') 292 chest3 = val.chests.get('3') 293 294 # Keep a few handy values on classic updated with the latest 295 # data. 296 classic = babase.app.classic 297 assert classic is not None 298 classic.remove_ads = val.remove_ads 299 classic.gold_pass = val.gold_pass 300 classic.chest_dock_full = ( 301 chest0 is not None 302 and chest1 is not None 303 and chest2 is not None 304 and chest3 is not None 305 ) 306 307 _baclassic.set_root_ui_account_values( 308 tickets=val.tickets, 309 tokens=val.tokens, 310 league_type=( 311 '' if val.league_type is None else val.league_type.value 312 ), 313 league_number=(-1 if val.league_num is None else val.league_num), 314 league_rank=(-1 if val.league_rank is None else val.league_rank), 315 achievements_percent_text=f'{achp}%', 316 level_text=str(val.level), 317 xp_text=f'{val.xp}/{val.xpmax}', 318 inbox_count_text=ibc, 319 gold_pass=val.gold_pass, 320 chest_0_appearance=( 321 '' if chest0 is None else chest0.appearance.value 322 ), 323 chest_1_appearance=( 324 '' if chest1 is None else chest1.appearance.value 325 ), 326 chest_2_appearance=( 327 '' if chest2 is None else chest2.appearance.value 328 ), 329 chest_3_appearance=( 330 '' if chest3 is None else chest3.appearance.value 331 ), 332 chest_0_unlock_time=( 333 -1.0 if chest0 is None else chest0.unlock_time.timestamp() 334 ), 335 chest_1_unlock_time=( 336 -1.0 if chest1 is None else chest1.unlock_time.timestamp() 337 ), 338 chest_2_unlock_time=( 339 -1.0 if chest2 is None else chest2.unlock_time.timestamp() 340 ), 341 chest_3_unlock_time=( 342 -1.0 if chest3 is None else chest3.unlock_time.timestamp() 343 ), 344 chest_0_ad_allow_time=( 345 -1.0 346 if chest0 is None or chest0.ad_allow_time is None 347 else chest0.ad_allow_time.timestamp() 348 ), 349 chest_1_ad_allow_time=( 350 -1.0 351 if chest1 is None or chest1.ad_allow_time is None 352 else chest1.ad_allow_time.timestamp() 353 ), 354 chest_2_ad_allow_time=( 355 -1.0 356 if chest2 is None or chest2.ad_allow_time is None 357 else chest2.ad_allow_time.timestamp() 358 ), 359 chest_3_ad_allow_time=( 360 -1.0 361 if chest3 is None or chest3.ad_allow_time is None 362 else chest3.ad_allow_time.timestamp() 363 ), 364 ) 365 if self._should_restore_account_league_vis_vals: 366 # If we have previous league vis vals for this account, 367 # restore them. This will cause us to animate or otherwise 368 # display league changes that have occurred since we were 369 # last visible. Note we need to do this *after* setting real 370 # vals so there is something to animate to. 371 self._restore_account_league_vis_vals() 372 self._should_restore_account_league_vis_vals = False 373 374 # Note that we have values and updated faded state accordingly. 375 self._have_account_values = True 376 self._update_ui_live_state() 377 378 def _root_ui_menu_press(self) -> None: 379 from babase import push_back_press 380 381 ui = babase.app.ui_v1 382 383 # If *any* main-window is up, kill it and resume play. 384 old_window = ui.get_main_window() 385 if old_window is not None: 386 387 classic = babase.app.classic 388 assert classic is not None 389 classic.resume() 390 391 ui.clear_main_window() 392 return 393 394 # Otherwise 395 push_back_press() 396 397 def _root_ui_account_press(self) -> None: 398 from bauiv1lib.account.settings import AccountSettingsWindow 399 400 self._auxiliary_window_nav( 401 win_type=AccountSettingsWindow, 402 win_create_call=lambda: AccountSettingsWindow( 403 origin_widget=bauiv1.get_special_widget('account_button') 404 ), 405 ) 406 407 def _root_ui_squad_press(self) -> None: 408 btn = bauiv1.get_special_widget('squad_button') 409 center = btn.get_screen_space_center() 410 if bauiv1.app.classic is not None: 411 bauiv1.app.classic.party_icon_activate(center) 412 else: 413 logging.warning('party_icon_activate: no classic.') 414 415 def _root_ui_settings_press(self) -> None: 416 from bauiv1lib.settings.allsettings import AllSettingsWindow 417 418 self._auxiliary_window_nav( 419 win_type=AllSettingsWindow, 420 win_create_call=lambda: AllSettingsWindow( 421 origin_widget=bauiv1.get_special_widget('settings_button') 422 ), 423 ) 424 425 def _auxiliary_window_nav( 426 self, 427 win_type: type[bauiv1.MainWindow], 428 win_create_call: Callable[[], bauiv1.MainWindow], 429 ) -> None: 430 """Navigate to or away from an Auxiliary window. 431 432 Auxiliary windows can be thought of as 'side quests' in the 433 window hierarchy; places such as settings windows or league 434 ranking windows that the user might want to visit without losing 435 their place in the regular hierarchy. 436 """ 437 # pylint: disable=unidiomatic-typecheck 438 439 ui = babase.app.ui_v1 440 441 current_main_window = ui.get_main_window() 442 443 # Scan our ancestors for auxiliary states matching our type as 444 # well as auxiliary states in general. 445 aux_matching_state: bauiv1.MainWindowState | None = None 446 aux_state: bauiv1.MainWindowState | None = None 447 448 if current_main_window is None: 449 raise RuntimeError( 450 'Not currently handling no-top-level-window case.' 451 ) 452 453 state = current_main_window.main_window_back_state 454 while state is not None: 455 assert state.window_type is not None 456 if state.is_auxiliary: 457 if state.window_type is win_type: 458 aux_matching_state = state 459 else: 460 aux_state = state 461 462 state = state.parent 463 464 # If there's an ancestor auxiliary window-state matching our 465 # type, back out past it (example: poking settings, navigating 466 # down a level or two, and then poking settings again should 467 # back out of settings). 468 if aux_matching_state is not None: 469 current_main_window.main_window_back_state = ( 470 aux_matching_state.parent 471 ) 472 current_main_window.main_window_back() 473 return 474 475 # If there's an ancestory auxiliary state *not* matching our 476 # type, crop the state and swap in our new auxiliary UI 477 # (example: poking settings, then poking account, then poking 478 # back should end up where things were before the settings 479 # poke). 480 if aux_state is not None: 481 # Blow away the window stack and build a fresh one. 482 ui.clear_main_window() 483 ui.set_main_window( 484 win_create_call(), 485 from_window=False, # Disable from-check. 486 back_state=aux_state.parent, 487 suppress_warning=True, 488 is_auxiliary=True, 489 ) 490 return 491 492 # Ok, no auxiliary states found. Now if current window is 493 # auxiliary and the type matches, simply do a back. 494 if ( 495 current_main_window.main_window_is_auxiliary 496 and type(current_main_window) is win_type 497 ): 498 current_main_window.main_window_back() 499 return 500 501 # If current window is auxiliary but type doesn't match, 502 # swap it out for our new auxiliary UI. 503 if current_main_window.main_window_is_auxiliary: 504 ui.clear_main_window() 505 ui.set_main_window( 506 win_create_call(), 507 from_window=False, # Disable from-check. 508 back_state=current_main_window.main_window_back_state, 509 suppress_warning=True, 510 is_auxiliary=True, 511 ) 512 return 513 514 # Ok, no existing auxiliary stuff was found period. Just 515 # navigate forward to this UI. 516 current_main_window.main_window_replace( 517 win_create_call(), is_auxiliary=True 518 ) 519 520 def _root_ui_achievements_press(self) -> None: 521 from bauiv1lib.achievements import AchievementsWindow 522 523 if not self._ensure_signed_in_v1(): 524 return 525 526 wait_for_connectivity( 527 on_connected=lambda: self._auxiliary_window_nav( 528 win_type=AchievementsWindow, 529 win_create_call=lambda: AchievementsWindow( 530 origin_widget=bauiv1.get_special_widget( 531 'achievements_button' 532 ) 533 ), 534 ) 535 ) 536 537 def _root_ui_inbox_press(self) -> None: 538 from bauiv1lib.inbox import InboxWindow 539 540 if not self._ensure_signed_in(): 541 return 542 543 wait_for_connectivity( 544 on_connected=lambda: self._auxiliary_window_nav( 545 win_type=InboxWindow, 546 win_create_call=lambda: InboxWindow( 547 origin_widget=bauiv1.get_special_widget('inbox_button') 548 ), 549 ) 550 ) 551 552 def _root_ui_store_press(self) -> None: 553 from bauiv1lib.store.browser import StoreBrowserWindow 554 555 if not self._ensure_signed_in_v1(): 556 return 557 558 wait_for_connectivity( 559 on_connected=lambda: self._auxiliary_window_nav( 560 win_type=StoreBrowserWindow, 561 win_create_call=lambda: StoreBrowserWindow( 562 origin_widget=bauiv1.get_special_widget('store_button') 563 ), 564 ) 565 ) 566 567 def _root_ui_tickets_meter_press(self) -> None: 568 from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 569 570 ResourceTypeInfoWindow( 571 'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter') 572 ) 573 574 def _root_ui_tokens_meter_press(self) -> None: 575 from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 576 577 ResourceTypeInfoWindow( 578 'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter') 579 ) 580 581 def _root_ui_trophy_meter_press(self) -> None: 582 from bauiv1lib.league.rankwindow import LeagueRankWindow 583 584 if not self._ensure_signed_in_v1(): 585 return 586 587 self._auxiliary_window_nav( 588 win_type=LeagueRankWindow, 589 win_create_call=lambda: LeagueRankWindow( 590 origin_widget=bauiv1.get_special_widget('trophy_meter') 591 ), 592 ) 593 594 def _root_ui_level_meter_press(self) -> None: 595 from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 596 597 ResourceTypeInfoWindow( 598 'xp', origin_widget=bauiv1.get_special_widget('level_meter') 599 ) 600 601 def _root_ui_inventory_press(self) -> None: 602 from bauiv1lib.inventory import InventoryWindow 603 604 if not self._ensure_signed_in_v1(): 605 return 606 607 self._auxiliary_window_nav( 608 win_type=InventoryWindow, 609 win_create_call=lambda: InventoryWindow( 610 origin_widget=bauiv1.get_special_widget('inventory_button') 611 ), 612 ) 613 614 def _ensure_signed_in(self) -> bool: 615 """Make sure we're signed in (requiring modern v2 accounts).""" 616 plus = bauiv1.app.plus 617 if plus is None: 618 bauiv1.screenmessage('This requires plus.', color=(1, 0, 0)) 619 bauiv1.getsound('error').play() 620 return False 621 if plus.accounts.primary is None: 622 show_sign_in_prompt() 623 return False 624 return True 625 626 def _ensure_signed_in_v1(self) -> bool: 627 """Make sure we're signed in (allowing legacy v1-only accounts).""" 628 plus = bauiv1.app.plus 629 if plus is None: 630 bauiv1.screenmessage('This requires plus.', color=(1, 0, 0)) 631 bauiv1.getsound('error').play() 632 return False 633 if plus.get_v1_account_state() != 'signed_in': 634 show_sign_in_prompt() 635 return False 636 return True 637 638 def _root_ui_get_tokens_press(self) -> None: 639 from bauiv1lib.gettokens import GetTokensWindow 640 641 if not self._ensure_signed_in(): 642 return 643 644 self._auxiliary_window_nav( 645 win_type=GetTokensWindow, 646 win_create_call=lambda: GetTokensWindow( 647 origin_widget=bauiv1.get_special_widget('get_tokens_button') 648 ), 649 ) 650 651 def _root_ui_chest_slot_pressed(self, index: int) -> None: 652 from bauiv1lib.chest import ( 653 ChestWindow0, 654 ChestWindow1, 655 ChestWindow2, 656 ChestWindow3, 657 ) 658 659 widgetid: Literal[ 660 'chest_0_button', 661 'chest_1_button', 662 'chest_2_button', 663 'chest_3_button', 664 ] 665 winclass: type[ChestWindow] 666 if index == 0: 667 widgetid = 'chest_0_button' 668 winclass = ChestWindow0 669 elif index == 1: 670 widgetid = 'chest_1_button' 671 winclass = ChestWindow1 672 elif index == 2: 673 widgetid = 'chest_2_button' 674 winclass = ChestWindow2 675 elif index == 3: 676 widgetid = 'chest_3_button' 677 winclass = ChestWindow3 678 else: 679 raise RuntimeError(f'Invalid index {index}') 680 681 wait_for_connectivity( 682 on_connected=lambda: self._auxiliary_window_nav( 683 win_type=winclass, 684 win_create_call=lambda: winclass( 685 index=index, 686 origin_widget=bauiv1.get_special_widget(widgetid), 687 ), 688 ) 689 ) 690 691 def _save_account_league_vis_vals(self) -> None: 692 693 # If we currently have an account, save any currently-displayed 694 # league info alongside the account id. We'll then restore this 695 # state as a starting point the next time we are active, meaning 696 # any changes that occur while we're away will be animated for 697 # the user to see. 698 699 if self._current_account_id is not None: 700 vals = _baclassic.get_root_ui_account_league_vis_values() 701 if vals is not None: 702 assert 'a' not in vals 703 vals['a'] = self._current_account_id 704 cfg = babase.app.config 705 cfg[self.LEAGUE_VIS_VALS_CONFIG_KEY] = vals 706 cfg.commit() 707 708 def _restore_account_league_vis_vals(self) -> None: 709 710 # If we currently have an account and it matches vis-vals we have 711 # stored in the config, restore those values. 712 if self._current_account_id is not None: 713 cfg = babase.app.config 714 vals = cfg.get(self.LEAGUE_VIS_VALS_CONFIG_KEY) 715 if isinstance(vals, dict): 716 valsaccount = vals.get('a') 717 if ( 718 isinstance(valsaccount, str) 719 and valsaccount == self._current_account_id 720 ): 721 _baclassic.set_root_ui_account_league_vis_values(vals) 722 723 def on_engine_will_reset(self) -> None: 724 """Called just before classic resets the engine. 725 726 This happens at various times such as session switches. 727 """ 728 729 self._save_account_league_vis_vals() 730 731 def on_engine_did_reset(self) -> None: 732 """Called just after classic resets the engine. 733 734 This happens at various times such as session switches. 735 """ 736 737 # Restore any old league vis state we had; this allows the user 738 # to see league improvements/etc. that occurred while we were 739 # gone. 740 self._restore_account_league_vis_vals()
AppMode for the classic BombSquad experience.
49 @override 50 @classmethod 51 def get_app_experience(cls) -> AppExperience: 52 return AppExperience.MELEE
Return the overall experience provided by this mode.
62 @override 63 def handle_intent(self, intent: babase.AppIntent) -> None: 64 if isinstance(intent, babase.AppIntentExec): 65 _baclassic.classic_app_mode_handle_app_intent_exec(intent.code) 66 return 67 assert isinstance(intent, babase.AppIntentDefault) 68 _baclassic.classic_app_mode_handle_app_intent_default()
Handle an intent.
70 @override 71 def on_activate(self) -> None: 72 73 # Let the native layer do its thing. 74 _baclassic.classic_app_mode_activate() 75 76 app = babase.app 77 plus = app.plus 78 assert plus is not None 79 80 # Wire up the root ui to do what we want. 81 ui = app.ui_v1 82 ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = ( 83 self._root_ui_account_press 84 ) 85 ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = ( 86 self._root_ui_menu_press 87 ) 88 ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = ( 89 self._root_ui_squad_press 90 ) 91 ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = ( 92 self._root_ui_settings_press 93 ) 94 ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = ( 95 self._root_ui_store_press 96 ) 97 ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = ( 98 self._root_ui_inventory_press 99 ) 100 ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = ( 101 self._root_ui_get_tokens_press 102 ) 103 ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = ( 104 self._root_ui_inbox_press 105 ) 106 ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = ( 107 self._root_ui_tickets_meter_press 108 ) 109 ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = ( 110 self._root_ui_tokens_meter_press 111 ) 112 ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = ( 113 self._root_ui_trophy_meter_press 114 ) 115 ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = ( 116 self._root_ui_level_meter_press 117 ) 118 ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = ( 119 self._root_ui_achievements_press 120 ) 121 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_0] = partial( 122 self._root_ui_chest_slot_pressed, 0 123 ) 124 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial( 125 self._root_ui_chest_slot_pressed, 1 126 ) 127 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial( 128 self._root_ui_chest_slot_pressed, 2 129 ) 130 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial( 131 self._root_ui_chest_slot_pressed, 3 132 ) 133 134 # We want to be informed when connectivity changes. 135 self._on_connectivity_changed_callback = ( 136 plus.cloud.on_connectivity_changed_callbacks.register( 137 self._update_for_connectivity_change 138 ) 139 ) 140 # We want to be informed when primary account changes. 141 self._on_primary_account_changed_callback = ( 142 plus.accounts.on_primary_account_changed_callbacks.register( 143 self._update_for_primary_account 144 ) 145 ) 146 # Establish subscriptions/etc. for any current primary account. 147 self._update_for_primary_account(plus.accounts.primary) 148 self._have_connectivity = plus.cloud.is_connected() 149 self._update_for_connectivity_change(self._have_connectivity)
Called when the mode is becoming the active one fro the app.
151 @override 152 def on_deactivate(self) -> None: 153 154 classic = babase.app.classic 155 156 # Store latest league vis vals for any active account. 157 self._save_account_league_vis_vals() 158 159 # Stop being informed of account changes. 160 self._on_primary_account_changed_callback = None 161 162 # Remove anything following any current account. 163 self._update_for_primary_account(None) 164 165 # Save where we were in the UI so we return there next time. 166 if classic is not None: 167 classic.save_ui_state() 168 169 # Let the native layer do its thing. 170 _baclassic.classic_app_mode_deactivate()
Called when the mode stops being the active one for the app.
Note: On platforms where the app is explicitly exited (such as desktop PC) this will also be called at app shutdown.
To best cover both mobile and desktop style platforms, actions such as saving state should generally happen in response to both on_deactivate() and on_app_active_changed() (when active is False).
172 @override 173 def on_app_active_changed(self) -> None: 174 if not babase.app.active: 175 # If we've gone inactive, bring up the main menu, which has the 176 # side effect of pausing the action (when possible). 177 babase.invoke_main_menu() 178 179 # Also store any league vis state for the active account. 180 # this may be our last chance to do this on mobile. 181 self._save_account_league_vis_vals()
Called when ba*.app.active changes while in this app-mode.
Active state becomes false when the app is hidden, minimized, backgrounded, etc. The app-mode may want to take action such as pausing a running game or saving state when this occurs.
Note: On platforms such as mobile where apps get suspended and later silently terminated by the OS, this is likely to be the last reliable place to save state/etc.
To best cover both mobile and desktop style platforms, actions such as saving state should generally happen in response to both on_deactivate() and on_app_active_changed() (when active is False).
723 def on_engine_will_reset(self) -> None: 724 """Called just before classic resets the engine. 725 726 This happens at various times such as session switches. 727 """ 728 729 self._save_account_league_vis_vals()
Called just before classic resets the engine.
This happens at various times such as session switches.
731 def on_engine_did_reset(self) -> None: 732 """Called just after classic resets the engine. 733 734 This happens at various times such as session switches. 735 """ 736 737 # Restore any old league vis state we had; this allows the user 738 # to see league improvements/etc. that occurred while we were 739 # gone. 740 self._restore_account_league_vis_vals()
Called just after classic resets the engine.
This happens at various times such as session switches.
38class ClassicAppSubsystem(babase.AppSubsystem): 39 """Subsystem for classic functionality in the app. 40 41 The single shared instance of this app can be accessed at 42 babase.app.classic. Note that it is possible for babase.app.classic to 43 be None if the classic package is not present, and code should handle 44 that case gracefully. 45 """ 46 47 # pylint: disable=too-many-public-methods 48 49 from baclassic._music import MusicPlayMode 50 51 def __init__(self) -> None: 52 super().__init__() 53 self._env = babase.env() 54 55 self.accounts = AccountV1Subsystem() 56 self.ads = AdsSubsystem() 57 self.ach = AchievementSubsystem() 58 self.store = StoreSubsystem() 59 self.music = MusicSubsystem() 60 61 # Co-op Campaigns. 62 self.campaigns: dict[str, bascenev1.Campaign] = {} 63 self.custom_coop_practice_games: list[str] = [] 64 65 # Lobby. 66 self.lobby_random_profile_index: int = 1 67 self.lobby_random_char_index_offset = random.randrange(1000) 68 self.lobby_account_profile_device_id: int | None = None 69 70 # Misc. 71 self.tips: list[str] = [] 72 self.stress_test_update_timer: babase.AppTimer | None = None 73 self.stress_test_update_timer_2: babase.AppTimer | None = None 74 self.value_test_defaults: dict = {} 75 self.ping_thread_count = 0 76 self.allow_ticket_purchases: bool = True 77 78 # Classic-specific account state. 79 self.remove_ads = False 80 self.gold_pass = False 81 self.chest_dock_full = False 82 83 # Main Menu. 84 self.main_menu_did_initial_transition = False 85 self.main_menu_last_news_fetch_time: float | None = None 86 87 # Spaz. 88 self.spaz_appearances: dict[str, spazappearance.Appearance] = {} 89 self.last_spaz_turbo_warn_time = babase.AppTime(-99999.0) 90 91 # Server Mode. 92 self.server: ServerController | None = None 93 94 self.log_have_new = False 95 self.log_upload_timer_started = False 96 self.printed_live_object_warning = False 97 98 # We include this extra hash with shared input-mapping names so 99 # that we don't share mappings between differently-configured 100 # systems. For instance, different android devices may give 101 # different key values for the same controller type so we keep 102 # their mappings distinct. 103 self.input_map_hash: str | None = None 104 105 # Maps. 106 self.maps: dict[str, type[bascenev1.Map]] = {} 107 108 # Gameplay. 109 self.teams_series_length = 7 # Deprecated, left for old mods. 110 self.ffa_series_length = 24 # Deprecated, left for old mods. 111 self.coop_session_args: dict = {} 112 113 # UI. 114 self.first_main_menu = True # FIXME: Move to mainmenu class. 115 self.did_menu_intro = False # FIXME: Move to mainmenu class. 116 self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu. 117 self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any. 118 self.party_window: weakref.ref[PartyWindow] | None = None 119 self.main_menu_resume_callbacks: list = [] 120 self.saved_ui_state: bauiv1.MainWindowState | None = None 121 122 # Store. 123 self.store_layout: dict[str, list[dict[str, Any]]] | None = None 124 self.store_items: dict[str, dict] | None = None 125 self.pro_sale_start_time: int | None = None 126 self.pro_sale_start_val: int | None = None 127 128 def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: 129 """(internal)""" 130 131 # If there's no main window up, just call immediately. 132 if not babase.app.ui_v1.has_main_window(): 133 with babase.ContextRef.empty(): 134 call() 135 else: 136 self.main_menu_resume_callbacks.append(call) 137 138 @property 139 def platform(self) -> str: 140 """Name of the current platform. 141 142 Examples are: 'mac', 'windows', android'. 143 """ 144 assert isinstance(self._env['platform'], str) 145 return self._env['platform'] 146 147 def scene_v1_protocol_version(self) -> int: 148 """(internal)""" 149 return bascenev1.protocol_version() 150 151 @property 152 def subplatform(self) -> str: 153 """String for subplatform. 154 155 Can be empty. For the 'android' platform, subplatform may 156 be 'google', 'amazon', etc. 157 """ 158 assert isinstance(self._env['subplatform'], str) 159 return self._env['subplatform'] 160 161 @property 162 def legacy_user_agent_string(self) -> str: 163 """String containing various bits of info about OS/device/etc.""" 164 assert isinstance(self._env['legacy_user_agent_string'], str) 165 return self._env['legacy_user_agent_string'] 166 167 @override 168 def on_app_loading(self) -> None: 169 from bascenev1lib.actor import spazappearance 170 from bascenev1lib import maps as stdmaps 171 172 plus = babase.app.plus 173 assert plus is not None 174 175 env = babase.app.env 176 cfg = babase.app.config 177 178 self.music.on_app_loading() 179 180 # Non-test, non-debug builds should generally be blessed; warn 181 # if not (so I don't accidentally release a build that can't 182 # play tourneys). 183 if not env.debug and not env.test and not plus.is_blessed(): 184 babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) 185 186 stdmaps.register_all_maps() 187 188 spazappearance.register_appearances() 189 bascenev1.init_campaigns() 190 191 launch_count = cfg.get('launchCount', 0) 192 launch_count += 1 193 194 # So we know how many times we've run the game at various 195 # version milestones. 196 for key in ('lc14173', 'lc14292'): 197 cfg.setdefault(key, launch_count) 198 199 cfg['launchCount'] = launch_count 200 cfg.commit() 201 202 # If there's a leftover log file, attempt to upload it to the 203 # master-server and/or get rid of it. 204 babase.handle_leftover_v1_cloud_log_file() 205 206 self.accounts.on_app_loading() 207 208 @override 209 def on_app_suspend(self) -> None: 210 self.accounts.on_app_suspend() 211 212 @override 213 def on_app_unsuspend(self) -> None: 214 self.accounts.on_app_unsuspend() 215 self.music.on_app_unsuspend() 216 217 @override 218 def on_app_shutdown(self) -> None: 219 self.music.on_app_shutdown() 220 221 def pause(self) -> None: 222 """Pause the game due to a user request or menu popping up. 223 224 If there's a foreground host-activity that says it's pausable, tell it 225 to pause. Note: we now no longer pause if there are connected clients. 226 """ 227 activity: bascenev1.Activity | None = ( 228 bascenev1.get_foreground_host_activity() 229 ) 230 if ( 231 activity is not None 232 and activity.allow_pausing 233 and not bascenev1.have_connected_clients() 234 ): 235 from babase import Lstr 236 from bascenev1 import NodeActor 237 238 # FIXME: Shouldn't be touching scene stuff here; should just 239 # pass the request on to the host-session. 240 with activity.context: 241 globs = activity.globalsnode 242 if not globs.paused: 243 bascenev1.getsound('refWhistle').play() 244 globs.paused = True 245 246 # FIXME: This should not be an attr on Actor. 247 activity.paused_text = NodeActor( 248 bascenev1.newnode( 249 'text', 250 attrs={ 251 'text': Lstr(resource='pausedByHostText'), 252 'client_only': True, 253 'flatness': 1.0, 254 'h_align': 'center', 255 }, 256 ) 257 ) 258 259 def resume(self) -> None: 260 """Resume the game due to a user request or menu closing. 261 262 If there's a foreground host-activity that's currently paused, tell it 263 to resume. 264 """ 265 266 # FIXME: Shouldn't be touching scene stuff here; should just 267 # pass the request on to the host-session. 268 activity = bascenev1.get_foreground_host_activity() 269 if activity is not None: 270 with activity.context: 271 globs = activity.globalsnode 272 if globs.paused: 273 bascenev1.getsound('refWhistle').play() 274 globs.paused = False 275 276 # FIXME: This should not be an actor attr. 277 activity.paused_text = None 278 279 def add_coop_practice_level(self, level: bascenev1.Level) -> None: 280 """Adds an individual level to the 'practice' section in Co-op.""" 281 282 # Assign this level to our catch-all campaign. 283 self.campaigns['Challenges'].addlevel(level) 284 285 # Make note to add it to our challenges UI. 286 self.custom_coop_practice_games.append(f'Challenges:{level.name}') 287 288 def launch_coop_game( 289 self, game: str, force: bool = False, args: dict | None = None 290 ) -> bool: 291 """High level way to launch a local co-op session.""" 292 # pylint: disable=cyclic-import 293 from bauiv1lib.coop.level import CoopLevelLockedWindow 294 295 assert babase.app.classic is not None 296 297 if args is None: 298 args = {} 299 if game == '': 300 raise ValueError('empty game name') 301 campaignname, levelname = game.split(':') 302 campaign = babase.app.classic.getcampaign(campaignname) 303 304 # If this campaign is sequential, make sure we've completed the 305 # one before this. 306 if campaign.sequential and not force: 307 for level in campaign.levels: 308 if level.name == levelname: 309 break 310 if not level.complete: 311 CoopLevelLockedWindow( 312 campaign.getlevel(levelname).displayname, 313 campaign.getlevel(level.name).displayname, 314 ) 315 return False 316 317 # Save where we are in the UI to come back to when done. 318 babase.app.classic.save_ui_state() 319 320 # Ok, we're good to go. 321 self.coop_session_args = { 322 'campaign': campaignname, 323 'level': levelname, 324 } 325 for arg_name, arg_val in list(args.items()): 326 self.coop_session_args[arg_name] = arg_val 327 328 def _fade_end() -> None: 329 from bascenev1 import CoopSession 330 331 try: 332 bascenev1.new_host_session(CoopSession) 333 except Exception: 334 logging.exception('Error creating coopsession after fade end.') 335 from bascenev1lib.mainmenu import MainMenuSession 336 337 bascenev1.new_host_session(MainMenuSession) 338 339 babase.fade_screen(False, endcall=_fade_end) 340 return True 341 342 def return_to_main_menu_session_gracefully( 343 self, reset_ui: bool = True 344 ) -> None: 345 """Attempt to cleanly get back to the main menu.""" 346 # pylint: disable=cyclic-import 347 from baclassic import _benchmark 348 from bascenev1lib.mainmenu import MainMenuSession 349 350 plus = babase.app.plus 351 assert plus is not None 352 353 if reset_ui: 354 babase.app.ui_v1.clear_main_window() 355 356 if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession): 357 # It may be possible we're on the main menu but the screen 358 # is faded so fade back in. 359 babase.fade_screen(True) 360 return 361 362 _benchmark.stop_stress_test() # Stop stress-test if in progress. 363 364 # If we're in a host-session, tell them to end. This lets them 365 # tear themselves down gracefully. 366 host_session: bascenev1.Session | None = ( 367 bascenev1.get_foreground_host_session() 368 ) 369 if host_session is not None: 370 # Kick off a little transaction so we'll hopefully have all 371 # the latest account state when we get back to the menu. 372 plus.add_v1_account_transaction( 373 {'type': 'END_SESSION', 'sType': str(type(host_session))} 374 ) 375 plus.run_v1_account_transactions() 376 377 host_session.end() 378 379 # Otherwise just force the issue. 380 else: 381 babase.pushcall( 382 babase.Call(bascenev1.new_host_session, MainMenuSession) 383 ) 384 385 def getmaps(self, playtype: str) -> list[str]: 386 """Return a list of bascenev1.Map types supporting a playtype str. 387 388 Category: **Asset Functions** 389 390 Maps supporting a given playtype must provide a particular set of 391 features and lend themselves to a certain style of play. 392 393 Play Types: 394 395 'melee' 396 General fighting map. 397 Has one or more 'spawn' locations. 398 399 'team_flag' 400 For games such as Capture The Flag where each team spawns by a flag. 401 Has two or more 'spawn' locations, each with a corresponding 'flag' 402 location (based on index). 403 404 'single_flag' 405 For games such as King of the Hill or Keep Away where multiple teams 406 are fighting over a single flag. 407 Has two or more 'spawn' locations and 1 'flag_default' location. 408 409 'conquest' 410 For games such as Conquest where flags are spread throughout the map 411 - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. 412 413 'king_of_the_hill' - has 2+ 'spawn' locations, 414 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations 415 416 'hockey' 417 For hockey games. 418 Has two 'goal' locations, corresponding 'spawn' locations, and one 419 'flag_default' location (for where puck spawns) 420 421 'football' 422 For football games. 423 Has two 'goal' locations, corresponding 'spawn' locations, and one 424 'flag_default' location (for where flag/ball/etc. spawns) 425 426 'race' 427 For racing games where players much touch each region in order. 428 Has two or more 'race_point' locations. 429 """ 430 return sorted( 431 key 432 for key, val in self.maps.items() 433 if playtype in val.get_play_types() 434 ) 435 436 def game_begin_analytics(self) -> None: 437 """(internal)""" 438 from baclassic import _analytics 439 440 _analytics.game_begin_analytics() 441 442 @classmethod 443 def json_prep(cls, data: Any) -> Any: 444 """Return a json-friendly version of the provided data. 445 446 This converts any tuples to lists and any bytes to strings 447 (interpreted as utf-8, ignoring errors). Logs errors (just once) 448 if any data is modified/discarded/unsupported. 449 """ 450 451 if isinstance(data, dict): 452 return dict( 453 (cls.json_prep(key), cls.json_prep(value)) 454 for key, value in list(data.items()) 455 ) 456 if isinstance(data, list): 457 return [cls.json_prep(element) for element in data] 458 if isinstance(data, tuple): 459 logging.exception('json_prep encountered tuple') 460 return [cls.json_prep(element) for element in data] 461 if isinstance(data, bytes): 462 try: 463 return data.decode(errors='ignore') 464 except Exception: 465 logging.exception('json_prep encountered utf-8 decode error') 466 return data.decode(errors='ignore') 467 if not isinstance(data, (str, float, bool, type(None), int)): 468 logging.exception( 469 'got unsupported type in json_prep: %s', type(data) 470 ) 471 return data 472 473 def master_server_v1_get( 474 self, 475 request: str, 476 data: dict[str, Any], 477 callback: MasterServerCallback | None = None, 478 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 479 ) -> None: 480 """Make a call to the master server via a http GET.""" 481 482 MasterServerV1CallThread( 483 request, 'get', data, callback, response_type 484 ).start() 485 486 def master_server_v1_post( 487 self, 488 request: str, 489 data: dict[str, Any], 490 callback: MasterServerCallback | None = None, 491 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 492 ) -> None: 493 """Make a call to the master server via a http POST.""" 494 MasterServerV1CallThread( 495 request, 'post', data, callback, response_type 496 ).start() 497 498 def set_tournament_prize_image( 499 self, entry: dict[str, Any], index: int, image: bauiv1.Widget 500 ) -> None: 501 """Given a tournament entry, return strings for its prize levels.""" 502 from baclassic import _tournament 503 504 return _tournament.set_tournament_prize_chest_image(entry, index, image) 505 506 def create_in_game_tournament_prize_image( 507 self, 508 entry: dict[str, Any], 509 index: int, 510 position: tuple[float, float], 511 ) -> None: 512 """Given a tournament entry, return strings for its prize levels.""" 513 from baclassic import _tournament 514 515 _tournament.create_in_game_tournament_prize_image( 516 entry, index, position 517 ) 518 519 def get_tournament_prize_strings( 520 self, entry: dict[str, Any], include_tickets: bool 521 ) -> list[str]: 522 """Given a tournament entry, return strings for its prize levels.""" 523 from baclassic import _tournament 524 525 return _tournament.get_tournament_prize_strings( 526 entry, include_tickets=include_tickets 527 ) 528 529 def getcampaign(self, name: str) -> bascenev1.Campaign: 530 """Return a campaign by name.""" 531 return self.campaigns[name] 532 533 def get_next_tip(self) -> str: 534 """Returns the next tip to be displayed.""" 535 if not self.tips: 536 for tip in get_all_tips(): 537 self.tips.insert(random.randint(0, len(self.tips)), tip) 538 tip = self.tips.pop() 539 return tip 540 541 def run_cpu_benchmark(self) -> None: 542 """Kick off a benchmark to test cpu speeds.""" 543 from baclassic._benchmark import run_cpu_benchmark 544 545 run_cpu_benchmark() 546 547 def run_media_reload_benchmark(self) -> None: 548 """Kick off a benchmark to test media reloading speeds.""" 549 from baclassic._benchmark import run_media_reload_benchmark 550 551 run_media_reload_benchmark() 552 553 def run_stress_test( 554 self, 555 *, 556 playlist_type: str = 'Random', 557 playlist_name: str = '__default__', 558 player_count: int = 8, 559 round_duration: int = 30, 560 attract_mode: bool = False, 561 ) -> None: 562 """Run a stress test.""" 563 from baclassic._benchmark import run_stress_test 564 565 run_stress_test( 566 playlist_type=playlist_type, 567 playlist_name=playlist_name, 568 player_count=player_count, 569 round_duration=round_duration, 570 attract_mode=attract_mode, 571 ) 572 573 def get_input_device_mapped_value( 574 self, 575 device: bascenev1.InputDevice, 576 name: str, 577 default: bool = False, 578 ) -> Any: 579 """Return a mapped value for an input device. 580 581 This checks the user config and falls back to default values 582 where available. 583 """ 584 return _input.get_input_device_mapped_value( 585 device.name, device.unique_identifier, name, default 586 ) 587 588 def get_input_device_map_hash( 589 self, inputdevice: bascenev1.InputDevice 590 ) -> str: 591 """Given an input device, return hash based on its raw input values.""" 592 del inputdevice # unused currently 593 return _input.get_input_device_map_hash() 594 595 def get_input_device_config( 596 self, inputdevice: bascenev1.InputDevice, default: bool 597 ) -> tuple[dict, str]: 598 """Given an input device, return its config dict in the app config. 599 600 The dict will be created if it does not exist. 601 """ 602 return _input.get_input_device_config( 603 inputdevice.name, inputdevice.unique_identifier, default 604 ) 605 606 def get_player_colors(self) -> list[tuple[float, float, float]]: 607 """Return user-selectable player colors.""" 608 return bascenev1.get_player_colors() 609 610 def get_player_profile_icon(self, profilename: str) -> str: 611 """Given a profile name, returns an icon string for it. 612 613 (non-account profiles only) 614 """ 615 return bascenev1.get_player_profile_icon(profilename) 616 617 def get_player_profile_colors( 618 self, 619 profilename: str | None, 620 profiles: dict[str, dict[str, Any]] | None = None, 621 ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: 622 """Given a profile, return colors for them.""" 623 return bascenev1.get_player_profile_colors(profilename, profiles) 624 625 def get_foreground_host_session(self) -> bascenev1.Session | None: 626 """(internal)""" 627 return bascenev1.get_foreground_host_session() 628 629 def get_foreground_host_activity(self) -> bascenev1.Activity | None: 630 """(internal)""" 631 return bascenev1.get_foreground_host_activity() 632 633 def value_test( 634 self, 635 arg: str, 636 change: float | None = None, 637 absolute: float | None = None, 638 ) -> float: 639 """(internal)""" 640 return _baclassic.value_test(arg, change, absolute) 641 642 def set_master_server_source(self, source: int) -> None: 643 """(internal)""" 644 bascenev1.set_master_server_source(source) 645 646 def get_game_port(self) -> int: 647 """(internal)""" 648 return bascenev1.get_game_port() 649 650 def v2_upgrade_window(self, login_name: str, code: str) -> None: 651 """(internal)""" 652 653 from bauiv1lib.v2upgrade import V2UpgradeWindow 654 655 V2UpgradeWindow(login_name, code) 656 657 def account_link_code_window(self, data: dict[str, Any]) -> None: 658 """(internal)""" 659 from bauiv1lib.account.link import AccountLinkCodeWindow 660 661 AccountLinkCodeWindow(data) 662 663 def server_dialog(self, delay: float, data: dict[str, Any]) -> None: 664 """(internal)""" 665 from bauiv1lib.serverdialog import ( 666 ServerDialogData, 667 ServerDialogWindow, 668 ) 669 670 try: 671 sddata = dataclass_from_dict(ServerDialogData, data) 672 except Exception: 673 sddata = None 674 logging.warning( 675 'Got malformatted ServerDialogData: %s', 676 data, 677 ) 678 if sddata is not None: 679 babase.apptimer( 680 delay, 681 babase.Call(ServerDialogWindow, sddata), 682 ) 683 684 def show_url_window(self, address: str) -> None: 685 """(internal)""" 686 from bauiv1lib.url import ShowURLWindow 687 688 ShowURLWindow(address) 689 690 def quit_window(self, quit_type: babase.QuitType) -> None: 691 """(internal)""" 692 from bauiv1lib.confirm import QuitWindow 693 694 QuitWindow(quit_type) 695 696 def tournament_entry_window( 697 self, 698 tournament_id: str, 699 *, 700 tournament_activity: bascenev1.Activity | None = None, 701 position: tuple[float, float] = (0.0, 0.0), 702 delegate: Any = None, 703 scale: float | None = None, 704 offset: tuple[float, float] = (0.0, 0.0), 705 on_close_call: Callable[[], Any] | None = None, 706 ) -> None: 707 """(internal)""" 708 from bauiv1lib.tournamententry import TournamentEntryWindow 709 710 TournamentEntryWindow( 711 tournament_id, 712 tournament_activity, 713 position, 714 delegate, 715 scale, 716 offset, 717 on_close_call, 718 ) 719 720 def get_main_menu_session(self) -> type[bascenev1.Session]: 721 """(internal)""" 722 from bascenev1lib.mainmenu import MainMenuSession 723 724 return MainMenuSession 725 726 def profile_browser_window( 727 self, 728 transition: str = 'in_right', 729 origin_widget: bauiv1.Widget | None = None, 730 selected_profile: str | None = None, 731 ) -> None: 732 """(internal)""" 733 from bauiv1lib.profile.browser import ProfileBrowserWindow 734 735 main_window = babase.app.ui_v1.get_main_window() 736 if main_window is not None: 737 logging.warning( 738 'profile_browser_window()' 739 ' called with existing main window; should not happen.' 740 ) 741 return 742 743 babase.app.ui_v1.set_main_window( 744 ProfileBrowserWindow( 745 transition=transition, 746 selected_profile=selected_profile, 747 origin_widget=origin_widget, 748 minimal_toolbar=True, 749 ), 750 is_top_level=True, 751 suppress_warning=True, 752 ) 753 754 def preload_map_preview_media(self) -> None: 755 """Preload media needed for map preview UIs. 756 757 Category: **Asset Functions** 758 """ 759 try: 760 bauiv1.getmesh('level_select_button_opaque') 761 bauiv1.getmesh('level_select_button_transparent') 762 for maptype in list(self.maps.values()): 763 map_tex_name = maptype.get_preview_texture_name() 764 if map_tex_name is not None: 765 bauiv1.gettexture(map_tex_name) 766 except Exception: 767 logging.exception('Error preloading map preview media.') 768 769 def party_icon_activate(self, origin: Sequence[float]) -> None: 770 """(internal)""" 771 from bauiv1lib.party import PartyWindow 772 from babase import app 773 774 assert app.env.gui 775 776 # Play explicit swish sound so it occurs due to keypresses/etc. 777 # This means we have to disable it for any button or else we get 778 # double. 779 bauiv1.getsound('swish').play() 780 781 # If it exists, dismiss it; otherwise make a new one. 782 party_window = ( 783 None if self.party_window is None else self.party_window() 784 ) 785 if party_window is not None: 786 party_window.close() 787 else: 788 self.party_window = weakref.ref(PartyWindow(origin=origin)) 789 790 def device_menu_press(self, device_id: int | None) -> None: 791 """(internal)""" 792 from bauiv1lib.ingamemenu import InGameMenuWindow 793 from bauiv1 import set_ui_input_device 794 795 assert babase.app is not None 796 in_main_menu = babase.app.ui_v1.has_main_window() 797 if not in_main_menu: 798 set_ui_input_device(device_id) 799 800 # Hack(ish). We play swish sound here so it happens for 801 # device presses, but this means we need to disable default 802 # swish sounds for any menu buttons or we'll get double. 803 if babase.app.env.gui: 804 bauiv1.getsound('swish').play() 805 806 # Pause gameplay. 807 self.pause() 808 809 babase.app.ui_v1.set_main_window( 810 InGameMenuWindow(), is_top_level=True, suppress_warning=True 811 ) 812 813 def save_ui_state(self) -> None: 814 """Store our current place in the UI.""" 815 ui = babase.app.ui_v1 816 mainwindow = ui.get_main_window() 817 if mainwindow is not None: 818 self.saved_ui_state = ui.save_main_window_state(mainwindow) 819 else: 820 self.saved_ui_state = None 821 822 def invoke_main_menu_ui(self) -> None: 823 """Bring up main menu ui.""" 824 825 # Bring up the last place we were, or start at the main menu 826 # otherwise. 827 app = bauiv1.app 828 env = app.env 829 with bascenev1.ContextRef.empty(): 830 831 assert app.classic is not None 832 if app.env.headless: 833 # UI stuff fails now in headless builds; avoid it. 834 pass 835 else: 836 837 # When coming back from a kiosk-mode game, jump to the 838 # kiosk start screen. 839 if env.demo or env.arcade: 840 # pylint: disable=cyclic-import 841 from bauiv1lib.kiosk import KioskWindow 842 843 app.ui_v1.set_main_window( 844 KioskWindow(), is_top_level=True, suppress_warning=True 845 ) 846 else: 847 # If there's a saved ui state, restore that. 848 if self.saved_ui_state is not None: 849 app.ui_v1.restore_main_window_state(self.saved_ui_state) 850 else: 851 # Otherwise start fresh at the main menu. 852 from bauiv1lib.mainmenu import MainMenuWindow 853 854 app.ui_v1.set_main_window( 855 MainMenuWindow(transition=None), 856 is_top_level=True, 857 suppress_warning=True, 858 ) 859 860 @staticmethod 861 def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None: 862 """Run client effects sent from the master server.""" 863 from baclassic._clienteffect import run_bs_client_effects 864 865 run_bs_client_effects(effects) 866 867 @staticmethod 868 def basic_client_ui_button_label_str( 869 label: bacommon.bs.BasicClientUI.ButtonLabel, 870 ) -> babase.Lstr: 871 """Given a client-ui label, return an Lstr.""" 872 import bacommon.bs 873 874 cls = bacommon.bs.BasicClientUI.ButtonLabel 875 if label is cls.UNKNOWN: 876 # Server should not be sending us unknown stuff; make noise 877 # if they do. 878 logging.error( 879 'Got BasicClientUI.ButtonLabel.UNKNOWN; should not happen.' 880 ) 881 return babase.Lstr(value='<error>') 882 883 rsrc: str | None = None 884 if label is cls.OK: 885 rsrc = 'okText' 886 elif label is cls.APPLY: 887 rsrc = 'applyText' 888 elif label is cls.CANCEL: 889 rsrc = 'cancelText' 890 elif label is cls.ACCEPT: 891 rsrc = 'gatherWindow.partyInviteAcceptText' 892 elif label is cls.DECLINE: 893 rsrc = 'gatherWindow.partyInviteDeclineText' 894 elif label is cls.IGNORE: 895 rsrc = 'gatherWindow.partyInviteIgnoreText' 896 elif label is cls.CLAIM: 897 rsrc = 'claimText' 898 elif label is cls.DISCARD: 899 rsrc = 'discardText' 900 else: 901 assert_never(label) 902 903 return babase.Lstr(resource=rsrc) 904 905 def required_purchases_for_game(self, game: str) -> list[str]: 906 """Return which purchase (if any) is required for a game.""" 907 # pylint: disable=too-many-return-statements 908 909 if game in ( 910 'Challenges:Infinite Runaround', 911 'Challenges:Tournament Infinite Runaround', 912 ): 913 # Special case: Pro used to unlock this. 914 return ( 915 [] 916 if self.accounts.have_pro() 917 else ['upgrades.infinite_runaround'] 918 ) 919 if game in ( 920 'Challenges:Infinite Onslaught', 921 'Challenges:Tournament Infinite Onslaught', 922 ): 923 # Special case: Pro used to unlock this. 924 return ( 925 [] 926 if self.accounts.have_pro() 927 else ['upgrades.infinite_onslaught'] 928 ) 929 if game in ( 930 'Challenges:Meteor Shower', 931 'Challenges:Epic Meteor Shower', 932 ): 933 return ['games.meteor_shower'] 934 935 if game in ( 936 'Challenges:Target Practice', 937 'Challenges:Target Practice B', 938 ): 939 return ['games.target_practice'] 940 941 if game in ( 942 'Challenges:Ninja Fight', 943 'Challenges:Pro Ninja Fight', 944 ): 945 return ['games.ninja_fight'] 946 947 if game in ('Challenges:Race', 'Challenges:Pro Race'): 948 return ['games.race'] 949 950 if game in ('Challenges:Lake Frigid Race',): 951 return ['games.race', 'maps.lake_frigid'] 952 953 if game in ( 954 'Challenges:Easter Egg Hunt', 955 'Challenges:Pro Easter Egg Hunt', 956 ): 957 return ['games.easter_egg_hunt'] 958 959 return [] 960 961 def is_game_unlocked(self, game: str) -> bool: 962 """Is a particular game unlocked?""" 963 plus = babase.app.plus 964 assert plus is not None 965 966 purchases = self.required_purchases_for_game(game) 967 if not purchases: 968 return True 969 970 for purchase in purchases: 971 if not plus.get_v1_account_product_purchased(purchase): 972 return False 973 return True
Subsystem for classic functionality in the app.
The single shared instance of this app can be accessed at babase.app.classic. Note that it is possible for babase.app.classic to be None if the classic package is not present, and code should handle that case gracefully.
138 @property 139 def platform(self) -> str: 140 """Name of the current platform. 141 142 Examples are: 'mac', 'windows', android'. 143 """ 144 assert isinstance(self._env['platform'], str) 145 return self._env['platform']
Name of the current platform.
Examples are: 'mac', 'windows', android'.
151 @property 152 def subplatform(self) -> str: 153 """String for subplatform. 154 155 Can be empty. For the 'android' platform, subplatform may 156 be 'google', 'amazon', etc. 157 """ 158 assert isinstance(self._env['subplatform'], str) 159 return self._env['subplatform']
String for subplatform.
Can be empty. For the 'android' platform, subplatform may be 'google', 'amazon', etc.
161 @property 162 def legacy_user_agent_string(self) -> str: 163 """String containing various bits of info about OS/device/etc.""" 164 assert isinstance(self._env['legacy_user_agent_string'], str) 165 return self._env['legacy_user_agent_string']
String containing various bits of info about OS/device/etc.
167 @override 168 def on_app_loading(self) -> None: 169 from bascenev1lib.actor import spazappearance 170 from bascenev1lib import maps as stdmaps 171 172 plus = babase.app.plus 173 assert plus is not None 174 175 env = babase.app.env 176 cfg = babase.app.config 177 178 self.music.on_app_loading() 179 180 # Non-test, non-debug builds should generally be blessed; warn 181 # if not (so I don't accidentally release a build that can't 182 # play tourneys). 183 if not env.debug and not env.test and not plus.is_blessed(): 184 babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) 185 186 stdmaps.register_all_maps() 187 188 spazappearance.register_appearances() 189 bascenev1.init_campaigns() 190 191 launch_count = cfg.get('launchCount', 0) 192 launch_count += 1 193 194 # So we know how many times we've run the game at various 195 # version milestones. 196 for key in ('lc14173', 'lc14292'): 197 cfg.setdefault(key, launch_count) 198 199 cfg['launchCount'] = launch_count 200 cfg.commit() 201 202 # If there's a leftover log file, attempt to upload it to the 203 # master-server and/or get rid of it. 204 babase.handle_leftover_v1_cloud_log_file() 205 206 self.accounts.on_app_loading()
Called when the app reaches the loading state.
Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.
212 @override 213 def on_app_unsuspend(self) -> None: 214 self.accounts.on_app_unsuspend() 215 self.music.on_app_unsuspend()
Called when the app exits the suspended state.
221 def pause(self) -> None: 222 """Pause the game due to a user request or menu popping up. 223 224 If there's a foreground host-activity that says it's pausable, tell it 225 to pause. Note: we now no longer pause if there are connected clients. 226 """ 227 activity: bascenev1.Activity | None = ( 228 bascenev1.get_foreground_host_activity() 229 ) 230 if ( 231 activity is not None 232 and activity.allow_pausing 233 and not bascenev1.have_connected_clients() 234 ): 235 from babase import Lstr 236 from bascenev1 import NodeActor 237 238 # FIXME: Shouldn't be touching scene stuff here; should just 239 # pass the request on to the host-session. 240 with activity.context: 241 globs = activity.globalsnode 242 if not globs.paused: 243 bascenev1.getsound('refWhistle').play() 244 globs.paused = True 245 246 # FIXME: This should not be an attr on Actor. 247 activity.paused_text = NodeActor( 248 bascenev1.newnode( 249 'text', 250 attrs={ 251 'text': Lstr(resource='pausedByHostText'), 252 'client_only': True, 253 'flatness': 1.0, 254 'h_align': 'center', 255 }, 256 ) 257 )
Pause the game due to a user request or menu popping up.
If there's a foreground host-activity that says it's pausable, tell it to pause. Note: we now no longer pause if there are connected clients.
259 def resume(self) -> None: 260 """Resume the game due to a user request or menu closing. 261 262 If there's a foreground host-activity that's currently paused, tell it 263 to resume. 264 """ 265 266 # FIXME: Shouldn't be touching scene stuff here; should just 267 # pass the request on to the host-session. 268 activity = bascenev1.get_foreground_host_activity() 269 if activity is not None: 270 with activity.context: 271 globs = activity.globalsnode 272 if globs.paused: 273 bascenev1.getsound('refWhistle').play() 274 globs.paused = False 275 276 # FIXME: This should not be an actor attr. 277 activity.paused_text = None
Resume the game due to a user request or menu closing.
If there's a foreground host-activity that's currently paused, tell it to resume.
279 def add_coop_practice_level(self, level: bascenev1.Level) -> None: 280 """Adds an individual level to the 'practice' section in Co-op.""" 281 282 # Assign this level to our catch-all campaign. 283 self.campaigns['Challenges'].addlevel(level) 284 285 # Make note to add it to our challenges UI. 286 self.custom_coop_practice_games.append(f'Challenges:{level.name}')
Adds an individual level to the 'practice' section in Co-op.
288 def launch_coop_game( 289 self, game: str, force: bool = False, args: dict | None = None 290 ) -> bool: 291 """High level way to launch a local co-op session.""" 292 # pylint: disable=cyclic-import 293 from bauiv1lib.coop.level import CoopLevelLockedWindow 294 295 assert babase.app.classic is not None 296 297 if args is None: 298 args = {} 299 if game == '': 300 raise ValueError('empty game name') 301 campaignname, levelname = game.split(':') 302 campaign = babase.app.classic.getcampaign(campaignname) 303 304 # If this campaign is sequential, make sure we've completed the 305 # one before this. 306 if campaign.sequential and not force: 307 for level in campaign.levels: 308 if level.name == levelname: 309 break 310 if not level.complete: 311 CoopLevelLockedWindow( 312 campaign.getlevel(levelname).displayname, 313 campaign.getlevel(level.name).displayname, 314 ) 315 return False 316 317 # Save where we are in the UI to come back to when done. 318 babase.app.classic.save_ui_state() 319 320 # Ok, we're good to go. 321 self.coop_session_args = { 322 'campaign': campaignname, 323 'level': levelname, 324 } 325 for arg_name, arg_val in list(args.items()): 326 self.coop_session_args[arg_name] = arg_val 327 328 def _fade_end() -> None: 329 from bascenev1 import CoopSession 330 331 try: 332 bascenev1.new_host_session(CoopSession) 333 except Exception: 334 logging.exception('Error creating coopsession after fade end.') 335 from bascenev1lib.mainmenu import MainMenuSession 336 337 bascenev1.new_host_session(MainMenuSession) 338 339 babase.fade_screen(False, endcall=_fade_end) 340 return True
High level way to launch a local co-op session.
385 def getmaps(self, playtype: str) -> list[str]: 386 """Return a list of bascenev1.Map types supporting a playtype str. 387 388 Category: **Asset Functions** 389 390 Maps supporting a given playtype must provide a particular set of 391 features and lend themselves to a certain style of play. 392 393 Play Types: 394 395 'melee' 396 General fighting map. 397 Has one or more 'spawn' locations. 398 399 'team_flag' 400 For games such as Capture The Flag where each team spawns by a flag. 401 Has two or more 'spawn' locations, each with a corresponding 'flag' 402 location (based on index). 403 404 'single_flag' 405 For games such as King of the Hill or Keep Away where multiple teams 406 are fighting over a single flag. 407 Has two or more 'spawn' locations and 1 'flag_default' location. 408 409 'conquest' 410 For games such as Conquest where flags are spread throughout the map 411 - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. 412 413 'king_of_the_hill' - has 2+ 'spawn' locations, 414 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations 415 416 'hockey' 417 For hockey games. 418 Has two 'goal' locations, corresponding 'spawn' locations, and one 419 'flag_default' location (for where puck spawns) 420 421 'football' 422 For football games. 423 Has two 'goal' locations, corresponding 'spawn' locations, and one 424 'flag_default' location (for where flag/ball/etc. spawns) 425 426 'race' 427 For racing games where players much touch each region in order. 428 Has two or more 'race_point' locations. 429 """ 430 return sorted( 431 key 432 for key, val in self.maps.items() 433 if playtype in val.get_play_types() 434 )
Return a list of bascenev1.Map types supporting a playtype str.
Category: Asset Functions
Maps supporting a given playtype must provide a particular set of features and lend themselves to a certain style of play.
Play Types:
'melee' General fighting map. Has one or more 'spawn' locations.
'team_flag' For games such as Capture The Flag where each team spawns by a flag. Has two or more 'spawn' locations, each with a corresponding 'flag' location (based on index).
'single_flag' For games such as King of the Hill or Keep Away where multiple teams are fighting over a single flag. Has two or more 'spawn' locations and 1 'flag_default' location.
'conquest' For games such as Conquest where flags are spread throughout the map
- has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations.
'king_of_the_hill' - has 2+ 'spawn' locations, 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations
'hockey' For hockey games. Has two 'goal' locations, corresponding 'spawn' locations, and one 'flag_default' location (for where puck spawns)
'football' For football games. Has two 'goal' locations, corresponding 'spawn' locations, and one 'flag_default' location (for where flag/ball/etc. spawns)
'race' For racing games where players much touch each region in order. Has two or more 'race_point' locations.
442 @classmethod 443 def json_prep(cls, data: Any) -> Any: 444 """Return a json-friendly version of the provided data. 445 446 This converts any tuples to lists and any bytes to strings 447 (interpreted as utf-8, ignoring errors). Logs errors (just once) 448 if any data is modified/discarded/unsupported. 449 """ 450 451 if isinstance(data, dict): 452 return dict( 453 (cls.json_prep(key), cls.json_prep(value)) 454 for key, value in list(data.items()) 455 ) 456 if isinstance(data, list): 457 return [cls.json_prep(element) for element in data] 458 if isinstance(data, tuple): 459 logging.exception('json_prep encountered tuple') 460 return [cls.json_prep(element) for element in data] 461 if isinstance(data, bytes): 462 try: 463 return data.decode(errors='ignore') 464 except Exception: 465 logging.exception('json_prep encountered utf-8 decode error') 466 return data.decode(errors='ignore') 467 if not isinstance(data, (str, float, bool, type(None), int)): 468 logging.exception( 469 'got unsupported type in json_prep: %s', type(data) 470 ) 471 return data
Return a json-friendly version of the provided data.
This converts any tuples to lists and any bytes to strings (interpreted as utf-8, ignoring errors). Logs errors (just once) if any data is modified/discarded/unsupported.
473 def master_server_v1_get( 474 self, 475 request: str, 476 data: dict[str, Any], 477 callback: MasterServerCallback | None = None, 478 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 479 ) -> None: 480 """Make a call to the master server via a http GET.""" 481 482 MasterServerV1CallThread( 483 request, 'get', data, callback, response_type 484 ).start()
Make a call to the master server via a http GET.
486 def master_server_v1_post( 487 self, 488 request: str, 489 data: dict[str, Any], 490 callback: MasterServerCallback | None = None, 491 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 492 ) -> None: 493 """Make a call to the master server via a http POST.""" 494 MasterServerV1CallThread( 495 request, 'post', data, callback, response_type 496 ).start()
Make a call to the master server via a http POST.
498 def set_tournament_prize_image( 499 self, entry: dict[str, Any], index: int, image: bauiv1.Widget 500 ) -> None: 501 """Given a tournament entry, return strings for its prize levels.""" 502 from baclassic import _tournament 503 504 return _tournament.set_tournament_prize_chest_image(entry, index, image)
Given a tournament entry, return strings for its prize levels.
506 def create_in_game_tournament_prize_image( 507 self, 508 entry: dict[str, Any], 509 index: int, 510 position: tuple[float, float], 511 ) -> None: 512 """Given a tournament entry, return strings for its prize levels.""" 513 from baclassic import _tournament 514 515 _tournament.create_in_game_tournament_prize_image( 516 entry, index, position 517 )
Given a tournament entry, return strings for its prize levels.
519 def get_tournament_prize_strings( 520 self, entry: dict[str, Any], include_tickets: bool 521 ) -> list[str]: 522 """Given a tournament entry, return strings for its prize levels.""" 523 from baclassic import _tournament 524 525 return _tournament.get_tournament_prize_strings( 526 entry, include_tickets=include_tickets 527 )
Given a tournament entry, return strings for its prize levels.
529 def getcampaign(self, name: str) -> bascenev1.Campaign: 530 """Return a campaign by name.""" 531 return self.campaigns[name]
Return a campaign by name.
533 def get_next_tip(self) -> str: 534 """Returns the next tip to be displayed.""" 535 if not self.tips: 536 for tip in get_all_tips(): 537 self.tips.insert(random.randint(0, len(self.tips)), tip) 538 tip = self.tips.pop() 539 return tip
Returns the next tip to be displayed.
541 def run_cpu_benchmark(self) -> None: 542 """Kick off a benchmark to test cpu speeds.""" 543 from baclassic._benchmark import run_cpu_benchmark 544 545 run_cpu_benchmark()
Kick off a benchmark to test cpu speeds.
547 def run_media_reload_benchmark(self) -> None: 548 """Kick off a benchmark to test media reloading speeds.""" 549 from baclassic._benchmark import run_media_reload_benchmark 550 551 run_media_reload_benchmark()
Kick off a benchmark to test media reloading speeds.
553 def run_stress_test( 554 self, 555 *, 556 playlist_type: str = 'Random', 557 playlist_name: str = '__default__', 558 player_count: int = 8, 559 round_duration: int = 30, 560 attract_mode: bool = False, 561 ) -> None: 562 """Run a stress test.""" 563 from baclassic._benchmark import run_stress_test 564 565 run_stress_test( 566 playlist_type=playlist_type, 567 playlist_name=playlist_name, 568 player_count=player_count, 569 round_duration=round_duration, 570 attract_mode=attract_mode, 571 )
Run a stress test.
573 def get_input_device_mapped_value( 574 self, 575 device: bascenev1.InputDevice, 576 name: str, 577 default: bool = False, 578 ) -> Any: 579 """Return a mapped value for an input device. 580 581 This checks the user config and falls back to default values 582 where available. 583 """ 584 return _input.get_input_device_mapped_value( 585 device.name, device.unique_identifier, name, default 586 )
Return a mapped value for an input device.
This checks the user config and falls back to default values where available.
588 def get_input_device_map_hash( 589 self, inputdevice: bascenev1.InputDevice 590 ) -> str: 591 """Given an input device, return hash based on its raw input values.""" 592 del inputdevice # unused currently 593 return _input.get_input_device_map_hash()
Given an input device, return hash based on its raw input values.
595 def get_input_device_config( 596 self, inputdevice: bascenev1.InputDevice, default: bool 597 ) -> tuple[dict, str]: 598 """Given an input device, return its config dict in the app config. 599 600 The dict will be created if it does not exist. 601 """ 602 return _input.get_input_device_config( 603 inputdevice.name, inputdevice.unique_identifier, default 604 )
Given an input device, return its config dict in the app config.
The dict will be created if it does not exist.
606 def get_player_colors(self) -> list[tuple[float, float, float]]: 607 """Return user-selectable player colors.""" 608 return bascenev1.get_player_colors()
Return user-selectable player colors.
610 def get_player_profile_icon(self, profilename: str) -> str: 611 """Given a profile name, returns an icon string for it. 612 613 (non-account profiles only) 614 """ 615 return bascenev1.get_player_profile_icon(profilename)
Given a profile name, returns an icon string for it.
(non-account profiles only)
617 def get_player_profile_colors( 618 self, 619 profilename: str | None, 620 profiles: dict[str, dict[str, Any]] | None = None, 621 ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: 622 """Given a profile, return colors for them.""" 623 return bascenev1.get_player_profile_colors(profilename, profiles)
Given a profile, return colors for them.
754 def preload_map_preview_media(self) -> None: 755 """Preload media needed for map preview UIs. 756 757 Category: **Asset Functions** 758 """ 759 try: 760 bauiv1.getmesh('level_select_button_opaque') 761 bauiv1.getmesh('level_select_button_transparent') 762 for maptype in list(self.maps.values()): 763 map_tex_name = maptype.get_preview_texture_name() 764 if map_tex_name is not None: 765 bauiv1.gettexture(map_tex_name) 766 except Exception: 767 logging.exception('Error preloading map preview media.')
Preload media needed for map preview UIs.
Category: Asset Functions
813 def save_ui_state(self) -> None: 814 """Store our current place in the UI.""" 815 ui = babase.app.ui_v1 816 mainwindow = ui.get_main_window() 817 if mainwindow is not None: 818 self.saved_ui_state = ui.save_main_window_state(mainwindow) 819 else: 820 self.saved_ui_state = None
Store our current place in the UI.
860 @staticmethod 861 def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None: 862 """Run client effects sent from the master server.""" 863 from baclassic._clienteffect import run_bs_client_effects 864 865 run_bs_client_effects(effects)
Run client effects sent from the master server.
905 def required_purchases_for_game(self, game: str) -> list[str]: 906 """Return which purchase (if any) is required for a game.""" 907 # pylint: disable=too-many-return-statements 908 909 if game in ( 910 'Challenges:Infinite Runaround', 911 'Challenges:Tournament Infinite Runaround', 912 ): 913 # Special case: Pro used to unlock this. 914 return ( 915 [] 916 if self.accounts.have_pro() 917 else ['upgrades.infinite_runaround'] 918 ) 919 if game in ( 920 'Challenges:Infinite Onslaught', 921 'Challenges:Tournament Infinite Onslaught', 922 ): 923 # Special case: Pro used to unlock this. 924 return ( 925 [] 926 if self.accounts.have_pro() 927 else ['upgrades.infinite_onslaught'] 928 ) 929 if game in ( 930 'Challenges:Meteor Shower', 931 'Challenges:Epic Meteor Shower', 932 ): 933 return ['games.meteor_shower'] 934 935 if game in ( 936 'Challenges:Target Practice', 937 'Challenges:Target Practice B', 938 ): 939 return ['games.target_practice'] 940 941 if game in ( 942 'Challenges:Ninja Fight', 943 'Challenges:Pro Ninja Fight', 944 ): 945 return ['games.ninja_fight'] 946 947 if game in ('Challenges:Race', 'Challenges:Pro Race'): 948 return ['games.race'] 949 950 if game in ('Challenges:Lake Frigid Race',): 951 return ['games.race', 'maps.lake_frigid'] 952 953 if game in ( 954 'Challenges:Easter Egg Hunt', 955 'Challenges:Pro Easter Egg Hunt', 956 ): 957 return ['games.easter_egg_hunt'] 958 959 return []
Return which purchase (if any) is required for a game.
961 def is_game_unlocked(self, game: str) -> bool: 962 """Is a particular game unlocked?""" 963 plus = babase.app.plus 964 assert plus is not None 965 966 purchases = self.required_purchases_for_game(game) 967 if not purchases: 968 return True 969 970 for purchase in purchases: 971 if not plus.get_v1_account_product_purchased(purchase): 972 return False 973 return True
Is a particular game unlocked?
23class MusicPlayMode(Enum): 24 """Influences behavior when playing music. 25 26 Category: **Enums** 27 """ 28 29 REGULAR = 'regular' 30 TEST = 'test'
Influences behavior when playing music.
Category: Enums
533class Achievement: 534 """Represents attributes and state for an individual achievement. 535 536 Category: **App Classes** 537 """ 538 539 def __init__( 540 self, 541 name: str, 542 icon_name: str, 543 icon_color: tuple[float, float, float], 544 level_name: str, 545 *, 546 award: int, 547 hard_mode_only: bool = False, 548 ): 549 self._name = name 550 self._icon_name = icon_name 551 assert len(icon_color) == 3 552 self._icon_color = icon_color + (1.0,) 553 self._level_name = level_name 554 self._completion_banner_slot: int | None = None 555 self._award = award 556 self._hard_mode_only = hard_mode_only 557 558 @property 559 def name(self) -> str: 560 """The name of this achievement.""" 561 return self._name 562 563 @property 564 def level_name(self) -> str: 565 """The name of the level this achievement applies to.""" 566 return self._level_name 567 568 def get_icon_ui_texture(self, complete: bool) -> bauiv1.Texture: 569 """Return the icon texture to display for this achievement""" 570 return bauiv1.gettexture( 571 self._icon_name if complete else 'achievementEmpty' 572 ) 573 574 def get_icon_texture(self, complete: bool) -> bascenev1.Texture: 575 """Return the icon texture to display for this achievement""" 576 return bascenev1.gettexture( 577 self._icon_name if complete else 'achievementEmpty' 578 ) 579 580 def get_icon_color(self, complete: bool) -> Sequence[float]: 581 """Return the color tint for this Achievement's icon.""" 582 if complete: 583 return self._icon_color 584 return 1.0, 1.0, 1.0, 0.6 585 586 @property 587 def hard_mode_only(self) -> bool: 588 """Whether this Achievement is only unlockable in hard-mode.""" 589 return self._hard_mode_only 590 591 @property 592 def complete(self) -> bool: 593 """Whether this Achievement is currently complete.""" 594 val: bool = self._getconfig()['Complete'] 595 assert isinstance(val, bool) 596 return val 597 598 def announce_completion(self, sound: bool = True) -> None: 599 """Kick off an announcement for this achievement's completion.""" 600 601 app = babase.app 602 plus = app.plus 603 classic = app.classic 604 if plus is None or classic is None: 605 logging.warning('ach account_completion not available.') 606 return 607 608 ach_ss = classic.ach 609 610 # Even though there are technically achievements when we're not 611 # signed in, lets not show them (otherwise we tend to get 612 # confusing 'controller connected' achievements popping up while 613 # waiting to sign in which can be confusing). 614 if plus.get_v1_account_state() != 'signed_in': 615 return 616 617 # If we're being freshly complete, display/report it and whatnot. 618 if (self, sound) not in ach_ss.achievements_to_display: 619 ach_ss.achievements_to_display.append((self, sound)) 620 621 # If there's no achievement display timer going, kick one off 622 # (if one's already running it will pick this up before it dies). 623 624 # Need to check last time too; its possible our timer wasn't able to 625 # clear itself if an activity died and took it down with it. 626 if ( 627 ach_ss.achievement_display_timer is None 628 or babase.apptime() - ach_ss.last_achievement_display_time > 2.0 629 ) and bascenev1.getactivity(doraise=False) is not None: 630 ach_ss.achievement_display_timer = bascenev1.BaseTimer( 631 1.0, _display_next_achievement, repeat=True 632 ) 633 634 # Show the first immediately. 635 _display_next_achievement() 636 637 def set_complete(self, complete: bool = True) -> None: 638 """Set an achievement's completed state. 639 640 note this only sets local state; use a transaction to 641 actually award achievements. 642 """ 643 config = self._getconfig() 644 if complete != config['Complete']: 645 config['Complete'] = complete 646 647 @property 648 def display_name(self) -> babase.Lstr: 649 """Return a babase.Lstr for this Achievement's name.""" 650 name: babase.Lstr | str 651 try: 652 if self._level_name != '': 653 campaignname, campaign_level = self._level_name.split(':') 654 classic = babase.app.classic 655 assert classic is not None 656 name = ( 657 classic.getcampaign(campaignname) 658 .getlevel(campaign_level) 659 .displayname 660 ) 661 else: 662 name = '' 663 except Exception: 664 name = '' 665 logging.exception('Error calcing achievement display-name.') 666 return babase.Lstr( 667 resource='achievements.' + self._name + '.name', 668 subs=[('${LEVEL}', name)], 669 ) 670 671 @property 672 def description(self) -> babase.Lstr: 673 """Get a babase.Lstr for the Achievement's brief description.""" 674 if ( 675 'description' 676 in babase.app.lang.get_resource('achievements')[self._name] 677 ): 678 return babase.Lstr( 679 resource='achievements.' + self._name + '.description' 680 ) 681 return babase.Lstr( 682 resource='achievements.' + self._name + '.descriptionFull' 683 ) 684 685 @property 686 def description_complete(self) -> babase.Lstr: 687 """Get a babase.Lstr for the Achievement's description when complete.""" 688 if ( 689 'descriptionComplete' 690 in babase.app.lang.get_resource('achievements')[self._name] 691 ): 692 return babase.Lstr( 693 resource='achievements.' + self._name + '.descriptionComplete' 694 ) 695 return babase.Lstr( 696 resource='achievements.' + self._name + '.descriptionFullComplete' 697 ) 698 699 @property 700 def description_full(self) -> babase.Lstr: 701 """Get a babase.Lstr for the Achievement's full description.""" 702 return babase.Lstr( 703 resource='achievements.' + self._name + '.descriptionFull', 704 subs=[ 705 ( 706 '${LEVEL}', 707 babase.Lstr( 708 translate=( 709 'coopLevelNames', 710 ACH_LEVEL_NAMES.get(self._name, '?'), 711 ) 712 ), 713 ) 714 ], 715 ) 716 717 @property 718 def description_full_complete(self) -> babase.Lstr: 719 """Get a babase.Lstr for the Achievement's full desc. when completed.""" 720 return babase.Lstr( 721 resource='achievements.' + self._name + '.descriptionFullComplete', 722 subs=[ 723 ( 724 '${LEVEL}', 725 babase.Lstr( 726 translate=( 727 'coopLevelNames', 728 ACH_LEVEL_NAMES.get(self._name, '?'), 729 ) 730 ), 731 ) 732 ], 733 ) 734 735 def get_award_chest_type(self) -> ClassicChestAppearance: 736 """Return the type of chest given for this achievement.""" 737 738 # For now just map our old ticket values to chest types. 739 # Can add distinct values if need be later. 740 plus = babase.app.plus 741 assert plus is not None 742 t = plus.get_v1_account_misc_read_val( 743 f'achAward.{self.name}', self._award 744 ) 745 return ( 746 ClassicChestAppearance.L6 747 if t >= 30 748 else ( 749 ClassicChestAppearance.L5 750 if t >= 25 751 else ( 752 ClassicChestAppearance.L4 753 if t >= 20 754 else ( 755 ClassicChestAppearance.L3 756 if t >= 15 757 else ( 758 ClassicChestAppearance.L2 759 if t >= 10 760 else ClassicChestAppearance.L1 761 ) 762 ) 763 ) 764 ) 765 ) 766 767 # def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: 768 # """Get the ticket award value for this achievement.""" 769 # plus = babase.app.plus 770 # if plus is None: 771 # return 0 772 # val: int = plus.get_v1_account_misc_read_val( 773 # 'achAward.' + self._name, self._award 774 # ) * _get_ach_mult(include_pro_bonus) 775 # assert isinstance(val, int) 776 # return val 777 778 @property 779 def power_ranking_value(self) -> int: 780 """Get the power-ranking award value for this achievement.""" 781 plus = babase.app.plus 782 if plus is None: 783 return 0 784 val: int = plus.get_v1_account_misc_read_val( 785 'achLeaguePoints.' + self._name, self._award 786 ) 787 assert isinstance(val, int) 788 return val 789 790 def create_display( 791 self, 792 x: float, 793 y: float, 794 delay: float, 795 *, 796 outdelay: float | None = None, 797 color: Sequence[float] | None = None, 798 style: str = 'post_game', 799 ) -> list[bascenev1.Actor]: 800 """Create a display for the Achievement. 801 802 Shows the Achievement icon, name, and description. 803 """ 804 # pylint: disable=cyclic-import 805 from bascenev1 import CoopSession 806 from bascenev1lib.actor.image import Image 807 from bascenev1lib.actor.text import Text 808 809 # Yeah this needs cleaning up. 810 if style == 'post_game': 811 in_game_colors = False 812 in_main_menu = False 813 h_attach = Text.HAttach.CENTER 814 v_attach = Text.VAttach.CENTER 815 attach = Image.Attach.CENTER 816 elif style == 'in_game': 817 in_game_colors = True 818 in_main_menu = False 819 h_attach = Text.HAttach.LEFT 820 v_attach = Text.VAttach.TOP 821 attach = Image.Attach.TOP_LEFT 822 elif style == 'news': 823 in_game_colors = True 824 in_main_menu = True 825 h_attach = Text.HAttach.CENTER 826 v_attach = Text.VAttach.TOP 827 attach = Image.Attach.TOP_CENTER 828 else: 829 raise ValueError('invalid style "' + style + '"') 830 831 # Attempt to determine what campaign we're in 832 # (so we know whether to show "hard mode only"). 833 if in_main_menu: 834 hmo = False 835 else: 836 try: 837 session = bascenev1.getsession() 838 if isinstance(session, CoopSession): 839 campaign = session.campaign 840 assert campaign is not None 841 hmo = self._hard_mode_only and campaign.name == 'Easy' 842 else: 843 hmo = False 844 except Exception: 845 logging.exception('Error determining campaign.') 846 hmo = False 847 848 objs: list[bascenev1.Actor] 849 850 if in_game_colors: 851 objs = [] 852 out_delay_fin = (delay + outdelay) if outdelay is not None else None 853 if color is not None: 854 cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3]) 855 cl2 = color 856 else: 857 cl1 = (1.5, 1.5, 2, 1.0) 858 cl2 = (0.8, 0.8, 1.0, 1.0) 859 860 if hmo: 861 cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) 862 cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) 863 864 objs.append( 865 Image( 866 self.get_icon_texture(False), 867 host_only=True, 868 color=cl1, 869 position=(x - 25, y + 5), 870 attach=attach, 871 transition=Image.Transition.FADE_IN, 872 transition_delay=delay, 873 vr_depth=4, 874 transition_out_delay=out_delay_fin, 875 scale=(40, 40), 876 ).autoretain() 877 ) 878 txt = self.display_name 879 txt_s = 0.85 880 txt_max_w = 300 881 objs.append( 882 Text( 883 txt, 884 host_only=True, 885 maxwidth=txt_max_w, 886 position=(x, y + 2), 887 transition=Text.Transition.FADE_IN, 888 scale=txt_s, 889 flatness=0.6, 890 shadow=0.5, 891 h_attach=h_attach, 892 v_attach=v_attach, 893 color=cl2, 894 transition_delay=delay + 0.05, 895 transition_out_delay=out_delay_fin, 896 ).autoretain() 897 ) 898 txt2_s = 0.62 899 txt2_max_w = 400 900 objs.append( 901 Text( 902 self.description_full if in_main_menu else self.description, 903 host_only=True, 904 maxwidth=txt2_max_w, 905 position=(x, y - 14), 906 transition=Text.Transition.FADE_IN, 907 vr_depth=-5, 908 h_attach=h_attach, 909 v_attach=v_attach, 910 scale=txt2_s, 911 flatness=1.0, 912 shadow=0.5, 913 color=cl2, 914 transition_delay=delay + 0.1, 915 transition_out_delay=out_delay_fin, 916 ).autoretain() 917 ) 918 919 if hmo: 920 txtactor = Text( 921 babase.Lstr(resource='difficultyHardOnlyText'), 922 host_only=True, 923 maxwidth=txt2_max_w * 0.7, 924 position=(x + 60, y + 5), 925 transition=Text.Transition.FADE_IN, 926 vr_depth=-5, 927 h_attach=h_attach, 928 v_attach=v_attach, 929 h_align=Text.HAlign.CENTER, 930 v_align=Text.VAlign.CENTER, 931 scale=txt_s * 0.8, 932 flatness=1.0, 933 shadow=0.5, 934 color=(1, 1, 0.6, 1), 935 transition_delay=delay + 0.1, 936 transition_out_delay=out_delay_fin, 937 ).autoretain() 938 txtactor.node.rotate = 10 939 objs.append(txtactor) 940 941 # Chest award. 942 award_x = -100 943 chesttype = self.get_award_chest_type() 944 chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( 945 chesttype, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 946 ) 947 objs.append( 948 Image( 949 # Provide magical extended dict version of texture 950 # that Image actor supports. 951 texture={ 952 'texture': bascenev1.gettexture( 953 chestdisplayinfo.texclosed 954 ), 955 'tint_texture': bascenev1.gettexture( 956 chestdisplayinfo.texclosedtint 957 ), 958 'tint_color': chestdisplayinfo.tint, 959 'tint2_color': chestdisplayinfo.tint2, 960 'mask_texture': None, 961 }, 962 color=chestdisplayinfo.color + (0.5 if hmo else 1.0,), 963 position=(x + award_x + 37, y + 12), 964 scale=(32.0, 32.0), 965 transition=Image.Transition.FADE_IN, 966 transition_delay=delay + 0.05, 967 transition_out_delay=out_delay_fin, 968 host_only=True, 969 attach=Image.Attach.TOP_LEFT, 970 ).autoretain() 971 ) 972 973 # objs.append( 974 # Text( 975 # babase.charstr(babase.SpecialChar.TICKET), 976 # host_only=True, 977 # position=(x + award_x + 33, y + 7), 978 # transition=Text.Transition.FADE_IN, 979 # scale=1.5, 980 # h_attach=h_attach, 981 # v_attach=v_attach, 982 # h_align=Text.HAlign.CENTER, 983 # v_align=Text.VAlign.CENTER, 984 # color=(1, 1, 1, 0.2 if hmo else 0.4), 985 # transition_delay=delay + 0.05, 986 # transition_out_delay=out_delay_fin, 987 # ).autoretain() 988 # ) 989 # objs.append( 990 # Text( 991 # '+' + str(self.get_award_ticket_value()), 992 # host_only=True, 993 # position=(x + award_x + 28, y + 16), 994 # transition=Text.Transition.FADE_IN, 995 # scale=0.7, 996 # flatness=1, 997 # h_attach=h_attach, 998 # v_attach=v_attach, 999 # h_align=Text.HAlign.CENTER, 1000 # v_align=Text.VAlign.CENTER, 1001 # color=cl2, 1002 # transition_delay=delay + 0.05, 1003 # transition_out_delay=out_delay_fin, 1004 # ).autoretain() 1005 # ) 1006 1007 else: 1008 complete = self.complete 1009 objs = [] 1010 c_icon = self.get_icon_color(complete) 1011 if hmo and not complete: 1012 c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) 1013 objs.append( 1014 Image( 1015 self.get_icon_texture(complete), 1016 host_only=True, 1017 color=c_icon, 1018 position=(x - 25, y + 5), 1019 attach=attach, 1020 vr_depth=4, 1021 transition=Image.Transition.IN_RIGHT, 1022 transition_delay=delay, 1023 transition_out_delay=None, 1024 scale=(40, 40), 1025 ).autoretain() 1026 ) 1027 if complete: 1028 objs.append( 1029 Image( 1030 bascenev1.gettexture('achievementOutline'), 1031 host_only=True, 1032 mesh_transparent=bascenev1.getmesh( 1033 'achievementOutline' 1034 ), 1035 color=(2, 1.4, 0.4, 1), 1036 vr_depth=8, 1037 position=(x - 25, y + 5), 1038 attach=attach, 1039 transition=Image.Transition.IN_RIGHT, 1040 transition_delay=delay, 1041 transition_out_delay=None, 1042 scale=(40, 40), 1043 ).autoretain() 1044 ) 1045 else: 1046 if not complete: 1047 award_x = -100 1048 # objs.append( 1049 # Text( 1050 # babase.charstr(babase.SpecialChar.TICKET), 1051 # host_only=True, 1052 # position=(x + award_x + 33, y + 7), 1053 # transition=Text.Transition.IN_RIGHT, 1054 # scale=1.5, 1055 # h_attach=h_attach, 1056 # v_attach=v_attach, 1057 # h_align=Text.HAlign.CENTER, 1058 # v_align=Text.VAlign.CENTER, 1059 # color=(1, 1, 1, (0.1 if hmo else 0.2)), 1060 # transition_delay=delay + 0.05, 1061 # transition_out_delay=None, 1062 # ).autoretain() 1063 # ) 1064 chesttype = self.get_award_chest_type() 1065 chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( 1066 chesttype, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 1067 ) 1068 objs.append( 1069 Image( 1070 # Provide magical extended dict version of texture 1071 # that Image actor supports. 1072 texture={ 1073 'texture': bascenev1.gettexture( 1074 chestdisplayinfo.texclosed 1075 ), 1076 'tint_texture': bascenev1.gettexture( 1077 chestdisplayinfo.texclosedtint 1078 ), 1079 'tint_color': chestdisplayinfo.tint, 1080 'tint2_color': chestdisplayinfo.tint2, 1081 'mask_texture': None, 1082 }, 1083 color=chestdisplayinfo.color 1084 + (0.5 if hmo else 1.0,), 1085 position=(x + award_x + 38, y + 14), 1086 scale=(32.0, 32.0), 1087 transition=Image.Transition.IN_RIGHT, 1088 transition_delay=delay + 0.05, 1089 transition_out_delay=None, 1090 host_only=True, 1091 attach=attach, 1092 ).autoretain() 1093 ) 1094 1095 # objs.append( 1096 # Text( 1097 # '+' + str(self.get_award_ticket_value()), 1098 # host_only=True, 1099 # position=(x + award_x + 28, y + 16), 1100 # transition=Text.Transition.IN_RIGHT, 1101 # scale=0.7, 1102 # flatness=1, 1103 # h_attach=h_attach, 1104 # v_attach=v_attach, 1105 # h_align=Text.HAlign.CENTER, 1106 # v_align=Text.VAlign.CENTER, 1107 # color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)), 1108 # transition_delay=delay + 0.05, 1109 # transition_out_delay=None, 1110 # ).autoretain() 1111 # ) 1112 1113 # Show 'hard-mode-only' only over incomplete achievements 1114 # when that's the case. 1115 if hmo: 1116 txtactor = Text( 1117 babase.Lstr(resource='difficultyHardOnlyText'), 1118 host_only=True, 1119 maxwidth=300 * 0.7, 1120 position=(x + 60, y + 5), 1121 transition=Text.Transition.FADE_IN, 1122 vr_depth=-5, 1123 h_attach=h_attach, 1124 v_attach=v_attach, 1125 h_align=Text.HAlign.CENTER, 1126 v_align=Text.VAlign.CENTER, 1127 scale=0.85 * 0.8, 1128 flatness=1.0, 1129 shadow=0.5, 1130 color=(1, 1, 0.6, 1), 1131 transition_delay=delay + 0.05, 1132 transition_out_delay=None, 1133 ).autoretain() 1134 assert txtactor.node 1135 txtactor.node.rotate = 10 1136 objs.append(txtactor) 1137 1138 objs.append( 1139 Text( 1140 self.display_name, 1141 host_only=True, 1142 maxwidth=300, 1143 position=(x, y + 2), 1144 transition=Text.Transition.IN_RIGHT, 1145 scale=0.85, 1146 flatness=0.6, 1147 h_attach=h_attach, 1148 v_attach=v_attach, 1149 color=( 1150 (0.8, 0.93, 0.8, 1.0) 1151 if complete 1152 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1153 ), 1154 transition_delay=delay + 0.05, 1155 transition_out_delay=None, 1156 ).autoretain() 1157 ) 1158 objs.append( 1159 Text( 1160 self.description_complete if complete else self.description, 1161 host_only=True, 1162 maxwidth=400, 1163 position=(x, y - 14), 1164 transition=Text.Transition.IN_RIGHT, 1165 vr_depth=-5, 1166 h_attach=h_attach, 1167 v_attach=v_attach, 1168 scale=0.62, 1169 flatness=1.0, 1170 color=( 1171 (0.6, 0.6, 0.6, 1.0) 1172 if complete 1173 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1174 ), 1175 transition_delay=delay + 0.1, 1176 transition_out_delay=None, 1177 ).autoretain() 1178 ) 1179 return objs 1180 1181 def _getconfig(self) -> dict[str, Any]: 1182 """ 1183 Return the sub-dict in settings where this achievement's 1184 state is stored, creating it if need be. 1185 """ 1186 val: dict[str, Any] = babase.app.config.setdefault( 1187 'Achievements', {} 1188 ).setdefault(self._name, {'Complete': False}) 1189 assert isinstance(val, dict) 1190 return val 1191 1192 def _remove_banner_slot(self) -> None: 1193 classic = babase.app.classic 1194 assert classic is not None 1195 assert self._completion_banner_slot is not None 1196 classic.ach.achievement_completion_banner_slots.remove( 1197 self._completion_banner_slot 1198 ) 1199 self._completion_banner_slot = None 1200 1201 def show_completion_banner(self, sound: bool = True) -> None: 1202 """Create the banner/sound for an acquired achievement announcement.""" 1203 from bascenev1lib.actor.text import Text 1204 from bascenev1lib.actor.image import Image 1205 1206 app = babase.app 1207 assert app.classic is not None 1208 app.classic.ach.last_achievement_display_time = babase.apptime() 1209 1210 # Just piggy-back onto any current activity 1211 # (should we use the session instead?..) 1212 activity = bascenev1.getactivity(doraise=False) 1213 1214 # If this gets called while this achievement is occupying a slot 1215 # already, ignore it. (probably should never happen in real 1216 # life but whatevs). 1217 if self._completion_banner_slot is not None: 1218 return 1219 1220 if activity is None: 1221 print('show_completion_banner() called with no current activity!') 1222 return 1223 1224 if sound: 1225 bascenev1.getsound('achievement').play(host_only=True) 1226 else: 1227 bascenev1.timer( 1228 0.5, lambda: bascenev1.getsound('ding').play(host_only=True) 1229 ) 1230 1231 in_time = 0.300 1232 out_time = 3.5 1233 1234 base_vr_depth = 200 1235 1236 # Find the first free slot. 1237 i = 0 1238 while True: 1239 if i not in app.classic.ach.achievement_completion_banner_slots: 1240 app.classic.ach.achievement_completion_banner_slots.add(i) 1241 self._completion_banner_slot = i 1242 1243 # Remove us from that slot when we close. 1244 # Use an app-timer in an empty context so the removal 1245 # runs even if our activity/session dies. 1246 with babase.ContextRef.empty(): 1247 babase.apptimer( 1248 in_time + out_time, self._remove_banner_slot 1249 ) 1250 break 1251 i += 1 1252 assert self._completion_banner_slot is not None 1253 y_offs = 110 * self._completion_banner_slot 1254 objs: list[bascenev1.Actor] = [] 1255 obj = Image( 1256 bascenev1.gettexture('shadow'), 1257 position=(-30, 30 + y_offs), 1258 front=True, 1259 attach=Image.Attach.BOTTOM_CENTER, 1260 transition=Image.Transition.IN_BOTTOM, 1261 vr_depth=base_vr_depth - 100, 1262 transition_delay=in_time, 1263 transition_out_delay=out_time, 1264 color=(0.0, 0.1, 0, 1), 1265 scale=(1000, 300), 1266 ).autoretain() 1267 objs.append(obj) 1268 assert obj.node 1269 obj.node.host_only = True 1270 obj = Image( 1271 bascenev1.gettexture('light'), 1272 position=(-180, 60 + y_offs), 1273 front=True, 1274 attach=Image.Attach.BOTTOM_CENTER, 1275 vr_depth=base_vr_depth, 1276 transition=Image.Transition.IN_BOTTOM, 1277 transition_delay=in_time, 1278 transition_out_delay=out_time, 1279 color=(1.8, 1.8, 1.0, 0.0), 1280 scale=(40, 300), 1281 ).autoretain() 1282 objs.append(obj) 1283 assert obj.node 1284 obj.node.host_only = True 1285 obj.node.premultiplied = True 1286 combine = bascenev1.newnode( 1287 'combine', owner=obj.node, attrs={'size': 2} 1288 ) 1289 bascenev1.animate( 1290 combine, 1291 'input0', 1292 { 1293 in_time: 0, 1294 in_time + 0.4: 30, 1295 in_time + 0.5: 40, 1296 in_time + 0.6: 30, 1297 in_time + 2.0: 0, 1298 }, 1299 ) 1300 bascenev1.animate( 1301 combine, 1302 'input1', 1303 { 1304 in_time: 0, 1305 in_time + 0.4: 200, 1306 in_time + 0.5: 500, 1307 in_time + 0.6: 200, 1308 in_time + 2.0: 0, 1309 }, 1310 ) 1311 combine.connectattr('output', obj.node, 'scale') 1312 bascenev1.animate(obj.node, 'rotate', {0: 0.0, 0.35: 360.0}, loop=True) 1313 obj = Image( 1314 self.get_icon_texture(True), 1315 position=(-180, 60 + y_offs), 1316 attach=Image.Attach.BOTTOM_CENTER, 1317 front=True, 1318 vr_depth=base_vr_depth - 10, 1319 transition=Image.Transition.IN_BOTTOM, 1320 transition_delay=in_time, 1321 transition_out_delay=out_time, 1322 scale=(100, 100), 1323 ).autoretain() 1324 objs.append(obj) 1325 assert obj.node 1326 obj.node.host_only = True 1327 1328 # Flash. 1329 color = self.get_icon_color(True) 1330 combine = bascenev1.newnode( 1331 'combine', owner=obj.node, attrs={'size': 3} 1332 ) 1333 keys = { 1334 in_time: 1.0 * color[0], 1335 in_time + 0.4: 1.5 * color[0], 1336 in_time + 0.5: 6.0 * color[0], 1337 in_time + 0.6: 1.5 * color[0], 1338 in_time + 2.0: 1.0 * color[0], 1339 } 1340 bascenev1.animate(combine, 'input0', keys) 1341 keys = { 1342 in_time: 1.0 * color[1], 1343 in_time + 0.4: 1.5 * color[1], 1344 in_time + 0.5: 6.0 * color[1], 1345 in_time + 0.6: 1.5 * color[1], 1346 in_time + 2.0: 1.0 * color[1], 1347 } 1348 bascenev1.animate(combine, 'input1', keys) 1349 keys = { 1350 in_time: 1.0 * color[2], 1351 in_time + 0.4: 1.5 * color[2], 1352 in_time + 0.5: 6.0 * color[2], 1353 in_time + 0.6: 1.5 * color[2], 1354 in_time + 2.0: 1.0 * color[2], 1355 } 1356 bascenev1.animate(combine, 'input2', keys) 1357 combine.connectattr('output', obj.node, 'color') 1358 1359 obj = Image( 1360 bascenev1.gettexture('achievementOutline'), 1361 mesh_transparent=bascenev1.getmesh('achievementOutline'), 1362 position=(-180, 60 + y_offs), 1363 front=True, 1364 attach=Image.Attach.BOTTOM_CENTER, 1365 vr_depth=base_vr_depth, 1366 transition=Image.Transition.IN_BOTTOM, 1367 transition_delay=in_time, 1368 transition_out_delay=out_time, 1369 scale=(100, 100), 1370 ).autoretain() 1371 assert obj.node 1372 obj.node.host_only = True 1373 1374 # Flash. 1375 color = (2, 1.4, 0.4, 1) 1376 combine = bascenev1.newnode( 1377 'combine', owner=obj.node, attrs={'size': 3} 1378 ) 1379 keys = { 1380 in_time: 1.0 * color[0], 1381 in_time + 0.4: 1.5 * color[0], 1382 in_time + 0.5: 6.0 * color[0], 1383 in_time + 0.6: 1.5 * color[0], 1384 in_time + 2.0: 1.0 * color[0], 1385 } 1386 bascenev1.animate(combine, 'input0', keys) 1387 keys = { 1388 in_time: 1.0 * color[1], 1389 in_time + 0.4: 1.5 * color[1], 1390 in_time + 0.5: 6.0 * color[1], 1391 in_time + 0.6: 1.5 * color[1], 1392 in_time + 2.0: 1.0 * color[1], 1393 } 1394 bascenev1.animate(combine, 'input1', keys) 1395 keys = { 1396 in_time: 1.0 * color[2], 1397 in_time + 0.4: 1.5 * color[2], 1398 in_time + 0.5: 6.0 * color[2], 1399 in_time + 0.6: 1.5 * color[2], 1400 in_time + 2.0: 1.0 * color[2], 1401 } 1402 bascenev1.animate(combine, 'input2', keys) 1403 combine.connectattr('output', obj.node, 'color') 1404 objs.append(obj) 1405 1406 objt = Text( 1407 babase.Lstr( 1408 value='${A}:', 1409 subs=[('${A}', babase.Lstr(resource='achievementText'))], 1410 ), 1411 position=(-120, 91 + y_offs), 1412 front=True, 1413 v_attach=Text.VAttach.BOTTOM, 1414 vr_depth=base_vr_depth - 10, 1415 transition=Text.Transition.IN_BOTTOM, 1416 flatness=0.5, 1417 transition_delay=in_time, 1418 transition_out_delay=out_time, 1419 color=(1, 1, 1, 0.8), 1420 scale=0.65, 1421 ).autoretain() 1422 objs.append(objt) 1423 assert objt.node 1424 objt.node.host_only = True 1425 1426 objt = Text( 1427 self.display_name, 1428 position=(-120, 50 + y_offs), 1429 front=True, 1430 v_attach=Text.VAttach.BOTTOM, 1431 transition=Text.Transition.IN_BOTTOM, 1432 vr_depth=base_vr_depth, 1433 flatness=0.5, 1434 transition_delay=in_time, 1435 transition_out_delay=out_time, 1436 flash=True, 1437 color=(1, 0.8, 0, 1.0), 1438 scale=1.5, 1439 ).autoretain() 1440 objs.append(objt) 1441 assert objt.node 1442 objt.node.host_only = True 1443 1444 # objt = Text( 1445 # babase.charstr(babase.SpecialChar.TICKET), 1446 # position=(-120 - 170 + 5, 75 + y_offs - 20), 1447 # front=True, 1448 # v_attach=Text.VAttach.BOTTOM, 1449 # h_align=Text.HAlign.CENTER, 1450 # v_align=Text.VAlign.CENTER, 1451 # transition=Text.Transition.IN_BOTTOM, 1452 # vr_depth=base_vr_depth, 1453 # transition_delay=in_time, 1454 # transition_out_delay=out_time, 1455 # flash=True, 1456 # color=(0.5, 0.5, 0.5, 1), 1457 # scale=3.0, 1458 # ).autoretain() 1459 # objs.append(objt) 1460 # assert objt.node 1461 # objt.node.host_only = True 1462 1463 # print('FIXME SHOW ACH CHEST3') 1464 # objt = Text( 1465 # '+' + str(self.get_award_ticket_value()), 1466 # position=(-120 - 180 + 5, 80 + y_offs - 20), 1467 # v_attach=Text.VAttach.BOTTOM, 1468 # front=True, 1469 # h_align=Text.HAlign.CENTER, 1470 # v_align=Text.VAlign.CENTER, 1471 # transition=Text.Transition.IN_BOTTOM, 1472 # vr_depth=base_vr_depth, 1473 # flatness=0.5, 1474 # shadow=1.0, 1475 # transition_delay=in_time, 1476 # transition_out_delay=out_time, 1477 # flash=True, 1478 # color=(0, 1, 0, 1), 1479 # scale=1.5, 1480 # ).autoretain() 1481 # objs.append(objt) 1482 1483 assert objt.node 1484 objt.node.host_only = True 1485 1486 # Add the 'x 2' if we've got pro. 1487 # if app.classic.accounts.have_pro(): 1488 # objt = Text( 1489 # 'x 2', 1490 # position=(-120 - 180 + 45, 80 + y_offs - 50), 1491 # v_attach=Text.VAttach.BOTTOM, 1492 # front=True, 1493 # h_align=Text.HAlign.CENTER, 1494 # v_align=Text.VAlign.CENTER, 1495 # transition=Text.Transition.IN_BOTTOM, 1496 # vr_depth=base_vr_depth, 1497 # flatness=0.5, 1498 # shadow=1.0, 1499 # transition_delay=in_time, 1500 # transition_out_delay=out_time, 1501 # flash=True, 1502 # color=(0.4, 0, 1, 1), 1503 # scale=0.9, 1504 # ).autoretain() 1505 # objs.append(objt) 1506 # assert objt.node 1507 # objt.node.host_only = True 1508 1509 objt = Text( 1510 self.description_complete, 1511 position=(-120, 30 + y_offs), 1512 front=True, 1513 v_attach=Text.VAttach.BOTTOM, 1514 transition=Text.Transition.IN_BOTTOM, 1515 vr_depth=base_vr_depth - 10, 1516 flatness=0.5, 1517 transition_delay=in_time, 1518 transition_out_delay=out_time, 1519 color=(1.0, 0.7, 0.5, 1.0), 1520 scale=0.8, 1521 ).autoretain() 1522 objs.append(objt) 1523 assert objt.node 1524 objt.node.host_only = True 1525 1526 for actor in objs: 1527 bascenev1.timer( 1528 out_time + 1.000, 1529 babase.WeakCall(actor.handlemessage, bascenev1.DieMessage()), 1530 )
Represents attributes and state for an individual achievement.
Category: App Classes
539 def __init__( 540 self, 541 name: str, 542 icon_name: str, 543 icon_color: tuple[float, float, float], 544 level_name: str, 545 *, 546 award: int, 547 hard_mode_only: bool = False, 548 ): 549 self._name = name 550 self._icon_name = icon_name 551 assert len(icon_color) == 3 552 self._icon_color = icon_color + (1.0,) 553 self._level_name = level_name 554 self._completion_banner_slot: int | None = None 555 self._award = award 556 self._hard_mode_only = hard_mode_only
558 @property 559 def name(self) -> str: 560 """The name of this achievement.""" 561 return self._name
The name of this achievement.
563 @property 564 def level_name(self) -> str: 565 """The name of the level this achievement applies to.""" 566 return self._level_name
The name of the level this achievement applies to.
568 def get_icon_ui_texture(self, complete: bool) -> bauiv1.Texture: 569 """Return the icon texture to display for this achievement""" 570 return bauiv1.gettexture( 571 self._icon_name if complete else 'achievementEmpty' 572 )
Return the icon texture to display for this achievement
574 def get_icon_texture(self, complete: bool) -> bascenev1.Texture: 575 """Return the icon texture to display for this achievement""" 576 return bascenev1.gettexture( 577 self._icon_name if complete else 'achievementEmpty' 578 )
Return the icon texture to display for this achievement
580 def get_icon_color(self, complete: bool) -> Sequence[float]: 581 """Return the color tint for this Achievement's icon.""" 582 if complete: 583 return self._icon_color 584 return 1.0, 1.0, 1.0, 0.6
Return the color tint for this Achievement's icon.
586 @property 587 def hard_mode_only(self) -> bool: 588 """Whether this Achievement is only unlockable in hard-mode.""" 589 return self._hard_mode_only
Whether this Achievement is only unlockable in hard-mode.
591 @property 592 def complete(self) -> bool: 593 """Whether this Achievement is currently complete.""" 594 val: bool = self._getconfig()['Complete'] 595 assert isinstance(val, bool) 596 return val
Whether this Achievement is currently complete.
598 def announce_completion(self, sound: bool = True) -> None: 599 """Kick off an announcement for this achievement's completion.""" 600 601 app = babase.app 602 plus = app.plus 603 classic = app.classic 604 if plus is None or classic is None: 605 logging.warning('ach account_completion not available.') 606 return 607 608 ach_ss = classic.ach 609 610 # Even though there are technically achievements when we're not 611 # signed in, lets not show them (otherwise we tend to get 612 # confusing 'controller connected' achievements popping up while 613 # waiting to sign in which can be confusing). 614 if plus.get_v1_account_state() != 'signed_in': 615 return 616 617 # If we're being freshly complete, display/report it and whatnot. 618 if (self, sound) not in ach_ss.achievements_to_display: 619 ach_ss.achievements_to_display.append((self, sound)) 620 621 # If there's no achievement display timer going, kick one off 622 # (if one's already running it will pick this up before it dies). 623 624 # Need to check last time too; its possible our timer wasn't able to 625 # clear itself if an activity died and took it down with it. 626 if ( 627 ach_ss.achievement_display_timer is None 628 or babase.apptime() - ach_ss.last_achievement_display_time > 2.0 629 ) and bascenev1.getactivity(doraise=False) is not None: 630 ach_ss.achievement_display_timer = bascenev1.BaseTimer( 631 1.0, _display_next_achievement, repeat=True 632 ) 633 634 # Show the first immediately. 635 _display_next_achievement()
Kick off an announcement for this achievement's completion.
637 def set_complete(self, complete: bool = True) -> None: 638 """Set an achievement's completed state. 639 640 note this only sets local state; use a transaction to 641 actually award achievements. 642 """ 643 config = self._getconfig() 644 if complete != config['Complete']: 645 config['Complete'] = complete
Set an achievement's completed state.
note this only sets local state; use a transaction to actually award achievements.
647 @property 648 def display_name(self) -> babase.Lstr: 649 """Return a babase.Lstr for this Achievement's name.""" 650 name: babase.Lstr | str 651 try: 652 if self._level_name != '': 653 campaignname, campaign_level = self._level_name.split(':') 654 classic = babase.app.classic 655 assert classic is not None 656 name = ( 657 classic.getcampaign(campaignname) 658 .getlevel(campaign_level) 659 .displayname 660 ) 661 else: 662 name = '' 663 except Exception: 664 name = '' 665 logging.exception('Error calcing achievement display-name.') 666 return babase.Lstr( 667 resource='achievements.' + self._name + '.name', 668 subs=[('${LEVEL}', name)], 669 )
Return a babase.Lstr for this Achievement's name.
671 @property 672 def description(self) -> babase.Lstr: 673 """Get a babase.Lstr for the Achievement's brief description.""" 674 if ( 675 'description' 676 in babase.app.lang.get_resource('achievements')[self._name] 677 ): 678 return babase.Lstr( 679 resource='achievements.' + self._name + '.description' 680 ) 681 return babase.Lstr( 682 resource='achievements.' + self._name + '.descriptionFull' 683 )
Get a babase.Lstr for the Achievement's brief description.
685 @property 686 def description_complete(self) -> babase.Lstr: 687 """Get a babase.Lstr for the Achievement's description when complete.""" 688 if ( 689 'descriptionComplete' 690 in babase.app.lang.get_resource('achievements')[self._name] 691 ): 692 return babase.Lstr( 693 resource='achievements.' + self._name + '.descriptionComplete' 694 ) 695 return babase.Lstr( 696 resource='achievements.' + self._name + '.descriptionFullComplete' 697 )
Get a babase.Lstr for the Achievement's description when complete.
699 @property 700 def description_full(self) -> babase.Lstr: 701 """Get a babase.Lstr for the Achievement's full description.""" 702 return babase.Lstr( 703 resource='achievements.' + self._name + '.descriptionFull', 704 subs=[ 705 ( 706 '${LEVEL}', 707 babase.Lstr( 708 translate=( 709 'coopLevelNames', 710 ACH_LEVEL_NAMES.get(self._name, '?'), 711 ) 712 ), 713 ) 714 ], 715 )
Get a babase.Lstr for the Achievement's full description.
717 @property 718 def description_full_complete(self) -> babase.Lstr: 719 """Get a babase.Lstr for the Achievement's full desc. when completed.""" 720 return babase.Lstr( 721 resource='achievements.' + self._name + '.descriptionFullComplete', 722 subs=[ 723 ( 724 '${LEVEL}', 725 babase.Lstr( 726 translate=( 727 'coopLevelNames', 728 ACH_LEVEL_NAMES.get(self._name, '?'), 729 ) 730 ), 731 ) 732 ], 733 )
Get a babase.Lstr for the Achievement's full desc. when completed.
735 def get_award_chest_type(self) -> ClassicChestAppearance: 736 """Return the type of chest given for this achievement.""" 737 738 # For now just map our old ticket values to chest types. 739 # Can add distinct values if need be later. 740 plus = babase.app.plus 741 assert plus is not None 742 t = plus.get_v1_account_misc_read_val( 743 f'achAward.{self.name}', self._award 744 ) 745 return ( 746 ClassicChestAppearance.L6 747 if t >= 30 748 else ( 749 ClassicChestAppearance.L5 750 if t >= 25 751 else ( 752 ClassicChestAppearance.L4 753 if t >= 20 754 else ( 755 ClassicChestAppearance.L3 756 if t >= 15 757 else ( 758 ClassicChestAppearance.L2 759 if t >= 10 760 else ClassicChestAppearance.L1 761 ) 762 ) 763 ) 764 ) 765 )
Return the type of chest given for this achievement.
778 @property 779 def power_ranking_value(self) -> int: 780 """Get the power-ranking award value for this achievement.""" 781 plus = babase.app.plus 782 if plus is None: 783 return 0 784 val: int = plus.get_v1_account_misc_read_val( 785 'achLeaguePoints.' + self._name, self._award 786 ) 787 assert isinstance(val, int) 788 return val
Get the power-ranking award value for this achievement.
790 def create_display( 791 self, 792 x: float, 793 y: float, 794 delay: float, 795 *, 796 outdelay: float | None = None, 797 color: Sequence[float] | None = None, 798 style: str = 'post_game', 799 ) -> list[bascenev1.Actor]: 800 """Create a display for the Achievement. 801 802 Shows the Achievement icon, name, and description. 803 """ 804 # pylint: disable=cyclic-import 805 from bascenev1 import CoopSession 806 from bascenev1lib.actor.image import Image 807 from bascenev1lib.actor.text import Text 808 809 # Yeah this needs cleaning up. 810 if style == 'post_game': 811 in_game_colors = False 812 in_main_menu = False 813 h_attach = Text.HAttach.CENTER 814 v_attach = Text.VAttach.CENTER 815 attach = Image.Attach.CENTER 816 elif style == 'in_game': 817 in_game_colors = True 818 in_main_menu = False 819 h_attach = Text.HAttach.LEFT 820 v_attach = Text.VAttach.TOP 821 attach = Image.Attach.TOP_LEFT 822 elif style == 'news': 823 in_game_colors = True 824 in_main_menu = True 825 h_attach = Text.HAttach.CENTER 826 v_attach = Text.VAttach.TOP 827 attach = Image.Attach.TOP_CENTER 828 else: 829 raise ValueError('invalid style "' + style + '"') 830 831 # Attempt to determine what campaign we're in 832 # (so we know whether to show "hard mode only"). 833 if in_main_menu: 834 hmo = False 835 else: 836 try: 837 session = bascenev1.getsession() 838 if isinstance(session, CoopSession): 839 campaign = session.campaign 840 assert campaign is not None 841 hmo = self._hard_mode_only and campaign.name == 'Easy' 842 else: 843 hmo = False 844 except Exception: 845 logging.exception('Error determining campaign.') 846 hmo = False 847 848 objs: list[bascenev1.Actor] 849 850 if in_game_colors: 851 objs = [] 852 out_delay_fin = (delay + outdelay) if outdelay is not None else None 853 if color is not None: 854 cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3]) 855 cl2 = color 856 else: 857 cl1 = (1.5, 1.5, 2, 1.0) 858 cl2 = (0.8, 0.8, 1.0, 1.0) 859 860 if hmo: 861 cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) 862 cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) 863 864 objs.append( 865 Image( 866 self.get_icon_texture(False), 867 host_only=True, 868 color=cl1, 869 position=(x - 25, y + 5), 870 attach=attach, 871 transition=Image.Transition.FADE_IN, 872 transition_delay=delay, 873 vr_depth=4, 874 transition_out_delay=out_delay_fin, 875 scale=(40, 40), 876 ).autoretain() 877 ) 878 txt = self.display_name 879 txt_s = 0.85 880 txt_max_w = 300 881 objs.append( 882 Text( 883 txt, 884 host_only=True, 885 maxwidth=txt_max_w, 886 position=(x, y + 2), 887 transition=Text.Transition.FADE_IN, 888 scale=txt_s, 889 flatness=0.6, 890 shadow=0.5, 891 h_attach=h_attach, 892 v_attach=v_attach, 893 color=cl2, 894 transition_delay=delay + 0.05, 895 transition_out_delay=out_delay_fin, 896 ).autoretain() 897 ) 898 txt2_s = 0.62 899 txt2_max_w = 400 900 objs.append( 901 Text( 902 self.description_full if in_main_menu else self.description, 903 host_only=True, 904 maxwidth=txt2_max_w, 905 position=(x, y - 14), 906 transition=Text.Transition.FADE_IN, 907 vr_depth=-5, 908 h_attach=h_attach, 909 v_attach=v_attach, 910 scale=txt2_s, 911 flatness=1.0, 912 shadow=0.5, 913 color=cl2, 914 transition_delay=delay + 0.1, 915 transition_out_delay=out_delay_fin, 916 ).autoretain() 917 ) 918 919 if hmo: 920 txtactor = Text( 921 babase.Lstr(resource='difficultyHardOnlyText'), 922 host_only=True, 923 maxwidth=txt2_max_w * 0.7, 924 position=(x + 60, y + 5), 925 transition=Text.Transition.FADE_IN, 926 vr_depth=-5, 927 h_attach=h_attach, 928 v_attach=v_attach, 929 h_align=Text.HAlign.CENTER, 930 v_align=Text.VAlign.CENTER, 931 scale=txt_s * 0.8, 932 flatness=1.0, 933 shadow=0.5, 934 color=(1, 1, 0.6, 1), 935 transition_delay=delay + 0.1, 936 transition_out_delay=out_delay_fin, 937 ).autoretain() 938 txtactor.node.rotate = 10 939 objs.append(txtactor) 940 941 # Chest award. 942 award_x = -100 943 chesttype = self.get_award_chest_type() 944 chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( 945 chesttype, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 946 ) 947 objs.append( 948 Image( 949 # Provide magical extended dict version of texture 950 # that Image actor supports. 951 texture={ 952 'texture': bascenev1.gettexture( 953 chestdisplayinfo.texclosed 954 ), 955 'tint_texture': bascenev1.gettexture( 956 chestdisplayinfo.texclosedtint 957 ), 958 'tint_color': chestdisplayinfo.tint, 959 'tint2_color': chestdisplayinfo.tint2, 960 'mask_texture': None, 961 }, 962 color=chestdisplayinfo.color + (0.5 if hmo else 1.0,), 963 position=(x + award_x + 37, y + 12), 964 scale=(32.0, 32.0), 965 transition=Image.Transition.FADE_IN, 966 transition_delay=delay + 0.05, 967 transition_out_delay=out_delay_fin, 968 host_only=True, 969 attach=Image.Attach.TOP_LEFT, 970 ).autoretain() 971 ) 972 973 # objs.append( 974 # Text( 975 # babase.charstr(babase.SpecialChar.TICKET), 976 # host_only=True, 977 # position=(x + award_x + 33, y + 7), 978 # transition=Text.Transition.FADE_IN, 979 # scale=1.5, 980 # h_attach=h_attach, 981 # v_attach=v_attach, 982 # h_align=Text.HAlign.CENTER, 983 # v_align=Text.VAlign.CENTER, 984 # color=(1, 1, 1, 0.2 if hmo else 0.4), 985 # transition_delay=delay + 0.05, 986 # transition_out_delay=out_delay_fin, 987 # ).autoretain() 988 # ) 989 # objs.append( 990 # Text( 991 # '+' + str(self.get_award_ticket_value()), 992 # host_only=True, 993 # position=(x + award_x + 28, y + 16), 994 # transition=Text.Transition.FADE_IN, 995 # scale=0.7, 996 # flatness=1, 997 # h_attach=h_attach, 998 # v_attach=v_attach, 999 # h_align=Text.HAlign.CENTER, 1000 # v_align=Text.VAlign.CENTER, 1001 # color=cl2, 1002 # transition_delay=delay + 0.05, 1003 # transition_out_delay=out_delay_fin, 1004 # ).autoretain() 1005 # ) 1006 1007 else: 1008 complete = self.complete 1009 objs = [] 1010 c_icon = self.get_icon_color(complete) 1011 if hmo and not complete: 1012 c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) 1013 objs.append( 1014 Image( 1015 self.get_icon_texture(complete), 1016 host_only=True, 1017 color=c_icon, 1018 position=(x - 25, y + 5), 1019 attach=attach, 1020 vr_depth=4, 1021 transition=Image.Transition.IN_RIGHT, 1022 transition_delay=delay, 1023 transition_out_delay=None, 1024 scale=(40, 40), 1025 ).autoretain() 1026 ) 1027 if complete: 1028 objs.append( 1029 Image( 1030 bascenev1.gettexture('achievementOutline'), 1031 host_only=True, 1032 mesh_transparent=bascenev1.getmesh( 1033 'achievementOutline' 1034 ), 1035 color=(2, 1.4, 0.4, 1), 1036 vr_depth=8, 1037 position=(x - 25, y + 5), 1038 attach=attach, 1039 transition=Image.Transition.IN_RIGHT, 1040 transition_delay=delay, 1041 transition_out_delay=None, 1042 scale=(40, 40), 1043 ).autoretain() 1044 ) 1045 else: 1046 if not complete: 1047 award_x = -100 1048 # objs.append( 1049 # Text( 1050 # babase.charstr(babase.SpecialChar.TICKET), 1051 # host_only=True, 1052 # position=(x + award_x + 33, y + 7), 1053 # transition=Text.Transition.IN_RIGHT, 1054 # scale=1.5, 1055 # h_attach=h_attach, 1056 # v_attach=v_attach, 1057 # h_align=Text.HAlign.CENTER, 1058 # v_align=Text.VAlign.CENTER, 1059 # color=(1, 1, 1, (0.1 if hmo else 0.2)), 1060 # transition_delay=delay + 0.05, 1061 # transition_out_delay=None, 1062 # ).autoretain() 1063 # ) 1064 chesttype = self.get_award_chest_type() 1065 chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get( 1066 chesttype, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 1067 ) 1068 objs.append( 1069 Image( 1070 # Provide magical extended dict version of texture 1071 # that Image actor supports. 1072 texture={ 1073 'texture': bascenev1.gettexture( 1074 chestdisplayinfo.texclosed 1075 ), 1076 'tint_texture': bascenev1.gettexture( 1077 chestdisplayinfo.texclosedtint 1078 ), 1079 'tint_color': chestdisplayinfo.tint, 1080 'tint2_color': chestdisplayinfo.tint2, 1081 'mask_texture': None, 1082 }, 1083 color=chestdisplayinfo.color 1084 + (0.5 if hmo else 1.0,), 1085 position=(x + award_x + 38, y + 14), 1086 scale=(32.0, 32.0), 1087 transition=Image.Transition.IN_RIGHT, 1088 transition_delay=delay + 0.05, 1089 transition_out_delay=None, 1090 host_only=True, 1091 attach=attach, 1092 ).autoretain() 1093 ) 1094 1095 # objs.append( 1096 # Text( 1097 # '+' + str(self.get_award_ticket_value()), 1098 # host_only=True, 1099 # position=(x + award_x + 28, y + 16), 1100 # transition=Text.Transition.IN_RIGHT, 1101 # scale=0.7, 1102 # flatness=1, 1103 # h_attach=h_attach, 1104 # v_attach=v_attach, 1105 # h_align=Text.HAlign.CENTER, 1106 # v_align=Text.VAlign.CENTER, 1107 # color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)), 1108 # transition_delay=delay + 0.05, 1109 # transition_out_delay=None, 1110 # ).autoretain() 1111 # ) 1112 1113 # Show 'hard-mode-only' only over incomplete achievements 1114 # when that's the case. 1115 if hmo: 1116 txtactor = Text( 1117 babase.Lstr(resource='difficultyHardOnlyText'), 1118 host_only=True, 1119 maxwidth=300 * 0.7, 1120 position=(x + 60, y + 5), 1121 transition=Text.Transition.FADE_IN, 1122 vr_depth=-5, 1123 h_attach=h_attach, 1124 v_attach=v_attach, 1125 h_align=Text.HAlign.CENTER, 1126 v_align=Text.VAlign.CENTER, 1127 scale=0.85 * 0.8, 1128 flatness=1.0, 1129 shadow=0.5, 1130 color=(1, 1, 0.6, 1), 1131 transition_delay=delay + 0.05, 1132 transition_out_delay=None, 1133 ).autoretain() 1134 assert txtactor.node 1135 txtactor.node.rotate = 10 1136 objs.append(txtactor) 1137 1138 objs.append( 1139 Text( 1140 self.display_name, 1141 host_only=True, 1142 maxwidth=300, 1143 position=(x, y + 2), 1144 transition=Text.Transition.IN_RIGHT, 1145 scale=0.85, 1146 flatness=0.6, 1147 h_attach=h_attach, 1148 v_attach=v_attach, 1149 color=( 1150 (0.8, 0.93, 0.8, 1.0) 1151 if complete 1152 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1153 ), 1154 transition_delay=delay + 0.05, 1155 transition_out_delay=None, 1156 ).autoretain() 1157 ) 1158 objs.append( 1159 Text( 1160 self.description_complete if complete else self.description, 1161 host_only=True, 1162 maxwidth=400, 1163 position=(x, y - 14), 1164 transition=Text.Transition.IN_RIGHT, 1165 vr_depth=-5, 1166 h_attach=h_attach, 1167 v_attach=v_attach, 1168 scale=0.62, 1169 flatness=1.0, 1170 color=( 1171 (0.6, 0.6, 0.6, 1.0) 1172 if complete 1173 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1174 ), 1175 transition_delay=delay + 0.1, 1176 transition_out_delay=None, 1177 ).autoretain() 1178 ) 1179 return objs
Create a display for the Achievement.
Shows the Achievement icon, name, and description.
74class AchievementSubsystem: 75 """Subsystem for achievement handling. 76 77 Category: **App Classes** 78 79 Access the single shared instance of this class at 'ba.app.ach'. 80 """ 81 82 def __init__(self) -> None: 83 self.achievements: list[Achievement] = [] 84 self.achievements_to_display: list[ 85 tuple[baclassic.Achievement, bool] 86 ] = [] 87 self.achievement_display_timer: bascenev1.BaseTimer | None = None 88 self.last_achievement_display_time: float = 0.0 89 self.achievement_completion_banner_slots: set[int] = set() 90 self._init_achievements() 91 92 def _init_achievements(self) -> None: 93 """Fill in available achievements.""" 94 95 self.achievements += [ 96 Achievement( 97 'In Control', 98 'achievementInControl', 99 (1, 1, 1), 100 '', 101 award=5, 102 ), 103 Achievement( 104 'Sharing is Caring', 105 'achievementSharingIsCaring', 106 (1, 1, 1), 107 '', 108 award=15, 109 ), 110 Achievement( 111 'Dual Wielding', 112 'achievementDualWielding', 113 (1, 1, 1), 114 '', 115 award=10, 116 ), 117 Achievement( 118 'Free Loader', 119 'achievementFreeLoader', 120 (1, 1, 1), 121 '', 122 award=10, 123 ), 124 Achievement( 125 'Team Player', 126 'achievementTeamPlayer', 127 (1, 1, 1), 128 '', 129 award=20, 130 ), 131 Achievement( 132 'Onslaught Training Victory', 133 'achievementOnslaught', 134 (1, 1, 1), 135 'Default:Onslaught Training', 136 award=5, 137 ), 138 Achievement( 139 'Off You Go Then', 140 'achievementOffYouGo', 141 (1, 1.1, 1.3), 142 'Default:Onslaught Training', 143 award=5, 144 ), 145 Achievement( 146 'Boxer', 147 'achievementBoxer', 148 (1, 0.6, 0.6), 149 'Default:Onslaught Training', 150 award=10, 151 hard_mode_only=True, 152 ), 153 Achievement( 154 'Rookie Onslaught Victory', 155 'achievementOnslaught', 156 (0.5, 1.4, 0.6), 157 'Default:Rookie Onslaught', 158 award=10, 159 ), 160 Achievement( 161 'Mine Games', 162 'achievementMine', 163 (1, 1, 1.4), 164 'Default:Rookie Onslaught', 165 award=10, 166 ), 167 Achievement( 168 'Flawless Victory', 169 'achievementFlawlessVictory', 170 (1, 1, 1), 171 'Default:Rookie Onslaught', 172 award=15, 173 hard_mode_only=True, 174 ), 175 Achievement( 176 'Rookie Football Victory', 177 'achievementFootballVictory', 178 (1.0, 1, 0.6), 179 'Default:Rookie Football', 180 award=10, 181 ), 182 Achievement( 183 'Super Punch', 184 'achievementSuperPunch', 185 (1, 1, 1.8), 186 'Default:Rookie Football', 187 award=10, 188 ), 189 Achievement( 190 'Rookie Football Shutout', 191 'achievementFootballShutout', 192 (1, 1, 1), 193 'Default:Rookie Football', 194 award=15, 195 hard_mode_only=True, 196 ), 197 Achievement( 198 'Pro Onslaught Victory', 199 'achievementOnslaught', 200 (0.3, 1, 2.0), 201 'Default:Pro Onslaught', 202 award=15, 203 ), 204 Achievement( 205 'Boom Goes the Dynamite', 206 'achievementTNT', 207 (1.4, 1.2, 0.8), 208 'Default:Pro Onslaught', 209 award=15, 210 ), 211 Achievement( 212 'Pro Boxer', 213 'achievementBoxer', 214 (2, 2, 0), 215 'Default:Pro Onslaught', 216 award=20, 217 hard_mode_only=True, 218 ), 219 Achievement( 220 'Pro Football Victory', 221 'achievementFootballVictory', 222 (1.3, 1.3, 2.0), 223 'Default:Pro Football', 224 award=15, 225 ), 226 Achievement( 227 'Super Mega Punch', 228 'achievementSuperPunch', 229 (2, 1, 0.6), 230 'Default:Pro Football', 231 award=15, 232 ), 233 Achievement( 234 'Pro Football Shutout', 235 'achievementFootballShutout', 236 (0.7, 0.7, 2.0), 237 'Default:Pro Football', 238 award=20, 239 hard_mode_only=True, 240 ), 241 Achievement( 242 'Pro Runaround Victory', 243 'achievementRunaround', 244 (1, 1, 1), 245 'Default:Pro Runaround', 246 award=15, 247 ), 248 Achievement( 249 'Precision Bombing', 250 'achievementCrossHair', 251 (1, 1, 1.3), 252 'Default:Pro Runaround', 253 award=20, 254 hard_mode_only=True, 255 ), 256 Achievement( 257 'The Wall', 258 'achievementWall', 259 (1, 0.7, 0.7), 260 'Default:Pro Runaround', 261 award=25, 262 hard_mode_only=True, 263 ), 264 Achievement( 265 'Uber Onslaught Victory', 266 'achievementOnslaught', 267 (2, 2, 1), 268 'Default:Uber Onslaught', 269 award=30, 270 ), 271 Achievement( 272 'Gold Miner', 273 'achievementMine', 274 (2, 1.6, 0.2), 275 'Default:Uber Onslaught', 276 award=30, 277 hard_mode_only=True, 278 ), 279 Achievement( 280 'TNT Terror', 281 'achievementTNT', 282 (2, 1.8, 0.3), 283 'Default:Uber Onslaught', 284 award=30, 285 hard_mode_only=True, 286 ), 287 Achievement( 288 'Uber Football Victory', 289 'achievementFootballVictory', 290 (1.8, 1.4, 0.3), 291 'Default:Uber Football', 292 award=30, 293 ), 294 Achievement( 295 'Got the Moves', 296 'achievementGotTheMoves', 297 (2, 1, 0), 298 'Default:Uber Football', 299 award=30, 300 hard_mode_only=True, 301 ), 302 Achievement( 303 'Uber Football Shutout', 304 'achievementFootballShutout', 305 (2, 2, 0), 306 'Default:Uber Football', 307 award=40, 308 hard_mode_only=True, 309 ), 310 Achievement( 311 'Uber Runaround Victory', 312 'achievementRunaround', 313 (1.5, 1.2, 0.2), 314 'Default:Uber Runaround', 315 award=30, 316 ), 317 Achievement( 318 'The Great Wall', 319 'achievementWall', 320 (2, 1.7, 0.4), 321 'Default:Uber Runaround', 322 award=40, 323 hard_mode_only=True, 324 ), 325 Achievement( 326 'Stayin\' Alive', 327 'achievementStayinAlive', 328 (2, 2, 1), 329 'Default:Uber Runaround', 330 award=40, 331 hard_mode_only=True, 332 ), 333 Achievement( 334 'Last Stand Master', 335 'achievementMedalSmall', 336 (2, 1.5, 0.3), 337 'Default:The Last Stand', 338 award=20, 339 hard_mode_only=True, 340 ), 341 Achievement( 342 'Last Stand Wizard', 343 'achievementMedalMedium', 344 (2, 1.5, 0.3), 345 'Default:The Last Stand', 346 award=40, 347 hard_mode_only=True, 348 ), 349 Achievement( 350 'Last Stand God', 351 'achievementMedalLarge', 352 (2, 1.5, 0.3), 353 'Default:The Last Stand', 354 award=60, 355 hard_mode_only=True, 356 ), 357 Achievement( 358 'Onslaught Master', 359 'achievementMedalSmall', 360 (0.7, 1, 0.7), 361 'Challenges:Infinite Onslaught', 362 award=5, 363 ), 364 Achievement( 365 'Onslaught Wizard', 366 'achievementMedalMedium', 367 (0.7, 1.0, 0.7), 368 'Challenges:Infinite Onslaught', 369 award=15, 370 ), 371 Achievement( 372 'Onslaught God', 373 'achievementMedalLarge', 374 (0.7, 1.0, 0.7), 375 'Challenges:Infinite Onslaught', 376 award=30, 377 ), 378 Achievement( 379 'Runaround Master', 380 'achievementMedalSmall', 381 (1.0, 1.0, 1.2), 382 'Challenges:Infinite Runaround', 383 award=5, 384 ), 385 Achievement( 386 'Runaround Wizard', 387 'achievementMedalMedium', 388 (1.0, 1.0, 1.2), 389 'Challenges:Infinite Runaround', 390 award=15, 391 ), 392 Achievement( 393 'Runaround God', 394 'achievementMedalLarge', 395 (1.0, 1.0, 1.2), 396 'Challenges:Infinite Runaround', 397 award=30, 398 ), 399 ] 400 401 def award_local_achievement(self, achname: str) -> None: 402 """For non-game-based achievements such as controller-connection.""" 403 plus = babase.app.plus 404 if plus is None: 405 logging.warning('achievements require plus feature-set') 406 return 407 try: 408 ach = self.get_achievement(achname) 409 if not ach.complete: 410 # Report new achievements to the game-service. 411 plus.report_achievement(achname) 412 413 # And to our account. 414 plus.add_v1_account_transaction( 415 {'type': 'ACHIEVEMENT', 'name': achname} 416 ) 417 418 # Now attempt to show a banner. 419 self.display_achievement_banner(achname) 420 421 except Exception: 422 logging.exception('Error in award_local_achievement.') 423 424 def display_achievement_banner(self, achname: str) -> None: 425 """Display a completion banner for an achievement. 426 427 (internal) 428 429 Used for server-driven achievements. 430 """ 431 try: 432 # FIXME: Need to get these using the UI context or some other 433 # purely local context somehow instead of trying to inject these 434 # into whatever activity happens to be active 435 # (since that won't work while in client mode). 436 activity = bascenev1.get_foreground_host_activity() 437 if activity is not None: 438 with activity.context: 439 self.get_achievement(achname).announce_completion() 440 except Exception: 441 logging.exception('Error in display_achievement_banner.') 442 443 def set_completed_achievements(self, achs: Sequence[str]) -> None: 444 """Set the current state of completed achievements. 445 446 (internal) 447 448 All achievements not included here will be set incomplete. 449 """ 450 451 # Note: This gets called whenever game-center/game-circle/etc tells 452 # us which achievements we currently have. We always defer to them, 453 # even if that means we have to un-set an achievement we think we have. 454 455 cfg = babase.app.config 456 cfg['Achievements'] = {} 457 for a_name in achs: 458 self.get_achievement(a_name).set_complete(True) 459 cfg.commit() 460 461 def get_achievement(self, name: str) -> Achievement: 462 """Return an Achievement by name.""" 463 achs = [a for a in self.achievements if a.name == name] 464 assert len(achs) < 2 465 if not achs: 466 raise ValueError("Invalid achievement name: '" + name + "'") 467 return achs[0] 468 469 def achievements_for_coop_level(self, level_name: str) -> list[Achievement]: 470 """Given a level name, return achievements available for it.""" 471 472 # For the Easy campaign we return achievements for the Default 473 # campaign too. (want the user to see what achievements are part of the 474 # level even if they can't unlock them all on easy mode). 475 return [ 476 a 477 for a in self.achievements 478 if a.level_name 479 in (level_name, level_name.replace('Easy', 'Default')) 480 ] 481 482 def _test(self) -> None: 483 """For testing achievement animations.""" 484 485 def testcall1() -> None: 486 self.achievements[0].announce_completion() 487 self.achievements[1].announce_completion() 488 self.achievements[2].announce_completion() 489 490 def testcall2() -> None: 491 self.achievements[3].announce_completion() 492 self.achievements[4].announce_completion() 493 self.achievements[5].announce_completion() 494 495 bascenev1.basetimer(3.0, testcall1) 496 bascenev1.basetimer(7.0, testcall2)
Subsystem for achievement handling.
Category: App Classes
Access the single shared instance of this class at 'ba.app.ach'.
401 def award_local_achievement(self, achname: str) -> None: 402 """For non-game-based achievements such as controller-connection.""" 403 plus = babase.app.plus 404 if plus is None: 405 logging.warning('achievements require plus feature-set') 406 return 407 try: 408 ach = self.get_achievement(achname) 409 if not ach.complete: 410 # Report new achievements to the game-service. 411 plus.report_achievement(achname) 412 413 # And to our account. 414 plus.add_v1_account_transaction( 415 {'type': 'ACHIEVEMENT', 'name': achname} 416 ) 417 418 # Now attempt to show a banner. 419 self.display_achievement_banner(achname) 420 421 except Exception: 422 logging.exception('Error in award_local_achievement.')
For non-game-based achievements such as controller-connection.
461 def get_achievement(self, name: str) -> Achievement: 462 """Return an Achievement by name.""" 463 achs = [a for a in self.achievements if a.name == name] 464 assert len(achs) < 2 465 if not achs: 466 raise ValueError("Invalid achievement name: '" + name + "'") 467 return achs[0]
Return an Achievement by name.
469 def achievements_for_coop_level(self, level_name: str) -> list[Achievement]: 470 """Given a level name, return achievements available for it.""" 471 472 # For the Easy campaign we return achievements for the Default 473 # campaign too. (want the user to see what achievements are part of the 474 # level even if they can't unlock them all on easy mode). 475 return [ 476 a 477 for a in self.achievements 478 if a.level_name 479 in (level_name, level_name.replace('Easy', 'Default')) 480 ]
Given a level name, return achievements available for it.
18def show_display_item( 19 itemwrapper: bacommon.bs.DisplayItemWrapper, 20 parent: bauiv1.Widget, 21 pos: tuple[float, float], 22 width: float, 23) -> None: 24 """Create ui to depict a display-item.""" 25 26 height = width * 0.666 27 28 # Silent no-op if our parent ui is dead. 29 if not parent: 30 return 31 32 img: str | None = None 33 img_y_offs = 0.0 34 text_y_offs = 0.0 35 show_text = True 36 37 if isinstance(itemwrapper.item, bacommon.bs.TicketsDisplayItem): 38 img = 'tickets' 39 img_y_offs = width * 0.11 40 text_y_offs = width * -0.15 41 elif isinstance(itemwrapper.item, bacommon.bs.TokensDisplayItem): 42 img = 'coin' 43 img_y_offs = width * 0.11 44 text_y_offs = width * -0.15 45 elif isinstance(itemwrapper.item, bacommon.bs.ChestDisplayItem): 46 from baclassic._chest import ( 47 CHEST_APPEARANCE_DISPLAY_INFOS, 48 CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT, 49 ) 50 51 img = None 52 show_text = False 53 c_info = CHEST_APPEARANCE_DISPLAY_INFOS.get( 54 itemwrapper.item.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT 55 ) 56 c_size = width * 0.85 57 bauiv1.imagewidget( 58 parent=parent, 59 position=(pos[0] - c_size * 0.5, pos[1] - c_size * 0.5), 60 color=c_info.color, 61 size=(c_size, c_size), 62 texture=bauiv1.gettexture(c_info.texclosed), 63 tint_texture=bauiv1.gettexture(c_info.texclosedtint), 64 tint_color=c_info.tint, 65 tint2_color=c_info.tint2, 66 ) 67 68 # Enable this for testing spacing. 69 if bool(False): 70 bauiv1.imagewidget( 71 parent=parent, 72 position=( 73 pos[0] - width * 0.5, 74 pos[1] - height * 0.5, 75 ), 76 size=(width, height), 77 texture=bauiv1.gettexture('white'), 78 color=(0, 1, 0), 79 opacity=0.1, 80 ) 81 82 imgsize = width * 0.33 83 if img is not None: 84 bauiv1.imagewidget( 85 parent=parent, 86 position=( 87 pos[0] - imgsize * 0.5, 88 pos[1] + img_y_offs - imgsize * 0.5, 89 ), 90 size=(imgsize, imgsize), 91 texture=bauiv1.gettexture(img), 92 ) 93 if show_text: 94 subs = itemwrapper.description_subs 95 if subs is None: 96 subs = [] 97 bauiv1.textwidget( 98 parent=parent, 99 position=(pos[0], pos[1] + text_y_offs), 100 scale=width * 0.006, 101 size=(0, 0), 102 text=bauiv1.Lstr( 103 translate=('displayItemNames', itemwrapper.description), 104 subs=pairs_from_flat(subs), 105 ), 106 maxwidth=width * 0.9, 107 color=(0.0, 1.0, 0.0), 108 h_align='center', 109 v_align='center', 110 )
Create ui to depict a display-item.