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
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
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
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
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.
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.
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.
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.
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.
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'.
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()
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.
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.
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.
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.
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.
The settings dict passed in when the activity was made. This attribute is deprecated and should be avoided when possible; activities should pull all values they need from the 'settings' arg passed to the Activity __init__ call.
The list of ba.Team-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game. (at least in free-for-all mode where every player gets their own team; in teams mode there are always 2 teams regardless of the player count).
The list of ba.Player-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
Joining activities are for waiting for initial player joins. They are treated slightly differently than regular activities, mainly in that all players are passed to the activity at once instead of as each joins.
Whether idle players can potentially be kicked (should not happen in menus/etc).
In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.
Set this to True to inherit slow motion setting from previous activity (useful for transitions to avoid hitches).
Set this to True to keep playing the music from the previous activity (without even restarting it).
Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).
Set this to true to inherit (non-fixed) VR overlay positioning from the previous activity (useful for prevent sporadic overlay jostling during transitions).
Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).
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.
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.
Is it ok to show an ad after this activity ends before showing the next activity?
The 'globals' ba.Node for the activity. This contains various global controls and values.
The stats instance accessible while the activity is running.
If access is attempted before or after, raises a ba.NotFoundError.
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.
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.
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'.
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.
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)
The ba.Session this ba.Activity belongs go.
Raises a ba.SessionNotFoundError if the Session no longer exists.
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)
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.
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Inherited Members
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
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