ba
The public face of Ballistica.
This top level module is a collection of most commonly used functionality. For many modding purposes, the bits exposed here are all you'll need. In some specific cases you may need to pull in individual submodules instead.
1# Released under the MIT License. See LICENSE for details. 2# 3"""The public face of Ballistica. 4 5This top level module is a collection of most commonly used functionality. 6For many modding purposes, the bits exposed here are all you'll need. 7In some specific cases you may need to pull in individual submodules instead. 8""" 9# pylint: disable=redefined-builtin 10 11from _ba import ( 12 CollideModel, 13 Context, 14 ContextCall, 15 Data, 16 InputDevice, 17 Material, 18 Model, 19 Node, 20 SessionPlayer, 21 Sound, 22 Texture, 23 Timer, 24 Vec3, 25 Widget, 26 buttonwidget, 27 camerashake, 28 checkboxwidget, 29 columnwidget, 30 containerwidget, 31 do_once, 32 emitfx, 33 getactivity, 34 getcollidemodel, 35 getmodel, 36 getnodes, 37 getsession, 38 getsound, 39 gettexture, 40 hscrollwidget, 41 imagewidget, 42 newactivity, 43 newnode, 44 playsound, 45 printnodes, 46 ls_objects, 47 ls_input_devices, 48 pushcall, 49 quit, 50 rowwidget, 51 safecolor, 52 screenmessage, 53 scrollwidget, 54 set_analytics_screen, 55 charstr, 56 textwidget, 57 time, 58 timer, 59 open_url, 60 widget, 61 clipboard_is_supported, 62 clipboard_has_text, 63 clipboard_get_text, 64 clipboard_set_text, 65 getdata, 66 in_logic_thread, 67) 68from ba._accountv2 import AccountV2Handle 69from ba._activity import Activity 70from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem 71from ba._actor import Actor 72from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation 73from ba._nodeactor import NodeActor 74from ba._app import App 75from ba._cloud import CloudSubsystem 76from ba._coopgame import CoopGameActivity 77from ba._coopsession import CoopSession 78from ba._dependency import ( 79 Dependency, 80 DependencyComponent, 81 DependencySet, 82 AssetPackage, 83) 84from ba._generated.enums import ( 85 TimeType, 86 Permission, 87 TimeFormat, 88 SpecialChar, 89 InputType, 90 UIScale, 91) 92from ba._error import ( 93 print_exception, 94 print_error, 95 ContextError, 96 NotFoundError, 97 PlayerNotFoundError, 98 SessionPlayerNotFoundError, 99 NodeNotFoundError, 100 ActorNotFoundError, 101 InputDeviceNotFoundError, 102 WidgetNotFoundError, 103 ActivityNotFoundError, 104 TeamNotFoundError, 105 MapNotFoundError, 106 SessionTeamNotFoundError, 107 SessionNotFoundError, 108 DelegateNotFoundError, 109 DependencyError, 110) 111from ba._freeforallsession import FreeForAllSession 112from ba._gameactivity import GameActivity 113from ba._gameresults import GameResults 114from ba._settings import ( 115 Setting, 116 IntSetting, 117 FloatSetting, 118 ChoiceSetting, 119 BoolSetting, 120 IntChoiceSetting, 121 FloatChoiceSetting, 122) 123from ba._language import Lstr, LanguageSubsystem 124from ba._map import Map, getmaps 125from ba._session import Session 126from ba._ui import UISubsystem 127from ba._servermode import ServerController 128from ba._score import ScoreType, ScoreConfig 129from ba._stats import PlayerScoredMessage, PlayerRecord, Stats 130from ba._team import SessionTeam, Team, EmptyTeam 131from ba._teamgame import TeamGameActivity 132from ba._dualteamsession import DualTeamSession 133from ba._achievement import Achievement, AchievementSubsystem 134from ba._appconfig import AppConfig 135from ba._appdelegate import AppDelegate 136from ba._apputils import is_browser_likely_available, garbage_collect 137from ba._campaign import Campaign 138from ba._gameutils import ( 139 GameTip, 140 animate, 141 animate_array, 142 show_damage_count, 143 timestring, 144 cameraflash, 145) 146from ba._general import ( 147 WeakCall, 148 Call, 149 existing, 150 Existable, 151 verify_object_death, 152 storagename, 153 getclass, 154) 155from ba._keyboard import Keyboard 156from ba._level import Level 157from ba._lobby import Lobby, Chooser 158from ba._math import normalized_color, is_point_in_box, vec3validate 159from ba._meta import MetadataSubsystem 160from ba._messages import ( 161 UNHANDLED, 162 OutOfBoundsMessage, 163 DeathType, 164 DieMessage, 165 PlayerDiedMessage, 166 StandMessage, 167 PickUpMessage, 168 DropMessage, 169 PickedUpMessage, 170 DroppedMessage, 171 ShouldShatterMessage, 172 ImpactDamageMessage, 173 FreezeMessage, 174 ThawMessage, 175 HitMessage, 176 CelebrateMessage, 177) 178from ba._music import ( 179 setmusic, 180 MusicPlayer, 181 MusicType, 182 MusicPlayMode, 183 MusicSubsystem, 184) 185from ba._powerup import PowerupMessage, PowerupAcceptMessage 186from ba._multiteamsession import MultiTeamSession 187from ba.ui import Window, UIController, uicleanupcheck 188from ba._collision import Collision, getcollision 189 190app: App 191 192__all__ = [ 193 'AccountV2Handle', 194 'Achievement', 195 'AchievementSubsystem', 196 'Activity', 197 'ActivityNotFoundError', 198 'Actor', 199 'ActorNotFoundError', 200 'animate', 201 'animate_array', 202 'app', 203 'App', 204 'AppConfig', 205 'AppDelegate', 206 'AssetPackage', 207 'BoolSetting', 208 'buttonwidget', 209 'Call', 210 'cameraflash', 211 'camerashake', 212 'Campaign', 213 'CelebrateMessage', 214 'charstr', 215 'checkboxwidget', 216 'ChoiceSetting', 217 'Chooser', 218 'clipboard_get_text', 219 'clipboard_has_text', 220 'clipboard_is_supported', 221 'clipboard_set_text', 222 'CollideModel', 223 'Collision', 224 'columnwidget', 225 'containerwidget', 226 'Context', 227 'ContextCall', 228 'ContextError', 229 'CloudSubsystem', 230 'CoopGameActivity', 231 'CoopSession', 232 'Data', 233 'DeathType', 234 'DelegateNotFoundError', 235 'Dependency', 236 'DependencyComponent', 237 'DependencyError', 238 'DependencySet', 239 'DieMessage', 240 'do_once', 241 'DropMessage', 242 'DroppedMessage', 243 'DualTeamSession', 244 'emitfx', 245 'EmptyPlayer', 246 'EmptyTeam', 247 'Existable', 248 'existing', 249 'FloatChoiceSetting', 250 'FloatSetting', 251 'FreeForAllSession', 252 'FreezeMessage', 253 'GameActivity', 254 'GameResults', 255 'GameTip', 256 'garbage_collect', 257 'getactivity', 258 'getclass', 259 'getcollidemodel', 260 'getcollision', 261 'getdata', 262 'getmaps', 263 'getmodel', 264 'getnodes', 265 'getsession', 266 'getsound', 267 'gettexture', 268 'HitMessage', 269 'hscrollwidget', 270 'imagewidget', 271 'ImpactDamageMessage', 272 'in_logic_thread', 273 'InputDevice', 274 'InputDeviceNotFoundError', 275 'InputType', 276 'IntChoiceSetting', 277 'IntSetting', 278 'is_browser_likely_available', 279 'is_point_in_box', 280 'Keyboard', 281 'LanguageSubsystem', 282 'Level', 283 'Lobby', 284 'Lstr', 285 'Map', 286 'MapNotFoundError', 287 'Material', 288 'MetadataSubsystem', 289 'Model', 290 'MultiTeamSession', 291 'MusicPlayer', 292 'MusicPlayMode', 293 'MusicSubsystem', 294 'MusicType', 295 'newactivity', 296 'newnode', 297 'Node', 298 'NodeActor', 299 'NodeNotFoundError', 300 'normalized_color', 301 'NotFoundError', 302 'open_url', 303 'OutOfBoundsMessage', 304 'Permission', 305 'PickedUpMessage', 306 'PickUpMessage', 307 'Player', 308 'PlayerDiedMessage', 309 'PlayerInfo', 310 'PlayerNotFoundError', 311 'PlayerRecord', 312 'PlayerScoredMessage', 313 'playsound', 314 'Plugin', 315 'PluginSubsystem', 316 'PotentialPlugin', 317 'PowerupAcceptMessage', 318 'PowerupMessage', 319 'print_error', 320 'print_exception', 321 'printnodes', 322 'ls_objects', 323 'ls_input_devices', 324 'pushcall', 325 'quit', 326 'rowwidget', 327 'safecolor', 328 'ScoreConfig', 329 'ScoreType', 330 'screenmessage', 331 'scrollwidget', 332 'ServerController', 333 'Session', 334 'SessionNotFoundError', 335 'SessionPlayer', 336 'SessionPlayerNotFoundError', 337 'SessionTeam', 338 'SessionTeamNotFoundError', 339 'set_analytics_screen', 340 'setmusic', 341 'Setting', 342 'ShouldShatterMessage', 343 'show_damage_count', 344 'Sound', 345 'SpecialChar', 346 'StandLocation', 347 'StandMessage', 348 'Stats', 349 'storagename', 350 'Team', 351 'TeamGameActivity', 352 'TeamNotFoundError', 353 'Texture', 354 'textwidget', 355 'ThawMessage', 356 'time', 357 'TimeFormat', 358 'Timer', 359 'timer', 360 'timestring', 361 'TimeType', 362 'uicleanupcheck', 363 'UIController', 364 'UIScale', 365 'UISubsystem', 366 'UNHANDLED', 367 'Vec3', 368 'vec3validate', 369 'verify_object_death', 370 'WeakCall', 371 'Widget', 372 'widget', 373 'WidgetNotFoundError', 374 'Window', 375] 376 377 378# Have these things present themselves cleanly as 'ba.Foo' 379# instead of 'ba._submodule.Foo' 380def _simplify_module_names() -> None: 381 import os 382 383 # Though pdoc gets confused when we override __module__, 384 # so let's make an exception for it. 385 if os.environ.get('BA_DOCS_GENERATION', '0') != '1': 386 from efro.util import set_canonical_module 387 388 globs = globals() 389 set_canonical_module( 390 module_globals=globs, 391 names=[n for n in globs.keys() if not n.startswith('_')], 392 ) 393 394 395_simplify_module_names() 396del _simplify_module_names
407class AccountV2Handle: 408 """Handle for interacting with a V2 account. 409 410 This class supports the 'with' statement, which is how it is 411 used with some operations such as cloud messaging. 412 """ 413 414 def __init__(self) -> None: 415 self.tag = '?' 416 417 self.workspacename: str | None = None 418 self.workspaceid: str | None = None 419 420 # Login types and their display-names associated with this account. 421 self.logins: dict[LoginType, str] = {} 422 423 def __enter__(self) -> None: 424 """Support for "with" statement. 425 426 This allows cloud messages to be sent on our behalf. 427 """ 428 429 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: 430 """Support for "with" statement. 431 432 This allows cloud messages to be sent on our behalf. 433 """
Handle for interacting with a V2 account.
This class supports the 'with' statement, which is how it is used with some operations such as cloud messaging.
635class Achievement: 636 """Represents attributes and state for an individual achievement. 637 638 Category: **App Classes** 639 """ 640 641 def __init__( 642 self, 643 name: str, 644 icon_name: str, 645 icon_color: Sequence[float], 646 level_name: str, 647 award: int, 648 hard_mode_only: bool = False, 649 ): 650 self._name = name 651 self._icon_name = icon_name 652 self._icon_color: Sequence[float] = list(icon_color) + [1] 653 self._level_name = level_name 654 self._completion_banner_slot: int | None = None 655 self._award = award 656 self._hard_mode_only = hard_mode_only 657 658 @property 659 def name(self) -> str: 660 """The name of this achievement.""" 661 return self._name 662 663 @property 664 def level_name(self) -> str: 665 """The name of the level this achievement applies to.""" 666 return self._level_name 667 668 def get_icon_texture(self, complete: bool) -> ba.Texture: 669 """Return the icon texture to display for this achievement""" 670 return _ba.gettexture( 671 self._icon_name if complete else 'achievementEmpty' 672 ) 673 674 def get_icon_color(self, complete: bool) -> Sequence[float]: 675 """Return the color tint for this Achievement's icon.""" 676 if complete: 677 return self._icon_color 678 return 1.0, 1.0, 1.0, 0.6 679 680 @property 681 def hard_mode_only(self) -> bool: 682 """Whether this Achievement is only unlockable in hard-mode.""" 683 return self._hard_mode_only 684 685 @property 686 def complete(self) -> bool: 687 """Whether this Achievement is currently complete.""" 688 val: bool = self._getconfig()['Complete'] 689 assert isinstance(val, bool) 690 return val 691 692 def announce_completion(self, sound: bool = True) -> None: 693 """Kick off an announcement for this achievement's completion.""" 694 from ba._generated.enums import TimeType 695 696 app = _ba.app 697 698 # Even though there are technically achievements when we're not 699 # signed in, lets not show them (otherwise we tend to get 700 # confusing 'controller connected' achievements popping up while 701 # waiting to sign in which can be confusing). 702 if _internal.get_v1_account_state() != 'signed_in': 703 return 704 705 # If we're being freshly complete, display/report it and whatnot. 706 if (self, sound) not in app.ach.achievements_to_display: 707 app.ach.achievements_to_display.append((self, sound)) 708 709 # If there's no achievement display timer going, kick one off 710 # (if one's already running it will pick this up before it dies). 711 712 # Need to check last time too; its possible our timer wasn't able to 713 # clear itself if an activity died and took it down with it. 714 if ( 715 app.ach.achievement_display_timer is None 716 or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time 717 > 2.0 718 ) and _ba.getactivity(doraise=False) is not None: 719 app.ach.achievement_display_timer = _ba.Timer( 720 1.0, 721 _display_next_achievement, 722 repeat=True, 723 timetype=TimeType.BASE, 724 ) 725 726 # Show the first immediately. 727 _display_next_achievement() 728 729 def set_complete(self, complete: bool = True) -> None: 730 """Set an achievement's completed state. 731 732 note this only sets local state; use a transaction to 733 actually award achievements. 734 """ 735 config = self._getconfig() 736 if complete != config['Complete']: 737 config['Complete'] = complete 738 739 @property 740 def display_name(self) -> ba.Lstr: 741 """Return a ba.Lstr for this Achievement's name.""" 742 from ba._language import Lstr 743 744 name: ba.Lstr | str 745 try: 746 if self._level_name != '': 747 from ba._campaign import getcampaign 748 749 campaignname, campaign_level = self._level_name.split(':') 750 name = ( 751 getcampaign(campaignname) 752 .getlevel(campaign_level) 753 .displayname 754 ) 755 else: 756 name = '' 757 except Exception: 758 name = '' 759 print_exception() 760 return Lstr( 761 resource='achievements.' + self._name + '.name', 762 subs=[('${LEVEL}', name)], 763 ) 764 765 @property 766 def description(self) -> ba.Lstr: 767 """Get a ba.Lstr for the Achievement's brief description.""" 768 from ba._language import Lstr 769 770 if ( 771 'description' 772 in _ba.app.lang.get_resource('achievements')[self._name] 773 ): 774 return Lstr(resource='achievements.' + self._name + '.description') 775 return Lstr(resource='achievements.' + self._name + '.descriptionFull') 776 777 @property 778 def description_complete(self) -> ba.Lstr: 779 """Get a ba.Lstr for the Achievement's description when completed.""" 780 from ba._language import Lstr 781 782 if ( 783 'descriptionComplete' 784 in _ba.app.lang.get_resource('achievements')[self._name] 785 ): 786 return Lstr( 787 resource='achievements.' + self._name + '.descriptionComplete' 788 ) 789 return Lstr( 790 resource='achievements.' + self._name + '.descriptionFullComplete' 791 ) 792 793 @property 794 def description_full(self) -> ba.Lstr: 795 """Get a ba.Lstr for the Achievement's full description.""" 796 from ba._language import Lstr 797 798 return Lstr( 799 resource='achievements.' + self._name + '.descriptionFull', 800 subs=[ 801 ( 802 '${LEVEL}', 803 Lstr( 804 translate=( 805 'coopLevelNames', 806 ACH_LEVEL_NAMES.get(self._name, '?'), 807 ) 808 ), 809 ) 810 ], 811 ) 812 813 @property 814 def description_full_complete(self) -> ba.Lstr: 815 """Get a ba.Lstr for the Achievement's full desc. when completed.""" 816 from ba._language import Lstr 817 818 return Lstr( 819 resource='achievements.' + self._name + '.descriptionFullComplete', 820 subs=[ 821 ( 822 '${LEVEL}', 823 Lstr( 824 translate=( 825 'coopLevelNames', 826 ACH_LEVEL_NAMES.get(self._name, '?'), 827 ) 828 ), 829 ) 830 ], 831 ) 832 833 def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: 834 """Get the ticket award value for this achievement.""" 835 val: int = _internal.get_v1_account_misc_read_val( 836 'achAward.' + self._name, self._award 837 ) * _get_ach_mult(include_pro_bonus) 838 assert isinstance(val, int) 839 return val 840 841 @property 842 def power_ranking_value(self) -> int: 843 """Get the power-ranking award value for this achievement.""" 844 val: int = _internal.get_v1_account_misc_read_val( 845 'achLeaguePoints.' + self._name, self._award 846 ) 847 assert isinstance(val, int) 848 return val 849 850 def create_display( 851 self, 852 x: float, 853 y: float, 854 delay: float, 855 outdelay: float | None = None, 856 color: Sequence[float] | None = None, 857 style: str = 'post_game', 858 ) -> list[ba.Actor]: 859 """Create a display for the Achievement. 860 861 Shows the Achievement icon, name, and description. 862 """ 863 # pylint: disable=cyclic-import 864 from ba._language import Lstr 865 from ba._generated.enums import SpecialChar 866 from ba._coopsession import CoopSession 867 from bastd.actor.image import Image 868 from bastd.actor.text import Text 869 870 # Yeah this needs cleaning up. 871 if style == 'post_game': 872 in_game_colors = False 873 in_main_menu = False 874 h_attach = Text.HAttach.CENTER 875 v_attach = Text.VAttach.CENTER 876 attach = Image.Attach.CENTER 877 elif style == 'in_game': 878 in_game_colors = True 879 in_main_menu = False 880 h_attach = Text.HAttach.LEFT 881 v_attach = Text.VAttach.TOP 882 attach = Image.Attach.TOP_LEFT 883 elif style == 'news': 884 in_game_colors = True 885 in_main_menu = True 886 h_attach = Text.HAttach.CENTER 887 v_attach = Text.VAttach.TOP 888 attach = Image.Attach.TOP_CENTER 889 else: 890 raise ValueError('invalid style "' + style + '"') 891 892 # Attempt to determine what campaign we're in 893 # (so we know whether to show "hard mode only"). 894 if in_main_menu: 895 hmo = False 896 else: 897 try: 898 session = _ba.getsession() 899 if isinstance(session, CoopSession): 900 campaign = session.campaign 901 assert campaign is not None 902 hmo = self._hard_mode_only and campaign.name == 'Easy' 903 else: 904 hmo = False 905 except Exception: 906 print_exception('Error determining campaign.') 907 hmo = False 908 909 objs: list[ba.Actor] 910 911 if in_game_colors: 912 objs = [] 913 out_delay_fin = (delay + outdelay) if outdelay is not None else None 914 if color is not None: 915 cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3]) 916 cl2 = color 917 else: 918 cl1 = (1.5, 1.5, 2, 1.0) 919 cl2 = (0.8, 0.8, 1.0, 1.0) 920 921 if hmo: 922 cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) 923 cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) 924 925 objs.append( 926 Image( 927 self.get_icon_texture(False), 928 host_only=True, 929 color=cl1, 930 position=(x - 25, y + 5), 931 attach=attach, 932 transition=Image.Transition.FADE_IN, 933 transition_delay=delay, 934 vr_depth=4, 935 transition_out_delay=out_delay_fin, 936 scale=(40, 40), 937 ).autoretain() 938 ) 939 txt = self.display_name 940 txt_s = 0.85 941 txt_max_w = 300 942 objs.append( 943 Text( 944 txt, 945 host_only=True, 946 maxwidth=txt_max_w, 947 position=(x, y + 2), 948 transition=Text.Transition.FADE_IN, 949 scale=txt_s, 950 flatness=0.6, 951 shadow=0.5, 952 h_attach=h_attach, 953 v_attach=v_attach, 954 color=cl2, 955 transition_delay=delay + 0.05, 956 transition_out_delay=out_delay_fin, 957 ).autoretain() 958 ) 959 txt2_s = 0.62 960 txt2_max_w = 400 961 objs.append( 962 Text( 963 self.description_full if in_main_menu else self.description, 964 host_only=True, 965 maxwidth=txt2_max_w, 966 position=(x, y - 14), 967 transition=Text.Transition.FADE_IN, 968 vr_depth=-5, 969 h_attach=h_attach, 970 v_attach=v_attach, 971 scale=txt2_s, 972 flatness=1.0, 973 shadow=0.5, 974 color=cl2, 975 transition_delay=delay + 0.1, 976 transition_out_delay=out_delay_fin, 977 ).autoretain() 978 ) 979 980 if hmo: 981 txtactor = Text( 982 Lstr(resource='difficultyHardOnlyText'), 983 host_only=True, 984 maxwidth=txt2_max_w * 0.7, 985 position=(x + 60, y + 5), 986 transition=Text.Transition.FADE_IN, 987 vr_depth=-5, 988 h_attach=h_attach, 989 v_attach=v_attach, 990 h_align=Text.HAlign.CENTER, 991 v_align=Text.VAlign.CENTER, 992 scale=txt_s * 0.8, 993 flatness=1.0, 994 shadow=0.5, 995 color=(1, 1, 0.6, 1), 996 transition_delay=delay + 0.1, 997 transition_out_delay=out_delay_fin, 998 ).autoretain() 999 txtactor.node.rotate = 10 1000 objs.append(txtactor) 1001 1002 # Ticket-award. 1003 award_x = -100 1004 objs.append( 1005 Text( 1006 _ba.charstr(SpecialChar.TICKET), 1007 host_only=True, 1008 position=(x + award_x + 33, y + 7), 1009 transition=Text.Transition.FADE_IN, 1010 scale=1.5, 1011 h_attach=h_attach, 1012 v_attach=v_attach, 1013 h_align=Text.HAlign.CENTER, 1014 v_align=Text.VAlign.CENTER, 1015 color=(1, 1, 1, 0.2 if hmo else 0.4), 1016 transition_delay=delay + 0.05, 1017 transition_out_delay=out_delay_fin, 1018 ).autoretain() 1019 ) 1020 objs.append( 1021 Text( 1022 '+' + str(self.get_award_ticket_value()), 1023 host_only=True, 1024 position=(x + award_x + 28, y + 16), 1025 transition=Text.Transition.FADE_IN, 1026 scale=0.7, 1027 flatness=1, 1028 h_attach=h_attach, 1029 v_attach=v_attach, 1030 h_align=Text.HAlign.CENTER, 1031 v_align=Text.VAlign.CENTER, 1032 color=cl2, 1033 transition_delay=delay + 0.05, 1034 transition_out_delay=out_delay_fin, 1035 ).autoretain() 1036 ) 1037 1038 else: 1039 complete = self.complete 1040 objs = [] 1041 c_icon = self.get_icon_color(complete) 1042 if hmo and not complete: 1043 c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) 1044 objs.append( 1045 Image( 1046 self.get_icon_texture(complete), 1047 host_only=True, 1048 color=c_icon, 1049 position=(x - 25, y + 5), 1050 attach=attach, 1051 vr_depth=4, 1052 transition=Image.Transition.IN_RIGHT, 1053 transition_delay=delay, 1054 transition_out_delay=None, 1055 scale=(40, 40), 1056 ).autoretain() 1057 ) 1058 if complete: 1059 objs.append( 1060 Image( 1061 _ba.gettexture('achievementOutline'), 1062 host_only=True, 1063 model_transparent=_ba.getmodel('achievementOutline'), 1064 color=(2, 1.4, 0.4, 1), 1065 vr_depth=8, 1066 position=(x - 25, y + 5), 1067 attach=attach, 1068 transition=Image.Transition.IN_RIGHT, 1069 transition_delay=delay, 1070 transition_out_delay=None, 1071 scale=(40, 40), 1072 ).autoretain() 1073 ) 1074 else: 1075 if not complete: 1076 award_x = -100 1077 objs.append( 1078 Text( 1079 _ba.charstr(SpecialChar.TICKET), 1080 host_only=True, 1081 position=(x + award_x + 33, y + 7), 1082 transition=Text.Transition.IN_RIGHT, 1083 scale=1.5, 1084 h_attach=h_attach, 1085 v_attach=v_attach, 1086 h_align=Text.HAlign.CENTER, 1087 v_align=Text.VAlign.CENTER, 1088 color=(1, 1, 1, 0.4) 1089 if complete 1090 else (1, 1, 1, (0.1 if hmo else 0.2)), 1091 transition_delay=delay + 0.05, 1092 transition_out_delay=None, 1093 ).autoretain() 1094 ) 1095 objs.append( 1096 Text( 1097 '+' + str(self.get_award_ticket_value()), 1098 host_only=True, 1099 position=(x + award_x + 28, y + 16), 1100 transition=Text.Transition.IN_RIGHT, 1101 scale=0.7, 1102 flatness=1, 1103 h_attach=h_attach, 1104 v_attach=v_attach, 1105 h_align=Text.HAlign.CENTER, 1106 v_align=Text.VAlign.CENTER, 1107 color=( 1108 (0.8, 0.93, 0.8, 1.0) 1109 if complete 1110 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1111 ), 1112 transition_delay=delay + 0.05, 1113 transition_out_delay=None, 1114 ).autoretain() 1115 ) 1116 1117 # Show 'hard-mode-only' only over incomplete achievements 1118 # when that's the case. 1119 if hmo: 1120 txtactor = Text( 1121 Lstr(resource='difficultyHardOnlyText'), 1122 host_only=True, 1123 maxwidth=300 * 0.7, 1124 position=(x + 60, y + 5), 1125 transition=Text.Transition.FADE_IN, 1126 vr_depth=-5, 1127 h_attach=h_attach, 1128 v_attach=v_attach, 1129 h_align=Text.HAlign.CENTER, 1130 v_align=Text.VAlign.CENTER, 1131 scale=0.85 * 0.8, 1132 flatness=1.0, 1133 shadow=0.5, 1134 color=(1, 1, 0.6, 1), 1135 transition_delay=delay + 0.05, 1136 transition_out_delay=None, 1137 ).autoretain() 1138 assert txtactor.node 1139 txtactor.node.rotate = 10 1140 objs.append(txtactor) 1141 1142 objs.append( 1143 Text( 1144 self.display_name, 1145 host_only=True, 1146 maxwidth=300, 1147 position=(x, y + 2), 1148 transition=Text.Transition.IN_RIGHT, 1149 scale=0.85, 1150 flatness=0.6, 1151 h_attach=h_attach, 1152 v_attach=v_attach, 1153 color=( 1154 (0.8, 0.93, 0.8, 1.0) 1155 if complete 1156 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1157 ), 1158 transition_delay=delay + 0.05, 1159 transition_out_delay=None, 1160 ).autoretain() 1161 ) 1162 objs.append( 1163 Text( 1164 self.description_complete if complete else self.description, 1165 host_only=True, 1166 maxwidth=400, 1167 position=(x, y - 14), 1168 transition=Text.Transition.IN_RIGHT, 1169 vr_depth=-5, 1170 h_attach=h_attach, 1171 v_attach=v_attach, 1172 scale=0.62, 1173 flatness=1.0, 1174 color=( 1175 (0.6, 0.6, 0.6, 1.0) 1176 if complete 1177 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1178 ), 1179 transition_delay=delay + 0.1, 1180 transition_out_delay=None, 1181 ).autoretain() 1182 ) 1183 return objs 1184 1185 def _getconfig(self) -> dict[str, Any]: 1186 """ 1187 Return the sub-dict in settings where this achievement's 1188 state is stored, creating it if need be. 1189 """ 1190 val: dict[str, Any] = _ba.app.config.setdefault( 1191 'Achievements', {} 1192 ).setdefault(self._name, {'Complete': False}) 1193 assert isinstance(val, dict) 1194 return val 1195 1196 def _remove_banner_slot(self) -> None: 1197 assert self._completion_banner_slot is not None 1198 _ba.app.ach.achievement_completion_banner_slots.remove( 1199 self._completion_banner_slot 1200 ) 1201 self._completion_banner_slot = None 1202 1203 def show_completion_banner(self, sound: bool = True) -> None: 1204 """Create the banner/sound for an acquired achievement announcement.""" 1205 from ba import _gameutils 1206 from bastd.actor.text import Text 1207 from bastd.actor.image import Image 1208 from ba._general import WeakCall 1209 from ba._language import Lstr 1210 from ba._messages import DieMessage 1211 from ba._generated.enums import TimeType, SpecialChar 1212 1213 app = _ba.app 1214 app.ach.last_achievement_display_time = _ba.time(TimeType.REAL) 1215 1216 # Just piggy-back onto any current activity 1217 # (should we use the session instead?..) 1218 activity = _ba.getactivity(doraise=False) 1219 1220 # If this gets called while this achievement is occupying a slot 1221 # already, ignore it. (probably should never happen in real 1222 # life but whatevs). 1223 if self._completion_banner_slot is not None: 1224 return 1225 1226 if activity is None: 1227 print('show_completion_banner() called with no current activity!') 1228 return 1229 1230 if sound: 1231 _ba.playsound(_ba.getsound('achievement'), host_only=True) 1232 else: 1233 _ba.timer( 1234 0.5, lambda: _ba.playsound(_ba.getsound('ding'), host_only=True) 1235 ) 1236 1237 in_time = 0.300 1238 out_time = 3.5 1239 1240 base_vr_depth = 200 1241 1242 # Find the first free slot. 1243 i = 0 1244 while True: 1245 if i not in app.ach.achievement_completion_banner_slots: 1246 app.ach.achievement_completion_banner_slots.add(i) 1247 self._completion_banner_slot = i 1248 1249 # Remove us from that slot when we close. 1250 # Use a real-timer in the UI context so the removal runs even 1251 # if our activity/session dies. 1252 with _ba.Context('ui'): 1253 _ba.timer( 1254 in_time + out_time, 1255 self._remove_banner_slot, 1256 timetype=TimeType.REAL, 1257 ) 1258 break 1259 i += 1 1260 assert self._completion_banner_slot is not None 1261 y_offs = 110 * self._completion_banner_slot 1262 objs: list[ba.Actor] = [] 1263 obj = Image( 1264 _ba.gettexture('shadow'), 1265 position=(-30, 30 + y_offs), 1266 front=True, 1267 attach=Image.Attach.BOTTOM_CENTER, 1268 transition=Image.Transition.IN_BOTTOM, 1269 vr_depth=base_vr_depth - 100, 1270 transition_delay=in_time, 1271 transition_out_delay=out_time, 1272 color=(0.0, 0.1, 0, 1), 1273 scale=(1000, 300), 1274 ).autoretain() 1275 objs.append(obj) 1276 assert obj.node 1277 obj.node.host_only = True 1278 obj = Image( 1279 _ba.gettexture('light'), 1280 position=(-180, 60 + y_offs), 1281 front=True, 1282 attach=Image.Attach.BOTTOM_CENTER, 1283 vr_depth=base_vr_depth, 1284 transition=Image.Transition.IN_BOTTOM, 1285 transition_delay=in_time, 1286 transition_out_delay=out_time, 1287 color=(1.8, 1.8, 1.0, 0.0), 1288 scale=(40, 300), 1289 ).autoretain() 1290 objs.append(obj) 1291 assert obj.node 1292 obj.node.host_only = True 1293 obj.node.premultiplied = True 1294 combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2}) 1295 _gameutils.animate( 1296 combine, 1297 'input0', 1298 { 1299 in_time: 0, 1300 in_time + 0.4: 30, 1301 in_time + 0.5: 40, 1302 in_time + 0.6: 30, 1303 in_time + 2.0: 0, 1304 }, 1305 ) 1306 _gameutils.animate( 1307 combine, 1308 'input1', 1309 { 1310 in_time: 0, 1311 in_time + 0.4: 200, 1312 in_time + 0.5: 500, 1313 in_time + 0.6: 200, 1314 in_time + 2.0: 0, 1315 }, 1316 ) 1317 combine.connectattr('output', obj.node, 'scale') 1318 _gameutils.animate(obj.node, 'rotate', {0: 0.0, 0.35: 360.0}, loop=True) 1319 obj = Image( 1320 self.get_icon_texture(True), 1321 position=(-180, 60 + y_offs), 1322 attach=Image.Attach.BOTTOM_CENTER, 1323 front=True, 1324 vr_depth=base_vr_depth - 10, 1325 transition=Image.Transition.IN_BOTTOM, 1326 transition_delay=in_time, 1327 transition_out_delay=out_time, 1328 scale=(100, 100), 1329 ).autoretain() 1330 objs.append(obj) 1331 assert obj.node 1332 obj.node.host_only = True 1333 1334 # Flash. 1335 color = self.get_icon_color(True) 1336 combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) 1337 keys = { 1338 in_time: 1.0 * color[0], 1339 in_time + 0.4: 1.5 * color[0], 1340 in_time + 0.5: 6.0 * color[0], 1341 in_time + 0.6: 1.5 * color[0], 1342 in_time + 2.0: 1.0 * color[0], 1343 } 1344 _gameutils.animate(combine, 'input0', keys) 1345 keys = { 1346 in_time: 1.0 * color[1], 1347 in_time + 0.4: 1.5 * color[1], 1348 in_time + 0.5: 6.0 * color[1], 1349 in_time + 0.6: 1.5 * color[1], 1350 in_time + 2.0: 1.0 * color[1], 1351 } 1352 _gameutils.animate(combine, 'input1', keys) 1353 keys = { 1354 in_time: 1.0 * color[2], 1355 in_time + 0.4: 1.5 * color[2], 1356 in_time + 0.5: 6.0 * color[2], 1357 in_time + 0.6: 1.5 * color[2], 1358 in_time + 2.0: 1.0 * color[2], 1359 } 1360 _gameutils.animate(combine, 'input2', keys) 1361 combine.connectattr('output', obj.node, 'color') 1362 1363 obj = Image( 1364 _ba.gettexture('achievementOutline'), 1365 model_transparent=_ba.getmodel('achievementOutline'), 1366 position=(-180, 60 + y_offs), 1367 front=True, 1368 attach=Image.Attach.BOTTOM_CENTER, 1369 vr_depth=base_vr_depth, 1370 transition=Image.Transition.IN_BOTTOM, 1371 transition_delay=in_time, 1372 transition_out_delay=out_time, 1373 scale=(100, 100), 1374 ).autoretain() 1375 assert obj.node 1376 obj.node.host_only = True 1377 1378 # Flash. 1379 color = (2, 1.4, 0.4, 1) 1380 combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3}) 1381 keys = { 1382 in_time: 1.0 * color[0], 1383 in_time + 0.4: 1.5 * color[0], 1384 in_time + 0.5: 6.0 * color[0], 1385 in_time + 0.6: 1.5 * color[0], 1386 in_time + 2.0: 1.0 * color[0], 1387 } 1388 _gameutils.animate(combine, 'input0', keys) 1389 keys = { 1390 in_time: 1.0 * color[1], 1391 in_time + 0.4: 1.5 * color[1], 1392 in_time + 0.5: 6.0 * color[1], 1393 in_time + 0.6: 1.5 * color[1], 1394 in_time + 2.0: 1.0 * color[1], 1395 } 1396 _gameutils.animate(combine, 'input1', keys) 1397 keys = { 1398 in_time: 1.0 * color[2], 1399 in_time + 0.4: 1.5 * color[2], 1400 in_time + 0.5: 6.0 * color[2], 1401 in_time + 0.6: 1.5 * color[2], 1402 in_time + 2.0: 1.0 * color[2], 1403 } 1404 _gameutils.animate(combine, 'input2', keys) 1405 combine.connectattr('output', obj.node, 'color') 1406 objs.append(obj) 1407 1408 objt = Text( 1409 Lstr( 1410 value='${A}:', subs=[('${A}', Lstr(resource='achievementText'))] 1411 ), 1412 position=(-120, 91 + y_offs), 1413 front=True, 1414 v_attach=Text.VAttach.BOTTOM, 1415 vr_depth=base_vr_depth - 10, 1416 transition=Text.Transition.IN_BOTTOM, 1417 flatness=0.5, 1418 transition_delay=in_time, 1419 transition_out_delay=out_time, 1420 color=(1, 1, 1, 0.8), 1421 scale=0.65, 1422 ).autoretain() 1423 objs.append(objt) 1424 assert objt.node 1425 objt.node.host_only = True 1426 1427 objt = Text( 1428 self.display_name, 1429 position=(-120, 50 + y_offs), 1430 front=True, 1431 v_attach=Text.VAttach.BOTTOM, 1432 transition=Text.Transition.IN_BOTTOM, 1433 vr_depth=base_vr_depth, 1434 flatness=0.5, 1435 transition_delay=in_time, 1436 transition_out_delay=out_time, 1437 flash=True, 1438 color=(1, 0.8, 0, 1.0), 1439 scale=1.5, 1440 ).autoretain() 1441 objs.append(objt) 1442 assert objt.node 1443 objt.node.host_only = True 1444 1445 objt = Text( 1446 _ba.charstr(SpecialChar.TICKET), 1447 position=(-120 - 170 + 5, 75 + y_offs - 20), 1448 front=True, 1449 v_attach=Text.VAttach.BOTTOM, 1450 h_align=Text.HAlign.CENTER, 1451 v_align=Text.VAlign.CENTER, 1452 transition=Text.Transition.IN_BOTTOM, 1453 vr_depth=base_vr_depth, 1454 transition_delay=in_time, 1455 transition_out_delay=out_time, 1456 flash=True, 1457 color=(0.5, 0.5, 0.5, 1), 1458 scale=3.0, 1459 ).autoretain() 1460 objs.append(objt) 1461 assert objt.node 1462 objt.node.host_only = True 1463 1464 objt = Text( 1465 '+' + str(self.get_award_ticket_value()), 1466 position=(-120 - 180 + 5, 80 + y_offs - 20), 1467 v_attach=Text.VAttach.BOTTOM, 1468 front=True, 1469 h_align=Text.HAlign.CENTER, 1470 v_align=Text.VAlign.CENTER, 1471 transition=Text.Transition.IN_BOTTOM, 1472 vr_depth=base_vr_depth, 1473 flatness=0.5, 1474 shadow=1.0, 1475 transition_delay=in_time, 1476 transition_out_delay=out_time, 1477 flash=True, 1478 color=(0, 1, 0, 1), 1479 scale=1.5, 1480 ).autoretain() 1481 objs.append(objt) 1482 assert objt.node 1483 objt.node.host_only = True 1484 1485 # Add the 'x 2' if we've got pro. 1486 if app.accounts_v1.have_pro(): 1487 objt = Text( 1488 'x 2', 1489 position=(-120 - 180 + 45, 80 + y_offs - 50), 1490 v_attach=Text.VAttach.BOTTOM, 1491 front=True, 1492 h_align=Text.HAlign.CENTER, 1493 v_align=Text.VAlign.CENTER, 1494 transition=Text.Transition.IN_BOTTOM, 1495 vr_depth=base_vr_depth, 1496 flatness=0.5, 1497 shadow=1.0, 1498 transition_delay=in_time, 1499 transition_out_delay=out_time, 1500 flash=True, 1501 color=(0.4, 0, 1, 1), 1502 scale=0.9, 1503 ).autoretain() 1504 objs.append(objt) 1505 assert objt.node 1506 objt.node.host_only = True 1507 1508 objt = Text( 1509 self.description_complete, 1510 position=(-120, 30 + y_offs), 1511 front=True, 1512 v_attach=Text.VAttach.BOTTOM, 1513 transition=Text.Transition.IN_BOTTOM, 1514 vr_depth=base_vr_depth - 10, 1515 flatness=0.5, 1516 transition_delay=in_time, 1517 transition_out_delay=out_time, 1518 color=(1.0, 0.7, 0.5, 1.0), 1519 scale=0.8, 1520 ).autoretain() 1521 objs.append(objt) 1522 assert objt.node 1523 objt.node.host_only = True 1524 1525 for actor in objs: 1526 _ba.timer( 1527 out_time + 1.000, WeakCall(actor.handlemessage, DieMessage()) 1528 )
Represents attributes and state for an individual achievement.
Category: App Classes
641 def __init__( 642 self, 643 name: str, 644 icon_name: str, 645 icon_color: Sequence[float], 646 level_name: str, 647 award: int, 648 hard_mode_only: bool = False, 649 ): 650 self._name = name 651 self._icon_name = icon_name 652 self._icon_color: Sequence[float] = list(icon_color) + [1] 653 self._level_name = level_name 654 self._completion_banner_slot: int | None = None 655 self._award = award 656 self._hard_mode_only = hard_mode_only
668 def get_icon_texture(self, complete: bool) -> ba.Texture: 669 """Return the icon texture to display for this achievement""" 670 return _ba.gettexture( 671 self._icon_name if complete else 'achievementEmpty' 672 )
Return the icon texture to display for this achievement
674 def get_icon_color(self, complete: bool) -> Sequence[float]: 675 """Return the color tint for this Achievement's icon.""" 676 if complete: 677 return self._icon_color 678 return 1.0, 1.0, 1.0, 0.6
Return the color tint for this Achievement's icon.
692 def announce_completion(self, sound: bool = True) -> None: 693 """Kick off an announcement for this achievement's completion.""" 694 from ba._generated.enums import TimeType 695 696 app = _ba.app 697 698 # Even though there are technically achievements when we're not 699 # signed in, lets not show them (otherwise we tend to get 700 # confusing 'controller connected' achievements popping up while 701 # waiting to sign in which can be confusing). 702 if _internal.get_v1_account_state() != 'signed_in': 703 return 704 705 # If we're being freshly complete, display/report it and whatnot. 706 if (self, sound) not in app.ach.achievements_to_display: 707 app.ach.achievements_to_display.append((self, sound)) 708 709 # If there's no achievement display timer going, kick one off 710 # (if one's already running it will pick this up before it dies). 711 712 # Need to check last time too; its possible our timer wasn't able to 713 # clear itself if an activity died and took it down with it. 714 if ( 715 app.ach.achievement_display_timer is None 716 or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time 717 > 2.0 718 ) and _ba.getactivity(doraise=False) is not None: 719 app.ach.achievement_display_timer = _ba.Timer( 720 1.0, 721 _display_next_achievement, 722 repeat=True, 723 timetype=TimeType.BASE, 724 ) 725 726 # Show the first immediately. 727 _display_next_achievement()
Kick off an announcement for this achievement's completion.
729 def set_complete(self, complete: bool = True) -> None: 730 """Set an achievement's completed state. 731 732 note this only sets local state; use a transaction to 733 actually award achievements. 734 """ 735 config = self._getconfig() 736 if complete != config['Complete']: 737 config['Complete'] = complete
Set an achievement's completed state.
note this only sets local state; use a transaction to actually award achievements.
833 def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int: 834 """Get the ticket award value for this achievement.""" 835 val: int = _internal.get_v1_account_misc_read_val( 836 'achAward.' + self._name, self._award 837 ) * _get_ach_mult(include_pro_bonus) 838 assert isinstance(val, int) 839 return val
Get the ticket award value for this achievement.
850 def create_display( 851 self, 852 x: float, 853 y: float, 854 delay: float, 855 outdelay: float | None = None, 856 color: Sequence[float] | None = None, 857 style: str = 'post_game', 858 ) -> list[ba.Actor]: 859 """Create a display for the Achievement. 860 861 Shows the Achievement icon, name, and description. 862 """ 863 # pylint: disable=cyclic-import 864 from ba._language import Lstr 865 from ba._generated.enums import SpecialChar 866 from ba._coopsession import CoopSession 867 from bastd.actor.image import Image 868 from bastd.actor.text import Text 869 870 # Yeah this needs cleaning up. 871 if style == 'post_game': 872 in_game_colors = False 873 in_main_menu = False 874 h_attach = Text.HAttach.CENTER 875 v_attach = Text.VAttach.CENTER 876 attach = Image.Attach.CENTER 877 elif style == 'in_game': 878 in_game_colors = True 879 in_main_menu = False 880 h_attach = Text.HAttach.LEFT 881 v_attach = Text.VAttach.TOP 882 attach = Image.Attach.TOP_LEFT 883 elif style == 'news': 884 in_game_colors = True 885 in_main_menu = True 886 h_attach = Text.HAttach.CENTER 887 v_attach = Text.VAttach.TOP 888 attach = Image.Attach.TOP_CENTER 889 else: 890 raise ValueError('invalid style "' + style + '"') 891 892 # Attempt to determine what campaign we're in 893 # (so we know whether to show "hard mode only"). 894 if in_main_menu: 895 hmo = False 896 else: 897 try: 898 session = _ba.getsession() 899 if isinstance(session, CoopSession): 900 campaign = session.campaign 901 assert campaign is not None 902 hmo = self._hard_mode_only and campaign.name == 'Easy' 903 else: 904 hmo = False 905 except Exception: 906 print_exception('Error determining campaign.') 907 hmo = False 908 909 objs: list[ba.Actor] 910 911 if in_game_colors: 912 objs = [] 913 out_delay_fin = (delay + outdelay) if outdelay is not None else None 914 if color is not None: 915 cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2], color[3]) 916 cl2 = color 917 else: 918 cl1 = (1.5, 1.5, 2, 1.0) 919 cl2 = (0.8, 0.8, 1.0, 1.0) 920 921 if hmo: 922 cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6) 923 cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2) 924 925 objs.append( 926 Image( 927 self.get_icon_texture(False), 928 host_only=True, 929 color=cl1, 930 position=(x - 25, y + 5), 931 attach=attach, 932 transition=Image.Transition.FADE_IN, 933 transition_delay=delay, 934 vr_depth=4, 935 transition_out_delay=out_delay_fin, 936 scale=(40, 40), 937 ).autoretain() 938 ) 939 txt = self.display_name 940 txt_s = 0.85 941 txt_max_w = 300 942 objs.append( 943 Text( 944 txt, 945 host_only=True, 946 maxwidth=txt_max_w, 947 position=(x, y + 2), 948 transition=Text.Transition.FADE_IN, 949 scale=txt_s, 950 flatness=0.6, 951 shadow=0.5, 952 h_attach=h_attach, 953 v_attach=v_attach, 954 color=cl2, 955 transition_delay=delay + 0.05, 956 transition_out_delay=out_delay_fin, 957 ).autoretain() 958 ) 959 txt2_s = 0.62 960 txt2_max_w = 400 961 objs.append( 962 Text( 963 self.description_full if in_main_menu else self.description, 964 host_only=True, 965 maxwidth=txt2_max_w, 966 position=(x, y - 14), 967 transition=Text.Transition.FADE_IN, 968 vr_depth=-5, 969 h_attach=h_attach, 970 v_attach=v_attach, 971 scale=txt2_s, 972 flatness=1.0, 973 shadow=0.5, 974 color=cl2, 975 transition_delay=delay + 0.1, 976 transition_out_delay=out_delay_fin, 977 ).autoretain() 978 ) 979 980 if hmo: 981 txtactor = Text( 982 Lstr(resource='difficultyHardOnlyText'), 983 host_only=True, 984 maxwidth=txt2_max_w * 0.7, 985 position=(x + 60, y + 5), 986 transition=Text.Transition.FADE_IN, 987 vr_depth=-5, 988 h_attach=h_attach, 989 v_attach=v_attach, 990 h_align=Text.HAlign.CENTER, 991 v_align=Text.VAlign.CENTER, 992 scale=txt_s * 0.8, 993 flatness=1.0, 994 shadow=0.5, 995 color=(1, 1, 0.6, 1), 996 transition_delay=delay + 0.1, 997 transition_out_delay=out_delay_fin, 998 ).autoretain() 999 txtactor.node.rotate = 10 1000 objs.append(txtactor) 1001 1002 # Ticket-award. 1003 award_x = -100 1004 objs.append( 1005 Text( 1006 _ba.charstr(SpecialChar.TICKET), 1007 host_only=True, 1008 position=(x + award_x + 33, y + 7), 1009 transition=Text.Transition.FADE_IN, 1010 scale=1.5, 1011 h_attach=h_attach, 1012 v_attach=v_attach, 1013 h_align=Text.HAlign.CENTER, 1014 v_align=Text.VAlign.CENTER, 1015 color=(1, 1, 1, 0.2 if hmo else 0.4), 1016 transition_delay=delay + 0.05, 1017 transition_out_delay=out_delay_fin, 1018 ).autoretain() 1019 ) 1020 objs.append( 1021 Text( 1022 '+' + str(self.get_award_ticket_value()), 1023 host_only=True, 1024 position=(x + award_x + 28, y + 16), 1025 transition=Text.Transition.FADE_IN, 1026 scale=0.7, 1027 flatness=1, 1028 h_attach=h_attach, 1029 v_attach=v_attach, 1030 h_align=Text.HAlign.CENTER, 1031 v_align=Text.VAlign.CENTER, 1032 color=cl2, 1033 transition_delay=delay + 0.05, 1034 transition_out_delay=out_delay_fin, 1035 ).autoretain() 1036 ) 1037 1038 else: 1039 complete = self.complete 1040 objs = [] 1041 c_icon = self.get_icon_color(complete) 1042 if hmo and not complete: 1043 c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3) 1044 objs.append( 1045 Image( 1046 self.get_icon_texture(complete), 1047 host_only=True, 1048 color=c_icon, 1049 position=(x - 25, y + 5), 1050 attach=attach, 1051 vr_depth=4, 1052 transition=Image.Transition.IN_RIGHT, 1053 transition_delay=delay, 1054 transition_out_delay=None, 1055 scale=(40, 40), 1056 ).autoretain() 1057 ) 1058 if complete: 1059 objs.append( 1060 Image( 1061 _ba.gettexture('achievementOutline'), 1062 host_only=True, 1063 model_transparent=_ba.getmodel('achievementOutline'), 1064 color=(2, 1.4, 0.4, 1), 1065 vr_depth=8, 1066 position=(x - 25, y + 5), 1067 attach=attach, 1068 transition=Image.Transition.IN_RIGHT, 1069 transition_delay=delay, 1070 transition_out_delay=None, 1071 scale=(40, 40), 1072 ).autoretain() 1073 ) 1074 else: 1075 if not complete: 1076 award_x = -100 1077 objs.append( 1078 Text( 1079 _ba.charstr(SpecialChar.TICKET), 1080 host_only=True, 1081 position=(x + award_x + 33, y + 7), 1082 transition=Text.Transition.IN_RIGHT, 1083 scale=1.5, 1084 h_attach=h_attach, 1085 v_attach=v_attach, 1086 h_align=Text.HAlign.CENTER, 1087 v_align=Text.VAlign.CENTER, 1088 color=(1, 1, 1, 0.4) 1089 if complete 1090 else (1, 1, 1, (0.1 if hmo else 0.2)), 1091 transition_delay=delay + 0.05, 1092 transition_out_delay=None, 1093 ).autoretain() 1094 ) 1095 objs.append( 1096 Text( 1097 '+' + str(self.get_award_ticket_value()), 1098 host_only=True, 1099 position=(x + award_x + 28, y + 16), 1100 transition=Text.Transition.IN_RIGHT, 1101 scale=0.7, 1102 flatness=1, 1103 h_attach=h_attach, 1104 v_attach=v_attach, 1105 h_align=Text.HAlign.CENTER, 1106 v_align=Text.VAlign.CENTER, 1107 color=( 1108 (0.8, 0.93, 0.8, 1.0) 1109 if complete 1110 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1111 ), 1112 transition_delay=delay + 0.05, 1113 transition_out_delay=None, 1114 ).autoretain() 1115 ) 1116 1117 # Show 'hard-mode-only' only over incomplete achievements 1118 # when that's the case. 1119 if hmo: 1120 txtactor = Text( 1121 Lstr(resource='difficultyHardOnlyText'), 1122 host_only=True, 1123 maxwidth=300 * 0.7, 1124 position=(x + 60, y + 5), 1125 transition=Text.Transition.FADE_IN, 1126 vr_depth=-5, 1127 h_attach=h_attach, 1128 v_attach=v_attach, 1129 h_align=Text.HAlign.CENTER, 1130 v_align=Text.VAlign.CENTER, 1131 scale=0.85 * 0.8, 1132 flatness=1.0, 1133 shadow=0.5, 1134 color=(1, 1, 0.6, 1), 1135 transition_delay=delay + 0.05, 1136 transition_out_delay=None, 1137 ).autoretain() 1138 assert txtactor.node 1139 txtactor.node.rotate = 10 1140 objs.append(txtactor) 1141 1142 objs.append( 1143 Text( 1144 self.display_name, 1145 host_only=True, 1146 maxwidth=300, 1147 position=(x, y + 2), 1148 transition=Text.Transition.IN_RIGHT, 1149 scale=0.85, 1150 flatness=0.6, 1151 h_attach=h_attach, 1152 v_attach=v_attach, 1153 color=( 1154 (0.8, 0.93, 0.8, 1.0) 1155 if complete 1156 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1157 ), 1158 transition_delay=delay + 0.05, 1159 transition_out_delay=None, 1160 ).autoretain() 1161 ) 1162 objs.append( 1163 Text( 1164 self.description_complete if complete else self.description, 1165 host_only=True, 1166 maxwidth=400, 1167 position=(x, y - 14), 1168 transition=Text.Transition.IN_RIGHT, 1169 vr_depth=-5, 1170 h_attach=h_attach, 1171 v_attach=v_attach, 1172 scale=0.62, 1173 flatness=1.0, 1174 color=( 1175 (0.6, 0.6, 0.6, 1.0) 1176 if complete 1177 else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4)) 1178 ), 1179 transition_delay=delay + 0.1, 1180 transition_out_delay=None, 1181 ).autoretain() 1182 ) 1183 return objs
Create a display for the Achievement.
Shows the Achievement icon, name, and description.
67class AchievementSubsystem: 68 """Subsystem for achievement handling. 69 70 Category: **App Classes** 71 72 Access the single shared instance of this class at 'ba.app.ach'. 73 """ 74 75 def __init__(self) -> None: 76 self.achievements: list[Achievement] = [] 77 self.achievements_to_display: (list[tuple[ba.Achievement, bool]]) = [] 78 self.achievement_display_timer: _ba.Timer | None = None 79 self.last_achievement_display_time: float = 0.0 80 self.achievement_completion_banner_slots: set[int] = set() 81 self._init_achievements() 82 83 def _init_achievements(self) -> None: 84 """Fill in available achievements.""" 85 86 achs = self.achievements 87 88 # 5 89 achs.append( 90 Achievement('In Control', 'achievementInControl', (1, 1, 1), '', 5) 91 ) 92 # 15 93 achs.append( 94 Achievement( 95 'Sharing is Caring', 96 'achievementSharingIsCaring', 97 (1, 1, 1), 98 '', 99 15, 100 ) 101 ) 102 # 10 103 achs.append( 104 Achievement( 105 'Dual Wielding', 'achievementDualWielding', (1, 1, 1), '', 10 106 ) 107 ) 108 109 # 10 110 achs.append( 111 Achievement( 112 'Free Loader', 'achievementFreeLoader', (1, 1, 1), '', 10 113 ) 114 ) 115 # 20 116 achs.append( 117 Achievement( 118 'Team Player', 'achievementTeamPlayer', (1, 1, 1), '', 20 119 ) 120 ) 121 122 # 5 123 achs.append( 124 Achievement( 125 'Onslaught Training Victory', 126 'achievementOnslaught', 127 (1, 1, 1), 128 'Default:Onslaught Training', 129 5, 130 ) 131 ) 132 # 5 133 achs.append( 134 Achievement( 135 'Off You Go Then', 136 'achievementOffYouGo', 137 (1, 1.1, 1.3), 138 'Default:Onslaught Training', 139 5, 140 ) 141 ) 142 # 10 143 achs.append( 144 Achievement( 145 'Boxer', 146 'achievementBoxer', 147 (1, 0.6, 0.6), 148 'Default:Onslaught Training', 149 10, 150 hard_mode_only=True, 151 ) 152 ) 153 154 # 10 155 achs.append( 156 Achievement( 157 'Rookie Onslaught Victory', 158 'achievementOnslaught', 159 (0.5, 1.4, 0.6), 160 'Default:Rookie Onslaught', 161 10, 162 ) 163 ) 164 # 10 165 achs.append( 166 Achievement( 167 'Mine Games', 168 'achievementMine', 169 (1, 1, 1.4), 170 'Default:Rookie Onslaught', 171 10, 172 ) 173 ) 174 # 15 175 achs.append( 176 Achievement( 177 'Flawless Victory', 178 'achievementFlawlessVictory', 179 (1, 1, 1), 180 'Default:Rookie Onslaught', 181 15, 182 hard_mode_only=True, 183 ) 184 ) 185 186 # 10 187 achs.append( 188 Achievement( 189 'Rookie Football Victory', 190 'achievementFootballVictory', 191 (1.0, 1, 0.6), 192 'Default:Rookie Football', 193 10, 194 ) 195 ) 196 # 10 197 achs.append( 198 Achievement( 199 'Super Punch', 200 'achievementSuperPunch', 201 (1, 1, 1.8), 202 'Default:Rookie Football', 203 10, 204 ) 205 ) 206 # 15 207 achs.append( 208 Achievement( 209 'Rookie Football Shutout', 210 'achievementFootballShutout', 211 (1, 1, 1), 212 'Default:Rookie Football', 213 15, 214 hard_mode_only=True, 215 ) 216 ) 217 218 # 15 219 achs.append( 220 Achievement( 221 'Pro Onslaught Victory', 222 'achievementOnslaught', 223 (0.3, 1, 2.0), 224 'Default:Pro Onslaught', 225 15, 226 ) 227 ) 228 # 15 229 achs.append( 230 Achievement( 231 'Boom Goes the Dynamite', 232 'achievementTNT', 233 (1.4, 1.2, 0.8), 234 'Default:Pro Onslaught', 235 15, 236 ) 237 ) 238 # 20 239 achs.append( 240 Achievement( 241 'Pro Boxer', 242 'achievementBoxer', 243 (2, 2, 0), 244 'Default:Pro Onslaught', 245 20, 246 hard_mode_only=True, 247 ) 248 ) 249 250 # 15 251 achs.append( 252 Achievement( 253 'Pro Football Victory', 254 'achievementFootballVictory', 255 (1.3, 1.3, 2.0), 256 'Default:Pro Football', 257 15, 258 ) 259 ) 260 # 15 261 achs.append( 262 Achievement( 263 'Super Mega Punch', 264 'achievementSuperPunch', 265 (2, 1, 0.6), 266 'Default:Pro Football', 267 15, 268 ) 269 ) 270 # 20 271 achs.append( 272 Achievement( 273 'Pro Football Shutout', 274 'achievementFootballShutout', 275 (0.7, 0.7, 2.0), 276 'Default:Pro Football', 277 20, 278 hard_mode_only=True, 279 ) 280 ) 281 282 # 15 283 achs.append( 284 Achievement( 285 'Pro Runaround Victory', 286 'achievementRunaround', 287 (1, 1, 1), 288 'Default:Pro Runaround', 289 15, 290 ) 291 ) 292 # 20 293 achs.append( 294 Achievement( 295 'Precision Bombing', 296 'achievementCrossHair', 297 (1, 1, 1.3), 298 'Default:Pro Runaround', 299 20, 300 hard_mode_only=True, 301 ) 302 ) 303 # 25 304 achs.append( 305 Achievement( 306 'The Wall', 307 'achievementWall', 308 (1, 0.7, 0.7), 309 'Default:Pro Runaround', 310 25, 311 hard_mode_only=True, 312 ) 313 ) 314 315 # 30 316 achs.append( 317 Achievement( 318 'Uber Onslaught Victory', 319 'achievementOnslaught', 320 (2, 2, 1), 321 'Default:Uber Onslaught', 322 30, 323 ) 324 ) 325 # 30 326 achs.append( 327 Achievement( 328 'Gold Miner', 329 'achievementMine', 330 (2, 1.6, 0.2), 331 'Default:Uber Onslaught', 332 30, 333 hard_mode_only=True, 334 ) 335 ) 336 # 30 337 achs.append( 338 Achievement( 339 'TNT Terror', 340 'achievementTNT', 341 (2, 1.8, 0.3), 342 'Default:Uber Onslaught', 343 30, 344 hard_mode_only=True, 345 ) 346 ) 347 348 # 30 349 achs.append( 350 Achievement( 351 'Uber Football Victory', 352 'achievementFootballVictory', 353 (1.8, 1.4, 0.3), 354 'Default:Uber Football', 355 30, 356 ) 357 ) 358 # 30 359 achs.append( 360 Achievement( 361 'Got the Moves', 362 'achievementGotTheMoves', 363 (2, 1, 0), 364 'Default:Uber Football', 365 30, 366 hard_mode_only=True, 367 ) 368 ) 369 # 40 370 achs.append( 371 Achievement( 372 'Uber Football Shutout', 373 'achievementFootballShutout', 374 (2, 2, 0), 375 'Default:Uber Football', 376 40, 377 hard_mode_only=True, 378 ) 379 ) 380 381 # 30 382 achs.append( 383 Achievement( 384 'Uber Runaround Victory', 385 'achievementRunaround', 386 (1.5, 1.2, 0.2), 387 'Default:Uber Runaround', 388 30, 389 ) 390 ) 391 # 40 392 achs.append( 393 Achievement( 394 'The Great Wall', 395 'achievementWall', 396 (2, 1.7, 0.4), 397 'Default:Uber Runaround', 398 40, 399 hard_mode_only=True, 400 ) 401 ) 402 # 40 403 achs.append( 404 Achievement( 405 'Stayin\' Alive', 406 'achievementStayinAlive', 407 (2, 2, 1), 408 'Default:Uber Runaround', 409 40, 410 hard_mode_only=True, 411 ) 412 ) 413 414 # 20 415 achs.append( 416 Achievement( 417 'Last Stand Master', 418 'achievementMedalSmall', 419 (2, 1.5, 0.3), 420 'Default:The Last Stand', 421 20, 422 hard_mode_only=True, 423 ) 424 ) 425 # 40 426 achs.append( 427 Achievement( 428 'Last Stand Wizard', 429 'achievementMedalMedium', 430 (2, 1.5, 0.3), 431 'Default:The Last Stand', 432 40, 433 hard_mode_only=True, 434 ) 435 ) 436 # 60 437 achs.append( 438 Achievement( 439 'Last Stand God', 440 'achievementMedalLarge', 441 (2, 1.5, 0.3), 442 'Default:The Last Stand', 443 60, 444 hard_mode_only=True, 445 ) 446 ) 447 448 # 5 449 achs.append( 450 Achievement( 451 'Onslaught Master', 452 'achievementMedalSmall', 453 (0.7, 1, 0.7), 454 'Challenges:Infinite Onslaught', 455 5, 456 ) 457 ) 458 # 15 459 achs.append( 460 Achievement( 461 'Onslaught Wizard', 462 'achievementMedalMedium', 463 (0.7, 1.0, 0.7), 464 'Challenges:Infinite Onslaught', 465 15, 466 ) 467 ) 468 # 30 469 achs.append( 470 Achievement( 471 'Onslaught God', 472 'achievementMedalLarge', 473 (0.7, 1.0, 0.7), 474 'Challenges:Infinite Onslaught', 475 30, 476 ) 477 ) 478 479 # 5 480 achs.append( 481 Achievement( 482 'Runaround Master', 483 'achievementMedalSmall', 484 (1.0, 1.0, 1.2), 485 'Challenges:Infinite Runaround', 486 5, 487 ) 488 ) 489 # 15 490 achs.append( 491 Achievement( 492 'Runaround Wizard', 493 'achievementMedalMedium', 494 (1.0, 1.0, 1.2), 495 'Challenges:Infinite Runaround', 496 15, 497 ) 498 ) 499 # 30 500 achs.append( 501 Achievement( 502 'Runaround God', 503 'achievementMedalLarge', 504 (1.0, 1.0, 1.2), 505 'Challenges:Infinite Runaround', 506 30, 507 ) 508 ) 509 510 def award_local_achievement(self, achname: str) -> None: 511 """For non-game-based achievements such as controller-connection.""" 512 try: 513 ach = self.get_achievement(achname) 514 if not ach.complete: 515 516 # Report new achievements to the game-service. 517 _internal.report_achievement(achname) 518 519 # And to our account. 520 _internal.add_transaction( 521 {'type': 'ACHIEVEMENT', 'name': achname} 522 ) 523 524 # Now attempt to show a banner. 525 self.display_achievement_banner(achname) 526 527 except Exception: 528 print_exception() 529 530 def display_achievement_banner(self, achname: str) -> None: 531 """Display a completion banner for an achievement. 532 533 (internal) 534 535 Used for server-driven achievements. 536 """ 537 try: 538 # FIXME: Need to get these using the UI context or some other 539 # purely local context somehow instead of trying to inject these 540 # into whatever activity happens to be active 541 # (since that won't work while in client mode). 542 activity = _ba.get_foreground_host_activity() 543 if activity is not None: 544 with _ba.Context(activity): 545 self.get_achievement(achname).announce_completion() 546 except Exception: 547 print_exception('error showing server ach') 548 549 def set_completed_achievements(self, achs: Sequence[str]) -> None: 550 """Set the current state of completed achievements. 551 552 (internal) 553 554 All achievements not included here will be set incomplete. 555 """ 556 557 # Note: This gets called whenever game-center/game-circle/etc tells 558 # us which achievements we currently have. We always defer to them, 559 # even if that means we have to un-set an achievement we think we have. 560 561 cfg = _ba.app.config 562 cfg['Achievements'] = {} 563 for a_name in achs: 564 self.get_achievement(a_name).set_complete(True) 565 cfg.commit() 566 567 def get_achievement(self, name: str) -> Achievement: 568 """Return an Achievement by name.""" 569 achs = [a for a in self.achievements if a.name == name] 570 assert len(achs) < 2 571 if not achs: 572 raise ValueError("Invalid achievement name: '" + name + "'") 573 return achs[0] 574 575 def achievements_for_coop_level(self, level_name: str) -> list[Achievement]: 576 """Given a level name, return achievements available for it.""" 577 578 # For the Easy campaign we return achievements for the Default 579 # campaign too. (want the user to see what achievements are part of the 580 # level even if they can't unlock them all on easy mode). 581 return [ 582 a 583 for a in self.achievements 584 if a.level_name 585 in (level_name, level_name.replace('Easy', 'Default')) 586 ] 587 588 def _test(self) -> None: 589 """For testing achievement animations.""" 590 from ba._generated.enums import TimeType 591 592 def testcall1() -> None: 593 self.achievements[0].announce_completion() 594 self.achievements[1].announce_completion() 595 self.achievements[2].announce_completion() 596 597 def testcall2() -> None: 598 self.achievements[3].announce_completion() 599 self.achievements[4].announce_completion() 600 self.achievements[5].announce_completion() 601 602 _ba.timer(3.0, testcall1, timetype=TimeType.BASE) 603 _ba.timer(7.0, testcall2, timetype=TimeType.BASE)
Subsystem for achievement handling.
Category: App Classes
Access the single shared instance of this class at 'ba.app.ach'.
75 def __init__(self) -> None: 76 self.achievements: list[Achievement] = [] 77 self.achievements_to_display: (list[tuple[ba.Achievement, bool]]) = [] 78 self.achievement_display_timer: _ba.Timer | None = None 79 self.last_achievement_display_time: float = 0.0 80 self.achievement_completion_banner_slots: set[int] = set() 81 self._init_achievements()
510 def award_local_achievement(self, achname: str) -> None: 511 """For non-game-based achievements such as controller-connection.""" 512 try: 513 ach = self.get_achievement(achname) 514 if not ach.complete: 515 516 # Report new achievements to the game-service. 517 _internal.report_achievement(achname) 518 519 # And to our account. 520 _internal.add_transaction( 521 {'type': 'ACHIEVEMENT', 'name': achname} 522 ) 523 524 # Now attempt to show a banner. 525 self.display_achievement_banner(achname) 526 527 except Exception: 528 print_exception()
For non-game-based achievements such as controller-connection.
567 def get_achievement(self, name: str) -> Achievement: 568 """Return an Achievement by name.""" 569 achs = [a for a in self.achievements if a.name == name] 570 assert len(achs) < 2 571 if not achs: 572 raise ValueError("Invalid achievement name: '" + name + "'") 573 return achs[0]
Return an Achievement by name.
575 def achievements_for_coop_level(self, level_name: str) -> list[Achievement]: 576 """Given a level name, return achievements available for it.""" 577 578 # For the Easy campaign we return achievements for the Default 579 # campaign too. (want the user to see what achievements are part of the 580 # level even if they can't unlock them all on easy mode). 581 return [ 582 a 583 for a in self.achievements 584 if a.level_name 585 in (level_name, level_name.replace('Easy', 'Default')) 586 ]
Given a level name, return achievements available for it.
33class Activity(DependencyComponent, Generic[PlayerType, TeamType]): 34 """Units of execution wrangled by a ba.Session. 35 36 Category: Gameplay Classes 37 38 Examples of Activities include games, score-screens, cutscenes, etc. 39 A ba.Session has one 'current' Activity at any time, though their existence 40 can overlap during transitions. 41 """ 42 43 # pylint: disable=too-many-public-methods 44 45 settings_raw: dict[str, Any] 46 """The settings dict passed in when the activity was made. 47 This attribute is deprecated and should be avoided when possible; 48 activities should pull all values they need from the 'settings' arg 49 passed to the Activity __init__ call.""" 50 51 teams: list[TeamType] 52 """The list of ba.Team-s in the Activity. This gets populated just 53 before on_begin() is called and is updated automatically as players 54 join or leave the game. (at least in free-for-all mode where every 55 player gets their own team; in teams mode there are always 2 teams 56 regardless of the player count).""" 57 58 players: list[PlayerType] 59 """The list of ba.Player-s in the Activity. This gets populated just 60 before on_begin() is called and is updated automatically as players 61 join or leave the game.""" 62 63 announce_player_deaths = False 64 """Whether to print every time a player dies. This can be pertinent 65 in games such as Death-Match but can be annoying in games where it 66 doesn't matter.""" 67 68 is_joining_activity = False 69 """Joining activities are for waiting for initial player joins. 70 They are treated slightly differently than regular activities, 71 mainly in that all players are passed to the activity at once 72 instead of as each joins.""" 73 74 allow_pausing = False 75 """Whether game-time should still progress when in menus/etc.""" 76 77 allow_kick_idle_players = True 78 """Whether idle players can potentially be kicked (should not happen in 79 menus/etc).""" 80 81 use_fixed_vr_overlay = False 82 """In vr mode, this determines whether overlay nodes (text, images, etc) 83 are created at a fixed position in space or one that moves based on 84 the current map. Generally this should be on for games and off for 85 transitions/score-screens/etc. that persist between maps.""" 86 87 slow_motion = False 88 """If True, runs in slow motion and turns down sound pitch.""" 89 90 inherits_slow_motion = False 91 """Set this to True to inherit slow motion setting from previous 92 activity (useful for transitions to avoid hitches).""" 93 94 inherits_music = False 95 """Set this to True to keep playing the music from the previous activity 96 (without even restarting it).""" 97 98 inherits_vr_camera_offset = False 99 """Set this to true to inherit VR camera offsets from the previous 100 activity (useful for preventing sporadic camera movement 101 during transitions).""" 102 103 inherits_vr_overlay_center = False 104 """Set this to true to inherit (non-fixed) VR overlay positioning from 105 the previous activity (useful for prevent sporadic overlay jostling 106 during transitions).""" 107 108 inherits_tint = False 109 """Set this to true to inherit screen tint/vignette colors from the 110 previous activity (useful to prevent sudden color changes during 111 transitions).""" 112 113 allow_mid_activity_joins: bool = True 114 """Whether players should be allowed to join in the middle of this 115 activity. Note that Sessions may not allow mid-activity-joins even 116 if the activity says its ok.""" 117 118 transition_time = 0.0 119 """If the activity fades or transitions in, it should set the length of 120 time here so that previous activities will be kept alive for that 121 long (avoiding 'holes' in the screen) 122 This value is given in real-time seconds.""" 123 124 can_show_ad_on_death = False 125 """Is it ok to show an ad after this activity ends before showing 126 the next activity?""" 127 128 def __init__(self, settings: dict): 129 """Creates an Activity in the current ba.Session. 130 131 The activity will not be actually run until ba.Session.setactivity 132 is called. 'settings' should be a dict of key/value pairs specific 133 to the activity. 134 135 Activities should preload as much of their media/etc as possible in 136 their constructor, but none of it should actually be used until they 137 are transitioned in. 138 """ 139 super().__init__() 140 141 # Create our internal engine data. 142 self._activity_data = _ba.register_activity(self) 143 144 assert isinstance(settings, dict) 145 assert _ba.getactivity() is self 146 147 self._globalsnode: ba.Node | None = None 148 149 # Player/Team types should have been specified as type args; 150 # grab those. 151 self._playertype: type[PlayerType] 152 self._teamtype: type[TeamType] 153 self._setup_player_and_team_types() 154 155 # FIXME: Relocate or remove the need for this stuff. 156 self.paused_text: ba.Actor | None = None 157 158 self._session = weakref.ref(_ba.getsession()) 159 160 # Preloaded data for actors, maps, etc; indexed by type. 161 self.preloads: dict[type, Any] = {} 162 163 # Hopefully can eventually kill this; activities should 164 # validate/store whatever settings they need at init time 165 # (in a more type-safe way). 166 self.settings_raw = settings 167 168 self._has_transitioned_in = False 169 self._has_begun = False 170 self._has_ended = False 171 self._activity_death_check_timer: ba.Timer | None = None 172 self._expired = False 173 self._delay_delete_players: list[PlayerType] = [] 174 self._delay_delete_teams: list[TeamType] = [] 175 self._players_that_left: list[weakref.ref[PlayerType]] = [] 176 self._teams_that_left: list[weakref.ref[TeamType]] = [] 177 self._transitioning_out = False 178 179 # A handy place to put most actors; this list is pruned of dead 180 # actors regularly and these actors are insta-killed as the activity 181 # is dying. 182 self._actor_refs: list[ba.Actor] = [] 183 self._actor_weak_refs: list[weakref.ref[ba.Actor]] = [] 184 self._last_prune_dead_actors_time = _ba.time() 185 self._prune_dead_actors_timer: ba.Timer | None = None 186 187 self.teams = [] 188 self.players = [] 189 190 self.lobby = None 191 self._stats: ba.Stats | None = None 192 self._customdata: dict | None = {} 193 194 def __del__(self) -> None: 195 196 # If the activity has been run then we should have already cleaned 197 # it up, but we still need to run expire calls for un-run activities. 198 if not self._expired: 199 with _ba.Context('empty'): 200 self._expire() 201 202 # Inform our owner that we officially kicked the bucket. 203 if self._transitioning_out: 204 session = self._session() 205 if session is not None: 206 _ba.pushcall( 207 Call( 208 session.transitioning_out_activity_was_freed, 209 self.can_show_ad_on_death, 210 ) 211 ) 212 213 @property 214 def globalsnode(self) -> ba.Node: 215 """The 'globals' ba.Node for the activity. This contains various 216 global controls and values. 217 """ 218 node = self._globalsnode 219 if not node: 220 raise NodeNotFoundError() 221 return node 222 223 @property 224 def stats(self) -> ba.Stats: 225 """The stats instance accessible while the activity is running. 226 227 If access is attempted before or after, raises a ba.NotFoundError. 228 """ 229 if self._stats is None: 230 from ba._error import NotFoundError 231 232 raise NotFoundError() 233 return self._stats 234 235 def on_expire(self) -> None: 236 """Called when your activity is being expired. 237 238 If your activity has created anything explicitly that may be retaining 239 a strong reference to the activity and preventing it from dying, you 240 should clear that out here. From this point on your activity's sole 241 purpose in life is to hit zero references and die so the next activity 242 can begin. 243 """ 244 245 @property 246 def customdata(self) -> dict: 247 """Entities needing to store simple data with an activity can put it 248 here. This dict will be deleted when the activity expires, so contained 249 objects generally do not need to worry about handling expired 250 activities. 251 """ 252 assert not self._expired 253 assert isinstance(self._customdata, dict) 254 return self._customdata 255 256 @property 257 def expired(self) -> bool: 258 """Whether the activity is expired. 259 260 An activity is set as expired when shutting down. 261 At this point no new nodes, timers, etc should be made, 262 run, etc, and the activity should be considered a 'zombie'. 263 """ 264 return self._expired 265 266 @property 267 def playertype(self) -> type[PlayerType]: 268 """The type of ba.Player this Activity is using.""" 269 return self._playertype 270 271 @property 272 def teamtype(self) -> type[TeamType]: 273 """The type of ba.Team this Activity is using.""" 274 return self._teamtype 275 276 def set_has_ended(self, val: bool) -> None: 277 """(internal)""" 278 self._has_ended = val 279 280 def expire(self) -> None: 281 """Begin the process of tearing down the activity. 282 283 (internal) 284 """ 285 from ba._generated.enums import TimeType 286 287 # Create a real-timer that watches a weak-ref of this activity 288 # and reports any lingering references keeping it alive. 289 # We store the timer on the activity so as soon as the activity dies 290 # it gets cleaned up. 291 with _ba.Context('ui'): 292 ref = weakref.ref(self) 293 self._activity_death_check_timer = _ba.Timer( 294 5.0, 295 Call(self._check_activity_death, ref, [0]), 296 repeat=True, 297 timetype=TimeType.REAL, 298 ) 299 300 # Run _expire in an empty context; nothing should be happening in 301 # there except deleting things which requires no context. 302 # (plus, _expire() runs in the destructor for un-run activities 303 # and we can't properly provide context in that situation anyway; might 304 # as well be consistent). 305 if not self._expired: 306 with _ba.Context('empty'): 307 self._expire() 308 else: 309 raise RuntimeError( 310 f'destroy() called when' f' already expired for {self}' 311 ) 312 313 def retain_actor(self, actor: ba.Actor) -> None: 314 """Add a strong-reference to a ba.Actor to this Activity. 315 316 The reference will be lazily released once ba.Actor.exists() 317 returns False for the Actor. The ba.Actor.autoretain() method 318 is a convenient way to access this same functionality. 319 """ 320 if __debug__: 321 from ba._actor import Actor 322 323 assert isinstance(actor, Actor) 324 self._actor_refs.append(actor) 325 326 def add_actor_weak_ref(self, actor: ba.Actor) -> None: 327 """Add a weak-reference to a ba.Actor to the ba.Activity. 328 329 (called by the ba.Actor base class) 330 """ 331 if __debug__: 332 from ba._actor import Actor 333 334 assert isinstance(actor, Actor) 335 self._actor_weak_refs.append(weakref.ref(actor)) 336 337 @property 338 def session(self) -> ba.Session: 339 """The ba.Session this ba.Activity belongs go. 340 341 Raises a ba.SessionNotFoundError if the Session no longer exists. 342 """ 343 session = self._session() 344 if session is None: 345 from ba._error import SessionNotFoundError 346 347 raise SessionNotFoundError() 348 return session 349 350 def on_player_join(self, player: PlayerType) -> None: 351 """Called when a new ba.Player has joined the Activity. 352 353 (including the initial set of Players) 354 """ 355 356 def on_player_leave(self, player: PlayerType) -> None: 357 """Called when a ba.Player is leaving the Activity.""" 358 359 def on_team_join(self, team: TeamType) -> None: 360 """Called when a new ba.Team joins the Activity. 361 362 (including the initial set of Teams) 363 """ 364 365 def on_team_leave(self, team: TeamType) -> None: 366 """Called when a ba.Team leaves the Activity.""" 367 368 def on_transition_in(self) -> None: 369 """Called when the Activity is first becoming visible. 370 371 Upon this call, the Activity should fade in backgrounds, 372 start playing music, etc. It does not yet have access to players 373 or teams, however. They remain owned by the previous Activity 374 up until ba.Activity.on_begin() is called. 375 """ 376 377 def on_transition_out(self) -> None: 378 """Called when your activity begins transitioning out. 379 380 Note that this may happen at any time even if ba.Activity.end() has 381 not been called. 382 """ 383 384 def on_begin(self) -> None: 385 """Called once the previous ba.Activity has finished transitioning out. 386 387 At this point the activity's initial players and teams are filled in 388 and it should begin its actual game logic. 389 """ 390 391 def handlemessage(self, msg: Any) -> Any: 392 """General message handling; can be passed any message object.""" 393 del msg # Unused arg. 394 return UNHANDLED 395 396 def has_transitioned_in(self) -> bool: 397 """Return whether ba.Activity.on_transition_in() 398 has been called.""" 399 return self._has_transitioned_in 400 401 def has_begun(self) -> bool: 402 """Return whether ba.Activity.on_begin() has been called.""" 403 return self._has_begun 404 405 def has_ended(self) -> bool: 406 """Return whether the activity has commenced ending.""" 407 return self._has_ended 408 409 def is_transitioning_out(self) -> bool: 410 """Return whether ba.Activity.on_transition_out() has been called.""" 411 return self._transitioning_out 412 413 def transition_in(self, prev_globals: ba.Node | None) -> None: 414 """Called by Session to kick off transition-in. 415 416 (internal) 417 """ 418 assert not self._has_transitioned_in 419 self._has_transitioned_in = True 420 421 # Set up the globals node based on our settings. 422 with _ba.Context(self): 423 glb = self._globalsnode = _ba.newnode('globals') 424 425 # Now that it's going to be front and center, 426 # set some global values based on what the activity wants. 427 glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay 428 glb.allow_kick_idle_players = self.allow_kick_idle_players 429 if self.inherits_slow_motion and prev_globals is not None: 430 glb.slow_motion = prev_globals.slow_motion 431 else: 432 glb.slow_motion = self.slow_motion 433 if self.inherits_music and prev_globals is not None: 434 glb.music_continuous = True # Prevent restarting same music. 435 glb.music = prev_globals.music 436 glb.music_count += 1 437 if self.inherits_vr_camera_offset and prev_globals is not None: 438 glb.vr_camera_offset = prev_globals.vr_camera_offset 439 if self.inherits_vr_overlay_center and prev_globals is not None: 440 glb.vr_overlay_center = prev_globals.vr_overlay_center 441 glb.vr_overlay_center_enabled = ( 442 prev_globals.vr_overlay_center_enabled 443 ) 444 445 # If they want to inherit tint from the previous self. 446 if self.inherits_tint and prev_globals is not None: 447 glb.tint = prev_globals.tint 448 glb.vignette_outer = prev_globals.vignette_outer 449 glb.vignette_inner = prev_globals.vignette_inner 450 451 # Start pruning our various things periodically. 452 self._prune_dead_actors() 453 self._prune_dead_actors_timer = _ba.Timer( 454 5.17, self._prune_dead_actors, repeat=True 455 ) 456 457 _ba.timer(13.3, self._prune_delay_deletes, repeat=True) 458 459 # Also start our low-level scene running. 460 self._activity_data.start() 461 462 try: 463 self.on_transition_in() 464 except Exception: 465 print_exception(f'Error in on_transition_in for {self}.') 466 467 # Tell the C++ layer that this activity is the main one, so it uses 468 # settings from our globals, directs various events to us, etc. 469 self._activity_data.make_foreground() 470 471 def transition_out(self) -> None: 472 """Called by the Session to start us transitioning out.""" 473 assert not self._transitioning_out 474 self._transitioning_out = True 475 with _ba.Context(self): 476 try: 477 self.on_transition_out() 478 except Exception: 479 print_exception(f'Error in on_transition_out for {self}.') 480 481 def begin(self, session: ba.Session) -> None: 482 """Begin the activity. 483 484 (internal) 485 """ 486 487 assert not self._has_begun 488 489 # Inherit stats from the session. 490 self._stats = session.stats 491 492 # Add session's teams in. 493 for team in session.sessionteams: 494 self.add_team(team) 495 496 # Add session's players in. 497 for player in session.sessionplayers: 498 self.add_player(player) 499 500 self._has_begun = True 501 502 # Let the activity do its thing. 503 with _ba.Context(self): 504 # Note: do we want to catch errors here? 505 # Currently I believe we wind up canceling the 506 # activity launch; just wanna be sure that is intentional. 507 self.on_begin() 508 509 def end( 510 self, results: Any = None, delay: float = 0.0, force: bool = False 511 ) -> None: 512 """Commences Activity shutdown and delivers results to the ba.Session. 513 514 'delay' is the time delay before the Activity actually ends 515 (in seconds). Further calls to end() will be ignored up until 516 this time, unless 'force' is True, in which case the new results 517 will replace the old. 518 """ 519 520 # Ask the session to end us. 521 self.session.end_activity(self, results, delay, force) 522 523 def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType: 524 """Create the Player instance for this Activity. 525 526 Subclasses can override this if the activity's player class 527 requires a custom constructor; otherwise it will be called with 528 no args. Note that the player object should not be used at this 529 point as it is not yet fully wired up; wait for 530 ba.Activity.on_player_join() for that. 531 """ 532 del sessionplayer # Unused. 533 player = self._playertype() 534 return player 535 536 def create_team(self, sessionteam: ba.SessionTeam) -> TeamType: 537 """Create the Team instance for this Activity. 538 539 Subclasses can override this if the activity's team class 540 requires a custom constructor; otherwise it will be called with 541 no args. Note that the team object should not be used at this 542 point as it is not yet fully wired up; wait for on_team_join() 543 for that. 544 """ 545 del sessionteam # Unused. 546 team = self._teamtype() 547 return team 548 549 def add_player(self, sessionplayer: ba.SessionPlayer) -> None: 550 """(internal)""" 551 assert sessionplayer.sessionteam is not None 552 sessionplayer.resetinput() 553 sessionteam = sessionplayer.sessionteam 554 assert sessionplayer in sessionteam.players 555 team = sessionteam.activityteam 556 assert team is not None 557 sessionplayer.setactivity(self) 558 with _ba.Context(self): 559 sessionplayer.activityplayer = player = self.create_player( 560 sessionplayer 561 ) 562 player.postinit(sessionplayer) 563 564 assert player not in team.players 565 team.players.append(player) 566 assert player in team.players 567 568 assert player not in self.players 569 self.players.append(player) 570 assert player in self.players 571 572 try: 573 self.on_player_join(player) 574 except Exception: 575 print_exception(f'Error in on_player_join for {self}.') 576 577 def remove_player(self, sessionplayer: ba.SessionPlayer) -> None: 578 """Remove a player from the Activity while it is running. 579 580 (internal) 581 """ 582 assert not self.expired 583 584 player: Any = sessionplayer.activityplayer 585 assert isinstance(player, self._playertype) 586 team: Any = sessionplayer.sessionteam.activityteam 587 assert isinstance(team, self._teamtype) 588 589 assert player in team.players 590 team.players.remove(player) 591 assert player not in team.players 592 593 assert player in self.players 594 self.players.remove(player) 595 assert player not in self.players 596 597 # This should allow our ba.Player instance to die. 598 # Complain if that doesn't happen. 599 # verify_object_death(player) 600 601 with _ba.Context(self): 602 try: 603 self.on_player_leave(player) 604 except Exception: 605 print_exception(f'Error in on_player_leave for {self}.') 606 try: 607 player.leave() 608 except Exception: 609 print_exception(f'Error on leave for {player} in {self}.') 610 611 self._reset_session_player_for_no_activity(sessionplayer) 612 613 # Add the player to a list to keep it around for a while. This is 614 # to discourage logic from firing on player object death, which 615 # may not happen until activity end if something is holding refs 616 # to it. 617 self._delay_delete_players.append(player) 618 self._players_that_left.append(weakref.ref(player)) 619 620 def add_team(self, sessionteam: ba.SessionTeam) -> None: 621 """Add a team to the Activity 622 623 (internal) 624 """ 625 assert not self.expired 626 627 with _ba.Context(self): 628 sessionteam.activityteam = team = self.create_team(sessionteam) 629 team.postinit(sessionteam) 630 self.teams.append(team) 631 try: 632 self.on_team_join(team) 633 except Exception: 634 print_exception(f'Error in on_team_join for {self}.') 635 636 def remove_team(self, sessionteam: ba.SessionTeam) -> None: 637 """Remove a team from a Running Activity 638 639 (internal) 640 """ 641 assert not self.expired 642 assert sessionteam.activityteam is not None 643 644 team: Any = sessionteam.activityteam 645 assert isinstance(team, self._teamtype) 646 647 assert team in self.teams 648 self.teams.remove(team) 649 assert team not in self.teams 650 651 with _ba.Context(self): 652 # Make a decent attempt to persevere if user code breaks. 653 try: 654 self.on_team_leave(team) 655 except Exception: 656 print_exception(f'Error in on_team_leave for {self}.') 657 try: 658 team.leave() 659 except Exception: 660 print_exception(f'Error on leave for {team} in {self}.') 661 662 sessionteam.activityteam = None 663 664 # Add the team to a list to keep it around for a while. This is 665 # to discourage logic from firing on team object death, which 666 # may not happen until activity end if something is holding refs 667 # to it. 668 self._delay_delete_teams.append(team) 669 self._teams_that_left.append(weakref.ref(team)) 670 671 def _reset_session_player_for_no_activity( 672 self, sessionplayer: ba.SessionPlayer 673 ) -> None: 674 675 # Let's be extra-defensive here: killing a node/input-call/etc 676 # could trigger user-code resulting in errors, but we would still 677 # like to complete the reset if possible. 678 try: 679 sessionplayer.setnode(None) 680 except Exception: 681 print_exception( 682 f'Error resetting SessionPlayer node on {sessionplayer}' 683 f' for {self}.' 684 ) 685 try: 686 sessionplayer.resetinput() 687 except Exception: 688 print_exception( 689 f'Error resetting SessionPlayer input on {sessionplayer}' 690 f' for {self}.' 691 ) 692 693 # These should never fail I think... 694 sessionplayer.setactivity(None) 695 sessionplayer.activityplayer = None 696 697 # noinspection PyUnresolvedReferences 698 def _setup_player_and_team_types(self) -> None: 699 """Pull player and team types from our typing.Generic params.""" 700 701 # TODO: There are proper calls for pulling these in Python 3.8; 702 # should update this code when we adopt that. 703 # NOTE: If we get Any as PlayerType or TeamType (generally due 704 # to no generic params being passed) we automatically use the 705 # base class types, but also warn the user since this will mean 706 # less type safety for that class. (its better to pass the base 707 # player/team types explicitly vs. having them be Any) 708 if not TYPE_CHECKING: 709 self._playertype = type(self).__orig_bases__[-1].__args__[0] 710 if not isinstance(self._playertype, type): 711 self._playertype = Player 712 print( 713 f'ERROR: {type(self)} was not passed a Player' 714 f' type argument; please explicitly pass ba.Player' 715 f' if you do not want to override it.' 716 ) 717 self._teamtype = type(self).__orig_bases__[-1].__args__[1] 718 if not isinstance(self._teamtype, type): 719 self._teamtype = Team 720 print( 721 f'ERROR: {type(self)} was not passed a Team' 722 f' type argument; please explicitly pass ba.Team' 723 f' if you do not want to override it.' 724 ) 725 assert issubclass(self._playertype, Player) 726 assert issubclass(self._teamtype, Team) 727 728 @classmethod 729 def _check_activity_death( 730 cls, activity_ref: weakref.ref[Activity], counter: list[int] 731 ) -> None: 732 """Sanity check to make sure an Activity was destroyed properly. 733 734 Receives a weakref to a ba.Activity which should have torn itself 735 down due to no longer being referenced anywhere. Will complain 736 and/or print debugging info if the Activity still exists. 737 """ 738 try: 739 activity = activity_ref() 740 print( 741 'ERROR: Activity is not dying when expected:', 742 activity, 743 '(warning ' + str(counter[0] + 1) + ')', 744 ) 745 print( 746 'This means something is still strong-referencing it.\n' 747 'Check out methods such as efro.debug.printrefs() to' 748 ' help debug this sort of thing.' 749 ) 750 # Note: no longer calling gc.get_referrers() here because it's 751 # usage can bork stuff. (see notes at top of efro.debug) 752 counter[0] += 1 753 if counter[0] == 4: 754 print('Killing app due to stuck activity... :-(') 755 _ba.quit() 756 757 except Exception: 758 print_exception('Error on _check_activity_death/') 759 760 def _expire(self) -> None: 761 """Put the activity in a state where it can be garbage-collected. 762 763 This involves clearing anything that might be holding a reference 764 to it, etc. 765 """ 766 assert not self._expired 767 self._expired = True 768 769 try: 770 self.on_expire() 771 except Exception: 772 print_exception(f'Error in Activity on_expire() for {self}.') 773 774 try: 775 self._customdata = None 776 except Exception: 777 print_exception(f'Error clearing customdata for {self}.') 778 779 # Don't want to be holding any delay-delete refs at this point. 780 self._prune_delay_deletes() 781 782 self._expire_actors() 783 self._expire_players() 784 self._expire_teams() 785 786 # This will kill all low level stuff: Timers, Nodes, etc., which 787 # should clear up any remaining refs to our Activity and allow us 788 # to die peacefully. 789 try: 790 self._activity_data.expire() 791 except Exception: 792 print_exception(f'Error expiring _activity_data for {self}.') 793 794 def _expire_actors(self) -> None: 795 # Expire all Actors. 796 for actor_ref in self._actor_weak_refs: 797 actor = actor_ref() 798 if actor is not None: 799 verify_object_death(actor) 800 try: 801 actor.on_expire() 802 except Exception: 803 print_exception( 804 f'Error in Actor.on_expire()' f' for {actor_ref()}.' 805 ) 806 807 def _expire_players(self) -> None: 808 809 # Issue warnings for any players that left the game but don't 810 # get freed soon. 811 for ex_player in (p() for p in self._players_that_left): 812 if ex_player is not None: 813 verify_object_death(ex_player) 814 815 for player in self.players: 816 # This should allow our ba.Player instance to be freed. 817 # Complain if that doesn't happen. 818 verify_object_death(player) 819 820 try: 821 player.expire() 822 except Exception: 823 print_exception(f'Error expiring {player}') 824 825 # Reset the SessionPlayer to a not-in-an-activity state. 826 try: 827 sessionplayer = player.sessionplayer 828 self._reset_session_player_for_no_activity(sessionplayer) 829 except SessionPlayerNotFoundError: 830 # Conceivably, someone could have held on to a Player object 831 # until now whos underlying SessionPlayer left long ago... 832 pass 833 except Exception: 834 print_exception(f'Error expiring {player}.') 835 836 def _expire_teams(self) -> None: 837 838 # Issue warnings for any teams that left the game but don't 839 # get freed soon. 840 for ex_team in (p() for p in self._teams_that_left): 841 if ex_team is not None: 842 verify_object_death(ex_team) 843 844 for team in self.teams: 845 # This should allow our ba.Team instance to die. 846 # Complain if that doesn't happen. 847 verify_object_death(team) 848 849 try: 850 team.expire() 851 except Exception: 852 print_exception(f'Error expiring {team}') 853 854 try: 855 sessionteam = team.sessionteam 856 sessionteam.activityteam = None 857 except SessionTeamNotFoundError: 858 # It is expected that Team objects may last longer than 859 # the SessionTeam they came from (game objects may hold 860 # team references past the point at which the underlying 861 # player/team has left the game) 862 pass 863 except Exception: 864 print_exception(f'Error expiring Team {team}.') 865 866 def _prune_delay_deletes(self) -> None: 867 self._delay_delete_players.clear() 868 self._delay_delete_teams.clear() 869 870 # Clear out any dead weak-refs. 871 self._teams_that_left = [ 872 t for t in self._teams_that_left if t() is not None 873 ] 874 self._players_that_left = [ 875 p for p in self._players_that_left if p() is not None 876 ] 877 878 def _prune_dead_actors(self) -> None: 879 self._last_prune_dead_actors_time = _ba.time() 880 881 # Prune our strong refs when the Actor's exists() call gives False 882 self._actor_refs = [a for a in self._actor_refs if a.exists()] 883 884 # Prune our weak refs once the Actor object has been freed. 885 self._actor_weak_refs = [ 886 a for a in self._actor_weak_refs if a() is not None 887 ]
Units of execution wrangled by a ba.Session.
Category: Gameplay Classes
Examples of Activities include games, score-screens, cutscenes, etc. A ba.Session has one 'current' Activity at any time, though their existence can overlap during transitions.
128 def __init__(self, settings: dict): 129 """Creates an Activity in the current ba.Session. 130 131 The activity will not be actually run until ba.Session.setactivity 132 is called. 'settings' should be a dict of key/value pairs specific 133 to the activity. 134 135 Activities should preload as much of their media/etc as possible in 136 their constructor, but none of it should actually be used until they 137 are transitioned in. 138 """ 139 super().__init__() 140 141 # Create our internal engine data. 142 self._activity_data = _ba.register_activity(self) 143 144 assert isinstance(settings, dict) 145 assert _ba.getactivity() is self 146 147 self._globalsnode: ba.Node | None = None 148 149 # Player/Team types should have been specified as type args; 150 # grab those. 151 self._playertype: type[PlayerType] 152 self._teamtype: type[TeamType] 153 self._setup_player_and_team_types() 154 155 # FIXME: Relocate or remove the need for this stuff. 156 self.paused_text: ba.Actor | None = None 157 158 self._session = weakref.ref(_ba.getsession()) 159 160 # Preloaded data for actors, maps, etc; indexed by type. 161 self.preloads: dict[type, Any] = {} 162 163 # Hopefully can eventually kill this; activities should 164 # validate/store whatever settings they need at init time 165 # (in a more type-safe way). 166 self.settings_raw = settings 167 168 self._has_transitioned_in = False 169 self._has_begun = False 170 self._has_ended = False 171 self._activity_death_check_timer: ba.Timer | None = None 172 self._expired = False 173 self._delay_delete_players: list[PlayerType] = [] 174 self._delay_delete_teams: list[TeamType] = [] 175 self._players_that_left: list[weakref.ref[PlayerType]] = [] 176 self._teams_that_left: list[weakref.ref[TeamType]] = [] 177 self._transitioning_out = False 178 179 # A handy place to put most actors; this list is pruned of dead 180 # actors regularly and these actors are insta-killed as the activity 181 # is dying. 182 self._actor_refs: list[ba.Actor] = [] 183 self._actor_weak_refs: list[weakref.ref[ba.Actor]] = [] 184 self._last_prune_dead_actors_time = _ba.time() 185 self._prune_dead_actors_timer: ba.Timer | None = None 186 187 self.teams = [] 188 self.players = [] 189 190 self.lobby = None 191 self._stats: ba.Stats | None = None 192 self._customdata: dict | None = {}
Creates an Activity in the current ba.Session.
The activity will not be actually run until ba.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.
Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.
The settings dict passed in when the activity was made. This attribute is deprecated and should be avoided when possible; activities should pull all values they need from the 'settings' arg passed to the Activity __init__ call.
The list of ba.Team-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game. (at least in free-for-all mode where every player gets their own team; in teams mode there are always 2 teams regardless of the player count).
The list of ba.Player-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
Joining activities are for waiting for initial player joins. They are treated slightly differently than regular activities, mainly in that all players are passed to the activity at once instead of as each joins.
Whether idle players can potentially be kicked (should not happen in menus/etc).
In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.
Set this to True to inherit slow motion setting from previous activity (useful for transitions to avoid hitches).
Set this to True to keep playing the music from the previous activity (without even restarting it).