bascenev1

Gameplay-centric api for classic BombSquad.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Gameplay-centric api for classic BombSquad."""
  4
  5# ba_meta require api 9
  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    add_clean_frame_callback,
 22    app,
 23    AppIntent,
 24    AppIntentDefault,
 25    AppIntentExec,
 26    AppMode,
 27    apptime,
 28    AppTime,
 29    apptimer,
 30    AppTimer,
 31    Call,
 32    ContextError,
 33    ContextRef,
 34    displaytime,
 35    DisplayTime,
 36    displaytimer,
 37    DisplayTimer,
 38    existing,
 39    fade_screen,
 40    get_remote_app_name,
 41    increment_analytics_count,
 42    InputType,
 43    is_point_in_box,
 44    lock_all_input,
 45    Lstr,
 46    NodeNotFoundError,
 47    normalized_color,
 48    NotFoundError,
 49    PlayerNotFoundError,
 50    Plugin,
 51    pushcall,
 52    safecolor,
 53    screenmessage,
 54    set_analytics_screen,
 55    storagename,
 56    timestring,
 57    UIScale,
 58    unlock_all_input,
 59    Vec3,
 60    WeakCall,
 61)
 62
 63from _bascenev1 import (
 64    ActivityData,
 65    basetime,
 66    basetimer,
 67    BaseTimer,
 68    camerashake,
 69    capture_gamepad_input,
 70    capture_keyboard_input,
 71    chatmessage,
 72    client_info_query_response,
 73    CollisionMesh,
 74    connect_to_party,
 75    Data,
 76    disconnect_client,
 77    disconnect_from_host,
 78    emitfx,
 79    end_host_scanning,
 80    get_chat_messages,
 81    get_connection_to_host_info,
 82    get_connection_to_host_info_2,
 83    get_foreground_host_activity,
 84    get_foreground_host_session,
 85    get_game_port,
 86    get_game_roster,
 87    get_local_active_input_devices_count,
 88    get_public_party_enabled,
 89    get_public_party_max_size,
 90    get_random_names,
 91    get_replay_speed_exponent,
 92    get_ui_input_device,
 93    getactivity,
 94    getcollisionmesh,
 95    getdata,
 96    getinputdevice,
 97    getmesh,
 98    getnodes,
 99    getsession,
100    getsound,
101    gettexture,
102    have_connected_clients,
103    have_touchscreen_input,
104    host_scan_cycle,
105    InputDevice,
106    is_in_replay,
107    is_replay_paused,
108    ls_input_devices,
109    ls_objects,
110    Material,
111    Mesh,
112    new_host_session,
113    new_replay_session,
114    newactivity,
115    newnode,
116    Node,
117    pause_replay,
118    printnodes,
119    protocol_version,
120    release_gamepad_input,
121    release_keyboard_input,
122    reset_random_player_names,
123    resume_replay,
124    seek_replay,
125    broadcastmessage,
126    SessionData,
127    SessionPlayer,
128    set_admins,
129    set_authenticate_clients,
130    set_debug_speed_exponent,
131    set_enable_default_kick_voting,
132    set_internal_music,
133    set_map_bounds,
134    set_master_server_source,
135    set_public_party_enabled,
136    set_public_party_max_size,
137    set_public_party_name,
138    set_public_party_public_address_ipv4,
139    set_public_party_public_address_ipv6,
140    set_public_party_queue_enabled,
141    set_public_party_stats_url,
142    set_replay_speed_exponent,
143    set_touchscreen_editing,
144    Sound,
145    Texture,
146    time,
147    timer,
148    Timer,
149)
150from bascenev1._activity import Activity
151from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity
152from bascenev1._actor import Actor
153from bascenev1._campaign import init_campaigns, Campaign
154from bascenev1._collision import Collision, getcollision
155from bascenev1._coopgame import CoopGameActivity
156from bascenev1._coopsession import CoopSession
157from bascenev1._debug import print_live_object_warnings
158from bascenev1._dependency import (
159    Dependency,
160    DependencyComponent,
161    DependencySet,
162    AssetPackage,
163)
164from bascenev1._dualteamsession import DualTeamSession
165from bascenev1._freeforallsession import FreeForAllSession
166from bascenev1._gameactivity import GameActivity
167from bascenev1._gameresults import GameResults
168from bascenev1._gameutils import (
169    animate,
170    animate_array,
171    BaseTime,
172    cameraflash,
173    GameTip,
174    get_trophy_string,
175    show_damage_count,
176    Time,
177)
178from bascenev1._level import Level
179from bascenev1._lobby import Lobby, Chooser
180from bascenev1._map import (
181    get_filtered_map_name,
182    get_map_class,
183    get_map_display_string,
184    Map,
185    register_map,
186)
187from bascenev1._messages import (
188    CelebrateMessage,
189    DeathType,
190    DieMessage,
191    DropMessage,
192    DroppedMessage,
193    FreezeMessage,
194    HitMessage,
195    ImpactDamageMessage,
196    OutOfBoundsMessage,
197    PickedUpMessage,
198    PickUpMessage,
199    PlayerDiedMessage,
200    PlayerProfilesChangedMessage,
201    ShouldShatterMessage,
202    StandMessage,
203    ThawMessage,
204    UNHANDLED,
205)
206from bascenev1._multiteamsession import (
207    MultiTeamSession,
208    DEFAULT_TEAM_COLORS,
209    DEFAULT_TEAM_NAMES,
210)
211from bascenev1._music import MusicType, setmusic
212from bascenev1._net import HostInfo
213from bascenev1._nodeactor import NodeActor
214from bascenev1._powerup import get_default_powerup_distribution
215from bascenev1._profile import (
216    get_player_colors,
217    get_player_profile_icon,
218    get_player_profile_colors,
219)
220from bascenev1._player import PlayerInfo, Player, EmptyPlayer, StandLocation
221from bascenev1._playlist import (
222    get_default_free_for_all_playlist,
223    get_default_teams_playlist,
224    filter_playlist,
225)
226from bascenev1._powerup import PowerupMessage, PowerupAcceptMessage
227from bascenev1._score import ScoreType, ScoreConfig
228from bascenev1._settings import (
229    BoolSetting,
230    ChoiceSetting,
231    FloatChoiceSetting,
232    FloatSetting,
233    IntChoiceSetting,
234    IntSetting,
235    Setting,
236)
237from bascenev1._session import (
238    Session,
239    set_player_rejoin_cooldown,
240    set_max_players_override,
241)
242from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats
243from bascenev1._team import SessionTeam, Team, EmptyTeam
244from bascenev1._teamgame import TeamGameActivity
245
246__all__ = [
247    'Activity',
248    'ActivityData',
249    'Actor',
250    'animate',
251    'animate_array',
252    'add_clean_frame_callback',
253    'app',
254    'AppIntent',
255    'AppIntentDefault',
256    'AppIntentExec',
257    'AppMode',
258    'AppTime',
259    'apptime',
260    'apptimer',
261    'AppTimer',
262    'AssetPackage',
263    'basetime',
264    'BaseTime',
265    'basetimer',
266    'BaseTimer',
267    'BoolSetting',
268    'Call',
269    'cameraflash',
270    'camerashake',
271    'Campaign',
272    'capture_gamepad_input',
273    'capture_keyboard_input',
274    'CelebrateMessage',
275    'chatmessage',
276    'ChoiceSetting',
277    'Chooser',
278    'client_info_query_response',
279    'Collision',
280    'CollisionMesh',
281    'connect_to_party',
282    'ContextError',
283    'ContextRef',
284    'CoopGameActivity',
285    'CoopSession',
286    'Data',
287    'DeathType',
288    'DEFAULT_TEAM_COLORS',
289    'DEFAULT_TEAM_NAMES',
290    'Dependency',
291    'DependencyComponent',
292    'DependencySet',
293    'DieMessage',
294    'disconnect_client',
295    'disconnect_from_host',
296    'displaytime',
297    'DisplayTime',
298    'displaytimer',
299    'DisplayTimer',
300    'DropMessage',
301    'DroppedMessage',
302    'DualTeamSession',
303    'emitfx',
304    'EmptyPlayer',
305    'EmptyTeam',
306    'end_host_scanning',
307    'existing',
308    'fade_screen',
309    'filter_playlist',
310    'FloatChoiceSetting',
311    'FloatSetting',
312    'FreeForAllSession',
313    'FreezeMessage',
314    'GameActivity',
315    'GameResults',
316    'GameTip',
317    'get_chat_messages',
318    'get_connection_to_host_info',
319    'get_connection_to_host_info_2',
320    'get_default_free_for_all_playlist',
321    'get_default_teams_playlist',
322    'get_default_powerup_distribution',
323    'get_filtered_map_name',
324    'get_foreground_host_activity',
325    'get_foreground_host_session',
326    'get_game_port',
327    'get_game_roster',
328    'get_game_roster',
329    'get_local_active_input_devices_count',
330    'get_map_class',
331    'get_map_display_string',
332    'get_player_colors',
333    'get_player_profile_colors',
334    'get_player_profile_icon',
335    'get_public_party_enabled',
336    'get_public_party_max_size',
337    'get_random_names',
338    'get_remote_app_name',
339    'get_replay_speed_exponent',
340    'get_trophy_string',
341    'get_ui_input_device',
342    'getactivity',
343    'getcollision',
344    'getcollisionmesh',
345    'getdata',
346    'getinputdevice',
347    'getmesh',
348    'getnodes',
349    'getsession',
350    'getsound',
351    'gettexture',
352    'have_connected_clients',
353    'have_touchscreen_input',
354    'HitMessage',
355    'HostInfo',
356    'host_scan_cycle',
357    'ImpactDamageMessage',
358    'increment_analytics_count',
359    'init_campaigns',
360    'InputDevice',
361    'InputType',
362    'IntChoiceSetting',
363    'IntSetting',
364    'is_in_replay',
365    'is_point_in_box',
366    'is_replay_paused',
367    'JoinActivity',
368    'Level',
369    'Lobby',
370    'lock_all_input',
371    'ls_input_devices',
372    'ls_objects',
373    'Lstr',
374    'Map',
375    'Material',
376    'Mesh',
377    'MultiTeamSession',
378    'MusicType',
379    'new_host_session',
380    'new_replay_session',
381    'newactivity',
382    'newnode',
383    'Node',
384    'NodeActor',
385    'NodeNotFoundError',
386    'normalized_color',
387    'NotFoundError',
388    'OutOfBoundsMessage',
389    'pause_replay',
390    'PickedUpMessage',
391    'PickUpMessage',
392    'Player',
393    'PlayerDiedMessage',
394    'PlayerProfilesChangedMessage',
395    'PlayerInfo',
396    'PlayerNotFoundError',
397    'PlayerRecord',
398    'PlayerScoredMessage',
399    'Plugin',
400    'PowerupAcceptMessage',
401    'PowerupMessage',
402    'print_live_object_warnings',
403    'printnodes',
404    'protocol_version',
405    'pushcall',
406    'register_map',
407    'release_gamepad_input',
408    'release_keyboard_input',
409    'reset_random_player_names',
410    'resume_replay',
411    'seek_replay',
412    'safecolor',
413    'screenmessage',
414    'ScoreConfig',
415    'ScoreScreenActivity',
416    'ScoreType',
417    'broadcastmessage',
418    'Session',
419    'SessionData',
420    'SessionPlayer',
421    'SessionTeam',
422    'set_admins',
423    'set_analytics_screen',
424    'set_authenticate_clients',
425    'set_debug_speed_exponent',
426    'set_debug_speed_exponent',
427    'set_enable_default_kick_voting',
428    'set_internal_music',
429    'set_map_bounds',
430    'set_master_server_source',
431    'set_public_party_enabled',
432    'set_public_party_max_size',
433    'set_public_party_name',
434    'set_public_party_public_address_ipv4',
435    'set_public_party_public_address_ipv6',
436    'set_public_party_queue_enabled',
437    'set_public_party_stats_url',
438    'set_player_rejoin_cooldown',
439    'set_max_players_override',
440    'set_replay_speed_exponent',
441    'set_touchscreen_editing',
442    'setmusic',
443    'Setting',
444    'ShouldShatterMessage',
445    'show_damage_count',
446    'Sound',
447    'StandLocation',
448    'StandMessage',
449    'Stats',
450    'storagename',
451    'Team',
452    'TeamGameActivity',
453    'Texture',
454    'ThawMessage',
455    'time',
456    'Time',
457    'timer',
458    'Timer',
459    'timestring',
460    'UIScale',
461    'UNHANDLED',
462    'unlock_all_input',
463    'Vec3',
464    'WeakCall',
465]
466
467# We want stuff here to show up as bascenev1.Foo instead of
468# bascenev1._submodule.Foo.
469set_canonical_module_names(globals())
470
471# Sanity check: we want to keep ballistica's dependencies and
472# bootstrapping order clearly defined; let's check a few particular
473# modules to make sure they never directly or indirectly import us
474# before their own execs complete.
475if __debug__:
476    for _mdl in 'babase', '_babase':
477        if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
478            logging.warning(
479                '%s was imported before %s finished importing;'
480                ' should not happen.',
481                __name__,
482                _mdl,
483            )
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: _babase.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: _bascenev1.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: _bascenev1.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: _bascenev1.Node, attr: str, keys: dict[float, float], loop: bool = False, offset: float = 0) -> _bascenev1.Node:
 52def animate(
 53    node: bascenev1.Node,
 54    attr: str,
 55    keys: dict[float, float],
 56    loop: bool = False,
 57    offset: float = 0,
 58) -> bascenev1.Node:
 59    """Animate values on a target bascenev1.Node.
 60
 61    Category: **Gameplay Functions**
 62
 63    Creates an 'animcurve' node with the provided values and time as an input,
 64    connect it to the provided attribute, and set it to die with the target.
 65    Key values are provided as time:value dictionary pairs.  Time values are
 66    relative to the current time. By default, times are specified in seconds,
 67    but timeformat can also be set to MILLISECONDS to recreate the old behavior
 68    (prior to ba 1.5) of taking milliseconds. Returns the animcurve node.
 69    """
 70    items = list(keys.items())
 71    items.sort()
 72
 73    curve = _bascenev1.newnode(
 74        'animcurve',
 75        owner=node,
 76        name='Driving ' + str(node) + ' \'' + attr + '\'',
 77    )
 78
 79    # We take seconds but operate on milliseconds internally.
 80    mult = 1000
 81
 82    curve.times = [int(mult * time) for time, val in items]
 83    curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
 84    curve.values = [val for time, val in items]
 85    curve.loop = loop
 86
 87    # If we're not looping, set a timer to kill this curve
 88    # after its done its job.
 89    # FIXME: Even if we are looping we should have a way to die once we
 90    #  get disconnected.
 91    if not loop:
 92        # noinspection PyUnresolvedReferences
 93        _bascenev1.timer(
 94            (int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete
 95        )
 96
 97    # Do the connects last so all our attrs are in place when we push initial
 98    # values through.
 99
100    # We operate in either activities or sessions..
101    try:
102        globalsnode = _bascenev1.getactivity().globalsnode
103    except babase.ActivityNotFoundError:
104        globalsnode = _bascenev1.getsession().sessionglobalsnode
105
106    globalsnode.connectattr('time', curve, 'in')
107    curve.connectattr('out', node, attr)
108    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: _bascenev1.Node, attr: str, size: int, keys: dict[float, typing.Sequence[float]], *, loop: bool = False, offset: float = 0) -> None:
111def animate_array(
112    node: bascenev1.Node,
113    attr: str,
114    size: int,
115    keys: dict[float, Sequence[float]],
116    *,
117    loop: bool = False,
118    offset: float = 0,
119) -> None:
120    """Animate an array of values on a target bascenev1.Node.
121
122    Category: **Gameplay Functions**
123
124    Like bs.animate, but operates on array attributes.
125    """
126    combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size})
127    items = list(keys.items())
128    items.sort()
129
130    # We take seconds but operate on milliseconds internally.
131    mult = 1000
132
133    # We operate in either activities or sessions..
134    try:
135        globalsnode = _bascenev1.getactivity().globalsnode
136    except babase.ActivityNotFoundError:
137        globalsnode = _bascenev1.getsession().sessionglobalsnode
138
139    for i in range(size):
140        curve = _bascenev1.newnode(
141            'animcurve',
142            owner=node,
143            name=(
144                'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
145            ),
146        )
147        globalsnode.connectattr('time', curve, 'in')
148        curve.times = [int(mult * time) for time, val in items]
149        curve.values = [val[i] for time, val in items]
150        curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
151        curve.loop = loop
152        curve.connectattr('out', combine, 'input' + str(i))
153
154        # If we're not looping, set a timer to kill this
155        # curve after its done its job.
156        if not loop:
157            # (PyCharm seems to think item is a float, not a tuple)
158            # noinspection PyUnresolvedReferences
159            _bascenev1.timer(
160                (int(mult * items[-1][0]) + 1000) / 1000.0,
161                curve.delete,
162            )
163    combine.connectattr('output', node, attr)
164
165    # If we're not looping, set a timer to kill the combine once
166    # the job is done.
167    # FIXME: Even if we are looping we should have a way to die
168    #  once we get disconnected.
169    if not loop:
170        # (PyCharm seems to think item is a float, not a tuple)
171        # noinspection PyUnresolvedReferences
172        _bascenev1.timer(
173            (int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete
174        )

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

Category: Gameplay Functions

Like bs.animate, but operates on array attributes.

app = <babase.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        # TODO: 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 ba*.app.active changes while this mode is active.
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        # TODO: 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 ba*.app.active changes while this mode is active.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

Called when ba*.app.active changes while this mode is active.

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

AppTime = AppTime
def apptime() -> AppTime:
552def apptime() -> babase.AppTime:
553    """Return the current app-time in seconds.
554
555    Category: **General Utility Functions**
556
557    App-time is a monotonic time value; it starts at 0.0 when the app
558    launches and will never jump by large amounts or go backwards, even if
559    the system time changes. Its progression will pause when the app is in
560    a suspended state.
561
562    Note that the AppTime returned here is simply float; it just has a
563    unique type in the type-checker's eyes to help prevent it from being
564    accidentally used with time functionality expecting other time types.
565    """
566    import babase  # pylint: disable=cyclic-import
567
568    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:
571def apptimer(time: float, call: Callable[[], Any]) -> None:
572    """Schedule a callable object to run based on app-time.
573
574    Category: **General Utility Functions**
575
576    This function creates a one-off timer which cannot be canceled or
577    modified once created. If you require the ability to do so, or need
578    a repeating timer, use the babase.AppTimer class instead.
579
580    ##### Arguments
581    ###### time (float)
582    > Length of time in seconds that the timer will wait before firing.
583
584    ###### call (Callable[[], Any])
585    > A callable Python object. Note that the timer will retain a
586    strong reference to the callable for as long as the timer exists, so you
587    may want to look into concepts such as babase.WeakCall if that is not
588    desired.
589
590    ##### Examples
591    Print some stuff through time:
592    >>> babase.screenmessage('hello from now!')
593    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
594                              'hello from the future!'))
595    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
596    ...                       'hello from the future 2!'))
597    """
598    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) -> _bascenev1.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) -> _bascenev1.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) -> _bascenev1.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) -> _bascenev1.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) -> _bascenev1.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
Call = <class 'babase._general._Call'>
def cameraflash(duration: float = 999.0) -> None:
240def cameraflash(duration: float = 999.0) -> None:
241    """Create a strobing camera flash effect.
242
243    Category: **Gameplay Functions**
244
245    (as seen when a team wins a game)
246    Duration is in seconds.
247    """
248    # pylint: disable=too-many-locals
249    from bascenev1._nodeactor import NodeActor
250
251    x_spread = 10
252    y_spread = 5
253    positions = [
254        [-x_spread, -y_spread],
255        [0, -y_spread],
256        [0, y_spread],
257        [x_spread, -y_spread],
258        [x_spread, y_spread],
259        [-x_spread, y_spread],
260    ]
261    times = [0, 2700, 1000, 1800, 500, 1400]
262
263    # Store this on the current activity so we only have one at a time.
264    # FIXME: Need a type safe way to do this.
265    activity = _bascenev1.getactivity()
266    activity.camera_flash_data = []  # type: ignore
267    for i in range(6):
268        light = NodeActor(
269            _bascenev1.newnode(
270                'light',
271                attrs={
272                    'position': (positions[i][0], 0, positions[i][1]),
273                    'radius': 1.0,
274                    'lights_volumes': False,
275                    'height_attenuated': False,
276                    'color': (0.2, 0.2, 0.8),
277                },
278            )
279        )
280        sval = 1.87
281        iscale = 1.3
282        tcombine = _bascenev1.newnode(
283            'combine',
284            owner=light.node,
285            attrs={
286                'size': 3,
287                'input0': positions[i][0],
288                'input1': 0,
289                'input2': positions[i][1],
290            },
291        )
292        assert light.node
293        tcombine.connectattr('output', light.node, 'position')
294        xval = positions[i][0]
295        yval = positions[i][1]
296        spd = 0.5 + random.random()
297        spd2 = 0.5 + random.random()
298        animate(
299            tcombine,
300            'input0',
301            {
302                0.0: xval + 0,
303                0.069 * spd: xval + 10.0,
304                0.143 * spd: xval - 10.0,
305                0.201 * spd: xval + 0,
306            },
307            loop=True,
308        )
309        animate(
310            tcombine,
311            'input2',
312            {
313                0.0: yval + 0,
314                0.15 * spd2: yval + 10.0,
315                0.287 * spd2: yval - 10.0,
316                0.398 * spd2: yval + 0,
317            },
318            loop=True,
319        )
320        animate(
321            light.node,
322            'intensity',
323            {
324                0.0: 0,
325                0.02 * sval: 0,
326                0.05 * sval: 0.8 * iscale,
327                0.08 * sval: 0,
328                0.1 * sval: 0,
329            },
330            loop=True,
331            offset=times[i],
332        )
333        _bascenev1.timer(
334            (times[i] + random.randint(1, int(duration)) * 40 * sval) / 1000.0,
335            light.node.delete,
336        )
337        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]]
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
593                classic.profile_browser_window()
594
595                # Give their input-device UI ownership too (prevent
596                # someone else from snatching it in crowded games).
597                babase.set_ui_input_device(self._sessionplayer.inputdevice.id)
598            return
599
600        if not ready:
601            self._sessionplayer.assigninput(
602                babase.InputType.LEFT_PRESS,
603                babase.Call(self.handlemessage, ChangeMessage('team', -1)),
604            )
605            self._sessionplayer.assigninput(
606                babase.InputType.RIGHT_PRESS,
607                babase.Call(self.handlemessage, ChangeMessage('team', 1)),
608            )
609            self._sessionplayer.assigninput(
610                babase.InputType.BOMB_PRESS,
611                babase.Call(self.handlemessage, ChangeMessage('character', 1)),
612            )
613            self._sessionplayer.assigninput(
614                babase.InputType.UP_PRESS,
615                babase.Call(
616                    self.handlemessage, ChangeMessage('profileindex', -1)
617                ),
618            )
619            self._sessionplayer.assigninput(
620                babase.InputType.DOWN_PRESS,
621                babase.Call(
622                    self.handlemessage, ChangeMessage('profileindex', 1)
623                ),
624            )
625            self._sessionplayer.assigninput(
626                (
627                    babase.InputType.JUMP_PRESS,
628                    babase.InputType.PICK_UP_PRESS,
629                    babase.InputType.PUNCH_PRESS,
630                ),
631                babase.Call(self.handlemessage, ChangeMessage('ready', 1)),
632            )
633            self._ready = False
634            self._update_text()
635            self._sessionplayer.setname('untitled', real=False)
636        else:
637            self._sessionplayer.assigninput(
638                (
639                    babase.InputType.LEFT_PRESS,
640                    babase.InputType.RIGHT_PRESS,
641                    babase.InputType.UP_PRESS,
642                    babase.InputType.DOWN_PRESS,
643                    babase.InputType.JUMP_PRESS,
644                    babase.InputType.BOMB_PRESS,
645                    babase.InputType.PICK_UP_PRESS,
646                ),
647                self._do_nothing,
648            )
649            self._sessionplayer.assigninput(
650                (
651                    babase.InputType.JUMP_PRESS,
652                    babase.InputType.BOMB_PRESS,
653                    babase.InputType.PICK_UP_PRESS,
654                    babase.InputType.PUNCH_PRESS,
655                ),
656                babase.Call(self.handlemessage, ChangeMessage('ready', 0)),
657            )
658
659            # Store the last profile picked by this input for reuse.
660            input_device = self._sessionplayer.inputdevice
661            name = input_device.name
662            unique_id = input_device.unique_identifier
663            device_profiles = babase.app.config.setdefault(
664                'Default Player Profiles', {}
665            )
666
667            # Make an exception if we have no custom profiles and are set
668            # to random; in that case we'll want to start picking up custom
669            # profiles if/when one is made so keep our setting cleared.
670            special = ('_random', '_edit', '__account__')
671            have_custom_profiles = any(p not in special for p in self._profiles)
672
673            profilekey = name + ' ' + unique_id
674            if profilename == '_random' and not have_custom_profiles:
675                if profilekey in device_profiles:
676                    del device_profiles[profilekey]
677            else:
678                device_profiles[profilekey] = profilename
679            babase.app.config.commit()
680
681            # Set this player's short and full name.
682            self._sessionplayer.setname(
683                self._getname(), self._getname(full=True), real=True
684            )
685            self._ready = True
686            self._update_text()
687
688            # Inform the session that this player is ready.
689            _bascenev1.getsession().handlemessage(PlayerReadyMessage(self))
690
691    def _handle_ready_msg(self, ready: bool) -> None:
692        force_team_switch = False
693
694        # Team auto-balance kicks us to another team if we try to
695        # join the team with the most players.
696        if not self._ready:
697            if babase.app.config.get('Auto Balance Teams', False):
698                lobby = self.lobby
699                sessionteams = lobby.sessionteams
700                if len(sessionteams) > 1:
701                    # First, calc how many players are on each team
702                    # ..we need to count both active players and
703                    # choosers that have been marked as ready.
704                    team_player_counts = {}
705                    for sessionteam in sessionteams:
706                        team_player_counts[sessionteam.id] = len(
707                            sessionteam.players
708                        )
709                    for chooser in lobby.choosers:
710                        if chooser.ready:
711                            team_player_counts[chooser.sessionteam.id] += 1
712                    largest_team_size = max(team_player_counts.values())
713                    smallest_team_size = min(team_player_counts.values())
714
715                    # Force switch if we're on the biggest sessionteam
716                    # and there's a smaller one available.
717                    if (
718                        largest_team_size != smallest_team_size
719                        and team_player_counts[self.sessionteam.id]
720                        >= largest_team_size
721                    ):
722                        force_team_switch = True
723
724        # Either force switch teams, or actually for realsies do the set-ready.
725        if force_team_switch:
726            self._errorsound.play()
727            self.handlemessage(ChangeMessage('team', 1))
728        else:
729            self._punchsound.play()
730            self._set_ready(ready)
731
732    # TODO: should handle this at the engine layer so this is unnecessary.
733    def _handle_repeat_message_attack(self) -> None:
734        now = babase.apptime()
735        count = self._last_change[1]
736        if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
737            count += 1
738            if count > MAX_QUICK_CHANGE_COUNT:
739                _bascenev1.disconnect_client(
740                    self._sessionplayer.inputdevice.client_id
741                )
742        elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
743            count = 0
744        self._last_change = (now, count)
745
746    def handlemessage(self, msg: Any) -> Any:
747        """Standard generic message handler."""
748
749        if isinstance(msg, ChangeMessage):
750            self._handle_repeat_message_attack()
751
752            # If we've been removed from the lobby, ignore this stuff.
753            if self._dead:
754                logging.error('chooser got ChangeMessage after dying')
755                return
756
757            if not self._text_node:
758                logging.error('got ChangeMessage after nodes died')
759                return
760
761            if msg.what == 'team':
762                sessionteams = self.lobby.sessionteams
763                if len(sessionteams) > 1:
764                    self._swish_sound.play()
765                self._selected_team_index = (
766                    self._selected_team_index + msg.value
767                ) % len(sessionteams)
768                self._update_text()
769                self.update_position()
770                self._update_icon()
771
772            elif msg.what == 'profileindex':
773                if len(self._profilenames) == 1:
774                    # This should be pretty hard to hit now with
775                    # automatic local accounts.
776                    _bascenev1.getsound('error').play()
777                else:
778                    # Pick the next player profile and assign our name
779                    # and character based on that.
780                    self._deek_sound.play()
781                    self._profileindex = (self._profileindex + msg.value) % len(
782                        self._profilenames
783                    )
784                    self.update_from_profile()
785
786            elif msg.what == 'character':
787                self._click_sound.play()
788                # update our index in our local list of characters
789                self._character_index = (
790                    self._character_index + msg.value
791                ) % len(self._character_names)
792                self._update_text()
793                self._update_icon()
794
795            elif msg.what == 'ready':
796                self._handle_ready_msg(bool(msg.value))
797
798    def _update_text(self) -> None:
799        assert self._text_node is not None
800        if self._ready:
801            # Once we're ready, we've saved the name, so lets ask the system
802            # for it so we get appended numbers and stuff.
803            text = babase.Lstr(value=self._sessionplayer.getname(full=True))
804            text = babase.Lstr(
805                value='${A} (${B})',
806                subs=[
807                    ('${A}', text),
808                    ('${B}', babase.Lstr(resource='readyText')),
809                ],
810            )
811        else:
812            text = babase.Lstr(value=self._getname(full=True))
813
814        can_switch_teams = len(self.lobby.sessionteams) > 1
815
816        # Flash as we're coming in.
817        fin_color = babase.safecolor(self.get_color()) + (1,)
818        if not self._inited:
819            animate_array(
820                self._text_node,
821                'color',
822                4,
823                {0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
824            )
825        else:
826            # Blend if we're in teams mode; switch instantly otherwise.
827            if can_switch_teams:
828                animate_array(
829                    self._text_node,
830                    'color',
831                    4,
832                    {0: self._text_node.color, 0.1: fin_color},
833                )
834            else:
835                self._text_node.color = fin_color
836
837        self._text_node.text = text
838
839    def get_color(self) -> Sequence[float]:
840        """Return the currently selected color."""
841        val: Sequence[float]
842        if self.lobby.use_team_colors:
843            val = self.lobby.sessionteams[self._selected_team_index].color
844        else:
845            val = self._color
846        if len(val) != 3:
847            print('get_color: ignoring invalid color of len', len(val))
848            val = (0, 1, 0)
849        return val
850
851    def get_highlight(self) -> Sequence[float]:
852        """Return the currently selected highlight."""
853        if self._profilenames[self._profileindex] == '_edit':
854            return 0, 1, 0
855
856        # If we're using team colors we wanna make sure our highlight color
857        # isn't too close to any other team's color.
858        highlight = list(self._highlight)
859        if self.lobby.use_team_colors:
860            for i, sessionteam in enumerate(self.lobby.sessionteams):
861                if i != self._selected_team_index:
862                    # Find the dominant component of this sessionteam's color
863                    # and adjust ours so that the component is
864                    # not super-dominant.
865                    max_val = 0.0
866                    max_index = 0
867                    for j in range(3):
868                        if sessionteam.color[j] > max_val:
869                            max_val = sessionteam.color[j]
870                            max_index = j
871                    that_color_for_us = highlight[max_index]
872                    our_second_biggest = max(
873                        highlight[(max_index + 1) % 3],
874                        highlight[(max_index + 2) % 3],
875                    )
876                    diff = that_color_for_us - our_second_biggest
877                    if diff > 0:
878                        highlight[max_index] -= diff * 0.6
879                        highlight[(max_index + 1) % 3] += diff * 0.3
880                        highlight[(max_index + 2) % 3] += diff * 0.2
881        return highlight
882
883    def getplayer(self) -> bascenev1.SessionPlayer:
884        """Return the player associated with this chooser."""
885        return self._sessionplayer
886
887    def _update_icon(self) -> None:
888        assert babase.app.classic is not None
889        if self._profilenames[self._profileindex] == '_edit':
890            tex = _bascenev1.gettexture('black')
891            tint_tex = _bascenev1.gettexture('black')
892            self.icon.color = (1, 1, 1)
893            self.icon.texture = tex
894            self.icon.tint_texture = tint_tex
895            self.icon.tint_color = (0, 1, 0)
896            return
897
898        try:
899            tex_name = babase.app.classic.spaz_appearances[
900                self._character_names[self._character_index]
901            ].icon_texture
902            tint_tex_name = babase.app.classic.spaz_appearances[
903                self._character_names[self._character_index]
904            ].icon_mask_texture
905        except Exception:
906            logging.exception('Error updating char icon list')
907            tex_name = 'neoSpazIcon'
908            tint_tex_name = 'neoSpazIconColorMask'
909
910        tex = _bascenev1.gettexture(tex_name)
911        tint_tex = _bascenev1.gettexture(tint_tex_name)
912
913        self.icon.color = (1, 1, 1)
914        self.icon.texture = tex
915        self.icon.tint_texture = tint_tex
916        clr = self.get_color()
917        clr2 = self.get_highlight()
918
919        can_switch_teams = len(self.lobby.sessionteams) > 1
920
921        # If we're initing, flash.
922        if not self._inited:
923            animate_array(
924                self.icon,
925                'color',
926                3,
927                {0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)},
928            )
929
930        # Blend in teams mode; switch instantly in ffa-mode.
931        if can_switch_teams:
932            animate_array(
933                self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
934            )
935        else:
936            self.icon.tint_color = clr
937        self.icon.tint2_color = clr2
938
939        # Store the icon info the the player.
940        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: _bascenev1.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: _bascenev1.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:
746    def handlemessage(self, msg: Any) -> Any:
747        """Standard generic message handler."""
748
749        if isinstance(msg, ChangeMessage):
750            self._handle_repeat_message_attack()
751
752            # If we've been removed from the lobby, ignore this stuff.
753            if self._dead:
754                logging.error('chooser got ChangeMessage after dying')
755                return
756
757            if not self._text_node:
758                logging.error('got ChangeMessage after nodes died')
759                return
760
761            if msg.what == 'team':
762                sessionteams = self.lobby.sessionteams
763                if len(sessionteams) > 1:
764                    self._swish_sound.play()
765                self._selected_team_index = (
766                    self._selected_team_index + msg.value
767                ) % len(sessionteams)
768                self._update_text()
769                self.update_position()
770                self._update_icon()
771
772            elif msg.what == 'profileindex':
773                if len(self._profilenames) == 1:
774                    # This should be pretty hard to hit now with
775                    # automatic local accounts.
776                    _bascenev1.getsound('error').play()
777                else:
778                    # Pick the next player profile and assign our name
779                    # and character based on that.
780                    self._deek_sound.play()
781                    self._profileindex = (self._profileindex + msg.value) % len(
782                        self._profilenames
783                    )
784                    self.update_from_profile()
785
786            elif msg.what == 'character':
787                self._click_sound.play()
788                # update our index in our local list of characters
789                self._character_index = (
790                    self._character_index + msg.value
791                ) % len(self._character_names)
792                self._update_text()
793                self._update_icon()
794
795            elif msg.what == 'ready':
796                self._handle_ready_msg(bool(msg.value))

Standard generic message handler.

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

Return the currently selected color.

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

Return the currently selected highlight.

def getplayer(self) -> _bascenev1.SessionPlayer:
883    def getplayer(self) -> bascenev1.SessionPlayer:
884        """Return the player associated with this chooser."""
885        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: _babase.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: _bascenev1.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: _bascenev1.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.

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

Common base class for all game bascenev1.Activities.

Category: Gameplay Classes

GameActivity(settings: dict)
208    def __init__(self, settings: dict):
209        """Instantiate the Activity."""
210        super().__init__(settings)
211
212        # Holds some flattened info about the player set at the point
213        # when on_begin() is called.
214        self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
215
216        # Go ahead and get our map loading.
217        self._map_type = _map.get_map_class(self._calc_map_name(settings))
218
219        self._spawn_sound = _bascenev1.getsound('spawn')
220        self._map_type.preload()
221        self._map: bascenev1.Map | None = None
222        self._powerup_drop_timer: bascenev1.Timer | None = None
223        self._tnt_spawners: dict[int, TNTSpawner] | None = None
224        self._tnt_drop_timer: bascenev1.Timer | None = None
225        self._game_scoreboard_name_text: bascenev1.Actor | None = None
226        self._game_scoreboard_description_text: bascenev1.Actor | None = None
227        self._standard_time_limit_time: int | None = None
228        self._standard_time_limit_timer: bascenev1.Timer | None = None
229        self._standard_time_limit_text: bascenev1.NodeActor | None = None
230        self._standard_time_limit_text_input: bascenev1.NodeActor | None = None
231        self._tournament_time_limit: int | None = None
232        self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None
233        self._tournament_time_limit_title_text: bascenev1.NodeActor | None = (
234            None
235        )
236        self._tournament_time_limit_text: bascenev1.NodeActor | None = None
237        self._tournament_time_limit_text_input: bascenev1.NodeActor | None = (
238            None
239        )
240        self._zoom_message_times: dict[int, float] = {}

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 getscoreconfig(cls) -> ScoreConfig:
68    @classmethod
69    def getscoreconfig(cls) -> bascenev1.ScoreConfig:
70        """Return info about game scoring setup; can be overridden by games."""
71        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:
73    @classmethod
74    def getname(cls) -> str:
75        """Return a str name for this game type.
76
77        This default implementation simply returns the 'name' class attr.
78        """
79        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:
 81    @classmethod
 82    def get_display_string(cls, settings: dict | None = None) -> babase.Lstr:
 83        """Return a descriptive name for this game/settings combo.
 84
 85        Subclasses should override getname(); not this.
 86        """
 87        name = babase.Lstr(translate=('gameNames', cls.getname()))
 88
 89        # A few substitutions for 'Epic', 'Solo' etc. modes.
 90        # FIXME: Should provide a way for game types to define filters of
 91        #  their own and should not rely on hard-coded settings names.
 92        if settings is not None:
 93            if 'Solo Mode' in settings and settings['Solo Mode']:
 94                name = babase.Lstr(
 95                    resource='soloNameFilterText', subs=[('${NAME}', name)]
 96                )
 97            if 'Epic Mode' in settings and settings['Epic Mode']:
 98                name = babase.Lstr(
 99                    resource='epicNameFilterText', subs=[('${NAME}', name)]
100                )
101
102        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:
104    @classmethod
105    def get_team_display_string(cls, name: str) -> babase.Lstr:
106        """Given a team name, returns a localized version of it."""
107        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:
109    @classmethod
110    def get_description(cls, sessiontype: type[bascenev1.Session]) -> str:
111        """Get a str description of this game type.
112
113        The default implementation simply returns the 'description' class var.
114        Classes which want to change their description depending on the session
115        can override this method.
116        """
117        del sessiontype  # Unused arg.
118        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:
120    @classmethod
121    def get_description_display_string(
122        cls, sessiontype: type[bascenev1.Session]
123    ) -> babase.Lstr:
124        """Return a translated version of get_description().
125
126        Sub-classes should override get_description(); not this.
127        """
128        description = cls.get_description(sessiontype)
129        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]:
131    @classmethod
132    def get_available_settings(
133        cls, sessiontype: type[bascenev1.Session]
134    ) -> list[bascenev1.Setting]:
135        """Return a list of settings relevant to this game type when
136        running under the provided session type.
137        """
138        del sessiontype  # Unused arg.
139        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]:
141    @classmethod
142    def get_supported_maps(
143        cls, sessiontype: type[bascenev1.Session]
144    ) -> list[str]:
145        """
146        Called by the default bascenev1.GameActivity.create_settings_ui()
147        implementation; should return a list of map names valid
148        for this game-type for the given bascenev1.Session type.
149        """
150        del sessiontype  # Unused arg.
151        assert babase.app.classic is not None
152        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:
154    @classmethod
155    def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr:
156        """Given a game config dict, return a short description for it.
157
158        This is used when viewing game-lists or showing what game
159        is up next in a series.
160        """
161        name = cls.get_display_string(config['settings'])
162
163        # In newer configs, map is in settings; it used to be in the
164        # config root.
165        if 'map' in config['settings']:
166            sval = babase.Lstr(
167                value='${NAME} @ ${MAP}',
168                subs=[
169                    ('${NAME}', name),
170                    (
171                        '${MAP}',
172                        _map.get_map_display_string(
173                            _map.get_filtered_map_name(
174                                config['settings']['map']
175                            )
176                        ),
177                    ),
178                ],
179            )
180        elif 'map' in config:
181            sval = babase.Lstr(
182                value='${NAME} @ ${MAP}',
183                subs=[
184                    ('${NAME}', name),
185                    (
186                        '${MAP}',
187                        _map.get_map_display_string(
188                            _map.get_filtered_map_name(config['map'])
189                        ),
190                    ),
191                ],
192            )
193        else:
194            print('invalid game config - expected map entry under settings')
195            sval = babase.Lstr(value='???')
196        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:
198    @classmethod
199    def supports_session_type(
200        cls, sessiontype: type[bascenev1.Session]
201    ) -> bool:
202        """Return whether this game supports the provided Session type."""
203        from bascenev1._multiteamsession import MultiTeamSession
204
205        # By default, games support any versus mode
206        return issubclass(sessiontype, MultiTeamSession)

Return whether this game supports the provided Session type.

initialplayerinfos: list[PlayerInfo] | None
map: Map
242    @property
243    def map(self) -> _map.Map:
244        """The map being used for this game.
245
246        Raises a bascenev1.MapNotFoundError if the map does not currently
247        exist.
248        """
249        if self._map is None:
250            raise babase.MapNotFoundError
251        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:
253    def get_instance_display_string(self) -> babase.Lstr:
254        """Return a name for this particular game instance."""
255        return self.get_display_string(self.settings_raw)

Return a name for this particular game instance.

def get_instance_scoreboard_display_string(self) -> Lstr:
258    def get_instance_scoreboard_display_string(self) -> babase.Lstr:
259        """Return a name for this particular game instance.
260
261        This name is used above the game scoreboard in the corner
262        of the screen, so it should be as concise as possible.
263        """
264        # If we're in a co-op session, use the level name.
265        # FIXME: Should clean this up.
266        try:
267            from bascenev1._coopsession import CoopSession
268
269            if isinstance(self.session, CoopSession):
270                campaign = self.session.campaign
271                assert campaign is not None
272                return campaign.getlevel(
273                    self.session.campaign_level_name
274                ).displayname
275        except Exception:
276            logging.exception('Error getting campaign level name.')
277        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]:
279    def get_instance_description(self) -> str | Sequence:
280        """Return a description for this game instance, in English.
281
282        This is shown in the center of the screen below the game name at the
283        start of a game. It should start with a capital letter and end with a
284        period, and can be a bit more verbose than the version returned by
285        get_instance_description_short().
286
287        Note that translation is applied by looking up the specific returned
288        value as a key, so the number of returned variations should be limited;
289        ideally just one or two. To include arbitrary values in the
290        description, you can return a sequence of values in the following
291        form instead of just a string:
292
293        # This will give us something like 'Score 3 goals.' in English
294        # and can properly translate to 'Anota 3 goles.' in Spanish.
295        # If we just returned the string 'Score 3 Goals' here, there would
296        # have to be a translation entry for each specific number. ew.
297        return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
298
299        This way the first string can be consistently translated, with any arg
300        values then substituted into the result. ${ARG1} will be replaced with
301        the first value, ${ARG2} with the second, etc.
302        """
303        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]:
305    def get_instance_description_short(self) -> str | Sequence:
306        """Return a short description for this game instance in English.
307
308        This description is used above the game scoreboard in the
309        corner of the screen, so it should be as concise as possible.
310        It should be lowercase and should not contain periods or other
311        punctuation.
312
313        Note that translation is applied by looking up the specific returned
314        value as a key, so the number of returned variations should be limited;
315        ideally just one or two. To include arbitrary values in the
316        description, you can return a sequence of values in the following form
317        instead of just a string:
318
319        # This will give us something like 'score 3 goals' in English
320        # and can properly translate to 'anota 3 goles' in Spanish.
321        # If we just returned the string 'score 3 goals' here, there would
322        # have to be a translation entry for each specific number. ew.
323        return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
324
325        This way the first string can be consistently translated, with any arg
326        values then substituted into the result. ${ARG1} will be replaced
327        with the first value, ${ARG2} with the second, etc.
328
329        """
330        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:
332    @override
333    def on_transition_in(self) -> None:
334        super().on_transition_in()
335
336        # Make our map.
337        self._map = self._map_type()
338
339        # Give our map a chance to override the music.
340        # (for happy-thoughts and other such themed maps)
341        map_music = self._map_type.get_music_type()
342        music = map_music if map_music is not None else self.default_music
343
344        if music is not None:
345            _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.

@override
def on_begin(self) -> None:
347    @override
348    def on_begin(self) -> None:
349        super().on_begin()
350
351        if babase.app.classic is not None:
352            babase.app.classic.game_begin_analytics()
353
354        # We don't do this in on_transition_in because it may depend on
355        # players/teams which aren't available until now.
356        _bascenev1.timer(0.001, self._show_scoreboard_info)
357        _bascenev1.timer(1.0, self._show_info)
358        _bascenev1.timer(2.5, self._show_tip)
359
360        # Store some basic info about players present at start time.
361        self.initialplayerinfos = [
362            PlayerInfo(name=p.getname(full=True), character=p.character)
363            for p in self.players
364        ]
365
366        # Sort this by name so high score lists/etc will be consistent
367        # regardless of player join order.
368        self.initialplayerinfos.sort(key=lambda x: x.name)
369
370        # If this is a tournament, query info about it such as how much
371        # time is left.
372        tournament_id = self.session.tournament_id
373        if tournament_id is not None:
374            assert babase.app.plus is not None
375            babase.app.plus.tournament_query(
376                args={
377                    'tournamentIDs': [tournament_id],
378                    'source': 'in-game time remaining query',
379                },
380                callback=babase.WeakCall(self._on_tournament_query_response),
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.

@override
def on_player_join(self, player: ~PlayerT) -> None:
396    @override
397    def on_player_join(self, player: PlayerT) -> None:
398        super().on_player_join(player)
399
400        # By default, just spawn a dude.
401        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:
403    @override
404    def handlemessage(self, msg: Any) -> Any:
405        if isinstance(msg, PlayerDiedMessage):
406            # pylint: disable=cyclic-import
407            from bascenev1lib.actor.spaz import Spaz
408
409            player = msg.getplayer(self.playertype)
410            killer = msg.getkillerplayer(self.playertype)
411
412            # Inform our stats of the demise.
413            self.stats.player_was_killed(
414                player, killed=msg.killed, killer=killer
415            )
416
417            # Award the killer points if he's on a different team.
418            # FIXME: This should not be linked to Spaz actors.
419            # (should move get_death_points to Actor or make it a message)
420            if killer and killer.team is not player.team:
421                assert isinstance(killer.actor, Spaz)
422                pts, importance = killer.actor.get_death_points(msg.how)
423                if not self.has_ended():
424                    self.stats.player_scored(
425                        killer,
426                        pts,
427                        kill=True,
428                        victim_player=player,
429                        importance=importance,
430                        showpoints=self.show_kill_points,
431                    )
432        else:
433            return super().handlemessage(msg)
434        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:
697    @override
698    def end(
699        self, results: Any = None, delay: float = 0.0, force: bool = False
700    ) -> None:
701        from bascenev1._gameresults import GameResults
702
703        # If results is a standard team-game-results, associate it with us
704        # so it can grab our score prefs.
705        if isinstance(results, GameResults):
706            results.set_game(self)
707
708        # If we had a standard time-limit that had not expired, stop it so
709        # it doesnt tick annoyingly.
710        if (
711            self._standard_time_limit_time is not None
712            and self._standard_time_limit_time > 0
713        ):
714            self._standard_time_limit_timer = None
715            self._standard_time_limit_text = None
716
717        # Ditto with tournament time limits.
718        if (
719            self._tournament_time_limit is not None
720            and self._tournament_time_limit > 0
721        ):
722            self._tournament_time_limit_timer = None
723            self._tournament_time_limit_text = None
724            self._tournament_time_limit_title_text = None
725
726        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:
728    def end_game(self) -> None:
729        """Tell the game to wrap up and call bascenev1.Activity.end().
730
731        This method should be overridden by subclasses. A game should always
732        be prepared to end and deliver results, even if there is no 'winner'
733        yet; this way things like the standard time-limit
734        (bascenev1.GameActivity.setup_standard_time_limit()) will work with
735        the game.
736        """
737        print(
738            'WARNING: default end_game() implementation called;'
739            ' your game should override this.'
740        )

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:
742    def respawn_player(
743        self, player: PlayerT, respawn_time: float | None = None
744    ) -> None:
745        """
746        Given a bascenev1.Player, sets up a standard respawn timer,
747        along with the standard counter display, etc.
748        At the end of the respawn period spawn_player() will
749        be called if the Player still exists.
750        An explicit 'respawn_time' can optionally be provided
751        (in seconds).
752        """
753        # pylint: disable=cyclic-import
754
755        assert player
756        if respawn_time is None:
757            teamsize = len(player.team.players)
758            if teamsize == 1:
759                respawn_time = 3.0
760            elif teamsize == 2:
761                respawn_time = 5.0
762            elif teamsize == 3:
763                respawn_time = 6.0
764            else:
765                respawn_time = 7.0
766
767        # If this standard setting is present, factor it in.
768        if 'Respawn Times' in self.settings_raw:
769            respawn_time *= self.settings_raw['Respawn Times']
770
771        # We want whole seconds.
772        assert respawn_time is not None
773        respawn_time = round(max(1.0, respawn_time), 0)
774
775        if player.actor and not self.has_ended():
776            from bascenev1lib.actor.respawnicon import RespawnIcon
777
778            player.customdata['respawn_timer'] = _bascenev1.Timer(
779                respawn_time,
780                babase.WeakCall(self.spawn_player_if_exists, player),
781            )
782            player.customdata['respawn_icon'] = RespawnIcon(
783                player, respawn_time
784            )

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:
786    def spawn_player_if_exists(self, player: PlayerT) -> None:
787        """
788        A utility method which calls self.spawn_player() *only* if the
789        bascenev1.Player provided still exists; handy for use in timers
790        and whatnot.
791
792        There is no need to override this; just override spawn_player().
793        """
794        if player:
795            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:
797    def spawn_player(self, player: PlayerT) -> bascenev1.Actor:
798        """Spawn *something* for the provided bascenev1.Player.
799
800        The default implementation simply calls spawn_player_spaz().
801        """
802        assert player  # Dead references should never be passed as args.
803
804        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:
806    def spawn_player_spaz(
807        self,
808        player: PlayerT,
809        position: Sequence[float] = (0, 0, 0),
810        angle: float | None = None,
811    ) -> PlayerSpaz:
812        """Create and wire up a bascenev1.PlayerSpaz for the provided Player."""
813        # pylint: disable=too-many-locals
814        # pylint: disable=cyclic-import
815        from bascenev1._gameutils import animate
816        from bascenev1._coopsession import CoopSession
817        from bascenev1lib.actor.playerspaz import PlayerSpaz
818
819        name = player.getname()
820        color = player.color
821        highlight = player.highlight
822
823        playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz)
824        if not issubclass(playerspaztype, PlayerSpaz):
825            playerspaztype = PlayerSpaz
826
827        light_color = babase.normalized_color(color)
828        display_color = babase.safecolor(color, target_intensity=0.75)
829        spaz = playerspaztype(
830            color=color,
831            highlight=highlight,
832            character=player.character,
833            player=player,
834        )
835
836        player.actor = spaz
837        assert spaz.node
838
839        # If this is co-op and we're on Courtyard or Runaround, add the
840        # material that allows us to collide with the player-walls.
841        # FIXME: Need to generalize this.
842        if isinstance(self.session, CoopSession) and self.map.getname() in [
843            'Courtyard',
844            'Tower D',
845        ]:
846            mat = self.map.preloaddata['collide_with_wall_material']
847            assert isinstance(spaz.node.materials, tuple)
848            assert isinstance(spaz.node.roller_materials, tuple)
849            spaz.node.materials += (mat,)
850            spaz.node.roller_materials += (mat,)
851
852        spaz.node.name = name
853        spaz.node.name_color = display_color
854        spaz.connect_controls_to_player()
855
856        # Move to the stand position and add a flash of light.
857        spaz.handlemessage(
858            StandMessage(
859                position, angle if angle is not None else random.uniform(0, 360)
860            )
861        )
862        self._spawn_sound.play(1, position=spaz.node.position)
863        light = _bascenev1.newnode('light', attrs={'color': light_color})
864        spaz.node.connectattr('position', light, 'position')
865        animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
866        _bascenev1.timer(0.5, light.delete)
867        return spaz

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

def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
869    def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
870        """Create standard powerup drops for the current map."""
871        # pylint: disable=cyclic-import
872        from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL
873
874        self._powerup_drop_timer = _bascenev1.Timer(
875            DEFAULT_POWERUP_INTERVAL,
876            babase.WeakCall(self._standard_drop_powerups),
877            repeat=True,
878        )
879        self._standard_drop_powerups()
880        if enable_tnt:
881            self._tnt_spawners = {}
882            self._setup_standard_tnt_drops()

Create standard powerup drops for the current map.

def setup_standard_time_limit(self, duration: float) -> None:
914    def setup_standard_time_limit(self, duration: float) -> None:
915        """
916        Create a standard game time-limit given the provided
917        duration in seconds.
918        This will be displayed at the top of the screen.
919        If the time-limit expires, end_game() will be called.
920        """
921        from bascenev1._nodeactor import NodeActor
922
923        if duration <= 0.0:
924            return
925        self._standard_time_limit_time = int(duration)
926        self._standard_time_limit_timer = _bascenev1.Timer(
927            1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True
928        )
929        self._standard_time_limit_text = NodeActor(
930            _bascenev1.newnode(
931                'text',
932                attrs={
933                    'v_attach': 'top',
934                    'h_attach': 'center',
935                    'h_align': 'left',
936                    'color': (1.0, 1.0, 1.0, 0.5),
937                    'position': (-25, -30),
938                    'flatness': 1.0,
939                    'scale': 0.9,
940                },
941            )
942        )
943        self._standard_time_limit_text_input = NodeActor(
944            _bascenev1.newnode(
945                'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0}
946            )
947        )
948        self.globalsnode.connectattr(
949            'time', self._standard_time_limit_text_input.node, 'time1'
950        )
951        assert self._standard_time_limit_text_input.node
952        assert self._standard_time_limit_text.node
953        self._standard_time_limit_text_input.node.connectattr(
954            'output', self._standard_time_limit_text.node, 'text'
955        )

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:
1132    def show_zoom_message(
1133        self,
1134        message: babase.Lstr,
1135        *,
1136        color: Sequence[float] = (0.9, 0.4, 0.0),
1137        scale: float = 0.8,
1138        duration: float = 2.0,
1139        trail: bool = False,
1140    ) -> None:
1141        """Zooming text used to announce game names and winners."""
1142        # pylint: disable=cyclic-import
1143        from bascenev1lib.actor.zoomtext import ZoomText
1144
1145        # Reserve a spot on the screen (in case we get multiple of these so
1146        # they don't overlap).
1147        i = 0
1148        cur_time = babase.apptime()
1149        while True:
1150            if (
1151                i not in self._zoom_message_times
1152                or self._zoom_message_times[i] < cur_time
1153            ):
1154                self._zoom_message_times[i] = cur_time + duration
1155                break
1156            i += 1
1157        ZoomText(
1158            message,
1159            lifespan=duration,
1160            jitter=2.0,
1161            position=(0, 200 - i * 100),
1162            scale=scale,
1163            maxwidth=800,
1164            trail=trail,
1165            color=color,
1166        ).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:
33@dataclass
34class GameTip:
35    """Defines a tip presentable to the user at the start of a game.
36
37    Category: **Gameplay Classes**
38    """
39
40    text: str
41    icon: bascenev1.Texture | None = None
42    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: _bascenev1.Texture | None = None, sound: _bascenev1.Sound | None = None)
text: str
icon: _bascenev1.Texture | None = None
sound: _bascenev1.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]]:
226def get_default_free_for_all_playlist() -> PlaylistType:
227    """Return a default playlist for free-for-all mode."""
228
229    # NOTE: these are currently using old type/map names,
230    # but filtering translates them properly to the new ones.
231    # (is kinda a handy way to ensure filtering is working).
232    # Eventually should update these though.
233    return [
234        {
235            'settings': {
236                'Epic Mode': False,
237                'Kills to Win Per Player': 10,
238                'Respawn Times': 1.0,
239                'Time Limit': 300,
240                'map': 'Doom Shroom',
241            },
242            'type': 'bs_death_match.DeathMatchGame',
243        },
244        {
245            'settings': {
246                'Chosen One Gets Gloves': True,
247                'Chosen One Gets Shield': False,
248                'Chosen One Time': 30,
249                'Epic Mode': 0,
250                'Respawn Times': 1.0,
251                'Time Limit': 300,
252                'map': 'Monkey Face',
253            },
254            'type': 'bs_chosen_one.ChosenOneGame',
255        },
256        {
257            'settings': {
258                'Hold Time': 30,
259                'Respawn Times': 1.0,
260                'Time Limit': 300,
261                'map': 'Zigzag',
262            },
263            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
264        },
265        {
266            'settings': {'Epic Mode': False, 'map': 'Rampage'},
267            'type': 'bs_meteor_shower.MeteorShowerGame',
268        },
269        {
270            'settings': {
271                'Epic Mode': 1,
272                'Lives Per Player': 1,
273                'Respawn Times': 1.0,
274                'Time Limit': 120,
275                'map': 'Tip Top',
276            },
277            'type': 'bs_elimination.EliminationGame',
278        },
279        {
280            'settings': {
281                'Hold Time': 30,
282                'Respawn Times': 1.0,
283                'Time Limit': 300,
284                'map': 'The Pad',
285            },
286            'type': 'bs_keep_away.KeepAwayGame',
287        },
288        {
289            'settings': {
290                'Epic Mode': True,
291                'Kills to Win Per Player': 10,
292                'Respawn Times': 0.25,
293                'Time Limit': 120,
294                'map': 'Rampage',
295            },
296            'type': 'bs_death_match.DeathMatchGame',
297        },
298        {
299            'settings': {
300                'Bomb Spawning': 1000,
301                'Epic Mode': False,
302                'Laps': 3,
303                'Mine Spawn Interval': 4000,
304                'Mine Spawning': 4000,
305                'Time Limit': 300,
306                'map': 'Big G',
307            },
308            'type': 'bs_race.RaceGame',
309        },
310        {
311            'settings': {
312                'Hold Time': 30,
313                'Respawn Times': 1.0,
314                'Time Limit': 300,
315                'map': 'Happy Thoughts',
316            },
317            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
318        },
319        {
320            'settings': {
321                'Enable Impact Bombs': 1,
322                'Enable Triple Bombs': False,
323                'Target Count': 2,
324                'map': 'Doom Shroom',
325            },
326            'type': 'bs_target_practice.TargetPracticeGame',
327        },
328        {
329            'settings': {
330                'Epic Mode': False,
331                'Lives Per Player': 5,
332                'Respawn Times': 1.0,
333                'Time Limit': 300,
334                'map': 'Step Right Up',
335            },
336            'type': 'bs_elimination.EliminationGame',
337        },
338        {
339            'settings': {
340                'Epic Mode': False,
341                'Kills to Win Per Player': 10,
342                'Respawn Times': 1.0,
343                'Time Limit': 300,
344                'map': 'Crag Castle',
345            },
346            'type': 'bs_death_match.DeathMatchGame',
347        },
348        {
349            'map': 'Lake Frigid',
350            'settings': {
351                'Bomb Spawning': 0,
352                'Epic Mode': False,
353                'Laps': 6,
354                'Mine Spawning': 2000,
355                'Time Limit': 300,
356                'map': 'Lake Frigid',
357            },
358            'type': 'bs_race.RaceGame',
359        },
360    ]

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

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

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:
45def get_trophy_string(trophy_id: str) -> str:
46    """Given a trophy id, returns a string to visualize it."""
47    if trophy_id in TROPHY_CHARS:
48        return babase.charstr(TROPHY_CHARS[trophy_id])
49    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) -> _bascenev1.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) -> _bascenev1.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) -> _bascenev1.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) -> _bascenev1.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) -> _bascenev1.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        *,
246        srcnode: bascenev1.Node | None = None,
247        pos: Sequence[float] | None = None,
248        velocity: Sequence[float] | None = None,
249        magnitude: float = 1.0,
250        velocity_magnitude: float = 0.0,
251        radius: float = 1.0,
252        source_player: bascenev1.Player | None = None,
253        kick_back: float = 1.0,
254        flat_damage: float | None = None,
255        hit_type: str = 'generic',
256        force_direction: Sequence[float] | None = None,
257        hit_subtype: str = 'default',
258    ):
259        """Instantiate a message with given values."""
260
261        self.srcnode = srcnode
262        self.pos = pos if pos is not None else babase.Vec3()
263        self.velocity = velocity if velocity is not None else babase.Vec3()
264        self.magnitude = magnitude
265        self.velocity_magnitude = velocity_magnitude
266        self.radius = radius
267
268        # We should not be getting passed an invalid ref.
269        assert source_player is None or source_player.exists()
270        self._source_player = source_player
271        self.kick_back = kick_back
272        self.flat_damage = flat_damage
273        self.hit_type = hit_type
274        self.hit_subtype = hit_subtype
275        self.force_direction = (
276            force_direction if force_direction is not None else velocity
277        )
278
279    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
280        """Return the source-player if one exists and is the provided type."""
281        player: Any = self._source_player
282
283        # We should not be delivering invalid refs.
284        # (we could translate to None here but technically we are changing
285        # the message delivered which seems wrong)
286        assert player is None or player.exists()
287
288        # Return the player *only* if they're the type given.
289        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: _bascenev1.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        *,
246        srcnode: bascenev1.Node | None = None,
247        pos: Sequence[float] | None = None,
248        velocity: Sequence[float] | None = None,
249        magnitude: float = 1.0,
250        velocity_magnitude: float = 0.0,
251        radius: float = 1.0,
252        source_player: bascenev1.Player | None = None,
253        kick_back: float = 1.0,
254        flat_damage: float | None = None,
255        hit_type: str = 'generic',
256        force_direction: Sequence[float] | None = None,
257        hit_subtype: str = 'default',
258    ):
259        """Instantiate a message with given values."""
260
261        self.srcnode = srcnode
262        self.pos = pos if pos is not None else babase.Vec3()
263        self.velocity = velocity if velocity is not None else babase.Vec3()
264        self.magnitude = magnitude
265        self.velocity_magnitude = velocity_magnitude
266        self.radius = radius
267
268        # We should not be getting passed an invalid ref.
269        assert source_player is None or source_player.exists()
270        self._source_player = source_player
271        self.kick_back = kick_back
272        self.flat_damage = flat_damage
273        self.hit_type = hit_type
274        self.hit_subtype = hit_subtype
275        self.force_direction = (
276            force_direction if force_direction is not None else velocity
277        )

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]:
279    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
280        """Return the source-player if one exists and is the provided type."""
281        player: Any = self._source_player
282
283        # We should not be delivering invalid refs.
284        # (we could translate to None here but technically we are changing
285        # the message delivered which seems wrong)
286        assert player is None or player.exists()
287
288        # Return the player *only* if they're the type given.
289        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: _bascenev1.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>
@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]]
@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
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).

59class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
60    """Standard activity for waiting for players to join.
61
62    It shows tips and other info and waits for all players to check ready.
63    """
64
65    def __init__(self, settings: dict):
66        super().__init__(settings)
67
68        # This activity is a special 'joiner' activity.
69        # It will get shut down as soon as all players have checked ready.
70        self.is_joining_activity = True
71
72        # Players may be idle waiting for joiners; lets not kick them for it.
73        self.allow_kick_idle_players = False
74
75        # In vr mode we don't want stuff moving around.
76        self.use_fixed_vr_overlay = True
77
78        self._background: bascenev1.Actor | None = None
79        self._tips_text: bascenev1.Actor | None = None
80        self._join_info: JoinInfo | None = None
81
82    @override
83    def on_transition_in(self) -> None:
84        # pylint: disable=cyclic-import
85        from bascenev1lib.actor.tipstext import TipsText
86        from bascenev1lib.actor.background import Background
87
88        super().on_transition_in()
89        self._background = Background(
90            fade_time=0.5, start_faded=True, show_logo=True
91        )
92        self._tips_text = TipsText()
93        setmusic(MusicType.CHAR_SELECT)
94        self._join_info = self.session.lobby.create_join_info()
95        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)
65    def __init__(self, settings: dict):
66        super().__init__(settings)
67
68        # This activity is a special 'joiner' activity.
69        # It will get shut down as soon as all players have checked ready.
70        self.is_joining_activity = True
71
72        # Players may be idle waiting for joiners; lets not kick them for it.
73        self.allow_kick_idle_players = False
74
75        # In vr mode we don't want stuff moving around.
76        self.use_fixed_vr_overlay = True
77
78        self._background: bascenev1.Actor | None = None
79        self._tips_text: bascenev1.Actor | None = None
80        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:
82    @override
83    def on_transition_in(self) -> None:
84        # pylint: disable=cyclic-import
85        from bascenev1lib.actor.tipstext import TipsText
86        from bascenev1lib.actor.background import Background
87
88        super().on_transition_in()
89        self._background = Background(
90            fade_time=0.5, start_faded=True, show_logo=True
91        )
92        self._tips_text = TipsText()
93        setmusic(MusicType.CHAR_SELECT)
94        self._join_info = self.session.lobby.create_join_info()
95        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        *,
 32        displayname: str | None = None,
 33    ):
 34        self._name = name
 35        self._gametype = gametype
 36        self._settings = settings
 37        self._preview_texture_name = preview_texture_name
 38        self._displayname = displayname
 39        self._campaign: weakref.ref[bascenev1.Campaign] | None = None
 40        self._index: int | None = None
 41        self._score_version_string: str | None = None
 42
 43    @override
 44    def __repr__(self) -> str:
 45        cls = type(self)
 46        return f"<{cls.__module__}.{cls.__name__} '{self._name}'>"
 47
 48    @property
 49    def name(self) -> str:
 50        """The unique name for this Level."""
 51        return self._name
 52
 53    def get_settings(self) -> dict[str, Any]:
 54        """Returns the settings for this Level."""
 55        settings = copy.deepcopy(self._settings)
 56
 57        # So the game knows what the level is called.
 58        # Hmm; seems hacky; I think we should take this out.
 59        settings['name'] = self._name
 60        return settings
 61
 62    @property
 63    def preview_texture_name(self) -> str:
 64        """The preview texture name for this Level."""
 65        return self._preview_texture_name
 66
 67    # def get_preview_texture(self) -> bauiv1.Texture:
 68    #     """Load/return the preview Texture for this Level."""
 69    #     return _bauiv1.gettexture(self._preview_texture_name)
 70
 71    @property
 72    def displayname(self) -> bascenev1.Lstr:
 73        """The localized name for this Level."""
 74        return babase.Lstr(
 75            translate=(
 76                'coopLevelNames',
 77                (
 78                    self._displayname
 79                    if self._displayname is not None
 80                    else self._name
 81                ),
 82            ),
 83            subs=[
 84                ('${GAME}', self._gametype.get_display_string(self._settings))
 85            ],
 86        )
 87
 88    @property
 89    def gametype(self) -> type[bascenev1.GameActivity]:
 90        """The type of game used for this Level."""
 91        return self._gametype
 92
 93    @property
 94    def campaign(self) -> bascenev1.Campaign | None:
 95        """The baclassic.Campaign this Level is associated with, or None."""
 96        return None if self._campaign is None else self._campaign()
 97
 98    @property
 99    def index(self) -> int:
100        """The zero-based index of this Level in its baclassic.Campaign.
101
102        Access results in a RuntimeError if the Level is  not assigned to a
103        Campaign.
104        """
105        if self._index is None:
106            raise RuntimeError('Level is not part of a Campaign')
107        return self._index
108
109    @property
110    def complete(self) -> bool:
111        """Whether this Level has been completed."""
112        config = self._get_config_dict()
113        val = config.get('Complete', False)
114        assert isinstance(val, bool)
115        return val
116
117    def set_complete(self, val: bool) -> None:
118        """Set whether or not this level is complete."""
119        old_val = self.complete
120        assert isinstance(old_val, bool)
121        assert isinstance(val, bool)
122        if val != old_val:
123            config = self._get_config_dict()
124            config['Complete'] = val
125
126    def get_high_scores(self) -> dict:
127        """Return the current high scores for this Level."""
128        config = self._get_config_dict()
129        high_scores_key = 'High Scores' + self.get_score_version_string()
130        if high_scores_key not in config:
131            return {}
132        return copy.deepcopy(config[high_scores_key])
133
134    def set_high_scores(self, high_scores: dict) -> None:
135        """Set high scores for this level."""
136        config = self._get_config_dict()
137        high_scores_key = 'High Scores' + self.get_score_version_string()
138        config[high_scores_key] = high_scores
139
140    def get_score_version_string(self) -> str:
141        """Return the score version string for this Level.
142
143        If a Level's gameplay changes significantly, its version string
144        can be changed to separate its new high score lists/etc. from the old.
145        """
146        if self._score_version_string is None:
147            scorever = self._gametype.getscoreconfig().version
148            if scorever != '':
149                scorever = ' ' + scorever
150            self._score_version_string = scorever
151        assert self._score_version_string is not None
152        return self._score_version_string
153
154    @property
155    def rating(self) -> float:
156        """The current rating for this Level."""
157        val = self._get_config_dict().get('Rating', 0.0)
158        assert isinstance(val, float)
159        return val
160
161    def set_rating(self, rating: float) -> None:
162        """Set a rating for this Level, replacing the old ONLY IF higher."""
163        old_rating = self.rating
164        config = self._get_config_dict()
165        config['Rating'] = max(old_rating, rating)
166
167    def _get_config_dict(self) -> dict[str, Any]:
168        """Return/create the persistent state dict for this level.
169
170        The referenced dict exists under the game's config dict and
171        can be modified in place."""
172        campaign = self.campaign
173        if campaign is None:
174            raise RuntimeError('Level is not in a campaign.')
175        configdict = campaign.configdict
176        val: dict[str, Any] = configdict.setdefault(
177            self._name, {'Rating': 0.0, 'Complete': False}
178        )
179        assert isinstance(val, dict)
180        return val
181
182    def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None:
183        """For use by baclassic.Campaign when adding levels to itself.
184
185        (internal)"""
186        self._campaign = weakref.ref(campaign)
187        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        *,
32        displayname: str | None = None,
33    ):
34        self._name = name
35        self._gametype = gametype
36        self._settings = settings
37        self._preview_texture_name = preview_texture_name
38        self._displayname = displayname
39        self._campaign: weakref.ref[bascenev1.Campaign] | None = None
40        self._index: int | None = None
41        self._score_version_string: str | None = None
name: str
48    @property
49    def name(self) -> str:
50        """The unique name for this Level."""
51        return self._name

The unique name for this Level.

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

Returns the settings for this Level.

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

The preview texture name for this Level.

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

The localized name for this Level.

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

The type of game used for this Level.

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

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

index: int
 98    @property
 99    def index(self) -> int:
100        """The zero-based index of this Level in its baclassic.Campaign.
101
102        Access results in a RuntimeError if the Level is  not assigned to a
103        Campaign.
104        """
105        if self._index is None:
106            raise RuntimeError('Level is not part of a Campaign')
107        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
109    @property
110    def complete(self) -> bool:
111        """Whether this Level has been completed."""
112        config = self._get_config_dict()
113        val = config.get('Complete', False)
114        assert isinstance(val, bool)
115        return val

Whether this Level has been completed.

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

Set whether or not this level is complete.

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

Return the current high scores for this Level.

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

Set high scores for this level.

def get_score_version_string(self) -> str:
140    def get_score_version_string(self) -> str:
141        """Return the score version string for this Level.
142
143        If a Level's gameplay changes significantly, its version string
144        can be changed to separate its new high score lists/etc. from the old.
145        """
146        if self._score_version_string is None:
147            scorever = self._gametype.getscoreconfig().version
148            if scorever != '':
149                scorever = ' ' + scorever
150            self._score_version_string = scorever
151        assert self._score_version_string is not None
152        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
154    @property
155    def rating(self) -> float:
156        """The current rating for this Level."""
157        val = self._get_config_dict().get('Rating', 0.0)
158        assert isinstance(val, float)
159        return val

The current rating for this Level.

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

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

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

bascenev1.SessionTeams available in this lobby.

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

Return the lobby's current choosers.

def create_join_info(self) -> bascenev1._lobby.JoinInfo:
1011    def create_join_info(self) -> JoinInfo:
1012        """Create a display of on-screen information for joiners.
1013
1014        (how to switch teams, players, etc.)
1015        Intended for use in initial joining-screens.
1016        """
1017        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:
1019    def reload_profiles(self) -> None:
1020        """Reload available player profiles."""
1021        # pylint: disable=cyclic-import
1022        from bascenev1lib.actor.spazappearance import get_appearances
1023
1024        assert babase.app.classic is not None
1025
1026        # We may have gained or lost character names if the user
1027        # bought something; reload these too.
1028        self.character_names_local_unlocked = get_appearances()
1029        self.character_names_local_unlocked.sort(key=lambda x: x.lower())
1030
1031        # Do any overall prep we need to such as creating account profile.
1032        babase.app.classic.accounts.ensure_have_account_player_profile()
1033        for chooser in self.choosers:
1034            try:
1035                chooser.reload_profiles()
1036                chooser.update_from_profile()
1037            except Exception:
1038                logging.exception('Error reloading profiles.')

Reload available player profiles.

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

Update positions for all choosers.

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

Return whether all choosers are marked ready.

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

Add a chooser to the lobby for the provided player.

def remove_chooser(self, player: _bascenev1.SessionPlayer) -> None:
1062    def remove_chooser(self, player: bascenev1.SessionPlayer) -> None:
1063        """Remove a single player's chooser; does not kick them.
1064
1065        This is used when a player enters the game and no longer
1066        needs a chooser."""
1067        found = False
1068        chooser = None
1069        for chooser in self.choosers:
1070            if chooser.getplayer() is player:
1071                found = True
1072
1073                # Mark it as dead since there could be more
1074                # change-commands/etc coming in still for it;
1075                # want to avoid duplicate player-adds/etc.
1076                chooser.set_dead(True)
1077                self.choosers.remove(chooser)
1078                break
1079        if not found:
1080            logging.exception('remove_chooser did not find player %s.', player)
1081        elif chooser in self.choosers:
1082            logging.exception('chooser remains after removal for %s.', player)
1083        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:
1085    def remove_all_choosers(self) -> None:
1086        """Remove all choosers without kicking players.
1087
1088        This is called after all players check in and enter a game.
1089        """
1090        self.choosers = []
1091        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:
1093    def remove_all_choosers_and_kick_players(self) -> None:
1094        """Remove all player choosers and kick attached players."""
1095
1096        # Copy the list; it can change under us otherwise.
1097        for chooser in list(self.choosers):
1098            if chooser.sessionplayer:
1099                chooser.sessionplayer.remove_from_game()
1100        self.remove_all_choosers()

Remove all player choosers and kick attached players.

def ls_input_devices() -> None:
1481def ls_input_devices() -> None:
1482    """Print debugging info about game objects.
1483
1484    Category: **General Utility Functions**
1485
1486    This call only functions in debug builds of the game.
1487    It prints various info about the current object count, etc.
1488    """
1489    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:
1492def ls_objects() -> None:
1493    """Log debugging info about C++ level objects.
1494
1495    Category: **General Utility Functions**
1496
1497    This call only functions in debug builds of the game.
1498    It prints various info about the current object count, etc.
1499    """
1500    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:
491class Lstr:
492    """Used to define strings in a language-independent way.
493
494    Category: **General Utility Classes**
495
496    These should be used whenever possible in place of hard-coded
497    strings so that in-game or UI elements show up correctly on all
498    clients in their currently-active language.
499
500    To see available resource keys, look at any of the bs_language_*.py
501    files in the game or the translations pages at
502    legacy.ballistica.net/translate.
503
504    ##### Examples
505    EXAMPLE 1: specify a string from a resource path
506    >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
507
508    EXAMPLE 2: specify a translated string via a category and english
509    value; if a translated value is available, it will be used; otherwise
510    the english value will be. To see available translation categories,
511    look under the 'translations' resource section.
512    >>> mynode.text = babase.Lstr(translate=('gameDescriptions',
513    ...                                  'Defeat all enemies'))
514
515    EXAMPLE 3: specify a raw value and some substitutions. Substitutions
516    can be used with resource and translate modes as well.
517    >>> mynode.text = babase.Lstr(value='${A} / ${B}',
518    ...               subs=[('${A}', str(score)), ('${B}', str(total))])
519
520    EXAMPLE 4: babase.Lstr's can be nested. This example would display the
521    resource at res_a but replace ${NAME} with the value of the
522    resource at res_b
523    >>> mytextnode.text = babase.Lstr(
524    ...     resource='res_a',
525    ...     subs=[('${NAME}', babase.Lstr(resource='res_b'))])
526    """
527
528    # pylint: disable=dangerous-default-value
529    # noinspection PyDefaultArgument
530    @overload
531    def __init__(
532        self,
533        *,
534        resource: str,
535        fallback_resource: str = '',
536        fallback_value: str = '',
537        subs: Sequence[tuple[str, str | Lstr]] = [],
538    ) -> None:
539        """Create an Lstr from a string resource."""
540
541    # noinspection PyShadowingNames,PyDefaultArgument
542    @overload
543    def __init__(
544        self,
545        *,
546        translate: tuple[str, str],
547        subs: Sequence[tuple[str, str | Lstr]] = [],
548    ) -> None:
549        """Create an Lstr by translating a string in a category."""
550
551    # noinspection PyDefaultArgument
552    @overload
553    def __init__(
554        self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = []
555    ) -> None:
556        """Create an Lstr from a raw string value."""
557
558    # pylint: enable=redefined-outer-name, dangerous-default-value
559
560    def __init__(self, *args: Any, **keywds: Any) -> None:
561        """Instantiate a Lstr.
562
563        Pass a value for either 'resource', 'translate',
564        or 'value'. (see Lstr help for examples).
565        'subs' can be a sequence of 2-member sequences consisting of values
566        and replacements.
567        'fallback_resource' can be a resource key that will be used if the
568        main one is not present for
569        the current language in place of falling back to the english value
570        ('resource' mode only).
571        'fallback_value' can be a literal string that will be used if neither
572        the resource nor the fallback resource is found ('resource' mode only).
573        """
574        # pylint: disable=too-many-branches
575        if args:
576            raise TypeError('Lstr accepts only keyword arguments')
577
578        # Basically just store the exact args they passed.
579        # However if they passed any Lstr values for subs,
580        # replace them with that Lstr's dict.
581        self.args = keywds
582        our_type = type(self)
583
584        if isinstance(self.args.get('value'), our_type):
585            raise TypeError("'value' must be a regular string; not an Lstr")
586
587        if 'subs' in self.args:
588            subs_new = []
589            for key, value in keywds['subs']:
590                if isinstance(value, our_type):
591                    subs_new.append((key, value.args))
592                else:
593                    subs_new.append((key, value))
594            self.args['subs'] = subs_new
595
596        # As of protocol 31 we support compact key names
597        # ('t' instead of 'translate', etc). Convert as needed.
598        if 'translate' in keywds:
599            keywds['t'] = keywds['translate']
600            del keywds['translate']
601        if 'resource' in keywds:
602            keywds['r'] = keywds['resource']
603            del keywds['resource']
604        if 'value' in keywds:
605            keywds['v'] = keywds['value']
606            del keywds['value']
607        if 'fallback' in keywds:
608            from babase import _error
609
610            _error.print_error(
611                'deprecated "fallback" arg passed to Lstr(); use '
612                'either "fallback_resource" or "fallback_value"',
613                once=True,
614            )
615            keywds['f'] = keywds['fallback']
616            del keywds['fallback']
617        if 'fallback_resource' in keywds:
618            keywds['f'] = keywds['fallback_resource']
619            del keywds['fallback_resource']
620        if 'subs' in keywds:
621            keywds['s'] = keywds['subs']
622            del keywds['subs']
623        if 'fallback_value' in keywds:
624            keywds['fv'] = keywds['fallback_value']
625            del keywds['fallback_value']
626
627    def evaluate(self) -> str:
628        """Evaluate the Lstr and returns a flat string in the current language.
629
630        You should avoid doing this as much as possible and instead pass
631        and store Lstr values.
632        """
633        return _babase.evaluate_lstr(self._get_json())
634
635    def is_flat_value(self) -> bool:
636        """Return whether the Lstr is a 'flat' value.
637
638        This is defined as a simple string value incorporating no
639        translations, resources, or substitutions. In this case it may
640        be reasonable to replace it with a raw string value, perform
641        string manipulation on it, etc.
642        """
643        return bool('v' in self.args and not self.args.get('s', []))
644
645    def _get_json(self) -> str:
646        try:
647            return json.dumps(self.args, separators=(',', ':'))
648        except Exception:
649            from babase import _error
650
651            _error.print_exception('_get_json failed for', self.args)
652            return 'JSON_ERR'
653
654    @override
655    def __str__(self) -> str:
656        return '<ba.Lstr: ' + self._get_json() + '>'
657
658    @override
659    def __repr__(self) -> str:
660        return '<ba.Lstr: ' + self._get_json() + '>'
661
662    @staticmethod
663    def from_json(json_string: str) -> babase.Lstr:
664        """Given a json string, returns a babase.Lstr. Does no validation."""
665        lstr = Lstr(value='')
666        lstr.args = json.loads(json_string)
667        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)
560    def __init__(self, *args: Any, **keywds: Any) -> None:
561        """Instantiate a Lstr.
562
563        Pass a value for either 'resource', 'translate',
564        or 'value'. (see Lstr help for examples).
565        'subs' can be a sequence of 2-member sequences consisting of values
566        and replacements.
567        'fallback_resource' can be a resource key that will be used if the
568        main one is not present for
569        the current language in place of falling back to the english value
570        ('resource' mode only).
571        'fallback_value' can be a literal string that will be used if neither
572        the resource nor the fallback resource is found ('resource' mode only).
573        """
574        # pylint: disable=too-many-branches
575        if args:
576            raise TypeError('Lstr accepts only keyword arguments')
577
578        # Basically just store the exact args they passed.
579        # However if they passed any Lstr values for subs,
580        # replace them with that Lstr's dict.
581        self.args = keywds
582        our_type = type(self)
583
584        if isinstance(self.args.get('value'), our_type):
585            raise TypeError("'value' must be a regular string; not an Lstr")
586
587        if 'subs' in self.args:
588            subs_new = []
589            for key, value in keywds['subs']:
590                if isinstance(value, our_type):
591                    subs_new.append((key, value.args))
592                else:
593                    subs_new.append((key, value))
594            self.args['subs'] = subs_new
595
596        # As of protocol 31 we support compact key names
597        # ('t' instead of 'translate', etc). Convert as needed.
598        if 'translate' in keywds:
599            keywds['t'] = keywds['translate']
600            del keywds['translate']
601        if 'resource' in keywds:
602            keywds['r'] = keywds['resource']
603            del keywds['resource']
604        if 'value' in keywds:
605            keywds['v'] = keywds['value']
606            del keywds['value']
607        if 'fallback' in keywds:
608            from babase import _error
609
610            _error.print_error(
611                'deprecated "fallback" arg passed to Lstr(); use '
612                'either "fallback_resource" or "fallback_value"',
613                once=True,
614            )
615            keywds['f'] = keywds['fallback']
616            del keywds['fallback']
617        if 'fallback_resource' in keywds:
618            keywds['f'] = keywds['fallback_resource']
619            del keywds['fallback_resource']
620        if 'subs' in keywds:
621            keywds['s'] = keywds['subs']
622            del keywds['subs']
623        if 'fallback_value' in keywds:
624            keywds['fv'] = keywds['fallback_value']
625            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:
627    def evaluate(self) -> str:
628        """Evaluate the Lstr and returns a flat string in the current language.
629
630        You should avoid doing this as much as possible and instead pass
631        and store Lstr values.
632        """
633        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:
635    def is_flat_value(self) -> bool:
636        """Return whether the Lstr is a 'flat' value.
637
638        This is defined as a simple string value incorporating no
639        translations, resources, or substitutions. In this case it may
640        be reasonable to replace it with a raw string value, perform
641        string manipulation on it, etc.
642        """
643        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:
662    @staticmethod
663    def from_json(json_string: str) -> babase.Lstr:
664        """Given a json string, returns a babase.Lstr. Does no validation."""
665        lstr = Lstr(value='')
666        lstr.args = json.loads(json_string)
667        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: _bascenev1.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: _babase.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'>
def newactivity( activity_type: type[Activity], settings: dict | None = None) -> Activity:
1515def newactivity(
1516    activity_type: type[bascenev1.Activity], settings: dict | None = None
1517) -> bascenev1.Activity:
1518    """Instantiates a bascenev1.Activity given a type object.
1519
1520    Category: **General Utility Functions**
1521
1522    Activities require special setup and thus cannot be directly
1523    instantiated; you must go through this function.
1524    """
1525    import bascenev1  # pylint: disable=cyclic-import
1526
1527    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: _bascenev1.Node | None = None, attrs: dict | None = None, name: str | None = None, delegate: Any = None) -> _bascenev1.Node:
1531def newnode(
1532    type: str,
1533    owner: bascenev1.Node | None = None,
1534    attrs: dict | None = None,
1535    name: str | None = None,
1536    delegate: Any = None,
1537) -> bascenev1.Node:
1538    """Add a node of the given type to the game.
1539
1540    Category: **Gameplay Functions**
1541
1542    If a dict is provided for 'attributes', the node's initial attributes
1543    will be set based on them.
1544
1545    'name', if provided, will be stored with the node purely for debugging
1546    purposes. If no name is provided, an automatic one will be generated
1547    such as 'terrain@foo.py:30'.
1548
1549    If 'delegate' is provided, Python messages sent to the node will go to
1550    that object's handlemessage() method. Note that the delegate is stored
1551    as a weak-ref, so the node itself will not keep the object alive.
1552
1553    if 'owner' is provided, the node will be automatically killed when that
1554    object dies. 'owner' can be another node or a bascenev1.Actor
1555    """
1556    import bascenev1  # pylint: disable=cyclic-import
1557
1558    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: _bascenev1.Texture | None = None
tint_texture: _bascenev1.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[_bascenev1.Material] = ()
roller_materials: Sequence[_bascenev1.Material] = ()
name: str = ''
punch_materials: Sequence[_bascenev1.Material] = ()
pickup_materials: Sequence[_bascenev1.Material] = ()
extras_material: Sequence[_bascenev1.Material] = ()
rotate: float = 0.0
hold_node: _bascenev1.Node | None = None
hold_body: int = 0
host_only: bool = False
premultiplied: bool = False
source_player: Player | None = None
mesh_opaque: _bascenev1.Mesh | None = None
mesh_transparent: _bascenev1.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: _bascenev1.Texture | None = None
mini_billboard_1_start_time: int = 0
mini_billboard_1_end_time: int = 0
mini_billboard_2_texture: _bascenev1.Texture | None = None
mini_billboard_2_start_time: int = 0
mini_billboard_2_end_time: int = 0
mini_billboard_3_texture: _bascenev1.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: _bascenev1.Texture | None = None
shattered: int = 0
billboard_texture: _bascenev1.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: _bascenev1.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: _bascenev1.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

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

@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: _bascenev1.Node)
node: _bascenev1.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: _bascenev1.Node)
node: _bascenev1.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: _bascenev1.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: _bascenev1.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: _babase.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:
292@dataclass
293class PlayerProfilesChangedMessage:
294    """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

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            # pylint: disable=too-many-positional-arguments
201            from bascenev1lib.actor.popuptext import PopupText
202
203            # Only award this if they're still alive and we can get
204            # a current position for them.
205            our_pos: babase.Vec3 | None = None
206            if self._sessionplayer:
207                if self._sessionplayer.activityplayer is not None:
208                    try:
209                        our_pos = self._sessionplayer.activityplayer.position
210                    except babase.NotFoundError:
211                        pass
212            if our_pos is None:
213                return
214
215            # Jitter position a bit since these often come in clusters.
216            our_pos = babase.Vec3(
217                our_pos[0] + (random.random() - 0.5) * 2.0,
218                our_pos[1] + (random.random() - 0.5) * 2.0,
219                our_pos[2] + (random.random() - 0.5) * 2.0,
220            )
221            activity = self.getactivity()
222            if activity is not None:
223                PopupText(
224                    babase.Lstr(
225                        value=(('+' + str(score2) + ' ') if showpoints2 else '')
226                        + '${N}',
227                        subs=[('${N}', name2)],
228                    ),
229                    color=color2,
230                    scale=scale2,
231                    position=our_pos,
232                ).autoretain()
233            if sound2:
234                sound2.play()
235
236            self.score += score2
237            self.accumscore += score2
238
239            # Inform a running game of the score.
240            if score2 != 0 and activity is not None:
241                activity.handlemessage(PlayerScoredMessage(score=score2))
242
243        if name is not None:
244            _bascenev1.timer(
245                0.3 + delay,
246                babase.Call(
247                    _apply, name, score, showpoints, color, scale, sound
248                ),
249            )
250
251        # Keep the tally rollin'...
252        # set a timer for a bit in the future.
253        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: _bascenev1.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: _bascenev1.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: _bascenev1.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) -> _bascenev1.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            # pylint: disable=too-many-positional-arguments
201            from bascenev1lib.actor.popuptext import PopupText
202
203            # Only award this if they're still alive and we can get
204            # a current position for them.
205            our_pos: babase.Vec3 | None = None
206            if self._sessionplayer:
207                if self._sessionplayer.activityplayer is not None:
208                    try:
209                        our_pos = self._sessionplayer.activityplayer.position
210                    except babase.NotFoundError:
211                        pass
212            if our_pos is None:
213                return
214
215            # Jitter position a bit since these often come in clusters.
216            our_pos = babase.Vec3(
217                our_pos[0] + (random.random() - 0.5) * 2.0,
218                our_pos[1] + (random.random() - 0.5) * 2.0,
219                our_pos[2] + (random.random() - 0.5) * 2.0,
220            )
221            activity = self.getactivity()
222            if activity is not None:
223                PopupText(
224                    babase.Lstr(
225                        value=(('+' + str(score2) + ' ') if showpoints2 else '')
226                        + '${N}',
227                        subs=[('${N}', name2)],
228                    ),
229                    color=color2,
230                    scale=scale2,
231                    position=our_pos,
232                ).autoretain()
233            if sound2:
234                sound2.play()
235
236            self.score += score2
237            self.accumscore += score2
238
239            # Inform a running game of the score.
240            if score2 != 0 and activity is not None:
241                activity.handlemessage(PlayerScoredMessage(score=score2))
242
243        if name is not None:
244            _bascenev1.timer(
245                0.3 + delay,
246                babase.Call(
247                    _apply, name, score, showpoints, color, scale, sound
248                ),
249            )
250
251        # Keep the tally rollin'...
252        # set a timer for a bit in the future.
253        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: _bascenev1.Node | None = None)
poweruptype: str

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

sourcenode: _bascenev1.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:
1569def printnodes() -> None:
1570    """Print various info about existing nodes; useful for debugging.
1571
1572    Category: **Gameplay Functions**
1573    """
1574    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:
1413def pushcall(
1414    call: Callable,
1415    from_other_thread: bool = False,
1416    suppress_other_thread_warning: bool = False,
1417    other_thread_use_fg_context: bool = False,
1418    raw: bool = False,
1419) -> None:
1420    """Push a call to the logic event-loop.
1421    Category: **General Utility Functions**
1422
1423    This call expects to be used in the logic thread, and will automatically
1424    save and restore the babase.Context to behave seamlessly.
1425
1426    If you want to push a call from outside of the logic thread,
1427    however, you can pass 'from_other_thread' as True. In this case
1428    the call will always run in the UI context_ref on the logic thread
1429    or whichever context_ref is in the foreground if
1430    other_thread_use_fg_context is True.
1431    Passing raw=True will disable thread checks and context_ref sets/restores.
1432    """
1433    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, ...]:
1487def safecolor(
1488    color: Sequence[float], target_intensity: float = 0.6
1489) -> tuple[float, ...]:
1490    """Given a color tuple, return a color safe to display as text.
1491
1492    Category: **General Utility Functions**
1493
1494    Accepts tuples of length 3 or 4. This will slightly brighten very
1495    dark colors, etc.
1496    """
1497    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:
1500def screenmessage(
1501    message: str | babase.Lstr,
1502    color: Sequence[float] | None = None,
1503    log: bool = False,
1504) -> None:
1505    """Print a message to the local client's screen, in a given color.
1506
1507    Category: **General Utility Functions**
1508
1509    Note that this version of the function is purely for local display.
1510    To broadcast screen messages in network play, look for methods such as
1511    broadcastmessage() provided by the scene-version packages.
1512    """
1513    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.

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

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

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)
147    def __init__(self, settings: dict):
148        super().__init__(settings)
149        self._birth_time = babase.apptime()
150        self._min_view_time = 5.0
151        self._allow_server_transition = False
152        self._background: bascenev1.Actor | None = None
153        self._tips_text: bascenev1.Actor | None = None
154        self._kicked_off_server_shutdown = False
155        self._kicked_off_server_restart = False
156        self._default_show_tips = True
157        self._custom_continue_message: babase.Lstr | None = None
158        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:
160    @override
161    def on_player_join(self, player: EmptyPlayer) -> None:
162        super().on_player_join(player)
163        time_till_assign = max(
164            0, self._birth_time + self._min_view_time - babase.apptime()
165        )
166
167        # If we're still kicking at the end of our assign-delay, assign this
168        # guy's input to trigger us.
169        _bascenev1.timer(
170            time_till_assign, babase.WeakCall(self._safe_assign, player)
171        )

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

(including the initial set of Players)

@override
def on_transition_in(self) -> None:
173    @override
174    def on_transition_in(self) -> None:
175        from bascenev1lib.actor.tipstext import TipsText
176        from bascenev1lib.actor.background import Background
177
178        super().on_transition_in()
179        self._background = Background(
180            fade_time=0.5, start_faded=False, show_logo=True
181        )
182        if self._default_show_tips:
183            self._tips_text = TipsText()
184        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:
186    @override
187    def on_begin(self) -> None:
188        # pylint: disable=cyclic-import
189        from bascenev1lib.actor.text import Text
190
191        super().on_begin()
192
193        # Pop up a 'press any button to continue' statement after our
194        # min-view-time show a 'press any button to continue..'
195        # thing after a bit.
196        assert babase.app.classic is not None
197        if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
198            # FIXME: Need a better way to determine whether we've probably
199            #  got a keyboard.
200            sval = babase.Lstr(resource='pressAnyKeyButtonText')
201        else:
202            sval = babase.Lstr(resource='pressAnyButtonText')
203
204        Text(
205            (
206                self._custom_continue_message
207                if self._custom_continue_message is not None
208                else sval
209            ),
210            v_attach=Text.VAttach.BOTTOM,
211            h_align=Text.HAlign.CENTER,
212            flash=True,
213            vr_depth=50,
214            position=(0, 10),
215            scale=0.8,
216            color=(0.5, 0.7, 0.5, 0.5),
217            transition=Text.Transition.IN_BOTTOM_SLOW,
218            transition_delay=self._min_view_time,
219        ).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'>
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        *,
101        team_names: Sequence[str] | None = None,
102        team_colors: Sequence[Sequence[float]] | None = None,
103        min_players: int = 1,
104        max_players: int = 8,
105        submit_score: bool = True,
106    ):
107        """Instantiate a session.
108
109        depsets should be a sequence of successfully resolved
110        bascenev1.DependencySet instances; one for each bascenev1.Activity
111        the session may potentially run.
112        """
113        # pylint: disable=too-many-statements
114        # pylint: disable=too-many-locals
115        # pylint: disable=cyclic-import
116        # pylint: disable=too-many-branches
117        from efro.util import empty_weakref
118        from bascenev1._dependency import (
119            Dependency,
120            AssetPackage,
121            DependencyError,
122        )
123        from bascenev1._lobby import Lobby
124        from bascenev1._stats import Stats
125        from bascenev1._gameactivity import GameActivity
126        from bascenev1._activity import Activity
127        from bascenev1._team import SessionTeam
128
129        # First off, resolve all dependency-sets we were passed.
130        # If things are missing, we'll try to gather them into a single
131        # missing-deps exception if possible to give the caller a clean
132        # path to download missing stuff and try again.
133        missing_asset_packages: set[str] = set()
134        for depset in depsets:
135            try:
136                depset.resolve()
137            except DependencyError as exc:
138                # Gather/report missing assets only; barf on anything else.
139                if all(issubclass(d.cls, AssetPackage) for d in exc.deps):
140                    for dep in exc.deps:
141                        assert isinstance(dep.config, str)
142                        missing_asset_packages.add(dep.config)
143                else:
144                    missing_info = [(d.cls, d.config) for d in exc.deps]
145                    raise RuntimeError(
146                        f'Missing non-asset dependencies: {missing_info}'
147                    ) from exc
148
149        # Throw a combined exception if we found anything missing.
150        if missing_asset_packages:
151            raise DependencyError(
152                [
153                    Dependency(AssetPackage, set_id)
154                    for set_id in missing_asset_packages
155                ]
156            )
157
158        # Ok; looks like our dependencies check out.
159        # Now give the engine a list of asset-set-ids to pass along to clients.
160        required_asset_packages: set[str] = set()
161        for depset in depsets:
162            required_asset_packages.update(depset.get_asset_package_ids())
163
164        # print('Would set host-session asset-reqs to:',
165        # required_asset_packages)
166
167        # Init our C++ layer data.
168        self._sessiondata = _bascenev1.register_session(self)
169
170        # Should remove this if possible.
171        self.tournament_id: str | None = None
172
173        self.sessionteams = []
174        self.sessionplayers = []
175        self.min_players = min_players
176        self.max_players = (
177            max_players
178            if _max_players_override is None
179            else _max_players_override
180        )
181        self.submit_score = submit_score
182
183        self.customdata = {}
184        self._in_set_activity = False
185        self._next_team_id = 0
186        self._activity_retained: bascenev1.Activity | None = None
187        self._launch_end_session_activity_time: float | None = None
188        self._activity_end_timer: bascenev1.BaseTimer | None = None
189        self._activity_weak = empty_weakref(Activity)
190        self._next_activity: bascenev1.Activity | None = None
191        self._wants_to_end = False
192        self._ending = False
193        self._activity_should_end_immediately = False
194        self._activity_should_end_immediately_results: (
195            bascenev1.GameResults | None
196        ) = None
197        self._activity_should_end_immediately_delay = 0.0
198
199        # Create static teams if we're using them.
200        if self.use_teams:
201            if team_names is None:
202                raise RuntimeError(
203                    'use_teams is True but team_names not provided.'
204                )
205            if team_colors is None:
206                raise RuntimeError(
207                    'use_teams is True but team_colors not provided.'
208                )
209            if len(team_colors) != len(team_names):
210                raise RuntimeError(
211                    f'Got {len(team_names)} team_names'
212                    f' and {len(team_colors)} team_colors;'
213                    f' these numbers must match.'
214                )
215            for i, color in enumerate(team_colors):
216                team = SessionTeam(
217                    team_id=self._next_team_id,
218                    name=GameActivity.get_team_display_string(team_names[i]),
219                    color=color,
220                )
221                self.sessionteams.append(team)
222                self._next_team_id += 1
223                try:
224                    with self.context:
225                        self.on_team_join(team)
226                except Exception:
227                    logging.exception('Error in on_team_join for %s.', self)
228
229        self.lobby = Lobby()
230        self.stats = Stats()
231
232        # Instantiate our session globals node which will apply its settings.
233        self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
234
235        # Rejoin cooldown stuff.
236        self._players_on_wait: dict = {}
237        self._player_requested_identifiers: dict = {}
238        self._waitlist_timers: dict = {}
239
240    @property
241    def context(self) -> bascenev1.ContextRef:
242        """A context-ref pointing at this activity."""
243        return self._sessiondata.context()
244
245    @property
246    def sessionglobalsnode(self) -> bascenev1.Node:
247        """The sessionglobals bascenev1.Node for the session."""
248        node = self._sessionglobalsnode
249        if not node:
250            raise babase.NodeNotFoundError()
251        return node
252
253    def should_allow_mid_activity_joins(
254        self, activity: bascenev1.Activity
255    ) -> bool:
256        """Ask ourself if we should allow joins during an Activity.
257
258        Note that for a join to be allowed, both the Session and Activity
259        have to be ok with it (via this function and the
260        Activity.allow_mid_activity_joins property.
261        """
262        del activity  # Unused.
263        return True
264
265    def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
266        """Called when a new bascenev1.Player wants to join the Session.
267
268        This should return True or False to accept/reject.
269        """
270        # Limit player counts *unless* we're in a stress test.
271        if (
272            babase.app.classic is not None
273            and babase.app.classic.stress_test_update_timer is None
274        ):
275            if len(self.sessionplayers) >= self.max_players >= 0:
276                # Print a rejection message *only* to the client trying to
277                # join (prevents spamming everyone else in the game).
278                _bascenev1.getsound('error').play()
279                _bascenev1.broadcastmessage(
280                    babase.Lstr(
281                        resource='playerLimitReachedText',
282                        subs=[('${COUNT}', str(self.max_players))],
283                    ),
284                    color=(0.8, 0.0, 0.0),
285                    clients=[player.inputdevice.client_id],
286                    transient=True,
287                )
288                return False
289
290        # Rejoin cooldown.
291        identifier = player.get_v1_account_id()
292        if identifier:
293            leave_time = self._players_on_wait.get(identifier)
294            if leave_time:
295                diff = str(
296                    math.ceil(
297                        _g_player_rejoin_cooldown
298                        - babase.apptime()
299                        + leave_time
300                    )
301                )
302                _bascenev1.broadcastmessage(
303                    babase.Lstr(
304                        translate=(
305                            'serverResponses',
306                            'You can join in ${COUNT} seconds.',
307                        ),
308                        subs=[('${COUNT}', diff)],
309                    ),
310                    color=(1, 1, 0),
311                    clients=[player.inputdevice.client_id],
312                    transient=True,
313                )
314                return False
315            self._player_requested_identifiers[player.id] = identifier
316
317        _bascenev1.getsound('dripity').play()
318        return True
319
320    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
321        """Called when a previously-accepted bascenev1.SessionPlayer leaves."""
322
323        if sessionplayer not in self.sessionplayers:
324            print(
325                'ERROR: Session.on_player_leave called'
326                ' for player not in our list.'
327            )
328            return
329
330        _bascenev1.getsound('playerLeft').play()
331
332        activity = self._activity_weak()
333
334        # Rejoin cooldown.
335        identifier = self._player_requested_identifiers.get(sessionplayer.id)
336        if identifier:
337            self._players_on_wait[identifier] = babase.apptime()
338            with babase.ContextRef.empty():
339                self._waitlist_timers[identifier] = babase.AppTimer(
340                    _g_player_rejoin_cooldown,
341                    babase.Call(self._remove_player_from_waitlist, identifier),
342                )
343
344        if not sessionplayer.in_game:
345            # Ok, the player is still in the lobby; simply remove them.
346            with self.context:
347                try:
348                    self.lobby.remove_chooser(sessionplayer)
349                except Exception:
350                    logging.exception('Error in Lobby.remove_chooser().')
351        else:
352            # Ok, they've already entered the game. Remove them from
353            # teams/activities/etc.
354            sessionteam = sessionplayer.sessionteam
355            assert sessionteam is not None
356
357            _bascenev1.broadcastmessage(
358                babase.Lstr(
359                    resource='playerLeftText',
360                    subs=[('${PLAYER}', sessionplayer.getname(full=True))],
361                )
362            )
363
364            # Remove them from their SessionTeam.
365            if sessionplayer in sessionteam.players:
366                sessionteam.players.remove(sessionplayer)
367            else:
368                print(
369                    'SessionPlayer not found in SessionTeam'
370                    ' in on_player_leave.'
371                )
372
373            # Grab their activity-specific player instance.
374            player = sessionplayer.activityplayer
375            assert isinstance(player, (Player, type(None)))
376
377            # Remove them from any current Activity.
378            if player is not None and activity is not None:
379                if player in activity.players:
380                    activity.remove_player(sessionplayer)
381                else:
382                    print('Player not found in Activity in on_player_leave.')
383
384            # If we're a non-team session, remove their team too.
385            if not self.use_teams:
386                self._remove_player_team(sessionteam, activity)
387
388        # Now remove them from the session list.
389        self.sessionplayers.remove(sessionplayer)
390
391    def _remove_player_team(
392        self,
393        sessionteam: bascenev1.SessionTeam,
394        activity: bascenev1.Activity | None,
395    ) -> None:
396        """Remove the player-specific team in non-teams mode."""
397
398        # They should have been the only one on their team.
399        assert not sessionteam.players
400
401        # Remove their Team from the Activity.
402        if activity is not None:
403            if sessionteam.activityteam in activity.teams:
404                activity.remove_team(sessionteam)
405            else:
406                print('Team not found in Activity in on_player_leave.')
407
408        # And then from the Session.
409        with self.context:
410            if sessionteam in self.sessionteams:
411                try:
412                    self.sessionteams.remove(sessionteam)
413                    self.on_team_leave(sessionteam)
414                except Exception:
415                    logging.exception(
416                        'Error in on_team_leave for Session %s.', self
417                    )
418            else:
419                print('Team no in Session teams in on_player_leave.')
420            try:
421                sessionteam.leave()
422            except Exception:
423                logging.exception(
424                    'Error clearing sessiondata for team %s in session %s.',
425                    sessionteam,
426                    self,
427                )
428
429    def end(self) -> None:
430        """Initiates an end to the session and a return to the main menu.
431
432        Note that this happens asynchronously, allowing the
433        session and its activities to shut down gracefully.
434        """
435        self._wants_to_end = True
436        if self._next_activity is None:
437            self._launch_end_session_activity()
438
439    def _launch_end_session_activity(self) -> None:
440        """(internal)"""
441        from bascenev1._activitytypes import EndSessionActivity
442
443        with self.context:
444            curtime = babase.apptime()
445            if self._ending:
446                # Ignore repeats unless its been a while.
447                assert self._launch_end_session_activity_time is not None
448                since_last = curtime - self._launch_end_session_activity_time
449                if since_last < 30.0:
450                    return
451                logging.error(
452                    '_launch_end_session_activity called twice (since_last=%s)',
453                    since_last,
454                )
455            self._launch_end_session_activity_time = curtime
456            self.setactivity(_bascenev1.newactivity(EndSessionActivity))
457            self._wants_to_end = False
458            self._ending = True  # Prevent further actions.
459
460    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
461        """Called when a new bascenev1.Team joins the session."""
462
463    def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
464        """Called when a bascenev1.Team is leaving the session."""
465
466    def end_activity(
467        self,
468        activity: bascenev1.Activity,
469        results: Any,
470        delay: float,
471        force: bool,
472    ) -> None:
473        """Commence shutdown of a bascenev1.Activity (if not already occurring).
474
475        'delay' is the time delay before the Activity actually ends
476        (in seconds). Further calls to end() will be ignored up until
477        this time, unless 'force' is True, in which case the new results
478        will replace the old.
479        """
480        # Only pay attention if this is coming from our current activity.
481        if activity is not self._activity_retained:
482            return
483
484        # If this activity hasn't begun yet, just set it up to end immediately
485        # once it does.
486        if not activity.has_begun():
487            # activity.set_immediate_end(results, delay, force)
488            if not self._activity_should_end_immediately or force:
489                self._activity_should_end_immediately = True
490                self._activity_should_end_immediately_results = results
491                self._activity_should_end_immediately_delay = delay
492
493        # The activity has already begun; get ready to end it.
494        else:
495            if (not activity.has_ended()) or force:
496                activity.set_has_ended(True)
497
498                # Set a timer to set in motion this activity's demise.
499                self._activity_end_timer = _bascenev1.BaseTimer(
500                    delay,
501                    babase.Call(self._complete_end_activity, activity, results),
502                )
503
504    def handlemessage(self, msg: Any) -> Any:
505        """General message handling; can be passed any message object."""
506        from bascenev1._lobby import PlayerReadyMessage
507        from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
508
509        if isinstance(msg, PlayerReadyMessage):
510            self._on_player_ready(msg.chooser)
511
512        elif isinstance(msg, PlayerProfilesChangedMessage):
513            # If we have a current activity with a lobby, ask it to reload
514            # profiles.
515            with self.context:
516                self.lobby.reload_profiles()
517            return None
518
519        else:
520            return UNHANDLED
521        return None
522
523    class _SetActivityScopedLock:
524        def __init__(self, session: Session) -> None:
525            self._session = session
526            if session._in_set_activity:
527                raise RuntimeError('Session.setactivity() called recursively.')
528            self._session._in_set_activity = True
529
530        def __del__(self) -> None:
531            self._session._in_set_activity = False
532
533    def setactivity(self, activity: bascenev1.Activity) -> None:
534        """Assign a new current bascenev1.Activity for the session.
535
536        Note that this will not change the current context to the new
537        Activity's. Code must be run in the new activity's methods
538        (on_transition_in, etc) to get it. (so you can't do
539        session.setactivity(foo) and then bascenev1.newnode() to add a node
540        to foo)
541        """
542
543        # Make sure we don't get called recursively.
544        _rlock = self._SetActivityScopedLock(self)
545
546        if activity.session is not _bascenev1.getsession():
547            raise RuntimeError("Provided Activity's Session is not current.")
548
549        # Quietly ignore this if the whole session is going down.
550        if self._ending:
551            return
552
553        if activity is self._activity_retained:
554            logging.error('Activity set to already-current activity.')
555            return
556
557        if self._next_activity is not None:
558            raise RuntimeError(
559                'Activity switch already in progress (to '
560                + str(self._next_activity)
561                + ')'
562            )
563
564        prev_activity = self._activity_retained
565        prev_globals = (
566            prev_activity.globalsnode if prev_activity is not None else None
567        )
568
569        # Let the activity do its thing.
570        activity.transition_in(prev_globals)
571
572        self._next_activity = activity
573
574        # If we have a current activity, tell it it's transitioning out;
575        # the next one will become current once this one dies.
576        if prev_activity is not None:
577            prev_activity.transition_out()
578
579            # Setting this to None should free up the old activity to die,
580            # which will call begin_next_activity.
581            # We can still access our old activity through
582            # self._activity_weak() to keep it up to date on player
583            # joins/departures/etc until it dies.
584            self._activity_retained = None
585
586        # There's no existing activity; lets just go ahead with the begin call.
587        else:
588            self.begin_next_activity()
589
590        # We want to call destroy() for the previous activity once it should
591        # tear itself down, clear out any self-refs, etc. After this call
592        # the activity should have no refs left to it and should die (which
593        # will trigger the next activity to run).
594        if prev_activity is not None:
595            with babase.ContextRef.empty():
596                babase.apptimer(
597                    max(0.0, activity.transition_time), prev_activity.expire
598                )
599        self._in_set_activity = False
600
601    def getactivity(self) -> bascenev1.Activity | None:
602        """Return the current foreground activity for this session."""
603        return self._activity_weak()
604
605    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
606        """Subclasses can override this to provide custom menu entries.
607
608        The returned value should be a list of dicts, each containing
609        a 'label' and 'call' entry, with 'label' being the text for
610        the entry and 'call' being the callable to trigger if the entry
611        is pressed.
612        """
613        return []
614
615    def _complete_end_activity(
616        self, activity: bascenev1.Activity, results: Any
617    ) -> None:
618        # Run the subclass callback in the session context.
619        try:
620            with self.context:
621                self.on_activity_end(activity, results)
622        except Exception:
623            logging.error(
624                'Error in on_activity_end() for session %s'
625                ' activity %s with results %s',
626                self,
627                activity,
628                results,
629            )
630
631    def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool:
632        """Called by the native layer when a player wants to join."""
633
634        # If we're ending, allow no new players.
635        if self._ending:
636            return False
637
638        # Ask the bascenev1.Session subclass to approve/deny this request.
639        try:
640            with self.context:
641                result = self.on_player_request(sessionplayer)
642        except Exception:
643            logging.exception('Error in on_player_request for %s.', self)
644            result = False
645
646        # If they said yes, add the player to the lobby.
647        if result:
648            self.sessionplayers.append(sessionplayer)
649            with self.context:
650                try:
651                    self.lobby.add_chooser(sessionplayer)
652                except Exception:
653                    logging.exception('Error in lobby.add_chooser().')
654
655        return result
656
657    def on_activity_end(
658        self, activity: bascenev1.Activity, results: Any
659    ) -> None:
660        """Called when the current bascenev1.Activity has ended.
661
662        The bascenev1.Session should look at the results and start
663        another bascenev1.Activity.
664        """
665
666    def begin_next_activity(self) -> None:
667        """Called once the previous activity has been totally torn down.
668
669        This means we're ready to begin the next one
670        """
671        if self._next_activity is None:
672            # Should this ever happen?
673            logging.error('begin_next_activity() called with no _next_activity')
674            return
675
676        # We store both a weak and a strong ref to the new activity;
677        # the strong is to keep it alive and the weak is so we can access
678        # it even after we've released the strong-ref to allow it to die.
679        self._activity_retained = self._next_activity
680        self._activity_weak = weakref.ref(self._next_activity)
681        self._next_activity = None
682        self._activity_should_end_immediately = False
683
684        # Kick out anyone loitering in the lobby.
685        self.lobby.remove_all_choosers_and_kick_players()
686
687        # Kick off the activity.
688        self._activity_retained.begin(self)
689
690        # If we want to completely end the session, we can now kick that off.
691        if self._wants_to_end:
692            self._launch_end_session_activity()
693        else:
694            # Otherwise, if the activity has already been told to end,
695            # do so now.
696            if self._activity_should_end_immediately:
697                self._activity_retained.end(
698                    self._activity_should_end_immediately_results,
699                    self._activity_should_end_immediately_delay,
700                )
701
702    def _on_player_ready(self, chooser: bascenev1.Chooser) -> None:
703        """Called when a bascenev1.Player has checked themself ready."""
704        lobby = chooser.lobby
705        activity = self._activity_weak()
706
707        # This happens sometimes. That seems like it shouldn't be happening;
708        # when would we have a session and a chooser with players but no
709        # active activity?
710        if activity is None:
711            print('_on_player_ready called with no activity.')
712            return
713
714        # In joining-activities, we wait till all choosers are ready
715        # and then create all players at once.
716        if activity.is_joining_activity:
717            if not lobby.check_all_ready():
718                return
719            choosers = lobby.get_choosers()
720            min_players = self.min_players
721            if len(choosers) >= min_players:
722                for lch in lobby.get_choosers():
723                    self._add_chosen_player(lch)
724                lobby.remove_all_choosers()
725
726                # Get our next activity going.
727                self._complete_end_activity(activity, {})
728            else:
729                _bascenev1.broadcastmessage(
730                    babase.Lstr(
731                        resource='notEnoughPlayersText',
732                        subs=[('${COUNT}', str(min_players))],
733                    ),
734                    color=(1, 1, 0),
735                )
736                _bascenev1.getsound('error').play()
737
738        # Otherwise just add players on the fly.
739        else:
740            self._add_chosen_player(chooser)
741            lobby.remove_chooser(chooser.getplayer())
742
743    def transitioning_out_activity_was_freed(
744        self, can_show_ad_on_death: bool
745    ) -> None:
746        """(internal)"""
747        # pylint: disable=cyclic-import
748
749        # Since things should be generally still right now, it's a good time
750        # to run garbage collection to clear out any circular dependency
751        # loops. We keep this disabled normally to avoid non-deterministic
752        # hitches.
753        babase.garbage_collect()
754
755        assert babase.app.classic is not None
756        with self.context:
757            if can_show_ad_on_death:
758                babase.app.classic.ads.call_after_ad(self.begin_next_activity)
759            else:
760                babase.pushcall(self.begin_next_activity)
761
762    def _add_chosen_player(
763        self, chooser: bascenev1.Chooser
764    ) -> bascenev1.SessionPlayer:
765        from bascenev1._team import SessionTeam
766
767        sessionplayer = chooser.getplayer()
768        assert sessionplayer in self.sessionplayers, (
769            'SessionPlayer not found in session '
770            'player-list after chooser selection.'
771        )
772
773        activity = self._activity_weak()
774        assert activity is not None
775
776        # Reset the player's input here, as it is probably
777        # referencing the chooser which could inadvertently keep it alive.
778        sessionplayer.resetinput()
779
780        # We can pass it to the current activity if it has already begun
781        # (otherwise it'll get passed once begin is called).
782        pass_to_activity = (
783            activity.has_begun() and not activity.is_joining_activity
784        )
785
786        # However, if we're not allowing mid-game joins, don't actually pass;
787        # just announce the arrival and say they'll partake next round.
788        if pass_to_activity:
789            if not (
790                activity.allow_mid_activity_joins
791                and self.should_allow_mid_activity_joins(activity)
792            ):
793                pass_to_activity = False
794                with self.context:
795                    _bascenev1.broadcastmessage(
796                        babase.Lstr(
797                            resource='playerDelayedJoinText',
798                            subs=[
799                                ('${PLAYER}', sessionplayer.getname(full=True))
800                            ],
801                        ),
802                        color=(0, 1, 0),
803                    )
804
805        # If we're a non-team session, each player gets their own team.
806        # (keeps mini-game coding simpler if we can always deal with teams).
807        if self.use_teams:
808            sessionteam = chooser.sessionteam
809        else:
810            our_team_id = self._next_team_id
811            self._next_team_id += 1
812            sessionteam = SessionTeam(
813                team_id=our_team_id,
814                color=chooser.get_color(),
815                name=chooser.getplayer().getname(full=True, icon=False),
816            )
817
818            # Add player's team to the Session.
819            self.sessionteams.append(sessionteam)
820
821            with self.context:
822                try:
823                    self.on_team_join(sessionteam)
824                except Exception:
825                    logging.exception('Error in on_team_join for %s.', self)
826
827            # Add player's team to the Activity.
828            if pass_to_activity:
829                activity.add_team(sessionteam)
830
831        assert sessionplayer not in sessionteam.players
832        sessionteam.players.append(sessionplayer)
833        sessionplayer.setdata(
834            team=sessionteam,
835            character=chooser.get_character_name(),
836            color=chooser.get_color(),
837            highlight=chooser.get_highlight(),
838        )
839
840        self.stats.register_sessionplayer(sessionplayer)
841        if pass_to_activity:
842            activity.add_player(sessionplayer)
843        return sessionplayer
844
845    def _remove_player_from_waitlist(self, identifier: str) -> None:
846        try:
847            self._players_on_wait.pop(identifier)
848        except KeyError:
849            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        *,
101        team_names: Sequence[str] | None = None,
102        team_colors: Sequence[Sequence[float]] | None = None,
103        min_players: int = 1,
104        max_players: int = 8,
105        submit_score: bool = True,
106    ):
107        """Instantiate a session.
108
109        depsets should be a sequence of successfully resolved
110        bascenev1.DependencySet instances; one for each bascenev1.Activity
111        the session may potentially run.
112        """
113        # pylint: disable=too-many-statements
114        # pylint: disable=too-many-locals
115        # pylint: disable=cyclic-import
116        # pylint: disable=too-many-branches
117        from efro.util import empty_weakref
118        from bascenev1._dependency import (
119            Dependency,
120            AssetPackage,
121            DependencyError,
122        )
123        from bascenev1._lobby import Lobby
124        from bascenev1._stats import Stats
125        from bascenev1._gameactivity import GameActivity
126        from bascenev1._activity import Activity
127        from bascenev1._team import SessionTeam
128
129        # First off, resolve all dependency-sets we were passed.
130        # If things are missing, we'll try to gather them into a single
131        # missing-deps exception if possible to give the caller a clean
132        # path to download missing stuff and try again.
133        missing_asset_packages: set[str] = set()
134        for depset in depsets:
135            try:
136                depset.resolve()
137            except DependencyError as exc:
138                # Gather/report missing assets only; barf on anything else.
139                if all(issubclass(d.cls, AssetPackage) for d in exc.deps):
140                    for dep in exc.deps:
141                        assert isinstance(dep.config, str)
142                        missing_asset_packages.add(dep.config)
143                else:
144                    missing_info = [(d.cls, d.config) for d in exc.deps]
145                    raise RuntimeError(
146                        f'Missing non-asset dependencies: {missing_info}'
147                    ) from exc
148
149        # Throw a combined exception if we found anything missing.
150        if missing_asset_packages:
151            raise DependencyError(
152                [
153                    Dependency(AssetPackage, set_id)
154                    for set_id in missing_asset_packages
155                ]
156            )
157
158        # Ok; looks like our dependencies check out.
159        # Now give the engine a list of asset-set-ids to pass along to clients.
160        required_asset_packages: set[str] = set()
161        for depset in depsets:
162            required_asset_packages.update(depset.get_asset_package_ids())
163
164        # print('Would set host-session asset-reqs to:',
165        # required_asset_packages)
166
167        # Init our C++ layer data.
168        self._sessiondata = _bascenev1.register_session(self)
169
170        # Should remove this if possible.
171        self.tournament_id: str | None = None
172
173        self.sessionteams = []
174        self.sessionplayers = []
175        self.min_players = min_players
176        self.max_players = (
177            max_players
178            if _max_players_override is None
179            else _max_players_override
180        )
181        self.submit_score = submit_score
182
183        self.customdata = {}
184        self._in_set_activity = False
185        self._next_team_id = 0
186        self._activity_retained: bascenev1.Activity | None = None
187        self._launch_end_session_activity_time: float | None = None
188        self._activity_end_timer: bascenev1.BaseTimer | None = None
189        self._activity_weak = empty_weakref(Activity)
190        self._next_activity: bascenev1.Activity | None = None
191        self._wants_to_end = False
192        self._ending = False
193        self._activity_should_end_immediately = False
194        self._activity_should_end_immediately_results: (
195            bascenev1.GameResults | None
196        ) = None
197        self._activity_should_end_immediately_delay = 0.0
198
199        # Create static teams if we're using them.
200        if self.use_teams:
201            if team_names is None:
202                raise RuntimeError(
203                    'use_teams is True but team_names not provided.'
204                )
205            if team_colors is None:
206                raise RuntimeError(
207                    'use_teams is True but team_colors not provided.'
208                )
209            if len(team_colors) != len(team_names):
210                raise RuntimeError(
211                    f'Got {len(team_names)} team_names'
212                    f' and {len(team_colors)} team_colors;'
213                    f' these numbers must match.'
214                )
215            for i, color in enumerate(team_colors):
216                team = SessionTeam(
217                    team_id=self._next_team_id,
218                    name=GameActivity.get_team_display_string(team_names[i]),
219                    color=color,
220                )
221                self.sessionteams.append(team)
222                self._next_team_id += 1
223                try:
224                    with self.context:
225                        self.on_team_join(team)
226                except Exception:
227                    logging.exception('Error in on_team_join for %s.', self)
228
229        self.lobby = Lobby()
230        self.stats = Stats()
231
232        # Instantiate our session globals node which will apply its settings.
233        self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
234
235        # Rejoin cooldown stuff.
236        self._players_on_wait: dict = {}
237        self._player_requested_identifiers: dict = {}
238        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[_bascenev1.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: _babase.ContextRef
240    @property
241    def context(self) -> bascenev1.ContextRef:
242        """A context-ref pointing at this activity."""
243        return self._sessiondata.context()

A context-ref pointing at this activity.

sessionglobalsnode: _bascenev1.Node
245    @property
246    def sessionglobalsnode(self) -> bascenev1.Node:
247        """The sessionglobals bascenev1.Node for the session."""
248        node = self._sessionglobalsnode
249        if not node:
250            raise babase.NodeNotFoundError()
251        return node

The sessionglobals bascenev1.Node for the session.

def should_allow_mid_activity_joins(self, activity: Activity) -> bool:
253    def should_allow_mid_activity_joins(
254        self, activity: bascenev1.Activity
255    ) -> bool:
256        """Ask ourself if we should allow joins during an Activity.
257
258        Note that for a join to be allowed, both the Session and Activity
259        have to be ok with it (via this function and the
260        Activity.allow_mid_activity_joins property.
261        """
262        del activity  # Unused.
263        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: _bascenev1.SessionPlayer) -> bool:
265    def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
266        """Called when a new bascenev1.Player wants to join the Session.
267
268        This should return True or False to accept/reject.
269        """
270        # Limit player counts *unless* we're in a stress test.
271        if (
272            babase.app.classic is not None
273            and babase.app.classic.stress_test_update_timer is None
274        ):
275            if len(self.sessionplayers) >= self.max_players >= 0:
276                # Print a rejection message *only* to the client trying to
277                # join (prevents spamming everyone else in the game).
278                _bascenev1.getsound('error').play()
279                _bascenev1.broadcastmessage(
280                    babase.Lstr(
281                        resource='playerLimitReachedText',
282                        subs=[('${COUNT}', str(self.max_players))],
283                    ),
284                    color=(0.8, 0.0, 0.0),
285                    clients=[player.inputdevice.client_id],
286                    transient=True,
287                )
288                return False
289
290        # Rejoin cooldown.
291        identifier = player.get_v1_account_id()
292        if identifier:
293            leave_time = self._players_on_wait.get(identifier)
294            if leave_time:
295                diff = str(
296                    math.ceil(
297                        _g_player_rejoin_cooldown
298                        - babase.apptime()
299                        + leave_time
300                    )
301                )
302                _bascenev1.broadcastmessage(
303                    babase.Lstr(
304                        translate=(
305                            'serverResponses',
306                            'You can join in ${COUNT} seconds.',
307                        ),
308                        subs=[('${COUNT}', diff)],
309                    ),
310                    color=(1, 1, 0),
311                    clients=[player.inputdevice.client_id],
312                    transient=True,
313                )
314                return False
315            self._player_requested_identifiers[player.id] = identifier
316
317        _bascenev1.getsound('dripity').play()
318        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: _bascenev1.SessionPlayer) -> None:
320    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
321        """Called when a previously-accepted bascenev1.SessionPlayer leaves."""
322
323        if sessionplayer not in self.sessionplayers:
324            print(
325                'ERROR: Session.on_player_leave called'
326                ' for player not in our list.'
327            )
328            return
329
330        _bascenev1.getsound('playerLeft').play()
331
332        activity = self._activity_weak()
333
334        # Rejoin cooldown.
335        identifier = self._player_requested_identifiers.get(sessionplayer.id)
336        if identifier:
337            self._players_on_wait[identifier] = babase.apptime()
338            with babase.ContextRef.empty():
339                self._waitlist_timers[identifier] = babase.AppTimer(
340                    _g_player_rejoin_cooldown,
341                    babase.Call(self._remove_player_from_waitlist, identifier),
342                )
343
344        if not sessionplayer.in_game:
345            # Ok, the player is still in the lobby; simply remove them.
346            with self.context:
347                try:
348                    self.lobby.remove_chooser(sessionplayer)
349                except Exception:
350                    logging.exception('Error in Lobby.remove_chooser().')
351        else:
352            # Ok, they've already entered the game. Remove them from
353            # teams/activities/etc.
354            sessionteam = sessionplayer.sessionteam
355            assert sessionteam is not None
356
357            _bascenev1.broadcastmessage(
358                babase.Lstr(
359                    resource='playerLeftText',
360                    subs=[('${PLAYER}', sessionplayer.getname(full=True))],
361                )
362            )
363
364            # Remove them from their SessionTeam.
365            if sessionplayer in sessionteam.players:
366                sessionteam.players.remove(sessionplayer)
367            else:
368                print(
369                    'SessionPlayer not found in SessionTeam'
370                    ' in on_player_leave.'
371                )
372
373            # Grab their activity-specific player instance.
374            player = sessionplayer.activityplayer
375            assert isinstance(player, (Player, type(None)))
376
377            # Remove them from any current Activity.
378            if player is not None and activity is not None:
379                if player in activity.players:
380                    activity.remove_player(sessionplayer)
381                else:
382                    print('Player not found in Activity in on_player_leave.')
383
384            # If we're a non-team session, remove their team too.
385            if not self.use_teams:
386                self._remove_player_team(sessionteam, activity)
387
388        # Now remove them from the session list.
389        self.sessionplayers.remove(sessionplayer)

Called when a previously-accepted bascenev1.SessionPlayer leaves.

def end(self) -> None:
429    def end(self) -> None:
430        """Initiates an end to the session and a return to the main menu.
431
432        Note that this happens asynchronously, allowing the
433        session and its activities to shut down gracefully.
434        """
435        self._wants_to_end = True
436        if self._next_activity is None:
437            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:
460    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
461        """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:
463    def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
464        """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:
466    def end_activity(
467        self,
468        activity: bascenev1.Activity,
469        results: Any,
470        delay: float,
471        force: bool,
472    ) -> None:
473        """Commence shutdown of a bascenev1.Activity (if not already occurring).
474
475        'delay' is the time delay before the Activity actually ends
476        (in seconds). Further calls to end() will be ignored up until
477        this time, unless 'force' is True, in which case the new results
478        will replace the old.
479        """
480        # Only pay attention if this is coming from our current activity.
481        if activity is not self._activity_retained:
482            return
483
484        # If this activity hasn't begun yet, just set it up to end immediately
485        # once it does.
486        if not activity.has_begun():
487            # activity.set_immediate_end(results, delay, force)
488            if not self._activity_should_end_immediately or force:
489                self._activity_should_end_immediately = True
490                self._activity_should_end_immediately_results = results
491                self._activity_should_end_immediately_delay = delay
492
493        # The activity has already begun; get ready to end it.
494        else:
495            if (not activity.has_ended()) or force:
496                activity.set_has_ended(True)
497
498                # Set a timer to set in motion this activity's demise.
499                self._activity_end_timer = _bascenev1.BaseTimer(
500                    delay,
501                    babase.Call(self._complete_end_activity, activity, results),
502                )

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:
504    def handlemessage(self, msg: Any) -> Any:
505        """General message handling; can be passed any message object."""
506        from bascenev1._lobby import PlayerReadyMessage
507        from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
508
509        if isinstance(msg, PlayerReadyMessage):
510            self._on_player_ready(msg.chooser)
511
512        elif isinstance(msg, PlayerProfilesChangedMessage):
513            # If we have a current activity with a lobby, ask it to reload
514            # profiles.
515            with self.context:
516                self.lobby.reload_profiles()
517            return None
518
519        else:
520            return UNHANDLED
521        return None

General message handling; can be passed any message object.

def setactivity(self, activity: Activity) -> None:
533    def setactivity(self, activity: bascenev1.Activity) -> None:
534        """Assign a new current bascenev1.Activity for the session.
535
536        Note that this will not change the current context to the new
537        Activity's. Code must be run in the new activity's methods
538        (on_transition_in, etc) to get it. (so you can't do
539        session.setactivity(foo) and then bascenev1.newnode() to add a node
540        to foo)
541        """
542
543        # Make sure we don't get called recursively.
544        _rlock = self._SetActivityScopedLock(self)
545
546        if activity.session is not _bascenev1.getsession():
547            raise RuntimeError("Provided Activity's Session is not current.")
548
549        # Quietly ignore this if the whole session is going down.
550        if self._ending:
551            return
552
553        if activity is self._activity_retained:
554            logging.error('Activity set to already-current activity.')
555            return
556
557        if self._next_activity is not None:
558            raise RuntimeError(
559                'Activity switch already in progress (to '
560                + str(self._next_activity)
561                + ')'
562            )
563
564        prev_activity = self._activity_retained
565        prev_globals = (
566            prev_activity.globalsnode if prev_activity is not None else None
567        )
568
569        # Let the activity do its thing.
570        activity.transition_in(prev_globals)
571
572        self._next_activity = activity
573
574        # If we have a current activity, tell it it's transitioning out;
575        # the next one will become current once this one dies.
576        if prev_activity is not None:
577            prev_activity.transition_out()
578
579            # Setting this to None should free up the old activity to die,
580            # which will call begin_next_activity.
581            # We can still access our old activity through
582            # self._activity_weak() to keep it up to date on player
583            # joins/departures/etc until it dies.
584            self._activity_retained = None
585
586        # There's no existing activity; lets just go ahead with the begin call.
587        else:
588            self.begin_next_activity()
589
590        # We want to call destroy() for the previous activity once it should
591        # tear itself down, clear out any self-refs, etc. After this call
592        # the activity should have no refs left to it and should die (which
593        # will trigger the next activity to run).
594        if prev_activity is not None:
595            with babase.ContextRef.empty():
596                babase.apptimer(
597                    max(0.0, activity.transition_time), prev_activity.expire
598                )
599        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:
601    def getactivity(self) -> bascenev1.Activity | None:
602        """Return the current foreground activity for this session."""
603        return self._activity_weak()

Return the current foreground activity for this session.

def get_custom_menu_entries(self) -> list[dict[str, typing.Any]]:
605    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
606        """Subclasses can override this to provide custom menu entries.
607
608        The returned value should be a list of dicts, each containing
609        a 'label' and 'call' entry, with 'label' being the text for
610        the entry and 'call' being the callable to trigger if the entry
611        is pressed.
612        """
613        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:
657    def on_activity_end(
658        self, activity: bascenev1.Activity, results: Any
659    ) -> None:
660        """Called when the current bascenev1.Activity has ended.
661
662        The bascenev1.Session should look at the results and start
663        another bascenev1.Activity.
664        """

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:
666    def begin_next_activity(self) -> None:
667        """Called once the previous activity has been totally torn down.
668
669        This means we're ready to begin the next one
670        """
671        if self._next_activity is None:
672            # Should this ever happen?
673            logging.error('begin_next_activity() called with no _next_activity')
674            return
675
676        # We store both a weak and a strong ref to the new activity;
677        # the strong is to keep it alive and the weak is so we can access
678        # it even after we've released the strong-ref to allow it to die.
679        self._activity_retained = self._next_activity
680        self._activity_weak = weakref.ref(self._next_activity)
681        self._next_activity = None
682        self._activity_should_end_immediately = False
683
684        # Kick out anyone loitering in the lobby.
685        self.lobby.remove_all_choosers_and_kick_players()
686
687        # Kick off the activity.
688        self._activity_retained.begin(self)
689
690        # If we want to completely end the session, we can now kick that off.
691        if self._wants_to_end:
692            self._launch_end_session_activity()
693        else:
694            # Otherwise, if the activity has already been told to end,
695            # do so now.
696            if self._activity_should_end_immediately:
697                self._activity_retained.end(
698                    self._activity_should_end_immediately_results,
699                    self._activity_should_end_immediately_delay,
700                )

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/bascenev1.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: _bascenev1.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[_bascenev1.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:
1516def set_analytics_screen(screen: str) -> None:
1517    """Used for analytics to see where in the app players spend their time.
1518
1519    Category: **General Utility Functions**
1520
1521    Generally called when opening a new window or entering some UI.
1522    'screen' should be a string description of an app location
1523    ('Main Menu', etc.)
1524    """
1525    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:
177def show_damage_count(
178    damage: str, position: Sequence[float], direction: Sequence[float]
179) -> None:
180    """Pop up a damage count at a position in space.
181
182    Category: **Gameplay Functions**
183    """
184    lifespan = 1.0
185    app = babase.app
186
187    # FIXME: Should never vary game elements based on local config.
188    #  (connected clients may have differing configs so they won't
189    #  get the intended results).
190    assert app.classic is not None
191    do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr
192    txtnode = _bascenev1.newnode(
193        'text',
194        attrs={
195            'text': damage,
196            'in_world': True,
197            'h_align': 'center',
198            'flatness': 1.0,
199            'shadow': 1.0 if do_big else 0.7,
200            'color': (1, 0.25, 0.25, 1),
201            'scale': 0.015 if do_big else 0.01,
202        },
203    )
204    # Translate upward.
205    tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3})
206    tcombine.connectattr('output', txtnode, 'position')
207    v_vals = []
208    pval = 0.0
209    vval = 0.07
210    count = 6
211    for i in range(count):
212        v_vals.append((float(i) / count, pval))
213        pval += vval
214        vval *= 0.5
215    p_start = position[0]
216    p_dir = direction[0]
217    animate(
218        tcombine,
219        'input0',
220        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
221    )
222    p_start = position[1]
223    p_dir = direction[1]
224    animate(
225        tcombine,
226        'input1',
227        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
228    )
229    p_start = position[2]
230    p_dir = direction[2]
231    animate(
232        tcombine,
233        'input2',
234        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
235    )
236    animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
237    _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: _babase.Vec3, angle: float | None = None)
position: _babase.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:
256class Stats:
257    """Manages scores and statistics for a bascenev1.Session.
258
259    Category: **Gameplay Classes**
260    """
261
262    def __init__(self) -> None:
263        self._activity: weakref.ref[bascenev1.Activity] | None = None
264        self._player_records: dict[str, PlayerRecord] = {}
265        self.orchestrahitsound1: bascenev1.Sound | None = None
266        self.orchestrahitsound2: bascenev1.Sound | None = None
267        self.orchestrahitsound3: bascenev1.Sound | None = None
268        self.orchestrahitsound4: bascenev1.Sound | None = None
269
270    def setactivity(self, activity: bascenev1.Activity | None) -> None:
271        """Set the current activity for this instance."""
272
273        self._activity = None if activity is None else weakref.ref(activity)
274
275        # Load our media into this activity's context.
276        if activity is not None:
277            if activity.expired:
278                logging.exception('Unexpected finalized activity.')
279            else:
280                with activity.context:
281                    self._load_activity_media()
282
283    def getactivity(self) -> bascenev1.Activity | None:
284        """Get the activity associated with this instance.
285
286        May return None.
287        """
288        if self._activity is None:
289            return None
290        return self._activity()
291
292    def _load_activity_media(self) -> None:
293        self.orchestrahitsound1 = _bascenev1.getsound('orchestraHit')
294        self.orchestrahitsound2 = _bascenev1.getsound('orchestraHit2')
295        self.orchestrahitsound3 = _bascenev1.getsound('orchestraHit3')
296        self.orchestrahitsound4 = _bascenev1.getsound('orchestraHit4')
297
298    def reset(self) -> None:
299        """Reset the stats instance completely."""
300
301        # Just to be safe, lets make sure no multi-kill timers are gonna go off
302        # for no-longer-on-the-list players.
303        for p_entry in list(self._player_records.values()):
304            p_entry.cancel_multi_kill_timer()
305        self._player_records = {}
306
307    def reset_accum(self) -> None:
308        """Reset per-sound sub-scores."""
309        for s_player in list(self._player_records.values()):
310            s_player.cancel_multi_kill_timer()
311            s_player.accumscore = 0
312            s_player.accum_kill_count = 0
313            s_player.accum_killed_count = 0
314            s_player.streak = 0
315
316    def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None:
317        """Register a bascenev1.SessionPlayer with this score-set."""
318        assert player.exists()  # Invalid refs should never be passed to funcs.
319        name = player.getname()
320        if name in self._player_records:
321            # If the player already exists, update his character and such as
322            # it may have changed.
323            self._player_records[name].associate_with_sessionplayer(player)
324        else:
325            name_full = player.getname(full=True)
326            self._player_records[name] = PlayerRecord(
327                name, name_full, player, self
328            )
329
330    def get_records(self) -> dict[str, bascenev1.PlayerRecord]:
331        """Get PlayerRecord corresponding to still-existing players."""
332        records = {}
333
334        # Go through our player records and return ones whose player id still
335        # corresponds to a player with that name.
336        for record_id, record in self._player_records.items():
337            lastplayer = record.get_last_sessionplayer()
338            if lastplayer and lastplayer.getname() == record_id:
339                records[record_id] = record
340        return records
341
342    def player_scored(
343        self,
344        player: bascenev1.Player,
345        base_points: int = 1,
346        *,
347        target: Sequence[float] | None = None,
348        kill: bool = False,
349        victim_player: bascenev1.Player | None = None,
350        scale: float = 1.0,
351        color: Sequence[float] | None = None,
352        title: str | babase.Lstr | None = None,
353        screenmessage: bool = True,
354        display: bool = True,
355        importance: int = 1,
356        showpoints: bool = True,
357        big_message: bool = False,
358    ) -> int:
359        """Register a score for the player.
360
361        Return value is actual score with multipliers and such factored in.
362        """
363        # FIXME: Tidy this up.
364        # pylint: disable=cyclic-import
365        # pylint: disable=too-many-branches
366        # pylint: disable=too-many-locals
367        from bascenev1lib.actor.popuptext import PopupText
368
369        from bascenev1._gameactivity import GameActivity
370
371        del victim_player  # Currently unused.
372        name = player.getname()
373        s_player = self._player_records[name]
374
375        if kill:
376            s_player.submit_kill(showpoints=showpoints)
377
378        display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
379
380        if color is not None:
381            display_color = color
382        elif importance != 1:
383            display_color = (1.0, 1.0, 0.4, 1.0)
384        points = base_points
385
386        # If they want a big announcement, throw a zoom-text up there.
387        if display and big_message:
388            try:
389                assert self._activity is not None
390                activity = self._activity()
391                if isinstance(activity, GameActivity):
392                    name_full = player.getname(full=True, icon=False)
393                    activity.show_zoom_message(
394                        babase.Lstr(
395                            resource='nameScoresText',
396                            subs=[('${NAME}', name_full)],
397                        ),
398                        color=babase.normalized_color(player.team.color),
399                    )
400            except Exception:
401                logging.exception('Error showing big_message.')
402
403        # If we currently have a actor, pop up a score over it.
404        if display and showpoints:
405            our_pos = player.node.position if player.node else None
406            if our_pos is not None:
407                if target is None:
408                    target = our_pos
409
410                # If display-pos is *way* lower than us, raise it up
411                # (so we can still see scores from dudes that fell off cliffs).
412                display_pos = (
413                    target[0],
414                    max(target[1], our_pos[1] - 2.0),
415                    min(target[2], our_pos[2] + 2.0),
416                )
417                activity = self.getactivity()
418                if activity is not None:
419                    if title is not None:
420                        sval = babase.Lstr(
421                            value='+${A} ${B}',
422                            subs=[('${A}', str(points)), ('${B}', title)],
423                        )
424                    else:
425                        sval = babase.Lstr(
426                            value='+${A}', subs=[('${A}', str(points))]
427                        )
428                    PopupText(
429                        sval,
430                        color=display_color,
431                        scale=1.2 * scale,
432                        position=display_pos,
433                    ).autoretain()
434
435        # Tally kills.
436        if kill:
437            s_player.accum_kill_count += 1
438            s_player.kill_count += 1
439
440        # Report non-kill scorings.
441        try:
442            if screenmessage and not kill:
443                _bascenev1.broadcastmessage(
444                    babase.Lstr(
445                        resource='nameScoresText', subs=[('${NAME}', name)]
446                    ),
447                    top=True,
448                    color=player.color,
449                    image=player.get_icon(),
450                )
451        except Exception:
452            logging.exception('Error announcing score.')
453
454        s_player.score += points
455        s_player.accumscore += points
456
457        # Inform a running game of the score.
458        if points != 0:
459            activity = self._activity() if self._activity is not None else None
460            if activity is not None:
461                activity.handlemessage(PlayerScoredMessage(score=points))
462
463        return points
464
465    def player_was_killed(
466        self,
467        player: bascenev1.Player,
468        killed: bool = False,
469        killer: bascenev1.Player | None = None,
470    ) -> None:
471        """Should be called when a player is killed."""
472        name = player.getname()
473        prec = self._player_records[name]
474        prec.streak = 0
475        if killed:
476            prec.accum_killed_count += 1
477            prec.killed_count += 1
478        try:
479            if killed and _bascenev1.getactivity().announce_player_deaths:
480                if killer is player:
481                    _bascenev1.broadcastmessage(
482                        babase.Lstr(
483                            resource='nameSuicideText', subs=[('${NAME}', name)]
484                        ),
485                        top=True,
486                        color=player.color,
487                        image=player.get_icon(),
488                    )
489                elif killer is not None:
490                    if killer.team is player.team:
491                        _bascenev1.broadcastmessage(
492                            babase.Lstr(
493                                resource='nameBetrayedText',
494                                subs=[
495                                    ('${NAME}', killer.getname()),
496                                    ('${VICTIM}', name),
497                                ],
498                            ),
499                            top=True,
500                            color=killer.color,
501                            image=killer.get_icon(),
502                        )
503                    else:
504                        _bascenev1.broadcastmessage(
505                            babase.Lstr(
506                                resource='nameKilledText',
507                                subs=[
508                                    ('${NAME}', killer.getname()),
509                                    ('${VICTIM}', name),
510                                ],
511                            ),
512                            top=True,
513                            color=killer.color,
514                            image=killer.get_icon(),
515                        )
516                else:
517                    _bascenev1.broadcastmessage(
518                        babase.Lstr(
519                            resource='nameDiedText', subs=[('${NAME}', name)]
520                        ),
521                        top=True,
522                        color=player.color,
523                        image=player.get_icon(),
524                    )
525        except Exception:
526            logging.exception('Error announcing kill.')

Manages scores and statistics for a bascenev1.Session.

Category: Gameplay Classes

orchestrahitsound1: _bascenev1.Sound | None
orchestrahitsound2: _bascenev1.Sound | None
orchestrahitsound3: _bascenev1.Sound | None
orchestrahitsound4: _bascenev1.Sound | None
def setactivity(self, activity: Activity | None) -> None:
270    def setactivity(self, activity: bascenev1.Activity | None) -> None:
271        """Set the current activity for this instance."""
272
273        self._activity = None if activity is None else weakref.ref(activity)
274
275        # Load our media into this activity's context.
276        if activity is not None:
277            if activity.expired:
278                logging.exception('Unexpected finalized activity.')
279            else:
280                with activity.context:
281                    self._load_activity_media()

Set the current activity for this instance.

def getactivity(self) -> Activity | None:
283    def getactivity(self) -> bascenev1.Activity | None:
284        """Get the activity associated with this instance.
285
286        May return None.
287        """
288        if self._activity is None:
289            return None
290        return self._activity()

Get the activity associated with this instance.

May return None.

def reset(self) -> None:
298    def reset(self) -> None:
299        """Reset the stats instance completely."""
300
301        # Just to be safe, lets make sure no multi-kill timers are gonna go off
302        # for no-longer-on-the-list players.
303        for p_entry in list(self._player_records.values()):
304            p_entry.cancel_multi_kill_timer()
305        self._player_records = {}

Reset the stats instance completely.

def reset_accum(self) -> None:
307    def reset_accum(self) -> None:
308        """Reset per-sound sub-scores."""
309        for s_player in list(self._player_records.values()):
310            s_player.cancel_multi_kill_timer()
311            s_player.accumscore = 0
312            s_player.accum_kill_count = 0
313            s_player.accum_killed_count = 0
314            s_player.streak = 0

Reset per-sound sub-scores.

def register_sessionplayer(self, player: _bascenev1.SessionPlayer) -> None:
316    def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None:
317        """Register a bascenev1.SessionPlayer with this score-set."""
318        assert player.exists()  # Invalid refs should never be passed to funcs.
319        name = player.getname()
320        if name in self._player_records:
321            # If the player already exists, update his character and such as
322            # it may have changed.
323            self._player_records[name].associate_with_sessionplayer(player)
324        else:
325            name_full = player.getname(full=True)
326            self._player_records[name] = PlayerRecord(
327                name, name_full, player, self
328            )

Register a bascenev1.SessionPlayer with this score-set.

def get_records(self) -> dict[str, PlayerRecord]:
330    def get_records(self) -> dict[str, bascenev1.PlayerRecord]:
331        """Get PlayerRecord corresponding to still-existing players."""
332        records = {}
333
334        # Go through our player records and return ones whose player id still
335        # corresponds to a player with that name.
336        for record_id, record in self._player_records.items():
337            lastplayer = record.get_last_sessionplayer()
338            if lastplayer and lastplayer.getname() == record_id:
339                records[record_id] = record
340        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:
342    def player_scored(
343        self,
344        player: bascenev1.Player,
345        base_points: int = 1,
346        *,
347        target: Sequence[float] | None = None,
348        kill: bool = False,
349        victim_player: bascenev1.Player | None = None,
350        scale: float = 1.0,
351        color: Sequence[float] | None = None,
352        title: str | babase.Lstr | None = None,
353        screenmessage: bool = True,
354        display: bool = True,
355        importance: int = 1,
356        showpoints: bool = True,
357        big_message: bool = False,
358    ) -> int:
359        """Register a score for the player.
360
361        Return value is actual score with multipliers and such factored in.
362        """
363        # FIXME: Tidy this up.
364        # pylint: disable=cyclic-import
365        # pylint: disable=too-many-branches
366        # pylint: disable=too-many-locals
367        from bascenev1lib.actor.popuptext import PopupText
368
369        from bascenev1._gameactivity import GameActivity
370
371        del victim_player  # Currently unused.
372        name = player.getname()
373        s_player = self._player_records[name]
374
375        if kill:
376            s_player.submit_kill(showpoints=showpoints)
377
378        display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
379
380        if color is not None:
381            display_color = color
382        elif importance != 1:
383            display_color = (1.0, 1.0, 0.4, 1.0)
384        points = base_points
385
386        # If they want a big announcement, throw a zoom-text up there.
387        if display and big_message:
388            try:
389                assert self._activity is not None
390                activity = self._activity()
391                if isinstance(activity, GameActivity):
392                    name_full = player.getname(full=True, icon=False)
393                    activity.show_zoom_message(
394                        babase.Lstr(
395                            resource='nameScoresText',
396                            subs=[('${NAME}', name_full)],
397                        ),
398                        color=babase.normalized_color(player.team.color),
399                    )
400            except Exception:
401                logging.exception('Error showing big_message.')
402
403        # If we currently have a actor, pop up a score over it.
404        if display and showpoints:
405            our_pos = player.node.position if player.node else None
406            if our_pos is not None:
407                if target is None:
408                    target = our_pos
409
410                # If display-pos is *way* lower than us, raise it up
411                # (so we can still see scores from dudes that fell off cliffs).
412                display_pos = (
413                    target[0],
414                    max(target[1], our_pos[1] - 2.0),
415                    min(target[2], our_pos[2] + 2.0),
416                )
417                activity = self.getactivity()
418                if activity is not None:
419                    if title is not None:
420                        sval = babase.Lstr(
421                            value='+${A} ${B}',
422                            subs=[('${A}', str(points)), ('${B}', title)],
423                        )
424                    else:
425                        sval = babase.Lstr(
426                            value='+${A}', subs=[('${A}', str(points))]
427                        )
428                    PopupText(
429                        sval,
430                        color=display_color,
431                        scale=1.2 * scale,
432                        position=display_pos,
433                    ).autoretain()
434
435        # Tally kills.
436        if kill:
437            s_player.accum_kill_count += 1
438            s_player.kill_count += 1
439
440        # Report non-kill scorings.
441        try:
442            if screenmessage and not kill:
443                _bascenev1.broadcastmessage(
444                    babase.Lstr(
445                        resource='nameScoresText', subs=[('${NAME}', name)]
446                    ),
447                    top=True,
448                    color=player.color,
449                    image=player.get_icon(),
450                )
451        except Exception:
452            logging.exception('Error announcing score.')
453
454        s_player.score += points
455        s_player.accumscore += points
456
457        # Inform a running game of the score.
458        if points != 0:
459            activity = self._activity() if self._activity is not None else None
460            if activity is not None:
461                activity.handlemessage(PlayerScoredMessage(score=points))
462
463        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:
465    def player_was_killed(
466        self,
467        player: bascenev1.Player,
468        killed: bool = False,
469        killer: bascenev1.Player | None = None,
470    ) -> None:
471        """Should be called when a player is killed."""
472        name = player.getname()
473        prec = self._player_records[name]
474        prec.streak = 0
475        if killed:
476            prec.accum_killed_count += 1
477            prec.killed_count += 1
478        try:
479            if killed and _bascenev1.getactivity().announce_player_deaths:
480                if killer is player:
481                    _bascenev1.broadcastmessage(
482                        babase.Lstr(
483                            resource='nameSuicideText', subs=[('${NAME}', name)]
484                        ),
485                        top=True,
486                        color=player.color,
487                        image=player.get_icon(),
488                    )
489                elif killer is not None:
490                    if killer.team is player.team:
491                        _bascenev1.broadcastmessage(
492                            babase.Lstr(
493                                resource='nameBetrayedText',
494                                subs=[
495                                    ('${NAME}', killer.getname()),
496                                    ('${VICTIM}', name),
497                                ],
498                            ),
499                            top=True,
500                            color=killer.color,
501                            image=killer.get_icon(),
502                        )
503                    else:
504                        _bascenev1.broadcastmessage(
505                            babase.Lstr(
506                                resource='nameKilledText',
507                                subs=[
508                                    ('${NAME}', killer.getname()),
509                                    ('${VICTIM}', name),
510                                ],
511                            ),
512                            top=True,
513                            color=killer.color,
514                            image=killer.get_icon(),
515                        )
516                else:
517                    _bascenev1.broadcastmessage(
518                        babase.Lstr(
519                            resource='nameDiedText', subs=[('${NAME}', name)]
520                        ),
521                        top=True,
522                        color=player.color,
523                        image=player.get_icon(),
524                    )
525        except Exception:
526            logging.exception('Error announcing kill.')

Should be called when a player is killed.

def storagename(suffix: str | None = None) -> str:
334def storagename(suffix: str | None = None) -> str:
335    """Generate a unique name for storing class data in shared places.
336
337    Category: **General Utility Functions**
338
339    This consists of a leading underscore, the module path at the
340    call site with dots replaced by underscores, the containing class's
341    qualified name, and the provided suffix. When storing data in public
342    places such as 'customdata' dicts, this minimizes the chance of
343    collisions with other similarly named classes.
344
345    Note that this will function even if called in the class definition.
346
347    ##### Examples
348    Generate a unique name for storage purposes:
349    >>> class MyThingie:
350    ...     # This will give something like
351    ...     # '_mymodule_submodule_mythingie_data'.
352    ...     _STORENAME = babase.storagename('data')
353    ...
354    ...     # Use that name to store some data in the Activity we were
355    ...     # passed.
356    ...     def __init__(self, activity):
357    ...         activity.customdata[self._STORENAME] = {}
358    """
359    frame = inspect.currentframe()
360    if frame is None:
361        raise RuntimeError('Cannot get current stack frame.')
362    fback = frame.f_back
363
364    # Note: We need to explicitly clear frame here to avoid a ref-loop
365    # that keeps all function-dicts in the stack alive until the next
366    # full GC cycle (the stack frame refers to this function's dict,
367    # which refers to the stack frame).
368    del frame
369
370    if fback is None:
371        raise RuntimeError('Cannot get parent stack frame.')
372    modulepath = fback.f_globals.get('__name__')
373    if modulepath is None:
374        raise RuntimeError('Cannot get parent stack module path.')
375    assert isinstance(modulepath, str)
376    qualname = fback.f_locals.get('__qualname__')
377    if qualname is not None:
378        assert isinstance(qualname, str)
379        fullpath = f'_{modulepath}_{qualname.lower()}'
380    else:
381        fullpath = f'_{modulepath}'
382    if suffix is not None:
383        fullpath = f'{fullpath}_{suffix}'
384    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:
1726def time() -> bascenev1.Time:
1727    """Return the current scene time in seconds.
1728
1729    Category: **General Utility Functions**
1730
1731    Scene time maps to local simulation time in bascenev1.Activity or
1732    bascenev1.Session Contexts. This means that it may progress slower
1733    in slow-motion play modes, stop when the game is paused, etc.
1734
1735    Note that the value returned here is simply a float; it just has a
1736    unique type in the type-checker's eyes to help prevent it from being
1737    accidentally used with time functionality expecting other time types.
1738    """
1739    import bascenev1  # pylint: disable=cyclic-import
1740
1741    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:
1745def timer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
1746    """Schedule a call to run at a later point in time.
1747
1748    Category: **General Utility Functions**
1749
1750    This function adds a scene-time timer to the current babase.Context.
1751    This timer cannot be canceled or modified once created. If you
1752     require the ability to do so, use the babase.Timer class instead.
1753
1754    Scene time maps to local simulation time in bascenev1.Activity or
1755    bascenev1.Session Contexts. This means that it may progress slower
1756    in slow-motion play modes, stop when the game is paused, etc.
1757
1758    ##### Arguments
1759    ###### time (float)
1760    > Length of scene time in seconds that the timer will wait
1761    before firing.
1762
1763    ###### call (Callable[[], Any])
1764    > A callable Python object. Note that the timer will retain a
1765    strong reference to the callable for as long as it exists, so you
1766    may want to look into concepts such as babase.WeakCall if that is not
1767    desired.
1768
1769    ###### repeat (bool)
1770    > If True, the timer will fire repeatedly, with each successive
1771    firing having the same delay as the first.
1772
1773    ##### Examples
1774    Print some stuff through time:
1775    >>> import bascenev1 as bs
1776    >>> bs.screenmessage('hello from now!')
1777    >>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
1778    >>> bs.timer(2.0, bs.Call(bs.screenmessage,
1779    ...                       'hello from the future 2!'))
1780    """
1781    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    SMALL = 0
85    MEDIUM = 1
86    LARGE = 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.

SMALL = <UIScale.SMALL: 0>
MEDIUM = <UIScale.MEDIUM: 1>
LARGE = <UIScale.LARGE: 2>
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: _babase.Vec3) -> _babase.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: _babase.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) -> _babase.Vec3:
499    def normalized(self) -> Vec3:
500        """Returns a normalized version of the vector."""
501        return Vec3()

Returns a normalized version of the vector.

WeakCall = <class 'babase._general._WeakCall'>