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 )
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.
The settings dict passed in when the activity was made. This attribute is deprecated and should be avoided when possible; activities should pull all values they need from the 'settings' arg passed to the Activity __init__ call.
The list of 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).
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.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
Joining activities are for waiting for initial player joins. They are treated slightly differently than regular activities, mainly in that all players are passed to the activity at once instead of as each joins.
Whether idle players can potentially be kicked (should not happen in menus/etc).
In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.
Set this to True to inherit slow motion setting from previous activity (useful for transitions to avoid hitches).
Set this to True to keep playing the music from the previous activity (without even restarting it).
Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).
Set this to true to inherit (non-fixed) VR overlay positioning from the previous activity (useful for prevent sporadic overlay jostling during transitions).
Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).
Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.
If the activity fades or transitions in, it should set the length of time here so that previous activities will be kept alive for that long (avoiding 'holes' in the screen) This value is given in real-time seconds.
Is it ok to show an ad after this activity ends before showing the next activity?
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.
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.
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.
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.
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.
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'.
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.
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.
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.
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)
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.
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)
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.
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)
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.
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.
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.
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.
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.
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.
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.
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.
400 def is_transitioning_out(self) -> bool: 401 """Return whether bascenev1.Activity.on_transition_out() has run.""" 402 return self._transitioning_out
Return whether bascenev1.Activity.on_transition_out() has run.
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.
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.
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.
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.
Inherited Members
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())
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.
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.
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()
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.Actor
s 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.
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.
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.
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.
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.
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.
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.
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.
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
Tells the app to simply run in its default mode.
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.
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
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.
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.
47 def handle_intent(self, intent: AppIntent) -> None: 48 """Handle an intent.""" 49 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
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.
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.
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!'))
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)
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
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.
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.
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()
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()
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()
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()
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()
Inherited Members
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.
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!'))
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)
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
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.
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.
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
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.
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.
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.
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.
78 def reset(self) -> None: 79 """Reset state for the Campaign.""" 80 babase.app.config.setdefault('Campaigns', {})[self._name] = {}
Reset state for the Campaign.
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).
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.
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.
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
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
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
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)
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.
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.
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.
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.
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.
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