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