diff --git a/.efrocachemap b/.efrocachemap index 1f3eee67..686b8d08 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/68/70/ecdec08c0236f5bebbf298c9f4cd", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/85/7f/53fb1e3f1414b4865315d815b17a", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9e/ae/586229f233660b6b49ca655b7c1b", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e5/a5/c6b90e3629a8041a68827aafec3f", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d3/66/82b2cab9b1438c30cfd33eefba5e", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/52/37/253765404b79740cd4a7ac5fe325", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c7/69/ecdbcee9df40225475391beb4cb2", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/34/51/cb528ea6eb5ac853794922a0cc34", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/d4/b8/d3a690970ca379d805432bde8533", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4d/16/b9014f5d983481731b54f7045dd9", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/1e/11/d3c478923937f376eecb25561ae9", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/d6/b4/b7854676ec826e42aa3ecb394b49", - "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/76/bf185c1ea65f3c25cfa63666511b", - "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/49/30/5acd0d56b736a3729e7689cd4e18", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f7/e8/197874ac6d756c341628ea4bb518", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/6f/140a3d77288e2355605c9d0c942a", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/ca/62d29425e0ad0b17c145ecbc97fe", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c0/fa/52a38fc153714bff3bcda077ed75", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/75/5b/e84edb24f5df313bb6ebafa91b19", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/2e/af28c0026c0379e5441e789e9721" + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4c/74/73bd143107ea1bc1cc150cf1d9a0", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/1f/b71688ce17abe2f1750d3b98c1a3", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7b/f5/0fb038ef5e8c9cb22f8487213622", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a7/22/7582b2dc2eb28e8c9b05c7860c83", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/01/6a/6787038c5a2fe07be6dd08d6df5f", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/c3/12a2c00732a90af63ae853b76a3b", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/63/e7/795fd31307cda51524ea0cfc4055", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/92/48/1bfcef97e05d641771a934e312f3", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/cf/25/4ef41f30fb1380505759a16a7302", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/78/eb/9ee7487ac9d633020c02bcdd475b", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/4e/59/bd1e3f68c9ccf0180187dbf60ce1", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3c/f2/e0eb7217dba038dd146421b0bf5e", + "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/66/0f/34874cece602328e1a2b27efbe09", + "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ee/58/ccc75a3b2679f01a9e9b9f693ab0", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2a/82/a34db5a370d695fb52891b0a7c54", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1b/c4/0b7f73a301f24177650efb8cfa69", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e6/55/d4819d9a2e5dcd48c2b2641c57df", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/21/3f/8c9d1590314cd5172e944f64e41c", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/91/72/b037555786dfb8a5ebc36467e60f", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e3/e1/c9e61438f6458b9ce4ac4cfe66ed" } \ No newline at end of file diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index d141478e..642f4643 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -52,7 +52,7 @@ from ba._stats import PlayerScoredMessage, PlayerRecord, Stats from ba._team import SessionTeam, Team, EmptyTeam from ba._teamgame import TeamGameActivity from ba._dualteamsession import DualTeamSession -from ba._achievement import Achievement +from ba._achievement import Achievement, AchievementSubsystem from ba._appconfig import AppConfig from ba._appdelegate import AppDelegate from ba._apputils import is_browser_likely_available diff --git a/assets/src/ba_data/python/ba/_account.py b/assets/src/ba_data/python/ba/_account.py index 5fee72c0..312770c2 100644 --- a/assets/src/ba_data/python/ba/_account.py +++ b/assets/src/ba_data/python/ba/_account.py @@ -47,7 +47,7 @@ def get_league_rank_points(data: Optional[Dict[str, Any]], total_ach_value = data['at'] else: total_ach_value = 0 - for ach in _ba.app.achievements: + for ach in _ba.app.ach.achievements: if ach.complete: total_ach_value += ach.power_ranking_value diff --git a/assets/src/ba_data/python/ba/_achievement.py b/assets/src/ba_data/python/ba/_achievement.py index 039fbe81..d8019e97 100644 --- a/assets/src/ba_data/python/ba/_achievement.py +++ b/assets/src/ba_data/python/ba/_achievement.py @@ -6,9 +6,10 @@ from __future__ import annotations from typing import TYPE_CHECKING import _ba +from ba._error import print_exception if TYPE_CHECKING: - from typing import Any, Sequence, List, Dict, Union, Optional + from typing import Any, Sequence, List, Dict, Union, Optional, Tuple, Set import ba # This could use some cleanup. @@ -61,69 +62,346 @@ ACH_LEVEL_NAMES = { } -def award_local_achievement(achname: str) -> None: - """For non-game-based achievements such as controller-connection ones.""" - try: - ach = get_achievement(achname) - if not ach.complete: +class AchievementSubsystem: + """Subsystem for achievement handling. - # Report new achievements to the game-service. - _ba.report_achievement(achname) + Category: App Classes - # And to our account. - _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname}) - - # Now attempt to show a banner. - display_achievement_banner(achname) - - except Exception: - from ba import _error - _error.print_exception() - - -def display_achievement_banner(achname: str) -> None: - """Display a completion banner for an achievement. - - Used for server-driven achievements. + Access the single shared instance of this class at 'ba.app.ach'. """ - try: - # FIXME: Need to get these using the UI context or some other - # purely local context somehow instead of trying to inject these - # into whatever activity happens to be active - # (since that won't work while in client mode). - activity = _ba.get_foreground_host_activity() - if activity is not None: - with _ba.Context(activity): - get_achievement(achname).announce_completion() - except Exception: - from ba import _error - _error.print_exception('error showing server ach') + def __init__(self) -> None: + self.achievements: List[Achievement] = [] + self.achievements_to_display: (List[Tuple[ba.Achievement, bool]]) = [] + self.achievement_display_timer: Optional[_ba.Timer] = None + self.last_achievement_display_time: float = 0.0 + self.achievement_completion_banner_slots: Set[int] = set() + self._init_achievements() -# This gets called whenever game-center/game-circle/etc tells us which -# achievements we currently have. We always defer to them, even if that -# means we have to un-set an achievement we think we have + def _init_achievements(self) -> None: + """Fill in available achievements.""" + achs = self.achievements -def set_completed_achievements(achs: Sequence[str]) -> None: - """Set the current state of completed achievements. + # 5 + achs.append( + Achievement('In Control', 'achievementInControl', (1, 1, 1), '', + 5)) + # 15 + achs.append( + Achievement('Sharing is Caring', 'achievementSharingIsCaring', + (1, 1, 1), '', 15)) + # 10 + achs.append( + Achievement('Dual Wielding', 'achievementDualWielding', (1, 1, 1), + '', 10)) - All achievements not included here will be set incomplete. - """ - cfg = _ba.app.config - cfg['Achievements'] = {} - for a_name in achs: - get_achievement(a_name).set_complete(True) - cfg.commit() + # 10 + achs.append( + Achievement('Free Loader', 'achievementFreeLoader', (1, 1, 1), '', + 10)) + # 20 + achs.append( + Achievement('Team Player', 'achievementTeamPlayer', (1, 1, 1), '', + 20)) + # 5 + achs.append( + Achievement('Onslaught Training Victory', 'achievementOnslaught', + (1, 1, 1), 'Default:Onslaught Training', 5)) + # 5 + achs.append( + Achievement('Off You Go Then', 'achievementOffYouGo', + (1, 1.1, 1.3), 'Default:Onslaught Training', 5)) + # 10 + achs.append( + Achievement('Boxer', + 'achievementBoxer', (1, 0.6, 0.6), + 'Default:Onslaught Training', + 10, + hard_mode_only=True)) -def get_achievement(name: str) -> Achievement: - """Return an Achievement by name.""" - achs = [a for a in _ba.app.achievements if a.name == name] - assert len(achs) < 2 - if not achs: - raise ValueError("Invalid achievement name: '" + name + "'") - return achs[0] + # 10 + achs.append( + Achievement('Rookie Onslaught Victory', 'achievementOnslaught', + (0.5, 1.4, 0.6), 'Default:Rookie Onslaught', 10)) + # 10 + achs.append( + Achievement('Mine Games', 'achievementMine', (1, 1, 1.4), + 'Default:Rookie Onslaught', 10)) + # 15 + achs.append( + Achievement('Flawless Victory', + 'achievementFlawlessVictory', (1, 1, 1), + 'Default:Rookie Onslaught', + 15, + hard_mode_only=True)) + + # 10 + achs.append( + Achievement('Rookie Football Victory', + 'achievementFootballVictory', (1.0, 1, 0.6), + 'Default:Rookie Football', 10)) + # 10 + achs.append( + Achievement('Super Punch', 'achievementSuperPunch', (1, 1, 1.8), + 'Default:Rookie Football', 10)) + # 15 + achs.append( + Achievement('Rookie Football Shutout', + 'achievementFootballShutout', (1, 1, 1), + 'Default:Rookie Football', + 15, + hard_mode_only=True)) + + # 15 + achs.append( + Achievement('Pro Onslaught Victory', 'achievementOnslaught', + (0.3, 1, 2.0), 'Default:Pro Onslaught', 15)) + # 15 + achs.append( + Achievement('Boom Goes the Dynamite', 'achievementTNT', + (1.4, 1.2, 0.8), 'Default:Pro Onslaught', 15)) + # 20 + achs.append( + Achievement('Pro Boxer', + 'achievementBoxer', (2, 2, 0), + 'Default:Pro Onslaught', + 20, + hard_mode_only=True)) + + # 15 + achs.append( + Achievement('Pro Football Victory', 'achievementFootballVictory', + (1.3, 1.3, 2.0), 'Default:Pro Football', 15)) + # 15 + achs.append( + Achievement('Super Mega Punch', 'achievementSuperPunch', + (2, 1, 0.6), 'Default:Pro Football', 15)) + # 20 + achs.append( + Achievement('Pro Football Shutout', + 'achievementFootballShutout', (0.7, 0.7, 2.0), + 'Default:Pro Football', + 20, + hard_mode_only=True)) + + # 15 + achs.append( + Achievement('Pro Runaround Victory', 'achievementRunaround', + (1, 1, 1), 'Default:Pro Runaround', 15)) + # 20 + achs.append( + Achievement('Precision Bombing', + 'achievementCrossHair', (1, 1, 1.3), + 'Default:Pro Runaround', + 20, + hard_mode_only=True)) + # 25 + achs.append( + Achievement('The Wall', + 'achievementWall', (1, 0.7, 0.7), + 'Default:Pro Runaround', + 25, + hard_mode_only=True)) + + # 30 + achs.append( + Achievement('Uber Onslaught Victory', 'achievementOnslaught', + (2, 2, 1), 'Default:Uber Onslaught', 30)) + # 30 + achs.append( + Achievement('Gold Miner', + 'achievementMine', (2, 1.6, 0.2), + 'Default:Uber Onslaught', + 30, + hard_mode_only=True)) + # 30 + achs.append( + Achievement('TNT Terror', + 'achievementTNT', (2, 1.8, 0.3), + 'Default:Uber Onslaught', + 30, + hard_mode_only=True)) + + # 30 + achs.append( + Achievement('Uber Football Victory', 'achievementFootballVictory', + (1.8, 1.4, 0.3), 'Default:Uber Football', 30)) + # 30 + achs.append( + Achievement('Got the Moves', + 'achievementGotTheMoves', (2, 1, 0), + 'Default:Uber Football', + 30, + hard_mode_only=True)) + # 40 + achs.append( + Achievement('Uber Football Shutout', + 'achievementFootballShutout', (2, 2, 0), + 'Default:Uber Football', + 40, + hard_mode_only=True)) + + # 30 + achs.append( + Achievement('Uber Runaround Victory', 'achievementRunaround', + (1.5, 1.2, 0.2), 'Default:Uber Runaround', 30)) + # 40 + achs.append( + Achievement('The Great Wall', + 'achievementWall', (2, 1.7, 0.4), + 'Default:Uber Runaround', + 40, + hard_mode_only=True)) + # 40 + achs.append( + Achievement('Stayin\' Alive', + 'achievementStayinAlive', (2, 2, 1), + 'Default:Uber Runaround', + 40, + hard_mode_only=True)) + + # 20 + achs.append( + Achievement('Last Stand Master', + 'achievementMedalSmall', (2, 1.5, 0.3), + 'Default:The Last Stand', + 20, + hard_mode_only=True)) + # 40 + achs.append( + Achievement('Last Stand Wizard', + 'achievementMedalMedium', (2, 1.5, 0.3), + 'Default:The Last Stand', + 40, + hard_mode_only=True)) + # 60 + achs.append( + Achievement('Last Stand God', + 'achievementMedalLarge', (2, 1.5, 0.3), + 'Default:The Last Stand', + 60, + hard_mode_only=True)) + + # 5 + achs.append( + Achievement('Onslaught Master', 'achievementMedalSmall', + (0.7, 1, 0.7), 'Challenges:Infinite Onslaught', 5)) + # 15 + achs.append( + Achievement('Onslaught Wizard', 'achievementMedalMedium', + (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 15)) + # 30 + achs.append( + Achievement('Onslaught God', 'achievementMedalLarge', + (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 30)) + + # 5 + achs.append( + Achievement('Runaround Master', 'achievementMedalSmall', + (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 5)) + # 15 + achs.append( + Achievement('Runaround Wizard', 'achievementMedalMedium', + (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 15)) + # 30 + achs.append( + Achievement('Runaround God', 'achievementMedalLarge', + (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 30)) + + def award_local_achievement(self, achname: str) -> None: + """For non-game-based achievements such as controller-connection.""" + try: + ach = self.get_achievement(achname) + if not ach.complete: + + # Report new achievements to the game-service. + _ba.report_achievement(achname) + + # And to our account. + _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname}) + + # Now attempt to show a banner. + self.display_achievement_banner(achname) + + except Exception: + print_exception() + + def display_achievement_banner(self, achname: str) -> None: + """Display a completion banner for an achievement. + + (internal) + + Used for server-driven achievements. + """ + try: + # FIXME: Need to get these using the UI context or some other + # purely local context somehow instead of trying to inject these + # into whatever activity happens to be active + # (since that won't work while in client mode). + activity = _ba.get_foreground_host_activity() + if activity is not None: + with _ba.Context(activity): + self.get_achievement(achname).announce_completion() + except Exception: + print_exception('error showing server ach') + + def set_completed_achievements(self, achs: Sequence[str]) -> None: + """Set the current state of completed achievements. + + (internal) + + All achievements not included here will be set incomplete. + """ + + # Note: This gets called whenever game-center/game-circle/etc tells + # us which achievements we currently have. We always defer to them, + # even if that means we have to un-set an achievement we think we have. + + cfg = _ba.app.config + cfg['Achievements'] = {} + for a_name in achs: + self.get_achievement(a_name).set_complete(True) + cfg.commit() + + def get_achievement(self, name: str) -> Achievement: + """Return an Achievement by name.""" + achs = [a for a in self.achievements if a.name == name] + assert len(achs) < 2 + if not achs: + raise ValueError("Invalid achievement name: '" + name + "'") + return achs[0] + + def achievements_for_coop_level(self, + level_name: str) -> List[Achievement]: + """Given a level name, return achievements available for it.""" + + # For the Easy campaign we return achievements for the Default + # campaign too. (want the user to see what achievements are part of the + # level even if they can't unlock them all on easy mode). + return [ + a for a in self.achievements + if a.level_name in (level_name, + level_name.replace('Easy', 'Default')) + ] + + def _test(self) -> None: + """For testing achievement animations.""" + from ba._enums import TimeType + + def testcall1() -> None: + self.achievements[0].announce_completion() + self.achievements[1].announce_completion() + self.achievements[2].announce_completion() + + def testcall2() -> None: + self.achievements[3].announce_completion() + self.achievements[4].announce_completion() + self.achievements[5].announce_completion() + + _ba.timer(3.0, testcall1, timetype=TimeType.BASE) + _ba.timer(7.0, testcall2, timetype=TimeType.BASE) def _get_ach_mult(include_pro_bonus: bool = False) -> int: @@ -139,34 +417,21 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int: return val -def get_achievements_for_coop_level(level_name: str) -> List[Achievement]: - """Given a level name, return achievements available for it.""" - - # For the Easy campaign we return achievements for the Default - # campaign too. (want the user to see what achievements are part of the - # level even if they can't unlock them all on easy mode). - return [ - a for a in _ba.app.achievements - if a.level_name in (level_name, level_name.replace('Easy', 'Default')) - ] - - def _display_next_achievement() -> None: # Pull the first achievement off the list and display it, or kill the # display-timer if the list is empty. app = _ba.app - if app.achievements_to_display: + if app.ach.achievements_to_display: try: - ach, sound = app.achievements_to_display.pop(0) + ach, sound = app.ach.achievements_to_display.pop(0) ach.show_completion_banner(sound) except Exception: - from ba import _error - _error.print_exception('error showing next achievement') - app.achievements_to_display = [] - app.achievement_display_timer = None + print_exception('error showing next achievement') + app.ach.achievements_to_display = [] + app.ach.achievement_display_timer = None else: - app.achievement_display_timer = None + app.ach.achievement_display_timer = None class Achievement: @@ -236,18 +501,18 @@ class Achievement: return # If we're being freshly complete, display/report it and whatnot. - if (self, sound) not in app.achievements_to_display: - app.achievements_to_display.append((self, sound)) + if (self, sound) not in app.ach.achievements_to_display: + app.ach.achievements_to_display.append((self, sound)) # If there's no achievement display timer going, kick one off # (if one's already running it will pick this up before it dies). # Need to check last time too; its possible our timer wasn't able to # clear itself if an activity died and took it down with it. - if ((app.achievement_display_timer is None or - _ba.time(TimeType.REAL) - app.last_achievement_display_time > 2.0) - and _ba.getactivity(doraise=False) is not None): - app.achievement_display_timer = _ba.Timer( + if ((app.ach.achievement_display_timer is None + or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time + > 2.0) and _ba.getactivity(doraise=False) is not None): + app.ach.achievement_display_timer = _ba.Timer( 1.0, _display_next_achievement, repeat=True, @@ -280,9 +545,8 @@ class Achievement: else: name = '' except Exception: - from ba import _error name = '' - _error.print_exception() + print_exception() return Lstr(resource='achievements.' + self._name + '.name', subs=[('${LEVEL}', name)]) @@ -397,8 +661,7 @@ class Achievement: else: hmo = False except Exception: - from ba import _error - _error.print_exception('Error determining campaign') + print_exception('Error determining campaign.') hmo = False objs: List[ba.Actor] @@ -648,7 +911,7 @@ class Achievement: def _remove_banner_slot(self) -> None: assert self._completion_banner_slot is not None - _ba.app.achievement_completion_banner_slots.remove( + _ba.app.ach.achievement_completion_banner_slots.remove( self._completion_banner_slot) self._completion_banner_slot = None @@ -663,7 +926,7 @@ class Achievement: from ba._messages import DieMessage from ba._enums import TimeType, SpecialChar app = _ba.app - app.last_achievement_display_time = _ba.time(TimeType.REAL) + app.ach.last_achievement_display_time = _ba.time(TimeType.REAL) # Just piggy-back onto any current activity # (should we use the session instead?..) @@ -694,8 +957,8 @@ class Achievement: # Find the first free slot. i = 0 while True: - if i not in app.achievement_completion_banner_slots: - app.achievement_completion_banner_slots.add(i) + if i not in app.ach.achievement_completion_banner_slots: + app.ach.achievement_completion_banner_slots.add(i) self._completion_banner_slot = i # Remove us from that slot when we close. @@ -953,252 +1216,3 @@ class Achievement: for actor in objs: _ba.timer(out_time + 1.000, WeakCall(actor.handlemessage, DieMessage())) - - -def init_achievements() -> None: - """Fill in available achievements.""" - - achs = _ba.app.achievements - - # 5 - achs.append( - Achievement('In Control', 'achievementInControl', (1, 1, 1), '', 5)) - # 15 - achs.append( - Achievement('Sharing is Caring', 'achievementSharingIsCaring', - (1, 1, 1), '', 15)) - # 10 - achs.append( - Achievement('Dual Wielding', 'achievementDualWielding', (1, 1, 1), '', - 10)) - - # 10 - achs.append( - Achievement('Free Loader', 'achievementFreeLoader', (1, 1, 1), '', 10)) - # 20 - achs.append( - Achievement('Team Player', 'achievementTeamPlayer', (1, 1, 1), '', 20)) - - # 5 - achs.append( - Achievement('Onslaught Training Victory', 'achievementOnslaught', - (1, 1, 1), 'Default:Onslaught Training', 5)) - # 5 - achs.append( - Achievement('Off You Go Then', 'achievementOffYouGo', (1, 1.1, 1.3), - 'Default:Onslaught Training', 5)) - # 10 - achs.append( - Achievement('Boxer', - 'achievementBoxer', (1, 0.6, 0.6), - 'Default:Onslaught Training', - 10, - hard_mode_only=True)) - - # 10 - achs.append( - Achievement('Rookie Onslaught Victory', 'achievementOnslaught', - (0.5, 1.4, 0.6), 'Default:Rookie Onslaught', 10)) - # 10 - achs.append( - Achievement('Mine Games', 'achievementMine', (1, 1, 1.4), - 'Default:Rookie Onslaught', 10)) - # 15 - achs.append( - Achievement('Flawless Victory', - 'achievementFlawlessVictory', (1, 1, 1), - 'Default:Rookie Onslaught', - 15, - hard_mode_only=True)) - - # 10 - achs.append( - Achievement('Rookie Football Victory', 'achievementFootballVictory', - (1.0, 1, 0.6), 'Default:Rookie Football', 10)) - # 10 - achs.append( - Achievement('Super Punch', 'achievementSuperPunch', (1, 1, 1.8), - 'Default:Rookie Football', 10)) - # 15 - achs.append( - Achievement('Rookie Football Shutout', - 'achievementFootballShutout', (1, 1, 1), - 'Default:Rookie Football', - 15, - hard_mode_only=True)) - - # 15 - achs.append( - Achievement('Pro Onslaught Victory', 'achievementOnslaught', - (0.3, 1, 2.0), 'Default:Pro Onslaught', 15)) - # 15 - achs.append( - Achievement('Boom Goes the Dynamite', 'achievementTNT', - (1.4, 1.2, 0.8), 'Default:Pro Onslaught', 15)) - # 20 - achs.append( - Achievement('Pro Boxer', - 'achievementBoxer', (2, 2, 0), - 'Default:Pro Onslaught', - 20, - hard_mode_only=True)) - - # 15 - achs.append( - Achievement('Pro Football Victory', 'achievementFootballVictory', - (1.3, 1.3, 2.0), 'Default:Pro Football', 15)) - # 15 - achs.append( - Achievement('Super Mega Punch', 'achievementSuperPunch', (2, 1, 0.6), - 'Default:Pro Football', 15)) - # 20 - achs.append( - Achievement('Pro Football Shutout', - 'achievementFootballShutout', (0.7, 0.7, 2.0), - 'Default:Pro Football', - 20, - hard_mode_only=True)) - - # 15 - achs.append( - Achievement('Pro Runaround Victory', 'achievementRunaround', (1, 1, 1), - 'Default:Pro Runaround', 15)) - # 20 - achs.append( - Achievement('Precision Bombing', - 'achievementCrossHair', (1, 1, 1.3), - 'Default:Pro Runaround', - 20, - hard_mode_only=True)) - # 25 - achs.append( - Achievement('The Wall', - 'achievementWall', (1, 0.7, 0.7), - 'Default:Pro Runaround', - 25, - hard_mode_only=True)) - - # 30 - achs.append( - Achievement('Uber Onslaught Victory', 'achievementOnslaught', - (2, 2, 1), 'Default:Uber Onslaught', 30)) - # 30 - achs.append( - Achievement('Gold Miner', - 'achievementMine', (2, 1.6, 0.2), - 'Default:Uber Onslaught', - 30, - hard_mode_only=True)) - # 30 - achs.append( - Achievement('TNT Terror', - 'achievementTNT', (2, 1.8, 0.3), - 'Default:Uber Onslaught', - 30, - hard_mode_only=True)) - - # 30 - achs.append( - Achievement('Uber Football Victory', 'achievementFootballVictory', - (1.8, 1.4, 0.3), 'Default:Uber Football', 30)) - # 30 - achs.append( - Achievement('Got the Moves', - 'achievementGotTheMoves', (2, 1, 0), - 'Default:Uber Football', - 30, - hard_mode_only=True)) - # 40 - achs.append( - Achievement('Uber Football Shutout', - 'achievementFootballShutout', (2, 2, 0), - 'Default:Uber Football', - 40, - hard_mode_only=True)) - - # 30 - achs.append( - Achievement('Uber Runaround Victory', 'achievementRunaround', - (1.5, 1.2, 0.2), 'Default:Uber Runaround', 30)) - # 40 - achs.append( - Achievement('The Great Wall', - 'achievementWall', (2, 1.7, 0.4), - 'Default:Uber Runaround', - 40, - hard_mode_only=True)) - # 40 - achs.append( - Achievement('Stayin\' Alive', - 'achievementStayinAlive', (2, 2, 1), - 'Default:Uber Runaround', - 40, - hard_mode_only=True)) - - # 20 - achs.append( - Achievement('Last Stand Master', - 'achievementMedalSmall', (2, 1.5, 0.3), - 'Default:The Last Stand', - 20, - hard_mode_only=True)) - # 40 - achs.append( - Achievement('Last Stand Wizard', - 'achievementMedalMedium', (2, 1.5, 0.3), - 'Default:The Last Stand', - 40, - hard_mode_only=True)) - # 60 - achs.append( - Achievement('Last Stand God', - 'achievementMedalLarge', (2, 1.5, 0.3), - 'Default:The Last Stand', - 60, - hard_mode_only=True)) - - # 5 - achs.append( - Achievement('Onslaught Master', 'achievementMedalSmall', (0.7, 1, 0.7), - 'Challenges:Infinite Onslaught', 5)) - # 15 - achs.append( - Achievement('Onslaught Wizard', 'achievementMedalMedium', - (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 15)) - # 30 - achs.append( - Achievement('Onslaught God', 'achievementMedalLarge', (0.7, 1.0, 0.7), - 'Challenges:Infinite Onslaught', 30)) - - # 5 - achs.append( - Achievement('Runaround Master', 'achievementMedalSmall', - (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 5)) - # 15 - achs.append( - Achievement('Runaround Wizard', 'achievementMedalMedium', - (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 15)) - # 30 - achs.append( - Achievement('Runaround God', 'achievementMedalLarge', (1.0, 1.0, 1.2), - 'Challenges:Infinite Runaround', 30)) - - -def _test() -> None: - """For testing achievement animations.""" - from ba._enums import TimeType - - def testcall1() -> None: - app = _ba.app - app.achievements[0].announce_completion() - app.achievements[1].announce_completion() - app.achievements[2].announce_completion() - - def testcall2() -> None: - app = _ba.app - app.achievements[3].announce_completion() - app.achievements[4].announce_completion() - app.achievements[5].announce_completion() - - _ba.timer(3.0, testcall1, timetype=TimeType.BASE) - _ba.timer(7.0, testcall2, timetype=TimeType.BASE) diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py index 7c0de6d6..e5b84b90 100644 --- a/assets/src/ba_data/python/ba/_app.py +++ b/assets/src/ba_data/python/ba/_app.py @@ -99,7 +99,6 @@ class App: """Path where the app looks for its bundled scripts.""" assert isinstance(self._env['python_directory_app'], str) return self._env['python_directory_app'] - # return self._python_directory_app @property def python_directory_app_site(self) -> str: @@ -175,6 +174,7 @@ class App: from ba._music import MusicSubsystem from ba._language import LanguageSubsystem from ba._ui import UISubsystem + from ba._achievement import AchievementSubsystem # Config. self.config_file_healthy = False @@ -241,18 +241,10 @@ class App: self.last_ad_purpose = 'invalid' self.attempted_first_ad = False - # Music. self.music = MusicSubsystem() - - # Language. self.lang = LanguageSubsystem() - - # Achievements. - self.achievements: List[ba.Achievement] = [] - self.achievements_to_display: (List[Tuple[ba.Achievement, bool]]) = [] - self.achievement_display_timer: Optional[_ba.Timer] = None - self.last_achievement_display_time: float = 0.0 - self.achievement_completion_banner_slots: Set[int] = set() + self.ach = AchievementSubsystem() + self.ui = UISubsystem() # Lobby. self.lobby_random_profile_index: int = 1 @@ -275,9 +267,6 @@ class App: self.ffa_series_length = 24 self.coop_session_args: Dict = {} - # UI. - self.ui = UISubsystem() - self.value_test_defaults: dict = {} self.first_main_menu = True # FIXME: Move to mainmenu class. self.did_menu_intro = False # FIXME: Move to mainmenu class. @@ -320,7 +309,7 @@ class App: self.ui.on_app_launch() - _achievement.init_achievements() + # _achievement.init_achievements() spazappearance.register_appearances() _campaign.init_campaigns() diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/assets/src/ba_data/python/ba/_coopgame.py index adc64ad4..ae7adca9 100644 --- a/assets/src/ba_data/python/ba/_coopgame.py +++ b/assets/src/ba_data/python/ba/_coopgame.py @@ -130,21 +130,19 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): player.actor.handlemessage(CelebrateMessage(duration)) def _preload_achievements(self) -> None: - from ba import _achievement - achievements = _achievement.get_achievements_for_coop_level( + achievements = _ba.app.ach.achievements_for_coop_level( self._get_coop_level_name()) for ach in achievements: ach.get_icon_texture(True) def _show_remaining_achievements(self) -> None: # pylint: disable=cyclic-import - from ba._achievement import get_achievements_for_coop_level from ba._language import Lstr from bastd.actor.text import Text ts_h_offs = 30 v_offs = -200 achievements = [ - a for a in get_achievements_for_coop_level( + a for a in _ba.app.ach.achievements_for_coop_level( self._get_coop_level_name()) if not a.complete ] vrmode = _ba.app.vr_mode @@ -193,12 +191,11 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): Returns True if a banner will be shown; False otherwise """ - from ba._achievement import get_achievement if achievement_name in self._achievements_awarded: return - ach = get_achievement(achievement_name) + ach = _ba.app.ach.get_achievement(achievement_name) # If we're in the easy campaign and this achievement is hard-mode-only, # ignore it. diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py index c1e10c8f..eed60f95 100644 --- a/assets/src/ba_data/python/ba/_hooks.py +++ b/assets/src/ba_data/python/ba/_hooks.py @@ -138,13 +138,11 @@ def language_test_toggle() -> None: def award_in_control_achievement() -> None: - from ba._achievement import award_local_achievement - award_local_achievement('In Control') + _ba.app.ach.award_local_achievement('In Control') def award_dual_wielding_achievement() -> None: - from ba._achievement import award_local_achievement - award_local_achievement('Dual Wielding') + _ba.app.ach.award_local_achievement('Dual Wielding') def play_gong_sound() -> None: diff --git a/assets/src/ba_data/python/ba/_music.py b/assets/src/ba_data/python/ba/_music.py index fda0630f..0bab82de 100644 --- a/assets/src/ba_data/python/ba/_music.py +++ b/assets/src/ba_data/python/ba/_music.py @@ -121,7 +121,7 @@ class MusicSubsystem: Category: App Classes - To use this class, access the single instance of it at 'ba.app.music'. + Access the single shared instance of this class at 'ba.app.music'. """ def __init__(self) -> None: diff --git a/assets/src/ba_data/python/ba/_teamgame.py b/assets/src/ba_data/python/ba/_teamgame.py index aa5563cf..afa8519e 100644 --- a/assets/src/ba_data/python/ba/_teamgame.py +++ b/assets/src/ba_data/python/ba/_teamgame.py @@ -6,11 +6,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar +import _ba from ba._freeforallsession import FreeForAllSession from ba._gameactivity import GameActivity from ba._gameresults import GameResults from ba._dualteamsession import DualTeamSession -import _ba if TYPE_CHECKING: from typing import Any, Dict, Type, Sequence @@ -78,12 +78,11 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]): # Award a few achievements. if isinstance(self.session, FreeForAllSession): if len(self.players) >= 2: - from ba import _achievement - _achievement.award_local_achievement('Free Loader') + _ba.app.ach.award_local_achievement('Free Loader') elif isinstance(self.session, DualTeamSession): if len(self.players) >= 4: from ba import _achievement - _achievement.award_local_achievement('Team Player') + _ba.app.ach.award_local_achievement('Team Player') except Exception: from ba import _error _error.print_exception() diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py index dd354fb5..9bf9568d 100644 --- a/assets/src/ba_data/python/ba/internal.py +++ b/assets/src/ba_data/python/ba/internal.py @@ -23,9 +23,6 @@ from ba._account import (on_account_state_changed, get_purchased_icons, get_cached_league_rank_data, get_league_rank_points, cache_league_rank_data) from ba._activitytypes import JoinActivity, ScoreScreenActivity -from ba._achievement import (get_achievement, set_completed_achievements, - display_achievement_banner, - get_achievements_for_coop_level) from ba._apputils import (is_browser_likely_available, get_remote_app_name, should_submit_debug_info, show_ad, show_ad_2) from ba._benchmark import (run_gpu_benchmark, run_cpu_benchmark, diff --git a/assets/src/ba_data/python/bastd/activity/coopjoin.py b/assets/src/ba_data/python/bastd/activity/coopjoin.py index 3a62e68b..1bb614a4 100644 --- a/assets/src/ba_data/python/bastd/activity/coopjoin.py +++ b/assets/src/ba_data/python/bastd/activity/coopjoin.py @@ -59,7 +59,6 @@ class CoopJoinActivity(JoinActivity): # pylint: disable=too-many-statements from efro.util import asserttype from bastd.actor.text import Text - from ba.internal import get_achievements_for_coop_level # Sort by originating date so that the most recent is first. if scores is not None: @@ -159,7 +158,8 @@ class CoopJoinActivity(JoinActivity): if not (ba.app.demo_mode or ba.app.arcade_mode): achievements = [ - a for a in get_achievements_for_coop_level(levelname) + a + for a in ba.app.ach.achievements_for_coop_level(levelname) if not a.complete ] have_achievements = bool(achievements) diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py index a870f8f9..1c9e8cfe 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/assets/src/ba_data/python/bastd/activity/coopscore.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING import _ba import ba -from ba.internal import get_achievements_for_coop_level from bastd.actor.text import Text from bastd.actor.zoomtext import ZoomText @@ -50,8 +49,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): self._campaign: ba.Campaign = settings['campaign'] self._have_achievements = bool( - get_achievements_for_coop_level(self._campaign.name + ':' + - settings['level'])) + ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' + + settings['level'])) self._account_type = (_ba.get_account_type() if _ba.get_account_state() == 'signed_in' else None) @@ -847,7 +846,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): transition_delay=2.8).autoretain() assert self._game_name_str is not None - achievements = get_achievements_for_coop_level(self._game_name_str) + achievements = ba.app.ach.achievements_for_coop_level( + self._game_name_str) hval = -455 vval = -100 tdelay = 0.0 diff --git a/assets/src/ba_data/python/bastd/mainmenu.py b/assets/src/ba_data/python/bastd/mainmenu.py index 44808147..9697e521 100644 --- a/assets/src/ba_data/python/bastd/mainmenu.py +++ b/assets/src/ba_data/python/bastd/mainmenu.py @@ -319,7 +319,8 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]): transition_out_delay=self._message_duration ).autoretain() achs = [ - a for a in app.achievements if not a.complete + a for a in app.ach.achievements + if not a.complete ] if achs: ach = achs.pop( diff --git a/assets/src/ba_data/python/bastd/ui/account/settings.py b/assets/src/ba_data/python/bastd/ui/account/settings.py index 30599418..a87e7a9a 100644 --- a/assets/src/ba_data/python/bastd/ui/account/settings.py +++ b/assets/src/ba_data/python/bastd/ui/account/settings.py @@ -995,8 +995,8 @@ class AccountSettingsWindow(ba.Window): if (self._achievements_text is None and self._achievements_button is None): return - complete = sum(1 if a.complete else 0 for a in ba.app.achievements) - total = len(ba.app.achievements) + complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements) + total = len(ba.app.ach.achievements) txt_final = ba.Lstr(resource=self._r + '.achievementProgressText', subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))]) diff --git a/assets/src/ba_data/python/bastd/ui/account/viewer.py b/assets/src/ba_data/python/bastd/ui/account/viewer.py index 3b72aef0..a8ea39af 100644 --- a/assets/src/ba_data/python/bastd/ui/account/viewer.py +++ b/assets/src/ba_data/python/bastd/ui/account/viewer.py @@ -426,7 +426,7 @@ class AccountViewerWindow(popup.PopupWindow): v_align='center', scale=0.55, text=str(data['achievementsCompleted']) + ' / ' + - str(len(ba.app.achievements)), + str(len(ba.app.ach.achievements)), maxwidth=sub_width * maxwidth_scale) v -= 25 diff --git a/assets/src/ba_data/python/bastd/ui/achievements.py b/assets/src/ba_data/python/bastd/ui/achievements.py index 66cb5b0b..ca56c436 100644 --- a/assets/src/ba_data/python/bastd/ui/achievements.py +++ b/assets/src/ba_data/python/bastd/ui/achievements.py @@ -47,7 +47,7 @@ class AchievementsWindow(popup.PopupWindow): icon=ba.gettexture('crossOut'), iconscale=1.2) - achievements = ba.app.achievements + achievements = ba.app.ach.achievements num_complete = len([a for a in achievements if a.complete]) txt_final = ba.Lstr( diff --git a/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py b/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py index 304d681e..6c6e8f07 100644 --- a/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py +++ b/assets/src/ba_data/python/bastd/ui/coop/gamebutton.py @@ -22,7 +22,7 @@ class GameButton: x: float, y: float, select: bool, row: str): # pylint: disable=too-many-statements # pylint: disable=too-many-locals - from ba.internal import (get_achievements_for_coop_level, getcampaign) + from ba.internal import getcampaign self._game = game sclx = 195.0 scly = 195.0 @@ -81,7 +81,7 @@ class GameButton: mask_texture=ba.gettexture('mapPreviewMask')) translated = campaign.getlevel(levelname).displayname - self._achievements = (get_achievements_for_coop_level(game)) + self._achievements = ba.app.ach.achievements_for_coop_level(game) self._name_widget = ba.textwidget(parent=parent, draw_controller=btn, diff --git a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py b/assets/src/ba_data/python/bastd/ui/league/rankwindow.py index 43df0935..e0c7230b 100644 --- a/assets/src/ba_data/python/bastd/ui/league/rankwindow.py +++ b/assets/src/ba_data/python/bastd/ui/league/rankwindow.py @@ -808,7 +808,7 @@ class LeagueRankWindow(ba.Window): # for the achievement value, use the number they gave us for # non-current seasons; otherwise calc our own total_ach_value = 0 - for ach in ba.app.achievements: + for ach in ba.app.ach.achievements: if ach.complete: total_ach_value += ach.power_ranking_value if self._season != 'a' and not self._is_current_season: diff --git a/docs/ba_module.md b/docs/ba_module.md index a25ce291..3fb8309b 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-10-15 for Ballistica version 1.5.27 build 20218

+

last updated on 2020-10-16 for Ballistica version 1.5.27 build 20219

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


@@ -148,6 +148,7 @@

App Classes