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, Context, ContextCall, Data, InputDevice, Material, Model,
 13    Node, SessionPlayer, Sound, Texture, Timer, Vec3, Widget, buttonwidget,
 14    camerashake, checkboxwidget, columnwidget, containerwidget, do_once,
 15    emitfx, getactivity, getcollidemodel, getmodel, getnodes, getsession,
 16    getsound, gettexture, hscrollwidget, imagewidget, log, newactivity,
 17    newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget,
 18    safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr,
 19    textwidget, time, timer, open_url, widget, clipboard_is_supported,
 20    clipboard_has_text, clipboard_get_text, clipboard_set_text, getdata)
 21from ba._activity import Activity
 22from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
 23from ba._actor import Actor
 24from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation
 25from ba._nodeactor import NodeActor
 26from ba._app import App
 27from ba._cloud import CloudSubsystem
 28from ba._coopgame import CoopGameActivity
 29from ba._coopsession import CoopSession
 30from ba._dependency import (Dependency, DependencyComponent, DependencySet,
 31                            AssetPackage)
 32from ba._generated.enums import (TimeType, Permission, TimeFormat, SpecialChar,
 33                                 InputType, UIScale)
 34from ba._error import (
 35    print_exception, print_error, ContextError, NotFoundError,
 36    PlayerNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError,
 37    ActorNotFoundError, InputDeviceNotFoundError, WidgetNotFoundError,
 38    ActivityNotFoundError, TeamNotFoundError, SessionTeamNotFoundError,
 39    SessionNotFoundError, DelegateNotFoundError, DependencyError)
 40from ba._freeforallsession import FreeForAllSession
 41from ba._gameactivity import GameActivity
 42from ba._gameresults import GameResults
 43from ba._settings import (Setting, IntSetting, FloatSetting, ChoiceSetting,
 44                          BoolSetting, IntChoiceSetting, FloatChoiceSetting)
 45from ba._language import Lstr, LanguageSubsystem
 46from ba._map import Map, getmaps
 47from ba._session import Session
 48from ba._ui import UISubsystem
 49from ba._servermode import ServerController
 50from ba._score import ScoreType, ScoreConfig
 51from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
 52from ba._team import SessionTeam, Team, EmptyTeam
 53from ba._teamgame import TeamGameActivity
 54from ba._dualteamsession import DualTeamSession
 55from ba._achievement import Achievement, AchievementSubsystem
 56from ba._appconfig import AppConfig
 57from ba._appdelegate import AppDelegate
 58from ba._apputils import is_browser_likely_available, garbage_collect
 59from ba._campaign import Campaign
 60from ba._gameutils import (GameTip, animate, animate_array, show_damage_count,
 61                           timestring, cameraflash)
 62from ba._general import (WeakCall, Call, existing, Existable,
 63                         verify_object_death, storagename, getclass)
 64from ba._keyboard import Keyboard
 65from ba._level import Level
 66from ba._lobby import Lobby, Chooser
 67from ba._math import normalized_color, is_point_in_box, vec3validate
 68from ba._meta import MetadataSubsystem
 69from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage,
 70                          PlayerDiedMessage, StandMessage, PickUpMessage,
 71                          DropMessage, PickedUpMessage, DroppedMessage,
 72                          ShouldShatterMessage, ImpactDamageMessage,
 73                          FreezeMessage, ThawMessage, HitMessage,
 74                          CelebrateMessage)
 75from ba._music import (setmusic, MusicPlayer, MusicType, MusicPlayMode,
 76                       MusicSubsystem)
 77from ba._powerup import PowerupMessage, PowerupAcceptMessage
 78from ba._multiteamsession import MultiTeamSession
 79from ba.ui import Window, UIController, uicleanupcheck
 80from ba._collision import Collision, getcollision
 81
 82app: App
 83
 84__all__ = [
 85    'Achievement', 'AchievementSubsystem', 'Activity', 'ActivityNotFoundError',
 86    'Actor', 'ActorNotFoundError', 'animate', 'animate_array', 'app', 'App',
 87    'AppConfig', 'AppDelegate', 'AssetPackage', 'BoolSetting', 'buttonwidget',
 88    'Call', 'cameraflash', 'camerashake', 'Campaign', 'CelebrateMessage',
 89    'charstr', 'checkboxwidget', 'ChoiceSetting', 'Chooser',
 90    'clipboard_get_text', 'clipboard_has_text', 'clipboard_is_supported',
 91    'clipboard_set_text', 'CollideModel', 'Collision', 'columnwidget',
 92    'containerwidget', 'Context', 'ContextCall', 'ContextError',
 93    'CloudSubsystem', 'CoopGameActivity', 'CoopSession', 'Data', 'DeathType',
 94    'DelegateNotFoundError', 'Dependency', 'DependencyComponent',
 95    'DependencyError', 'DependencySet', 'DieMessage', 'do_once', 'DropMessage',
 96    'DroppedMessage', 'DualTeamSession', 'emitfx', 'EmptyPlayer', 'EmptyTeam',
 97    'Existable', 'existing', 'FloatChoiceSetting', 'FloatSetting',
 98    'FreeForAllSession', 'FreezeMessage', 'GameActivity', 'GameResults',
 99    'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel',
100    'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession',
101    'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget',
102    'ImpactDamageMessage', 'InputDevice', 'InputDeviceNotFoundError',
103    'InputType', 'IntChoiceSetting', 'IntSetting',
104    'is_browser_likely_available', 'is_point_in_box', 'Keyboard',
105    'LanguageSubsystem', 'Level', 'Lobby', 'log', 'Lstr', 'Map', 'Material',
106    'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer',
107    'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode',
108    'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color',
109    'NotFoundError', 'open_url', 'OutOfBoundsMessage', 'Permission',
110    'PickedUpMessage', 'PickUpMessage', 'Player', 'PlayerDiedMessage',
111    'PlayerInfo', 'PlayerNotFoundError', 'PlayerRecord', 'PlayerScoredMessage',
112    'playsound', 'Plugin', 'PluginSubsystem', 'PotentialPlugin',
113    'PowerupAcceptMessage', 'PowerupMessage', 'print_error', 'print_exception',
114    'printnodes', 'printobjects', 'pushcall', 'quit', 'rowwidget', 'safecolor',
115    'ScoreConfig', 'ScoreType', 'screenmessage', 'scrollwidget',
116    'ServerController', 'Session', 'SessionNotFoundError', 'SessionPlayer',
117    'SessionPlayerNotFoundError', 'SessionTeam', 'SessionTeamNotFoundError',
118    'set_analytics_screen', 'setmusic', 'Setting', 'ShouldShatterMessage',
119    'show_damage_count', 'Sound', 'SpecialChar', 'StandLocation',
120    'StandMessage', 'Stats', 'storagename', 'Team', 'TeamGameActivity',
121    'TeamNotFoundError', 'Texture', 'textwidget', 'ThawMessage', 'time',
122    'TimeFormat', 'Timer', 'timer', 'timestring', 'TimeType', 'uicleanupcheck',
123    'UIController', 'UIScale', 'UISubsystem', 'UNHANDLED', 'Vec3',
124    'vec3validate', 'verify_object_death', 'WeakCall', 'Widget', 'widget',
125    'WidgetNotFoundError', 'Window'
126]
127
128
129# Have these things present themselves cleanly as 'ba.Foo'
130# instead of 'ba._submodule.Foo'
131def _simplify_module_names() -> None:
132    import os
133
134    # Though pdoc gets confused when we override __module__,
135    # so let's make an exception for it.
136    if os.environ.get('BA_DOCS_GENERATION', '0') != '1':
137        from efro.util import set_canonical_module
138        globs = globals()
139        set_canonical_module(
140            module_globals=globs,
141            names=[n for n in globs.keys() if not n.startswith('_')])
142
143
144_simplify_module_names()
145del _simplify_module_names
class Achievement:
 437class Achievement:
 438    """Represents attributes and state for an individual achievement.
 439
 440    Category: **App Classes**
 441    """
 442
 443    def __init__(self,
 444                 name: str,
 445                 icon_name: str,
 446                 icon_color: Sequence[float],
 447                 level_name: str,
 448                 award: int,
 449                 hard_mode_only: bool = False):
 450        self._name = name
 451        self._icon_name = icon_name
 452        self._icon_color: Sequence[float] = list(icon_color) + [1]
 453        self._level_name = level_name
 454        self._completion_banner_slot: int | None = None
 455        self._award = award
 456        self._hard_mode_only = hard_mode_only
 457
 458    @property
 459    def name(self) -> str:
 460        """The name of this achievement."""
 461        return self._name
 462
 463    @property
 464    def level_name(self) -> str:
 465        """The name of the level this achievement applies to."""
 466        return self._level_name
 467
 468    def get_icon_texture(self, complete: bool) -> ba.Texture:
 469        """Return the icon texture to display for this achievement"""
 470        return _ba.gettexture(
 471            self._icon_name if complete else 'achievementEmpty')
 472
 473    def get_icon_color(self, complete: bool) -> Sequence[float]:
 474        """Return the color tint for this Achievement's icon."""
 475        if complete:
 476            return self._icon_color
 477        return 1.0, 1.0, 1.0, 0.6
 478
 479    @property
 480    def hard_mode_only(self) -> bool:
 481        """Whether this Achievement is only unlockable in hard-mode."""
 482        return self._hard_mode_only
 483
 484    @property
 485    def complete(self) -> bool:
 486        """Whether this Achievement is currently complete."""
 487        val: bool = self._getconfig()['Complete']
 488        assert isinstance(val, bool)
 489        return val
 490
 491    def announce_completion(self, sound: bool = True) -> None:
 492        """Kick off an announcement for this achievement's completion."""
 493        from ba._generated.enums import TimeType
 494        app = _ba.app
 495
 496        # Even though there are technically achievements when we're not
 497        # signed in, lets not show them (otherwise we tend to get
 498        # confusing 'controller connected' achievements popping up while
 499        # waiting to log in which can be confusing).
 500        if _ba.get_v1_account_state() != 'signed_in':
 501            return
 502
 503        # If we're being freshly complete, display/report it and whatnot.
 504        if (self, sound) not in app.ach.achievements_to_display:
 505            app.ach.achievements_to_display.append((self, sound))
 506
 507        # If there's no achievement display timer going, kick one off
 508        # (if one's already running it will pick this up before it dies).
 509
 510        # Need to check last time too; its possible our timer wasn't able to
 511        # clear itself if an activity died and took it down with it.
 512        if ((app.ach.achievement_display_timer is None
 513             or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time
 514             > 2.0) and _ba.getactivity(doraise=False) is not None):
 515            app.ach.achievement_display_timer = _ba.Timer(
 516                1.0,
 517                _display_next_achievement,
 518                repeat=True,
 519                timetype=TimeType.BASE)
 520
 521            # Show the first immediately.
 522            _display_next_achievement()
 523
 524    def set_complete(self, complete: bool = True) -> None:
 525        """Set an achievement's completed state.
 526
 527        note this only sets local state; use a transaction to
 528        actually award achievements.
 529        """
 530        config = self._getconfig()
 531        if complete != config['Complete']:
 532            config['Complete'] = complete
 533
 534    @property
 535    def display_name(self) -> ba.Lstr:
 536        """Return a ba.Lstr for this Achievement's name."""
 537        from ba._language import Lstr
 538        name: ba.Lstr | str
 539        try:
 540            if self._level_name != '':
 541                from ba._campaign import getcampaign
 542                campaignname, campaign_level = self._level_name.split(':')
 543                name = getcampaign(campaignname).getlevel(
 544                    campaign_level).displayname
 545            else:
 546                name = ''
 547        except Exception:
 548            name = ''
 549            print_exception()
 550        return Lstr(resource='achievements.' + self._name + '.name',
 551                    subs=[('${LEVEL}', name)])
 552
 553    @property
 554    def description(self) -> ba.Lstr:
 555        """Get a ba.Lstr for the Achievement's brief description."""
 556        from ba._language import Lstr
 557        if 'description' in _ba.app.lang.get_resource('achievements')[
 558                self._name]:
 559            return Lstr(resource='achievements.' + self._name + '.description')
 560        return Lstr(resource='achievements.' + self._name + '.descriptionFull')
 561
 562    @property
 563    def description_complete(self) -> ba.Lstr:
 564        """Get a ba.Lstr for the Achievement's description when completed."""
 565        from ba._language import Lstr
 566        if 'descriptionComplete' in _ba.app.lang.get_resource('achievements')[
 567                self._name]:
 568            return Lstr(resource='achievements.' + self._name +
 569                        '.descriptionComplete')
 570        return Lstr(resource='achievements.' + self._name +
 571                    '.descriptionFullComplete')
 572
 573    @property
 574    def description_full(self) -> ba.Lstr:
 575        """Get a ba.Lstr for the Achievement's full description."""
 576        from ba._language import Lstr
 577
 578        return Lstr(
 579            resource='achievements.' + self._name + '.descriptionFull',
 580            subs=[('${LEVEL}',
 581                   Lstr(translate=('coopLevelNames',
 582                                   ACH_LEVEL_NAMES.get(self._name, '?'))))])
 583
 584    @property
 585    def description_full_complete(self) -> ba.Lstr:
 586        """Get a ba.Lstr for the Achievement's full desc. when completed."""
 587        from ba._language import Lstr
 588        return Lstr(
 589            resource='achievements.' + self._name + '.descriptionFullComplete',
 590            subs=[('${LEVEL}',
 591                   Lstr(translate=('coopLevelNames',
 592                                   ACH_LEVEL_NAMES.get(self._name, '?'))))])
 593
 594    def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
 595        """Get the ticket award value for this achievement."""
 596        val: int = (_ba.get_v1_account_misc_read_val('achAward.' + self._name,
 597                                                     self._award) *
 598                    _get_ach_mult(include_pro_bonus))
 599        assert isinstance(val, int)
 600        return val
 601
 602    @property
 603    def power_ranking_value(self) -> int:
 604        """Get the power-ranking award value for this achievement."""
 605        val: int = _ba.get_v1_account_misc_read_val(
 606            'achLeaguePoints.' + self._name, self._award)
 607        assert isinstance(val, int)
 608        return val
 609
 610    def create_display(self,
 611                       x: float,
 612                       y: float,
 613                       delay: float,
 614                       outdelay: float | None = None,
 615                       color: Sequence[float] | None = None,
 616                       style: str = 'post_game') -> list[ba.Actor]:
 617        """Create a display for the Achievement.
 618
 619        Shows the Achievement icon, name, and description.
 620        """
 621        # pylint: disable=cyclic-import
 622        from ba._language import Lstr
 623        from ba._generated.enums import SpecialChar
 624        from ba._coopsession import CoopSession
 625        from bastd.actor.image import Image
 626        from bastd.actor.text import Text
 627
 628        # Yeah this needs cleaning up.
 629        if style == 'post_game':
 630            in_game_colors = False
 631            in_main_menu = False
 632            h_attach = Text.HAttach.CENTER
 633            v_attach = Text.VAttach.CENTER
 634            attach = Image.Attach.CENTER
 635        elif style == 'in_game':
 636            in_game_colors = True
 637            in_main_menu = False
 638            h_attach = Text.HAttach.LEFT
 639            v_attach = Text.VAttach.TOP
 640            attach = Image.Attach.TOP_LEFT
 641        elif style == 'news':
 642            in_game_colors = True
 643            in_main_menu = True
 644            h_attach = Text.HAttach.CENTER
 645            v_attach = Text.VAttach.TOP
 646            attach = Image.Attach.TOP_CENTER
 647        else:
 648            raise ValueError('invalid style "' + style + '"')
 649
 650        # Attempt to determine what campaign we're in
 651        # (so we know whether to show "hard mode only").
 652        if in_main_menu:
 653            hmo = False
 654        else:
 655            try:
 656                session = _ba.getsession()
 657                if isinstance(session, CoopSession):
 658                    campaign = session.campaign
 659                    assert campaign is not None
 660                    hmo = (self._hard_mode_only and campaign.name == 'Easy')
 661                else:
 662                    hmo = False
 663            except Exception:
 664                print_exception('Error determining campaign.')
 665                hmo = False
 666
 667        objs: list[ba.Actor]
 668
 669        if in_game_colors:
 670            objs = []
 671            out_delay_fin = (delay +
 672                             outdelay) if outdelay is not None else None
 673            if color is not None:
 674                cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2],
 675                       color[3])
 676                cl2 = color
 677            else:
 678                cl1 = (1.5, 1.5, 2, 1.0)
 679                cl2 = (0.8, 0.8, 1.0, 1.0)
 680
 681            if hmo:
 682                cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6)
 683                cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2)
 684
 685            objs.append(
 686                Image(self.get_icon_texture(False),
 687                      host_only=True,
 688                      color=cl1,
 689                      position=(x - 25, y + 5),
 690                      attach=attach,
 691                      transition=Image.Transition.FADE_IN,
 692                      transition_delay=delay,
 693                      vr_depth=4,
 694                      transition_out_delay=out_delay_fin,
 695                      scale=(40, 40)).autoretain())
 696            txt = self.display_name
 697            txt_s = 0.85
 698            txt_max_w = 300
 699            objs.append(
 700                Text(txt,
 701                     host_only=True,
 702                     maxwidth=txt_max_w,
 703                     position=(x, y + 2),
 704                     transition=Text.Transition.FADE_IN,
 705                     scale=txt_s,
 706                     flatness=0.6,
 707                     shadow=0.5,
 708                     h_attach=h_attach,
 709                     v_attach=v_attach,
 710                     color=cl2,
 711                     transition_delay=delay + 0.05,
 712                     transition_out_delay=out_delay_fin).autoretain())
 713            txt2_s = 0.62
 714            txt2_max_w = 400
 715            objs.append(
 716                Text(self.description_full
 717                     if in_main_menu else self.description,
 718                     host_only=True,
 719                     maxwidth=txt2_max_w,
 720                     position=(x, y - 14),
 721                     transition=Text.Transition.FADE_IN,
 722                     vr_depth=-5,
 723                     h_attach=h_attach,
 724                     v_attach=v_attach,
 725                     scale=txt2_s,
 726                     flatness=1.0,
 727                     shadow=0.5,
 728                     color=cl2,
 729                     transition_delay=delay + 0.1,
 730                     transition_out_delay=out_delay_fin).autoretain())
 731
 732            if hmo:
 733                txtactor = Text(
 734                    Lstr(resource='difficultyHardOnlyText'),
 735                    host_only=True,
 736                    maxwidth=txt2_max_w * 0.7,
 737                    position=(x + 60, y + 5),
 738                    transition=Text.Transition.FADE_IN,
 739                    vr_depth=-5,
 740                    h_attach=h_attach,
 741                    v_attach=v_attach,
 742                    h_align=Text.HAlign.CENTER,
 743                    v_align=Text.VAlign.CENTER,
 744                    scale=txt_s * 0.8,
 745                    flatness=1.0,
 746                    shadow=0.5,
 747                    color=(1, 1, 0.6, 1),
 748                    transition_delay=delay + 0.1,
 749                    transition_out_delay=out_delay_fin).autoretain()
 750                txtactor.node.rotate = 10
 751                objs.append(txtactor)
 752
 753            # Ticket-award.
 754            award_x = -100
 755            objs.append(
 756                Text(_ba.charstr(SpecialChar.TICKET),
 757                     host_only=True,
 758                     position=(x + award_x + 33, y + 7),
 759                     transition=Text.Transition.FADE_IN,
 760                     scale=1.5,
 761                     h_attach=h_attach,
 762                     v_attach=v_attach,
 763                     h_align=Text.HAlign.CENTER,
 764                     v_align=Text.VAlign.CENTER,
 765                     color=(1, 1, 1, 0.2 if hmo else 0.4),
 766                     transition_delay=delay + 0.05,
 767                     transition_out_delay=out_delay_fin).autoretain())
 768            objs.append(
 769                Text('+' + str(self.get_award_ticket_value()),
 770                     host_only=True,
 771                     position=(x + award_x + 28, y + 16),
 772                     transition=Text.Transition.FADE_IN,
 773                     scale=0.7,
 774                     flatness=1,
 775                     h_attach=h_attach,
 776                     v_attach=v_attach,
 777                     h_align=Text.HAlign.CENTER,
 778                     v_align=Text.VAlign.CENTER,
 779                     color=cl2,
 780                     transition_delay=delay + 0.05,
 781                     transition_out_delay=out_delay_fin).autoretain())
 782
 783        else:
 784            complete = self.complete
 785            objs = []
 786            c_icon = self.get_icon_color(complete)
 787            if hmo and not complete:
 788                c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3)
 789            objs.append(
 790                Image(self.get_icon_texture(complete),
 791                      host_only=True,
 792                      color=c_icon,
 793                      position=(x - 25, y + 5),
 794                      attach=attach,
 795                      vr_depth=4,
 796                      transition=Image.Transition.IN_RIGHT,
 797                      transition_delay=delay,
 798                      transition_out_delay=None,
 799                      scale=(40, 40)).autoretain())
 800            if complete:
 801                objs.append(
 802                    Image(_ba.gettexture('achievementOutline'),
 803                          host_only=True,
 804                          model_transparent=_ba.getmodel('achievementOutline'),
 805                          color=(2, 1.4, 0.4, 1),
 806                          vr_depth=8,
 807                          position=(x - 25, y + 5),
 808                          attach=attach,
 809                          transition=Image.Transition.IN_RIGHT,
 810                          transition_delay=delay,
 811                          transition_out_delay=None,
 812                          scale=(40, 40)).autoretain())
 813            else:
 814                if not complete:
 815                    award_x = -100
 816                    objs.append(
 817                        Text(_ba.charstr(SpecialChar.TICKET),
 818                             host_only=True,
 819                             position=(x + award_x + 33, y + 7),
 820                             transition=Text.Transition.IN_RIGHT,
 821                             scale=1.5,
 822                             h_attach=h_attach,
 823                             v_attach=v_attach,
 824                             h_align=Text.HAlign.CENTER,
 825                             v_align=Text.VAlign.CENTER,
 826                             color=(1, 1, 1, 0.4) if complete else
 827                             (1, 1, 1, (0.1 if hmo else 0.2)),
 828                             transition_delay=delay + 0.05,
 829                             transition_out_delay=None).autoretain())
 830                    objs.append(
 831                        Text('+' + str(self.get_award_ticket_value()),
 832                             host_only=True,
 833                             position=(x + award_x + 28, y + 16),
 834                             transition=Text.Transition.IN_RIGHT,
 835                             scale=0.7,
 836                             flatness=1,
 837                             h_attach=h_attach,
 838                             v_attach=v_attach,
 839                             h_align=Text.HAlign.CENTER,
 840                             v_align=Text.VAlign.CENTER,
 841                             color=((0.8, 0.93, 0.8, 1.0) if complete else
 842                                    (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))),
 843                             transition_delay=delay + 0.05,
 844                             transition_out_delay=None).autoretain())
 845
 846                    # Show 'hard-mode-only' only over incomplete achievements
 847                    # when that's the case.
 848                    if hmo:
 849                        txtactor = Text(
 850                            Lstr(resource='difficultyHardOnlyText'),
 851                            host_only=True,
 852                            maxwidth=300 * 0.7,
 853                            position=(x + 60, y + 5),
 854                            transition=Text.Transition.FADE_IN,
 855                            vr_depth=-5,
 856                            h_attach=h_attach,
 857                            v_attach=v_attach,
 858                            h_align=Text.HAlign.CENTER,
 859                            v_align=Text.VAlign.CENTER,
 860                            scale=0.85 * 0.8,
 861                            flatness=1.0,
 862                            shadow=0.5,
 863                            color=(1, 1, 0.6, 1),
 864                            transition_delay=delay + 0.05,
 865                            transition_out_delay=None).autoretain()
 866                        assert txtactor.node
 867                        txtactor.node.rotate = 10
 868                        objs.append(txtactor)
 869
 870            objs.append(
 871                Text(self.display_name,
 872                     host_only=True,
 873                     maxwidth=300,
 874                     position=(x, y + 2),
 875                     transition=Text.Transition.IN_RIGHT,
 876                     scale=0.85,
 877                     flatness=0.6,
 878                     h_attach=h_attach,
 879                     v_attach=v_attach,
 880                     color=((0.8, 0.93, 0.8, 1.0) if complete else
 881                            (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))),
 882                     transition_delay=delay + 0.05,
 883                     transition_out_delay=None).autoretain())
 884            objs.append(
 885                Text(self.description_complete
 886                     if complete else self.description,
 887                     host_only=True,
 888                     maxwidth=400,
 889                     position=(x, y - 14),
 890                     transition=Text.Transition.IN_RIGHT,
 891                     vr_depth=-5,
 892                     h_attach=h_attach,
 893                     v_attach=v_attach,
 894                     scale=0.62,
 895                     flatness=1.0,
 896                     color=((0.6, 0.6, 0.6, 1.0) if complete else
 897                            (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))),
 898                     transition_delay=delay + 0.1,
 899                     transition_out_delay=None).autoretain())
 900        return objs
 901
 902    def _getconfig(self) -> dict[str, Any]:
 903        """
 904        Return the sub-dict in settings where this achievement's
 905        state is stored, creating it if need be.
 906        """
 907        val: dict[str, Any] = (_ba.app.config.setdefault(
 908            'Achievements', {}).setdefault(self._name, {'Complete': False}))
 909        assert isinstance(val, dict)
 910        return val
 911
 912    def _remove_banner_slot(self) -> None:
 913        assert self._completion_banner_slot is not None
 914        _ba.app.ach.achievement_completion_banner_slots.remove(
 915            self._completion_banner_slot)
 916        self._completion_banner_slot = None
 917
 918    def show_completion_banner(self, sound: bool = True) -> None:
 919        """Create the banner/sound for an acquired achievement announcement."""
 920        from ba import _gameutils
 921        from bastd.actor.text import Text
 922        from bastd.actor.image import Image
 923        from ba._general import WeakCall
 924        from ba._language import Lstr
 925        from ba._messages import DieMessage
 926        from ba._generated.enums import TimeType, SpecialChar
 927        app = _ba.app
 928        app.ach.last_achievement_display_time = _ba.time(TimeType.REAL)
 929
 930        # Just piggy-back onto any current activity
 931        # (should we use the session instead?..)
 932        activity = _ba.getactivity(doraise=False)
 933
 934        # If this gets called while this achievement is occupying a slot
 935        # already, ignore it. (probably should never happen in real
 936        # life but whatevs).
 937        if self._completion_banner_slot is not None:
 938            return
 939
 940        if activity is None:
 941            print('show_completion_banner() called with no current activity!')
 942            return
 943
 944        if sound:
 945            _ba.playsound(_ba.getsound('achievement'), host_only=True)
 946        else:
 947            _ba.timer(
 948                0.5,
 949                lambda: _ba.playsound(_ba.getsound('ding'), host_only=True))
 950
 951        in_time = 0.300
 952        out_time = 3.5
 953
 954        base_vr_depth = 200
 955
 956        # Find the first free slot.
 957        i = 0
 958        while True:
 959            if i not in app.ach.achievement_completion_banner_slots:
 960                app.ach.achievement_completion_banner_slots.add(i)
 961                self._completion_banner_slot = i
 962
 963                # Remove us from that slot when we close.
 964                # Use a real-timer in the UI context so the removal runs even
 965                # if our activity/session dies.
 966                with _ba.Context('ui'):
 967                    _ba.timer(in_time + out_time,
 968                              self._remove_banner_slot,
 969                              timetype=TimeType.REAL)
 970                break
 971            i += 1
 972        assert self._completion_banner_slot is not None
 973        y_offs = 110 * self._completion_banner_slot
 974        objs: list[ba.Actor] = []
 975        obj = Image(_ba.gettexture('shadow'),
 976                    position=(-30, 30 + y_offs),
 977                    front=True,
 978                    attach=Image.Attach.BOTTOM_CENTER,
 979                    transition=Image.Transition.IN_BOTTOM,
 980                    vr_depth=base_vr_depth - 100,
 981                    transition_delay=in_time,
 982                    transition_out_delay=out_time,
 983                    color=(0.0, 0.1, 0, 1),
 984                    scale=(1000, 300)).autoretain()
 985        objs.append(obj)
 986        assert obj.node
 987        obj.node.host_only = True
 988        obj = Image(_ba.gettexture('light'),
 989                    position=(-180, 60 + y_offs),
 990                    front=True,
 991                    attach=Image.Attach.BOTTOM_CENTER,
 992                    vr_depth=base_vr_depth,
 993                    transition=Image.Transition.IN_BOTTOM,
 994                    transition_delay=in_time,
 995                    transition_out_delay=out_time,
 996                    color=(1.8, 1.8, 1.0, 0.0),
 997                    scale=(40, 300)).autoretain()
 998        objs.append(obj)
 999        assert obj.node
1000        obj.node.host_only = True
1001        obj.node.premultiplied = True
1002        combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2})
1003        _gameutils.animate(
1004            combine, 'input0', {
1005                in_time: 0,
1006                in_time + 0.4: 30,
1007                in_time + 0.5: 40,
1008                in_time + 0.6: 30,
1009                in_time + 2.0: 0
1010            })
1011        _gameutils.animate(
1012            combine, 'input1', {
1013                in_time: 0,
1014                in_time + 0.4: 200,
1015                in_time + 0.5: 500,
1016                in_time + 0.6: 200,
1017                in_time + 2.0: 0
1018            })
1019        combine.connectattr('output', obj.node, 'scale')
1020        _gameutils.animate(obj.node,
1021                           'rotate', {
1022                               0: 0.0,
1023                               0.35: 360.0
1024                           },
1025                           loop=True)
1026        obj = Image(self.get_icon_texture(True),
1027                    position=(-180, 60 + y_offs),
1028                    attach=Image.Attach.BOTTOM_CENTER,
1029                    front=True,
1030                    vr_depth=base_vr_depth - 10,
1031                    transition=Image.Transition.IN_BOTTOM,
1032                    transition_delay=in_time,
1033                    transition_out_delay=out_time,
1034                    scale=(100, 100)).autoretain()
1035        objs.append(obj)
1036        assert obj.node
1037        obj.node.host_only = True
1038
1039        # Flash.
1040        color = self.get_icon_color(True)
1041        combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3})
1042        keys = {
1043            in_time: 1.0 * color[0],
1044            in_time + 0.4: 1.5 * color[0],
1045            in_time + 0.5: 6.0 * color[0],
1046            in_time + 0.6: 1.5 * color[0],
1047            in_time + 2.0: 1.0 * color[0]
1048        }
1049        _gameutils.animate(combine, 'input0', keys)
1050        keys = {
1051            in_time: 1.0 * color[1],
1052            in_time + 0.4: 1.5 * color[1],
1053            in_time + 0.5: 6.0 * color[1],
1054            in_time + 0.6: 1.5 * color[1],
1055            in_time + 2.0: 1.0 * color[1]
1056        }
1057        _gameutils.animate(combine, 'input1', keys)
1058        keys = {
1059            in_time: 1.0 * color[2],
1060            in_time + 0.4: 1.5 * color[2],
1061            in_time + 0.5: 6.0 * color[2],
1062            in_time + 0.6: 1.5 * color[2],
1063            in_time + 2.0: 1.0 * color[2]
1064        }
1065        _gameutils.animate(combine, 'input2', keys)
1066        combine.connectattr('output', obj.node, 'color')
1067
1068        obj = Image(_ba.gettexture('achievementOutline'),
1069                    model_transparent=_ba.getmodel('achievementOutline'),
1070                    position=(-180, 60 + y_offs),
1071                    front=True,
1072                    attach=Image.Attach.BOTTOM_CENTER,
1073                    vr_depth=base_vr_depth,
1074                    transition=Image.Transition.IN_BOTTOM,
1075                    transition_delay=in_time,
1076                    transition_out_delay=out_time,
1077                    scale=(100, 100)).autoretain()
1078        assert obj.node
1079        obj.node.host_only = True
1080
1081        # Flash.
1082        color = (2, 1.4, 0.4, 1)
1083        combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3})
1084        keys = {
1085            in_time: 1.0 * color[0],
1086            in_time + 0.4: 1.5 * color[0],
1087            in_time + 0.5: 6.0 * color[0],
1088            in_time + 0.6: 1.5 * color[0],
1089            in_time + 2.0: 1.0 * color[0]
1090        }
1091        _gameutils.animate(combine, 'input0', keys)
1092        keys = {
1093            in_time: 1.0 * color[1],
1094            in_time + 0.4: 1.5 * color[1],
1095            in_time + 0.5: 6.0 * color[1],
1096            in_time + 0.6: 1.5 * color[1],
1097            in_time + 2.0: 1.0 * color[1]
1098        }
1099        _gameutils.animate(combine, 'input1', keys)
1100        keys = {
1101            in_time: 1.0 * color[2],
1102            in_time + 0.4: 1.5 * color[2],
1103            in_time + 0.5: 6.0 * color[2],
1104            in_time + 0.6: 1.5 * color[2],
1105            in_time + 2.0: 1.0 * color[2]
1106        }
1107        _gameutils.animate(combine, 'input2', keys)
1108        combine.connectattr('output', obj.node, 'color')
1109        objs.append(obj)
1110
1111        objt = Text(Lstr(value='${A}:',
1112                         subs=[('${A}', Lstr(resource='achievementText'))]),
1113                    position=(-120, 91 + y_offs),
1114                    front=True,
1115                    v_attach=Text.VAttach.BOTTOM,
1116                    vr_depth=base_vr_depth - 10,
1117                    transition=Text.Transition.IN_BOTTOM,
1118                    flatness=0.5,
1119                    transition_delay=in_time,
1120                    transition_out_delay=out_time,
1121                    color=(1, 1, 1, 0.8),
1122                    scale=0.65).autoretain()
1123        objs.append(objt)
1124        assert objt.node
1125        objt.node.host_only = True
1126
1127        objt = Text(self.display_name,
1128                    position=(-120, 50 + y_offs),
1129                    front=True,
1130                    v_attach=Text.VAttach.BOTTOM,
1131                    transition=Text.Transition.IN_BOTTOM,
1132                    vr_depth=base_vr_depth,
1133                    flatness=0.5,
1134                    transition_delay=in_time,
1135                    transition_out_delay=out_time,
1136                    flash=True,
1137                    color=(1, 0.8, 0, 1.0),
1138                    scale=1.5).autoretain()
1139        objs.append(objt)
1140        assert objt.node
1141        objt.node.host_only = True
1142
1143        objt = Text(_ba.charstr(SpecialChar.TICKET),
1144                    position=(-120 - 170 + 5, 75 + y_offs - 20),
1145                    front=True,
1146                    v_attach=Text.VAttach.BOTTOM,
1147                    h_align=Text.HAlign.CENTER,
1148                    v_align=Text.VAlign.CENTER,
1149                    transition=Text.Transition.IN_BOTTOM,
1150                    vr_depth=base_vr_depth,
1151                    transition_delay=in_time,
1152                    transition_out_delay=out_time,
1153                    flash=True,
1154                    color=(0.5, 0.5, 0.5, 1),
1155                    scale=3.0).autoretain()
1156        objs.append(objt)
1157        assert objt.node
1158        objt.node.host_only = True
1159
1160        objt = Text('+' + str(self.get_award_ticket_value()),
1161                    position=(-120 - 180 + 5, 80 + y_offs - 20),
1162                    v_attach=Text.VAttach.BOTTOM,
1163                    front=True,
1164                    h_align=Text.HAlign.CENTER,
1165                    v_align=Text.VAlign.CENTER,
1166                    transition=Text.Transition.IN_BOTTOM,
1167                    vr_depth=base_vr_depth,
1168                    flatness=0.5,
1169                    shadow=1.0,
1170                    transition_delay=in_time,
1171                    transition_out_delay=out_time,
1172                    flash=True,
1173                    color=(0, 1, 0, 1),
1174                    scale=1.5).autoretain()
1175        objs.append(objt)
1176        assert objt.node
1177        objt.node.host_only = True
1178
1179        # Add the 'x 2' if we've got pro.
1180        if app.accounts_v1.have_pro():
1181            objt = Text('x 2',
1182                        position=(-120 - 180 + 45, 80 + y_offs - 50),
1183                        v_attach=Text.VAttach.BOTTOM,
1184                        front=True,
1185                        h_align=Text.HAlign.CENTER,
1186                        v_align=Text.VAlign.CENTER,
1187                        transition=Text.Transition.IN_BOTTOM,
1188                        vr_depth=base_vr_depth,
1189                        flatness=0.5,
1190                        shadow=1.0,
1191                        transition_delay=in_time,
1192                        transition_out_delay=out_time,
1193                        flash=True,
1194                        color=(0.4, 0, 1, 1),
1195                        scale=0.9).autoretain()
1196            objs.append(objt)
1197            assert objt.node
1198            objt.node.host_only = True
1199
1200        objt = Text(self.description_complete,
1201                    position=(-120, 30 + y_offs),
1202                    front=True,
1203                    v_attach=Text.VAttach.BOTTOM,
1204                    transition=Text.Transition.IN_BOTTOM,
1205                    vr_depth=base_vr_depth - 10,
1206                    flatness=0.5,
1207                    transition_delay=in_time,
1208                    transition_out_delay=out_time,
1209                    color=(1.0, 0.7, 0.5, 1.0),
1210                    scale=0.8).autoretain()
1211        objs.append(objt)
1212        assert objt.node
1213        objt.node.host_only = True
1214
1215        for actor in objs:
1216            _ba.timer(out_time + 1.000,
1217                      WeakCall(actor.handlemessage, DieMessage()))

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)
443    def __init__(self,
444                 name: str,
445                 icon_name: str,
446                 icon_color: Sequence[float],
447                 level_name: str,
448                 award: int,
449                 hard_mode_only: bool = False):
450        self._name = name
451        self._icon_name = icon_name
452        self._icon_color: Sequence[float] = list(icon_color) + [1]
453        self._level_name = level_name
454        self._completion_banner_slot: int | None = None
455        self._award = award
456        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:
468    def get_icon_texture(self, complete: bool) -> ba.Texture:
469        """Return the icon texture to display for this achievement"""
470        return _ba.gettexture(
471            self._icon_name if complete else 'achievementEmpty')

Return the icon texture to display for this achievement

def get_icon_color(self, complete: bool) -> Sequence[float]:
473    def get_icon_color(self, complete: bool) -> Sequence[float]:
474        """Return the color tint for this Achievement's icon."""
475        if complete:
476            return self._icon_color
477        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:
491    def announce_completion(self, sound: bool = True) -> None:
492        """Kick off an announcement for this achievement's completion."""
493        from ba._generated.enums import TimeType
494        app = _ba.app
495
496        # Even though there are technically achievements when we're not
497        # signed in, lets not show them (otherwise we tend to get
498        # confusing 'controller connected' achievements popping up while
499        # waiting to log in which can be confusing).
500        if _ba.get_v1_account_state() != 'signed_in':
501            return
502
503        # If we're being freshly complete, display/report it and whatnot.
504        if (self, sound) not in app.ach.achievements_to_display:
505            app.ach.achievements_to_display.append((self, sound))
506
507        # If there's no achievement display timer going, kick one off
508        # (if one's already running it will pick this up before it dies).
509
510        # Need to check last time too; its possible our timer wasn't able to
511        # clear itself if an activity died and took it down with it.
512        if ((app.ach.achievement_display_timer is None
513             or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time
514             > 2.0) and _ba.getactivity(doraise=False) is not None):
515            app.ach.achievement_display_timer = _ba.Timer(
516                1.0,
517                _display_next_achievement,
518                repeat=True,
519                timetype=TimeType.BASE)
520
521            # Show the first immediately.
522            _display_next_achievement()

Kick off an announcement for this achievement's completion.

def set_complete(self, complete: bool = True) -> None:
524    def set_complete(self, complete: bool = True) -> None:
525        """Set an achievement's completed state.
526
527        note this only sets local state; use a transaction to
528        actually award achievements.
529        """
530        config = self._getconfig()
531        if complete != config['Complete']:
532            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:
594    def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
595        """Get the ticket award value for this achievement."""
596        val: int = (_ba.get_v1_account_misc_read_val('achAward.' + self._name,
597                                                     self._award) *
598                    _get_ach_mult(include_pro_bonus))
599        assert isinstance(val, int)
600        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]:
610    def create_display(self,
611                       x: float,
612                       y: float,
613                       delay: float,
614                       outdelay: float | None = None,
615                       color: Sequence[float] | None = None,
616                       style: str = 'post_game') -> list[ba.Actor]:
617        """Create a display for the Achievement.
618
619        Shows the Achievement icon, name, and description.
620        """
621        # pylint: disable=cyclic-import
622        from ba._language import Lstr
623        from ba._generated.enums import SpecialChar
624        from ba._coopsession import CoopSession
625        from bastd.actor.image import Image
626        from bastd.actor.text import Text
627
628        # Yeah this needs cleaning up.
629        if style == 'post_game':
630            in_game_colors = False
631            in_main_menu = False
632            h_attach = Text.HAttach.CENTER
633            v_attach = Text.VAttach.CENTER
634            attach = Image.Attach.CENTER
635        elif style == 'in_game':
636            in_game_colors = True
637            in_main_menu = False
638            h_attach = Text.HAttach.LEFT
639            v_attach = Text.VAttach.TOP
640            attach = Image.Attach.TOP_LEFT
641        elif style == 'news':
642            in_game_colors = True
643            in_main_menu = True
644            h_attach = Text.HAttach.CENTER
645            v_attach = Text.VAttach.TOP
646            attach = Image.Attach.TOP_CENTER
647        else:
648            raise ValueError('invalid style "' + style + '"')
649
650        # Attempt to determine what campaign we're in
651        # (so we know whether to show "hard mode only").
652        if in_main_menu:
653            hmo = False
654        else:
655            try:
656                session = _ba.getsession()
657                if isinstance(session, CoopSession):
658                    campaign = session.campaign
659                    assert campaign is not None
660                    hmo = (self._hard_mode_only and campaign.name == 'Easy')
661                else:
662                    hmo = False
663            except Exception:
664                print_exception('Error determining campaign.')
665                hmo = False
666
667        objs: list[ba.Actor]
668
669        if in_game_colors:
670            objs = []
671            out_delay_fin = (delay +
672                             outdelay) if outdelay is not None else None
673            if color is not None:
674                cl1 = (2.0 * color[0], 2.0 * color[1], 2.0 * color[2],
675                       color[3])
676                cl2 = color
677            else:
678                cl1 = (1.5, 1.5, 2, 1.0)
679                cl2 = (0.8, 0.8, 1.0, 1.0)
680
681            if hmo:
682                cl1 = (cl1[0], cl1[1], cl1[2], cl1[3] * 0.6)
683                cl2 = (cl2[0], cl2[1], cl2[2], cl2[3] * 0.2)
684
685            objs.append(
686                Image(self.get_icon_texture(False),
687                      host_only=True,
688                      color=cl1,
689                      position=(x - 25, y + 5),
690                      attach=attach,
691                      transition=Image.Transition.FADE_IN,
692                      transition_delay=delay,
693                      vr_depth=4,
694                      transition_out_delay=out_delay_fin,
695                      scale=(40, 40)).autoretain())
696            txt = self.display_name
697            txt_s = 0.85
698            txt_max_w = 300
699            objs.append(
700                Text(txt,
701                     host_only=True,
702                     maxwidth=txt_max_w,
703                     position=(x, y + 2),
704                     transition=Text.Transition.FADE_IN,
705                     scale=txt_s,
706                     flatness=0.6,
707                     shadow=0.5,
708                     h_attach=h_attach,
709                     v_attach=v_attach,
710                     color=cl2,
711                     transition_delay=delay + 0.05,
712                     transition_out_delay=out_delay_fin).autoretain())
713            txt2_s = 0.62
714            txt2_max_w = 400
715            objs.append(
716                Text(self.description_full
717                     if in_main_menu else self.description,
718                     host_only=True,
719                     maxwidth=txt2_max_w,
720                     position=(x, y - 14),
721                     transition=Text.Transition.FADE_IN,
722                     vr_depth=-5,
723                     h_attach=h_attach,
724                     v_attach=v_attach,
725                     scale=txt2_s,
726                     flatness=1.0,
727                     shadow=0.5,
728                     color=cl2,
729                     transition_delay=delay + 0.1,
730                     transition_out_delay=out_delay_fin).autoretain())
731
732            if hmo:
733                txtactor = Text(
734                    Lstr(resource='difficultyHardOnlyText'),
735                    host_only=True,
736                    maxwidth=txt2_max_w * 0.7,
737                    position=(x + 60, y + 5),
738                    transition=Text.Transition.FADE_IN,
739                    vr_depth=-5,
740                    h_attach=h_attach,
741                    v_attach=v_attach,
742                    h_align=Text.HAlign.CENTER,
743                    v_align=Text.VAlign.CENTER,
744                    scale=txt_s * 0.8,
745                    flatness=1.0,
746                    shadow=0.5,
747                    color=(1, 1, 0.6, 1),
748                    transition_delay=delay + 0.1,
749                    transition_out_delay=out_delay_fin).autoretain()
750                txtactor.node.rotate = 10
751                objs.append(txtactor)
752
753            # Ticket-award.
754            award_x = -100
755            objs.append(
756                Text(_ba.charstr(SpecialChar.TICKET),
757                     host_only=True,
758                     position=(x + award_x + 33, y + 7),
759                     transition=Text.Transition.FADE_IN,
760                     scale=1.5,
761                     h_attach=h_attach,
762                     v_attach=v_attach,
763                     h_align=Text.HAlign.CENTER,
764                     v_align=Text.VAlign.CENTER,
765                     color=(1, 1, 1, 0.2 if hmo else 0.4),
766                     transition_delay=delay + 0.05,
767                     transition_out_delay=out_delay_fin).autoretain())
768            objs.append(
769                Text('+' + str(self.get_award_ticket_value()),
770                     host_only=True,
771                     position=(x + award_x + 28, y + 16),
772                     transition=Text.Transition.FADE_IN,
773                     scale=0.7,
774                     flatness=1,
775                     h_attach=h_attach,
776                     v_attach=v_attach,
777                     h_align=Text.HAlign.CENTER,
778                     v_align=Text.VAlign.CENTER,
779                     color=cl2,
780                     transition_delay=delay + 0.05,
781                     transition_out_delay=out_delay_fin).autoretain())
782
783        else:
784            complete = self.complete
785            objs = []
786            c_icon = self.get_icon_color(complete)
787            if hmo and not complete:
788                c_icon = (c_icon[0], c_icon[1], c_icon[2], c_icon[3] * 0.3)
789            objs.append(
790                Image(self.get_icon_texture(complete),
791                      host_only=True,
792                      color=c_icon,
793                      position=(x - 25, y + 5),
794                      attach=attach,
795                      vr_depth=4,
796                      transition=Image.Transition.IN_RIGHT,
797                      transition_delay=delay,
798                      transition_out_delay=None,
799                      scale=(40, 40)).autoretain())
800            if complete:
801                objs.append(
802                    Image(_ba.gettexture('achievementOutline'),
803                          host_only=True,
804                          model_transparent=_ba.getmodel('achievementOutline'),
805                          color=(2, 1.4, 0.4, 1),
806                          vr_depth=8,
807                          position=(x - 25, y + 5),
808                          attach=attach,
809                          transition=Image.Transition.IN_RIGHT,
810                          transition_delay=delay,
811                          transition_out_delay=None,
812                          scale=(40, 40)).autoretain())
813            else:
814                if not complete:
815                    award_x = -100
816                    objs.append(
817                        Text(_ba.charstr(SpecialChar.TICKET),
818                             host_only=True,
819                             position=(x + award_x + 33, y + 7),
820                             transition=Text.Transition.IN_RIGHT,
821                             scale=1.5,
822                             h_attach=h_attach,
823                             v_attach=v_attach,
824                             h_align=Text.HAlign.CENTER,
825                             v_align=Text.VAlign.CENTER,
826                             color=(1, 1, 1, 0.4) if complete else
827                             (1, 1, 1, (0.1 if hmo else 0.2)),
828                             transition_delay=delay + 0.05,
829                             transition_out_delay=None).autoretain())
830                    objs.append(
831                        Text('+' + str(self.get_award_ticket_value()),
832                             host_only=True,
833                             position=(x + award_x + 28, y + 16),
834                             transition=Text.Transition.IN_RIGHT,
835                             scale=0.7,
836                             flatness=1,
837                             h_attach=h_attach,
838                             v_attach=v_attach,
839                             h_align=Text.HAlign.CENTER,
840                             v_align=Text.VAlign.CENTER,
841                             color=((0.8, 0.93, 0.8, 1.0) if complete else
842                                    (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))),
843                             transition_delay=delay + 0.05,
844                             transition_out_delay=None).autoretain())
845
846                    # Show 'hard-mode-only' only over incomplete achievements
847                    # when that's the case.
848                    if hmo:
849                        txtactor = Text(
850                            Lstr(resource='difficultyHardOnlyText'),
851                            host_only=True,
852                            maxwidth=300 * 0.7,
853                            position=(x + 60, y + 5),
854                            transition=Text.Transition.FADE_IN,
855                            vr_depth=-5,
856                            h_attach=h_attach,
857                            v_attach=v_attach,
858                            h_align=Text.HAlign.CENTER,
859                            v_align=Text.VAlign.CENTER,
860                            scale=0.85 * 0.8,
861                            flatness=1.0,
862                            shadow=0.5,
863                            color=(1, 1, 0.6, 1),
864                            transition_delay=delay + 0.05,
865                            transition_out_delay=None).autoretain()
866                        assert txtactor.node
867                        txtactor.node.rotate = 10
868                        objs.append(txtactor)
869
870            objs.append(
871                Text(self.display_name,
872                     host_only=True,
873                     maxwidth=300,
874                     position=(x, y + 2),
875                     transition=Text.Transition.IN_RIGHT,
876                     scale=0.85,
877                     flatness=0.6,
878                     h_attach=h_attach,
879                     v_attach=v_attach,
880                     color=((0.8, 0.93, 0.8, 1.0) if complete else
881                            (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))),
882                     transition_delay=delay + 0.05,
883                     transition_out_delay=None).autoretain())
884            objs.append(
885                Text(self.description_complete
886                     if complete else self.description,
887                     host_only=True,
888                     maxwidth=400,
889                     position=(x, y - 14),
890                     transition=Text.Transition.IN_RIGHT,
891                     vr_depth=-5,
892                     h_attach=h_attach,
893                     v_attach=v_attach,
894                     scale=0.62,
895                     flatness=1.0,
896                     color=((0.6, 0.6, 0.6, 1.0) if complete else
897                            (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))),
898                     transition_delay=delay + 0.1,
899                     transition_out_delay=None).autoretain())
900        return objs

Create a display for the Achievement.

Shows the Achievement icon, name, and description.

def show_completion_banner(self, sound: bool = True) -> None:
 918    def show_completion_banner(self, sound: bool = True) -> None:
 919        """Create the banner/sound for an acquired achievement announcement."""
 920        from ba import _gameutils
 921        from bastd.actor.text import Text
 922        from bastd.actor.image import Image
 923        from ba._general import WeakCall
 924        from ba._language import Lstr
 925        from ba._messages import DieMessage
 926        from ba._generated.enums import TimeType, SpecialChar
 927        app = _ba.app
 928        app.ach.last_achievement_display_time = _ba.time(TimeType.REAL)
 929
 930        # Just piggy-back onto any current activity
 931        # (should we use the session instead?..)
 932        activity = _ba.getactivity(doraise=False)
 933
 934        # If this gets called while this achievement is occupying a slot
 935        # already, ignore it. (probably should never happen in real
 936        # life but whatevs).
 937        if self._completion_banner_slot is not None:
 938            return
 939
 940        if activity is None:
 941            print('show_completion_banner() called with no current activity!')
 942            return
 943
 944        if sound:
 945            _ba.playsound(_ba.getsound('achievement'), host_only=True)
 946        else:
 947            _ba.timer(
 948                0.5,
 949                lambda: _ba.playsound(_ba.getsound('ding'), host_only=True))
 950
 951        in_time = 0.300
 952        out_time = 3.5
 953
 954        base_vr_depth = 200
 955
 956        # Find the first free slot.
 957        i = 0
 958        while True:
 959            if i not in app.ach.achievement_completion_banner_slots:
 960                app.ach.achievement_completion_banner_slots.add(i)
 961                self._completion_banner_slot = i
 962
 963                # Remove us from that slot when we close.
 964                # Use a real-timer in the UI context so the removal runs even
 965                # if our activity/session dies.
 966                with _ba.Context('ui'):
 967                    _ba.timer(in_time + out_time,
 968                              self._remove_banner_slot,
 969                              timetype=TimeType.REAL)
 970                break
 971            i += 1
 972        assert self._completion_banner_slot is not None
 973        y_offs = 110 * self._completion_banner_slot
 974        objs: list[ba.Actor] = []
 975        obj = Image(_ba.gettexture('shadow'),
 976                    position=(-30, 30 + y_offs),
 977                    front=True,
 978                    attach=Image.Attach.BOTTOM_CENTER,
 979                    transition=Image.Transition.IN_BOTTOM,
 980                    vr_depth=base_vr_depth - 100,
 981                    transition_delay=in_time,
 982                    transition_out_delay=out_time,
 983                    color=(0.0, 0.1, 0, 1),
 984                    scale=(1000, 300)).autoretain()
 985        objs.append(obj)
 986        assert obj.node
 987        obj.node.host_only = True
 988        obj = Image(_ba.gettexture('light'),
 989                    position=(-180, 60 + y_offs),
 990                    front=True,
 991                    attach=Image.Attach.BOTTOM_CENTER,
 992                    vr_depth=base_vr_depth,
 993                    transition=Image.Transition.IN_BOTTOM,
 994                    transition_delay=in_time,
 995                    transition_out_delay=out_time,
 996                    color=(1.8, 1.8, 1.0, 0.0),
 997                    scale=(40, 300)).autoretain()
 998        objs.append(obj)
 999        assert obj.node
1000        obj.node.host_only = True
1001        obj.node.premultiplied = True
1002        combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2})
1003        _gameutils.animate(
1004            combine, 'input0', {
1005                in_time: 0,
1006                in_time + 0.4: 30,
1007                in_time + 0.5: 40,
1008                in_time + 0.6: 30,
1009                in_time + 2.0: 0
1010            })
1011        _gameutils.animate(
1012            combine, 'input1', {
1013                in_time: 0,
1014                in_time + 0.4: 200,
1015                in_time + 0.5: 500,
1016                in_time + 0.6: 200,
1017                in_time + 2.0: 0
1018            })
1019        combine.connectattr('output', obj.node, 'scale')
1020        _gameutils.animate(obj.node,
1021                           'rotate', {
1022                               0: 0.0,
1023                               0.35: 360.0
1024                           },
1025                           loop=True)
1026        obj = Image(self.get_icon_texture(True),
1027                    position=(-180, 60 + y_offs),
1028                    attach=Image.Attach.BOTTOM_CENTER,
1029                    front=True,
1030                    vr_depth=base_vr_depth - 10,
1031                    transition=Image.Transition.IN_BOTTOM,
1032                    transition_delay=in_time,
1033                    transition_out_delay=out_time,
1034                    scale=(100, 100)).autoretain()
1035        objs.append(obj)
1036        assert obj.node
1037        obj.node.host_only = True
1038
1039        # Flash.
1040        color = self.get_icon_color(True)
1041        combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3})
1042        keys = {
1043            in_time: 1.0 * color[0],
1044            in_time + 0.4: 1.5 * color[0],
1045            in_time + 0.5: 6.0 * color[0],
1046            in_time + 0.6: 1.5 * color[0],
1047            in_time + 2.0: 1.0 * color[0]
1048        }
1049        _gameutils.animate(combine, 'input0', keys)
1050        keys = {
1051            in_time: 1.0 * color[1],
1052            in_time + 0.4: 1.5 * color[1],
1053            in_time + 0.5: 6.0 * color[1],
1054            in_time + 0.6: 1.5 * color[1],
1055            in_time + 2.0: 1.0 * color[1]
1056        }
1057        _gameutils.animate(combine, 'input1', keys)
1058        keys = {
1059            in_time: 1.0 * color[2],
1060            in_time + 0.4: 1.5 * color[2],
1061            in_time + 0.5: 6.0 * color[2],
1062            in_time + 0.6: 1.5 * color[2],
1063            in_time + 2.0: 1.0 * color[2]
1064        }
1065        _gameutils.animate(combine, 'input2', keys)
1066        combine.connectattr('output', obj.node, 'color')
1067
1068        obj = Image(_ba.gettexture('achievementOutline'),
1069                    model_transparent=_ba.getmodel('achievementOutline'),
1070                    position=(-180, 60 + y_offs),
1071                    front=True,
1072                    attach=Image.Attach.BOTTOM_CENTER,
1073                    vr_depth=base_vr_depth,
1074                    transition=Image.Transition.IN_BOTTOM,
1075                    transition_delay=in_time,
1076                    transition_out_delay=out_time,
1077                    scale=(100, 100)).autoretain()
1078        assert obj.node
1079        obj.node.host_only = True
1080
1081        # Flash.
1082        color = (2, 1.4, 0.4, 1)
1083        combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3})
1084        keys = {
1085            in_time: 1.0 * color[0],
1086            in_time + 0.4: 1.5 * color[0],
1087            in_time + 0.5: 6.0 * color[0],
1088            in_time + 0.6: 1.5 * color[0],
1089            in_time + 2.0: 1.0 * color[0]
1090        }
1091        _gameutils.animate(combine, 'input0', keys)
1092        keys = {
1093            in_time: 1.0 * color[1],
1094            in_time + 0.4: 1.5 * color[1],
1095            in_time + 0.5: 6.0 * color[1],
1096            in_time + 0.6: 1.5 * color[1],
1097            in_time + 2.0: 1.0 * color[1]
1098        }
1099        _gameutils.animate(combine, 'input1', keys)
1100        keys = {
1101            in_time: 1.0 * color[2],
1102            in_time + 0.4: 1.5 * color[2],
1103            in_time + 0.5: 6.0 * color[2],
1104            in_time + 0.6: 1.5 * color[2],
1105            in_time + 2.0: 1.0 * color[2]
1106        }
1107        _gameutils.animate(combine, 'input2', keys)
1108        combine.connectattr('output', obj.node, 'color')
1109        objs.append(obj)
1110
1111        objt = Text(Lstr(value='${A}:',
1112                         subs=[('${A}', Lstr(resource='achievementText'))]),
1113                    position=(-120, 91 + y_offs),
1114                    front=True,
1115                    v_attach=Text.VAttach.BOTTOM,
1116                    vr_depth=base_vr_depth - 10,
1117                    transition=Text.Transition.IN_BOTTOM,
1118                    flatness=0.5,
1119                    transition_delay=in_time,
1120                    transition_out_delay=out_time,
1121                    color=(1, 1, 1, 0.8),
1122                    scale=0.65).autoretain()
1123        objs.append(objt)
1124        assert objt.node
1125        objt.node.host_only = True
1126
1127        objt = Text(self.display_name,
1128                    position=(-120, 50 + y_offs),
1129                    front=True,
1130                    v_attach=Text.VAttach.BOTTOM,
1131                    transition=Text.Transition.IN_BOTTOM,
1132                    vr_depth=base_vr_depth,
1133                    flatness=0.5,
1134                    transition_delay=in_time,
1135                    transition_out_delay=out_time,
1136                    flash=True,
1137                    color=(1, 0.8, 0, 1.0),
1138                    scale=1.5).autoretain()
1139        objs.append(objt)
1140        assert objt.node
1141        objt.node.host_only = True
1142
1143        objt = Text(_ba.charstr(SpecialChar.TICKET),
1144                    position=(-120 - 170 + 5, 75 + y_offs - 20),
1145                    front=True,
1146                    v_attach=Text.VAttach.BOTTOM,
1147                    h_align=Text.HAlign.CENTER,
1148                    v_align=Text.VAlign.CENTER,
1149                    transition=Text.Transition.IN_BOTTOM,
1150                    vr_depth=base_vr_depth,
1151                    transition_delay=in_time,
1152                    transition_out_delay=out_time,
1153                    flash=True,
1154                    color=(0.5, 0.5, 0.5, 1),
1155                    scale=3.0).autoretain()
1156        objs.append(objt)
1157        assert objt.node
1158        objt.node.host_only = True
1159
1160        objt = Text('+' + str(self.get_award_ticket_value()),
1161                    position=(-120 - 180 + 5, 80 + y_offs - 20),
1162                    v_attach=Text.VAttach.BOTTOM,
1163                    front=True,
1164                    h_align=Text.HAlign.CENTER,
1165                    v_align=Text.VAlign.CENTER,
1166                    transition=Text.Transition.IN_BOTTOM,
1167                    vr_depth=base_vr_depth,
1168                    flatness=0.5,
1169                    shadow=1.0,
1170                    transition_delay=in_time,
1171                    transition_out_delay=out_time,
1172                    flash=True,
1173                    color=(0, 1, 0, 1),
1174                    scale=1.5).autoretain()
1175        objs.append(objt)
1176        assert objt.node
1177        objt.node.host_only = True
1178
1179        # Add the 'x 2' if we've got pro.
1180        if app.accounts_v1.have_pro():
1181            objt = Text('x 2',
1182                        position=(-120 - 180 + 45, 80 + y_offs - 50),
1183                        v_attach=Text.VAttach.BOTTOM,
1184                        front=True,
1185                        h_align=Text.HAlign.CENTER,
1186                        v_align=Text.VAlign.CENTER,
1187                        transition=Text.Transition.IN_BOTTOM,
1188                        vr_depth=base_vr_depth,
1189                        flatness=0.5,
1190                        shadow=1.0,
1191                        transition_delay=in_time,
1192                        transition_out_delay=out_time,
1193                        flash=True,
1194                        color=(0.4, 0, 1, 1),
1195                        scale=0.9).autoretain()
1196            objs.append(objt)
1197            assert objt.node
1198            objt.node.host_only = True
1199
1200        objt = Text(self.description_complete,
1201                    position=(-120, 30 + y_offs),
1202                    front=True,
1203                    v_attach=Text.VAttach.BOTTOM,
1204                    transition=Text.Transition.IN_BOTTOM,
1205                    vr_depth=base_vr_depth - 10,
1206                    flatness=0.5,
1207                    transition_delay=in_time,
1208                    transition_out_delay=out_time,
1209                    color=(1.0, 0.7, 0.5, 1.0),
1210                    scale=0.8).autoretain()
1211        objs.append(objt)
1212        assert objt.node
1213        objt.node.host_only = True
1214
1215        for actor in objs:
1216            _ba.timer(out_time + 1.000,
1217                      WeakCall(actor.handlemessage, DieMessage()))

Create the banner/sound for an acquired achievement announcement.

class AchievementSubsystem:
 66class AchievementSubsystem:
 67    """Subsystem for achievement handling.
 68
 69    Category: **App Classes**
 70
 71    Access the single shared instance of this class at 'ba.app.ach'.
 72    """
 73
 74    def __init__(self) -> None:
 75        self.achievements: list[Achievement] = []
 76        self.achievements_to_display: (list[tuple[ba.Achievement, bool]]) = []
 77        self.achievement_display_timer: _ba.Timer | None = None
 78        self.last_achievement_display_time: float = 0.0
 79        self.achievement_completion_banner_slots: set[int] = set()
 80        self._init_achievements()
 81
 82    def _init_achievements(self) -> None:
 83        """Fill in available achievements."""
 84
 85        achs = self.achievements
 86
 87        # 5
 88        achs.append(
 89            Achievement('In Control', 'achievementInControl', (1, 1, 1), '',
 90                        5))
 91        # 15
 92        achs.append(
 93            Achievement('Sharing is Caring', 'achievementSharingIsCaring',
 94                        (1, 1, 1), '', 15))
 95        # 10
 96        achs.append(
 97            Achievement('Dual Wielding', 'achievementDualWielding', (1, 1, 1),
 98                        '', 10))
 99
100        # 10
101        achs.append(
102            Achievement('Free Loader', 'achievementFreeLoader', (1, 1, 1), '',
103                        10))
104        # 20
105        achs.append(
106            Achievement('Team Player', 'achievementTeamPlayer', (1, 1, 1), '',
107                        20))
108
109        # 5
110        achs.append(
111            Achievement('Onslaught Training Victory', 'achievementOnslaught',
112                        (1, 1, 1), 'Default:Onslaught Training', 5))
113        # 5
114        achs.append(
115            Achievement('Off You Go Then', 'achievementOffYouGo',
116                        (1, 1.1, 1.3), 'Default:Onslaught Training', 5))
117        # 10
118        achs.append(
119            Achievement('Boxer',
120                        'achievementBoxer', (1, 0.6, 0.6),
121                        'Default:Onslaught Training',
122                        10,
123                        hard_mode_only=True))
124
125        # 10
126        achs.append(
127            Achievement('Rookie Onslaught Victory', 'achievementOnslaught',
128                        (0.5, 1.4, 0.6), 'Default:Rookie Onslaught', 10))
129        # 10
130        achs.append(
131            Achievement('Mine Games', 'achievementMine', (1, 1, 1.4),
132                        'Default:Rookie Onslaught', 10))
133        # 15
134        achs.append(
135            Achievement('Flawless Victory',
136                        'achievementFlawlessVictory', (1, 1, 1),
137                        'Default:Rookie Onslaught',
138                        15,
139                        hard_mode_only=True))
140
141        # 10
142        achs.append(
143            Achievement('Rookie Football Victory',
144                        'achievementFootballVictory', (1.0, 1, 0.6),
145                        'Default:Rookie Football', 10))
146        # 10
147        achs.append(
148            Achievement('Super Punch', 'achievementSuperPunch', (1, 1, 1.8),
149                        'Default:Rookie Football', 10))
150        # 15
151        achs.append(
152            Achievement('Rookie Football Shutout',
153                        'achievementFootballShutout', (1, 1, 1),
154                        'Default:Rookie Football',
155                        15,
156                        hard_mode_only=True))
157
158        # 15
159        achs.append(
160            Achievement('Pro Onslaught Victory', 'achievementOnslaught',
161                        (0.3, 1, 2.0), 'Default:Pro Onslaught', 15))
162        # 15
163        achs.append(
164            Achievement('Boom Goes the Dynamite', 'achievementTNT',
165                        (1.4, 1.2, 0.8), 'Default:Pro Onslaught', 15))
166        # 20
167        achs.append(
168            Achievement('Pro Boxer',
169                        'achievementBoxer', (2, 2, 0),
170                        'Default:Pro Onslaught',
171                        20,
172                        hard_mode_only=True))
173
174        # 15
175        achs.append(
176            Achievement('Pro Football Victory', 'achievementFootballVictory',
177                        (1.3, 1.3, 2.0), 'Default:Pro Football', 15))
178        # 15
179        achs.append(
180            Achievement('Super Mega Punch', 'achievementSuperPunch',
181                        (2, 1, 0.6), 'Default:Pro Football', 15))
182        # 20
183        achs.append(
184            Achievement('Pro Football Shutout',
185                        'achievementFootballShutout', (0.7, 0.7, 2.0),
186                        'Default:Pro Football',
187                        20,
188                        hard_mode_only=True))
189
190        # 15
191        achs.append(
192            Achievement('Pro Runaround Victory', 'achievementRunaround',
193                        (1, 1, 1), 'Default:Pro Runaround', 15))
194        # 20
195        achs.append(
196            Achievement('Precision Bombing',
197                        'achievementCrossHair', (1, 1, 1.3),
198                        'Default:Pro Runaround',
199                        20,
200                        hard_mode_only=True))
201        # 25
202        achs.append(
203            Achievement('The Wall',
204                        'achievementWall', (1, 0.7, 0.7),
205                        'Default:Pro Runaround',
206                        25,
207                        hard_mode_only=True))
208
209        # 30
210        achs.append(
211            Achievement('Uber Onslaught Victory', 'achievementOnslaught',
212                        (2, 2, 1), 'Default:Uber Onslaught', 30))
213        # 30
214        achs.append(
215            Achievement('Gold Miner',
216                        'achievementMine', (2, 1.6, 0.2),
217                        'Default:Uber Onslaught',
218                        30,
219                        hard_mode_only=True))
220        # 30
221        achs.append(
222            Achievement('TNT Terror',
223                        'achievementTNT', (2, 1.8, 0.3),
224                        'Default:Uber Onslaught',
225                        30,
226                        hard_mode_only=True))
227
228        # 30
229        achs.append(
230            Achievement('Uber Football Victory', 'achievementFootballVictory',
231                        (1.8, 1.4, 0.3), 'Default:Uber Football', 30))
232        # 30
233        achs.append(
234            Achievement('Got the Moves',
235                        'achievementGotTheMoves', (2, 1, 0),
236                        'Default:Uber Football',
237                        30,
238                        hard_mode_only=True))
239        # 40
240        achs.append(
241            Achievement('Uber Football Shutout',
242                        'achievementFootballShutout', (2, 2, 0),
243                        'Default:Uber Football',
244                        40,
245                        hard_mode_only=True))
246
247        # 30
248        achs.append(
249            Achievement('Uber Runaround Victory', 'achievementRunaround',
250                        (1.5, 1.2, 0.2), 'Default:Uber Runaround', 30))
251        # 40
252        achs.append(
253            Achievement('The Great Wall',
254                        'achievementWall', (2, 1.7, 0.4),
255                        'Default:Uber Runaround',
256                        40,
257                        hard_mode_only=True))
258        # 40
259        achs.append(
260            Achievement('Stayin\' Alive',
261                        'achievementStayinAlive', (2, 2, 1),
262                        'Default:Uber Runaround',
263                        40,
264                        hard_mode_only=True))
265
266        # 20
267        achs.append(
268            Achievement('Last Stand Master',
269                        'achievementMedalSmall', (2, 1.5, 0.3),
270                        'Default:The Last Stand',
271                        20,
272                        hard_mode_only=True))
273        # 40
274        achs.append(
275            Achievement('Last Stand Wizard',
276                        'achievementMedalMedium', (2, 1.5, 0.3),
277                        'Default:The Last Stand',
278                        40,
279                        hard_mode_only=True))
280        # 60
281        achs.append(
282            Achievement('Last Stand God',
283                        'achievementMedalLarge', (2, 1.5, 0.3),
284                        'Default:The Last Stand',
285                        60,
286                        hard_mode_only=True))
287
288        # 5
289        achs.append(
290            Achievement('Onslaught Master', 'achievementMedalSmall',
291                        (0.7, 1, 0.7), 'Challenges:Infinite Onslaught', 5))
292        # 15
293        achs.append(
294            Achievement('Onslaught Wizard', 'achievementMedalMedium',
295                        (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 15))
296        # 30
297        achs.append(
298            Achievement('Onslaught God', 'achievementMedalLarge',
299                        (0.7, 1.0, 0.7), 'Challenges:Infinite Onslaught', 30))
300
301        # 5
302        achs.append(
303            Achievement('Runaround Master', 'achievementMedalSmall',
304                        (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 5))
305        # 15
306        achs.append(
307            Achievement('Runaround Wizard', 'achievementMedalMedium',
308                        (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 15))
309        # 30
310        achs.append(
311            Achievement('Runaround God', 'achievementMedalLarge',
312                        (1.0, 1.0, 1.2), 'Challenges:Infinite Runaround', 30))
313
314    def award_local_achievement(self, achname: str) -> None:
315        """For non-game-based achievements such as controller-connection."""
316        try:
317            ach = self.get_achievement(achname)
318            if not ach.complete:
319
320                # Report new achievements to the game-service.
321                _ba.report_achievement(achname)
322
323                # And to our account.
324                _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname})
325
326                # Now attempt to show a banner.
327                self.display_achievement_banner(achname)
328
329        except Exception:
330            print_exception()
331
332    def display_achievement_banner(self, achname: str) -> None:
333        """Display a completion banner for an achievement.
334
335        (internal)
336
337        Used for server-driven achievements.
338        """
339        try:
340            # FIXME: Need to get these using the UI context or some other
341            #  purely local context somehow instead of trying to inject these
342            #  into whatever activity happens to be active
343            #  (since that won't work while in client mode).
344            activity = _ba.get_foreground_host_activity()
345            if activity is not None:
346                with _ba.Context(activity):
347                    self.get_achievement(achname).announce_completion()
348        except Exception:
349            print_exception('error showing server ach')
350
351    def set_completed_achievements(self, achs: Sequence[str]) -> None:
352        """Set the current state of completed achievements.
353
354        (internal)
355
356        All achievements not included here will be set incomplete.
357        """
358
359        # Note: This gets called whenever game-center/game-circle/etc tells
360        # us which achievements we currently have.  We always defer to them,
361        # even if that means we have to un-set an achievement we think we have.
362
363        cfg = _ba.app.config
364        cfg['Achievements'] = {}
365        for a_name in achs:
366            self.get_achievement(a_name).set_complete(True)
367        cfg.commit()
368
369    def get_achievement(self, name: str) -> Achievement:
370        """Return an Achievement by name."""
371        achs = [a for a in self.achievements if a.name == name]
372        assert len(achs) < 2
373        if not achs:
374            raise ValueError("Invalid achievement name: '" + name + "'")
375        return achs[0]
376
377    def achievements_for_coop_level(self,
378                                    level_name: str) -> list[Achievement]:
379        """Given a level name, return achievements available for it."""
380
381        # For the Easy campaign we return achievements for the Default
382        # campaign too. (want the user to see what achievements are part of the
383        # level even if they can't unlock them all on easy mode).
384        return [
385            a for a in self.achievements
386            if a.level_name in (level_name,
387                                level_name.replace('Easy', 'Default'))
388        ]
389
390    def _test(self) -> None:
391        """For testing achievement animations."""
392        from ba._generated.enums import TimeType
393
394        def testcall1() -> None:
395            self.achievements[0].announce_completion()
396            self.achievements[1].announce_completion()
397            self.achievements[2].announce_completion()
398
399        def testcall2() -> None:
400            self.achievements[3].announce_completion()
401            self.achievements[4].announce_completion()
402            self.achievements[5].announce_completion()
403
404        _ba.timer(3.0, testcall1, timetype=TimeType.BASE)
405        _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()
74    def __init__(self) -> None:
75        self.achievements: list[Achievement] = []
76        self.achievements_to_display: (list[tuple[ba.Achievement, bool]]) = []
77        self.achievement_display_timer: _ba.Timer | None = None
78        self.last_achievement_display_time: float = 0.0
79        self.achievement_completion_banner_slots: set[int] = set()
80        self._init_achievements()
def award_local_achievement(self, achname: str) -> None:
314    def award_local_achievement(self, achname: str) -> None:
315        """For non-game-based achievements such as controller-connection."""
316        try:
317            ach = self.get_achievement(achname)
318            if not ach.complete:
319
320                # Report new achievements to the game-service.
321                _ba.report_achievement(achname)
322
323                # And to our account.
324                _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname})
325
326                # Now attempt to show a banner.
327                self.display_achievement_banner(achname)
328
329        except Exception:
330            print_exception()

For non-game-based achievements such as controller-connection.

def get_achievement(self, name: str) -> ba.Achievement:
369    def get_achievement(self, name: str) -> Achievement:
370        """Return an Achievement by name."""
371        achs = [a for a in self.achievements if a.name == name]
372        assert len(achs) < 2
373        if not achs:
374            raise ValueError("Invalid achievement name: '" + name + "'")
375        return achs[0]

Return an Achievement by name.

def achievements_for_coop_level(self, level_name: str) -> list[ba.Achievement]:
377    def achievements_for_coop_level(self,
378                                    level_name: str) -> list[Achievement]:
379        """Given a level name, return achievements available for it."""
380
381        # For the Easy campaign we return achievements for the Default
382        # campaign too. (want the user to see what achievements are part of the
383        # level even if they can't unlock them all on easy mode).
384        return [
385            a for a in self.achievements
386            if a.level_name in (level_name,
387                                level_name.replace('Easy', 'Default'))
388        ]

Given a level name, return achievements available for it.

class Activity(ba.DependencyComponent, typing.Generic[~PlayerType, ~TeamType]):
 29class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
 30    """Units of execution wrangled by a ba.Session.
 31
 32    Category: Gameplay Classes
 33
 34    Examples of Activities include games, score-screens, cutscenes, etc.
 35    A ba.Session has one 'current' Activity at any time, though their existence
 36    can overlap during transitions.
 37    """
 38
 39    # pylint: disable=too-many-public-methods
 40
 41    settings_raw: dict[str, Any]
 42    """The settings dict passed in when the activity was made.
 43       This attribute is deprecated and should be avoided when possible;
 44       activities should pull all values they need from the 'settings' arg
 45       passed to the Activity __init__ call."""
 46
 47    teams: list[TeamType]
 48    """The list of ba.Team-s in the Activity. This gets populated just
 49       before on_begin() is called and is updated automatically as players
 50       join or leave the game. (at least in free-for-all mode where every
 51       player gets their own team; in teams mode there are always 2 teams
 52       regardless of the player count)."""
 53
 54    players: list[PlayerType]
 55    """The list of ba.Player-s in the Activity. This gets populated just
 56       before on_begin() is called and is updated automatically as players
 57       join or leave the game."""
 58
 59    announce_player_deaths = False
 60    """Whether to print every time a player dies. This can be pertinent
 61       in games such as Death-Match but can be annoying in games where it
 62       doesn't matter."""
 63
 64    is_joining_activity = False
 65    """Joining activities are for waiting for initial player joins.
 66       They are treated slightly differently than regular activities,
 67       mainly in that all players are passed to the activity at once
 68       instead of as each joins."""
 69
 70    allow_pausing = False
 71    """Whether game-time should still progress when in menus/etc."""
 72
 73    allow_kick_idle_players = True
 74    """Whether idle players can potentially be kicked (should not happen in
 75       menus/etc)."""
 76
 77    use_fixed_vr_overlay = False
 78    """In vr mode, this determines whether overlay nodes (text, images, etc)
 79       are created at a fixed position in space or one that moves based on
 80       the current map. Generally this should be on for games and off for
 81       transitions/score-screens/etc. that persist between maps."""
 82
 83    slow_motion = False
 84    """If True, runs in slow motion and turns down sound pitch."""
 85
 86    inherits_slow_motion = False
 87    """Set this to True to inherit slow motion setting from previous
 88       activity (useful for transitions to avoid hitches)."""
 89
 90    inherits_music = False
 91    """Set this to True to keep playing the music from the previous activity
 92       (without even restarting it)."""
 93
 94    inherits_vr_camera_offset = False
 95    """Set this to true to inherit VR camera offsets from the previous
 96       activity (useful for preventing sporadic camera movement
 97       during transitions)."""
 98
 99    inherits_vr_overlay_center = False
100    """Set this to true to inherit (non-fixed) VR overlay positioning from
101       the previous activity (useful for prevent sporadic overlay jostling
102       during transitions)."""
103
104    inherits_tint = False
105    """Set this to true to inherit screen tint/vignette colors from the
106       previous activity (useful to prevent sudden color changes during
107       transitions)."""
108
109    allow_mid_activity_joins: bool = True
110    """Whether players should be allowed to join in the middle of this
111       activity. Note that Sessions may not allow mid-activity-joins even
112       if the activity says its ok."""
113
114    transition_time = 0.0
115    """If the activity fades or transitions in, it should set the length of
116       time here so that previous activities will be kept alive for that
117       long (avoiding 'holes' in the screen)
118       This value is given in real-time seconds."""
119
120    can_show_ad_on_death = False
121    """Is it ok to show an ad after this activity ends before showing
122       the next activity?"""
123
124    def __init__(self, settings: dict):
125        """Creates an Activity in the current ba.Session.
126
127        The activity will not be actually run until ba.Session.setactivity
128        is called. 'settings' should be a dict of key/value pairs specific
129        to the activity.
130
131        Activities should preload as much of their media/etc as possible in
132        their constructor, but none of it should actually be used until they
133        are transitioned in.
134        """
135        super().__init__()
136
137        # Create our internal engine data.
138        self._activity_data = _ba.register_activity(self)
139
140        assert isinstance(settings, dict)
141        assert _ba.getactivity() is self
142
143        self._globalsnode: ba.Node | None = None
144
145        # Player/Team types should have been specified as type args;
146        # grab those.
147        self._playertype: type[PlayerType]
148        self._teamtype: type[TeamType]
149        self._setup_player_and_team_types()
150
151        # FIXME: Relocate or remove the need for this stuff.
152        self.paused_text: ba.Actor | None = None
153
154        self._session = weakref.ref(_ba.getsession())
155
156        # Preloaded data for actors, maps, etc; indexed by type.
157        self.preloads: dict[type, Any] = {}
158
159        # Hopefully can eventually kill this; activities should
160        # validate/store whatever settings they need at init time
161        # (in a more type-safe way).
162        self.settings_raw = settings
163
164        self._has_transitioned_in = False
165        self._has_begun = False
166        self._has_ended = False
167        self._activity_death_check_timer: ba.Timer | None = None
168        self._expired = False
169        self._delay_delete_players: list[PlayerType] = []
170        self._delay_delete_teams: list[TeamType] = []
171        self._players_that_left: list[weakref.ref[PlayerType]] = []
172        self._teams_that_left: list[weakref.ref[TeamType]] = []
173        self._transitioning_out = False
174
175        # A handy place to put most actors; this list is pruned of dead
176        # actors regularly and these actors are insta-killed as the activity
177        # is dying.
178        self._actor_refs: list[ba.Actor] = []
179        self._actor_weak_refs: list[weakref.ref[ba.Actor]] = []
180        self._last_prune_dead_actors_time = _ba.time()
181        self._prune_dead_actors_timer: ba.Timer | None = None
182
183        self.teams = []
184        self.players = []
185
186        self.lobby = None
187        self._stats: ba.Stats | None = None
188        self._customdata: dict | None = {}
189
190    def __del__(self) -> None:
191
192        # If the activity has been run then we should have already cleaned
193        # it up, but we still need to run expire calls for un-run activities.
194        if not self._expired:
195            with _ba.Context('empty'):
196                self._expire()
197
198        # Inform our owner that we officially kicked the bucket.
199        if self._transitioning_out:
200            session = self._session()
201            if session is not None:
202                _ba.pushcall(
203                    Call(session.transitioning_out_activity_was_freed,
204                         self.can_show_ad_on_death))
205
206    @property
207    def globalsnode(self) -> ba.Node:
208        """The 'globals' ba.Node for the activity. This contains various
209        global controls and values.
210        """
211        node = self._globalsnode
212        if not node:
213            raise NodeNotFoundError()
214        return node
215
216    @property
217    def stats(self) -> ba.Stats:
218        """The stats instance accessible while the activity is running.
219
220        If access is attempted before or after, raises a ba.NotFoundError.
221        """
222        if self._stats is None:
223            from ba._error import NotFoundError
224            raise NotFoundError()
225        return self._stats
226
227    def on_expire(self) -> None:
228        """Called when your activity is being expired.
229
230        If your activity has created anything explicitly that may be retaining
231        a strong reference to the activity and preventing it from dying, you
232        should clear that out here. From this point on your activity's sole
233        purpose in life is to hit zero references and die so the next activity
234        can begin.
235        """
236
237    @property
238    def customdata(self) -> dict:
239        """Entities needing to store simple data with an activity can put it
240        here. This dict will be deleted when the activity expires, so contained
241        objects generally do not need to worry about handling expired
242        activities.
243        """
244        assert not self._expired
245        assert isinstance(self._customdata, dict)
246        return self._customdata
247
248    @property
249    def expired(self) -> bool:
250        """Whether the activity is expired.
251
252        An activity is set as expired when shutting down.
253        At this point no new nodes, timers, etc should be made,
254        run, etc, and the activity should be considered a 'zombie'.
255        """
256        return self._expired
257
258    @property
259    def playertype(self) -> type[PlayerType]:
260        """The type of ba.Player this Activity is using."""
261        return self._playertype
262
263    @property
264    def teamtype(self) -> type[TeamType]:
265        """The type of ba.Team this Activity is using."""
266        return self._teamtype
267
268    def set_has_ended(self, val: bool) -> None:
269        """(internal)"""
270        self._has_ended = val
271
272    def expire(self) -> None:
273        """Begin the process of tearing down the activity.
274
275        (internal)
276        """
277        from ba._generated.enums import TimeType
278
279        # Create a real-timer that watches a weak-ref of this activity
280        # and reports any lingering references keeping it alive.
281        # We store the timer on the activity so as soon as the activity dies
282        # it gets cleaned up.
283        with _ba.Context('ui'):
284            ref = weakref.ref(self)
285            self._activity_death_check_timer = _ba.Timer(
286                5.0,
287                Call(self._check_activity_death, ref, [0]),
288                repeat=True,
289                timetype=TimeType.REAL)
290
291        # Run _expire in an empty context; nothing should be happening in
292        # there except deleting things which requires no context.
293        # (plus, _expire() runs in the destructor for un-run activities
294        # and we can't properly provide context in that situation anyway; might
295        # as well be consistent).
296        if not self._expired:
297            with _ba.Context('empty'):
298                self._expire()
299        else:
300            raise RuntimeError(f'destroy() called when'
301                               f' already expired for {self}')
302
303    def retain_actor(self, actor: ba.Actor) -> None:
304        """Add a strong-reference to a ba.Actor to this Activity.
305
306        The reference will be lazily released once ba.Actor.exists()
307        returns False for the Actor. The ba.Actor.autoretain() method
308        is a convenient way to access this same functionality.
309        """
310        if __debug__:
311            from ba._actor import Actor
312            assert isinstance(actor, Actor)
313        self._actor_refs.append(actor)
314
315    def add_actor_weak_ref(self, actor: ba.Actor) -> None:
316        """Add a weak-reference to a ba.Actor to the ba.Activity.
317
318        (called by the ba.Actor base class)
319        """
320        if __debug__:
321            from ba._actor import Actor
322            assert isinstance(actor, Actor)
323        self._actor_weak_refs.append(weakref.ref(actor))
324
325    @property
326    def session(self) -> ba.Session:
327        """The ba.Session this ba.Activity belongs go.
328
329        Raises a ba.SessionNotFoundError if the Session no longer exists.
330        """
331        session = self._session()
332        if session is None:
333            from ba._error import SessionNotFoundError
334            raise SessionNotFoundError()
335        return session
336
337    def on_player_join(self, player: PlayerType) -> None:
338        """Called when a new ba.Player has joined the Activity.
339
340        (including the initial set of Players)
341        """
342
343    def on_player_leave(self, player: PlayerType) -> None:
344        """Called when a ba.Player is leaving the Activity."""
345
346    def on_team_join(self, team: TeamType) -> None:
347        """Called when a new ba.Team joins the Activity.
348
349        (including the initial set of Teams)
350        """
351
352    def on_team_leave(self, team: TeamType) -> None:
353        """Called when a ba.Team leaves the Activity."""
354
355    def on_transition_in(self) -> None:
356        """Called when the Activity is first becoming visible.
357
358        Upon this call, the Activity should fade in backgrounds,
359        start playing music, etc. It does not yet have access to players
360        or teams, however. They remain owned by the previous Activity
361        up until ba.Activity.on_begin() is called.
362        """
363
364    def on_transition_out(self) -> None:
365        """Called when your activity begins transitioning out.
366
367        Note that this may happen at any time even if ba.Activity.end() has
368        not been called.
369        """
370
371    def on_begin(self) -> None:
372        """Called once the previous ba.Activity has finished transitioning out.
373
374        At this point the activity's initial players and teams are filled in
375        and it should begin its actual game logic.
376        """
377
378    def handlemessage(self, msg: Any) -> Any:
379        """General message handling; can be passed any message object."""
380        del msg  # Unused arg.
381        return UNHANDLED
382
383    def has_transitioned_in(self) -> bool:
384        """Return whether ba.Activity.on_transition_in()
385         has been called."""
386        return self._has_transitioned_in
387
388    def has_begun(self) -> bool:
389        """Return whether ba.Activity.on_begin() has been called."""
390        return self._has_begun
391
392    def has_ended(self) -> bool:
393        """Return whether the activity has commenced ending."""
394        return self._has_ended
395
396    def is_transitioning_out(self) -> bool:
397        """Return whether ba.Activity.on_transition_out() has been called."""
398        return self._transitioning_out
399
400    def transition_in(self, prev_globals: ba.Node | None) -> None:
401        """Called by Session to kick off transition-in.
402
403        (internal)
404        """
405        assert not self._has_transitioned_in
406        self._has_transitioned_in = True
407
408        # Set up the globals node based on our settings.
409        with _ba.Context(self):
410            glb = self._globalsnode = _ba.newnode('globals')
411
412            # Now that it's going to be front and center,
413            # set some global values based on what the activity wants.
414            glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay
415            glb.allow_kick_idle_players = self.allow_kick_idle_players
416            if self.inherits_slow_motion and prev_globals is not None:
417                glb.slow_motion = prev_globals.slow_motion
418            else:
419                glb.slow_motion = self.slow_motion
420            if self.inherits_music and prev_globals is not None:
421                glb.music_continuous = True  # Prevent restarting same music.
422                glb.music = prev_globals.music
423                glb.music_count += 1
424            if self.inherits_vr_camera_offset and prev_globals is not None:
425                glb.vr_camera_offset = prev_globals.vr_camera_offset
426            if self.inherits_vr_overlay_center and prev_globals is not None:
427                glb.vr_overlay_center = prev_globals.vr_overlay_center
428                glb.vr_overlay_center_enabled = (
429                    prev_globals.vr_overlay_center_enabled)
430
431            # If they want to inherit tint from the previous self.
432            if self.inherits_tint and prev_globals is not None:
433                glb.tint = prev_globals.tint
434                glb.vignette_outer = prev_globals.vignette_outer
435                glb.vignette_inner = prev_globals.vignette_inner
436
437            # Start pruning our various things periodically.
438            self._prune_dead_actors()
439            self._prune_dead_actors_timer = _ba.Timer(5.17,
440                                                      self._prune_dead_actors,
441                                                      repeat=True)
442
443            _ba.timer(13.3, self._prune_delay_deletes, repeat=True)
444
445            # Also start our low-level scene running.
446            self._activity_data.start()
447
448            try:
449                self.on_transition_in()
450            except Exception:
451                print_exception(f'Error in on_transition_in for {self}.')
452
453        # Tell the C++ layer that this activity is the main one, so it uses
454        # settings from our globals, directs various events to us, etc.
455        self._activity_data.make_foreground()
456
457    def transition_out(self) -> None:
458        """Called by the Session to start us transitioning out."""
459        assert not self._transitioning_out
460        self._transitioning_out = True
461        with _ba.Context(self):
462            try:
463                self.on_transition_out()
464            except Exception:
465                print_exception(f'Error in on_transition_out for {self}.')
466
467    def begin(self, session: ba.Session) -> None:
468        """Begin the activity.
469
470        (internal)
471        """
472
473        assert not self._has_begun
474
475        # Inherit stats from the session.
476        self._stats = session.stats
477
478        # Add session's teams in.
479        for team in session.sessionteams:
480            self.add_team(team)
481
482        # Add session's players in.
483        for player in session.sessionplayers:
484            self.add_player(player)
485
486        self._has_begun = True
487
488        # Let the activity do its thing.
489        with _ba.Context(self):
490            # Note: do we want to catch errors here?
491            # Currently I believe we wind up canceling the
492            # activity launch; just wanna be sure that is intentional.
493            self.on_begin()
494
495    def end(self,
496            results: Any = None,
497            delay: float = 0.0,
498            force: bool = False) -> None:
499        """Commences Activity shutdown and delivers results to the ba.Session.
500
501        'delay' is the time delay before the Activity actually ends
502        (in seconds). Further calls to end() will be ignored up until
503        this time, unless 'force' is True, in which case the new results
504        will replace the old.
505        """
506
507        # Ask the session to end us.
508        self.session.end_activity(self, results, delay, force)
509
510    def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
511        """Create the Player instance for this Activity.
512
513        Subclasses can override this if the activity's player class
514        requires a custom constructor; otherwise it will be called with
515        no args. Note that the player object should not be used at this
516        point as it is not yet fully wired up; wait for
517        ba.Activity.on_player_join() for that.
518        """
519        del sessionplayer  # Unused.
520        player = self._playertype()
521        return player
522
523    def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
524        """Create the Team instance for this Activity.
525
526        Subclasses can override this if the activity's team class
527        requires a custom constructor; otherwise it will be called with
528        no args. Note that the team object should not be used at this
529        point as it is not yet fully wired up; wait for on_team_join()
530        for that.
531        """
532        del sessionteam  # Unused.
533        team = self._teamtype()
534        return team
535
536    def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
537        """(internal)"""
538        assert sessionplayer.sessionteam is not None
539        sessionplayer.resetinput()
540        sessionteam = sessionplayer.sessionteam
541        assert sessionplayer in sessionteam.players
542        team = sessionteam.activityteam
543        assert team is not None
544        sessionplayer.setactivity(self)
545        with _ba.Context(self):
546            sessionplayer.activityplayer = player = self.create_player(
547                sessionplayer)
548            player.postinit(sessionplayer)
549
550            assert player not in team.players
551            team.players.append(player)
552            assert player in team.players
553
554            assert player not in self.players
555            self.players.append(player)
556            assert player in self.players
557
558            try:
559                self.on_player_join(player)
560            except Exception:
561                print_exception(f'Error in on_player_join for {self}.')
562
563    def remove_player(self, sessionplayer: ba.SessionPlayer) -> None:
564        """Remove a player from the Activity while it is running.
565
566        (internal)
567        """
568        assert not self.expired
569
570        player: Any = sessionplayer.activityplayer
571        assert isinstance(player, self._playertype)
572        team: Any = sessionplayer.sessionteam.activityteam
573        assert isinstance(team, self._teamtype)
574
575        assert player in team.players
576        team.players.remove(player)
577        assert player not in team.players
578
579        assert player in self.players
580        self.players.remove(player)
581        assert player not in self.players
582
583        # This should allow our ba.Player instance to die.
584        # Complain if that doesn't happen.
585        # verify_object_death(player)
586
587        with _ba.Context(self):
588            try:
589                self.on_player_leave(player)
590            except Exception:
591                print_exception(f'Error in on_player_leave for {self}.')
592            try:
593                player.leave()
594            except Exception:
595                print_exception(f'Error on leave for {player} in {self}.')
596
597            self._reset_session_player_for_no_activity(sessionplayer)
598
599        # Add the player to a list to keep it around for a while. This is
600        # to discourage logic from firing on player object death, which
601        # may not happen until activity end if something is holding refs
602        # to it.
603        self._delay_delete_players.append(player)
604        self._players_that_left.append(weakref.ref(player))
605
606    def add_team(self, sessionteam: ba.SessionTeam) -> None:
607        """Add a team to the Activity
608
609        (internal)
610        """
611        assert not self.expired
612
613        with _ba.Context(self):
614            sessionteam.activityteam = team = self.create_team(sessionteam)
615            team.postinit(sessionteam)
616            self.teams.append(team)
617            try:
618                self.on_team_join(team)
619            except Exception:
620                print_exception(f'Error in on_team_join for {self}.')
621
622    def remove_team(self, sessionteam: ba.SessionTeam) -> None:
623        """Remove a team from a Running Activity
624
625        (internal)
626        """
627        assert not self.expired
628        assert sessionteam.activityteam is not None
629
630        team: Any = sessionteam.activityteam
631        assert isinstance(team, self._teamtype)
632
633        assert team in self.teams
634        self.teams.remove(team)
635        assert team not in self.teams
636
637        with _ba.Context(self):
638            # Make a decent attempt to persevere if user code breaks.
639            try:
640                self.on_team_leave(team)
641            except Exception:
642                print_exception(f'Error in on_team_leave for {self}.')
643            try:
644                team.leave()
645            except Exception:
646                print_exception(f'Error on leave for {team} in {self}.')
647
648            sessionteam.activityteam = None
649
650        # Add the team to a list to keep it around for a while. This is
651        # to discourage logic from firing on team object death, which
652        # may not happen until activity end if something is holding refs
653        # to it.
654        self._delay_delete_teams.append(team)
655        self._teams_that_left.append(weakref.ref(team))
656
657    def _reset_session_player_for_no_activity(
658            self, sessionplayer: ba.SessionPlayer) -> None:
659
660        # Let's be extra-defensive here: killing a node/input-call/etc
661        # could trigger user-code resulting in errors, but we would still
662        # like to complete the reset if possible.
663        try:
664            sessionplayer.setnode(None)
665        except Exception:
666            print_exception(
667                f'Error resetting SessionPlayer node on {sessionplayer}'
668                f' for {self}.')
669        try:
670            sessionplayer.resetinput()
671        except Exception:
672            print_exception(
673                f'Error resetting SessionPlayer input on {sessionplayer}'
674                f' for {self}.')
675
676        # These should never fail I think...
677        sessionplayer.setactivity(None)
678        sessionplayer.activityplayer = None
679
680    # noinspection PyUnresolvedReferences
681    def _setup_player_and_team_types(self) -> None:
682        """Pull player and team types from our typing.Generic params."""
683
684        # TODO: There are proper calls for pulling these in Python 3.8;
685        # should update this code when we adopt that.
686        # NOTE: If we get Any as PlayerType or TeamType (generally due
687        # to no generic params being passed) we automatically use the
688        # base class types, but also warn the user since this will mean
689        # less type safety for that class. (its better to pass the base
690        # player/team types explicitly vs. having them be Any)
691        if not TYPE_CHECKING:
692            self._playertype = type(self).__orig_bases__[-1].__args__[0]
693            if not isinstance(self._playertype, type):
694                self._playertype = Player
695                print(f'ERROR: {type(self)} was not passed a Player'
696                      f' type argument; please explicitly pass ba.Player'
697                      f' if you do not want to override it.')
698            self._teamtype = type(self).__orig_bases__[-1].__args__[1]
699            if not isinstance(self._teamtype, type):
700                self._teamtype = Team
701                print(f'ERROR: {type(self)} was not passed a Team'
702                      f' type argument; please explicitly pass ba.Team'
703                      f' if you do not want to override it.')
704        assert issubclass(self._playertype, Player)
705        assert issubclass(self._teamtype, Team)
706
707    @classmethod
708    def _check_activity_death(cls, activity_ref: weakref.ref[Activity],
709                              counter: list[int]) -> None:
710        """Sanity check to make sure an Activity was destroyed properly.
711
712        Receives a weakref to a ba.Activity which should have torn itself
713        down due to no longer being referenced anywhere. Will complain
714        and/or print debugging info if the Activity still exists.
715        """
716        try:
717            import gc
718            import types
719            activity = activity_ref()
720            print('ERROR: Activity is not dying when expected:', activity,
721                  '(warning ' + str(counter[0] + 1) + ')')
722            print('This means something is still strong-referencing it.')
723            counter[0] += 1
724
725            # FIXME: Running the code below shows us references but winds up
726            #  keeping the object alive; need to figure out why.
727            #  For now we just print refs if the count gets to 3, and then we
728            #  kill the app at 4 so it doesn't matter anyway.
729            if counter[0] == 3:
730                print('Activity references for', activity, ':')
731                refs = list(gc.get_referrers(activity))
732                i = 1
733                for ref in refs:
734                    if isinstance(ref, types.FrameType):
735                        continue
736                    print('  reference', i, ':', ref)
737                    i += 1
738            if counter[0] == 4:
739                print('Killing app due to stuck activity... :-(')
740                _ba.quit()
741
742        except Exception:
743            print_exception('Error on _check_activity_death/')
744
745    def _expire(self) -> None:
746        """Put the activity in a state where it can be garbage-collected.
747
748        This involves clearing anything that might be holding a reference
749        to it, etc.
750        """
751        assert not self._expired
752        self._expired = True
753
754        try:
755            self.on_expire()
756        except Exception:
757            print_exception(f'Error in Activity on_expire() for {self}.')
758
759        try:
760            self._customdata = None
761        except Exception:
762            print_exception(f'Error clearing customdata for {self}.')
763
764        # Don't want to be holding any delay-delete refs at this point.
765        self._prune_delay_deletes()
766
767        self._expire_actors()
768        self._expire_players()
769        self._expire_teams()
770
771        # This will kill all low level stuff: Timers, Nodes, etc., which
772        # should clear up any remaining refs to our Activity and allow us
773        # to die peacefully.
774        try:
775            self._activity_data.expire()
776        except Exception:
777            print_exception(f'Error expiring _activity_data for {self}.')
778
779    def _expire_actors(self) -> None:
780        # Expire all Actors.
781        for actor_ref in self._actor_weak_refs:
782            actor = actor_ref()
783            if actor is not None:
784                verify_object_death(actor)
785                try:
786                    actor.on_expire()
787                except Exception:
788                    print_exception(f'Error in Actor.on_expire()'
789                                    f' for {actor_ref()}.')
790
791    def _expire_players(self) -> None:
792
793        # Issue warnings for any players that left the game but don't
794        # get freed soon.
795        for ex_player in (p() for p in self._players_that_left):
796            if ex_player is not None:
797                verify_object_death(ex_player)
798
799        for player in self.players:
800            # This should allow our ba.Player instance to be freed.
801            # Complain if that doesn't happen.
802            verify_object_death(player)
803
804            try:
805                player.expire()
806            except Exception:
807                print_exception(f'Error expiring {player}')
808
809            # Reset the SessionPlayer to a not-in-an-activity state.
810            try:
811                sessionplayer = player.sessionplayer
812                self._reset_session_player_for_no_activity(sessionplayer)
813            except SessionPlayerNotFoundError:
814                # Conceivably, someone could have held on to a Player object
815                # until now whos underlying SessionPlayer left long ago...
816                pass
817            except Exception:
818                print_exception(f'Error expiring {player}.')
819
820    def _expire_teams(self) -> None:
821
822        # Issue warnings for any teams that left the game but don't
823        # get freed soon.
824        for ex_team in (p() for p in self._teams_that_left):
825            if ex_team is not None:
826                verify_object_death(ex_team)
827
828        for team in self.teams:
829            # This should allow our ba.Team instance to die.
830            # Complain if that doesn't happen.
831            verify_object_death(team)
832
833            try:
834                team.expire()
835            except Exception:
836                print_exception(f'Error expiring {team}')
837
838            try:
839                sessionteam = team.sessionteam
840                sessionteam.activityteam = None
841            except SessionTeamNotFoundError:
842                # It is expected that Team objects may last longer than
843                # the SessionTeam they came from (game objects may hold
844                # team references past the point at which the underlying
845                # player/team has left the game)
846                pass
847            except Exception:
848                print_exception(f'Error expiring Team {team}.')
849
850    def _prune_delay_deletes(self) -> None:
851        self._delay_delete_players.clear()
852        self._delay_delete_teams.clear()
853
854        # Clear out any dead weak-refs.
855        self._teams_that_left = [
856            t for t in self._teams_that_left if t() is not None
857        ]
858        self._players_that_left = [
859            p for p in self._players_that_left if p() is not None
860        ]
861
862    def _prune_dead_actors(self) -> None:
863        self._last_prune_dead_actors_time = _ba.time()
864
865        # Prune our strong refs when the Actor's exists() call gives False
866        self._actor_refs = [a for a in self._actor_refs if a.exists()]
867
868        # Prune our weak refs once the Actor object has been freed.
869        self._actor_weak_refs = [
870            a for a in self._actor_weak_refs if a() is not None
871        ]

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)
124    def __init__(self, settings: dict):
125        """Creates an Activity in the current ba.Session.
126
127        The activity will not be actually run until ba.Session.setactivity
128        is called. 'settings' should be a dict of key/value pairs specific
129        to the activity.
130
131        Activities should preload as much of their media/etc as possible in
132        their constructor, but none of it should actually be used until they
133        are transitioned in.
134        """
135        super().__init__()
136
137        # Create our internal engine data.
138        self._activity_data = _ba.register_activity(self)
139
140        assert isinstance(settings, dict)
141        assert _ba.getactivity() is self
142
143        self._globalsnode: ba.Node | None = None
144
145        # Player/Team types should have been specified as type args;
146        # grab those.
147        self._playertype: type[PlayerType]
148        self._teamtype: type[TeamType]
149        self._setup_player_and_team_types()
150
151        # FIXME: Relocate or remove the need for this stuff.
152        self.paused_text: ba.Actor | None = None
153
154        self._session = weakref.ref(_ba.getsession())
155
156        # Preloaded data for actors, maps, etc; indexed by type.
157        self.preloads: dict[type, Any] = {}
158
159        # Hopefully can eventually kill this; activities should
160        # validate/store whatever settings they need at init time
161        # (in a more type-safe way).
162        self.settings_raw = settings
163
164        self._has_transitioned_in = False
165        self._has_begun = False
166        self._has_ended = False
167        self._activity_death_check_timer: ba.Timer | None = None
168        self._expired = False
169        self._delay_delete_players: list[PlayerType] = []
170        self._delay_delete_teams: list[TeamType] = []
171        self._players_that_left: list[weakref.ref[PlayerType]] = []
172        self._teams_that_left: list[weakref.ref[TeamType]] = []
173        self._transitioning_out = False
174
175        # A handy place to put most actors; this list is pruned of dead
176        # actors regularly and these actors are insta-killed as the activity
177        # is dying.
178        self._actor_refs: list[ba.Actor] = []
179        self._actor_weak_refs: list[weakref.ref[ba.Actor]] = []
180        self._last_prune_dead_actors_time = _ba.time()
181        self._prune_dead_actors_timer: ba.Timer | None = None
182
183        self.teams = []
184        self.players = []
185
186        self.lobby = None
187        self._stats: ba.Stats | None = None
188        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

Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).

inherits_vr_overlay_center = False

Set this to true to inherit (non-fixed) VR overlay positioning from the previous activity (useful for prevent sporadic overlay jostling during transitions).

inherits_tint = False

Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).

allow_mid_activity_joins: bool = True

Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.

transition_time = 0.0

If the activity fades or transitions in, it should set the length of time here so that previous activities will be kept alive for that long (avoiding 'holes' in the screen) This value is given in real-time seconds.

can_show_ad_on_death = False

Is it ok to show an ad after this activity ends before showing the next activity?

globalsnode: ba.Node

The 'globals' ba.Node for the activity. This contains various global controls and values.

stats: ba.Stats

The stats instance accessible while the activity is running.

If access is attempted before or after, raises a ba.NotFoundError.

def on_expire(self) -> None:
227    def on_expire(self) -> None:
228        """Called when your activity is being expired.
229
230        If your activity has created anything explicitly that may be retaining
231        a strong reference to the activity and preventing it from dying, you
232        should clear that out here. From this point on your activity's sole
233        purpose in life is to hit zero references and die so the next activity
234        can begin.
235        """

Called when your activity is being expired.

If your activity has created anything explicitly that may be retaining a strong reference to the activity and preventing it from dying, you should clear that out here. From this point on your activity's sole purpose in life is to hit zero references and die so the next activity can begin.

customdata: dict

Entities needing to store simple data with an activity can put it here. This dict will be deleted when the activity expires, so contained objects generally do not need to worry about handling expired activities.

expired: bool

Whether the activity is expired.

An activity is set as expired when shutting down. At this point no new nodes, timers, etc should be made, run, etc, and the activity should be considered a 'zombie'.

playertype: type[~PlayerType]

The type of ba.Player this Activity is using.

teamtype: type[~TeamType]

The type of ba.Team this Activity is using.

def retain_actor(self, actor: ba.Actor) -> None:
303    def retain_actor(self, actor: ba.Actor) -> None:
304        """Add a strong-reference to a ba.Actor to this Activity.
305
306        The reference will be lazily released once ba.Actor.exists()
307        returns False for the Actor. The ba.Actor.autoretain() method
308        is a convenient way to access this same functionality.
309        """
310        if __debug__:
311            from ba._actor import Actor
312            assert isinstance(actor, Actor)
313        self._actor_refs.append(actor)

Add a strong-reference to a ba.Actor to this Activity.

The reference will be lazily released once ba.Actor.exists() returns False for the Actor. The ba.Actor.autoretain() method is a convenient way to access this same functionality.

def add_actor_weak_ref(self, actor: ba.Actor) -> None:
315    def add_actor_weak_ref(self, actor: ba.Actor) -> None:
316        """Add a weak-reference to a ba.Actor to the ba.Activity.
317
318        (called by the ba.Actor base class)
319        """
320        if __debug__:
321            from ba._actor import Actor
322            assert isinstance(actor, Actor)
323        self._actor_weak_refs.append(weakref.ref(actor))

Add a weak-reference to a ba.Actor to the ba.Activity.

(called by the ba.Actor base class)

session: ba.Session

The ba.Session this ba.Activity belongs go.

Raises a ba.SessionNotFoundError if the Session no longer exists.

def on_player_join(self, player: ~PlayerType) -> None:
337    def on_player_join(self, player: PlayerType) -> None:
338        """Called when a new ba.Player has joined the Activity.
339
340        (including the initial set of Players)
341        """

Called when a new ba.Player has joined the Activity.

(including the initial set of Players)

def on_player_leave(self, player: ~PlayerType) -> None:
343    def on_player_leave(self, player: PlayerType) -> None:
344        """Called when a ba.Player is leaving the Activity."""

Called when a ba.Player is leaving the Activity.

def on_team_join(self, team: ~TeamType) -> None:
346    def on_team_join(self, team: TeamType) -> None:
347        """Called when a new ba.Team joins the Activity.
348
349        (including the initial set of Teams)
350        """

Called when a new ba.Team joins the Activity.

(including the initial set of Teams)

def on_team_leave(self, team: ~TeamType) -> None:
352    def on_team_leave(self, team: TeamType) -> None:
353        """Called when a ba.Team leaves the Activity."""

Called when a ba.Team leaves the Activity.

def on_transition_in(self) -> None:
355    def on_transition_in(self) -> None:
356        """Called when the Activity is first becoming visible.
357
358        Upon this call, the Activity should fade in backgrounds,
359        start playing music, etc. It does not yet have access to players
360        or teams, however. They remain owned by the previous Activity
361        up until ba.Activity.on_begin() is called.
362        """

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until ba.Activity.on_begin() is called.

def on_transition_out(self) -> None:
364    def on_transition_out(self) -> None:
365        """Called when your activity begins transitioning out.
366
367        Note that this may happen at any time even if ba.Activity.end() has
368        not been called.
369        """

Called when your activity begins transitioning out.

Note that this may happen at any time even if ba.Activity.end() has not been called.

def on_begin(self) -> None:
371    def on_begin(self) -> None:
372        """Called once the previous ba.Activity has finished transitioning out.
373
374        At this point the activity's initial players and teams are filled in
375        and it should begin its actual game logic.
376        """

Called once the previous ba.Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

def handlemessage(self, msg: Any) -> Any:
378    def handlemessage(self, msg: Any) -> Any:
379        """General message handling; can be passed any message object."""
380        del msg  # Unused arg.
381        return UNHANDLED

General message handling; can be passed any message object.

def has_transitioned_in(self) -> bool:
383    def has_transitioned_in(self) -> bool:
384        """Return whether ba.Activity.on_transition_in()
385         has been called."""
386        return self._has_transitioned_in

Return whether ba.Activity.on_transition_in() has been called.

def has_begun(self) -> bool:
388    def has_begun(self) -> bool:
389        """Return whether ba.Activity.on_begin() has been called."""
390        return self._has_begun

Return whether ba.Activity.on_begin() has been called.

def has_ended(self) -> bool:
392    def has_ended(self) -> bool:
393        """Return whether the activity has commenced ending."""
394        return self._has_ended

Return whether the activity has commenced ending.

def is_transitioning_out(self) -> bool:
396    def is_transitioning_out(self) -> bool:
397        """Return whether ba.Activity.on_transition_out() has been called."""
398        return self._transitioning_out

Return whether ba.Activity.on_transition_out() has been called.

def transition_out(self) -> None:
457    def transition_out(self) -> None:
458        """Called by the Session to start us transitioning out."""
459        assert not self._transitioning_out
460        self._transitioning_out = True
461        with _ba.Context(self):
462            try:
463                self.on_transition_out()
464            except Exception:
465                print_exception(f'Error in on_transition_out for {self}.')

Called by the Session to start us transitioning out.

def end( self, results: Any = None, delay: float = 0.0, force: bool = False) -> None:
495    def end(self,
496            results: Any = None,
497            delay: float = 0.0,
498            force: bool = False) -> None:
499        """Commences Activity shutdown and delivers results to the ba.Session.
500
501        'delay' is the time delay before the Activity actually ends
502        (in seconds). Further calls to end() will be ignored up until
503        this time, unless 'force' is True, in which case the new results
504        will replace the old.
505        """
506
507        # Ask the session to end us.
508        self.session.end_activity(self, results, delay, force)

Commences Activity shutdown and delivers results to the ba.Session.

'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old.

def create_player(self, sessionplayer: ba.SessionPlayer) -> ~PlayerType:
510    def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
511        """Create the Player instance for this Activity.
512
513        Subclasses can override this if the activity's player class
514        requires a custom constructor; otherwise it will be called with
515        no args. Note that the player object should not be used at this
516        point as it is not yet fully wired up; wait for
517        ba.Activity.on_player_join() for that.
518        """
519        del sessionplayer  # Unused.
520        player = self._playertype()
521        return player

Create the Player instance for this Activity.

Subclasses can override this if the activity's player class requires a custom constructor; otherwise it will be called with no args. Note that the player object should not be used at this point as it is not yet fully wired up; wait for ba.Activity.on_player_join() for that.

def create_team(self, sessionteam: ba.SessionTeam) -> ~TeamType:
523    def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
524        """Create the Team instance for this Activity.
525
526        Subclasses can override this if the activity's team class
527        requires a custom constructor; otherwise it will be called with
528        no args. Note that the team object should not be used at this
529        point as it is not yet fully wired up; wait for on_team_join()
530        for that.
531        """
532        del sessionteam  # Unused.
533        team = self._teamtype()
534        return team

Create the Team instance for this Activity.

Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.

class ActivityNotFoundError(ba.NotFoundError):
101class ActivityNotFoundError(NotFoundError):
102    """Exception raised when an expected ba.Activity does not exist.
103
104    Category: **Exception Classes**
105    """

Exception raised when an expected ba.Activity does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
args
class Actor:
 22class Actor:
 23    """High level logical entities in a ba.Activity.
 24
 25    Category: **Gameplay Classes**
 26
 27    Actors act as controllers, combining some number of ba.Nodes,
 28    ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
 29
 30    Some example actors include the Bomb, Flag, and Spaz classes that
 31    live in the bastd.actor.* modules.
 32
 33    One key feature of Actors is that they generally 'die'
 34    (killing off or transitioning out their nodes) when the last Python
 35    reference to them disappears, so you can use logic such as:
 36