bascenev1

Ballistica scene api version 1. Basically all gameplay related code.

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

Units of execution wrangled by a bascenev1.Session.

Category: Gameplay Classes

Examples of Activities include games, score-screens, cutscenes, etc. A bascenev1.Session has one 'current' Activity at any time, though their existence can overlap during transitions.

settings_raw: dict[str, typing.Any]

The settings dict passed in when the activity was made. This attribute is deprecated and should be avoided when possible; activities should pull all values they need from the 'settings' arg passed to the Activity __init__ call.

teams: list[~TeamT]

The list of bascenev1.Team-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game. (at least in free-for-all mode where every player gets their own team; in teams mode there are always 2 teams regardless of the player count).

players: list[~PlayerT]

The list of bascenev1.Player-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game.

announce_player_deaths = False

Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.

is_joining_activity = False

Joining activities are for waiting for initial player joins. They are treated slightly differently than regular activities, mainly in that all players are passed to the activity at once instead of as each joins.

allow_pausing = False

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

allow_kick_idle_players = True

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

use_fixed_vr_overlay = False

In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.

slow_motion = False

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

inherits_slow_motion = False

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

inherits_music = False

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

inherits_vr_camera_offset = False

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

inherits_vr_overlay_center = False

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

inherits_tint = False

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

allow_mid_activity_joins: bool = True

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

transition_time = 0.0

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

can_show_ad_on_death = False

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

paused_text: Actor | None
preloads: dict[type, typing.Any]
lobby
context: ContextRef
205    @property
206    def context(self) -> bascenev1.ContextRef:
207        """A context-ref pointing at this activity."""
208        return self._activity_data.context()

A context-ref pointing at this activity.

globalsnode: Node
210    @property
211    def globalsnode(self) -> bascenev1.Node:
212        """The 'globals' bascenev1.Node for the activity. This contains various
213        global controls and values.
214        """
215        node = self._globalsnode
216        if not node:
217            raise babase.NodeNotFoundError()
218        return node

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

stats: Stats
220    @property
221    def stats(self) -> bascenev1.Stats:
222        """The stats instance accessible while the activity is running.
223
224        If access is attempted before or after, raises a
225        bascenev1.NotFoundError.
226        """
227        if self._stats is None:
228            raise babase.NotFoundError()
229        return self._stats

The stats instance accessible while the activity is running.

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

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

Called when your activity is being expired.

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

customdata: dict
241    @property
242    def customdata(self) -> dict:
243        """Entities needing to store simple data with an activity can put it
244        here. This dict will be deleted when the activity expires, so contained
245        objects generally do not need to worry about handling expired
246        activities.
247        """
248        assert not self._expired
249        assert isinstance(self._customdata, dict)
250        return self._customdata

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

expired: bool
252    @property
253    def expired(self) -> bool:
254        """Whether the activity is expired.
255
256        An activity is set as expired when shutting down.
257        At this point no new nodes, timers, etc should be made,
258        run, etc, and the activity should be considered a 'zombie'.
259        """
260        return self._expired

Whether the activity is expired.

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

playertype: type[~PlayerT]
262    @property
263    def playertype(self) -> type[PlayerT]:
264        """The type of bascenev1.Player this Activity is using."""
265        return self._playertype

The type of bascenev1.Player this Activity is using.

teamtype: type[~TeamT]
267    @property
268    def teamtype(self) -> type[TeamT]:
269        """The type of bascenev1.Team this Activity is using."""
270        return self._teamtype

The type of bascenev1.Team this Activity is using.

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

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

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

def add_actor_weak_ref(self, actor: Actor) -> None:
320    def add_actor_weak_ref(self, actor: bascenev1.Actor) -> None:
321        """Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity.
322
323        (called by the bascenev1.Actor base class)
324        """
325        if __debug__:
326            from bascenev1._actor import Actor
327
328            assert isinstance(actor, Actor)
329        self._actor_weak_refs.append(weakref.ref(actor))

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

(called by the bascenev1.Actor base class)

session: Session
331    @property
332    def session(self) -> bascenev1.Session:
333        """The bascenev1.Session this bascenev1.Activity belongs to.
334
335        Raises a babase.SessionNotFoundError if the Session no longer exists.
336        """
337        session = self._session()
338        if session is None:
339            raise babase.SessionNotFoundError()
340        return session

The bascenev1.Session this bascenev1.Activity belongs to.

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

def on_player_join(self, player: ~PlayerT) -> None:
342    def on_player_join(self, player: PlayerT) -> None:
343        """Called when a new bascenev1.Player has joined the Activity.
344
345        (including the initial set of Players)
346        """

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

(including the initial set of Players)

def on_player_leave(self, player: ~PlayerT) -> None:
348    def on_player_leave(self, player: PlayerT) -> None:
349        """Called when a bascenev1.Player is leaving the Activity."""

Called when a bascenev1.Player is leaving the Activity.

def on_team_join(self, team: ~TeamT) -> None:
351    def on_team_join(self, team: TeamT) -> None:
352        """Called when a new bascenev1.Team joins the Activity.
353
354        (including the initial set of Teams)
355        """

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

def on_team_leave(self, team: ~TeamT) -> None:
357    def on_team_leave(self, team: TeamT) -> None:
358        """Called when a bascenev1.Team leaves the Activity."""

Called when a bascenev1.Team leaves the Activity.

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

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 bascenev1.Activity.on_begin() is called.

def on_transition_out(self) -> None:
369    def on_transition_out(self) -> None:
370        """Called when your activity begins transitioning out.
371
372        Note that this may happen at any time even if bascenev1.Activity.end()
373        has not been called.
374        """

Called when your activity begins transitioning out.

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

def on_begin(self) -> None:
376    def on_begin(self) -> None:
377        """Called once the previous Activity has finished transitioning out.
378
379        At this point the activity's initial players and teams are filled in
380        and it should begin its actual game logic.
381        """

Called once the previous Activity has finished transitioning out.

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

def handlemessage(self, msg: Any) -> Any:
383    def handlemessage(self, msg: Any) -> Any:
384        """General message handling; can be passed any message object."""
385        del msg  # Unused arg.
386        return UNHANDLED

General message handling; can be passed any message object.

def has_transitioned_in(self) -> bool:
388    def has_transitioned_in(self) -> bool:
389        """Return whether bascenev1.Activity.on_transition_in() has run."""
390        return self._has_transitioned_in

Return whether bascenev1.Activity.on_transition_in() has run.

def has_begun(self) -> bool:
392    def has_begun(self) -> bool:
393        """Return whether bascenev1.Activity.on_begin() has run."""
394        return self._has_begun

Return whether bascenev1.Activity.on_begin() has run.

def has_ended(self) -> bool:
396    def has_ended(self) -> bool:
397        """Return whether the activity has commenced ending."""
398        return self._has_ended

Return whether the activity has commenced ending.

def is_transitioning_out(self) -> bool:
400    def is_transitioning_out(self) -> bool:
401        """Return whether bascenev1.Activity.on_transition_out() has run."""
402        return self._transitioning_out
def transition_out(self) -> None:
462    def transition_out(self) -> None:
463        """Called by the Session to start us transitioning out."""
464        assert not self._transitioning_out
465        self._transitioning_out = True
466        with self.context:
467            try:
468                self.on_transition_out()
469            except Exception:
470                logging.exception('Error in on_transition_out for %s.', self)

Called by the Session to start us transitioning out.

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

Commences Activity shutdown and delivers results to the Session.

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

def create_player(self, sessionplayer: SessionPlayer) -> ~PlayerT:
514    def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT:
515        """Create the Player instance for this Activity.
516
517        Subclasses can override this if the activity's player class
518        requires a custom constructor; otherwise it will be called with
519        no args. Note that the player object should not be used at this
520        point as it is not yet fully wired up; wait for
521        bascenev1.Activity.on_player_join() for that.
522        """
523        del sessionplayer  # Unused.
524        player = self._playertype()
525        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 bascenev1.Activity.on_player_join() for that.

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

Create the Team instance for this Activity.

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

class Actor:
 30class Actor:
 31    """High level logical entities in a bascenev1.Activity.
 32
 33    Category: **Gameplay Classes**
 34
 35    Actors act as controllers, combining some number of Nodes, Textures,
 36    Sounds, etc. into a high-level cohesive unit.
 37
 38    Some example actors include the Bomb, Flag, and Spaz classes that
 39    live in the bascenev1lib.actor.* modules.
 40
 41    One key feature of Actors is that they generally 'die'
 42    (killing off or transitioning out their nodes) when the last Python
 43    reference to them disappears, so you can use logic such as:
 44
 45    ##### Example
 46    >>> # Create a flag Actor in our game activity:
 47    ... from bascenev1lib.actor.flag import Flag
 48    ... self.flag = Flag(position=(0, 10, 0))
 49    ...
 50    ... # Later, destroy the flag.
 51    ... # (provided nothing else is holding a reference to it)
 52    ... # We could also just assign a new flag to this value.
 53    ... # Either way, the old flag disappears.
 54    ... self.flag = None
 55
 56    This is in contrast to the behavior of the more low level
 57    bascenev1.Node, which is always explicitly created and destroyed
 58    and doesn't care how many Python references to it exist.
 59
 60    Note, however, that you can use the bascenev1.Actor.autoretain() method
 61    if you want an Actor to stick around until explicitly killed
 62    regardless of references.
 63
 64    Another key feature of bascenev1.Actor is its
 65    bascenev1.Actor.handlemessage() method, which takes a single arbitrary
 66    object as an argument. This provides a safe way to communicate between
 67    bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other
 68    class providing a handlemessage() method. The most universally handled
 69    message type for Actors is the bascenev1.DieMessage.
 70
 71    Another way to kill the flag from the example above:
 72    We can safely call this on any type with a 'handlemessage' method
 73    (though its not guaranteed to always have a meaningful effect).
 74    In this case the Actor instance will still be around, but its
 75    bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will
 76    both return False.
 77    >>> self.flag.handlemessage(bascenev1.DieMessage())
 78    """
 79
 80    def __init__(self) -> None:
 81        """Instantiates an Actor in the current bascenev1.Activity."""
 82
 83        if __debug__:
 84            self._root_actor_init_called = True
 85        activity = _bascenev1.getactivity()
 86        self._activity = weakref.ref(activity)
 87        activity.add_actor_weak_ref(self)
 88
 89    def __del__(self) -> None:
 90        try:
 91            # Unexpired Actors send themselves a DieMessage when going down.
 92            # That way we can treat DieMessage handling as the single
 93            # point-of-action for death.
 94            if not self.expired:
 95                self.handlemessage(DieMessage())
 96        except Exception:
 97            logging.exception(
 98                'Error in bascenev1.Actor.__del__() for %s.', self
 99            )
100
101    def handlemessage(self, msg: Any) -> Any:
102        """General message handling; can be passed any message object."""
103        assert not self.expired
104
105        # By default, actors going out-of-bounds simply kill themselves.
106        if isinstance(msg, OutOfBoundsMessage):
107            return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
108
109        return UNHANDLED
110
111    def autoretain(self: ActorT) -> ActorT:
112        """Keep this Actor alive without needing to hold a reference to it.
113
114        This keeps the bascenev1.Actor in existence by storing a reference
115        to it with the bascenev1.Activity it was created in. The reference
116        is lazily released once bascenev1.Actor.exists() returns False for
117        it or when the Activity is set as expired.  This can be a convenient
118        alternative to storing references explicitly just to keep a
119        bascenev1.Actor from dying.
120        For convenience, this method returns the bascenev1.Actor it is called
121        with, enabling chained statements such as:
122        myflag = bascenev1.Flag().autoretain()
123        """
124        activity = self._activity()
125        if activity is None:
126            raise babase.ActivityNotFoundError()
127        activity.retain_actor(self)
128        return self
129
130    def on_expire(self) -> None:
131        """Called for remaining `bascenev1.Actor`s when their activity dies.
132
133        Actors can use this opportunity to clear callbacks or other
134        references which have the potential of keeping the bascenev1.Activity
135        alive inadvertently (Activities can not exit cleanly while
136        any Python references to them remain.)
137
138        Once an actor is expired (see bascenev1.Actor.is_expired()) it should
139        no longer perform any game-affecting operations (creating, modifying,
140        or deleting nodes, media, timers, etc.) Attempts to do so will
141        likely result in errors.
142        """
143
144    @property
145    def expired(self) -> bool:
146        """Whether the Actor is expired.
147
148        (see bascenev1.Actor.on_expire())
149        """
150        activity = self.getactivity(doraise=False)
151        return True if activity is None else activity.expired
152
153    def exists(self) -> bool:
154        """Returns whether the Actor is still present in a meaningful way.
155
156        Note that a dying character should still return True here as long as
157        their corpse is visible; this is about presence, not being 'alive'
158        (see bascenev1.Actor.is_alive() for that).
159
160        If this returns False, it is assumed the Actor can be completely
161        deleted without affecting the game; this call is often used
162        when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
163
164        The default implementation of this method always return True.
165
166        Note that the boolean operator for the Actor class calls this method,
167        so a simple "if myactor" test will conveniently do the right thing
168        even if myactor is set to None.
169        """
170        return True
171
172    def __bool__(self) -> bool:
173        # Cleaner way to test existence; friendlier to None values.
174        return self.exists()
175
176    def is_alive(self) -> bool:
177        """Returns whether the Actor is 'alive'.
178
179        What this means is up to the Actor.
180        It is not a requirement for Actors to be able to die;
181        just that they report whether they consider themselves
182        to be alive or not. In cases where dead/alive is
183        irrelevant, True should be returned.
184        """
185        return True
186
187    @property
188    def activity(self) -> bascenev1.Activity:
189        """The Activity this Actor was created in.
190
191        Raises a bascenev1.ActivityNotFoundError if the Activity no longer
192        exists.
193        """
194        activity = self._activity()
195        if activity is None:
196            raise babase.ActivityNotFoundError()
197        return activity
198
199    # Overloads to convey our exact return type depending on 'doraise' value.
200
201    @overload
202    def getactivity(
203        self, doraise: Literal[True] = True
204    ) -> bascenev1.Activity: ...
205
206    @overload
207    def getactivity(
208        self, doraise: Literal[False]
209    ) -> bascenev1.Activity | None: ...
210
211    def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
212        """Return the bascenev1.Activity this Actor is associated with.
213
214        If the Activity no longer exists, raises a
215        bascenev1.ActivityNotFoundError or returns None depending on whether
216        'doraise' is True.
217        """
218        activity = self._activity()
219        if activity is None and doraise:
220            raise babase.ActivityNotFoundError()
221        return activity

High level logical entities in a bascenev1.Activity.

Category: Gameplay Classes

Actors act as controllers, combining some number of Nodes, Textures, Sounds, etc. into a high-level cohesive unit.

Some example actors include the Bomb, Flag, and Spaz classes that live in the bascenev1lib.actor.* modules.

One key feature of Actors is that they generally 'die' (killing off or transitioning out their nodes) when the last Python reference to them disappears, so you can use logic such as:

Example
>>> # Create a flag Actor in our game activity:
... from bascenev1lib.actor.flag import Flag
... self.flag = Flag(position=(0, 10, 0))
...
... # Later, destroy the flag.
... # (provided nothing else is holding a reference to it)
... # We could also just assign a new flag to this value.
... # Either way, the old flag disappears.
... self.flag = None

This is in contrast to the behavior of the more low level bascenev1.Node, which is always explicitly created and destroyed and doesn't care how many Python references to it exist.

Note, however, that you can use the bascenev1.Actor.autoretain() method if you want an Actor to stick around until explicitly killed regardless of references.

Another key feature of bascenev1.Actor is its bascenev1.Actor.handlemessage() method, which takes a single arbitrary object as an argument. This provides a safe way to communicate between bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other class providing a handlemessage() method. The most universally handled message type for Actors is the bascenev1.DieMessage.

Another way to kill the flag from the example above: We can safely call this on any type with a 'handlemessage' method (though its not guaranteed to always have a meaningful effect). In this case the Actor instance will still be around, but its bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will both return False.

>>> self.flag.handlemessage(bascenev1.DieMessage())
Actor()
80    def __init__(self) -> None:
81        """Instantiates an Actor in the current bascenev1.Activity."""
82
83        if __debug__:
84            self._root_actor_init_called = True
85        activity = _bascenev1.getactivity()
86        self._activity = weakref.ref(activity)
87        activity.add_actor_weak_ref(self)

Instantiates an Actor in the current bascenev1.Activity.

def handlemessage(self, msg: Any) -> Any:
101    def handlemessage(self, msg: Any) -> Any:
102        """General message handling; can be passed any message object."""
103        assert not self.expired
104
105        # By default, actors going out-of-bounds simply kill themselves.
106        if isinstance(msg, OutOfBoundsMessage):
107            return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
108
109        return UNHANDLED

General message handling; can be passed any message object.

def autoretain(self: ~ActorT) -> ~ActorT:
111    def autoretain(self: ActorT) -> ActorT:
112        """Keep this Actor alive without needing to hold a reference to it.
113
114        This keeps the bascenev1.Actor in existence by storing a reference
115        to it with the bascenev1.Activity it was created in. The reference
116        is lazily released once bascenev1.Actor.exists() returns False for
117        it or when the Activity is set as expired.  This can be a convenient
118        alternative to storing references explicitly just to keep a
119        bascenev1.Actor from dying.
120        For convenience, this method returns the bascenev1.Actor it is called
121        with, enabling chained statements such as:
122        myflag = bascenev1.Flag().autoretain()
123        """
124        activity = self._activity()
125        if activity is None:
126            raise babase.ActivityNotFoundError()
127        activity.retain_actor(self)
128        return self

Keep this Actor alive without needing to hold a reference to it.

This keeps the bascenev1.Actor in existence by storing a reference to it with the bascenev1.Activity it was created in. The reference is lazily released once bascenev1.Actor.exists() returns False for it or when the Activity is set as expired. This can be a convenient alternative to storing references explicitly just to keep a bascenev1.Actor from dying. For convenience, this method returns the bascenev1.Actor it is called with, enabling chained statements such as: myflag = bascenev1.Flag().autoretain()

def on_expire(self) -> None:
130    def on_expire(self) -> None:
131        """Called for remaining `bascenev1.Actor`s when their activity dies.
132
133        Actors can use this opportunity to clear callbacks or other
134        references which have the potential of keeping the bascenev1.Activity
135        alive inadvertently (Activities can not exit cleanly while
136        any Python references to them remain.)
137
138        Once an actor is expired (see bascenev1.Actor.is_expired()) it should
139        no longer perform any game-affecting operations (creating, modifying,
140        or deleting nodes, media, timers, etc.) Attempts to do so will
141        likely result in errors.
142        """

Called for remaining bascenev1.Actors when their activity dies.

Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the bascenev1.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)

Once an actor is expired (see bascenev1.Actor.is_expired()) it should no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors.

expired: bool
144    @property
145    def expired(self) -> bool:
146        """Whether the Actor is expired.
147
148        (see bascenev1.Actor.on_expire())
149        """
150        activity = self.getactivity(doraise=False)
151        return True if activity is None else activity.expired

Whether the Actor is expired.

(see bascenev1.Actor.on_expire())

def exists(self) -> bool:
153    def exists(self) -> bool:
154        """Returns whether the Actor is still present in a meaningful way.
155
156        Note that a dying character should still return True here as long as
157        their corpse is visible; this is about presence, not being 'alive'
158        (see bascenev1.Actor.is_alive() for that).
159
160        If this returns False, it is assumed the Actor can be completely
161        deleted without affecting the game; this call is often used
162        when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
163
164        The default implementation of this method always return True.
165
166        Note that the boolean operator for the Actor class calls this method,
167        so a simple "if myactor" test will conveniently do the right thing
168        even if myactor is set to None.
169        """
170        return True

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

def is_alive(self) -> bool:
176    def is_alive(self) -> bool:
177        """Returns whether the Actor is 'alive'.
178
179        What this means is up to the Actor.
180        It is not a requirement for Actors to be able to die;
181        just that they report whether they consider themselves
182        to be alive or not. In cases where dead/alive is
183        irrelevant, True should be returned.
184        """
185        return True

Returns whether the Actor is 'alive'.

What this means is up to the Actor. It is not a requirement for Actors to be able to die; just that they report whether they consider themselves to be alive or not. In cases where dead/alive is irrelevant, True should be returned.

activity: Activity
187    @property
188    def activity(self) -> bascenev1.Activity:
189        """The Activity this Actor was created in.
190
191        Raises a bascenev1.ActivityNotFoundError if the Activity no longer
192        exists.
193        """
194        activity = self._activity()
195        if activity is None:
196            raise babase.ActivityNotFoundError()
197        return activity

The Activity this Actor was created in.

Raises a bascenev1.ActivityNotFoundError if the Activity no longer exists.

def getactivity(self, doraise: bool = True) -> Activity | None:
211    def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
212        """Return the bascenev1.Activity this Actor is associated with.
213
214        If the Activity no longer exists, raises a
215        bascenev1.ActivityNotFoundError or returns None depending on whether
216        'doraise' is True.
217        """
218        activity = self._activity()
219        if activity is None and doraise:
220            raise babase.ActivityNotFoundError()
221        return activity

Return the bascenev1.Activity this Actor is associated with.

If the Activity no longer exists, raises a bascenev1.ActivityNotFoundError or returns None depending on whether 'doraise' is True.

def animate( node: Node, attr: str, keys: dict[float, float], loop: bool = False, offset: float = 0) -> Node:
 51def animate(
 52    node: bascenev1.Node,
 53    attr: str,
 54    keys: dict[float, float],
 55    loop: bool = False,
 56    offset: float = 0,
 57) -> bascenev1.Node:
 58    """Animate values on a target bascenev1.Node.
 59
 60    Category: **Gameplay Functions**
 61
 62    Creates an 'animcurve' node with the provided values and time as an input,
 63    connect it to the provided attribute, and set it to die with the target.
 64    Key values are provided as time:value dictionary pairs.  Time values are
 65    relative to the current time. By default, times are specified in seconds,
 66    but timeformat can also be set to MILLISECONDS to recreate the old behavior
 67    (prior to ba 1.5) of taking milliseconds. Returns the animcurve node.
 68    """
 69    items = list(keys.items())
 70    items.sort()
 71
 72    curve = _bascenev1.newnode(
 73        'animcurve',
 74        owner=node,
 75        name='Driving ' + str(node) + ' \'' + attr + '\'',
 76    )
 77
 78    # We take seconds but operate on milliseconds internally.
 79    mult = 1000
 80
 81    curve.times = [int(mult * time) for time, val in items]
 82    curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
 83    curve.values = [val for time, val in items]
 84    curve.loop = loop
 85
 86    # If we're not looping, set a timer to kill this curve
 87    # after its done its job.
 88    # FIXME: Even if we are looping we should have a way to die once we
 89    #  get disconnected.
 90    if not loop:
 91        # noinspection PyUnresolvedReferences
 92        _bascenev1.timer(
 93            (int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete
 94        )
 95
 96    # Do the connects last so all our attrs are in place when we push initial
 97    # values through.
 98
 99    # We operate in either activities or sessions..
100    try:
101        globalsnode = _bascenev1.getactivity().globalsnode
102    except babase.ActivityNotFoundError:
103        globalsnode = _bascenev1.getsession().sessionglobalsnode
104
105    globalsnode.connectattr('time', curve, 'in')
106    curve.connectattr('out', node, attr)
107    return curve

Animate values on a target bascenev1.Node.

Category: Gameplay Functions

Creates an 'animcurve' node with the provided values and time as an input, connect it to the provided attribute, and set it to die with the target. Key values are provided as time:value dictionary pairs. Time values are relative to the current time. By default, times are specified in seconds, but timeformat can also be set to MILLISECONDS to recreate the old behavior (prior to ba 1.5) of taking milliseconds. Returns the animcurve node.

def animate_array( node: Node, attr: str, size: int, keys: dict[float, typing.Sequence[float]], loop: bool = False, offset: float = 0) -> None:
110def animate_array(
111    node: bascenev1.Node,
112    attr: str,
113    size: int,
114    keys: dict[float, Sequence[float]],
115    loop: bool = False,
116    offset: float = 0,
117) -> None:
118    """Animate an array of values on a target bascenev1.Node.
119
120    Category: **Gameplay Functions**
121
122    Like bs.animate, but operates on array attributes.
123    """
124    combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size})
125    items = list(keys.items())
126    items.sort()
127
128    # We take seconds but operate on milliseconds internally.
129    mult = 1000
130
131    # We operate in either activities or sessions..
132    try:
133        globalsnode = _bascenev1.getactivity().globalsnode
134    except babase.ActivityNotFoundError:
135        globalsnode = _bascenev1.getsession().sessionglobalsnode
136
137    for i in range(size):
138        curve = _bascenev1.newnode(
139            'animcurve',
140            owner=node,
141            name=(
142                'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
143            ),
144        )
145        globalsnode.connectattr('time', curve, 'in')
146        curve.times = [int(mult * time) for time, val in items]
147        curve.values = [val[i] for time, val in items]
148        curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
149        curve.loop = loop
150        curve.connectattr('out', combine, 'input' + str(i))
151
152        # If we're not looping, set a timer to kill this
153        # curve after its done its job.
154        if not loop:
155            # (PyCharm seems to think item is a float, not a tuple)
156            # noinspection PyUnresolvedReferences
157            _bascenev1.timer(
158                (int(mult * items[-1][0]) + 1000) / 1000.0,
159                curve.delete,
160            )
161    combine.connectattr('output', node, attr)
162
163    # If we're not looping, set a timer to kill the combine once
164    # the job is done.
165    # FIXME: Even if we are looping we should have a way to die
166    #  once we get disconnected.
167    if not loop:
168        # (PyCharm seems to think item is a float, not a tuple)
169        # noinspection PyUnresolvedReferences
170        _bascenev1.timer(
171            (int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete
172        )

Animate an array of values on a target bascenev1.Node.

Category: Gameplay Functions

Like bs.animate, but operates on array attributes.

app = <babase._app.App object>
class AppIntent:
13class AppIntent:
14    """A high level directive given to the app.
15
16    Category: **App Classes**
17    """

A high level directive given to the app.

Category: App Classes

class AppIntentDefault(bascenev1.AppIntent):
20class AppIntentDefault(AppIntent):
21    """Tells the app to simply run in its default mode."""

Tells the app to simply run in its default mode.

class AppIntentExec(bascenev1.AppIntent):
24class AppIntentExec(AppIntent):
25    """Tells the app to exec some Python code."""
26
27    def __init__(self, code: str):
28        self.code = code

Tells the app to exec some Python code.

AppIntentExec(code: str)
27    def __init__(self, code: str):
28        self.code = code
code
class AppMode:
14class AppMode:
15    """A high level mode for the app.
16
17    Category: **App Classes**
18
19    """
20
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')
25
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # FIXME: check AppExperience.
36        return cls._supports_intent(intent)
37
38    @classmethod
39    def _supports_intent(cls, intent: AppIntent) -> bool:
40        """Return whether our mode can handle the provided intent.
41
42        AppModes should override this to define what they can handle.
43        Note that AppExperience does not have to be considered here; that
44        is handled automatically by the can_handle_intent() call."""
45        raise NotImplementedError('AppMode subclasses must override this.')
46
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')
50
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""
53
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""
56
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

A high level mode for the app.

Category: App Classes

@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')

Return the overall experience provided by this mode.

@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # FIXME: check AppExperience.
36        return cls._supports_intent(intent)

Return whether this mode can handle the provided intent.

For this to return True, the AppMode must claim to support the provided intent (via its _supports_intent() method) AND the AppExperience associated with the AppMode must be supported by the current app and runtime environment.

def handle_intent(self, intent: AppIntent) -> None:
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

def on_activate(self) -> None:
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""

Called when the mode is being activated.

def on_deactivate(self) -> None:
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""

Called when the mode is being deactivated.

def on_app_active_changed(self) -> None:
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

Called when babase.app.active changes.

The app-mode may want to take action such as pausing a running game in such cases.

AppTime = AppTime
def apptime() -> AppTime:
547def apptime() -> babase.AppTime:
548    """Return the current app-time in seconds.
549
550    Category: **General Utility Functions**
551
552    App-time is a monotonic time value; it starts at 0.0 when the app
553    launches and will never jump by large amounts or go backwards, even if
554    the system time changes. Its progression will pause when the app is in
555    a suspended state.
556
557    Note that the AppTime returned here is simply float; it just has a
558    unique type in the type-checker's eyes to help prevent it from being
559    accidentally used with time functionality expecting other time types.
560    """
561    import babase  # pylint: disable=cyclic-import
562
563    return babase.AppTime(0.0)

Return the current app-time in seconds.

Category: General Utility Functions

App-time is a monotonic time value; it starts at 0.0 when the app launches and will never jump by large amounts or go backwards, even if the system time changes. Its progression will pause when the app is in a suspended state.

Note that the AppTime returned here is simply float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

def apptimer(time: float, call: Callable[[], Any]) -> None:
566def apptimer(time: float, call: Callable[[], Any]) -> None:
567    """Schedule a callable object to run based on app-time.
568
569    Category: **General Utility Functions**
570
571    This function creates a one-off timer which cannot be canceled or
572    modified once created. If you require the ability to do so, or need
573    a repeating timer, use the babase.AppTimer class instead.
574
575    ##### Arguments
576    ###### time (float)
577    > Length of time in seconds that the timer will wait before firing.
578
579    ###### call (Callable[[], Any])
580    > A callable Python object. Note that the timer will retain a
581    strong reference to the callable for as long as the timer exists, so you
582    may want to look into concepts such as babase.WeakCall if that is not
583    desired.
584
585    ##### Examples
586    Print some stuff through time:
587    >>> babase.screenmessage('hello from now!')
588    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
589                              'hello from the future!'))
590    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
591    ...                       'hello from the future 2!'))
592    """
593    return None

Schedule a callable object to run based on app-time.

Category: General Utility Functions

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.AppTimer class instead.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> babase.screenmessage('hello from now!')
>>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
                          'hello from the future!'))
>>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
...                       'hello from the future 2!'))
class AppTimer:
53class AppTimer:
54    """Timers are used to run code at later points in time.
55
56    Category: **General Utility Classes**
57
58    This class encapsulates a timer based on app-time.
59    The underlying timer will be destroyed when this object is no longer
60    referenced. If you do not want to worry about keeping a reference to
61    your timer around, use the babase.apptimer() function instead to get a
62    one-off timer.
63
64    ##### Arguments
65    ###### time
66    > Length of time in seconds that the timer will wait before firing.
67
68    ###### call
69    > A callable Python object. Remember that the timer will retain a
70    strong reference to the callable for as long as it exists, so you
71    may want to look into concepts such as babase.WeakCall if that is not
72    desired.
73
74    ###### repeat
75    > If True, the timer will fire repeatedly, with each successive
76    firing having the same delay as the first.
77
78    ##### Example
79
80    Use a Timer object to print repeatedly for a few seconds:
81    ... def say_it():
82    ...     babase.screenmessage('BADGER!')
83    ... def stop_saying_it():
84    ...     global g_timer
85    ...     g_timer = None
86    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
87    ... # Create our timer; it will run as long as we have the self.t ref.
88    ... g_timer = babase.AppTimer(0.3, say_it, repeat=True)
89    ... # Now fire off a one-shot timer to kill it.
90    ... babase.apptimer(3.89, stop_saying_it)
91    """
92
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.apptimer() function instead to get a one-off timer.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.apptimer(3.89, stop_saying_it)

AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass
class AssetPackage(bascenev1.DependencyComponent):
299class AssetPackage(DependencyComponent):
300    """bascenev1.DependencyComponent representing a bundled package of assets.
301
302    Category: **Asset Classes**
303    """
304
305    def __init__(self) -> None:
306        super().__init__()
307
308        # This is used internally by the get_package_xxx calls.
309        self.context = babase.ContextRef()
310
311        entry = self._dep_entry()
312        assert entry is not None
313        assert isinstance(entry.config, str)
314        self.package_id = entry.config
315        print(f'LOADING ASSET PACKAGE {self.package_id}')
316
317    @override
318    @classmethod
319    def dep_is_present(cls, config: Any = None) -> bool:
320        assert isinstance(config, str)
321
322        # Temp: hard-coding for a single asset-package at the moment.
323        if config == 'stdassets@1':
324            return True
325        return False
326
327    def gettexture(self, name: str) -> bascenev1.Texture:
328        """Load a named bascenev1.Texture from the AssetPackage.
329
330        Behavior is similar to bascenev1.gettexture()
331        """
332        return _bascenev1.get_package_texture(self, name)
333
334    def getmesh(self, name: str) -> bascenev1.Mesh:
335        """Load a named bascenev1.Mesh from the AssetPackage.
336
337        Behavior is similar to bascenev1.getmesh()
338        """
339        return _bascenev1.get_package_mesh(self, name)
340
341    def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
342        """Load a named bascenev1.CollisionMesh from the AssetPackage.
343
344        Behavior is similar to bascenev1.getcollisionmesh()
345        """
346        return _bascenev1.get_package_collision_mesh(self, name)
347
348    def getsound(self, name: str) -> bascenev1.Sound:
349        """Load a named bascenev1.Sound from the AssetPackage.
350
351        Behavior is similar to bascenev1.getsound()
352        """
353        return _bascenev1.get_package_sound(self, name)
354
355    def getdata(self, name: str) -> bascenev1.Data:
356        """Load a named bascenev1.Data from the AssetPackage.
357
358        Behavior is similar to bascenev1.getdata()
359        """
360        return _bascenev1.get_package_data(self, name)

bascenev1.DependencyComponent representing a bundled package of assets.

Category: Asset Classes

AssetPackage()
305    def __init__(self) -> None:
306        super().__init__()
307
308        # This is used internally by the get_package_xxx calls.
309        self.context = babase.ContextRef()
310
311        entry = self._dep_entry()
312        assert entry is not None
313        assert isinstance(entry.config, str)
314        self.package_id = entry.config
315        print(f'LOADING ASSET PACKAGE {self.package_id}')

Instantiate a DependencyComponent.

context
package_id
@override
@classmethod
def dep_is_present(cls, config: Any = None) -> bool:
317    @override
318    @classmethod
319    def dep_is_present(cls, config: Any = None) -> bool:
320        assert isinstance(config, str)
321
322        # Temp: hard-coding for a single asset-package at the moment.
323        if config == 'stdassets@1':
324            return True
325        return False

Return whether this component/config is present on this device.

def gettexture(self, name: str) -> Texture:
327    def gettexture(self, name: str) -> bascenev1.Texture:
328        """Load a named bascenev1.Texture from the AssetPackage.
329
330        Behavior is similar to bascenev1.gettexture()
331        """
332        return _bascenev1.get_package_texture(self, name)

Load a named bascenev1.Texture from the AssetPackage.

Behavior is similar to bascenev1.gettexture()

def getmesh(self, name: str) -> Mesh:
334    def getmesh(self, name: str) -> bascenev1.Mesh:
335        """Load a named bascenev1.Mesh from the AssetPackage.
336
337        Behavior is similar to bascenev1.getmesh()
338        """
339        return _bascenev1.get_package_mesh(self, name)

Load a named bascenev1.Mesh from the AssetPackage.

Behavior is similar to bascenev1.getmesh()

def getcollisionmesh(self, name: str) -> CollisionMesh:
341    def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
342        """Load a named bascenev1.CollisionMesh from the AssetPackage.
343
344        Behavior is similar to bascenev1.getcollisionmesh()
345        """
346        return _bascenev1.get_package_collision_mesh(self, name)

Load a named bascenev1.CollisionMesh from the AssetPackage.

Behavior is similar to bascenev1.getcollisionmesh()

def getsound(self, name: str) -> Sound:
348    def getsound(self, name: str) -> bascenev1.Sound:
349        """Load a named bascenev1.Sound from the AssetPackage.
350
351        Behavior is similar to bascenev1.getsound()
352        """
353        return _bascenev1.get_package_sound(self, name)

Load a named bascenev1.Sound from the AssetPackage.

Behavior is similar to bascenev1.getsound()

def getdata(self, name: str) -> Data:
355    def getdata(self, name: str) -> bascenev1.Data:
356        """Load a named bascenev1.Data from the AssetPackage.
357
358        Behavior is similar to bascenev1.getdata()
359        """
360        return _bascenev1.get_package_data(self, name)

Load a named bascenev1.Data from the AssetPackage.

Behavior is similar to bascenev1.getdata()

def basetime() -> BaseTime:
940def basetime() -> bascenev1.BaseTime:
941    """Return the base-time in seconds for the current scene-v1 context.
942
943    Category: **General Utility Functions**
944
945    Base-time is a time value that progresses at a constant rate for a scene,
946    even when the scene is sped up, slowed down, or paused. It may, however,
947    speed up or slow down due to replay speed adjustments or may slow down
948    if the cpu is overloaded.
949    Note that the value returned here is simply a float; it just has a
950    unique type in the type-checker's eyes to help prevent it from being
951    accidentally used with time functionality expecting other time types.
952    """
953    import bascenev1  # pylint: disable=cyclic-import
954
955    return bascenev1.BaseTime(0.0)

Return the base-time in seconds for the current scene-v1 context.

Category: General Utility Functions

Base-time is a time value that progresses at a constant rate for a scene, even when the scene is sped up, slowed down, or paused. It may, however, speed up or slow down due to replay speed adjustments or may slow down if the cpu is overloaded. Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

BaseTime = BaseTime
def basetimer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
960def basetimer(
961    time: float, call: Callable[[], Any], repeat: bool = False
962) -> None:
963    """Schedule a call to run at a later point in scene base-time.
964    Base-time is a value that progresses at a constant rate for a scene,
965     even when the scene is sped up, slowed down, or paused. It may,
966     however, speed up or slow down due to replay speed adjustments or may
967     slow down if the cpu is overloaded.
968
969    Category: **General Utility Functions**
970
971    This function adds a timer to the current scene context.
972    This timer cannot be canceled or modified once created. If you
973     require the ability to do so, use the bascenev1.BaseTimer class
974     instead.
975
976    ##### Arguments
977    ###### time (float)
978    > Length of time in seconds that the timer will wait before firing.
979
980    ###### call (Callable[[], Any])
981    > A callable Python object. Remember that the timer will retain a
982    strong reference to the callable for the duration of the timer, so you
983    may want to look into concepts such as babase.WeakCall if that is not
984    desired.
985
986    ###### repeat (bool)
987    > If True, the timer will fire repeatedly, with each successive
988    firing having the same delay as the first.
989
990    ##### Examples
991    Print some stuff through time:
992    >>> import bascenev1 as bs
993    >>> bs.screenmessage('hello from now!')
994    >>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
995    >>> bs.basetimer(2.0, bs.Call(bs.screenmessage,
996    ...                       'hello from the future 2!'))
997    """
998    return None

Schedule a call to run at a later point in scene base-time. Base-time is a value that progresses at a constant rate for a scene, even when the scene is sped up, slowed down, or paused. It may, however, speed up or slow down due to replay speed adjustments or may slow down if the cpu is overloaded.

Category: General Utility Functions

This function adds a timer to the current scene context. This timer cannot be canceled or modified once created. If you require the ability to do so, use the bascenev1.BaseTimer class instead.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Remember that the timer will retain a strong reference to the callable for the duration of the timer, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat (bool)

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Examples

Print some stuff through time:

>>> import bascenev1 as bs
>>> bs.screenmessage('hello from now!')
>>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
>>> bs.basetimer(2.0, bs.Call(bs.screenmessage,
...                       'hello from the future 2!'))
class BaseTimer:
 80class BaseTimer:
 81    """Timers are used to run code at later points in time.
 82
 83    Category: **General Utility Classes**
 84
 85    This class encapsulates a base-time timer in the current scene
 86    context.
 87    The underlying timer will be destroyed when either this object is
 88    no longer referenced or when its Context (Activity, etc.) dies. If you
 89    do not want to worry about keeping a reference to your timer around,
 90    you should use the bascenev1.basetimer() function instead.
 91
 92    ###### time (float)
 93    > Length of time in seconds that the timer will wait
 94    before firing.
 95
 96    ###### call (Callable[[], Any])
 97    > A callable Python object. Remember that the timer will retain a
 98    strong reference to the callable for as long as it exists, so you
 99    may want to look into concepts such as babase.WeakCall if that is not
100    desired.
101
102    ###### repeat (bool)
103    > If True, the timer will fire repeatedly, with each successive
104    firing having the same delay as the first.
105
106    ##### Example
107
108    Use a BaseTimer object to print repeatedly for a few seconds:
109    >>> import bascenev1 as bs
110    ... def say_it():
111    ...     bs.screenmessage('BADGER!')
112    ... def stop_saying_it():
113    ...     global g_timer
114    ...     g_timer = None
115    ...     bs.screenmessage('MUSHROOM MUSHROOM!')
116    ... # Create our timer; it will run as long as we have the self.t ref.
117    ... g_timer = bs.BaseTimer(0.3, say_it, repeat=True)
118    ... # Now fire off a one-shot timer to kill it.
119    ... bs.basetimer(3.89, stop_saying_it)
120    """
121
122    def __init__(
123        self, time: float, call: Callable[[], Any], repeat: bool = False
124    ) -> None:
125        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a base-time timer in the current scene context. The underlying timer will be destroyed when either this object is no longer referenced or when its Context (Activity, etc.) dies. If you do not want to worry about keeping a reference to your timer around, you should use the bascenev1.basetimer() function instead.

time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat (bool)

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a BaseTimer object to print repeatedly for a few seconds:

>>> import bascenev1 as bs
... def say_it():
...     bs.screenmessage('BADGER!')
... def stop_saying_it():
...     global g_timer
...     g_timer = None
...     bs.screenmessage('MUSHROOM MUSHROOM!')
... # Create our timer; it will run as long as we have the self.t ref.
... g_timer = bs.BaseTimer(0.3, say_it, repeat=True)
... # Now fire off a one-shot timer to kill it.
... bs.basetimer(3.89, stop_saying_it)
BaseTimer(time: float, call: Callable[[], Any], repeat: bool = False)
122    def __init__(
123        self, time: float, call: Callable[[], Any], repeat: bool = False
124    ) -> None:
125        pass
@dataclass
class BoolSetting(bascenev1.Setting):
26@dataclass
27class BoolSetting(Setting):
28    """A boolean game setting.
29
30    Category: Settings Classes
31    """
32
33    default: bool

A boolean game setting.

Category: Settings Classes

BoolSetting(name: str, default: bool)
default: bool
Inherited Members
Setting
name
Call = <class 'babase._general._Call'>
def cameraflash(duration: float = 999.0) -> None:
238def cameraflash(duration: float = 999.0) -> None:
239    """Create a strobing camera flash effect.
240
241    Category: **Gameplay Functions**
242
243    (as seen when a team wins a game)
244    Duration is in seconds.
245    """
246    # pylint: disable=too-many-locals
247    import random
248    from bascenev1._nodeactor import NodeActor
249
250    x_spread = 10
251    y_spread = 5
252    positions = [
253        [-x_spread, -y_spread],
254        [0, -y_spread],
255        [0, y_spread],
256        [x_spread, -y_spread],
257        [x_spread, y_spread],
258        [-x_spread, y_spread],
259    ]
260    times = [0, 2700, 1000, 1800, 500, 1400]
261
262    # Store this on the current activity so we only have one at a time.
263    # FIXME: Need a type safe way to do this.
264    activity = _bascenev1.getactivity()
265    activity.camera_flash_data = []  # type: ignore
266    for i in range(6):
267        light = NodeActor(
268            _bascenev1.newnode(
269                'light',
270                attrs={
271                    'position': (positions[i][0], 0, positions[i][1]),
272                    'radius': 1.0,
273                    'lights_volumes': False,
274                    'height_attenuated': False,
275                    'color': (0.2, 0.2, 0.8),
276                },
277            )
278        )
279        sval = 1.87
280        iscale = 1.3
281        tcombine = _bascenev1.newnode(
282            'combine',
283            owner=light.node,
284            attrs={
285                'size': 3,
286                'input0': positions[i][0],
287                'input1': 0,
288                'input2': positions[i][1],
289            },
290        )
291        assert light.node
292        tcombine.connectattr('output', light.node, 'position')
293        xval = positions[i][0]
294        yval = positions[i][1]
295        spd = 0.5 + random.random()
296        spd2 = 0.5 + random.random()
297        animate(
298            tcombine,
299            'input0',
300            {
301                0.0: xval + 0,
302                0.069 * spd: xval + 10.0,
303                0.143 * spd: xval - 10.0,
304                0.201 * spd: xval + 0,
305            },
306            loop=True,
307        )
308        animate(
309            tcombine,
310            'input2',
311            {
312                0.0: yval + 0,
313                0.15 * spd2: yval + 10.0,
314                0.287 * spd2: yval - 10.0,
315                0.398 * spd2: yval + 0,
316            },
317            loop=True,
318        )
319        animate(
320            light.node,
321            'intensity',
322            {
323                0.0: 0,
324                0.02 * sval: 0,
325                0.05 * sval: 0.8 * iscale,
326                0.08 * sval: 0,
327                0.1 * sval: 0,
328            },
329            loop=True,
330            offset=times[i],
331        )
332        _bascenev1.timer(
333            (times[i] + random.randint(1, int(duration)) * 40 * sval) / 1000.0,
334            light.node.delete,
335        )
336        activity.camera_flash_data.append(light)  # type: ignore

Create a strobing camera flash effect.

Category: Gameplay Functions

(as seen when a team wins a game) Duration is in seconds.

def camerashake(intensity: float = 1.0) -> None:
1028def camerashake(intensity: float = 1.0) -> None:
1029    """Shake the camera.
1030
1031    Category: **Gameplay Functions**
1032
1033    Note that some cameras and/or platforms (such as VR) may not display
1034    camera-shake, so do not rely on this always being visible to the
1035    player as a gameplay cue.
1036    """
1037    return None

Shake the camera.

Category: Gameplay Functions

Note that some cameras and/or platforms (such as VR) may not display camera-shake, so do not rely on this always being visible to the player as a gameplay cue.

class Campaign:
 22class Campaign:
 23    """Represents a unique set or series of baclassic.Level-s.
 24
 25    Category: **App Classes**
 26    """
 27
 28    def __init__(
 29        self,
 30        name: str,
 31        sequential: bool = True,
 32        levels: list[bascenev1.Level] | None = None,
 33    ):
 34        self._name = name
 35        self._sequential = sequential
 36        self._levels: list[bascenev1.Level] = []
 37        if levels is not None:
 38            for level in levels:
 39                self.addlevel(level)
 40
 41    @property
 42    def name(self) -> str:
 43        """The name of the Campaign."""
 44        return self._name
 45
 46    @property
 47    def sequential(self) -> bool:
 48        """Whether this Campaign's levels must be played in sequence."""
 49        return self._sequential
 50
 51    def addlevel(
 52        self, level: bascenev1.Level, index: int | None = None
 53    ) -> None:
 54        """Adds a baclassic.Level to the Campaign."""
 55        if level.campaign is not None:
 56            raise RuntimeError('Level already belongs to a campaign.')
 57        level.set_campaign(self, len(self._levels))
 58        if index is None:
 59            self._levels.append(level)
 60        else:
 61            self._levels.insert(index, level)
 62
 63    @property
 64    def levels(self) -> list[bascenev1.Level]:
 65        """The list of baclassic.Level-s in the Campaign."""
 66        return self._levels
 67
 68    def getlevel(self, name: str) -> bascenev1.Level:
 69        """Return a contained baclassic.Level by name."""
 70
 71        for level in self._levels:
 72            if level.name == name:
 73                return level
 74        raise babase.NotFoundError(
 75            "Level '" + name + "' not found in campaign '" + self.name + "'"
 76        )
 77
 78    def reset(self) -> None:
 79        """Reset state for the Campaign."""
 80        babase.app.config.setdefault('Campaigns', {})[self._name] = {}
 81
 82    # FIXME should these give/take baclassic.Level instances instead
 83    #  of level names?..
 84    def set_selected_level(self, levelname: str) -> None:
 85        """Set the Level currently selected in the UI (by name)."""
 86        self.configdict['Selection'] = levelname
 87        babase.app.config.commit()
 88
 89    def get_selected_level(self) -> str:
 90        """Return the name of the Level currently selected in the UI."""
 91        val = self.configdict.get('Selection', self._levels[0].name)
 92        assert isinstance(val, str)
 93        return val
 94
 95    @property
 96    def configdict(self) -> dict[str, Any]:
 97        """Return the live config dict for this campaign."""
 98        val: dict[str, Any] = babase.app.config.setdefault(
 99            'Campaigns', {}
100        ).setdefault(self._name, {})
101        assert isinstance(val, dict)
102        return val

Represents a unique set or series of baclassic.Level-s.

Category: App Classes

Campaign( name: str, sequential: bool = True, levels: list[Level] | None = None)
28    def __init__(
29        self,
30        name: str,
31        sequential: bool = True,
32        levels: list[bascenev1.Level] | None = None,
33    ):
34        self._name = name
35        self._sequential = sequential
36        self._levels: list[bascenev1.Level] = []
37        if levels is not None:
38            for level in levels:
39                self.addlevel(level)
name: str
41    @property
42    def name(self) -> str:
43        """The name of the Campaign."""
44        return self._name

The name of the Campaign.

sequential: bool
46    @property
47    def sequential(self) -> bool:
48        """Whether this Campaign's levels must be played in sequence."""
49        return self._sequential

Whether this Campaign's levels must be played in sequence.

def addlevel(self, level: Level, index: int | None = None) -> None:
51    def addlevel(
52        self, level: bascenev1.Level, index: int | None = None
53    ) -> None:
54        """Adds a baclassic.Level to the Campaign."""
55        if level.campaign is not None:
56            raise RuntimeError('Level already belongs to a campaign.')
57        level.set_campaign(self, len(self._levels))
58        if index is None:
59            self._levels.append(level)
60        else:
61            self._levels.insert(index, level)

Adds a baclassic.Level to the Campaign.

levels: list[Level]
63    @property
64    def levels(self) -> list[bascenev1.Level]:
65        """The list of baclassic.Level-s in the Campaign."""
66        return self._levels

The list of baclassic.Level-s in the Campaign.

def getlevel(self, name: str) -> Level:
68    def getlevel(self, name: str) -> bascenev1.Level:
69        """Return a contained baclassic.Level by name."""
70
71        for level in self._levels:
72            if level.name == name:
73                return level
74        raise babase.NotFoundError(
75            "Level '" + name + "' not found in campaign '" + self.name + "'"
76        )

Return a contained baclassic.Level by name.

def reset(self) -> None:
78    def reset(self) -> None:
79        """Reset state for the Campaign."""
80        babase.app.config.setdefault('Campaigns', {})[self._name] = {}

Reset state for the Campaign.

def set_selected_level(self, levelname: str) -> None:
84    def set_selected_level(self, levelname: str) -> None:
85        """Set the Level currently selected in the UI (by name)."""
86        self.configdict['Selection'] = levelname
87        babase.app.config.commit()

Set the Level currently selected in the UI (by name).

def get_selected_level(self) -> str:
89    def get_selected_level(self) -> str:
90        """Return the name of the Level currently selected in the UI."""
91        val = self.configdict.get('Selection', self._levels[0].name)
92        assert isinstance(val, str)
93        return val

Return the name of the Level currently selected in the UI.

configdict: dict[str, typing.Any]
 95    @property
 96    def configdict(self) -> dict[str, Any]:
 97        """Return the live config dict for this campaign."""
 98        val: dict[str, Any] = babase.app.config.setdefault(
 99            'Campaigns', {}
100        ).setdefault(self._name, {})
101        assert isinstance(val, dict)
102        return val

Return the live config dict for this campaign.

@dataclass
class CelebrateMessage:
223@dataclass
224class CelebrateMessage:
225    """Tells an object to celebrate.
226
227    Category: **Message Classes**
228    """
229
230    duration: float = 10.0
231    """Amount of time to celebrate in seconds."""

Tells an object to celebrate.

Category: Message Classes

CelebrateMessage(duration: float = 10.0)
duration: float = 10.0

Amount of time to celebrate in seconds.

@dataclass
class ChoiceSetting(bascenev1.Setting):
62@dataclass
63class ChoiceSetting(Setting):
64    """A setting with multiple choices.
65
66    Category: Settings Classes
67    """
68
69    choices: list[tuple[str, Any]]

A setting with multiple choices.

Category: Settings Classes

ChoiceSetting(name: str, default: Any, choices: list[tuple[str, typing.Any]])
choices: list[tuple[str, typing.Any]]
Inherited Members
Setting
name
default
class Chooser:
181class Chooser:
182    """A character/team selector for a bascenev1.Player.
183
184    Category: Gameplay Classes
185    """
186
187    def __del__(self) -> None:
188        # Just kill off our base node; the rest should go down with it.
189        if self._text_node:
190            self._text_node.delete()
191
192    def __init__(
193        self,
194        vpos: float,
195        sessionplayer: bascenev1.SessionPlayer,
196        lobby: 'Lobby',
197    ) -> None:
198        self._deek_sound = _bascenev1.getsound('deek')
199        self._click_sound = _bascenev1.getsound('click01')
200        self._punchsound = _bascenev1.getsound('punch01')
201        self._swish_sound = _bascenev1.getsound('punchSwish')
202        self._errorsound = _bascenev1.getsound('error')
203        self._mask_texture = _bascenev1.gettexture('characterIconMask')
204        self._vpos = vpos
205        self._lobby = weakref.ref(lobby)
206        self._sessionplayer = sessionplayer
207        self._inited = False
208        self._dead = False
209        self._text_node: bascenev1.Node | None = None
210        self._profilename = ''
211        self._profilenames: list[str] = []
212        self._ready: bool = False
213        self._character_names: list[str] = []
214        self._last_change: Sequence[float | int] = (0, 0)
215        self._profiles: dict[str, dict[str, Any]] = {}
216
217        app = babase.app
218        assert app.classic is not None
219
220        # Load available player profiles either from the local config or
221        # from the remote device.
222        self.reload_profiles()
223
224        # Note: this is just our local index out of available teams; *not*
225        # the team-id!
226        self._selected_team_index: int = self.lobby.next_add_team
227
228        # Store a persistent random character index and colors; we'll use this
229        # for the '_random' profile. Let's use their input_device id to seed
230        # it. This will give a persistent character for them between games
231        # and will distribute characters nicely if everyone is random.
232        self._random_color, self._random_highlight = get_player_profile_colors(
233            None
234        )
235
236        # To calc our random character we pick a random one out of our
237        # unlocked list and then locate that character's index in the full
238        # list.
239        char_index_offset: int = app.classic.lobby_random_char_index_offset
240        self._random_character_index = (
241            sessionplayer.inputdevice.id + char_index_offset
242        ) % len(self._character_names)
243
244        # Attempt to set an initial profile based on what was used previously
245        # for this input-device, etc.
246        self._profileindex = self._select_initial_profile()
247        self._profilename = self._profilenames[self._profileindex]
248
249        self._text_node = _bascenev1.newnode(
250            'text',
251            delegate=self,
252            attrs={
253                'position': (-100, self._vpos),
254                'maxwidth': 160,
255                'shadow': 0.5,
256                'vr_depth': -20,
257                'h_align': 'left',
258                'v_align': 'center',
259                'v_attach': 'top',
260            },
261        )
262        animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
263        self.icon = _bascenev1.newnode(
264            'image',
265            owner=self._text_node,
266            attrs={
267                'position': (-130, self._vpos + 20),
268                'mask_texture': self._mask_texture,
269                'vr_depth': -10,
270                'attach': 'topCenter',
271            },
272        )
273
274        animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
275
276        # Set our initial name to '<choosing player>' in case anyone asks.
277        self._sessionplayer.setname(
278            babase.Lstr(resource='choosingPlayerText').evaluate(), real=False
279        )
280
281        # Init these to our rando but they should get switched to the
282        # selected profile (if any) right after.
283        self._character_index = self._random_character_index
284        self._color = self._random_color
285        self._highlight = self._random_highlight
286
287        self.update_from_profile()
288        self.update_position()
289        self._inited = True
290
291        self._set_ready(False)
292
293    def _select_initial_profile(self) -> int:
294        app = babase.app
295        assert app.classic is not None
296        profilenames = self._profilenames
297        inputdevice = self._sessionplayer.inputdevice
298
299        # If we've got a set profile name for this device, work backwards
300        # from that to get our index.
301        dprofilename = app.config.get('Default Player Profiles', {}).get(
302            inputdevice.name + ' ' + inputdevice.unique_identifier
303        )
304        if dprofilename is not None and dprofilename in profilenames:
305            # If we got '__account__' and its local and we haven't marked
306            # anyone as the 'account profile' device yet, mark this guy as
307            # it. (prevents the next joiner from getting the account
308            # profile too).
309            if (
310                dprofilename == '__account__'
311                and not inputdevice.is_remote_client
312                and app.classic.lobby_account_profile_device_id is None
313            ):
314                app.classic.lobby_account_profile_device_id = inputdevice.id
315            return profilenames.index(dprofilename)
316
317        # We want to mark the first local input-device in the game
318        # as the 'account profile' device.
319        if (
320            not inputdevice.is_remote_client
321            and not inputdevice.is_controller_app
322        ):
323            if (
324                app.classic.lobby_account_profile_device_id is None
325                and '__account__' in profilenames
326            ):
327                app.classic.lobby_account_profile_device_id = inputdevice.id
328
329        # If this is the designated account-profile-device, try to default
330        # to the account profile.
331        if (
332            inputdevice.id == app.classic.lobby_account_profile_device_id
333            and '__account__' in profilenames
334        ):
335            return profilenames.index('__account__')
336
337        # If this is the controller app, it defaults to using a random
338        # profile (since we can pull the random name from the app).
339        if inputdevice.is_controller_app and '_random' in profilenames:
340            return profilenames.index('_random')
341
342        # If its a client connection, for now just force
343        # the account profile if possible.. (need to provide a
344        # way for clients to specify/remember their default
345        # profile on remote servers that do not already know them).
346        if inputdevice.is_remote_client and '__account__' in profilenames:
347            return profilenames.index('__account__')
348
349        # Cycle through our non-random profiles once; after
350        # that, everyone gets random.
351        while app.classic.lobby_random_profile_index < len(
352            profilenames
353        ) and profilenames[app.classic.lobby_random_profile_index] in (
354            '_random',
355            '__account__',
356            '_edit',
357        ):
358            app.classic.lobby_random_profile_index += 1
359        if app.classic.lobby_random_profile_index < len(profilenames):
360            profileindex: int = app.classic.lobby_random_profile_index
361            app.classic.lobby_random_profile_index += 1
362            return profileindex
363        assert '_random' in profilenames
364        return profilenames.index('_random')
365
366    @property
367    def sessionplayer(self) -> bascenev1.SessionPlayer:
368        """The bascenev1.SessionPlayer associated with this chooser."""
369        return self._sessionplayer
370
371    @property
372    def ready(self) -> bool:
373        """Whether this chooser is checked in as ready."""
374        return self._ready
375
376    def set_vpos(self, vpos: float) -> None:
377        """(internal)"""
378        self._vpos = vpos
379
380    def set_dead(self, val: bool) -> None:
381        """(internal)"""
382        self._dead = val
383
384    @property
385    def sessionteam(self) -> bascenev1.SessionTeam:
386        """Return this chooser's currently selected bascenev1.SessionTeam."""
387        return self.lobby.sessionteams[self._selected_team_index]
388
389    @property
390    def lobby(self) -> bascenev1.Lobby:
391        """The chooser's baclassic.Lobby."""
392        lobby = self._lobby()
393        if lobby is None:
394            raise babase.NotFoundError('Lobby does not exist.')
395        return lobby
396
397    def get_lobby(self) -> bascenev1.Lobby | None:
398        """Return this chooser's lobby if it still exists; otherwise None."""
399        return self._lobby()
400
401    def update_from_profile(self) -> None:
402        """Set character/colors based on the current profile."""
403        assert babase.app.classic is not None
404        self._profilename = self._profilenames[self._profileindex]
405        if self._profilename == '_edit':
406            pass
407        elif self._profilename == '_random':
408            self._character_index = self._random_character_index
409            self._color = self._random_color
410            self._highlight = self._random_highlight
411        else:
412            character = self._profiles[self._profilename]['character']
413
414            # At the moment we're not properly pulling the list
415            # of available characters from clients, so profiles might use a
416            # character not in their list. For now, just go ahead and add
417            # a character name to their list as long as we're aware of it.
418            # This just means they won't always be able to override their
419            # character to others they own, but profile characters
420            # should work (and we validate profiles on the master server
421            # so no exploit opportunities)
422            if (
423                character not in self._character_names
424                and character in babase.app.classic.spaz_appearances
425            ):
426                self._character_names.append(character)
427            self._character_index = self._character_names.index(character)
428            self._color, self._highlight = get_player_profile_colors(
429                self._profilename, profiles=self._profiles
430            )
431        self._update_icon()
432        self._update_text()
433
434    def reload_profiles(self) -> None:
435        """Reload all player profiles."""
436
437        app = babase.app
438        env = app.env
439        assert app.classic is not None
440
441        # Re-construct our profile index and other stuff since the profile
442        # list might have changed.
443        input_device = self._sessionplayer.inputdevice
444        is_remote = input_device.is_remote_client
445        is_test_input = input_device.is_test_input
446
447        # Pull this player's list of unlocked characters.
448        if is_remote:
449            # TODO: Pull this from the remote player.
450            # (but make sure to filter it to the ones we've got).
451            self._character_names = ['Spaz']
452        else:
453            self._character_names = self.lobby.character_names_local_unlocked
454
455        # If we're a local player, pull our local profiles from the config.
456        # Otherwise ask the remote-input-device for its profile list.
457        if is_remote:
458            self._profiles = input_device.get_player_profiles()
459        else:
460            self._profiles = app.config.get('Player Profiles', {})
461
462        # These may have come over the wire from an older
463        # (non-unicode/non-json) version.
464        # Make sure they conform to our standards
465        # (unicode strings, no tuples, etc)
466        self._profiles = app.classic.json_prep(self._profiles)
467
468        # Filter out any characters we're unaware of.
469        for profile in list(self._profiles.items()):
470            if (
471                profile[1].get('character', '')
472                not in app.classic.spaz_appearances
473            ):
474                profile[1]['character'] = 'Spaz'
475
476        # Add in a random one so we're ok even if there's no user profiles.
477        self._profiles['_random'] = {}
478
479        # In kiosk mode we disable account profiles to force random.
480        if env.demo or env.arcade:
481            if '__account__' in self._profiles:
482                del self._profiles['__account__']
483
484        # For local devices, add it an 'edit' option which will pop up
485        # the profile window.
486        if not is_remote and not is_test_input and not (env.demo or env.arcade):
487            self._profiles['_edit'] = {}
488
489        # Build a sorted name list we can iterate through.
490        self._profilenames = list(self._profiles.keys())
491        self._profilenames.sort(key=lambda x: x.lower())
492
493        if self._profilename in self._profilenames:
494            self._profileindex = self._profilenames.index(self._profilename)
495        else:
496            self._profileindex = 0
497            # noinspection PyUnresolvedReferences
498            self._profilename = self._profilenames[self._profileindex]
499
500    def update_position(self) -> None:
501        """Update this chooser's position."""
502
503        assert self._text_node
504        spacing = 350
505        sessionteams = self.lobby.sessionteams
506        offs = (
507            spacing * -0.5 * len(sessionteams)
508            + spacing * self._selected_team_index
509            + 250
510        )
511        if len(sessionteams) > 1:
512            offs -= 35
513        animate_array(
514            self._text_node,
515            'position',
516            2,
517            {0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
518        )
519        animate_array(
520            self.icon,
521            'position',
522            2,
523            {0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
524        )
525
526    def get_character_name(self) -> str:
527        """Return the selected character name."""
528        return self._character_names[self._character_index]
529
530    def _do_nothing(self) -> None:
531        """Does nothing! (hacky way to disable callbacks)"""
532
533    def _getname(self, full: bool = False) -> str:
534        name_raw = name = self._profilenames[self._profileindex]
535        clamp = False
536        if name == '_random':
537            try:
538                name = self._sessionplayer.inputdevice.get_default_player_name()
539            except Exception:
540                logging.exception('Error getting _random chooser name.')
541                name = 'Invalid'
542            clamp = not full
543        elif name == '__account__':
544            try:
545                name = self._sessionplayer.inputdevice.get_v1_account_name(full)
546            except Exception:
547                logging.exception('Error getting account name for chooser.')
548                name = 'Invalid'
549            clamp = not full
550        elif name == '_edit':
551            # Explicitly flattening this to a str; it's only relevant on
552            # the host so that's ok.
553            name = babase.Lstr(
554                resource='createEditPlayerText',
555                fallback_resource='editProfileWindow.titleNewText',
556            ).evaluate()
557        else:
558            # If we have a regular profile marked as global with an icon,
559            # use it (for full only).
560            if full:
561                try:
562                    if self._profiles[name_raw].get('global', False):
563                        icon = (
564                            self._profiles[name_raw]['icon']
565                            if 'icon' in self._profiles[name_raw]
566                            else babase.charstr(babase.SpecialChar.LOGO)
567                        )
568                        name = icon + name
569                except Exception:
570                    logging.exception('Error applying global icon.')
571            else:
572                # We now clamp non-full versions of names so there's at
573                # least some hope of reading them in-game.
574                clamp = True
575
576        if clamp:
577            if len(name) > 10:
578                name = name[:10] + '...'
579        return name
580
581    def _set_ready(self, ready: bool) -> None:
582        # pylint: disable=cyclic-import
583
584        classic = babase.app.classic
585        assert classic is not None
586
587        profilename = self._profilenames[self._profileindex]
588
589        # Handle '_edit' as a special case.
590        if profilename == '_edit' and ready:
591            with babase.ContextRef.empty():
592                classic.profile_browser_window(in_main_menu=False)
593
594                # Give their input-device UI ownership too
595                # (prevent someone else from snatching it in crowded games)
596                babase.set_ui_input_device(self._sessionplayer.inputdevice.id)
597            return
598
599        if not ready:
600            self._sessionplayer.assigninput(
601                babase.InputType.LEFT_PRESS,
602                babase.Call(self.handlemessage, ChangeMessage('team', -1)),
603            )
604            self._sessionplayer.assigninput(
605                babase.InputType.RIGHT_PRESS,
606                babase.Call(self.handlemessage, ChangeMessage('team', 1)),
607            )
608            self._sessionplayer.assigninput(
609                babase.InputType.BOMB_PRESS,
610                babase.Call(self.handlemessage, ChangeMessage('character', 1)),
611            )
612            self._sessionplayer.assigninput(
613                babase.InputType.UP_PRESS,
614                babase.Call(
615                    self.handlemessage, ChangeMessage('profileindex', -1)
616                ),
617            )
618            self._sessionplayer.assigninput(
619                babase.InputType.DOWN_PRESS,
620                babase.Call(
621                    self.handlemessage, ChangeMessage('profileindex', 1)
622                ),
623            )
624            self._sessionplayer.assigninput(
625                (
626                    babase.InputType.JUMP_PRESS,
627                    babase.InputType.PICK_UP_PRESS,
628                    babase.InputType.PUNCH_PRESS,
629                ),
630                babase.Call(self.handlemessage, ChangeMessage('ready', 1)),
631            )
632            self._ready = False
633            self._update_text()
634            self._sessionplayer.setname('untitled', real=False)
635        else:
636            self._sessionplayer.assigninput(
637                (
638                    babase.InputType.LEFT_PRESS,
639                    babase.InputType.RIGHT_PRESS,
640                    babase.InputType.UP_PRESS,
641                    babase.InputType.DOWN_PRESS,
642                    babase.InputType.JUMP_PRESS,
643                    babase.InputType.BOMB_PRESS,
644                    babase.InputType.PICK_UP_PRESS,
645                ),
646                self._do_nothing,
647            )
648            self._sessionplayer.assigninput(
649                (
650                    babase.InputType.JUMP_PRESS,
651                    babase.InputType.BOMB_PRESS,
652                    babase.InputType.PICK_UP_PRESS,
653                    babase.InputType.PUNCH_PRESS,
654                ),
655                babase.Call(self.handlemessage, ChangeMessage('ready', 0)),
656            )
657
658            # Store the last profile picked by this input for reuse.
659            input_device = self._sessionplayer.inputdevice
660            name = input_device.name
661            unique_id = input_device.unique_identifier
662            device_profiles = babase.app.config.setdefault(
663                'Default Player Profiles', {}
664            )
665
666            # Make an exception if we have no custom profiles and are set
667            # to random; in that case we'll want to start picking up custom
668            # profiles if/when one is made so keep our setting cleared.
669            special = ('_random', '_edit', '__account__')
670            have_custom_profiles = any(p not in special for p in self._profiles)
671
672            profilekey = name + ' ' + unique_id
673            if profilename == '_random' and not have_custom_profiles:
674                if profilekey in device_profiles:
675                    del device_profiles[profilekey]
676            else:
677                device_profiles[profilekey] = profilename
678            babase.app.config.commit()
679
680            # Set this player's short and full name.
681            self._sessionplayer.setname(
682                self._getname(), self._getname(full=True), real=True
683            )
684            self._ready = True
685            self._update_text()
686
687            # Inform the session that this player is ready.
688            _bascenev1.getsession().handlemessage(PlayerReadyMessage(self))
689
690    def _handle_ready_msg(self, ready: bool) -> None:
691        force_team_switch = False
692
693        # Team auto-balance kicks us to another team if we try to
694        # join the team with the most players.
695        if not self._ready:
696            if babase.app.config.get('Auto Balance Teams', False):
697                lobby = self.lobby
698                sessionteams = lobby.sessionteams
699                if len(sessionteams) > 1:
700                    # First, calc how many players are on each team
701                    # ..we need to count both active players and
702                    # choosers that have been marked as ready.
703                    team_player_counts = {}
704                    for sessionteam in sessionteams:
705                        team_player_counts[sessionteam.id] = len(
706                            sessionteam.players
707                        )
708                    for chooser in lobby.choosers:
709                        if chooser.ready:
710                            team_player_counts[chooser.sessionteam.id] += 1
711                    largest_team_size = max(team_player_counts.values())
712                    smallest_team_size = min(team_player_counts.values())
713
714                    # Force switch if we're on the biggest sessionteam
715                    # and there's a smaller one available.
716                    if (
717                        largest_team_size != smallest_team_size
718                        and team_player_counts[self.sessionteam.id]
719                        >= largest_team_size
720                    ):
721                        force_team_switch = True
722
723        # Either force switch teams, or actually for realsies do the set-ready.
724        if force_team_switch:
725            self._errorsound.play()
726            self.handlemessage(ChangeMessage('team', 1))
727        else:
728            self._punchsound.play()
729            self._set_ready(ready)
730
731    # TODO: should handle this at the engine layer so this is unnecessary.
732    def _handle_repeat_message_attack(self) -> None:
733        now = babase.apptime()
734        count = self._last_change[1]
735        if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
736            count += 1
737            if count > MAX_QUICK_CHANGE_COUNT:
738                _bascenev1.disconnect_client(
739                    self._sessionplayer.inputdevice.client_id
740                )
741        elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
742            count = 0
743        self._last_change = (now, count)
744
745    def handlemessage(self, msg: Any) -> Any:
746        """Standard generic message handler."""
747
748        if isinstance(msg, ChangeMessage):
749            self._handle_repeat_message_attack()
750
751            # If we've been removed from the lobby, ignore this stuff.
752            if self._dead:
753                logging.error('chooser got ChangeMessage after dying')
754                return
755
756            if not self._text_node:
757                logging.error('got ChangeMessage after nodes died')
758                return
759
760            if msg.what == 'team':
761                sessionteams = self.lobby.sessionteams
762                if len(sessionteams) > 1:
763                    self._swish_sound.play()
764                self._selected_team_index = (
765                    self._selected_team_index + msg.value
766                ) % len(sessionteams)
767                self._update_text()
768                self.update_position()
769                self._update_icon()
770
771            elif msg.what == 'profileindex':
772                if len(self._profilenames) == 1:
773                    # This should be pretty hard to hit now with
774                    # automatic local accounts.
775                    _bascenev1.getsound('error').play()
776                else:
777                    # Pick the next player profile and assign our name
778                    # and character based on that.
779                    self._deek_sound.play()
780                    self._profileindex = (self._profileindex + msg.value) % len(
781                        self._profilenames
782                    )
783                    self.update_from_profile()
784
785            elif msg.what == 'character':
786                self._click_sound.play()
787                # update our index in our local list of characters
788                self._character_index = (
789                    self._character_index + msg.value
790                ) % len(self._character_names)
791                self._update_text()
792                self._update_icon()
793
794            elif msg.what == 'ready':
795                self._handle_ready_msg(bool(msg.value))
796
797    def _update_text(self) -> None:
798        assert self._text_node is not None
799        if self._ready:
800            # Once we're ready, we've saved the name, so lets ask the system
801            # for it so we get appended numbers and stuff.
802            text = babase.Lstr(value=self._sessionplayer.getname(full=True))
803            text = babase.Lstr(
804                value='${A} (${B})',
805                subs=[
806                    ('${A}', text),
807                    ('${B}', babase.Lstr(resource='readyText')),
808                ],
809            )
810        else:
811            text = babase.Lstr(value=self._getname(full=True))
812
813        can_switch_teams = len(self.lobby.sessionteams) > 1
814
815        # Flash as we're coming in.
816        fin_color = babase.safecolor(self.get_color()) + (1,)
817        if not self._inited:
818            animate_array(
819                self._text_node,
820                'color',
821                4,
822                {0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
823            )
824        else:
825            # Blend if we're in teams mode; switch instantly otherwise.
826            if can_switch_teams:
827                animate_array(
828                    self._text_node,
829                    'color',
830                    4,
831                    {0: self._text_node.color, 0.1: fin_color},
832                )
833            else:
834                self._text_node.color = fin_color
835
836        self._text_node.text = text
837
838    def get_color(self) -> Sequence[float]:
839        """Return the currently selected color."""
840        val: Sequence[float]
841        if self.lobby.use_team_colors:
842            val = self.lobby.sessionteams[self._selected_team_index].color
843        else:
844            val = self._color
845        if len(val) != 3:
846            print('get_color: ignoring invalid color of len', len(val))
847            val = (0, 1, 0)
848        return val
849
850    def get_highlight(self) -> Sequence[float]:
851        """Return the currently selected highlight."""
852        if self._profilenames[self._profileindex] == '_edit':
853            return 0, 1, 0
854
855        # If we're using team colors we wanna make sure our highlight color
856        # isn't too close to any other team's color.
857        highlight = list(self._highlight)
858        if self.lobby.use_team_colors:
859            for i, sessionteam in enumerate(self.lobby.sessionteams):
860                if i != self._selected_team_index:
861                    # Find the dominant component of this sessionteam's color
862                    # and adjust ours so that the component is
863                    # not super-dominant.
864                    max_val = 0.0
865                    max_index = 0
866                    for j in range(3):
867                        if sessionteam.color[j] > max_val:
868                            max_val = sessionteam.color[j]
869                            max_index = j
870                    that_color_for_us = highlight[max_index]
871                    our_second_biggest = max(
872                        highlight[(max_index + 1) % 3],
873                        highlight[(max_index + 2) % 3],
874                    )
875                    diff = that_color_for_us - our_second_biggest
876                    if diff > 0:
877                        highlight[max_index] -= diff * 0.6
878                        highlight[(max_index + 1) % 3] += diff * 0.3
879                        highlight[(max_index + 2) % 3] += diff * 0.2
880        return highlight
881
882    def getplayer(self) -> bascenev1.SessionPlayer:
883        """Return the player associated with this chooser."""
884        return self._sessionplayer
885
886    def _update_icon(self) -> None:
887        assert babase.app.classic is not None
888        if self._profilenames[self._profileindex] == '_edit':
889            tex = _bascenev1.gettexture('black')
890            tint_tex = _bascenev1.gettexture('black')
891            self.icon.color = (1, 1, 1)
892            self.icon.texture = tex
893            self.icon.tint_texture = tint_tex
894            self.icon.tint_color = (0, 1, 0)
895            return
896
897        try:
898            tex_name = babase.app.classic.spaz_appearances[
899                self._character_names[self._character_index]
900            ].icon_texture
901            tint_tex_name = babase.app.classic.spaz_appearances[
902                self._character_names[self._character_index]
903            ].icon_mask_texture
904        except Exception:
905            logging.exception('Error updating char icon list')
906            tex_name = 'neoSpazIcon'
907            tint_tex_name = 'neoSpazIconColorMask'
908
909        tex = _bascenev1.gettexture(tex_name)
910        tint_tex = _bascenev1.gettexture(tint_tex_name)
911
912        self.icon.color = (1, 1, 1)
913        self.icon.texture = tex
914        self.icon.tint_texture = tint_tex
915        clr = self.get_color()
916        clr2 = self.get_highlight()
917
918        can_switch_teams = len(self.lobby.sessionteams) > 1
919
920        # If we're initing, flash.
921        if not self._inited:
922            animate_array(
923                self.icon,
924                'color',
925                3,
926                {0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)},
927            )
928
929        # Blend in teams mode; switch instantly in ffa-mode.
930        if can_switch_teams:
931            animate_array(
932                self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
933            )
934        else:
935            self.icon.tint_color = clr
936        self.icon.tint2_color = clr2
937
938        # Store the icon info the the player.
939        self._sessionplayer.set_icon_info(tex_name, tint_tex_name, clr, clr2)

A character/team selector for a bascenev1.Player.

Category: Gameplay Classes

Chooser( vpos: float, sessionplayer: SessionPlayer, lobby: Lobby)
192    def __init__(
193        self,
194        vpos: float,
195        sessionplayer: bascenev1.SessionPlayer,
196        lobby: 'Lobby',
197    ) -> None:
198        self._deek_sound = _bascenev1.getsound('deek')
199        self._click_sound = _bascenev1.getsound('click01')
200        self._punchsound = _bascenev1.getsound('punch01')
201        self._swish_sound = _bascenev1.getsound('punchSwish')
202        self._errorsound = _bascenev1.getsound('error')
203        self._mask_texture = _bascenev1.gettexture('characterIconMask')
204        self._vpos = vpos
205        self._lobby = weakref.ref(lobby)
206        self._sessionplayer = sessionplayer
207        self._inited = False
208        self._dead = False
209        self._text_node: bascenev1.Node | None = None
210        self._profilename = ''
211        self._profilenames: list[str] = []
212        self._ready: bool = False
213        self._character_names: list[str] = []
214        self._last_change: Sequence[float | int] = (0, 0)
215        self._profiles: dict[str, dict[str, Any]] = {}
216
217        app = babase.app
218        assert app.classic is not None
219
220        # Load available player profiles either from the local config or
221        # from the remote device.
222        self.reload_profiles()
223
224        # Note: this is just our local index out of available teams; *not*
225        # the team-id!
226        self._selected_team_index: int = self.lobby.next_add_team
227
228        # Store a persistent random character index and colors; we'll use this
229        # for the '_random' profile. Let's use their input_device id to seed
230        # it. This will give a persistent character for them between games
231        # and will distribute characters nicely if everyone is random.
232        self._random_color, self._random_highlight = get_player_profile_colors(
233            None
234        )
235
236        # To calc our random character we pick a random one out of our
237        # unlocked list and then locate that character's index in the full
238        # list.
239        char_index_offset: int = app.classic.lobby_random_char_index_offset
240        self._random_character_index = (
241            sessionplayer.inputdevice.id + char_index_offset
242        ) % len(self._character_names)
243
244        # Attempt to set an initial profile based on what was used previously
245        # for this input-device, etc.
246        self._profileindex = self._select_initial_profile()
247        self._profilename = self._profilenames[self._profileindex]
248
249        self._text_node = _bascenev1.newnode(
250            'text',
251            delegate=self,
252            attrs={
253                'position': (-100, self._vpos),
254                'maxwidth': 160,
255                'shadow': 0.5,
256                'vr_depth': -20,
257                'h_align': 'left',
258                'v_align': 'center',
259                'v_attach': 'top',
260            },
261        )
262        animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
263        self.icon = _bascenev1.newnode(
264            'image',
265            owner=self._text_node,
266            attrs={
267                'position': (-130, self._vpos + 20),
268                'mask_texture': self._mask_texture,
269                'vr_depth': -10,
270                'attach': 'topCenter',
271            },
272        )
273
274        animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
275
276        # Set our initial name to '<choosing player>' in case anyone asks.
277        self._sessionplayer.setname(
278            babase.Lstr(resource='choosingPlayerText').evaluate(), real=False
279        )
280
281        # Init these to our rando but they should get switched to the
282        # selected profile (if any) right after.
283        self._character_index = self._random_character_index
284        self._color = self._random_color
285        self._highlight = self._random_highlight
286
287        self.update_from_profile()
288        self.update_position()
289        self._inited = True
290
291        self._set_ready(False)
icon
sessionplayer: SessionPlayer
366    @property
367    def sessionplayer(self) -> bascenev1.SessionPlayer:
368        """The bascenev1.SessionPlayer associated with this chooser."""
369        return self._sessionplayer

The bascenev1.SessionPlayer associated with this chooser.

ready: bool
371    @property
372    def ready(self) -> bool:
373        """Whether this chooser is checked in as ready."""
374        return self._ready

Whether this chooser is checked in as ready.

sessionteam: SessionTeam
384    @property
385    def sessionteam(self) -> bascenev1.SessionTeam:
386        """Return this chooser's currently selected bascenev1.SessionTeam."""
387        return self.lobby.sessionteams[self._selected_team_index]

Return this chooser's currently selected bascenev1.SessionTeam.

lobby: Lobby
389    @property
390    def lobby(self) -> bascenev1.Lobby:
391        """The chooser's baclassic.Lobby."""
392        lobby = self._lobby()
393        if lobby is None:
394            raise babase.NotFoundError('Lobby does not exist.')
395        return lobby

The chooser's baclassic.Lobby.

def get_lobby(self) -> Lobby | None:
397    def get_lobby(self) -> bascenev1.Lobby | None:
398        """Return this chooser's lobby if it still exists; otherwise None."""
399        return self._lobby()

Return this chooser's lobby if it still exists; otherwise None.

def update_from_profile(self) -> None:
401    def update_from_profile(self) -> None:
402        """Set character/colors based on the current profile."""
403        assert babase.app.classic is not None
404        self._profilename = self._profilenames[self._profileindex]
405        if self._profilename == '_edit':
406            pass
407        elif self._profilename == '_random':
408            self._character_index = self._random_character_index
409            self._color = self._random_color
410            self._highlight = self._random_highlight
411        else:
412            character = self._profiles[self._profilename]['character']
413
414            # At the moment we're not properly pulling the list
415            # of available characters from clients, so profiles might use a
416            # character not in their list. For now, just go ahead and add
417            # a character name to their list as long as we're aware of it.
418            # This just means they won't always be able to override their
419            # character to others they own, but profile characters
420            # should work (and we validate profiles on the master server
421            # so no exploit opportunities)
422            if (
423                character not in self._character_names
424                and character in babase.app.classic.spaz_appearances
425            ):
426                self._character_names.append(character)
427            self._character_index = self._character_names.index(character)
428            self._color, self._highlight = get_player_profile_colors(
429                self._profilename, profiles=self._profiles
430            )
431        self._update_icon()
432        self._update_text()

Set character/colors based on the current profile.

def reload_profiles(self) -> None:
434    def reload_profiles(self) -> None:
435        """Reload all player profiles."""
436
437        app = babase.app
438        env = app.env
439        assert app.classic is not None
440
441        # Re-construct our profile index and other stuff since the profile
442        # list might have changed.
443        input_device = self._sessionplayer.inputdevice
444        is_remote = input_device.is_remote_client
445        is_test_input = input_device.is_test_input
446
447        # Pull this player's list of unlocked characters.
448        if is_remote:
449            # TODO: Pull this from the remote player.
450            # (but make sure to filter it to the ones we've got).
451            self._character_names = ['Spaz']
452        else:
453            self._character_names = self.lobby.character_names_local_unlocked
454
455        # If we're a local player, pull our local profiles from the config.
456        # Otherwise ask the remote-input-device for its profile list.
457        if is_remote:
458            self._profiles = input_device.get_player_profiles()
459        else:
460            self._profiles = app.config.get('Player Profiles', {})
461
462        # These may have come over the wire from an older
463        # (non-unicode/non-json) version.
464        # Make sure they conform to our standards
465        # (unicode strings, no tuples, etc)
466        self._profiles = app.classic.json_prep(self._profiles)
467
468        # Filter out any characters we're unaware of.
469        for profile in list(self._profiles.items()):
470            if (
471                profile[1].get('character', '')
472                not in app.classic.spaz_appearances
473            ):
474                profile[1]['character'] = 'Spaz'
475
476        # Add in a random one so we're ok even if there's no user profiles.
477        self._profiles['_random'] = {}
478
479        # In kiosk mode we disable account profiles to force random.
480        if env.demo or env.arcade:
481            if '__account__' in self._profiles:
482                del self._profiles['__account__']
483
484        # For local devices, add it an 'edit' option which will pop up
485        # the profile window.
486        if not is_remote and not is_test_input and not (env.demo or env.arcade):
487            self._profiles['_edit'] = {}
488
489        # Build a sorted name list we can iterate through.
490        self._profilenames = list(self._profiles.keys())
491        self._profilenames.sort(key=lambda x: x.lower())
492
493        if self._profilename in self._profilenames:
494            self._profileindex = self._profilenames.index(self._profilename)
495        else:
496            self._profileindex = 0
497            # noinspection PyUnresolvedReferences
498            self._profilename = self._profilenames[self._profileindex]

Reload all player profiles.

def update_position(self) -> None:
500    def update_position(self) -> None:
501        """Update this chooser's position."""
502
503        assert self._text_node
504        spacing = 350
505        sessionteams = self.lobby.sessionteams
506        offs = (
507            spacing * -0.5 * len(sessionteams)
508            + spacing * self._selected_team_index
509            + 250
510        )
511        if len(sessionteams) > 1:
512            offs -= 35
513        animate_array(
514            self._text_node,
515            'position',
516            2,
517            {0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
518        )
519        animate_array(
520            self.icon,
521            'position',
522            2,
523            {0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
524        )

Update this chooser's position.

def get_character_name(self) -> str:
526    def get_character_name(self) -> str:
527        """Return the selected character name."""
528        return self._character_names[self._character_index]

Return the selected character name.

def handlemessage(self, msg: Any) -> Any:
745    def handlemessage(self, msg: Any) -> Any:
746        """Standard generic message handler."""
747
748        if isinstance(msg, ChangeMessage):
749            self._handle_repeat_message_attack()
750
751            # If we've been removed from the lobby, ignore this stuff.
752            if self._dead:
753                logging.error('chooser got ChangeMessage after dying')
754                return
755
756            if not self._text_node:
757                logging.error('got ChangeMessage after nodes died')
758                return
759
760            if msg.what == 'team':
761                sessionteams = self.lobby.sessionteams
762                if len(sessionteams) > 1:
763                    self._swish_sound.play()
764                self._selected_team_index = (
765                    self._selected_team_index + msg.value
766                ) % len(sessionteams)
767                self._update_text()
768                self.update_position()
769                self._update_icon()
770
771            elif msg.what == 'profileindex':
772                if len(self._profilenames) == 1:
773                    # This should be pretty hard to hit now with
774                    # automatic local accounts.
775                    _bascenev1.getsound('error').play()
776                else:
777                    # Pick the next player profile and assign our name
778                    # and character based on that.
779                    self._deek_sound.play()
780                    self._profileindex = (self._profileindex + msg.value) % len(
781                        self._profilenames
782                    )
783                    self.update_from_profile()
784
785            elif msg.what == 'character':
786                self._click_sound.play()
787                # update our index in our local list of characters
788                self._character_index = (
789                    self._character_index + msg.value
790                ) % len(self._character_names)
791                self._update_text()
792                self._update_icon()
793
794            elif msg.what == 'ready':
795                self._handle_ready_msg(bool(msg.value))

Standard generic message handler.

def get_color(self) -> Sequence[float]:
838    def get_color(self) -> Sequence[float]:
839        """Return the currently selected color."""
840        val: Sequence[float]
841        if self.lobby.use_team_colors:
842            val = self.lobby.sessionteams[self._selected_team_index].color
843        else:
844            val = self._color
845        if len(val) != 3:
846            print('get_color: ignoring invalid color of len', len(val))
847            val = (0, 1, 0)
848        return val

Return the currently selected color.

def get_highlight(self) -> Sequence[float]:
850    def get_highlight(self) -> Sequence[float]:
851        """Return the currently selected highlight."""
852        if self._profilenames[self._profileindex] == '_edit':
853            return 0, 1, 0
854
855        # If we're using team colors we wanna make sure our highlight color
856        # isn't too close to any other team's color.
857        highlight = list(self._highlight)
858        if self.lobby.use_team_colors:
859            for i, sessionteam in enumerate(self.lobby.sessionteams):
860                if i != self._selected_team_index:
861                    # Find the dominant component of this sessionteam's color
862                    # and adjust ours so that the component is
863                    # not super-dominant.
864                    max_val = 0.0
865                    max_index = 0
866                    for j in range(3):
867                        if sessionteam.color[j] > max_val:
868                            max_val = sessionteam.color[j]
869                            max_index = j
870                    that_color_for_us = highlight[max_index]
871                    our_second_biggest = max(
872                        highlight[(max_index + 1) % 3],
873                        highlight[(max_index + 2) % 3],
874                    )
875                    diff = that_color_for_us - our_second_biggest
876                    if diff > 0:
877                        highlight[max_index] -= diff * 0.6
878                        highlight[(max_index + 1) % 3] += diff * 0.3
879                        highlight[(max_index + 2) % 3] += diff * 0.2
880        return highlight

Return the currently selected highlight.

def getplayer(self) -> SessionPlayer:
882    def getplayer(self) -> bascenev1.SessionPlayer:
883        """Return the player associated with this chooser."""
884        return self._sessionplayer

Return the player associated with this chooser.

class Collision:
17class Collision:
18    """A class providing info about occurring collisions.
19
20    Category: **Gameplay Classes**
21    """
22
23    @property
24    def position(self) -> bascenev1.Vec3:
25        """The position of the current collision."""
26        return babase.Vec3(_bascenev1.get_collision_info('position'))
27
28    @property
29    def sourcenode(self) -> bascenev1.Node:
30        """The node containing the material triggering the current callback.
31
32        Throws a bascenev1.NodeNotFoundError if the node does not exist,
33        though the node should always exist (at least at the start of the
34        collision callback).
35        """
36        node = _bascenev1.get_collision_info('sourcenode')
37        assert isinstance(node, (_bascenev1.Node, type(None)))
38        if not node:
39            raise babase.NodeNotFoundError()
40        return node
41
42    @property
43    def opposingnode(self) -> bascenev1.Node:
44        """The node the current callback material node is hitting.
45
46        Throws a bascenev1.NodeNotFoundError if the node does not exist.
47        This can be expected in some cases such as in 'disconnect'
48        callbacks triggered by deleting a currently-colliding node.
49        """
50        node = _bascenev1.get_collision_info('opposingnode')
51        assert isinstance(node, (_bascenev1.Node, type(None)))
52        if not node:
53            raise babase.NodeNotFoundError()
54        return node
55
56    @property
57    def opposingbody(self) -> int:
58        """The body index on the opposing node in the current collision."""
59        body = _bascenev1.get_collision_info('opposingbody')
60        assert isinstance(body, int)
61        return body

A class providing info about occurring collisions.

Category: Gameplay Classes

position: Vec3
23    @property
24    def position(self) -> bascenev1.Vec3:
25        """The position of the current collision."""
26        return babase.Vec3(_bascenev1.get_collision_info('position'))

The position of the current collision.

sourcenode: Node
28    @property
29    def sourcenode(self) -> bascenev1.Node:
30        """The node containing the material triggering the current callback.
31
32        Throws a bascenev1.NodeNotFoundError if the node does not exist,
33        though the node should always exist (at least at the start of the
34        collision callback).
35        """
36        node = _bascenev1.get_collision_info('sourcenode')
37        assert isinstance(node, (_bascenev1.Node, type(None)))
38        if not node:
39            raise babase.NodeNotFoundError()
40        return node

The node containing the material triggering the current callback.

Throws a bascenev1.NodeNotFoundError if the node does not exist, though the node should always exist (at least at the start of the collision callback).

opposingnode: Node
42    @property
43    def opposingnode(self) -> bascenev1.Node:
44        """The node the current callback material node is hitting.
45
46        Throws a bascenev1.NodeNotFoundError if the node does not exist.
47        This can be expected in some cases such as in 'disconnect'
48        callbacks triggered by deleting a currently-colliding node.
49        """
50        node = _bascenev1.get_collision_info('opposingnode')
51        assert isinstance(node, (_bascenev1.Node, type(None)))
52        if not node:
53            raise babase.NodeNotFoundError()
54        return node

The node the current callback material node is hitting.

Throws a bascenev1.NodeNotFoundError if the node does not exist. This can be expected in some cases such as in 'disconnect' callbacks triggered by deleting a currently-colliding node.

opposingbody: int
56    @property
57    def opposingbody(self) -> int:
58        """The body index on the opposing node in the current collision."""
59        body = _bascenev1.get_collision_info('opposingbody')
60        assert isinstance(body, int)
61        return body

The body index on the opposing node in the current collision.

class CollisionMesh:
128class CollisionMesh:
129    """A reference to a collision-mesh.
130
131    Category: **Asset Classes**
132
133    Use bascenev1.getcollisionmesh() to instantiate one.
134    """
135
136    pass

A reference to a collision-mesh.

Category: Asset Classes

Use bascenev1.getcollisionmesh() to instantiate one.

class ContextError(builtins.Exception):
16class ContextError(Exception):
17    """Exception raised when a call is made in an invalid context.
18
19    Category: **Exception Classes**
20
21    Examples of this include calling UI functions within an Activity context
22    or calling scene manipulation functions outside of a game context.
23    """

Exception raised when a call is made in an invalid context.

Category: Exception Classes

Examples of this include calling UI functions within an Activity context or calling scene manipulation functions outside of a game context.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class ContextRef:
148class ContextRef:
149    """Store or use a ballistica context.
150
151    Category: **General Utility Classes**
152
153    Many operations such as bascenev1.newnode() or bascenev1.gettexture()
154    operate implicitly on a current 'context'. A context is some sort of
155    state that functionality can implicitly use. Context determines, for
156    example, which scene nodes or textures get added to without having to
157    specify it explicitly in the newnode()/gettexture() call. Contexts can
158    also affect object lifecycles; for example a babase.ContextCall will
159    become a no-op when the context it was created in is destroyed.
160
161    In general, if you are a modder, you should not need to worry about
162    contexts; mod code should mostly be getting run in the correct
163    context and timers and other callbacks will take care of saving
164    and restoring contexts automatically. There may be rare cases,
165    however, where you need to deal directly with contexts, and that is
166    where this class comes in.
167
168    Creating a babase.ContextRef() will capture a reference to the current
169    context. Other modules may provide ways to access their contexts; for
170    example a bascenev1.Activity instance has a 'context' attribute. You
171    can also use babase.ContextRef.empty() to create a reference to *no*
172    context. Some code such as UI calls may expect this and may complain
173    if you try to use them within a context.
174
175    ##### Usage
176    ContextRefs are generally used with the Python 'with' statement, which
177    sets the context they point to as current on entry and resets it to
178    the previous value on exit.
179
180    ##### Example
181    Explicitly create a few UI bits with no context set.
182    (UI stuff may complain if called within a context):
183    >>> with bui.ContextRef.empty():
184    ...     my_container = bui.containerwidget()
185    """
186
187    def __init__(
188        self,
189    ) -> None:
190        pass
191
192    def __enter__(self) -> None:
193        """Support for "with" statement."""
194        pass
195
196    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
197        """Support for "with" statement."""
198        pass
199
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()
210
211    def is_empty(self) -> bool:
212        """Whether the context was created as empty."""
213        return bool()
214
215    def is_expired(self) -> bool:
216        """Whether the context has expired."""
217        return bool()

Store or use a ballistica context.

Category: General Utility Classes

Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a babase.ContextCall will become a no-op when the context it was created in is destroyed.

In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.

Creating a babase.ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use babase.ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.

Usage

ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.

Example

Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):

>>> with bui.ContextRef.empty():
...     my_container = bui.containerwidget()
@classmethod
def empty(cls) -> ContextRef:
200    @classmethod
201    def empty(cls) -> ContextRef:
202        """Return a ContextRef pointing to no context.
203
204        This is useful when code should be run free of a context.
205        For example, UI code generally insists on being run this way.
206        Otherwise, callbacks set on the UI could inadvertently stop working
207        due to a game activity ending, which would be unintuitive behavior.
208        """
209        return ContextRef()

Return a ContextRef pointing to no context.

This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.

def is_empty(self) -> bool:
211    def is_empty(self) -> bool:
212        """Whether the context was created as empty."""
213        return bool()

Whether the context was created as empty.

def is_expired(self) -> bool:
215    def is_expired(self) -> bool:
216        """Whether the context has expired."""
217        return bool()

Whether the context has expired.

class CoopGameActivity(bascenev1.GameActivity[~PlayerT, ~TeamT]):
 26class CoopGameActivity(GameActivity[PlayerT, TeamT]):
 27    """Base class for cooperative-mode games.
 28
 29    Category: **Gameplay Classes**
 30    """
 31
 32    # We can assume our session is a CoopSession.
 33    session: bascenev1.CoopSession
 34
 35    @override
 36    @classmethod
 37    def supports_session_type(
 38        cls, sessiontype: type[bascenev1.Session]
 39    ) -> bool:
 40        from bascenev1._coopsession import CoopSession
 41
 42        return issubclass(sessiontype, CoopSession)
 43
 44    def __init__(self, settings: dict):
 45        super().__init__(settings)
 46
 47        # Cache these for efficiency.
 48        self._achievements_awarded: set[str] = set()
 49
 50        self._life_warning_beep: bascenev1.Actor | None = None
 51        self._life_warning_beep_timer: bascenev1.Timer | None = None
 52        self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')
 53
 54    @override
 55    def on_begin(self) -> None:
 56        super().on_begin()
 57
 58        # Show achievements remaining.
 59        env = babase.app.env
 60        if not (env.demo or env.arcade):
 61            _bascenev1.timer(
 62                3.8, babase.WeakCall(self._show_remaining_achievements)
 63            )
 64
 65        # Preload achievement images in case we get some.
 66        _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements))
 67
 68    # FIXME: this is now redundant with activityutils.getscoreconfig();
 69    #  need to kill this.
 70    def get_score_type(self) -> str:
 71        """
 72        Return the score unit this co-op game uses ('point', 'seconds', etc.)
 73        """
 74        return 'points'
 75
 76    def _get_coop_level_name(self) -> str:
 77        assert self.session.campaign is not None
 78        return self.session.campaign.name + ':' + str(self.settings_raw['name'])
 79
 80    def celebrate(self, duration: float) -> None:
 81        """Tells all existing player-controlled characters to celebrate.
 82
 83        Can be useful in co-op games when the good guys score or complete
 84        a wave.
 85        duration is given in seconds.
 86        """
 87        from bascenev1._messages import CelebrateMessage
 88
 89        for player in self.players:
 90            if player.actor:
 91                player.actor.handlemessage(CelebrateMessage(duration))
 92
 93    def _preload_achievements(self) -> None:
 94        assert babase.app.classic is not None
 95        achievements = babase.app.classic.ach.achievements_for_coop_level(
 96            self._get_coop_level_name()
 97        )
 98        for ach in achievements:
 99            ach.get_icon_texture(True)
100
101    def _show_remaining_achievements(self) -> None:
102        # pylint: disable=cyclic-import
103        from bascenev1lib.actor.text import Text
104
105        assert babase.app.classic is not None
106        ts_h_offs = 30
107        v_offs = -200
108        achievements = [
109            a
110            for a in babase.app.classic.ach.achievements_for_coop_level(
111                self._get_coop_level_name()
112            )
113            if not a.complete
114        ]
115        vrmode = babase.app.env.vr
116        if achievements:
117            Text(
118                babase.Lstr(resource='achievementsRemainingText'),
119                host_only=True,
120                position=(ts_h_offs - 10 + 40, v_offs - 10),
121                transition=Text.Transition.FADE_IN,
122                scale=1.1,
123                h_attach=Text.HAttach.LEFT,
124                v_attach=Text.VAttach.TOP,
125                color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0),
126                flatness=1.0 if vrmode else 0.6,
127                shadow=1.0 if vrmode else 0.5,
128                transition_delay=0.0,
129                transition_out_delay=1.3 if self.slow_motion else 4.0,
130            ).autoretain()
131            hval = 70
132            vval = -50
133            tdelay = 0.0
134            for ach in achievements:
135                tdelay += 0.05
136                ach.create_display(
137                    hval + 40,
138                    vval + v_offs,
139                    0 + tdelay,
140                    outdelay=1.3 if self.slow_motion else 4.0,
141                    style='in_game',
142                )
143                vval -= 55
144
145    @override
146    def spawn_player_spaz(
147        self,
148        player: PlayerT,
149        position: Sequence[float] = (0.0, 0.0, 0.0),
150        angle: float | None = None,
151    ) -> PlayerSpaz:
152        """Spawn and wire up a standard player spaz."""
153        spaz = super().spawn_player_spaz(player, position, angle)
154
155        # Deaths are noteworthy in co-op games.
156        spaz.play_big_death_sound = True
157        return spaz
158
159    def _award_achievement(
160        self, achievement_name: str, sound: bool = True
161    ) -> None:
162        """Award an achievement.
163
164        Returns True if a banner will be shown;
165        False otherwise
166        """
167
168        classic = babase.app.classic
169        plus = babase.app.plus
170        if classic is None or plus is None:
171            logging.warning(
172                '_award_achievement is a no-op without classic and plus.'
173            )
174            return
175
176        if achievement_name in self._achievements_awarded:
177            return
178
179        ach = classic.ach.get_achievement(achievement_name)
180
181        # If we're in the easy campaign and this achievement is hard-mode-only,
182        # ignore it.
183        try:
184            campaign = self.session.campaign
185            assert campaign is not None
186            if ach.hard_mode_only and campaign.name == 'Easy':
187                return
188        except Exception:
189            logging.exception('Error in _award_achievement.')
190
191        # If we haven't awarded this one, check to see if we've got it.
192        # If not, set it through the game service *and* add a transaction
193        # for it.
194        if not ach.complete:
195            self._achievements_awarded.add(achievement_name)
196
197            # Report new achievements to the game-service.
198            plus.report_achievement(achievement_name)
199
200            # ...and to our account.
201            plus.add_v1_account_transaction(
202                {'type': 'ACHIEVEMENT', 'name': achievement_name}
203            )
204
205            # Now bring up a celebration banner.
206            ach.announce_completion(sound=sound)
207
208    def fade_to_red(self) -> None:
209        """Fade the screen to red; (such as when the good guys have lost)."""
210        from bascenev1 import _gameutils
211
212        c_existing = self.globalsnode.tint
213        cnode = _bascenev1.newnode(
214            'combine',
215            attrs={
216                'input0': c_existing[0],
217                'input1': c_existing[1],
218                'input2': c_existing[2],
219                'size': 3,
220            },
221        )
222        _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0})
223        _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0})
224        cnode.connectattr('output', self.globalsnode, 'tint')
225
226    def setup_low_life_warning_sound(self) -> None:
227        """Set up a beeping noise to play when any players are near death."""
228        self._life_warning_beep = None
229        self._life_warning_beep_timer = _bascenev1.Timer(
230            1.0, babase.WeakCall(self._update_life_warning), repeat=True
231        )
232
233    def _update_life_warning(self) -> None:
234        # Beep continuously if anyone is close to death.
235        should_beep = False
236        for player in self.players:
237            if player.is_alive():
238                # FIXME: Should abstract this instead of
239                #  reading hitpoints directly.
240                if getattr(player.actor, 'hitpoints', 999) < 200:
241                    should_beep = True
242                    break
243        if should_beep and self._life_warning_beep is None:
244            from bascenev1._nodeactor import NodeActor
245
246            self._life_warning_beep = NodeActor(
247                _bascenev1.newnode(
248                    'sound',
249                    attrs={
250                        'sound': self._warn_beeps_sound,
251                        'positional': False,
252                        'loop': True,
253                    },
254                )
255            )
256        if self._life_warning_beep is not None and not should_beep:
257            self._life_warning_beep = None

Base class for cooperative-mode games.

Category: Gameplay Classes

CoopGameActivity(settings: dict)
44    def __init__(self, settings: dict):
45        super().__init__(settings)
46
47        # Cache these for efficiency.
48        self._achievements_awarded: set[str] = set()
49
50        self._life_warning_beep: bascenev1.Actor | None = None
51        self._life_warning_beep_timer: bascenev1.Timer | None = None
52        self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')

Instantiate the Activity.

session: Session
331    @property
332    def session(self) -> bascenev1.Session:
333        """The bascenev1.Session this bascenev1.Activity belongs to.
334
335        Raises a babase.SessionNotFoundError if the Session no longer exists.
336        """
337        session = self._session()
338        if session is None:
339            raise babase.SessionNotFoundError()
340        return session

The bascenev1.Session this bascenev1.Activity belongs to.

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

@override
@classmethod
def supports_session_type(cls, sessiontype: type[Session]) -> bool:
35    @override
36    @classmethod
37    def supports_session_type(
38        cls, sessiontype: type[bascenev1.Session]
39    ) -> bool:
40        from bascenev1._coopsession import CoopSession
41
42        return issubclass(sessiontype, CoopSession)

Return whether this game supports the provided Session type.

@override
def on_begin(self) -> None:
54    @override
55    def on_begin(self) -> None:
56        super().on_begin()
57
58        # Show achievements remaining.
59        env = babase.app.env
60        if not (env.demo or env.arcade):
61            _bascenev1.timer(
62                3.8, babase.WeakCall(self._show_remaining_achievements)
63            )
64
65        # Preload achievement images in case we get some.
66        _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements))

Called once the previous Activity has finished transitioning out.

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

def get_score_type(self) -> str:
70    def get_score_type(self) -> str:
71        """
72        Return the score unit this co-op game uses ('point', 'seconds', etc.)
73        """
74        return 'points'

Return the score unit this co-op game uses ('point', 'seconds', etc.)

def celebrate(self, duration: float) -> None:
80    def celebrate(self, duration: float) -> None:
81        """Tells all existing player-controlled characters to celebrate.
82
83        Can be useful in co-op games when the good guys score or complete
84        a wave.
85        duration is given in seconds.
86        """
87        from bascenev1._messages import CelebrateMessage
88
89        for player in self.players:
90            if player.actor:
91                player.actor.handlemessage(CelebrateMessage(duration))

Tells all existing player-controlled characters to celebrate.

Can be useful in co-op games when the good guys score or complete a wave. duration is given in seconds.

@override
def spawn_player_spaz( self, player: ~PlayerT, position: Sequence[float] = (0.0, 0.0, 0.0), angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
145    @override
146    def spawn_player_spaz(
147        self,
148        player: PlayerT,
149        position: Sequence[float] = (0.0, 0.0, 0.0),
150        angle: float | None = None,
151    ) -> PlayerSpaz:
152        """Spawn and wire up a standard player spaz."""
153        spaz = super().spawn_player_spaz(player, position, angle)
154
155        # Deaths are noteworthy in co-op games.
156        spaz.play_big_death_sound = True
157        return spaz

Spawn and wire up a standard player spaz.

def fade_to_red(self) -> None:
208    def fade_to_red(self) -> None:
209        """Fade the screen to red; (such as when the good guys have lost)."""
210        from bascenev1 import _gameutils
211
212        c_existing = self.globalsnode.tint
213        cnode = _bascenev1.newnode(
214            'combine',
215            attrs={
216                'input0': c_existing[0],
217                'input1': c_existing[1],
218                'input2': c_existing[2],
219                'size': 3,
220            },
221        )
222        _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0})
223        _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0})
224        cnode.connectattr('output', self.globalsnode, 'tint')

Fade the screen to red; (such as when the good guys have lost).

def setup_low_life_warning_sound(self) -> None:
226    def setup_low_life_warning_sound(self) -> None:
227        """Set up a beeping noise to play when any players are near death."""
228        self._life_warning_beep = None
229        self._life_warning_beep_timer = _bascenev1.Timer(
230            1.0, babase.WeakCall(self._update_life_warning), repeat=True
231        )

Set up a beeping noise to play when any players are near death.

class CoopSession(bascenev1.Session):
 23class CoopSession(Session):
 24    """A bascenev1.Session which runs cooperative-mode games.
 25
 26    Category: **Gameplay Classes**
 27
 28    These generally consist of 1-4 players against
 29    the computer and include functionality such as
 30    high score lists.
 31    """
 32
 33    use_teams = True
 34    use_team_colors = False
 35    allow_mid_activity_joins = False
 36
 37    # Note: even though these are instance vars, we annotate them at the
 38    # class level so that docs generation can access their types.
 39
 40    campaign: bascenev1.Campaign | None
 41    """The baclassic.Campaign instance this Session represents, or None if
 42       there is no associated Campaign."""
 43
 44    def __init__(self) -> None:
 45        """Instantiate a co-op mode session."""
 46        # pylint: disable=cyclic-import
 47        from bascenev1lib.activity.coopjoin import CoopJoinActivity
 48
 49        babase.increment_analytics_count('Co-op session start')
 50        app = babase.app
 51        classic = app.classic
 52        assert classic is not None
 53
 54        # If they passed in explicit min/max, honor that.
 55        # Otherwise defer to user overrides or defaults.
 56        if 'min_players' in classic.coop_session_args:
 57            min_players = classic.coop_session_args['min_players']
 58        else:
 59            min_players = 1
 60        if 'max_players' in classic.coop_session_args:
 61            max_players = classic.coop_session_args['max_players']
 62        else:
 63            max_players = app.config.get('Coop Game Max Players', 4)
 64        if 'submit_score' in classic.coop_session_args:
 65            submit_score = classic.coop_session_args['submit_score']
 66        else:
 67            submit_score = True
 68
 69        # print('FIXME: COOP SESSION WOULD CALC DEPS.')
 70        depsets: Sequence[bascenev1.DependencySet] = []
 71
 72        super().__init__(
 73            depsets,
 74            team_names=TEAM_NAMES,
 75            team_colors=TEAM_COLORS,
 76            min_players=min_players,
 77            max_players=max_players,
 78            submit_score=submit_score,
 79        )
 80
 81        # Tournament-ID if we correspond to a co-op tournament (otherwise None)
 82        self.tournament_id: str | None = classic.coop_session_args.get(
 83            'tournament_id'
 84        )
 85
 86        self.campaign = classic.getcampaign(
 87            classic.coop_session_args['campaign']
 88        )
 89        self.campaign_level_name: str = classic.coop_session_args['level']
 90
 91        self._ran_tutorial_activity = False
 92        self._tutorial_activity: bascenev1.Activity | None = None
 93        self._custom_menu_ui: list[dict[str, Any]] = []
 94
 95        # Start our joining screen.
 96        self.setactivity(_bascenev1.newactivity(CoopJoinActivity))
 97
 98        self._next_game_instance: bascenev1.GameActivity | None = None
 99        self._next_game_level_name: str | None = None
100        self._update_on_deck_game_instances()
101
102    def get_current_game_instance(self) -> bascenev1.GameActivity:
103        """Get the game instance currently being played."""
104        return self._current_game_instance
105
106    @override
107    def should_allow_mid_activity_joins(
108        self, activity: bascenev1.Activity
109    ) -> bool:
110        # pylint: disable=cyclic-import
111        from bascenev1._gameactivity import GameActivity
112
113        # Disallow any joins in the middle of the game.
114        if isinstance(activity, GameActivity):
115            return False
116
117        return True
118
119    def _update_on_deck_game_instances(self) -> None:
120        # pylint: disable=cyclic-import
121        from bascenev1._gameactivity import GameActivity
122
123        classic = babase.app.classic
124        assert classic is not None
125
126        # Instantiate levels we may be running soon to let them load in the bg.
127
128        # Build an instance for the current level.
129        assert self.campaign is not None
130        level = self.campaign.getlevel(self.campaign_level_name)
131        gametype = level.gametype
132        settings = level.get_settings()
133
134        # Make sure all settings the game expects are present.
135        neededsettings = gametype.get_available_settings(type(self))
136        for setting in neededsettings:
137            if setting.name not in settings:
138                settings[setting.name] = setting.default
139
140        newactivity = _bascenev1.newactivity(gametype, settings)
141        assert isinstance(newactivity, GameActivity)
142        self._current_game_instance: GameActivity = newactivity
143
144        # Find the next level and build an instance for it too.
145        levels = self.campaign.levels
146        level = self.campaign.getlevel(self.campaign_level_name)
147
148        nextlevel: bascenev1.Level | None
149        if level.index < len(levels) - 1:
150            nextlevel = levels[level.index + 1]
151        else:
152            nextlevel = None
153        if nextlevel:
154            gametype = nextlevel.gametype
155            settings = nextlevel.get_settings()
156
157            # Make sure all settings the game expects are present.
158            neededsettings = gametype.get_available_settings(type(self))
159            for setting in neededsettings:
160                if setting.name not in settings:
161                    settings[setting.name] = setting.default
162
163            # We wanna be in the activity's context while taking it down.
164            newactivity = _bascenev1.newactivity(gametype, settings)
165            assert isinstance(newactivity, GameActivity)
166            self._next_game_instance = newactivity
167            self._next_game_level_name = nextlevel.name
168        else:
169            self._next_game_instance = None
170            self._next_game_level_name = None
171
172        # Special case:
173        # If our current level is 'onslaught training', instantiate
174        # our tutorial so its ready to go. (if we haven't run it yet).
175        if (
176            self.campaign_level_name == 'Onslaught Training'
177            and self._tutorial_activity is None
178            and not self._ran_tutorial_activity
179        ):
180            from bascenev1lib.tutorial import TutorialActivity
181
182            self._tutorial_activity = _bascenev1.newactivity(TutorialActivity)
183
184    @override
185    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
186        return self._custom_menu_ui
187
188    @override
189    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
190        super().on_player_leave(sessionplayer)
191
192        _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity))
193
194    def _handle_empty_activity(self) -> None:
195        """Handle cases where all players have left the current activity."""
196
197        from bascenev1._gameactivity import GameActivity
198
199        activity = self.getactivity()
200        if activity is None:
201            return  # Hmm what should we do in this case?
202
203        # If there are still players in the current activity, we're good.
204        if activity.players:
205            return
206
207        # If there are *not* players in the current activity but there
208        # *are* in the session:
209        if not activity.players and self.sessionplayers:
210            # If we're in a game, we should restart to pull in players
211            # currently waiting in the session.
212            if isinstance(activity, GameActivity):
213                # Never restart tourney games however; just end the session
214                # if all players are gone.
215                if self.tournament_id is not None:
216                    self.end()
217                else:
218                    self.restart()
219
220        # Hmm; no players anywhere. Let's end the entire session if we're
221        # running a GUI (or just the current game if we're running headless).
222        else:
223            if babase.app.env.gui:
224                self.end()
225            else:
226                if isinstance(activity, GameActivity):
227                    with activity.context:
228                        activity.end_game()
229
230    def _on_tournament_restart_menu_press(
231        self, resume_callback: Callable[[], Any]
232    ) -> None:
233        # pylint: disable=cyclic-import
234        from bascenev1._gameactivity import GameActivity
235
236        assert babase.app.classic is not None
237        activity = self.getactivity()
238        if activity is not None and not activity.expired:
239            assert self.tournament_id is not None
240            assert isinstance(activity, GameActivity)
241            babase.app.classic.tournament_entry_window(
242                tournament_id=self.tournament_id,
243                tournament_activity=activity,
244                on_close_call=resume_callback,
245            )
246
247    def restart(self) -> None:
248        """Restart the current game activity."""
249
250        # Tell the current activity to end with a 'restart' outcome.
251        # We use 'force' so that we apply even if end has already been called
252        # (but is in its delay period).
253
254        # Make an exception if there's no players left. Otherwise this
255        # can override the default session end that occurs in that case.
256        if not self.sessionplayers:
257            return
258
259        # This method may get called from the UI context so make sure we
260        # explicitly run in the activity's context.
261        activity = self.getactivity()
262        if activity is not None and not activity.expired:
263            activity.can_show_ad_on_death = True
264            with activity.context:
265                activity.end(results={'outcome': 'restart'}, force=True)
266
267    # noinspection PyUnresolvedReferences
268    @override
269    def on_activity_end(
270        self, activity: bascenev1.Activity, results: Any
271    ) -> None:
272        """Method override for co-op sessions.
273
274        Jumps between co-op games and score screens.
275        """
276        # pylint: disable=too-many-branches
277        # pylint: disable=too-many-locals
278        # pylint: disable=too-many-statements
279        # pylint: disable=cyclic-import
280        from bascenev1lib.activity.coopscore import CoopScoreScreen
281        from bascenev1lib.tutorial import TutorialActivity
282
283        from bascenev1._gameresults import GameResults
284        from bascenev1._player import PlayerInfo
285        from bascenev1._activitytypes import JoinActivity, TransitionActivity
286        from bascenev1._coopgame import CoopGameActivity
287        from bascenev1._score import ScoreType
288
289        app = babase.app
290        env = app.env
291        classic = app.classic
292        assert classic is not None
293
294        # If we're running a TeamGameActivity we'll have a GameResults
295        # as results. Otherwise its an old CoopGameActivity so its giving
296        # us a dict of random stuff.
297        if isinstance(results, GameResults):
298            outcome = 'defeat'  # This can't be 'beaten'.
299        else:
300            outcome = '' if results is None else results.get('outcome', '')
301
302        # If we're running with a gui and at any point we have no
303        # in-game players, quit out of the session (this can happen if
304        # someone leaves in the tutorial for instance).
305        if env.gui:
306            active_players = [p for p in self.sessionplayers if p.in_game]
307            if not active_players:
308                self.end()
309                return
310
311        # If we're in a between-round activity or a restart-activity,
312        # hop into a round.
313        if isinstance(
314            activity, (JoinActivity, CoopScoreScreen, TransitionActivity)
315        ):
316            if outcome == 'next_level':
317                if self._next_game_instance is None:
318                    raise RuntimeError()
319                assert self._next_game_level_name is not None
320                self.campaign_level_name = self._next_game_level_name
321                next_game = self._next_game_instance
322            else:
323                next_game = self._current_game_instance
324
325            # Special case: if we're coming from a joining-activity
326            # and will be going into onslaught-training, show the
327            # tutorial first.
328            if (
329                isinstance(activity, JoinActivity)
330                and self.campaign_level_name == 'Onslaught Training'
331                and not (env.demo or env.arcade)
332            ):
333                if self._tutorial_activity is None:
334                    raise RuntimeError('Tutorial not preloaded properly.')
335                self.setactivity(self._tutorial_activity)
336                self._tutorial_activity = None
337                self._ran_tutorial_activity = True
338                self._custom_menu_ui = []
339
340            # Normal case; launch the next round.
341            else:
342                # Reset stats for the new activity.
343                self.stats.reset()
344                for player in self.sessionplayers:
345                    # Skip players that are still choosing a team.
346                    if player.in_game:
347                        self.stats.register_sessionplayer(player)
348                self.stats.setactivity(next_game)
349
350                # Now flip the current activity..
351                self.setactivity(next_game)
352
353                if not (env.demo or env.arcade):
354                    if (
355                        self.tournament_id is not None
356                        and classic.coop_session_args['submit_score']
357                    ):
358                        self._custom_menu_ui = [
359                            {
360                                'label': babase.Lstr(resource='restartText'),
361                                'resume_on_call': False,
362                                'call': babase.WeakCall(
363                                    self._on_tournament_restart_menu_press
364                                ),
365                            }
366                        ]
367                    else:
368                        self._custom_menu_ui = [
369                            {
370                                'label': babase.Lstr(resource='restartText'),
371                                'call': babase.WeakCall(self.restart),
372                            }
373                        ]
374
375        # If we were in a tutorial, just pop a transition to get to the
376        # actual round.
377        elif isinstance(activity, TutorialActivity):
378            self.setactivity(_bascenev1.newactivity(TransitionActivity))
379        else:
380            playerinfos: list[bascenev1.PlayerInfo]
381
382            # Generic team games.
383            if isinstance(results, GameResults):
384                playerinfos = results.playerinfos
385                score = results.get_sessionteam_score(results.sessionteams[0])
386                fail_message = None
387                score_order = (
388                    'decreasing' if results.lower_is_better else 'increasing'
389                )
390                if results.scoretype in (
391                    ScoreType.SECONDS,
392                    ScoreType.MILLISECONDS,
393                ):
394                    scoretype = 'time'
395
396                    # ScoreScreen wants hundredths of a second.
397                    if score is not None:
398                        if results.scoretype is ScoreType.SECONDS:
399                            score *= 100
400                        elif results.scoretype is ScoreType.MILLISECONDS:
401                            score //= 10
402                        else:
403                            raise RuntimeError('FIXME')
404                else:
405                    if results.scoretype is not ScoreType.POINTS:
406                        print(f'Unknown ScoreType:' f' "{results.scoretype}"')
407                    scoretype = 'points'
408
409            # Old coop-game-specific results; should migrate away from these.
410            else:
411                playerinfos = results.get('playerinfos')
412                score = results['score'] if 'score' in results else None
413                fail_message = (
414                    results['fail_message']
415                    if 'fail_message' in results
416                    else None
417                )
418                score_order = (
419                    results['score_order']
420                    if 'score_order' in results
421                    else 'increasing'
422                )
423                activity_score_type = (
424                    activity.get_score_type()
425                    if isinstance(activity, CoopGameActivity)
426                    else None
427                )
428                assert activity_score_type is not None
429                scoretype = activity_score_type
430
431            # Validate types.
432            if playerinfos is not None:
433                assert isinstance(playerinfos, list)
434                assert all(isinstance(i, PlayerInfo) for i in playerinfos)
435
436            # Looks like we were in a round - check the outcome and
437            # go from there.
438            if outcome == 'restart':
439                # This will pop up back in the same round.
440                self.setactivity(_bascenev1.newactivity(TransitionActivity))
441            else:
442                self.setactivity(
443                    _bascenev1.newactivity(
444                        CoopScoreScreen,
445                        {
446                            'playerinfos': playerinfos,
447                            'score': score,
448                            'fail_message': fail_message,
449                            'score_order': score_order,
450                            'score_type': scoretype,
451                            'outcome': outcome,
452                            'campaign': self.campaign,
453                            'level': self.campaign_level_name,
454                        },
455                    )
456                )
457
458        # No matter what, get the next 2 levels ready to go.
459        self._update_on_deck_game_instances()

A bascenev1.Session which runs cooperative-mode games.

Category: Gameplay Classes

These generally consist of 1-4 players against the computer and include functionality such as high score lists.

CoopSession()
 44    def __init__(self) -> None:
 45        """Instantiate a co-op mode session."""
 46        # pylint: disable=cyclic-import
 47        from bascenev1lib.activity.coopjoin import CoopJoinActivity
 48
 49        babase.increment_analytics_count('Co-op session start')
 50        app = babase.app
 51        classic = app.classic
 52        assert classic is not None
 53
 54        # If they passed in explicit min/max, honor that.
 55        # Otherwise defer to user overrides or defaults.
 56        if 'min_players' in classic.coop_session_args:
 57            min_players = classic.coop_session_args['min_players']
 58        else:
 59            min_players = 1
 60        if 'max_players' in classic.coop_session_args:
 61            max_players = classic.coop_session_args['max_players']
 62        else:
 63            max_players = app.config.get('Coop Game Max Players', 4)
 64        if 'submit_score' in classic.coop_session_args:
 65            submit_score = classic.coop_session_args['submit_score']
 66        else:
 67            submit_score = True
 68
 69        # print('FIXME: COOP SESSION WOULD CALC DEPS.')
 70        depsets: Sequence[bascenev1.DependencySet] = []
 71
 72        super().__init__(
 73            depsets,
 74            team_names=TEAM_NAMES,
 75            team_colors=TEAM_COLORS,
 76            min_players=min_players,
 77            max_players=max_players,
 78            submit_score=submit_score,
 79        )
 80
 81        # Tournament-ID if we correspond to a co-op tournament (otherwise None)
 82        self.tournament_id: str | None = classic.coop_session_args.get(
 83            'tournament_id'
 84        )
 85
 86        self.campaign = classic.getcampaign(
 87            classic.coop_session_args['campaign']
 88        )
 89        self.campaign_level_name: str = classic.coop_session_args['level']
 90
 91        self._ran_tutorial_activity = False
 92        self._tutorial_activity: bascenev1.Activity | None = None
 93        self._custom_menu_ui: list[dict[str, Any]] = []
 94
 95        # Start our joining screen.
 96        self.setactivity(_bascenev1.newactivity(CoopJoinActivity))
 97
 98        self._next_game_instance: bascenev1.GameActivity | None = None
 99        self._next_game_level_name: str | None = None
100        self._update_on_deck_game_instances()

Instantiate a co-op mode session.

use_teams = True

Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.

use_team_colors = False

Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.

allow_mid_activity_joins = False
campaign: Campaign | None

The baclassic.Campaign instance this Session represents, or None if there is no associated Campaign.

tournament_id: str | None
campaign_level_name: str
def get_current_game_instance(self) -> GameActivity:
102    def get_current_game_instance(self) -> bascenev1.GameActivity:
103        """Get the game instance currently being played."""
104        return self._current_game_instance

Get the game instance currently being played.

@override
def should_allow_mid_activity_joins(self, activity: Activity) -> bool:
106    @override
107    def should_allow_mid_activity_joins(
108        self, activity: bascenev1.Activity
109    ) -> bool:
110        # pylint: disable=cyclic-import
111        from bascenev1._gameactivity import GameActivity
112
113        # Disallow any joins in the middle of the game.
114        if isinstance(activity, GameActivity):
115            return False
116
117        return True

Ask ourself if we should allow joins during an Activity.

Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.

@override
def get_custom_menu_entries(self) -> list[dict[str, typing.Any]]:
184    @override
185    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
186        return self._custom_menu_ui

Subclasses can override this to provide custom menu entries.

The returned value should be a list of dicts, each containing a 'label' and 'call' entry, with 'label' being the text for the entry and 'call' being the callable to trigger if the entry is pressed.

@override
def on_player_leave(self, sessionplayer: SessionPlayer) -> None:
188    @override
189    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
190        super().on_player_leave(sessionplayer)
191
192        _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity))

Called when a previously-accepted bascenev1.SessionPlayer leaves.

def restart(self) -> None:
247    def restart(self) -> None:
248        """Restart the current game activity."""
249
250        # Tell the current activity to end with a 'restart' outcome.
251        # We use 'force' so that we apply even if end has already been called
252        # (but is in its delay period).
253
254        # Make an exception if there's no players left. Otherwise this
255        # can override the default session end that occurs in that case.
256        if not self.sessionplayers:
257            return
258
259        # This method may get called from the UI context so make sure we
260        # explicitly run in the activity's context.
261        activity = self.getactivity()
262        if activity is not None and not activity.expired:
263            activity.can_show_ad_on_death = True
264            with activity.context:
265                activity.end(results={'outcome': 'restart'}, force=True)

Restart the current game activity.

@override
def on_activity_end(self, activity: Activity, results: Any) -> None:
268    @override
269    def on_activity_end(
270        self, activity: bascenev1.Activity, results: Any
271    ) -> None:
272        """Method override for co-op sessions.
273
274        Jumps between co-op games and score screens.
275        """
276        # pylint: disable=too-many-branches
277        # pylint: disable=too-many-locals
278        # pylint: disable=too-many-statements
279        # pylint: disable=cyclic-import
280        from bascenev1lib.activity.coopscore import CoopScoreScreen
281        from bascenev1lib.tutorial import TutorialActivity
282
283        from bascenev1._gameresults import GameResults
284        from bascenev1._player import PlayerInfo
285        from bascenev1._activitytypes import JoinActivity, TransitionActivity
286        from bascenev1._coopgame import CoopGameActivity
287        from bascenev1._score import ScoreType
288
289        app = babase.app
290        env = app.env
291        classic = app.classic
292        assert classic is not None
293
294        # If we're running a TeamGameActivity we'll have a GameResults
295        # as results. Otherwise its an old CoopGameActivity so its giving
296        # us a dict of random stuff.
297        if isinstance(results, GameResults):
298            outcome = 'defeat'  # This can't be 'beaten'.
299        else:
300            outcome = '' if results is None else results.get('outcome', '')
301
302        # If we're running with a gui and at any point we have no
303        # in-game players, quit out of the session (this can happen if
304        # someone leaves in the tutorial for instance).
305        if env.gui:
306            active_players = [p for p in self.sessionplayers if p.in_game]
307            if not active_players:
308                self.end()
309                return
310
311        # If we're in a between-round activity or a restart-activity,
312        # hop into a round.
313        if isinstance(
314            activity, (JoinActivity, CoopScoreScreen, TransitionActivity)
315        ):
316            if outcome == 'next_level':
317                if self._next_game_instance is None:
318                    raise RuntimeError()
319                assert self._next_game_level_name is not None
320                self.campaign_level_name = self._next_game_level_name
321                next_game = self._next_game_instance
322            else:
323                next_game = self._current_game_instance
324
325            # Special case: if we're coming from a joining-activity
326            # and will be going into onslaught-training, show the
327            # tutorial first.
328            if (
329                isinstance(activity, JoinActivity)
330                and self.campaign_level_name == 'Onslaught Training'
331                and not (env.demo or env.arcade)
332            ):
333                if self._tutorial_activity is None:
334                    raise RuntimeError('Tutorial not preloaded properly.')
335                self.setactivity(self._tutorial_activity)
336                self._tutorial_activity = None
337                self._ran_tutorial_activity = True
338                self._custom_menu_ui = []
339
340            # Normal case; launch the next round.
341            else:
342                # Reset stats for the new activity.
343                self.stats.reset()
344                for player in self.sessionplayers:
345                    # Skip players that are still choosing a team.
346                    if player.in_game:
347                        self.stats.register_sessionplayer(player)
348                self.stats.setactivity(next_game)
349
350                # Now flip the current activity..
351                self.setactivity(next_game)
352
353                if not (env.demo or env.arcade):
354                    if (
355                        self.tournament_id is not None
356                        and classic.coop_session_args['submit_score']
357                    ):
358                        self._custom_menu_ui = [
359                            {
360                                'label': babase.Lstr(resource='restartText'),
361                                'resume_on_call': False,
362                                'call': babase.WeakCall(
363                                    self._on_tournament_restart_menu_press
364                                ),
365                            }
366                        ]
367                    else:
368                        self._custom_menu_ui = [
369                            {
370                                'label': babase.Lstr(resource='restartText'),
371                                'call': babase.WeakCall(self.restart),
372                            }
373                        ]
374
375        # If we were in a tutorial, just pop a transition to get to the
376        # actual round.
377        elif isinstance(activity, TutorialActivity):
378            self.setactivity(_bascenev1.newactivity(TransitionActivity))
379        else:
380            playerinfos: list[bascenev1.PlayerInfo]
381
382            # Generic team games.
383            if isinstance(results, GameResults):
384                playerinfos = results.playerinfos
385                score = results.get_sessionteam_score(results.sessionteams[0])
386                fail_message = None
387                score_order = (
388                    'decreasing' if results.lower_is_better else 'increasing'
389                )
390                if results.scoretype in (
391                    ScoreType.SECONDS,
392                    ScoreType.MILLISECONDS,
393                ):
394                    scoretype = 'time'
395
396                    # ScoreScreen wants hundredths of a second.
397                    if score is not None:
398                        if results.scoretype is ScoreType.SECONDS:
399                            score *= 100
400                        elif results.scoretype is ScoreType.MILLISECONDS:
401                            score //= 10
402                        else:
403                            raise RuntimeError('FIXME')
404                else:
405                    if results.scoretype is not ScoreType.POINTS:
406                        print(f'Unknown ScoreType:' f' "{results.scoretype}"')
407                    scoretype = 'points'
408
409            # Old coop-game-specific results; should migrate away from these.
410            else:
411                playerinfos = results.get('playerinfos')
412                score = results['score'] if 'score' in results else None
413                fail_message = (
414                    results['fail_message']
415                    if 'fail_message' in results
416                    else None
417                )
418                score_order = (
419                    results['score_order']
420                    if 'score_order' in results
421                    else 'increasing'
422                )
423                activity_score_type = (
424                    activity.get_score_type()
425                    if isinstance(activity, CoopGameActivity)
426                    else None
427                )
428                assert activity_score_type is not None
429                scoretype = activity_score_type
430
431            # Validate types.
432            if playerinfos is not None:
433                assert isinstance(playerinfos, list)
434                assert all(isinstance(i, PlayerInfo) for i in playerinfos)
435
436            # Looks like we were in a round - check the outcome and
437            # go from there.
438            if outcome == 'restart':
439                # This will pop up back in the same round.
440                self.setactivity(_bascenev1.newactivity(TransitionActivity))
441            else:
442                self.setactivity(
443                    _bascenev1.newactivity(
444                        CoopScoreScreen,
445                        {
446                            'playerinfos': playerinfos,
447                            'score': score,
448                            'fail_message': fail_message,
449                            'score_order': score_order,
450                            'score_type': scoretype,
451                            'outcome': outcome,
452                            'campaign': self.campaign,
453                            'level': self.campaign_level_name,
454                        },
455                    )
456                )
457
458        # No matter what, get the next 2 levels ready to go.
459        self._update_on_deck_game_instances()

Method override for co-op sessions.

Jumps between co-op games and score screens.

class Data:
139class Data:
140    """A reference to a data object.
141
142    Category: **Asset Classes**
143
144    Use bascenev1.getdata() to instantiate one.
145    """
146
147    def getvalue(self) -> Any:
148        """Return the data object's value.
149
150        This can consist of anything representable by json (dicts, lists,
151        numbers, bools, None, etc).
152        Note that this call will block if the data has not yet been loaded,
153        so it can be beneficial to plan a short bit of time between when
154        the data object is requested and when it's value is accessed.
155        """
156        return _uninferrable()

A reference to a data object.

Category: Asset Classes

Use bascenev1.getdata() to instantiate one.

def getvalue(self) -> Any:
147    def getvalue(self) -> Any:
148        """Return the data object's value.
149
150        This can consist of anything representable by json (dicts, lists,
151        numbers, bools, None, etc).
152        Note that this call will block if the data has not yet been loaded,
153        so it can be beneficial to plan a short bit of time between when
154        the data object is requested and when it's value is accessed.
155        """
156        return _uninferrable()

Return the data object's value.

This can consist of anything representable by json (dicts, lists, numbers, bools, None, etc). Note that this call will block if the data has not yet been loaded, so it can be beneficial to plan a short bit of time between when the data object is requested and when it's value is accessed.

class DeathType(enum.Enum):
38class DeathType(Enum):
39    """A reason for a death.
40
41    Category: Enums
42    """
43
44    GENERIC = 'generic'
45    OUT_OF_BOUNDS = 'out_of_bounds'
46    IMPACT = 'impact'
47    FALL = 'fall'
48    REACHED_GOAL = 'reached_goal'
49    LEFT_GAME = 'left_game'

A reason for a death.

Category: Enums

GENERIC = <DeathType.GENERIC: 'generic'>
OUT_OF_BOUNDS = <DeathType.OUT_OF_BOUNDS: 'out_of_bounds'>
IMPACT = <DeathType.IMPACT: 'impact'>
FALL = <DeathType.FALL: 'fall'>
REACHED_GOAL = <DeathType.REACHED_GOAL: 'reached_goal'>
LEFT_GAME = <DeathType.LEFT_GAME: 'left_game'>
Inherited Members
enum.Enum
name
value
DEFAULT_TEAM_COLORS = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2))
DEFAULT_TEAM_NAMES = ('Blue', 'Red')
class Dependency(typing.Generic[~T]):
23class Dependency(Generic[T]):
24    """A dependency on a DependencyComponent (with an optional config).
25
26    Category: **Dependency Classes**
27
28    This class is used to request and access functionality provided
29    by other DependencyComponent classes from a DependencyComponent class.
30    The class functions as a descriptor, allowing dependencies to
31    be added at a class level much the same as properties or methods
32    and then used with class instances to access those dependencies.
33    For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)'
34    you would then be able to instantiate a FloofClass in your class's
35    methods via self.floofcls().
36    """
37
38    def __init__(self, cls: type[T], config: Any = None):
39        """Instantiate a Dependency given a bascenev1.DependencyComponent type.
40
41        Optionally, an arbitrary object can be passed as 'config' to
42        influence dependency calculation for the target class.
43        """
44        self.cls: type[T] = cls
45        self.config = config
46        self._hash: int | None = None
47
48    def get_hash(self) -> int:
49        """Return the dependency's hash, calculating it if necessary."""
50        from efro.util import make_hash
51
52        if self._hash is None:
53            self._hash = make_hash((self.cls, self.config))
54        return self._hash
55
56    def __get__(self, obj: Any, cls: Any = None) -> T:
57        if not isinstance(obj, DependencyComponent):
58            if obj is None:
59                raise TypeError(
60                    'Dependency must be accessed through an instance.'
61                )
62            raise TypeError(
63                f'Dependency cannot be added to class of type {type(obj)}'
64                ' (class must inherit from bascenev1.DependencyComponent).'
65            )
66
67        # We expect to be instantiated from an already living
68        # DependencyComponent with valid dep-data in place..
69        assert cls is not None
70
71        # Get the DependencyEntry this instance is associated with and from
72        # there get back to the DependencySet
73        entry = getattr(obj, '_dep_entry')
74        if entry is None:
75            raise RuntimeError('Invalid dependency access.')
76        entry = entry()
77        assert isinstance(entry, DependencyEntry)
78        depset = entry.depset()
79        assert isinstance(depset, DependencySet)
80
81        if not depset.resolved:
82            raise RuntimeError(
83                "Can't access data on an unresolved DependencySet."
84            )
85
86        # Look up the data in the set based on the hash for this Dependency.
87        assert self._hash in depset.entries
88        entry = depset.entries[self._hash]
89        assert isinstance(entry, DependencyEntry)
90        retval = entry.get_component()
91        assert isinstance(retval, self.cls)
92        return retval

A dependency on a DependencyComponent (with an optional config).

Category: Dependency Classes

This class is used to request and access functionality provided by other DependencyComponent classes from a DependencyComponent class. The class functions as a descriptor, allowing dependencies to be added at a class level much the same as properties or methods and then used with class instances to access those dependencies. For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)' you would then be able to instantiate a FloofClass in your class's methods via self.floofcls().

Dependency(cls: type[~T], config: Any = None)
38    def __init__(self, cls: type[T], config: Any = None):
39        """Instantiate a Dependency given a bascenev1.DependencyComponent type.
40
41        Optionally, an arbitrary object can be passed as 'config' to
42        influence dependency calculation for the target class.
43        """
44        self.cls: type[T] = cls
45        self.config = config
46        self._hash: int | None = None

Instantiate a Dependency given a bascenev1.DependencyComponent type.

Optionally, an arbitrary object can be passed as 'config' to influence dependency calculation for the target class.

cls: type[~T]
config
def get_hash(self) -> int:
48    def get_hash(self) -> int:
49        """Return the dependency's hash, calculating it if necessary."""
50        from efro.util import make_hash
51
52        if self._hash is None:
53            self._hash = make_hash((self.cls, self.config))
54        return self._hash

Return the dependency's hash, calculating it if necessary.

class DependencyComponent:
 95class DependencyComponent:
 96    """Base class for all classes that can act as or use dependencies.
 97
 98    Category: **Dependency Classes**
 99    """
100
101    _dep_entry: weakref.ref[DependencyEntry]
102
103    def __init__(self) -> None:
104        """Instantiate a DependencyComponent."""
105
106        # For now lets issue a warning if these are instantiated without
107        # a dep-entry; we'll make this an error once we're no longer
108        # seeing warnings.
109        # entry = getattr(self, '_dep_entry', None)
110        # if entry is None:
111        #     print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')
112
113    @classmethod
114    def dep_is_present(cls, config: Any = None) -> bool:
115        """Return whether this component/config is present on this device."""
116        del config  # Unused here.
117        return True
118
119    @classmethod
120    def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]:
121        """Return any dynamically-calculated deps for this component/config.
122
123        Deps declared statically as part of the class do not need to be
124        included here; this is only for additional deps that may vary based
125        on the dep config value. (for instance a map required by a game type)
126        """
127        del config  # Unused here.
128        return []

Base class for all classes that can act as or use dependencies.

Category: Dependency Classes

DependencyComponent()
103    def __init__(self) -> None:
104        """Instantiate a DependencyComponent."""
105
106        # For now lets issue a warning if these are instantiated without
107        # a dep-entry; we'll make this an error once we're no longer
108        # seeing warnings.
109        # entry = getattr(self, '_dep_entry', None)
110        # if entry is None:
111        #     print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')

Instantiate a DependencyComponent.

@classmethod
def dep_is_present(cls, config: Any = None) -> bool:
113    @classmethod
114    def dep_is_present(cls, config: Any = None) -> bool:
115        """Return whether this component/config is present on this device."""
116        del config  # Unused here.
117        return True

Return whether this component/config is present on this device.

@classmethod
def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]:
119    @classmethod
120    def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]:
121        """Return any dynamically-calculated deps for this component/config.
122
123        Deps declared statically as part of the class do not need to be
124        included here; this is only for additional deps that may vary based
125        on the dep config value. (for instance a map required by a game type)
126        """
127        del config  # Unused here.
128        return []

Return any dynamically-calculated deps for this component/config.

Deps declared statically as part of the class do not need to be included here; this is only for additional deps that may vary based on the dep config value. (for instance a map required by a game type)

class DependencySet(typing.Generic[~T]):
174class DependencySet(Generic[T]):
175    """Set of resolved dependencies and their associated data.
176
177    Category: **Dependency Classes**
178
179    To use DependencyComponents, a set must be created, resolved, and then
180    loaded. The DependencyComponents are only valid while the set remains
181    in existence.
182    """
183
184    def __init__(self, root_dependency: Dependency[T]):
185        # print('DepSet()')
186        self._root_dependency = root_dependency
187        self._resolved = False
188        self._loaded = False
189
190        # Dependency data indexed by hash.
191        self.entries: dict[int, DependencyEntry] = {}
192
193    # def __del__(self) -> None:
194    #     print("~DepSet()")
195
196    def resolve(self) -> None:
197        """Resolve the complete set of required dependencies for this set.
198
199        Raises a bascenev1.DependencyError if dependencies are missing (or
200        other Exception types on other errors).
201        """
202
203        if self._resolved:
204            raise RuntimeError('DependencySet has already been resolved.')
205
206        # print('RESOLVING DEP SET')
207
208        # First, recursively expand out all dependencies.
209        self._resolve(self._root_dependency, 0)
210
211        # Now, if any dependencies are not present, raise an Exception
212        # telling exactly which ones (so hopefully they'll be able to be
213        # downloaded/etc.
214        missing = [
215            Dependency(entry.cls, entry.config)
216            for entry in self.entries.values()
217            if not entry.cls.dep_is_present(entry.config)
218        ]
219        if missing:
220            raise DependencyError(missing)
221
222        self._resolved = True
223        # print('RESOLVE SUCCESS!')
224
225    @property
226    def resolved(self) -> bool:
227        """Whether this set has been successfully resolved."""
228        return self._resolved
229
230    def get_asset_package_ids(self) -> set[str]:
231        """Return the set of asset-package-ids required by this dep-set.
232
233        Must be called on a resolved dep-set.
234        """
235        ids: set[str] = set()
236        if not self._resolved:
237            raise RuntimeError('Must be called on a resolved dep-set.')
238        for entry in self.entries.values():
239            if issubclass(entry.cls, AssetPackage):
240                assert isinstance(entry.config, str)
241                ids.add(entry.config)
242        return ids
243
244    def load(self) -> None:
245        """Instantiate all DependencyComponents in the set.
246
247        Returns a wrapper which can be used to instantiate the root dep.
248        """
249        # NOTE: stuff below here should probably go in a separate 'instantiate'
250        # method or something.
251        if not self._resolved:
252            raise RuntimeError("Can't load an unresolved DependencySet")
253
254        for entry in self.entries.values():
255            # Do a get on everything which will init all payloads
256            # in the proper order recursively.
257            entry.get_component()
258
259        self._loaded = True
260
261    @property
262    def root(self) -> T:
263        """The instantiated root DependencyComponent instance for the set."""
264        if not self._loaded:
265            raise RuntimeError('DependencySet is not loaded.')
266
267        rootdata = self.entries[self._root_dependency.get_hash()].component
268        assert isinstance(rootdata, self._root_dependency.cls)
269        return rootdata
270
271    def _resolve(self, dep: Dependency[T], recursion: int) -> None:
272        # Watch for wacky infinite dep loops.
273        if recursion > 10:
274            raise RecursionError('Max recursion reached')
275
276        hashval = dep.get_hash()
277
278        if hashval in self.entries:
279            # Found an already resolved one; we're done here.
280            return
281
282        # Add our entry before we recurse so we don't repeat add it if
283        # there's a dependency loop.
284        self.entries[hashval] = DependencyEntry(self, dep)
285
286        # Grab all Dependency instances we find in the class.
287        subdeps = [
288            cls
289            for cls in dep.cls.__dict__.values()
290            if isinstance(cls, Dependency)
291        ]
292
293        # ..and add in any dynamic ones it provides.
294        subdeps += dep.cls.get_dynamic_deps(dep.config)
295        for subdep in subdeps:
296            self._resolve(subdep, recursion + 1)

Set of resolved dependencies and their associated data.

Category: Dependency Classes

To use DependencyComponents, a set must be created, resolved, and then loaded. The DependencyComponents are only valid while the set remains in existence.

DependencySet(root_dependency: Dependency[~T])
184    def __init__(self, root_dependency: Dependency[T]):
185        # print('DepSet()')
186        self._root_dependency = root_dependency
187        self._resolved = False
188        self._loaded = False
189
190        # Dependency data indexed by hash.
191        self.entries: dict[int, DependencyEntry] = {}
entries: dict[int, bascenev1._dependency.DependencyEntry]
def resolve(self) -> None:
196    def resolve(self) -> None:
197        """Resolve the complete set of required dependencies for this set.
198
199        Raises a bascenev1.DependencyError if dependencies are missing (or
200        other Exception types on other errors).
201        """
202
203        if self._resolved:
204            raise RuntimeError('DependencySet has already been resolved.')
205
206        # print('RESOLVING DEP SET')
207
208        # First, recursively expand out all dependencies.
209        self._resolve(self._root_dependency, 0)
210
211        # Now, if any dependencies are not present, raise an Exception
212        # telling exactly which ones (so hopefully they'll be able to be
213        # downloaded/etc.
214        missing = [
215            Dependency(entry.cls, entry.config)
216            for entry in self.entries.values()
217            if not entry.cls.dep_is_present(entry.config)
218        ]
219        if missing:
220            raise DependencyError(missing)
221
222        self._resolved = True
223        # print('RESOLVE SUCCESS!')

Resolve the complete set of required dependencies for this set.

Raises a bascenev1.DependencyError if dependencies are missing (or other Exception types on other errors).

resolved: bool
225    @property
226    def resolved(self) -> bool:
227        """Whether this set has been successfully resolved."""
228        return self._resolved

Whether this set has been successfully resolved.

def get_asset_package_ids(self) -> set[str]:
230    def get_asset_package_ids(self) -> set[str]:
231        """Return the set of asset-package-ids required by this dep-set.
232
233        Must be called on a resolved dep-set.
234        """
235        ids: set[str] = set()
236        if not self._resolved:
237            raise RuntimeError('Must be called on a resolved dep-set.')
238        for entry in self.entries.values():
239            if issubclass(entry.cls, AssetPackage):
240                assert isinstance(entry.config, str)
241                ids.add(entry.config)
242        return ids

Return the set of asset-package-ids required by this dep-set.

Must be called on a resolved dep-set.

def load(self) -> None:
244    def load(self) -> None:
245        """Instantiate all DependencyComponents in the set.
246
247        Returns a wrapper which can be used to instantiate the root dep.
248        """
249        # NOTE: stuff below here should probably go in a separate 'instantiate'
250        # method or something.
251        if not self._resolved:
252            raise RuntimeError("Can't load an unresolved DependencySet")
253
254        for entry in self.entries.values():
255            # Do a get on everything which will init all payloads
256            # in the proper order recursively.
257            entry.get_component()
258
259        self._loaded = True

Instantiate all DependencyComponents in the set.

Returns a wrapper which can be used to instantiate the root dep.

root: ~T
261    @property
262    def root(self) -> T:
263        """The instantiated root DependencyComponent instance for the set."""
264        if not self._loaded:
265            raise RuntimeError('DependencySet is not loaded.')
266
267        rootdata = self.entries[self._root_dependency.get_hash()].component
268        assert isinstance(rootdata, self._root_dependency.cls)
269        return rootdata

The instantiated root DependencyComponent instance for the set.

@dataclass
class DieMessage:
52@dataclass
53class DieMessage:
54    """A message telling an object to die.
55
56    Category: **Message Classes**
57
58    Most bascenev1.Actor-s respond to this.
59    """
60
61    immediate: bool = False
62    """If this is set to True, the actor should disappear immediately.
63       This is for 'removing' stuff from the game more so than 'killing'
64       it. If False, the actor should die a 'normal' death and can take
65       its time with lingering corpses, sound effects, etc."""
66
67    how: DeathType = DeathType.GENERIC
68    """The particular reason for death."""

A message telling an object to die.

Category: Message Classes

Most bascenev1.Actor-s respond to this.

DieMessage( immediate: bool = False, how: DeathType = <DeathType.GENERIC: 'generic'>)
immediate: bool = False

If this is set to True, the actor should disappear immediately. This is for 'removing' stuff from the game more so than 'killing' it. If False, the actor should die a 'normal' death and can take its time with lingering corpses, sound effects, etc.

how: DeathType = <DeathType.GENERIC: 'generic'>

The particular reason for death.

def displaytime() -> DisplayTime:
756def displaytime() -> babase.DisplayTime:
757    """Return the current display-time in seconds.
758
759    Category: **General Utility Functions**
760
761    Display-time is a time value intended to be used for animation and other
762    visual purposes. It will generally increment by a consistent amount each
763    frame. It will pass at an overall similar rate to AppTime, but trades
764    accuracy for smoothness.
765
766    Note that the value returned here is simply a float; it just has a
767    unique type in the type-checker's eyes to help prevent it from being
768    accidentally used with time functionality expecting other time types.
769    """
770    import babase  # pylint: disable=cyclic-import
771
772    return babase.DisplayTime(0.0)

Return the current display-time in seconds.

Category: General Utility Functions

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

DisplayTime = DisplayTime
def displaytimer(time: float, call: Callable[[], Any]) -> None:
775def displaytimer(time: float, call: Callable[[], Any]) -> None:
776    """Schedule a callable object to run based on display-time.
777
778    Category: **General Utility Functions**
779
780    This function creates a one-off timer which cannot be canceled or
781    modified once created. If you require the ability to do so, or need
782    a repeating timer, use the babase.DisplayTimer class instead.
783
784    Display-time is a time value intended to be used for animation and other
785    visual purposes. It will generally increment by a consistent amount each
786    frame. It will pass at an overall similar rate to AppTime, but trades
787    accuracy for smoothness.
788
789    ##### Arguments
790    ###### time (float)
791    > Length of time in seconds that the timer will wait before firing.
792
793    ###### call (Callable[[], Any])
794    > A callable Python object. Note that the timer will retain a
795    strong reference to the callable for as long as the timer exists, so you
796    may want to look into concepts such as babase.WeakCall if that is not
797    desired.
798
799    ##### Examples
800    Print some stuff through time:
801    >>> babase.screenmessage('hello from now!')
802    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
803    ...                       'hello from the future!'))
804    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
805    ...                       'hello from the future 2!'))
806    """
807    return None

Schedule a callable object to run based on display-time.

Category: General Utility Functions

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.DisplayTimer class instead.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> babase.screenmessage('hello from now!')
>>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
...                       'hello from the future!'))
>>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
...                       'hello from the future 2!'))
class DisplayTimer:
220class DisplayTimer:
221    """Timers are used to run code at later points in time.
222
223    Category: **General Utility Classes**
224
225    This class encapsulates a timer based on display-time.
226    The underlying timer will be destroyed when this object is no longer
227    referenced. If you do not want to worry about keeping a reference to
228    your timer around, use the babase.displaytimer() function instead to get a
229    one-off timer.
230
231    Display-time is a time value intended to be used for animation and
232    other visual purposes. It will generally increment by a consistent
233    amount each frame. It will pass at an overall similar rate to AppTime,
234    but trades accuracy for smoothness.
235
236    ##### Arguments
237    ###### time
238    > Length of time in seconds that the timer will wait before firing.
239
240    ###### call
241    > A callable Python object. Remember that the timer will retain a
242    strong reference to the callable for as long as it exists, so you
243    may want to look into concepts such as babase.WeakCall if that is not
244    desired.
245
246    ###### repeat
247    > If True, the timer will fire repeatedly, with each successive
248    firing having the same delay as the first.
249
250    ##### Example
251
252    Use a Timer object to print repeatedly for a few seconds:
253    ... def say_it():
254    ...     babase.screenmessage('BADGER!')
255    ... def stop_saying_it():
256    ...     global g_timer
257    ...     g_timer = None
258    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
259    ... # Create our timer; it will run as long as we have the self.t ref.
260    ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)
261    ... # Now fire off a one-shot timer to kill it.
262    ... babase.displaytimer(3.89, stop_saying_it)
263    """
264
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.displaytimer() function instead to get a one-off timer.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.displaytimer(3.89, stop_saying_it)

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
265    def __init__(
266        self, time: float, call: Callable[[], Any], repeat: bool = False
267    ) -> None:
268        pass
@dataclass
class DropMessage:
156@dataclass
157class DropMessage:
158    """Tells an object that it has dropped what it was holding.
159
160    Category: **Message Classes**
161    """

Tells an object that it has dropped what it was holding.

Category: Message Classes

@dataclass
class DroppedMessage:
175@dataclass
176class DroppedMessage:
177    """Tells an object that it has been dropped.
178
179    Category: **Message Classes**
180    """
181
182    node: bascenev1.Node
183    """The bascenev1.Node doing the dropping."""

Tells an object that it has been dropped.

Category: Message Classes

DroppedMessage(node: Node)
node: Node

The bascenev1.Node doing the dropping.

class DualTeamSession(bascenev1.MultiTeamSession):
18class DualTeamSession(MultiTeamSession):
19    """bascenev1.Session type for teams mode games.
20
21    Category: **Gameplay Classes**
22    """
23
24    # Base class overrides:
25    use_teams = True
26    use_team_colors = True
27
28    _playlist_selection_var = 'Team Tournament Playlist Selection'
29    _playlist_randomize_var = 'Team Tournament Playlist Randomize'
30    _playlists_var = 'Team Tournament Playlists'
31
32    def __init__(self) -> None:
33        babase.increment_analytics_count('Teams session start')
34        super().__init__()
35
36    @override
37    def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
38        # pylint: disable=cyclic-import
39        from bascenev1lib.activity.multiteamvictory import (
40            TeamSeriesVictoryScoreScreenActivity,
41        )
42        from bascenev1lib.activity.dualteamscore import (
43            TeamVictoryScoreScreenActivity,
44        )
45        from bascenev1lib.activity.drawscore import DrawScoreScreenActivity
46
47        winnergroups = results.winnergroups
48
49        # If everyone has the same score, call it a draw.
50        if len(winnergroups) < 2:
51            self.setactivity(_bascenev1.newactivity(DrawScoreScreenActivity))
52        else:
53            winner = winnergroups[0].teams[0]
54            winner.customdata['score'] += 1
55
56            # If a team has won, show final victory screen.
57            if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
58                self.setactivity(
59                    _bascenev1.newactivity(
60                        TeamSeriesVictoryScoreScreenActivity,
61                        {'winner': winner},
62                    )
63                )
64            else:
65                self.setactivity(
66                    _bascenev1.newactivity(
67                        TeamVictoryScoreScreenActivity, {'winner': winner}
68                    )
69                )

bascenev1.Session type for teams mode games.

Category: Gameplay Classes

DualTeamSession()
32    def __init__(self) -> None:
33        babase.increment_analytics_count('Teams session start')
34        super().__init__()

Set up playlists & launch a bascenev1.Activity to accept joiners.

use_teams = True

Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.

use_team_colors = True

Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.

def emitfx( position: Sequence[float], velocity: Optional[Sequence[float]] = None, count: int = 10, scale: float = 1.0, spread: float = 1.0, chunk_type: str = 'rock', emit_type: str = 'chunks', tendril_type: str = 'smoke') -> None:
1092def emitfx(
1093    position: Sequence[float],
1094    velocity: Sequence[float] | None = None,
1095    count: int = 10,
1096    scale: float = 1.0,
1097    spread: float = 1.0,
1098    chunk_type: str = 'rock',
1099    emit_type: str = 'chunks',
1100    tendril_type: str = 'smoke',
1101) -> None:
1102    """Emit particles, smoke, etc. into the fx sim layer.
1103
1104    Category: **Gameplay Functions**
1105
1106    The fx sim layer is a secondary dynamics simulation that runs in
1107    the background and just looks pretty; it does not affect gameplay.
1108    Note that the actual amount emitted may vary depending on graphics
1109    settings, exiting element counts, or other factors.
1110    """
1111    return None

Emit particles, smoke, etc. into the fx sim layer.

Category: Gameplay Functions

The fx sim layer is a secondary dynamics simulation that runs in the background and just looks pretty; it does not affect gameplay. Note that the actual amount emitted may vary depending on graphics settings, exiting element counts, or other factors.

class EmptyPlayer(bascenev1.Player[ForwardRef('bascenev1.EmptyTeam')]):
283class EmptyPlayer(Player['bascenev1.EmptyTeam']):
284    """An empty player for use by Activities that don't need to define one.
285
286    Category: Gameplay Classes
287
288    bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing
289    those top level classes as type arguments when defining a
290    bascenev1.Activity reduces type safety. For example,
291    activity.teams[0].player will have type 'Any' in that case. For that
292    reason, it is better to pass EmptyPlayer and EmptyTeam when defining
293    a bascenev1.Activity that does not need custom types of its own.
294
295    Note that EmptyPlayer defines its team type as EmptyTeam and vice versa,
296    so if you want to define your own class for one of them you should do so
297    for both.
298    """

An empty player for use by Activities that don't need to define one.

Category: Gameplay Classes

bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing those top level classes as type arguments when defining a bascenev1.Activity reduces type safety. For example, activity.teams[0].player will have type 'Any' in that case. For that reason, it is better to pass EmptyPlayer and EmptyTeam when defining a bascenev1.Activity that does not need custom types of its own.

Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so for both.

197class EmptyTeam(Team['bascenev1.EmptyPlayer']):
198    """An empty player for use by Activities that don't need to define one.
199
200    Category: **Gameplay Classes**
201
202    bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing
203    those top level classes as type arguments when defining a
204    bascenev1.Activity reduces type safety. For example,
205    activity.teams[0].player will have type 'Any' in that case. For that
206    reason, it is better to pass EmptyPlayer and EmptyTeam when defining
207    a bascenev1.Activity that does not need custom types of its own.
208
209    Note that EmptyPlayer defines its team type as EmptyTeam and vice versa,
210    so if you want to define your own class for one of them you should do so
211    for both.
212    """

An empty player for use by Activities that don't need to define one.

Category: Gameplay Classes

bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing those top level classes as type arguments when defining a bascenev1.Activity reduces type safety. For example, activity.teams[0].player will have type 'Any' in that case. For that reason, it is better to pass EmptyPlayer and EmptyTeam when defining a bascenev1.Activity that does not need custom types of its own.

Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so for both.

def existing(obj: Optional[~ExistableT]) -> Optional[~ExistableT]:
50def existing(obj: ExistableT | None) -> ExistableT | None:
51    """Convert invalid references to None for any babase.Existable object.
52
53    Category: **Gameplay Functions**
54
55    To best support type checking, it is important that invalid references
56    not be passed around and instead get converted to values of None.
57    That way the type checker can properly flag attempts to pass possibly-dead
58    objects (FooType | None) into functions expecting only live ones
59    (FooType), etc. This call can be used on any 'existable' object
60    (one with an exists() method) and will convert it to a None value
61    if it does not exist.
62
63    For more info, see notes on 'existables' here:
64    https://ballistica.net/wiki/Coding-Style-Guide
65    """
66    assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}'
67    return obj if obj is not None and obj.exists() else None

Convert invalid references to None for any babase.Existable object.

Category: Gameplay Functions

To best support type checking, it is important that invalid references not be passed around and instead get converted to values of None. That way the type checker can properly flag attempts to pass possibly-dead objects (FooType | None) into functions expecting only live ones (FooType), etc. This call can be used on any 'existable' object (one with an exists() method) and will convert it to a None value if it does not exist.

For more info, see notes on 'existables' here: https://ballistica.net/wiki/Coding-Style-Guide

def filter_playlist( playlist: list[dict[str, typing.Any]], sessiontype: type[Session], add_resolved_type: bool = False, remove_unowned: bool = True, mark_unowned: bool = False, name: str = '?') -> list[dict[str, typing.Any]]:
 22def filter_playlist(
 23    playlist: PlaylistType,
 24    sessiontype: type[Session],
 25    add_resolved_type: bool = False,
 26    remove_unowned: bool = True,
 27    mark_unowned: bool = False,
 28    name: str = '?',
 29) -> PlaylistType:
 30    """Return a filtered version of a playlist.
 31
 32    Strips out or replaces invalid or unowned game types, makes sure all
 33    settings are present, and adds in a 'resolved_type' which is the actual
 34    type.
 35    """
 36    # pylint: disable=too-many-locals
 37    # pylint: disable=too-many-branches
 38    # pylint: disable=too-many-statements
 39    from bascenev1._map import get_filtered_map_name
 40    from bascenev1._gameactivity import GameActivity
 41
 42    assert babase.app.classic is not None
 43
 44    goodlist: list[dict] = []
 45    unowned_maps: Sequence[str]
 46    available_maps: list[str] = list(babase.app.classic.maps.keys())
 47    if (remove_unowned or mark_unowned) and babase.app.classic is not None:
 48        unowned_maps = babase.app.classic.store.get_unowned_maps()
 49        unowned_game_types = babase.app.classic.store.get_unowned_game_types()
 50    else:
 51        unowned_maps = []
 52        unowned_game_types = set()
 53
 54    for entry in copy.deepcopy(playlist):
 55        # 'map' used to be called 'level' here.
 56        if 'level' in entry:
 57            entry['map'] = entry['level']
 58            del entry['level']
 59
 60        # We now stuff map into settings instead of it being its own thing.
 61        if 'map' in entry:
 62            entry['settings']['map'] = entry['map']
 63            del entry['map']
 64
 65        # Update old map names to new ones.
 66        entry['settings']['map'] = get_filtered_map_name(
 67            entry['settings']['map']
 68        )
 69        if remove_unowned and entry['settings']['map'] in unowned_maps:
 70            continue
 71
 72        # Ok, for each game in our list, try to import the module and grab
 73        # the actual game class. add successful ones to our initial list
 74        # to present to the user.
 75        if not isinstance(entry['type'], str):
 76            raise TypeError('invalid entry format')
 77        try:
 78            # Do some type filters for backwards compat.
 79            if entry['type'] in (
 80                'Assault.AssaultGame',
 81                'Happy_Thoughts.HappyThoughtsGame',
 82                'bsAssault.AssaultGame',
 83                'bs_assault.AssaultGame',
 84                'bastd.game.assault.AssaultGame',
 85            ):
 86                entry['type'] = 'bascenev1lib.game.assault.AssaultGame'
 87            if entry['type'] in (
 88                'King_of_the_Hill.KingOfTheHillGame',
 89                'bsKingOfTheHill.KingOfTheHillGame',
 90                'bs_king_of_the_hill.KingOfTheHillGame',
 91                'bastd.game.kingofthehill.KingOfTheHillGame',
 92            ):
 93                entry['type'] = (
 94                    'bascenev1lib.game.kingofthehill.KingOfTheHillGame'
 95                )
 96            if entry['type'] in (
 97                'Capture_the_Flag.CTFGame',
 98                'bsCaptureTheFlag.CTFGame',
 99                'bs_capture_the_flag.CTFGame',
100                'bastd.game.capturetheflag.CaptureTheFlagGame',
101            ):
102                entry['type'] = (
103                    'bascenev1lib.game.capturetheflag.CaptureTheFlagGame'
104                )
105            if entry['type'] in (
106                'Death_Match.DeathMatchGame',
107                'bsDeathMatch.DeathMatchGame',
108                'bs_death_match.DeathMatchGame',
109                'bastd.game.deathmatch.DeathMatchGame',
110            ):
111                entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame'
112            if entry['type'] in (
113                'ChosenOne.ChosenOneGame',
114                'bsChosenOne.ChosenOneGame',
115                'bs_chosen_one.ChosenOneGame',
116                'bastd.game.chosenone.ChosenOneGame',
117            ):
118                entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame'
119            if entry['type'] in (
120                'Conquest.Conquest',
121                'Conquest.ConquestGame',
122                'bsConquest.ConquestGame',
123                'bs_conquest.ConquestGame',
124                'bastd.game.conquest.ConquestGame',
125            ):
126                entry['type'] = 'bascenev1lib.game.conquest.ConquestGame'
127            if entry['type'] in (
128                'Elimination.EliminationGame',
129                'bsElimination.EliminationGame',
130                'bs_elimination.EliminationGame',
131                'bastd.game.elimination.EliminationGame',
132            ):
133                entry['type'] = 'bascenev1lib.game.elimination.EliminationGame'
134            if entry['type'] in (
135                'Football.FootballGame',
136                'bsFootball.FootballTeamGame',
137                'bs_football.FootballTeamGame',
138                'bastd.game.football.FootballTeamGame',
139            ):
140                entry['type'] = 'bascenev1lib.game.football.FootballTeamGame'
141            if entry['type'] in (
142                'Hockey.HockeyGame',
143                'bsHockey.HockeyGame',
144                'bs_hockey.HockeyGame',
145                'bastd.game.hockey.HockeyGame',
146            ):
147                entry['type'] = 'bascenev1lib.game.hockey.HockeyGame'
148            if entry['type'] in (
149                'Keep_Away.KeepAwayGame',
150                'bsKeepAway.KeepAwayGame',
151                'bs_keep_away.KeepAwayGame',
152                'bastd.game.keepaway.KeepAwayGame',
153            ):
154                entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame'
155            if entry['type'] in (
156                'Race.RaceGame',
157                'bsRace.RaceGame',
158                'bs_race.RaceGame',
159                'bastd.game.race.RaceGame',
160            ):
161                entry['type'] = 'bascenev1lib.game.race.RaceGame'
162            if entry['type'] in (
163                'bsEasterEggHunt.EasterEggHuntGame',
164                'bs_easter_egg_hunt.EasterEggHuntGame',
165                'bastd.game.easteregghunt.EasterEggHuntGame',
166            ):
167                entry['type'] = (
168                    'bascenev1lib.game.easteregghunt.EasterEggHuntGame'
169                )
170            if entry['type'] in (
171                'bsMeteorShower.MeteorShowerGame',
172                'bs_meteor_shower.MeteorShowerGame',
173                'bastd.game.meteorshower.MeteorShowerGame',
174            ):
175                entry['type'] = (
176                    'bascenev1lib.game.meteorshower.MeteorShowerGame'
177                )
178            if entry['type'] in (
179                'bsTargetPractice.TargetPracticeGame',
180                'bs_target_practice.TargetPracticeGame',
181                'bastd.game.targetpractice.TargetPracticeGame',
182            ):
183                entry['type'] = (
184                    'bascenev1lib.game.targetpractice.TargetPracticeGame'
185                )
186
187            gameclass = babase.getclass(entry['type'], GameActivity)
188
189            if entry['settings']['map'] not in available_maps:
190                raise babase.MapNotFoundError()
191
192            if remove_unowned and gameclass in unowned_game_types:
193                continue
194            if add_resolved_type:
195                entry['resolved_type'] = gameclass
196            if mark_unowned and entry['settings']['map'] in unowned_maps:
197                entry['is_unowned_map'] = True
198            if mark_unowned and gameclass in unowned_game_types:
199                entry['is_unowned_game'] = True
200
201            # Make sure all settings the game defines are present.
202            neededsettings = gameclass.get_available_settings(sessiontype)
203            for setting in neededsettings:
204                if setting.name not in entry['settings']:
205                    entry['settings'][setting.name] = setting.default
206
207            goodlist.append(entry)
208
209        except babase.MapNotFoundError:
210            logging.warning(
211                'Map \'%s\' not found while scanning playlist \'%s\'.',
212                entry['settings']['map'],
213                name,
214            )
215        except ImportError as exc:
216            logging.warning(
217                'Import failed while scanning playlist \'%s\': %s', name, exc
218            )
219        except Exception:
220            logging.exception('Error in filter_playlist.')
221
222    return goodlist

Return a filtered version of a playlist.

Strips out or replaces invalid or unowned game types, makes sure all settings are present, and adds in a 'resolved_type' which is the actual type.

@dataclass
class FloatChoiceSetting(bascenev1.ChoiceSetting):
83@dataclass
84class FloatChoiceSetting(ChoiceSetting):
85    """A float setting with multiple choices.
86
87    Category: Settings Classes
88    """
89
90    default: float
91    choices: list[tuple[str, float]]

A float setting with multiple choices.

Category: Settings Classes

FloatChoiceSetting(name: str, default: float, choices: list[tuple[str, float]])
default: float
choices: list[tuple[str, float]]
Inherited Members
Setting
name
@dataclass
class FloatSetting(bascenev1.Setting):
49@dataclass
50class FloatSetting(Setting):
51    """A floating point game setting.
52
53    Category: Settings Classes
54    """
55
56    default: float
57    min_value: float = 0.0
58    max_value: float = 9999.0
59    increment: float = 1.0

A floating point game setting.

Category: Settings Classes

FloatSetting( name: str, default: float, min_value: float = 0.0, max_value: float = 9999.0, increment: float = 1.0)
default: float
min_value: float = 0.0
max_value: float = 9999.0
increment: float = 1.0
Inherited Members
Setting
name
class FreeForAllSession(bascenev1.MultiTeamSession):
 19class FreeForAllSession(MultiTeamSession):
 20    """bascenev1.Session type for free-for-all mode games.
 21
 22    Category: **Gameplay Classes**
 23    """
 24
 25    use_teams = False
 26    use_team_colors = False
 27    _playlist_selection_var = 'Free-for-All Playlist Selection'
 28    _playlist_randomize_var = 'Free-for-All Playlist Randomize'
 29    _playlists_var = 'Free-for-All Playlists'
 30
 31    def get_ffa_point_awards(self) -> dict[int, int]:
 32        """Return the number of points awarded for different rankings.
 33
 34        This is based on the current number of players.
 35        """
 36        point_awards: dict[int, int]
 37        if len(self.sessionplayers) == 1:
 38            point_awards = {}
 39        elif len(self.sessionplayers) == 2:
 40            point_awards = {0: 6}
 41        elif len(self.sessionplayers) == 3:
 42            point_awards = {0: 6, 1: 3}
 43        elif len(self.sessionplayers) == 4:
 44            point_awards = {0: 8, 1: 4, 2: 2}
 45        elif len(self.sessionplayers) == 5:
 46            point_awards = {0: 8, 1: 4, 2: 2}
 47        elif len(self.sessionplayers) == 6:
 48            point_awards = {0: 8, 1: 4, 2: 2}
 49        else:
 50            point_awards = {0: 8, 1: 4, 2: 2, 3: 1}
 51        return point_awards
 52
 53    def __init__(self) -> None:
 54        babase.increment_analytics_count('Free-for-all session start')
 55        super().__init__()
 56
 57    @override
 58    def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
 59        # pylint: disable=cyclic-import
 60        from efro.util import asserttype
 61        from bascenev1lib.activity.multiteamvictory import (
 62            TeamSeriesVictoryScoreScreenActivity,
 63        )
 64        from bascenev1lib.activity.freeforallvictory import (
 65            FreeForAllVictoryScoreScreenActivity,
 66        )
 67        from bascenev1lib.activity.drawscore import DrawScoreScreenActivity
 68
 69        winners = results.winnergroups
 70
 71        # If there's multiple players and everyone has the same score,
 72        # call it a draw.
 73        if len(self.sessionplayers) > 1 and len(winners) < 2:
 74            self.setactivity(
 75                _bascenev1.newactivity(
 76                    DrawScoreScreenActivity, {'results': results}
 77                )
 78            )
 79        else:
 80            # Award different point amounts based on number of players.
 81            point_awards = self.get_ffa_point_awards()
 82
 83            for i, winner in enumerate(winners):
 84                for team in winner.teams:
 85                    points = point_awards[i] if i in point_awards else 0
 86                    team.customdata['previous_score'] = team.customdata['score']
 87                    team.customdata['score'] += points
 88
 89            series_winners = [
 90                team
 91                for team in self.sessionteams
 92                if team.customdata['score'] >= self._ffa_series_length
 93            ]
 94            series_winners.sort(
 95                reverse=True,
 96                key=lambda t: asserttype(t.customdata['score'], int),
 97            )
 98            if len(series_winners) == 1 or (
 99                len(series_winners) > 1
100                and series_winners[0].customdata['score']
101                != series_winners[1].customdata['score']
102            ):
103                self.setactivity(
104                    _bascenev1.newactivity(
105                        TeamSeriesVictoryScoreScreenActivity,
106                        {'winner': series_winners[0]},
107                    )
108                )
109            else:
110                self.setactivity(
111                    _bascenev1.newactivity(
112                        FreeForAllVictoryScoreScreenActivity,
113                        {'results': results},
114                    )
115                )

bascenev1.Session type for free-for-all mode games.

Category: Gameplay Classes

FreeForAllSession()
53    def __init__(self) -> None:
54        babase.increment_analytics_count('Free-for-all session start')
55        super().__init__()

Set up playlists & launch a bascenev1.Activity to accept joiners.

use_teams = False

Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.

use_team_colors = False

Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.

def get_ffa_point_awards(self) -> dict[int, int]:
31    def get_ffa_point_awards(self) -> dict[int, int]:
32        """Return the number of points awarded for different rankings.
33
34        This is based on the current number of players.
35        """
36        point_awards: dict[int, int]
37        if len(self.sessionplayers) == 1:
38            point_awards = {}
39        elif len(self.sessionplayers) == 2:
40            point_awards = {0: 6}
41        elif len(self.sessionplayers) == 3:
42            point_awards = {0: 6, 1: 3}
43        elif len(self.sessionplayers) == 4:
44            point_awards = {0: 8, 1: 4, 2: 2}
45        elif len(self.sessionplayers) == 5:
46            point_awards = {0: 8, 1: 4, 2: 2}
47        elif len(self.sessionplayers) == 6:
48            point_awards = {0: 8, 1: 4, 2: 2}
49        else:
50            point_awards = {0: 8, 1: 4, 2: 2, 3: 1}
51        return point_awards

Return the number of points awarded for different rankings.

This is based on the current number of players.

@dataclass
class FreezeMessage:
205@dataclass
206class FreezeMessage:
207    """Tells an object to become frozen.
208
209    Category: **Message Classes**
210
211    As seen in the effects of an ice bascenev1.Bomb.
212    """

Tells an object to become frozen.

Category: Message Classes

As seen in the effects of an ice bascenev1.Bomb.

class GameActivity(bascenev1.Activity[~PlayerT, ~TeamT]):
  34class GameActivity(Activity[PlayerT, TeamT]):
  35    """Common base class for all game bascenev1.Activities.
  36
  37    Category: **Gameplay Classes**
  38    """
  39
  40    # pylint: disable=too-many-public-methods
  41
  42    # Tips to be presented to the user at the start of the game.
  43    tips: list[str | bascenev1.GameTip] = []
  44
  45    # Default getname() will return this if not None.
  46    name: str | None = None
  47
  48    # Default get_description() will return this if not None.
  49    description: str | None = None
  50
  51    # Default get_available_settings() will return this if not None.
  52    available_settings: list[bascenev1.Setting] | None = None
  53
  54    # Default getscoreconfig() will return this if not None.
  55    scoreconfig: bascenev1.ScoreConfig | None = None
  56
  57    # Override some defaults.
  58    allow_pausing = True
  59    allow_kick_idle_players = True
  60
  61    # Whether to show points for kills.
  62    show_kill_points = True
  63
  64    # If not None, the music type that should play in on_transition_in()
  65    # (unless overridden by the map).
  66    default_music: bascenev1.MusicType | None = None
  67
  68    @classmethod
  69    def create_settings_ui(
  70        cls,
  71        sessiontype: type[bascenev1.Session],
  72        settings: dict | None,
  73        completion_call: Callable[[dict | None], None],
  74    ) -> None:
  75        """Launch an in-game UI to configure settings for a game type.
  76
  77        'sessiontype' should be the bascenev1.Session class the game will
  78          be used in.
  79
  80        'settings' should be an existing settings dict (implies 'edit'
  81          ui mode) or None (implies 'add' ui mode).
  82
  83        'completion_call' will be called with a filled-out settings dict on
  84          success or None on cancel.
  85
  86        Generally subclasses don't need to override this; if they override
  87        bascenev1.GameActivity.get_available_settings() and
  88        bascenev1.GameActivity.get_supported_maps() they can just rely on
  89        the default implementation here which calls those methods.
  90        """
  91        assert babase.app.classic is not None
  92        delegate = babase.app.classic.delegate
  93        assert delegate is not None
  94        delegate.create_default_game_settings_ui(
  95            cls, sessiontype, settings, completion_call
  96        )
  97
  98    @classmethod
  99    def getscoreconfig(cls) -> bascenev1.ScoreConfig:
 100        """Return info about game scoring setup; can be overridden by games."""
 101        return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig()
 102
 103    @classmethod
 104    def getname(cls) -> str:
 105        """Return a str name for this game type.
 106
 107        This default implementation simply returns the 'name' class attr.
 108        """
 109        return cls.name if cls.name is not None else 'Untitled Game'
 110
 111    @classmethod
 112    def get_display_string(cls, settings: dict | None = None) -> babase.Lstr:
 113        """Return a descriptive name for this game/settings combo.
 114
 115        Subclasses should override getname(); not this.
 116        """
 117        name = babase.Lstr(translate=('gameNames', cls.getname()))
 118
 119        # A few substitutions for 'Epic', 'Solo' etc. modes.
 120        # FIXME: Should provide a way for game types to define filters of
 121        #  their own and should not rely on hard-coded settings names.
 122        if settings is not None:
 123            if 'Solo Mode' in settings and settings['Solo Mode']:
 124                name = babase.Lstr(
 125                    resource='soloNameFilterText', subs=[('${NAME}', name)]
 126                )
 127            if 'Epic Mode' in settings and settings['Epic Mode']:
 128                name = babase.Lstr(
 129                    resource='epicNameFilterText', subs=[('${NAME}', name)]
 130                )
 131
 132        return name
 133
 134    @classmethod
 135    def get_team_display_string(cls, name: str) -> babase.Lstr:
 136        """Given a team name, returns a localized version of it."""
 137        return babase.Lstr(translate=('teamNames', name))
 138
 139    @classmethod
 140    def get_description(cls, sessiontype: type[bascenev1.Session]) -> str:
 141        """Get a str description of this game type.
 142
 143        The default implementation simply returns the 'description' class var.
 144        Classes which want to change their description depending on the session
 145        can override this method.
 146        """
 147        del sessiontype  # Unused arg.
 148        return cls.description if cls.description is not None else ''
 149
 150    @classmethod
 151    def get_description_display_string(
 152        cls, sessiontype: type[bascenev1.Session]
 153    ) -> babase.Lstr:
 154        """Return a translated version of get_description().
 155
 156        Sub-classes should override get_description(); not this.
 157        """
 158        description = cls.get_description(sessiontype)
 159        return babase.Lstr(translate=('gameDescriptions', description))
 160
 161    @classmethod
 162    def get_available_settings(
 163        cls, sessiontype: type[bascenev1.Session]
 164    ) -> list[bascenev1.Setting]:
 165        """Return a list of settings relevant to this game type when
 166        running under the provided session type.
 167        """
 168        del sessiontype  # Unused arg.
 169        return [] if cls.available_settings is None else cls.available_settings
 170
 171    @classmethod
 172    def get_supported_maps(
 173        cls, sessiontype: type[bascenev1.Session]
 174    ) -> list[str]:
 175        """
 176        Called by the default bascenev1.GameActivity.create_settings_ui()
 177        implementation; should return a list of map names valid
 178        for this game-type for the given bascenev1.Session type.
 179        """
 180        del sessiontype  # Unused arg.
 181        assert babase.app.classic is not None
 182        return babase.app.classic.getmaps('melee')
 183
 184    @classmethod
 185    def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr:
 186        """Given a game config dict, return a short description for it.
 187
 188        This is used when viewing game-lists or showing what game
 189        is up next in a series.
 190        """
 191        name = cls.get_display_string(config['settings'])
 192
 193        # In newer configs, map is in settings; it used to be in the
 194        # config root.
 195        if 'map' in config['settings']:
 196            sval = babase.Lstr(
 197                value='${NAME} @ ${MAP}',
 198                subs=[
 199                    ('${NAME}', name),
 200                    (
 201                        '${MAP}',
 202                        _map.get_map_display_string(
 203                            _map.get_filtered_map_name(
 204                                config['settings']['map']
 205                            )
 206                        ),
 207                    ),
 208                ],
 209            )
 210        elif 'map' in config:
 211            sval = babase.Lstr(
 212                value='${NAME} @ ${MAP}',
 213                subs=[
 214                    ('${NAME}', name),
 215                    (
 216                        '${MAP}',
 217                        _map.get_map_display_string(
 218                            _map.get_filtered_map_name(config['map'])
 219                        ),
 220                    ),
 221                ],
 222            )
 223        else:
 224            print('invalid game config - expected map entry under settings')
 225            sval = babase.Lstr(value='???')
 226        return sval
 227
 228    @classmethod
 229    def supports_session_type(
 230        cls, sessiontype: type[bascenev1.Session]
 231    ) -> bool:
 232        """Return whether this game supports the provided Session type."""
 233        from bascenev1._multiteamsession import MultiTeamSession
 234
 235        # By default, games support any versus mode
 236        return issubclass(sessiontype, MultiTeamSession)
 237
 238    def __init__(self, settings: dict):
 239        """Instantiate the Activity."""
 240        super().__init__(settings)
 241
 242        plus = babase.app.plus
 243
 244        # Holds some flattened info about the player set at the point
 245        # when on_begin() is called.
 246        self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
 247
 248        # Go ahead and get our map loading.
 249        self._map_type = _map.get_map_class(self._calc_map_name(settings))
 250
 251        self._spawn_sound = _bascenev1.getsound('spawn')
 252        self._map_type.preload()
 253        self._map: bascenev1.Map | None = None
 254        self._powerup_drop_timer: bascenev1.Timer | None = None
 255        self._tnt_spawners: dict[int, TNTSpawner] | None = None
 256        self._tnt_drop_timer: bascenev1.Timer | None = None
 257        self._game_scoreboard_name_text: bascenev1.Actor | None = None
 258        self._game_scoreboard_description_text: bascenev1.Actor | None = None
 259        self._standard_time_limit_time: int | None = None
 260        self._standard_time_limit_timer: bascenev1.Timer | None = None
 261        self._standard_time_limit_text: bascenev1.NodeActor | None = None
 262        self._standard_time_limit_text_input: bascenev1.NodeActor | None = None
 263        self._tournament_time_limit: int | None = None
 264        self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None
 265        self._tournament_time_limit_title_text: bascenev1.NodeActor | None = (
 266            None
 267        )
 268        self._tournament_time_limit_text: bascenev1.NodeActor | None = None
 269        self._tournament_time_limit_text_input: bascenev1.NodeActor | None = (
 270            None
 271        )
 272        self._zoom_message_times: dict[int, float] = {}
 273        self._is_waiting_for_continue = False
 274
 275        self._continue_cost = (
 276            25
 277            if plus is None
 278            else plus.get_v1_account_misc_read_val('continueStartCost', 25)
 279        )
 280        self._continue_cost_mult = (
 281            2
 282            if plus is None
 283            else plus.get_v1_account_misc_read_val('continuesMult', 2)
 284        )
 285        self._continue_cost_offset = (
 286            0
 287            if plus is None
 288            else plus.get_v1_account_misc_read_val('continuesOffset', 0)
 289        )
 290
 291    @property
 292    def map(self) -> _map.Map:
 293        """The map being used for this game.
 294
 295        Raises a bascenev1.MapNotFoundError if the map does not currently
 296        exist.
 297        """
 298        if self._map is None:
 299            raise babase.MapNotFoundError
 300        return self._map
 301
 302    def get_instance_display_string(self) -> babase.Lstr:
 303        """Return a name for this particular game instance."""
 304        return self.get_display_string(self.settings_raw)
 305
 306    # noinspection PyUnresolvedReferences
 307    def get_instance_scoreboard_display_string(self) -> babase.Lstr:
 308        """Return a name for this particular game instance.
 309
 310        This name is used above the game scoreboard in the corner
 311        of the screen, so it should be as concise as possible.
 312        """
 313        # If we're in a co-op session, use the level name.
 314        # FIXME: Should clean this up.
 315        try:
 316            from bascenev1._coopsession import CoopSession
 317
 318            if isinstance(self.session, CoopSession):
 319                campaign = self.session.campaign
 320                assert campaign is not None
 321                return campaign.getlevel(
 322                    self.session.campaign_level_name
 323                ).displayname
 324        except Exception:
 325            logging.exception('Error getting campaign level name.')
 326        return self.get_instance_display_string()
 327
 328    def get_instance_description(self) -> str | Sequence:
 329        """Return a description for this game instance, in English.
 330
 331        This is shown in the center of the screen below the game name at the
 332        start of a game. It should start with a capital letter and end with a
 333        period, and can be a bit more verbose than the version returned by
 334        get_instance_description_short().
 335
 336        Note that translation is applied by looking up the specific returned
 337        value as a key, so the number of returned variations should be limited;
 338        ideally just one or two. To include arbitrary values in the
 339        description, you can return a sequence of values in the following
 340        form instead of just a string:
 341
 342        # This will give us something like 'Score 3 goals.' in English
 343        # and can properly translate to 'Anota 3 goles.' in Spanish.
 344        # If we just returned the string 'Score 3 Goals' here, there would
 345        # have to be a translation entry for each specific number. ew.
 346        return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
 347
 348        This way the first string can be consistently translated, with any arg
 349        values then substituted into the result. ${ARG1} will be replaced with
 350        the first value, ${ARG2} with the second, etc.
 351        """
 352        return self.get_description(type(self.session))
 353
 354    def get_instance_description_short(self) -> str | Sequence:
 355        """Return a short description for this game instance in English.
 356
 357        This description is used above the game scoreboard in the
 358        corner of the screen, so it should be as concise as possible.
 359        It should be lowercase and should not contain periods or other
 360        punctuation.
 361
 362        Note that translation is applied by looking up the specific returned
 363        value as a key, so the number of returned variations should be limited;
 364        ideally just one or two. To include arbitrary values in the
 365        description, you can return a sequence of values in the following form
 366        instead of just a string:
 367
 368        # This will give us something like 'score 3 goals' in English
 369        # and can properly translate to 'anota 3 goles' in Spanish.
 370        # If we just returned the string 'score 3 goals' here, there would
 371        # have to be a translation entry for each specific number. ew.
 372        return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
 373
 374        This way the first string can be consistently translated, with any arg
 375        values then substituted into the result. ${ARG1} will be replaced
 376        with the first value, ${ARG2} with the second, etc.
 377
 378        """
 379        return ''
 380
 381    @override
 382    def on_transition_in(self) -> None:
 383        super().on_transition_in()
 384
 385        # Make our map.
 386        self._map = self._map_type()
 387
 388        # Give our map a chance to override the music.
 389        # (for happy-thoughts and other such themed maps)
 390        map_music = self._map_type.get_music_type()
 391        music = map_music if map_music is not None else self.default_music
 392
 393        if music is not None:
 394            _music.setmusic(music)
 395
 396    def on_continue(self) -> None:
 397        """
 398        This is called if a game supports and offers a continue and the player
 399        accepts.  In this case the player should be given an extra life or
 400        whatever is relevant to keep the game going.
 401        """
 402
 403    def _continue_choice(self, do_continue: bool) -> None:
 404        plus = babase.app.plus
 405        assert plus is not None
 406        self._is_waiting_for_continue = False
 407        if self.has_ended():
 408            return
 409        with self.context:
 410            if do_continue:
 411                _bascenev1.getsound('shieldUp').play()
 412                _bascenev1.getsound('cashRegister').play()
 413                plus.add_v1_account_transaction(
 414                    {'type': 'CONTINUE', 'cost': self._continue_cost}
 415                )
 416                if plus is not None:
 417                    plus.run_v1_account_transactions()
 418                self._continue_cost = (
 419                    self._continue_cost * self._continue_cost_mult
 420                    + self._continue_cost_offset
 421                )
 422                self.on_continue()
 423            else:
 424                self.end_game()
 425
 426    def is_waiting_for_continue(self) -> bool:
 427        """Returns whether or not this activity is currently waiting for the
 428        player to continue (or timeout)"""
 429        return self._is_waiting_for_continue
 430
 431    def continue_or_end_game(self) -> None:
 432        """If continues are allowed, prompts the player to purchase a continue
 433        and calls either end_game or continue_game depending on the result
 434        """
 435        # pylint: disable=too-many-nested-blocks
 436        # pylint: disable=cyclic-import
 437        from bascenev1._coopsession import CoopSession
 438
 439        classic = babase.app.classic
 440        assert classic is not None
 441        continues_window = classic.continues_window
 442
 443        # Turning these off. I want to migrate towards monetization that
 444        # feels less pay-to-win-ish.
 445        allow_continues = False
 446
 447        plus = babase.app.plus
 448        try:
 449            if (
 450                plus is not None
 451                and plus.get_v1_account_misc_read_val('enableContinues', False)
 452                and allow_continues
 453            ):
 454                session = self.session
 455
 456                # We only support continuing in non-tournament games.
 457                tournament_id = session.tournament_id
 458                if tournament_id is None:
 459                    # We currently only support continuing in sequential
 460                    # co-op campaigns.
 461                    if isinstance(session, CoopSession):
 462                        assert session.campaign is not None
 463                        if session.campaign.sequential:
 464                            gnode = self.globalsnode
 465
 466                            # Only attempt this if we're not currently paused
 467                            # and there appears to be no UI.
 468                            assert babase.app.classic is not None
 469                            hmmw = babase.app.ui_v1.has_main_menu_window()
 470                            if not gnode.paused and not hmmw:
 471                                self._is_waiting_for_continue = True
 472                                with babase.ContextRef.empty():
 473                                    babase.apptimer(
 474                                        0.5,
 475                                        lambda: continues_window(
 476                                            self,
 477                                            self._continue_cost,
 478                                            continue_call=babase.WeakCall(
 479                                                self._continue_choice, True
 480                                            ),
 481                                            cancel_call=babase.WeakCall(
 482                                                self._continue_choice, False
 483                                            ),
 484                                        ),
 485                                    )
 486                                return
 487
 488        except Exception:
 489            logging.exception('Error handling continues.')
 490
 491        self.end_game()
 492
 493    @override
 494    def on_begin(self) -> None:
 495        super().on_begin()
 496
 497        if babase.app.classic is not None:
 498            babase.app.classic.game_begin_analytics()
 499
 500        # We don't do this in on_transition_in because it may depend on
 501        # players/teams which aren't available until now.
 502        _bascenev1.timer(0.001, self._show_scoreboard_info)
 503        _bascenev1.timer(1.0, self._show_info)
 504        _bascenev1.timer(2.5, self._show_tip)
 505
 506        # Store some basic info about players present at start time.
 507        self.initialplayerinfos = [
 508            PlayerInfo(name=p.getname(full=True), character=p.character)
 509            for p in self.players
 510        ]
 511
 512        # Sort this by name so high score lists/etc will be consistent
 513        # regardless of player join order.
 514        self.initialplayerinfos.sort(key=lambda x: x.name)
 515
 516        # If this is a tournament, query info about it such as how much
 517        # time is left.
 518        tournament_id = self.session.tournament_id
 519        if tournament_id is not None:
 520            assert babase.app.plus is not None
 521            babase.app.plus.tournament_query(
 522                args={
 523                    'tournamentIDs': [tournament_id],
 524                    'source': 'in-game time remaining query',
 525                },
 526                callback=babase.WeakCall(self._on_tournament_query_response),
 527            )
 528
 529    def _on_tournament_query_response(
 530        self, data: dict[str, Any] | None
 531    ) -> None:
 532        if data is not None:
 533            data_t = data['t']  # This used to be the whole payload.
 534
 535            # Keep our cached tourney info up to date
 536            assert babase.app.classic is not None
 537            babase.app.classic.accounts.cache_tournament_info(data_t)
 538            self._setup_tournament_time_limit(
 539                max(5, data_t[0]['timeRemaining'])
 540            )
 541
 542    @override
 543    def on_player_join(self, player: PlayerT) -> None:
 544        super().on_player_join(player)
 545
 546        # By default, just spawn a dude.
 547        self.spawn_player(player)
 548
 549    @override
 550    def handlemessage(self, msg: Any) -> Any:
 551        if isinstance(msg, PlayerDiedMessage):
 552            # pylint: disable=cyclic-import
 553            from bascenev1lib.actor.spaz import Spaz
 554
 555            player = msg.getplayer(self.playertype)
 556            killer = msg.getkillerplayer(self.playertype)
 557
 558            # Inform our stats of the demise.
 559            self.stats.player_was_killed(
 560                player, killed=msg.killed, killer=killer
 561            )
 562
 563            # Award the killer points if he's on a different team.
 564            # FIXME: This should not be linked to Spaz actors.
 565            # (should move get_death_points to Actor or make it a message)
 566            if killer and killer.team is not player.team:
 567                assert isinstance(killer.actor, Spaz)
 568                pts, importance = killer.actor.get_death_points(msg.how)
 569                if not self.has_ended():
 570                    self.stats.player_scored(
 571                        killer,
 572                        pts,
 573                        kill=True,
 574                        victim_player=player,
 575                        importance=importance,
 576                        showpoints=self.show_kill_points,
 577                    )
 578        else:
 579            return super().handlemessage(msg)
 580        return None
 581
 582    def _show_scoreboard_info(self) -> None:
 583        """Create the game info display.
 584
 585        This is the thing in the top left corner showing the name
 586        and short description of the game.
 587        """
 588        # pylint: disable=too-many-locals
 589        from bascenev1._freeforallsession import FreeForAllSession
 590        from bascenev1._gameutils import animate
 591        from bascenev1._nodeactor import NodeActor
 592
 593        sb_name = self.get_instance_scoreboard_display_string()
 594
 595        # The description can be either a string or a sequence with args
 596        # to swap in post-translation.
 597        sb_desc_in = self.get_instance_description_short()
 598        sb_desc_l: Sequence
 599        if isinstance(sb_desc_in, str):
 600            sb_desc_l = [sb_desc_in]  # handle simple string case
 601        else:
 602            sb_desc_l = sb_desc_in
 603        if not isinstance(sb_desc_l[0], str):
 604            raise TypeError('Invalid format for instance description.')
 605
 606        is_empty = sb_desc_l[0] == ''
 607        subs = []
 608        for i in range(len(sb_desc_l) - 1):
 609            subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1])))
 610        translation = babase.Lstr(
 611            translate=('gameDescriptions', sb_desc_l[0]), subs=subs
 612        )
 613        sb_desc = translation
 614        vrmode = babase.app.env.vr
 615        yval = -34 if is_empty else -20
 616        yval -= 16
 617        sbpos = (
 618            (15, yval)
 619            if isinstance(self.session, FreeForAllSession)
 620            else (15, yval)
 621        )
 622        self._game_scoreboard_name_text = NodeActor(
 623            _bascenev1.newnode(
 624                'text',
 625                attrs={
 626                    'text': sb_name,
 627                    'maxwidth': 300,
 628                    'position': sbpos,
 629                    'h_attach': 'left',
 630                    'vr_depth': 10,
 631                    'v_attach': 'top',
 632                    'v_align': 'bottom',
 633                    'color': (1.0, 1.0, 1.0, 1.0),
 634                    'shadow': 1.0 if vrmode else 0.6,
 635                    'flatness': 1.0 if vrmode else 0.5,
 636                    'scale': 1.1,
 637                },
 638            )
 639        )
 640
 641        assert self._game_scoreboard_name_text.node
 642        animate(
 643            self._game_scoreboard_name_text.node, 'opacity', {0: 0.0, 1.0: 1.0}
 644        )
 645
 646        descpos = (
 647            (17, -44 + 10)
 648            if isinstance(self.session, FreeForAllSession)
 649            else (17, -44 + 10)
 650        )
 651        self._game_scoreboard_description_text = NodeActor(
 652            _bascenev1.newnode(
 653                'text',
 654                attrs={
 655                    'text': sb_desc,
 656                    'maxwidth': 480,
 657                    'position': descpos,
 658                    'scale': 0.7,
 659                    'h_attach': 'left',
 660                    'v_attach': 'top',
 661                    'v_align': 'top',
 662                    'shadow': 1.0 if vrmode else 0.7,
 663                    'flatness': 1.0 if vrmode else 0.8,
 664                    'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0),
 665                },
 666            )
 667        )
 668
 669        assert self._game_scoreboard_description_text.node
 670        animate(
 671            self._game_scoreboard_description_text.node,
 672            'opacity',
 673            {0: 0.0, 1.0: 1.0},
 674        )
 675
 676    def _show_info(self) -> None:
 677        """Show the game description."""
 678        from bascenev1._gameutils import animate
 679        from bascenev1lib.actor.zoomtext import ZoomText
 680
 681        name = self.get_instance_display_string()
 682        ZoomText(
 683            name,
 684            maxwidth=800,
 685            lifespan=2.5,
 686            jitter=2.0,
 687            position=(0, 180),
 688            flash=False,
 689            color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25),
 690            trailcolor=(0.15, 0.05, 1.0, 0.0),
 691        ).autoretain()
 692        _bascenev1.timer(0.2, _bascenev1.getsound('gong').play)
 693        # _bascenev1.timer(
 694        #     0.2, Call(_bascenev1.playsound, _bascenev1.getsound('gong'))
 695        # )
 696
 697        # The description can be either a string or a sequence with args
 698        # to swap in post-translation.
 699        desc_in = self.get_instance_description()
 700        desc_l: Sequence
 701        if isinstance(desc_in, str):
 702            desc_l = [desc_in]  # handle simple string case
 703        else:
 704            desc_l = desc_in
 705        if not isinstance(desc_l[0], str):
 706            raise TypeError('Invalid format for instance description')
 707        subs = []
 708        for i in range(len(desc_l) - 1):
 709            subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1])))
 710        translation = babase.Lstr(
 711            translate=('gameDescriptions', desc_l[0]), subs=subs
 712        )
 713
 714        # Do some standard filters (epic mode, etc).
 715        if self.settings_raw.get('Epic Mode', False):
 716            translation = babase.Lstr(
 717                resource='epicDescriptionFilterText',
 718                subs=[('${DESCRIPTION}', translation)],
 719            )
 720        vrmode = babase.app.env.vr
 721        dnode = _bascenev1.newnode(
 722            'text',
 723            attrs={
 724                'v_attach': 'center',
 725                'h_attach': 'center',
 726                'h_align': 'center',
 727                'color': (1, 1, 1, 1),
 728                'shadow': 1.0 if vrmode else 0.5,
 729                'flatness': 1.0 if vrmode else 0.5,
 730                'vr_depth': -30,
 731                'position': (0, 80),
 732                'scale': 1.2,
 733                'maxwidth': 700,
 734                'text': translation,
 735            },
 736        )
 737        cnode = _bascenev1.newnode(
 738            'combine',
 739            owner=dnode,
 740            attrs={'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4},
 741        )
 742        cnode.connectattr('output', dnode, 'color')
 743        keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0}
 744        animate(cnode, 'input3', keys)
 745        _bascenev1.timer(4.0, dnode.delete)
 746
 747    def _show_tip(self) -> None:
 748        # pylint: disable=too-many-locals
 749        from bascenev1._gameutils import animate, GameTip
 750
 751        # If there's any tips left on the list, display one.
 752        if self.tips:
 753            tip = self.tips.pop(random.randrange(len(self.tips)))
 754            tip_title = babase.Lstr(
 755                value='${A}:', subs=[('${A}', babase.Lstr(resource='tipText'))]
 756            )
 757            icon: bascenev1.Texture | None = None
 758            sound: bascenev1.Sound | None = None
 759            if isinstance(tip, GameTip):
 760                icon = tip.icon
 761                sound = tip.sound
 762                tip = tip.text
 763                assert isinstance(tip, str)
 764
 765            # Do a few substitutions.
 766            tip_lstr = babase.Lstr(
 767                translate=('tips', tip),
 768                subs=[
 769                    ('${PICKUP}', babase.charstr(babase.SpecialChar.TOP_BUTTON))
 770                ],
 771            )
 772            base_position = (75, 50)
 773            tip_scale = 0.8
 774            tip_title_scale = 1.2
 775            vrmode = babase.app.env.vr
 776
 777            t_offs = -350.0
 778            tnode = _bascenev1.newnode(
 779                'text',
 780                attrs={
 781                    'text': tip_lstr,
 782                    'scale': tip_scale,
 783                    'maxwidth': 900,
 784                    'position': (base_position[0] + t_offs, base_position[1]),
 785                    'h_align': 'left',
 786                    'vr_depth': 300,
 787                    'shadow': 1.0 if vrmode else 0.5,
 788                    'flatness': 1.0 if vrmode else 0.5,
 789                    'v_align': 'center',
 790                    'v_attach': 'bottom',
 791                },
 792            )
 793            t2pos = (
 794                base_position[0] + t_offs - (20 if icon is None else 82),
 795                base_position[1] + 2,
 796            )
 797            t2node = _bascenev1.newnode(
 798                'text',
 799                owner=tnode,
 800                attrs={
 801                    'text': tip_title,
 802                    'scale': tip_title_scale,
 803                    'position': t2pos,
 804                    'h_align': 'right',
 805                    'vr_depth': 300,
 806                    'shadow': 1.0 if vrmode else 0.5,
 807                    'flatness': 1.0 if vrmode else 0.5,
 808                    'maxwidth': 140,
 809                    'v_align': 'center',
 810                    'v_attach': 'bottom',
 811                },
 812            )
 813            if icon is not None:
 814                ipos = (base_position[0] + t_offs - 40, base_position[1] + 1)
 815                img = _bascenev1.newnode(
 816                    'image',
 817                    attrs={
 818                        'texture': icon,
 819                        'position': ipos,
 820                        'scale': (50, 50),
 821                        'opacity': 1.0,
 822                        'vr_depth': 315,
 823                        'color': (1, 1, 1),
 824                        'absolute_scale': True,
 825                        'attach': 'bottomCenter',
 826                    },
 827                )
 828                animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
 829                _bascenev1.timer(5.0, img.delete)
 830            if sound is not None:
 831                sound.play()
 832
 833            combine = _bascenev1.newnode(
 834                'combine',
 835                owner=tnode,
 836                attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4},
 837            )
 838            combine.connectattr('output', tnode, 'color')
 839            combine.connectattr('output', t2node, 'color')
 840            animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
 841            _bascenev1.timer(5.0, tnode.delete)
 842
 843    @override
 844    def end(
 845        self, results: Any = None, delay: float = 0.0, force: bool = False
 846    ) -> None:
 847        from bascenev1._gameresults import GameResults
 848
 849        # If results is a standard team-game-results, associate it with us
 850        # so it can grab our score prefs.
 851        if isinstance(results, GameResults):
 852            results.set_game(self)
 853
 854        # If we had a standard time-limit that had not expired, stop it so
 855        # it doesnt tick annoyingly.
 856        if (
 857            self._standard_time_limit_time is not None
 858            and self._standard_time_limit_time > 0
 859        ):
 860            self._standard_time_limit_timer = None
 861            self._standard_time_limit_text = None
 862
 863        # Ditto with tournament time limits.
 864        if (
 865            self._tournament_time_limit is not None
 866            and self._tournament_time_limit > 0
 867        ):
 868            self._tournament_time_limit_timer = None
 869            self._tournament_time_limit_text = None
 870            self._tournament_time_limit_title_text = None
 871
 872        super().end(results, delay, force)
 873
 874    def end_game(self) -> None:
 875        """Tell the game to wrap up and call bascenev1.Activity.end().
 876
 877        This method should be overridden by subclasses. A game should always
 878        be prepared to end and deliver results, even if there is no 'winner'
 879        yet; this way things like the standard time-limit
 880        (bascenev1.GameActivity.setup_standard_time_limit()) will work with
 881        the game.
 882        """
 883        print(
 884            'WARNING: default end_game() implementation called;'
 885            ' your game should override this.'
 886        )
 887
 888    def respawn_player(
 889        self, player: PlayerT, respawn_time: float | None = None
 890    ) -> None:
 891        """
 892        Given a bascenev1.Player, sets up a standard respawn timer,
 893        along with the standard counter display, etc.
 894        At the end of the respawn period spawn_player() will
 895        be called if the Player still exists.
 896        An explicit 'respawn_time' can optionally be provided
 897        (in seconds).
 898        """
 899        # pylint: disable=cyclic-import
 900
 901        assert player
 902        if respawn_time is None:
 903            teamsize = len(player.team.players)
 904            if teamsize == 1:
 905                respawn_time = 3.0
 906            elif teamsize == 2:
 907                respawn_time = 5.0
 908            elif teamsize == 3:
 909                respawn_time = 6.0
 910            else:
 911                respawn_time = 7.0
 912
 913        # If this standard setting is present, factor it in.
 914        if 'Respawn Times' in self.settings_raw:
 915            respawn_time *= self.settings_raw['Respawn Times']
 916
 917        # We want whole seconds.
 918        assert respawn_time is not None
 919        respawn_time = round(max(1.0, respawn_time), 0)
 920
 921        if player.actor and not self.has_ended():
 922            from bascenev1lib.actor.respawnicon import RespawnIcon
 923
 924            player.customdata['respawn_timer'] = _bascenev1.Timer(
 925                respawn_time,
 926                babase.WeakCall(self.spawn_player_if_exists, player),
 927            )
 928            player.customdata['respawn_icon'] = RespawnIcon(
 929                player, respawn_time
 930            )
 931
 932    def spawn_player_if_exists(self, player: PlayerT) -> None:
 933        """
 934        A utility method which calls self.spawn_player() *only* if the
 935        bascenev1.Player provided still exists; handy for use in timers
 936        and whatnot.
 937
 938        There is no need to override this; just override spawn_player().
 939        """
 940        if player:
 941            self.spawn_player(player)
 942
 943    def spawn_player(self, player: PlayerT) -> bascenev1.Actor:
 944        """Spawn *something* for the provided bascenev1.Player.
 945
 946        The default implementation simply calls spawn_player_spaz().
 947        """
 948        assert player  # Dead references should never be passed as args.
 949
 950        return self.spawn_player_spaz(player)
 951
 952    def spawn_player_spaz(
 953        self,
 954        player: PlayerT,
 955        position: Sequence[float] = (0, 0, 0),
 956        angle: float | None = None,
 957    ) -> PlayerSpaz:
 958        """Create and wire up a bascenev1.PlayerSpaz for the provided Player."""
 959        # pylint: disable=too-many-locals
 960        # pylint: disable=cyclic-import
 961        from bascenev1._gameutils import animate
 962        from bascenev1._coopsession import CoopSession
 963        from bascenev1lib.actor.playerspaz import PlayerSpaz
 964
 965        name = player.getname()
 966        color = player.color
 967        highlight = player.highlight
 968
 969        playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz)
 970        if not issubclass(playerspaztype, PlayerSpaz):
 971            playerspaztype = PlayerSpaz
 972
 973        light_color = babase.normalized_color(color)
 974        display_color = babase.safecolor(color, target_intensity=0.75)
 975        spaz = playerspaztype(
 976            color=color,
 977            highlight=highlight,
 978            character=player.character,
 979            player=player,
 980        )
 981
 982        player.actor = spaz
 983        assert spaz.node
 984
 985        # If this is co-op and we're on Courtyard or Runaround, add the
 986        # material that allows us to collide with the player-walls.
 987        # FIXME: Need to generalize this.
 988        if isinstance(self.session, CoopSession) and self.map.getname() in [
 989            'Courtyard',
 990            'Tower D',
 991        ]:
 992            mat = self.map.preloaddata['collide_with_wall_material']
 993            assert isinstance(spaz.node.materials, tuple)
 994            assert isinstance(spaz.node.roller_materials, tuple)
 995            spaz.node.materials += (mat,)
 996            spaz.node.roller_materials += (mat,)
 997
 998        spaz.node.name = name
 999        spaz.node.name_color = display_color
1000        spaz.connect_controls_to_player()
1001
1002        # Move to the stand position and add a flash of light.
1003        spaz.handlemessage(
1004            StandMessage(
1005                position, angle if angle is not None else random.uniform(0, 360)
1006            )
1007        )
1008        self._spawn_sound.play(1, position=spaz.node.position)
1009        light = _bascenev1.newnode('light', attrs={'color': light_color})
1010        spaz.node.connectattr('position', light, 'position')
1011        animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
1012        _bascenev1.timer(0.5, light.delete)
1013        return spaz
1014
1015    def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
1016        """Create standard powerup drops for the current map."""
1017        # pylint: disable=cyclic-import
1018        from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL
1019
1020        self._powerup_drop_timer = _bascenev1.Timer(
1021            DEFAULT_POWERUP_INTERVAL,
1022            babase.WeakCall(self._standard_drop_powerups),
1023            repeat=True,
1024        )
1025        self._standard_drop_powerups()
1026        if enable_tnt:
1027            self._tnt_spawners = {}
1028            self._setup_standard_tnt_drops()
1029
1030    def _standard_drop_powerup(self, index: int, expire: bool = True) -> None:
1031        # pylint: disable=cyclic-import
1032        from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory
1033
1034        PowerupBox(
1035            position=self.map.powerup_spawn_points[index],
1036            poweruptype=PowerupBoxFactory.get().get_random_powerup_type(),
1037            expire=expire,
1038        ).autoretain()
1039
1040    def _standard_drop_powerups(self) -> None:
1041        """Standard powerup drop."""
1042
1043        # Drop one powerup per point.
1044        points = self.map.powerup_spawn_points
1045        for i in range(len(points)):
1046            _bascenev1.timer(
1047                i * 0.4, babase.WeakCall(self._standard_drop_powerup, i)
1048            )
1049
1050    def _setup_standard_tnt_drops(self) -> None:
1051        """Standard tnt drop."""
1052        # pylint: disable=cyclic-import
1053        from bascenev1lib.actor.bomb import TNTSpawner
1054
1055        for i, point in enumerate(self.map.tnt_points):
1056            assert self._tnt_spawners is not None
1057            if self._tnt_spawners.get(i) is None:
1058                self._tnt_spawners[i] = TNTSpawner(point)
1059
1060    def setup_standard_time_limit(self, duration: float) -> None:
1061        """
1062        Create a standard game time-limit given the provided
1063        duration in seconds.
1064        This will be displayed at the top of the screen.
1065        If the time-limit expires, end_game() will be called.
1066        """
1067        from bascenev1._nodeactor import NodeActor
1068
1069        if duration <= 0.0:
1070            return
1071        self._standard_time_limit_time = int(duration)
1072        self._standard_time_limit_timer = _bascenev1.Timer(
1073            1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True
1074        )
1075        self._standard_time_limit_text = NodeActor(
1076            _bascenev1.newnode(
1077                'text',
1078                attrs={
1079                    'v_attach': 'top',
1080                    'h_attach': 'center',
1081                    'h_align': 'left',
1082                    'color': (1.0, 1.0, 1.0, 0.5),
1083                    'position': (-25, -30),
1084                    'flatness': 1.0,
1085                    'scale': 0.9,
1086                },
1087            )
1088        )
1089        self._standard_time_limit_text_input = NodeActor(
1090            _bascenev1.newnode(
1091                'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0}
1092            )
1093        )
1094        self.globalsnode.connectattr(
1095            'time', self._standard_time_limit_text_input.node, 'time1'
1096        )
1097        assert self._standard_time_limit_text_input.node
1098        assert self._standard_time_limit_text.node
1099        self._standard_time_limit_text_input.node.connectattr(
1100            'output', self._standard_time_limit_text.node, 'text'
1101        )
1102
1103    def _standard_time_limit_tick(self) -> None:
1104        from bascenev1._gameutils import animate
1105
1106        assert self._standard_time_limit_time is not None
1107        self._standard_time_limit_time -= 1
1108        if self._standard_time_limit_time <= 10:
1109            if self._standard_time_limit_time == 10:
1110                assert self._standard_time_limit_text is not None
1111                assert self._standard_time_limit_text.node
1112                self._standard_time_limit_text.node.scale = 1.3
1113                self._standard_time_limit_text.node.position = (-30, -45)
1114                cnode = _bascenev1.newnode(
1115                    'combine',
1116                    owner=self._standard_time_limit_text.node,
1117                    attrs={'size': 4},
1118                )
1119                cnode.connectattr(
1120                    'output', self._standard_time_limit_text.node, 'color'
1121                )
1122                animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True)
1123                animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True)
1124                animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True)
1125                cnode.input3 = 1.0
1126            _bascenev1.getsound('tick').play()
1127        if self._standard_time_limit_time <= 0:
1128            self._standard_time_limit_timer = None
1129            self.end_game()
1130            node = _bascenev1.newnode(
1131                'text',
1132                attrs={
1133                    'v_attach': 'top',
1134                    'h_attach': 'center',
1135                    'h_align': 'center',
1136                    'color': (1, 0.7, 0, 1),
1137                    'position': (0, -90),
1138                    'scale': 1.2,
1139                    'text': babase.Lstr(resource='timeExpiredText'),
1140                },
1141            )
1142            _bascenev1.getsound('refWhistle').play()
1143            animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2})
1144
1145    def _setup_tournament_time_limit(self, duration: float) -> None:
1146        """
1147        Create a tournament game time-limit given the provided
1148        duration in seconds.
1149        This will be displayed at the top of the screen.
1150        If the time-limit expires, end_game() will be called.
1151        """
1152        from bascenev1._nodeactor import NodeActor
1153
1154        if duration <= 0.0:
1155            return
1156        self._tournament_time_limit = int(duration)
1157
1158        # We want this timer to match the server's time as close as possible,
1159        # so lets go with base-time. Theoretically we should do real-time but
1160        # then we have to mess with contexts and whatnot since its currently
1161        # not available in activity contexts. :-/
1162        self._tournament_time_limit_timer = _bascenev1.BaseTimer(
1163            1.0, babase.WeakCall(self._tournament_time_limit_tick), repeat=True
1164        )
1165        self._tournament_time_limit_title_text = NodeActor(
1166            _bascenev1.newnode(
1167                'text',
1168                attrs={
1169                    'v_attach': 'bottom',
1170                    'h_attach': 'left',
1171                    'h_align': 'center',
1172                    'v_align': 'center',
1173                    'vr_depth': 300,
1174                    'maxwidth': 100,
1175                    'color': (1.0, 1.0, 1.0, 0.5),
1176                    'position': (60, 50),
1177                    'flatness': 1.0,
1178                    'scale': 0.5,
1179                    'text': babase.Lstr(resource='tournamentText'),
1180                },
1181            )
1182        )
1183        self._tournament_time_limit_text = NodeActor(
1184            _bascenev1.newnode(
1185                'text',
1186                attrs={
1187                    'v_attach': 'bottom',
1188                    'h_attach': 'left',
1189                    'h_align': 'center',
1190                    'v_align': 'center',
1191                    'vr_depth': 300,
1192                    'maxwidth': 100,
1193                    'color': (1.0, 1.0, 1.0, 0.5),
1194                    'position': (60, 30),
1195                    'flatness': 1.0,
1196                    'scale': 0.9,
1197                },
1198            )
1199        )
1200        self._tournament_time_limit_text_input = NodeActor(
1201            _bascenev1.newnode(
1202                'timedisplay',
1203                attrs={
1204                    'timemin': 0,
1205                    'time2': self._tournament_time_limit * 1000,
1206                },
1207            )
1208        )
1209        assert self._tournament_time_limit_text.node
1210        assert self._tournament_time_limit_text_input.node
1211        self._tournament_time_limit_text_input.node.connectattr(
1212            'output', self._tournament_time_limit_text.node, 'text'
1213        )
1214
1215    def _tournament_time_limit_tick(self) -> None:
1216        from bascenev1._gameutils import animate
1217
1218        assert self._tournament_time_limit is not None
1219        self._tournament_time_limit -= 1
1220        if self._tournament_time_limit <= 10:
1221            if self._tournament_time_limit == 10:
1222                assert self._tournament_time_limit_title_text is not None
1223                assert self._tournament_time_limit_title_text.node
1224                assert self._tournament_time_limit_text is not None
1225                assert self._tournament_time_limit_text.node
1226                self._tournament_time_limit_title_text.node.scale = 1.0
1227                self._tournament_time_limit_text.node.scale = 1.3
1228                self._tournament_time_limit_title_text.node.position = (80, 85)
1229                self._tournament_time_limit_text.node.position = (80, 60)
1230                cnode = _bascenev1.newnode(
1231                    'combine',
1232                    owner=self._tournament_time_limit_text.node,
1233                    attrs={'size': 4},
1234                )
1235                cnode.connectattr(
1236                    'output',
1237                    self._tournament_time_limit_title_text.node,
1238                    'color',
1239                )
1240                cnode.connectattr(
1241                    'output', self._tournament_time_limit_text.node, 'color'
1242                )
1243                animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True)
1244                animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True)
1245                animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True)
1246                cnode.input3 = 1.0
1247            _bascenev1.getsound('tick').play()
1248        if self._tournament_time_limit <= 0:
1249            self._tournament_time_limit_timer = None
1250            self.end_game()
1251            tval = babase.Lstr(
1252                resource='tournamentTimeExpiredText',
1253                fallback_resource='timeExpiredText',
1254            )
1255            node = _bascenev1.newnode(
1256                'text',
1257                attrs={
1258                    'v_attach': 'top',
1259                    'h_attach': 'center',
1260                    'h_align': 'center',
1261                    'color': (1, 0.7, 0, 1),
1262                    'position': (0, -200),
1263                    'scale': 1.6,
1264                    'text': tval,
1265                },
1266            )
1267            _bascenev1.getsound('refWhistle').play()
1268            animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2})
1269
1270        # Normally we just connect this to time, but since this is a bit of a
1271        # funky setup we just update it manually once per second.
1272        assert self._tournament_time_limit_text_input is not None
1273        assert self._tournament_time_limit_text_input.node
1274        self._tournament_time_limit_text_input.node.time2 = (
1275            self._tournament_time_limit * 1000
1276        )
1277
1278    def show_zoom_message(
1279        self,
1280        message: babase.Lstr,
1281        color: Sequence[float] = (0.9, 0.4, 0.0),
1282        scale: float = 0.8,
1283        duration: float = 2.0,
1284        trail: bool = False,
1285    ) -> None:
1286        """Zooming text used to announce game names and winners."""
1287        # pylint: disable=cyclic-import
1288        from bascenev1lib.actor.zoomtext import ZoomText
1289
1290        # Reserve a spot on the screen (in case we get multiple of these so
1291        # they don't overlap).
1292        i = 0
1293        cur_time = babase.apptime()
1294        while True:
1295            if (
1296                i not in self._zoom_message_times
1297                or self._zoom_message_times[i] < cur_time
1298            ):
1299                self._zoom_message_times[i] = cur_time + duration
1300                break
1301            i += 1
1302        ZoomText(
1303            message,
1304            lifespan=duration,
1305            jitter=2.0,
1306            position=(0, 200 - i * 100),
1307            scale=scale,
1308            maxwidth=800,
1309            trail=trail,
1310            color=color,
1311        ).autoretain()
1312
1313    def _calc_map_name(self, settings: dict) -> str:
1314        map_name: str
1315        if 'map' in settings:
1316            map_name = settings['map']
1317        else:
1318            # If settings doesn't specify a map, pick a random one from the
1319            # list of supported ones.
1320            unowned_maps: list[str] = (
1321                babase.app.classic.store.get_unowned_maps()
1322                if babase.app.classic is not None
1323                else []
1324            )
1325            valid_maps: list[str] = [
1326                m
1327                for m in self.get_supported_maps(type(self.session))
1328                if m not in unowned_maps
1329            ]
1330            if not valid_maps:
1331                _bascenev1.broadcastmessage(
1332                    babase.Lstr(resource='noValidMapsErrorText')
1333                )
1334                raise RuntimeError('No valid maps')
1335            map_name = valid_maps[random.randrange(len(valid_maps))]
1336        return map_name

Common base class for all game bascenev1.Activities.

Category: Gameplay Classes

GameActivity(settings: dict)
238    def __init__(self, settings: dict):
239        """Instantiate the Activity."""
240        super().__init__(settings)
241
242        plus = babase.app.plus
243
244        # Holds some flattened info about the player set at the point
245        # when on_begin() is called.
246        self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
247
248        # Go ahead and get our map loading.
249        self._map_type = _map.get_map_class(self._calc_map_name(settings))
250
251        self._spawn_sound = _bascenev1.getsound('spawn')
252        self._map_type.preload()
253        self._map: bascenev1.Map | None = None
254        self._powerup_drop_timer: bascenev1.Timer | None = None
255        self._tnt_spawners: dict[int, TNTSpawner] | None = None
256        self._tnt_drop_timer: bascenev1.Timer | None = None
257        self._game_scoreboard_name_text: bascenev1.Actor | None = None
258        self._game_scoreboard_description_text: bascenev1.Actor | None = None
259        self._standard_time_limit_time: int | None = None
260        self._standard_time_limit_timer: bascenev1.Timer | None = None
261        self._standard_time_limit_text: bascenev1.NodeActor | None = None
262        self._standard_time_limit_text_input: bascenev1.NodeActor | None = None
263        self._tournament_time_limit: int | None = None
264        self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None
265        self._tournament_time_limit_title_text: bascenev1.NodeActor | None = (
266            None
267        )
268        self._tournament_time_limit_text: bascenev1.NodeActor | None = None
269        self._tournament_time_limit_text_input: bascenev1.NodeActor | None = (
270            None
271        )
272        self._zoom_message_times: dict[int, float] = {}
273        self._is_waiting_for_continue = False
274
275        self._continue_cost = (
276            25
277            if plus is None
278            else plus.get_v1_account_misc_read_val('continueStartCost', 25)
279        )
280        self._continue_cost_mult = (
281            2
282            if plus is None
283            else plus.get_v1_account_misc_read_val('continuesMult', 2)
284        )
285        self._continue_cost_offset = (
286            0
287            if plus is None
288            else plus.get_v1_account_misc_read_val('continuesOffset', 0)
289        )

Instantiate the Activity.

tips: list[str | GameTip] = []
name: str | None = None
description: str | None = None
available_settings: list[Setting] | None = None
scoreconfig: ScoreConfig | None = None
allow_pausing = True

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

allow_kick_idle_players = True

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

show_kill_points = True
default_music: MusicType | None = None
@classmethod
def create_settings_ui( cls, sessiontype: type[Session], settings: dict | None, completion_call: Callable[[dict | None], NoneType]) -> None:
68    @classmethod
69    def create_settings_ui(
70        cls,
71        sessiontype: type[bascenev1.Session],
72        settings: dict | None,
73        completion_call: Callable[[dict | None], None],
74    ) -> None:
75        """Launch an in-game UI to configure settings for a game type.
76
77        'sessiontype' should be the bascenev1.Session class the game will
78          be used in.
79
80        'settings' should be an existing settings dict (implies 'edit'
81          ui mode) or None (implies 'add' ui mode).
82
83        'completion_call' will be called with a filled-out settings dict on
84          success or None on cancel.
85
86        Generally subclasses don't need to override this; if they override
87        bascenev1.GameActivity.get_available_settings() and
88        bascenev1.GameActivity.get_supported_maps() they can just rely on
89        the default implementation here which calls those methods.
90        """
91        assert babase.app.classic is not None
92        delegate = babase.app.classic.delegate
93        assert delegate is not None
94        delegate.create_default_game_settings_ui(
95            cls, sessiontype, settings, completion_call
96        )

Launch an in-game UI to configure settings for a game type.

'sessiontype' should be the bascenev1.Session class the game will be used in.

'settings' should be an existing settings dict (implies 'edit' ui mode) or None (implies 'add' ui mode).

'completion_call' will be called with a filled-out settings dict on success or None on cancel.

Generally subclasses don't need to override this; if they override bascenev1.GameActivity.get_available_settings() and bascenev1.GameActivity.get_supported_maps() they can just rely on the default implementation here which calls those methods.

@classmethod
def getscoreconfig(cls) -> ScoreConfig:
 98    @classmethod
 99    def getscoreconfig(cls) -> bascenev1.ScoreConfig:
100        """Return info about game scoring setup; can be overridden by games."""
101        return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig()

Return info about game scoring setup; can be overridden by games.

@classmethod
def getname(cls) -> str:
103    @classmethod
104    def getname(cls) -> str:
105        """Return a str name for this game type.
106
107        This default implementation simply returns the 'name' class attr.
108        """
109        return cls.name if cls.name is not None else 'Untitled Game'

Return a str name for this game type.

This default implementation simply returns the 'name' class attr.

@classmethod
def get_display_string(cls, settings: dict | None = None) -> Lstr:
111    @classmethod
112    def get_display_string(cls, settings: dict | None = None) -> babase.Lstr:
113        """Return a descriptive name for this game/settings combo.
114
115        Subclasses should override getname(); not this.
116        """
117        name = babase.Lstr(translate=('gameNames', cls.getname()))
118
119        # A few substitutions for 'Epic', 'Solo' etc. modes.
120        # FIXME: Should provide a way for game types to define filters of
121        #  their own and should not rely on hard-coded settings names.
122        if settings is not None:
123            if 'Solo Mode' in settings and settings['Solo Mode']:
124                name = babase.Lstr(
125                    resource='soloNameFilterText', subs=[('${NAME}', name)]
126                )
127            if 'Epic Mode' in settings and settings['Epic Mode']:
128                name = babase.Lstr(
129                    resource='epicNameFilterText', subs=[('${NAME}', name)]
130                )
131
132        return name

Return a descriptive name for this game/settings combo.

Subclasses should override getname(); not this.

@classmethod
def get_team_display_string(cls, name: str) -> Lstr:
134    @classmethod
135    def get_team_display_string(cls, name: str) -> babase.Lstr:
136        """Given a team name, returns a localized version of it."""
137        return babase.Lstr(translate=('teamNames', name))

Given a team name, returns a localized version of it.

@classmethod
def get_description(cls, sessiontype: type[Session]) -> str:
139    @classmethod
140    def get_description(cls, sessiontype: type[bascenev1.Session]) -> str:
141        """Get a str description of this game type.
142
143        The default implementation simply returns the 'description' class var.
144        Classes which want to change their description depending on the session
145        can override this method.
146        """
147        del sessiontype  # Unused arg.
148        return cls.description if cls.description is not None else ''

Get a str description of this game type.

The default implementation simply returns the 'description' class var. Classes which want to change their description depending on the session can override this method.

@classmethod
def get_description_display_string( cls, sessiontype: type[Session]) -> Lstr:
150    @classmethod
151    def get_description_display_string(
152        cls, sessiontype: type[bascenev1.Session]
153    ) -> babase.Lstr:
154        """Return a translated version of get_description().
155
156        Sub-classes should override get_description(); not this.
157        """
158        description = cls.get_description(sessiontype)
159        return babase.Lstr(translate=('gameDescriptions', description))

Return a translated version of get_description().

Sub-classes should override get_description(); not this.

@classmethod
def get_available_settings( cls, sessiontype: type[Session]) -> list[Setting]:
161    @classmethod
162    def get_available_settings(
163        cls, sessiontype: type[bascenev1.Session]
164    ) -> list[bascenev1.Setting]:
165        """Return a list of settings relevant to this game type when
166        running under the provided session type.
167        """
168        del sessiontype  # Unused arg.
169        return [] if cls.available_settings is None else cls.available_settings

Return a list of settings relevant to this game type when running under the provided session type.

@classmethod
def get_supported_maps(cls, sessiontype: type[Session]) -> list[str]:
171    @classmethod
172    def get_supported_maps(
173        cls, sessiontype: type[bascenev1.Session]
174    ) -> list[str]:
175        """
176        Called by the default bascenev1.GameActivity.create_settings_ui()
177        implementation; should return a list of map names valid
178        for this game-type for the given bascenev1.Session type.
179        """
180        del sessiontype  # Unused arg.
181        assert babase.app.classic is not None
182        return babase.app.classic.getmaps('melee')

Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.

@classmethod
def get_settings_display_string(cls, config: dict[str, typing.Any]) -> Lstr:
184    @classmethod
185    def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr:
186        """Given a game config dict, return a short description for it.
187
188        This is used when viewing game-lists or showing what game
189        is up next in a series.
190        """
191        name = cls.get_display_string(config['settings'])
192
193        # In newer configs, map is in settings; it used to be in the
194        # config root.
195        if 'map' in config['settings']:
196            sval = babase.Lstr(
197                value='${NAME} @ ${MAP}',
198                subs=[
199                    ('${NAME}', name),
200                    (
201                        '${MAP}',
202                        _map.get_map_display_string(
203                            _map.get_filtered_map_name(
204                                config['settings']['map']
205                            )
206                        ),
207                    ),
208                ],
209            )
210        elif 'map' in config:
211            sval = babase.Lstr(
212                value='${NAME} @ ${MAP}',
213                subs=[
214                    ('${NAME}', name),
215                    (
216                        '${MAP}',
217                        _map.get_map_display_string(
218                            _map.get_filtered_map_name(config['map'])
219                        ),
220                    ),
221                ],
222            )
223        else:
224            print('invalid game config - expected map entry under settings')
225            sval = babase.Lstr(value='???')
226        return sval

Given a game config dict, return a short description for it.

This is used when viewing game-lists or showing what game is up next in a series.

@classmethod
def supports_session_type(cls, sessiontype: type[Session]) -> bool:
228    @classmethod
229    def supports_session_type(
230        cls, sessiontype: type[bascenev1.Session]
231    ) -> bool:
232        """Return whether this game supports the provided Session type."""
233        from bascenev1._multiteamsession import MultiTeamSession
234
235        # By default, games support any versus mode
236        return issubclass(sessiontype, MultiTeamSession)

Return whether this game supports the provided Session type.

initialplayerinfos: list[PlayerInfo] | None
map: Map
291    @property
292    def map(self) -> _map.Map:
293        """The map being used for this game.
294
295        Raises a bascenev1.MapNotFoundError if the map does not currently
296        exist.
297        """
298        if self._map is None:
299            raise babase.MapNotFoundError
300        return self._map

The map being used for this game.

Raises a bascenev1.MapNotFoundError if the map does not currently exist.

def get_instance_display_string(self) -> Lstr:
302    def get_instance_display_string(self) -> babase.Lstr:
303        """Return a name for this particular game instance."""
304        return self.get_display_string(self.settings_raw)

Return a name for this particular game instance.

def get_instance_scoreboard_display_string(self) -> Lstr:
307    def get_instance_scoreboard_display_string(self) -> babase.Lstr:
308        """Return a name for this particular game instance.
309
310        This name is used above the game scoreboard in the corner
311        of the screen, so it should be as concise as possible.
312        """
313        # If we're in a co-op session, use the level name.
314        # FIXME: Should clean this up.
315        try:
316            from bascenev1._coopsession import CoopSession
317
318            if isinstance(self.session, CoopSession):
319                campaign = self.session.campaign
320                assert campaign is not None
321                return campaign.getlevel(
322                    self.session.campaign_level_name
323                ).displayname
324        except Exception:
325            logging.exception('Error getting campaign level name.')
326        return self.get_instance_display_string()

Return a name for this particular game instance.

This name is used above the game scoreboard in the corner of the screen, so it should be as concise as possible.

def get_instance_description(self) -> Union[str, Sequence]:
328    def get_instance_description(self) -> str | Sequence:
329        """Return a description for this game instance, in English.
330
331        This is shown in the center of the screen below the game name at the
332        start of a game. It should start with a capital letter and end with a
333        period, and can be a bit more verbose than the version returned by
334        get_instance_description_short().
335
336        Note that translation is applied by looking up the specific returned
337        value as a key, so the number of returned variations should be limited;
338        ideally just one or two. To include arbitrary values in the
339        description, you can return a sequence of values in the following
340        form instead of just a string:
341
342        # This will give us something like 'Score 3 goals.' in English
343        # and can properly translate to 'Anota 3 goles.' in Spanish.
344        # If we just returned the string 'Score 3 Goals' here, there would
345        # have to be a translation entry for each specific number. ew.
346        return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
347
348        This way the first string can be consistently translated, with any arg
349        values then substituted into the result. ${ARG1} will be replaced with
350        the first value, ${ARG2} with the second, etc.
351        """
352        return self.get_description(type(self.session))

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

def get_instance_description_short(self) -> Union[str, Sequence]:
354    def get_instance_description_short(self) -> str | Sequence:
355        """Return a short description for this game instance in English.
356
357        This description is used above the game scoreboard in the
358        corner of the screen, so it should be as concise as possible.
359        It should be lowercase and should not contain periods or other
360        punctuation.
361
362        Note that translation is applied by looking up the specific returned
363        value as a key, so the number of returned variations should be limited;
364        ideally just one or two. To include arbitrary values in the
365        description, you can return a sequence of values in the following form
366        instead of just a string:
367
368        # This will give us something like 'score 3 goals' in English
369        # and can properly translate to 'anota 3 goles' in Spanish.
370        # If we just returned the string 'score 3 goals' here, there would
371        # have to be a translation entry for each specific number. ew.
372        return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
373
374        This way the first string can be consistently translated, with any arg
375        values then substituted into the result. ${ARG1} will be replaced
376        with the first value, ${ARG2} with the second, etc.
377
378        """
379        return ''

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

@override
def on_transition_in(self) -> None:
381    @override
382    def on_transition_in(self) -> None:
383        super().on_transition_in()
384
385        # Make our map.
386        self._map = self._map_type()
387
388        # Give our map a chance to override the music.
389        # (for happy-thoughts and other such themed maps)
390        map_music = self._map_type.get_music_type()
391        music = map_music if map_music is not None else self.default_music
392
393        if music is not None:
394            _music.setmusic(music)

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 bascenev1.Activity.on_begin() is called.

def on_continue(self) -> None:
396    def on_continue(self) -> None:
397        """
398        This is called if a game supports and offers a continue and the player
399        accepts.  In this case the player should be given an extra life or
400        whatever is relevant to keep the game going.
401        """

This is called if a game supports and offers a continue and the player accepts. In this case the player should be given an extra life or whatever is relevant to keep the game going.

def is_waiting_for_continue(self) -> bool:
426    def is_waiting_for_continue(self) -> bool:
427        """Returns whether or not this activity is currently waiting for the
428        player to continue (or timeout)"""
429        return self._is_waiting_for_continue

Returns whether or not this activity is currently waiting for the player to continue (or timeout)

def continue_or_end_game(self) -> None:
431    def continue_or_end_game(self) -> None:
432        """If continues are allowed, prompts the player to purchase a continue
433        and calls either end_game or continue_game depending on the result
434        """
435        # pylint: disable=too-many-nested-blocks
436        # pylint: disable=cyclic-import
437        from bascenev1._coopsession import CoopSession
438
439        classic = babase.app.classic
440        assert classic is not None
441        continues_window = classic.continues_window
442
443        # Turning these off. I want to migrate towards monetization that
444        # feels less pay-to-win-ish.
445        allow_continues = False
446
447        plus = babase.app.plus
448        try:
449            if (
450                plus is not None
451                and plus.get_v1_account_misc_read_val('enableContinues', False)
452                and allow_continues
453            ):
454                session = self.session
455
456                # We only support continuing in non-tournament games.
457                tournament_id = session.tournament_id
458                if tournament_id is None:
459                    # We currently only support continuing in sequential
460                    # co-op campaigns.
461                    if isinstance(session, CoopSession):
462                        assert session.campaign is not None
463                        if session.campaign.sequential:
464                            gnode = self.globalsnode
465
466                            # Only attempt this if we're not currently paused
467                            # and there appears to be no UI.
468                            assert babase.app.classic is not None
469                            hmmw = babase.app.ui_v1.has_main_menu_window()
470                            if not gnode.paused and not hmmw:
471                                self._is_waiting_for_continue = True
472                                with babase.ContextRef.empty():
473                                    babase.apptimer(
474                                        0.5,
475                                        lambda: continues_window(
476                                            self,
477                                            self._continue_cost,
478                                            continue_call=babase.WeakCall(
479                                                self._continue_choice, True
480                                            ),
481                                            cancel_call=babase.WeakCall(
482                                                self._continue_choice, False
483                                            ),
484                                        ),
485                                    )
486                                return
487
488        except Exception:
489            logging.exception('Error handling continues.')
490
491        self.end_game()

If continues are allowed, prompts the player to purchase a continue and calls either end_game or continue_game depending on the result

@override
def on_begin(self) -> None:
493    @override
494    def on_begin(self) -> None:
495        super().on_begin()
496
497        if babase.app.classic is not None:
498            babase.app.classic.game_begin_analytics()
499
500        # We don't do this in on_transition_in because it may depend on
501        # players/teams which aren't available until now.
502        _bascenev1.timer(0.001, self._show_scoreboard_info)
503        _bascenev1.timer(1.0, self._show_info)
504        _bascenev1.timer(2.5, self._show_tip)
505
506        # Store some basic info about players present at start time.
507        self.initialplayerinfos = [
508            PlayerInfo(name=p.getname(full=True), character=p.character)
509            for p in self.players
510        ]
511
512        # Sort this by name so high score lists/etc will be consistent
513        # regardless of player join order.
514        self.initialplayerinfos.sort(key=lambda x: x.name)
515
516        # If this is a tournament, query info about it such as how much
517        # time is left.
518        tournament_id = self.session.tournament_id
519        if tournament_id is not None:
520            assert babase.app.plus is not None
521            babase.app.plus.tournament_query(
522                args={
523                    'tournamentIDs': [tournament_id],
524                    'source': 'in-game time remaining query',
525                },
526                callback=babase.WeakCall(self._on_tournament_query_response),
527            )

Called once the previous 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.

@override
def on_player_join(self, player: ~PlayerT) -> None:
542    @override
543    def on_player_join(self, player: PlayerT) -> None:
544        super().on_player_join(player)
545
546        # By default, just spawn a dude.
547        self.spawn_player(player)

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

(including the initial set of Players)

@override
def handlemessage(self, msg: Any) -> Any:
549    @override
550    def handlemessage(self, msg: Any) -> Any:
551        if isinstance(msg, PlayerDiedMessage):
552            # pylint: disable=cyclic-import
553            from bascenev1lib.actor.spaz import Spaz
554
555            player = msg.getplayer(self.playertype)
556            killer = msg.getkillerplayer(self.playertype)
557
558            # Inform our stats of the demise.
559            self.stats.player_was_killed(
560                player, killed=msg.killed, killer=killer
561            )
562
563            # Award the killer points if he's on a different team.
564            # FIXME: This should not be linked to Spaz actors.
565            # (should move get_death_points to Actor or make it a message)
566            if killer and killer.team is not player.team:
567                assert isinstance(killer.actor, Spaz)
568                pts, importance = killer.actor.get_death_points(msg.how)
569                if not self.has_ended():
570                    self.stats.player_scored(
571                        killer,
572                        pts,
573                        kill=True,
574                        victim_player=player,
575                        importance=importance,
576                        showpoints=self.show_kill_points,
577                    )
578        else:
579            return super().handlemessage(msg)
580        return None

General message handling; can be passed any message object.

@override
def end( self, results: Any = None, delay: float = 0.0, force: bool = False) -> None:
843    @override
844    def end(
845        self, results: Any = None, delay: float = 0.0, force: bool = False
846    ) -> None:
847        from bascenev1._gameresults import GameResults
848
849        # If results is a standard team-game-results, associate it with us
850        # so it can grab our score prefs.
851        if isinstance(results, GameResults):
852            results.set_game(self)
853
854        # If we had a standard time-limit that had not expired, stop it so
855        # it doesnt tick annoyingly.
856        if (
857            self._standard_time_limit_time is not None
858            and self._standard_time_limit_time > 0
859        ):
860            self._standard_time_limit_timer = None
861            self._standard_time_limit_text = None
862
863        # Ditto with tournament time limits.
864        if (
865            self._tournament_time_limit is not None
866            and self._tournament_time_limit > 0
867        ):
868            self._tournament_time_limit_timer = None
869            self._tournament_time_limit_text = None
870            self._tournament_time_limit_title_text = None
871
872        super().end(results, delay, force)

Commences Activity shutdown and delivers results to the Session.

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

def end_game(self) -> None:
874    def end_game(self) -> None:
875        """Tell the game to wrap up and call bascenev1.Activity.end().
876
877        This method should be overridden by subclasses. A game should always
878        be prepared to end and deliver results, even if there is no 'winner'
879        yet; this way things like the standard time-limit
880        (bascenev1.GameActivity.setup_standard_time_limit()) will work with
881        the game.
882        """
883        print(
884            'WARNING: default end_game() implementation called;'
885            ' your game should override this.'
886        )

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.

def respawn_player(self, player: ~PlayerT, respawn_time: float | None = None) -> None:
888    def respawn_player(
889        self, player: PlayerT, respawn_time: float | None = None
890    ) -> None:
891        """
892        Given a bascenev1.Player, sets up a standard respawn timer,
893        along with the standard counter display, etc.
894        At the end of the respawn period spawn_player() will
895        be called if the Player still exists.
896        An explicit 'respawn_time' can optionally be provided
897        (in seconds).
898        """
899        # pylint: disable=cyclic-import
900
901        assert player
902        if respawn_time is None:
903            teamsize = len(player.team.players)
904            if teamsize == 1:
905                respawn_time = 3.0
906            elif teamsize == 2:
907                respawn_time = 5.0
908            elif teamsize == 3:
909                respawn_time = 6.0
910            else:
911                respawn_time = 7.0
912
913        # If this standard setting is present, factor it in.
914        if 'Respawn Times' in self.settings_raw:
915            respawn_time *= self.settings_raw['Respawn Times']
916
917        # We want whole seconds.
918        assert respawn_time is not None
919        respawn_time = round(max(1.0, respawn_time), 0)
920
921        if player.actor and not self.has_ended():
922            from bascenev1lib.actor.respawnicon import RespawnIcon
923
924            player.customdata['respawn_timer'] = _bascenev1.Timer(
925                respawn_time,
926                babase.WeakCall(self.spawn_player_if_exists, player),
927            )
928            player.customdata['respawn_icon'] = RespawnIcon(
929                player, respawn_time
930            )

Given a bascenev1.Player, sets up a standard respawn timer, along with the standard counter display, etc. At the end of the respawn period spawn_player() will be called if the Player still exists. An explicit 'respawn_time' can optionally be provided (in seconds).

def spawn_player_if_exists(self, player: ~PlayerT) -> None:
932    def spawn_player_if_exists(self, player: PlayerT) -> None:
933        """
934        A utility method which calls self.spawn_player() *only* if the
935        bascenev1.Player provided still exists; handy for use in timers
936        and whatnot.
937
938        There is no need to override this; just override spawn_player().
939        """
940        if player:
941            self.spawn_player(player)

A utility method which calls self.spawn_player() only if the bascenev1.Player provided still exists; handy for use in timers and whatnot.

There is no need to override this; just override spawn_player().

def spawn_player(self, player: ~PlayerT) -> Actor:
943    def spawn_player(self, player: PlayerT) -> bascenev1.Actor:
944        """Spawn *something* for the provided bascenev1.Player.
945
946        The default implementation simply calls spawn_player_spaz().
947        """
948        assert player  # Dead references should never be passed as args.
949
950        return self.spawn_player_spaz(player)

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

def spawn_player_spaz( self, player: ~PlayerT, position: Sequence[float] = (0, 0, 0), angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
 952    def spawn_player_spaz(
 953        self,
 954        player: PlayerT,
 955        position: Sequence[float] = (0, 0, 0),
 956        angle: float | None = None,
 957    ) -> PlayerSpaz:
 958        """Create and wire up a bascenev1.PlayerSpaz for the provided Player."""
 959        # pylint: disable=too-many-locals
 960        # pylint: disable=cyclic-import
 961        from bascenev1._gameutils import animate
 962        from bascenev1._coopsession import CoopSession
 963        from bascenev1lib.actor.playerspaz import PlayerSpaz
 964
 965        name = player.getname()
 966        color = player.color
 967        highlight = player.highlight
 968
 969        playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz)
 970        if not issubclass(playerspaztype, PlayerSpaz):
 971            playerspaztype = PlayerSpaz
 972
 973        light_color = babase.normalized_color(color)
 974        display_color = babase.safecolor(color, target_intensity=0.75)
 975        spaz = playerspaztype(
 976            color=color,
 977            highlight=highlight,
 978            character=player.character,
 979            player=player,
 980        )
 981
 982        player.actor = spaz
 983        assert spaz.node
 984
 985        # If this is co-op and we're on Courtyard or Runaround, add the
 986        # material that allows us to collide with the player-walls.
 987        # FIXME: Need to generalize this.
 988        if isinstance(self.session, CoopSession) and self.map.getname() in [
 989            'Courtyard',
 990            'Tower D',
 991        ]:
 992            mat = self.map.preloaddata['collide_with_wall_material']
 993            assert isinstance(spaz.node.materials, tuple)
 994            assert isinstance(spaz.node.roller_materials, tuple)
 995            spaz.node.materials += (mat,)
 996            spaz.node.roller_materials += (mat,)
 997
 998        spaz.node.name = name
 999        spaz.node.name_color = display_color
1000        spaz.connect_controls_to_player()
1001
1002        # Move to the stand position and add a flash of light.
1003        spaz.handlemessage(
1004            StandMessage(
1005                position, angle if angle is not None else random.uniform(0, 360)
1006            )
1007        )
1008        self._spawn_sound.play(1, position=spaz.node.position)
1009        light = _bascenev1.newnode('light', attrs={'color': light_color})
1010        spaz.node.connectattr('position', light, 'position')
1011        animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
1012        _bascenev1.timer(0.5, light.delete)
1013        return spaz

Create and wire up a bascenev1.PlayerSpaz for the provided Player.

def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
1015    def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
1016        """Create standard powerup drops for the current map."""
1017        # pylint: disable=cyclic-import
1018        from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL
1019
1020        self._powerup_drop_timer = _bascenev1.Timer(
1021            DEFAULT_POWERUP_INTERVAL,
1022            babase.WeakCall(self._standard_drop_powerups),
1023            repeat=True,
1024        )
1025        self._standard_drop_powerups()
1026        if enable_tnt:
1027            self._tnt_spawners = {}
1028            self._setup_standard_tnt_drops()

Create standard powerup drops for the current map.

def setup_standard_time_limit(self, duration: float) -> None:
1060    def setup_standard_time_limit(self, duration: float) -> None:
1061        """
1062        Create a standard game time-limit given the provided
1063        duration in seconds.
1064        This will be displayed at the top of the screen.
1065        If the time-limit expires, end_game() will be called.
1066        """
1067        from bascenev1._nodeactor import NodeActor
1068
1069        if duration <= 0.0:
1070            return
1071        self._standard_time_limit_time = int(duration)
1072        self._standard_time_limit_timer = _bascenev1.Timer(
1073            1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True
1074        )
1075        self._standard_time_limit_text = NodeActor(
1076            _bascenev1.newnode(
1077                'text',
1078                attrs={
1079                    'v_attach': 'top',
1080                    'h_attach': 'center',
1081                    'h_align': 'left',
1082                    'color': (1.0, 1.0, 1.0, 0.5),
1083                    'position': (-25, -30),
1084                    'flatness': 1.0,
1085                    'scale': 0.9,
1086                },
1087            )
1088        )
1089        self._standard_time_limit_text_input = NodeActor(
1090            _bascenev1.newnode(
1091                'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0}
1092            )
1093        )
1094        self.globalsnode.connectattr(
1095            'time', self._standard_time_limit_text_input.node, 'time1'
1096        )
1097        assert self._standard_time_limit_text_input.node
1098        assert self._standard_time_limit_text.node
1099        self._standard_time_limit_text_input.node.connectattr(
1100            'output', self._standard_time_limit_text.node, 'text'
1101        )

Create a standard game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called.

def show_zoom_message( self, message: Lstr, color: Sequence[float] = (0.9, 0.4, 0.0), scale: float = 0.8, duration: float = 2.0, trail: bool = False) -> None:
1278    def show_zoom_message(
1279        self,
1280        message: babase.Lstr,
1281        color: Sequence[float] = (0.9, 0.4, 0.0),
1282        scale: float = 0.8,
1283        duration: float = 2.0,
1284        trail: bool = False,
1285    ) -> None:
1286        """Zooming text used to announce game names and winners."""
1287        # pylint: disable=cyclic-import
1288        from bascenev1lib.actor.zoomtext import ZoomText
1289
1290        # Reserve a spot on the screen (in case we get multiple of these so
1291        # they don't overlap).
1292        i = 0
1293        cur_time = babase.apptime()
1294        while True:
1295            if (
1296                i not in self._zoom_message_times
1297                or self._zoom_message_times[i] < cur_time
1298            ):
1299                self._zoom_message_times[i] = cur_time + duration
1300                break
1301            i += 1
1302        ZoomText(
1303            message,
1304            lifespan=duration,
1305            jitter=2.0,
1306            position=(0, 200 - i * 100),
1307            scale=scale,
1308            maxwidth=800,
1309            trail=trail,
1310            color=color,
1311        ).autoretain()

Zooming text used to announce game names and winners.

class GameResults:
 31class GameResults:
 32    """
 33    Results for a completed game.
 34
 35    Category: **Gameplay Classes**
 36
 37    Upon completion, a game should fill one of these out and pass it to its
 38    bascenev1.Activity.end call.
 39    """
 40
 41    def __init__(self) -> None:
 42        self._game_set = False
 43        self._scores: dict[
 44            int, tuple[weakref.ref[bascenev1.SessionTeam], int | None]
 45        ] = {}
 46        self._sessionteams: list[weakref.ref[bascenev1.SessionTeam]] | None = (
 47            None
 48        )
 49        self._playerinfos: list[bascenev1.PlayerInfo] | None = None
 50        self._lower_is_better: bool | None = None
 51        self._score_label: str | None = None
 52        self._none_is_winner: bool | None = None
 53        self._scoretype: bascenev1.ScoreType | None = None
 54
 55    def set_game(self, game: bascenev1.GameActivity) -> None:
 56        """Set the game instance these results are applying to."""
 57        if self._game_set:
 58            raise RuntimeError('Game set twice for GameResults.')
 59        self._game_set = True
 60        self._sessionteams = [
 61            weakref.ref(team.sessionteam) for team in game.teams
 62        ]
 63        scoreconfig = game.getscoreconfig()
 64        self._playerinfos = copy.deepcopy(game.initialplayerinfos)
 65        self._lower_is_better = scoreconfig.lower_is_better
 66        self._score_label = scoreconfig.label
 67        self._none_is_winner = scoreconfig.none_is_winner
 68        self._scoretype = scoreconfig.scoretype
 69
 70    def set_team_score(self, team: bascenev1.Team, score: int | None) -> None:
 71        """Set the score for a given team.
 72
 73        This can be a number or None.
 74        (see the none_is_winner arg in the constructor)
 75        """
 76        assert isinstance(team, Team)
 77        sessionteam = team.sessionteam
 78        self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
 79
 80    def get_sessionteam_score(
 81        self, sessionteam: bascenev1.SessionTeam
 82    ) -> int | None:
 83        """Return the score for a given bascenev1.SessionTeam."""
 84        assert isinstance(sessionteam, SessionTeam)
 85        for score in list(self._scores.values()):
 86            if score[0]() is sessionteam:
 87                return score[1]
 88
 89        # If we have no score value, assume None.
 90        return None
 91
 92    @property
 93    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 94        """Return all bascenev1.SessionTeams in the results."""
 95        if not self._game_set:
 96            raise RuntimeError("Can't get teams until game is set.")
 97        teams = []
 98        assert self._sessionteams is not None
 99        for team_ref in self._sessionteams:
100            team = team_ref()
101            if team is not None:
102                teams.append(team)
103        return teams
104
105    def has_score_for_sessionteam(
106        self, sessionteam: bascenev1.SessionTeam
107    ) -> bool:
108        """Return whether there is a score for a given session-team."""
109        return any(s[0]() is sessionteam for s in self._scores.values())
110
111    def get_sessionteam_score_str(
112        self, sessionteam: bascenev1.SessionTeam
113    ) -> babase.Lstr:
114        """Return the score for the given session-team as an Lstr.
115
116        (properly formatted for the score type.)
117        """
118        from bascenev1._score import ScoreType
119
120        if not self._game_set:
121            raise RuntimeError("Can't get team-score-str until game is set.")
122        for score in list(self._scores.values()):
123            if score[0]() is sessionteam:
124                if score[1] is None:
125                    return babase.Lstr(value='-')
126                if self._scoretype is ScoreType.SECONDS:
127                    return babase.timestring(score[1], centi=False)
128                if self._scoretype is ScoreType.MILLISECONDS:
129                    return babase.timestring(score[1] / 1000.0, centi=True)
130                return babase.Lstr(value=str(score[1]))
131        return babase.Lstr(value='-')
132
133    @property
134    def playerinfos(self) -> list[bascenev1.PlayerInfo]:
135        """Get info about the players represented by the results."""
136        if not self._game_set:
137            raise RuntimeError("Can't get player-info until game is set.")
138        assert self._playerinfos is not None
139        return self._playerinfos
140
141    @property
142    def scoretype(self) -> bascenev1.ScoreType:
143        """The type of score."""
144        if not self._game_set:
145            raise RuntimeError("Can't get score-type until game is set.")
146        assert self._scoretype is not None
147        return self._scoretype
148
149    @property
150    def score_label(self) -> str:
151        """The label associated with scores ('points', etc)."""
152        if not self._game_set:
153            raise RuntimeError("Can't get score-label until game is set.")
154        assert self._score_label is not None
155        return self._score_label
156
157    @property
158    def lower_is_better(self) -> bool:
159        """Whether lower scores are better."""
160        if not self._game_set:
161            raise RuntimeError("Can't get lower-is-better until game is set.")
162        assert self._lower_is_better is not None
163        return self._lower_is_better
164
165    @property
166    def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
167        """The winning SessionTeam if there is exactly one, or else None."""
168        if not self._game_set:
169            raise RuntimeError("Can't get winners until game is set.")
170        winners = self.winnergroups
171        if winners and len(winners[0].teams) == 1:
172            return winners[0].teams[0]
173        return None
174
175    @property
176    def winnergroups(self) -> list[WinnerGroup]:
177        """Get an ordered list of winner groups."""
178        if not self._game_set:
179            raise RuntimeError("Can't get winners until game is set.")
180
181        # Group by best scoring teams.
182        winners: dict[int, list[bascenev1.SessionTeam]] = {}
183        scores = [
184            score
185            for score in self._scores.values()
186            if score[0]() is not None and score[1] is not None
187        ]
188        for score in scores:
189            assert score[1] is not None
190            sval = winners.setdefault(score[1], [])
191            team = score[0]()
192            assert team is not None
193            sval.append(team)
194        results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list(
195            winners.items()
196        )
197        results.sort(
198            reverse=not self._lower_is_better,
199            key=lambda x: asserttype(x[0], int),
200        )
201
202        # Also group the 'None' scores.
203        none_sessionteams: list[bascenev1.SessionTeam] = []
204        for score in self._scores.values():
205            scoreteam = score[0]()
206            if scoreteam is not None and score[1] is None:
207                none_sessionteams.append(scoreteam)
208
209        # Add the Nones to the list (either as winners or losers
210        # depending on the rules).
211        if none_sessionteams:
212            nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [
213                (None, none_sessionteams)
214            ]
215            if self._none_is_winner:
216                results = nones + results
217            else:
218                results = results + nones
219
220        return [WinnerGroup(score, team) for score, team in results]

Results for a completed game.

Category: Gameplay Classes

Upon completion, a game should fill one of these out and pass it to its bascenev1.Activity.end call.

def set_game(self, game: GameActivity) -> None:
55    def set_game(self, game: bascenev1.GameActivity) -> None:
56        """Set the game instance these results are applying to."""
57        if self._game_set:
58            raise RuntimeError('Game set twice for GameResults.')
59        self._game_set = True
60        self._sessionteams = [
61            weakref.ref(team.sessionteam) for team in game.teams
62        ]
63        scoreconfig = game.getscoreconfig()
64        self._playerinfos = copy.deepcopy(game.initialplayerinfos)
65        self._lower_is_better = scoreconfig.lower_is_better
66        self._score_label = scoreconfig.label
67        self._none_is_winner = scoreconfig.none_is_winner
68        self._scoretype = scoreconfig.scoretype

Set the game instance these results are applying to.

def set_team_score(self, team: Team, score: int | None) -> None:
70    def set_team_score(self, team: bascenev1.Team, score: int | None) -> None:
71        """Set the score for a given team.
72
73        This can be a number or None.
74        (see the none_is_winner arg in the constructor)
75        """
76        assert isinstance(team, Team)
77        sessionteam = team.sessionteam
78        self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)

Set the score for a given team.

This can be a number or None. (see the none_is_winner arg in the constructor)

def get_sessionteam_score(self, sessionteam: SessionTeam) -> int | None:
80    def get_sessionteam_score(
81        self, sessionteam: bascenev1.SessionTeam
82    ) -> int | None:
83        """Return the score for a given bascenev1.SessionTeam."""
84        assert isinstance(sessionteam, SessionTeam)
85        for score in list(self._scores.values()):
86            if score[0]() is sessionteam:
87                return score[1]
88
89        # If we have no score value, assume None.
90        return None

Return the score for a given bascenev1.SessionTeam.

sessionteams: list[SessionTeam]
 92    @property
 93    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 94        """Return all bascenev1.SessionTeams in the results."""
 95        if not self._game_set:
 96            raise RuntimeError("Can't get teams until game is set.")
 97        teams = []
 98        assert self._sessionteams is not None
 99        for team_ref in self._sessionteams:
100            team = team_ref()
101            if team is not None:
102                teams.append(team)
103        return teams

Return all bascenev1.SessionTeams in the results.

def has_score_for_sessionteam(self, sessionteam: SessionTeam) -> bool:
105    def has_score_for_sessionteam(
106        self, sessionteam: bascenev1.SessionTeam
107    ) -> bool:
108        """Return whether there is a score for a given session-team."""
109        return any(s[0]() is sessionteam for s in self._scores.values())

Return whether there is a score for a given session-team.

def get_sessionteam_score_str(self, sessionteam: SessionTeam) -> Lstr:
111    def get_sessionteam_score_str(
112        self, sessionteam: bascenev1.SessionTeam
113    ) -> babase.Lstr:
114        """Return the score for the given session-team as an Lstr.
115
116        (properly formatted for the score type.)
117        """
118        from bascenev1._score import ScoreType
119
120        if not self._game_set:
121            raise RuntimeError("Can't get team-score-str until game is set.")
122        for score in list(self._scores.values()):
123            if score[0]() is sessionteam:
124                if score[1] is None:
125                    return babase.Lstr(value='-')
126                if self._scoretype is ScoreType.SECONDS:
127                    return babase.timestring(score[1], centi=False)
128                if self._scoretype is ScoreType.MILLISECONDS:
129                    return babase.timestring(score[1] / 1000.0, centi=True)
130                return babase.Lstr(value=str(score[1]))
131        return babase.Lstr(value='-')

Return the score for the given session-team as an Lstr.

(properly formatted for the score type.)

playerinfos: list[PlayerInfo]
133    @property
134    def playerinfos(self) -> list[bascenev1.PlayerInfo]:
135        """Get info about the players represented by the results."""
136        if not self._game_set:
137            raise RuntimeError("Can't get player-info until game is set.")
138        assert self._playerinfos is not None
139        return self._playerinfos

Get info about the players represented by the results.

scoretype: ScoreType
141    @property
142    def scoretype(self) -> bascenev1.ScoreType:
143        """The type of score."""
144        if not self._game_set:
145            raise RuntimeError("Can't get score-type until game is set.")
146        assert self._scoretype is not None
147        return self._scoretype

The type of score.

score_label: str
149    @property
150    def score_label(self) -> str:
151        """The label associated with scores ('points', etc)."""
152        if not self._game_set:
153            raise RuntimeError("Can't get score-label until game is set.")
154        assert self._score_label is not None
155        return self._score_label

The label associated with scores ('points', etc).

lower_is_better: bool
157    @property
158    def lower_is_better(self) -> bool:
159        """Whether lower scores are better."""
160        if not self._game_set:
161            raise RuntimeError("Can't get lower-is-better until game is set.")
162        assert self._lower_is_better is not None
163        return self._lower_is_better

Whether lower scores are better.

winning_sessionteam: SessionTeam | None
165    @property
166    def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
167        """The winning SessionTeam if there is exactly one, or else None."""
168        if not self._game_set:
169            raise RuntimeError("Can't get winners until game is set.")
170        winners = self.winnergroups
171        if winners and len(winners[0].teams) == 1:
172            return winners[0].teams[0]
173        return None

The winning SessionTeam if there is exactly one, or else None.

winnergroups: list[bascenev1._gameresults.WinnerGroup]
175    @property
176    def winnergroups(self) -> list[WinnerGroup]:
177        """Get an ordered list of winner groups."""
178        if not self._game_set:
179            raise RuntimeError("Can't get winners until game is set.")
180
181        # Group by best scoring teams.
182        winners: dict[int, list[bascenev1.SessionTeam]] = {}
183        scores = [
184            score
185            for score in self._scores.values()
186            if score[0]() is not None and score[1] is not None
187        ]
188        for score in scores:
189            assert score[1] is not None
190            sval = winners.setdefault(score[1], [])
191            team = score[0]()
192            assert team is not None
193            sval.append(team)
194        results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list(
195            winners.items()
196        )
197        results.sort(
198            reverse=not self._lower_is_better,
199            key=lambda x: asserttype(x[0], int),
200        )
201
202        # Also group the 'None' scores.
203        none_sessionteams: list[bascenev1.SessionTeam] = []
204        for score in self._scores.values():
205            scoreteam = score[0]()
206            if scoreteam is not None and score[1] is None:
207                none_sessionteams.append(scoreteam)
208
209        # Add the Nones to the list (either as winners or losers
210        # depending on the rules).
211        if none_sessionteams:
212            nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [
213                (None, none_sessionteams)
214            ]
215            if self._none_is_winner:
216                results = nones + results
217            else:
218                results = results + nones
219
220        return [WinnerGroup(score, team) for score, team in results]

Get an ordered list of winner groups.

@dataclass
class GameTip:
32@dataclass
33class GameTip:
34    """Defines a tip presentable to the user at the start of a game.
35
36    Category: **Gameplay Classes**
37    """
38
39    text: str
40    icon: bascenev1.Texture | None = None
41    sound: bascenev1.Sound | None = None

Defines a tip presentable to the user at the start of a game.

Category: Gameplay Classes

GameTip( text: str, icon: Texture | None = None, sound: Sound | None = None)
text: str
icon: Texture | None = None
sound: Sound | None = None
def get_connection_to_host_info_2() -> HostInfo | None:
1167def get_connection_to_host_info_2() -> bascenev1.HostInfo | None:
1168    """Return info about the host we are currently connected to."""
1169    import bascenev1  # pylint: disable=cyclic-import
1170
1171    return bascenev1.HostInfo('dummyname', -1, 'dummy_addr', -1)

Return info about the host we are currently connected to.

def get_default_free_for_all_playlist() -> list[dict[str, typing.Any]]:
225def get_default_free_for_all_playlist() -> PlaylistType:
226    """Return a default playlist for free-for-all mode."""
227
228    # NOTE: these are currently using old type/map names,
229    # but filtering translates them properly to the new ones.
230    # (is kinda a handy way to ensure filtering is working).
231    # Eventually should update these though.
232    return [
233        {
234            'settings': {
235                'Epic Mode': False,
236                'Kills to Win Per Player': 10,
237                'Respawn Times': 1.0,
238                'Time Limit': 300,
239                'map': 'Doom Shroom',
240            },
241            'type': 'bs_death_match.DeathMatchGame',
242        },
243        {
244            'settings': {
245                'Chosen One Gets Gloves': True,
246                'Chosen One Gets Shield': False,
247                'Chosen One Time': 30,
248                'Epic Mode': 0,
249                'Respawn Times': 1.0,
250                'Time Limit': 300,
251                'map': 'Monkey Face',
252            },
253            'type': 'bs_chosen_one.ChosenOneGame',
254        },
255        {
256            'settings': {
257                'Hold Time': 30,
258                'Respawn Times': 1.0,
259                'Time Limit': 300,
260                'map': 'Zigzag',
261            },
262            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
263        },
264        {
265            'settings': {'Epic Mode': False, 'map': 'Rampage'},
266            'type': 'bs_meteor_shower.MeteorShowerGame',
267        },
268        {
269            'settings': {
270                'Epic Mode': 1,
271                'Lives Per Player': 1,
272                'Respawn Times': 1.0,
273                'Time Limit': 120,
274                'map': 'Tip Top',
275            },
276            'type': 'bs_elimination.EliminationGame',
277        },
278        {
279            'settings': {
280                'Hold Time': 30,
281                'Respawn Times': 1.0,
282                'Time Limit': 300,
283                'map': 'The Pad',
284            },
285            'type': 'bs_keep_away.KeepAwayGame',
286        },
287        {
288            'settings': {
289                'Epic Mode': True,
290                'Kills to Win Per Player': 10,
291                'Respawn Times': 0.25,
292                'Time Limit': 120,
293                'map': 'Rampage',
294            },
295            'type': 'bs_death_match.DeathMatchGame',
296        },
297        {
298            'settings': {
299                'Bomb Spawning': 1000,
300                'Epic Mode': False,
301                'Laps': 3,
302                'Mine Spawn Interval': 4000,
303                'Mine Spawning': 4000,
304                'Time Limit': 300,
305                'map': 'Big G',
306            },
307            'type': 'bs_race.RaceGame',
308        },
309        {
310            'settings': {
311                'Hold Time': 30,
312                'Respawn Times': 1.0,
313                'Time Limit': 300,
314                'map': 'Happy Thoughts',
315            },
316            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
317        },
318        {
319            'settings': {
320                'Enable Impact Bombs': 1,
321                'Enable Triple Bombs': False,
322                'Target Count': 2,
323                'map': 'Doom Shroom',
324            },
325            'type': 'bs_target_practice.TargetPracticeGame',
326        },
327        {
328            'settings': {
329                'Epic Mode': False,
330                'Lives Per Player': 5,
331                'Respawn Times': 1.0,
332                'Time Limit': 300,
333                'map': 'Step Right Up',
334            },
335            'type': 'bs_elimination.EliminationGame',
336        },
337        {
338            'settings': {
339                'Epic Mode': False,
340                'Kills to Win Per Player': 10,
341                'Respawn Times': 1.0,
342                'Time Limit': 300,
343                'map': 'Crag Castle',
344            },
345            'type': 'bs_death_match.DeathMatchGame',
346        },
347        {
348            'map': 'Lake Frigid',
349            'settings': {
350                'Bomb Spawning': 0,
351                'Epic Mode': False,
352                'Laps': 6,
353                'Mine Spawning': 2000,
354                'Time Limit': 300,
355                'map': 'Lake Frigid',
356            },
357            'type': 'bs_race.RaceGame',
358        },
359    ]

Return a default playlist for free-for-all mode.

def get_default_teams_playlist() -> list[dict[str, typing.Any]]:
362def get_default_teams_playlist() -> PlaylistType:
363    """Return a default playlist for teams mode."""
364
365    # NOTE: these are currently using old type/map names,
366    # but filtering translates them properly to the new ones.
367    # (is kinda a handy way to ensure filtering is working).
368    # Eventually should update these though.
369    return [
370        {
371            'settings': {
372                'Epic Mode': False,
373                'Flag Idle Return Time': 30,
374                'Flag Touch Return Time': 0,
375                'Respawn Times': 1.0,
376                'Score to Win': 3,
377                'Time Limit': 600,
378                'map': 'Bridgit',
379            },
380            'type': 'bs_capture_the_flag.CTFGame',
381        },
382        {
383            'settings': {
384                'Epic Mode': False,
385                'Respawn Times': 1.0,
386                'Score to Win': 3,
387                'Time Limit': 600,
388                'map': 'Step Right Up',
389            },
390            'type': 'bs_assault.AssaultGame',
391        },
392        {
393            'settings': {
394                'Balance Total Lives': False,
395                'Epic Mode': False,
396                'Lives Per Player': 3,
397                'Respawn Times': 1.0,
398                'Solo Mode': True,
399                'Time Limit': 600,
400                'map': 'Rampage',
401            },
402            'type': 'bs_elimination.EliminationGame',
403        },
404        {
405            'settings': {
406                'Epic Mode': False,
407                'Kills to Win Per Player': 5,
408                'Respawn Times': 1.0,
409                'Time Limit': 300,
410                'map': 'Roundabout',
411            },
412            'type': 'bs_death_match.DeathMatchGame',
413        },
414        {
415            'settings': {
416                'Respawn Times': 1.0,
417                'Score to Win': 1,
418                'Time Limit': 600,
419                'map': 'Hockey Stadium',
420            },
421            'type': 'bs_hockey.HockeyGame',
422        },
423        {
424            'settings': {
425                'Hold Time': 30,
426                'Respawn Times': 1.0,
427                'Time Limit': 300,
428                'map': 'Monkey Face',
429            },
430            'type': 'bs_keep_away.KeepAwayGame',
431        },
432        {
433            'settings': {
434                'Balance Total Lives': False,
435                'Epic Mode': True,
436                'Lives Per Player': 1,
437                'Respawn Times': 1.0,
438                'Solo Mode': False,
439                'Time Limit': 120,
440                'map': 'Tip Top',
441            },
442            'type': 'bs_elimination.EliminationGame',
443        },
444        {
445            'settings': {
446                'Epic Mode': False,
447                'Respawn Times': 1.0,
448                'Score to Win': 3,
449                'Time Limit': 300,
450                'map': 'Crag Castle',
451            },
452            'type': 'bs_assault.AssaultGame',
453        },
454        {
455            'settings': {
456                'Epic Mode': False,
457                'Kills to Win Per Player': 5,
458                'Respawn Times': 1.0,
459                'Time Limit': 300,
460                'map': 'Doom Shroom',
461            },
462            'type': 'bs_death_match.DeathMatchGame',
463        },
464        {
465            'settings': {'Epic Mode': False, 'map': 'Rampage'},
466            'type': 'bs_meteor_shower.MeteorShowerGame',
467        },
468        {
469            'settings': {
470                'Epic Mode': False,
471                'Flag Idle Return Time': 30,
472                'Flag Touch Return Time': 0,
473                'Respawn Times': 1.0,
474                'Score to Win': 2,
475                'Time Limit': 600,
476                'map': 'Roundabout',
477            },
478            'type': 'bs_capture_the_flag.CTFGame',
479        },
480        {
481            'settings': {
482                'Respawn Times': 1.0,
483                'Score to Win': 21,
484                'Time Limit': 600,
485                'map': 'Football Stadium',
486            },
487            'type': 'bs_football.FootballTeamGame',
488        },
489        {
490            'settings': {
491                'Epic Mode': True,
492                'Respawn Times': 0.25,
493                'Score to Win': 3,
494                'Time Limit': 120,
495                'map': 'Bridgit',
496            },
497            'type': 'bs_assault.AssaultGame',
498        },
499        {
500            'map': 'Doom Shroom',
501            'settings': {
502                'Enable Impact Bombs': 1,
503                'Enable Triple Bombs': False,
504                'Target Count': 2,
505                'map': 'Doom Shroom',
506            },
507            'type': 'bs_target_practice.TargetPracticeGame',
508        },
509        {
510            'settings': {
511                'Hold Time': 30,
512                'Respawn Times': 1.0,
513                'Time Limit': 300,
514                'map': 'Tip Top',
515            },
516            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
517        },
518        {
519            'settings': {
520                'Epic Mode': False,
521                'Respawn Times': 1.0,
522                'Score to Win': 2,
523                'Time Limit': 300,
524                'map': 'Zigzag',
525            },
526            'type': 'bs_assault.AssaultGame',
527        },
528        {
529            'settings': {
530                'Epic Mode': False,
531                'Flag Idle Return Time': 30,
532                'Flag Touch Return Time': 0,
533                'Respawn Times': 1.0,
534                'Score to Win': 3,
535                'Time Limit': 300,
536                'map': 'Happy Thoughts',
537            },
538            'type': 'bs_capture_the_flag.CTFGame',
539        },
540        {
541            'settings': {
542                'Bomb Spawning': 1000,
543                'Epic Mode': True,
544                'Laps': 1,
545                'Mine Spawning': 2000,
546                'Time Limit': 300,
547                'map': 'Big G',
548            },
549            'type': 'bs_race.RaceGame',
550        },
551        {
552            'settings': {
553                'Epic Mode': False,
554                'Kills to Win Per Player': 5,
555                'Respawn Times': 1.0,
556                'Time Limit': 300,
557                'map': 'Monkey Face',
558            },
559            'type': 'bs_death_match.DeathMatchGame',
560        },
561        {
562            'settings': {
563                'Hold Time': 30,
564                'Respawn Times': 1.0,
565                'Time Limit': 300,
566                'map': 'Lake Frigid',
567            },
568            'type': 'bs_keep_away.KeepAwayGame',
569        },
570        {
571            'settings': {
572                'Epic Mode': False,
573                'Flag Idle Return Time': 30,
574                'Flag Touch Return Time': 3,
575                'Respawn Times': 1.0,
576                'Score to Win': 2,
577                'Time Limit': 300,
578                'map': 'Tip Top',
579            },
580            'type': 'bs_capture_the_flag.CTFGame',
581        },
582        {
583            'settings': {
584                'Balance Total Lives': False,
585                'Epic Mode': False,
586                'Lives Per Player': 3,
587                'Respawn Times': 1.0,
588                'Solo Mode': False,
589                'Time Limit': 300,
590                'map': 'Crag Castle',
591            },
592            'type': 'bs_elimination.EliminationGame',
593        },
594        {
595            'settings': {
596                'Epic Mode': True,
597                'Respawn Times': 0.25,
598                'Time Limit': 120,
599                'map': 'Zigzag',
600            },
601            'type': 'bs_conquest.ConquestGame',
602        },
603    ]

Return a default playlist for teams mode.

def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
49def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
50    """Standard set of powerups."""
51    return (
52        ('triple_bombs', 3),
53        ('ice_bombs', 3),
54        ('punch', 3),
55        ('impact_bombs', 3),
56        ('land_mines', 2),
57        ('sticky_bombs', 3),
58        ('shield', 2),
59        ('health', 1),
60        ('curse', 1),
61    )

Standard set of powerups.

def get_filtered_map_name(name: str) -> str:
21def get_filtered_map_name(name: str) -> str:
22    """Filter a map name to account for name changes, etc.
23
24    Category: **Asset Functions**
25
26    This can be used to support old playlists, etc.
27    """
28    # Some legacy name fallbacks... can remove these eventually.
29    if name in ('AlwaysLand', 'Happy Land'):
30        name = 'Happy Thoughts'
31    if name == 'Hockey Arena':
32        name = 'Hockey Stadium'
33    return name

Filter a map name to account for name changes, etc.

Category: Asset Functions

This can be used to support old playlists, etc.

def get_map_class(name: str) -> type[Map]:
44def get_map_class(name: str) -> type[Map]:
45    """Return a map type given a name.
46
47    Category: **Asset Functions**
48    """
49    assert babase.app.classic is not None
50    name = get_filtered_map_name(name)
51    try:
52        mapclass: type[Map] = babase.app.classic.maps[name]
53        return mapclass
54    except KeyError:
55        raise babase.NotFoundError(f"Map not found: '{name}'") from None

Return a map type given a name.

Category: Asset Functions

def get_map_display_string(name: str) -> Lstr:
36def get_map_display_string(name: str) -> babase.Lstr:
37    """Return a babase.Lstr for displaying a given map\'s name.
38
39    Category: **Asset Functions**
40    """
41    return babase.Lstr(translate=('mapsNames', name))

Return a babase.Lstr for displaying a given map's name.

Category: Asset Functions

def get_player_colors() -> list[tuple[float, float, float]]:
37def get_player_colors() -> list[tuple[float, float, float]]:
38    """Return user-selectable player colors."""
39    return PLAYER_COLORS

Return user-selectable player colors.

def get_player_profile_colors( profilename: str | None, profiles: dict[str, dict[str, typing.Any]] | None = None) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
 63def get_player_profile_colors(
 64    profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None
 65) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
 66    """Given a profile, return colors for them."""
 67    appconfig = babase.app.config
 68    if profiles is None:
 69        profiles = appconfig['Player Profiles']
 70
 71    # Special case: when being asked for a random color in kiosk mode,
 72    # always return default purple.
 73    if (babase.app.env.demo or babase.app.env.arcade) and profilename is None:
 74        color = (0.5, 0.4, 1.0)
 75        highlight = (0.4, 0.4, 0.5)
 76    else:
 77        try:
 78            assert profilename is not None
 79            color = profiles[profilename]['color']
 80        except (KeyError, AssertionError):
 81            # Key off name if possible.
 82            if profilename is None:
 83                # First 6 are bright-ish.
 84                color = PLAYER_COLORS[random.randrange(6)]
 85            else:
 86                # First 6 are bright-ish.
 87                color = PLAYER_COLORS[sum(ord(c) for c in profilename) % 6]
 88
 89        try:
 90            assert profilename is not None
 91            highlight = profiles[profilename]['highlight']
 92        except (KeyError, AssertionError):
 93            # Key off name if possible.
 94            if profilename is None:
 95                # Last 2 are grey and white; ignore those or we
 96                # get lots of old-looking players.
 97                highlight = PLAYER_COLORS[
 98                    random.randrange(len(PLAYER_COLORS) - 2)
 99                ]
100            else:
101                highlight = PLAYER_COLORS[
102                    sum(ord(c) + 1 for c in profilename)
103                    % (len(PLAYER_COLORS) - 2)
104                ]
105
106    return color, highlight

Given a profile, return colors for them.

def get_player_profile_icon(profilename: str) -> str:
42def get_player_profile_icon(profilename: str) -> str:
43    """Given a profile name, returns an icon string for it.
44
45    (non-account profiles only)
46    """
47    appconfig = babase.app.config
48    icon: str
49    try:
50        is_global = appconfig['Player Profiles'][profilename]['global']
51    except KeyError:
52        is_global = False
53    if is_global:
54        try:
55            icon = appconfig['Player Profiles'][profilename]['icon']
56        except KeyError:
57            icon = babase.charstr(babase.SpecialChar.LOGO)
58    else:
59        icon = ''
60    return icon

Given a profile name, returns an icon string for it.

(non-account profiles only)

def get_trophy_string(trophy_id: str) -> str:
44def get_trophy_string(trophy_id: str) -> str:
45    """Given a trophy id, returns a string to visualize it."""
46    if trophy_id in TROPHY_CHARS:
47        return babase.charstr(TROPHY_CHARS[trophy_id])
48    return '?'

Given a trophy id, returns a string to visualize it.

def getactivity(doraise: bool = True) -> Activity | None:
1303def getactivity(doraise: bool = True) -> bascenev1.Activity | None:
1304    """Return the current bascenev1.Activity instance.
1305
1306    Category: **Gameplay Functions**
1307
1308    Note that this is based on context_ref; thus code run in a timer
1309    generated in Activity 'foo' will properly return 'foo' here, even if
1310    another Activity has since been created or is transitioning in.
1311    If there is no current Activity, raises a babase.ActivityNotFoundError.
1312    If doraise is False, None will be returned instead in that case.
1313    """
1314    return None

Return the current bascenev1.Activity instance.

Category: Gameplay Functions

Note that this is based on context_ref; thus code run in a timer generated in Activity 'foo' will properly return 'foo' here, even if another Activity has since been created or is transitioning in. If there is no current Activity, raises a babase.ActivityNotFoundError. If doraise is False, None will be returned instead in that case.

def getcollision() -> Collision:
68def getcollision() -> Collision:
69    """Return the in-progress collision.
70
71    Category: **Gameplay Functions**
72    """
73    return _collision

Return the in-progress collision.

Category: Gameplay Functions

def getcollisionmesh(name: str) -> CollisionMesh:
1317def getcollisionmesh(name: str) -> bascenev1.CollisionMesh:
1318    """Return a collision-mesh, loading it if necessary.
1319
1320    Category: **Asset Functions**
1321
1322    Collision-meshes are used in physics calculations for such things as
1323    terrain.
1324
1325    Note that this function returns immediately even if the asset has yet
1326    to be loaded. To avoid hitches, instantiate your asset objects in
1327    advance of when you will be using them, allowing time for them to
1328    load in the background if necessary.
1329    """
1330    import bascenev1  # pylint: disable=cyclic-import
1331
1332    return bascenev1.CollisionMesh()

Return a collision-mesh, loading it if necessary.

Category: Asset Functions

Collision-meshes are used in physics calculations for such things as terrain.

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def getdata(name: str) -> Data:
1335def getdata(name: str) -> bascenev1.Data:
1336    """Return a data, loading it if necessary.
1337
1338    Category: **Asset Functions**
1339
1340    Note that this function returns immediately even if the asset has yet
1341    to be loaded. To avoid hitches, instantiate your asset objects in
1342    advance of when you will be using them, allowing time for them to
1343    load in the background if necessary.
1344    """
1345    import bascenev1  # pylint: disable=cyclic-import
1346
1347    return bascenev1.Data()

Return a data, loading it if necessary.

Category: Asset Functions

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def getmesh(name: str) -> Mesh:
1373def getmesh(name: str) -> bascenev1.Mesh:
1374    """Return a mesh, loading it if necessary.
1375
1376    Category: **Asset Functions**
1377
1378    Note that this function returns immediately even if the asset has yet
1379    to be loaded. To avoid hitches, instantiate your asset objects in
1380    advance of when you will be using them, allowing time for them to
1381     load in the background if necessary.
1382    """
1383    import bascenev1  # pylint: disable=cyclic-import
1384
1385    return bascenev1.Mesh()

Return a mesh, loading it if necessary.

Category: Asset Functions

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def getnodes() -> list:
1388def getnodes() -> list:
1389    """Return all nodes in the current bascenev1.Context.
1390
1391    Category: **Gameplay Functions**
1392    """
1393    return list()

Return all nodes in the current bascenev1.Context.

Category: Gameplay Functions

def getsession(doraise: bool = True) -> Session | None:
1405def getsession(doraise: bool = True) -> bascenev1.Session | None:
1406    """Category: **Gameplay Functions**
1407
1408    Returns the current bascenev1.Session instance.
1409    Note that this is based on context_ref; thus code being run in the UI
1410    context will return the UI context_ref here even if a game Session also
1411    exists, etc. If there is no current Session, an Exception is raised, or
1412    if doraise is False then None is returned instead.
1413    """
1414    return None

Category: Gameplay Functions

Returns the current bascenev1.Session instance. Note that this is based on context_ref; thus code being run in the UI context will return the UI context_ref here even if a game Session also exists, etc. If there is no current Session, an Exception is raised, or if doraise is False then None is returned instead.

def getsound(name: str) -> Sound:
1417def getsound(name: str) -> bascenev1.Sound:
1418    """Return a sound, loading it if necessary.
1419
1420    Category: **Asset Functions**
1421
1422    Note that this function returns immediately even if the asset has yet
1423    to be loaded. To avoid hitches, instantiate your asset objects in
1424    advance of when you will be using them, allowing time for them to
1425    load in the background if necessary.
1426    """
1427    import bascenev1  # pylint: disable=cyclic-import
1428
1429    return bascenev1.Sound()

Return a sound, loading it if necessary.

Category: Asset Functions

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def gettexture(name: str) -> Texture:
1432def gettexture(name: str) -> bascenev1.Texture:
1433    """Return a texture, loading it if necessary.
1434
1435    Category: **Asset Functions**
1436
1437    Note that this function returns immediately even if the asset has yet
1438    to be loaded. To avoid hitches, instantiate your asset objects in
1439    advance of when you will be using them, allowing time for them to
1440    load in the background if necessary.
1441    """
1442    import bascenev1  # pylint: disable=cyclic-import
1443
1444    return bascenev1.Texture()

Return a texture, loading it if necessary.

Category: Asset Functions

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

class HitMessage:
234class HitMessage:
235    """Tells an object it has been hit in some way.
236
237    Category: **Message Classes**
238
239    This is used by punches, explosions, etc to convey
240    their effect to a target.
241    """
242
243    def __init__(
244        self,
245        srcnode: bascenev1.Node | None = None,
246        pos: Sequence[float] | None = None,
247        velocity: Sequence[float] | None = None,
248        magnitude: float = 1.0,
249        velocity_magnitude: float = 0.0,
250        radius: float = 1.0,
251        source_player: bascenev1.Player | None = None,
252        kick_back: float = 1.0,
253        flat_damage: float | None = None,
254        hit_type: str = 'generic',
255        force_direction: Sequence[float] | None = None,
256        hit_subtype: str = 'default',
257    ):
258        """Instantiate a message with given values."""
259
260        self.srcnode = srcnode
261        self.pos = pos if pos is not None else babase.Vec3()
262        self.velocity = velocity if velocity is not None else babase.Vec3()
263        self.magnitude = magnitude
264        self.velocity_magnitude = velocity_magnitude
265        self.radius = radius
266
267        # We should not be getting passed an invalid ref.
268        assert source_player is None or source_player.exists()
269        self._source_player = source_player
270        self.kick_back = kick_back
271        self.flat_damage = flat_damage
272        self.hit_type = hit_type
273        self.hit_subtype = hit_subtype
274        self.force_direction = (
275            force_direction if force_direction is not None else velocity
276        )
277
278    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
279        """Return the source-player if one exists and is the provided type."""
280        player: Any = self._source_player
281
282        # We should not be delivering invalid refs.
283        # (we could translate to None here but technically we are changing
284        # the message delivered which seems wrong)
285        assert player is None or player.exists()
286
287        # Return the player *only* if they're the type given.
288        return player if isinstance(player, playertype) else None

Tells an object it has been hit in some way.

Category: Message Classes

This is used by punches, explosions, etc to convey their effect to a target.

HitMessage( srcnode: Node | None = None, pos: Optional[Sequence[float]] = None, velocity: Optional[Sequence[float]] = None, magnitude: float = 1.0, velocity_magnitude: float = 0.0, radius: float = 1.0, source_player: Player | None = None, kick_back: float = 1.0, flat_damage: float | None = None, hit_type: str = 'generic', force_direction: Optional[Sequence[float]] = None, hit_subtype: str = 'default')
243    def __init__(
244        self,
245        srcnode: bascenev1.Node | None = None,
246        pos: Sequence[float] | None = None,
247        velocity: Sequence[float] | None = None,
248        magnitude: float = 1.0,
249        velocity_magnitude: float = 0.0,
250        radius: float = 1.0,
251        source_player: bascenev1.Player | None = None,
252        kick_back: float = 1.0,
253        flat_damage: float | None = None,
254        hit_type: str = 'generic',
255        force_direction: Sequence[float] | None = None,
256        hit_subtype: str = 'default',
257    ):
258        """Instantiate a message with given values."""
259
260        self.srcnode = srcnode
261        self.pos = pos if pos is not None else babase.Vec3()
262        self.velocity = velocity if velocity is not None else babase.Vec3()
263        self.magnitude = magnitude
264        self.velocity_magnitude = velocity_magnitude
265        self.radius = radius
266
267        # We should not be getting passed an invalid ref.
268        assert source_player is None or source_player.exists()
269        self._source_player = source_player
270        self.kick_back = kick_back
271        self.flat_damage = flat_damage
272        self.hit_type = hit_type
273        self.hit_subtype = hit_subtype
274        self.force_direction = (
275            force_direction if force_direction is not None else velocity
276        )

Instantiate a message with given values.

srcnode
pos
velocity
magnitude
velocity_magnitude
radius
kick_back
flat_damage
hit_type
hit_subtype
force_direction
def get_source_player(self, playertype: type[~PlayerT]) -> Optional[~PlayerT]:
278    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
279        """Return the source-player if one exists and is the provided type."""
280        player: Any = self._source_player
281
282        # We should not be delivering invalid refs.
283        # (we could translate to None here but technically we are changing
284        # the message delivered which seems wrong)
285        assert player is None or player.exists()
286
287        # Return the player *only* if they're the type given.
288        return player if isinstance(player, playertype) else None

Return the source-player if one exists and is the provided type.

@dataclass
class HostInfo:
14@dataclass
15class HostInfo:
16    """Info about a host."""
17
18    name: str
19    build_number: int
20
21    # Note this can be None for non-ip hosts such as bluetooth.
22    address: str | None
23
24    # Note this can be None for non-ip hosts such as bluetooth.
25    port: int | None

Info about a host.

HostInfo(name: str, build_number: int, address: str | None, port: int | None)
name: str
build_number: int
address: str | None
port: int | None
@dataclass
class ImpactDamageMessage:
194@dataclass
195class ImpactDamageMessage:
196    """Tells an object that it has been jarred violently.
197
198    Category: **Message Classes**
199    """
200
201    intensity: float
202    """The intensity of the impact."""

Tells an object that it has been jarred violently.

Category: Message Classes

ImpactDamageMessage(intensity: float)
intensity: float

The intensity of the impact.

def init_campaigns() -> None:
105def init_campaigns() -> None:
106    """Fill out initial default Campaigns."""
107    # pylint: disable=cyclic-import
108    from bascenev1._level import Level
109    from bascenev1lib.game.onslaught import OnslaughtGame
110    from bascenev1lib.game.football import FootballCoopGame
111    from bascenev1lib.game.runaround import RunaroundGame
112    from bascenev1lib.game.thelaststand import TheLastStandGame
113    from bascenev1lib.game.race import RaceGame
114    from bascenev1lib.game.targetpractice import TargetPracticeGame
115    from bascenev1lib.game.meteorshower import MeteorShowerGame
116    from bascenev1lib.game.easteregghunt import EasterEggHuntGame
117    from bascenev1lib.game.ninjafight import NinjaFightGame
118
119    # TODO: Campaigns should be load-on-demand; not all imported at launch
120    #  like this.
121
122    # FIXME: Once translations catch up, we can convert these to use the
123    #  generic display-name '${GAME} Training' type stuff.
124    register_campaign(
125        Campaign(
126            'Easy',
127            levels=[
128                Level(
129                    'Onslaught Training',
130                    gametype=OnslaughtGame,
131                    settings={'preset': 'training_easy'},
132                    preview_texture_name='doomShroomPreview',
133                ),
134                Level(
135                    'Rookie Onslaught',
136                    gametype=OnslaughtGame,
137                    settings={'preset': 'rookie_easy'},
138                    preview_texture_name='courtyardPreview',
139                ),
140                Level(
141                    'Rookie Football',
142                    gametype=FootballCoopGame,
143                    settings={'preset': 'rookie_easy'},
144                    preview_texture_name='footballStadiumPreview',
145                ),
146                Level(
147                    'Pro Onslaught',
148                    gametype=OnslaughtGame,
149                    settings={'preset': 'pro_easy'},
150                    preview_texture_name='doomShroomPreview',
151                ),
152                Level(
153                    'Pro Football',
154                    gametype=FootballCoopGame,
155                    settings={'preset': 'pro_easy'},
156                    preview_texture_name='footballStadiumPreview',
157                ),
158                Level(
159                    'Pro Runaround',
160                    gametype=RunaroundGame,
161                    settings={'preset': 'pro_easy'},
162                    preview_texture_name='towerDPreview',
163                ),
164                Level(
165                    'Uber Onslaught',
166                    gametype=OnslaughtGame,
167                    settings={'preset': 'uber_easy'},
168                    preview_texture_name='courtyardPreview',
169                ),
170                Level(
171                    'Uber Football',
172                    gametype=FootballCoopGame,
173                    settings={'preset': 'uber_easy'},
174                    preview_texture_name='footballStadiumPreview',
175                ),
176                Level(
177                    'Uber Runaround',
178                    gametype=RunaroundGame,
179                    settings={'preset': 'uber_easy'},
180                    preview_texture_name='towerDPreview',
181                ),
182            ],
183        )
184    )
185
186    # "hard" mode
187    register_campaign(
188        Campaign(
189            'Default',
190            levels=[
191                Level(
192                    'Onslaught Training',
193                    gametype=OnslaughtGame,
194                    settings={'preset': 'training'},
195                    preview_texture_name='doomShroomPreview',
196                ),
197                Level(
198                    'Rookie Onslaught',
199                    gametype=OnslaughtGame,
200                    settings={'preset': 'rookie'},
201                    preview_texture_name='courtyardPreview',
202                ),
203                Level(
204                    'Rookie Football',
205                    gametype=FootballCoopGame,
206                    settings={'preset': 'rookie'},
207                    preview_texture_name='footballStadiumPreview',
208                ),
209                Level(
210                    'Pro Onslaught',
211                    gametype=OnslaughtGame,
212                    settings={'preset': 'pro'},
213                    preview_texture_name='doomShroomPreview',
214                ),
215                Level(
216                    'Pro Football',
217                    gametype=FootballCoopGame,
218                    settings={'preset': 'pro'},
219                    preview_texture_name='footballStadiumPreview',
220                ),
221                Level(
222                    'Pro Runaround',
223                    gametype=RunaroundGame,
224                    settings={'preset': 'pro'},
225                    preview_texture_name='towerDPreview',
226                ),
227                Level(
228                    'Uber Onslaught',
229                    gametype=OnslaughtGame,
230                    settings={'preset': 'uber'},
231                    preview_texture_name='courtyardPreview',
232                ),
233                Level(
234                    'Uber Football',
235                    gametype=FootballCoopGame,
236                    settings={'preset': 'uber'},
237                    preview_texture_name='footballStadiumPreview',
238                ),
239                Level(
240                    'Uber Runaround',
241                    gametype=RunaroundGame,
242                    settings={'preset': 'uber'},
243                    preview_texture_name='towerDPreview',
244                ),
245                Level(
246                    'The Last Stand',
247                    gametype=TheLastStandGame,
248                    settings={},
249                    preview_texture_name='rampagePreview',
250                ),
251            ],
252        )
253    )
254
255    # challenges: our 'official' random extra co-op levels
256    register_campaign(
257        Campaign(
258            'Challenges',
259            sequential=False,
260            levels=[
261                Level(
262                    'Infinite Onslaught',
263                    gametype=OnslaughtGame,
264                    settings={'preset': 'endless'},
265                    preview_texture_name='doomShroomPreview',
266                ),
267                Level(
268                    'Infinite Runaround',
269                    gametype=RunaroundGame,
270                    settings={'preset': 'endless'},
271                    preview_texture_name='towerDPreview',
272                ),
273                Level(
274                    'Race',
275                    displayname='${GAME}',
276                    gametype=RaceGame,
277                    settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 0},
278                    preview_texture_name='bigGPreview',
279                ),
280                Level(
281                    'Pro Race',
282                    displayname='Pro ${GAME}',
283                    gametype=RaceGame,
284                    settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 1000},
285                    preview_texture_name='bigGPreview',
286                ),
287                Level(
288                    'Lake Frigid Race',
289                    displayname='${GAME}',
290                    gametype=RaceGame,
291                    settings={
292                        'map': 'Lake Frigid',
293                        'Laps': 6,
294                        'Mine Spawning': 2000,
295                        'Bomb Spawning': 0,
296                    },
297                    preview_texture_name='lakeFrigidPreview',
298                ),
299                Level(
300                    'Football',
301                    displayname='${GAME}',
302                    gametype=FootballCoopGame,
303                    settings={'preset': 'tournament'},
304                    preview_texture_name='footballStadiumPreview',
305                ),
306                Level(
307                    'Pro Football',
308                    displayname='Pro ${GAME}',
309                    gametype=FootballCoopGame,
310                    settings={'preset': 'tournament_pro'},
311                    preview_texture_name='footballStadiumPreview',
312                ),
313                Level(
314                    'Runaround',
315                    displayname='${GAME}',
316                    gametype=RunaroundGame,
317                    settings={'preset': 'tournament'},
318                    preview_texture_name='towerDPreview',
319                ),
320                Level(
321                    'Uber Runaround',
322                    displayname='Uber ${GAME}',
323                    gametype=RunaroundGame,
324                    settings={'preset': 'tournament_uber'},
325                    preview_texture_name='towerDPreview',
326                ),
327                Level(
328                    'The Last Stand',
329                    displayname='${GAME}',
330                    gametype=TheLastStandGame,
331                    settings={'preset': 'tournament'},
332                    preview_texture_name='rampagePreview',
333                ),
334                Level(
335                    'Tournament Infinite Onslaught',
336                    displayname='Infinite Onslaught',
337                    gametype=OnslaughtGame,
338                    settings={'preset': 'endless_tournament'},
339                    preview_texture_name='doomShroomPreview',
340                ),
341                Level(
342                    'Tournament Infinite Runaround',
343                    displayname='Infinite Runaround',
344                    gametype=RunaroundGame,
345                    settings={'preset': 'endless_tournament'},
346                    preview_texture_name='towerDPreview',
347                ),
348                Level(
349                    'Target Practice',
350                    displayname='Pro ${GAME}',
351                    gametype=TargetPracticeGame,
352                    settings={},
353                    preview_texture_name='doomShroomPreview',
354                ),
355                Level(
356                    'Target Practice B',
357                    displayname='${GAME}',
358                    gametype=TargetPracticeGame,
359                    settings={
360                        'Target Count': 2,
361                        'Enable Impact Bombs': False,
362                        'Enable Triple Bombs': False,
363                    },
364                    preview_texture_name='doomShroomPreview',
365                ),
366                Level(
367                    'Meteor Shower',
368                    displayname='${GAME}',
369                    gametype=MeteorShowerGame,
370                    settings={},
371                    preview_texture_name='rampagePreview',
372                ),
373                Level(
374                    'Epic Meteor Shower',
375                    displayname='${GAME}',
376                    gametype=MeteorShowerGame,
377                    settings={'Epic Mode': True},
378                    preview_texture_name='rampagePreview',
379                ),
380                Level(
381                    'Easter Egg Hunt',
382                    displayname='${GAME}',
383                    gametype=EasterEggHuntGame,
384                    settings={},
385                    preview_texture_name='towerDPreview',
386                ),
387                Level(
388                    'Pro Easter Egg Hunt',
389                    displayname='Pro ${GAME}',
390                    gametype=EasterEggHuntGame,
391                    settings={'Pro Mode': True},
392                    preview_texture_name='towerDPreview',
393                ),
394                Level(
395                    name='Ninja Fight',  # (unique id not seen by player)
396                    displayname='${GAME}',  # (readable name seen by player)
397                    gametype=NinjaFightGame,
398                    settings={'preset': 'regular'},
399                    preview_texture_name='courtyardPreview',
400                ),
401                Level(
402                    name='Pro Ninja Fight',
403                    displayname='Pro ${GAME}',
404                    gametype=NinjaFightGame,
405                    settings={'preset': 'pro'},
406                    preview_texture_name='courtyardPreview',
407                ),
408            ],
409        )
410    )

Fill out initial default Campaigns.

class InputDevice:
159class InputDevice:
160    """An input-device such as a gamepad, touchscreen, or keyboard.
161
162    Category: **Gameplay Classes**
163    """
164
165    allows_configuring: bool
166    """Whether the input-device can be configured in the app."""
167
168    allows_configuring_in_system_settings: bool
169    """Whether the input-device can be configured in the system.
170       setings app. This can be used to redirect the user to go there
171       if they attempt to configure the device."""
172
173    has_meaningful_button_names: bool
174    """Whether button names returned by this instance match labels
175       on the actual device. (Can be used to determine whether to show
176       them in controls-overlays, etc.)."""
177
178    player: bascenev1.SessionPlayer | None
179    """The player associated with this input device."""
180
181    client_id: int
182    """The numeric client-id this device is associated with.
183       This is only meaningful for remote client inputs; for
184       all local devices this will be -1."""
185
186    name: str
187    """The name of the device."""
188
189    unique_identifier: str
190    """A string that can be used to persistently identify the device,
191       even among other devices of the same type. Used for saving
192       prefs, etc."""
193
194    id: int
195    """The unique numeric id of this device."""
196
197    instance_number: int
198    """The number of this device among devices of the same type."""
199
200    is_controller_app: bool
201    """Whether this input-device represents a locally-connected
202       controller-app."""
203
204    is_remote_client: bool
205    """Whether this input-device represents a remotely-connected
206       client."""
207
208    is_test_input: bool
209    """Whether this input-device is a dummy device for testing."""
210
211    def __bool__(self) -> bool:
212        """Support for bool evaluation."""
213        return bool(True)  # Slight obfuscation.
214
215    def detach_from_player(self) -> None:
216        """Detach the device from any player it is controlling.
217
218        This applies both to local players and remote players.
219        """
220        return None
221
222    def exists(self) -> bool:
223        """Return whether the underlying device for this object is
224        still present.
225        """
226        return bool()
227
228    def get_axis_name(self, axis_id: int) -> str:
229        """Given an axis ID, return the name of the axis on this device.
230
231        Can return an empty string if the value is not meaningful to humans.
232        """
233        return str()
234
235    def get_button_name(self, button_id: int) -> babase.Lstr:
236        """Given a button ID, return a human-readable name for that key/button.
237
238        Can return an empty string if the value is not meaningful to humans.
239        """
240        import babase  # pylint: disable=cyclic-import
241
242        return babase.Lstr(value='')
243
244    def get_default_player_name(self) -> str:
245        """(internal)
246
247        Returns the default player name for this device. (used for the 'random'
248        profile)
249        """
250        return str()
251
252    def get_player_profiles(self) -> dict:
253        """(internal)"""
254        return dict()
255
256    def get_v1_account_name(self, full: bool) -> str:
257        """Returns the account name associated with this device.
258
259        (can be used to get account names for remote players)
260        """
261        return str()
262
263    def is_attached_to_player(self) -> bool:
264        """Return whether this device is controlling a player of some sort.
265
266        This can mean either a local player or a remote player.
267        """
268        return bool()

An input-device such as a gamepad, touchscreen, or keyboard.

Category: Gameplay Classes

allows_configuring: bool

Whether the input-device can be configured in the app.

allows_configuring_in_system_settings: bool

Whether the input-device can be configured in the system. setings app. This can be used to redirect the user to go there if they attempt to configure the device.

has_meaningful_button_names: bool

Whether button names returned by this instance match labels on the actual device. (Can be used to determine whether to show them in controls-overlays, etc.).

player: SessionPlayer | None

The player associated with this input device.

client_id: int

The numeric client-id this device is associated with. This is only meaningful for remote client inputs; for all local devices this will be -1.

name: str

The name of the device.

unique_identifier: str

A string that can be used to persistently identify the device, even among other devices of the same type. Used for saving prefs, etc.

id: int

The unique numeric id of this device.

instance_number: int

The number of this device among devices of the same type.

is_controller_app: bool

Whether this input-device represents a locally-connected controller-app.

is_remote_client: bool

Whether this input-device represents a remotely-connected client.

is_test_input: bool

Whether this input-device is a dummy device for testing.

def detach_from_player(self) -> None:
215    def detach_from_player(self) -> None:
216        """Detach the device from any player it is controlling.
217
218        This applies both to local players and remote players.
219        """
220        return None

Detach the device from any player it is controlling.

This applies both to local players and remote players.

def exists(self) -> bool:
222    def exists(self) -> bool:
223        """Return whether the underlying device for this object is
224        still present.
225        """
226        return bool()

Return whether the underlying device for this object is still present.

def get_axis_name(self, axis_id: int) -> str:
228    def get_axis_name(self, axis_id: int) -> str:
229        """Given an axis ID, return the name of the axis on this device.
230
231        Can return an empty string if the value is not meaningful to humans.
232        """
233        return str()

Given an axis ID, return the name of the axis on this device.

Can return an empty string if the value is not meaningful to humans.

def get_button_name(self, button_id: int) -> Lstr:
235    def get_button_name(self, button_id: int) -> babase.Lstr:
236        """Given a button ID, return a human-readable name for that key/button.
237
238        Can return an empty string if the value is not meaningful to humans.
239        """
240        import babase  # pylint: disable=cyclic-import
241
242        return babase.Lstr(value='')

Given a button ID, return a human-readable name for that key/button.

Can return an empty string if the value is not meaningful to humans.

def get_v1_account_name(self, full: bool) -> str:
256    def get_v1_account_name(self, full: bool) -> str:
257        """Returns the account name associated with this device.
258
259        (can be used to get account names for remote players)
260        """
261        return str()

Returns the account name associated with this device.

(can be used to get account names for remote players)

def is_attached_to_player(self) -> bool:
263    def is_attached_to_player(self) -> bool:
264        """Return whether this device is controlling a player of some sort.
265
266        This can mean either a local player or a remote player.
267        """
268        return bool()

Return whether this device is controlling a player of some sort.

This can mean either a local player or a remote player.

class InputType(enum.Enum):
 8class InputType(Enum):
 9    """Types of input a controller can send to the game.
10
11    Category: Enums
12
13    """
14
15    UP_DOWN = 2
16    LEFT_RIGHT = 3
17    JUMP_PRESS = 4
18    JUMP_RELEASE = 5
19    PUNCH_PRESS = 6
20    PUNCH_RELEASE = 7
21    BOMB_PRESS = 8
22    BOMB_RELEASE = 9
23    PICK_UP_PRESS = 10
24    PICK_UP_RELEASE = 11
25    RUN = 12
26    FLY_PRESS = 13
27    FLY_RELEASE = 14
28    START_PRESS = 15
29    START_RELEASE = 16
30    HOLD_POSITION_PRESS = 17
31    HOLD_POSITION_RELEASE = 18
32    LEFT_PRESS = 19
33    LEFT_RELEASE = 20
34    RIGHT_PRESS = 21
35    RIGHT_RELEASE = 22
36    UP_PRESS = 23
37    UP_RELEASE = 24
38    DOWN_PRESS = 25
39    DOWN_RELEASE = 26

Types of input a controller can send to the game.

Category: Enums

UP_DOWN = <InputType.UP_DOWN: 2>
LEFT_RIGHT = <InputType.LEFT_RIGHT: 3>
JUMP_PRESS = <InputType.JUMP_PRESS: 4>
JUMP_RELEASE = <InputType.JUMP_RELEASE: 5>
PUNCH_PRESS = <InputType.PUNCH_PRESS: 6>
PUNCH_RELEASE = <InputType.PUNCH_RELEASE: 7>
BOMB_PRESS = <InputType.BOMB_PRESS: 8>
BOMB_RELEASE = <InputType.BOMB_RELEASE: 9>
PICK_UP_PRESS = <InputType.PICK_UP_PRESS: 10>
PICK_UP_RELEASE = <InputType.PICK_UP_RELEASE: 11>
RUN = <InputType.RUN: 12>
FLY_PRESS = <InputType.FLY_PRESS: 13>
FLY_RELEASE = <InputType.FLY_RELEASE: 14>
START_PRESS = <InputType.START_PRESS: 15>
START_RELEASE = <InputType.START_RELEASE: 16>
HOLD_POSITION_PRESS = <InputType.HOLD_POSITION_PRESS: 17>
HOLD_POSITION_RELEASE = <InputType.HOLD_POSITION_RELEASE: 18>
LEFT_PRESS = <InputType.LEFT_PRESS: 19>
LEFT_RELEASE = <InputType.LEFT_RELEASE: 20>
RIGHT_PRESS = <InputType.RIGHT_PRESS: 21>
RIGHT_RELEASE = <InputType.RIGHT_RELEASE: 22>
UP_PRESS = <InputType.UP_PRESS: 23>
UP_RELEASE = <InputType.UP_RELEASE: 24>
DOWN_PRESS = <InputType.DOWN_PRESS: 25>
DOWN_RELEASE = <InputType.DOWN_RELEASE: 26>
Inherited Members
enum.Enum
name
value
@dataclass
class IntChoiceSetting(bascenev1.ChoiceSetting):
72@dataclass
73class IntChoiceSetting(ChoiceSetting):
74    """An int setting with multiple choices.
75
76    Category: Settings Classes
77    """
78
79    default: int
80    choices: list[tuple[str, int]]

An int setting with multiple choices.

Category: Settings Classes

IntChoiceSetting(name: str, default: int, choices: list[tuple[str, int]])
default: int
choices: list[tuple[str, int]]
Inherited Members
Setting
name
@dataclass
class IntSetting(bascenev1.Setting):
36@dataclass
37class IntSetting(Setting):
38    """An integer game setting.
39
40    Category: Settings Classes
41    """
42
43    default: int
44    min_value: int = 0
45    max_value: int = 9999
46    increment: int = 1

An integer game setting.

Category: Settings Classes

IntSetting( name: str, default: int, min_value: int = 0, max_value: int = 9999, increment: int = 1)
default: int
min_value: int = 0
max_value: int = 9999
increment: int = 1
Inherited Members
Setting
name
def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
38def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
39    """Return whether a given point is within a given box.
40
41    category: General Utility Functions
42
43    For use with standard def boxes (position|rotate|scale).
44    """
45    return (
46        (abs(pnt[0] - box[0]) <= box[6] * 0.5)
47        and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
48        and (abs(pnt[2] - box[2]) <= box[8] * 0.5)
49    )

Return whether a given point is within a given box.

category: General Utility Functions

For use with standard def boxes (position|rotate|scale).

60class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
61    """Standard activity for waiting for players to join.
62
63    It shows tips and other info and waits for all players to check ready.
64    """
65
66    def __init__(self, settings: dict):
67        super().__init__(settings)
68
69        # This activity is a special 'joiner' activity.
70        # It will get shut down as soon as all players have checked ready.
71        self.is_joining_activity = True
72
73        # Players may be idle waiting for joiners; lets not kick them for it.
74        self.allow_kick_idle_players = False
75
76        # In vr mode we don't want stuff moving around.
77        self.use_fixed_vr_overlay = True
78
79        self._background: bascenev1.Actor | None = None
80        self._tips_text: bascenev1.Actor | None = None
81        self._join_info: JoinInfo | None = None
82
83    @override
84    def on_transition_in(self) -> None:
85        # pylint: disable=cyclic-import
86        from bascenev1lib.actor.tipstext import TipsText
87        from bascenev1lib.actor.background import Background
88
89        super().on_transition_in()
90        self._background = Background(
91            fade_time=0.5, start_faded=True, show_logo=True
92        )
93        self._tips_text = TipsText()
94        setmusic(MusicType.CHAR_SELECT)
95        self._join_info = self.session.lobby.create_join_info()
96        babase.set_analytics_screen('Joining Screen')

Standard activity for waiting for players to join.

It shows tips and other info and waits for all players to check ready.

JoinActivity(settings: dict)
66    def __init__(self, settings: dict):
67        super().__init__(settings)
68
69        # This activity is a special 'joiner' activity.
70        # It will get shut down as soon as all players have checked ready.
71        self.is_joining_activity = True
72
73        # Players may be idle waiting for joiners; lets not kick them for it.
74        self.allow_kick_idle_players = False
75
76        # In vr mode we don't want stuff moving around.
77        self.use_fixed_vr_overlay = True
78
79        self._background: bascenev1.Actor | None = None
80        self._tips_text: bascenev1.Actor | None = None
81        self._join_info: JoinInfo | None = None

Creates an Activity in the current bascenev1.Session.

The activity will not be actually run until bascenev1.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.

is_joining_activity = False

Joining activities are for waiting for initial player joins. They are treated slightly differently than regular activities, mainly in that all players are passed to the activity at once instead of as each joins.

allow_kick_idle_players = True

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

use_fixed_vr_overlay = False

In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.

@override
def on_transition_in(self) -> None:
83    @override
84    def on_transition_in(self) -> None:
85        # pylint: disable=cyclic-import
86        from bascenev1lib.actor.tipstext import TipsText
87        from bascenev1lib.actor.background import Background
88
89        super().on_transition_in()
90        self._background = Background(
91            fade_time=0.5, start_faded=True, show_logo=True
92        )
93        self._tips_text = TipsText()
94        setmusic(MusicType.CHAR_SELECT)
95        self._join_info = self.session.lobby.create_join_info()
96        babase.set_analytics_screen('Joining Screen')

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 bascenev1.Activity.on_begin() is called.

class Level:
 19class Level:
 20    """An entry in a bascenev1.Campaign.
 21
 22    Category: **Gameplay Classes**
 23    """
 24
 25    def __init__(
 26        self,
 27        name: str,
 28        gametype: type[bascenev1.GameActivity],
 29        settings: dict,
 30        preview_texture_name: str,
 31        displayname: str | None = None,
 32    ):
 33        self._name = name
 34        self._gametype = gametype
 35        self._settings = settings
 36        self._preview_texture_name = preview_texture_name
 37        self._displayname = displayname
 38        self._campaign: weakref.ref[bascenev1.Campaign] | None = None
 39        self._index: int | None = None
 40        self._score_version_string: str | None = None
 41
 42    @override
 43    def __repr__(self) -> str:
 44        cls = type(self)
 45        return f"<{cls.__module__}.{cls.__name__} '{self._name}'>"
 46
 47    @property
 48    def name(self) -> str:
 49        """The unique name for this Level."""
 50        return self._name
 51
 52    def get_settings(self) -> dict[str, Any]:
 53        """Returns the settings for this Level."""
 54        settings = copy.deepcopy(self._settings)
 55
 56        # So the game knows what the level is called.
 57        # Hmm; seems hacky; I think we should take this out.
 58        settings['name'] = self._name
 59        return settings
 60
 61    @property
 62    def preview_texture_name(self) -> str:
 63        """The preview texture name for this Level."""
 64        return self._preview_texture_name
 65
 66    # def get_preview_texture(self) -> bauiv1.Texture:
 67    #     """Load/return the preview Texture for this Level."""
 68    #     return _bauiv1.gettexture(self._preview_texture_name)
 69
 70    @property
 71    def displayname(self) -> bascenev1.Lstr:
 72        """The localized name for this Level."""
 73        return babase.Lstr(
 74            translate=(
 75                'coopLevelNames',
 76                (
 77                    self._displayname
 78                    if self._displayname is not None
 79                    else self._name
 80                ),
 81            ),
 82            subs=[
 83                ('${GAME}', self._gametype.get_display_string(self._settings))
 84            ],
 85        )
 86
 87    @property
 88    def gametype(self) -> type[bascenev1.GameActivity]:
 89        """The type of game used for this Level."""
 90        return self._gametype
 91
 92    @property
 93    def campaign(self) -> bascenev1.Campaign | None:
 94        """The baclassic.Campaign this Level is associated with, or None."""
 95        return None if self._campaign is None else self._campaign()
 96
 97    @property
 98    def index(self) -> int:
 99        """The zero-based index of this Level in its baclassic.Campaign.
100
101        Access results in a RuntimeError if the Level is  not assigned to a
102        Campaign.
103        """
104        if self._index is None:
105            raise RuntimeError('Level is not part of a Campaign')
106        return self._index
107
108    @property
109    def complete(self) -> bool:
110        """Whether this Level has been completed."""
111        config = self._get_config_dict()
112        val = config.get('Complete', False)
113        assert isinstance(val, bool)
114        return val
115
116    def set_complete(self, val: bool) -> None:
117        """Set whether or not this level is complete."""
118        old_val = self.complete
119        assert isinstance(old_val, bool)
120        assert isinstance(val, bool)
121        if val != old_val:
122            config = self._get_config_dict()
123            config['Complete'] = val
124
125    def get_high_scores(self) -> dict:
126        """Return the current high scores for this Level."""
127        config = self._get_config_dict()
128        high_scores_key = 'High Scores' + self.get_score_version_string()
129        if high_scores_key not in config:
130            return {}
131        return copy.deepcopy(config[high_scores_key])
132
133    def set_high_scores(self, high_scores: dict) -> None:
134        """Set high scores for this level."""
135        config = self._get_config_dict()
136        high_scores_key = 'High Scores' + self.get_score_version_string()
137        config[high_scores_key] = high_scores
138
139    def get_score_version_string(self) -> str:
140        """Return the score version string for this Level.
141
142        If a Level's gameplay changes significantly, its version string
143        can be changed to separate its new high score lists/etc. from the old.
144        """
145        if self._score_version_string is None:
146            scorever = self._gametype.getscoreconfig().version
147            if scorever != '':
148                scorever = ' ' + scorever
149            self._score_version_string = scorever
150        assert self._score_version_string is not None
151        return self._score_version_string
152
153    @property
154    def rating(self) -> float:
155        """The current rating for this Level."""
156        val = self._get_config_dict().get('Rating', 0.0)
157        assert isinstance(val, float)
158        return val
159
160    def set_rating(self, rating: float) -> None:
161        """Set a rating for this Level, replacing the old ONLY IF higher."""
162        old_rating = self.rating
163        config = self._get_config_dict()
164        config['Rating'] = max(old_rating, rating)
165
166    def _get_config_dict(self) -> dict[str, Any]:
167        """Return/create the persistent state dict for this level.
168
169        The referenced dict exists under the game's config dict and
170        can be modified in place."""
171        campaign = self.campaign
172        if campaign is None:
173            raise RuntimeError('Level is not in a campaign.')
174        configdict = campaign.configdict
175        val: dict[str, Any] = configdict.setdefault(
176            self._name, {'Rating': 0.0, 'Complete': False}
177        )
178        assert isinstance(val, dict)
179        return val
180
181    def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None:
182        """For use by baclassic.Campaign when adding levels to itself.
183
184        (internal)"""
185        self._campaign = weakref.ref(campaign)
186        self._index = index

An entry in a bascenev1.Campaign.

Category: Gameplay Classes

Level( name: str, gametype: type[GameActivity], settings: dict, preview_texture_name: str, displayname: str | None = None)
25    def __init__(
26        self,
27        name: str,
28        gametype: type[bascenev1.GameActivity],
29        settings: dict,
30        preview_texture_name: str,
31        displayname: str | None = None,
32    ):
33        self._name = name
34        self._gametype = gametype
35        self._settings = settings
36        self._preview_texture_name = preview_texture_name
37        self._displayname = displayname
38        self._campaign: weakref.ref[bascenev1.Campaign] | None = None
39        self._index: int | None = None
40        self._score_version_string: str | None = None
name: str
47    @property
48    def name(self) -> str:
49        """The unique name for this Level."""
50        return self._name

The unique name for this Level.

def get_settings(self) -> dict[str, typing.Any]:
52    def get_settings(self) -> dict[str, Any]:
53        """Returns the settings for this Level."""
54        settings = copy.deepcopy(self._settings)
55
56        # So the game knows what the level is called.
57        # Hmm; seems hacky; I think we should take this out.
58        settings['name'] = self._name
59        return settings

Returns the settings for this Level.

preview_texture_name: str
61    @property
62    def preview_texture_name(self) -> str:
63        """The preview texture name for this Level."""
64        return self._preview_texture_name

The preview texture name for this Level.

displayname: Lstr
70    @property
71    def displayname(self) -> bascenev1.Lstr:
72        """The localized name for this Level."""
73        return babase.Lstr(
74            translate=(
75                'coopLevelNames',
76                (
77                    self._displayname
78                    if self._displayname is not None
79                    else self._name
80                ),
81            ),
82            subs=[
83                ('${GAME}', self._gametype.get_display_string(self._settings))
84            ],
85        )

The localized name for this Level.

gametype: type[GameActivity]
87    @property
88    def gametype(self) -> type[bascenev1.GameActivity]:
89        """The type of game used for this Level."""
90        return self._gametype

The type of game used for this Level.

campaign: Campaign | None
92    @property
93    def campaign(self) -> bascenev1.Campaign | None:
94        """The baclassic.Campaign this Level is associated with, or None."""
95        return None if self._campaign is None else self._campaign()

The baclassic.Campaign this Level is associated with, or None.

index: int
 97    @property
 98    def index(self) -> int:
 99        """The zero-based index of this Level in its baclassic.Campaign.
100
101        Access results in a RuntimeError if the Level is  not assigned to a
102        Campaign.
103        """
104        if self._index is None:
105            raise RuntimeError('Level is not part of a Campaign')
106        return self._index

The zero-based index of this Level in its baclassic.Campaign.

Access results in a RuntimeError if the Level is not assigned to a Campaign.

complete: bool
108    @property
109    def complete(self) -> bool:
110        """Whether this Level has been completed."""
111        config = self._get_config_dict()
112        val = config.get('Complete', False)
113        assert isinstance(val, bool)
114        return val

Whether this Level has been completed.

def set_complete(self, val: bool) -> None:
116    def set_complete(self, val: bool) -> None:
117        """Set whether or not this level is complete."""
118        old_val = self.complete
119        assert isinstance(old_val, bool)
120        assert isinstance(val, bool)
121        if val != old_val:
122            config = self._get_config_dict()
123            config['Complete'] = val

Set whether or not this level is complete.

def get_high_scores(self) -> dict:
125    def get_high_scores(self) -> dict:
126        """Return the current high scores for this Level."""
127        config = self._get_config_dict()
128        high_scores_key = 'High Scores' + self.get_score_version_string()
129        if high_scores_key not in config:
130            return {}
131        return copy.deepcopy(config[high_scores_key])

Return the current high scores for this Level.

def set_high_scores(self, high_scores: dict) -> None:
133    def set_high_scores(self, high_scores: dict) -> None:
134        """Set high scores for this level."""
135        config = self._get_config_dict()
136        high_scores_key = 'High Scores' + self.get_score_version_string()
137        config[high_scores_key] = high_scores

Set high scores for this level.

def get_score_version_string(self) -> str:
139    def get_score_version_string(self) -> str:
140        """Return the score version string for this Level.
141
142        If a Level's gameplay changes significantly, its version string
143        can be changed to separate its new high score lists/etc. from the old.
144        """
145        if self._score_version_string is None:
146            scorever = self._gametype.getscoreconfig().version
147            if scorever != '':
148                scorever = ' ' + scorever
149            self._score_version_string = scorever
150        assert self._score_version_string is not None
151        return self._score_version_string

Return the score version string for this Level.

If a Level's gameplay changes significantly, its version string can be changed to separate its new high score lists/etc. from the old.

rating: float
153    @property
154    def rating(self) -> float:
155        """The current rating for this Level."""
156        val = self._get_config_dict().get('Rating', 0.0)
157        assert isinstance(val, float)
158        return val

The current rating for this Level.

def set_rating(self, rating: float) -> None:
160    def set_rating(self, rating: float) -> None:
161        """Set a rating for this Level, replacing the old ONLY IF higher."""
162        old_rating = self.rating
163        config = self._get_config_dict()
164        config['Rating'] = max(old_rating, rating)

Set a rating for this Level, replacing the old ONLY IF higher.

class Lobby:
 942class Lobby:
 943    """Container for baclassic.Choosers.
 944
 945    Category: Gameplay Classes
 946    """
 947
 948    def __del__(self) -> None:
 949        # Reset any players that still have a chooser in us.
 950        # (should allow the choosers to die).
 951        sessionplayers = [
 952            c.sessionplayer for c in self.choosers if c.sessionplayer
 953        ]
 954        for sessionplayer in sessionplayers:
 955            sessionplayer.resetinput()
 956
 957    def __init__(self) -> None:
 958        from bascenev1._team import SessionTeam
 959        from bascenev1._coopsession import CoopSession
 960
 961        session = _bascenev1.getsession()
 962        self._use_team_colors = session.use_team_colors
 963        if session.use_teams:
 964            self._sessionteams = [
 965                weakref.ref(team) for team in session.sessionteams
 966            ]
 967        else:
 968            self._dummy_teams = SessionTeam()
 969            self._sessionteams = [weakref.ref(self._dummy_teams)]
 970        v_offset = -150 if isinstance(session, CoopSession) else -50
 971        self.choosers: list[Chooser] = []
 972        self.base_v_offset = v_offset
 973        self.update_positions()
 974        self._next_add_team = 0
 975        self.character_names_local_unlocked: list[str] = []
 976        self._vpos = 0
 977
 978        # Grab available profiles.
 979        self.reload_profiles()
 980
 981        self._join_info_text = None
 982
 983    @property
 984    def next_add_team(self) -> int:
 985        """(internal)"""
 986        return self._next_add_team
 987
 988    @property
 989    def use_team_colors(self) -> bool:
 990        """A bool for whether this lobby is using team colors.
 991
 992        If False, inidividual player colors are used instead.
 993        """
 994        return self._use_team_colors
 995
 996    @property
 997    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 998        """bascenev1.SessionTeams available in this lobby."""
 999        allteams = []
1000        for tref in self._sessionteams:
1001            team = tref()
1002            assert team is not None
1003            allteams.append(team)
1004        return allteams
1005
1006    def get_choosers(self) -> list[Chooser]:
1007        """Return the lobby's current choosers."""
1008        return self.choosers
1009
1010    def create_join_info(self) -> JoinInfo:
1011        """Create a display of on-screen information for joiners.
1012
1013        (how to switch teams, players, etc.)
1014        Intended for use in initial joining-screens.
1015        """
1016        return JoinInfo(self)
1017
1018    def reload_profiles(self) -> None:
1019        """Reload available player profiles."""
1020        # pylint: disable=cyclic-import
1021        from bascenev1lib.actor.spazappearance import get_appearances
1022
1023        assert babase.app.classic is not None
1024
1025        # We may have gained or lost character names if the user
1026        # bought something; reload these too.
1027        self.character_names_local_unlocked = get_appearances()
1028        self.character_names_local_unlocked.sort(key=lambda x: x.lower())
1029
1030        # Do any overall prep we need to such as creating account profile.
1031        babase.app.classic.accounts.ensure_have_account_player_profile()
1032        for chooser in self.choosers:
1033            try:
1034                chooser.reload_profiles()
1035                chooser.update_from_profile()
1036            except Exception:
1037                logging.exception('Error reloading profiles.')
1038
1039    def update_positions(self) -> None:
1040        """Update positions for all choosers."""
1041        self._vpos = -100 + self.base_v_offset
1042        for chooser in self.choosers:
1043            chooser.set_vpos(self._vpos)
1044            chooser.update_position()
1045            self._vpos -= 48
1046
1047    def check_all_ready(self) -> bool:
1048        """Return whether all choosers are marked ready."""
1049        return all(chooser.ready for chooser in self.choosers)
1050
1051    def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None:
1052        """Add a chooser to the lobby for the provided player."""
1053        self.choosers.append(
1054            Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
1055        )
1056        self._next_add_team = (self._next_add_team + 1) % len(
1057            self._sessionteams
1058        )
1059        self._vpos -= 48
1060
1061    def remove_chooser(self, player: bascenev1.SessionPlayer) -> None:
1062        """Remove a single player's chooser; does not kick them.
1063
1064        This is used when a player enters the game and no longer
1065        needs a chooser."""
1066        found = False
1067        chooser = None
1068        for chooser in self.choosers:
1069            if chooser.getplayer() is player:
1070                found = True
1071
1072                # Mark it as dead since there could be more
1073                # change-commands/etc coming in still for it;
1074                # want to avoid duplicate player-adds/etc.
1075                chooser.set_dead(True)
1076                self.choosers.remove(chooser)
1077                break
1078        if not found:
1079            logging.exception('remove_chooser did not find player %s.', player)
1080        elif chooser in self.choosers:
1081            logging.exception('chooser remains after removal for %s.', player)
1082        self.update_positions()
1083
1084    def remove_all_choosers(self) -> None:
1085        """Remove all choosers without kicking players.
1086
1087        This is called after all players check in and enter a game.
1088        """
1089        self.choosers = []
1090        self.update_positions()
1091
1092    def remove_all_choosers_and_kick_players(self) -> None:
1093        """Remove all player choosers and kick attached players."""
1094
1095        # Copy the list; it can change under us otherwise.
1096        for chooser in list(self.choosers):
1097            if chooser.sessionplayer:
1098                chooser.sessionplayer.remove_from_game()
1099        self.remove_all_choosers()

Container for baclassic.Choosers.

Category: Gameplay Classes

choosers: list[Chooser]
base_v_offset
character_names_local_unlocked: list[str]
use_team_colors: bool
988    @property
989    def use_team_colors(self) -> bool:
990        """A bool for whether this lobby is using team colors.
991
992        If False, inidividual player colors are used instead.
993        """
994        return self._use_team_colors

A bool for whether this lobby is using team colors.

If False, inidividual player colors are used instead.

sessionteams: list[SessionTeam]
 996    @property
 997    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 998        """bascenev1.SessionTeams available in this lobby."""
 999        allteams = []
1000        for tref in self._sessionteams:
1001            team = tref()
1002            assert team is not None
1003            allteams.append(team)
1004        return allteams

bascenev1.SessionTeams available in this lobby.

def get_choosers(self) -> list[Chooser]:
1006    def get_choosers(self) -> list[Chooser]:
1007        """Return the lobby's current choosers."""
1008        return self.choosers

Return the lobby's current choosers.

def create_join_info(self) -> bascenev1._lobby.JoinInfo:
1010    def create_join_info(self) -> JoinInfo:
1011        """Create a display of on-screen information for joiners.
1012
1013        (how to switch teams, players, etc.)
1014        Intended for use in initial joining-screens.
1015        """
1016        return JoinInfo(self)

Create a display of on-screen information for joiners.

(how to switch teams, players, etc.) Intended for use in initial joining-screens.

def reload_profiles(self) -> None:
1018    def reload_profiles(self) -> None:
1019        """Reload available player profiles."""
1020        # pylint: disable=cyclic-import
1021        from bascenev1lib.actor.spazappearance import get_appearances
1022
1023        assert babase.app.classic is not None
1024
1025        # We may have gained or lost character names if the user
1026        # bought something; reload these too.
1027        self.character_names_local_unlocked = get_appearances()
1028        self.character_names_local_unlocked.sort(key=lambda x: x.lower())
1029
1030        # Do any overall prep we need to such as creating account profile.
1031        babase.app.classic.accounts.ensure_have_account_player_profile()
1032        for chooser in self.choosers:
1033            try:
1034                chooser.reload_profiles()
1035                chooser.update_from_profile()
1036            except Exception:
1037                logging.exception('Error reloading profiles.')

Reload available player profiles.

def update_positions(self) -> None:
1039    def update_positions(self) -> None:
1040        """Update positions for all choosers."""
1041        self._vpos = -100 + self.base_v_offset
1042        for chooser in self.choosers:
1043            chooser.set_vpos(self._vpos)
1044            chooser.update_position()
1045            self._vpos -= 48

Update positions for all choosers.

def check_all_ready(self) -> bool:
1047    def check_all_ready(self) -> bool:
1048        """Return whether all choosers are marked ready."""
1049        return all(chooser.ready for chooser in self.choosers)

Return whether all choosers are marked ready.

def add_chooser(self, sessionplayer: SessionPlayer) -> None:
1051    def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None:
1052        """Add a chooser to the lobby for the provided player."""
1053        self.choosers.append(
1054            Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
1055        )
1056        self._next_add_team = (self._next_add_team + 1) % len(
1057            self._sessionteams
1058        )
1059        self._vpos -= 48

Add a chooser to the lobby for the provided player.

def remove_chooser(self, player: SessionPlayer) -> None:
1061    def remove_chooser(self, player: bascenev1.SessionPlayer) -> None:
1062        """Remove a single player's chooser; does not kick them.
1063
1064        This is used when a player enters the game and no longer
1065        needs a chooser."""
1066        found = False
1067        chooser = None
1068        for chooser in self.choosers:
1069            if chooser.getplayer() is player:
1070                found = True
1071
1072                # Mark it as dead since there could be more
1073                # change-commands/etc coming in still for it;
1074                # want to avoid duplicate player-adds/etc.
1075                chooser.set_dead(True)
1076                self.choosers.remove(chooser)
1077                break
1078        if not found:
1079            logging.exception('remove_chooser did not find player %s.', player)
1080        elif chooser in self.choosers:
1081            logging.exception('chooser remains after removal for %s.', player)
1082        self.update_positions()

Remove a single player's chooser; does not kick them.

This is used when a player enters the game and no longer needs a chooser.

def remove_all_choosers(self) -> None:
1084    def remove_all_choosers(self) -> None:
1085        """Remove all choosers without kicking players.
1086
1087        This is called after all players check in and enter a game.
1088        """
1089        self.choosers = []
1090        self.update_positions()

Remove all choosers without kicking players.

This is called after all players check in and enter a game.

def remove_all_choosers_and_kick_players(self) -> None:
1092    def remove_all_choosers_and_kick_players(self) -> None:
1093        """Remove all player choosers and kick attached players."""
1094
1095        # Copy the list; it can change under us otherwise.
1096        for chooser in list(self.choosers):
1097            if chooser.sessionplayer:
1098                chooser.sessionplayer.remove_from_game()
1099        self.remove_all_choosers()

Remove all player choosers and kick attached players.

def ls_input_devices() -> None:
1491def ls_input_devices() -> None:
1492    """Print debugging info about game objects.
1493
1494    Category: **General Utility Functions**
1495
1496    This call only functions in debug builds of the game.
1497    It prints various info about the current object count, etc.
1498    """
1499    return None

Print debugging info about game objects.

Category: General Utility Functions

This call only functions in debug builds of the game. It prints various info about the current object count, etc.

def ls_objects() -> None:
1502def ls_objects() -> None:
1503    """Log debugging info about C++ level objects.
1504
1505    Category: **General Utility Functions**
1506
1507    This call only functions in debug builds of the game.
1508    It prints various info about the current object count, etc.
1509    """
1510    return None

Log debugging info about C++ level objects.

Category: General Utility Functions

This call only functions in debug builds of the game. It prints various info about the current object count, etc.

class Lstr:
440class Lstr:
441    """Used to define strings in a language-independent way.
442
443    Category: **General Utility Classes**
444
445    These should be used whenever possible in place of hard-coded
446    strings so that in-game or UI elements show up correctly on all
447    clients in their currently-active language.
448
449    To see available resource keys, look at any of the bs_language_*.py
450    files in the game or the translations pages at
451    legacy.ballistica.net/translate.
452
453    ##### Examples
454    EXAMPLE 1: specify a string from a resource path
455    >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
456
457    EXAMPLE 2: specify a translated string via a category and english
458    value; if a translated value is available, it will be used; otherwise
459    the english value will be. To see available translation categories,
460    look under the 'translations' resource section.
461    >>> mynode.text = babase.Lstr(translate=('gameDescriptions',
462    ...                                  'Defeat all enemies'))
463
464    EXAMPLE 3: specify a raw value and some substitutions. Substitutions
465    can be used with resource and translate modes as well.
466    >>> mynode.text = babase.Lstr(value='${A} / ${B}',
467    ...               subs=[('${A}', str(score)), ('${B}', str(total))])
468
469    EXAMPLE 4: babase.Lstr's can be nested. This example would display the
470    resource at res_a but replace ${NAME} with the value of the
471    resource at res_b
472    >>> mytextnode.text = babase.Lstr(
473    ...     resource='res_a',
474    ...     subs=[('${NAME}', babase.Lstr(resource='res_b'))])
475    """
476
477    # pylint: disable=dangerous-default-value
478    # noinspection PyDefaultArgument
479    @overload
480    def __init__(
481        self,
482        *,
483        resource: str,
484        fallback_resource: str = '',
485        fallback_value: str = '',
486        subs: Sequence[tuple[str, str | Lstr]] = [],
487    ) -> None:
488        """Create an Lstr from a string resource."""
489
490    # noinspection PyShadowingNames,PyDefaultArgument
491    @overload
492    def __init__(
493        self,
494        *,
495        translate: tuple[str, str],
496        subs: Sequence[tuple[str, str | Lstr]] = [],
497    ) -> None:
498        """Create an Lstr by translating a string in a category."""
499
500    # noinspection PyDefaultArgument
501    @overload
502    def __init__(
503        self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
504    ) -> None:
505        """Create an Lstr from a raw string value."""
506
507    # pylint: enable=redefined-outer-name, dangerous-default-value
508
509    def __init__(self, *args: Any, **keywds: Any) -> None:
510        """Instantiate a Lstr.
511
512        Pass a value for either 'resource', 'translate',
513        or 'value'. (see Lstr help for examples).
514        'subs' can be a sequence of 2-member sequences consisting of values
515        and replacements.
516        'fallback_resource' can be a resource key that will be used if the
517        main one is not present for
518        the current language in place of falling back to the english value
519        ('resource' mode only).
520        'fallback_value' can be a literal string that will be used if neither
521        the resource nor the fallback resource is found ('resource' mode only).
522        """
523        # pylint: disable=too-many-branches
524        if args:
525            raise TypeError('Lstr accepts only keyword arguments')
526
527        # Basically just store the exact args they passed.
528        # However if they passed any Lstr values for subs,
529        # replace them with that Lstr's dict.
530        self.args = keywds
531        our_type = type(self)
532
533        if isinstance(self.args.get('value'), our_type):
534            raise TypeError("'value' must be a regular string; not an Lstr")
535
536        if 'subs' in self.args:
537            subs_new = []
538            for key, value in keywds['subs']:
539                if isinstance(value, our_type):
540                    subs_new.append((key, value.args))
541                else:
542                    subs_new.append((key, value))
543            self.args['subs'] = subs_new
544
545        # As of protocol 31 we support compact key names
546        # ('t' instead of 'translate', etc). Convert as needed.
547        if 'translate' in keywds:
548            keywds['t'] = keywds['translate']
549            del keywds['translate']
550        if 'resource' in keywds:
551            keywds['r'] = keywds['resource']
552            del keywds['resource']
553        if 'value' in keywds:
554            keywds['v'] = keywds['value']
555            del keywds['value']
556        if 'fallback' in keywds:
557            from babase import _error
558
559            _error.print_error(
560                'deprecated "fallback" arg passed to Lstr(); use '
561                'either "fallback_resource" or "fallback_value"',
562                once=True,
563            )
564            keywds['f'] = keywds['fallback']
565            del keywds['fallback']
566        if 'fallback_resource' in keywds:
567            keywds['f'] = keywds['fallback_resource']
568            del keywds['fallback_resource']
569        if 'subs' in keywds:
570            keywds['s'] = keywds['subs']
571            del keywds['subs']
572        if 'fallback_value' in keywds:
573            keywds['fv'] = keywds['fallback_value']
574            del keywds['fallback_value']
575
576    def evaluate(self) -> str:
577        """Evaluate the Lstr and returns a flat string in the current language.
578
579        You should avoid doing this as much as possible and instead pass
580        and store Lstr values.
581        """
582        return _babase.evaluate_lstr(self._get_json())
583
584    def is_flat_value(self) -> bool:
585        """Return whether the Lstr is a 'flat' value.
586
587        This is defined as a simple string value incorporating no
588        translations, resources, or substitutions. In this case it may
589        be reasonable to replace it with a raw string value, perform
590        string manipulation on it, etc.
591        """
592        return bool('v' in self.args and not self.args.get('s', []))
593
594    def _get_json(self) -> str:
595        try:
596            return json.dumps(self.args, separators=(',', ':'))
597        except Exception:
598            from babase import _error
599
600            _error.print_exception('_get_json failed for', self.args)
601            return 'JSON_ERR'
602
603    @override
604    def __str__(self) -> str:
605        return '<ba.Lstr: ' + self._get_json() + '>'
606
607    @override
608    def __repr__(self) -> str:
609        return '<ba.Lstr: ' + self._get_json() + '>'
610
611    @staticmethod
612    def from_json(json_string: str) -> babase.Lstr:
613        """Given a json string, returns a babase.Lstr. Does no validation."""
614        lstr = Lstr(value='')
615        lstr.args = json.loads(json_string)
616        return lstr

Used to define strings in a language-independent way.

Category: General Utility Classes

These should be used whenever possible in place of hard-coded strings so that in-game or UI elements show up correctly on all clients in their currently-active language.

To see available resource keys, look at any of the bs_language_*.py files in the game or the translations pages at legacy.ballistica.net/translate.

Examples

EXAMPLE 1: specify a string from a resource path

>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')

EXAMPLE 2: specify a translated string via a category and english value; if a translated value is available, it will be used; otherwise the english value will be. To see available translation categories, look under the 'translations' resource section.

>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
...                                  'Defeat all enemies'))

EXAMPLE 3: specify a raw value and some substitutions. Substitutions can be used with resource and translate modes as well.

>>> mynode.text = babase.Lstr(value='${A} / ${B}',
...               subs=[('${A}', str(score)), ('${B}', str(total))])

EXAMPLE 4: babase.Lstr's can be nested. This example would display the resource at res_a but replace ${NAME} with the value of the resource at res_b

>>> mytextnode.text = babase.Lstr(
...     resource='res_a',
...     subs=[('${NAME}', babase.Lstr(resource='res_b'))])
Lstr(*args: Any, **keywds: Any)
509    def __init__(self, *args: Any, **keywds: Any) -> None:
510        """Instantiate a Lstr.
511
512        Pass a value for either 'resource', 'translate',
513        or 'value'. (see Lstr help for examples).
514        'subs' can be a sequence of 2-member sequences consisting of values
515        and replacements.
516        'fallback_resource' can be a resource key that will be used if the
517        main one is not present for
518        the current language in place of falling back to the english value
519        ('resource' mode only).
520        'fallback_value' can be a literal string that will be used if neither
521        the resource nor the fallback resource is found ('resource' mode only).
522        """
523        # pylint: disable=too-many-branches
524        if args:
525            raise TypeError('Lstr accepts only keyword arguments')
526
527        # Basically just store the exact args they passed.
528        # However if they passed any Lstr values for subs,
529        # replace them with that Lstr's dict.
530        self.args = keywds
531        our_type = type(self)
532
533        if isinstance(self.args.get('value'), our_type):
534            raise TypeError("'value' must be a regular string; not an Lstr")
535
536        if 'subs' in self.args:
537            subs_new = []
538            for key, value in keywds['subs']:
539                if isinstance(value, our_type):
540                    subs_new.append((key, value.args))
541                else:
542                    subs_new.append((key, value))
543            self.args['subs'] = subs_new
544
545        # As of protocol 31 we support compact key names
546        # ('t' instead of 'translate', etc). Convert as needed.
547        if 'translate' in keywds:
548            keywds['t'] = keywds['translate']
549            del keywds['translate']
550        if 'resource' in keywds:
551            keywds['r'] = keywds['resource']
552            del keywds['resource']
553        if 'value' in keywds:
554            keywds['v'] = keywds['value']
555            del keywds['value']
556        if 'fallback' in keywds:
557            from babase import _error
558
559            _error.print_error(
560                'deprecated "fallback" arg passed to Lstr(); use '
561                'either "fallback_resource" or "fallback_value"',
562                once=True,
563            )
564            keywds['f'] = keywds['fallback']
565            del keywds['fallback']
566        if 'fallback_resource' in keywds:
567            keywds['f'] = keywds['fallback_resource']
568            del keywds['fallback_resource']
569        if 'subs' in keywds:
570            keywds['s'] = keywds['subs']
571            del keywds['subs']
572        if 'fallback_value' in keywds:
573            keywds['fv'] = keywds['fallback_value']
574            del keywds['fallback_value']

Instantiate a Lstr.

Pass a value for either 'resource', 'translate', or 'value'. (see Lstr help for examples). 'subs' can be a sequence of 2-member sequences consisting of values and replacements. 'fallback_resource' can be a resource key that will be used if the main one is not present for the current language in place of falling back to the english value ('resource' mode only). 'fallback_value' can be a literal string that will be used if neither the resource nor the fallback resource is found ('resource' mode only).

args
def evaluate(self) -> str:
576    def evaluate(self) -> str:
577        """Evaluate the Lstr and returns a flat string in the current language.
578
579        You should avoid doing this as much as possible and instead pass
580        and store Lstr values.
581        """
582        return _babase.evaluate_lstr(self._get_json())

Evaluate the Lstr and returns a flat string in the current language.

You should avoid doing this as much as possible and instead pass and store Lstr values.

def is_flat_value(self) -> bool:
584    def is_flat_value(self) -> bool:
585        """Return whether the Lstr is a 'flat' value.
586
587        This is defined as a simple string value incorporating no
588        translations, resources, or substitutions. In this case it may
589        be reasonable to replace it with a raw string value, perform
590        string manipulation on it, etc.
591        """
592        return bool('v' in self.args and not self.args.get('s', []))

Return whether the Lstr is a 'flat' value.

This is defined as a simple string value incorporating no translations, resources, or substitutions. In this case it may be reasonable to replace it with a raw string value, perform string manipulation on it, etc.

@staticmethod
def from_json(json_string: str) -> Lstr:
611    @staticmethod
612    def from_json(json_string: str) -> babase.Lstr:
613        """Given a json string, returns a babase.Lstr. Does no validation."""
614        lstr = Lstr(value='')
615        lstr.args = json.loads(json_string)
616        return lstr

Given a json string, returns a babase.Lstr. Does no validation.

class Map(bascenev1.Actor):
 58class Map(Actor):
 59    """A game map.
 60
 61    Category: **Gameplay Classes**
 62
 63    Consists of a collection of terrain nodes, metadata, and other
 64    functionality comprising a game map.
 65    """
 66
 67    defs: Any = None
 68    name = 'Map'
 69    _playtypes: list[str] = []
 70
 71    @classmethod
 72    def preload(cls) -> None:
 73        """Preload map media.
 74
 75        This runs the class's on_preload() method as needed to prep it to run.
 76        Preloading should generally be done in a bascenev1.Activity's
 77        __init__ method. Note that this is a classmethod since it is not
 78        operate on map instances but rather on the class itself before
 79        instances are made
 80        """
 81        activity = _bascenev1.getactivity()
 82        if cls not in activity.preloads:
 83            activity.preloads[cls] = cls.on_preload()
 84
 85    @classmethod
 86    def get_play_types(cls) -> list[str]:
 87        """Return valid play types for this map."""
 88        return []
 89
 90    @classmethod
 91    def get_preview_texture_name(cls) -> str | None:
 92        """Return the name of the preview texture for this map."""
 93        return None
 94
 95    @classmethod
 96    def on_preload(cls) -> Any:
 97        """Called when the map is being preloaded.
 98
 99        It should return any media/data it requires to operate
100        """
101        return None
102
103    @classmethod
104    def getname(cls) -> str:
105        """Return the unique name of this map, in English."""
106        return cls.name
107
108    @classmethod
109    def get_music_type(cls) -> bascenev1.MusicType | None:
110        """Return a music-type string that should be played on this map.
111
112        If None is returned, default music will be used.
113        """
114        return None
115
116    def __init__(
117        self, vr_overlay_offset: Sequence[float] | None = None
118    ) -> None:
119        """Instantiate a map."""
120        super().__init__()
121
122        # This is expected to always be a bascenev1.Node object
123        # (whether valid or not) should be set to something meaningful
124        # by child classes.
125        self.node: _bascenev1.Node | None = None
126
127        # Make our class' preload-data available to us
128        # (and instruct the user if we weren't preloaded properly).
129        try:
130            self.preloaddata = _bascenev1.getactivity().preloads[type(self)]
131        except Exception as exc:
132            raise babase.NotFoundError(
133                'Preload data not found for '
134                + str(type(self))
135                + '; make sure to call the type\'s preload()'
136                ' staticmethod in the activity constructor'
137            ) from exc
138
139        # Set various globals.
140        gnode = _bascenev1.getactivity().globalsnode
141
142        # Set area-of-interest bounds.
143        aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
144        if aoi_bounds is None:
145            print('WARNING: no "aoi_bounds" found for map:', self.getname())
146            aoi_bounds = (-1, -1, -1, 1, 1, 1)
147        gnode.area_of_interest_bounds = aoi_bounds
148
149        # Set map bounds.
150        map_bounds = self.get_def_bound_box('map_bounds')
151        if map_bounds is None:
152            print('WARNING: no "map_bounds" found for map:', self.getname())
153            map_bounds = (-30, -10, -30, 30, 100, 30)
154        _bascenev1.set_map_bounds(map_bounds)
155
156        # Set shadow ranges.
157        try:
158            gnode.shadow_range = [
159                self.defs.points[v][1]
160                for v in [
161                    'shadow_lower_bottom',
162                    'shadow_lower_top',
163                    'shadow_upper_bottom',
164                    'shadow_upper_top',
165                ]
166            ]
167        except Exception:
168            pass
169
170        # In vr, set a fixed point in space for the overlay to show up at.
171        # By default we use the bounds center but allow the map to override it.
172        center = (
173            (aoi_bounds[0] + aoi_bounds[3]) * 0.5,
174            (aoi_bounds[1] + aoi_bounds[4]) * 0.5,
175            (aoi_bounds[2] + aoi_bounds[5]) * 0.5,
176        )
177        if vr_overlay_offset is not None:
178            center = (
179                center[0] + vr_overlay_offset[0],
180                center[1] + vr_overlay_offset[1],
181                center[2] + vr_overlay_offset[2],
182            )
183        gnode.vr_overlay_center = center
184        gnode.vr_overlay_center_enabled = True
185
186        self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
187        self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
188            (0, 0, 0, 0, 0, 0)
189        ]
190        self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
191            (0, 0, 0, 0, 0, 0)
192        ]
193        self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
194
195        # We just want points.
196        self.flag_points = [p[:3] for p in self.flag_points]
197        self.flag_points_default = self.get_def_point('flag_default') or (
198            0,
199            1,
200            0,
201        )
202        self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
203            (0, 0, 0)
204        ]
205
206        # We just want points.
207        self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
208        self.tnt_points = self.get_def_points('tnt') or []
209
210        # We just want points.
211        self.tnt_points = [p[:3] for p in self.tnt_points]
212
213        self.is_hockey = False
214        self.is_flying = False
215
216        # FIXME: this should be part of game; not map.
217        # Let's select random index for first spawn point,
218        # so that no one is offended by the constant spawn on the edge.
219        self._next_ffa_start_index = random.randrange(
220            len(self.ffa_spawn_points)
221        )
222
223    def is_point_near_edge(
224        self, point: babase.Vec3, running: bool = False
225    ) -> bool:
226        """Return whether the provided point is near an edge of the map.
227
228        Simple bot logic uses this call to determine if they
229        are approaching a cliff or wall. If this returns True they will
230        generally not walk/run any farther away from the origin.
231        If 'running' is True, the buffer should be a bit larger.
232        """
233        del point, running  # Unused.
234        return False
235
236    def get_def_bound_box(
237        self, name: str
238    ) -> tuple[float, float, float, float, float, float] | None:
239        """Return a 6 member bounds tuple or None if it is not defined."""
240        try:
241            box = self.defs.boxes[name]
242            return (
243                box[0] - box[6] / 2.0,
244                box[1] - box[7] / 2.0,
245                box[2] - box[8] / 2.0,
246                box[0] + box[6] / 2.0,
247                box[1] + box[7] / 2.0,
248                box[2] + box[8] / 2.0,
249            )
250        except Exception:
251            return None
252
253    def get_def_point(self, name: str) -> Sequence[float] | None:
254        """Return a single defined point or a default value in its absence."""
255        val = self.defs.points.get(name)
256        return (
257            None
258            if val is None
259            else babase.vec3validate(val) if __debug__ else val
260        )
261
262    def get_def_points(self, name: str) -> list[Sequence[float]]:
263        """Return a list of named points.
264
265        Return as many sequential ones are defined (flag1, flag2, flag3), etc.
266        If none are defined, returns an empty list.
267        """
268        point_list = []
269        if self.defs and name + '1' in self.defs.points:
270            i = 1
271            while name + str(i) in self.defs.points:
272                pts = self.defs.points[name + str(i)]
273                if len(pts) == 6:
274                    point_list.append(pts)
275                else:
276                    if len(pts) != 3:
277                        raise ValueError('invalid point')
278                    point_list.append(pts + (0, 0, 0))
279                i += 1
280        return point_list
281
282    def get_start_position(self, team_index: int) -> Sequence[float]:
283        """Return a random starting position for the given team index."""
284        pnt = self.spawn_points[team_index % len(self.spawn_points)]
285        x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3])
286        z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5])
287        pnt = (
288            pnt[0] + random.uniform(*x_range),
289            pnt[1],
290            pnt[2] + random.uniform(*z_range),
291        )
292        return pnt
293
294    def get_ffa_start_position(
295        self, players: Sequence[bascenev1.Player]
296    ) -> Sequence[float]:
297        """Return a random starting position in one of the FFA spawn areas.
298
299        If a list of bascenev1.Player-s is provided; the returned points
300        will be as far from these players as possible.
301        """
302
303        # Get positions for existing players.
304        player_pts = []
305        for player in players:
306            if player.is_alive():
307                player_pts.append(player.position)
308
309        def _getpt() -> Sequence[float]:
310            point = self.ffa_spawn_points[self._next_ffa_start_index]
311            self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
312                self.ffa_spawn_points
313            )
314            x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3])
315            z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5])
316            point = (
317                point[0] + random.uniform(*x_range),
318                point[1],
319                point[2] + random.uniform(*z_range),
320            )
321            return point
322
323        if not player_pts:
324            return _getpt()
325
326        # Let's calc several start points and then pick whichever is
327        # farthest from all existing players.
328        farthestpt_dist = -1.0
329        farthestpt = None
330        for _i in range(10):
331            testpt = babase.Vec3(_getpt())
332            closest_player_dist = 9999.0
333            for ppt in player_pts:
334                dist = (ppt - testpt).length()
335                closest_player_dist = min(dist, closest_player_dist)
336            if closest_player_dist > farthestpt_dist:
337                farthestpt_dist = closest_player_dist
338                farthestpt = testpt
339        assert farthestpt is not None
340        return tuple(farthestpt)
341
342    def get_flag_position(
343        self, team_index: int | None = None
344    ) -> Sequence[float]:
345        """Return a flag position on the map for the given team index.
346
347        Pass None to get the default flag point.
348        (used for things such as king-of-the-hill)
349        """
350        if team_index is None:
351            return self.flag_points_default[:3]
352        return self.flag_points[team_index % len(self.flag_points)][:3]
353
354    @override
355    def exists(self) -> bool:
356        return bool(self.node)
357
358    @override
359    def handlemessage(self, msg: Any) -> Any:
360        from bascenev1 import _messages
361
362        if isinstance(msg, _messages.DieMessage):
363            if self.node:
364                self.node.delete()
365        else:
366            return super().handlemessage(msg)
367        return None

A game map.

Category: Gameplay Classes

Consists of a collection of terrain nodes, metadata, and other functionality comprising a game map.

Map(vr_overlay_offset: Optional[Sequence[float]] = None)
116    def __init__(
117        self, vr_overlay_offset: Sequence[float] | None = None
118    ) -> None:
119        """Instantiate a map."""
120        super().__init__()
121
122        # This is expected to always be a bascenev1.Node object
123        # (whether valid or not) should be set to something meaningful
124        # by child classes.
125        self.node: _bascenev1.Node | None = None
126
127        # Make our class' preload-data available to us
128        # (and instruct the user if we weren't preloaded properly).
129        try:
130            self.preloaddata = _bascenev1.getactivity().preloads[type(self)]
131        except Exception as exc:
132            raise babase.NotFoundError(
133                'Preload data not found for '
134                + str(type(self))
135                + '; make sure to call the type\'s preload()'
136                ' staticmethod in the activity constructor'
137            ) from exc
138
139        # Set various globals.
140        gnode = _bascenev1.getactivity().globalsnode
141
142        # Set area-of-interest bounds.
143        aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
144        if aoi_bounds is None:
145            print('WARNING: no "aoi_bounds" found for map:', self.getname())
146            aoi_bounds = (-1, -1, -1, 1, 1, 1)
147        gnode.area_of_interest_bounds = aoi_bounds
148
149        # Set map bounds.
150        map_bounds = self.get_def_bound_box('map_bounds')
151        if map_bounds is None:
152            print('WARNING: no "map_bounds" found for map:', self.getname())
153            map_bounds = (-30, -10, -30, 30, 100, 30)
154        _bascenev1.set_map_bounds(map_bounds)
155
156        # Set shadow ranges.
157        try:
158            gnode.shadow_range = [
159                self.defs.points[v][1]
160                for v in [
161                    'shadow_lower_bottom',
162                    'shadow_lower_top',
163                    'shadow_upper_bottom',
164                    'shadow_upper_top',
165                ]
166            ]
167        except Exception:
168            pass
169
170        # In vr, set a fixed point in space for the overlay to show up at.
171        # By default we use the bounds center but allow the map to override it.
172        center = (
173            (aoi_bounds[0] + aoi_bounds[3]) * 0.5,
174            (aoi_bounds[1] + aoi_bounds[4]) * 0.5,
175            (aoi_bounds[2] + aoi_bounds[5]) * 0.5,
176        )
177        if vr_overlay_offset is not None:
178            center = (
179                center[0] + vr_overlay_offset[0],
180                center[1] + vr_overlay_offset[1],
181                center[2] + vr_overlay_offset[2],
182            )
183        gnode.vr_overlay_center = center
184        gnode.vr_overlay_center_enabled = True
185
186        self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
187        self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
188            (0, 0, 0, 0, 0, 0)
189        ]
190        self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
191            (0, 0, 0, 0, 0, 0)
192        ]
193        self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
194
195        # We just want points.
196        self.flag_points = [p[:3] for p in self.flag_points]
197        self.flag_points_default = self.get_def_point('flag_default') or (
198            0,
199            1,
200            0,
201        )
202        self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
203            (0, 0, 0)
204        ]
205
206        # We just want points.
207        self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
208        self.tnt_points = self.get_def_points('tnt') or []
209
210        # We just want points.
211        self.tnt_points = [p[:3] for p in self.tnt_points]
212
213        self.is_hockey = False
214        self.is_flying = False
215
216        # FIXME: this should be part of game; not map.
217        # Let's select random index for first spawn point,
218        # so that no one is offended by the constant spawn on the edge.
219        self._next_ffa_start_index = random.randrange(
220            len(self.ffa_spawn_points)
221        )

Instantiate a map.

defs: Any = None
name = 'Map'
@classmethod
def preload(cls) -> None:
71    @classmethod
72    def preload(cls) -> None:
73        """Preload map media.
74
75        This runs the class's on_preload() method as needed to prep it to run.
76        Preloading should generally be done in a bascenev1.Activity's
77        __init__ method. Note that this is a classmethod since it is not
78        operate on map instances but rather on the class itself before
79        instances are made
80        """
81        activity = _bascenev1.getactivity()
82        if cls not in activity.preloads:
83            activity.preloads[cls] = cls.on_preload()

Preload map media.

This runs the class's on_preload() method as needed to prep it to run. Preloading should generally be done in a bascenev1.Activity's __init__ method. Note that this is a classmethod since it is not operate on map instances but rather on the class itself before instances are made

@classmethod
def get_play_types(cls) -> list[str]:
85    @classmethod
86    def get_play_types(cls) -> list[str]:
87        """Return valid play types for this map."""
88        return []

Return valid play types for this map.

@classmethod
def get_preview_texture_name(cls) -> str | None:
90    @classmethod
91    def get_preview_texture_name(cls) -> str | None:
92        """Return the name of the preview texture for this map."""
93        return None

Return the name of the preview texture for this map.

@classmethod
def on_preload(cls) -> Any:
 95    @classmethod
 96    def on_preload(cls) -> Any:
 97        """Called when the map is being preloaded.
 98
 99        It should return any media/data it requires to operate
100        """
101        return None

Called when the map is being preloaded.

It should return any media/data it requires to operate

@classmethod
def getname(cls) -> str:
103    @classmethod
104    def getname(cls) -> str:
105        """Return the unique name of this map, in English."""
106        return cls.name

Return the unique name of this map, in English.

@classmethod
def get_music_type(cls) -> MusicType | None:
108    @classmethod
109    def get_music_type(cls) -> bascenev1.MusicType | None:
110        """Return a music-type string that should be played on this map.
111
112        If None is returned, default music will be used.
113        """
114        return None

Return a music-type string that should be played on this map.

If None is returned, default music will be used.

node: Node | None
spawn_points
ffa_spawn_points
spawn_by_flag_points
flag_points
flag_points_default
powerup_spawn_points
tnt_points
is_hockey
is_flying
def is_point_near_edge(self, point: Vec3, running: bool = False) -> bool:
223    def is_point_near_edge(
224        self, point: babase.Vec3, running: bool = False
225    ) -> bool:
226        """Return whether the provided point is near an edge of the map.
227
228        Simple bot logic uses this call to determine if they
229        are approaching a cliff or wall. If this returns True they will
230        generally not walk/run any farther away from the origin.
231        If 'running' is True, the buffer should be a bit larger.
232        """
233        del point, running  # Unused.
234        return False

Return whether the provided point is near an edge of the map.

Simple bot logic uses this call to determine if they are approaching a cliff or wall. If this returns True they will generally not walk/run any farther away from the origin. If 'running' is True, the buffer should be a bit larger.

def get_def_bound_box( self, name: str) -> tuple[float, float, float, float, float, float] | None:
236    def get_def_bound_box(
237        self, name: str
238    ) -> tuple[float, float, float, float, float, float] | None:
239        """Return a 6 member bounds tuple or None if it is not defined."""
240        try:
241            box = self.defs.boxes[name]
242            return (
243                box[0] - box[6] / 2.0,
244                box[1] - box[7] / 2.0,
245                box[2] - box[8] / 2.0,
246                box[0] + box[6] / 2.0,
247                box[1] + box[7] / 2.0,
248                box[2] + box[8] / 2.0,
249            )
250        except Exception:
251            return None

Return a 6 member bounds tuple or None if it is not defined.

def get_def_point(self, name: str) -> Optional[Sequence[float]]:
253    def get_def_point(self, name: str) -> Sequence[float] | None:
254        """Return a single defined point or a default value in its absence."""
255        val = self.defs.points.get(name)
256        return (
257            None
258            if val is None
259            else babase.vec3validate(val) if __debug__ else val
260        )

Return a single defined point or a default value in its absence.

def get_def_points(self, name: str) -> list[typing.Sequence[float]]:
262    def get_def_points(self, name: str) -> list[Sequence[float]]:
263        """Return a list of named points.
264
265        Return as many sequential ones are defined (flag1, flag2, flag3), etc.
266        If none are defined, returns an empty list.
267        """
268        point_list = []
269        if self.defs and name + '1' in self.defs.points:
270            i = 1
271            while name + str(i) in self.defs.points:
272                pts = self.defs.points[name + str(i)]
273                if len(pts) == 6:
274                    point_list.append(pts)
275                else:
276                    if len(pts) != 3:
277                        raise ValueError('invalid point')
278                    point_list.append(pts + (0, 0, 0))
279                i += 1
280        return point_list

Return a list of named points.

Return as many sequential ones are defined (flag1, flag2, flag3), etc. If none are defined, returns an empty list.

def get_start_position(self, team_index: int) -> Sequence[float]:
282    def get_start_position(self, team_index: int) -> Sequence[float]:
283        """Return a random starting position for the given team index."""
284        pnt = self.spawn_points[team_index % len(self.spawn_points)]
285        x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3])
286        z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5])
287        pnt = (
288            pnt[0] + random.uniform(*x_range),
289            pnt[1],
290            pnt[2] + random.uniform(*z_range),
291        )
292        return pnt

Return a random starting position for the given team index.

def get_ffa_start_position(self, players: Sequence[Player]) -> Sequence[float]:
294    def get_ffa_start_position(
295        self, players: Sequence[bascenev1.Player]
296    ) -> Sequence[float]:
297        """Return a random starting position in one of the FFA spawn areas.
298
299        If a list of bascenev1.Player-s is provided; the returned points
300        will be as far from these players as possible.
301        """
302
303        # Get positions for existing players.
304        player_pts = []
305        for player in players:
306            if player.is_alive():
307                player_pts.append(player.position)
308
309        def _getpt() -> Sequence[float]:
310            point = self.ffa_spawn_points[self._next_ffa_start_index]
311            self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
312                self.ffa_spawn_points
313            )
314            x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3])
315            z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5])
316            point = (
317                point[0] + random.uniform(*x_range),
318                point[1],
319                point[2] + random.uniform(*z_range),
320            )
321            return point
322
323        if not player_pts:
324            return _getpt()
325
326        # Let's calc several start points and then pick whichever is
327        # farthest from all existing players.
328        farthestpt_dist = -1.0
329        farthestpt = None
330        for _i in range(10):
331            testpt = babase.Vec3(_getpt())
332            closest_player_dist = 9999.0
333            for ppt in player_pts:
334                dist = (ppt - testpt).length()
335                closest_player_dist = min(dist, closest_player_dist)
336            if closest_player_dist > farthestpt_dist:
337                farthestpt_dist = closest_player_dist
338                farthestpt = testpt
339        assert farthestpt is not None
340        return tuple(farthestpt)

Return a random starting position in one of the FFA spawn areas.

If a list of bascenev1.Player-s is provided; the returned points will be as far from these players as possible.

def get_flag_position(self, team_index: int | None = None) -> Sequence[float]:
342    def get_flag_position(
343        self, team_index: int | None = None
344    ) -> Sequence[float]:
345        """Return a flag position on the map for the given team index.
346
347        Pass None to get the default flag point.
348        (used for things such as king-of-the-hill)
349        """
350        if team_index is None:
351            return self.flag_points_default[:3]
352        return self.flag_points[team_index % len(self.flag_points)][:3]

Return a flag position on the map for the given team index.

Pass None to get the default flag point. (used for things such as king-of-the-hill)

@override
def exists(self) -> bool:
354    @override
355    def exists(self) -> bool:
356        return bool(self.node)

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

@override
def handlemessage(self, msg: Any) -> Any:
358    @override
359    def handlemessage(self, msg: Any) -> Any:
360        from bascenev1 import _messages
361
362        if isinstance(msg, _messages.DieMessage):
363            if self.node:
364                self.node.delete()
365        else:
366            return super().handlemessage(msg)
367        return None

General message handling; can be passed any message object.

class Material:
271class Material:
272    """An entity applied to game objects to modify collision behavior.
273
274    Category: **Gameplay Classes**
275
276    A material can affect physical characteristics, generate sounds,
277    or trigger callback functions when collisions occur.
278
279    Materials are applied to 'parts', which are groups of one or more
280    rigid bodies created as part of a bascenev1.Node. Nodes can have any
281    number of parts, each with its own set of materials. Generally
282    materials are specified as array attributes on the Node. The `spaz`
283    node, for example, has various attributes such as `materials`,
284    `roller_materials`, and `punch_materials`, which correspond
285    to the various parts it creates.
286
287    Use bascenev1.Material to instantiate a blank material, and then use
288    its babase.Material.add_actions() method to define what the material
289    does.
290    """
291
292    def __init__(self, label: str | None = None) -> None:
293        pass
294
295    label: str
296    """A label for the material; only used for debugging."""
297
298    def add_actions(
299        self, actions: tuple, conditions: tuple | None = None
300    ) -> None:
301        """Add one or more actions to the material, optionally with conditions.
302
303        ##### Conditions
304        Conditions are provided as tuples which can be combined
305        to form boolean logic. A single condition might look like
306        `('condition_name', cond_arg)`, or a more complex nested one
307        might look like `(('some_condition', cond_arg), 'or',
308        ('another_condition', cond2_arg))`.
309
310        `'and'`, `'or'`, and `'xor'` are available to chain
311        together 2 conditions, as seen above.
312
313        ##### Available Conditions
314        ###### `('they_have_material', material)`
315        > Does the part we're hitting have a given bascenev1.Material?
316
317        ###### `('they_dont_have_material', material)`
318        > Does the part we're hitting not have a given bascenev1.Material?
319
320        ###### `('eval_colliding')`
321        > Is `'collide'` true at this point
322        in material evaluation? (see the `modify_part_collision` action)
323
324        ###### `('eval_not_colliding')`
325        > Is 'collide' false at this point
326        in material evaluation? (see the `modify_part_collision` action)
327
328        ###### `('we_are_younger_than', age)`
329        > Is our part younger than `age` (in milliseconds)?
330
331        ###### `('we_are_older_than', age)`
332        > Is our part older than `age` (in milliseconds)?
333
334        ###### `('they_are_younger_than', age)`
335        > Is the part we're hitting younger than `age` (in milliseconds)?
336
337        ###### `('they_are_older_than', age)`
338        > Is the part we're hitting older than `age` (in milliseconds)?
339
340        ###### `('they_are_same_node_as_us')`
341        > Does the part we're hitting belong to the same bascenev1.Node as us?
342
343        ###### `('they_are_different_node_than_us')`
344        > Does the part we're hitting belong to a different bascenev1.Node?
345
346        ##### Actions
347        In a similar manner, actions are specified as tuples.
348        Multiple actions can be specified by providing a tuple
349        of tuples.
350
351        ##### Available Actions
352        ###### `('call', when, callable)`
353        > Calls the provided callable;
354        `when` can be either `'at_connect'` or `'at_disconnect'`.
355        `'at_connect'` means to fire
356        when the two parts first come in contact; `'at_disconnect'`
357        means to fire once they cease being in contact.
358
359        ###### `('message', who, when, message_obj)`
360        > Sends a message object;
361        `who` can be either `'our_node'` or `'their_node'`, `when` can be
362        `'at_connect'` or `'at_disconnect'`, and `message_obj` is the message
363        object to send.
364        This has the same effect as calling the node's
365        babase.Node.handlemessage() method.
366
367        ###### `('modify_part_collision', attr, value)`
368        > Changes some
369        characteristic of the physical collision that will occur between
370        our part and their part. This change will remain in effect as
371        long as the two parts remain overlapping. This means if you have a
372        part with a material that turns `'collide'` off against parts
373        younger than 100ms, and it touches another part that is 50ms old,
374        it will continue to not collide with that part until they separate,
375        even if the 100ms threshold is passed. Options for attr/value are:
376        `'physical'` (boolean value; whether a *physical* response will
377        occur at all), `'friction'` (float value; how friction-y the
378        physical response will be), `'collide'` (boolean value;
379        whether *any* collision will occur at all, including non-physical
380        stuff like callbacks), `'use_node_collide'`
381        (boolean value; whether to honor modify_node_collision
382        overrides for this collision), `'stiffness'` (float value,
383        how springy the physical response is), `'damping'` (float
384        value, how damped the physical response is), `'bounce'` (float
385        value; how bouncy the physical response is).
386
387        ###### `('modify_node_collision', attr, value)`
388        > Similar to
389        `modify_part_collision`, but operates at a node-level.
390        collision attributes set here will remain in effect as long as
391        *anything* from our part's node and their part's node overlap.
392        A key use of this functionality is to prevent new nodes from
393        colliding with each other if they appear overlapped;
394        if `modify_part_collision` is used, only the individual
395        parts that were overlapping would avoid contact, but other parts
396        could still contact leaving the two nodes 'tangled up'. Using
397        `modify_node_collision` ensures that the nodes must completely
398        separate before they can start colliding. Currently the only attr
399        available here is `'collide'` (a boolean value).
400
401        ###### `('sound', sound, volume)`
402        > Plays a bascenev1.Sound when a collision
403        occurs, at a given volume, regardless of the collision speed/etc.
404
405        ###### `('impact_sound', sound, targetImpulse, volume)`
406        > Plays a sound
407        when a collision occurs, based on the speed of impact.
408        Provide a bascenev1.Sound, a target-impulse, and a volume.
409
410        ###### `('skid_sound', sound, targetImpulse, volume)`
411        > Plays a sound
412        during a collision when parts are 'scraping' against each other.
413        Provide a bascenev1.Sound, a target-impulse, and a volume.
414
415        ###### `('roll_sound', sound, targetImpulse, volume)`
416        > Plays a sound
417        during a collision when parts are 'rolling' against each other.
418        Provide a bascenev1.Sound, a target-impulse, and a volume.
419
420        ##### Examples
421        **Example 1:** create a material that lets us ignore
422        collisions against any nodes we touch in the first
423        100 ms of our existence; handy for preventing us from
424        exploding outward if we spawn on top of another object:
425        >>> m = bascenev1.Material()
426        ... m.add_actions(
427        ...     conditions=(('we_are_younger_than', 100),
428        ...                 'or', ('they_are_younger_than', 100)),
429        ...     actions=('modify_node_collision', 'collide', False))
430
431        **Example 2:** send a bascenev1.DieMessage to anything we touch, but
432        cause no physical response. This should cause any bascenev1.Actor to
433        drop dead:
434        >>> m = bascenev1.Material()
435        ... m.add_actions(
436        ...     actions=(('modify_part_collision', 'physical', False),
437        ...              ('message', 'their_node', 'at_connect',
438        ...                  bascenev1.DieMessage())))
439
440        **Example 3:** play some sounds when we're contacting the ground:
441        >>> m = bascenev1.Material()
442        ... m.add_actions(
443        ...     conditions=('they_have_material', shared.footing_material),
444        ...     actions=(
445                  ('impact_sound', bascenev1.getsound('metalHit'), 2, 5),
446                  ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)))
447        """
448        return None

An entity applied to game objects to modify collision behavior.

Category: Gameplay Classes

A material can affect physical characteristics, generate sounds, or trigger callback functions when collisions occur.

Materials are applied to 'parts', which are groups of one or more rigid bodies created as part of a bascenev1.Node. Nodes can have any number of parts, each with its own set of materials. Generally materials are specified as array attributes on the Node. The spaz node, for example, has various attributes such as materials, roller_materials, and punch_materials, which correspond to the various parts it creates.

Use bascenev1.Material to instantiate a blank material, and then use its babase.Material.add_actions() method to define what the material does.

Material(label: str | None = None)
292    def __init__(self, label: str | None = None) -> None:
293        pass
label: str

A label for the material; only used for debugging.

def add_actions(self, actions: tuple, conditions: tuple | None = None) -> None:
298    def add_actions(
299        self, actions: tuple, conditions: tuple | None = None
300    ) -> None:
301        """Add one or more actions to the material, optionally with conditions.
302
303        ##### Conditions
304        Conditions are provided as tuples which can be combined
305        to form boolean logic. A single condition might look like
306        `('condition_name', cond_arg)`, or a more complex nested one
307        might look like `(('some_condition', cond_arg), 'or',
308        ('another_condition', cond2_arg))`.
309
310        `'and'`, `'or'`, and `'xor'` are available to chain
311        together 2 conditions, as seen above.
312
313        ##### Available Conditions
314        ###### `('they_have_material', material)`
315        > Does the part we're hitting have a given bascenev1.Material?
316
317        ###### `('they_dont_have_material', material)`
318        > Does the part we're hitting not have a given bascenev1.Material?
319
320        ###### `('eval_colliding')`
321        > Is `'collide'` true at this point
322        in material evaluation? (see the `modify_part_collision` action)
323
324        ###### `('eval_not_colliding')`
325        > Is 'collide' false at this point
326        in material evaluation? (see the `modify_part_collision` action)
327
328        ###### `('we_are_younger_than', age)`
329        > Is our part younger than `age` (in milliseconds)?
330
331        ###### `('we_are_older_than', age)`
332        > Is our part older than `age` (in milliseconds)?
333
334        ###### `('they_are_younger_than', age)`
335        > Is the part we're hitting younger than `age` (in milliseconds)?
336
337        ###### `('they_are_older_than', age)`
338        > Is the part we're hitting older than `age` (in milliseconds)?
339
340        ###### `('they_are_same_node_as_us')`
341        > Does the part we're hitting belong to the same bascenev1.Node as us?
342
343        ###### `('they_are_different_node_than_us')`
344        > Does the part we're hitting belong to a different bascenev1.Node?
345
346        ##### Actions
347        In a similar manner, actions are specified as tuples.
348        Multiple actions can be specified by providing a tuple
349        of tuples.
350
351        ##### Available Actions
352        ###### `('call', when, callable)`
353        > Calls the provided callable;
354        `when` can be either `'at_connect'` or `'at_disconnect'`.
355        `'at_connect'` means to fire
356        when the two parts first come in contact; `'at_disconnect'`
357        means to fire once they cease being in contact.
358
359        ###### `('message', who, when, message_obj)`
360        > Sends a message object;
361        `who` can be either `'our_node'` or `'their_node'`, `when` can be
362        `'at_connect'` or `'at_disconnect'`, and `message_obj` is the message
363        object to send.
364        This has the same effect as calling the node's
365        babase.Node.handlemessage() method.
366
367        ###### `('modify_part_collision', attr, value)`
368        > Changes some
369        characteristic of the physical collision that will occur between
370        our part and their part. This change will remain in effect as
371        long as the two parts remain overlapping. This means if you have a
372        part with a material that turns `'collide'` off against parts
373        younger than 100ms, and it touches another part that is 50ms old,
374        it will continue to not collide with that part until they separate,
375        even if the 100ms threshold is passed. Options for attr/value are:
376        `'physical'` (boolean value; whether a *physical* response will
377        occur at all), `'friction'` (float value; how friction-y the
378        physical response will be), `'collide'` (boolean value;
379        whether *any* collision will occur at all, including non-physical
380        stuff like callbacks), `'use_node_collide'`
381        (boolean value; whether to honor modify_node_collision
382        overrides for this collision), `'stiffness'` (float value,
383        how springy the physical response is), `'damping'` (float
384        value, how damped the physical response is), `'bounce'` (float
385        value; how bouncy the physical response is).
386
387        ###### `('modify_node_collision', attr, value)`
388        > Similar to
389        `modify_part_collision`, but operates at a node-level.
390        collision attributes set here will remain in effect as long as
391        *anything* from our part's node and their part's node overlap.
392        A key use of this functionality is to prevent new nodes from
393        colliding with each other if they appear overlapped;
394        if `modify_part_collision` is used, only the individual
395        parts that were overlapping would avoid contact, but other parts
396        could still contact leaving the two nodes 'tangled up'. Using
397        `modify_node_collision` ensures that the nodes must completely
398        separate before they can start colliding. Currently the only attr
399        available here is `'collide'` (a boolean value).
400
401        ###### `('sound', sound, volume)`
402        > Plays a bascenev1.Sound when a collision
403        occurs, at a given volume, regardless of the collision speed/etc.
404
405        ###### `('impact_sound', sound, targetImpulse, volume)`
406        > Plays a sound
407        when a collision occurs, based on the speed of impact.
408        Provide a bascenev1.Sound, a target-impulse, and a volume.
409
410        ###### `('skid_sound', sound, targetImpulse, volume)`
411        > Plays a sound
412        during a collision when parts are 'scraping' against each other.
413        Provide a bascenev1.Sound, a target-impulse, and a volume.
414
415        ###### `('roll_sound', sound, targetImpulse, volume)`
416        > Plays a sound
417        during a collision when parts are 'rolling' against each other.
418        Provide a bascenev1.Sound, a target-impulse, and a volume.
419
420        ##### Examples
421        **Example 1:** create a material that lets us ignore
422        collisions against any nodes we touch in the first
423        100 ms of our existence; handy for preventing us from
424        exploding outward if we spawn on top of another object:
425        >>> m = bascenev1.Material()
426        ... m.add_actions(
427        ...     conditions=(('we_are_younger_than', 100),
428        ...                 'or', ('they_are_younger_than', 100)),
429        ...     actions=('modify_node_collision', 'collide', False))
430
431        **Example 2:** send a bascenev1.DieMessage to anything we touch, but
432        cause no physical response. This should cause any bascenev1.Actor to
433        drop dead:
434        >>> m = bascenev1.Material()
435        ... m.add_actions(
436        ...     actions=(('modify_part_collision', 'physical', False),
437        ...              ('message', 'their_node', 'at_connect',
438        ...                  bascenev1.DieMessage())))
439
440        **Example 3:** play some sounds when we're contacting the ground:
441        >>> m = bascenev1.Material()
442        ... m.add_actions(
443        ...     conditions=('they_have_material', shared.footing_material),
444        ...     actions=(
445                  ('impact_sound', bascenev1.getsound('metalHit'), 2, 5),
446                  ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)))
447        """
448        return None

Add one or more actions to the material, optionally with conditions.

Conditions

Conditions are provided as tuples which can be combined to form boolean logic. A single condition might look like ('condition_name', cond_arg), or a more complex nested one might look like (('some_condition', cond_arg), 'or', ('another_condition', cond2_arg)).

'and', 'or', and 'xor' are available to chain together 2 conditions, as seen above.

Available Conditions
('they_have_material', material)

Does the part we're hitting have a given bascenev1.Material?

('they_dont_have_material', material)

Does the part we're hitting not have a given bascenev1.Material?

('eval_colliding')

Is 'collide' true at this point in material evaluation? (see the modify_part_collision action)

('eval_not_colliding')

Is 'collide' false at this point in material evaluation? (see the modify_part_collision action)

('we_are_younger_than', age)

Is our part younger than age (in milliseconds)?

('we_are_older_than', age)

Is our part older than age (in milliseconds)?

('they_are_younger_than', age)

Is the part we're hitting younger than age (in milliseconds)?

('they_are_older_than', age)

Is the part we're hitting older than age (in milliseconds)?

('they_are_same_node_as_us')

Does the part we're hitting belong to the same bascenev1.Node as us?

('they_are_different_node_than_us')

Does the part we're hitting belong to a different bascenev1.Node?

Actions

In a similar manner, actions are specified as tuples. Multiple actions can be specified by providing a tuple of tuples.

Available Actions
('call', when, callable)

Calls the provided callable; when can be either 'at_connect' or 'at_disconnect'. 'at_connect' means to fire when the two parts first come in contact; 'at_disconnect' means to fire once they cease being in contact.

('message', who, when, message_obj)

Sends a message object; who can be either 'our_node' or 'their_node', when can be 'at_connect' or 'at_disconnect', and message_obj is the message object to send. This has the same effect as calling the node's babase.Node.handlemessage() method.

('modify_part_collision', attr, value)

Changes some characteristic of the physical collision that will occur between our part and their part. This change will remain in effect as long as the two parts remain overlapping. This means if you have a part with a material that turns 'collide' off against parts younger than 100ms, and it touches another part that is 50ms old, it will continue to not collide with that part until they separate, even if the 100ms threshold is passed. Options for attr/value are: 'physical' (boolean value; whether a physical response will occur at all), 'friction' (float value; how friction-y the physical response will be), 'collide' (boolean value; whether any collision will occur at all, including non-physical stuff like callbacks), 'use_node_collide' (boolean value; whether to honor modify_node_collision overrides for this collision), 'stiffness' (float value, how springy the physical response is), 'damping' (float value, how damped the physical response is), 'bounce' (float value; how bouncy the physical response is).

('modify_node_collision', attr, value)

Similar to modify_part_collision, but operates at a node-level. collision attributes set here will remain in effect as long as anything from our part's node and their part's node overlap. A key use of this functionality is to prevent new nodes from colliding with each other if they appear overlapped; if modify_part_collision is used, only the individual parts that were overlapping would avoid contact, but other parts could still contact leaving the two nodes 'tangled up'. Using modify_node_collision ensures that the nodes must completely separate before they can start colliding. Currently the only attr available here is 'collide' (a boolean value).

('sound', sound, volume)

Plays a bascenev1.Sound when a collision occurs, at a given volume, regardless of the collision speed/etc.

('impact_sound', sound, targetImpulse, volume)

Plays a sound when a collision occurs, based on the speed of impact. Provide a bascenev1.Sound, a target-impulse, and a volume.

('skid_sound', sound, targetImpulse, volume)

Plays a sound during a collision when parts are 'scraping' against each other. Provide a bascenev1.Sound, a target-impulse, and a volume.

('roll_sound', sound, targetImpulse, volume)

Plays a sound during a collision when parts are 'rolling' against each other. Provide a bascenev1.Sound, a target-impulse, and a volume.

Examples

Example 1: create a material that lets us ignore collisions against any nodes we touch in the first 100 ms of our existence; handy for preventing us from exploding outward if we spawn on top of another object:

>>> m = bascenev1.Material()
... m.add_actions(
...     conditions=(('we_are_younger_than', 100),
...                 'or', ('they_are_younger_than', 100)),
...     actions=('modify_node_collision', 'collide', False))

Example 2: send a bascenev1.DieMessage to anything we touch, but cause no physical response. This should cause any bascenev1.Actor to drop dead:

>>> m = bascenev1.Material()
... m.add_actions(
...     actions=(('modify_part_collision', 'physical', False),
...              ('message', 'their_node', 'at_connect',
...                  bascenev1.DieMessage())))

Example 3: play some sounds when we're contacting the ground:

>>> m = bascenev1.Material()
... m.add_actions(
...     conditions=('they_have_material', shared.footing_material),
...     actions=(
          ('impact_sound', bascenev1.getsound('metalHit'), 2, 5),
          ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)))
class Mesh:
451class Mesh:
452    """A reference to a mesh.
453
454    Category: **Asset Classes**
455
456    Meshes are used for drawing.
457    Use bascenev1.getmesh() to instantiate one.
458    """
459
460    pass

A reference to a mesh.

Category: Asset Classes

Meshes are used for drawing. Use bascenev1.getmesh() to instantiate one.

class MultiTeamSession(bascenev1.Session):
 26class MultiTeamSession(Session):
 27    """Common base for DualTeamSession and FreeForAllSession.
 28
 29    Category: **Gameplay Classes**
 30
 31    Free-for-all-mode is essentially just teams-mode with each
 32    bascenev1.Player having their own bascenev1.Team, so there is much
 33    overlap in functionality.
 34    """
 35
 36    # These should be overridden.
 37    _playlist_selection_var = 'UNSET Playlist Selection'
 38    _playlist_randomize_var = 'UNSET Playlist Randomize'
 39    _playlists_var = 'UNSET Playlists'
 40
 41    def __init__(self) -> None:
 42        """Set up playlists & launch a bascenev1.Activity to accept joiners."""
 43        # pylint: disable=cyclic-import
 44        from bascenev1 import _playlist
 45        from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity
 46
 47        app = babase.app
 48        classic = app.classic
 49        assert classic is not None
 50        cfg = app.config
 51
 52        if self.use_teams:
 53            team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES)
 54            team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS)
 55        else:
 56            team_names = None
 57            team_colors = None
 58
 59        # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
 60        depsets: Sequence[bascenev1.DependencySet] = []
 61
 62        super().__init__(
 63            depsets,
 64            team_names=team_names,
 65            team_colors=team_colors,
 66            min_players=1,
 67            max_players=self.get_max_players(),
 68        )
 69
 70        self._series_length: int = int(cfg.get('Teams Series Length', 7))
 71        self._ffa_series_length: int = int(cfg.get('FFA Series Length', 24))
 72
 73        show_tutorial = cfg.get('Show Tutorial', True)
 74
 75        # Special case: don't show tutorial while stress testing.
 76        if classic.stress_test_update_timer is not None:
 77            show_tutorial = False
 78
 79        self._tutorial_activity_instance: bascenev1.Activity | None
 80        if show_tutorial:
 81            from bascenev1lib.tutorial import TutorialActivity
 82
 83            tutorial_activity = TutorialActivity
 84
 85            # Get this loading.
 86            self._tutorial_activity_instance = _bascenev1.newactivity(
 87                tutorial_activity
 88            )
 89        else:
 90            self._tutorial_activity_instance = None
 91
 92        self._playlist_name = cfg.get(
 93            self._playlist_selection_var, '__default__'
 94        )
 95        self._playlist_randomize = cfg.get(self._playlist_randomize_var, False)
 96
 97        # Which game activity we're on.
 98        self._game_number = 0
 99
100        playlists = cfg.get(self._playlists_var, {})
101
102        if (
103            self._playlist_name != '__default__'
104            and self._playlist_name in playlists
105        ):
106            # Make sure to copy this, as we muck with it in place once we've
107            # got it and we don't want that to affect our config.
108            playlist = copy.deepcopy(playlists[self._playlist_name])
109        else:
110            if self.use_teams:
111                playlist = _playlist.get_default_teams_playlist()
112            else:
113                playlist = _playlist.get_default_free_for_all_playlist()
114
115        # Resolve types and whatnot to get our final playlist.
116        playlist_resolved = _playlist.filter_playlist(
117            playlist,
118            sessiontype=type(self),
119            add_resolved_type=True,
120            name='default teams' if self.use_teams else 'default ffa',
121        )
122
123        if not playlist_resolved:
124            raise RuntimeError('Playlist contains no valid games.')
125
126        self._playlist = ShuffleList(
127            playlist_resolved, shuffle=self._playlist_randomize
128        )
129
130        # Get a game on deck ready to go.
131        self._current_game_spec: dict[str, Any] | None = None
132        self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
133        self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[
134            'resolved_type'
135        ]
136
137        # Go ahead and instantiate the next game we'll
138        # use so it has lots of time to load.
139        self._instantiate_next_game()
140
141        # Start in our custom join screen.
142        self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity))
143
144    def get_ffa_series_length(self) -> int:
145        """Return free-for-all series length."""
146        return self._ffa_series_length
147
148    def get_series_length(self) -> int:
149        """Return teams series length."""
150        return self._series_length
151
152    def get_next_game_description(self) -> babase.Lstr:
153        """Returns a description of the next game on deck."""
154        # pylint: disable=cyclic-import
155        from bascenev1._gameactivity import GameActivity
156
157        gametype: type[GameActivity] = self._next_game_spec['resolved_type']
158        assert issubclass(gametype, GameActivity)
159        return gametype.get_settings_display_string(self._next_game_spec)
160
161    def get_game_number(self) -> int:
162        """Returns which game in the series is currently being played."""
163        return self._game_number
164
165    @override
166    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
167        team.customdata['previous_score'] = team.customdata['score'] = 0
168
169    def get_max_players(self) -> int:
170        """Return max number of Players allowed to join the game at once."""
171        if self.use_teams:
172            val = babase.app.config.get('Team Game Max Players', 8)
173        else:
174            val = babase.app.config.get('Free-for-All Max Players', 8)
175        assert isinstance(val, int)
176        return val
177
178    def _instantiate_next_game(self) -> None:
179        self._next_game_instance = _bascenev1.newactivity(
180            self._next_game_spec['resolved_type'],
181            self._next_game_spec['settings'],
182        )
183
184    @override
185    def on_activity_end(
186        self, activity: bascenev1.Activity, results: Any
187    ) -> None:
188        # pylint: disable=cyclic-import
189        from bascenev1lib.tutorial import TutorialActivity
190        from bascenev1lib.activity.multiteamvictory import (
191            TeamSeriesVictoryScoreScreenActivity,
192        )
193        from bascenev1._activitytypes import (
194            TransitionActivity,
195            JoinActivity,
196            ScoreScreenActivity,
197        )
198
199        # If we have a tutorial to show, that's the first thing we do no
200        # matter what.
201        if self._tutorial_activity_instance is not None:
202            self.setactivity(self._tutorial_activity_instance)
203            self._tutorial_activity_instance = None
204
205        # If we're leaving the tutorial activity, pop a transition activity
206        # to transition us into a round gracefully (otherwise we'd snap from
207        # one terrain to another instantly).
208        elif isinstance(activity, TutorialActivity):
209            self.setactivity(_bascenev1.newactivity(TransitionActivity))
210
211        # If we're in a between-round activity or a restart-activity, hop
212        # into a round.
213        elif isinstance(
214            activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
215        ):
216            # If we're coming from a series-end activity, reset scores.
217            if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
218                self.stats.reset()
219                self._game_number = 0
220                for team in self.sessionteams:
221                    team.customdata['score'] = 0
222
223            # Otherwise just set accum (per-game) scores.
224            else:
225                self.stats.reset_accum()
226
227            next_game = self._next_game_instance
228
229            self._current_game_spec = self._next_game_spec
230            self._next_game_spec = self._playlist.pull_next()
231            self._game_number += 1
232
233            # Instantiate the next now so they have plenty of time to load.
234            self._instantiate_next_game()
235
236            # (Re)register all players and wire stats to our next activity.
237            for player in self.sessionplayers:
238                # ..but only ones who have been placed on a team
239                # (ie: no longer sitting in the lobby).
240                try:
241                    has_team = player.sessionteam is not None
242                except babase.NotFoundError:
243                    has_team = False
244                if has_team:
245                    self.stats.register_sessionplayer(player)
246            self.stats.setactivity(next_game)
247
248            # Now flip the current activity.
249            self.setactivity(next_game)
250
251        # If we're leaving a round, go to the score screen.
252        else:
253            self._switch_to_score_screen(results)
254
255    def _switch_to_score_screen(self, results: Any) -> None:
256        """Switch to a score screen after leaving a round."""
257        del results  # Unused arg.
258        logging.error('This should be overridden.', stack_info=True)
259
260    def announce_game_results(
261        self,
262        activity: bascenev1.GameActivity,
263        results: bascenev1.GameResults,
264        delay: float,
265        announce_winning_team: bool = True,
266    ) -> None:
267        """Show basic game result at the end of a game.
268
269        (before transitioning to a score screen).
270        This will include a zoom-text of 'BLUE WINS'
271        or whatnot, along with a possible audio
272        announcement of the same.
273        """
274        # pylint: disable=cyclic-import
275        from bascenev1._gameutils import cameraflash
276        from bascenev1._freeforallsession import FreeForAllSession
277        from bascenev1._messages import CelebrateMessage
278
279        _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play)
280
281        if announce_winning_team:
282            winning_sessionteam = results.winning_sessionteam
283            if winning_sessionteam is not None:
284                # Have all players celebrate.
285                celebrate_msg = CelebrateMessage(duration=10.0)
286                assert winning_sessionteam.activityteam is not None
287                for player in winning_sessionteam.activityteam.players:
288                    if player.actor:
289                        player.actor.handlemessage(celebrate_msg)
290                cameraflash()
291
292                # Some languages say "FOO WINS" different for teams vs players.
293                if isinstance(self, FreeForAllSession):
294                    wins_resource = 'winsPlayerText'
295                else:
296                    wins_resource = 'winsTeamText'
297                wins_text = babase.Lstr(
298                    resource=wins_resource,
299                    subs=[('${NAME}', winning_sessionteam.name)],
300                )
301                activity.show_zoom_message(
302                    wins_text,
303                    scale=0.85,
304                    color=babase.normalized_color(winning_sessionteam.color),
305                )

Common base for DualTeamSession and FreeForAllSession.

Category: Gameplay Classes

Free-for-all-mode is essentially just teams-mode with each bascenev1.Player having their own bascenev1.Team, so there is much overlap in functionality.

MultiTeamSession()
 41    def __init__(self) -> None:
 42        """Set up playlists & launch a bascenev1.Activity to accept joiners."""
 43        # pylint: disable=cyclic-import
 44        from bascenev1 import _playlist
 45        from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity
 46
 47        app = babase.app
 48        classic = app.classic
 49        assert classic is not None
 50        cfg = app.config
 51
 52        if self.use_teams:
 53            team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES)
 54            team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS)
 55        else:
 56            team_names = None
 57            team_colors = None
 58
 59        # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
 60        depsets: Sequence[bascenev1.DependencySet] = []
 61
 62        super().__init__(
 63            depsets,
 64            team_names=team_names,
 65            team_colors=team_colors,
 66            min_players=1,
 67            max_players=self.get_max_players(),
 68        )
 69
 70        self._series_length: int = int(cfg.get('Teams Series Length', 7))
 71        self._ffa_series_length: int = int(cfg.get('FFA Series Length', 24))
 72
 73        show_tutorial = cfg.get('Show Tutorial', True)
 74
 75        # Special case: don't show tutorial while stress testing.
 76        if classic.stress_test_update_timer is not None:
 77            show_tutorial = False
 78
 79        self._tutorial_activity_instance: bascenev1.Activity | None
 80        if show_tutorial:
 81            from bascenev1lib.tutorial import TutorialActivity
 82
 83            tutorial_activity = TutorialActivity
 84
 85            # Get this loading.
 86            self._tutorial_activity_instance = _bascenev1.newactivity(
 87                tutorial_activity
 88            )
 89        else:
 90            self._tutorial_activity_instance = None
 91
 92        self._playlist_name = cfg.get(
 93            self._playlist_selection_var, '__default__'
 94        )
 95        self._playlist_randomize = cfg.get(self._playlist_randomize_var, False)
 96
 97        # Which game activity we're on.
 98        self._game_number = 0
 99
100        playlists = cfg.get(self._playlists_var, {})
101
102        if (
103            self._playlist_name != '__default__'
104            and self._playlist_name in playlists
105        ):
106            # Make sure to copy this, as we muck with it in place once we've
107            # got it and we don't want that to affect our config.
108            playlist = copy.deepcopy(playlists[self._playlist_name])
109        else:
110            if self.use_teams:
111                playlist = _playlist.get_default_teams_playlist()
112            else:
113                playlist = _playlist.get_default_free_for_all_playlist()
114
115        # Resolve types and whatnot to get our final playlist.
116        playlist_resolved = _playlist.filter_playlist(
117            playlist,
118            sessiontype=type(self),
119            add_resolved_type=True,
120            name='default teams' if self.use_teams else 'default ffa',
121        )
122
123        if not playlist_resolved:
124            raise RuntimeError('Playlist contains no valid games.')
125
126        self._playlist = ShuffleList(
127            playlist_resolved, shuffle=self._playlist_randomize
128        )
129
130        # Get a game on deck ready to go.
131        self._current_game_spec: dict[str, Any] | None = None
132        self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
133        self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[
134            'resolved_type'
135        ]
136
137        # Go ahead and instantiate the next game we'll
138        # use so it has lots of time to load.
139        self._instantiate_next_game()
140
141        # Start in our custom join screen.
142        self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity))

Set up playlists & launch a bascenev1.Activity to accept joiners.

def get_ffa_series_length(self) -> int:
144    def get_ffa_series_length(self) -> int:
145        """Return free-for-all series length."""
146        return self._ffa_series_length

Return free-for-all series length.

def get_series_length(self) -> int:
148    def get_series_length(self) -> int:
149        """Return teams series length."""
150        return self._series_length

Return teams series length.

def get_next_game_description(self) -> Lstr:
152    def get_next_game_description(self) -> babase.Lstr:
153        """Returns a description of the next game on deck."""
154        # pylint: disable=cyclic-import
155        from bascenev1._gameactivity import GameActivity
156
157        gametype: type[GameActivity] = self._next_game_spec['resolved_type']
158        assert issubclass(gametype, GameActivity)
159        return gametype.get_settings_display_string(self._next_game_spec)

Returns a description of the next game on deck.

def get_game_number(self) -> int:
161    def get_game_number(self) -> int:
162        """Returns which game in the series is currently being played."""
163        return self._game_number

Returns which game in the series is currently being played.

@override
def on_team_join(self, team: SessionTeam) -> None:
165    @override
166    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
167        team.customdata['previous_score'] = team.customdata['score'] = 0

Called when a new bascenev1.Team joins the session.

def get_max_players(self) -> int:
169    def get_max_players(self) -> int:
170        """Return max number of Players allowed to join the game at once."""
171        if self.use_teams:
172            val = babase.app.config.get('Team Game Max Players', 8)
173        else:
174            val = babase.app.config.get('Free-for-All Max Players', 8)
175        assert isinstance(val, int)
176        return val

Return max number of Players allowed to join the game at once.

@override
def on_activity_end(self, activity: Activity, results: Any) -> None:
184    @override
185    def on_activity_end(
186        self, activity: bascenev1.Activity, results: Any
187    ) -> None:
188        # pylint: disable=cyclic-import
189        from bascenev1lib.tutorial import TutorialActivity
190        from bascenev1lib.activity.multiteamvictory import (
191            TeamSeriesVictoryScoreScreenActivity,
192        )
193        from bascenev1._activitytypes import (
194            TransitionActivity,
195            JoinActivity,
196            ScoreScreenActivity,
197        )
198
199        # If we have a tutorial to show, that's the first thing we do no
200        # matter what.
201        if self._tutorial_activity_instance is not None:
202            self.setactivity(self._tutorial_activity_instance)
203            self._tutorial_activity_instance = None
204
205        # If we're leaving the tutorial activity, pop a transition activity
206        # to transition us into a round gracefully (otherwise we'd snap from
207        # one terrain to another instantly).
208        elif isinstance(activity, TutorialActivity):
209            self.setactivity(_bascenev1.newactivity(TransitionActivity))
210
211        # If we're in a between-round activity or a restart-activity, hop
212        # into a round.
213        elif isinstance(
214            activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
215        ):
216            # If we're coming from a series-end activity, reset scores.
217            if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
218                self.stats.reset()
219                self._game_number = 0
220                for team in self.sessionteams:
221                    team.customdata['score'] = 0
222
223            # Otherwise just set accum (per-game) scores.
224            else:
225                self.stats.reset_accum()
226
227            next_game = self._next_game_instance
228
229            self._current_game_spec = self._next_game_spec
230            self._next_game_spec = self._playlist.pull_next()
231            self._game_number += 1
232
233            # Instantiate the next now so they have plenty of time to load.
234            self._instantiate_next_game()
235
236            # (Re)register all players and wire stats to our next activity.
237            for player in self.sessionplayers:
238                # ..but only ones who have been placed on a team
239                # (ie: no longer sitting in the lobby).
240                try:
241                    has_team = player.sessionteam is not None
242                except babase.NotFoundError:
243                    has_team = False
244                if has_team:
245                    self.stats.register_sessionplayer(player)
246            self.stats.setactivity(next_game)
247
248            # Now flip the current activity.
249            self.setactivity(next_game)
250
251        # If we're leaving a round, go to the score screen.
252        else:
253            self._switch_to_score_screen(results)

Called when the current bascenev1.Activity has ended.

The bascenev1.Session should look at the results and start another bascenev1.Activity.

def announce_game_results( self, activity: GameActivity, results: GameResults, delay: float, announce_winning_team: bool = True) -> None:
260    def announce_game_results(
261        self,
262        activity: bascenev1.GameActivity,
263        results: bascenev1.GameResults,
264        delay: float,
265        announce_winning_team: bool = True,
266    ) -> None:
267        """Show basic game result at the end of a game.
268
269        (before transitioning to a score screen).
270        This will include a zoom-text of 'BLUE WINS'
271        or whatnot, along with a possible audio
272        announcement of the same.
273        """
274        # pylint: disable=cyclic-import
275        from bascenev1._gameutils import cameraflash
276        from bascenev1._freeforallsession import FreeForAllSession
277        from bascenev1._messages import CelebrateMessage
278
279        _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play)
280
281        if announce_winning_team:
282            winning_sessionteam = results.winning_sessionteam
283            if winning_sessionteam is not None:
284                # Have all players celebrate.
285                celebrate_msg = CelebrateMessage(duration=10.0)
286                assert winning_sessionteam.activityteam is not None
287                for player in winning_sessionteam.activityteam.players:
288                    if player.actor:
289                        player.actor.handlemessage(celebrate_msg)
290                cameraflash()
291
292                # Some languages say "FOO WINS" different for teams vs players.
293                if isinstance(self, FreeForAllSession):
294                    wins_resource = 'winsPlayerText'
295                else:
296                    wins_resource = 'winsTeamText'
297                wins_text = babase.Lstr(
298                    resource=wins_resource,
299                    subs=[('${NAME}', winning_sessionteam.name)],
300                )
301                activity.show_zoom_message(
302                    wins_text,
303                    scale=0.85,
304                    color=babase.normalized_color(winning_sessionteam.color),
305                )

Show basic game result at the end of a game.

(before transitioning to a score screen). This will include a zoom-text of 'BLUE WINS' or whatnot, along with a possible audio announcement of the same.

class MusicType(enum.Enum):
17class MusicType(Enum):
18    """Types of music available to play in-game.
19
20    Category: **Enums**
21
22    These do not correspond to specific pieces of music, but rather to
23    'situations'. The actual music played for each type can be overridden
24    by the game or by the user.
25    """
26
27    MENU = 'Menu'
28    VICTORY = 'Victory'
29    CHAR_SELECT = 'CharSelect'
30    RUN_AWAY = 'RunAway'
31    ONSLAUGHT = 'Onslaught'
32    KEEP_AWAY = 'Keep Away'
33    RACE = 'Race'
34    EPIC_RACE = 'Epic Race'
35    SCORES = 'Scores'
36    GRAND_ROMP = 'GrandRomp'
37    TO_THE_DEATH = 'ToTheDeath'
38    CHOSEN_ONE = 'Chosen One'
39    FORWARD_MARCH = 'ForwardMarch'
40    FLAG_CATCHER = 'FlagCatcher'
41    SURVIVAL = 'Survival'
42    EPIC = 'Epic'
43    SPORTS = 'Sports'
44    HOCKEY = 'Hockey'
45    FOOTBALL = 'Football'
46    FLYING = 'Flying'
47    SCARY = 'Scary'
48    MARCHING = 'Marching'

Types of music available to play in-game.

Category: Enums

These do not correspond to specific pieces of music, but rather to 'situations'. The actual music played for each type can be overridden by the game or by the user.

MENU = <MusicType.MENU: 'Menu'>
VICTORY = <MusicType.VICTORY: 'Victory'>
CHAR_SELECT = <MusicType.CHAR_SELECT: 'CharSelect'>
RUN_AWAY = <MusicType.RUN_AWAY: 'RunAway'>
ONSLAUGHT = <MusicType.ONSLAUGHT: 'Onslaught'>
KEEP_AWAY = <MusicType.KEEP_AWAY: 'Keep Away'>
RACE = <MusicType.RACE: 'Race'>
EPIC_RACE = <MusicType.EPIC_RACE: 'Epic Race'>
SCORES = <MusicType.SCORES: 'Scores'>
GRAND_ROMP = <MusicType.GRAND_ROMP: 'GrandRomp'>
TO_THE_DEATH = <MusicType.TO_THE_DEATH: 'ToTheDeath'>
CHOSEN_ONE = <MusicType.CHOSEN_ONE: 'Chosen One'>
FORWARD_MARCH = <MusicType.FORWARD_MARCH: 'ForwardMarch'>
FLAG_CATCHER = <MusicType.FLAG_CATCHER: 'FlagCatcher'>
SURVIVAL = <MusicType.SURVIVAL: 'Survival'>
EPIC = <MusicType.EPIC: 'Epic'>
SPORTS = <MusicType.SPORTS: 'Sports'>
HOCKEY = <MusicType.HOCKEY: 'Hockey'>
FOOTBALL = <MusicType.FOOTBALL: 'Football'>
FLYING = <MusicType.FLYING: 'Flying'>
SCARY = <MusicType.SCARY: 'Scary'>
MARCHING = <MusicType.MARCHING: 'Marching'>
Inherited Members
enum.Enum
name
value
def newactivity( activity_type: type[Activity], settings: dict | None = None) -> Activity:
1525def newactivity(
1526    activity_type: type[bascenev1.Activity], settings: dict | None = None
1527) -> bascenev1.Activity:
1528    """Instantiates a bascenev1.Activity given a type object.
1529
1530    Category: **General Utility Functions**
1531
1532    Activities require special setup and thus cannot be directly
1533    instantiated; you must go through this function.
1534    """
1535    import bascenev1  # pylint: disable=cyclic-import
1536
1537    return bascenev1.Activity(settings={})

Instantiates a bascenev1.Activity given a type object.

Category: General Utility Functions

Activities require special setup and thus cannot be directly instantiated; you must go through this function.

def newnode( type: str, owner: Node | None = None, attrs: dict | None = None, name: str | None = None, delegate: Any = None) -> Node:
1541def newnode(
1542    type: str,
1543    owner: bascenev1.Node | None = None,
1544    attrs: dict | None = None,
1545    name: str | None = None,
1546    delegate: Any = None,
1547) -> bascenev1.Node:
1548    """Add a node of the given type to the game.
1549
1550    Category: **Gameplay Functions**
1551
1552    If a dict is provided for 'attributes', the node's initial attributes
1553    will be set based on them.
1554
1555    'name', if provided, will be stored with the node purely for debugging
1556    purposes. If no name is provided, an automatic one will be generated
1557    such as 'terrain@foo.py:30'.
1558
1559    If 'delegate' is provided, Python messages sent to the node will go to
1560    that object's handlemessage() method. Note that the delegate is stored
1561    as a weak-ref, so the node itself will not keep the object alive.
1562
1563    if 'owner' is provided, the node will be automatically killed when that
1564    object dies. 'owner' can be another node or a bascenev1.Actor
1565    """
1566    import bascenev1  # pylint: disable=cyclic-import
1567
1568    return bascenev1.Node()

Add a node of the given type to the game.

Category: Gameplay Functions

If a dict is provided for 'attributes', the node's initial attributes will be set based on them.

'name', if provided, will be stored with the node purely for debugging purposes. If no name is provided, an automatic one will be generated such as 'terrain@foo.py:30'.

If 'delegate' is provided, Python messages sent to the node will go to that object's handlemessage() method. Note that the delegate is stored as a weak-ref, so the node itself will not keep the object alive.

if 'owner' is provided, the node will be automatically killed when that object dies. 'owner' can be another node or a bascenev1.Actor

class Node:
464class Node:
465    """Reference to a Node; the low level building block of a game.
466
467    Category: **Gameplay Classes**
468
469    At its core, a game is nothing more than a scene of Nodes
470    with attributes getting interconnected or set over time.
471
472    A bascenev1.Node instance should be thought of as a weak-reference
473    to a game node; *not* the node itself. This means a Node's
474    lifecycle is completely independent of how many Python references
475    to it exist. To explicitly add a new node to the game, use
476    bascenev1.newnode(), and to explicitly delete one,
477     use bascenev1.Node.delete().
478    babase.Node.exists() can be used to determine if a Node still points
479    to a live node in the game.
480
481    You can use `ba.Node(None)` to instantiate an invalid
482    Node reference (sometimes used as attr values/etc).
483    """
484
485    # Note attributes:
486    # NOTE: I'm just adding *all* possible node attrs here
487    # now now since we have a single bascenev1.Node type; in the
488    # future I hope to create proper individual classes
489    # corresponding to different node types with correct
490    # attributes per node-type.
491    color: Sequence[float] = (0.0, 0.0, 0.0)
492    size: Sequence[float] = (0.0, 0.0, 0.0)
493    position: Sequence[float] = (0.0, 0.0, 0.0)
494    position_center: Sequence[float] = (0.0, 0.0, 0.0)
495    position_forward: Sequence[float] = (0.0, 0.0, 0.0)
496    punch_position: Sequence[float] = (0.0, 0.0, 0.0)
497    punch_velocity: Sequence[float] = (0.0, 0.0, 0.0)
498    velocity: Sequence[float] = (0.0, 0.0, 0.0)
499    name_color: Sequence[float] = (0.0, 0.0, 0.0)
500    tint_color: Sequence[float] = (0.0, 0.0, 0.0)
501    tint2_color: Sequence[float] = (0.0, 0.0, 0.0)
502    text: babase.Lstr | str = ''
503    texture: bascenev1.Texture | None = None
504    tint_texture: bascenev1.Texture | None = None
505    times: Sequence[int] = (1, 2, 3, 4, 5)
506    values: Sequence[float] = (1.0, 2.0, 3.0, 4.0)
507    offset: float = 0.0
508    input0: float = 0.0
509    input1: float = 0.0
510    input2: float = 0.0
511    input3: float = 0.0
512    flashing: bool = False
513    scale: float | Sequence[float] = 0.0
514    opacity: float = 0.0
515    loop: bool = False
516    time1: int = 0
517    time2: int = 0
518    timemax: int = 0
519    client_only: bool = False
520    materials: Sequence[bascenev1.Material] = ()
521    roller_materials: Sequence[bascenev1.Material] = ()
522    name: str = ''
523    punch_materials: Sequence[bascenev1.Material] = ()
524    pickup_materials: Sequence[bascenev1.Material] = ()
525    extras_material: Sequence[bascenev1.Material] = ()
526    rotate: float = 0.0
527    hold_node: bascenev1.Node | None = None
528    hold_body: int = 0
529    host_only: bool = False
530    premultiplied: bool = False
531    source_player: bascenev1.Player | None = None
532    mesh_opaque: bascenev1.Mesh | None = None
533    mesh_transparent: bascenev1.Mesh | None = None
534    damage_smoothed: float = 0.0
535    gravity_scale: float = 1.0
536    punch_power: float = 0.0
537    punch_momentum_linear: Sequence[float] = (0.0, 0.0, 0.0)
538    punch_momentum_angular: float = 0.0
539    rate: int = 0
540    vr_depth: float = 0.0
541    is_area_of_interest: bool = False
542    jump_pressed: bool = False
543    pickup_pressed: bool = False
544    punch_pressed: bool = False
545    bomb_pressed: bool = False
546    fly_pressed: bool = False
547    hold_position_pressed: bool = False
548    knockout: float = 0.0
549    invincible: bool = False
550    stick_to_owner: bool = False
551    damage: int = 0
552    run: float = 0.0
553    move_up_down: float = 0.0
554    move_left_right: float = 0.0
555    curse_death_time: int = 0
556    boxing_gloves: bool = False
557    hockey: bool = False
558    use_fixed_vr_overlay: bool = False
559    allow_kick_idle_players: bool = False
560    music_continuous: bool = False
561    music_count: int = 0
562    hurt: float = 0.0
563    always_show_health_bar: bool = False
564    mini_billboard_1_texture: bascenev1.Texture | None = None
565    mini_billboard_1_start_time: int = 0
566    mini_billboard_1_end_time: int = 0
567    mini_billboard_2_texture: bascenev1.Texture | None = None
568    mini_billboard_2_start_time: int = 0
569    mini_billboard_2_end_time: int = 0
570    mini_billboard_3_texture: bascenev1.Texture | None = None
571    mini_billboard_3_start_time: int = 0
572    mini_billboard_3_end_time: int = 0
573    boxing_gloves_flashing: bool = False
574    dead: bool = False
575    floor_reflection: bool = False
576    debris_friction: float = 0.0
577    debris_kill_height: float = 0.0
578    vr_near_clip: float = 0.0
579    shadow_ortho: bool = False
580    happy_thoughts_mode: bool = False
581    shadow_offset: Sequence[float] = (0.0, 0.0)
582    paused: bool = False
583    time: int = 0
584    ambient_color: Sequence[float] = (1.0, 1.0, 1.0)
585    camera_mode: str = 'rotate'
586    frozen: bool = False
587    area_of_interest_bounds: Sequence[float] = (-1, -1, -1, 1, 1, 1)
588    shadow_range: Sequence[float] = (0, 0, 0, 0)
589    counter_text: str = ''
590    counter_texture: bascenev1.Texture | None = None
591    shattered: int = 0
592    billboard_texture: bascenev1.Texture | None = None
593    billboard_cross_out: bool = False
594    billboard_opacity: float = 0.0
595    slow_motion: bool = False
596    music: str = ''
597    vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)
598    vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)
599    vr_overlay_center_enabled: bool = False
600    vignette_outer: Sequence[float] = (0.0, 0.0)
601    vignette_inner: Sequence[float] = (0.0, 0.0)
602    tint: Sequence[float] = (1.0, 1.0, 1.0)
603
604    def __bool__(self) -> bool:
605        """Support for bool evaluation."""
606        return bool(True)  # Slight obfuscation.
607
608    def add_death_action(self, action: Callable[[], None]) -> None:
609        """Add a callable object to be called upon this node's death.
610        Note that these actions are run just after the node dies, not before.
611        """
612        return None
613
614    def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None:
615        """Connect one of this node's attributes to an attribute on another
616        node. This will immediately set the target attribute's value to that
617        of the source attribute, and will continue to do so once per step
618        as long as the two nodes exist. The connection can be severed by
619        setting the target attribute to any value or connecting another
620        node attribute to it.
621
622        ##### Example
623        Create a locator and attach a light to it:
624        >>> light = bascenev1.newnode('light')
625        ... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
626        ... loc.connectattr('position', light, 'position')
627        """
628        return None
629
630    def delete(self, ignore_missing: bool = True) -> None:
631        """Delete the node. Ignores already-deleted nodes if `ignore_missing`
632        is True; otherwise a bascenev1.NodeNotFoundError is thrown.
633        """
634        return None
635
636    def exists(self) -> bool:
637        """Returns whether the Node still exists.
638        Most functionality will fail on a nonexistent Node, so it's never a bad
639        idea to check this.
640
641        Note that you can also use the boolean operator for this same
642        functionality, so a statement such as "if mynode" will do
643        the right thing both for Node objects and values of None.
644        """
645        return bool()
646
647    # Show that ur return type varies based on "doraise" value:
648    @overload
649    def getdelegate(
650        self, type: type[_T], doraise: Literal[False] = False
651    ) -> _T | None: ...
652
653    @overload
654    def getdelegate(self, type: type[_T], doraise: Literal[True]) -> _T: ...
655
656    def getdelegate(self, type: Any, doraise: bool = False) -> Any:
657        """Return the node's current delegate object if it matches
658        a certain type.
659
660        If the node has no delegate or it is not an instance of the passed
661        type, then None will be returned. If 'doraise' is True, then an
662        babase.DelegateNotFoundError will be raised instead.
663        """
664        return None
665
666    def getname(self) -> str:
667        """Return the name assigned to a Node; used mainly for debugging"""
668        return str()
669
670    def getnodetype(self) -> str:
671        """Return the type of Node referenced by this object as a string.
672        (Note this is different from the Python type which is always
673         bascenev1.Node)
674        """
675        return str()
676
677    def handlemessage(self, *args: Any) -> None:
678        """General message handling; can be passed any message object.
679
680        All standard message objects are forwarded along to the
681        bascenev1.Node's delegate for handling (generally the bascenev1.Actor
682        that made the node).
683
684        bascenev1.Node-s are unique, however, in that they can be passed a
685        second form of message; 'node-messages'.  These consist of a string
686        type-name as a first argument along with the args specific to that type
687        name as additional arguments.
688        Node-messages communicate directly with the low-level node layer
689        and are delivered simultaneously on all game clients,
690        acting as an alternative to setting node attributes.
691        """
692        return None

Reference to a Node; the low level building block of a game.

Category: Gameplay Classes

At its core, a game is nothing more than a scene of Nodes with attributes getting interconnected or set over time.

A bascenev1.Node instance should be thought of as a weak-reference to a game node; not the node itself. This means a Node's lifecycle is completely independent of how many Python references to it exist. To explicitly add a new node to the game, use bascenev1.newnode(), and to explicitly delete one, use bascenev1.Node.delete(). babase.Node.exists() can be used to determine if a Node still points to a live node in the game.

You can use ba.Node(None) to instantiate an invalid Node reference (sometimes used as attr values/etc).

color: Sequence[float] = (0.0, 0.0, 0.0)
size: Sequence[float] = (0.0, 0.0, 0.0)
position: Sequence[float] = (0.0, 0.0, 0.0)
position_center: Sequence[float] = (0.0, 0.0, 0.0)
position_forward: Sequence[float] = (0.0, 0.0, 0.0)
punch_position: Sequence[float] = (0.0, 0.0, 0.0)
punch_velocity: Sequence[float] = (0.0, 0.0, 0.0)
velocity: Sequence[float] = (0.0, 0.0, 0.0)
name_color: Sequence[float] = (0.0, 0.0, 0.0)
tint_color: Sequence[float] = (0.0, 0.0, 0.0)
tint2_color: Sequence[float] = (0.0, 0.0, 0.0)
text: Lstr | str = ''
texture: Texture | None = None
tint_texture: Texture | None = None
times: Sequence[int] = (1, 2, 3, 4, 5)
values: Sequence[float] = (1.0, 2.0, 3.0, 4.0)
offset: float = 0.0
input0: float = 0.0
input1: float = 0.0
input2: float = 0.0
input3: float = 0.0
flashing: bool = False
scale: Union[float, Sequence[float]] = 0.0
opacity: float = 0.0
loop: bool = False
time1: int = 0
time2: int = 0
timemax: int = 0
client_only: bool = False
materials: Sequence[Material] = ()
roller_materials: Sequence[Material] = ()
name: str = ''
punch_materials: Sequence[Material] = ()
pickup_materials: Sequence[Material] = ()
extras_material: Sequence[Material] = ()
rotate: float = 0.0
hold_node: Node | None = None
hold_body: int = 0
host_only: bool = False
premultiplied: bool = False
source_player: Player | None = None
mesh_opaque: Mesh | None = None
mesh_transparent: Mesh | None = None
damage_smoothed: float = 0.0
gravity_scale: float = 1.0
punch_power: float = 0.0
punch_momentum_linear: Sequence[float] = (0.0, 0.0, 0.0)
punch_momentum_angular: float = 0.0
rate: int = 0
vr_depth: float = 0.0
is_area_of_interest: bool = False
jump_pressed: bool = False
pickup_pressed: bool = False
punch_pressed: bool = False
bomb_pressed: bool = False
fly_pressed: bool = False
hold_position_pressed: bool = False
knockout: float = 0.0
invincible: bool = False
stick_to_owner: bool = False
damage: int = 0
run: float = 0.0
move_up_down: float = 0.0
move_left_right: float = 0.0
curse_death_time: int = 0
boxing_gloves: bool = False
hockey: bool = False
use_fixed_vr_overlay: bool = False
allow_kick_idle_players: bool = False
music_continuous: bool = False
music_count: int = 0
hurt: float = 0.0
always_show_health_bar: bool = False
mini_billboard_1_texture: Texture | None = None
mini_billboard_1_start_time: int = 0
mini_billboard_1_end_time: int = 0
mini_billboard_2_texture: Texture | None = None
mini_billboard_2_start_time: int = 0
mini_billboard_2_end_time: int = 0
mini_billboard_3_texture: Texture | None = None
mini_billboard_3_start_time: int = 0
mini_billboard_3_end_time: int = 0
boxing_gloves_flashing: bool = False
dead: bool = False
floor_reflection: bool = False
debris_friction: float = 0.0
debris_kill_height: float = 0.0
vr_near_clip: float = 0.0
shadow_ortho: bool = False
happy_thoughts_mode: bool = False
shadow_offset: Sequence[float] = (0.0, 0.0)
paused: bool = False
time: int = 0
ambient_color: Sequence[float] = (1.0, 1.0, 1.0)
camera_mode: str = 'rotate'
frozen: bool = False
area_of_interest_bounds: Sequence[float] = (-1, -1, -1, 1, 1, 1)
shadow_range: Sequence[float] = (0, 0, 0, 0)
counter_text: str = ''
counter_texture: Texture | None = None
shattered: int = 0
billboard_texture: Texture | None = None
billboard_cross_out: bool = False
billboard_opacity: float = 0.0
slow_motion: bool = False
music: str = ''
vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)
vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)
vr_overlay_center_enabled: bool = False
vignette_outer: Sequence[float] = (0.0, 0.0)
vignette_inner: Sequence[float] = (0.0, 0.0)
tint: Sequence[float] = (1.0, 1.0, 1.0)
def add_death_action(self, action: Callable[[], NoneType]) -> None:
608    def add_death_action(self, action: Callable[[], None]) -> None:
609        """Add a callable object to be called upon this node's death.
610        Note that these actions are run just after the node dies, not before.
611        """
612        return None

Add a callable object to be called upon this node's death. Note that these actions are run just after the node dies, not before.

def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None:
614    def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None:
615        """Connect one of this node's attributes to an attribute on another
616        node. This will immediately set the target attribute's value to that
617        of the source attribute, and will continue to do so once per step
618        as long as the two nodes exist. The connection can be severed by
619        setting the target attribute to any value or connecting another
620        node attribute to it.
621
622        ##### Example
623        Create a locator and attach a light to it:
624        >>> light = bascenev1.newnode('light')
625        ... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
626        ... loc.connectattr('position', light, 'position')
627        """
628        return None

Connect one of this node's attributes to an attribute on another node. This will immediately set the target attribute's value to that of the source attribute, and will continue to do so once per step as long as the two nodes exist. The connection can be severed by setting the target attribute to any value or connecting another node attribute to it.

Example

Create a locator and attach a light to it:

>>> light = bascenev1.newnode('light')
... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
... loc.connectattr('position', light, 'position')
def delete(self, ignore_missing: bool = True) -> None:
630    def delete(self, ignore_missing: bool = True) -> None:
631        """Delete the node. Ignores already-deleted nodes if `ignore_missing`
632        is True; otherwise a bascenev1.NodeNotFoundError is thrown.
633        """
634        return None

Delete the node. Ignores already-deleted nodes if ignore_missing is True; otherwise a bascenev1.NodeNotFoundError is thrown.

def exists(self) -> bool:
636    def exists(self) -> bool:
637        """Returns whether the Node still exists.
638        Most functionality will fail on a nonexistent Node, so it's never a bad
639        idea to check this.
640
641        Note that you can also use the boolean operator for this same
642        functionality, so a statement such as "if mynode" will do
643        the right thing both for Node objects and values of None.
644        """
645        return bool()

Returns whether the Node still exists. Most functionality will fail on a nonexistent Node, so it's never a bad idea to check this.

Note that you can also use the boolean operator for this same functionality, so a statement such as "if mynode" will do the right thing both for Node objects and values of None.

def getdelegate(self, type: Any, doraise: bool = False) -> Any:
656    def getdelegate(self, type: Any, doraise: bool = False) -> Any:
657        """Return the node's current delegate object if it matches
658        a certain type.
659
660        If the node has no delegate or it is not an instance of the passed
661        type, then None will be returned. If 'doraise' is True, then an
662        babase.DelegateNotFoundError will be raised instead.
663        """
664        return None

Return the node's current delegate object if it matches a certain type.

If the node has no delegate or it is not an instance of the passed type, then None will be returned. If 'doraise' is True, then an babase.DelegateNotFoundError will be raised instead.

def getname(self) -> str:
666    def getname(self) -> str:
667        """Return the name assigned to a Node; used mainly for debugging"""
668        return str()

Return the name assigned to a Node; used mainly for debugging

def getnodetype(self) -> str:
670    def getnodetype(self) -> str:
671        """Return the type of Node referenced by this object as a string.
672        (Note this is different from the Python type which is always
673         bascenev1.Node)
674        """
675        return str()

Return the type of Node referenced by this object as a string. (Note this is different from the Python type which is always bascenev1.Node)

def handlemessage(self, *args: Any) -> None:
677    def handlemessage(self, *args: Any) -> None:
678        """General message handling; can be passed any message object.
679
680        All standard message objects are forwarded along to the
681        bascenev1.Node's delegate for handling (generally the bascenev1.Actor
682        that made the node).
683
684        bascenev1.Node-s are unique, however, in that they can be passed a
685        second form of message; 'node-messages'.  These consist of a string
686        type-name as a first argument along with the args specific to that type
687        name as additional arguments.
688        Node-messages communicate directly with the low-level node layer
689        and are delivered simultaneously on all game clients,
690        acting as an alternative to setting node attributes.
691        """
692        return None

General message handling; can be passed any message object.

All standard message objects are forwarded along to the bascenev1.Node's delegate for handling (generally the bascenev1.Actor that made the node).

bascenev1.Node-s are unique, however, in that they can be passed a second form of message; 'node-messages'. These consist of a string type-name as a first argument along with the args specific to that type name as additional arguments. Node-messages communicate directly with the low-level node layer and are delivered simultaneously on all game clients, acting as an alternative to setting node attributes.

class NodeActor(bascenev1.Actor):
19class NodeActor(Actor):
20    """A simple bascenev1.Actor type that wraps a single bascenev1.Node.
21
22    Category: **Gameplay Classes**
23
24    This Actor will delete its Node when told to die, and it's
25    exists() call will return whether the Node still exists or not.
26    """
27
28    def __init__(self, node: bascenev1.Node):
29        super().__init__()
30        self.node = node
31
32    @override
33    def handlemessage(self, msg: Any) -> Any:
34        if isinstance(msg, DieMessage):
35            if self.node:
36                self.node.delete()
37                return None
38        return super().handlemessage(msg)
39
40    @override
41    def exists(self) -> bool:
42        return bool(self.node)

A simple bascenev1.Actor type that wraps a single bascenev1.Node.

Category: Gameplay Classes

This Actor will delete its Node when told to die, and it's exists() call will return whether the Node still exists or not.

NodeActor(node: Node)
28    def __init__(self, node: bascenev1.Node):
29        super().__init__()
30        self.node = node

Instantiates an Actor in the current bascenev1.Activity.

node
@override
def handlemessage(self, msg: Any) -> Any:
32    @override
33    def handlemessage(self, msg: Any) -> Any:
34        if isinstance(msg, DieMessage):
35            if self.node:
36                self.node.delete()
37                return None
38        return super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def exists(self) -> bool:
40    @override
41    def exists(self) -> bool:
42        return bool(self.node)

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

class NodeNotFoundError(bascenev1.NotFoundError):
75class NodeNotFoundError(NotFoundError):
76    """Exception raised when an expected Node does not exist.
77
78    Category: **Exception Classes**
79    """

Exception raised when an expected Node does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
def normalized_color(color: Sequence[float]) -> tuple[float, ...]:
52def normalized_color(color: Sequence[float]) -> tuple[float, ...]:
53    """Scale a color so its largest value is 1; useful for coloring lights.
54
55    category: General Utility Functions
56    """
57    color_biased = tuple(max(c, 0.01) for c in color)  # account for black
58    mult = 1.0 / max(color_biased)
59    return tuple(c * mult for c in color_biased)

Scale a color so its largest value is 1; useful for coloring lights.

category: General Utility Functions

class NotFoundError(builtins.Exception):
26class NotFoundError(Exception):
27    """Exception raised when a referenced object does not exist.
28
29    Category: **Exception Classes**
30    """

Exception raised when a referenced object does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
@dataclass
class OutOfBoundsMessage:
30@dataclass
31class OutOfBoundsMessage:
32    """A message telling an object that it is out of bounds.
33
34    Category: Message Classes
35    """

A message telling an object that it is out of bounds.

Category: Message Classes

@dataclass
class PickedUpMessage:
164@dataclass
165class PickedUpMessage:
166    """Tells an object that it has been picked up by something.
167
168    Category: **Message Classes**
169    """
170
171    node: bascenev1.Node
172    """The bascenev1.Node doing the picking up."""

Tells an object that it has been picked up by something.

Category: Message Classes

PickedUpMessage(node: Node)
node: Node

The bascenev1.Node doing the picking up.

@dataclass
class PickUpMessage:
145@dataclass
146class PickUpMessage:
147    """Tells an object that it has picked something up.
148
149    Category: **Message Classes**
150    """
151
152    node: bascenev1.Node
153    """The bascenev1.Node that is getting picked up."""

Tells an object that it has picked something up.

Category: Message Classes

PickUpMessage(node: Node)
node: Node

The bascenev1.Node that is getting picked up.

class Player(typing.Generic[~TeamT]):
 48class Player(Generic[TeamT]):
 49    """A player in a specific bascenev1.Activity.
 50
 51    Category: Gameplay Classes
 52
 53    These correspond to bascenev1.SessionPlayer objects, but are associated
 54    with a single bascenev1.Activity instance. This allows activities to
 55    specify their own custom bascenev1.Player types.
 56    """
 57
 58    # These are instance attrs but we define them at the type level so
 59    # their type annotations are introspectable (for docs generation).
 60    character: str
 61
 62    actor: bascenev1.Actor | None
 63    """The bascenev1.Actor associated with the player."""
 64
 65    color: Sequence[float]
 66    highlight: Sequence[float]
 67
 68    _team: TeamT
 69    _sessionplayer: bascenev1.SessionPlayer
 70    _nodeactor: bascenev1.NodeActor | None
 71    _expired: bool
 72    _postinited: bool
 73    _customdata: dict
 74
 75    # NOTE: avoiding having any __init__() here since it seems to not
 76    # get called by default if a dataclass inherits from us.
 77    # This also lets us keep trivial player classes cleaner by skipping
 78    # the super().__init__() line.
 79
 80    def postinit(self, sessionplayer: bascenev1.SessionPlayer) -> None:
 81        """Wire up a newly created player.
 82
 83        (internal)
 84        """
 85        from bascenev1._nodeactor import NodeActor
 86
 87        # Sanity check; if a dataclass is created that inherits from us,
 88        # it will define an equality operator by default which will break
 89        # internal game logic. So complain loudly if we find one.
 90        if type(self).__eq__ is not object.__eq__:
 91            raise RuntimeError(
 92                f'Player class {type(self)} defines an equality'
 93                f' operator (__eq__) which will break internal'
 94                f' logic. Please remove it.\n'
 95                f'For dataclasses you can do "dataclass(eq=False)"'
 96                f' in the class decorator.'
 97            )
 98
 99        self.actor = None
100        self.character = ''
101        self._nodeactor: bascenev1.NodeActor | None = None
102        self._sessionplayer = sessionplayer
103        self.character = sessionplayer.character
104        self.color = sessionplayer.color
105        self.highlight = sessionplayer.highlight
106        self._team = cast(TeamT, sessionplayer.sessionteam.activityteam)
107        assert self._team is not None
108        self._customdata = {}
109        self._expired = False
110        self._postinited = True
111        node = _bascenev1.newnode(
112            'player', attrs={'playerID': sessionplayer.id}
113        )
114        self._nodeactor = NodeActor(node)
115        sessionplayer.setnode(node)
116
117    def leave(self) -> None:
118        """Called when the Player leaves a running game.
119
120        (internal)
121        """
122        assert self._postinited
123        assert not self._expired
124        try:
125            # If they still have an actor, kill it.
126            if self.actor:
127                self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME))
128            self.actor = None
129        except Exception:
130            logging.exception('Error killing actor on leave for %s.', self)
131        self._nodeactor = None
132        del self._team
133        del self._customdata
134
135    def expire(self) -> None:
136        """Called when the Player is expiring (when its Activity does so).
137
138        (internal)
139        """
140        assert self._postinited
141        assert not self._expired
142        self._expired = True
143
144        try:
145            self.on_expire()
146        except Exception:
147            logging.exception('Error in on_expire for %s.', self)
148
149        self._nodeactor = None
150        self.actor = None
151        del self._team
152        del self._customdata
153
154    def on_expire(self) -> None:
155        """Can be overridden to handle player expiration.
156
157        The player expires when the Activity it is a part of expires.
158        Expired players should no longer run any game logic (which will
159        likely error). They should, however, remove any references to
160        players/teams/games/etc. which could prevent them from being freed.
161        """
162
163    @property
164    def team(self) -> TeamT:
165        """The bascenev1.Team for this player."""
166        assert self._postinited
167        assert not self._expired
168        return self._team
169
170    @property
171    def customdata(self) -> dict:
172        """Arbitrary values associated with the player.
173        Though it is encouraged that most player values be properly defined
174        on the bascenev1.Player subclass, it may be useful for player-agnostic
175        objects to store values here. This dict is cleared when the player
176        leaves or expires so objects stored here will be disposed of at
177        the expected time, unlike the Player instance itself which may
178        continue to be referenced after it is no longer part of the game.
179        """
180        assert self._postinited
181        assert not self._expired
182        return self._customdata
183
184    @property
185    def sessionplayer(self) -> bascenev1.SessionPlayer:
186        """Return the bascenev1.SessionPlayer corresponding to this Player.
187
188        Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.
189        """
190        assert self._postinited
191        if bool(self._sessionplayer):
192            return self._sessionplayer
193        raise babase.SessionPlayerNotFoundError()
194
195    @property
196    def node(self) -> bascenev1.Node:
197        """A bascenev1.Node of type 'player' associated with this Player.
198
199        This node can be used to get a generic player position/etc.
200        """
201        assert self._postinited
202        assert not self._expired
203        assert self._nodeactor
204        return self._nodeactor.node
205
206    @property
207    def position(self) -> babase.Vec3:
208        """The position of the player, as defined by its bascenev1.Actor.
209
210        If the player currently has no actor, raises a
211        babase.ActorNotFoundError.
212        """
213        assert self._postinited
214        assert not self._expired
215        if self.actor is None:
216            raise babase.ActorNotFoundError
217        return babase.Vec3(self.node.position)
218
219    def exists(self) -> bool:
220        """Whether the underlying player still exists.
221
222        This will return False if the underlying bascenev1.SessionPlayer has
223        left the game or if the bascenev1.Activity this player was
224        associated with has ended.
225        Most functionality will fail on a nonexistent player.
226        Note that you can also use the boolean operator for this same
227        functionality, so a statement such as "if player" will do
228        the right thing both for Player objects and values of None.
229        """
230        assert self._postinited
231        return self._sessionplayer.exists() and not self._expired
232
233    def getname(self, full: bool = False, icon: bool = True) -> str:
234        """
235        Returns the player's name. If icon is True, the long version of the
236        name may include an icon.
237        """
238        assert self._postinited
239        assert not self._expired
240        return self._sessionplayer.getname(full=full, icon=icon)
241
242    def is_alive(self) -> bool:
243        """
244        Returns True if the player has a bascenev1.Actor assigned and its
245        is_alive() method return True. False is returned otherwise.
246        """
247        assert self._postinited
248        assert not self._expired
249        return self.actor is not None and self.actor.is_alive()
250
251    def get_icon(self) -> dict[str, Any]:
252        """
253        Returns the character's icon (images, colors, etc contained in a dict)
254        """
255        assert self._postinited
256        assert not self._expired
257        return self._sessionplayer.get_icon()
258
259    def assigninput(
260        self,
261        inputtype: babase.InputType | tuple[babase.InputType, ...],
262        call: Callable,
263    ) -> None:
264        """
265        Set the python callable to be run for one or more types of input.
266        """
267        assert self._postinited
268        assert not self._expired
269        return self._sessionplayer.assigninput(type=inputtype, call=call)
270
271    def resetinput(self) -> None:
272        """
273        Clears out the player's assigned input actions.
274        """
275        assert self._postinited
276        assert not self._expired
277        self._sessionplayer.resetinput()
278
279    def __bool__(self) -> bool:
280        return self.exists()

A player in a specific bascenev1.Activity.

Category: Gameplay Classes

These correspond to bascenev1.SessionPlayer objects, but are associated with a single bascenev1.Activity instance. This allows activities to specify their own custom bascenev1.Player types.

character: str
actor: Actor | None

The bascenev1.Actor associated with the player.

color: Sequence[float]
highlight: Sequence[float]
def on_expire(self) -> None:
154    def on_expire(self) -> None:
155        """Can be overridden to handle player expiration.
156
157        The player expires when the Activity it is a part of expires.
158        Expired players should no longer run any game logic (which will
159        likely error). They should, however, remove any references to
160        players/teams/games/etc. which could prevent them from being freed.
161        """

Can be overridden to handle player expiration.

The player expires when the Activity it is a part of expires. Expired players should no longer run any game logic (which will likely error). They should, however, remove any references to players/teams/games/etc. which could prevent them from being freed.

team: ~TeamT
163    @property
164    def team(self) -> TeamT:
165        """The bascenev1.Team for this player."""
166        assert self._postinited
167        assert not self._expired
168        return self._team

The bascenev1.Team for this player.

customdata: dict
170    @property
171    def customdata(self) -> dict:
172        """Arbitrary values associated with the player.
173        Though it is encouraged that most player values be properly defined
174        on the bascenev1.Player subclass, it may be useful for player-agnostic
175        objects to store values here. This dict is cleared when the player
176        leaves or expires so objects stored here will be disposed of at
177        the expected time, unlike the Player instance itself which may
178        continue to be referenced after it is no longer part of the game.
179        """
180        assert self._postinited
181        assert not self._expired
182        return self._customdata

Arbitrary values associated with the player. Though it is encouraged that most player values be properly defined on the bascenev1.Player subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the player leaves or expires so objects stored here will be disposed of at the expected time, unlike the Player instance itself which may continue to be referenced after it is no longer part of the game.

sessionplayer: SessionPlayer
184    @property
185    def sessionplayer(self) -> bascenev1.SessionPlayer:
186        """Return the bascenev1.SessionPlayer corresponding to this Player.
187
188        Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.
189        """
190        assert self._postinited
191        if bool(self._sessionplayer):
192            return self._sessionplayer
193        raise babase.SessionPlayerNotFoundError()

Return the bascenev1.SessionPlayer corresponding to this Player.

Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.

node: Node
195    @property
196    def node(self) -> bascenev1.Node:
197        """A bascenev1.Node of type 'player' associated with this Player.
198
199        This node can be used to get a generic player position/etc.
200        """
201        assert self._postinited
202        assert not self._expired
203        assert self._nodeactor
204        return self._nodeactor.node

A bascenev1.Node of type 'player' associated with this Player.

This node can be used to get a generic player position/etc.

position: Vec3
206    @property
207    def position(self) -> babase.Vec3:
208        """The position of the player, as defined by its bascenev1.Actor.
209
210        If the player currently has no actor, raises a
211        babase.ActorNotFoundError.
212        """
213        assert self._postinited
214        assert not self._expired
215        if self.actor is None:
216            raise babase.ActorNotFoundError
217        return babase.Vec3(self.node.position)

The position of the player, as defined by its bascenev1.Actor.

If the player currently has no actor, raises a babase.ActorNotFoundError.

def exists(self) -> bool:
219    def exists(self) -> bool:
220        """Whether the underlying player still exists.
221
222        This will return False if the underlying bascenev1.SessionPlayer has
223        left the game or if the bascenev1.Activity this player was
224        associated with has ended.
225        Most functionality will fail on a nonexistent player.
226        Note that you can also use the boolean operator for this same
227        functionality, so a statement such as "if player" will do
228        the right thing both for Player objects and values of None.
229        """
230        assert self._postinited
231        return self._sessionplayer.exists() and not self._expired

Whether the underlying player still exists.

This will return False if the underlying bascenev1.SessionPlayer has left the game or if the bascenev1.Activity this player was associated with has ended. Most functionality will fail on a nonexistent player. Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do the right thing both for Player objects and values of None.

def getname(self, full: bool = False, icon: bool = True) -> str:
233    def getname(self, full: bool = False, icon: bool = True) -> str:
234        """
235        Returns the player's name. If icon is True, the long version of the
236        name may include an icon.
237        """
238        assert self._postinited
239        assert not self._expired
240        return self._sessionplayer.getname(full=full, icon=icon)

Returns the player's name. If icon is True, the long version of the name may include an icon.

def is_alive(self) -> bool:
242    def is_alive(self) -> bool:
243        """
244        Returns True if the player has a bascenev1.Actor assigned and its
245        is_alive() method return True. False is returned otherwise.
246        """
247        assert self._postinited
248        assert not self._expired
249        return self.actor is not None and self.actor.is_alive()

Returns True if the player has a bascenev1.Actor assigned and its is_alive() method return True. False is returned otherwise.

def get_icon(self) -> dict[str, typing.Any]:
251    def get_icon(self) -> dict[str, Any]:
252        """
253        Returns the character's icon (images, colors, etc contained in a dict)
254        """
255        assert self._postinited
256        assert not self._expired
257        return self._sessionplayer.get_icon()

Returns the character's icon (images, colors, etc contained in a dict)

def assigninput( self, inputtype: InputType | tuple[InputType, ...], call: Callable) -> None:
259    def assigninput(
260        self,
261        inputtype: babase.InputType | tuple[babase.InputType, ...],
262        call: Callable,
263    ) -> None:
264        """
265        Set the python callable to be run for one or more types of input.
266        """
267        assert self._postinited
268        assert not self._expired
269        return self._sessionplayer.assigninput(type=inputtype, call=call)

Set the python callable to be run for one or more types of input.

def resetinput(self) -> None:
271    def resetinput(self) -> None:
272        """
273        Clears out the player's assigned input actions.
274        """
275        assert self._postinited
276        assert not self._expired
277        self._sessionplayer.resetinput()

Clears out the player's assigned input actions.

class PlayerDiedMessage:
 74class PlayerDiedMessage:
 75    """A message saying a bascenev1.Player has died.
 76
 77    Category: **Message Classes**
 78    """
 79
 80    killed: bool
 81    """If True, the player was killed;
 82       If False, they left the game or the round ended."""
 83
 84    how: DeathType
 85    """The particular type of death."""
 86
 87    def __init__(
 88        self,
 89        player: bascenev1.Player,
 90        was_killed: bool,
 91        killerplayer: bascenev1.Player | None,
 92        how: DeathType,
 93    ):
 94        """Instantiate a message with the given values."""
 95
 96        # Invalid refs should never be passed as args.
 97        assert player.exists()
 98        self._player = player
 99
100        # Invalid refs should never be passed as args.
101        assert killerplayer is None or killerplayer.exists()
102        self._killerplayer = killerplayer
103        self.killed = was_killed
104        self.how = how
105
106    def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None:
107        """Return the bascenev1.Player responsible for the killing, if any.
108
109        Pass the Player type being used by the current game.
110        """
111        assert isinstance(self._killerplayer, (playertype, type(None)))
112        return self._killerplayer
113
114    def getplayer(self, playertype: type[PlayerT]) -> PlayerT:
115        """Return the bascenev1.Player that died.
116
117        The type of player for the current activity should be passed so that
118        the type-checker properly identifies the returned value as one.
119        """
120        player: Any = self._player
121        assert isinstance(player, playertype)
122
123        # We should never be delivering invalid refs.
124        # (could theoretically happen if someone holds on to us)
125        assert player.exists()
126        return player

A message saying a bascenev1.Player has died.

Category: Message Classes

PlayerDiedMessage( player: Player, was_killed: bool, killerplayer: Player | None, how: DeathType)
 87    def __init__(
 88        self,
 89        player: bascenev1.Player,
 90        was_killed: bool,
 91        killerplayer: bascenev1.Player | None,
 92        how: DeathType,
 93    ):
 94        """Instantiate a message with the given values."""
 95
 96        # Invalid refs should never be passed as args.
 97        assert player.exists()
 98        self._player = player
 99
100        # Invalid refs should never be passed as args.
101        assert killerplayer is None or killerplayer.exists()
102        self._killerplayer = killerplayer
103        self.killed = was_killed
104        self.how = how

Instantiate a message with the given values.

killed: bool

If True, the player was killed; If False, they left the game or the round ended.

how: DeathType

The particular type of death.

def getkillerplayer(self, playertype: type[~PlayerT]) -> Optional[~PlayerT]:
106    def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None:
107        """Return the bascenev1.Player responsible for the killing, if any.
108
109        Pass the Player type being used by the current game.
110        """
111        assert isinstance(self._killerplayer, (playertype, type(None)))
112        return self._killerplayer

Return the bascenev1.Player responsible for the killing, if any.

Pass the Player type being used by the current game.

def getplayer(self, playertype: type[~PlayerT]) -> ~PlayerT:
114    def getplayer(self, playertype: type[PlayerT]) -> PlayerT:
115        """Return the bascenev1.Player that died.
116
117        The type of player for the current activity should be passed so that
118        the type-checker properly identifies the returned value as one.
119        """
120        player: Any = self._player
121        assert isinstance(player, playertype)
122
123        # We should never be delivering invalid refs.
124        # (could theoretically happen if someone holds on to us)
125        assert player.exists()
126        return player

Return the bascenev1.Player that died.

The type of player for the current activity should be passed so that the type-checker properly identifies the returned value as one.

@dataclass
class PlayerProfilesChangedMessage:
291@dataclass
292class PlayerProfilesChangedMessage:
293    """Signals player profiles may have changed and should be reloaded."""

Signals player profiles may have changed and should be reloaded.

@dataclass
class PlayerInfo:
26@dataclass
27class PlayerInfo:
28    """Holds basic info about a player.
29
30    Category: Gameplay Classes
31    """
32
33    name: str
34    character: str

Holds basic info about a player.

Category: Gameplay Classes

PlayerInfo(name: str, character: str)
name: str
character: str
class PlayerNotFoundError(bascenev1.NotFoundError):
33class PlayerNotFoundError(NotFoundError):
34    """Exception raised when an expected player does not exist.
35
36    Category: **Exception Classes**
37    """

Exception raised when an expected player does not exist.

Category: Exception Classes

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class PlayerRecord:
 35class PlayerRecord:
 36    """Stats for an individual player in a bascenev1.Stats object.
 37
 38    Category: **Gameplay Classes**
 39
 40    This does not necessarily correspond to a bascenev1.Player that is
 41    still present (stats may be retained for players that leave
 42    mid-game)
 43    """
 44
 45    character: str
 46
 47    def __init__(
 48        self,
 49        name: str,
 50        name_full: str,
 51        sessionplayer: bascenev1.SessionPlayer,
 52        stats: bascenev1.Stats,
 53    ):
 54        self.name = name
 55        self.name_full = name_full
 56        self.score = 0
 57        self.accumscore = 0
 58        self.kill_count = 0
 59        self.accum_kill_count = 0
 60        self.killed_count = 0
 61        self.accum_killed_count = 0
 62        self._multi_kill_timer: bascenev1.Timer | None = None
 63        self._multi_kill_count = 0
 64        self._stats = weakref.ref(stats)
 65        self._last_sessionplayer: bascenev1.SessionPlayer | None = None
 66        self._sessionplayer: bascenev1.SessionPlayer | None = None
 67        self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None
 68        self.streak = 0
 69        self.associate_with_sessionplayer(sessionplayer)
 70
 71    @property
 72    def team(self) -> bascenev1.SessionTeam:
 73        """The bascenev1.SessionTeam the last associated player was last on.
 74
 75        This can still return a valid result even if the player is gone.
 76        Raises a bascenev1.SessionTeamNotFoundError if the team no longer
 77        exists.
 78        """
 79        assert self._sessionteam is not None
 80        team = self._sessionteam()
 81        if team is None:
 82            raise babase.SessionTeamNotFoundError()
 83        return team
 84
 85    @property
 86    def player(self) -> bascenev1.SessionPlayer:
 87        """Return the instance's associated bascenev1.SessionPlayer.
 88
 89        Raises a bascenev1.SessionPlayerNotFoundError if the player
 90        no longer exists.
 91        """
 92        if not self._sessionplayer:
 93            raise babase.SessionPlayerNotFoundError()
 94        return self._sessionplayer
 95
 96    def getname(self, full: bool = False) -> str:
 97        """Return the player entry's name."""
 98        return self.name_full if full else self.name
 99
100    def get_icon(self) -> dict[str, Any]:
101        """Get the icon for this instance's player."""
102        player = self._last_sessionplayer
103        assert player is not None
104        return player.get_icon()
105
106    def cancel_multi_kill_timer(self) -> None:
107        """Cancel any multi-kill timer for this player entry."""
108        self._multi_kill_timer = None
109
110    def getactivity(self) -> bascenev1.Activity | None:
111        """Return the bascenev1.Activity this instance is associated with.
112
113        Returns None if the activity no longer exists."""
114        stats = self._stats()
115        if stats is not None:
116            return stats.getactivity()
117        return None
118
119    def associate_with_sessionplayer(
120        self, sessionplayer: bascenev1.SessionPlayer
121    ) -> None:
122        """Associate this entry with a bascenev1.SessionPlayer."""
123        self._sessionteam = weakref.ref(sessionplayer.sessionteam)
124        self.character = sessionplayer.character
125        self._last_sessionplayer = sessionplayer
126        self._sessionplayer = sessionplayer
127        self.streak = 0
128
129    def _end_multi_kill(self) -> None:
130        self._multi_kill_timer = None
131        self._multi_kill_count = 0
132
133    def get_last_sessionplayer(self) -> bascenev1.SessionPlayer:
134        """Return the last bascenev1.Player we were associated with."""
135        assert self._last_sessionplayer is not None
136        return self._last_sessionplayer
137
138    def submit_kill(self, showpoints: bool = True) -> None:
139        """Submit a kill for this player entry."""
140        # FIXME Clean this up.
141        # pylint: disable=too-many-statements
142
143        self._multi_kill_count += 1
144        stats = self._stats()
145        assert stats
146        if self._multi_kill_count == 1:
147            score = 0
148            name = None
149            delay = 0.0
150            color = (0.0, 0.0, 0.0, 1.0)
151            scale = 1.0
152            sound = None
153        elif self._multi_kill_count == 2:
154            score = 20
155            name = babase.Lstr(resource='twoKillText')
156            color = (0.1, 1.0, 0.0, 1)
157            scale = 1.0
158            delay = 0.0
159            sound = stats.orchestrahitsound1
160        elif self._multi_kill_count == 3:
161            score = 40
162            name = babase.Lstr(resource='threeKillText')
163            color = (1.0, 0.7, 0.0, 1)
164            scale = 1.1
165            delay = 0.3
166            sound = stats.orchestrahitsound2
167        elif self._multi_kill_count == 4:
168            score = 60
169            name = babase.Lstr(resource='fourKillText')
170            color = (1.0, 1.0, 0.0, 1)
171            scale = 1.2
172            delay = 0.6
173            sound = stats.orchestrahitsound3
174        elif self._multi_kill_count == 5:
175            score = 80
176            name = babase.Lstr(resource='fiveKillText')
177            color = (1.0, 0.5, 0.0, 1)
178            scale = 1.3
179            delay = 0.9
180            sound = stats.orchestrahitsound4
181        else:
182            score = 100
183            name = babase.Lstr(
184                resource='multiKillText',
185                subs=[('${COUNT}', str(self._multi_kill_count))],
186            )
187            color = (1.0, 0.5, 0.0, 1)
188            scale = 1.3
189            delay = 1.0
190            sound = stats.orchestrahitsound4
191
192        def _apply(
193            name2: babase.Lstr,
194            score2: int,
195            showpoints2: bool,
196            color2: tuple[float, float, float, float],
197            scale2: float,
198            sound2: bascenev1.Sound | None,
199        ) -> None:
200            from bascenev1lib.actor.popuptext import PopupText
201
202            # Only award this if they're still alive and we can get
203            # a current position for them.
204            our_pos: babase.Vec3 | None = None
205            if self._sessionplayer:
206                if self._sessionplayer.activityplayer is not None:
207                    try:
208                        our_pos = self._sessionplayer.activityplayer.position
209                    except babase.NotFoundError:
210                        pass
211            if our_pos is None:
212                return
213
214            # Jitter position a bit since these often come in clusters.
215            our_pos = babase.Vec3(
216                our_pos[0] + (random.random() - 0.5) * 2.0,
217                our_pos[1] + (random.random() - 0.5) * 2.0,
218                our_pos[2] + (random.random() - 0.5) * 2.0,
219            )
220            activity = self.getactivity()
221            if activity is not None:
222                PopupText(
223                    babase.Lstr(
224                        value=(('+' + str(score2) + ' ') if showpoints2 else '')
225                        + '${N}',
226                        subs=[('${N}', name2)],
227                    ),
228                    color=color2,
229                    scale=scale2,
230                    position=our_pos,
231                ).autoretain()
232            if sound2:
233                sound2.play()
234
235            self.score += score2
236            self.accumscore += score2
237
238            # Inform a running game of the score.
239            if score2 != 0 and activity is not None:
240                activity.handlemessage(PlayerScoredMessage(score=score2))
241
242        if name is not None:
243            _bascenev1.timer(
244                0.3 + delay,
245                babase.Call(
246                    _apply, name, score, showpoints, color, scale, sound
247                ),
248            )
249
250        # Keep the tally rollin'...
251        # set a timer for a bit in the future.
252        self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)

Stats for an individual player in a bascenev1.Stats object.

Category: Gameplay Classes

This does not necessarily correspond to a bascenev1.Player that is still present (stats may be retained for players that leave mid-game)

PlayerRecord( name: str, name_full: str, sessionplayer: SessionPlayer, stats: Stats)
47    def __init__(
48        self,
49        name: str,
50        name_full: str,
51        sessionplayer: bascenev1.SessionPlayer,
52        stats: bascenev1.Stats,
53    ):
54        self.name = name
55        self.name_full = name_full
56        self.score = 0
57        self.accumscore = 0
58        self.kill_count = 0
59        self.accum_kill_count = 0
60        self.killed_count = 0
61        self.accum_killed_count = 0
62        self._multi_kill_timer: bascenev1.Timer | None = None
63        self._multi_kill_count = 0
64        self._stats = weakref.ref(stats)
65        self._last_sessionplayer: bascenev1.SessionPlayer | None = None
66        self._sessionplayer: bascenev1.SessionPlayer | None = None
67        self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None
68        self.streak = 0
69        self.associate_with_sessionplayer(sessionplayer)
character: str
name
name_full
score
accumscore
kill_count
accum_kill_count
killed_count
accum_killed_count
streak
team: SessionTeam
71    @property
72    def team(self) -> bascenev1.SessionTeam:
73        """The bascenev1.SessionTeam the last associated player was last on.
74
75        This can still return a valid result even if the player is gone.
76        Raises a bascenev1.SessionTeamNotFoundError if the team no longer
77        exists.
78        """
79        assert self._sessionteam is not None
80        team = self._sessionteam()
81        if team is None:
82            raise babase.SessionTeamNotFoundError()
83        return team

The bascenev1.SessionTeam the last associated player was last on.

This can still return a valid result even if the player is gone. Raises a bascenev1.SessionTeamNotFoundError if the team no longer exists.

player: SessionPlayer
85    @property
86    def player(self) -> bascenev1.SessionPlayer:
87        """Return the instance's associated bascenev1.SessionPlayer.
88
89        Raises a bascenev1.SessionPlayerNotFoundError if the player
90        no longer exists.
91        """
92        if not self._sessionplayer:
93            raise babase.SessionPlayerNotFoundError()
94        return self._sessionplayer

Return the instance's associated bascenev1.SessionPlayer.

Raises a bascenev1.SessionPlayerNotFoundError if the player no longer exists.

def getname(self, full: bool = False) -> str:
96    def getname(self, full: bool = False) -> str:
97        """Return the player entry's name."""
98        return self.name_full if full else self.name

Return the player entry's name.

def get_icon(self) -> dict[str, typing.Any]:
100    def get_icon(self) -> dict[str, Any]:
101        """Get the icon for this instance's player."""
102        player = self._last_sessionplayer
103        assert player is not None
104        return player.get_icon()

Get the icon for this instance's player.

def cancel_multi_kill_timer(self) -> None:
106    def cancel_multi_kill_timer(self) -> None:
107        """Cancel any multi-kill timer for this player entry."""
108        self._multi_kill_timer = None

Cancel any multi-kill timer for this player entry.

def getactivity(self) -> Activity | None:
110    def getactivity(self) -> bascenev1.Activity | None:
111        """Return the bascenev1.Activity this instance is associated with.
112
113        Returns None if the activity no longer exists."""
114        stats = self._stats()
115        if stats is not None:
116            return stats.getactivity()
117        return None

Return the bascenev1.Activity this instance is associated with.

Returns None if the activity no longer exists.

def associate_with_sessionplayer(self, sessionplayer: SessionPlayer) -> None:
119    def associate_with_sessionplayer(
120        self, sessionplayer: bascenev1.SessionPlayer
121    ) -> None:
122        """Associate this entry with a bascenev1.SessionPlayer."""
123        self._sessionteam = weakref.ref(sessionplayer.sessionteam)
124        self.character = sessionplayer.character
125        self._last_sessionplayer = sessionplayer
126        self._sessionplayer = sessionplayer
127        self.streak = 0

Associate this entry with a bascenev1.SessionPlayer.

def get_last_sessionplayer(self) -> SessionPlayer:
133    def get_last_sessionplayer(self) -> bascenev1.SessionPlayer:
134        """Return the last bascenev1.Player we were associated with."""
135        assert self._last_sessionplayer is not None
136        return self._last_sessionplayer

Return the last bascenev1.Player we were associated with.

def submit_kill(self, showpoints: bool = True) -> None:
138    def submit_kill(self, showpoints: bool = True) -> None:
139        """Submit a kill for this player entry."""
140        # FIXME Clean this up.
141        # pylint: disable=too-many-statements
142
143        self._multi_kill_count += 1
144        stats = self._stats()
145        assert stats
146        if self._multi_kill_count == 1:
147            score = 0
148            name = None
149            delay = 0.0
150            color = (0.0, 0.0, 0.0, 1.0)
151            scale = 1.0
152            sound = None
153        elif self._multi_kill_count == 2:
154            score = 20
155            name = babase.Lstr(resource='twoKillText')
156            color = (0.1, 1.0, 0.0, 1)
157            scale = 1.0
158            delay = 0.0
159            sound = stats.orchestrahitsound1
160        elif self._multi_kill_count == 3:
161            score = 40
162            name = babase.Lstr(resource='threeKillText')
163            color = (1.0, 0.7, 0.0, 1)
164            scale = 1.1
165            delay = 0.3
166            sound = stats.orchestrahitsound2
167        elif self._multi_kill_count == 4:
168            score = 60
169            name = babase.Lstr(resource='fourKillText')
170            color = (1.0, 1.0, 0.0, 1)
171            scale = 1.2
172            delay = 0.6
173            sound = stats.orchestrahitsound3
174        elif self._multi_kill_count == 5:
175            score = 80
176            name = babase.Lstr(resource='fiveKillText')
177            color = (1.0, 0.5, 0.0, 1)
178            scale = 1.3
179            delay = 0.9
180            sound = stats.orchestrahitsound4
181        else:
182            score = 100
183            name = babase.Lstr(
184                resource='multiKillText',
185                subs=[('${COUNT}', str(self._multi_kill_count))],
186            )
187            color = (1.0, 0.5, 0.0, 1)
188            scale = 1.3
189            delay = 1.0
190            sound = stats.orchestrahitsound4
191
192        def _apply(
193            name2: babase.Lstr,
194            score2: int,
195            showpoints2: bool,
196            color2: tuple[float, float, float, float],
197            scale2: float,
198            sound2: bascenev1.Sound | None,
199        ) -> None:
200            from bascenev1lib.actor.popuptext import PopupText
201
202            # Only award this if they're still alive and we can get
203            # a current position for them.
204            our_pos: babase.Vec3 | None = None
205            if self._sessionplayer:
206                if self._sessionplayer.activityplayer is not None:
207                    try:
208                        our_pos = self._sessionplayer.activityplayer.position
209                    except babase.NotFoundError:
210                        pass
211            if our_pos is None:
212                return
213
214            # Jitter position a bit since these often come in clusters.
215            our_pos = babase.Vec3(
216                our_pos[0] + (random.random() - 0.5) * 2.0,
217                our_pos[1] + (random.random() - 0.5) * 2.0,
218                our_pos[2] + (random.random() - 0.5) * 2.0,
219            )
220            activity = self.getactivity()
221            if activity is not None:
222                PopupText(
223                    babase.Lstr(
224                        value=(('+' + str(score2) + ' ') if showpoints2 else '')
225                        + '${N}',
226                        subs=[('${N}', name2)],
227                    ),
228                    color=color2,
229                    scale=scale2,
230                    position=our_pos,
231                ).autoretain()
232            if sound2:
233                sound2.play()
234
235            self.score += score2
236            self.accumscore += score2
237
238            # Inform a running game of the score.
239            if score2 != 0 and activity is not None:
240                activity.handlemessage(PlayerScoredMessage(score=score2))
241
242        if name is not None:
243            _bascenev1.timer(
244                0.3 + delay,
245                babase.Call(
246                    _apply, name, score, showpoints, color, scale, sound
247                ),
248            )
249
250        # Keep the tally rollin'...
251        # set a timer for a bit in the future.
252        self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)

Submit a kill for this player entry.

@dataclass
class PlayerScoredMessage:
24@dataclass
25class PlayerScoredMessage:
26    """Informs something that a bascenev1.Player scored.
27
28    Category: **Message Classes**
29    """
30
31    score: int
32    """The score value."""

Informs something that a bascenev1.Player scored.

Category: Message Classes

PlayerScoredMessage(score: int)
score: int

The score value.

class Plugin:
322class Plugin:
323    """A plugin to alter app behavior in some way.
324
325    Category: **App Classes**
326
327    Plugins are discoverable by the meta-tag system
328    and the user can select which ones they want to enable.
329    Enabled plugins are then called at specific times as the
330    app is running in order to modify its behavior in some way.
331    """
332
333    def on_app_running(self) -> None:
334        """Called when the app reaches the running state."""
335
336    def on_app_suspend(self) -> None:
337        """Called when the app enters the suspended state."""
338
339    def on_app_unsuspend(self) -> None:
340        """Called when the app exits the suspended state."""
341
342    def on_app_shutdown(self) -> None:
343        """Called when the app is beginning the shutdown process."""
344
345    def on_app_shutdown_complete(self) -> None:
346        """Called when the app has completed the shutdown process."""
347
348    def has_settings_ui(self) -> bool:
349        """Called to ask if we have settings UI we can show."""
350        return False
351
352    def show_settings_ui(self, source_widget: Any | None) -> None:
353        """Called to show our settings UI."""

A plugin to alter app behavior in some way.

Category: App Classes

Plugins are discoverable by the meta-tag system and the user can select which ones they want to enable. Enabled plugins are then called at specific times as the app is running in order to modify its behavior in some way.

def on_app_running(self) -> None:
333    def on_app_running(self) -> None:
334        """Called when the app reaches the running state."""

Called when the app reaches the running state.

def on_app_suspend(self) -> None:
336    def on_app_suspend(self) -> None:
337        """Called when the app enters the suspended state."""

Called when the app enters the suspended state.

def on_app_unsuspend(self) -> None:
339    def on_app_unsuspend(self) -> None:
340        """Called when the app exits the suspended state."""

Called when the app exits the suspended state.

def on_app_shutdown(self) -> None:
342    def on_app_shutdown(self) -> None:
343        """Called when the app is beginning the shutdown process."""

Called when the app is beginning the shutdown process.

def on_app_shutdown_complete(self) -> None:
345    def on_app_shutdown_complete(self) -> None:
346        """Called when the app has completed the shutdown process."""

Called when the app has completed the shutdown process.

def has_settings_ui(self) -> bool:
348    def has_settings_ui(self) -> bool:
349        """Called to ask if we have settings UI we can show."""
350        return False

Called to ask if we have settings UI we can show.

def show_settings_ui(self, source_widget: typing.Any | None) -> None:
352    def show_settings_ui(self, source_widget: Any | None) -> None:
353        """Called to show our settings UI."""

Called to show our settings UI.

@dataclass
class PowerupAcceptMessage:
38@dataclass
39class PowerupAcceptMessage:
40    """A message informing a bascenev1.Powerup that it was accepted.
41
42    Category: **Message Classes**
43
44    This is generally sent in response to a bascenev1.PowerupMessage
45    to inform the box (or whoever granted it) that it can go away.
46    """

A message informing a bascenev1.Powerup that it was accepted.

Category: Message Classes

This is generally sent in response to a bascenev1.PowerupMessage to inform the box (or whoever granted it) that it can go away.

@dataclass
class PowerupMessage:
17@dataclass
18class PowerupMessage:
19    """A message telling an object to accept a powerup.
20
21    Category: **Message Classes**
22
23    This message is normally received by touching a bascenev1.PowerupBox.
24    """
25
26    poweruptype: str
27    """The type of powerup to be granted (a string).
28       See bascenev1.Powerup.poweruptype for available type values."""
29
30    sourcenode: bascenev1.Node | None = None
31    """The node the powerup game from, or None otherwise.
32       If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be
33       sent back to the sourcenode to inform it of the fact. This will
34       generally cause the powerup box to make a sound and disappear or
35       whatnot."""

A message telling an object to accept a powerup.

Category: Message Classes

This message is normally received by touching a bascenev1.PowerupBox.

PowerupMessage(poweruptype: str, sourcenode: Node | None = None)
poweruptype: str

The type of powerup to be granted (a string). See bascenev1.Powerup.poweruptype for available type values.

sourcenode: Node | None = None

The node the powerup game from, or None otherwise. If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be sent back to the sourcenode to inform it of the fact. This will generally cause the powerup box to make a sound and disappear or whatnot.

def printnodes() -> None:
1589def printnodes() -> None:
1590    """Print various info about existing nodes; useful for debugging.
1591
1592    Category: **Gameplay Functions**
1593    """
1594    return None

Print various info about existing nodes; useful for debugging.

Category: Gameplay Functions

def pushcall( call: Callable, from_other_thread: bool = False, suppress_other_thread_warning: bool = False, other_thread_use_fg_context: bool = False, raw: bool = False) -> None:
1326def pushcall(
1327    call: Callable,
1328    from_other_thread: bool = False,
1329    suppress_other_thread_warning: bool = False,
1330    other_thread_use_fg_context: bool = False,
1331    raw: bool = False,
1332) -> None:
1333    """Push a call to the logic event-loop.
1334    Category: **General Utility Functions**
1335
1336    This call expects to be used in the logic thread, and will automatically
1337    save and restore the babase.Context to behave seamlessly.
1338
1339    If you want to push a call from outside of the logic thread,
1340    however, you can pass 'from_other_thread' as True. In this case
1341    the call will always run in the UI context_ref on the logic thread
1342    or whichever context_ref is in the foreground if
1343    other_thread_use_fg_context is True.
1344    Passing raw=True will disable thread checks and context_ref sets/restores.
1345    """
1346    return None

Push a call to the logic event-loop. Category: General Utility Functions

This call expects to be used in the logic thread, and will automatically save and restore the babase.Context to behave seamlessly.

If you want to push a call from outside of the logic thread, however, you can pass 'from_other_thread' as True. In this case the call will always run in the UI context_ref on the logic thread or whichever context_ref is in the foreground if other_thread_use_fg_context is True. Passing raw=True will disable thread checks and context_ref sets/restores.

def register_map(maptype: type[Map]) -> None:
370def register_map(maptype: type[Map]) -> None:
371    """Register a map class with the game."""
372    assert babase.app.classic is not None
373    if maptype.name in babase.app.classic.maps:
374        raise RuntimeError(f'Map "{maptype.name}" is already registered.')
375    babase.app.classic.maps[maptype.name] = maptype

Register a map class with the game.

def safecolor( color: Sequence[float], target_intensity: float = 0.6) -> tuple[float, ...]:
1400def safecolor(
1401    color: Sequence[float], target_intensity: float = 0.6
1402) -> tuple[float, ...]:
1403    """Given a color tuple, return a color safe to display as text.
1404
1405    Category: **General Utility Functions**
1406
1407    Accepts tuples of length 3 or 4. This will slightly brighten very
1408    dark colors, etc.
1409    """
1410    return (0.0, 0.0, 0.0)

Given a color tuple, return a color safe to display as text.

Category: General Utility Functions

Accepts tuples of length 3 or 4. This will slightly brighten very dark colors, etc.

def screenmessage( message: str | Lstr, color: Optional[Sequence[float]] = None, log: bool = False) -> None:
1413def screenmessage(
1414    message: str | babase.Lstr,
1415    color: Sequence[float] | None = None,
1416    log: bool = False,
1417) -> None:
1418    """Print a message to the local client's screen, in a given color.
1419
1420    Category: **General Utility Functions**
1421
1422    Note that this version of the function is purely for local display.
1423    To broadcast screen messages in network play, look for methods such as
1424    broadcastmessage() provided by the scene-version packages.
1425    """
1426    return None

Print a message to the local client's screen, in a given color.

Category: General Utility Functions

Note that this version of the function is purely for local display. To broadcast screen messages in network play, look for methods such as broadcastmessage() provided by the scene-version packages.

class SceneV1AppMode(bascenev1.AppMode):
24class SceneV1AppMode(AppMode):
25    """Our app-mode."""
26
27    @override
28    @classmethod
29    def get_app_experience(cls) -> AppExperience:
30        return AppExperience.MELEE
31
32    @override
33    @classmethod
34    def _supports_intent(cls, intent: AppIntent) -> bool:
35        # We support default and exec intents currently.
36        return isinstance(intent, AppIntentExec | AppIntentDefault)
37
38    @override
39    def handle_intent(self, intent: AppIntent) -> None:
40        if isinstance(intent, AppIntentExec):
41            _bascenev1.handle_app_intent_exec(intent.code)
42            return
43        assert isinstance(intent, AppIntentDefault)
44        _bascenev1.handle_app_intent_default()
45
46    @override
47    def on_activate(self) -> None:
48        # Let the native layer do its thing.
49        _bascenev1.on_app_mode_activate()
50
51    @override
52    def on_deactivate(self) -> None:
53        # Let the native layer do its thing.
54        _bascenev1.on_app_mode_deactivate()
55
56    @override
57    def on_app_active_changed(self) -> None:
58        # If we've gone inactive, bring up the main menu, which has the
59        # side effect of pausing the action (when possible).
60        if not app.active:
61            invoke_main_menu()

Our app-mode.

@override
@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
27    @override
28    @classmethod
29    def get_app_experience(cls) -> AppExperience:
30        return AppExperience.MELEE

Return the overall experience provided by this mode.

@override
def handle_intent(self, intent: AppIntent) -> None:
38    @override
39    def handle_intent(self, intent: AppIntent) -> None:
40        if isinstance(intent, AppIntentExec):
41            _bascenev1.handle_app_intent_exec(intent.code)
42            return
43        assert isinstance(intent, AppIntentDefault)
44        _bascenev1.handle_app_intent_default()

Handle an intent.

@override
def on_activate(self) -> None:
46    @override
47    def on_activate(self) -> None:
48        # Let the native layer do its thing.
49        _bascenev1.on_app_mode_activate()

Called when the mode is being activated.

@override
def on_deactivate(self) -> None:
51    @override
52    def on_deactivate(self) -> None:
53        # Let the native layer do its thing.
54        _bascenev1.on_app_mode_deactivate()

Called when the mode is being deactivated.

@override
def on_app_active_changed(self) -> None:
56    @override
57    def on_app_active_changed(self) -> None:
58        # If we've gone inactive, bring up the main menu, which has the
59        # side effect of pausing the action (when possible).
60        if not app.active:
61            invoke_main_menu()

Called when babase.app.active changes.

The app-mode may want to take action such as pausing a running game in such cases.

Inherited Members
AppMode
can_handle_intent
@dataclass
class ScoreConfig:
28@dataclass
29class ScoreConfig:
30    """Settings for how a game handles scores.
31
32    Category: **Gameplay Classes**
33    """
34
35    label: str = 'Score'
36    """A label show to the user for scores; 'Score', 'Time Survived', etc."""
37
38    scoretype: bascenev1.ScoreType = ScoreType.POINTS
39    """How the score value should be displayed."""
40
41    lower_is_better: bool = False
42    """Whether lower scores are preferable. Higher scores are by default."""
43
44    none_is_winner: bool = False
45    """Whether a value of None is considered better than other scores.
46       By default it is not."""
47
48    version: str = ''
49    """To change high-score lists used by a game without renaming the game,
50       change this. Defaults to an empty string."""

Settings for how a game handles scores.

Category: Gameplay Classes

ScoreConfig( label: str = 'Score', scoretype: ScoreType = <ScoreType.POINTS: 'p'>, lower_is_better: bool = False, none_is_winner: bool = False, version: str = '')
label: str = 'Score'

A label show to the user for scores; 'Score', 'Time Survived', etc.

scoretype: ScoreType = <ScoreType.POINTS: 'p'>

How the score value should be displayed.

lower_is_better: bool = False

Whether lower scores are preferable. Higher scores are by default.

none_is_winner: bool = False

Whether a value of None is considered better than other scores. By default it is not.

version: str = ''

To change high-score lists used by a game without renaming the game, change this. Defaults to an empty string.

135class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
136    """A standard score screen that fades in and shows stuff for a while.
137
138    After a specified delay, player input is assigned to end the activity.
139    """
140
141    transition_time = 0.5
142    inherits_tint = True
143    inherits_vr_camera_offset = True
144    use_fixed_vr_overlay = True
145
146    default_music: MusicType | None = MusicType.SCORES
147
148    def __init__(self, settings: dict):
149        super().__init__(settings)
150        self._birth_time = babase.apptime()
151        self._min_view_time = 5.0
152        self._allow_server_transition = False
153        self._background: bascenev1.Actor | None = None
154        self._tips_text: bascenev1.Actor | None = None
155        self._kicked_off_server_shutdown = False
156        self._kicked_off_server_restart = False
157        self._default_show_tips = True
158        self._custom_continue_message: babase.Lstr | None = None
159        self._server_transitioning: bool | None = None
160
161    @override
162    def on_player_join(self, player: EmptyPlayer) -> None:
163        super().on_player_join(player)
164        time_till_assign = max(
165            0, self._birth_time + self._min_view_time - babase.apptime()
166        )
167
168        # If we're still kicking at the end of our assign-delay, assign this
169        # guy's input to trigger us.
170        _bascenev1.timer(
171            time_till_assign, babase.WeakCall(self._safe_assign, player)
172        )
173
174    @override
175    def on_transition_in(self) -> None:
176        from bascenev1lib.actor.tipstext import TipsText
177        from bascenev1lib.actor.background import Background
178
179        super().on_transition_in()
180        self._background = Background(
181            fade_time=0.5, start_faded=False, show_logo=True
182        )
183        if self._default_show_tips:
184            self._tips_text = TipsText()
185        setmusic(self.default_music)
186
187    @override
188    def on_begin(self) -> None:
189        # pylint: disable=cyclic-import
190        from bascenev1lib.actor.text import Text
191
192        super().on_begin()
193
194        # Pop up a 'press any button to continue' statement after our
195        # min-view-time show a 'press any button to continue..'
196        # thing after a bit.
197        assert babase.app.classic is not None
198        if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
199            # FIXME: Need a better way to determine whether we've probably
200            #  got a keyboard.
201            sval = babase.Lstr(resource='pressAnyKeyButtonText')
202        else:
203            sval = babase.Lstr(resource='pressAnyButtonText')
204
205        Text(
206            (
207                self._custom_continue_message
208                if self._custom_continue_message is not None
209                else sval
210            ),
211            v_attach=Text.VAttach.BOTTOM,
212            h_align=Text.HAlign.CENTER,
213            flash=True,
214            vr_depth=50,
215            position=(0, 10),
216            scale=0.8,
217            color=(0.5, 0.7, 0.5, 0.5),
218            transition=Text.Transition.IN_BOTTOM_SLOW,
219            transition_delay=self._min_view_time,
220        ).autoretain()
221
222    def _player_press(self) -> None:
223        # If this activity is a good 'end point', ask server-mode just once if
224        # it wants to do anything special like switch sessions or kill the app.
225        if (
226            self._allow_server_transition
227            and babase.app.classic is not None
228            and babase.app.classic.server is not None
229            and self._server_transitioning is None
230        ):
231            self._server_transitioning = (
232                babase.app.classic.server.handle_transition()
233            )
234            assert isinstance(self._server_transitioning, bool)
235
236        # If server-mode is handling this, don't do anything ourself.
237        if self._server_transitioning is True:
238            return
239
240        # Otherwise end the activity normally.
241        self.end()
242
243    def _safe_assign(self, player: EmptyPlayer) -> None:
244        # Just to be extra careful, don't assign if we're transitioning out.
245        # (though theoretically that should be ok).
246        if not self.is_transitioning_out() and player:
247            player.assigninput(
248                (
249                    babase.InputType.JUMP_PRESS,
250                    babase.InputType.PUNCH_PRESS,
251                    babase.InputType.BOMB_PRESS,
252                    babase.InputType.PICK_UP_PRESS,
253                ),
254                self._player_press,
255            )

A standard score screen that fades in and shows stuff for a while.

After a specified delay, player input is assigned to end the activity.

ScoreScreenActivity(settings: dict)
148    def __init__(self, settings: dict):
149        super().__init__(settings)
150        self._birth_time = babase.apptime()
151        self._min_view_time = 5.0
152        self._allow_server_transition = False
153        self._background: bascenev1.Actor | None = None
154        self._tips_text: bascenev1.Actor | None = None
155        self._kicked_off_server_shutdown = False
156        self._kicked_off_server_restart = False
157        self._default_show_tips = True
158        self._custom_continue_message: babase.Lstr | None = None
159        self._server_transitioning: bool | None = None

Creates an Activity in the current bascenev1.Session.

The activity will not be actually run until bascenev1.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.

transition_time = 0.5

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.

inherits_tint = True

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

inherits_vr_camera_offset = True

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

use_fixed_vr_overlay = True

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.

default_music: MusicType | None = <MusicType.SCORES: 'Scores'>
@override
def on_player_join(self, player: EmptyPlayer) -> None:
161    @override
162    def on_player_join(self, player: EmptyPlayer) -> None:
163        super().on_player_join(player)
164        time_till_assign = max(
165            0, self._birth_time + self._min_view_time - babase.apptime()
166        )
167
168        # If we're still kicking at the end of our assign-delay, assign this
169        # guy's input to trigger us.
170        _bascenev1.timer(
171            time_till_assign, babase.WeakCall(self._safe_assign, player)
172        )

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

(including the initial set of Players)

@override
def on_transition_in(self) -> None:
174    @override
175    def on_transition_in(self) -> None:
176        from bascenev1lib.actor.tipstext import TipsText
177        from bascenev1lib.actor.background import Background
178
179        super().on_transition_in()
180        self._background = Background(
181            fade_time=0.5, start_faded=False, show_logo=True
182        )
183        if self._default_show_tips:
184            self._tips_text = TipsText()
185        setmusic(self.default_music)

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 bascenev1.Activity.on_begin() is called.

@override
def on_begin(self) -> None:
187    @override
188    def on_begin(self) -> None:
189        # pylint: disable=cyclic-import
190        from bascenev1lib.actor.text import Text
191
192        super().on_begin()
193
194        # Pop up a 'press any button to continue' statement after our
195        # min-view-time show a 'press any button to continue..'
196        # thing after a bit.
197        assert babase.app.classic is not None
198        if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
199            # FIXME: Need a better way to determine whether we've probably
200            #  got a keyboard.
201            sval = babase.Lstr(resource='pressAnyKeyButtonText')
202        else:
203            sval = babase.Lstr(resource='pressAnyButtonText')
204
205        Text(
206            (
207                self._custom_continue_message
208                if self._custom_continue_message is not None
209                else sval
210            ),
211            v_attach=Text.VAttach.BOTTOM,
212            h_align=Text.HAlign.CENTER,
213            flash=True,
214            vr_depth=50,
215            position=(0, 10),
216            scale=0.8,
217            color=(0.5, 0.7, 0.5, 0.5),
218            transition=Text.Transition.IN_BOTTOM_SLOW,
219            transition_delay=self._min_view_time,
220        ).autoretain()

Called once the previous 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.

@unique
class ScoreType(enum.Enum):
16@unique
17class ScoreType(Enum):
18    """Type of scores.
19
20    Category: **Enums**
21    """
22
23    SECONDS = 's'
24    MILLISECONDS = 'ms'
25    POINTS = 'p'

Type of scores.

Category: Enums

SECONDS = <ScoreType.SECONDS: 's'>
MILLISECONDS = <ScoreType.MILLISECONDS: 'ms'>
POINTS = <ScoreType.POINTS: 'p'>
Inherited Members
enum.Enum
name
value
def broadcastmessage( message: str | Lstr, color: Optional[Sequence[float]] = None, top: bool = False, image: dict[str, typing.Any] | None = None, log: bool = False, clients: Optional[Sequence[int]] = None, transient: bool = False) -> None:
1001def broadcastmessage(
1002    message: str | babase.Lstr,
1003    color: Sequence[float] | None = None,
1004    top: bool = False,
1005    image: dict[str, Any] | None = None,
1006    log: bool = False,
1007    clients: Sequence[int] | None = None,
1008    transient: bool = False,
1009) -> None:
1010    """Broadcast a screen-message to clients in the current session.
1011
1012    Category: **General Utility Functions**
1013
1014    If 'top' is True, the message will go to the top message area.
1015    For 'top' messages, 'image' must be a dict containing 'texture'
1016    and 'tint_texture' textures and 'tint_color' and 'tint2_color'
1017    colors. This defines an icon to display alongside the message.
1018    If 'log' is True, the message will also be submitted to the log.
1019    'clients' can be a list of client-ids the message should be sent
1020    to, or None to specify that everyone should receive it.
1021    If 'transient' is True, the message will not be included in the
1022    game-stream and thus will not show up when viewing replays.
1023    Currently the 'clients' option only works for transient messages.
1024    """
1025    return None

Broadcast a screen-message to clients in the current session.

Category: General Utility Functions

If 'top' is True, the message will go to the top message area. For 'top' messages, 'image' must be a dict containing 'texture' and 'tint_texture' textures and 'tint_color' and 'tint2_color' colors. This defines an icon to display alongside the message. If 'log' is True, the message will also be submitted to the log. 'clients' can be a list of client-ids the message should be sent to, or None to specify that everyone should receive it. If 'transient' is True, the message will not be included in the game-stream and thus will not show up when viewing replays. Currently the 'clients' option only works for transient messages.

class Session:
 43class Session:
 44    """Defines a high level series of bascenev1.Activity-es.
 45
 46    Category: **Gameplay Classes**
 47
 48    Examples of sessions are bascenev1.FreeForAllSession,
 49    bascenev1.DualTeamSession, and bascenev1.CoopSession.
 50
 51    A Session is responsible for wrangling and transitioning between various
 52    bascenev1.Activity instances such as mini-games and score-screens, and for
 53    maintaining state between them (players, teams, score tallies, etc).
 54    """
 55
 56    use_teams: bool = False
 57    """Whether this session groups players into an explicit set of
 58       teams. If this is off, a unique team is generated for each
 59       player that joins."""
 60
 61    use_team_colors: bool = True
 62    """Whether players on a team should all adopt the colors of that
 63       team instead of their own profile colors. This only applies if
 64       use_teams is enabled."""
 65
 66    # Note: even though these are instance vars, we annotate and document them
 67    # at the class level so that looks better and nobody get lost while
 68    # reading large __init__
 69
 70    lobby: bascenev1.Lobby
 71    """The baclassic.Lobby instance where new bascenev1.Player-s go to select
 72       a Profile/Team/etc. before being added to games.
 73       Be aware this value may be None if a Session does not allow
 74       any such selection."""
 75
 76    max_players: int
 77    """The maximum number of players allowed in the Session."""
 78
 79    min_players: int
 80    """The minimum number of players who must be present for the Session
 81       to proceed past the initial joining screen"""
 82
 83    sessionplayers: list[bascenev1.SessionPlayer]
 84    """All bascenev1.SessionPlayers in the Session. Most things should use
 85       the list of bascenev1.Player-s in bascenev1.Activity; not this. Some
 86       players, such as those who have not yet selected a character, will
 87       only be found on this list."""
 88
 89    customdata: dict
 90    """A shared dictionary for objects to use as storage on this session.
 91       Ensure that keys here are unique to avoid collisions."""
 92
 93    sessionteams: list[bascenev1.SessionTeam]
 94    """All the bascenev1.SessionTeams in the Session. Most things should
 95       use the list of bascenev1.Team-s in bascenev1.Activity; not this."""
 96
 97    def __init__(
 98        self,
 99        depsets: Sequence[bascenev1.DependencySet],
100        team_names: Sequence[str] | None = None,
101        team_colors: Sequence[Sequence[float]] | None = None,
102        min_players: int = 1,
103        max_players: int = 8,
104        submit_score: bool = True,
105    ):
106        """Instantiate a session.
107
108        depsets should be a sequence of successfully resolved
109        bascenev1.DependencySet instances; one for each bascenev1.Activity
110        the session may potentially run.
111        """
112        # pylint: disable=too-many-statements
113        # pylint: disable=too-many-locals
114        # pylint: disable=cyclic-import
115        # pylint: disable=too-many-branches
116        from efro.util import empty_weakref
117        from bascenev1._dependency import (
118            Dependency,
119            AssetPackage,
120            DependencyError,
121        )
122        from bascenev1._lobby import Lobby
123        from bascenev1._stats import Stats
124        from bascenev1._gameactivity import GameActivity
125        from bascenev1._activity import Activity
126        from bascenev1._team import SessionTeam
127
128        # First off, resolve all dependency-sets we were passed.
129        # If things are missing, we'll try to gather them into a single
130        # missing-deps exception if possible to give the caller a clean
131        # path to download missing stuff and try again.
132        missing_asset_packages: set[str] = set()
133        for depset in depsets:
134            try:
135                depset.resolve()
136            except DependencyError as exc:
137                # Gather/report missing assets only; barf on anything else.
138                if all(issubclass(d.cls, AssetPackage) for d in exc.deps):
139                    for dep in exc.deps:
140                        assert isinstance(dep.config, str)
141                        missing_asset_packages.add(dep.config)
142                else:
143                    missing_info = [(d.cls, d.config) for d in exc.deps]
144                    raise RuntimeError(
145                        f'Missing non-asset dependencies: {missing_info}'
146                    ) from exc
147
148        # Throw a combined exception if we found anything missing.
149        if missing_asset_packages:
150            raise DependencyError(
151                [
152                    Dependency(AssetPackage, set_id)
153                    for set_id in missing_asset_packages
154                ]
155            )
156
157        # Ok; looks like our dependencies check out.
158        # Now give the engine a list of asset-set-ids to pass along to clients.
159        required_asset_packages: set[str] = set()
160        for depset in depsets:
161            required_asset_packages.update(depset.get_asset_package_ids())
162
163        # print('Would set host-session asset-reqs to:',
164        # required_asset_packages)
165
166        # Init our C++ layer data.
167        self._sessiondata = _bascenev1.register_session(self)
168
169        # Should remove this if possible.
170        self.tournament_id: str | None = None
171
172        self.sessionteams = []
173        self.sessionplayers = []
174        self.min_players = min_players
175        self.max_players = (
176            max_players
177            if _max_players_override is None
178            else _max_players_override
179        )
180        self.submit_score = submit_score
181
182        self.customdata = {}
183        self._in_set_activity = False
184        self._next_team_id = 0
185        self._activity_retained: bascenev1.Activity | None = None
186        self._launch_end_session_activity_time: float | None = None
187        self._activity_end_timer: bascenev1.BaseTimer | None = None
188        self._activity_weak = empty_weakref(Activity)
189        self._next_activity: bascenev1.Activity | None = None
190        self._wants_to_end = False
191        self._ending = False
192        self._activity_should_end_immediately = False
193        self._activity_should_end_immediately_results: (
194            bascenev1.GameResults | None
195        ) = None
196        self._activity_should_end_immediately_delay = 0.0
197
198        # Create static teams if we're using them.
199        if self.use_teams:
200            if team_names is None:
201                raise RuntimeError(
202                    'use_teams is True but team_names not provided.'
203                )
204            if team_colors is None:
205                raise RuntimeError(
206                    'use_teams is True but team_colors not provided.'
207                )
208            if len(team_colors) != len(team_names):
209                raise RuntimeError(
210                    f'Got {len(team_names)} team_names'
211                    f' and {len(team_colors)} team_colors;'
212                    f' these numbers must match.'
213                )
214            for i, color in enumerate(team_colors):
215                team = SessionTeam(
216                    team_id=self._next_team_id,
217                    name=GameActivity.get_team_display_string(team_names[i]),
218                    color=color,
219                )
220                self.sessionteams.append(team)
221                self._next_team_id += 1
222                try:
223                    with self.context:
224                        self.on_team_join(team)
225                except Exception:
226                    logging.exception('Error in on_team_join for %s.', self)
227
228        self.lobby = Lobby()
229        self.stats = Stats()
230
231        # Instantiate our session globals node which will apply its settings.
232        self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
233
234        # Rejoin cooldown stuff.
235        self._players_on_wait: dict = {}
236        self._player_requested_identifiers: dict = {}
237        self._waitlist_timers: dict = {}
238
239    @property
240    def context(self) -> bascenev1.ContextRef:
241        """A context-ref pointing at this activity."""
242        return self._sessiondata.context()
243
244    @property
245    def sessionglobalsnode(self) -> bascenev1.Node:
246        """The sessionglobals bascenev1.Node for the session."""
247        node = self._sessionglobalsnode
248        if not node:
249            raise babase.NodeNotFoundError()
250        return node
251
252    def should_allow_mid_activity_joins(
253        self, activity: bascenev1.Activity
254    ) -> bool:
255        """Ask ourself if we should allow joins during an Activity.
256
257        Note that for a join to be allowed, both the Session and Activity
258        have to be ok with it (via this function and the
259        Activity.allow_mid_activity_joins property.
260        """
261        del activity  # Unused.
262        return True
263
264    def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
265        """Called when a new bascenev1.Player wants to join the Session.
266
267        This should return True or False to accept/reject.
268        """
269        # Limit player counts *unless* we're in a stress test.
270        if (
271            babase.app.classic is not None
272            and babase.app.classic.stress_test_update_timer is None
273        ):
274            if len(self.sessionplayers) >= self.max_players >= 0:
275                # Print a rejection message *only* to the client trying to
276                # join (prevents spamming everyone else in the game).
277                _bascenev1.getsound('error').play()
278                _bascenev1.broadcastmessage(
279                    babase.Lstr(
280                        resource='playerLimitReachedText',
281                        subs=[('${COUNT}', str(self.max_players))],
282                    ),
283                    color=(0.8, 0.0, 0.0),
284                    clients=[player.inputdevice.client_id],
285                    transient=True,
286                )
287                return False
288
289        # Rejoin cooldown.
290        identifier = player.get_v1_account_id()
291        if identifier:
292            leave_time = self._players_on_wait.get(identifier)
293            if leave_time:
294                diff = str(
295                    math.ceil(
296                        _g_player_rejoin_cooldown
297                        - babase.apptime()
298                        + leave_time
299                    )
300                )
301                _bascenev1.broadcastmessage(
302                    babase.Lstr(
303                        translate=(
304                            'serverResponses',
305                            'You can join in ${COUNT} seconds.',
306                        ),
307                        subs=[('${COUNT}', diff)],
308                    ),
309                    color=(1, 1, 0),
310                    clients=[player.inputdevice.client_id],
311                    transient=True,
312                )
313                return False
314            self._player_requested_identifiers[player.id] = identifier
315
316        _bascenev1.getsound('dripity').play()
317        return True
318
319    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
320        """Called when a previously-accepted bascenev1.SessionPlayer leaves."""
321
322        if sessionplayer not in self.sessionplayers:
323            print(
324                'ERROR: Session.on_player_leave called'
325                ' for player not in our list.'
326            )
327            return
328
329        _bascenev1.getsound('playerLeft').play()
330
331        activity = self._activity_weak()
332
333        # Rejoin cooldown.
334        identifier = self._player_requested_identifiers.get(sessionplayer.id)
335        if identifier:
336            self._players_on_wait[identifier] = babase.apptime()
337            with babase.ContextRef.empty():
338                self._waitlist_timers[identifier] = babase.AppTimer(
339                    _g_player_rejoin_cooldown,
340                    babase.Call(self._remove_player_from_waitlist, identifier),
341                )
342
343        if not sessionplayer.in_game:
344            # Ok, the player is still in the lobby; simply remove them.
345            with self.context:
346                try:
347                    self.lobby.remove_chooser(sessionplayer)
348                except Exception:
349                    logging.exception('Error in Lobby.remove_chooser().')
350        else:
351            # Ok, they've already entered the game. Remove them from
352            # teams/activities/etc.
353            sessionteam = sessionplayer.sessionteam
354            assert sessionteam is not None
355
356            _bascenev1.broadcastmessage(
357                babase.Lstr(
358                    resource='playerLeftText',
359                    subs=[('${PLAYER}', sessionplayer.getname(full=True))],
360                )
361            )
362
363            # Remove them from their SessionTeam.
364            if sessionplayer in sessionteam.players:
365                sessionteam.players.remove(sessionplayer)
366            else:
367                print(
368                    'SessionPlayer not found in SessionTeam'
369                    ' in on_player_leave.'
370                )
371
372            # Grab their activity-specific player instance.
373            player = sessionplayer.activityplayer
374            assert isinstance(player, (Player, type(None)))
375
376            # Remove them from any current Activity.
377            if player is not None and activity is not None:
378                if player in activity.players:
379                    activity.remove_player(sessionplayer)
380                else:
381                    print('Player not found in Activity in on_player_leave.')
382
383            # If we're a non-team session, remove their team too.
384            if not self.use_teams:
385                self._remove_player_team(sessionteam, activity)
386
387        # Now remove them from the session list.
388        self.sessionplayers.remove(sessionplayer)
389
390    def _remove_player_team(
391        self,
392        sessionteam: bascenev1.SessionTeam,
393        activity: bascenev1.Activity | None,
394    ) -> None:
395        """Remove the player-specific team in non-teams mode."""
396
397        # They should have been the only one on their team.
398        assert not sessionteam.players
399
400        # Remove their Team from the Activity.
401        if activity is not None:
402            if sessionteam.activityteam in activity.teams:
403                activity.remove_team(sessionteam)
404            else:
405                print('Team not found in Activity in on_player_leave.')
406
407        # And then from the Session.
408        with self.context:
409            if sessionteam in self.sessionteams:
410                try:
411                    self.sessionteams.remove(sessionteam)
412                    self.on_team_leave(sessionteam)
413                except Exception:
414                    logging.exception(
415                        'Error in on_team_leave for Session %s.', self
416                    )
417            else:
418                print('Team no in Session teams in on_player_leave.')
419            try:
420                sessionteam.leave()
421            except Exception:
422                logging.exception(
423                    'Error clearing sessiondata for team %s in session %s.',
424                    sessionteam,
425                    self,
426                )
427
428    def end(self) -> None:
429        """Initiates an end to the session and a return to the main menu.
430
431        Note that this happens asynchronously, allowing the
432        session and its activities to shut down gracefully.
433        """
434        self._wants_to_end = True
435        if self._next_activity is None:
436            self._launch_end_session_activity()
437
438    def _launch_end_session_activity(self) -> None:
439        """(internal)"""
440        from bascenev1._activitytypes import EndSessionActivity
441
442        with self.context:
443            curtime = babase.apptime()
444            if self._ending:
445                # Ignore repeats unless its been a while.
446                assert self._launch_end_session_activity_time is not None
447                since_last = curtime - self._launch_end_session_activity_time
448                if since_last < 30.0:
449                    return
450                logging.error(
451                    '_launch_end_session_activity called twice (since_last=%s)',
452                    since_last,
453                )
454            self._launch_end_session_activity_time = curtime
455            self.setactivity(_bascenev1.newactivity(EndSessionActivity))
456            self._wants_to_end = False
457            self._ending = True  # Prevent further actions.
458
459    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
460        """Called when a new bascenev1.Team joins the session."""
461
462    def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
463        """Called when a bascenev1.Team is leaving the session."""
464
465    def end_activity(
466        self,
467        activity: bascenev1.Activity,
468        results: Any,
469        delay: float,
470        force: bool,
471    ) -> None:
472        """Commence shutdown of a bascenev1.Activity (if not already occurring).
473
474        'delay' is the time delay before the Activity actually ends
475        (in seconds). Further calls to end() will be ignored up until
476        this time, unless 'force' is True, in which case the new results
477        will replace the old.
478        """
479        # Only pay attention if this is coming from our current activity.
480        if activity is not self._activity_retained:
481            return
482
483        # If this activity hasn't begun yet, just set it up to end immediately
484        # once it does.
485        if not activity.has_begun():
486            # activity.set_immediate_end(results, delay, force)
487            if not self._activity_should_end_immediately or force:
488                self._activity_should_end_immediately = True
489                self._activity_should_end_immediately_results = results
490                self._activity_should_end_immediately_delay = delay
491
492        # The activity has already begun; get ready to end it.
493        else:
494            if (not activity.has_ended()) or force:
495                activity.set_has_ended(True)
496
497                # Set a timer to set in motion this activity's demise.
498                self._activity_end_timer = _bascenev1.BaseTimer(
499                    delay,
500                    babase.Call(self._complete_end_activity, activity, results),
501                )
502
503    def handlemessage(self, msg: Any) -> Any:
504        """General message handling; can be passed any message object."""
505        from bascenev1._lobby import PlayerReadyMessage
506        from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
507
508        if isinstance(msg, PlayerReadyMessage):
509            self._on_player_ready(msg.chooser)
510
511        elif isinstance(msg, PlayerProfilesChangedMessage):
512            # If we have a current activity with a lobby, ask it to reload
513            # profiles.
514            with self.context:
515                self.lobby.reload_profiles()
516            return None
517
518        else:
519            return UNHANDLED
520        return None
521
522    class _SetActivityScopedLock:
523        def __init__(self, session: Session) -> None:
524            self._session = session
525            if session._in_set_activity:
526                raise RuntimeError('Session.setactivity() called recursively.')
527            self._session._in_set_activity = True
528
529        def __del__(self) -> None:
530            self._session._in_set_activity = False
531
532    def setactivity(self, activity: bascenev1.Activity) -> None:
533        """Assign a new current bascenev1.Activity for the session.
534
535        Note that this will not change the current context to the new
536        Activity's. Code must be run in the new activity's methods
537        (on_transition_in, etc) to get it. (so you can't do
538        session.setactivity(foo) and then bascenev1.newnode() to add a node
539        to foo)
540        """
541
542        # Make sure we don't get called recursively.
543        _rlock = self._SetActivityScopedLock(self)
544
545        if activity.session is not _bascenev1.getsession():
546            raise RuntimeError("Provided Activity's Session is not current.")
547
548        # Quietly ignore this if the whole session is going down.
549        if self._ending:
550            return
551
552        if activity is self._activity_retained:
553            logging.error('Activity set to already-current activity.')
554            return
555
556        if self._next_activity is not None:
557            raise RuntimeError(
558                'Activity switch already in progress (to '
559                + str(self._next_activity)
560                + ')'
561            )
562
563        prev_activity = self._activity_retained
564        prev_globals = (
565            prev_activity.globalsnode if prev_activity is not None else None
566        )
567
568        # Let the activity do its thing.
569        activity.transition_in(prev_globals)
570
571        self._next_activity = activity
572
573        # If we have a current activity, tell it it's transitioning out;
574        # the next one will become current once this one dies.
575        if prev_activity is not None:
576            prev_activity.transition_out()
577
578            # Setting this to None should free up the old activity to die,
579            # which will call begin_next_activity.
580            # We can still access our old activity through
581            # self._activity_weak() to keep it up to date on player
582            # joins/departures/etc until it dies.
583            self._activity_retained = None
584
585        # There's no existing activity; lets just go ahead with the begin call.
586        else:
587            self.begin_next_activity()
588
589        # We want to call destroy() for the previous activity once it should
590        # tear itself down, clear out any self-refs, etc. After this call
591        # the activity should have no refs left to it and should die (which
592        # will trigger the next activity to run).
593        if prev_activity is not None:
594            with babase.ContextRef.empty():
595                babase.apptimer(
596                    max(0.0, activity.transition_time), prev_activity.expire
597                )
598        self._in_set_activity = False
599
600    def getactivity(self) -> bascenev1.Activity | None:
601        """Return the current foreground activity for this session."""
602        return self._activity_weak()
603
604    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
605        """Subclasses can override this to provide custom menu entries.
606
607        The returned value should be a list of dicts, each containing
608        a 'label' and 'call' entry, with 'label' being the text for
609        the entry and 'call' being the callable to trigger if the entry
610        is pressed.
611        """
612        return []
613
614    def _complete_end_activity(
615        self, activity: bascenev1.Activity, results: Any
616    ) -> None:
617        # Run the subclass callback in the session context.
618        try:
619            with self.context:
620                self.on_activity_end(activity, results)
621        except Exception:
622            logging.error(
623                'Error in on_activity_end() for session %s'
624                ' activity %s with results %s',
625                self,
626                activity,
627                results,
628            )
629
630    def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool:
631        """Called by the native layer when a player wants to join."""
632
633        # If we're ending, allow no new players.
634        if self._ending:
635            return False
636
637        # Ask the bascenev1.Session subclass to approve/deny this request.
638        try:
639            with self.context:
640                result = self.on_player_request(sessionplayer)
641        except Exception:
642            logging.exception('Error in on_player_request for %s.', self)
643            result = False
644
645        # If they said yes, add the player to the lobby.
646        if result:
647            self.sessionplayers.append(sessionplayer)
648            with self.context:
649                try:
650                    self.lobby.add_chooser(sessionplayer)
651                except Exception:
652                    logging.exception('Error in lobby.add_chooser().')
653
654        return result
655
656    def on_activity_end(
657        self, activity: bascenev1.Activity, results: Any
658    ) -> None:
659        """Called when the current bascenev1.Activity has ended.
660
661        The bascenev1.Session should look at the results and start
662        another bascenev1.Activity.
663        """
664
665    def begin_next_activity(self) -> None:
666        """Called once the previous activity has been totally torn down.
667
668        This means we're ready to begin the next one
669        """
670        if self._next_activity is None:
671            # Should this ever happen?
672            logging.error('begin_next_activity() called with no _next_activity')
673            return
674
675        # We store both a weak and a strong ref to the new activity;
676        # the strong is to keep it alive and the weak is so we can access
677        # it even after we've released the strong-ref to allow it to die.
678        self._activity_retained = self._next_activity
679        self._activity_weak = weakref.ref(self._next_activity)
680        self._next_activity = None
681        self._activity_should_end_immediately = False
682
683        # Kick out anyone loitering in the lobby.
684        self.lobby.remove_all_choosers_and_kick_players()
685
686        # Kick off the activity.
687        self._activity_retained.begin(self)
688
689        # If we want to completely end the session, we can now kick that off.
690        if self._wants_to_end:
691            self._launch_end_session_activity()
692        else:
693            # Otherwise, if the activity has already been told to end,
694            # do so now.
695            if self._activity_should_end_immediately:
696                self._activity_retained.end(
697                    self._activity_should_end_immediately_results,
698                    self._activity_should_end_immediately_delay,
699                )
700
701    def _on_player_ready(self, chooser: bascenev1.Chooser) -> None:
702        """Called when a bascenev1.Player has checked themself ready."""
703        lobby = chooser.lobby
704        activity = self._activity_weak()
705
706        # This happens sometimes. That seems like it shouldn't be happening;
707        # when would we have a session and a chooser with players but no
708        # active activity?
709        if activity is None:
710            print('_on_player_ready called with no activity.')
711            return
712
713        # In joining-activities, we wait till all choosers are ready
714        # and then create all players at once.
715        if activity.is_joining_activity:
716            if not lobby.check_all_ready():
717                return
718            choosers = lobby.get_choosers()
719            min_players = self.min_players
720            if len(choosers) >= min_players:
721                for lch in lobby.get_choosers():
722                    self._add_chosen_player(lch)
723                lobby.remove_all_choosers()
724
725                # Get our next activity going.
726                self._complete_end_activity(activity, {})
727            else:
728                _bascenev1.broadcastmessage(
729                    babase.Lstr(
730                        resource='notEnoughPlayersText',
731                        subs=[('${COUNT}', str(min_players))],
732                    ),
733                    color=(1, 1, 0),
734                )
735                _bascenev1.getsound('error').play()
736
737        # Otherwise just add players on the fly.
738        else:
739            self._add_chosen_player(chooser)
740            lobby.remove_chooser(chooser.getplayer())
741
742    def transitioning_out_activity_was_freed(
743        self, can_show_ad_on_death: bool
744    ) -> None:
745        """(internal)"""
746        # pylint: disable=cyclic-import
747
748        # Since things should be generally still right now, it's a good time
749        # to run garbage collection to clear out any circular dependency
750        # loops. We keep this disabled normally to avoid non-deterministic
751        # hitches.
752        babase.garbage_collect()
753
754        assert babase.app.classic is not None
755        with self.context:
756            if can_show_ad_on_death:
757                babase.app.classic.ads.call_after_ad(self.begin_next_activity)
758            else:
759                babase.pushcall(self.begin_next_activity)
760
761    def _add_chosen_player(
762        self, chooser: bascenev1.Chooser
763    ) -> bascenev1.SessionPlayer:
764        from bascenev1._team import SessionTeam
765
766        sessionplayer = chooser.getplayer()
767        assert sessionplayer in self.sessionplayers, (
768            'SessionPlayer not found in session '
769            'player-list after chooser selection.'
770        )
771
772        activity = self._activity_weak()
773        assert activity is not None
774
775        # Reset the player's input here, as it is probably
776        # referencing the chooser which could inadvertently keep it alive.
777        sessionplayer.resetinput()
778
779        # We can pass it to the current activity if it has already begun
780        # (otherwise it'll get passed once begin is called).
781        pass_to_activity = (
782            activity.has_begun() and not activity.is_joining_activity
783        )
784
785        # However, if we're not allowing mid-game joins, don't actually pass;
786        # just announce the arrival and say they'll partake next round.
787        if pass_to_activity:
788            if not (
789                activity.allow_mid_activity_joins
790                and self.should_allow_mid_activity_joins(activity)
791            ):
792                pass_to_activity = False
793                with self.context:
794                    _bascenev1.broadcastmessage(
795                        babase.Lstr(
796                            resource='playerDelayedJoinText',
797                            subs=[
798                                ('${PLAYER}', sessionplayer.getname(full=True))
799                            ],
800                        ),
801                        color=(0, 1, 0),
802                    )
803
804        # If we're a non-team session, each player gets their own team.
805        # (keeps mini-game coding simpler if we can always deal with teams).
806        if self.use_teams:
807            sessionteam = chooser.sessionteam
808        else:
809            our_team_id = self._next_team_id
810            self._next_team_id += 1
811            sessionteam = SessionTeam(
812                team_id=our_team_id,
813                color=chooser.get_color(),
814                name=chooser.getplayer().getname(full=True, icon=False),
815            )
816
817            # Add player's team to the Session.
818            self.sessionteams.append(sessionteam)
819
820            with self.context:
821                try:
822                    self.on_team_join(sessionteam)
823                except Exception:
824                    logging.exception('Error in on_team_join for %s.', self)
825
826            # Add player's team to the Activity.
827            if pass_to_activity:
828                activity.add_team(sessionteam)
829
830        assert sessionplayer not in sessionteam.players
831        sessionteam.players.append(sessionplayer)
832        sessionplayer.setdata(
833            team=sessionteam,
834            character=chooser.get_character_name(),
835            color=chooser.get_color(),
836            highlight=chooser.get_highlight(),
837        )
838
839        self.stats.register_sessionplayer(sessionplayer)
840        if pass_to_activity:
841            activity.add_player(sessionplayer)
842        return sessionplayer
843
844    def _remove_player_from_waitlist(self, identifier: str) -> None:
845        try:
846            self._players_on_wait.pop(identifier)
847        except KeyError:
848            pass

Defines a high level series of bascenev1.Activity-es.

Category: Gameplay Classes

Examples of sessions are bascenev1.FreeForAllSession, bascenev1.DualTeamSession, and bascenev1.CoopSession.

A Session is responsible for wrangling and transitioning between various bascenev1.Activity instances such as mini-games and score-screens, and for maintaining state between them (players, teams, score tallies, etc).

Session( depsets: Sequence[DependencySet], team_names: Optional[Sequence[str]] = None, team_colors: Optional[Sequence[Sequence[float]]] = None, min_players: int = 1, max_players: int = 8, submit_score: bool = True)
 97    def __init__(
 98        self,
 99        depsets: Sequence[bascenev1.DependencySet],
100        team_names: Sequence[str] | None = None,
101        team_colors: Sequence[Sequence[float]] | None = None,
102        min_players: int = 1,
103        max_players: int = 8,
104        submit_score: bool = True,
105    ):
106        """Instantiate a session.
107
108        depsets should be a sequence of successfully resolved
109        bascenev1.DependencySet instances; one for each bascenev1.Activity
110        the session may potentially run.
111        """
112        # pylint: disable=too-many-statements
113        # pylint: disable=too-many-locals
114        # pylint: disable=cyclic-import
115        # pylint: disable=too-many-branches
116        from efro.util import empty_weakref
117        from bascenev1._dependency import (
118            Dependency,
119            AssetPackage,
120            DependencyError,
121        )
122        from bascenev1._lobby import Lobby
123        from bascenev1._stats import Stats
124        from bascenev1._gameactivity import GameActivity
125        from bascenev1._activity import Activity
126        from bascenev1._team import SessionTeam
127
128        # First off, resolve all dependency-sets we were passed.
129        # If things are missing, we'll try to gather them into a single
130        # missing-deps exception if possible to give the caller a clean
131        # path to download missing stuff and try again.
132        missing_asset_packages: set[str] = set()
133        for depset in depsets:
134            try:
135                depset.resolve()
136            except DependencyError as exc:
137                # Gather/report missing assets only; barf on anything else.
138                if all(issubclass(d.cls, AssetPackage) for d in exc.deps):
139                    for dep in exc.deps:
140                        assert isinstance(dep.config, str)
141                        missing_asset_packages.add(dep.config)
142                else:
143                    missing_info = [(d.cls, d.config) for d in exc.deps]
144                    raise RuntimeError(
145                        f'Missing non-asset dependencies: {missing_info}'
146                    ) from exc
147
148        # Throw a combined exception if we found anything missing.
149        if missing_asset_packages:
150            raise DependencyError(
151                [
152                    Dependency(AssetPackage, set_id)
153                    for set_id in missing_asset_packages
154                ]
155            )
156
157        # Ok; looks like our dependencies check out.
158        # Now give the engine a list of asset-set-ids to pass along to clients.
159        required_asset_packages: set[str] = set()
160        for depset in depsets:
161            required_asset_packages.update(depset.get_asset_package_ids())
162
163        # print('Would set host-session asset-reqs to:',
164        # required_asset_packages)
165
166        # Init our C++ layer data.
167        self._sessiondata = _bascenev1.register_session(self)
168
169        # Should remove this if possible.
170        self.tournament_id: str | None = None
171
172        self.sessionteams = []
173        self.sessionplayers = []
174        self.min_players = min_players
175        self.max_players = (
176            max_players
177            if _max_players_override is None
178            else _max_players_override
179        )
180        self.submit_score = submit_score
181
182        self.customdata = {}
183        self._in_set_activity = False
184        self._next_team_id = 0
185        self._activity_retained: bascenev1.Activity | None = None
186        self._launch_end_session_activity_time: float | None = None
187        self._activity_end_timer: bascenev1.BaseTimer | None = None
188        self._activity_weak = empty_weakref(Activity)
189        self._next_activity: bascenev1.Activity | None = None
190        self._wants_to_end = False
191        self._ending = False
192        self._activity_should_end_immediately = False
193        self._activity_should_end_immediately_results: (
194            bascenev1.GameResults | None
195        ) = None
196        self._activity_should_end_immediately_delay = 0.0
197
198        # Create static teams if we're using them.
199        if self.use_teams:
200            if team_names is None:
201                raise RuntimeError(
202                    'use_teams is True but team_names not provided.'
203                )
204            if team_colors is None:
205                raise RuntimeError(
206                    'use_teams is True but team_colors not provided.'
207                )
208            if len(team_colors) != len(team_names):
209                raise RuntimeError(
210                    f'Got {len(team_names)} team_names'
211                    f' and {len(team_colors)} team_colors;'
212                    f' these numbers must match.'
213                )
214            for i, color in enumerate(team_colors):
215                team = SessionTeam(
216                    team_id=self._next_team_id,
217                    name=GameActivity.get_team_display_string(team_names[i]),
218                    color=color,
219                )
220                self.sessionteams.append(team)
221                self._next_team_id += 1
222                try:
223                    with self.context:
224                        self.on_team_join(team)
225                except Exception:
226                    logging.exception('Error in on_team_join for %s.', self)
227
228        self.lobby = Lobby()
229        self.stats = Stats()
230
231        # Instantiate our session globals node which will apply its settings.
232        self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
233
234        # Rejoin cooldown stuff.
235        self._players_on_wait: dict = {}
236        self._player_requested_identifiers: dict = {}
237        self._waitlist_timers: dict = {}

Instantiate a session.

depsets should be a sequence of successfully resolved bascenev1.DependencySet instances; one for each bascenev1.Activity the session may potentially run.

use_teams: bool = False

Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.

use_team_colors: bool = True

Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.

lobby: Lobby

The baclassic.Lobby instance where new bascenev1.Player-s go to select a Profile/Team/etc. before being added to games. Be aware this value may be None if a Session does not allow any such selection.

max_players: int

The maximum number of players allowed in the Session.

min_players: int

The minimum number of players who must be present for the Session to proceed past the initial joining screen

sessionplayers: list[SessionPlayer]

All bascenev1.SessionPlayers in the Session. Most things should use the list of bascenev1.Player-s in bascenev1.Activity; not this. Some players, such as those who have not yet selected a character, will only be found on this list.

customdata: dict

A shared dictionary for objects to use as storage on this session. Ensure that keys here are unique to avoid collisions.

sessionteams: list[SessionTeam]

All the bascenev1.SessionTeams in the Session. Most things should use the list of bascenev1.Team-s in bascenev1.Activity; not this.

tournament_id: str | None
submit_score
stats
context: ContextRef
239    @property
240    def context(self) -> bascenev1.ContextRef:
241        """A context-ref pointing at this activity."""
242        return self._sessiondata.context()

A context-ref pointing at this activity.

sessionglobalsnode: Node
244    @property
245    def sessionglobalsnode(self) -> bascenev1.Node:
246        """The sessionglobals bascenev1.Node for the session."""
247        node = self._sessionglobalsnode
248        if not node:
249            raise babase.NodeNotFoundError()
250        return node

The sessionglobals bascenev1.Node for the session.

def should_allow_mid_activity_joins(self, activity: Activity) -> bool:
252    def should_allow_mid_activity_joins(
253        self, activity: bascenev1.Activity
254    ) -> bool:
255        """Ask ourself if we should allow joins during an Activity.
256
257        Note that for a join to be allowed, both the Session and Activity
258        have to be ok with it (via this function and the
259        Activity.allow_mid_activity_joins property.
260        """
261        del activity  # Unused.
262        return True

Ask ourself if we should allow joins during an Activity.

Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.

def on_player_request(self, player: SessionPlayer) -> bool:
264    def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
265        """Called when a new bascenev1.Player wants to join the Session.
266
267        This should return True or False to accept/reject.
268        """
269        # Limit player counts *unless* we're in a stress test.
270        if (
271            babase.app.classic is not None
272            and babase.app.classic.stress_test_update_timer is None
273        ):
274            if len(self.sessionplayers) >= self.max_players >= 0:
275                # Print a rejection message *only* to the client trying to
276                # join (prevents spamming everyone else in the game).
277                _bascenev1.getsound('error').play()
278                _bascenev1.broadcastmessage(
279                    babase.Lstr(
280                        resource='playerLimitReachedText',
281                        subs=[('${COUNT}', str(self.max_players))],
282                    ),
283                    color=(0.8, 0.0, 0.0),
284                    clients=[player.inputdevice.client_id],
285                    transient=True,
286                )
287                return False
288
289        # Rejoin cooldown.
290        identifier = player.get_v1_account_id()
291        if identifier:
292            leave_time = self._players_on_wait.get(identifier)
293            if leave_time:
294                diff = str(
295                    math.ceil(
296                        _g_player_rejoin_cooldown
297                        - babase.apptime()
298                        + leave_time
299                    )
300                )
301                _bascenev1.broadcastmessage(
302                    babase.Lstr(
303                        translate=(
304                            'serverResponses',
305                            'You can join in ${COUNT} seconds.',
306                        ),
307                        subs=[('${COUNT}', diff)],
308                    ),
309                    color=(1, 1, 0),
310                    clients=[player.inputdevice.client_id],
311                    transient=True,
312                )
313                return False
314            self._player_requested_identifiers[player.id] = identifier
315
316        _bascenev1.getsound('dripity').play()
317        return True

Called when a new bascenev1.Player wants to join the Session.

This should return True or False to accept/reject.

def on_player_leave(self, sessionplayer: SessionPlayer) -> None:
319    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
320        """Called when a previously-accepted bascenev1.SessionPlayer leaves."""
321
322        if sessionplayer not in self.sessionplayers:
323            print(
324                'ERROR: Session.on_player_leave called'
325                ' for player not in our list.'
326            )
327            return
328
329        _bascenev1.getsound('playerLeft').play()
330
331        activity = self._activity_weak()
332
333        # Rejoin cooldown.
334        identifier = self._player_requested_identifiers.get(sessionplayer.id)
335        if identifier:
336            self._players_on_wait[identifier] = babase.apptime()
337            with babase.ContextRef.empty():
338                self._waitlist_timers[identifier] = babase.AppTimer(
339                    _g_player_rejoin_cooldown,
340                    babase.Call(self._remove_player_from_waitlist, identifier),
341                )
342
343        if not sessionplayer.in_game:
344            # Ok, the player is still in the lobby; simply remove them.
345            with self.context:
346                try:
347                    self.lobby.remove_chooser(sessionplayer)
348                except Exception:
349                    logging.exception('Error in Lobby.remove_chooser().')
350        else:
351            # Ok, they've already entered the game. Remove them from
352            # teams/activities/etc.
353            sessionteam = sessionplayer.sessionteam
354            assert sessionteam is not None
355
356            _bascenev1.broadcastmessage(
357                babase.Lstr(
358                    resource='playerLeftText',
359                    subs=[('${PLAYER}', sessionplayer.getname(full=True))],
360                )
361            )
362
363            # Remove them from their SessionTeam.
364            if sessionplayer in sessionteam.players:
365                sessionteam.players.remove(sessionplayer)
366            else:
367                print(
368                    'SessionPlayer not found in SessionTeam'
369                    ' in on_player_leave.'
370                )
371
372            # Grab their activity-specific player instance.
373            player = sessionplayer.activityplayer
374            assert isinstance(player, (Player, type(None)))
375
376            # Remove them from any current Activity.
377            if player is not None and activity is not None:
378                if player in activity.players:
379                    activity.remove_player(sessionplayer)
380                else:
381                    print('Player not found in Activity in on_player_leave.')
382
383            # If we're a non-team session, remove their team too.
384            if not self.use_teams:
385                self._remove_player_team(sessionteam, activity)
386
387        # Now remove them from the session list.
388        self.sessionplayers.remove(sessionplayer)

Called when a previously-accepted bascenev1.SessionPlayer leaves.

def end(self) -> None:
428    def end(self) -> None:
429        """Initiates an end to the session and a return to the main menu.
430
431        Note that this happens asynchronously, allowing the
432        session and its activities to shut down gracefully.
433        """
434        self._wants_to_end = True
435        if self._next_activity is None:
436            self._launch_end_session_activity()

Initiates an end to the session and a return to the main menu.

Note that this happens asynchronously, allowing the session and its activities to shut down gracefully.

def on_team_join(self, team: SessionTeam) -> None:
459    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
460        """Called when a new bascenev1.Team joins the session."""

Called when a new bascenev1.Team joins the session.

def on_team_leave(self, team: SessionTeam) -> None:
462    def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
463        """Called when a bascenev1.Team is leaving the session."""

Called when a bascenev1.Team is leaving the session.

def end_activity( self, activity: Activity, results: Any, delay: float, force: bool) -> None:
465    def end_activity(
466        self,
467        activity: bascenev1.Activity,
468        results: Any,
469        delay: float,
470        force: bool,
471    ) -> None:
472        """Commence shutdown of a bascenev1.Activity (if not already occurring).
473
474        'delay' is the time delay before the Activity actually ends
475        (in seconds). Further calls to end() will be ignored up until
476        this time, unless 'force' is True, in which case the new results
477        will replace the old.
478        """
479        # Only pay attention if this is coming from our current activity.
480        if activity is not self._activity_retained:
481            return
482
483        # If this activity hasn't begun yet, just set it up to end immediately
484        # once it does.
485        if not activity.has_begun():
486            # activity.set_immediate_end(results, delay, force)
487            if not self._activity_should_end_immediately or force:
488                self._activity_should_end_immediately = True
489                self._activity_should_end_immediately_results = results
490                self._activity_should_end_immediately_delay = delay
491
492        # The activity has already begun; get ready to end it.
493        else:
494            if (not activity.has_ended()) or force:
495                activity.set_has_ended(True)
496
497                # Set a timer to set in motion this activity's demise.
498                self._activity_end_timer = _bascenev1.BaseTimer(
499                    delay,
500                    babase.Call(self._complete_end_activity, activity, results),
501                )

Commence shutdown of a bascenev1.Activity (if not already occurring).

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

def handlemessage(self, msg: Any) -> Any:
503    def handlemessage(self, msg: Any) -> Any:
504        """General message handling; can be passed any message object."""
505        from bascenev1._lobby import PlayerReadyMessage
506        from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
507
508        if isinstance(msg, PlayerReadyMessage):
509            self._on_player_ready(msg.chooser)
510
511        elif isinstance(msg, PlayerProfilesChangedMessage):
512            # If we have a current activity with a lobby, ask it to reload
513            # profiles.
514            with self.context:
515                self.lobby.reload_profiles()
516            return None
517
518        else:
519            return UNHANDLED
520        return None

General message handling; can be passed any message object.

def setactivity(self, activity: Activity) -> None:
532    def setactivity(self, activity: bascenev1.Activity) -> None:
533        """Assign a new current bascenev1.Activity for the session.
534
535        Note that this will not change the current context to the new
536        Activity's. Code must be run in the new activity's methods
537        (on_transition_in, etc) to get it. (so you can't do
538        session.setactivity(foo) and then bascenev1.newnode() to add a node
539        to foo)
540        """
541
542        # Make sure we don't get called recursively.
543        _rlock = self._SetActivityScopedLock(self)
544
545        if activity.session is not _bascenev1.getsession():
546            raise RuntimeError("Provided Activity's Session is not current.")
547
548        # Quietly ignore this if the whole session is going down.
549        if self._ending:
550            return
551
552        if activity is self._activity_retained:
553            logging.error('Activity set to already-current activity.')
554            return
555
556        if self._next_activity is not None:
557            raise RuntimeError(
558                'Activity switch already in progress (to '
559                + str(self._next_activity)
560                + ')'
561            )
562
563        prev_activity = self._activity_retained
564        prev_globals = (
565            prev_activity.globalsnode if prev_activity is not None else None
566        )
567
568        # Let the activity do its thing.
569        activity.transition_in(prev_globals)
570
571        self._next_activity = activity
572
573        # If we have a current activity, tell it it's transitioning out;
574        # the next one will become current once this one dies.
575        if prev_activity is not None:
576            prev_activity.transition_out()
577
578            # Setting this to None should free up the old activity to die,
579            # which will call begin_next_activity.
580            # We can still access our old activity through
581            # self._activity_weak() to keep it up to date on player
582            # joins/departures/etc until it dies.
583            self._activity_retained = None
584
585        # There's no existing activity; lets just go ahead with the begin call.
586        else:
587            self.begin_next_activity()
588
589        # We want to call destroy() for the previous activity once it should
590        # tear itself down, clear out any self-refs, etc. After this call
591        # the activity should have no refs left to it and should die (which
592        # will trigger the next activity to run).
593        if prev_activity is not None:
594            with babase.ContextRef.empty():
595                babase.apptimer(
596                    max(0.0, activity.transition_time), prev_activity.expire
597                )
598        self._in_set_activity = False

Assign a new current bascenev1.Activity for the session.

Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do session.setactivity(foo) and then bascenev1.newnode() to add a node to foo)

def getactivity(self) -> Activity | None:
600    def getactivity(self) -> bascenev1.Activity | None:
601        """Return the current foreground activity for this session."""
602        return self._activity_weak()

Return the current foreground activity for this session.

def get_custom_menu_entries(self) -> list[dict[str, typing.Any]]:
604    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
605        """Subclasses can override this to provide custom menu entries.
606
607        The returned value should be a list of dicts, each containing
608        a 'label' and 'call' entry, with 'label' being the text for
609        the entry and 'call' being the callable to trigger if the entry
610        is pressed.
611        """
612        return []

Subclasses can override this to provide custom menu entries.

The returned value should be a list of dicts, each containing a 'label' and 'call' entry, with 'label' being the text for the entry and 'call' being the callable to trigger if the entry is pressed.

def on_activity_end(self, activity: Activity, results: Any) -> None:
656    def on_activity_end(
657        self, activity: bascenev1.Activity, results: Any
658    ) -> None:
659        """Called when the current bascenev1.Activity has ended.
660
661        The bascenev1.Session should look at the results and start
662        another bascenev1.Activity.
663        """

Called when the current bascenev1.Activity has ended.

The bascenev1.Session should look at the results and start another bascenev1.Activity.

def begin_next_activity(self) -> None:
665    def begin_next_activity(self) -> None:
666        """Called once the previous activity has been totally torn down.
667
668        This means we're ready to begin the next one
669        """
670        if self._next_activity is None:
671            # Should this ever happen?
672            logging.error('begin_next_activity() called with no _next_activity')
673            return
674
675        # We store both a weak and a strong ref to the new activity;
676        # the strong is to keep it alive and the weak is so we can access
677        # it even after we've released the strong-ref to allow it to die.
678        self._activity_retained = self._next_activity
679        self._activity_weak = weakref.ref(self._next_activity)
680        self._next_activity = None
681        self._activity_should_end_immediately = False
682
683        # Kick out anyone loitering in the lobby.
684        self.lobby.remove_all_choosers_and_kick_players()
685
686        # Kick off the activity.
687        self._activity_retained.begin(self)
688
689        # If we want to completely end the session, we can now kick that off.
690        if self._wants_to_end:
691            self._launch_end_session_activity()
692        else:
693            # Otherwise, if the activity has already been told to end,
694            # do so now.
695            if self._activity_should_end_immediately:
696                self._activity_retained.end(
697                    self._activity_should_end_immediately_results,
698                    self._activity_should_end_immediately_delay,
699                )

Called once the previous activity has been totally torn down.

This means we're ready to begin the next one

class SessionPlayer:
712class SessionPlayer:
713    """A reference to a player in the bascenev1.Session.
714
715    Category: **Gameplay Classes**
716
717    These are created and managed internally and
718    provided to your bascenev1.Session/bascenev1.Activity instances.
719    Be aware that, like `ba.Node`s, bascenev1.SessionPlayer objects are
720    'weak' references under-the-hood; a player can leave the game at
721     any point. For this reason, you should make judicious use of the
722    babase.SessionPlayer.exists() method (or boolean operator) to ensure
723    that a SessionPlayer is still present if retaining references to one
724    for any length of time.
725    """
726
727    id: int
728    """The unique numeric ID of the Player.
729
730       Note that you can also use the boolean operator for this same
731       functionality, so a statement such as "if player" will do
732       the right thing both for Player objects and values of None."""
733
734    in_game: bool
735    """This bool value will be True once the Player has completed
736       any lobby character/team selection."""
737
738    sessionteam: bascenev1.SessionTeam
739    """The bascenev1.SessionTeam this Player is on. If the
740       SessionPlayer is still in its lobby selecting a team/etc.
741       then a bascenev1.SessionTeamNotFoundError will be raised."""
742
743    inputdevice: bascenev1.InputDevice
744    """The input device associated with the player."""
745
746    color: Sequence[float]
747    """The base color for this Player.
748       In team games this will match the bascenev1.SessionTeam's
749       color."""
750
751    highlight: Sequence[float]
752    """A secondary color for this player.
753       This is used for minor highlights and accents
754       to allow a player to stand apart from his teammates
755       who may all share the same team (primary) color."""
756
757    character: str
758    """The character this player has selected in their profile."""
759
760    activityplayer: bascenev1.Player | None
761    """The current game-specific instance for this player."""
762
763    def __bool__(self) -> bool:
764        """Support for bool evaluation."""
765        return bool(True)  # Slight obfuscation.
766
767    def assigninput(
768        self,
769        type: bascenev1.InputType | tuple[bascenev1.InputType, ...],
770        call: Callable,
771    ) -> None:
772        """Set the python callable to be run for one or more types of input."""
773        return None
774
775    def exists(self) -> bool:
776        """Return whether the underlying player is still in the game."""
777        return bool()
778
779    def get_icon(self) -> dict[str, Any]:
780        """Returns the character's icon (images, colors, etc contained
781        in a dict.
782        """
783        return {'foo': 'bar'}
784
785    def get_icon_info(self) -> dict[str, Any]:
786        """(internal)"""
787        return {'foo': 'bar'}
788
789    def get_v1_account_id(self) -> str:
790        """Return the V1 Account ID this player is signed in under, if
791        there is one and it can be determined with relative certainty.
792        Returns None otherwise. Note that this may require an active
793        internet connection (especially for network-connected players)
794        and may return None for a short while after a player initially
795        joins (while verification occurs).
796        """
797        return str()
798
799    def getname(self, full: bool = False, icon: bool = True) -> str:
800        """Returns the player's name. If icon is True, the long version of the
801        name may include an icon.
802        """
803        return str()
804
805    def remove_from_game(self) -> None:
806        """Removes the player from the game."""
807        return None
808
809    def resetinput(self) -> None:
810        """Clears out the player's assigned input actions."""
811        return None
812
813    def set_icon_info(
814        self,
815        texture: str,
816        tint_texture: str,
817        tint_color: Sequence[float],
818        tint2_color: Sequence[float],
819    ) -> None:
820        """(internal)"""
821        return None
822
823    def setactivity(self, activity: bascenev1.Activity | None) -> None:
824        """(internal)"""
825        return None
826
827    def setdata(
828        self,
829        team: bascenev1.SessionTeam,
830        character: str,
831        color: Sequence[float],
832        highlight: Sequence[float],
833    ) -> None:
834        """(internal)"""
835        return None
836
837    def setname(
838        self, name: str, full_name: str | None = None, real: bool = True
839    ) -> None:
840        """Set the player's name to the provided string.
841        A number will automatically be appended if the name is not unique from
842        other players.
843        """
844        return None
845
846    def setnode(self, node: bascenev1.Node | None) -> None:
847        """(internal)"""
848        return None

A reference to a player in the bascenev1.Session.

Category: Gameplay Classes

These are created and managed internally and provided to your bascenev1.Session/bascenev1bascenev1.Activity instances. Be aware that, like ba.Nodes, bascenev1.SessionPlayer objects are 'weak' references under-the-hood; a player can leave the game at any point. For this reason, you should make judicious use of the babase.SessionPlayer.exists() method (or boolean operator) to ensure that a SessionPlayer is still present if retaining references to one for any length of time.

id: int

The unique numeric ID of the Player.

Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do the right thing both for Player objects and values of None.

in_game: bool

This bool value will be True once the Player has completed any lobby character/team selection.

sessionteam: SessionTeam

The bascenev1.SessionTeam this Player is on. If the SessionPlayer is still in its lobby selecting a team/etc. then a bascenev1.SessionTeamNotFoundError will be raised.

inputdevice: InputDevice

The input device associated with the player.

color: Sequence[float]

The base color for this Player. In team games this will match the bascenev1.SessionTeam's color.

highlight: Sequence[float]

A secondary color for this player. This is used for minor highlights and accents to allow a player to stand apart from his teammates who may all share the same team (primary) color.

character: str

The character this player has selected in their profile.

activityplayer: Player | None

The current game-specific instance for this player.

def assigninput( self, type: InputType | tuple[InputType, ...], call: Callable) -> None:
767    def assigninput(
768        self,
769        type: bascenev1.InputType | tuple[bascenev1.InputType, ...],
770        call: Callable,
771    ) -> None:
772        """Set the python callable to be run for one or more types of input."""
773        return None

Set the python callable to be run for one or more types of input.

def exists(self) -> bool:
775    def exists(self) -> bool:
776        """Return whether the underlying player is still in the game."""
777        return bool()

Return whether the underlying player is still in the game.

def get_icon(self) -> dict[str, typing.Any]:
779    def get_icon(self) -> dict[str, Any]:
780        """Returns the character's icon (images, colors, etc contained
781        in a dict.
782        """
783        return {'foo': 'bar'}

Returns the character's icon (images, colors, etc contained in a dict.

def get_v1_account_id(self) -> str:
789    def get_v1_account_id(self) -> str:
790        """Return the V1 Account ID this player is signed in under, if
791        there is one and it can be determined with relative certainty.
792        Returns None otherwise. Note that this may require an active
793        internet connection (especially for network-connected players)
794        and may return None for a short while after a player initially
795        joins (while verification occurs).
796        """
797        return str()

Return the V1 Account ID this player is signed in under, if there is one and it can be determined with relative certainty. Returns None otherwise. Note that this may require an active internet connection (especially for network-connected players) and may return None for a short while after a player initially joins (while verification occurs).

def getname(self, full: bool = False, icon: bool = True) -> str:
799    def getname(self, full: bool = False, icon: bool = True) -> str:
800        """Returns the player's name. If icon is True, the long version of the
801        name may include an icon.
802        """
803        return str()

Returns the player's name. If icon is True, the long version of the name may include an icon.

def remove_from_game(self) -> None:
805    def remove_from_game(self) -> None:
806        """Removes the player from the game."""
807        return None

Removes the player from the game.

def resetinput(self) -> None:
809    def resetinput(self) -> None:
810        """Clears out the player's assigned input actions."""
811        return None

Clears out the player's assigned input actions.

def setname(self, name: str, full_name: str | None = None, real: bool = True) -> None:
837    def setname(
838        self, name: str, full_name: str | None = None, real: bool = True
839    ) -> None:
840        """Set the player's name to the provided string.
841        A number will automatically be appended if the name is not unique from
842        other players.
843        """
844        return None

Set the player's name to the provided string. A number will automatically be appended if the name is not unique from other players.

class SessionTeam:
20class SessionTeam:
21    """A team of one or more bascenev1.SessionPlayers.
22
23    Category: **Gameplay Classes**
24
25    Note that a SessionPlayer *always* has a SessionTeam;
26    in some cases, such as free-for-all bascenev1.Sessions,
27    each SessionTeam consists of just one SessionPlayer.
28    """
29
30    # Annotate our attr types at the class level so they're introspectable.
31
32    name: babase.Lstr | str
33    """The team's name."""
34
35    color: tuple[float, ...]  # FIXME: can't we make this fixed len?
36    """The team's color."""
37
38    players: list[bascenev1.SessionPlayer]
39    """The list of bascenev1.SessionPlayer-s on the team."""
40
41    customdata: dict
42    """A dict for use by the current bascenev1.Session for
43       storing data associated with this team.
44       Unlike customdata, this persists for the duration
45       of the session."""
46
47    id: int
48    """The unique numeric id of the team."""
49
50    def __init__(
51        self,
52        team_id: int = 0,
53        name: babase.Lstr | str = '',
54        color: Sequence[float] = (1.0, 1.0, 1.0),
55    ):
56        """Instantiate a bascenev1.SessionTeam.
57
58        In most cases, all teams are provided to you by the bascenev1.Session,
59        bascenev1.Session, so calling this shouldn't be necessary.
60        """
61
62        self.id = team_id
63        self.name = name
64        self.color = tuple(color)
65        self.players = []
66        self.customdata = {}
67        self.activityteam: Team | None = None
68
69    def leave(self) -> None:
70        """(internal)"""
71        self.customdata = {}

A team of one or more bascenev1.SessionPlayers.

Category: Gameplay Classes

Note that a SessionPlayer always has a SessionTeam; in some cases, such as free-for-all bascenev1.Sessions, each SessionTeam consists of just one SessionPlayer.

SessionTeam( team_id: int = 0, name: Lstr | str = '', color: Sequence[float] = (1.0, 1.0, 1.0))
50    def __init__(
51        self,
52        team_id: int = 0,
53        name: babase.Lstr | str = '',
54        color: Sequence[float] = (1.0, 1.0, 1.0),
55    ):
56        """Instantiate a bascenev1.SessionTeam.
57
58        In most cases, all teams are provided to you by the bascenev1.Session,
59        bascenev1.Session, so calling this shouldn't be necessary.
60        """
61
62        self.id = team_id
63        self.name = name
64        self.color = tuple(color)
65        self.players = []
66        self.customdata = {}
67        self.activityteam: Team | None = None

Instantiate a bascenev1.SessionTeam.

In most cases, all teams are provided to you by the bascenev1.Session, bascenev1.Session, so calling this shouldn't be necessary.

name: Lstr | str

The team's name.

color: tuple[float, ...]

The team's color.

players: list[SessionPlayer]

The list of bascenev1.SessionPlayer-s on the team.

customdata: dict

A dict for use by the current bascenev1.Session for storing data associated with this team. Unlike customdata, this persists for the duration of the session.

id: int

The unique numeric id of the team.

activityteam: Team | None
def set_analytics_screen(screen: str) -> None:
1429def set_analytics_screen(screen: str) -> None:
1430    """Used for analytics to see where in the app players spend their time.
1431
1432    Category: **General Utility Functions**
1433
1434    Generally called when opening a new window or entering some UI.
1435    'screen' should be a string description of an app location
1436    ('Main Menu', etc.)
1437    """
1438    return None

Used for analytics to see where in the app players spend their time.

Category: General Utility Functions

Generally called when opening a new window or entering some UI. 'screen' should be a string description of an app location ('Main Menu', etc.)

def set_player_rejoin_cooldown(cooldown: float) -> None:
31def set_player_rejoin_cooldown(cooldown: float) -> None:
32    """Set the cooldown for individual players rejoining after leaving."""
33    global _g_player_rejoin_cooldown  # pylint: disable=global-statement
34    _g_player_rejoin_cooldown = max(0.0, cooldown)

Set the cooldown for individual players rejoining after leaving.

def set_max_players_override(max_players: int | None) -> None:
37def set_max_players_override(max_players: int | None) -> None:
38    """Set the override for how many players can join a session"""
39    global _max_players_override  # pylint: disable=global-statement
40    _max_players_override = max_players

Set the override for how many players can join a session

def setmusic( musictype: MusicType | None, continuous: bool = False) -> None:
51def setmusic(musictype: MusicType | None, continuous: bool = False) -> None:
52    """Set the app to play (or stop playing) a certain type of music.
53
54    category: **Gameplay Functions**
55
56    This function will handle loading and playing sound assets as necessary,
57    and also supports custom user soundtracks on specific platforms so the
58    user can override particular game music with their own.
59
60    Pass None to stop music.
61
62    if 'continuous' is True and musictype is the same as what is already
63    playing, the playing track will not be restarted.
64    """
65
66    # All we do here now is set a few music attrs on the current globals
67    # node. The foreground globals' current playing music then gets fed to
68    # the do_play_music call in our music controller. This way we can
69    # seamlessly support custom soundtracks in replays/etc since we're being
70    # driven purely by node data.
71    gnode = _bascenev1.getactivity().globalsnode
72    gnode.music_continuous = continuous
73    gnode.music = '' if musictype is None else musictype.value
74    gnode.music_count += 1

Set the app to play (or stop playing) a certain type of music.

category: Gameplay Functions

This function will handle loading and playing sound assets as necessary, and also supports custom user soundtracks on specific platforms so the user can override particular game music with their own.

Pass None to stop music.

if 'continuous' is True and musictype is the same as what is already playing, the playing track will not be restarted.

@dataclass
class Setting:
15@dataclass
16class Setting:
17    """Defines a user-controllable setting for a game or other entity.
18
19    Category: Gameplay Classes
20    """
21
22    name: str
23    default: Any

Defines a user-controllable setting for a game or other entity.

Category: Gameplay Classes

Setting(name: str, default: Any)
name: str
default: Any
@dataclass
class ShouldShatterMessage:
186@dataclass
187class ShouldShatterMessage:
188    """Tells an object that it should shatter.
189
190    Category: **Message Classes**
191    """

Tells an object that it should shatter.

Category: Message Classes

def show_damage_count( damage: str, position: Sequence[float], direction: Sequence[float]) -> None:
175def show_damage_count(
176    damage: str, position: Sequence[float], direction: Sequence[float]
177) -> None:
178    """Pop up a damage count at a position in space.
179
180    Category: **Gameplay Functions**
181    """
182    lifespan = 1.0
183    app = babase.app
184
185    # FIXME: Should never vary game elements based on local config.
186    #  (connected clients may have differing configs so they won't
187    #  get the intended results).
188    assert app.classic is not None
189    do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr
190    txtnode = _bascenev1.newnode(
191        'text',
192        attrs={
193            'text': damage,
194            'in_world': True,
195            'h_align': 'center',
196            'flatness': 1.0,
197            'shadow': 1.0 if do_big else 0.7,
198            'color': (1, 0.25, 0.25, 1),
199            'scale': 0.015 if do_big else 0.01,
200        },
201    )
202    # Translate upward.
203    tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3})
204    tcombine.connectattr('output', txtnode, 'position')
205    v_vals = []
206    pval = 0.0
207    vval = 0.07
208    count = 6
209    for i in range(count):
210        v_vals.append((float(i) / count, pval))
211        pval += vval
212        vval *= 0.5
213    p_start = position[0]
214    p_dir = direction[0]
215    animate(
216        tcombine,
217        'input0',
218        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
219    )
220    p_start = position[1]
221    p_dir = direction[1]
222    animate(
223        tcombine,
224        'input1',
225        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
226    )
227    p_start = position[2]
228    p_dir = direction[2]
229    animate(
230        tcombine,
231        'input2',
232        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
233    )
234    animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
235    _bascenev1.timer(lifespan, txtnode.delete)

Pop up a damage count at a position in space.

Category: Gameplay Functions

class Sound:
851class Sound:
852    """A reference to a sound.
853
854    Category: **Asset Classes**
855
856    Use bascenev1.getsound() to instantiate one.
857    """
858
859    def play(
860        self,
861        volume: float = 1.0,
862        position: Sequence[float] | None = None,
863        host_only: bool = False,
864    ) -> None:
865        """Play the sound a single time.
866
867        Category: **Gameplay Functions**
868
869        If position is not provided, the sound will be at a constant volume
870        everywhere. Position should be a float tuple of size 3.
871        """
872        return None

A reference to a sound.

Category: Asset Classes

Use bascenev1.getsound() to instantiate one.

def play( self, volume: float = 1.0, position: Optional[Sequence[float]] = None, host_only: bool = False) -> None:
859    def play(
860        self,
861        volume: float = 1.0,
862        position: Sequence[float] | None = None,
863        host_only: bool = False,
864    ) -> None:
865        """Play the sound a single time.
866
867        Category: **Gameplay Functions**
868
869        If position is not provided, the sound will be at a constant volume
870        everywhere. Position should be a float tuple of size 3.
871        """
872        return None

Play the sound a single time.

Category: Gameplay Functions

If position is not provided, the sound will be at a constant volume everywhere. Position should be a float tuple of size 3.

@dataclass
class StandLocation:
37@dataclass
38class StandLocation:
39    """Describes a point in space and an angle to face.
40
41    Category: Gameplay Classes
42    """
43
44    position: babase.Vec3
45    angle: float | None = None

Describes a point in space and an angle to face.

Category: Gameplay Classes

StandLocation(position: Vec3, angle: float | None = None)
position: Vec3
angle: float | None = None
@dataclass
class StandMessage:
129@dataclass
130class StandMessage:
131    """A message telling an object to move to a position in space.
132
133    Category: **Message Classes**
134
135    Used when teleporting players to home base, etc.
136    """
137
138    position: Sequence[float] = (0.0, 0.0, 0.0)
139    """Where to move to."""
140
141    angle: float = 0.0
142    """The angle to face (in degrees)"""

A message telling an object to move to a position in space.

Category: Message Classes

Used when teleporting players to home base, etc.

StandMessage(position: Sequence[float] = (0.0, 0.0, 0.0), angle: float = 0.0)
position: Sequence[float] = (0.0, 0.0, 0.0)

Where to move to.

angle: float = 0.0

The angle to face (in degrees)

class Stats:
255class Stats:
256    """Manages scores and statistics for a bascenev1.Session.
257
258    Category: **Gameplay Classes**
259    """
260
261    def __init__(self) -> None:
262        self._activity: weakref.ref[bascenev1.Activity] | None = None
263        self._player_records: dict[str, PlayerRecord] = {}
264        self.orchestrahitsound1: bascenev1.Sound | None = None
265        self.orchestrahitsound2: bascenev1.Sound | None = None
266        self.orchestrahitsound3: bascenev1.Sound | None = None
267        self.orchestrahitsound4: bascenev1.Sound | None = None
268
269    def setactivity(self, activity: bascenev1.Activity | None) -> None:
270        """Set the current activity for this instance."""
271
272        self._activity = None if activity is None else weakref.ref(activity)
273
274        # Load our media into this activity's context.
275        if activity is not None:
276            if activity.expired:
277                logging.exception('Unexpected finalized activity.')
278            else:
279                with activity.context:
280                    self._load_activity_media()
281
282    def getactivity(self) -> bascenev1.Activity | None:
283        """Get the activity associated with this instance.
284
285        May return None.
286        """
287        if self._activity is None:
288            return None
289        return self._activity()
290
291    def _load_activity_media(self) -> None:
292        self.orchestrahitsound1 = _bascenev1.getsound('orchestraHit')
293        self.orchestrahitsound2 = _bascenev1.getsound('orchestraHit2')
294        self.orchestrahitsound3 = _bascenev1.getsound('orchestraHit3')
295        self.orchestrahitsound4 = _bascenev1.getsound('orchestraHit4')
296
297    def reset(self) -> None:
298        """Reset the stats instance completely."""
299
300        # Just to be safe, lets make sure no multi-kill timers are gonna go off
301        # for no-longer-on-the-list players.
302        for p_entry in list(self._player_records.values()):
303            p_entry.cancel_multi_kill_timer()
304        self._player_records = {}
305
306    def reset_accum(self) -> None:
307        """Reset per-sound sub-scores."""
308        for s_player in list(self._player_records.values()):
309            s_player.cancel_multi_kill_timer()
310            s_player.accumscore = 0
311            s_player.accum_kill_count = 0
312            s_player.accum_killed_count = 0
313            s_player.streak = 0
314
315    def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None:
316        """Register a bascenev1.SessionPlayer with this score-set."""
317        assert player.exists()  # Invalid refs should never be passed to funcs.
318        name = player.getname()
319        if name in self._player_records:
320            # If the player already exists, update his character and such as
321            # it may have changed.
322            self._player_records[name].associate_with_sessionplayer(player)
323        else:
324            name_full = player.getname(full=True)
325            self._player_records[name] = PlayerRecord(
326                name, name_full, player, self
327            )
328
329    def get_records(self) -> dict[str, bascenev1.PlayerRecord]:
330        """Get PlayerRecord corresponding to still-existing players."""
331        records = {}
332
333        # Go through our player records and return ones whose player id still
334        # corresponds to a player with that name.
335        for record_id, record in self._player_records.items():
336            lastplayer = record.get_last_sessionplayer()
337            if lastplayer and lastplayer.getname() == record_id:
338                records[record_id] = record
339        return records
340
341    def player_scored(
342        self,
343        player: bascenev1.Player,
344        base_points: int = 1,
345        target: Sequence[float] | None = None,
346        kill: bool = False,
347        victim_player: bascenev1.Player | None = None,
348        scale: float = 1.0,
349        color: Sequence[float] | None = None,
350        title: str | babase.Lstr | None = None,
351        screenmessage: bool = True,
352        display: bool = True,
353        importance: int = 1,
354        showpoints: bool = True,
355        big_message: bool = False,
356    ) -> int:
357        """Register a score for the player.
358
359        Return value is actual score with multipliers and such factored in.
360        """
361        # FIXME: Tidy this up.
362        # pylint: disable=cyclic-import
363        # pylint: disable=too-many-branches
364        # pylint: disable=too-many-locals
365        from bascenev1lib.actor.popuptext import PopupText
366
367        from bascenev1._gameactivity import GameActivity
368
369        del victim_player  # Currently unused.
370        name = player.getname()
371        s_player = self._player_records[name]
372
373        if kill:
374            s_player.submit_kill(showpoints=showpoints)
375
376        display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
377
378        if color is not None:
379            display_color = color
380        elif importance != 1:
381            display_color = (1.0, 1.0, 0.4, 1.0)
382        points = base_points
383
384        # If they want a big announcement, throw a zoom-text up there.
385        if display and big_message:
386            try:
387                assert self._activity is not None
388                activity = self._activity()
389                if isinstance(activity, GameActivity):
390                    name_full = player.getname(full=True, icon=False)
391                    activity.show_zoom_message(
392                        babase.Lstr(
393                            resource='nameScoresText',
394                            subs=[('${NAME}', name_full)],
395                        ),
396                        color=babase.normalized_color(player.team.color),
397                    )
398            except Exception:
399                logging.exception('Error showing big_message.')
400
401        # If we currently have a actor, pop up a score over it.
402        if display and showpoints:
403            our_pos = player.node.position if player.node else None
404            if our_pos is not None:
405                if target is None:
406                    target = our_pos
407
408                # If display-pos is *way* lower than us, raise it up
409                # (so we can still see scores from dudes that fell off cliffs).
410                display_pos = (
411                    target[0],
412                    max(target[1], our_pos[1] - 2.0),
413                    min(target[2], our_pos[2] + 2.0),
414                )
415                activity = self.getactivity()
416                if activity is not None:
417                    if title is not None:
418                        sval = babase.Lstr(
419                            value='+${A} ${B}',
420                            subs=[('${A}', str(points)), ('${B}', title)],
421                        )
422                    else:
423                        sval = babase.Lstr(
424                            value='+${A}', subs=[('${A}', str(points))]
425                        )
426                    PopupText(
427                        sval,
428                        color=display_color,
429                        scale=1.2 * scale,
430                        position=display_pos,
431                    ).autoretain()
432
433        # Tally kills.
434        if kill:
435            s_player.accum_kill_count += 1
436            s_player.kill_count += 1
437
438        # Report non-kill scorings.
439        try:
440            if screenmessage and not kill:
441                _bascenev1.broadcastmessage(
442                    babase.Lstr(
443                        resource='nameScoresText', subs=[('${NAME}', name)]
444                    ),
445                    top=True,
446                    color=player.color,
447                    image=player.get_icon(),
448                )
449        except Exception:
450            logging.exception('Error announcing score.')
451
452        s_player.score += points
453        s_player.accumscore += points
454
455        # Inform a running game of the score.
456        if points != 0:
457            activity = self._activity() if self._activity is not None else None
458            if activity is not None:
459                activity.handlemessage(PlayerScoredMessage(score=points))
460
461        return points
462
463    def player_was_killed(
464        self,
465        player: bascenev1.Player,
466        killed: bool = False,
467        killer: bascenev1.Player | None = None,
468    ) -> None:
469        """Should be called when a player is killed."""
470        name = player.getname()
471        prec = self._player_records[name]
472        prec.streak = 0
473        if killed:
474            prec.accum_killed_count += 1
475            prec.killed_count += 1
476        try:
477            if killed and _bascenev1.getactivity().announce_player_deaths:
478                if killer is player:
479                    _bascenev1.broadcastmessage(
480                        babase.Lstr(
481                            resource='nameSuicideText', subs=[('${NAME}', name)]
482                        ),
483                        top=True,
484                        color=player.color,
485                        image=player.get_icon(),
486                    )
487                elif killer is not None:
488                    if killer.team is player.team:
489                        _bascenev1.broadcastmessage(
490                            babase.Lstr(
491                                resource='nameBetrayedText',
492                                subs=[
493                                    ('${NAME}', killer.getname()),
494                                    ('${VICTIM}', name),
495                                ],
496                            ),
497                            top=True,
498                            color=killer.color,
499                            image=killer.get_icon(),
500                        )
501                    else:
502                        _bascenev1.broadcastmessage(
503                            babase.Lstr(
504                                resource='nameKilledText',
505                                subs=[
506                                    ('${NAME}', killer.getname()),
507                                    ('${VICTIM}', name),
508                                ],
509                            ),
510                            top=True,
511                            color=killer.color,
512                            image=killer.get_icon(),
513                        )
514                else:
515                    _bascenev1.broadcastmessage(
516                        babase.Lstr(
517                            resource='nameDiedText', subs=[('${NAME}', name)]
518                        ),
519                        top=True,
520                        color=player.color,
521                        image=player.get_icon(),
522                    )
523        except Exception:
524            logging.exception('Error announcing kill.')

Manages scores and statistics for a bascenev1.Session.

Category: Gameplay Classes

orchestrahitsound1: Sound | None
orchestrahitsound2: Sound | None
orchestrahitsound3: Sound | None
orchestrahitsound4: Sound | None
def setactivity(self, activity: Activity | None) -> None:
269    def setactivity(self, activity: bascenev1.Activity | None) -> None:
270        """Set the current activity for this instance."""
271
272        self._activity = None if activity is None else weakref.ref(activity)
273
274        # Load our media into this activity's context.
275        if activity is not None:
276            if activity.expired:
277                logging.exception('Unexpected finalized activity.')
278            else:
279                with activity.context:
280                    self._load_activity_media()

Set the current activity for this instance.

def getactivity(self) -> Activity | None:
282    def getactivity(self) -> bascenev1.Activity | None:
283        """Get the activity associated with this instance.
284
285        May return None.
286        """
287        if self._activity is None:
288            return None
289        return self._activity()

Get the activity associated with this instance.

May return None.

def reset(self) -> None:
297    def reset(self) -> None:
298        """Reset the stats instance completely."""
299
300        # Just to be safe, lets make sure no multi-kill timers are gonna go off
301        # for no-longer-on-the-list players.
302        for p_entry in list(self._player_records.values()):
303            p_entry.cancel_multi_kill_timer()
304        self._player_records = {}

Reset the stats instance completely.

def reset_accum(self) -> None:
306    def reset_accum(self) -> None:
307        """Reset per-sound sub-scores."""
308        for s_player in list(self._player_records.values()):
309            s_player.cancel_multi_kill_timer()
310            s_player.accumscore = 0
311            s_player.accum_kill_count = 0
312            s_player.accum_killed_count = 0
313            s_player.streak = 0

Reset per-sound sub-scores.

def register_sessionplayer(self, player: SessionPlayer) -> None:
315    def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None:
316        """Register a bascenev1.SessionPlayer with this score-set."""
317        assert player.exists()  # Invalid refs should never be passed to funcs.
318        name = player.getname()
319        if name in self._player_records:
320            # If the player already exists, update his character and such as
321            # it may have changed.
322            self._player_records[name].associate_with_sessionplayer(player)
323        else:
324            name_full = player.getname(full=True)
325            self._player_records[name] = PlayerRecord(
326                name, name_full, player, self
327            )

Register a bascenev1.SessionPlayer with this score-set.

def get_records(self) -> dict[str, PlayerRecord]:
329    def get_records(self) -> dict[str, bascenev1.PlayerRecord]:
330        """Get PlayerRecord corresponding to still-existing players."""
331        records = {}
332
333        # Go through our player records and return ones whose player id still
334        # corresponds to a player with that name.
335        for record_id, record in self._player_records.items():
336            lastplayer = record.get_last_sessionplayer()
337            if lastplayer and lastplayer.getname() == record_id:
338                records[record_id] = record
339        return records

Get PlayerRecord corresponding to still-existing players.

def player_scored( self, player: Player, base_points: int = 1, target: Optional[Sequence[float]] = None, kill: bool = False, victim_player: Player | None = None, scale: float = 1.0, color: Optional[Sequence[float]] = None, title: str | Lstr | None = None, screenmessage: bool = True, display: bool = True, importance: int = 1, showpoints: bool = True, big_message: bool = False) -> int:
341    def player_scored(
342        self,
343        player: bascenev1.Player,
344        base_points: int = 1,
345        target: Sequence[float] | None = None,
346        kill: bool = False,
347        victim_player: bascenev1.Player | None = None,
348        scale: float = 1.0,
349        color: Sequence[float] | None = None,
350        title: str | babase.Lstr | None = None,
351        screenmessage: bool = True,
352        display: bool = True,
353        importance: int = 1,
354        showpoints: bool = True,
355        big_message: bool = False,
356    ) -> int:
357        """Register a score for the player.
358
359        Return value is actual score with multipliers and such factored in.
360        """
361        # FIXME: Tidy this up.
362        # pylint: disable=cyclic-import
363        # pylint: disable=too-many-branches
364        # pylint: disable=too-many-locals
365        from bascenev1lib.actor.popuptext import PopupText
366
367        from bascenev1._gameactivity import GameActivity
368
369        del victim_player  # Currently unused.
370        name = player.getname()
371        s_player = self._player_records[name]
372
373        if kill:
374            s_player.submit_kill(showpoints=showpoints)
375
376        display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
377
378        if color is not None:
379            display_color = color
380        elif importance != 1:
381            display_color = (1.0, 1.0, 0.4, 1.0)
382        points = base_points
383
384        # If they want a big announcement, throw a zoom-text up there.
385        if display and big_message:
386            try:
387                assert self._activity is not None
388                activity = self._activity()
389                if isinstance(activity, GameActivity):
390                    name_full = player.getname(full=True, icon=False)
391                    activity.show_zoom_message(
392                        babase.Lstr(
393                            resource='nameScoresText',
394                            subs=[('${NAME}', name_full)],
395                        ),
396                        color=babase.normalized_color(player.team.color),
397                    )
398            except Exception:
399                logging.exception('Error showing big_message.')
400
401        # If we currently have a actor, pop up a score over it.
402        if display and showpoints:
403            our_pos = player.node.position if player.node else None
404            if our_pos is not None:
405                if target is None:
406                    target = our_pos
407
408                # If display-pos is *way* lower than us, raise it up
409                # (so we can still see scores from dudes that fell off cliffs).
410                display_pos = (
411                    target[0],
412                    max(target[1], our_pos[1] - 2.0),
413                    min(target[2], our_pos[2] + 2.0),
414                )
415                activity = self.getactivity()
416                if activity is not None:
417                    if title is not None:
418                        sval = babase.Lstr(
419                            value='+${A} ${B}',
420                            subs=[('${A}', str(points)), ('${B}', title)],
421                        )
422                    else:
423                        sval = babase.Lstr(
424                            value='+${A}', subs=[('${A}', str(points))]
425                        )
426                    PopupText(
427                        sval,
428                        color=display_color,
429                        scale=1.2 * scale,
430                        position=display_pos,
431                    ).autoretain()
432
433        # Tally kills.
434        if kill:
435            s_player.accum_kill_count += 1
436            s_player.kill_count += 1
437
438        # Report non-kill scorings.
439        try:
440            if screenmessage and not kill:
441                _bascenev1.broadcastmessage(
442                    babase.Lstr(
443                        resource='nameScoresText', subs=[('${NAME}', name)]
444                    ),
445                    top=True,
446                    color=player.color,
447                    image=player.get_icon(),
448                )
449        except Exception:
450            logging.exception('Error announcing score.')
451
452        s_player.score += points
453        s_player.accumscore += points
454
455        # Inform a running game of the score.
456        if points != 0:
457            activity = self._activity() if self._activity is not None else None
458            if activity is not None:
459                activity.handlemessage(PlayerScoredMessage(score=points))
460
461        return points

Register a score for the player.

Return value is actual score with multipliers and such factored in.

def player_was_killed( self, player: Player, killed: bool = False, killer: Player | None = None) -> None:
463    def player_was_killed(
464        self,
465        player: bascenev1.Player,
466        killed: bool = False,
467        killer: bascenev1.Player | None = None,
468    ) -> None:
469        """Should be called when a player is killed."""
470        name = player.getname()
471        prec = self._player_records[name]
472        prec.streak = 0
473        if killed:
474            prec.accum_killed_count += 1
475            prec.killed_count += 1
476        try:
477            if killed and _bascenev1.getactivity().announce_player_deaths:
478                if killer is player:
479                    _bascenev1.broadcastmessage(
480                        babase.Lstr(
481                            resource='nameSuicideText', subs=[('${NAME}', name)]
482                        ),
483                        top=True,
484                        color=player.color,
485                        image=player.get_icon(),
486                    )
487                elif killer is not None:
488                    if killer.team is player.team:
489                        _bascenev1.broadcastmessage(
490                            babase.Lstr(
491                                resource='nameBetrayedText',
492                                subs=[
493                                    ('${NAME}', killer.getname()),
494                                    ('${VICTIM}', name),
495                                ],
496                            ),
497                            top=True,
498                            color=killer.color,
499                            image=killer.get_icon(),
500                        )
501                    else:
502                        _bascenev1.broadcastmessage(
503                            babase.Lstr(
504                                resource='nameKilledText',
505                                subs=[
506                                    ('${NAME}', killer.getname()),
507                                    ('${VICTIM}', name),
508                                ],
509                            ),
510                            top=True,
511                            color=killer.color,
512                            image=killer.get_icon(),
513                        )
514                else:
515                    _bascenev1.broadcastmessage(
516                        babase.Lstr(
517                            resource='nameDiedText', subs=[('${NAME}', name)]
518                        ),
519                        top=True,
520                        color=player.color,
521                        image=player.get_icon(),
522                    )
523        except Exception:
524            logging.exception('Error announcing kill.')

Should be called when a player is killed.

def storagename(suffix: str | None = None) -> str:
321def storagename(suffix: str | None = None) -> str:
322    """Generate a unique name for storing class data in shared places.
323
324    Category: **General Utility Functions**
325
326    This consists of a leading underscore, the module path at the
327    call site with dots replaced by underscores, the containing class's
328    qualified name, and the provided suffix. When storing data in public
329    places such as 'customdata' dicts, this minimizes the chance of
330    collisions with other similarly named classes.
331
332    Note that this will function even if called in the class definition.
333
334    ##### Examples
335    Generate a unique name for storage purposes:
336    >>> class MyThingie:
337    ...     # This will give something like
338    ...     # '_mymodule_submodule_mythingie_data'.
339    ...     _STORENAME = babase.storagename('data')
340    ...
341    ...     # Use that name to store some data in the Activity we were
342    ...     # passed.
343    ...     def __init__(self, activity):
344    ...         activity.customdata[self._STORENAME] = {}
345    """
346    frame = inspect.currentframe()
347    if frame is None:
348        raise RuntimeError('Cannot get current stack frame.')
349    fback = frame.f_back
350
351    # Note: We need to explicitly clear frame here to avoid a ref-loop
352    # that keeps all function-dicts in the stack alive until the next
353    # full GC cycle (the stack frame refers to this function's dict,
354    # which refers to the stack frame).
355    del frame
356
357    if fback is None:
358        raise RuntimeError('Cannot get parent stack frame.')
359    modulepath = fback.f_globals.get('__name__')
360    if modulepath is None:
361        raise RuntimeError('Cannot get parent stack module path.')
362    assert isinstance(modulepath, str)
363    qualname = fback.f_locals.get('__qualname__')
364    if qualname is not None:
365        assert isinstance(qualname, str)
366        fullpath = f'_{modulepath}_{qualname.lower()}'
367    else:
368        fullpath = f'_{modulepath}'
369    if suffix is not None:
370        fullpath = f'{fullpath}_{suffix}'
371    return fullpath.replace('.', '_')

Generate a unique name for storing class data in shared places.

Category: General Utility Functions

This consists of a leading underscore, the module path at the call site with dots replaced by underscores, the containing class's qualified name, and the provided suffix. When storing data in public places such as 'customdata' dicts, this minimizes the chance of collisions with other similarly named classes.

Note that this will function even if called in the class definition.

Examples

Generate a unique name for storage purposes:

>>> class MyThingie:
...     # This will give something like
...     # '_mymodule_submodule_mythingie_data'.
...     _STORENAME = babase.storagename('data')
...
...     # Use that name to store some data in the Activity we were
...     # passed.
...     def __init__(self, activity):
...         activity.customdata[self._STORENAME] = {}
class Team(typing.Generic[~PlayerT]):
 77class Team(Generic[PlayerT]):
 78    """A team in a specific bascenev1.Activity.
 79
 80    Category: **Gameplay Classes**
 81
 82    These correspond to bascenev1.SessionTeam objects, but are created
 83    per activity so that the activity can use its own custom team subclass.
 84    """
 85
 86    # Defining these types at the class level instead of in __init__ so
 87    # that types are introspectable (these are still instance attrs).
 88    players: list[PlayerT]
 89    id: int
 90    name: babase.Lstr | str
 91    color: tuple[float, ...]  # FIXME: can't we make this fixed length?
 92    _sessionteam: weakref.ref[SessionTeam]
 93    _expired: bool
 94    _postinited: bool
 95    _customdata: dict
 96
 97    # NOTE: avoiding having any __init__() here since it seems to not
 98    # get called by default if a dataclass inherits from us.
 99
100    def postinit(self, sessionteam: SessionTeam) -> None:
101        """Wire up a newly created SessionTeam.
102
103        (internal)
104        """
105
106        # Sanity check; if a dataclass is created that inherits from us,
107        # it will define an equality operator by default which will break
108        # internal game logic. So complain loudly if we find one.
109        if type(self).__eq__ is not object.__eq__:
110            raise RuntimeError(
111                f'Team class {type(self)} defines an equality'
112                f' operator (__eq__) which will break internal'
113                f' logic. Please remove it.\n'
114                f'For dataclasses you can do "dataclass(eq=False)"'
115                f' in the class decorator.'
116            )
117
118        self.players = []
119        self._sessionteam = weakref.ref(sessionteam)
120        self.id = sessionteam.id
121        self.name = sessionteam.name
122        self.color = sessionteam.color
123        self._customdata = {}
124        self._expired = False
125        self._postinited = True
126
127    def manual_init(
128        self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...]
129    ) -> None:
130        """Manually init a team for uses such as bots."""
131        self.id = team_id
132        self.name = name
133        self.color = color
134        self._customdata = {}
135        self._expired = False
136        self._postinited = True
137
138    @property
139    def customdata(self) -> dict:
140        """Arbitrary values associated with the team.
141        Though it is encouraged that most player values be properly defined
142        on the bascenev1.Team subclass, it may be useful for player-agnostic
143        objects to store values here. This dict is cleared when the team
144        leaves or expires so objects stored here will be disposed of at
145        the expected time, unlike the Team instance itself which may
146        continue to be referenced after it is no longer part of the game.
147        """
148        assert self._postinited
149        assert not self._expired
150        return self._customdata
151
152    def leave(self) -> None:
153        """Called when the Team leaves a running game.
154
155        (internal)
156        """
157        assert self._postinited
158        assert not self._expired
159        del self._customdata
160        del self.players
161
162    def expire(self) -> None:
163        """Called when the Team is expiring (due to the Activity expiring).
164
165        (internal)
166        """
167        assert self._postinited
168        assert not self._expired
169        self._expired = True
170
171        try:
172            self.on_expire()
173        except Exception:
174            logging.exception('Error in on_expire for %s.', self)
175
176        del self._customdata
177        del self.players
178
179    def on_expire(self) -> None:
180        """Can be overridden to handle team expiration."""
181
182    @property
183    def sessionteam(self) -> SessionTeam:
184        """Return the bascenev1.SessionTeam corresponding to this Team.
185
186        Throws a babase.SessionTeamNotFoundError if there is none.
187        """
188        assert self._postinited
189        if self._sessionteam is not None:
190            sessionteam = self._sessionteam()
191            if sessionteam is not None:
192                return sessionteam
193
194        raise babase.SessionTeamNotFoundError()

A team in a specific bascenev1.Activity.

Category: Gameplay Classes

These correspond to bascenev1.SessionTeam objects, but are created per activity so that the activity can use its own custom team subclass.

players: list[~PlayerT]
id: int
name: Lstr | str
color: tuple[float, ...]
def manual_init( self, team_id: int, name: Lstr | str, color: tuple[float, ...]) -> None:
127    def manual_init(
128        self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...]
129    ) -> None:
130        """Manually init a team for uses such as bots."""
131        self.id = team_id
132        self.name = name
133        self.color = color
134        self._customdata = {}
135        self._expired = False
136        self._postinited = True

Manually init a team for uses such as bots.

customdata: dict
138    @property
139    def customdata(self) -> dict:
140        """Arbitrary values associated with the team.
141        Though it is encouraged that most player values be properly defined
142        on the bascenev1.Team subclass, it may be useful for player-agnostic
143        objects to store values here. This dict is cleared when the team
144        leaves or expires so objects stored here will be disposed of at
145        the expected time, unlike the Team instance itself which may
146        continue to be referenced after it is no longer part of the game.
147        """
148        assert self._postinited
149        assert not self._expired
150        return self._customdata

Arbitrary values associated with the team. Though it is encouraged that most player values be properly defined on the bascenev1.Team subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the team leaves or expires so objects stored here will be disposed of at the expected time, unlike the Team instance itself which may continue to be referenced after it is no longer part of the game.

def on_expire(self) -> None:
179    def on_expire(self) -> None:
180        """Can be overridden to handle team expiration."""

Can be overridden to handle team expiration.

sessionteam: SessionTeam
182    @property
183    def sessionteam(self) -> SessionTeam:
184        """Return the bascenev1.SessionTeam corresponding to this Team.
185
186        Throws a babase.SessionTeamNotFoundError if there is none.
187        """
188        assert self._postinited
189        if self._sessionteam is not None:
190            sessionteam = self._sessionteam()
191            if sessionteam is not None:
192                return sessionteam
193
194        raise babase.SessionTeamNotFoundError()

Return the bascenev1.SessionTeam corresponding to this Team.

Throws a babase.SessionTeamNotFoundError if there is none.

class TeamGameActivity(bascenev1.GameActivity[~PlayerT, ~TeamT]):
 30class TeamGameActivity(GameActivity[PlayerT, TeamT]):
 31    """Base class for teams and free-for-all mode games.
 32
 33    Category: **Gameplay Classes**
 34
 35    (Free-for-all is essentially just a special case where every
 36    bascenev1.Player has their own bascenev1.Team)
 37    """
 38
 39    @override
 40    @classmethod
 41    def supports_session_type(
 42        cls, sessiontype: type[bascenev1.Session]
 43    ) -> bool:
 44        """
 45        Class method override;
 46        returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
 47        False otherwise.
 48        """
 49        return issubclass(sessiontype, DualTeamSession) or issubclass(
 50            sessiontype, FreeForAllSession
 51        )
 52
 53    def __init__(self, settings: dict):
 54        super().__init__(settings)
 55
 56        # By default we don't show kill-points in free-for-all sessions.
 57        # (there's usually some activity-specific score and we don't
 58        # wanna confuse things)
 59        if isinstance(self.session, FreeForAllSession):
 60            self.show_kill_points = False
 61
 62    @override
 63    def on_transition_in(self) -> None:
 64        # pylint: disable=cyclic-import
 65        from bascenev1._coopsession import CoopSession
 66        from bascenev1lib.actor.controlsguide import ControlsGuide
 67
 68        super().on_transition_in()
 69
 70        # On the first game, show the controls UI momentarily.
 71        # (unless we're being run in co-op mode, in which case we leave
 72        # it up to them)
 73        if not isinstance(self.session, CoopSession) and getattr(
 74            self, 'show_controls_guide', True
 75        ):
 76            attrname = '_have_shown_ctrl_help_overlay'
 77            if not getattr(self.session, attrname, False):
 78                delay = 4.0
 79                lifespan = 10.0
 80                if self.slow_motion:
 81                    lifespan *= 0.3
 82                ControlsGuide(
 83                    delay=delay,
 84                    lifespan=lifespan,
 85                    scale=0.8,
 86                    position=(380, 200),
 87                    bright=True,
 88                ).autoretain()
 89                setattr(self.session, attrname, True)
 90
 91    @override
 92    def on_begin(self) -> None:
 93        super().on_begin()
 94        try:
 95            # Award a few (classic) achievements.
 96            if isinstance(self.session, FreeForAllSession):
 97                if len(self.players) >= 2:
 98                    if babase.app.classic is not None:
 99                        babase.app.classic.ach.award_local_achievement(
100                            'Free Loader'
101                        )
102            elif isinstance(self.session, DualTeamSession):
103                if len(self.players) >= 4:
104                    if babase.app.classic is not None:
105                        babase.app.classic.ach.award_local_achievement(
106                            'Team Player'
107                        )
108        except Exception:
109            logging.exception('Error in on_begin.')
110
111    @override
112    def spawn_player_spaz(
113        self,
114        player: PlayerT,
115        position: Sequence[float] | None = None,
116        angle: float | None = None,
117    ) -> PlayerSpaz:
118        """
119        Method override; spawns and wires up a standard bascenev1.PlayerSpaz
120        for a bascenev1.Player.
121
122        If position or angle is not supplied, a default will be chosen based
123        on the bascenev1.Player and their bascenev1.Team.
124        """
125        if position is None:
126            # In teams-mode get our team-start-location.
127            if isinstance(self.session, DualTeamSession):
128                position = self.map.get_start_position(player.team.id)
129            else:
130                # Otherwise do free-for-all spawn locations.
131                position = self.map.get_ffa_start_position(self.players)
132
133        return super().spawn_player_spaz(player, position, angle)
134
135    # FIXME: need to unify these arguments with GameActivity.end()
136    def end(  # type: ignore
137        self,
138        results: Any = None,
139        announce_winning_team: bool = True,
140        announce_delay: float = 0.1,
141        force: bool = False,
142    ) -> None:
143        """
144        End the game and announce the single winning team
145        unless 'announce_winning_team' is False.
146        (for results without a single most-important winner).
147        """
148        # pylint: disable=arguments-renamed
149        from bascenev1._coopsession import CoopSession
150        from bascenev1._multiteamsession import MultiTeamSession
151
152        # Announce win (but only for the first finish() call)
153        # (also don't announce in co-op sessions; we leave that up to them).
154        session = self.session
155        if not isinstance(session, CoopSession):
156            do_announce = not self.has_ended()
157            super().end(results, delay=2.0 + announce_delay, force=force)
158
159            # Need to do this *after* end end call so that results is valid.
160            assert isinstance(results, GameResults)
161            if do_announce and isinstance(session, MultiTeamSession):
162                session.announce_game_results(
163                    self,
164                    results,
165                    delay=announce_delay,
166                    announce_winning_team=announce_winning_team,
167                )
168
169        # For co-op we just pass this up the chain with a delay added
170        # (in most cases). Team games expect a delay for the announce
171        # portion in teams/ffa mode so this keeps it consistent.
172        else:
173            # don't want delay on restarts..
174            if (
175                isinstance(results, dict)
176                and 'outcome' in results
177                and results['outcome'] == 'restart'
178            ):
179                delay = 0.0
180            else:
181                delay = 2.0
182                _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play)
183            super().end(results, delay=delay, force=force)

Base class for teams and free-for-all mode games.

Category: Gameplay Classes

(Free-for-all is essentially just a special case where every bascenev1.Player has their own bascenev1.Team)

TeamGameActivity(settings: dict)
53    def __init__(self, settings: dict):
54        super().__init__(settings)
55
56        # By default we don't show kill-points in free-for-all sessions.
57        # (there's usually some activity-specific score and we don't
58        # wanna confuse things)
59        if isinstance(self.session, FreeForAllSession):
60            self.show_kill_points = False

Instantiate the Activity.

@override
@classmethod
def supports_session_type(cls, sessiontype: type[Session]) -> bool:
39    @override
40    @classmethod
41    def supports_session_type(
42        cls, sessiontype: type[bascenev1.Session]
43    ) -> bool:
44        """
45        Class method override;
46        returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
47        False otherwise.
48        """
49        return issubclass(sessiontype, DualTeamSession) or issubclass(
50            sessiontype, FreeForAllSession
51        )

Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.

@override
def on_transition_in(self) -> None:
62    @override
63    def on_transition_in(self) -> None:
64        # pylint: disable=cyclic-import
65        from bascenev1._coopsession import CoopSession
66        from bascenev1lib.actor.controlsguide import ControlsGuide
67
68        super().on_transition_in()
69
70        # On the first game, show the controls UI momentarily.
71        # (unless we're being run in co-op mode, in which case we leave
72        # it up to them)
73        if not isinstance(self.session, CoopSession) and getattr(
74            self, 'show_controls_guide', True
75        ):
76            attrname = '_have_shown_ctrl_help_overlay'
77            if not getattr(self.session, attrname, False):
78                delay = 4.0
79                lifespan = 10.0
80                if self.slow_motion:
81                    lifespan *= 0.3
82                ControlsGuide(
83                    delay=delay,
84                    lifespan=lifespan,
85                    scale=0.8,
86                    position=(380, 200),
87                    bright=True,
88                ).autoretain()
89                setattr(self.session, attrname, True)

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 bascenev1.Activity.on_begin() is called.

@override
def on_begin(self) -> None:
 91    @override
 92    def on_begin(self) -> None:
 93        super().on_begin()
 94        try:
 95            # Award a few (classic) achievements.
 96            if isinstance(self.session, FreeForAllSession):
 97                if len(self.players) >= 2:
 98                    if babase.app.classic is not None:
 99                        babase.app.classic.ach.award_local_achievement(
100                            'Free Loader'
101                        )
102            elif isinstance(self.session, DualTeamSession):
103                if len(self.players) >= 4:
104                    if babase.app.classic is not None:
105                        babase.app.classic.ach.award_local_achievement(
106                            'Team Player'
107                        )
108        except Exception:
109            logging.exception('Error in on_begin.')

Called once the previous 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.

@override
def spawn_player_spaz( self, player: ~PlayerT, position: Optional[Sequence[float]] = None, angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
111    @override
112    def spawn_player_spaz(
113        self,
114        player: PlayerT,
115        position: Sequence[float] | None = None,
116        angle: float | None = None,
117    ) -> PlayerSpaz:
118        """
119        Method override; spawns and wires up a standard bascenev1.PlayerSpaz
120        for a bascenev1.Player.
121
122        If position or angle is not supplied, a default will be chosen based
123        on the bascenev1.Player and their bascenev1.Team.
124        """
125        if position is None:
126            # In teams-mode get our team-start-location.
127            if isinstance(self.session, DualTeamSession):
128                position = self.map.get_start_position(player.team.id)
129            else:
130                # Otherwise do free-for-all spawn locations.
131                position = self.map.get_ffa_start_position(self.players)
132
133        return super().spawn_player_spaz(player, position, angle)

Method override; spawns and wires up a standard bascenev1.PlayerSpaz for a bascenev1.Player.

If position or angle is not supplied, a default will be chosen based on the bascenev1.Player and their bascenev1.Team.

def end( self, results: Any = None, announce_winning_team: bool = True, announce_delay: float = 0.1, force: bool = False) -> None:
136    def end(  # type: ignore
137        self,
138        results: Any = None,
139        announce_winning_team: bool = True,
140        announce_delay: float = 0.1,
141        force: bool = False,
142    ) -> None:
143        """
144        End the game and announce the single winning team
145        unless 'announce_winning_team' is False.
146        (for results without a single most-important winner).
147        """
148        # pylint: disable=arguments-renamed
149        from bascenev1._coopsession import CoopSession
150        from bascenev1._multiteamsession import MultiTeamSession
151
152        # Announce win (but only for the first finish() call)
153        # (also don't announce in co-op sessions; we leave that up to them).
154        session = self.session
155        if not isinstance(session, CoopSession):
156            do_announce = not self.has_ended()
157            super().end(results, delay=2.0 + announce_delay, force=force)
158
159            # Need to do this *after* end end call so that results is valid.
160            assert isinstance(results, GameResults)
161            if do_announce and isinstance(session, MultiTeamSession):
162                session.announce_game_results(
163                    self,
164                    results,
165                    delay=announce_delay,
166                    announce_winning_team=announce_winning_team,
167                )
168
169        # For co-op we just pass this up the chain with a delay added
170        # (in most cases). Team games expect a delay for the announce
171        # portion in teams/ffa mode so this keeps it consistent.
172        else:
173            # don't want delay on restarts..
174            if (
175                isinstance(results, dict)
176                and 'outcome' in results
177                and results['outcome'] == 'restart'
178            ):
179                delay = 0.0
180            else:
181                delay = 2.0
182                _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play)
183            super().end(results, delay=delay, force=force)

End the game and announce the single winning team unless 'announce_winning_team' is False. (for results without a single most-important winner).

class Texture:
875class Texture:
876    """A reference to a texture.
877
878    Category: **Asset Classes**
879
880    Use bascenev1.gettexture() to instantiate one.
881    """
882
883    pass

A reference to a texture.

Category: Asset Classes

Use bascenev1.gettexture() to instantiate one.

@dataclass
class ThawMessage:
215@dataclass
216class ThawMessage:
217    """Tells an object to stop being frozen.
218
219    Category: **Message Classes**
220    """

Tells an object to stop being frozen.

Category: Message Classes

def time() -> Time:
1736def time() -> bascenev1.Time:
1737    """Return the current scene time in seconds.
1738
1739    Category: **General Utility Functions**
1740
1741    Scene time maps to local simulation time in bascenev1.Activity or
1742    bascenev1.Session Contexts. This means that it may progress slower
1743    in slow-motion play modes, stop when the game is paused, etc.
1744
1745    Note that the value returned here is simply a float; it just has a
1746    unique type in the type-checker's eyes to help prevent it from being
1747    accidentally used with time functionality expecting other time types.
1748    """
1749    import bascenev1  # pylint: disable=cyclic-import
1750
1751    return bascenev1.Time(0.0)

Return the current scene time in seconds.

Category: General Utility Functions

Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.

Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

Time = Time
def timer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
1755def timer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
1756    """Schedule a call to run at a later point in time.
1757
1758    Category: **General Utility Functions**
1759
1760    This function adds a scene-time timer to the current babase.Context.
1761    This timer cannot be canceled or modified once created. If you
1762     require the ability to do so, use the babase.Timer class instead.
1763
1764    Scene time maps to local simulation time in bascenev1.Activity or
1765    bascenev1.Session Contexts. This means that it may progress slower
1766    in slow-motion play modes, stop when the game is paused, etc.
1767
1768    ##### Arguments
1769    ###### time (float)
1770    > Length of scene time in seconds that the timer will wait
1771    before firing.
1772
1773    ###### call (Callable[[], Any])
1774    > A callable Python object. Note that the timer will retain a
1775    strong reference to the callable for as long as it exists, so you
1776    may want to look into concepts such as babase.WeakCall if that is not
1777    desired.
1778
1779    ###### repeat (bool)
1780    > If True, the timer will fire repeatedly, with each successive
1781    firing having the same delay as the first.
1782
1783    ##### Examples
1784    Print some stuff through time:
1785    >>> import bascenev1 as bs
1786    >>> bs.screenmessage('hello from now!')
1787    >>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
1788    >>> bs.timer(2.0, bs.Call(bs.screenmessage,
1789    ...                       'hello from the future 2!'))
1790    """
1791    return None

Schedule a call to run at a later point in time.

Category: General Utility Functions

This function adds a scene-time timer to the current babase.Context. This timer cannot be canceled or modified once created. If you require the ability to do so, use the babase.Timer class instead.

Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.

Arguments
time (float)

Length of scene time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat (bool)

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Examples

Print some stuff through time:

>>> import bascenev1 as bs
>>> bs.screenmessage('hello from now!')
>>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
>>> bs.timer(2.0, bs.Call(bs.screenmessage,
...                       'hello from the future 2!'))
class Timer:
887class Timer:
888    """Timers are used to run code at later points in time.
889
890    Category: **General Utility Classes**
891
892    This class encapsulates a scene-time timer in the current
893    bascenev1.Context. The underlying timer will be destroyed when either
894    this object is no longer referenced or when its Context (Activity,
895    etc.) dies. If you do not want to worry about keeping a reference to
896    your timer around,
897    you should use the bs.timer() function instead.
898
899    Scene time maps to local simulation time in bascenev1.Activity or
900    bascenev1.Session Contexts. This means that it may progress slower
901    in slow-motion play modes, stop when the game is paused, etc.
902
903    ###### time
904    > Length of time (in seconds by default) that the timer will wait
905    before firing. Note that the actual delay experienced may vary
906    depending on the timetype. (see below)
907
908    ###### call
909    > A callable Python object. Note that the timer will retain a
910    strong reference to the callable for as long as it exists, so you
911    may want to look into concepts such as babase.WeakCall if that is not
912    desired.
913
914    ###### repeat
915    > If True, the timer will fire repeatedly, with each successive
916    firing having the same delay as the first.
917
918    ##### Example
919
920    Use a Timer object to print repeatedly for a few seconds:
921    >>> import bascenev1 as bs
922    ... def say_it():
923    ...     bs.screenmessage('BADGER!')
924    ... def stop_saying_it():
925    ...     global g_timer
926    ...     g_timer = None
927    ...     bs.screenmessage('MUSHROOM MUSHROOM!')
928    ... # Create our timer; it will run as long as we have the self.t ref.
929    ... g_timer = bs.Timer(0.3, say_it, repeat=True)
930    ... # Now fire off a one-shot timer to kill it.
931    ... bs.timer(3.89, stop_saying_it)
932    """
933
934    def __init__(
935        self, time: float, call: Callable[[], Any], repeat: bool = False
936    ) -> None:
937        pass

Timers are used to run code at later points in time.

Category: General Utility Classes

This class encapsulates a scene-time timer in the current bascenev1.Context. The underlying timer will be destroyed when either this object is no longer referenced or when its Context (Activity, etc.) dies. If you do not want to worry about keeping a reference to your timer around, you should use the bs.timer() function instead.

Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.

time

Length of time (in seconds by default) that the timer will wait before firing. Note that the actual delay experienced may vary depending on the timetype. (see below)

call

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds:

>>> import bascenev1 as bs
... def say_it():
...     bs.screenmessage('BADGER!')
... def stop_saying_it():
...     global g_timer
...     g_timer = None
...     bs.screenmessage('MUSHROOM MUSHROOM!')
... # Create our timer; it will run as long as we have the self.t ref.
... g_timer = bs.Timer(0.3, say_it, repeat=True)
... # Now fire off a one-shot timer to kill it.
... bs.timer(3.89, stop_saying_it)
Timer(time: float, call: Callable[[], Any], repeat: bool = False)
934    def __init__(
935        self, time: float, call: Callable[[], Any], repeat: bool = False
936    ) -> None:
937        pass
def timestring(timeval: float | int, centi: bool = True) -> Lstr:
15def timestring(
16    timeval: float | int,
17    centi: bool = True,
18) -> babase.Lstr:
19    """Generate a babase.Lstr for displaying a time value.
20
21    Category: **General Utility Functions**
22
23    Given a time value, returns a babase.Lstr with:
24    (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
25
26    WARNING: the underlying Lstr value is somewhat large so don't use this
27    to rapidly update Node text values for an onscreen timer or you may
28    consume significant network bandwidth.  For that purpose you should
29    use a 'timedisplay' Node and attribute connections.
30
31    """
32    from babase._language import Lstr
33
34    # We take float seconds but operate on int milliseconds internally.
35    timeval = int(1000 * timeval)
36    bits = []
37    subs = []
38    hval = (timeval // 1000) // (60 * 60)
39    if hval != 0:
40        bits.append('${H}')
41        subs.append(
42            (
43                '${H}',
44                Lstr(
45                    resource='timeSuffixHoursText',
46                    subs=[('${COUNT}', str(hval))],
47                ),
48            )
49        )
50    mval = ((timeval // 1000) // 60) % 60
51    if mval != 0:
52        bits.append('${M}')
53        subs.append(
54            (
55                '${M}',
56                Lstr(
57                    resource='timeSuffixMinutesText',
58                    subs=[('${COUNT}', str(mval))],
59                ),
60            )
61        )
62
63    # We add seconds if its non-zero *or* we haven't added anything else.
64    if centi:
65        # pylint: disable=consider-using-f-string
66        sval = timeval / 1000.0 % 60.0
67        if sval >= 0.005 or not bits:
68            bits.append('${S}')
69            subs.append(
70                (
71                    '${S}',
72                    Lstr(
73                        resource='timeSuffixSecondsText',
74                        subs=[('${COUNT}', ('%.2f' % sval))],
75                    ),
76                )
77            )
78    else:
79        sval = timeval // 1000 % 60
80        if sval != 0 or not bits:
81            bits.append('${S}')
82            subs.append(
83                (
84                    '${S}',
85                    Lstr(
86                        resource='timeSuffixSecondsText',
87                        subs=[('${COUNT}', str(sval))],
88                    ),
89                )
90            )
91    return Lstr(value=' '.join(bits), subs=subs)

Generate a babase.Lstr for displaying a time value.

Category: General Utility Functions

Given a time value, returns a babase.Lstr with: (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).

WARNING: the underlying Lstr value is somewhat large so don't use this to rapidly update Node text values for an onscreen timer or you may consume significant network bandwidth. For that purpose you should use a 'timedisplay' Node and attribute connections.

class UIScale(enum.Enum):
63class UIScale(Enum):
64    """The overall scale the UI is being rendered for. Note that this is
65    independent of pixel resolution. For example, a phone and a desktop PC
66    might render the game at similar pixel resolutions but the size they
67    display content at will vary significantly.
68
69    Category: Enums
70
71    'large' is used for devices such as desktop PCs where fine details can
72       be clearly seen. UI elements are generally smaller on the screen
73       and more content can be seen at once.
74
75    'medium' is used for devices such as tablets, TVs, or VR headsets.
76       This mode strikes a balance between clean readability and amount of
77       content visible.
78
79    'small' is used primarily for phones or other small devices where
80       content needs to be presented as large and clear in order to remain
81       readable from an average distance.
82    """
83
84    LARGE = 0
85    MEDIUM = 1
86    SMALL = 2

The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.

Category: Enums

'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.

'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.

'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.

LARGE = <UIScale.LARGE: 0>
MEDIUM = <UIScale.MEDIUM: 1>
SMALL = <UIScale.SMALL: 2>
Inherited Members
enum.Enum
name
value
UNHANDLED = <bascenev1._messages._UnhandledType object>
class Vec3(typing.Sequence[float]):
394class Vec3(Sequence[float]):
395    """A vector of 3 floats.
396
397    Category: **General Utility Classes**
398
399    These can be created the following ways (checked in this order):
400    - with no args, all values are set to 0
401    - with a single numeric arg, all values are set to that value
402    - with a single three-member sequence arg, sequence values are copied
403    - otherwise assumes individual x/y/z args (positional or keywords)
404    """
405
406    x: float
407    """The vector's X component."""
408
409    y: float
410    """The vector's Y component."""
411
412    z: float
413    """The vector's Z component."""
414
415    # pylint: disable=function-redefined
416
417    @overload
418    def __init__(self) -> None:
419        pass
420
421    @overload
422    def __init__(self, value: float):
423        pass
424
425    @overload
426    def __init__(self, values: Sequence[float]):
427        pass
428
429    @overload
430    def __init__(self, x: float, y: float, z: float):
431        pass
432
433    def __init__(self, *args: Any, **kwds: Any):
434        pass
435
436    def __add__(self, other: Vec3) -> Vec3:
437        return self
438
439    def __sub__(self, other: Vec3) -> Vec3:
440        return self
441
442    @overload
443    def __mul__(self, other: float) -> Vec3:
444        return self
445
446    @overload
447    def __mul__(self, other: Sequence[float]) -> Vec3:
448        return self
449
450    def __mul__(self, other: Any) -> Any:
451        return self
452
453    @overload
454    def __rmul__(self, other: float) -> Vec3:
455        return self
456
457    @overload
458    def __rmul__(self, other: Sequence[float]) -> Vec3:
459        return self
460
461    def __rmul__(self, other: Any) -> Any:
462        return self
463
464    # (for index access)
465    @override
466    def __getitem__(self, typeargs: Any) -> Any:
467        return 0.0
468
469    @override
470    def __len__(self) -> int:
471        return 3
472
473    # (for iterator access)
474    @override
475    def __iter__(self) -> Any:
476        return self
477
478    def __next__(self) -> float:
479        return 0.0
480
481    def __neg__(self) -> Vec3:
482        return self
483
484    def __setitem__(self, index: int, val: float) -> None:
485        pass
486
487    def cross(self, other: Vec3) -> Vec3:
488        """Returns the cross product of this vector and another."""
489        return Vec3()
490
491    def dot(self, other: Vec3) -> float:
492        """Returns the dot product of this vector and another."""
493        return float()
494
495    def length(self) -> float:
496        """Returns the length of the vector."""
497        return float()
498
499    def normalized(self) -> Vec3:
500        """Returns a normalized version of the vector."""
501        return Vec3()

A vector of 3 floats.

Category: General Utility Classes

These can be created the following ways (checked in this order):

  • with no args, all values are set to 0
  • with a single numeric arg, all values are set to that value
  • with a single three-member sequence arg, sequence values are copied
  • otherwise assumes individual x/y/z args (positional or keywords)
Vec3(*args: Any, **kwds: Any)
433    def __init__(self, *args: Any, **kwds: Any):
434        pass
x: float

The vector's X component.

y: float

The vector's Y component.

z: float

The vector's Z component.

def cross(self, other: Vec3) -> Vec3:
487    def cross(self, other: Vec3) -> Vec3:
488        """Returns the cross product of this vector and another."""
489        return Vec3()

Returns the cross product of this vector and another.

def dot(self, other: Vec3) -> float:
491    def dot(self, other: Vec3) -> float:
492        """Returns the dot product of this vector and another."""
493        return float()

Returns the dot product of this vector and another.

def length(self) -> float:
495    def length(self) -> float:
496        """Returns the length of the vector."""
497        return float()

Returns the length of the vector.

def normalized(self) -> Vec3:
499    def normalized(self) -> Vec3:
500        """Returns a normalized version of the vector."""
501        return Vec3()

Returns a normalized version of the vector.

Inherited Members
collections.abc.Sequence
index
count
WeakCall = <class 'babase._general._WeakCall'>