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 9 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 )
29class ClassicAppMode(AppMode): 30 """AppMode for the classic BombSquad experience.""" 31 32 @override 33 @classmethod 34 def get_app_experience(cls) -> AppExperience: 35 return AppExperience.MELEE 36 37 @override 38 @classmethod 39 def _supports_intent(cls, intent: AppIntent) -> bool: 40 # We support default and exec intents currently. 41 return isinstance(intent, AppIntentExec | AppIntentDefault) 42 43 @override 44 def handle_intent(self, intent: AppIntent) -> None: 45 if isinstance(intent, AppIntentExec): 46 _baclassic.classic_app_mode_handle_app_intent_exec(intent.code) 47 return 48 assert isinstance(intent, AppIntentDefault) 49 _baclassic.classic_app_mode_handle_app_intent_default() 50 51 @override 52 def on_activate(self) -> None: 53 print('CLASSIC ACTIVATING') 54 55 # Let the native layer do its thing. 56 _baclassic.classic_app_mode_activate() 57 58 # Wire up the root ui to do what we want. 59 ui = app.ui_v1 60 ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = ( 61 self._root_ui_account_press 62 ) 63 ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = ( 64 self._root_ui_menu_press 65 ) 66 ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = ( 67 self._root_ui_squad_press 68 ) 69 ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = ( 70 self._root_ui_settings_press 71 ) 72 ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = ( 73 self._root_ui_store_press 74 ) 75 ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = ( 76 self._root_ui_inventory_press 77 ) 78 ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = ( 79 self._root_ui_get_tokens_press 80 ) 81 ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = ( 82 self._root_ui_inbox_press 83 ) 84 ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = ( 85 self._root_ui_tickets_meter_press 86 ) 87 ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = ( 88 self._root_ui_tokens_meter_press 89 ) 90 ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = ( 91 self._root_ui_trophy_meter_press 92 ) 93 ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = ( 94 self._root_ui_level_meter_press 95 ) 96 ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = ( 97 self._root_ui_achievements_press 98 ) 99 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial( 100 self._root_ui_chest_slot_pressed, 1 101 ) 102 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial( 103 self._root_ui_chest_slot_pressed, 2 104 ) 105 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial( 106 self._root_ui_chest_slot_pressed, 3 107 ) 108 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_4] = partial( 109 self._root_ui_chest_slot_pressed, 4 110 ) 111 112 @override 113 def on_deactivate(self) -> None: 114 print('CLASSIC DEACTIVATING') 115 # Let the native layer do its thing. 116 _baclassic.classic_app_mode_deactivate() 117 118 @override 119 def on_app_active_changed(self) -> None: 120 # If we've gone inactive, bring up the main menu, which has the 121 # side effect of pausing the action (when possible). 122 if not app.active: 123 invoke_main_menu() 124 125 def _jump_to_main_window(self, window: MainWindow) -> None: 126 """Jump to a window with the main menu as its parent.""" 127 from bauiv1lib.mainmenu import MainMenuWindow 128 from bauiv1lib.ingamemenu import InGameMenuWindow 129 130 ui = app.ui_v1 131 132 old_window = ui.get_main_window() 133 134 if isinstance(old_window, (MainMenuWindow, InGameMenuWindow)): 135 # If we're currently in the top level menu window, just push 136 # our mainwindow on to the end. 137 old_window.main_window_replace(window) 138 else: 139 # Blow away the window stack and build a fresh one. 140 ui.clear_main_window() 141 142 back_state = ( 143 MainMenuWindow.do_get_main_window_state() 144 if in_main_menu() 145 else InGameMenuWindow.do_get_main_window_state() 146 ) 147 # set_main_window() needs this to be set. 148 back_state.is_top_level = True 149 150 ui.set_main_window( 151 window, 152 from_window=False, # Disable from-check. 153 back_state=back_state, 154 suppress_warning=True, 155 ) 156 157 def _root_ui_menu_press(self) -> None: 158 from babase import push_back_press 159 160 ui = app.ui_v1 161 162 # If *any* main-window is up, kill it. 163 old_window = ui.get_main_window() 164 if old_window is not None: 165 ui.clear_main_window() 166 return 167 168 push_back_press() 169 170 def _root_ui_account_press(self) -> None: 171 import bauiv1 172 from bauiv1lib.account.settings import AccountSettingsWindow 173 174 ui = app.ui_v1 175 176 # If the window is already showing, back out of it. 177 current_main_window = ui.get_main_window() 178 if isinstance(current_main_window, AccountSettingsWindow): 179 current_main_window.main_window_back() 180 return 181 182 self._jump_to_main_window( 183 AccountSettingsWindow( 184 origin_widget=bauiv1.get_special_widget('account_button') 185 ) 186 ) 187 188 def _root_ui_squad_press(self) -> None: 189 import bauiv1 190 191 btn = bauiv1.get_special_widget('squad_button') 192 center = btn.get_screen_space_center() 193 if bauiv1.app.classic is not None: 194 bauiv1.app.classic.party_icon_activate(center) 195 else: 196 logging.warning('party_icon_activate: no classic.') 197 198 def _root_ui_settings_press(self) -> None: 199 import bauiv1 200 from bauiv1lib.settings.allsettings import AllSettingsWindow 201 202 ui = app.ui_v1 203 204 # If the window is already showing, back out of it. 205 current_main_window = ui.get_main_window() 206 if isinstance(current_main_window, AllSettingsWindow): 207 current_main_window.main_window_back() 208 return 209 210 self._jump_to_main_window( 211 AllSettingsWindow( 212 origin_widget=bauiv1.get_special_widget('settings_button') 213 ) 214 ) 215 216 def _root_ui_achievements_press(self) -> None: 217 import bauiv1 218 from bauiv1lib.achievements import AchievementsWindow 219 220 btn = bauiv1.get_special_widget('achievements_button') 221 222 AchievementsWindow(position=btn.get_screen_space_center()) 223 224 def _root_ui_inbox_press(self) -> None: 225 import bauiv1 226 from bauiv1lib.inbox import InboxWindow 227 228 btn = bauiv1.get_special_widget('inbox_button') 229 230 InboxWindow(position=btn.get_screen_space_center()) 231 232 def _root_ui_store_press(self) -> None: 233 import bauiv1 234 from bauiv1lib.store.browser import StoreBrowserWindow 235 236 ui = app.ui_v1 237 238 # If the window is already showing, back out of it. 239 current_main_window = ui.get_main_window() 240 if isinstance(current_main_window, StoreBrowserWindow): 241 current_main_window.main_window_back() 242 return 243 244 self._jump_to_main_window( 245 StoreBrowserWindow( 246 origin_widget=bauiv1.get_special_widget('store_button') 247 ) 248 ) 249 250 def _root_ui_tickets_meter_press(self) -> None: 251 import bauiv1 252 from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 253 254 ResourceTypeInfoWindow( 255 'tickets', origin_widget=bauiv1.get_special_widget('tickets_meter') 256 ) 257 258 def _root_ui_tokens_meter_press(self) -> None: 259 import bauiv1 260 from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 261 262 ResourceTypeInfoWindow( 263 'tokens', origin_widget=bauiv1.get_special_widget('tokens_meter') 264 ) 265 266 def _root_ui_trophy_meter_press(self) -> None: 267 import bauiv1 268 from bauiv1lib.account import show_sign_in_prompt 269 from bauiv1lib.league.rankwindow import LeagueRankWindow 270 271 ui = app.ui_v1 272 273 # If the window is already showing, back out of it. 274 current_main_window = ui.get_main_window() 275 if isinstance(current_main_window, LeagueRankWindow): 276 current_main_window.main_window_back() 277 return 278 279 plus = bauiv1.app.plus 280 assert plus is not None 281 282 if plus.get_v1_account_state() != 'signed_in': 283 show_sign_in_prompt() 284 return 285 286 self._jump_to_main_window( 287 LeagueRankWindow( 288 origin_widget=bauiv1.get_special_widget('trophy_meter') 289 ) 290 ) 291 292 def _root_ui_level_meter_press(self) -> None: 293 import bauiv1 294 from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 295 296 ResourceTypeInfoWindow( 297 'xp', origin_widget=bauiv1.get_special_widget('level_meter') 298 ) 299 300 def _root_ui_inventory_press(self) -> None: 301 import bauiv1 302 from bauiv1lib.inventory import InventoryWindow 303 304 ui = app.ui_v1 305 306 # If the window is already showing, back out of it. 307 current_main_window = ui.get_main_window() 308 if isinstance(current_main_window, InventoryWindow): 309 current_main_window.main_window_back() 310 return 311 312 self._jump_to_main_window( 313 InventoryWindow( 314 origin_widget=bauiv1.get_special_widget('inventory_button') 315 ) 316 ) 317 318 def _root_ui_get_tokens_press(self) -> None: 319 import bauiv1 320 from bauiv1lib.gettokens import GetTokensWindow 321 322 GetTokensWindow( 323 origin_widget=bauiv1.get_special_widget('get_tokens_button') 324 ) 325 326 def _root_ui_chest_slot_pressed(self, index: int) -> None: 327 print(f'CHEST {index} PRESSED') 328 screenmessage('UNDER CONSTRUCTION.')
AppMode for the classic BombSquad experience.
32 @override 33 @classmethod 34 def get_app_experience(cls) -> AppExperience: 35 return AppExperience.MELEE
Return the overall experience provided by this mode.
43 @override 44 def handle_intent(self, intent: AppIntent) -> None: 45 if isinstance(intent, AppIntentExec): 46 _baclassic.classic_app_mode_handle_app_intent_exec(intent.code) 47 return 48 assert isinstance(intent, AppIntentDefault) 49 _baclassic.classic_app_mode_handle_app_intent_default()
Handle an intent.
51 @override 52 def on_activate(self) -> None: 53 print('CLASSIC ACTIVATING') 54 55 # Let the native layer do its thing. 56 _baclassic.classic_app_mode_activate() 57 58 # Wire up the root ui to do what we want. 59 ui = app.ui_v1 60 ui.root_ui_calls[ui.RootUIElement.ACCOUNT_BUTTON] = ( 61 self._root_ui_account_press 62 ) 63 ui.root_ui_calls[ui.RootUIElement.MENU_BUTTON] = ( 64 self._root_ui_menu_press 65 ) 66 ui.root_ui_calls[ui.RootUIElement.SQUAD_BUTTON] = ( 67 self._root_ui_squad_press 68 ) 69 ui.root_ui_calls[ui.RootUIElement.SETTINGS_BUTTON] = ( 70 self._root_ui_settings_press 71 ) 72 ui.root_ui_calls[ui.RootUIElement.STORE_BUTTON] = ( 73 self._root_ui_store_press 74 ) 75 ui.root_ui_calls[ui.RootUIElement.INVENTORY_BUTTON] = ( 76 self._root_ui_inventory_press 77 ) 78 ui.root_ui_calls[ui.RootUIElement.GET_TOKENS_BUTTON] = ( 79 self._root_ui_get_tokens_press 80 ) 81 ui.root_ui_calls[ui.RootUIElement.INBOX_BUTTON] = ( 82 self._root_ui_inbox_press 83 ) 84 ui.root_ui_calls[ui.RootUIElement.TICKETS_METER] = ( 85 self._root_ui_tickets_meter_press 86 ) 87 ui.root_ui_calls[ui.RootUIElement.TOKENS_METER] = ( 88 self._root_ui_tokens_meter_press 89 ) 90 ui.root_ui_calls[ui.RootUIElement.TROPHY_METER] = ( 91 self._root_ui_trophy_meter_press 92 ) 93 ui.root_ui_calls[ui.RootUIElement.LEVEL_METER] = ( 94 self._root_ui_level_meter_press 95 ) 96 ui.root_ui_calls[ui.RootUIElement.ACHIEVEMENTS_BUTTON] = ( 97 self._root_ui_achievements_press 98 ) 99 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_1] = partial( 100 self._root_ui_chest_slot_pressed, 1 101 ) 102 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_2] = partial( 103 self._root_ui_chest_slot_pressed, 2 104 ) 105 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_3] = partial( 106 self._root_ui_chest_slot_pressed, 3 107 ) 108 ui.root_ui_calls[ui.RootUIElement.CHEST_SLOT_4] = partial( 109 self._root_ui_chest_slot_pressed, 4 110 )
Called when the mode is being activated.
112 @override 113 def on_deactivate(self) -> None: 114 print('CLASSIC DEACTIVATING') 115 # Let the native layer do its thing. 116 _baclassic.classic_app_mode_deactivate()
Called when the mode is being deactivated.
118 @override 119 def on_app_active_changed(self) -> None: 120 # If we've gone inactive, bring up the main menu, which has the 121 # side effect of pausing the action (when possible). 122 if not app.active: 123 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 117 # Store. 118 self.store_layout: dict[str, list[dict[str, Any]]] | None = None 119 self.store_items: dict[str, dict] | None = None 120 self.pro_sale_start_time: int | None = None 121 self.pro_sale_start_val: int | None = None 122 123 def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None: 124 """(internal)""" 125 126 # If there's no main window up, just call immediately. 127 if not babase.app.ui_v1.has_main_window(): 128 with babase.ContextRef.empty(): 129 call() 130 else: 131 self.main_menu_resume_callbacks.append(call) 132 133 @property 134 def platform(self) -> str: 135 """Name of the current platform. 136 137 Examples are: 'mac', 'windows', android'. 138 """ 139 assert isinstance(self._env['platform'], str) 140 return self._env['platform'] 141 142 def scene_v1_protocol_version(self) -> int: 143 """(internal)""" 144 return bascenev1.protocol_version() 145 146 @property 147 def subplatform(self) -> str: 148 """String for subplatform. 149 150 Can be empty. For the 'android' platform, subplatform may 151 be 'google', 'amazon', etc. 152 """ 153 assert isinstance(self._env['subplatform'], str) 154 return self._env['subplatform'] 155 156 @property 157 def legacy_user_agent_string(self) -> str: 158 """String containing various bits of info about OS/device/etc.""" 159 assert isinstance(self._env['legacy_user_agent_string'], str) 160 return self._env['legacy_user_agent_string'] 161 162 @override 163 def on_app_loading(self) -> None: 164 from bascenev1lib.actor import spazappearance 165 from bascenev1lib import maps as stdmaps 166 167 plus = babase.app.plus 168 assert plus is not None 169 170 env = babase.app.env 171 cfg = babase.app.config 172 173 self.music.on_app_loading() 174 175 # Non-test, non-debug builds should generally be blessed; warn if not. 176 # (so I don't accidentally release a build that can't play tourneys) 177 if not env.debug and not env.test and not plus.is_blessed(): 178 babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) 179 180 # FIXME: This should not be hard-coded. 181 for maptype in [ 182 stdmaps.HockeyStadium, 183 stdmaps.FootballStadium, 184 stdmaps.Bridgit, 185 stdmaps.BigG, 186 stdmaps.Roundabout, 187 stdmaps.MonkeyFace, 188 stdmaps.ZigZag, 189 stdmaps.ThePad, 190 stdmaps.DoomShroom, 191 stdmaps.LakeFrigid, 192 stdmaps.TipTop, 193 stdmaps.CragCastle, 194 stdmaps.TowerD, 195 stdmaps.HappyThoughts, 196 stdmaps.StepRightUp, 197 stdmaps.Courtyard, 198 stdmaps.Rampage, 199 ]: 200 bascenev1.register_map(maptype) 201 202 spazappearance.register_appearances() 203 bascenev1.init_campaigns() 204 205 launch_count = cfg.get('launchCount', 0) 206 launch_count += 1 207 208 # So we know how many times we've run the game at various 209 # version milestones. 210 for key in ('lc14173', 'lc14292'): 211 cfg.setdefault(key, launch_count) 212 213 cfg['launchCount'] = launch_count 214 cfg.commit() 215 216 # If there's a leftover log file, attempt to upload it to the 217 # master-server and/or get rid of it. 218 babase.handle_leftover_v1_cloud_log_file() 219 220 self.accounts.on_app_loading() 221 222 @override 223 def on_app_suspend(self) -> None: 224 self.accounts.on_app_suspend() 225 226 @override 227 def on_app_unsuspend(self) -> None: 228 self.accounts.on_app_unsuspend() 229 self.music.on_app_unsuspend() 230 231 @override 232 def on_app_shutdown(self) -> None: 233 self.music.on_app_shutdown() 234 235 def pause(self) -> None: 236 """Pause the game due to a user request or menu popping up. 237 238 If there's a foreground host-activity that says it's pausable, tell it 239 to pause. Note: we now no longer pause if there are connected clients. 240 """ 241 activity: bascenev1.Activity | None = ( 242 bascenev1.get_foreground_host_activity() 243 ) 244 if ( 245 activity is not None 246 and activity.allow_pausing 247 and not bascenev1.have_connected_clients() 248 ): 249 from babase import Lstr 250 from bascenev1 import NodeActor 251 252 # FIXME: Shouldn't be touching scene stuff here; 253 # should just pass the request on to the host-session. 254 with activity.context: 255 globs = activity.globalsnode 256 if not globs.paused: 257 bascenev1.getsound('refWhistle').play() 258 globs.paused = True 259 260 # FIXME: This should not be an attr on Actor. 261 activity.paused_text = NodeActor( 262 bascenev1.newnode( 263 'text', 264 attrs={ 265 'text': Lstr(resource='pausedByHostText'), 266 'client_only': True, 267 'flatness': 1.0, 268 'h_align': 'center', 269 }, 270 ) 271 ) 272 273 def resume(self) -> None: 274 """Resume the game due to a user request or menu closing. 275 276 If there's a foreground host-activity that's currently paused, tell it 277 to resume. 278 """ 279 280 # FIXME: Shouldn't be touching scene stuff here; 281 # should just pass the request on to the host-session. 282 activity = bascenev1.get_foreground_host_activity() 283 if activity is not None: 284 with activity.context: 285 globs = activity.globalsnode 286 if globs.paused: 287 bascenev1.getsound('refWhistle').play() 288 globs.paused = False 289 290 # FIXME: This should not be an actor attr. 291 activity.paused_text = None 292 293 def add_coop_practice_level(self, level: bascenev1.Level) -> None: 294 """Adds an individual level to the 'practice' section in Co-op.""" 295 296 # Assign this level to our catch-all campaign. 297 self.campaigns['Challenges'].addlevel(level) 298 299 # Make note to add it to our challenges UI. 300 self.custom_coop_practice_games.append(f'Challenges:{level.name}') 301 302 def launch_coop_game( 303 self, game: str, force: bool = False, args: dict | None = None 304 ) -> bool: 305 """High level way to launch a local co-op session.""" 306 # pylint: disable=cyclic-import 307 from bauiv1lib.coop.level import CoopLevelLockedWindow 308 309 assert babase.app.classic is not None 310 311 if args is None: 312 args = {} 313 if game == '': 314 raise ValueError('empty game name') 315 campaignname, levelname = game.split(':') 316 campaign = babase.app.classic.getcampaign(campaignname) 317 318 # If this campaign is sequential, make sure we've completed the 319 # one before this. 320 if campaign.sequential and not force: 321 for level in campaign.levels: 322 if level.name == levelname: 323 break 324 if not level.complete: 325 CoopLevelLockedWindow( 326 campaign.getlevel(levelname).displayname, 327 campaign.getlevel(level.name).displayname, 328 ) 329 return False 330 331 # Ok, we're good to go. 332 self.coop_session_args = { 333 'campaign': campaignname, 334 'level': levelname, 335 } 336 for arg_name, arg_val in list(args.items()): 337 self.coop_session_args[arg_name] = arg_val 338 339 def _fade_end() -> None: 340 from bascenev1 import CoopSession 341 342 try: 343 bascenev1.new_host_session(CoopSession) 344 except Exception: 345 logging.exception('Error creating coopsession after fade end.') 346 from bascenev1lib.mainmenu import MainMenuSession 347 348 bascenev1.new_host_session(MainMenuSession) 349 350 babase.fade_screen(False, endcall=_fade_end) 351 return True 352 353 def return_to_main_menu_session_gracefully( 354 self, reset_ui: bool = True 355 ) -> None: 356 """Attempt to cleanly get back to the main menu.""" 357 # pylint: disable=cyclic-import 358 from baclassic import _benchmark 359 from bascenev1lib.mainmenu import MainMenuSession 360 361 plus = babase.app.plus 362 assert plus is not None 363 364 if reset_ui: 365 babase.app.ui_v1.clear_main_window() 366 367 if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession): 368 # It may be possible we're on the main menu but the screen is faded 369 # so fade back in. 370 babase.fade_screen(True) 371 return 372 373 _benchmark.stop_stress_test() # Stop stress-test if in progress. 374 375 # If we're in a host-session, tell them to end. 376 # This lets them tear themselves down gracefully. 377 host_session: bascenev1.Session | None = ( 378 bascenev1.get_foreground_host_session() 379 ) 380 if host_session is not None: 381 # Kick off a little transaction so we'll hopefully have all the 382 # latest account state when we get back to the menu. 383 plus.add_v1_account_transaction( 384 {'type': 'END_SESSION', 'sType': str(type(host_session))} 385 ) 386 plus.run_v1_account_transactions() 387 388 host_session.end() 389 390 # Otherwise just force the issue. 391 else: 392 babase.pushcall( 393 babase.Call(bascenev1.new_host_session, MainMenuSession) 394 ) 395 396 def getmaps(self, playtype: str) -> list[str]: 397 """Return a list of bascenev1.Map types supporting a playtype str. 398 399 Category: **Asset Functions** 400 401 Maps supporting a given playtype must provide a particular set of 402 features and lend themselves to a certain style of play. 403 404 Play Types: 405 406 'melee' 407 General fighting map. 408 Has one or more 'spawn' locations. 409 410 'team_flag' 411 For games such as Capture The Flag where each team spawns by a flag. 412 Has two or more 'spawn' locations, each with a corresponding 'flag' 413 location (based on index). 414 415 'single_flag' 416 For games such as King of the Hill or Keep Away where multiple teams 417 are fighting over a single flag. 418 Has two or more 'spawn' locations and 1 'flag_default' location. 419 420 'conquest' 421 For games such as Conquest where flags are spread throughout the map 422 - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. 423 424 'king_of_the_hill' - has 2+ 'spawn' locations, 425 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations 426 427 'hockey' 428 For hockey games. 429 Has two 'goal' locations, corresponding 'spawn' locations, and one 430 'flag_default' location (for where puck spawns) 431 432 'football' 433 For football games. 434 Has two 'goal' locations, corresponding 'spawn' locations, and one 435 'flag_default' location (for where flag/ball/etc. spawns) 436 437 'race' 438 For racing games where players much touch each region in order. 439 Has two or more 'race_point' locations. 440 """ 441 return sorted( 442 key 443 for key, val in self.maps.items() 444 if playtype in val.get_play_types() 445 ) 446 447 def game_begin_analytics(self) -> None: 448 """(internal)""" 449 from baclassic import _analytics 450 451 _analytics.game_begin_analytics() 452 453 @classmethod 454 def json_prep(cls, data: Any) -> Any: 455 """Return a json-friendly version of the provided data. 456 457 This converts any tuples to lists and any bytes to strings 458 (interpreted as utf-8, ignoring errors). Logs errors (just once) 459 if any data is modified/discarded/unsupported. 460 """ 461 462 if isinstance(data, dict): 463 return dict( 464 (cls.json_prep(key), cls.json_prep(value)) 465 for key, value in list(data.items()) 466 ) 467 if isinstance(data, list): 468 return [cls.json_prep(element) for element in data] 469 if isinstance(data, tuple): 470 logging.exception('json_prep encountered tuple') 471 return [cls.json_prep(element) for element in data] 472 if isinstance(data, bytes): 473 try: 474 return data.decode(errors='ignore') 475 except Exception: 476 logging.exception('json_prep encountered utf-8 decode error') 477 return data.decode(errors='ignore') 478 if not isinstance(data, (str, float, bool, type(None), int)): 479 logging.exception( 480 'got unsupported type in json_prep: %s', type(data) 481 ) 482 return data 483 484 def master_server_v1_get( 485 self, 486 request: str, 487 data: dict[str, Any], 488 callback: MasterServerCallback | None = None, 489 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 490 ) -> None: 491 """Make a call to the master server via a http GET.""" 492 493 MasterServerV1CallThread( 494 request, 'get', data, callback, response_type 495 ).start() 496 497 def master_server_v1_post( 498 self, 499 request: str, 500 data: dict[str, Any], 501 callback: MasterServerCallback | None = None, 502 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 503 ) -> None: 504 """Make a call to the master server via a http POST.""" 505 MasterServerV1CallThread( 506 request, 'post', data, callback, response_type 507 ).start() 508 509 def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]: 510 """Given a tournament entry, return strings for its prize levels.""" 511 from baclassic import _tournament 512 513 return _tournament.get_tournament_prize_strings(entry) 514 515 def getcampaign(self, name: str) -> bascenev1.Campaign: 516 """Return a campaign by name.""" 517 return self.campaigns[name] 518 519 def get_next_tip(self) -> str: 520 """Returns the next tip to be displayed.""" 521 if not self.tips: 522 for tip in get_all_tips(): 523 self.tips.insert(random.randint(0, len(self.tips)), tip) 524 tip = self.tips.pop() 525 return tip 526 527 def run_gpu_benchmark(self) -> None: 528 """Kick off a benchmark to test gpu speeds.""" 529 from baclassic._benchmark import run_gpu_benchmark as run 530 531 run() 532 533 def run_cpu_benchmark(self) -> None: 534 """Kick off a benchmark to test cpu speeds.""" 535 from baclassic._benchmark import run_cpu_benchmark as run 536 537 run() 538 539 def run_media_reload_benchmark(self) -> None: 540 """Kick off a benchmark to test media reloading speeds.""" 541 from baclassic._benchmark import run_media_reload_benchmark as run 542 543 run() 544 545 def run_stress_test( 546 self, 547 playlist_type: str = 'Random', 548 playlist_name: str = '__default__', 549 player_count: int = 8, 550 round_duration: int = 30, 551 attract_mode: bool = False, 552 ) -> None: 553 """Run a stress test.""" 554 from baclassic._benchmark import run_stress_test as run 555 556 run( 557 playlist_type=playlist_type, 558 playlist_name=playlist_name, 559 player_count=player_count, 560 round_duration=round_duration, 561 attract_mode=attract_mode, 562 ) 563 564 def get_input_device_mapped_value( 565 self, 566 device: bascenev1.InputDevice, 567 name: str, 568 default: bool = False, 569 ) -> Any: 570 """Return a mapped value for an input device. 571 572 This checks the user config and falls back to default values 573 where available. 574 """ 575 return _input.get_input_device_mapped_value( 576 device.name, device.unique_identifier, name, default 577 ) 578 579 def get_input_device_map_hash( 580 self, inputdevice: bascenev1.InputDevice 581 ) -> str: 582 """Given an input device, return hash based on its raw input values.""" 583 del inputdevice # unused currently 584 return _input.get_input_device_map_hash() 585 586 def get_input_device_config( 587 self, inputdevice: bascenev1.InputDevice, default: bool 588 ) -> tuple[dict, str]: 589 """Given an input device, return its config dict in the app config. 590 591 The dict will be created if it does not exist. 592 """ 593 return _input.get_input_device_config( 594 inputdevice.name, inputdevice.unique_identifier, default 595 ) 596 597 def get_player_colors(self) -> list[tuple[float, float, float]]: 598 """Return user-selectable player colors.""" 599 return bascenev1.get_player_colors() 600 601 def get_player_profile_icon(self, profilename: str) -> str: 602 """Given a profile name, returns an icon string for it. 603 604 (non-account profiles only) 605 """ 606 return bascenev1.get_player_profile_icon(profilename) 607 608 def get_player_profile_colors( 609 self, 610 profilename: str | None, 611 profiles: dict[str, dict[str, Any]] | None = None, 612 ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: 613 """Given a profile, return colors for them.""" 614 return bascenev1.get_player_profile_colors(profilename, profiles) 615 616 def get_foreground_host_session(self) -> bascenev1.Session | None: 617 """(internal)""" 618 return bascenev1.get_foreground_host_session() 619 620 def get_foreground_host_activity(self) -> bascenev1.Activity | None: 621 """(internal)""" 622 return bascenev1.get_foreground_host_activity() 623 624 def value_test( 625 self, 626 arg: str, 627 change: float | None = None, 628 absolute: float | None = None, 629 ) -> float: 630 """(internal)""" 631 return _baclassic.value_test(arg, change, absolute) 632 633 def set_master_server_source(self, source: int) -> None: 634 """(internal)""" 635 bascenev1.set_master_server_source(source) 636 637 def get_game_port(self) -> int: 638 """(internal)""" 639 return bascenev1.get_game_port() 640 641 def v2_upgrade_window(self, login_name: str, code: str) -> None: 642 """(internal)""" 643 644 from bauiv1lib.v2upgrade import V2UpgradeWindow 645 646 V2UpgradeWindow(login_name, code) 647 648 def account_link_code_window(self, data: dict[str, Any]) -> None: 649 """(internal)""" 650 from bauiv1lib.account.link import AccountLinkCodeWindow 651 652 AccountLinkCodeWindow(data) 653 654 def server_dialog(self, delay: float, data: dict[str, Any]) -> None: 655 """(internal)""" 656 from bauiv1lib.serverdialog import ( 657 ServerDialogData, 658 ServerDialogWindow, 659 ) 660 661 try: 662 sddata = dataclass_from_dict(ServerDialogData, data) 663 except Exception: 664 sddata = None 665 logging.warning( 666 'Got malformatted ServerDialogData: %s', 667 data, 668 ) 669 if sddata is not None: 670 babase.apptimer( 671 delay, 672 babase.Call(ServerDialogWindow, sddata), 673 ) 674 675 # def root_ui_ticket_icon_press(self) -> None: 676 # """(internal)""" 677 # from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow 678 679 # ResourceTypeInfoWindow( 680 # origin_widget=bauiv1.get_special_widget('tickets_meter') 681 # ) 682 683 def show_url_window(self, address: str) -> None: 684 """(internal)""" 685 from bauiv1lib.url import ShowURLWindow 686 687 ShowURLWindow(address) 688 689 def quit_window(self, quit_type: babase.QuitType) -> None: 690 """(internal)""" 691 from bauiv1lib.confirm import QuitWindow 692 693 QuitWindow(quit_type) 694 695 def tournament_entry_window( 696 self, 697 tournament_id: str, 698 tournament_activity: bascenev1.Activity | None = None, 699 position: tuple[float, float] = (0.0, 0.0), 700 delegate: Any = None, 701 scale: float | None = None, 702 offset: tuple[float, float] = (0.0, 0.0), 703 on_close_call: Callable[[], Any] | None = None, 704 ) -> None: 705 """(internal)""" 706 from bauiv1lib.tournamententry import TournamentEntryWindow 707 708 TournamentEntryWindow( 709 tournament_id, 710 tournament_activity, 711 position, 712 delegate, 713 scale, 714 offset, 715 on_close_call, 716 ) 717 718 def get_main_menu_session(self) -> type[bascenev1.Session]: 719 """(internal)""" 720 from bascenev1lib.mainmenu import MainMenuSession 721 722 return MainMenuSession 723 724 def continues_window( 725 self, 726 activity: bascenev1.Activity, 727 cost: int, 728 continue_call: Callable[[], Any], 729 cancel_call: Callable[[], Any], 730 ) -> None: 731 """(internal)""" 732 from bauiv1lib.continues import ContinuesWindow 733 734 ContinuesWindow(activity, cost, continue_call, cancel_call) 735 736 def profile_browser_window( 737 self, 738 transition: str = 'in_right', 739 origin_widget: bauiv1.Widget | None = None, 740 # in_main_menu: bool = True, 741 selected_profile: str | None = None, 742 ) -> None: 743 """(internal)""" 744 from bauiv1lib.profile.browser import ProfileBrowserWindow 745 746 main_window = babase.app.ui_v1.get_main_window() 747 if main_window is not None: 748 logging.warning( 749 'profile_browser_window()' 750 ' called with existing main window; should not happen.' 751 ) 752 return 753 754 babase.app.ui_v1.set_main_window( 755 ProfileBrowserWindow( 756 transition=transition, 757 selected_profile=selected_profile, 758 origin_widget=origin_widget, 759 minimal_toolbar=True, 760 ), 761 is_top_level=True, 762 suppress_warning=True, 763 ) 764 765 def preload_map_preview_media(self) -> None: 766 """Preload media needed for map preview UIs. 767 768 Category: **Asset Functions** 769 """ 770 try: 771 bauiv1.getmesh('level_select_button_opaque') 772 bauiv1.getmesh('level_select_button_transparent') 773 for maptype in list(self.maps.values()): 774 map_tex_name = maptype.get_preview_texture_name() 775 if map_tex_name is not None: 776 bauiv1.gettexture(map_tex_name) 777 except Exception: 778 logging.exception('Error preloading map preview media.') 779 780 def party_icon_activate(self, origin: Sequence[float]) -> None: 781 """(internal)""" 782 from bauiv1lib.party import PartyWindow 783 from babase import app 784 785 assert app.env.gui 786 787 # Play explicit swish sound so it occurs due to keypresses/etc. 788 # This means we have to disable it for any button or else we get 789 # double. 790 bauiv1.getsound('swish').play() 791 792 # If it exists, dismiss it; otherwise make a new one. 793 party_window = ( 794 None if self.party_window is None else self.party_window() 795 ) 796 if party_window is not None: 797 party_window.close() 798 else: 799 self.party_window = weakref.ref(PartyWindow(origin=origin)) 800 801 def device_menu_press(self, device_id: int | None) -> None: 802 """(internal)""" 803 from bauiv1lib.ingamemenu import InGameMenuWindow 804 from bauiv1 import set_ui_input_device 805 806 assert babase.app is not None 807 in_main_menu = babase.app.ui_v1.has_main_window() 808 if not in_main_menu: 809 set_ui_input_device(device_id) 810 811 # Hack(ish). We play swish sound here so it happens for 812 # device presses, but this means we need to disable default 813 # swish sounds for any menu buttons or we'll get double. 814 if babase.app.env.gui: 815 bauiv1.getsound('swish').play() 816 817 babase.app.ui_v1.set_main_window( 818 InGameMenuWindow(), is_top_level=True, suppress_warning=True 819 ) 820 821 def invoke_main_menu_ui(self) -> None: 822 """Bring up main menu ui.""" 823 print('INVOKING MAIN MENU UI') 824 825 # Bring up the last place we were, or start at the main menu otherwise. 826 app = bauiv1.app 827 env = app.env 828 with bascenev1.ContextRef.empty(): 829 # from bauiv1lib import specialoffer 830 831 assert app.classic is not None 832 if app.env.headless: 833 # UI stuff fails now in headless builds; avoid it. 834 pass 835 else: 836 837 # When coming back from a kiosk-mode game, jump to the 838 # kiosk start screen. 839 if env.demo or env.arcade: 840 # pylint: disable=cyclic-import 841 from bauiv1lib.kiosk import KioskWindow 842 843 app.ui_v1.set_main_window( 844 KioskWindow(), is_top_level=True, suppress_warning=True 845 ) 846 # ..or in normal cases go back to the main menu 847 else: 848 # if main_menu_location == 'Gather': 849 # # pylint: disable=cyclic-import 850 # from bauiv1lib.gather import GatherWindow 851 852 # app.ui_v1.set_main_window( 853 # GatherWindow(transition=None), 854 # from_window=False, # Disable check here. 855 # ) 856 # elif main_menu_location == 'Watch': 857 # # pylint: disable=cyclic-import 858 # from bauiv1lib.watch import WatchWindow 859 860 # app.ui_v1.set_main_window( 861 # WatchWindow(transition=None), 862 # from_window=False, # Disable check here. 863 # ) 864 # elif main_menu_location == 'Team Game Select': 865 # # pylint: disable=cyclic-import 866 # from bauiv1lib.playlist.browser import ( 867 # PlaylistBrowserWindow, 868 # ) 869 870 # app.ui_v1.set_main_window( 871 # PlaylistBrowserWindow( 872 # sessiontype=bascenev1.DualTeamSession, 873 # transition=None, 874 # ), 875 # from_window=False, # Disable check here. 876 # ) 877 # elif main_menu_location == 'Free-for-All Game Select': 878 # # pylint: disable=cyclic-import 879 # from bauiv1lib.playlist.browser import ( 880 # PlaylistBrowserWindow, 881 # ) 882 883 # app.ui_v1.set_main_window( 884 # PlaylistBrowserWindow( 885 # sessiontype=bascenev1.FreeForAllSession, 886 # transition=None, 887 # ), 888 # from_window=False, # Disable check here. 889 # ) 890 # elif main_menu_location == 'Coop Select': 891 # # pylint: disable=cyclic-import 892 # from bauiv1lib.coop.browser import CoopBrowserWindow 893 894 # app.ui_v1.set_main_window( 895 # CoopBrowserWindow(transition=None), 896 # from_window=False, # Disable check here. 897 # ) 898 # elif main_menu_location == 'Benchmarks & Stress Tests': 899 # # pylint: disable=cyclic-import 900 # from bauiv1lib.debug import DebugWindow 901 902 # app.ui_v1.set_main_window( 903 # DebugWindow(transition=None), 904 # from_window=False, # Disable check here. 905 # ) 906 # else: 907 # pylint: disable=cyclic-import 908 from bauiv1lib.mainmenu import MainMenuWindow 909 910 app.ui_v1.set_main_window( 911 MainMenuWindow(transition=None), 912 is_top_level=True, 913 suppress_warning=True, 914 )
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.
133 @property 134 def platform(self) -> str: 135 """Name of the current platform. 136 137 Examples are: 'mac', 'windows', android'. 138 """ 139 assert isinstance(self._env['platform'], str) 140 return self._env['platform']
Name of the current platform.
Examples are: 'mac', 'windows', android'.
146 @property 147 def subplatform(self) -> str: 148 """String for subplatform. 149 150 Can be empty. For the 'android' platform, subplatform may 151 be 'google', 'amazon', etc. 152 """ 153 assert isinstance(self._env['subplatform'], str) 154 return self._env['subplatform']
String for subplatform.
Can be empty. For the 'android' platform, subplatform may be 'google', 'amazon', etc.
156 @property 157 def legacy_user_agent_string(self) -> str: 158 """String containing various bits of info about OS/device/etc.""" 159 assert isinstance(self._env['legacy_user_agent_string'], str) 160 return self._env['legacy_user_agent_string']
String containing various bits of info about OS/device/etc.
162 @override 163 def on_app_loading(self) -> None: 164 from bascenev1lib.actor import spazappearance 165 from bascenev1lib import maps as stdmaps 166 167 plus = babase.app.plus 168 assert plus is not None 169 170 env = babase.app.env 171 cfg = babase.app.config 172 173 self.music.on_app_loading() 174 175 # Non-test, non-debug builds should generally be blessed; warn if not. 176 # (so I don't accidentally release a build that can't play tourneys) 177 if not env.debug and not env.test and not plus.is_blessed(): 178 babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0)) 179 180 # FIXME: This should not be hard-coded. 181 for maptype in [ 182 stdmaps.HockeyStadium, 183 stdmaps.FootballStadium, 184 stdmaps.Bridgit, 185 stdmaps.BigG, 186 stdmaps.Roundabout, 187 stdmaps.MonkeyFace, 188 stdmaps.ZigZag, 189 stdmaps.ThePad, 190 stdmaps.DoomShroom, 191 stdmaps.LakeFrigid, 192 stdmaps.TipTop, 193 stdmaps.CragCastle, 194 stdmaps.TowerD, 195 stdmaps.HappyThoughts, 196 stdmaps.StepRightUp, 197 stdmaps.Courtyard, 198 stdmaps.Rampage, 199 ]: 200 bascenev1.register_map(maptype) 201 202 spazappearance.register_appearances() 203 bascenev1.init_campaigns() 204 205 launch_count = cfg.get('launchCount', 0) 206 launch_count += 1 207 208 # So we know how many times we've run the game at various 209 # version milestones. 210 for key in ('lc14173', 'lc14292'): 211 cfg.setdefault(key, launch_count) 212 213 cfg['launchCount'] = launch_count 214 cfg.commit() 215 216 # If there's a leftover log file, attempt to upload it to the 217 # master-server and/or get rid of it. 218 babase.handle_leftover_v1_cloud_log_file() 219 220 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.
226 @override 227 def on_app_unsuspend(self) -> None: 228 self.accounts.on_app_unsuspend() 229 self.music.on_app_unsuspend()
Called when the app exits the suspended state.
235 def pause(self) -> None: 236 """Pause the game due to a user request or menu popping up. 237 238 If there's a foreground host-activity that says it's pausable, tell it 239 to pause. Note: we now no longer pause if there are connected clients. 240 """ 241 activity: bascenev1.Activity | None = ( 242 bascenev1.get_foreground_host_activity() 243 ) 244 if ( 245 activity is not None 246 and activity.allow_pausing 247 and not bascenev1.have_connected_clients() 248 ): 249 from babase import Lstr 250 from bascenev1 import NodeActor 251 252 # FIXME: Shouldn't be touching scene stuff here; 253 # should just pass the request on to the host-session. 254 with activity.context: 255 globs = activity.globalsnode 256 if not globs.paused: 257 bascenev1.getsound('refWhistle').play() 258 globs.paused = True 259 260 # FIXME: This should not be an attr on Actor. 261 activity.paused_text = NodeActor( 262 bascenev1.newnode( 263 'text', 264 attrs={ 265 'text': Lstr(resource='pausedByHostText'), 266 'client_only': True, 267 'flatness': 1.0, 268 'h_align': 'center', 269 }, 270 ) 271 )
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.
273 def resume(self) -> None: 274 """Resume the game due to a user request or menu closing. 275 276 If there's a foreground host-activity that's currently paused, tell it 277 to resume. 278 """ 279 280 # FIXME: Shouldn't be touching scene stuff here; 281 # should just pass the request on to the host-session. 282 activity = bascenev1.get_foreground_host_activity() 283 if activity is not None: 284 with activity.context: 285 globs = activity.globalsnode 286 if globs.paused: 287 bascenev1.getsound('refWhistle').play() 288 globs.paused = False 289 290 # FIXME: This should not be an actor attr. 291 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.
293 def add_coop_practice_level(self, level: bascenev1.Level) -> None: 294 """Adds an individual level to the 'practice' section in Co-op.""" 295 296 # Assign this level to our catch-all campaign. 297 self.campaigns['Challenges'].addlevel(level) 298 299 # Make note to add it to our challenges UI. 300 self.custom_coop_practice_games.append(f'Challenges:{level.name}')
Adds an individual level to the 'practice' section in Co-op.
302 def launch_coop_game( 303 self, game: str, force: bool = False, args: dict | None = None 304 ) -> bool: 305 """High level way to launch a local co-op session.""" 306 # pylint: disable=cyclic-import 307 from bauiv1lib.coop.level import CoopLevelLockedWindow 308 309 assert babase.app.classic is not None 310 311 if args is None: 312 args = {} 313 if game == '': 314 raise ValueError('empty game name') 315 campaignname, levelname = game.split(':') 316 campaign = babase.app.classic.getcampaign(campaignname) 317 318 # If this campaign is sequential, make sure we've completed the 319 # one before this. 320 if campaign.sequential and not force: 321 for level in campaign.levels: 322 if level.name == levelname: 323 break 324 if not level.complete: 325 CoopLevelLockedWindow( 326 campaign.getlevel(levelname).displayname, 327 campaign.getlevel(level.name).displayname, 328 ) 329 return False 330 331 # Ok, we're good to go. 332 self.coop_session_args = { 333 'campaign': campaignname, 334 'level': levelname, 335 } 336 for arg_name, arg_val in list(args.items()): 337 self.coop_session_args[arg_name] = arg_val 338 339 def _fade_end() -> None: 340 from bascenev1 import CoopSession 341 342 try: 343 bascenev1.new_host_session(CoopSession) 344 except Exception: 345 logging.exception('Error creating coopsession after fade end.') 346 from bascenev1lib.mainmenu import MainMenuSession 347 348 bascenev1.new_host_session(MainMenuSession) 349 350 babase.fade_screen(False, endcall=_fade_end) 351 return True
High level way to launch a local co-op session.
396 def getmaps(self, playtype: str) -> list[str]: 397 """Return a list of bascenev1.Map types supporting a playtype str. 398 399 Category: **Asset Functions** 400 401 Maps supporting a given playtype must provide a particular set of 402 features and lend themselves to a certain style of play. 403 404 Play Types: 405 406 'melee' 407 General fighting map. 408 Has one or more 'spawn' locations. 409 410 'team_flag' 411 For games such as Capture The Flag where each team spawns by a flag. 412 Has two or more 'spawn' locations, each with a corresponding 'flag' 413 location (based on index). 414 415 'single_flag' 416 For games such as King of the Hill or Keep Away where multiple teams 417 are fighting over a single flag. 418 Has two or more 'spawn' locations and 1 'flag_default' location. 419 420 'conquest' 421 For games such as Conquest where flags are spread throughout the map 422 - has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations. 423 424 'king_of_the_hill' - has 2+ 'spawn' locations, 425 1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations 426 427 'hockey' 428 For hockey games. 429 Has two 'goal' locations, corresponding 'spawn' locations, and one 430 'flag_default' location (for where puck spawns) 431 432 'football' 433 For football games. 434 Has two 'goal' locations, corresponding 'spawn' locations, and one 435 'flag_default' location (for where flag/ball/etc. spawns) 436 437 'race' 438 For racing games where players much touch each region in order. 439 Has two or more 'race_point' locations. 440 """ 441 return sorted( 442 key 443 for key, val in self.maps.items() 444 if playtype in val.get_play_types() 445 )
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.
453 @classmethod 454 def json_prep(cls, data: Any) -> Any: 455 """Return a json-friendly version of the provided data. 456 457 This converts any tuples to lists and any bytes to strings 458 (interpreted as utf-8, ignoring errors). Logs errors (just once) 459 if any data is modified/discarded/unsupported. 460 """ 461 462 if isinstance(data, dict): 463 return dict( 464 (cls.json_prep(key), cls.json_prep(value)) 465 for key, value in list(data.items()) 466 ) 467 if isinstance(data, list): 468 return [cls.json_prep(element) for element in data] 469 if isinstance(data, tuple): 470 logging.exception('json_prep encountered tuple') 471 return [cls.json_prep(element) for element in data] 472 if isinstance(data, bytes): 473 try: 474 return data.decode(errors='ignore') 475 except Exception: 476 logging.exception('json_prep encountered utf-8 decode error') 477 return data.decode(errors='ignore') 478 if not isinstance(data, (str, float, bool, type(None), int)): 479 logging.exception( 480 'got unsupported type in json_prep: %s', type(data) 481 ) 482 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.
484 def master_server_v1_get( 485 self, 486 request: str, 487 data: dict[str, Any], 488 callback: MasterServerCallback | None = None, 489 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 490 ) -> None: 491 """Make a call to the master server via a http GET.""" 492 493 MasterServerV1CallThread( 494 request, 'get', data, callback, response_type 495 ).start()
Make a call to the master server via a http GET.
497 def master_server_v1_post( 498 self, 499 request: str, 500 data: dict[str, Any], 501 callback: MasterServerCallback | None = None, 502 response_type: MasterServerResponseType = MasterServerResponseType.JSON, 503 ) -> None: 504 """Make a call to the master server via a http POST.""" 505 MasterServerV1CallThread( 506 request, 'post', data, callback, response_type 507 ).start()
Make a call to the master server via a http POST.
509 def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]: 510 """Given a tournament entry, return strings for its prize levels.""" 511 from baclassic import _tournament 512 513 return _tournament.get_tournament_prize_strings(entry)
Given a tournament entry, return strings for its prize levels.
515 def getcampaign(self, name: str) -> bascenev1.Campaign: 516 """Return a campaign by name.""" 517 return self.campaigns[name]
Return a campaign by name.
519 def get_next_tip(self) -> str: 520 """Returns the next tip to be displayed.""" 521 if not self.tips: 522 for tip in get_all_tips(): 523 self.tips.insert(random.randint(0, len(self.tips)), tip) 524 tip = self.tips.pop() 525 return tip
Returns the next tip to be displayed.
527 def run_gpu_benchmark(self) -> None: 528 """Kick off a benchmark to test gpu speeds.""" 529 from baclassic._benchmark import run_gpu_benchmark as run 530 531 run()
Kick off a benchmark to test gpu speeds.
533 def run_cpu_benchmark(self) -> None: 534 """Kick off a benchmark to test cpu speeds.""" 535 from baclassic._benchmark import run_cpu_benchmark as run 536 537 run()
Kick off a benchmark to test cpu speeds.
539 def run_media_reload_benchmark(self) -> None: 540 """Kick off a benchmark to test media reloading speeds.""" 541 from baclassic._benchmark import run_media_reload_benchmark as run 542 543 run()
Kick off a benchmark to test media reloading speeds.
545 def run_stress_test( 546 self, 547 playlist_type: str = 'Random', 548 playlist_name: str = '__default__', 549 player_count: int = 8, 550 round_duration: int = 30, 551 attract_mode: bool = False, 552 ) -> None: 553 """Run a stress test.""" 554 from baclassic._benchmark import run_stress_test as run 555 556 run( 557 playlist_type=playlist_type, 558 playlist_name=playlist_name, 559 player_count=player_count, 560 round_duration=round_duration, 561 attract_mode=attract_mode, 562 )
Run a stress test.
564 def get_input_device_mapped_value( 565 self, 566 device: bascenev1.InputDevice, 567 name: str, 568 default: bool = False, 569 ) -> Any: 570 """Return a mapped value for an input device. 571 572 This checks the user config and falls back to default values 573 where available. 574 """ 575 return _input.get_input_device_mapped_value( 576 device.name, device.unique_identifier, name, default 577 )
Return a mapped value for an input device.
This checks the user config and falls back to default values where available.
579 def get_input_device_map_hash( 580 self, inputdevice: bascenev1.InputDevice 581 ) -> str: 582 """Given an input device, return hash based on its raw input values.""" 583 del inputdevice # unused currently 584 return _input.get_input_device_map_hash()
Given an input device, return hash based on its raw input values.
586 def get_input_device_config( 587 self, inputdevice: bascenev1.InputDevice, default: bool 588 ) -> tuple[dict, str]: 589 """Given an input device, return its config dict in the app config. 590 591 The dict will be created if it does not exist. 592 """ 593 return _input.get_input_device_config( 594 inputdevice.name, inputdevice.unique_identifier, default 595 )
Given an input device, return its config dict in the app config.
The dict will be created if it does not exist.
597 def get_player_colors(self) -> list[tuple[float, float, float]]: 598 """Return user-selectable player colors.""" 599 return bascenev1.get_player_colors()
Return user-selectable player colors.
601 def get_player_profile_icon(self, profilename: str) -> str: 602 """Given a profile name, returns an icon string for it. 603 604 (non-account profiles only) 605 """ 606 return bascenev1.get_player_profile_icon(profilename)
Given a profile name, returns an icon string for it.
(non-account profiles only)
608 def get_player_profile_colors( 609 self, 610 profilename: str | None, 611 profiles: dict[str, dict[str, Any]] | None = None, 612 ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: 613 """Given a profile, return colors for them.""" 614 return bascenev1.get_player_profile_colors(profilename, profiles)
Given a profile, return colors for them.
765 def preload_map_preview_media(self) -> None: 766 """Preload media needed for map preview UIs. 767 768 Category: **Asset Functions** 769 """ 770 try: 771 bauiv1.getmesh('level_select_button_opaque') 772 bauiv1.getmesh('level_select_button_transparent') 773 for maptype in list(self.maps.values()): 774 map_tex_name = maptype.get_preview_texture_name() 775 if map_tex_name is not None: 776 bauiv1.gettexture(map_tex_name) 777 except Exception: 778 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
- on_screen_change
- 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.