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
class AccountV2Handle:
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.

AccountV2Handle()
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] = {}
class Achievement:
 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

Achievement( name: str, icon_name: str, icon_color: Sequence[float], level_name: str, award: int, hard_mode_only: bool = False)
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
name: str

The name of this achievement.

level_name: str

The name of the level this achievement applies to.

def get_icon_texture(self, complete: bool) -> ba.Texture:
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

def get_icon_color(self, complete: bool) -> Sequence[float]:
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.

hard_mode_only: bool

Whether this Achievement is only unlockable in hard-mode.

complete: bool

Whether this Achievement is currently complete.

def announce_completion(self, sound: bool = True) -> None:
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.

def set_complete(self, complete: bool = True) -> None:
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.

display_name: ba.Lstr

Return a ba.Lstr for this Achievement's name.

description: ba.Lstr

Get a ba.Lstr for the Achievement's brief description.

description_complete: ba.Lstr

Get a ba.Lstr for the Achievement's description when completed.

description_full: ba.Lstr

Get a ba.Lstr for the Achievement's full description.

description_full_complete: ba.Lstr

Get a ba.Lstr for the Achievement's full desc. when completed.

def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
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.

power_ranking_value: int

Get the power-ranking award value for this achievement.

def create_display( self, x: float, y: float, delay: float, outdelay: float | None = None, color: Optional[Sequence[float]] = None, style: str = 'post_game') -> list[ba.Actor]:
 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.

def show_completion_banner(self, sound: bool = True) -> None:
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            )

Create the banner/sound for an acquired achievement announcement.

class AchievementSubsystem:
 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'.

AchievementSubsystem()
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()
def award_local_achievement(self, achname: str) -> None:
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.

def get_achievement(self, name: str) -> ba.Achievement:
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.

def achievements_for_coop_level(self, level_name: str) -> list[ba.Achievement]:
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.

class Activity(ba.DependencyComponent, typing.Generic[~PlayerType, ~TeamType]):
 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.

Activity(settings: dict)
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.

settings_raw: dict[str, typing.Any]

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.

teams: list[~TeamType]

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).

players: list[~PlayerType]

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.

announce_player_deaths = False

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.

is_joining_activity = False

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.

allow_pausing = False

Whether game-time should still progress when in menus/etc.

allow_kick_idle_players = True

Whether idle players can potentially be kicked (should not happen in menus/etc).

use_fixed_vr_overlay = False

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.

slow_motion = False

If True, runs in slow motion and turns down sound pitch.

inherits_slow_motion = False

Set this to True to inherit slow motion setting from previous activity (useful for transitions to avoid hitches).

inherits_music = False

Set this to True to keep playing the music from the previous activity (without even restarting it).

inherits_vr_camera_offset = False