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.
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()
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 425 ): 426 self._character_names.append(character) 427 self._character_index = self._character_names.index(character) 428 self._color, self._highlight = get_player_profile_colors( 429 self._profilename, profiles=self._profiles 430 ) 431 self._update_icon() 432 self._update_text()
Set character/colors based on the current profile.
434 def reload_profiles(self) -> None: 435 """Reload all player profiles.""" 436 437 app = babase.app 438 env = app.env 439 assert app.classic is not None 440 441 # Re-construct our profile index and other stuff since the profile 442 # list might have changed. 443 input_device = self._sessionplayer.inputdevice 444 is_remote = input_device.is_remote_client 445 is_test_input = input_device.is_test_input 446 447 # Pull this player's list of unlocked characters. 448 if is_remote: 449 # TODO: Pull this from the remote player. 450 # (but make sure to filter it to the ones we've got). 451 self._character_names = ['Spaz'] 452 else: 453 self._character_names = self.lobby.character_names_local_unlocked 454 455 # If we're a local player, pull our local profiles from the config. 456 # Otherwise ask the remote-input-device for its profile list. 457 if is_remote: 458 self._profiles = input_device.get_player_profiles() 459 else: 460 self._profiles = app.config.get('Player Profiles', {}) 461 462 # These may have come over the wire from an older 463 # (non-unicode/non-json) version. 464 # Make sure they conform to our standards 465 # (unicode strings, no tuples, etc) 466 self._profiles = app.classic.json_prep(self._profiles) 467 468 # Filter out any characters we're unaware of. 469 for profile in list(self._profiles.items()): 470 if ( 471 profile[1].get('character', '') 472 not in app.classic.spaz_appearances 473 ): 474 profile[1]['character'] = 'Spaz' 475 476 # Add in a random one so we're ok even if there's no user profiles. 477 self._profiles['_random'] = {} 478 479 # In kiosk mode we disable account profiles to force random. 480 if env.demo or env.arcade: 481 if '__account__' in self._profiles: 482 del self._profiles['__account__'] 483 484 # For local devices, add it an 'edit' option which will pop up 485 # the profile window. 486 if not is_remote and not is_test_input and not (env.demo or env.arcade): 487 self._profiles['_edit'] = {} 488 489 # Build a sorted name list we can iterate through. 490 self._profilenames = list(self._profiles.keys()) 491 self._profilenames.sort(key=lambda x: x.lower()) 492 493 if self._profilename in self._profilenames: 494 self._profileindex = self._profilenames.index(self._profilename) 495 else: 496 self._profileindex = 0 497 # noinspection PyUnresolvedReferences 498 self._profilename = self._profilenames[self._profileindex]
Reload all player profiles.
500 def update_position(self) -> None: 501 """Update this chooser's position.""" 502 503 assert self._text_node 504 spacing = 350 505 sessionteams = self.lobby.sessionteams 506 offs = ( 507 spacing * -0.5 * len(sessionteams) 508 + spacing * self._selected_team_index 509 + 250 510 ) 511 if len(sessionteams) > 1: 512 offs -= 35 513 animate_array( 514 self._text_node, 515 'position', 516 2, 517 {0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)}, 518 ) 519 animate_array( 520 self.icon, 521 'position', 522 2, 523 {0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)}, 524 )
Update this chooser's position.
526 def get_character_name(self) -> str: 527 """Return the selected character name.""" 528 return self._character_names[self._character_index]
Return the selected character name.
746 def handlemessage(self, msg: Any) -> Any: 747 """Standard generic message handler.""" 748 749 if isinstance(msg, ChangeMessage): 750 self._handle_repeat_message_attack() 751 752 # If we've been removed from the lobby, ignore this stuff. 753 if self._dead: 754 logging.error('chooser got ChangeMessage after dying') 755 return 756 757 if not self._text_node: 758 logging.error('got ChangeMessage after nodes died') 759 return 760 761 if msg.what == 'team': 762 sessionteams = self.lobby.sessionteams 763 if len(sessionteams) > 1: 764 self._swish_sound.play() 765 self._selected_team_index = ( 766 self._selected_team_index + msg.value 767 ) % len(sessionteams) 768 self._update_text() 769 self.update_position() 770 self._update_icon() 771 772 elif msg.what == 'profileindex': 773 if len(self._profilenames) == 1: 774 # This should be pretty hard to hit now with 775 # automatic local accounts. 776 _bascenev1.getsound('error').play() 777 else: 778 # Pick the next player profile and assign our name 779 # and character based on that. 780 self._deek_sound.play() 781 self._profileindex = (self._profileindex + msg.value) % len( 782 self._profilenames 783 ) 784 self.update_from_profile() 785 786 elif msg.what == 'character': 787 self._click_sound.play() 788 # update our index in our local list of characters 789 self._character_index = ( 790 self._character_index + msg.value 791 ) % len(self._character_names) 792 self._update_text() 793 self._update_icon() 794 795 elif msg.what == 'ready': 796 self._handle_ready_msg(bool(msg.value))
Standard generic message handler.
839 def get_color(self) -> Sequence[float]: 840 """Return the currently selected color.""" 841 val: Sequence[float] 842 if self.lobby.use_team_colors: 843 val = self.lobby.sessionteams[self._selected_team_index].color 844 else: 845 val = self._color 846 if len(val) != 3: 847 print('get_color: ignoring invalid color of len', len(val)) 848 val = (0, 1, 0) 849 return val
Return the currently selected color.
851 def get_highlight(self) -> Sequence[float]: 852 """Return the currently selected highlight.""" 853 if self._profilenames[self._profileindex] == '_edit': 854 return 0, 1, 0 855 856 # If we're using team colors we wanna make sure our highlight color 857 # isn't too close to any other team's color. 858 highlight = list(self._highlight) 859 if self.lobby.use_team_colors: 860 for i, sessionteam in enumerate(self.lobby.sessionteams): 861 if i != self._selected_team_index: 862 # Find the dominant component of this sessionteam's color 863 # and adjust ours so that the component is 864 # not super-dominant. 865 max_val = 0.0 866 max_index = 0 867 for j in range(3): 868 if sessionteam.color[j] > max_val: 869 max_val = sessionteam.color[j] 870 max_index = j 871 that_color_for_us = highlight[max_index] 872 our_second_biggest = max( 873 highlight[(max_index + 1) % 3], 874 highlight[(max_index + 2) % 3], 875 ) 876 diff = that_color_for_us - our_second_biggest 877 if diff > 0: 878 highlight[max_index] -= diff * 0.6 879 highlight[(max_index + 1) % 3] += diff * 0.3 880 highlight[(max_index + 2) % 3] += diff * 0.2 881 return highlight
Return the currently selected highlight.
17class Collision: 18 """A class providing info about occurring collisions. 19 20 Category: **Gameplay Classes** 21 """ 22 23 @property 24 def position(self) -> bascenev1.Vec3: 25 """The position of the current collision.""" 26 return babase.Vec3(_bascenev1.get_collision_info('position')) 27 28 @property 29 def sourcenode(self) -> bascenev1.Node: 30 """The node containing the material triggering the current callback. 31 32 Throws a bascenev1.NodeNotFoundError if the node does not exist, 33 though the node should always exist (at least at the start of the 34 collision callback). 35 """ 36 node = _bascenev1.get_collision_info('sourcenode') 37 assert isinstance(node, (_bascenev1.Node, type(None))) 38 if not node: 39 raise babase.NodeNotFoundError() 40 return node 41 42 @property 43 def opposingnode(self) -> bascenev1.Node: 44 """The node the current callback material node is hitting. 45 46 Throws a bascenev1.NodeNotFoundError if the node does not exist. 47 This can be expected in some cases such as in 'disconnect' 48 callbacks triggered by deleting a currently-colliding node. 49 """ 50 node = _bascenev1.get_collision_info('opposingnode') 51 assert isinstance(node, (_bascenev1.Node, type(None))) 52 if not node: 53 raise babase.NodeNotFoundError() 54 return node 55 56 @property 57 def opposingbody(self) -> int: 58 """The body index on the opposing node in the current collision.""" 59 body = _bascenev1.get_collision_info('opposingbody') 60 assert isinstance(body, int) 61 return body
A class providing info about occurring collisions.
Category: Gameplay Classes
23 @property 24 def position(self) -> bascenev1.Vec3: 25 """The position of the current collision.""" 26 return babase.Vec3(_bascenev1.get_collision_info('position'))
The position of the current collision.
28 @property 29 def sourcenode(self) -> bascenev1.Node: 30 """The node containing the material triggering the current callback. 31 32 Throws a bascenev1.NodeNotFoundError if the node does not exist, 33 though the node should always exist (at least at the start of the 34 collision callback). 35 """ 36 node = _bascenev1.get_collision_info('sourcenode') 37 assert isinstance(node, (_bascenev1.Node, type(None))) 38 if not node: 39 raise babase.NodeNotFoundError() 40 return node
The node containing the material triggering the current callback.
Throws a bascenev1.NodeNotFoundError if the node does not exist, though the node should always exist (at least at the start of the collision callback).
42 @property 43 def opposingnode(self) -> bascenev1.Node: 44 """The node the current callback material node is hitting. 45 46 Throws a bascenev1.NodeNotFoundError if the node does not exist. 47 This can be expected in some cases such as in 'disconnect' 48 callbacks triggered by deleting a currently-colliding node. 49 """ 50 node = _bascenev1.get_collision_info('opposingnode') 51 assert isinstance(node, (_bascenev1.Node, type(None))) 52 if not node: 53 raise babase.NodeNotFoundError() 54 return node
The node the current callback material node is hitting.
Throws a bascenev1.NodeNotFoundError if the node does not exist. This can be expected in some cases such as in 'disconnect' callbacks triggered by deleting a currently-colliding node.
56 @property 57 def opposingbody(self) -> int: 58 """The body index on the opposing node in the current collision.""" 59 body = _bascenev1.get_collision_info('opposingbody') 60 assert isinstance(body, int) 61 return body
The body index on the opposing node in the current collision.
128class CollisionMesh: 129 """A reference to a collision-mesh. 130 131 Category: **Asset Classes** 132 133 Use bascenev1.getcollisionmesh() to instantiate one. 134 """ 135 136 pass
A reference to a collision-mesh.
Category: Asset Classes
Use bascenev1.getcollisionmesh() to instantiate one.
16class ContextError(Exception): 17 """Exception raised when a call is made in an invalid context. 18 19 Category: **Exception Classes** 20 21 Examples of this include calling UI functions within an Activity context 22 or calling scene manipulation functions outside of a game context. 23 """
Exception raised when a call is made in an invalid context.
Category: Exception Classes
Examples of this include calling UI functions within an Activity context or calling scene manipulation functions outside of a game context.
148class ContextRef: 149 """Store or use a ballistica context. 150 151 Category: **General Utility Classes** 152 153 Many operations such as bascenev1.newnode() or bascenev1.gettexture() 154 operate implicitly on a current 'context'. A context is some sort of 155 state that functionality can implicitly use. Context determines, for 156 example, which scene nodes or textures get added to without having to 157 specify it explicitly in the newnode()/gettexture() call. Contexts can 158 also affect object lifecycles; for example a babase.ContextCall will 159 become a no-op when the context it was created in is destroyed. 160 161 In general, if you are a modder, you should not need to worry about 162 contexts; mod code should mostly be getting run in the correct 163 context and timers and other callbacks will take care of saving 164 and restoring contexts automatically. There may be rare cases, 165 however, where you need to deal directly with contexts, and that is 166 where this class comes in. 167 168 Creating a babase.ContextRef() will capture a reference to the current 169 context. Other modules may provide ways to access their contexts; for 170 example a bascenev1.Activity instance has a 'context' attribute. You 171 can also use babase.ContextRef.empty() to create a reference to *no* 172 context. Some code such as UI calls may expect this and may complain 173 if you try to use them within a context. 174 175 ##### Usage 176 ContextRefs are generally used with the Python 'with' statement, which 177 sets the context they point to as current on entry and resets it to 178 the previous value on exit. 179 180 ##### Example 181 Explicitly create a few UI bits with no context set. 182 (UI stuff may complain if called within a context): 183 >>> with bui.ContextRef.empty(): 184 ... my_container = bui.containerwidget() 185 """ 186 187 def __init__( 188 self, 189 ) -> None: 190 pass 191 192 def __enter__(self) -> None: 193 """Support for "with" statement.""" 194 pass 195 196 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any: 197 """Support for "with" statement.""" 198 pass 199 200 @classmethod 201 def empty(cls) -> ContextRef: 202 """Return a ContextRef pointing to no context. 203 204 This is useful when code should be run free of a context. 205 For example, UI code generally insists on being run this way. 206 Otherwise, callbacks set on the UI could inadvertently stop working 207 due to a game activity ending, which would be unintuitive behavior. 208 """ 209 return ContextRef() 210 211 def is_empty(self) -> bool: 212 """Whether the context was created as empty.""" 213 return bool() 214 215 def is_expired(self) -> bool: 216 """Whether the context has expired.""" 217 return bool()
Store or use a ballistica context.
Category: General Utility Classes
Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a babase.ContextCall will become a no-op when the context it was created in is destroyed.
In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.
Creating a babase.ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use babase.ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.
Usage
ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.
Example
Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):
>>> with bui.ContextRef.empty():
... my_container = bui.containerwidget()
200 @classmethod 201 def empty(cls) -> ContextRef: 202 """Return a ContextRef pointing to no context. 203 204 This is useful when code should be run free of a context. 205 For example, UI code generally insists on being run this way. 206 Otherwise, callbacks set on the UI could inadvertently stop working 207 due to a game activity ending, which would be unintuitive behavior. 208 """ 209 return ContextRef()
Return a ContextRef pointing to no context.
This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.
26class CoopGameActivity(GameActivity[PlayerT, TeamT]): 27 """Base class for cooperative-mode games. 28 29 Category: **Gameplay Classes** 30 """ 31 32 # We can assume our session is a CoopSession. 33 session: bascenev1.CoopSession 34 35 @override 36 @classmethod 37 def supports_session_type( 38 cls, sessiontype: type[bascenev1.Session] 39 ) -> bool: 40 from bascenev1._coopsession import CoopSession 41 42 return issubclass(sessiontype, CoopSession) 43 44 def __init__(self, settings: dict): 45 super().__init__(settings) 46 47 # Cache these for efficiency. 48 self._achievements_awarded: set[str] = set() 49 50 self._life_warning_beep: bascenev1.Actor | None = None 51 self._life_warning_beep_timer: bascenev1.Timer | None = None 52 self._warn_beeps_sound = _bascenev1.getsound('warnBeeps') 53 54 @override 55 def on_begin(self) -> None: 56 super().on_begin() 57 58 # Show achievements remaining. 59 env = babase.app.env 60 if not (env.demo or env.arcade): 61 _bascenev1.timer( 62 3.8, babase.WeakCall(self._show_remaining_achievements) 63 ) 64 65 # Preload achievement images in case we get some. 66 _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements)) 67 68 # FIXME: this is now redundant with activityutils.getscoreconfig(); 69 # need to kill this. 70 def get_score_type(self) -> str: 71 """ 72 Return the score unit this co-op game uses ('point', 'seconds', etc.) 73 """ 74 return 'points' 75 76 def _get_coop_level_name(self) -> str: 77 assert self.session.campaign is not None 78 return self.session.campaign.name + ':' + str(self.settings_raw['name']) 79 80 def celebrate(self, duration: float) -> None: 81 """Tells all existing player-controlled characters to celebrate. 82 83 Can be useful in co-op games when the good guys score or complete 84 a wave. 85 duration is given in seconds. 86 """ 87 from bascenev1._messages import CelebrateMessage 88 89 for player in self.players: 90 if player.actor: 91 player.actor.handlemessage(CelebrateMessage(duration)) 92 93 def _preload_achievements(self) -> None: 94 assert babase.app.classic is not None 95 achievements = babase.app.classic.ach.achievements_for_coop_level( 96 self._get_coop_level_name() 97 ) 98 for ach in achievements: 99 ach.get_icon_texture(True) 100 101 def _show_remaining_achievements(self) -> None: 102 # pylint: disable=cyclic-import 103 from bascenev1lib.actor.text import Text 104 105 assert babase.app.classic is not None 106 ts_h_offs = 30 107 v_offs = -200 108 achievements = [ 109 a 110 for a in babase.app.classic.ach.achievements_for_coop_level( 111 self._get_coop_level_name() 112 ) 113 if not a.complete 114 ] 115 vrmode = babase.app.env.vr 116 if achievements: 117 Text( 118 babase.Lstr(resource='achievementsRemainingText'), 119 host_only=True, 120 position=(ts_h_offs - 10 + 40, v_offs - 10), 121 transition=Text.Transition.FADE_IN, 122 scale=1.1, 123 h_attach=Text.HAttach.LEFT, 124 v_attach=Text.VAttach.TOP, 125 color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0), 126 flatness=1.0 if vrmode else 0.6, 127 shadow=1.0 if vrmode else 0.5, 128 transition_delay=0.0, 129 transition_out_delay=1.3 if self.slow_motion else 4.0, 130 ).autoretain() 131 hval = 70 132 vval = -50 133 tdelay = 0.0 134 for ach in achievements: 135 tdelay += 0.05 136 ach.create_display( 137 hval + 40, 138 vval + v_offs, 139 0 + tdelay, 140 outdelay=1.3 if self.slow_motion else 4.0, 141 style='in_game', 142 ) 143 vval -= 55 144 145 @override 146 def spawn_player_spaz( 147 self, 148 player: PlayerT, 149 position: Sequence[float] = (0.0, 0.0, 0.0), 150 angle: float | None = None, 151 ) -> PlayerSpaz: 152 """Spawn and wire up a standard player spaz.""" 153 spaz = super().spawn_player_spaz(player, position, angle) 154 155 # Deaths are noteworthy in co-op games. 156 spaz.play_big_death_sound = True 157 return spaz 158 159 def _award_achievement( 160 self, achievement_name: str, sound: bool = True 161 ) -> None: 162 """Award an achievement. 163 164 Returns True if a banner will be shown; 165 False otherwise 166 """ 167 168 classic = babase.app.classic 169 plus = babase.app.plus 170 if classic is None or plus is None: 171 logging.warning( 172 '_award_achievement is a no-op without classic and plus.' 173 ) 174 return 175 176 if achievement_name in self._achievements_awarded: 177 return 178 179 ach = classic.ach.get_achievement(achievement_name) 180 181 # If we're in the easy campaign and this achievement is hard-mode-only, 182 # ignore it. 183 try: 184 campaign = self.session.campaign 185 assert campaign is not None 186 if ach.hard_mode_only and campaign.name == 'Easy': 187 return 188 except Exception: 189 logging.exception('Error in _award_achievement.') 190 191 # If we haven't awarded this one, check to see if we've got it. 192 # If not, set it through the game service *and* add a transaction 193 # for it. 194 if not ach.complete: 195 self._achievements_awarded.add(achievement_name) 196 197 # Report new achievements to the game-service. 198 plus.report_achievement(achievement_name) 199 200 # ...and to our account. 201 plus.add_v1_account_transaction( 202 {'type': 'ACHIEVEMENT', 'name': achievement_name} 203 ) 204 205 # Now bring up a celebration banner. 206 ach.announce_completion(sound=sound) 207 208 def fade_to_red(self) -> None: 209 """Fade the screen to red; (such as when the good guys have lost).""" 210 from bascenev1 import _gameutils 211 212 c_existing = self.globalsnode.tint 213 cnode = _bascenev1.newnode( 214 'combine', 215 attrs={ 216 'input0': c_existing[0], 217 'input1': c_existing[1], 218 'input2': c_existing[2], 219 'size': 3, 220 }, 221 ) 222 _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0}) 223 _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0}) 224 cnode.connectattr('output', self.globalsnode, 'tint') 225 226 def setup_low_life_warning_sound(self) -> None: 227 """Set up a beeping noise to play when any players are near death.""" 228 self._life_warning_beep = None 229 self._life_warning_beep_timer = _bascenev1.Timer( 230 1.0, babase.WeakCall(self._update_life_warning), repeat=True 231 ) 232 233 def _update_life_warning(self) -> None: 234 # Beep continuously if anyone is close to death. 235 should_beep = False 236 for player in self.players: 237 if player.is_alive(): 238 # FIXME: Should abstract this instead of 239 # reading hitpoints directly. 240 if getattr(player.actor, 'hitpoints', 999) < 200: 241 should_beep = True 242 break 243 if should_beep and self._life_warning_beep is None: 244 from bascenev1._nodeactor import NodeActor 245 246 self._life_warning_beep = NodeActor( 247 _bascenev1.newnode( 248 'sound', 249 attrs={ 250 'sound': self._warn_beeps_sound, 251 'positional': False, 252 'loop': True, 253 }, 254 ) 255 ) 256 if self._life_warning_beep is not None and not should_beep: 257 self._life_warning_beep = None
Base class for cooperative-mode games.
Category: Gameplay Classes
44 def __init__(self, settings: dict): 45 super().__init__(settings) 46 47 # Cache these for efficiency. 48 self._achievements_awarded: set[str] = set() 49 50 self._life_warning_beep: bascenev1.Actor | None = None 51 self._life_warning_beep_timer: bascenev1.Timer | None = None 52 self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')
Instantiate the Activity.
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.
35 @override 36 @classmethod 37 def supports_session_type( 38 cls, sessiontype: type[bascenev1.Session] 39 ) -> bool: 40 from bascenev1._coopsession import CoopSession 41 42 return issubclass(sessiontype, CoopSession)
Return whether this game supports the provided Session type.
54 @override 55 def on_begin(self) -> None: 56 super().on_begin() 57 58 # Show achievements remaining. 59 env = babase.app.env 60 if not (env.demo or env.arcade): 61 _bascenev1.timer( 62 3.8, babase.WeakCall(self._show_remaining_achievements) 63 ) 64 65 # Preload achievement images in case we get some. 66 _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements))
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
70 def get_score_type(self) -> str: 71 """ 72 Return the score unit this co-op game uses ('point', 'seconds', etc.) 73 """ 74 return 'points'
Return the score unit this co-op game uses ('point', 'seconds', etc.)
80 def celebrate(self, duration: float) -> None: 81 """Tells all existing player-controlled characters to celebrate. 82 83 Can be useful in co-op games when the good guys score or complete 84 a wave. 85 duration is given in seconds. 86 """ 87 from bascenev1._messages import CelebrateMessage 88 89 for player in self.players: 90 if player.actor: 91 player.actor.handlemessage(CelebrateMessage(duration))
Tells all existing player-controlled characters to celebrate.
Can be useful in co-op games when the good guys score or complete a wave. duration is given in seconds.
145 @override 146 def spawn_player_spaz( 147 self, 148 player: PlayerT, 149 position: Sequence[float] = (0.0, 0.0, 0.0), 150 angle: float | None = None, 151 ) -> PlayerSpaz: 152 """Spawn and wire up a standard player spaz.""" 153 spaz = super().spawn_player_spaz(player, position, angle) 154 155 # Deaths are noteworthy in co-op games. 156 spaz.play_big_death_sound = True 157 return spaz
Spawn and wire up a standard player spaz.
208 def fade_to_red(self) -> None: 209 """Fade the screen to red; (such as when the good guys have lost).""" 210 from bascenev1 import _gameutils 211 212 c_existing = self.globalsnode.tint 213 cnode = _bascenev1.newnode( 214 'combine', 215 attrs={ 216 'input0': c_existing[0], 217 'input1': c_existing[1], 218 'input2': c_existing[2], 219 'size': 3, 220 }, 221 ) 222 _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0}) 223 _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0}) 224 cnode.connectattr('output', self.globalsnode, 'tint')
Fade the screen to red; (such as when the good guys have lost).
226 def setup_low_life_warning_sound(self) -> None: 227 """Set up a beeping noise to play when any players are near death.""" 228 self._life_warning_beep = None 229 self._life_warning_beep_timer = _bascenev1.Timer( 230 1.0, babase.WeakCall(self._update_life_warning), repeat=True 231 )
Set up a beeping noise to play when any players are near death.
23class CoopSession(Session): 24 """A bascenev1.Session which runs cooperative-mode games. 25 26 Category: **Gameplay Classes** 27 28 These generally consist of 1-4 players against 29 the computer and include functionality such as 30 high score lists. 31 """ 32 33 use_teams = True 34 use_team_colors = False 35 allow_mid_activity_joins = False 36 37 # Note: even though these are instance vars, we annotate them at the 38 # class level so that docs generation can access their types. 39 40 campaign: bascenev1.Campaign | None 41 """The baclassic.Campaign instance this Session represents, or None if 42 there is no associated Campaign.""" 43 44 def __init__(self) -> None: 45 """Instantiate a co-op mode session.""" 46 # pylint: disable=cyclic-import 47 from bascenev1lib.activity.coopjoin import CoopJoinActivity 48 49 babase.increment_analytics_count('Co-op session start') 50 app = babase.app 51 classic = app.classic 52 assert classic is not None 53 54 # If they passed in explicit min/max, honor that. 55 # Otherwise defer to user overrides or defaults. 56 if 'min_players' in classic.coop_session_args: 57 min_players = classic.coop_session_args['min_players'] 58 else: 59 min_players = 1 60 if 'max_players' in classic.coop_session_args: 61 max_players = classic.coop_session_args['max_players'] 62 else: 63 max_players = app.config.get('Coop Game Max Players', 4) 64 if 'submit_score' in classic.coop_session_args: 65 submit_score = classic.coop_session_args['submit_score'] 66 else: 67 submit_score = True 68 69 # print('FIXME: COOP SESSION WOULD CALC DEPS.') 70 depsets: Sequence[bascenev1.DependencySet] = [] 71 72 super().__init__( 73 depsets, 74 team_names=TEAM_NAMES, 75 team_colors=TEAM_COLORS, 76 min_players=min_players, 77 max_players=max_players, 78 submit_score=submit_score, 79 ) 80 81 # Tournament-ID if we correspond to a co-op tournament (otherwise None) 82 self.tournament_id: str | None = classic.coop_session_args.get( 83 'tournament_id' 84 ) 85 86 self.campaign = classic.getcampaign( 87 classic.coop_session_args['campaign'] 88 ) 89 self.campaign_level_name: str = classic.coop_session_args['level'] 90 91 self._ran_tutorial_activity = False 92 self._tutorial_activity: bascenev1.Activity | None = None 93 self._custom_menu_ui: list[dict[str, Any]] = [] 94 95 # Start our joining screen. 96 self.setactivity(_bascenev1.newactivity(CoopJoinActivity)) 97 98 self._next_game_instance: bascenev1.GameActivity | None = None 99 self._next_game_level_name: str | None = None 100 self._update_on_deck_game_instances() 101 102 def get_current_game_instance(self) -> bascenev1.GameActivity: 103 """Get the game instance currently being played.""" 104 return self._current_game_instance 105 106 @override 107 def should_allow_mid_activity_joins( 108 self, activity: bascenev1.Activity 109 ) -> bool: 110 # pylint: disable=cyclic-import 111 from bascenev1._gameactivity import GameActivity 112 113 # Disallow any joins in the middle of the game. 114 if isinstance(activity, GameActivity): 115 return False 116 117 return True 118 119 def _update_on_deck_game_instances(self) -> None: 120 # pylint: disable=cyclic-import 121 from bascenev1._gameactivity import GameActivity 122 123 classic = babase.app.classic 124 assert classic is not None 125 126 # Instantiate levels we may be running soon to let them load in the bg. 127 128 # Build an instance for the current level. 129 assert self.campaign is not None 130 level = self.campaign.getlevel(self.campaign_level_name) 131 gametype = level.gametype 132 settings = level.get_settings() 133 134 # Make sure all settings the game expects are present. 135 neededsettings = gametype.get_available_settings(type(self)) 136 for setting in neededsettings: 137 if setting.name not in settings: 138 settings[setting.name] = setting.default 139 140 newactivity = _bascenev1.newactivity(gametype, settings) 141 assert isinstance(newactivity, GameActivity) 142 self._current_game_instance: GameActivity = newactivity 143 144 # Find the next level and build an instance for it too. 145 levels = self.campaign.levels 146 level = self.campaign.getlevel(self.campaign_level_name) 147 148 nextlevel: bascenev1.Level | None 149 if level.index < len(levels) - 1: 150 nextlevel = levels[level.index + 1] 151 else: 152 nextlevel = None 153 if nextlevel: 154 gametype = nextlevel.gametype 155 settings = nextlevel.get_settings() 156 157 # Make sure all settings the game expects are present. 158 neededsettings = gametype.get_available_settings(type(self)) 159 for setting in neededsettings: 160 if setting.name not in settings: 161 settings[setting.name] = setting.default 162 163 # We wanna be in the activity's context while taking it down. 164 newactivity = _bascenev1.newactivity(gametype, settings) 165 assert isinstance(newactivity, GameActivity) 166 self._next_game_instance = newactivity 167 self._next_game_level_name = nextlevel.name 168 else: 169 self._next_game_instance = None 170 self._next_game_level_name = None 171 172 # Special case: 173 # If our current level is 'onslaught training', instantiate 174 # our tutorial so its ready to go. (if we haven't run it yet). 175 if ( 176 self.campaign_level_name == 'Onslaught Training' 177 and self._tutorial_activity is None 178 and not self._ran_tutorial_activity 179 ): 180 from bascenev1lib.tutorial import TutorialActivity 181 182 self._tutorial_activity = _bascenev1.newactivity(TutorialActivity) 183 184 @override 185 def get_custom_menu_entries(self) -> list[dict[str, Any]]: 186 return self._custom_menu_ui 187 188 @override 189 def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: 190 super().on_player_leave(sessionplayer) 191 192 _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity)) 193 194 def _handle_empty_activity(self) -> None: 195 """Handle cases where all players have left the current activity.""" 196 197 from bascenev1._gameactivity import GameActivity 198 199 activity = self.getactivity() 200 if activity is None: 201 return # Hmm what should we do in this case? 202 203 # If there are still players in the current activity, we're good. 204 if activity.players: 205 return 206 207 # If there are *not* players in the current activity but there 208 # *are* in the session: 209 if not activity.players and self.sessionplayers: 210 # If we're in a game, we should restart to pull in players 211 # currently waiting in the session. 212 if isinstance(activity, GameActivity): 213 # Never restart tourney games however; just end the session 214 # if all players are gone. 215 if self.tournament_id is not None: 216 self.end() 217 else: 218 self.restart() 219 220 # Hmm; no players anywhere. Let's end the entire session if we're 221 # running a GUI (or just the current game if we're running headless). 222 else: 223 if babase.app.env.gui: 224 self.end() 225 else: 226 if isinstance(activity, GameActivity): 227 with activity.context: 228 activity.end_game() 229 230 def _on_tournament_restart_menu_press( 231 self, resume_callback: Callable[[], Any] 232 ) -> None: 233 # pylint: disable=cyclic-import 234 from bascenev1._gameactivity import GameActivity 235 236 assert babase.app.classic is not None 237 activity = self.getactivity() 238 if activity is not None and not activity.expired: 239 assert self.tournament_id is not None 240 assert isinstance(activity, GameActivity) 241 babase.app.classic.tournament_entry_window( 242 tournament_id=self.tournament_id, 243 tournament_activity=activity, 244 on_close_call=resume_callback, 245 ) 246 247 def restart(self) -> None: 248 """Restart the current game activity.""" 249 250 # Tell the current activity to end with a 'restart' outcome. 251 # We use 'force' so that we apply even if end has already been called 252 # (but is in its delay period). 253 254 # Make an exception if there's no players left. Otherwise this 255 # can override the default session end that occurs in that case. 256 if not self.sessionplayers: 257 return 258 259 # This method may get called from the UI context so make sure we 260 # explicitly run in the activity's context. 261 activity = self.getactivity() 262 if activity is not None and not activity.expired: 263 activity.can_show_ad_on_death = True 264 with activity.context: 265 activity.end(results={'outcome': 'restart'}, force=True) 266 267 # noinspection PyUnresolvedReferences 268 @override 269 def on_activity_end( 270 self, activity: bascenev1.Activity, results: Any 271 ) -> None: 272 """Method override for co-op sessions. 273 274 Jumps between co-op games and score screens. 275 """ 276 # pylint: disable=too-many-branches 277 # pylint: disable=too-many-locals 278 # pylint: disable=too-many-statements 279 # pylint: disable=cyclic-import 280 from bascenev1lib.activity.coopscore import CoopScoreScreen 281 from bascenev1lib.tutorial import TutorialActivity 282 283 from bascenev1._gameresults import GameResults 284 from bascenev1._player import PlayerInfo 285 from bascenev1._activitytypes import JoinActivity, TransitionActivity 286 from bascenev1._coopgame import CoopGameActivity 287 from bascenev1._score import ScoreType 288 289 app = babase.app 290 env = app.env 291 classic = app.classic 292 assert classic is not None 293 294 # If we're running a TeamGameActivity we'll have a GameResults 295 # as results. Otherwise its an old CoopGameActivity so its giving 296 # us a dict of random stuff. 297 if isinstance(results, GameResults): 298 outcome = 'defeat' # This can't be 'beaten'. 299 else: 300 outcome = '' if results is None else results.get('outcome', '') 301 302 # If we're running with a gui and at any point we have no 303 # in-game players, quit out of the session (this can happen if 304 # someone leaves in the tutorial for instance). 305 if env.gui: 306 active_players = [p for p in self.sessionplayers if p.in_game] 307 if not active_players: 308 self.end() 309 return 310 311 # If we're in a between-round activity or a restart-activity, 312 # hop into a round. 313 if isinstance( 314 activity, (JoinActivity, CoopScoreScreen, TransitionActivity) 315 ): 316 if outcome == 'next_level': 317 if self._next_game_instance is None: 318 raise RuntimeError() 319 assert self._next_game_level_name is not None 320 self.campaign_level_name = self._next_game_level_name 321 next_game = self._next_game_instance 322 else: 323 next_game = self._current_game_instance 324 325 # Special case: if we're coming from a joining-activity 326 # and will be going into onslaught-training, show the 327 # tutorial first. 328 if ( 329 isinstance(activity, JoinActivity) 330 and self.campaign_level_name == 'Onslaught Training' 331 and not (env.demo or env.arcade) 332 ): 333 if self._tutorial_activity is None: 334 raise RuntimeError('Tutorial not preloaded properly.') 335 self.setactivity(self._tutorial_activity) 336 self._tutorial_activity = None 337 self._ran_tutorial_activity = True 338 self._custom_menu_ui = [] 339 340 # Normal case; launch the next round. 341 else: 342 # Reset stats for the new activity. 343 self.stats.reset() 344 for player in self.sessionplayers: 345 # Skip players that are still choosing a team. 346 if player.in_game: 347 self.stats.register_sessionplayer(player) 348 self.stats.setactivity(next_game) 349 350 # Now flip the current activity.. 351 self.setactivity(next_game) 352 353 if not (env.demo or env.arcade): 354 if ( 355 self.tournament_id is not None 356 and classic.coop_session_args['submit_score'] 357 ): 358 self._custom_menu_ui = [ 359 { 360 'label': babase.Lstr(resource='restartText'), 361 'resume_on_call': False, 362 'call': babase.WeakCall( 363 self._on_tournament_restart_menu_press 364 ), 365 } 366 ] 367 else: 368 self._custom_menu_ui = [ 369 { 370 'label': babase.Lstr(resource='restartText'), 371 'call': babase.WeakCall(self.restart), 372 } 373 ] 374 375 # If we were in a tutorial, just pop a transition to get to the 376 # actual round. 377 elif isinstance(activity, TutorialActivity): 378 self.setactivity(_bascenev1.newactivity(TransitionActivity)) 379 else: 380 playerinfos: list[bascenev1.PlayerInfo] 381 382 # Generic team games. 383 if isinstance(results, GameResults): 384 playerinfos = results.playerinfos 385 score = results.get_sessionteam_score(results.sessionteams[0]) 386 fail_message = None 387 score_order = ( 388 'decreasing' if results.lower_is_better else 'increasing' 389 ) 390 if results.scoretype in ( 391 ScoreType.SECONDS, 392 ScoreType.MILLISECONDS, 393 ): 394 scoretype = 'time' 395 396 # ScoreScreen wants hundredths of a second. 397 if score is not None: 398 if results.scoretype is ScoreType.SECONDS: 399 score *= 100 400 elif results.scoretype is ScoreType.MILLISECONDS: 401 score //= 10 402 else: 403 raise RuntimeError('FIXME') 404 else: 405 if results.scoretype is not ScoreType.POINTS: 406 print(f'Unknown ScoreType:' f' "{results.scoretype}"') 407 scoretype = 'points' 408 409 # Old coop-game-specific results; should migrate away from these. 410 else: 411 playerinfos = results.get('playerinfos') 412 score = results['score'] if 'score' in results else None 413 fail_message = ( 414 results['fail_message'] 415 if 'fail_message' in results 416 else None 417 ) 418 score_order = ( 419 results['score_order'] 420 if 'score_order' in results 421 else 'increasing' 422 ) 423 activity_score_type = ( 424 activity.get_score_type() 425 if isinstance(activity, CoopGameActivity) 426 else None 427 ) 428 assert activity_score_type is not None 429 scoretype = activity_score_type 430 431 # Validate types. 432 if playerinfos is not None: 433 assert isinstance(playerinfos, list) 434 assert all(isinstance(i, PlayerInfo) for i in playerinfos) 435 436 # Looks like we were in a round - check the outcome and 437 # go from there. 438 if outcome == 'restart': 439 # This will pop up back in the same round. 440 self.setactivity(_bascenev1.newactivity(TransitionActivity)) 441 else: 442 self.setactivity( 443 _bascenev1.newactivity( 444 CoopScoreScreen, 445 { 446 'playerinfos': playerinfos, 447 'score': score, 448 'fail_message': fail_message, 449 'score_order': score_order, 450 'score_type': scoretype, 451 'outcome': outcome, 452 'campaign': self.campaign, 453 'level': self.campaign_level_name, 454 }, 455 ) 456 ) 457 458 # No matter what, get the next 2 levels ready to go. 459 self._update_on_deck_game_instances()
A bascenev1.Session which runs cooperative-mode games.
Category: Gameplay Classes
These generally consist of 1-4 players against the computer and include functionality such as high score lists.
44 def __init__(self) -> None: 45 """Instantiate a co-op mode session.""" 46 # pylint: disable=cyclic-import 47 from bascenev1lib.activity.coopjoin import CoopJoinActivity 48 49 babase.increment_analytics_count('Co-op session start') 50 app = babase.app 51 classic = app.classic 52 assert classic is not None 53 54 # If they passed in explicit min/max, honor that. 55 # Otherwise defer to user overrides or defaults. 56 if 'min_players' in classic.coop_session_args: 57 min_players = classic.coop_session_args['min_players'] 58 else: 59 min_players = 1 60 if 'max_players' in classic.coop_session_args: 61 max_players = classic.coop_session_args['max_players'] 62 else: 63 max_players = app.config.get('Coop Game Max Players', 4) 64 if 'submit_score' in classic.coop_session_args: 65 submit_score = classic.coop_session_args['submit_score'] 66 else: 67 submit_score = True 68 69 # print('FIXME: COOP SESSION WOULD CALC DEPS.') 70 depsets: Sequence[bascenev1.DependencySet] = [] 71 72 super().__init__( 73 depsets, 74 team_names=TEAM_NAMES, 75 team_colors=TEAM_COLORS, 76 min_players=min_players, 77 max_players=max_players, 78 submit_score=submit_score, 79 ) 80 81 # Tournament-ID if we correspond to a co-op tournament (otherwise None) 82 self.tournament_id: str | None = classic.coop_session_args.get( 83 'tournament_id' 84 ) 85 86 self.campaign = classic.getcampaign( 87 classic.coop_session_args['campaign'] 88 ) 89 self.campaign_level_name: str = classic.coop_session_args['level'] 90 91 self._ran_tutorial_activity = False 92 self._tutorial_activity: bascenev1.Activity | None = None 93 self._custom_menu_ui: list[dict[str, Any]] = [] 94 95 # Start our joining screen. 96 self.setactivity(_bascenev1.newactivity(CoopJoinActivity)) 97 98 self._next_game_instance: bascenev1.GameActivity | None = None 99 self._next_game_level_name: str | None = None 100 self._update_on_deck_game_instances()
Instantiate a co-op mode session.
Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.
Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.
The baclassic.Campaign instance this Session represents, or None if there is no associated Campaign.
102 def get_current_game_instance(self) -> bascenev1.GameActivity: 103 """Get the game instance currently being played.""" 104 return self._current_game_instance
Get the game instance currently being played.
106 @override 107 def should_allow_mid_activity_joins( 108 self, activity: bascenev1.Activity 109 ) -> bool: 110 # pylint: disable=cyclic-import 111 from bascenev1._gameactivity import GameActivity 112 113 # Disallow any joins in the middle of the game. 114 if isinstance(activity, GameActivity): 115 return False 116 117 return True
Ask ourself if we should allow joins during an Activity.
Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.
188 @override 189 def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: 190 super().on_player_leave(sessionplayer) 191 192 _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity))
Called when a previously-accepted bascenev1.SessionPlayer leaves.
247 def restart(self) -> None: 248 """Restart the current game activity.""" 249 250 # Tell the current activity to end with a 'restart' outcome. 251 # We use 'force' so that we apply even if end has already been called 252 # (but is in its delay period). 253 254 # Make an exception if there's no players left. Otherwise this 255 # can override the default session end that occurs in that case. 256 if not self.sessionplayers: 257 return 258 259 # This method may get called from the UI context so make sure we 260 # explicitly run in the activity's context. 261 activity = self.getactivity() 262 if activity is not None and not activity.expired: 263 activity.can_show_ad_on_death = True 264 with activity.context: 265 activity.end(results={'outcome': 'restart'}, force=True)
Restart the current game activity.
268 @override 269 def on_activity_end( 270 self, activity: bascenev1.Activity, results: Any 271 ) -> None: 272 """Method override for co-op sessions. 273 274 Jumps between co-op games and score screens. 275 """ 276 # pylint: disable=too-many-branches 277 # pylint: disable=too-many-locals 278 # pylint: disable=too-many-statements 279 # pylint: disable=cyclic-import 280 from bascenev1lib.activity.coopscore import CoopScoreScreen 281 from bascenev1lib.tutorial import TutorialActivity 282 283 from bascenev1._gameresults import GameResults 284 from bascenev1._player import PlayerInfo 285 from bascenev1._activitytypes import JoinActivity, TransitionActivity 286 from bascenev1._coopgame import CoopGameActivity 287 from bascenev1._score import ScoreType 288 289 app = babase.app 290 env = app.env 291 classic = app.classic 292 assert classic is not None 293 294 # If we're running a TeamGameActivity we'll have a GameResults 295 # as results. Otherwise its an old CoopGameActivity so its giving 296 # us a dict of random stuff. 297 if isinstance(results, GameResults): 298 outcome = 'defeat' # This can't be 'beaten'. 299 else: 300 outcome = '' if results is None else results.get('outcome', '') 301 302 # If we're running with a gui and at any point we have no 303 # in-game players, quit out of the session (this can happen if 304 # someone leaves in the tutorial for instance). 305 if env.gui: 306 active_players = [p for p in self.sessionplayers if p.in_game] 307 if not active_players: 308 self.end() 309 return 310 311 # If we're in a between-round activity or a restart-activity, 312 # hop into a round. 313 if isinstance( 314 activity, (JoinActivity, CoopScoreScreen, TransitionActivity) 315 ): 316 if outcome == 'next_level': 317 if self._next_game_instance is None: 318 raise RuntimeError() 319 assert self._next_game_level_name is not None 320 self.campaign_level_name = self._next_game_level_name 321 next_game = self._next_game_instance 322 else: 323 next_game = self._current_game_instance 324 325 # Special case: if we're coming from a joining-activity 326 # and will be going into onslaught-training, show the 327 # tutorial first. 328 if ( 329 isinstance(activity, JoinActivity) 330 and self.campaign_level_name == 'Onslaught Training' 331 and not (env.demo or env.arcade) 332 ): 333 if self._tutorial_activity is None: 334 raise RuntimeError('Tutorial not preloaded properly.') 335 self.setactivity(self._tutorial_activity) 336 self._tutorial_activity = None 337 self._ran_tutorial_activity = True 338 self._custom_menu_ui = [] 339 340 # Normal case; launch the next round. 341 else: 342 # Reset stats for the new activity. 343 self.stats.reset() 344 for player in self.sessionplayers: 345 # Skip players that are still choosing a team. 346 if player.in_game: 347 self.stats.register_sessionplayer(player) 348 self.stats.setactivity(next_game) 349 350 # Now flip the current activity.. 351 self.setactivity(next_game) 352 353 if not (env.demo or env.arcade): 354 if ( 355 self.tournament_id is not None 356 and classic.coop_session_args['submit_score'] 357 ): 358 self._custom_menu_ui = [ 359 { 360 'label': babase.Lstr(resource='restartText'), 361 'resume_on_call': False, 362 'call': babase.WeakCall( 363 self._on_tournament_restart_menu_press 364 ), 365 } 366 ] 367 else: 368 self._custom_menu_ui = [ 369 { 370 'label': babase.Lstr(resource='restartText'), 371 'call': babase.WeakCall(self.restart), 372 } 373 ] 374 375 # If we were in a tutorial, just pop a transition to get to the 376 # actual round. 377 elif isinstance(activity, TutorialActivity): 378 self.setactivity(_bascenev1.newactivity(TransitionActivity)) 379 else: 380 playerinfos: list[bascenev1.PlayerInfo] 381 382 # Generic team games. 383 if isinstance(results, GameResults): 384 playerinfos = results.playerinfos 385 score = results.get_sessionteam_score(results.sessionteams[0]) 386 fail_message = None 387 score_order = ( 388 'decreasing' if results.lower_is_better else 'increasing' 389 ) 390 if results.scoretype in ( 391 ScoreType.SECONDS, 392 ScoreType.MILLISECONDS, 393 ): 394 scoretype = 'time' 395 396 # ScoreScreen wants hundredths of a second. 397 if score is not None: 398 if results.scoretype is ScoreType.SECONDS: 399 score *= 100 400 elif results.scoretype is ScoreType.MILLISECONDS: 401 score //= 10 402 else: 403 raise RuntimeError('FIXME') 404 else: 405 if results.scoretype is not ScoreType.POINTS: 406 print(f'Unknown ScoreType:' f' "{results.scoretype}"') 407 scoretype = 'points' 408 409 # Old coop-game-specific results; should migrate away from these. 410 else: 411 playerinfos = results.get('playerinfos') 412 score = results['score'] if 'score' in results else None 413 fail_message = ( 414 results['fail_message'] 415 if 'fail_message' in results 416 else None 417 ) 418 score_order = ( 419 results['score_order'] 420 if 'score_order' in results 421 else 'increasing' 422 ) 423 activity_score_type = ( 424 activity.get_score_type() 425 if isinstance(activity, CoopGameActivity) 426 else None 427 ) 428 assert activity_score_type is not None 429 scoretype = activity_score_type 430 431 # Validate types. 432 if playerinfos is not None: 433 assert isinstance(playerinfos, list) 434 assert all(isinstance(i, PlayerInfo) for i in playerinfos) 435 436 # Looks like we were in a round - check the outcome and 437 # go from there. 438 if outcome == 'restart': 439 # This will pop up back in the same round. 440 self.setactivity(_bascenev1.newactivity(TransitionActivity)) 441 else: 442 self.setactivity( 443 _bascenev1.newactivity( 444 CoopScoreScreen, 445 { 446 'playerinfos': playerinfos, 447 'score': score, 448 'fail_message': fail_message, 449 'score_order': score_order, 450 'score_type': scoretype, 451 'outcome': outcome, 452 'campaign': self.campaign, 453 'level': self.campaign_level_name, 454 }, 455 ) 456 ) 457 458 # No matter what, get the next 2 levels ready to go. 459 self._update_on_deck_game_instances()
Method override for co-op sessions.
Jumps between co-op games and score screens.
139class Data: 140 """A reference to a data object. 141 142 Category: **Asset Classes** 143 144 Use bascenev1.getdata() to instantiate one. 145 """ 146 147 def getvalue(self) -> Any: 148 """Return the data object's value. 149 150 This can consist of anything representable by json (dicts, lists, 151 numbers, bools, None, etc). 152 Note that this call will block if the data has not yet been loaded, 153 so it can be beneficial to plan a short bit of time between when 154 the data object is requested and when it's value is accessed. 155 """ 156 return _uninferrable()
147 def getvalue(self) -> Any: 148 """Return the data object's value. 149 150 This can consist of anything representable by json (dicts, lists, 151 numbers, bools, None, etc). 152 Note that this call will block if the data has not yet been loaded, 153 so it can be beneficial to plan a short bit of time between when 154 the data object is requested and when it's value is accessed. 155 """ 156 return _uninferrable()
Return the data object's value.
This can consist of anything representable by json (dicts, lists, numbers, bools, None, etc). Note that this call will block if the data has not yet been loaded, so it can be beneficial to plan a short bit of time between when the data object is requested and when it's value is accessed.
38class DeathType(Enum): 39 """A reason for a death. 40 41 Category: Enums 42 """ 43 44 GENERIC = 'generic' 45 OUT_OF_BOUNDS = 'out_of_bounds' 46 IMPACT = 'impact' 47 FALL = 'fall' 48 REACHED_GOAL = 'reached_goal' 49 LEFT_GAME = 'left_game'
A reason for a death.
Category: Enums
23class Dependency(Generic[T]): 24 """A dependency on a DependencyComponent (with an optional config). 25 26 Category: **Dependency Classes** 27 28 This class is used to request and access functionality provided 29 by other DependencyComponent classes from a DependencyComponent class. 30 The class functions as a descriptor, allowing dependencies to 31 be added at a class level much the same as properties or methods 32 and then used with class instances to access those dependencies. 33 For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)' 34 you would then be able to instantiate a FloofClass in your class's 35 methods via self.floofcls(). 36 """ 37 38 def __init__(self, cls: type[T], config: Any = None): 39 """Instantiate a Dependency given a bascenev1.DependencyComponent type. 40 41 Optionally, an arbitrary object can be passed as 'config' to 42 influence dependency calculation for the target class. 43 """ 44 self.cls: type[T] = cls 45 self.config = config 46 self._hash: int | None = None 47 48 def get_hash(self) -> int: 49 """Return the dependency's hash, calculating it if necessary.""" 50 from efro.util import make_hash 51 52 if self._hash is None: 53 self._hash = make_hash((self.cls, self.config)) 54 return self._hash 55 56 def __get__(self, obj: Any, cls: Any = None) -> T: 57 if not isinstance(obj, DependencyComponent): 58 if obj is None: 59 raise TypeError( 60 'Dependency must be accessed through an instance.' 61 ) 62 raise TypeError( 63 f'Dependency cannot be added to class of type {type(obj)}' 64 ' (class must inherit from bascenev1.DependencyComponent).' 65 ) 66 67 # We expect to be instantiated from an already living 68 # DependencyComponent with valid dep-data in place.. 69 assert cls is not None 70 71 # Get the DependencyEntry this instance is associated with and from 72 # there get back to the DependencySet 73 entry = getattr(obj, '_dep_entry') 74 if entry is None: 75 raise RuntimeError('Invalid dependency access.') 76 entry = entry() 77 assert isinstance(entry, DependencyEntry) 78 depset = entry.depset() 79 assert isinstance(depset, DependencySet) 80 81 if not depset.resolved: 82 raise RuntimeError( 83 "Can't access data on an unresolved DependencySet." 84 ) 85 86 # Look up the data in the set based on the hash for this Dependency. 87 assert self._hash in depset.entries 88 entry = depset.entries[self._hash] 89 assert isinstance(entry, DependencyEntry) 90 retval = entry.get_component() 91 assert isinstance(retval, self.cls) 92 return retval
A dependency on a DependencyComponent (with an optional config).
Category: Dependency Classes
This class is used to request and access functionality provided by other DependencyComponent classes from a DependencyComponent class. The class functions as a descriptor, allowing dependencies to be added at a class level much the same as properties or methods and then used with class instances to access those dependencies. For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)' you would then be able to instantiate a FloofClass in your class's methods via self.floofcls().
38 def __init__(self, cls: type[T], config: Any = None): 39 """Instantiate a Dependency given a bascenev1.DependencyComponent type. 40 41 Optionally, an arbitrary object can be passed as 'config' to 42 influence dependency calculation for the target class. 43 """ 44 self.cls: type[T] = cls 45 self.config = config 46 self._hash: int | None = None
Instantiate a Dependency given a bascenev1.DependencyComponent type.
Optionally, an arbitrary object can be passed as 'config' to influence dependency calculation for the target class.
48 def get_hash(self) -> int: 49 """Return the dependency's hash, calculating it if necessary.""" 50 from efro.util import make_hash 51 52 if self._hash is None: 53 self._hash = make_hash((self.cls, self.config)) 54 return self._hash
Return the dependency's hash, calculating it if necessary.
95class DependencyComponent: 96 """Base class for all classes that can act as or use dependencies. 97 98 Category: **Dependency Classes** 99 """ 100 101 _dep_entry: weakref.ref[DependencyEntry] 102 103 def __init__(self) -> None: 104 """Instantiate a DependencyComponent.""" 105 106 # For now lets issue a warning if these are instantiated without 107 # a dep-entry; we'll make this an error once we're no longer 108 # seeing warnings. 109 # entry = getattr(self, '_dep_entry', None) 110 # if entry is None: 111 # print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.') 112 113 @classmethod 114 def dep_is_present(cls, config: Any = None) -> bool: 115 """Return whether this component/config is present on this device.""" 116 del config # Unused here. 117 return True 118 119 @classmethod 120 def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]: 121 """Return any dynamically-calculated deps for this component/config. 122 123 Deps declared statically as part of the class do not need to be 124 included here; this is only for additional deps that may vary based 125 on the dep config value. (for instance a map required by a game type) 126 """ 127 del config # Unused here. 128 return []
Base class for all classes that can act as or use dependencies.
Category: Dependency Classes
103 def __init__(self) -> None: 104 """Instantiate a DependencyComponent.""" 105 106 # For now lets issue a warning if these are instantiated without 107 # a dep-entry; we'll make this an error once we're no longer 108 # seeing warnings. 109 # entry = getattr(self, '_dep_entry', None) 110 # if entry is None: 111 # print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')
Instantiate a DependencyComponent.
113 @classmethod 114 def dep_is_present(cls, config: Any = None) -> bool: 115 """Return whether this component/config is present on this device.""" 116 del config # Unused here. 117 return True
Return whether this component/config is present on this device.
119 @classmethod 120 def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]: 121 """Return any dynamically-calculated deps for this component/config. 122 123 Deps declared statically as part of the class do not need to be 124 included here; this is only for additional deps that may vary based 125 on the dep config value. (for instance a map required by a game type) 126 """ 127 del config # Unused here. 128 return []
Return any dynamically-calculated deps for this component/config.
Deps declared statically as part of the class do not need to be included here; this is only for additional deps that may vary based on the dep config value. (for instance a map required by a game type)
174class DependencySet(Generic[T]): 175 """Set of resolved dependencies and their associated data. 176 177 Category: **Dependency Classes** 178 179 To use DependencyComponents, a set must be created, resolved, and then 180 loaded. The DependencyComponents are only valid while the set remains 181 in existence. 182 """ 183 184 def __init__(self, root_dependency: Dependency[T]): 185 # print('DepSet()') 186 self._root_dependency = root_dependency 187 self._resolved = False 188 self._loaded = False 189 190 # Dependency data indexed by hash. 191 self.entries: dict[int, DependencyEntry] = {} 192 193 # def __del__(self) -> None: 194 # print("~DepSet()") 195 196 def resolve(self) -> None: 197 """Resolve the complete set of required dependencies for this set. 198 199 Raises a bascenev1.DependencyError if dependencies are missing (or 200 other Exception types on other errors). 201 """ 202 203 if self._resolved: 204 raise RuntimeError('DependencySet has already been resolved.') 205 206 # print('RESOLVING DEP SET') 207 208 # First, recursively expand out all dependencies. 209 self._resolve(self._root_dependency, 0) 210 211 # Now, if any dependencies are not present, raise an Exception 212 # telling exactly which ones (so hopefully they'll be able to be 213 # downloaded/etc. 214 missing = [ 215 Dependency(entry.cls, entry.config) 216 for entry in self.entries.values() 217 if not entry.cls.dep_is_present(entry.config) 218 ] 219 if missing: 220 raise DependencyError(missing) 221 222 self._resolved = True 223 # print('RESOLVE SUCCESS!') 224 225 @property 226 def resolved(self) -> bool: 227 """Whether this set has been successfully resolved.""" 228 return self._resolved 229 230 def get_asset_package_ids(self) -> set[str]: 231 """Return the set of asset-package-ids required by this dep-set. 232 233 Must be called on a resolved dep-set. 234 """ 235 ids: set[str] = set() 236 if not self._resolved: 237 raise RuntimeError('Must be called on a resolved dep-set.') 238 for entry in self.entries.values(): 239 if issubclass(entry.cls, AssetPackage): 240 assert isinstance(entry.config, str) 241 ids.add(entry.config) 242 return ids 243 244 def load(self) -> None: 245 """Instantiate all DependencyComponents in the set. 246 247 Returns a wrapper which can be used to instantiate the root dep. 248 """ 249 # NOTE: stuff below here should probably go in a separate 'instantiate' 250 # method or something. 251 if not self._resolved: 252 raise RuntimeError("Can't load an unresolved DependencySet") 253 254 for entry in self.entries.values(): 255 # Do a get on everything which will init all payloads 256 # in the proper order recursively. 257 entry.get_component() 258 259 self._loaded = True 260 261 @property 262 def root(self) -> T: 263 """The instantiated root DependencyComponent instance for the set.""" 264 if not self._loaded: 265 raise RuntimeError('DependencySet is not loaded.') 266 267 rootdata = self.entries[self._root_dependency.get_hash()].component 268 assert isinstance(rootdata, self._root_dependency.cls) 269 return rootdata 270 271 def _resolve(self, dep: Dependency[T], recursion: int) -> None: 272 # Watch for wacky infinite dep loops. 273 if recursion > 10: 274 raise RecursionError('Max recursion reached') 275 276 hashval = dep.get_hash() 277 278 if hashval in self.entries: 279 # Found an already resolved one; we're done here. 280 return 281 282 # Add our entry before we recurse so we don't repeat add it if 283 # there's a dependency loop. 284 self.entries[hashval] = DependencyEntry(self, dep) 285 286 # Grab all Dependency instances we find in the class. 287 subdeps = [ 288 cls 289 for cls in dep.cls.__dict__.values() 290 if isinstance(cls, Dependency) 291 ] 292 293 # ..and add in any dynamic ones it provides. 294 subdeps += dep.cls.get_dynamic_deps(dep.config) 295 for subdep in subdeps: 296 self._resolve(subdep, recursion + 1)
Set of resolved dependencies and their associated data.
Category: Dependency Classes
To use DependencyComponents, a set must be created, resolved, and then loaded. The DependencyComponents are only valid while the set remains in existence.
196 def resolve(self) -> None: 197 """Resolve the complete set of required dependencies for this set. 198 199 Raises a bascenev1.DependencyError if dependencies are missing (or 200 other Exception types on other errors). 201 """ 202 203 if self._resolved: 204 raise RuntimeError('DependencySet has already been resolved.') 205 206 # print('RESOLVING DEP SET') 207 208 # First, recursively expand out all dependencies. 209 self._resolve(self._root_dependency, 0) 210 211 # Now, if any dependencies are not present, raise an Exception 212 # telling exactly which ones (so hopefully they'll be able to be 213 # downloaded/etc. 214 missing = [ 215 Dependency(entry.cls, entry.config) 216 for entry in self.entries.values() 217 if not entry.cls.dep_is_present(entry.config) 218 ] 219 if missing: 220 raise DependencyError(missing) 221 222 self._resolved = True 223 # print('RESOLVE SUCCESS!')
Resolve the complete set of required dependencies for this set.
Raises a bascenev1.DependencyError if dependencies are missing (or other Exception types on other errors).
225 @property 226 def resolved(self) -> bool: 227 """Whether this set has been successfully resolved.""" 228 return self._resolved
Whether this set has been successfully resolved.
230 def get_asset_package_ids(self) -> set[str]: 231 """Return the set of asset-package-ids required by this dep-set. 232 233 Must be called on a resolved dep-set. 234 """ 235 ids: set[str] = set() 236 if not self._resolved: 237 raise RuntimeError('Must be called on a resolved dep-set.') 238 for entry in self.entries.values(): 239 if issubclass(entry.cls, AssetPackage): 240 assert isinstance(entry.config, str) 241 ids.add(entry.config) 242 return ids
Return the set of asset-package-ids required by this dep-set.
Must be called on a resolved dep-set.
244 def load(self) -> None: 245 """Instantiate all DependencyComponents in the set. 246 247 Returns a wrapper which can be used to instantiate the root dep. 248 """ 249 # NOTE: stuff below here should probably go in a separate 'instantiate' 250 # method or something. 251 if not self._resolved: 252 raise RuntimeError("Can't load an unresolved DependencySet") 253 254 for entry in self.entries.values(): 255 # Do a get on everything which will init all payloads 256 # in the proper order recursively. 257 entry.get_component() 258 259 self._loaded = True
Instantiate all DependencyComponents in the set.
Returns a wrapper which can be used to instantiate the root dep.
261 @property 262 def root(self) -> T: 263 """The instantiated root DependencyComponent instance for the set.""" 264 if not self._loaded: 265 raise RuntimeError('DependencySet is not loaded.') 266 267 rootdata = self.entries[self._root_dependency.get_hash()].component 268 assert isinstance(rootdata, self._root_dependency.cls) 269 return rootdata
The instantiated root DependencyComponent instance for the set.
52@dataclass 53class DieMessage: 54 """A message telling an object to die. 55 56 Category: **Message Classes** 57 58 Most bascenev1.Actor-s respond to this. 59 """ 60 61 immediate: bool = False 62 """If this is set to True, the actor should disappear immediately. 63 This is for 'removing' stuff from the game more so than 'killing' 64 it. If False, the actor should die a 'normal' death and can take 65 its time with lingering corpses, sound effects, etc.""" 66 67 how: DeathType = DeathType.GENERIC 68 """The particular reason for death."""
A message telling an object to die.
Category: Message Classes
Most bascenev1.Actor-s respond to this.
762def displaytime() -> babase.DisplayTime: 763 """Return the current display-time in seconds. 764 765 Category: **General Utility Functions** 766 767 Display-time is a time value intended to be used for animation and other 768 visual purposes. It will generally increment by a consistent amount each 769 frame. It will pass at an overall similar rate to AppTime, but trades 770 accuracy for smoothness. 771 772 Note that the value returned here is simply a float; it just has a 773 unique type in the type-checker's eyes to help prevent it from being 774 accidentally used with time functionality expecting other time types. 775 """ 776 import babase # pylint: disable=cyclic-import 777 778 return babase.DisplayTime(0.0)
Return the current display-time in seconds.
Category: General Utility Functions
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.
781def displaytimer(time: float, call: Callable[[], Any]) -> None: 782 """Schedule a callable object to run based on display-time. 783 784 Category: **General Utility Functions** 785 786 This function creates a one-off timer which cannot be canceled or 787 modified once created. If you require the ability to do so, or need 788 a repeating timer, use the babase.DisplayTimer class instead. 789 790 Display-time is a time value intended to be used for animation and other 791 visual purposes. It will generally increment by a consistent amount each 792 frame. It will pass at an overall similar rate to AppTime, but trades 793 accuracy for smoothness. 794 795 ##### Arguments 796 ###### time (float) 797 > Length of time in seconds that the timer will wait before firing. 798 799 ###### call (Callable[[], Any]) 800 > A callable Python object. Note that the timer will retain a 801 strong reference to the callable for as long as the timer exists, so you 802 may want to look into concepts such as babase.WeakCall if that is not 803 desired. 804 805 ##### Examples 806 Print some stuff through time: 807 >>> babase.screenmessage('hello from now!') 808 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 809 ... 'hello from the future!')) 810 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 811 ... 'hello from the future 2!')) 812 """ 813 return None
Schedule a callable object to run based on display-time.
Category: General Utility Functions
This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.DisplayTimer class instead.
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Arguments
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> babase.screenmessage('hello from now!')
>>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
... 'hello from the future!'))
>>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
... 'hello from the future 2!'))
220class DisplayTimer: 221 """Timers are used to run code at later points in time. 222 223 Category: **General Utility Classes** 224 225 This class encapsulates a timer based on display-time. 226 The underlying timer will be destroyed when this object is no longer 227 referenced. If you do not want to worry about keeping a reference to 228 your timer around, use the babase.displaytimer() function instead to get a 229 one-off timer. 230 231 Display-time is a time value intended to be used for animation and 232 other visual purposes. It will generally increment by a consistent 233 amount each frame. It will pass at an overall similar rate to AppTime, 234 but trades accuracy for smoothness. 235 236 ##### Arguments 237 ###### time 238 > Length of time in seconds that the timer will wait before firing. 239 240 ###### call 241 > A callable Python object. Remember that the timer will retain a 242 strong reference to the callable for as long as it exists, so you 243 may want to look into concepts such as babase.WeakCall if that is not 244 desired. 245 246 ###### repeat 247 > If True, the timer will fire repeatedly, with each successive 248 firing having the same delay as the first. 249 250 ##### Example 251 252 Use a Timer object to print repeatedly for a few seconds: 253 ... def say_it(): 254 ... babase.screenmessage('BADGER!') 255 ... def stop_saying_it(): 256 ... global g_timer 257 ... g_timer = None 258 ... babase.screenmessage('MUSHROOM MUSHROOM!') 259 ... # Create our timer; it will run as long as we have the self.t ref. 260 ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) 261 ... # Now fire off a one-shot timer to kill it. 262 ... babase.displaytimer(3.89, stop_saying_it) 263 """ 264 265 def __init__( 266 self, time: float, call: Callable[[], Any], repeat: bool = False 267 ) -> None: 268 pass
Timers are used to run code at later points in time.
Category: General Utility Classes
This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.displaytimer() function instead to get a one-off timer.
Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.
Arguments
time
Length of time in seconds that the timer will wait before firing.
call
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.displaytimer(3.89, stop_saying_it)
156@dataclass 157class DropMessage: 158 """Tells an object that it has dropped what it was holding. 159 160 Category: **Message Classes** 161 """
Tells an object that it has dropped what it was holding.
Category: Message Classes
175@dataclass 176class DroppedMessage: 177 """Tells an object that it has been dropped. 178 179 Category: **Message Classes** 180 """ 181 182 node: bascenev1.Node 183 """The bascenev1.Node doing the dropping."""
Tells an object that it has been dropped.
Category: Message Classes
18class DualTeamSession(MultiTeamSession): 19 """bascenev1.Session type for teams mode games. 20 21 Category: **Gameplay Classes** 22 """ 23 24 # Base class overrides: 25 use_teams = True 26 use_team_colors = True 27 28 _playlist_selection_var = 'Team Tournament Playlist Selection' 29 _playlist_randomize_var = 'Team Tournament Playlist Randomize' 30 _playlists_var = 'Team Tournament Playlists' 31 32 def __init__(self) -> None: 33 babase.increment_analytics_count('Teams session start') 34 super().__init__() 35 36 @override 37 def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None: 38 # pylint: disable=cyclic-import 39 from bascenev1lib.activity.multiteamvictory import ( 40 TeamSeriesVictoryScoreScreenActivity, 41 ) 42 from bascenev1lib.activity.dualteamscore import ( 43 TeamVictoryScoreScreenActivity, 44 ) 45 from bascenev1lib.activity.drawscore import DrawScoreScreenActivity 46 47 winnergroups = results.winnergroups 48 49 # If everyone has the same score, call it a draw. 50 if len(winnergroups) < 2: 51 self.setactivity(_bascenev1.newactivity(DrawScoreScreenActivity)) 52 else: 53 winner = winnergroups[0].teams[0] 54 winner.customdata['score'] += 1 55 56 # If a team has won, show final victory screen. 57 if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1: 58 self.setactivity( 59 _bascenev1.newactivity( 60 TeamSeriesVictoryScoreScreenActivity, 61 {'winner': winner}, 62 ) 63 ) 64 else: 65 self.setactivity( 66 _bascenev1.newactivity( 67 TeamVictoryScoreScreenActivity, {'winner': winner} 68 ) 69 )
bascenev1.Session type for teams mode games.
Category: Gameplay Classes
32 def __init__(self) -> None: 33 babase.increment_analytics_count('Teams session start') 34 super().__init__()
Set up playlists & launch a bascenev1.Activity to accept joiners.
1092def emitfx( 1093 position: Sequence[float], 1094 velocity: Sequence[float] | None = None, 1095 count: int = 10, 1096 scale: float = 1.0, 1097 spread: float = 1.0, 1098 chunk_type: str = 'rock', 1099 emit_type: str = 'chunks', 1100 tendril_type: str = 'smoke', 1101) -> None: 1102 """Emit particles, smoke, etc. into the fx sim layer. 1103 1104 Category: **Gameplay Functions** 1105 1106 The fx sim layer is a secondary dynamics simulation that runs in 1107 the background and just looks pretty; it does not affect gameplay. 1108 Note that the actual amount emitted may vary depending on graphics 1109 settings, exiting element counts, or other factors. 1110 """ 1111 return None
Emit particles, smoke, etc. into the fx sim layer.
Category: Gameplay Functions
The fx sim layer is a secondary dynamics simulation that runs in the background and just looks pretty; it does not affect gameplay. Note that the actual amount emitted may vary depending on graphics settings, exiting element counts, or other factors.
283class EmptyPlayer(Player['bascenev1.EmptyTeam']): 284 """An empty player for use by Activities that don't need to define one. 285 286 Category: Gameplay Classes 287 288 bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing 289 those top level classes as type arguments when defining a 290 bascenev1.Activity reduces type safety. For example, 291 activity.teams[0].player will have type 'Any' in that case. For that 292 reason, it is better to pass EmptyPlayer and EmptyTeam when defining 293 a bascenev1.Activity that does not need custom types of its own. 294 295 Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, 296 so if you want to define your own class for one of them you should do so 297 for both. 298 """
An empty player for use by Activities that don't need to define one.
Category: Gameplay Classes
bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing those top level classes as type arguments when defining a bascenev1.Activity reduces type safety. For example, activity.teams[0].player will have type 'Any' in that case. For that reason, it is better to pass EmptyPlayer and EmptyTeam when defining a bascenev1.Activity that does not need custom types of its own.
Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so for both.
197class EmptyTeam(Team['bascenev1.EmptyPlayer']): 198 """An empty player for use by Activities that don't need to define one. 199 200 Category: **Gameplay Classes** 201 202 bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing 203 those top level classes as type arguments when defining a 204 bascenev1.Activity reduces type safety. For example, 205 activity.teams[0].player will have type 'Any' in that case. For that 206 reason, it is better to pass EmptyPlayer and EmptyTeam when defining 207 a bascenev1.Activity that does not need custom types of its own. 208 209 Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, 210 so if you want to define your own class for one of them you should do so 211 for both. 212 """
An empty player for use by Activities that don't need to define one.
Category: Gameplay Classes
bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing those top level classes as type arguments when defining a bascenev1.Activity reduces type safety. For example, activity.teams[0].player will have type 'Any' in that case. For that reason, it is better to pass EmptyPlayer and EmptyTeam when defining a bascenev1.Activity that does not need custom types of its own.
Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so for both.
51def existing(obj: ExistableT | None) -> ExistableT | None: 52 """Convert invalid references to None for any babase.Existable object. 53 54 Category: **Gameplay Functions** 55 56 To best support type checking, it is important that invalid references 57 not be passed around and instead get converted to values of None. 58 That way the type checker can properly flag attempts to pass possibly-dead 59 objects (FooType | None) into functions expecting only live ones 60 (FooType), etc. This call can be used on any 'existable' object 61 (one with an exists() method) and will convert it to a None value 62 if it does not exist. 63 64 For more info, see notes on 'existables' here: 65 https://ballistica.net/wiki/Coding-Style-Guide 66 """ 67 assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}' 68 return obj if obj is not None and obj.exists() else None
Convert invalid references to None for any babase.Existable object.
Category: Gameplay Functions
To best support type checking, it is important that invalid references not be passed around and instead get converted to values of None. That way the type checker can properly flag attempts to pass possibly-dead objects (FooType | None) into functions expecting only live ones (FooType), etc. This call can be used on any 'existable' object (one with an exists() method) and will convert it to a None value if it does not exist.
For more info, see notes on 'existables' here: https://ballistica.net/wiki/Coding-Style-Guide
22def filter_playlist( 23 playlist: PlaylistType, 24 sessiontype: type[Session], 25 *, 26 add_resolved_type: bool = False, 27 remove_unowned: bool = True, 28 mark_unowned: bool = False, 29 name: str = '?', 30) -> PlaylistType: 31 """Return a filtered version of a playlist. 32 33 Strips out or replaces invalid or unowned game types, makes sure all 34 settings are present, and adds in a 'resolved_type' which is the actual 35 type. 36 """ 37 # pylint: disable=too-many-locals 38 # pylint: disable=too-many-branches 39 # pylint: disable=too-many-statements 40 from bascenev1._map import get_filtered_map_name 41 from bascenev1._gameactivity import GameActivity 42 43 assert babase.app.classic is not None 44 45 goodlist: list[dict] = [] 46 unowned_maps: Sequence[str] 47 available_maps: list[str] = list(babase.app.classic.maps.keys()) 48 if (remove_unowned or mark_unowned) and babase.app.classic is not None: 49 unowned_maps = babase.app.classic.store.get_unowned_maps() 50 unowned_game_types = babase.app.classic.store.get_unowned_game_types() 51 else: 52 unowned_maps = [] 53 unowned_game_types = set() 54 55 for entry in copy.deepcopy(playlist): 56 # 'map' used to be called 'level' here. 57 if 'level' in entry: 58 entry['map'] = entry['level'] 59 del entry['level'] 60 61 # We now stuff map into settings instead of it being its own thing. 62 if 'map' in entry: 63 entry['settings']['map'] = entry['map'] 64 del entry['map'] 65 66 # Update old map names to new ones. 67 entry['settings']['map'] = get_filtered_map_name( 68 entry['settings']['map'] 69 ) 70 if remove_unowned and entry['settings']['map'] in unowned_maps: 71 continue 72 73 # Ok, for each game in our list, try to import the module and grab 74 # the actual game class. add successful ones to our initial list 75 # to present to the user. 76 if not isinstance(entry['type'], str): 77 raise TypeError('invalid entry format') 78 try: 79 # Do some type filters for backwards compat. 80 if entry['type'] in ( 81 'Assault.AssaultGame', 82 'Happy_Thoughts.HappyThoughtsGame', 83 'bsAssault.AssaultGame', 84 'bs_assault.AssaultGame', 85 'bastd.game.assault.AssaultGame', 86 ): 87 entry['type'] = 'bascenev1lib.game.assault.AssaultGame' 88 if entry['type'] in ( 89 'King_of_the_Hill.KingOfTheHillGame', 90 'bsKingOfTheHill.KingOfTheHillGame', 91 'bs_king_of_the_hill.KingOfTheHillGame', 92 'bastd.game.kingofthehill.KingOfTheHillGame', 93 ): 94 entry['type'] = ( 95 'bascenev1lib.game.kingofthehill.KingOfTheHillGame' 96 ) 97 if entry['type'] in ( 98 'Capture_the_Flag.CTFGame', 99 'bsCaptureTheFlag.CTFGame', 100 'bs_capture_the_flag.CTFGame', 101 'bastd.game.capturetheflag.CaptureTheFlagGame', 102 ): 103 entry['type'] = ( 104 'bascenev1lib.game.capturetheflag.CaptureTheFlagGame' 105 ) 106 if entry['type'] in ( 107 'Death_Match.DeathMatchGame', 108 'bsDeathMatch.DeathMatchGame', 109 'bs_death_match.DeathMatchGame', 110 'bastd.game.deathmatch.DeathMatchGame', 111 ): 112 entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame' 113 if entry['type'] in ( 114 'ChosenOne.ChosenOneGame', 115 'bsChosenOne.ChosenOneGame', 116 'bs_chosen_one.ChosenOneGame', 117 'bastd.game.chosenone.ChosenOneGame', 118 ): 119 entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame' 120 if entry['type'] in ( 121 'Conquest.Conquest', 122 'Conquest.ConquestGame', 123 'bsConquest.ConquestGame', 124 'bs_conquest.ConquestGame', 125 'bastd.game.conquest.ConquestGame', 126 ): 127 entry['type'] = 'bascenev1lib.game.conquest.ConquestGame' 128 if entry['type'] in ( 129 'Elimination.EliminationGame', 130 'bsElimination.EliminationGame', 131 'bs_elimination.EliminationGame', 132 'bastd.game.elimination.EliminationGame', 133 ): 134 entry['type'] = 'bascenev1lib.game.elimination.EliminationGame' 135 if entry['type'] in ( 136 'Football.FootballGame', 137 'bsFootball.FootballTeamGame', 138 'bs_football.FootballTeamGame', 139 'bastd.game.football.FootballTeamGame', 140 ): 141 entry['type'] = 'bascenev1lib.game.football.FootballTeamGame' 142 if entry['type'] in ( 143 'Hockey.HockeyGame', 144 'bsHockey.HockeyGame', 145 'bs_hockey.HockeyGame', 146 'bastd.game.hockey.HockeyGame', 147 ): 148 entry['type'] = 'bascenev1lib.game.hockey.HockeyGame' 149 if entry['type'] in ( 150 'Keep_Away.KeepAwayGame', 151 'bsKeepAway.KeepAwayGame', 152 'bs_keep_away.KeepAwayGame', 153 'bastd.game.keepaway.KeepAwayGame', 154 ): 155 entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame' 156 if entry['type'] in ( 157 'Race.RaceGame', 158 'bsRace.RaceGame', 159 'bs_race.RaceGame', 160 'bastd.game.race.RaceGame', 161 ): 162 entry['type'] = 'bascenev1lib.game.race.RaceGame' 163 if entry['type'] in ( 164 'bsEasterEggHunt.EasterEggHuntGame', 165 'bs_easter_egg_hunt.EasterEggHuntGame', 166 'bastd.game.easteregghunt.EasterEggHuntGame', 167 ): 168 entry['type'] = ( 169 'bascenev1lib.game.easteregghunt.EasterEggHuntGame' 170 ) 171 if entry['type'] in ( 172 'bsMeteorShower.MeteorShowerGame', 173 'bs_meteor_shower.MeteorShowerGame', 174 'bastd.game.meteorshower.MeteorShowerGame', 175 ): 176 entry['type'] = ( 177 'bascenev1lib.game.meteorshower.MeteorShowerGame' 178 ) 179 if entry['type'] in ( 180 'bsTargetPractice.TargetPracticeGame', 181 'bs_target_practice.TargetPracticeGame', 182 'bastd.game.targetpractice.TargetPracticeGame', 183 ): 184 entry['type'] = ( 185 'bascenev1lib.game.targetpractice.TargetPracticeGame' 186 ) 187 188 gameclass = babase.getclass(entry['type'], GameActivity) 189 190 if entry['settings']['map'] not in available_maps: 191 raise babase.MapNotFoundError() 192 193 if remove_unowned and gameclass in unowned_game_types: 194 continue 195 if add_resolved_type: 196 entry['resolved_type'] = gameclass 197 if mark_unowned and entry['settings']['map'] in unowned_maps: 198 entry['is_unowned_map'] = True 199 if mark_unowned and gameclass in unowned_game_types: 200 entry['is_unowned_game'] = True 201 202 # Make sure all settings the game defines are present. 203 neededsettings = gameclass.get_available_settings(sessiontype) 204 for setting in neededsettings: 205 if setting.name not in entry['settings']: 206 entry['settings'][setting.name] = setting.default 207 208 goodlist.append(entry) 209 210 except babase.MapNotFoundError: 211 logging.warning( 212 'Map \'%s\' not found while scanning playlist \'%s\'.', 213 entry['settings']['map'], 214 name, 215 ) 216 except ImportError as exc: 217 logging.warning( 218 'Import failed while scanning playlist \'%s\': %s', name, exc 219 ) 220 except Exception: 221 logging.exception('Error in filter_playlist.') 222 223 return goodlist
Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all settings are present, and adds in a 'resolved_type' which is the actual type.
83@dataclass 84class FloatChoiceSetting(ChoiceSetting): 85 """A float setting with multiple choices. 86 87 Category: Settings Classes 88 """ 89 90 default: float 91 choices: list[tuple[str, float]]
A float setting with multiple choices.
Category: Settings Classes
49@dataclass 50class FloatSetting(Setting): 51 """A floating point game setting. 52 53 Category: Settings Classes 54 """ 55 56 default: float 57 min_value: float = 0.0 58 max_value: float = 9999.0 59 increment: float = 1.0
A floating point game setting.
Category: Settings Classes
19class FreeForAllSession(MultiTeamSession): 20 """bascenev1.Session type for free-for-all mode games. 21 22 Category: **Gameplay Classes** 23 """ 24 25 use_teams = False 26 use_team_colors = False 27 _playlist_selection_var = 'Free-for-All Playlist Selection' 28 _playlist_randomize_var = 'Free-for-All Playlist Randomize' 29 _playlists_var = 'Free-for-All Playlists' 30 31 def get_ffa_point_awards(self) -> dict[int, int]: 32 """Return the number of points awarded for different rankings. 33 34 This is based on the current number of players. 35 """ 36 point_awards: dict[int, int] 37 if len(self.sessionplayers) == 1: 38 point_awards = {} 39 elif len(self.sessionplayers) == 2: 40 point_awards = {0: 6} 41 elif len(self.sessionplayers) == 3: 42 point_awards = {0: 6, 1: 3} 43 elif len(self.sessionplayers) == 4: 44 point_awards = {0: 8, 1: 4, 2: 2} 45 elif len(self.sessionplayers) == 5: 46 point_awards = {0: 8, 1: 4, 2: 2} 47 elif len(self.sessionplayers) == 6: 48 point_awards = {0: 8, 1: 4, 2: 2} 49 else: 50 point_awards = {0: 8, 1: 4, 2: 2, 3: 1} 51 return point_awards 52 53 def __init__(self) -> None: 54 babase.increment_analytics_count('Free-for-all session start') 55 super().__init__() 56 57 @override 58 def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None: 59 # pylint: disable=cyclic-import 60 from efro.util import asserttype 61 from bascenev1lib.activity.multiteamvictory import ( 62 TeamSeriesVictoryScoreScreenActivity, 63 ) 64 from bascenev1lib.activity.freeforallvictory import ( 65 FreeForAllVictoryScoreScreenActivity, 66 ) 67 from bascenev1lib.activity.drawscore import DrawScoreScreenActivity 68 69 winners = results.winnergroups 70 71 # If there's multiple players and everyone has the same score, 72 # call it a draw. 73 if len(self.sessionplayers) > 1 and len(winners) < 2: 74 self.setactivity( 75 _bascenev1.newactivity( 76 DrawScoreScreenActivity, {'results': results} 77 ) 78 ) 79 else: 80 # Award different point amounts based on number of players. 81 point_awards = self.get_ffa_point_awards() 82 83 for i, winner in enumerate(winners): 84 for team in winner.teams: 85 points = point_awards[i] if i in point_awards else 0 86 team.customdata['previous_score'] = team.customdata['score'] 87 team.customdata['score'] += points 88 89 series_winners = [ 90 team 91 for team in self.sessionteams 92 if team.customdata['score'] >= self._ffa_series_length 93 ] 94 series_winners.sort( 95 reverse=True, 96 key=lambda t: asserttype(t.customdata['score'], int), 97 ) 98 if len(series_winners) == 1 or ( 99 len(series_winners) > 1 100 and series_winners[0].customdata['score'] 101 != series_winners[1].customdata['score'] 102 ): 103 self.setactivity( 104 _bascenev1.newactivity( 105 TeamSeriesVictoryScoreScreenActivity, 106 {'winner': series_winners[0]}, 107 ) 108 ) 109 else: 110 self.setactivity( 111 _bascenev1.newactivity( 112 FreeForAllVictoryScoreScreenActivity, 113 {'results': results}, 114 ) 115 )
bascenev1.Session type for free-for-all mode games.
Category: Gameplay Classes
53 def __init__(self) -> None: 54 babase.increment_analytics_count('Free-for-all session start') 55 super().__init__()
Set up playlists & launch a bascenev1.Activity to accept joiners.
Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.
Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.
31 def get_ffa_point_awards(self) -> dict[int, int]: 32 """Return the number of points awarded for different rankings. 33 34 This is based on the current number of players. 35 """ 36 point_awards: dict[int, int] 37 if len(self.sessionplayers) == 1: 38 point_awards = {} 39 elif len(self.sessionplayers) == 2: 40 point_awards = {0: 6} 41 elif len(self.sessionplayers) == 3: 42 point_awards = {0: 6, 1: 3} 43 elif len(self.sessionplayers) == 4: 44 point_awards = {0: 8, 1: 4, 2: 2} 45 elif len(self.sessionplayers) == 5: 46 point_awards = {0: 8, 1: 4, 2: 2} 47 elif len(self.sessionplayers) == 6: 48 point_awards = {0: 8, 1: 4, 2: 2} 49 else: 50 point_awards = {0: 8, 1: 4, 2: 2, 3: 1} 51 return point_awards
Return the number of points awarded for different rankings.
This is based on the current number of players.
205@dataclass 206class FreezeMessage: 207 """Tells an object to become frozen. 208 209 Category: **Message Classes** 210 211 As seen in the effects of an ice bascenev1.Bomb. 212 """
Tells an object to become frozen.
Category: Message Classes
As seen in the effects of an ice bascenev1.Bomb.
34class GameActivity(Activity[PlayerT, TeamT]): 35 """Common base class for all game bascenev1.Activities. 36 37 Category: **Gameplay Classes** 38 """ 39 40 # pylint: disable=too-many-public-methods 41 42 # Tips to be presented to the user at the start of the game. 43 tips: list[str | bascenev1.GameTip] = [] 44 45 # Default getname() will return this if not None. 46 name: str | None = None 47 48 # Default get_description() will return this if not None. 49 description: str | None = None 50 51 # Default get_available_settings() will return this if not None. 52 available_settings: list[bascenev1.Setting] | None = None 53 54 # Default getscoreconfig() will return this if not None. 55 scoreconfig: bascenev1.ScoreConfig | None = None 56 57 # Override some defaults. 58 allow_pausing = True 59 allow_kick_idle_players = True 60 61 # Whether to show points for kills. 62 show_kill_points = True 63 64 # If not None, the music type that should play in on_transition_in() 65 # (unless overridden by the map). 66 default_music: bascenev1.MusicType | None = None 67 68 @classmethod 69 def getscoreconfig(cls) -> bascenev1.ScoreConfig: 70 """Return info about game scoring setup; can be overridden by games.""" 71 return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig() 72 73 @classmethod 74 def getname(cls) -> str: 75 """Return a str name for this game type. 76 77 This default implementation simply returns the 'name' class attr. 78 """ 79 return cls.name if cls.name is not None else 'Untitled Game' 80 81 @classmethod 82 def get_display_string(cls, settings: dict | None = None) -> babase.Lstr: 83 """Return a descriptive name for this game/settings combo. 84 85 Subclasses should override getname(); not this. 86 """ 87 name = babase.Lstr(translate=('gameNames', cls.getname())) 88 89 # A few substitutions for 'Epic', 'Solo' etc. modes. 90 # FIXME: Should provide a way for game types to define filters of 91 # their own and should not rely on hard-coded settings names. 92 if settings is not None: 93 if 'Solo Mode' in settings and settings['Solo Mode']: 94 name = babase.Lstr( 95 resource='soloNameFilterText', subs=[('${NAME}', name)] 96 ) 97 if 'Epic Mode' in settings and settings['Epic Mode']: 98 name = babase.Lstr( 99 resource='epicNameFilterText', subs=[('${NAME}', name)] 100 ) 101 102 return name 103 104 @classmethod 105 def get_team_display_string(cls, name: str) -> babase.Lstr: 106 """Given a team name, returns a localized version of it.""" 107 return babase.Lstr(translate=('teamNames', name)) 108 109 @classmethod 110 def get_description(cls, sessiontype: type[bascenev1.Session]) -> str: 111 """Get a str description of this game type. 112 113 The default implementation simply returns the 'description' class var. 114 Classes which want to change their description depending on the session 115 can override this method. 116 """ 117 del sessiontype # Unused arg. 118 return cls.description if cls.description is not None else '' 119 120 @classmethod 121 def get_description_display_string( 122 cls, sessiontype: type[bascenev1.Session] 123 ) -> babase.Lstr: 124 """Return a translated version of get_description(). 125 126 Sub-classes should override get_description(); not this. 127 """ 128 description = cls.get_description(sessiontype) 129 return babase.Lstr(translate=('gameDescriptions', description)) 130 131 @classmethod 132 def get_available_settings( 133 cls, sessiontype: type[bascenev1.Session] 134 ) -> list[bascenev1.Setting]: 135 """Return a list of settings relevant to this game type when 136 running under the provided session type. 137 """ 138 del sessiontype # Unused arg. 139 return [] if cls.available_settings is None else cls.available_settings 140 141 @classmethod 142 def get_supported_maps( 143 cls, sessiontype: type[bascenev1.Session] 144 ) -> list[str]: 145 """ 146 Called by the default bascenev1.GameActivity.create_settings_ui() 147 implementation; should return a list of map names valid 148 for this game-type for the given bascenev1.Session type. 149 """ 150 del sessiontype # Unused arg. 151 assert babase.app.classic is not None 152 return babase.app.classic.getmaps('melee') 153 154 @classmethod 155 def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr: 156 """Given a game config dict, return a short description for it. 157 158 This is used when viewing game-lists or showing what game 159 is up next in a series. 160 """ 161 name = cls.get_display_string(config['settings']) 162 163 # In newer configs, map is in settings; it used to be in the 164 # config root. 165 if 'map' in config['settings']: 166 sval = babase.Lstr( 167 value='${NAME} @ ${MAP}', 168 subs=[ 169 ('${NAME}', name), 170 ( 171 '${MAP}', 172 _map.get_map_display_string( 173 _map.get_filtered_map_name( 174 config['settings']['map'] 175 ) 176 ), 177 ), 178 ], 179 ) 180 elif 'map' in config: 181 sval = babase.Lstr( 182 value='${NAME} @ ${MAP}', 183 subs=[ 184 ('${NAME}', name), 185 ( 186 '${MAP}', 187 _map.get_map_display_string( 188 _map.get_filtered_map_name(config['map']) 189 ), 190 ), 191 ], 192 ) 193 else: 194 print('invalid game config - expected map entry under settings') 195 sval = babase.Lstr(value='???') 196 return sval 197 198 @classmethod 199 def supports_session_type( 200 cls, sessiontype: type[bascenev1.Session] 201 ) -> bool: 202 """Return whether this game supports the provided Session type.""" 203 from bascenev1._multiteamsession import MultiTeamSession 204 205 # By default, games support any versus mode 206 return issubclass(sessiontype, MultiTeamSession) 207 208 def __init__(self, settings: dict): 209 """Instantiate the Activity.""" 210 super().__init__(settings) 211 212 # Holds some flattened info about the player set at the point 213 # when on_begin() is called. 214 self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None 215 216 # Go ahead and get our map loading. 217 self._map_type = _map.get_map_class(self._calc_map_name(settings)) 218 219 self._spawn_sound = _bascenev1.getsound('spawn') 220 self._map_type.preload() 221 self._map: bascenev1.Map | None = None 222 self._powerup_drop_timer: bascenev1.Timer | None = None 223 self._tnt_spawners: dict[int, TNTSpawner] | None = None 224 self._tnt_drop_timer: bascenev1.Timer | None = None 225 self._game_scoreboard_name_text: bascenev1.Actor | None = None 226 self._game_scoreboard_description_text: bascenev1.Actor | None = None 227 self._standard_time_limit_time: int | None = None 228 self._standard_time_limit_timer: bascenev1.Timer | None = None 229 self._standard_time_limit_text: bascenev1.NodeActor | None = None 230 self._standard_time_limit_text_input: bascenev1.NodeActor | None = None 231 self._tournament_time_limit: int | None = None 232 self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None 233 self._tournament_time_limit_title_text: bascenev1.NodeActor | None = ( 234 None 235 ) 236 self._tournament_time_limit_text: bascenev1.NodeActor | None = None 237 self._tournament_time_limit_text_input: bascenev1.NodeActor | None = ( 238 None 239 ) 240 self._zoom_message_times: dict[int, float] = {} 241 242 @property 243 def map(self) -> _map.Map: 244 """The map being used for this game. 245 246 Raises a bascenev1.MapNotFoundError if the map does not currently 247 exist. 248 """ 249 if self._map is None: 250 raise babase.MapNotFoundError 251 return self._map 252 253 def get_instance_display_string(self) -> babase.Lstr: 254 """Return a name for this particular game instance.""" 255 return self.get_display_string(self.settings_raw) 256 257 # noinspection PyUnresolvedReferences 258 def get_instance_scoreboard_display_string(self) -> babase.Lstr: 259 """Return a name for this particular game instance. 260 261 This name is used above the game scoreboard in the corner 262 of the screen, so it should be as concise as possible. 263 """ 264 # If we're in a co-op session, use the level name. 265 # FIXME: Should clean this up. 266 try: 267 from bascenev1._coopsession import CoopSession 268 269 if isinstance(self.session, CoopSession): 270 campaign = self.session.campaign 271 assert campaign is not None 272 return campaign.getlevel( 273 self.session.campaign_level_name 274 ).displayname 275 except Exception: 276 logging.exception('Error getting campaign level name.') 277 return self.get_instance_display_string() 278 279 def get_instance_description(self) -> str | Sequence: 280 """Return a description for this game instance, in English. 281 282 This is shown in the center of the screen below the game name at the 283 start of a game. It should start with a capital letter and end with a 284 period, and can be a bit more verbose than the version returned by 285 get_instance_description_short(). 286 287 Note that translation is applied by looking up the specific returned 288 value as a key, so the number of returned variations should be limited; 289 ideally just one or two. To include arbitrary values in the 290 description, you can return a sequence of values in the following 291 form instead of just a string: 292 293 # This will give us something like 'Score 3 goals.' in English 294 # and can properly translate to 'Anota 3 goles.' in Spanish. 295 # If we just returned the string 'Score 3 Goals' here, there would 296 # have to be a translation entry for each specific number. ew. 297 return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']] 298 299 This way the first string can be consistently translated, with any arg 300 values then substituted into the result. ${ARG1} will be replaced with 301 the first value, ${ARG2} with the second, etc. 302 """ 303 return self.get_description(type(self.session)) 304 305 def get_instance_description_short(self) -> str | Sequence: 306 """Return a short description for this game instance in English. 307 308 This description is used above the game scoreboard in the 309 corner of the screen, so it should be as concise as possible. 310 It should be lowercase and should not contain periods or other 311 punctuation. 312 313 Note that translation is applied by looking up the specific returned 314 value as a key, so the number of returned variations should be limited; 315 ideally just one or two. To include arbitrary values in the 316 description, you can return a sequence of values in the following form 317 instead of just a string: 318 319 # This will give us something like 'score 3 goals' in English 320 # and can properly translate to 'anota 3 goles' in Spanish. 321 # If we just returned the string 'score 3 goals' here, there would 322 # have to be a translation entry for each specific number. ew. 323 return ['score ${ARG1} goals', self.settings_raw['Score to Win']] 324 325 This way the first string can be consistently translated, with any arg 326 values then substituted into the result. ${ARG1} will be replaced 327 with the first value, ${ARG2} with the second, etc. 328 329 """ 330 return '' 331 332 @override 333 def on_transition_in(self) -> None: 334 super().on_transition_in() 335 336 # Make our map. 337 self._map = self._map_type() 338 339 # Give our map a chance to override the music. 340 # (for happy-thoughts and other such themed maps) 341 map_music = self._map_type.get_music_type() 342 music = map_music if map_music is not None else self.default_music 343 344 if music is not None: 345 _music.setmusic(music) 346 347 @override 348 def on_begin(self) -> None: 349 super().on_begin() 350 351 if babase.app.classic is not None: 352 babase.app.classic.game_begin_analytics() 353 354 # We don't do this in on_transition_in because it may depend on 355 # players/teams which aren't available until now. 356 _bascenev1.timer(0.001, self._show_scoreboard_info) 357 _bascenev1.timer(1.0, self._show_info) 358 _bascenev1.timer(2.5, self._show_tip) 359 360 # Store some basic info about players present at start time. 361 self.initialplayerinfos = [ 362 PlayerInfo(name=p.getname(full=True), character=p.character) 363 for p in self.players 364 ] 365 366 # Sort this by name so high score lists/etc will be consistent 367 # regardless of player join order. 368 self.initialplayerinfos.sort(key=lambda x: x.name) 369 370 # If this is a tournament, query info about it such as how much 371 # time is left. 372 tournament_id = self.session.tournament_id 373 if tournament_id is not None: 374 assert babase.app.plus is not None 375 babase.app.plus.tournament_query( 376 args={ 377 'tournamentIDs': [tournament_id], 378 'source': 'in-game time remaining query', 379 }, 380 callback=babase.WeakCall(self._on_tournament_query_response), 381 ) 382 383 def _on_tournament_query_response( 384 self, data: dict[str, Any] | None 385 ) -> None: 386 if data is not None: 387 data_t = data['t'] # This used to be the whole payload. 388 389 # Keep our cached tourney info up to date 390 assert babase.app.classic is not None 391 babase.app.classic.accounts.cache_tournament_info(data_t) 392 self._setup_tournament_time_limit( 393 max(5, data_t[0]['timeRemaining']) 394 ) 395 396 @override 397 def on_player_join(self, player: PlayerT) -> None: 398 super().on_player_join(player) 399 400 # By default, just spawn a dude. 401 self.spawn_player(player) 402 403 @override 404 def handlemessage(self, msg: Any) -> Any: 405 if isinstance(msg, PlayerDiedMessage): 406 # pylint: disable=cyclic-import 407 from bascenev1lib.actor.spaz import Spaz 408 409 player = msg.getplayer(self.playertype) 410 killer = msg.getkillerplayer(self.playertype) 411 412 # Inform our stats of the demise. 413 self.stats.player_was_killed( 414 player, killed=msg.killed, killer=killer 415 ) 416 417 # Award the killer points if he's on a different team. 418 # FIXME: This should not be linked to Spaz actors. 419 # (should move get_death_points to Actor or make it a message) 420 if killer and killer.team is not player.team: 421 assert isinstance(killer.actor, Spaz) 422 pts, importance = killer.actor.get_death_points(msg.how) 423 if not self.has_ended(): 424 self.stats.player_scored( 425 killer, 426 pts, 427 kill=True, 428 victim_player=player, 429 importance=importance, 430 showpoints=self.show_kill_points, 431 ) 432 else: 433 return super().handlemessage(msg) 434 return None 435 436 def _show_scoreboard_info(self) -> None: 437 """Create the game info display. 438 439 This is the thing in the top left corner showing the name 440 and short description of the game. 441 """ 442 # pylint: disable=too-many-locals 443 from bascenev1._freeforallsession import FreeForAllSession 444 from bascenev1._gameutils import animate 445 from bascenev1._nodeactor import NodeActor 446 447 sb_name = self.get_instance_scoreboard_display_string() 448 449 # The description can be either a string or a sequence with args 450 # to swap in post-translation. 451 sb_desc_in = self.get_instance_description_short() 452 sb_desc_l: Sequence 453 if isinstance(sb_desc_in, str): 454 sb_desc_l = [sb_desc_in] # handle simple string case 455 else: 456 sb_desc_l = sb_desc_in 457 if not isinstance(sb_desc_l[0], str): 458 raise TypeError('Invalid format for instance description.') 459 460 is_empty = sb_desc_l[0] == '' 461 subs = [] 462 for i in range(len(sb_desc_l) - 1): 463 subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1]))) 464 translation = babase.Lstr( 465 translate=('gameDescriptions', sb_desc_l[0]), subs=subs 466 ) 467 sb_desc = translation 468 vrmode = babase.app.env.vr 469 yval = -34 if is_empty else -20 470 yval -= 16 471 sbpos = ( 472 (15, yval) 473 if isinstance(self.session, FreeForAllSession) 474 else (15, yval) 475 ) 476 self._game_scoreboard_name_text = NodeActor( 477 _bascenev1.newnode( 478 'text', 479 attrs={ 480 'text': sb_name, 481 'maxwidth': 300, 482 'position': sbpos, 483 'h_attach': 'left', 484 'vr_depth': 10, 485 'v_attach': 'top', 486 'v_align': 'bottom', 487 'color': (1.0, 1.0, 1.0, 1.0), 488 'shadow': 1.0 if vrmode else 0.6, 489 'flatness': 1.0 if vrmode else 0.5, 490 'scale': 1.1, 491 }, 492 ) 493 ) 494 495 assert self._game_scoreboard_name_text.node 496 animate( 497 self._game_scoreboard_name_text.node, 'opacity', {0: 0.0, 1.0: 1.0} 498 ) 499 500 descpos = ( 501 (17, -44 + 10) 502 if isinstance(self.session, FreeForAllSession) 503 else (17, -44 + 10) 504 ) 505 self._game_scoreboard_description_text = NodeActor( 506 _bascenev1.newnode( 507 'text', 508 attrs={ 509 'text': sb_desc, 510 'maxwidth': 480, 511 'position': descpos, 512 'scale': 0.7, 513 'h_attach': 'left', 514 'v_attach': 'top', 515 'v_align': 'top', 516 'shadow': 1.0 if vrmode else 0.7, 517 'flatness': 1.0 if vrmode else 0.8, 518 'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0), 519 }, 520 ) 521 ) 522 523 assert self._game_scoreboard_description_text.node 524 animate( 525 self._game_scoreboard_description_text.node, 526 'opacity', 527 {0: 0.0, 1.0: 1.0}, 528 ) 529 530 def _show_info(self) -> None: 531 """Show the game description.""" 532 from bascenev1._gameutils import animate 533 from bascenev1lib.actor.zoomtext import ZoomText 534 535 name = self.get_instance_display_string() 536 ZoomText( 537 name, 538 maxwidth=800, 539 lifespan=2.5, 540 jitter=2.0, 541 position=(0, 180), 542 flash=False, 543 color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), 544 trailcolor=(0.15, 0.05, 1.0, 0.0), 545 ).autoretain() 546 _bascenev1.timer(0.2, _bascenev1.getsound('gong').play) 547 # _bascenev1.timer( 548 # 0.2, Call(_bascenev1.playsound, _bascenev1.getsound('gong')) 549 # ) 550 551 # The description can be either a string or a sequence with args 552 # to swap in post-translation. 553 desc_in = self.get_instance_description() 554 desc_l: Sequence 555 if isinstance(desc_in, str): 556 desc_l = [desc_in] # handle simple string case 557 else: 558 desc_l = desc_in 559 if not isinstance(desc_l[0], str): 560 raise TypeError('Invalid format for instance description') 561 subs = [] 562 for i in range(len(desc_l) - 1): 563 subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1]))) 564 translation = babase.Lstr( 565 translate=('gameDescriptions', desc_l[0]), subs=subs 566 ) 567 568 # Do some standard filters (epic mode, etc). 569 if self.settings_raw.get('Epic Mode', False): 570 translation = babase.Lstr( 571 resource='epicDescriptionFilterText', 572 subs=[('${DESCRIPTION}', translation)], 573 ) 574 vrmode = babase.app.env.vr 575 dnode = _bascenev1.newnode( 576 'text', 577 attrs={ 578 'v_attach': 'center', 579 'h_attach': 'center', 580 'h_align': 'center', 581 'color': (1, 1, 1, 1), 582 'shadow': 1.0 if vrmode else 0.5, 583 'flatness': 1.0 if vrmode else 0.5, 584 'vr_depth': -30, 585 'position': (0, 80), 586 'scale': 1.2, 587 'maxwidth': 700, 588 'text': translation, 589 }, 590 ) 591 cnode = _bascenev1.newnode( 592 'combine', 593 owner=dnode, 594 attrs={'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4}, 595 ) 596 cnode.connectattr('output', dnode, 'color') 597 keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0} 598 animate(cnode, 'input3', keys) 599 _bascenev1.timer(4.0, dnode.delete) 600 601 def _show_tip(self) -> None: 602 # pylint: disable=too-many-locals 603 from bascenev1._gameutils import animate, GameTip 604 605 # If there's any tips left on the list, display one. 606 if self.tips: 607 tip = self.tips.pop(random.randrange(len(self.tips))) 608 tip_title = babase.Lstr( 609 value='${A}:', subs=[('${A}', babase.Lstr(resource='tipText'))] 610 ) 611 icon: bascenev1.Texture | None = None 612 sound: bascenev1.Sound | None = None 613 if isinstance(tip, GameTip): 614 icon = tip.icon 615 sound = tip.sound 616 tip = tip.text 617 assert isinstance(tip, str) 618 619 # Do a few substitutions. 620 tip_lstr = babase.Lstr( 621 translate=('tips', tip), 622 subs=[ 623 ('${PICKUP}', babase.charstr(babase.SpecialChar.TOP_BUTTON)) 624 ], 625 ) 626 base_position = (75, 50) 627 tip_scale = 0.8 628 tip_title_scale = 1.2 629 vrmode = babase.app.env.vr 630 631 t_offs = -350.0 632 tnode = _bascenev1.newnode( 633 'text', 634 attrs={ 635 'text': tip_lstr, 636 'scale': tip_scale, 637 'maxwidth': 900, 638 'position': (base_position[0] + t_offs, base_position[1]), 639 'h_align': 'left', 640 'vr_depth': 300, 641 'shadow': 1.0 if vrmode else 0.5, 642 'flatness': 1.0 if vrmode else 0.5, 643 'v_align': 'center', 644 'v_attach': 'bottom', 645 }, 646 ) 647 t2pos = ( 648 base_position[0] + t_offs - (20 if icon is None else 82), 649 base_position[1] + 2, 650 ) 651 t2node = _bascenev1.newnode( 652 'text', 653 owner=tnode, 654 attrs={ 655 'text': tip_title, 656 'scale': tip_title_scale, 657 'position': t2pos, 658 'h_align': 'right', 659 'vr_depth': 300, 660 'shadow': 1.0 if vrmode else 0.5, 661 'flatness': 1.0 if vrmode else 0.5, 662 'maxwidth': 140, 663 'v_align': 'center', 664 'v_attach': 'bottom', 665 }, 666 ) 667 if icon is not None: 668 ipos = (base_position[0] + t_offs - 40, base_position[1] + 1) 669 img = _bascenev1.newnode( 670 'image', 671 attrs={ 672 'texture': icon, 673 'position': ipos, 674 'scale': (50, 50), 675 'opacity': 1.0, 676 'vr_depth': 315, 677 'color': (1, 1, 1), 678 'absolute_scale': True, 679 'attach': 'bottomCenter', 680 }, 681 ) 682 animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) 683 _bascenev1.timer(5.0, img.delete) 684 if sound is not None: 685 sound.play() 686 687 combine = _bascenev1.newnode( 688 'combine', 689 owner=tnode, 690 attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4}, 691 ) 692 combine.connectattr('output', tnode, 'color') 693 combine.connectattr('output', t2node, 'color') 694 animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) 695 _bascenev1.timer(5.0, tnode.delete) 696 697 @override 698 def end( 699 self, results: Any = None, delay: float = 0.0, force: bool = False 700 ) -> None: 701 from bascenev1._gameresults import GameResults 702 703 # If results is a standard team-game-results, associate it with us 704 # so it can grab our score prefs. 705 if isinstance(results, GameResults): 706 results.set_game(self) 707 708 # If we had a standard time-limit that had not expired, stop it so 709 # it doesnt tick annoyingly. 710 if ( 711 self._standard_time_limit_time is not None 712 and self._standard_time_limit_time > 0 713 ): 714 self._standard_time_limit_timer = None 715 self._standard_time_limit_text = None 716 717 # Ditto with tournament time limits. 718 if ( 719 self._tournament_time_limit is not None 720 and self._tournament_time_limit > 0 721 ): 722 self._tournament_time_limit_timer = None 723 self._tournament_time_limit_text = None 724 self._tournament_time_limit_title_text = None 725 726 super().end(results, delay, force) 727 728 def end_game(self) -> None: 729 """Tell the game to wrap up and call bascenev1.Activity.end(). 730 731 This method should be overridden by subclasses. A game should always 732 be prepared to end and deliver results, even if there is no 'winner' 733 yet; this way things like the standard time-limit 734 (bascenev1.GameActivity.setup_standard_time_limit()) will work with 735 the game. 736 """ 737 print( 738 'WARNING: default end_game() implementation called;' 739 ' your game should override this.' 740 ) 741 742 def respawn_player( 743 self, player: PlayerT, respawn_time: float | None = None 744 ) -> None: 745 """ 746 Given a bascenev1.Player, sets up a standard respawn timer, 747 along with the standard counter display, etc. 748 At the end of the respawn period spawn_player() will 749 be called if the Player still exists. 750 An explicit 'respawn_time' can optionally be provided 751 (in seconds). 752 """ 753 # pylint: disable=cyclic-import 754 755 assert player 756 if respawn_time is None: 757 teamsize = len(player.team.players) 758 if teamsize == 1: 759 respawn_time = 3.0 760 elif teamsize == 2: 761 respawn_time = 5.0 762 elif teamsize == 3: 763 respawn_time = 6.0 764 else: 765 respawn_time = 7.0 766 767 # If this standard setting is present, factor it in. 768 if 'Respawn Times' in self.settings_raw: 769 respawn_time *= self.settings_raw['Respawn Times'] 770 771 # We want whole seconds. 772 assert respawn_time is not None 773 respawn_time = round(max(1.0, respawn_time), 0) 774 775 if player.actor and not self.has_ended(): 776 from bascenev1lib.actor.respawnicon import RespawnIcon 777 778 player.customdata['respawn_timer'] = _bascenev1.Timer( 779 respawn_time, 780 babase.WeakCall(self.spawn_player_if_exists, player), 781 ) 782 player.customdata['respawn_icon'] = RespawnIcon( 783 player, respawn_time 784 ) 785 786 def spawn_player_if_exists(self, player: PlayerT) -> None: 787 """ 788 A utility method which calls self.spawn_player() *only* if the 789 bascenev1.Player provided still exists; handy for use in timers 790 and whatnot. 791 792 There is no need to override this; just override spawn_player(). 793 """ 794 if player: 795 self.spawn_player(player) 796 797 def spawn_player(self, player: PlayerT) -> bascenev1.Actor: 798 """Spawn *something* for the provided bascenev1.Player. 799 800 The default implementation simply calls spawn_player_spaz(). 801 """ 802 assert player # Dead references should never be passed as args. 803 804 return self.spawn_player_spaz(player) 805 806 def spawn_player_spaz( 807 self, 808 player: PlayerT, 809 position: Sequence[float] = (0, 0, 0), 810 angle: float | None = None, 811 ) -> PlayerSpaz: 812 """Create and wire up a bascenev1.PlayerSpaz for the provided Player.""" 813 # pylint: disable=too-many-locals 814 # pylint: disable=cyclic-import 815 from bascenev1._gameutils import animate 816 from bascenev1._coopsession import CoopSession 817 from bascenev1lib.actor.playerspaz import PlayerSpaz 818 819 name = player.getname() 820 color = player.color 821 highlight = player.highlight 822 823 playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz) 824 if not issubclass(playerspaztype, PlayerSpaz): 825 playerspaztype = PlayerSpaz 826 827 light_color = babase.normalized_color(color) 828 display_color = babase.safecolor(color, target_intensity=0.75) 829 spaz = playerspaztype( 830 color=color, 831 highlight=highlight, 832 character=player.character, 833 player=player, 834 ) 835 836 player.actor = spaz 837 assert spaz.node 838 839 # If this is co-op and we're on Courtyard or Runaround, add the 840 # material that allows us to collide with the player-walls. 841 # FIXME: Need to generalize this. 842 if isinstance(self.session, CoopSession) and self.map.getname() in [ 843 'Courtyard', 844 'Tower D', 845 ]: 846 mat = self.map.preloaddata['collide_with_wall_material'] 847 assert isinstance(spaz.node.materials, tuple) 848 assert isinstance(spaz.node.roller_materials, tuple) 849 spaz.node.materials += (mat,) 850 spaz.node.roller_materials += (mat,) 851 852 spaz.node.name = name 853 spaz.node.name_color = display_color 854 spaz.connect_controls_to_player() 855 856 # Move to the stand position and add a flash of light. 857 spaz.handlemessage( 858 StandMessage( 859 position, angle if angle is not None else random.uniform(0, 360) 860 ) 861 ) 862 self._spawn_sound.play(1, position=spaz.node.position) 863 light = _bascenev1.newnode('light', attrs={'color': light_color}) 864 spaz.node.connectattr('position', light, 'position') 865 animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) 866 _bascenev1.timer(0.5, light.delete) 867 return spaz 868 869 def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: 870 """Create standard powerup drops for the current map.""" 871 # pylint: disable=cyclic-import 872 from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL 873 874 self._powerup_drop_timer = _bascenev1.Timer( 875 DEFAULT_POWERUP_INTERVAL, 876 babase.WeakCall(self._standard_drop_powerups), 877 repeat=True, 878 ) 879 self._standard_drop_powerups() 880 if enable_tnt: 881 self._tnt_spawners = {} 882 self._setup_standard_tnt_drops() 883 884 def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: 885 # pylint: disable=cyclic-import 886 from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory 887 888 PowerupBox( 889 position=self.map.powerup_spawn_points[index], 890 poweruptype=PowerupBoxFactory.get().get_random_powerup_type(), 891 expire=expire, 892 ).autoretain() 893 894 def _standard_drop_powerups(self) -> None: 895 """Standard powerup drop.""" 896 897 # Drop one powerup per point. 898 points = self.map.powerup_spawn_points 899 for i in range(len(points)): 900 _bascenev1.timer( 901 i * 0.4, babase.WeakCall(self._standard_drop_powerup, i) 902 ) 903 904 def _setup_standard_tnt_drops(self) -> None: 905 """Standard tnt drop.""" 906 # pylint: disable=cyclic-import 907 from bascenev1lib.actor.bomb import TNTSpawner 908 909 for i, point in enumerate(self.map.tnt_points): 910 assert self._tnt_spawners is not None 911 if self._tnt_spawners.get(i) is None: 912 self._tnt_spawners[i] = TNTSpawner(point) 913 914 def setup_standard_time_limit(self, duration: float) -> None: 915 """ 916 Create a standard game time-limit given the provided 917 duration in seconds. 918 This will be displayed at the top of the screen. 919 If the time-limit expires, end_game() will be called. 920 """ 921 from bascenev1._nodeactor import NodeActor 922 923 if duration <= 0.0: 924 return 925 self._standard_time_limit_time = int(duration) 926 self._standard_time_limit_timer = _bascenev1.Timer( 927 1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True 928 ) 929 self._standard_time_limit_text = NodeActor( 930 _bascenev1.newnode( 931 'text', 932 attrs={ 933 'v_attach': 'top', 934 'h_attach': 'center', 935 'h_align': 'left', 936 'color': (1.0, 1.0, 1.0, 0.5), 937 'position': (-25, -30), 938 'flatness': 1.0, 939 'scale': 0.9, 940 }, 941 ) 942 ) 943 self._standard_time_limit_text_input = NodeActor( 944 _bascenev1.newnode( 945 'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0} 946 ) 947 ) 948 self.globalsnode.connectattr( 949 'time', self._standard_time_limit_text_input.node, 'time1' 950 ) 951 assert self._standard_time_limit_text_input.node 952 assert self._standard_time_limit_text.node 953 self._standard_time_limit_text_input.node.connectattr( 954 'output', self._standard_time_limit_text.node, 'text' 955 ) 956 957 def _standard_time_limit_tick(self) -> None: 958 from bascenev1._gameutils import animate 959 960 assert self._standard_time_limit_time is not None 961 self._standard_time_limit_time -= 1 962 if self._standard_time_limit_time <= 10: 963 if self._standard_time_limit_time == 10: 964 assert self._standard_time_limit_text is not None 965 assert self._standard_time_limit_text.node 966 self._standard_time_limit_text.node.scale = 1.3 967 self._standard_time_limit_text.node.position = (-30, -45) 968 cnode = _bascenev1.newnode( 969 'combine', 970 owner=self._standard_time_limit_text.node, 971 attrs={'size': 4}, 972 ) 973 cnode.connectattr( 974 'output', self._standard_time_limit_text.node, 'color' 975 ) 976 animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) 977 animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) 978 animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) 979 cnode.input3 = 1.0 980 _bascenev1.getsound('tick').play() 981 if self._standard_time_limit_time <= 0: 982 self._standard_time_limit_timer = None 983 self.end_game() 984 node = _bascenev1.newnode( 985 'text', 986 attrs={ 987 'v_attach': 'top', 988 'h_attach': 'center', 989 'h_align': 'center', 990 'color': (1, 0.7, 0, 1), 991 'position': (0, -90), 992 'scale': 1.2, 993 'text': babase.Lstr(resource='timeExpiredText'), 994 }, 995 ) 996 _bascenev1.getsound('refWhistle').play() 997 animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2}) 998 999 def _setup_tournament_time_limit(self, duration: float) -> None: 1000 """ 1001 Create a tournament game time-limit given the provided 1002 duration in seconds. 1003 This will be displayed at the top of the screen. 1004 If the time-limit expires, end_game() will be called. 1005 """ 1006 from bascenev1._nodeactor import NodeActor 1007 1008 if duration <= 0.0: 1009 return 1010 self._tournament_time_limit = int(duration) 1011 1012 # We want this timer to match the server's time as close as possible, 1013 # so lets go with base-time. Theoretically we should do real-time but 1014 # then we have to mess with contexts and whatnot since its currently 1015 # not available in activity contexts. :-/ 1016 self._tournament_time_limit_timer = _bascenev1.BaseTimer( 1017 1.0, babase.WeakCall(self._tournament_time_limit_tick), repeat=True 1018 ) 1019 self._tournament_time_limit_title_text = NodeActor( 1020 _bascenev1.newnode( 1021 'text', 1022 attrs={ 1023 'v_attach': 'bottom', 1024 'h_attach': 'left', 1025 'h_align': 'center', 1026 'v_align': 'center', 1027 'vr_depth': 300, 1028 'maxwidth': 100, 1029 'color': (1.0, 1.0, 1.0, 0.5), 1030 'position': (60, 50), 1031 'flatness': 1.0, 1032 'scale': 0.5, 1033 'text': babase.Lstr(resource='tournamentText'), 1034 }, 1035 ) 1036 ) 1037 self._tournament_time_limit_text = NodeActor( 1038 _bascenev1.newnode( 1039 'text', 1040 attrs={ 1041 'v_attach': 'bottom', 1042 'h_attach': 'left', 1043 'h_align': 'center', 1044 'v_align': 'center', 1045 'vr_depth': 300, 1046 'maxwidth': 100, 1047 'color': (1.0, 1.0, 1.0, 0.5), 1048 'position': (60, 30), 1049 'flatness': 1.0, 1050 'scale': 0.9, 1051 }, 1052 ) 1053 ) 1054 self._tournament_time_limit_text_input = NodeActor( 1055 _bascenev1.newnode( 1056 'timedisplay', 1057 attrs={ 1058 'timemin': 0, 1059 'time2': self._tournament_time_limit * 1000, 1060 }, 1061 ) 1062 ) 1063 assert self._tournament_time_limit_text.node 1064 assert self._tournament_time_limit_text_input.node 1065 self._tournament_time_limit_text_input.node.connectattr( 1066 'output', self._tournament_time_limit_text.node, 'text' 1067 ) 1068 1069 def _tournament_time_limit_tick(self) -> None: 1070 from bascenev1._gameutils import animate 1071 1072 assert self._tournament_time_limit is not None 1073 self._tournament_time_limit -= 1 1074 if self._tournament_time_limit <= 10: 1075 if self._tournament_time_limit == 10: 1076 assert self._tournament_time_limit_title_text is not None 1077 assert self._tournament_time_limit_title_text.node 1078 assert self._tournament_time_limit_text is not None 1079 assert self._tournament_time_limit_text.node 1080 self._tournament_time_limit_title_text.node.scale = 1.0 1081 self._tournament_time_limit_text.node.scale = 1.3 1082 self._tournament_time_limit_title_text.node.position = (80, 85) 1083 self._tournament_time_limit_text.node.position = (80, 60) 1084 cnode = _bascenev1.newnode( 1085 'combine', 1086 owner=self._tournament_time_limit_text.node, 1087 attrs={'size': 4}, 1088 ) 1089 cnode.connectattr( 1090 'output', 1091 self._tournament_time_limit_title_text.node, 1092 'color', 1093 ) 1094 cnode.connectattr( 1095 'output', self._tournament_time_limit_text.node, 'color' 1096 ) 1097 animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) 1098 animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) 1099 animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) 1100 cnode.input3 = 1.0 1101 _bascenev1.getsound('tick').play() 1102 if self._tournament_time_limit <= 0: 1103 self._tournament_time_limit_timer = None 1104 self.end_game() 1105 tval = babase.Lstr( 1106 resource='tournamentTimeExpiredText', 1107 fallback_resource='timeExpiredText', 1108 ) 1109 node = _bascenev1.newnode( 1110 'text', 1111 attrs={ 1112 'v_attach': 'top', 1113 'h_attach': 'center', 1114 'h_align': 'center', 1115 'color': (1, 0.7, 0, 1), 1116 'position': (0, -200), 1117 'scale': 1.6, 1118 'text': tval, 1119 }, 1120 ) 1121 _bascenev1.getsound('refWhistle').play() 1122 animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2}) 1123 1124 # Normally we just connect this to time, but since this is a bit of a 1125 # funky setup we just update it manually once per second. 1126 assert self._tournament_time_limit_text_input is not None 1127 assert self._tournament_time_limit_text_input.node 1128 self._tournament_time_limit_text_input.node.time2 = ( 1129 self._tournament_time_limit * 1000 1130 ) 1131 1132 def show_zoom_message( 1133 self, 1134 message: babase.Lstr, 1135 *, 1136 color: Sequence[float] = (0.9, 0.4, 0.0), 1137 scale: float = 0.8, 1138 duration: float = 2.0, 1139 trail: bool = False, 1140 ) -> None: 1141 """Zooming text used to announce game names and winners.""" 1142 # pylint: disable=cyclic-import 1143 from bascenev1lib.actor.zoomtext import ZoomText 1144 1145 # Reserve a spot on the screen (in case we get multiple of these so 1146 # they don't overlap). 1147 i = 0 1148 cur_time = babase.apptime() 1149 while True: 1150 if ( 1151 i not in self._zoom_message_times 1152 or self._zoom_message_times[i] < cur_time 1153 ): 1154 self._zoom_message_times[i] = cur_time + duration 1155 break 1156 i += 1 1157 ZoomText( 1158 message, 1159 lifespan=duration, 1160 jitter=2.0, 1161 position=(0, 200 - i * 100), 1162 scale=scale, 1163 maxwidth=800, 1164 trail=trail, 1165 color=color, 1166 ).autoretain() 1167 1168 def _calc_map_name(self, settings: dict) -> str: 1169 map_name: str 1170 if 'map' in settings: 1171 map_name = settings['map'] 1172 else: 1173 # If settings doesn't specify a map, pick a random one from the 1174 # list of supported ones. 1175 unowned_maps: list[str] = ( 1176 babase.app.classic.store.get_unowned_maps() 1177 if babase.app.classic is not None 1178 else [] 1179 ) 1180 valid_maps: list[str] = [ 1181 m 1182 for m in self.get_supported_maps(type(self.session)) 1183 if m not in unowned_maps 1184 ] 1185 if not valid_maps: 1186 _bascenev1.broadcastmessage( 1187 babase.Lstr(resource='noValidMapsErrorText') 1188 ) 1189 raise RuntimeError('No valid maps') 1190 map_name = valid_maps[random.randrange(len(valid_maps))] 1191 return map_name
Common base class for all game bascenev1.Activities.
Category: Gameplay Classes
208 def __init__(self, settings: dict): 209 """Instantiate the Activity.""" 210 super().__init__(settings) 211 212 # Holds some flattened info about the player set at the point 213 # when on_begin() is called. 214 self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None 215 216 # Go ahead and get our map loading. 217 self._map_type = _map.get_map_class(self._calc_map_name(settings)) 218 219 self._spawn_sound = _bascenev1.getsound('spawn') 220 self._map_type.preload() 221 self._map: bascenev1.Map | None = None 222 self._powerup_drop_timer: bascenev1.Timer | None = None 223 self._tnt_spawners: dict[int, TNTSpawner] | None = None 224 self._tnt_drop_timer: bascenev1.Timer | None = None 225 self._game_scoreboard_name_text: bascenev1.Actor | None = None 226 self._game_scoreboard_description_text: bascenev1.Actor | None = None 227 self._standard_time_limit_time: int | None = None 228 self._standard_time_limit_timer: bascenev1.Timer | None = None 229 self._standard_time_limit_text: bascenev1.NodeActor | None = None 230 self._standard_time_limit_text_input: bascenev1.NodeActor | None = None 231 self._tournament_time_limit: int | None = None 232 self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None 233 self._tournament_time_limit_title_text: bascenev1.NodeActor | None = ( 234 None 235 ) 236 self._tournament_time_limit_text: bascenev1.NodeActor | None = None 237 self._tournament_time_limit_text_input: bascenev1.NodeActor | None = ( 238 None 239 ) 240 self._zoom_message_times: dict[int, float] = {}
Instantiate the Activity.
Whether idle players can potentially be kicked (should not happen in menus/etc).
68 @classmethod 69 def getscoreconfig(cls) -> bascenev1.ScoreConfig: 70 """Return info about game scoring setup; can be overridden by games.""" 71 return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig()
Return info about game scoring setup; can be overridden by games.
73 @classmethod 74 def getname(cls) -> str: 75 """Return a str name for this game type. 76 77 This default implementation simply returns the 'name' class attr. 78 """ 79 return cls.name if cls.name is not None else 'Untitled Game'
Return a str name for this game type.
This default implementation simply returns the 'name' class attr.
81 @classmethod 82 def get_display_string(cls, settings: dict | None = None) -> babase.Lstr: 83 """Return a descriptive name for this game/settings combo. 84 85 Subclasses should override getname(); not this. 86 """ 87 name = babase.Lstr(translate=('gameNames', cls.getname())) 88 89 # A few substitutions for 'Epic', 'Solo' etc. modes. 90 # FIXME: Should provide a way for game types to define filters of 91 # their own and should not rely on hard-coded settings names. 92 if settings is not None: 93 if 'Solo Mode' in settings and settings['Solo Mode']: 94 name = babase.Lstr( 95 resource='soloNameFilterText', subs=[('${NAME}', name)] 96 ) 97 if 'Epic Mode' in settings and settings['Epic Mode']: 98 name = babase.Lstr( 99 resource='epicNameFilterText', subs=[('${NAME}', name)] 100 ) 101 102 return name
Return a descriptive name for this game/settings combo.
Subclasses should override getname(); not this.
104 @classmethod 105 def get_team_display_string(cls, name: str) -> babase.Lstr: 106 """Given a team name, returns a localized version of it.""" 107 return babase.Lstr(translate=('teamNames', name))
Given a team name, returns a localized version of it.
109 @classmethod 110 def get_description(cls, sessiontype: type[bascenev1.Session]) -> str: 111 """Get a str description of this game type. 112 113 The default implementation simply returns the 'description' class var. 114 Classes which want to change their description depending on the session 115 can override this method. 116 """ 117 del sessiontype # Unused arg. 118 return cls.description if cls.description is not None else ''
Get a str description of this game type.
The default implementation simply returns the 'description' class var. Classes which want to change their description depending on the session can override this method.
120 @classmethod 121 def get_description_display_string( 122 cls, sessiontype: type[bascenev1.Session] 123 ) -> babase.Lstr: 124 """Return a translated version of get_description(). 125 126 Sub-classes should override get_description(); not this. 127 """ 128 description = cls.get_description(sessiontype) 129 return babase.Lstr(translate=('gameDescriptions', description))
Return a translated version of get_description().
Sub-classes should override get_description(); not this.
131 @classmethod 132 def get_available_settings( 133 cls, sessiontype: type[bascenev1.Session] 134 ) -> list[bascenev1.Setting]: 135 """Return a list of settings relevant to this game type when 136 running under the provided session type. 137 """ 138 del sessiontype # Unused arg. 139 return [] if cls.available_settings is None else cls.available_settings
Return a list of settings relevant to this game type when running under the provided session type.
141 @classmethod 142 def get_supported_maps( 143 cls, sessiontype: type[bascenev1.Session] 144 ) -> list[str]: 145 """ 146 Called by the default bascenev1.GameActivity.create_settings_ui() 147 implementation; should return a list of map names valid 148 for this game-type for the given bascenev1.Session type. 149 """ 150 del sessiontype # Unused arg. 151 assert babase.app.classic is not None 152 return babase.app.classic.getmaps('melee')
Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.
154 @classmethod 155 def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr: 156 """Given a game config dict, return a short description for it. 157 158 This is used when viewing game-lists or showing what game 159 is up next in a series. 160 """ 161 name = cls.get_display_string(config['settings']) 162 163 # In newer configs, map is in settings; it used to be in the 164 # config root. 165 if 'map' in config['settings']: 166 sval = babase.Lstr( 167 value='${NAME} @ ${MAP}', 168 subs=[ 169 ('${NAME}', name), 170 ( 171 '${MAP}', 172 _map.get_map_display_string( 173 _map.get_filtered_map_name( 174 config['settings']['map'] 175 ) 176 ), 177 ), 178 ], 179 ) 180 elif 'map' in config: 181 sval = babase.Lstr( 182 value='${NAME} @ ${MAP}', 183 subs=[ 184 ('${NAME}', name), 185 ( 186 '${MAP}', 187 _map.get_map_display_string( 188 _map.get_filtered_map_name(config['map']) 189 ), 190 ), 191 ], 192 ) 193 else: 194 print('invalid game config - expected map entry under settings') 195 sval = babase.Lstr(value='???') 196 return sval
Given a game config dict, return a short description for it.
This is used when viewing game-lists or showing what game is up next in a series.
198 @classmethod 199 def supports_session_type( 200 cls, sessiontype: type[bascenev1.Session] 201 ) -> bool: 202 """Return whether this game supports the provided Session type.""" 203 from bascenev1._multiteamsession import MultiTeamSession 204 205 # By default, games support any versus mode 206 return issubclass(sessiontype, MultiTeamSession)
Return whether this game supports the provided Session type.
242 @property 243 def map(self) -> _map.Map: 244 """The map being used for this game. 245 246 Raises a bascenev1.MapNotFoundError if the map does not currently 247 exist. 248 """ 249 if self._map is None: 250 raise babase.MapNotFoundError 251 return self._map
The map being used for this game.
Raises a bascenev1.MapNotFoundError if the map does not currently exist.
253 def get_instance_display_string(self) -> babase.Lstr: 254 """Return a name for this particular game instance.""" 255 return self.get_display_string(self.settings_raw)
Return a name for this particular game instance.
258 def get_instance_scoreboard_display_string(self) -> babase.Lstr: 259 """Return a name for this particular game instance. 260 261 This name is used above the game scoreboard in the corner 262 of the screen, so it should be as concise as possible. 263 """ 264 # If we're in a co-op session, use the level name. 265 # FIXME: Should clean this up. 266 try: 267 from bascenev1._coopsession import CoopSession 268 269 if isinstance(self.session, CoopSession): 270 campaign = self.session.campaign 271 assert campaign is not None 272 return campaign.getlevel( 273 self.session.campaign_level_name 274 ).displayname 275 except Exception: 276 logging.exception('Error getting campaign level name.') 277 return self.get_instance_display_string()
Return a name for this particular game instance.
This name is used above the game scoreboard in the corner of the screen, so it should be as concise as possible.
279 def get_instance_description(self) -> str | Sequence: 280 """Return a description for this game instance, in English. 281 282 This is shown in the center of the screen below the game name at the 283 start of a game. It should start with a capital letter and end with a 284 period, and can be a bit more verbose than the version returned by 285 get_instance_description_short(). 286 287 Note that translation is applied by looking up the specific returned 288 value as a key, so the number of returned variations should be limited; 289 ideally just one or two. To include arbitrary values in the 290 description, you can return a sequence of values in the following 291 form instead of just a string: 292 293 # This will give us something like 'Score 3 goals.' in English 294 # and can properly translate to 'Anota 3 goles.' in Spanish. 295 # If we just returned the string 'Score 3 Goals' here, there would 296 # have to be a translation entry for each specific number. ew. 297 return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']] 298 299 This way the first string can be consistently translated, with any arg 300 values then substituted into the result. ${ARG1} will be replaced with 301 the first value, ${ARG2} with the second, etc. 302 """ 303 return self.get_description(type(self.session))
Return a description for this game instance, in English.
This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'Score 3 goals.' in English
and can properly translate to 'Anota 3 goles.' in Spanish.
If we just returned the string 'Score 3 Goals' here, there would
have to be a translation entry for each specific number. ew.
return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
305 def get_instance_description_short(self) -> str | Sequence: 306 """Return a short description for this game instance in English. 307 308 This description is used above the game scoreboard in the 309 corner of the screen, so it should be as concise as possible. 310 It should be lowercase and should not contain periods or other 311 punctuation. 312 313 Note that translation is applied by looking up the specific returned 314 value as a key, so the number of returned variations should be limited; 315 ideally just one or two. To include arbitrary values in the 316 description, you can return a sequence of values in the following form 317 instead of just a string: 318 319 # This will give us something like 'score 3 goals' in English 320 # and can properly translate to 'anota 3 goles' in Spanish. 321 # If we just returned the string 'score 3 goals' here, there would 322 # have to be a translation entry for each specific number. ew. 323 return ['score ${ARG1} goals', self.settings_raw['Score to Win']] 324 325 This way the first string can be consistently translated, with any arg 326 values then substituted into the result. ${ARG1} will be replaced 327 with the first value, ${ARG2} with the second, etc. 328 329 """ 330 return ''
Return a short description for this game instance in English.
This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.
Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:
This will give us something like 'score 3 goals' in English
and can properly translate to 'anota 3 goles' in Spanish.
If we just returned the string 'score 3 goals' here, there would
have to be a translation entry for each specific number. ew.
return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.
332 @override 333 def on_transition_in(self) -> None: 334 super().on_transition_in() 335 336 # Make our map. 337 self._map = self._map_type() 338 339 # Give our map a chance to override the music. 340 # (for happy-thoughts and other such themed maps) 341 map_music = self._map_type.get_music_type() 342 music = map_music if map_music is not None else self.default_music 343 344 if music is not None: 345 _music.setmusic(music)
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.
347 @override 348 def on_begin(self) -> None: 349 super().on_begin() 350 351 if babase.app.classic is not None: 352 babase.app.classic.game_begin_analytics() 353 354 # We don't do this in on_transition_in because it may depend on 355 # players/teams which aren't available until now. 356 _bascenev1.timer(0.001, self._show_scoreboard_info) 357 _bascenev1.timer(1.0, self._show_info) 358 _bascenev1.timer(2.5, self._show_tip) 359 360 # Store some basic info about players present at start time. 361 self.initialplayerinfos = [ 362 PlayerInfo(name=p.getname(full=True), character=p.character) 363 for p in self.players 364 ] 365 366 # Sort this by name so high score lists/etc will be consistent 367 # regardless of player join order. 368 self.initialplayerinfos.sort(key=lambda x: x.name) 369 370 # If this is a tournament, query info about it such as how much 371 # time is left. 372 tournament_id = self.session.tournament_id 373 if tournament_id is not None: 374 assert babase.app.plus is not None 375 babase.app.plus.tournament_query( 376 args={ 377 'tournamentIDs': [tournament_id], 378 'source': 'in-game time remaining query', 379 }, 380 callback=babase.WeakCall(self._on_tournament_query_response), 381 )
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
396 @override 397 def on_player_join(self, player: PlayerT) -> None: 398 super().on_player_join(player) 399 400 # By default, just spawn a dude. 401 self.spawn_player(player)
Called when a new bascenev1.Player has joined the Activity.
(including the initial set of Players)
403 @override 404 def handlemessage(self, msg: Any) -> Any: 405 if isinstance(msg, PlayerDiedMessage): 406 # pylint: disable=cyclic-import 407 from bascenev1lib.actor.spaz import Spaz 408 409 player = msg.getplayer(self.playertype) 410 killer = msg.getkillerplayer(self.playertype) 411 412 # Inform our stats of the demise. 413 self.stats.player_was_killed( 414 player, killed=msg.killed, killer=killer 415 ) 416 417 # Award the killer points if he's on a different team. 418 # FIXME: This should not be linked to Spaz actors. 419 # (should move get_death_points to Actor or make it a message) 420 if killer and killer.team is not player.team: 421 assert isinstance(killer.actor, Spaz) 422 pts, importance = killer.actor.get_death_points(msg.how) 423 if not self.has_ended(): 424 self.stats.player_scored( 425 killer, 426 pts, 427 kill=True, 428 victim_player=player, 429 importance=importance, 430 showpoints=self.show_kill_points, 431 ) 432 else: 433 return super().handlemessage(msg) 434 return None
General message handling; can be passed any message object.
697 @override 698 def end( 699 self, results: Any = None, delay: float = 0.0, force: bool = False 700 ) -> None: 701 from bascenev1._gameresults import GameResults 702 703 # If results is a standard team-game-results, associate it with us 704 # so it can grab our score prefs. 705 if isinstance(results, GameResults): 706 results.set_game(self) 707 708 # If we had a standard time-limit that had not expired, stop it so 709 # it doesnt tick annoyingly. 710 if ( 711 self._standard_time_limit_time is not None 712 and self._standard_time_limit_time > 0 713 ): 714 self._standard_time_limit_timer = None 715 self._standard_time_limit_text = None 716 717 # Ditto with tournament time limits. 718 if ( 719 self._tournament_time_limit is not None 720 and self._tournament_time_limit > 0 721 ): 722 self._tournament_time_limit_timer = None 723 self._tournament_time_limit_text = None 724 self._tournament_time_limit_title_text = None 725 726 super().end(results, delay, force)
Commences Activity shutdown and delivers results to the Session.
'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old.
728 def end_game(self) -> None: 729 """Tell the game to wrap up and call bascenev1.Activity.end(). 730 731 This method should be overridden by subclasses. A game should always 732 be prepared to end and deliver results, even if there is no 'winner' 733 yet; this way things like the standard time-limit 734 (bascenev1.GameActivity.setup_standard_time_limit()) will work with 735 the game. 736 """ 737 print( 738 'WARNING: default end_game() implementation called;' 739 ' your game should override this.' 740 )
Tell the game to wrap up and call bascenev1.Activity.end().
This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.
742 def respawn_player( 743 self, player: PlayerT, respawn_time: float | None = None 744 ) -> None: 745 """ 746 Given a bascenev1.Player, sets up a standard respawn timer, 747 along with the standard counter display, etc. 748 At the end of the respawn period spawn_player() will 749 be called if the Player still exists. 750 An explicit 'respawn_time' can optionally be provided 751 (in seconds). 752 """ 753 # pylint: disable=cyclic-import 754 755 assert player 756 if respawn_time is None: 757 teamsize = len(player.team.players) 758 if teamsize == 1: 759 respawn_time = 3.0 760 elif teamsize == 2: 761 respawn_time = 5.0 762 elif teamsize == 3: 763 respawn_time = 6.0 764 else: 765 respawn_time = 7.0 766 767 # If this standard setting is present, factor it in. 768 if 'Respawn Times' in self.settings_raw: 769 respawn_time *= self.settings_raw['Respawn Times'] 770 771 # We want whole seconds. 772 assert respawn_time is not None 773 respawn_time = round(max(1.0, respawn_time), 0) 774 775 if player.actor and not self.has_ended(): 776 from bascenev1lib.actor.respawnicon import RespawnIcon 777 778 player.customdata['respawn_timer'] = _bascenev1.Timer( 779 respawn_time, 780 babase.WeakCall(self.spawn_player_if_exists, player), 781 ) 782 player.customdata['respawn_icon'] = RespawnIcon( 783 player, respawn_time 784 )
Given a bascenev1.Player, sets up a standard respawn timer, along with the standard counter display, etc. At the end of the respawn period spawn_player() will be called if the Player still exists. An explicit 'respawn_time' can optionally be provided (in seconds).
786 def spawn_player_if_exists(self, player: PlayerT) -> None: 787 """ 788 A utility method which calls self.spawn_player() *only* if the 789 bascenev1.Player provided still exists; handy for use in timers 790 and whatnot. 791 792 There is no need to override this; just override spawn_player(). 793 """ 794 if player: 795 self.spawn_player(player)
A utility method which calls self.spawn_player() only if the bascenev1.Player provided still exists; handy for use in timers and whatnot.
There is no need to override this; just override spawn_player().
797 def spawn_player(self, player: PlayerT) -> bascenev1.Actor: 798 """Spawn *something* for the provided bascenev1.Player. 799 800 The default implementation simply calls spawn_player_spaz(). 801 """ 802 assert player # Dead references should never be passed as args. 803 804 return self.spawn_player_spaz(player)
Spawn something for the provided bascenev1.Player.
The default implementation simply calls spawn_player_spaz().
806 def spawn_player_spaz( 807 self, 808 player: PlayerT, 809 position: Sequence[float] = (0, 0, 0), 810 angle: float | None = None, 811 ) -> PlayerSpaz: 812 """Create and wire up a bascenev1.PlayerSpaz for the provided Player.""" 813 # pylint: disable=too-many-locals 814 # pylint: disable=cyclic-import 815 from bascenev1._gameutils import animate 816 from bascenev1._coopsession import CoopSession 817 from bascenev1lib.actor.playerspaz import PlayerSpaz 818 819 name = player.getname() 820 color = player.color 821 highlight = player.highlight 822 823 playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz) 824 if not issubclass(playerspaztype, PlayerSpaz): 825 playerspaztype = PlayerSpaz 826 827 light_color = babase.normalized_color(color) 828 display_color = babase.safecolor(color, target_intensity=0.75) 829 spaz = playerspaztype( 830 color=color, 831 highlight=highlight, 832 character=player.character, 833 player=player, 834 ) 835 836 player.actor = spaz 837 assert spaz.node 838 839 # If this is co-op and we're on Courtyard or Runaround, add the 840 # material that allows us to collide with the player-walls. 841 # FIXME: Need to generalize this. 842 if isinstance(self.session, CoopSession) and self.map.getname() in [ 843 'Courtyard', 844 'Tower D', 845 ]: 846 mat = self.map.preloaddata['collide_with_wall_material'] 847 assert isinstance(spaz.node.materials, tuple) 848 assert isinstance(spaz.node.roller_materials, tuple) 849 spaz.node.materials += (mat,) 850 spaz.node.roller_materials += (mat,) 851 852 spaz.node.name = name 853 spaz.node.name_color = display_color 854 spaz.connect_controls_to_player() 855 856 # Move to the stand position and add a flash of light. 857 spaz.handlemessage( 858 StandMessage( 859 position, angle if angle is not None else random.uniform(0, 360) 860 ) 861 ) 862 self._spawn_sound.play(1, position=spaz.node.position) 863 light = _bascenev1.newnode('light', attrs={'color': light_color}) 864 spaz.node.connectattr('position', light, 'position') 865 animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) 866 _bascenev1.timer(0.5, light.delete) 867 return spaz
Create and wire up a bascenev1.PlayerSpaz for the provided Player.
869 def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: 870 """Create standard powerup drops for the current map.""" 871 # pylint: disable=cyclic-import 872 from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL 873 874 self._powerup_drop_timer = _bascenev1.Timer( 875 DEFAULT_POWERUP_INTERVAL, 876 babase.WeakCall(self._standard_drop_powerups), 877 repeat=True, 878 ) 879 self._standard_drop_powerups() 880 if enable_tnt: 881 self._tnt_spawners = {} 882 self._setup_standard_tnt_drops()
Create standard powerup drops for the current map.
914 def setup_standard_time_limit(self, duration: float) -> None: 915 """ 916 Create a standard game time-limit given the provided 917 duration in seconds. 918 This will be displayed at the top of the screen. 919 If the time-limit expires, end_game() will be called. 920 """ 921 from bascenev1._nodeactor import NodeActor 922 923 if duration <= 0.0: 924 return 925 self._standard_time_limit_time = int(duration) 926 self._standard_time_limit_timer = _bascenev1.Timer( 927 1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True 928 ) 929 self._standard_time_limit_text = NodeActor( 930 _bascenev1.newnode( 931 'text', 932 attrs={ 933 'v_attach': 'top', 934 'h_attach': 'center', 935 'h_align': 'left', 936 'color': (1.0, 1.0, 1.0, 0.5), 937 'position': (-25, -30), 938 'flatness': 1.0, 939 'scale': 0.9, 940 }, 941 ) 942 ) 943 self._standard_time_limit_text_input = NodeActor( 944 _bascenev1.newnode( 945 'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0} 946 ) 947 ) 948 self.globalsnode.connectattr( 949 'time', self._standard_time_limit_text_input.node, 'time1' 950 ) 951 assert self._standard_time_limit_text_input.node 952 assert self._standard_time_limit_text.node 953 self._standard_time_limit_text_input.node.connectattr( 954 'output', self._standard_time_limit_text.node, 'text' 955 )
Create a standard game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called.
1132 def show_zoom_message( 1133 self, 1134 message: babase.Lstr, 1135 *, 1136 color: Sequence[float] = (0.9, 0.4, 0.0), 1137 scale: float = 0.8, 1138 duration: float = 2.0, 1139 trail: bool = False, 1140 ) -> None: 1141 """Zooming text used to announce game names and winners.""" 1142 # pylint: disable=cyclic-import 1143 from bascenev1lib.actor.zoomtext import ZoomText 1144 1145 # Reserve a spot on the screen (in case we get multiple of these so 1146 # they don't overlap). 1147 i = 0 1148 cur_time = babase.apptime() 1149 while True: 1150 if ( 1151 i not in self._zoom_message_times 1152 or self._zoom_message_times[i] < cur_time 1153 ): 1154 self._zoom_message_times[i] = cur_time + duration 1155 break 1156 i += 1 1157 ZoomText( 1158 message, 1159 lifespan=duration, 1160 jitter=2.0, 1161 position=(0, 200 - i * 100), 1162 scale=scale, 1163 maxwidth=800, 1164 trail=trail, 1165 color=color, 1166 ).autoretain()
Zooming text used to announce game names and winners.
31class GameResults: 32 """ 33 Results for a completed game. 34 35 Category: **Gameplay Classes** 36 37 Upon completion, a game should fill one of these out and pass it to its 38 bascenev1.Activity.end call. 39 """ 40 41 def __init__(self) -> None: 42 self._game_set = False 43 self._scores: dict[ 44 int, tuple[weakref.ref[bascenev1.SessionTeam], int | None] 45 ] = {} 46 self._sessionteams: list[weakref.ref[bascenev1.SessionTeam]] | None = ( 47 None 48 ) 49 self._playerinfos: list[bascenev1.PlayerInfo] | None = None 50 self._lower_is_better: bool | None = None 51 self._score_label: str | None = None 52 self._none_is_winner: bool | None = None 53 self._scoretype: bascenev1.ScoreType | None = None 54 55 def set_game(self, game: bascenev1.GameActivity) -> None: 56 """Set the game instance these results are applying to.""" 57 if self._game_set: 58 raise RuntimeError('Game set twice for GameResults.') 59 self._game_set = True 60 self._sessionteams = [ 61 weakref.ref(team.sessionteam) for team in game.teams 62 ] 63 scoreconfig = game.getscoreconfig() 64 self._playerinfos = copy.deepcopy(game.initialplayerinfos) 65 self._lower_is_better = scoreconfig.lower_is_better 66 self._score_label = scoreconfig.label 67 self._none_is_winner = scoreconfig.none_is_winner 68 self._scoretype = scoreconfig.scoretype 69 70 def set_team_score(self, team: bascenev1.Team, score: int | None) -> None: 71 """Set the score for a given team. 72 73 This can be a number or None. 74 (see the none_is_winner arg in the constructor) 75 """ 76 assert isinstance(team, Team) 77 sessionteam = team.sessionteam 78 self._scores[sessionteam.id] = (weakref.ref(sessionteam), score) 79 80 def get_sessionteam_score( 81 self, sessionteam: bascenev1.SessionTeam 82 ) -> int | None: 83 """Return the score for a given bascenev1.SessionTeam.""" 84 assert isinstance(sessionteam, SessionTeam) 85 for score in list(self._scores.values()): 86 if score[0]() is sessionteam: 87 return score[1] 88 89 # If we have no score value, assume None. 90 return None 91 92 @property 93 def sessionteams(self) -> list[bascenev1.SessionTeam]: 94 """Return all bascenev1.SessionTeams in the results.""" 95 if not self._game_set: 96 raise RuntimeError("Can't get teams until game is set.") 97 teams = [] 98 assert self._sessionteams is not None 99 for team_ref in self._sessionteams: 100 team = team_ref() 101 if team is not None: 102 teams.append(team) 103 return teams 104 105 def has_score_for_sessionteam( 106 self, sessionteam: bascenev1.SessionTeam 107 ) -> bool: 108 """Return whether there is a score for a given session-team.""" 109 return any(s[0]() is sessionteam for s in self._scores.values()) 110 111 def get_sessionteam_score_str( 112 self, sessionteam: bascenev1.SessionTeam 113 ) -> babase.Lstr: 114 """Return the score for the given session-team as an Lstr. 115 116 (properly formatted for the score type.) 117 """ 118 from bascenev1._score import ScoreType 119 120 if not self._game_set: 121 raise RuntimeError("Can't get team-score-str until game is set.") 122 for score in list(self._scores.values()): 123 if score[0]() is sessionteam: 124 if score[1] is None: 125 return babase.Lstr(value='-') 126 if self._scoretype is ScoreType.SECONDS: 127 return babase.timestring(score[1], centi=False) 128 if self._scoretype is ScoreType.MILLISECONDS: 129 return babase.timestring(score[1] / 1000.0, centi=True) 130 return babase.Lstr(value=str(score[1])) 131 return babase.Lstr(value='-') 132 133 @property 134 def playerinfos(self) -> list[bascenev1.PlayerInfo]: 135 """Get info about the players represented by the results.""" 136 if not self._game_set: 137 raise RuntimeError("Can't get player-info until game is set.") 138 assert self._playerinfos is not None 139 return self._playerinfos 140 141 @property 142 def scoretype(self) -> bascenev1.ScoreType: 143 """The type of score.""" 144 if not self._game_set: 145 raise RuntimeError("Can't get score-type until game is set.") 146 assert self._scoretype is not None 147 return self._scoretype 148 149 @property 150 def score_label(self) -> str: 151 """The label associated with scores ('points', etc).""" 152 if not self._game_set: 153 raise RuntimeError("Can't get score-label until game is set.") 154 assert self._score_label is not None 155 return self._score_label 156 157 @property 158 def lower_is_better(self) -> bool: 159 """Whether lower scores are better.""" 160 if not self._game_set: 161 raise RuntimeError("Can't get lower-is-better until game is set.") 162 assert self._lower_is_better is not None 163 return self._lower_is_better 164 165 @property 166 def winning_sessionteam(self) -> bascenev1.SessionTeam | None: 167 """The winning SessionTeam if there is exactly one, or else None.""" 168 if not self._game_set: 169 raise RuntimeError("Can't get winners until game is set.") 170 winners = self.winnergroups 171 if winners and len(winners[0].teams) == 1: 172 return winners[0].teams[0] 173 return None 174 175 @property 176 def winnergroups(self) -> list[WinnerGroup]: 177 """Get an ordered list of winner groups.""" 178 if not self._game_set: 179 raise RuntimeError("Can't get winners until game is set.") 180 181 # Group by best scoring teams. 182 winners: dict[int, list[bascenev1.SessionTeam]] = {} 183 scores = [ 184 score 185 for score in self._scores.values() 186 if score[0]() is not None and score[1] is not None 187 ] 188 for score in scores: 189 assert score[1] is not None 190 sval = winners.setdefault(score[1], []) 191 team = score[0]() 192 assert team is not None 193 sval.append(team) 194 results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list( 195 winners.items() 196 ) 197 results.sort( 198 reverse=not self._lower_is_better, 199 key=lambda x: asserttype(x[0], int), 200 ) 201 202 # Also group the 'None' scores. 203 none_sessionteams: list[bascenev1.SessionTeam] = [] 204 for score in self._scores.values(): 205 scoreteam = score[0]() 206 if scoreteam is not None and score[1] is None: 207 none_sessionteams.append(scoreteam) 208 209 # Add the Nones to the list (either as winners or losers 210 # depending on the rules). 211 if none_sessionteams: 212 nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [ 213 (None, none_sessionteams) 214 ] 215 if self._none_is_winner: 216 results = nones + results 217 else: 218 results = results + nones 219 220 return [WinnerGroup(score, team) for score, team in results]
Results for a completed game.
Category: Gameplay Classes
Upon completion, a game should fill one of these out and pass it to its bascenev1.Activity.end call.
55 def set_game(self, game: bascenev1.GameActivity) -> None: 56 """Set the game instance these results are applying to.""" 57 if self._game_set: 58 raise RuntimeError('Game set twice for GameResults.') 59 self._game_set = True 60 self._sessionteams = [ 61 weakref.ref(team.sessionteam) for team in game.teams 62 ] 63 scoreconfig = game.getscoreconfig() 64 self._playerinfos = copy.deepcopy(game.initialplayerinfos) 65 self._lower_is_better = scoreconfig.lower_is_better 66 self._score_label = scoreconfig.label 67 self._none_is_winner = scoreconfig.none_is_winner 68 self._scoretype = scoreconfig.scoretype
Set the game instance these results are applying to.
70 def set_team_score(self, team: bascenev1.Team, score: int | None) -> None: 71 """Set the score for a given team. 72 73 This can be a number or None. 74 (see the none_is_winner arg in the constructor) 75 """ 76 assert isinstance(team, Team) 77 sessionteam = team.sessionteam 78 self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
Set the score for a given team.
This can be a number or None. (see the none_is_winner arg in the constructor)
80 def get_sessionteam_score( 81 self, sessionteam: bascenev1.SessionTeam 82 ) -> int | None: 83 """Return the score for a given bascenev1.SessionTeam.""" 84 assert isinstance(sessionteam, SessionTeam) 85 for score in list(self._scores.values()): 86 if score[0]() is sessionteam: 87 return score[1] 88 89 # If we have no score value, assume None. 90 return None
Return the score for a given bascenev1.SessionTeam.
92 @property 93 def sessionteams(self) -> list[bascenev1.SessionTeam]: 94 """Return all bascenev1.SessionTeams in the results.""" 95 if not self._game_set: 96 raise RuntimeError("Can't get teams until game is set.") 97 teams = [] 98 assert self._sessionteams is not None 99 for team_ref in self._sessionteams: 100 team = team_ref() 101 if team is not None: 102 teams.append(team) 103 return teams
Return all bascenev1.SessionTeams in the results.
105 def has_score_for_sessionteam( 106 self, sessionteam: bascenev1.SessionTeam 107 ) -> bool: 108 """Return whether there is a score for a given session-team.""" 109 return any(s[0]() is sessionteam for s in self._scores.values())
Return whether there is a score for a given session-team.
111 def get_sessionteam_score_str( 112 self, sessionteam: bascenev1.SessionTeam 113 ) -> babase.Lstr: 114 """Return the score for the given session-team as an Lstr. 115 116 (properly formatted for the score type.) 117 """ 118 from bascenev1._score import ScoreType 119 120 if not self._game_set: 121 raise RuntimeError("Can't get team-score-str until game is set.") 122 for score in list(self._scores.values()): 123 if score[0]() is sessionteam: 124 if score[1] is None: 125 return babase.Lstr(value='-') 126 if self._scoretype is ScoreType.SECONDS: 127 return babase.timestring(score[1], centi=False) 128 if self._scoretype is ScoreType.MILLISECONDS: 129 return babase.timestring(score[1] / 1000.0, centi=True) 130 return babase.Lstr(value=str(score[1])) 131 return babase.Lstr(value='-')
Return the score for the given session-team as an Lstr.
(properly formatted for the score type.)
133 @property 134 def playerinfos(self) -> list[bascenev1.PlayerInfo]: 135 """Get info about the players represented by the results.""" 136 if not self._game_set: 137 raise RuntimeError("Can't get player-info until game is set.") 138 assert self._playerinfos is not None 139 return self._playerinfos
Get info about the players represented by the results.
141 @property 142 def scoretype(self) -> bascenev1.ScoreType: 143 """The type of score.""" 144 if not self._game_set: 145 raise RuntimeError("Can't get score-type until game is set.") 146 assert self._scoretype is not None 147 return self._scoretype
The type of score.
149 @property 150 def score_label(self) -> str: 151 """The label associated with scores ('points', etc).""" 152 if not self._game_set: 153 raise RuntimeError("Can't get score-label until game is set.") 154 assert self._score_label is not None 155 return self._score_label
The label associated with scores ('points', etc).
157 @property 158 def lower_is_better(self) -> bool: 159 """Whether lower scores are better.""" 160 if not self._game_set: 161 raise RuntimeError("Can't get lower-is-better until game is set.") 162 assert self._lower_is_better is not None 163 return self._lower_is_better
Whether lower scores are better.
165 @property 166 def winning_sessionteam(self) -> bascenev1.SessionTeam | None: 167 """The winning SessionTeam if there is exactly one, or else None.""" 168 if not self._game_set: 169 raise RuntimeError("Can't get winners until game is set.") 170 winners = self.winnergroups 171 if winners and len(winners[0].teams) == 1: 172 return winners[0].teams[0] 173 return None
The winning SessionTeam if there is exactly one, or else None.
175 @property 176 def winnergroups(self) -> list[WinnerGroup]: 177 """Get an ordered list of winner groups.""" 178 if not self._game_set: 179 raise RuntimeError("Can't get winners until game is set.") 180 181 # Group by best scoring teams. 182 winners: dict[int, list[bascenev1.SessionTeam]] = {} 183 scores = [ 184 score 185 for score in self._scores.values() 186 if score[0]() is not None and score[1] is not None 187 ] 188 for score in scores: 189 assert score[1] is not None 190 sval = winners.setdefault(score[1], []) 191 team = score[0]() 192 assert team is not None 193 sval.append(team) 194 results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list( 195 winners.items() 196 ) 197 results.sort( 198 reverse=not self._lower_is_better, 199 key=lambda x: asserttype(x[0], int), 200 ) 201 202 # Also group the 'None' scores. 203 none_sessionteams: list[bascenev1.SessionTeam] = [] 204 for score in self._scores.values(): 205 scoreteam = score[0]() 206 if scoreteam is not None and score[1] is None: 207 none_sessionteams.append(scoreteam) 208 209 # Add the Nones to the list (either as winners or losers 210 # depending on the rules). 211 if none_sessionteams: 212 nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [ 213 (None, none_sessionteams) 214 ] 215 if self._none_is_winner: 216 results = nones + results 217 else: 218 results = results + nones 219 220 return [WinnerGroup(score, team) for score, team in results]
Get an ordered list of winner groups.
33@dataclass 34class GameTip: 35 """Defines a tip presentable to the user at the start of a game. 36 37 Category: **Gameplay Classes** 38 """ 39 40 text: str 41 icon: bascenev1.Texture | None = None 42 sound: bascenev1.Sound | None = None
Defines a tip presentable to the user at the start of a game.
Category: Gameplay Classes
1167def get_connection_to_host_info_2() -> bascenev1.HostInfo | None: 1168 """Return info about the host we are currently connected to.""" 1169 import bascenev1 # pylint: disable=cyclic-import 1170 1171 return bascenev1.HostInfo('dummyname', -1, 'dummy_addr', -1)
Return info about the host we are currently connected to.
226def get_default_free_for_all_playlist() -> PlaylistType: 227 """Return a default playlist for free-for-all mode.""" 228 229 # NOTE: these are currently using old type/map names, 230 # but filtering translates them properly to the new ones. 231 # (is kinda a handy way to ensure filtering is working). 232 # Eventually should update these though. 233 return [ 234 { 235 'settings': { 236 'Epic Mode': False, 237 'Kills to Win Per Player': 10, 238 'Respawn Times': 1.0, 239 'Time Limit': 300, 240 'map': 'Doom Shroom', 241 }, 242 'type': 'bs_death_match.DeathMatchGame', 243 }, 244 { 245 'settings': { 246 'Chosen One Gets Gloves': True, 247 'Chosen One Gets Shield': False, 248 'Chosen One Time': 30, 249 'Epic Mode': 0, 250 'Respawn Times': 1.0, 251 'Time Limit': 300, 252 'map': 'Monkey Face', 253 }, 254 'type': 'bs_chosen_one.ChosenOneGame', 255 }, 256 { 257 'settings': { 258 'Hold Time': 30, 259 'Respawn Times': 1.0, 260 'Time Limit': 300, 261 'map': 'Zigzag', 262 }, 263 'type': 'bs_king_of_the_hill.KingOfTheHillGame', 264 }, 265 { 266 'settings': {'Epic Mode': False, 'map': 'Rampage'}, 267 'type': 'bs_meteor_shower.MeteorShowerGame', 268 }, 269 { 270 'settings': { 271 'Epic Mode': 1, 272 'Lives Per Player': 1, 273 'Respawn Times': 1.0, 274 'Time Limit': 120, 275 'map': 'Tip Top', 276 }, 277 'type': 'bs_elimination.EliminationGame', 278 }, 279 { 280 'settings': { 281 'Hold Time': 30, 282 'Respawn Times': 1.0, 283 'Time Limit': 300, 284 'map': 'The Pad', 285 }, 286 'type': 'bs_keep_away.KeepAwayGame', 287 }, 288 { 289 'settings': { 290 'Epic Mode': True, 291 'Kills to Win Per Player': 10, 292 'Respawn Times': 0.25, 293 'Time Limit': 120, 294 'map': 'Rampage', 295 }, 296 'type': 'bs_death_match.DeathMatchGame', 297 }, 298 { 299 'settings': { 300 'Bomb Spawning': 1000, 301 'Epic Mode': False, 302 'Laps': 3, 303 'Mine Spawn Interval': 4000, 304 'Mine Spawning': 4000, 305 'Time Limit': 300, 306 'map': 'Big G', 307 }, 308 'type': 'bs_race.RaceGame', 309 }, 310 { 311 'settings': { 312 'Hold Time': 30, 313 'Respawn Times': 1.0, 314 'Time Limit': 300, 315 'map': 'Happy Thoughts', 316 }, 317 'type': 'bs_king_of_the_hill.KingOfTheHillGame', 318 }, 319 { 320 'settings': { 321 'Enable Impact Bombs': 1, 322 'Enable Triple Bombs': False, 323 'Target Count': 2, 324 'map': 'Doom Shroom', 325 }, 326 'type': 'bs_target_practice.TargetPracticeGame', 327 }, 328 { 329 'settings': { 330 'Epic Mode': False, 331 'Lives Per Player': 5, 332 'Respawn Times': 1.0, 333 'Time Limit': 300, 334 'map': 'Step Right Up', 335 }, 336 'type': 'bs_elimination.EliminationGame', 337 }, 338 { 339 'settings': { 340 'Epic Mode': False, 341 'Kills to Win Per Player': 10, 342 'Respawn Times': 1.0, 343 'Time Limit': 300, 344 'map': 'Crag Castle', 345 }, 346 'type': 'bs_death_match.DeathMatchGame', 347 }, 348 { 349 'map': 'Lake Frigid', 350 'settings': { 351 'Bomb Spawning': 0, 352 'Epic Mode': False, 353 'Laps': 6, 354 'Mine Spawning': 2000, 355 'Time Limit': 300, 356 'map': 'Lake Frigid', 357 }, 358 'type': 'bs_race.RaceGame', 359 }, 360 ]
Return a default playlist for free-for-all mode.
363def get_default_teams_playlist() -> PlaylistType: 364 """Return a default playlist for teams mode.""" 365 366 # NOTE: these are currently using old type/map names, 367 # but filtering translates them properly to the new ones. 368 # (is kinda a handy way to ensure filtering is working). 369 # Eventually should update these though. 370 return [ 371 { 372 'settings': { 373 'Epic Mode': False, 374 'Flag Idle Return Time': 30, 375 'Flag Touch Return Time': 0, 376 'Respawn Times': 1.0, 377 'Score to Win': 3, 378 'Time Limit': 600, 379 'map': 'Bridgit', 380 }, 381 'type': 'bs_capture_the_flag.CTFGame', 382 }, 383 { 384 'settings': { 385 'Epic Mode': False, 386 'Respawn Times': 1.0, 387 'Score to Win': 3, 388 'Time Limit': 600, 389 'map': 'Step Right Up', 390 }, 391 'type': 'bs_assault.AssaultGame', 392 }, 393 { 394 'settings': { 395 'Balance Total Lives': False, 396 'Epic Mode': False, 397 'Lives Per Player': 3, 398 'Respawn Times': 1.0, 399 'Solo Mode': True, 400 'Time Limit': 600, 401 'map': 'Rampage', 402 }, 403 'type': 'bs_elimination.EliminationGame', 404 }, 405 { 406 'settings': { 407 'Epic Mode': False, 408 'Kills to Win Per Player': 5, 409 'Respawn Times': 1.0, 410 'Time Limit': 300, 411 'map': 'Roundabout', 412 }, 413 'type': 'bs_death_match.DeathMatchGame', 414 }, 415 { 416 'settings': { 417 'Respawn Times': 1.0, 418 'Score to Win': 1, 419 'Time Limit': 600, 420 'map': 'Hockey Stadium', 421 }, 422 'type': 'bs_hockey.HockeyGame', 423 }, 424 { 425 'settings': { 426 'Hold Time': 30, 427 'Respawn Times': 1.0, 428 'Time Limit': 300, 429 'map': 'Monkey Face', 430 }, 431 'type': 'bs_keep_away.KeepAwayGame', 432 }, 433 { 434 'settings': { 435 'Balance Total Lives': False, 436 'Epic Mode': True, 437 'Lives Per Player': 1, 438 'Respawn Times': 1.0, 439 'Solo Mode': False, 440 'Time Limit': 120, 441 'map': 'Tip Top', 442 }, 443 'type': 'bs_elimination.EliminationGame', 444 }, 445 { 446 'settings': { 447 'Epic Mode': False, 448 'Respawn Times': 1.0, 449 'Score to Win': 3, 450 'Time Limit': 300, 451 'map': 'Crag Castle', 452 }, 453 'type': 'bs_assault.AssaultGame', 454 }, 455 { 456 'settings': { 457 'Epic Mode': False, 458 'Kills to Win Per Player': 5, 459 'Respawn Times': 1.0, 460 'Time Limit': 300, 461 'map': 'Doom Shroom', 462 }, 463 'type': 'bs_death_match.DeathMatchGame', 464 }, 465 { 466 'settings': {'Epic Mode': False, 'map': 'Rampage'}, 467 'type': 'bs_meteor_shower.MeteorShowerGame', 468 }, 469 { 470 'settings': { 471 'Epic Mode': False, 472 'Flag Idle Return Time': 30, 473 'Flag Touch Return Time': 0, 474 'Respawn Times': 1.0, 475 'Score to Win': 2, 476 'Time Limit': 600, 477 'map': 'Roundabout', 478 }, 479 'type': 'bs_capture_the_flag.CTFGame', 480 }, 481 { 482 'settings': { 483 'Respawn Times': 1.0, 484 'Score to Win': 21, 485 'Time Limit': 600, 486 'map': 'Football Stadium', 487 }, 488 'type': 'bs_football.FootballTeamGame', 489 }, 490 { 491 'settings': { 492 'Epic Mode': True, 493 'Respawn Times': 0.25, 494 'Score to Win': 3, 495 'Time Limit': 120, 496 'map': 'Bridgit', 497 }, 498 'type': 'bs_assault.AssaultGame', 499 }, 500 { 501 'map': 'Doom Shroom', 502 'settings': { 503 'Enable Impact Bombs': 1, 504 'Enable Triple Bombs': False, 505 'Target Count': 2, 506 'map': 'Doom Shroom', 507 }, 508 'type': 'bs_target_practice.TargetPracticeGame', 509 }, 510 { 511 'settings': { 512 'Hold Time': 30, 513 'Respawn Times': 1.0, 514 'Time Limit': 300, 515 'map': 'Tip Top', 516 }, 517 'type': 'bs_king_of_the_hill.KingOfTheHillGame', 518 }, 519 { 520 'settings': { 521 'Epic Mode': False, 522 'Respawn Times': 1.0, 523 'Score to Win': 2, 524 'Time Limit': 300, 525 'map': 'Zigzag', 526 }, 527 'type': 'bs_assault.AssaultGame', 528 }, 529 { 530 'settings': { 531 'Epic Mode': False, 532 'Flag Idle Return Time': 30, 533 'Flag Touch Return Time': 0, 534 'Respawn Times': 1.0, 535 'Score to Win': 3, 536 'Time Limit': 300, 537 'map': 'Happy Thoughts', 538 }, 539 'type': 'bs_capture_the_flag.CTFGame', 540 }, 541 { 542 'settings': { 543 'Bomb Spawning': 1000, 544 'Epic Mode': True, 545 'Laps': 1, 546 'Mine Spawning': 2000, 547 'Time Limit': 300, 548 'map': 'Big G', 549 }, 550 'type': 'bs_race.RaceGame', 551 }, 552 { 553 'settings': { 554 'Epic Mode': False, 555 'Kills to Win Per Player': 5, 556 'Respawn Times': 1.0, 557 'Time Limit': 300, 558 'map': 'Monkey Face', 559 }, 560 'type': 'bs_death_match.DeathMatchGame', 561 }, 562 { 563 'settings': { 564 'Hold Time': 30, 565 'Respawn Times': 1.0, 566 'Time Limit': 300, 567 'map': 'Lake Frigid', 568 }, 569 'type': 'bs_keep_away.KeepAwayGame', 570 }, 571 { 572 'settings': { 573 'Epic Mode': False, 574 'Flag Idle Return Time': 30, 575 'Flag Touch Return Time': 3, 576 'Respawn Times': 1.0, 577 'Score to Win': 2, 578 'Time Limit': 300, 579 'map': 'Tip Top', 580 }, 581 'type': 'bs_capture_the_flag.CTFGame', 582 }, 583 { 584 'settings': { 585 'Balance Total Lives': False, 586 'Epic Mode': False, 587 'Lives Per Player': 3, 588 'Respawn Times': 1.0, 589 'Solo Mode': False, 590 'Time Limit': 300, 591 'map': 'Crag Castle', 592 }, 593 'type': 'bs_elimination.EliminationGame', 594 }, 595 { 596 'settings': { 597 'Epic Mode': True, 598 'Respawn Times': 0.25, 599 'Time Limit': 120, 600 'map': 'Zigzag', 601 }, 602 'type': 'bs_conquest.ConquestGame', 603 }, 604 ]
Return a default playlist for teams mode.
49def get_default_powerup_distribution() -> Sequence[tuple[str, int]]: 50 """Standard set of powerups.""" 51 return ( 52 ('triple_bombs', 3), 53 ('ice_bombs', 3), 54 ('punch', 3), 55 ('impact_bombs', 3), 56 ('land_mines', 2), 57 ('sticky_bombs', 3), 58 ('shield', 2), 59 ('health', 1), 60 ('curse', 1), 61 )
Standard set of powerups.
21def get_filtered_map_name(name: str) -> str: 22 """Filter a map name to account for name changes, etc. 23 24 Category: **Asset Functions** 25 26 This can be used to support old playlists, etc. 27 """ 28 # Some legacy name fallbacks... can remove these eventually. 29 if name in ('AlwaysLand', 'Happy Land'): 30 name = 'Happy Thoughts' 31 if name == 'Hockey Arena': 32 name = 'Hockey Stadium' 33 return name
Filter a map name to account for name changes, etc.
Category: Asset Functions
This can be used to support old playlists, etc.
44def get_map_class(name: str) -> type[Map]: 45 """Return a map type given a name. 46 47 Category: **Asset Functions** 48 """ 49 assert babase.app.classic is not None 50 name = get_filtered_map_name(name) 51 try: 52 mapclass: type[Map] = babase.app.classic.maps[name] 53 return mapclass 54 except KeyError: 55 raise babase.NotFoundError(f"Map not found: '{name}'") from None
Return a map type given a name.
Category: Asset Functions
36def get_map_display_string(name: str) -> babase.Lstr: 37 """Return a babase.Lstr for displaying a given map\'s name. 38 39 Category: **Asset Functions** 40 """ 41 return babase.Lstr(translate=('mapsNames', name))
Return a babase.Lstr for displaying a given map's name.
Category: Asset Functions
37def get_player_colors() -> list[tuple[float, float, float]]: 38 """Return user-selectable player colors.""" 39 return PLAYER_COLORS
Return user-selectable player colors.
63def get_player_profile_colors( 64 profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None 65) -> tuple[tuple[float, float, float], tuple[float, float, float]]: 66 """Given a profile, return colors for them.""" 67 appconfig = babase.app.config 68 if profiles is None: 69 profiles = appconfig['Player Profiles'] 70 71 # Special case: when being asked for a random color in kiosk mode, 72 # always return default purple. 73 if (babase.app.env.demo or babase.app.env.arcade) and profilename is None: 74 color = (0.5, 0.4, 1.0) 75 highlight = (0.4, 0.4, 0.5) 76 else: 77 try: 78 assert profilename is not None 79 color = profiles[profilename]['color'] 80 except (KeyError, AssertionError): 81 # Key off name if possible. 82 if profilename is None: 83 # First 6 are bright-ish. 84 color = PLAYER_COLORS[random.randrange(6)] 85 else: 86 # First 6 are bright-ish. 87 color = PLAYER_COLORS[sum(ord(c) for c in profilename) % 6] 88 89 try: 90 assert profilename is not None 91 highlight = profiles[profilename]['highlight'] 92 except (KeyError, AssertionError): 93 # Key off name if possible. 94 if profilename is None: 95 # Last 2 are grey and white; ignore those or we 96 # get lots of old-looking players. 97 highlight = PLAYER_COLORS[ 98 random.randrange(len(PLAYER_COLORS) - 2) 99 ] 100 else: 101 highlight = PLAYER_COLORS[ 102 sum(ord(c) + 1 for c in profilename) 103 % (len(PLAYER_COLORS) - 2) 104 ] 105 106 return color, highlight
Given a profile, return colors for them.
42def get_player_profile_icon(profilename: str) -> str: 43 """Given a profile name, returns an icon string for it. 44 45 (non-account profiles only) 46 """ 47 appconfig = babase.app.config 48 icon: str 49 try: 50 is_global = appconfig['Player Profiles'][profilename]['global'] 51 except KeyError: 52 is_global = False 53 if is_global: 54 try: 55 icon = appconfig['Player Profiles'][profilename]['icon'] 56 except KeyError: 57 icon = babase.charstr(babase.SpecialChar.LOGO) 58 else: 59 icon = '' 60 return icon
Given a profile name, returns an icon string for it.
(non-account profiles only)
45def get_trophy_string(trophy_id: str) -> str: 46 """Given a trophy id, returns a string to visualize it.""" 47 if trophy_id in TROPHY_CHARS: 48 return babase.charstr(TROPHY_CHARS[trophy_id]) 49 return '?'
Given a trophy id, returns a string to visualize it.
1303def getactivity(doraise: bool = True) -> bascenev1.Activity | None: 1304 """Return the current bascenev1.Activity instance. 1305 1306 Category: **Gameplay Functions** 1307 1308 Note that this is based on context_ref; thus code run in a timer 1309 generated in Activity 'foo' will properly return 'foo' here, even if 1310 another Activity has since been created or is transitioning in. 1311 If there is no current Activity, raises a babase.ActivityNotFoundError. 1312 If doraise is False, None will be returned instead in that case. 1313 """ 1314 return None
Return the current bascenev1.Activity instance.
Category: Gameplay Functions
Note that this is based on context_ref; thus code run in a timer generated in Activity 'foo' will properly return 'foo' here, even if another Activity has since been created or is transitioning in. If there is no current Activity, raises a babase.ActivityNotFoundError. If doraise is False, None will be returned instead in that case.
68def getcollision() -> Collision: 69 """Return the in-progress collision. 70 71 Category: **Gameplay Functions** 72 """ 73 return _collision
Return the in-progress collision.
Category: Gameplay Functions
1317def getcollisionmesh(name: str) -> bascenev1.CollisionMesh: 1318 """Return a collision-mesh, loading it if necessary. 1319 1320 Category: **Asset Functions** 1321 1322 Collision-meshes are used in physics calculations for such things as 1323 terrain. 1324 1325 Note that this function returns immediately even if the asset has yet 1326 to be loaded. To avoid hitches, instantiate your asset objects in 1327 advance of when you will be using them, allowing time for them to 1328 load in the background if necessary. 1329 """ 1330 import bascenev1 # pylint: disable=cyclic-import 1331 1332 return bascenev1.CollisionMesh()
Return a collision-mesh, loading it if necessary.
Category: Asset Functions
Collision-meshes are used in physics calculations for such things as terrain.
Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.
1335def getdata(name: str) -> bascenev1.Data: 1336 """Return a data, loading it if necessary. 1337 1338 Category: **Asset Functions** 1339 1340 Note that this function returns immediately even if the asset has yet 1341 to be loaded. To avoid hitches, instantiate your asset objects in 1342 advance of when you will be using them, allowing time for them to 1343 load in the background if necessary. 1344 """ 1345 import bascenev1 # pylint: disable=cyclic-import 1346 1347 return bascenev1.Data()
Return a data, loading it if necessary.
Category: Asset Functions
Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.
1373def getmesh(name: str) -> bascenev1.Mesh: 1374 """Return a mesh, loading it if necessary. 1375 1376 Category: **Asset Functions** 1377 1378 Note that this function returns immediately even if the asset has yet 1379 to be loaded. To avoid hitches, instantiate your asset objects in 1380 advance of when you will be using them, allowing time for them to 1381 load in the background if necessary. 1382 """ 1383 import bascenev1 # pylint: disable=cyclic-import 1384 1385 return bascenev1.Mesh()
Return a mesh, loading it if necessary.
Category: Asset Functions
Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.
1388def getnodes() -> list: 1389 """Return all nodes in the current bascenev1.Context. 1390 1391 Category: **Gameplay Functions** 1392 """ 1393 return list()
Return all nodes in the current bascenev1.Context.
Category: Gameplay Functions
1405def getsession(doraise: bool = True) -> bascenev1.Session | None: 1406 """Category: **Gameplay Functions** 1407 1408 Returns the current bascenev1.Session instance. 1409 Note that this is based on context_ref; thus code being run in the UI 1410 context will return the UI context_ref here even if a game Session also 1411 exists, etc. If there is no current Session, an Exception is raised, or 1412 if doraise is False then None is returned instead. 1413 """ 1414 return None
Category: Gameplay Functions
Returns the current bascenev1.Session instance. Note that this is based on context_ref; thus code being run in the UI context will return the UI context_ref here even if a game Session also exists, etc. If there is no current Session, an Exception is raised, or if doraise is False then None is returned instead.
1417def getsound(name: str) -> bascenev1.Sound: 1418 """Return a sound, loading it if necessary. 1419 1420 Category: **Asset Functions** 1421 1422 Note that this function returns immediately even if the asset has yet 1423 to be loaded. To avoid hitches, instantiate your asset objects in 1424 advance of when you will be using them, allowing time for them to 1425 load in the background if necessary. 1426 """ 1427 import bascenev1 # pylint: disable=cyclic-import 1428 1429 return bascenev1.Sound()
Return a sound, loading it if necessary.
Category: Asset Functions
Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.
1432def gettexture(name: str) -> bascenev1.Texture: 1433 """Return a texture, loading it if necessary. 1434 1435 Category: **Asset Functions** 1436 1437 Note that this function returns immediately even if the asset has yet 1438 to be loaded. To avoid hitches, instantiate your asset objects in 1439 advance of when you will be using them, allowing time for them to 1440 load in the background if necessary. 1441 """ 1442 import bascenev1 # pylint: disable=cyclic-import 1443 1444 return bascenev1.Texture()
Return a texture, loading it if necessary.
Category: Asset Functions
Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.
234class HitMessage: 235 """Tells an object it has been hit in some way. 236 237 Category: **Message Classes** 238 239 This is used by punches, explosions, etc to convey 240 their effect to a target. 241 """ 242 243 def __init__( 244 self, 245 *, 246 srcnode: bascenev1.Node | None = None, 247 pos: Sequence[float] | None = None, 248 velocity: Sequence[float] | None = None, 249 magnitude: float = 1.0, 250 velocity_magnitude: float = 0.0, 251 radius: float = 1.0, 252 source_player: bascenev1.Player | None = None, 253 kick_back: float = 1.0, 254 flat_damage: float | None = None, 255 hit_type: str = 'generic', 256 force_direction: Sequence[float] | None = None, 257 hit_subtype: str = 'default', 258 ): 259 """Instantiate a message with given values.""" 260 261 self.srcnode = srcnode 262 self.pos = pos if pos is not None else babase.Vec3() 263 self.velocity = velocity if velocity is not None else babase.Vec3() 264 self.magnitude = magnitude 265 self.velocity_magnitude = velocity_magnitude 266 self.radius = radius 267 268 # We should not be getting passed an invalid ref. 269 assert source_player is None or source_player.exists() 270 self._source_player = source_player 271 self.kick_back = kick_back 272 self.flat_damage = flat_damage 273 self.hit_type = hit_type 274 self.hit_subtype = hit_subtype 275 self.force_direction = ( 276 force_direction if force_direction is not None else velocity 277 ) 278 279 def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: 280 """Return the source-player if one exists and is the provided type.""" 281 player: Any = self._source_player 282 283 # We should not be delivering invalid refs. 284 # (we could translate to None here but technically we are changing 285 # the message delivered which seems wrong) 286 assert player is None or player.exists() 287 288 # Return the player *only* if they're the type given. 289 return player if isinstance(player, playertype) else None
Tells an object it has been hit in some way.
Category: Message Classes
This is used by punches, explosions, etc to convey their effect to a target.
243 def __init__( 244 self, 245 *, 246 srcnode: bascenev1.Node | None = None, 247 pos: Sequence[float] | None = None, 248 velocity: Sequence[float] | None = None, 249 magnitude: float = 1.0, 250 velocity_magnitude: float = 0.0, 251 radius: float = 1.0, 252 source_player: bascenev1.Player | None = None, 253 kick_back: float = 1.0, 254 flat_damage: float | None = None, 255 hit_type: str = 'generic', 256 force_direction: Sequence[float] | None = None, 257 hit_subtype: str = 'default', 258 ): 259 """Instantiate a message with given values.""" 260 261 self.srcnode = srcnode 262 self.pos = pos if pos is not None else babase.Vec3() 263 self.velocity = velocity if velocity is not None else babase.Vec3() 264 self.magnitude = magnitude 265 self.velocity_magnitude = velocity_magnitude 266 self.radius = radius 267 268 # We should not be getting passed an invalid ref. 269 assert source_player is None or source_player.exists() 270 self._source_player = source_player 271 self.kick_back = kick_back 272 self.flat_damage = flat_damage 273 self.hit_type = hit_type 274 self.hit_subtype = hit_subtype 275 self.force_direction = ( 276 force_direction if force_direction is not None else velocity 277 )
Instantiate a message with given values.
279 def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: 280 """Return the source-player if one exists and is the provided type.""" 281 player: Any = self._source_player 282 283 # We should not be delivering invalid refs. 284 # (we could translate to None here but technically we are changing 285 # the message delivered which seems wrong) 286 assert player is None or player.exists() 287 288 # Return the player *only* if they're the type given. 289 return player if isinstance(player, playertype) else None
Return the source-player if one exists and is the provided type.
14@dataclass 15class HostInfo: 16 """Info about a host.""" 17 18 name: str 19 build_number: int 20 21 # Note this can be None for non-ip hosts such as bluetooth. 22 address: str | None 23 24 # Note this can be None for non-ip hosts such as bluetooth. 25 port: int | None
Info about a host.
194@dataclass 195class ImpactDamageMessage: 196 """Tells an object that it has been jarred violently. 197 198 Category: **Message Classes** 199 """ 200 201 intensity: float 202 """The intensity of the impact."""
Tells an object that it has been jarred violently.
Category: Message Classes
105def init_campaigns() -> None: 106 """Fill out initial default Campaigns.""" 107 # pylint: disable=cyclic-import 108 from bascenev1._level import Level 109 from bascenev1lib.game.onslaught import OnslaughtGame 110 from bascenev1lib.game.football import FootballCoopGame 111 from bascenev1lib.game.runaround import RunaroundGame 112 from bascenev1lib.game.thelaststand import TheLastStandGame 113 from bascenev1lib.game.race import RaceGame 114 from bascenev1lib.game.targetpractice import TargetPracticeGame 115 from bascenev1lib.game.meteorshower import MeteorShowerGame 116 from bascenev1lib.game.easteregghunt import EasterEggHuntGame 117 from bascenev1lib.game.ninjafight import NinjaFightGame 118 119 # TODO: Campaigns should be load-on-demand; not all imported at launch 120 # like this. 121 122 # FIXME: Once translations catch up, we can convert these to use the 123 # generic display-name '${GAME} Training' type stuff. 124 register_campaign( 125 Campaign( 126 'Easy', 127 levels=[ 128 Level( 129 'Onslaught Training', 130 gametype=OnslaughtGame, 131 settings={'preset': 'training_easy'}, 132 preview_texture_name='doomShroomPreview', 133 ), 134 Level( 135 'Rookie Onslaught', 136 gametype=OnslaughtGame, 137 settings={'preset': 'rookie_easy'}, 138 preview_texture_name='courtyardPreview', 139 ), 140 Level( 141 'Rookie Football', 142 gametype=FootballCoopGame, 143 settings={'preset': 'rookie_easy'}, 144 preview_texture_name='footballStadiumPreview', 145 ), 146 Level( 147 'Pro Onslaught', 148 gametype=OnslaughtGame, 149 settings={'preset': 'pro_easy'}, 150 preview_texture_name='doomShroomPreview', 151 ), 152 Level( 153 'Pro Football', 154 gametype=FootballCoopGame, 155 settings={'preset': 'pro_easy'}, 156 preview_texture_name='footballStadiumPreview', 157 ), 158 Level( 159 'Pro Runaround', 160 gametype=RunaroundGame, 161 settings={'preset': 'pro_easy'}, 162 preview_texture_name='towerDPreview', 163 ), 164 Level( 165 'Uber Onslaught', 166 gametype=OnslaughtGame, 167 settings={'preset': 'uber_easy'}, 168 preview_texture_name='courtyardPreview', 169 ), 170 Level( 171 'Uber Football', 172 gametype=FootballCoopGame, 173 settings={'preset': 'uber_easy'}, 174 preview_texture_name='footballStadiumPreview', 175 ), 176 Level( 177 'Uber Runaround', 178 gametype=RunaroundGame, 179 settings={'preset': 'uber_easy'}, 180 preview_texture_name='towerDPreview', 181 ), 182 ], 183 ) 184 ) 185 186 # "hard" mode 187 register_campaign( 188 Campaign( 189 'Default', 190 levels=[ 191 Level( 192 'Onslaught Training', 193 gametype=OnslaughtGame, 194 settings={'preset': 'training'}, 195 preview_texture_name='doomShroomPreview', 196 ), 197 Level( 198 'Rookie Onslaught', 199 gametype=OnslaughtGame, 200 settings={'preset': 'rookie'}, 201 preview_texture_name='courtyardPreview', 202 ), 203 Level( 204 'Rookie Football', 205 gametype=FootballCoopGame, 206 settings={'preset': 'rookie'}, 207 preview_texture_name='footballStadiumPreview', 208 ), 209 Level( 210 'Pro Onslaught', 211 gametype=OnslaughtGame, 212 settings={'preset': 'pro'}, 213 preview_texture_name='doomShroomPreview', 214 ), 215 Level( 216 'Pro Football', 217 gametype=FootballCoopGame, 218 settings={'preset': 'pro'}, 219 preview_texture_name='footballStadiumPreview', 220 ), 221 Level( 222 'Pro Runaround', 223 gametype=RunaroundGame, 224 settings={'preset': 'pro'}, 225 preview_texture_name='towerDPreview', 226 ), 227 Level( 228 'Uber Onslaught', 229 gametype=OnslaughtGame, 230 settings={'preset': 'uber'}, 231 preview_texture_name='courtyardPreview', 232 ), 233 Level( 234 'Uber Football', 235 gametype=FootballCoopGame, 236 settings={'preset': 'uber'}, 237 preview_texture_name='footballStadiumPreview', 238 ), 239 Level( 240 'Uber Runaround', 241 gametype=RunaroundGame, 242 settings={'preset': 'uber'}, 243 preview_texture_name='towerDPreview', 244 ), 245 Level( 246 'The Last Stand', 247 gametype=TheLastStandGame, 248 settings={}, 249 preview_texture_name='rampagePreview', 250 ), 251 ], 252 ) 253 ) 254 255 # challenges: our 'official' random extra co-op levels 256 register_campaign( 257 Campaign( 258 'Challenges', 259 sequential=False, 260 levels=[ 261 Level( 262 'Infinite Onslaught', 263 gametype=OnslaughtGame, 264 settings={'preset': 'endless'}, 265 preview_texture_name='doomShroomPreview', 266 ), 267 Level( 268 'Infinite Runaround', 269 gametype=RunaroundGame, 270 settings={'preset': 'endless'}, 271 preview_texture_name='towerDPreview', 272 ), 273 Level( 274 'Race', 275 displayname='${GAME}', 276 gametype=RaceGame, 277 settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 0}, 278 preview_texture_name='bigGPreview', 279 ), 280 Level( 281 'Pro Race', 282 displayname='Pro ${GAME}', 283 gametype=RaceGame, 284 settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 1000}, 285 preview_texture_name='bigGPreview', 286 ), 287 Level( 288 'Lake Frigid Race', 289 displayname='${GAME}', 290 gametype=RaceGame, 291 settings={ 292 'map': 'Lake Frigid', 293 'Laps': 6, 294 'Mine Spawning': 2000, 295 'Bomb Spawning': 0, 296 }, 297 preview_texture_name='lakeFrigidPreview', 298 ), 299 Level( 300 'Football', 301 displayname='${GAME}', 302 gametype=FootballCoopGame, 303 settings={'preset': 'tournament'}, 304 preview_texture_name='footballStadiumPreview', 305 ), 306 Level( 307 'Pro Football', 308 displayname='Pro ${GAME}', 309 gametype=FootballCoopGame, 310 settings={'preset': 'tournament_pro'}, 311 preview_texture_name='footballStadiumPreview', 312 ), 313 Level( 314 'Runaround', 315 displayname='${GAME}', 316 gametype=RunaroundGame, 317 settings={'preset': 'tournament'}, 318 preview_texture_name='towerDPreview', 319 ), 320 Level( 321 'Uber Runaround', 322 displayname='Uber ${GAME}', 323 gametype=RunaroundGame, 324 settings={'preset': 'tournament_uber'}, 325 preview_texture_name='towerDPreview', 326 ), 327 Level( 328 'The Last Stand', 329 displayname='${GAME}', 330 gametype=TheLastStandGame, 331 settings={'preset': 'tournament'}, 332 preview_texture_name='rampagePreview', 333 ), 334 Level( 335 'Tournament Infinite Onslaught', 336 displayname='Infinite Onslaught', 337 gametype=OnslaughtGame, 338 settings={'preset': 'endless_tournament'}, 339 preview_texture_name='doomShroomPreview', 340 ), 341 Level( 342 'Tournament Infinite Runaround', 343 displayname='Infinite Runaround', 344 gametype=RunaroundGame, 345 settings={'preset': 'endless_tournament'}, 346 preview_texture_name='towerDPreview', 347 ), 348 Level( 349 'Target Practice', 350 displayname='Pro ${GAME}', 351 gametype=TargetPracticeGame, 352 settings={}, 353 preview_texture_name='doomShroomPreview', 354 ), 355 Level( 356 'Target Practice B', 357 displayname='${GAME}', 358 gametype=TargetPracticeGame, 359 settings={ 360 'Target Count': 2, 361 'Enable Impact Bombs': False, 362 'Enable Triple Bombs': False, 363 }, 364 preview_texture_name='doomShroomPreview', 365 ), 366 Level( 367 'Meteor Shower', 368 displayname='${GAME}', 369 gametype=MeteorShowerGame, 370 settings={}, 371 preview_texture_name='rampagePreview', 372 ), 373 Level( 374 'Epic Meteor Shower', 375 displayname='${GAME}', 376 gametype=MeteorShowerGame, 377 settings={'Epic Mode': True}, 378 preview_texture_name='rampagePreview', 379 ), 380 Level( 381 'Easter Egg Hunt', 382 displayname='${GAME}', 383 gametype=EasterEggHuntGame, 384 settings={}, 385 preview_texture_name='towerDPreview', 386 ), 387 Level( 388 'Pro Easter Egg Hunt', 389 displayname='Pro ${GAME}', 390 gametype=EasterEggHuntGame, 391 settings={'Pro Mode': True}, 392 preview_texture_name='towerDPreview', 393 ), 394 Level( 395 name='Ninja Fight', # (unique id not seen by player) 396 displayname='${GAME}', # (readable name seen by player) 397 gametype=NinjaFightGame, 398 settings={'preset': 'regular'}, 399 preview_texture_name='courtyardPreview', 400 ), 401 Level( 402 name='Pro Ninja Fight', 403 displayname='Pro ${GAME}', 404 gametype=NinjaFightGame, 405 settings={'preset': 'pro'}, 406 preview_texture_name='courtyardPreview', 407 ), 408 ], 409 ) 410 )
Fill out initial default Campaigns.
159class InputDevice: 160 """An input-device such as a gamepad, touchscreen, or keyboard. 161 162 Category: **Gameplay Classes** 163 """ 164 165 allows_configuring: bool 166 """Whether the input-device can be configured in the app.""" 167 168 allows_configuring_in_system_settings: bool 169 """Whether the input-device can be configured in the system. 170 setings app. This can be used to redirect the user to go there 171 if they attempt to configure the device.""" 172 173 has_meaningful_button_names: bool 174 """Whether button names returned by this instance match labels 175 on the actual device. (Can be used to determine whether to show 176 them in controls-overlays, etc.).""" 177 178 player: bascenev1.SessionPlayer | None 179 """The player associated with this input device.""" 180 181 client_id: int 182 """The numeric client-id this device is associated with. 183 This is only meaningful for remote client inputs; for 184 all local devices this will be -1.""" 185 186 name: str 187 """The name of the device.""" 188 189 unique_identifier: str 190 """A string that can be used to persistently identify the device, 191 even among other devices of the same type. Used for saving 192 prefs, etc.""" 193 194 id: int 195 """The unique numeric id of this device.""" 196 197 instance_number: int 198 """The number of this device among devices of the same type.""" 199 200 is_controller_app: bool 201 """Whether this input-device represents a locally-connected 202 controller-app.""" 203 204 is_remote_client: bool 205 """Whether this input-device represents a remotely-connected 206 client.""" 207 208 is_test_input: bool 209 """Whether this input-device is a dummy device for testing.""" 210 211 def __bool__(self) -> bool: 212 """Support for bool evaluation.""" 213 return bool(True) # Slight obfuscation. 214 215 def detach_from_player(self) -> None: 216 """Detach the device from any player it is controlling. 217 218 This applies both to local players and remote players. 219 """ 220 return None 221 222 def exists(self) -> bool: 223 """Return whether the underlying device for this object is 224 still present. 225 """ 226 return bool() 227 228 def get_axis_name(self, axis_id: int) -> str: 229 """Given an axis ID, return the name of the axis on this device. 230 231 Can return an empty string if the value is not meaningful to humans. 232 """ 233 return str() 234 235 def get_button_name(self, button_id: int) -> babase.Lstr: 236 """Given a button ID, return a human-readable name for that key/button. 237 238 Can return an empty string if the value is not meaningful to humans. 239 """ 240 import babase # pylint: disable=cyclic-import 241 242 return babase.Lstr(value='') 243 244 def get_default_player_name(self) -> str: 245 """(internal) 246 247 Returns the default player name for this device. (used for the 'random' 248 profile) 249 """ 250 return str() 251 252 def get_player_profiles(self) -> dict: 253 """(internal)""" 254 return dict() 255 256 def get_v1_account_name(self, full: bool) -> str: 257 """Returns the account name associated with this device. 258 259 (can be used to get account names for remote players) 260 """ 261 return str() 262 263 def is_attached_to_player(self) -> bool: 264 """Return whether this device is controlling a player of some sort. 265 266 This can mean either a local player or a remote player. 267 """ 268 return bool()
An input-device such as a gamepad, touchscreen, or keyboard.
Category: Gameplay Classes
Whether the input-device can be configured in the system. setings app. This can be used to redirect the user to go there if they attempt to configure the device.
The numeric client-id this device is associated with. This is only meaningful for remote client inputs; for all local devices this will be -1.
A string that can be used to persistently identify the device, even among other devices of the same type. Used for saving prefs, etc.
215 def detach_from_player(self) -> None: 216 """Detach the device from any player it is controlling. 217 218 This applies both to local players and remote players. 219 """ 220 return None
Detach the device from any player it is controlling.
This applies both to local players and remote players.
222 def exists(self) -> bool: 223 """Return whether the underlying device for this object is 224 still present. 225 """ 226 return bool()
Return whether the underlying device for this object is still present.
228 def get_axis_name(self, axis_id: int) -> str: 229 """Given an axis ID, return the name of the axis on this device. 230 231 Can return an empty string if the value is not meaningful to humans. 232 """ 233 return str()
Given an axis ID, return the name of the axis on this device.
Can return an empty string if the value is not meaningful to humans.
256 def get_v1_account_name(self, full: bool) -> str: 257 """Returns the account name associated with this device. 258 259 (can be used to get account names for remote players) 260 """ 261 return str()
Returns the account name associated with this device.
(can be used to get account names for remote players)
263 def is_attached_to_player(self) -> bool: 264 """Return whether this device is controlling a player of some sort. 265 266 This can mean either a local player or a remote player. 267 """ 268 return bool()
Return whether this device is controlling a player of some sort.
This can mean either a local player or a remote player.
8class InputType(Enum): 9 """Types of input a controller can send to the game. 10 11 Category: Enums 12 13 """ 14 15 UP_DOWN = 2 16 LEFT_RIGHT = 3 17 JUMP_PRESS = 4 18 JUMP_RELEASE = 5 19 PUNCH_PRESS = 6 20 PUNCH_RELEASE = 7 21 BOMB_PRESS = 8 22 BOMB_RELEASE = 9 23 PICK_UP_PRESS = 10 24 PICK_UP_RELEASE = 11 25 RUN = 12 26 FLY_PRESS = 13 27 FLY_RELEASE = 14 28 START_PRESS = 15 29 START_RELEASE = 16 30 HOLD_POSITION_PRESS = 17 31 HOLD_POSITION_RELEASE = 18 32 LEFT_PRESS = 19 33 LEFT_RELEASE = 20 34 RIGHT_PRESS = 21 35 RIGHT_RELEASE = 22 36 UP_PRESS = 23 37 UP_RELEASE = 24 38 DOWN_PRESS = 25 39 DOWN_RELEASE = 26
Types of input a controller can send to the game.
Category: Enums
72@dataclass 73class IntChoiceSetting(ChoiceSetting): 74 """An int setting with multiple choices. 75 76 Category: Settings Classes 77 """ 78 79 default: int 80 choices: list[tuple[str, int]]
An int setting with multiple choices.
Category: Settings Classes
36@dataclass 37class IntSetting(Setting): 38 """An integer game setting. 39 40 Category: Settings Classes 41 """ 42 43 default: int 44 min_value: int = 0 45 max_value: int = 9999 46 increment: int = 1
An integer game setting.
Category: Settings Classes
38def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool: 39 """Return whether a given point is within a given box. 40 41 category: General Utility Functions 42 43 For use with standard def boxes (position|rotate|scale). 44 """ 45 return ( 46 (abs(pnt[0] - box[0]) <= box[6] * 0.5) 47 and (abs(pnt[1] - box[1]) <= box[7] * 0.5) 48 and (abs(pnt[2] - box[2]) <= box[8] * 0.5) 49 )
Return whether a given point is within a given box.
category: General Utility Functions
For use with standard def boxes (position|rotate|scale).
59class JoinActivity(Activity[EmptyPlayer, EmptyTeam]): 60 """Standard activity for waiting for players to join. 61 62 It shows tips and other info and waits for all players to check ready. 63 """ 64 65 def __init__(self, settings: dict): 66 super().__init__(settings) 67 68 # This activity is a special 'joiner' activity. 69 # It will get shut down as soon as all players have checked ready. 70 self.is_joining_activity = True 71 72 # Players may be idle waiting for joiners; lets not kick them for it. 73 self.allow_kick_idle_players = False 74 75 # In vr mode we don't want stuff moving around. 76 self.use_fixed_vr_overlay = True 77 78 self._background: bascenev1.Actor | None = None 79 self._tips_text: bascenev1.Actor | None = None 80 self._join_info: JoinInfo | None = None 81 82 @override 83 def on_transition_in(self) -> None: 84 # pylint: disable=cyclic-import 85 from bascenev1lib.actor.tipstext import TipsText 86 from bascenev1lib.actor.background import Background 87 88 super().on_transition_in() 89 self._background = Background( 90 fade_time=0.5, start_faded=True, show_logo=True 91 ) 92 self._tips_text = TipsText() 93 setmusic(MusicType.CHAR_SELECT) 94 self._join_info = self.session.lobby.create_join_info() 95 babase.set_analytics_screen('Joining Screen')
Standard activity for waiting for players to join.
It shows tips and other info and waits for all players to check ready.
65 def __init__(self, settings: dict): 66 super().__init__(settings) 67 68 # This activity is a special 'joiner' activity. 69 # It will get shut down as soon as all players have checked ready. 70 self.is_joining_activity = True 71 72 # Players may be idle waiting for joiners; lets not kick them for it. 73 self.allow_kick_idle_players = False 74 75 # In vr mode we don't want stuff moving around. 76 self.use_fixed_vr_overlay = True 77 78 self._background: bascenev1.Actor | None = None 79 self._tips_text: bascenev1.Actor | None = None 80 self._join_info: JoinInfo | None = None
Creates an Activity in the current bascenev1.Session.
The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.
Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.
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.
82 @override 83 def on_transition_in(self) -> None: 84 # pylint: disable=cyclic-import 85 from bascenev1lib.actor.tipstext import TipsText 86 from bascenev1lib.actor.background import Background 87 88 super().on_transition_in() 89 self._background = Background( 90 fade_time=0.5, start_faded=True, show_logo=True 91 ) 92 self._tips_text = TipsText() 93 setmusic(MusicType.CHAR_SELECT) 94 self._join_info = self.session.lobby.create_join_info() 95 babase.set_analytics_screen('Joining Screen')
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.
19class Level: 20 """An entry in a bascenev1.Campaign. 21 22 Category: **Gameplay Classes** 23 """ 24 25 def __init__( 26 self, 27 name: str, 28 gametype: type[bascenev1.GameActivity], 29 settings: dict, 30 preview_texture_name: str, 31 *, 32 displayname: str | None = None, 33 ): 34 self._name = name 35 self._gametype = gametype 36 self._settings = settings 37 self._preview_texture_name = preview_texture_name 38 self._displayname = displayname 39 self._campaign: weakref.ref[bascenev1.Campaign] | None = None 40 self._index: int | None = None 41 self._score_version_string: str | None = None 42 43 @override 44 def __repr__(self) -> str: 45 cls = type(self) 46 return f"<{cls.__module__}.{cls.__name__} '{self._name}'>" 47 48 @property 49 def name(self) -> str: 50 """The unique name for this Level.""" 51 return self._name 52 53 def get_settings(self) -> dict[str, Any]: 54 """Returns the settings for this Level.""" 55 settings = copy.deepcopy(self._settings) 56 57 # So the game knows what the level is called. 58 # Hmm; seems hacky; I think we should take this out. 59 settings['name'] = self._name 60 return settings 61 62 @property 63 def preview_texture_name(self) -> str: 64 """The preview texture name for this Level.""" 65 return self._preview_texture_name 66 67 # def get_preview_texture(self) -> bauiv1.Texture: 68 # """Load/return the preview Texture for this Level.""" 69 # return _bauiv1.gettexture(self._preview_texture_name) 70 71 @property 72 def displayname(self) -> bascenev1.Lstr: 73 """The localized name for this Level.""" 74 return babase.Lstr( 75 translate=( 76 'coopLevelNames', 77 ( 78 self._displayname 79 if self._displayname is not None 80 else self._name 81 ), 82 ), 83 subs=[ 84 ('${GAME}', self._gametype.get_display_string(self._settings)) 85 ], 86 ) 87 88 @property 89 def gametype(self) -> type[bascenev1.GameActivity]: 90 """The type of game used for this Level.""" 91 return self._gametype 92 93 @property 94 def campaign(self) -> bascenev1.Campaign | None: 95 """The baclassic.Campaign this Level is associated with, or None.""" 96 return None if self._campaign is None else self._campaign() 97 98 @property 99 def index(self) -> int: 100 """The zero-based index of this Level in its baclassic.Campaign. 101 102 Access results in a RuntimeError if the Level is not assigned to a 103 Campaign. 104 """ 105 if self._index is None: 106 raise RuntimeError('Level is not part of a Campaign') 107 return self._index 108 109 @property 110 def complete(self) -> bool: 111 """Whether this Level has been completed.""" 112 config = self._get_config_dict() 113 val = config.get('Complete', False) 114 assert isinstance(val, bool) 115 return val 116 117 def set_complete(self, val: bool) -> None: 118 """Set whether or not this level is complete.""" 119 old_val = self.complete 120 assert isinstance(old_val, bool) 121 assert isinstance(val, bool) 122 if val != old_val: 123 config = self._get_config_dict() 124 config['Complete'] = val 125 126 def get_high_scores(self) -> dict: 127 """Return the current high scores for this Level.""" 128 config = self._get_config_dict() 129 high_scores_key = 'High Scores' + self.get_score_version_string() 130 if high_scores_key not in config: 131 return {} 132 return copy.deepcopy(config[high_scores_key]) 133 134 def set_high_scores(self, high_scores: dict) -> None: 135 """Set high scores for this level.""" 136 config = self._get_config_dict() 137 high_scores_key = 'High Scores' + self.get_score_version_string() 138 config[high_scores_key] = high_scores 139 140 def get_score_version_string(self) -> str: 141 """Return the score version string for this Level. 142 143 If a Level's gameplay changes significantly, its version string 144 can be changed to separate its new high score lists/etc. from the old. 145 """ 146 if self._score_version_string is None: 147 scorever = self._gametype.getscoreconfig().version 148 if scorever != '': 149 scorever = ' ' + scorever 150 self._score_version_string = scorever 151 assert self._score_version_string is not None 152 return self._score_version_string 153 154 @property 155 def rating(self) -> float: 156 """The current rating for this Level.""" 157 val = self._get_config_dict().get('Rating', 0.0) 158 assert isinstance(val, float) 159 return val 160 161 def set_rating(self, rating: float) -> None: 162 """Set a rating for this Level, replacing the old ONLY IF higher.""" 163 old_rating = self.rating 164 config = self._get_config_dict() 165 config['Rating'] = max(old_rating, rating) 166 167 def _get_config_dict(self) -> dict[str, Any]: 168 """Return/create the persistent state dict for this level. 169 170 The referenced dict exists under the game's config dict and 171 can be modified in place.""" 172 campaign = self.campaign 173 if campaign is None: 174 raise RuntimeError('Level is not in a campaign.') 175 configdict = campaign.configdict 176 val: dict[str, Any] = configdict.setdefault( 177 self._name, {'Rating': 0.0, 'Complete': False} 178 ) 179 assert isinstance(val, dict) 180 return val 181 182 def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None: 183 """For use by baclassic.Campaign when adding levels to itself. 184 185 (internal)""" 186 self._campaign = weakref.ref(campaign) 187 self._index = index
An entry in a bascenev1.Campaign.
Category: Gameplay Classes
25 def __init__( 26 self, 27 name: str, 28 gametype: type[bascenev1.GameActivity], 29 settings: dict, 30 preview_texture_name: str, 31 *, 32 displayname: str | None = None, 33 ): 34 self._name = name 35 self._gametype = gametype 36 self._settings = settings 37 self._preview_texture_name = preview_texture_name 38 self._displayname = displayname 39 self._campaign: weakref.ref[bascenev1.Campaign] | None = None 40 self._index: int | None = None 41 self._score_version_string: str | None = None
48 @property 49 def name(self) -> str: 50 """The unique name for this Level.""" 51 return self._name
The unique name for this Level.
53 def get_settings(self) -> dict[str, Any]: 54 """Returns the settings for this Level.""" 55 settings = copy.deepcopy(self._settings) 56 57 # So the game knows what the level is called. 58 # Hmm; seems hacky; I think we should take this out. 59 settings['name'] = self._name 60 return settings
Returns the settings for this Level.
62 @property 63 def preview_texture_name(self) -> str: 64 """The preview texture name for this Level.""" 65 return self._preview_texture_name
The preview texture name for this Level.
71 @property 72 def displayname(self) -> bascenev1.Lstr: 73 """The localized name for this Level.""" 74 return babase.Lstr( 75 translate=( 76 'coopLevelNames', 77 ( 78 self._displayname 79 if self._displayname is not None 80 else self._name 81 ), 82 ), 83 subs=[ 84 ('${GAME}', self._gametype.get_display_string(self._settings)) 85 ], 86 )
The localized name for this Level.
88 @property 89 def gametype(self) -> type[bascenev1.GameActivity]: 90 """The type of game used for this Level.""" 91 return self._gametype
The type of game used for this Level.
93 @property 94 def campaign(self) -> bascenev1.Campaign | None: 95 """The baclassic.Campaign this Level is associated with, or None.""" 96 return None if self._campaign is None else self._campaign()
The baclassic.Campaign this Level is associated with, or None.
98 @property 99 def index(self) -> int: 100 """The zero-based index of this Level in its baclassic.Campaign. 101 102 Access results in a RuntimeError if the Level is not assigned to a 103 Campaign. 104 """ 105 if self._index is None: 106 raise RuntimeError('Level is not part of a Campaign') 107 return self._index
The zero-based index of this Level in its baclassic.Campaign.
Access results in a RuntimeError if the Level is not assigned to a Campaign.
109 @property 110 def complete(self) -> bool: 111 """Whether this Level has been completed.""" 112 config = self._get_config_dict() 113 val = config.get('Complete', False) 114 assert isinstance(val, bool) 115 return val
Whether this Level has been completed.
117 def set_complete(self, val: bool) -> None: 118 """Set whether or not this level is complete.""" 119 old_val = self.complete 120 assert isinstance(old_val, bool) 121 assert isinstance(val, bool) 122 if val != old_val: 123 config = self._get_config_dict() 124 config['Complete'] = val
Set whether or not this level is complete.
126 def get_high_scores(self) -> dict: 127 """Return the current high scores for this Level.""" 128 config = self._get_config_dict() 129 high_scores_key = 'High Scores' + self.get_score_version_string() 130 if high_scores_key not in config: 131 return {} 132 return copy.deepcopy(config[high_scores_key])
Return the current high scores for this Level.
134 def set_high_scores(self, high_scores: dict) -> None: 135 """Set high scores for this level.""" 136 config = self._get_config_dict() 137 high_scores_key = 'High Scores' + self.get_score_version_string() 138 config[high_scores_key] = high_scores
Set high scores for this level.
140 def get_score_version_string(self) -> str: 141 """Return the score version string for this Level. 142 143 If a Level's gameplay changes significantly, its version string 144 can be changed to separate its new high score lists/etc. from the old. 145 """ 146 if self._score_version_string is None: 147 scorever = self._gametype.getscoreconfig().version 148 if scorever != '': 149 scorever = ' ' + scorever 150 self._score_version_string = scorever 151 assert self._score_version_string is not None 152 return self._score_version_string
Return the score version string for this Level.
If a Level's gameplay changes significantly, its version string can be changed to separate its new high score lists/etc. from the old.
154 @property 155 def rating(self) -> float: 156 """The current rating for this Level.""" 157 val = self._get_config_dict().get('Rating', 0.0) 158 assert isinstance(val, float) 159 return val
The current rating for this Level.
161 def set_rating(self, rating: float) -> None: 162 """Set a rating for this Level, replacing the old ONLY IF higher.""" 163 old_rating = self.rating 164 config = self._get_config_dict() 165 config['Rating'] = max(old_rating, rating)
Set a rating for this Level, replacing the old ONLY IF higher.
943class Lobby: 944 """Container for baclassic.Choosers. 945 946 Category: Gameplay Classes 947 """ 948 949 def __del__(self) -> None: 950 # Reset any players that still have a chooser in us. 951 # (should allow the choosers to die). 952 sessionplayers = [ 953 c.sessionplayer for c in self.choosers if c.sessionplayer 954 ] 955 for sessionplayer in sessionplayers: 956 sessionplayer.resetinput() 957 958 def __init__(self) -> None: 959 from bascenev1._team import SessionTeam 960 from bascenev1._coopsession import CoopSession 961 962 session = _bascenev1.getsession() 963 self._use_team_colors = session.use_team_colors 964 if session.use_teams: 965 self._sessionteams = [ 966 weakref.ref(team) for team in session.sessionteams 967 ] 968 else: 969 self._dummy_teams = SessionTeam() 970 self._sessionteams = [weakref.ref(self._dummy_teams)] 971 v_offset = -150 if isinstance(session, CoopSession) else -50 972 self.choosers: list[Chooser] = [] 973 self.base_v_offset = v_offset 974 self.update_positions() 975 self._next_add_team = 0 976 self.character_names_local_unlocked: list[str] = [] 977 self._vpos = 0 978 979 # Grab available profiles. 980 self.reload_profiles() 981 982 self._join_info_text = None 983 984 @property 985 def next_add_team(self) -> int: 986 """(internal)""" 987 return self._next_add_team 988 989 @property 990 def use_team_colors(self) -> bool: 991 """A bool for whether this lobby is using team colors. 992 993 If False, inidividual player colors are used instead. 994 """ 995 return self._use_team_colors 996 997 @property 998 def sessionteams(self) -> list[bascenev1.SessionTeam]: 999 """bascenev1.SessionTeams available in this lobby.""" 1000 allteams = [] 1001 for tref in self._sessionteams: 1002 team = tref() 1003 assert team is not None 1004 allteams.append(team) 1005 return allteams 1006 1007 def get_choosers(self) -> list[Chooser]: 1008 """Return the lobby's current choosers.""" 1009 return self.choosers 1010 1011 def create_join_info(self) -> JoinInfo: 1012 """Create a display of on-screen information for joiners. 1013 1014 (how to switch teams, players, etc.) 1015 Intended for use in initial joining-screens. 1016 """ 1017 return JoinInfo(self) 1018 1019 def reload_profiles(self) -> None: 1020 """Reload available player profiles.""" 1021 # pylint: disable=cyclic-import 1022 from bascenev1lib.actor.spazappearance import get_appearances 1023 1024 assert babase.app.classic is not None 1025 1026 # We may have gained or lost character names if the user 1027 # bought something; reload these too. 1028 self.character_names_local_unlocked = get_appearances() 1029 self.character_names_local_unlocked.sort(key=lambda x: x.lower()) 1030 1031 # Do any overall prep we need to such as creating account profile. 1032 babase.app.classic.accounts.ensure_have_account_player_profile() 1033 for chooser in self.choosers: 1034 try: 1035 chooser.reload_profiles() 1036 chooser.update_from_profile() 1037 except Exception: 1038 logging.exception('Error reloading profiles.') 1039 1040 def update_positions(self) -> None: 1041 """Update positions for all choosers.""" 1042 self._vpos = -100 + self.base_v_offset 1043 for chooser in self.choosers: 1044 chooser.set_vpos(self._vpos) 1045 chooser.update_position() 1046 self._vpos -= 48 1047 1048 def check_all_ready(self) -> bool: 1049 """Return whether all choosers are marked ready.""" 1050 return all(chooser.ready for chooser in self.choosers) 1051 1052 def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None: 1053 """Add a chooser to the lobby for the provided player.""" 1054 self.choosers.append( 1055 Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self) 1056 ) 1057 self._next_add_team = (self._next_add_team + 1) % len( 1058 self._sessionteams 1059 ) 1060 self._vpos -= 48 1061 1062 def remove_chooser(self, player: bascenev1.SessionPlayer) -> None: 1063 """Remove a single player's chooser; does not kick them. 1064 1065 This is used when a player enters the game and no longer 1066 needs a chooser.""" 1067 found = False 1068 chooser = None 1069 for chooser in self.choosers: 1070 if chooser.getplayer() is player: 1071 found = True 1072 1073 # Mark it as dead since there could be more 1074 # change-commands/etc coming in still for it; 1075 # want to avoid duplicate player-adds/etc. 1076 chooser.set_dead(True) 1077 self.choosers.remove(chooser) 1078 break 1079 if not found: 1080 logging.exception('remove_chooser did not find player %s.', player) 1081 elif chooser in self.choosers: 1082 logging.exception('chooser remains after removal for %s.', player) 1083 self.update_positions() 1084 1085 def remove_all_choosers(self) -> None: 1086 """Remove all choosers without kicking players. 1087 1088 This is called after all players check in and enter a game. 1089 """ 1090 self.choosers = [] 1091 self.update_positions() 1092 1093 def remove_all_choosers_and_kick_players(self) -> None: 1094 """Remove all player choosers and kick attached players.""" 1095 1096 # Copy the list; it can change under us otherwise. 1097 for chooser in list(self.choosers): 1098 if chooser.sessionplayer: 1099 chooser.sessionplayer.remove_from_game() 1100 self.remove_all_choosers()
Container for baclassic.Choosers.
Category: Gameplay Classes
989 @property 990 def use_team_colors(self) -> bool: 991 """A bool for whether this lobby is using team colors. 992 993 If False, inidividual player colors are used instead. 994 """ 995 return self._use_team_colors
A bool for whether this lobby is using team colors.
If False, inidividual player colors are used instead.
997 @property 998 def sessionteams(self) -> list[bascenev1.SessionTeam]: 999 """bascenev1.SessionTeams available in this lobby.""" 1000 allteams = [] 1001 for tref in self._sessionteams: 1002 team = tref() 1003 assert team is not None 1004 allteams.append(team) 1005 return allteams
bascenev1.SessionTeams available in this lobby.
1007 def get_choosers(self) -> list[Chooser]: 1008 """Return the lobby's current choosers.""" 1009 return self.choosers
Return the lobby's current choosers.
1011 def create_join_info(self) -> JoinInfo: 1012 """Create a display of on-screen information for joiners. 1013 1014 (how to switch teams, players, etc.) 1015 Intended for use in initial joining-screens. 1016 """ 1017 return JoinInfo(self)
Create a display of on-screen information for joiners.
(how to switch teams, players, etc.) Intended for use in initial joining-screens.
1019 def reload_profiles(self) -> None: 1020 """Reload available player profiles.""" 1021 # pylint: disable=cyclic-import 1022 from bascenev1lib.actor.spazappearance import get_appearances 1023 1024 assert babase.app.classic is not None 1025 1026 # We may have gained or lost character names if the user 1027 # bought something; reload these too. 1028 self.character_names_local_unlocked = get_appearances() 1029 self.character_names_local_unlocked.sort(key=lambda x: x.lower()) 1030 1031 # Do any overall prep we need to such as creating account profile. 1032 babase.app.classic.accounts.ensure_have_account_player_profile() 1033 for chooser in self.choosers: 1034 try: 1035 chooser.reload_profiles() 1036 chooser.update_from_profile() 1037 except Exception: 1038 logging.exception('Error reloading profiles.')
Reload available player profiles.
1040 def update_positions(self) -> None: 1041 """Update positions for all choosers.""" 1042 self._vpos = -100 + self.base_v_offset 1043 for chooser in self.choosers: 1044 chooser.set_vpos(self._vpos) 1045 chooser.update_position() 1046 self._vpos -= 48
Update positions for all choosers.
1048 def check_all_ready(self) -> bool: 1049 """Return whether all choosers are marked ready.""" 1050 return all(chooser.ready for chooser in self.choosers)
Return whether all choosers are marked ready.
1052 def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None: 1053 """Add a chooser to the lobby for the provided player.""" 1054 self.choosers.append( 1055 Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self) 1056 ) 1057 self._next_add_team = (self._next_add_team + 1) % len( 1058 self._sessionteams 1059 ) 1060 self._vpos -= 48
Add a chooser to the lobby for the provided player.
1062 def remove_chooser(self, player: bascenev1.SessionPlayer) -> None: 1063 """Remove a single player's chooser; does not kick them. 1064 1065 This is used when a player enters the game and no longer 1066 needs a chooser.""" 1067 found = False 1068 chooser = None 1069 for chooser in self.choosers: 1070 if chooser.getplayer() is player: 1071 found = True 1072 1073 # Mark it as dead since there could be more 1074 # change-commands/etc coming in still for it; 1075 # want to avoid duplicate player-adds/etc. 1076 chooser.set_dead(True) 1077 self.choosers.remove(chooser) 1078 break 1079 if not found: 1080 logging.exception('remove_chooser did not find player %s.', player) 1081 elif chooser in self.choosers: 1082 logging.exception('chooser remains after removal for %s.', player) 1083 self.update_positions()
Remove a single player's chooser; does not kick them.
This is used when a player enters the game and no longer needs a chooser.
1085 def remove_all_choosers(self) -> None: 1086 """Remove all choosers without kicking players. 1087 1088 This is called after all players check in and enter a game. 1089 """ 1090 self.choosers = [] 1091 self.update_positions()
Remove all choosers without kicking players.
This is called after all players check in and enter a game.
1093 def remove_all_choosers_and_kick_players(self) -> None: 1094 """Remove all player choosers and kick attached players.""" 1095 1096 # Copy the list; it can change under us otherwise. 1097 for chooser in list(self.choosers): 1098 if chooser.sessionplayer: 1099 chooser.sessionplayer.remove_from_game() 1100 self.remove_all_choosers()
Remove all player choosers and kick attached players.
1481def ls_input_devices() -> None: 1482 """Print debugging info about game objects. 1483 1484 Category: **General Utility Functions** 1485 1486 This call only functions in debug builds of the game. 1487 It prints various info about the current object count, etc. 1488 """ 1489 return None
Print debugging info about game objects.
Category: General Utility Functions
This call only functions in debug builds of the game. It prints various info about the current object count, etc.
1492def ls_objects() -> None: 1493 """Log debugging info about C++ level objects. 1494 1495 Category: **General Utility Functions** 1496 1497 This call only functions in debug builds of the game. 1498 It prints various info about the current object count, etc. 1499 """ 1500 return None
Log debugging info about C++ level objects.
Category: General Utility Functions
This call only functions in debug builds of the game. It prints various info about the current object count, etc.
491class Lstr: 492 """Used to define strings in a language-independent way. 493 494 Category: **General Utility Classes** 495 496 These should be used whenever possible in place of hard-coded 497 strings so that in-game or UI elements show up correctly on all 498 clients in their currently-active language. 499 500 To see available resource keys, look at any of the bs_language_*.py 501 files in the game or the translations pages at 502 legacy.ballistica.net/translate. 503 504 ##### Examples 505 EXAMPLE 1: specify a string from a resource path 506 >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 507 508 EXAMPLE 2: specify a translated string via a category and english 509 value; if a translated value is available, it will be used; otherwise 510 the english value will be. To see available translation categories, 511 look under the 'translations' resource section. 512 >>> mynode.text = babase.Lstr(translate=('gameDescriptions', 513 ... 'Defeat all enemies')) 514 515 EXAMPLE 3: specify a raw value and some substitutions. Substitutions 516 can be used with resource and translate modes as well. 517 >>> mynode.text = babase.Lstr(value='${A} / ${B}', 518 ... subs=[('${A}', str(score)), ('${B}', str(total))]) 519 520 EXAMPLE 4: babase.Lstr's can be nested. This example would display the 521 resource at res_a but replace ${NAME} with the value of the 522 resource at res_b 523 >>> mytextnode.text = babase.Lstr( 524 ... resource='res_a', 525 ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 526 """ 527 528 # pylint: disable=dangerous-default-value 529 # noinspection PyDefaultArgument 530 @overload 531 def __init__( 532 self, 533 *, 534 resource: str, 535 fallback_resource: str = '', 536 fallback_value: str = '', 537 subs: Sequence[tuple[str, str | Lstr]] = [], 538 ) -> None: 539 """Create an Lstr from a string resource.""" 540 541 # noinspection PyShadowingNames,PyDefaultArgument 542 @overload 543 def __init__( 544 self, 545 *, 546 translate: tuple[str, str], 547 subs: Sequence[tuple[str, str | Lstr]] = [], 548 ) -> None: 549 """Create an Lstr by translating a string in a category.""" 550 551 # noinspection PyDefaultArgument 552 @overload 553 def __init__( 554 self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = [] 555 ) -> None: 556 """Create an Lstr from a raw string value.""" 557 558 # pylint: enable=redefined-outer-name, dangerous-default-value 559 560 def __init__(self, *args: Any, **keywds: Any) -> None: 561 """Instantiate a Lstr. 562 563 Pass a value for either 'resource', 'translate', 564 or 'value'. (see Lstr help for examples). 565 'subs' can be a sequence of 2-member sequences consisting of values 566 and replacements. 567 'fallback_resource' can be a resource key that will be used if the 568 main one is not present for 569 the current language in place of falling back to the english value 570 ('resource' mode only). 571 'fallback_value' can be a literal string that will be used if neither 572 the resource nor the fallback resource is found ('resource' mode only). 573 """ 574 # pylint: disable=too-many-branches 575 if args: 576 raise TypeError('Lstr accepts only keyword arguments') 577 578 # Basically just store the exact args they passed. 579 # However if they passed any Lstr values for subs, 580 # replace them with that Lstr's dict. 581 self.args = keywds 582 our_type = type(self) 583 584 if isinstance(self.args.get('value'), our_type): 585 raise TypeError("'value' must be a regular string; not an Lstr") 586 587 if 'subs' in self.args: 588 subs_new = [] 589 for key, value in keywds['subs']: 590 if isinstance(value, our_type): 591 subs_new.append((key, value.args)) 592 else: 593 subs_new.append((key, value)) 594 self.args['subs'] = subs_new 595 596 # As of protocol 31 we support compact key names 597 # ('t' instead of 'translate', etc). Convert as needed. 598 if 'translate' in keywds: 599 keywds['t'] = keywds['translate'] 600 del keywds['translate'] 601 if 'resource' in keywds: 602 keywds['r'] = keywds['resource'] 603 del keywds['resource'] 604 if 'value' in keywds: 605 keywds['v'] = keywds['value'] 606 del keywds['value'] 607 if 'fallback' in keywds: 608 from babase import _error 609 610 _error.print_error( 611 'deprecated "fallback" arg passed to Lstr(); use ' 612 'either "fallback_resource" or "fallback_value"', 613 once=True, 614 ) 615 keywds['f'] = keywds['fallback'] 616 del keywds['fallback'] 617 if 'fallback_resource' in keywds: 618 keywds['f'] = keywds['fallback_resource'] 619 del keywds['fallback_resource'] 620 if 'subs' in keywds: 621 keywds['s'] = keywds['subs'] 622 del keywds['subs'] 623 if 'fallback_value' in keywds: 624 keywds['fv'] = keywds['fallback_value'] 625 del keywds['fallback_value'] 626 627 def evaluate(self) -> str: 628 """Evaluate the Lstr and returns a flat string in the current language. 629 630 You should avoid doing this as much as possible and instead pass 631 and store Lstr values. 632 """ 633 return _babase.evaluate_lstr(self._get_json()) 634 635 def is_flat_value(self) -> bool: 636 """Return whether the Lstr is a 'flat' value. 637 638 This is defined as a simple string value incorporating no 639 translations, resources, or substitutions. In this case it may 640 be reasonable to replace it with a raw string value, perform 641 string manipulation on it, etc. 642 """ 643 return bool('v' in self.args and not self.args.get('s', [])) 644 645 def _get_json(self) -> str: 646 try: 647 return json.dumps(self.args, separators=(',', ':')) 648 except Exception: 649 from babase import _error 650 651 _error.print_exception('_get_json failed for', self.args) 652 return 'JSON_ERR' 653 654 @override 655 def __str__(self) -> str: 656 return '<ba.Lstr: ' + self._get_json() + '>' 657 658 @override 659 def __repr__(self) -> str: 660 return '<ba.Lstr: ' + self._get_json() + '>' 661 662 @staticmethod 663 def from_json(json_string: str) -> babase.Lstr: 664 """Given a json string, returns a babase.Lstr. Does no validation.""" 665 lstr = Lstr(value='') 666 lstr.args = json.loads(json_string) 667 return lstr
Used to define strings in a language-independent way.
Category: General Utility Classes
These should be used whenever possible in place of hard-coded strings so that in-game or UI elements show up correctly on all clients in their currently-active language.
To see available resource keys, look at any of the bs_language_*.py files in the game or the translations pages at legacy.ballistica.net/translate.
Examples
EXAMPLE 1: specify a string from a resource path
>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
EXAMPLE 2: specify a translated string via a category and english value; if a translated value is available, it will be used; otherwise the english value will be. To see available translation categories, look under the 'translations' resource section.
>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
... 'Defeat all enemies'))
EXAMPLE 3: specify a raw value and some substitutions. Substitutions can be used with resource and translate modes as well.
>>> mynode.text = babase.Lstr(value='${A} / ${B}',
... subs=[('${A}', str(score)), ('${B}', str(total))])
EXAMPLE 4: babase.Lstr's can be nested. This example would display the resource at res_a but replace ${NAME} with the value of the resource at res_b
>>> mytextnode.text = babase.Lstr(
... resource='res_a',
... subs=[('${NAME}', babase.Lstr(resource='res_b'))])
560 def __init__(self, *args: Any, **keywds: Any) -> None: 561 """Instantiate a Lstr. 562 563 Pass a value for either 'resource', 'translate', 564 or 'value'. (see Lstr help for examples). 565 'subs' can be a sequence of 2-member sequences consisting of values 566 and replacements. 567 'fallback_resource' can be a resource key that will be used if the 568 main one is not present for 569 the current language in place of falling back to the english value 570 ('resource' mode only). 571 'fallback_value' can be a literal string that will be used if neither 572 the resource nor the fallback resource is found ('resource' mode only). 573 """ 574 # pylint: disable=too-many-branches 575 if args: 576 raise TypeError('Lstr accepts only keyword arguments') 577 578 # Basically just store the exact args they passed. 579 # However if they passed any Lstr values for subs, 580 # replace them with that Lstr's dict. 581 self.args = keywds 582 our_type = type(self) 583 584 if isinstance(self.args.get('value'), our_type): 585 raise TypeError("'value' must be a regular string; not an Lstr") 586 587 if 'subs' in self.args: 588 subs_new = [] 589 for key, value in keywds['subs']: 590 if isinstance(value, our_type): 591 subs_new.append((key, value.args)) 592 else: 593 subs_new.append((key, value)) 594 self.args['subs'] = subs_new 595 596 # As of protocol 31 we support compact key names 597 # ('t' instead of 'translate', etc). Convert as needed. 598 if 'translate' in keywds: 599 keywds['t'] = keywds['translate'] 600 del keywds['translate'] 601 if 'resource' in keywds: 602 keywds['r'] = keywds['resource'] 603 del keywds['resource'] 604 if 'value' in keywds: 605 keywds['v'] = keywds['value'] 606 del keywds['value'] 607 if 'fallback' in keywds: 608 from babase import _error 609 610 _error.print_error( 611 'deprecated "fallback" arg passed to Lstr(); use ' 612 'either "fallback_resource" or "fallback_value"', 613 once=True, 614 ) 615 keywds['f'] = keywds['fallback'] 616 del keywds['fallback'] 617 if 'fallback_resource' in keywds: 618 keywds['f'] = keywds['fallback_resource'] 619 del keywds['fallback_resource'] 620 if 'subs' in keywds: 621 keywds['s'] = keywds['subs'] 622 del keywds['subs'] 623 if 'fallback_value' in keywds: 624 keywds['fv'] = keywds['fallback_value'] 625 del keywds['fallback_value']
Instantiate a Lstr.
Pass a value for either 'resource', 'translate', or 'value'. (see Lstr help for examples). 'subs' can be a sequence of 2-member sequences consisting of values and replacements. 'fallback_resource' can be a resource key that will be used if the main one is not present for the current language in place of falling back to the english value ('resource' mode only). 'fallback_value' can be a literal string that will be used if neither the resource nor the fallback resource is found ('resource' mode only).
627 def evaluate(self) -> str: 628 """Evaluate the Lstr and returns a flat string in the current language. 629 630 You should avoid doing this as much as possible and instead pass 631 and store Lstr values. 632 """ 633 return _babase.evaluate_lstr(self._get_json())
Evaluate the Lstr and returns a flat string in the current language.
You should avoid doing this as much as possible and instead pass and store Lstr values.
635 def is_flat_value(self) -> bool: 636 """Return whether the Lstr is a 'flat' value. 637 638 This is defined as a simple string value incorporating no 639 translations, resources, or substitutions. In this case it may 640 be reasonable to replace it with a raw string value, perform 641 string manipulation on it, etc. 642 """ 643 return bool('v' in self.args and not self.args.get('s', []))
Return whether the Lstr is a 'flat' value.
This is defined as a simple string value incorporating no translations, resources, or substitutions. In this case it may be reasonable to replace it with a raw string value, perform string manipulation on it, etc.
58class Map(Actor): 59 """A game map. 60 61 Category: **Gameplay Classes** 62 63 Consists of a collection of terrain nodes, metadata, and other 64 functionality comprising a game map. 65 """ 66 67 defs: Any = None 68 name = 'Map' 69 _playtypes: list[str] = [] 70 71 @classmethod 72 def preload(cls) -> None: 73 """Preload map media. 74 75 This runs the class's on_preload() method as needed to prep it to run. 76 Preloading should generally be done in a bascenev1.Activity's 77 __init__ method. Note that this is a classmethod since it is not 78 operate on map instances but rather on the class itself before 79 instances are made 80 """ 81 activity = _bascenev1.getactivity() 82 if cls not in activity.preloads: 83 activity.preloads[cls] = cls.on_preload() 84 85 @classmethod 86 def get_play_types(cls) -> list[str]: 87 """Return valid play types for this map.""" 88 return [] 89 90 @classmethod 91 def get_preview_texture_name(cls) -> str | None: 92 """Return the name of the preview texture for this map.""" 93 return None 94 95 @classmethod 96 def on_preload(cls) -> Any: 97 """Called when the map is being preloaded. 98 99 It should return any media/data it requires to operate 100 """ 101 return None 102 103 @classmethod 104 def getname(cls) -> str: 105 """Return the unique name of this map, in English.""" 106 return cls.name 107 108 @classmethod 109 def get_music_type(cls) -> bascenev1.MusicType | None: 110 """Return a music-type string that should be played on this map. 111 112 If None is returned, default music will be used. 113 """ 114 return None 115 116 def __init__( 117 self, vr_overlay_offset: Sequence[float] | None = None 118 ) -> None: 119 """Instantiate a map.""" 120 super().__init__() 121 122 # This is expected to always be a bascenev1.Node object 123 # (whether valid or not) should be set to something meaningful 124 # by child classes. 125 self.node: _bascenev1.Node | None = None 126 127 # Make our class' preload-data available to us 128 # (and instruct the user if we weren't preloaded properly). 129 try: 130 self.preloaddata = _bascenev1.getactivity().preloads[type(self)] 131 except Exception as exc: 132 raise babase.NotFoundError( 133 'Preload data not found for ' 134 + str(type(self)) 135 + '; make sure to call the type\'s preload()' 136 ' staticmethod in the activity constructor' 137 ) from exc 138 139 # Set various globals. 140 gnode = _bascenev1.getactivity().globalsnode 141 142 # Set area-of-interest bounds. 143 aoi_bounds = self.get_def_bound_box('area_of_interest_bounds') 144 if aoi_bounds is None: 145 print('WARNING: no "aoi_bounds" found for map:', self.getname()) 146 aoi_bounds = (-1, -1, -1, 1, 1, 1) 147 gnode.area_of_interest_bounds = aoi_bounds 148 149 # Set map bounds. 150 map_bounds = self.get_def_bound_box('map_bounds') 151 if map_bounds is None: 152 print('WARNING: no "map_bounds" found for map:', self.getname()) 153 map_bounds = (-30, -10, -30, 30, 100, 30) 154 _bascenev1.set_map_bounds(map_bounds) 155 156 # Set shadow ranges. 157 try: 158 gnode.shadow_range = [ 159 self.defs.points[v][1] 160 for v in [ 161 'shadow_lower_bottom', 162 'shadow_lower_top', 163 'shadow_upper_bottom', 164 'shadow_upper_top', 165 ] 166 ] 167 except Exception: 168 pass 169 170 # In vr, set a fixed point in space for the overlay to show up at. 171 # By default we use the bounds center but allow the map to override it. 172 center = ( 173 (aoi_bounds[0] + aoi_bounds[3]) * 0.5, 174 (aoi_bounds[1] + aoi_bounds[4]) * 0.5, 175 (aoi_bounds[2] + aoi_bounds[5]) * 0.5, 176 ) 177 if vr_overlay_offset is not None: 178 center = ( 179 center[0] + vr_overlay_offset[0], 180 center[1] + vr_overlay_offset[1], 181 center[2] + vr_overlay_offset[2], 182 ) 183 gnode.vr_overlay_center = center 184 gnode.vr_overlay_center_enabled = True 185 186 self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)] 187 self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [ 188 (0, 0, 0, 0, 0, 0) 189 ] 190 self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [ 191 (0, 0, 0, 0, 0, 0) 192 ] 193 self.flag_points = self.get_def_points('flag') or [(0, 0, 0)] 194 195 # We just want points. 196 self.flag_points = [p[:3] for p in self.flag_points] 197 self.flag_points_default = self.get_def_point('flag_default') or ( 198 0, 199 1, 200 0, 201 ) 202 self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [ 203 (0, 0, 0) 204 ] 205 206 # We just want points. 207 self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points] 208 self.tnt_points = self.get_def_points('tnt') or [] 209 210 # We just want points. 211 self.tnt_points = [p[:3] for p in self.tnt_points] 212 213 self.is_hockey = False 214 self.is_flying = False 215 216 # FIXME: this should be part of game; not map. 217 # Let's select random index for first spawn point, 218 # so that no one is offended by the constant spawn on the edge. 219 self._next_ffa_start_index = random.randrange( 220 len(self.ffa_spawn_points) 221 ) 222 223 def is_point_near_edge( 224 self, point: babase.Vec3, running: bool = False 225 ) -> bool: 226 """Return whether the provided point is near an edge of the map. 227 228 Simple bot logic uses this call to determine if they 229 are approaching a cliff or wall. If this returns True they will 230 generally not walk/run any farther away from the origin. 231 If 'running' is True, the buffer should be a bit larger. 232 """ 233 del point, running # Unused. 234 return False 235 236 def get_def_bound_box( 237 self, name: str 238 ) -> tuple[float, float, float, float, float, float] | None: 239 """Return a 6 member bounds tuple or None if it is not defined.""" 240 try: 241 box = self.defs.boxes[name] 242 return ( 243 box[0] - box[6] / 2.0, 244 box[1] - box[7] / 2.0, 245 box[2] - box[8] / 2.0, 246 box[0] + box[6] / 2.0, 247 box[1] + box[7] / 2.0, 248 box[2] + box[8] / 2.0, 249 ) 250 except Exception: 251 return None 252 253 def get_def_point(self, name: str) -> Sequence[float] | None: 254 """Return a single defined point or a default value in its absence.""" 255 val = self.defs.points.get(name) 256 return ( 257 None 258 if val is None 259 else babase.vec3validate(val) if __debug__ else val 260 ) 261 262 def get_def_points(self, name: str) -> list[Sequence[float]]: 263 """Return a list of named points. 264 265 Return as many sequential ones are defined (flag1, flag2, flag3), etc. 266 If none are defined, returns an empty list. 267 """ 268 point_list = [] 269 if self.defs and name + '1' in self.defs.points: 270 i = 1 271 while name + str(i) in self.defs.points: 272 pts = self.defs.points[name + str(i)] 273 if len(pts) == 6: 274 point_list.append(pts) 275 else: 276 if len(pts) != 3: 277 raise ValueError('invalid point') 278 point_list.append(pts + (0, 0, 0)) 279 i += 1 280 return point_list 281 282 def get_start_position(self, team_index: int) -> Sequence[float]: 283 """Return a random starting position for the given team index.""" 284 pnt = self.spawn_points[team_index % len(self.spawn_points)] 285 x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3]) 286 z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5]) 287 pnt = ( 288 pnt[0] + random.uniform(*x_range), 289 pnt[1], 290 pnt[2] + random.uniform(*z_range), 291 ) 292 return pnt 293 294 def get_ffa_start_position( 295 self, players: Sequence[bascenev1.Player] 296 ) -> Sequence[float]: 297 """Return a random starting position in one of the FFA spawn areas. 298 299 If a list of bascenev1.Player-s is provided; the returned points 300 will be as far from these players as possible. 301 """ 302 303 # Get positions for existing players. 304 player_pts = [] 305 for player in players: 306 if player.is_alive(): 307 player_pts.append(player.position) 308 309 def _getpt() -> Sequence[float]: 310 point = self.ffa_spawn_points[self._next_ffa_start_index] 311 self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len( 312 self.ffa_spawn_points 313 ) 314 x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) 315 z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5]) 316 point = ( 317 point[0] + random.uniform(*x_range), 318 point[1], 319 point[2] + random.uniform(*z_range), 320 ) 321 return point 322 323 if not player_pts: 324 return _getpt() 325 326 # Let's calc several start points and then pick whichever is 327 # farthest from all existing players. 328 farthestpt_dist = -1.0 329 farthestpt = None 330 for _i in range(10): 331 testpt = babase.Vec3(_getpt()) 332 closest_player_dist = 9999.0 333 for ppt in player_pts: 334 dist = (ppt - testpt).length() 335 closest_player_dist = min(dist, closest_player_dist) 336 if closest_player_dist > farthestpt_dist: 337 farthestpt_dist = closest_player_dist 338 farthestpt = testpt 339 assert farthestpt is not None 340 return tuple(farthestpt) 341 342 def get_flag_position( 343 self, team_index: int | None = None 344 ) -> Sequence[float]: 345 """Return a flag position on the map for the given team index. 346 347 Pass None to get the default flag point. 348 (used for things such as king-of-the-hill) 349 """ 350 if team_index is None: 351 return self.flag_points_default[:3] 352 return self.flag_points[team_index % len(self.flag_points)][:3] 353 354 @override 355 def exists(self) -> bool: 356 return bool(self.node) 357 358 @override 359 def handlemessage(self, msg: Any) -> Any: 360 from bascenev1 import _messages 361 362 if isinstance(msg, _messages.DieMessage): 363 if self.node: 364 self.node.delete() 365 else: 366 return super().handlemessage(msg) 367 return None
A game map.
Category: Gameplay Classes
Consists of a collection of terrain nodes, metadata, and other functionality comprising a game map.
116 def __init__( 117 self, vr_overlay_offset: Sequence[float] | None = None 118 ) -> None: 119 """Instantiate a map.""" 120 super().__init__() 121 122 # This is expected to always be a bascenev1.Node object 123 # (whether valid or not) should be set to something meaningful 124 # by child classes. 125 self.node: _bascenev1.Node | None = None 126 127 # Make our class' preload-data available to us 128 # (and instruct the user if we weren't preloaded properly). 129 try: 130 self.preloaddata = _bascenev1.getactivity().preloads[type(self)] 131 except Exception as exc: 132 raise babase.NotFoundError( 133 'Preload data not found for ' 134 + str(type(self)) 135 + '; make sure to call the type\'s preload()' 136 ' staticmethod in the activity constructor' 137 ) from exc 138 139 # Set various globals. 140 gnode = _bascenev1.getactivity().globalsnode 141 142 # Set area-of-interest bounds. 143 aoi_bounds = self.get_def_bound_box('area_of_interest_bounds') 144 if aoi_bounds is None: 145 print('WARNING: no "aoi_bounds" found for map:', self.getname()) 146 aoi_bounds = (-1, -1, -1, 1, 1, 1) 147 gnode.area_of_interest_bounds = aoi_bounds 148 149 # Set map bounds. 150 map_bounds = self.get_def_bound_box('map_bounds') 151 if map_bounds is None: 152 print('WARNING: no "map_bounds" found for map:', self.getname()) 153 map_bounds = (-30, -10, -30, 30, 100, 30) 154 _bascenev1.set_map_bounds(map_bounds) 155 156 # Set shadow ranges. 157 try: 158 gnode.shadow_range = [ 159 self.defs.points[v][1] 160 for v in [ 161 'shadow_lower_bottom', 162 'shadow_lower_top', 163 'shadow_upper_bottom', 164 'shadow_upper_top', 165 ] 166 ] 167 except Exception: 168 pass 169 170 # In vr, set a fixed point in space for the overlay to show up at. 171 # By default we use the bounds center but allow the map to override it. 172 center = ( 173 (aoi_bounds[0] + aoi_bounds[3]) * 0.5, 174 (aoi_bounds[1] + aoi_bounds[4]) * 0.5, 175 (aoi_bounds[2] + aoi_bounds[5]) * 0.5, 176 ) 177 if vr_overlay_offset is not None: 178 center = ( 179 center[0] + vr_overlay_offset[0], 180 center[1] + vr_overlay_offset[1], 181 center[2] + vr_overlay_offset[2], 182 ) 183 gnode.vr_overlay_center = center 184 gnode.vr_overlay_center_enabled = True 185 186 self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)] 187 self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [ 188 (0, 0, 0, 0, 0, 0) 189 ] 190 self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [ 191 (0, 0, 0, 0, 0, 0) 192 ] 193 self.flag_points = self.get_def_points('flag') or [(0, 0, 0)] 194 195 # We just want points. 196 self.flag_points = [p[:3] for p in self.flag_points] 197 self.flag_points_default = self.get_def_point('flag_default') or ( 198 0, 199 1, 200 0, 201 ) 202 self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [ 203 (0, 0, 0) 204 ] 205 206 # We just want points. 207 self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points] 208 self.tnt_points = self.get_def_points('tnt') or [] 209 210 # We just want points. 211 self.tnt_points = [p[:3] for p in self.tnt_points] 212 213 self.is_hockey = False 214 self.is_flying = False 215 216 # FIXME: this should be part of game; not map. 217 # Let's select random index for first spawn point, 218 # so that no one is offended by the constant spawn on the edge. 219 self._next_ffa_start_index = random.randrange( 220 len(self.ffa_spawn_points) 221 )
Instantiate a map.
71 @classmethod 72 def preload(cls) -> None: 73 """Preload map media. 74 75 This runs the class's on_preload() method as needed to prep it to run. 76 Preloading should generally be done in a bascenev1.Activity's 77 __init__ method. Note that this is a classmethod since it is not 78 operate on map instances but rather on the class itself before 79 instances are made 80 """ 81 activity = _bascenev1.getactivity() 82 if cls not in activity.preloads: 83 activity.preloads[cls] = cls.on_preload()
Preload map media.
This runs the class's on_preload() method as needed to prep it to run. Preloading should generally be done in a bascenev1.Activity's __init__ method. Note that this is a classmethod since it is not operate on map instances but rather on the class itself before instances are made
85 @classmethod 86 def get_play_types(cls) -> list[str]: 87 """Return valid play types for this map.""" 88 return []
Return valid play types for this map.
90 @classmethod 91 def get_preview_texture_name(cls) -> str | None: 92 """Return the name of the preview texture for this map.""" 93 return None
Return the name of the preview texture for this map.
95 @classmethod 96 def on_preload(cls) -> Any: 97 """Called when the map is being preloaded. 98 99 It should return any media/data it requires to operate 100 """ 101 return None
Called when the map is being preloaded.
It should return any media/data it requires to operate
103 @classmethod 104 def getname(cls) -> str: 105 """Return the unique name of this map, in English.""" 106 return cls.name
Return the unique name of this map, in English.
108 @classmethod 109 def get_music_type(cls) -> bascenev1.MusicType | None: 110 """Return a music-type string that should be played on this map. 111 112 If None is returned, default music will be used. 113 """ 114 return None
Return a music-type string that should be played on this map.
If None is returned, default music will be used.
223 def is_point_near_edge( 224 self, point: babase.Vec3, running: bool = False 225 ) -> bool: 226 """Return whether the provided point is near an edge of the map. 227 228 Simple bot logic uses this call to determine if they 229 are approaching a cliff or wall. If this returns True they will 230 generally not walk/run any farther away from the origin. 231 If 'running' is True, the buffer should be a bit larger. 232 """ 233 del point, running # Unused. 234 return False
Return whether the provided point is near an edge of the map.
Simple bot logic uses this call to determine if they are approaching a cliff or wall. If this returns True they will generally not walk/run any farther away from the origin. If 'running' is True, the buffer should be a bit larger.
236 def get_def_bound_box( 237 self, name: str 238 ) -> tuple[float, float, float, float, float, float] | None: 239 """Return a 6 member bounds tuple or None if it is not defined.""" 240 try: 241 box = self.defs.boxes[name] 242 return ( 243 box[0] - box[6] / 2.0, 244 box[1] - box[7] / 2.0, 245 box[2] - box[8] / 2.0, 246 box[0] + box[6] / 2.0, 247 box[1] + box[7] / 2.0, 248 box[2] + box[8] / 2.0, 249 ) 250 except Exception: 251 return None
Return a 6 member bounds tuple or None if it is not defined.
253 def get_def_point(self, name: str) -> Sequence[float] | None: 254 """Return a single defined point or a default value in its absence.""" 255 val = self.defs.points.get(name) 256 return ( 257 None 258 if val is None 259 else babase.vec3validate(val) if __debug__ else val 260 )
Return a single defined point or a default value in its absence.
262 def get_def_points(self, name: str) -> list[Sequence[float]]: 263 """Return a list of named points. 264 265 Return as many sequential ones are defined (flag1, flag2, flag3), etc. 266 If none are defined, returns an empty list. 267 """ 268 point_list = [] 269 if self.defs and name + '1' in self.defs.points: 270 i = 1 271 while name + str(i) in self.defs.points: 272 pts = self.defs.points[name + str(i)] 273 if len(pts) == 6: 274 point_list.append(pts) 275 else: 276 if len(pts) != 3: 277 raise ValueError('invalid point') 278 point_list.append(pts + (0, 0, 0)) 279 i += 1 280 return point_list
Return a list of named points.
Return as many sequential ones are defined (flag1, flag2, flag3), etc. If none are defined, returns an empty list.
282 def get_start_position(self, team_index: int) -> Sequence[float]: 283 """Return a random starting position for the given team index.""" 284 pnt = self.spawn_points[team_index % len(self.spawn_points)] 285 x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3]) 286 z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5]) 287 pnt = ( 288 pnt[0] + random.uniform(*x_range), 289 pnt[1], 290 pnt[2] + random.uniform(*z_range), 291 ) 292 return pnt
Return a random starting position for the given team index.
294 def get_ffa_start_position( 295 self, players: Sequence[bascenev1.Player] 296 ) -> Sequence[float]: 297 """Return a random starting position in one of the FFA spawn areas. 298 299 If a list of bascenev1.Player-s is provided; the returned points 300 will be as far from these players as possible. 301 """ 302 303 # Get positions for existing players. 304 player_pts = [] 305 for player in players: 306 if player.is_alive(): 307 player_pts.append(player.position) 308 309 def _getpt() -> Sequence[float]: 310 point = self.ffa_spawn_points[self._next_ffa_start_index] 311 self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len( 312 self.ffa_spawn_points 313 ) 314 x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3]) 315 z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5]) 316 point = ( 317 point[0] + random.uniform(*x_range), 318 point[1], 319 point[2] + random.uniform(*z_range), 320 ) 321 return point 322 323 if not player_pts: 324 return _getpt() 325 326 # Let's calc several start points and then pick whichever is 327 # farthest from all existing players. 328 farthestpt_dist = -1.0 329 farthestpt = None 330 for _i in range(10): 331 testpt = babase.Vec3(_getpt()) 332 closest_player_dist = 9999.0 333 for ppt in player_pts: 334 dist = (ppt - testpt).length() 335 closest_player_dist = min(dist, closest_player_dist) 336 if closest_player_dist > farthestpt_dist: 337 farthestpt_dist = closest_player_dist 338 farthestpt = testpt 339 assert farthestpt is not None 340 return tuple(farthestpt)
Return a random starting position in one of the FFA spawn areas.
If a list of bascenev1.Player-s is provided; the returned points will be as far from these players as possible.
342 def get_flag_position( 343 self, team_index: int | None = None 344 ) -> Sequence[float]: 345 """Return a flag position on the map for the given team index. 346 347 Pass None to get the default flag point. 348 (used for things such as king-of-the-hill) 349 """ 350 if team_index is None: 351 return self.flag_points_default[:3] 352 return self.flag_points[team_index % len(self.flag_points)][:3]
Return a flag position on the map for the given team index.
Pass None to get the default flag point. (used for things such as king-of-the-hill)
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.
358 @override 359 def handlemessage(self, msg: Any) -> Any: 360 from bascenev1 import _messages 361 362 if isinstance(msg, _messages.DieMessage): 363 if self.node: 364 self.node.delete() 365 else: 366 return super().handlemessage(msg) 367 return None
General message handling; can be passed any message object.
271class Material: 272 """An entity applied to game objects to modify collision behavior. 273 274 Category: **Gameplay Classes** 275 276 A material can affect physical characteristics, generate sounds, 277 or trigger callback functions when collisions occur. 278 279 Materials are applied to 'parts', which are groups of one or more 280 rigid bodies created as part of a bascenev1.Node. Nodes can have any 281 number of parts, each with its own set of materials. Generally 282 materials are specified as array attributes on the Node. The `spaz` 283 node, for example, has various attributes such as `materials`, 284 `roller_materials`, and `punch_materials`, which correspond 285 to the various parts it creates. 286 287 Use bascenev1.Material to instantiate a blank material, and then use 288 its babase.Material.add_actions() method to define what the material 289 does. 290 """ 291 292 def __init__(self, label: str | None = None) -> None: 293 pass 294 295 label: str 296 """A label for the material; only used for debugging.""" 297 298 def add_actions( 299 self, actions: tuple, conditions: tuple | None = None 300 ) -> None: 301 """Add one or more actions to the material, optionally with conditions. 302 303 ##### Conditions 304 Conditions are provided as tuples which can be combined 305 to form boolean logic. A single condition might look like 306 `('condition_name', cond_arg)`, or a more complex nested one 307 might look like `(('some_condition', cond_arg), 'or', 308 ('another_condition', cond2_arg))`. 309 310 `'and'`, `'or'`, and `'xor'` are available to chain 311 together 2 conditions, as seen above. 312 313 ##### Available Conditions 314 ###### `('they_have_material', material)` 315 > Does the part we're hitting have a given bascenev1.Material? 316 317 ###### `('they_dont_have_material', material)` 318 > Does the part we're hitting not have a given bascenev1.Material? 319 320 ###### `('eval_colliding')` 321 > Is `'collide'` true at this point 322 in material evaluation? (see the `modify_part_collision` action) 323 324 ###### `('eval_not_colliding')` 325 > Is 'collide' false at this point 326 in material evaluation? (see the `modify_part_collision` action) 327 328 ###### `('we_are_younger_than', age)` 329 > Is our part younger than `age` (in milliseconds)? 330 331 ###### `('we_are_older_than', age)` 332 > Is our part older than `age` (in milliseconds)? 333 334 ###### `('they_are_younger_than', age)` 335 > Is the part we're hitting younger than `age` (in milliseconds)? 336 337 ###### `('they_are_older_than', age)` 338 > Is the part we're hitting older than `age` (in milliseconds)? 339 340 ###### `('they_are_same_node_as_us')` 341 > Does the part we're hitting belong to the same bascenev1.Node as us? 342 343 ###### `('they_are_different_node_than_us')` 344 > Does the part we're hitting belong to a different bascenev1.Node? 345 346 ##### Actions 347 In a similar manner, actions are specified as tuples. 348 Multiple actions can be specified by providing a tuple 349 of tuples. 350 351 ##### Available Actions 352 ###### `('call', when, callable)` 353 > Calls the provided callable; 354 `when` can be either `'at_connect'` or `'at_disconnect'`. 355 `'at_connect'` means to fire 356 when the two parts first come in contact; `'at_disconnect'` 357 means to fire once they cease being in contact. 358 359 ###### `('message', who, when, message_obj)` 360 > Sends a message object; 361 `who` can be either `'our_node'` or `'their_node'`, `when` can be 362 `'at_connect'` or `'at_disconnect'`, and `message_obj` is the message 363 object to send. 364 This has the same effect as calling the node's 365 babase.Node.handlemessage() method. 366 367 ###### `('modify_part_collision', attr, value)` 368 > Changes some 369 characteristic of the physical collision that will occur between 370 our part and their part. This change will remain in effect as 371 long as the two parts remain overlapping. This means if you have a 372 part with a material that turns `'collide'` off against parts 373 younger than 100ms, and it touches another part that is 50ms old, 374 it will continue to not collide with that part until they separate, 375 even if the 100ms threshold is passed. Options for attr/value are: 376 `'physical'` (boolean value; whether a *physical* response will 377 occur at all), `'friction'` (float value; how friction-y the 378 physical response will be), `'collide'` (boolean value; 379 whether *any* collision will occur at all, including non-physical 380 stuff like callbacks), `'use_node_collide'` 381 (boolean value; whether to honor modify_node_collision 382 overrides for this collision), `'stiffness'` (float value, 383 how springy the physical response is), `'damping'` (float 384 value, how damped the physical response is), `'bounce'` (float 385 value; how bouncy the physical response is). 386 387 ###### `('modify_node_collision', attr, value)` 388 > Similar to 389 `modify_part_collision`, but operates at a node-level. 390 collision attributes set here will remain in effect as long as 391 *anything* from our part's node and their part's node overlap. 392 A key use of this functionality is to prevent new nodes from 393 colliding with each other if they appear overlapped; 394 if `modify_part_collision` is used, only the individual 395 parts that were overlapping would avoid contact, but other parts 396 could still contact leaving the two nodes 'tangled up'. Using 397 `modify_node_collision` ensures that the nodes must completely 398 separate before they can start colliding. Currently the only attr 399 available here is `'collide'` (a boolean value). 400 401 ###### `('sound', sound, volume)` 402 > Plays a bascenev1.Sound when a collision 403 occurs, at a given volume, regardless of the collision speed/etc. 404 405 ###### `('impact_sound', sound, targetImpulse, volume)` 406 > Plays a sound 407 when a collision occurs, based on the speed of impact. 408 Provide a bascenev1.Sound, a target-impulse, and a volume. 409 410 ###### `('skid_sound', sound, targetImpulse, volume)` 411 > Plays a sound 412 during a collision when parts are 'scraping' against each other. 413 Provide a bascenev1.Sound, a target-impulse, and a volume. 414 415 ###### `('roll_sound', sound, targetImpulse, volume)` 416 > Plays a sound 417 during a collision when parts are 'rolling' against each other. 418 Provide a bascenev1.Sound, a target-impulse, and a volume. 419 420 ##### Examples 421 **Example 1:** create a material that lets us ignore 422 collisions against any nodes we touch in the first 423 100 ms of our existence; handy for preventing us from 424 exploding outward if we spawn on top of another object: 425 >>> m = bascenev1.Material() 426 ... m.add_actions( 427 ... conditions=(('we_are_younger_than', 100), 428 ... 'or', ('they_are_younger_than', 100)), 429 ... actions=('modify_node_collision', 'collide', False)) 430 431 **Example 2:** send a bascenev1.DieMessage to anything we touch, but 432 cause no physical response. This should cause any bascenev1.Actor to 433 drop dead: 434 >>> m = bascenev1.Material() 435 ... m.add_actions( 436 ... actions=(('modify_part_collision', 'physical', False), 437 ... ('message', 'their_node', 'at_connect', 438 ... bascenev1.DieMessage()))) 439 440 **Example 3:** play some sounds when we're contacting the ground: 441 >>> m = bascenev1.Material() 442 ... m.add_actions( 443 ... conditions=('they_have_material', shared.footing_material), 444 ... actions=( 445 ('impact_sound', bascenev1.getsound('metalHit'), 2, 5), 446 ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5))) 447 """ 448 return None
An entity applied to game objects to modify collision behavior.
Category: Gameplay Classes
A material can affect physical characteristics, generate sounds, or trigger callback functions when collisions occur.
Materials are applied to 'parts', which are groups of one or more
rigid bodies created as part of a bascenev1.Node. Nodes can have any
number of parts, each with its own set of materials. Generally
materials are specified as array attributes on the Node. The spaz
node, for example, has various attributes such as materials
,
roller_materials
, and punch_materials
, which correspond
to the various parts it creates.
Use bascenev1.Material to instantiate a blank material, and then use its babase.Material.add_actions() method to define what the material does.
298 def add_actions( 299 self, actions: tuple, conditions: tuple | None = None 300 ) -> None: 301 """Add one or more actions to the material, optionally with conditions. 302 303 ##### Conditions 304 Conditions are provided as tuples which can be combined 305 to form boolean logic. A single condition might look like 306 `('condition_name', cond_arg)`, or a more complex nested one 307 might look like `(('some_condition', cond_arg), 'or', 308 ('another_condition', cond2_arg))`. 309 310 `'and'`, `'or'`, and `'xor'` are available to chain 311 together 2 conditions, as seen above. 312 313 ##### Available Conditions 314 ###### `('they_have_material', material)` 315 > Does the part we're hitting have a given bascenev1.Material? 316 317 ###### `('they_dont_have_material', material)` 318 > Does the part we're hitting not have a given bascenev1.Material? 319 320 ###### `('eval_colliding')` 321 > Is `'collide'` true at this point 322 in material evaluation? (see the `modify_part_collision` action) 323 324 ###### `('eval_not_colliding')` 325 > Is 'collide' false at this point 326 in material evaluation? (see the `modify_part_collision` action) 327 328 ###### `('we_are_younger_than', age)` 329 > Is our part younger than `age` (in milliseconds)? 330 331 ###### `('we_are_older_than', age)` 332 > Is our part older than `age` (in milliseconds)? 333 334 ###### `('they_are_younger_than', age)` 335 > Is the part we're hitting younger than `age` (in milliseconds)? 336 337 ###### `('they_are_older_than', age)` 338 > Is the part we're hitting older than `age` (in milliseconds)? 339 340 ###### `('they_are_same_node_as_us')` 341 > Does the part we're hitting belong to the same bascenev1.Node as us? 342 343 ###### `('they_are_different_node_than_us')` 344 > Does the part we're hitting belong to a different bascenev1.Node? 345 346 ##### Actions 347 In a similar manner, actions are specified as tuples. 348 Multiple actions can be specified by providing a tuple 349 of tuples. 350 351 ##### Available Actions 352 ###### `('call', when, callable)` 353 > Calls the provided callable; 354 `when` can be either `'at_connect'` or `'at_disconnect'`. 355 `'at_connect'` means to fire 356 when the two parts first come in contact; `'at_disconnect'` 357 means to fire once they cease being in contact. 358 359 ###### `('message', who, when, message_obj)` 360 > Sends a message object; 361 `who` can be either `'our_node'` or `'their_node'`, `when` can be 362 `'at_connect'` or `'at_disconnect'`, and `message_obj` is the message 363 object to send. 364 This has the same effect as calling the node's 365 babase.Node.handlemessage() method. 366 367 ###### `('modify_part_collision', attr, value)` 368 > Changes some 369 characteristic of the physical collision that will occur between 370 our part and their part. This change will remain in effect as 371 long as the two parts remain overlapping. This means if you have a 372 part with a material that turns `'collide'` off against parts 373 younger than 100ms, and it touches another part that is 50ms old, 374 it will continue to not collide with that part until they separate, 375 even if the 100ms threshold is passed. Options for attr/value are: 376 `'physical'` (boolean value; whether a *physical* response will 377 occur at all), `'friction'` (float value; how friction-y the 378 physical response will be), `'collide'` (boolean value; 379 whether *any* collision will occur at all, including non-physical 380 stuff like callbacks), `'use_node_collide'` 381 (boolean value; whether to honor modify_node_collision 382 overrides for this collision), `'stiffness'` (float value, 383 how springy the physical response is), `'damping'` (float 384 value, how damped the physical response is), `'bounce'` (float 385 value; how bouncy the physical response is). 386 387 ###### `('modify_node_collision', attr, value)` 388 > Similar to 389 `modify_part_collision`, but operates at a node-level. 390 collision attributes set here will remain in effect as long as 391 *anything* from our part's node and their part's node overlap. 392 A key use of this functionality is to prevent new nodes from 393 colliding with each other if they appear overlapped; 394 if `modify_part_collision` is used, only the individual 395 parts that were overlapping would avoid contact, but other parts 396 could still contact leaving the two nodes 'tangled up'. Using 397 `modify_node_collision` ensures that the nodes must completely 398 separate before they can start colliding. Currently the only attr 399 available here is `'collide'` (a boolean value). 400 401 ###### `('sound', sound, volume)` 402 > Plays a bascenev1.Sound when a collision 403 occurs, at a given volume, regardless of the collision speed/etc. 404 405 ###### `('impact_sound', sound, targetImpulse, volume)` 406 > Plays a sound 407 when a collision occurs, based on the speed of impact. 408 Provide a bascenev1.Sound, a target-impulse, and a volume. 409 410 ###### `('skid_sound', sound, targetImpulse, volume)` 411 > Plays a sound 412 during a collision when parts are 'scraping' against each other. 413 Provide a bascenev1.Sound, a target-impulse, and a volume. 414 415 ###### `('roll_sound', sound, targetImpulse, volume)` 416 > Plays a sound 417 during a collision when parts are 'rolling' against each other. 418 Provide a bascenev1.Sound, a target-impulse, and a volume. 419 420 ##### Examples 421 **Example 1:** create a material that lets us ignore 422 collisions against any nodes we touch in the first 423 100 ms of our existence; handy for preventing us from 424 exploding outward if we spawn on top of another object: 425 >>> m = bascenev1.Material() 426 ... m.add_actions( 427 ... conditions=(('we_are_younger_than', 100), 428 ... 'or', ('they_are_younger_than', 100)), 429 ... actions=('modify_node_collision', 'collide', False)) 430 431 **Example 2:** send a bascenev1.DieMessage to anything we touch, but 432 cause no physical response. This should cause any bascenev1.Actor to 433 drop dead: 434 >>> m = bascenev1.Material() 435 ... m.add_actions( 436 ... actions=(('modify_part_collision', 'physical', False), 437 ... ('message', 'their_node', 'at_connect', 438 ... bascenev1.DieMessage()))) 439 440 **Example 3:** play some sounds when we're contacting the ground: 441 >>> m = bascenev1.Material() 442 ... m.add_actions( 443 ... conditions=('they_have_material', shared.footing_material), 444 ... actions=( 445 ('impact_sound', bascenev1.getsound('metalHit'), 2, 5), 446 ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5))) 447 """ 448 return None
Add one or more actions to the material, optionally with conditions.
Conditions
Conditions are provided as tuples which can be combined
to form boolean logic. A single condition might look like
('condition_name', cond_arg)
, or a more complex nested one
might look like (('some_condition', cond_arg), 'or',
('another_condition', cond2_arg))
.
'and'
, 'or'
, and 'xor'
are available to chain
together 2 conditions, as seen above.
Available Conditions
('they_have_material', material)
Does the part we're hitting have a given bascenev1.Material?
('they_dont_have_material', material)
Does the part we're hitting not have a given bascenev1.Material?
('eval_colliding')
Is
'collide'
true at this point in material evaluation? (see themodify_part_collision
action)
('eval_not_colliding')
Is 'collide' false at this point in material evaluation? (see the
modify_part_collision
action)
('we_are_younger_than', age)
Is our part younger than
age
(in milliseconds)?
('we_are_older_than', age)
Is our part older than
age
(in milliseconds)?
('they_are_younger_than', age)
Is the part we're hitting younger than
age
(in milliseconds)?
('they_are_older_than', age)
Is the part we're hitting older than
age
(in milliseconds)?
('they_are_same_node_as_us')
Does the part we're hitting belong to the same bascenev1.Node as us?
('they_are_different_node_than_us')
Does the part we're hitting belong to a different bascenev1.Node?
Actions
In a similar manner, actions are specified as tuples. Multiple actions can be specified by providing a tuple of tuples.
Available Actions
('call', when, callable)
Calls the provided callable;
when
can be either'at_connect'
or'at_disconnect'
.'at_connect'
means to fire when the two parts first come in contact;'at_disconnect'
means to fire once they cease being in contact.
('message', who, when, message_obj)
Sends a message object;
who
can be either'our_node'
or'their_node'
,when
can be'at_connect'
or'at_disconnect'
, andmessage_obj
is the message object to send. This has the same effect as calling the node's babase.Node.handlemessage() method.
('modify_part_collision', attr, value)
Changes some characteristic of the physical collision that will occur between our part and their part. This change will remain in effect as long as the two parts remain overlapping. This means if you have a part with a material that turns
'collide'
off against parts younger than 100ms, and it touches another part that is 50ms old, it will continue to not collide with that part until they separate, even if the 100ms threshold is passed. Options for attr/value are:'physical'
(boolean value; whether a physical response will occur at all),'friction'
(float value; how friction-y the physical response will be),'collide'
(boolean value; whether any collision will occur at all, including non-physical stuff like callbacks),'use_node_collide'
(boolean value; whether to honor modify_node_collision overrides for this collision),'stiffness'
(float value, how springy the physical response is),'damping'
(float value, how damped the physical response is),'bounce'
(float value; how bouncy the physical response is).
('modify_node_collision', attr, value)
Similar to
modify_part_collision
, but operates at a node-level. collision attributes set here will remain in effect as long as anything from our part's node and their part's node overlap. A key use of this functionality is to prevent new nodes from colliding with each other if they appear overlapped; ifmodify_part_collision
is used, only the individual parts that were overlapping would avoid contact, but other parts could still contact leaving the two nodes 'tangled up'. Usingmodify_node_collision
ensures that the nodes must completely separate before they can start colliding. Currently the only attr available here is'collide'
(a boolean value).
('sound', sound, volume)
Plays a bascenev1.Sound when a collision occurs, at a given volume, regardless of the collision speed/etc.
('impact_sound', sound, targetImpulse, volume)
Plays a sound when a collision occurs, based on the speed of impact. Provide a bascenev1.Sound, a target-impulse, and a volume.
('skid_sound', sound, targetImpulse, volume)
Plays a sound during a collision when parts are 'scraping' against each other. Provide a bascenev1.Sound, a target-impulse, and a volume.
('roll_sound', sound, targetImpulse, volume)
Plays a sound during a collision when parts are 'rolling' against each other. Provide a bascenev1.Sound, a target-impulse, and a volume.
Examples
Example 1: create a material that lets us ignore collisions against any nodes we touch in the first 100 ms of our existence; handy for preventing us from exploding outward if we spawn on top of another object:
>>> m = bascenev1.Material()
... m.add_actions(
... conditions=(('we_are_younger_than', 100),
... 'or', ('they_are_younger_than', 100)),
... actions=('modify_node_collision', 'collide', False))
Example 2: send a bascenev1.DieMessage to anything we touch, but cause no physical response. This should cause any bascenev1.Actor to drop dead:
>>> m = bascenev1.Material()
... m.add_actions(
... actions=(('modify_part_collision', 'physical', False),
... ('message', 'their_node', 'at_connect',
... bascenev1.DieMessage())))
Example 3: play some sounds when we're contacting the ground:
>>> m = bascenev1.Material()
... m.add_actions(
... conditions=('they_have_material', shared.footing_material),
... actions=(
('impact_sound', bascenev1.getsound('metalHit'), 2, 5),
('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)))
451class Mesh: 452 """A reference to a mesh. 453 454 Category: **Asset Classes** 455 456 Meshes are used for drawing. 457 Use bascenev1.getmesh() to instantiate one. 458 """ 459 460 pass
A reference to a mesh.
Category: Asset Classes
Meshes are used for drawing. Use bascenev1.getmesh() to instantiate one.
26class MultiTeamSession(Session): 27 """Common base for DualTeamSession and FreeForAllSession. 28 29 Category: **Gameplay Classes** 30 31 Free-for-all-mode is essentially just teams-mode with each 32 bascenev1.Player having their own bascenev1.Team, so there is much 33 overlap in functionality. 34 """ 35 36 # These should be overridden. 37 _playlist_selection_var = 'UNSET Playlist Selection' 38 _playlist_randomize_var = 'UNSET Playlist Randomize' 39 _playlists_var = 'UNSET Playlists' 40 41 def __init__(self) -> None: 42 """Set up playlists & launch a bascenev1.Activity to accept joiners.""" 43 # pylint: disable=cyclic-import 44 from bascenev1 import _playlist 45 from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity 46 47 app = babase.app 48 classic = app.classic 49 assert classic is not None 50 cfg = app.config 51 52 if self.use_teams: 53 team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES) 54 team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS) 55 else: 56 team_names = None 57 team_colors = None 58 59 # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.') 60 depsets: Sequence[bascenev1.DependencySet] = [] 61 62 super().__init__( 63 depsets, 64 team_names=team_names, 65 team_colors=team_colors, 66 min_players=1, 67 max_players=self.get_max_players(), 68 ) 69 70 self._series_length: int = int(cfg.get('Teams Series Length', 7)) 71 self._ffa_series_length: int = int(cfg.get('FFA Series Length', 24)) 72 73 show_tutorial = cfg.get('Show Tutorial', True) 74 75 # Special case: don't show tutorial while stress testing. 76 if classic.stress_test_update_timer is not None: 77 show_tutorial = False 78 79 self._tutorial_activity_instance: bascenev1.Activity | None 80 if show_tutorial: 81 from bascenev1lib.tutorial import TutorialActivity 82 83 tutorial_activity = TutorialActivity 84 85 # Get this loading. 86 self._tutorial_activity_instance = _bascenev1.newactivity( 87 tutorial_activity 88 ) 89 else: 90 self._tutorial_activity_instance = None 91 92 self._playlist_name = cfg.get( 93 self._playlist_selection_var, '__default__' 94 ) 95 self._playlist_randomize = cfg.get(self._playlist_randomize_var, False) 96 97 # Which game activity we're on. 98 self._game_number = 0 99 100 playlists = cfg.get(self._playlists_var, {}) 101 102 if ( 103 self._playlist_name != '__default__' 104 and self._playlist_name in playlists 105 ): 106 # Make sure to copy this, as we muck with it in place once we've 107 # got it and we don't want that to affect our config. 108 playlist = copy.deepcopy(playlists[self._playlist_name]) 109 else: 110 if self.use_teams: 111 playlist = _playlist.get_default_teams_playlist() 112 else: 113 playlist = _playlist.get_default_free_for_all_playlist() 114 115 # Resolve types and whatnot to get our final playlist. 116 playlist_resolved = _playlist.filter_playlist( 117 playlist, 118 sessiontype=type(self), 119 add_resolved_type=True, 120 name='default teams' if self.use_teams else 'default ffa', 121 ) 122 123 if not playlist_resolved: 124 raise RuntimeError('Playlist contains no valid games.') 125 126 self._playlist = ShuffleList( 127 playlist_resolved, shuffle=self._playlist_randomize 128 ) 129 130 # Get a game on deck ready to go. 131 self._current_game_spec: dict[str, Any] | None = None 132 self._next_game_spec: dict[str, Any] = self._playlist.pull_next() 133 self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[ 134 'resolved_type' 135 ] 136 137 # Go ahead and instantiate the next game we'll 138 # use so it has lots of time to load. 139 self._instantiate_next_game() 140 141 # Start in our custom join screen. 142 self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity)) 143 144 def get_ffa_series_length(self) -> int: 145 """Return free-for-all series length.""" 146 return self._ffa_series_length 147 148 def get_series_length(self) -> int: 149 """Return teams series length.""" 150 return self._series_length 151 152 def get_next_game_description(self) -> babase.Lstr: 153 """Returns a description of the next game on deck.""" 154 # pylint: disable=cyclic-import 155 from bascenev1._gameactivity import GameActivity 156 157 gametype: type[GameActivity] = self._next_game_spec['resolved_type'] 158 assert issubclass(gametype, GameActivity) 159 return gametype.get_settings_display_string(self._next_game_spec) 160 161 def get_game_number(self) -> int: 162 """Returns which game in the series is currently being played.""" 163 return self._game_number 164 165 @override 166 def on_team_join(self, team: bascenev1.SessionTeam) -> None: 167 team.customdata['previous_score'] = team.customdata['score'] = 0 168 169 def get_max_players(self) -> int: 170 """Return max number of Players allowed to join the game at once.""" 171 if self.use_teams: 172 val = babase.app.config.get('Team Game Max Players', 8) 173 else: 174 val = babase.app.config.get('Free-for-All Max Players', 8) 175 assert isinstance(val, int) 176 return val 177 178 def _instantiate_next_game(self) -> None: 179 self._next_game_instance = _bascenev1.newactivity( 180 self._next_game_spec['resolved_type'], 181 self._next_game_spec['settings'], 182 ) 183 184 @override 185 def on_activity_end( 186 self, activity: bascenev1.Activity, results: Any 187 ) -> None: 188 # pylint: disable=cyclic-import 189 from bascenev1lib.tutorial import TutorialActivity 190 from bascenev1lib.activity.multiteamvictory import ( 191 TeamSeriesVictoryScoreScreenActivity, 192 ) 193 from bascenev1._activitytypes import ( 194 TransitionActivity, 195 JoinActivity, 196 ScoreScreenActivity, 197 ) 198 199 # If we have a tutorial to show, that's the first thing we do no 200 # matter what. 201 if self._tutorial_activity_instance is not None: 202 self.setactivity(self._tutorial_activity_instance) 203 self._tutorial_activity_instance = None 204 205 # If we're leaving the tutorial activity, pop a transition activity 206 # to transition us into a round gracefully (otherwise we'd snap from 207 # one terrain to another instantly). 208 elif isinstance(activity, TutorialActivity): 209 self.setactivity(_bascenev1.newactivity(TransitionActivity)) 210 211 # If we're in a between-round activity or a restart-activity, hop 212 # into a round. 213 elif isinstance( 214 activity, (JoinActivity, TransitionActivity, ScoreScreenActivity) 215 ): 216 # If we're coming from a series-end activity, reset scores. 217 if isinstance(activity, TeamSeriesVictoryScoreScreenActivity): 218 self.stats.reset() 219 self._game_number = 0 220 for team in self.sessionteams: 221 team.customdata['score'] = 0 222 223 # Otherwise just set accum (per-game) scores. 224 else: 225 self.stats.reset_accum() 226 227 next_game = self._next_game_instance 228 229 self._current_game_spec = self._next_game_spec 230 self._next_game_spec = self._playlist.pull_next() 231 self._game_number += 1 232 233 # Instantiate the next now so they have plenty of time to load. 234 self._instantiate_next_game() 235 236 # (Re)register all players and wire stats to our next activity. 237 for player in self.sessionplayers: 238 # ..but only ones who have been placed on a team 239 # (ie: no longer sitting in the lobby). 240 try: 241 has_team = player.sessionteam is not None 242 except babase.NotFoundError: 243 has_team = False 244 if has_team: 245 self.stats.register_sessionplayer(player) 246 self.stats.setactivity(next_game) 247 248 # Now flip the current activity. 249 self.setactivity(next_game) 250 251 # If we're leaving a round, go to the score screen. 252 else: 253 self._switch_to_score_screen(results) 254 255 def _switch_to_score_screen(self, results: Any) -> None: 256 """Switch to a score screen after leaving a round.""" 257 del results # Unused arg. 258 logging.error('This should be overridden.', stack_info=True) 259 260 def announce_game_results( 261 self, 262 activity: bascenev1.GameActivity, 263 results: bascenev1.GameResults, 264 delay: float, 265 announce_winning_team: bool = True, 266 ) -> None: 267 """Show basic game result at the end of a game. 268 269 (before transitioning to a score screen). 270 This will include a zoom-text of 'BLUE WINS' 271 or whatnot, along with a possible audio 272 announcement of the same. 273 """ 274 # pylint: disable=cyclic-import 275 from bascenev1._gameutils import cameraflash 276 from bascenev1._freeforallsession import FreeForAllSession 277 from bascenev1._messages import CelebrateMessage 278 279 _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play) 280 281 if announce_winning_team: 282 winning_sessionteam = results.winning_sessionteam 283 if winning_sessionteam is not None: 284 # Have all players celebrate. 285 celebrate_msg = CelebrateMessage(duration=10.0) 286 assert winning_sessionteam.activityteam is not None 287 for player in winning_sessionteam.activityteam.players: 288 if player.actor: 289 player.actor.handlemessage(celebrate_msg) 290 cameraflash() 291 292 # Some languages say "FOO WINS" different for teams vs players. 293 if isinstance(self, FreeForAllSession): 294 wins_resource = 'winsPlayerText' 295 else: 296 wins_resource = 'winsTeamText' 297 wins_text = babase.Lstr( 298 resource=wins_resource, 299 subs=[('${NAME}', winning_sessionteam.name)], 300 ) 301 activity.show_zoom_message( 302 wins_text, 303 scale=0.85, 304 color=babase.normalized_color(winning_sessionteam.color), 305 )
Common base for DualTeamSession and FreeForAllSession.
Category: Gameplay Classes
Free-for-all-mode is essentially just teams-mode with each bascenev1.Player having their own bascenev1.Team, so there is much overlap in functionality.
41 def __init__(self) -> None: 42 """Set up playlists & launch a bascenev1.Activity to accept joiners.""" 43 # pylint: disable=cyclic-import 44 from bascenev1 import _playlist 45 from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity 46 47 app = babase.app 48 classic = app.classic 49 assert classic is not None 50 cfg = app.config 51 52 if self.use_teams: 53 team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES) 54 team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS) 55 else: 56 team_names = None 57 team_colors = None 58 59 # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.') 60 depsets: Sequence[bascenev1.DependencySet] = [] 61 62 super().__init__( 63 depsets, 64 team_names=team_names, 65 team_colors=team_colors, 66 min_players=1, 67 max_players=self.get_max_players(), 68 ) 69 70 self._series_length: int = int(cfg.get('Teams Series Length', 7)) 71 self._ffa_series_length: int = int(cfg.get('FFA Series Length', 24)) 72 73 show_tutorial = cfg.get('Show Tutorial', True) 74 75 # Special case: don't show tutorial while stress testing. 76 if classic.stress_test_update_timer is not None: 77 show_tutorial = False 78 79 self._tutorial_activity_instance: bascenev1.Activity | None 80 if show_tutorial: 81 from bascenev1lib.tutorial import TutorialActivity 82 83 tutorial_activity = TutorialActivity 84 85 # Get this loading. 86 self._tutorial_activity_instance = _bascenev1.newactivity( 87 tutorial_activity 88 ) 89 else: 90 self._tutorial_activity_instance = None 91 92 self._playlist_name = cfg.get( 93 self._playlist_selection_var, '__default__' 94 ) 95 self._playlist_randomize = cfg.get(self._playlist_randomize_var, False) 96 97 # Which game activity we're on. 98 self._game_number = 0 99 100 playlists = cfg.get(self._playlists_var, {}) 101 102 if ( 103 self._playlist_name != '__default__' 104 and self._playlist_name in playlists 105 ): 106 # Make sure to copy this, as we muck with it in place once we've 107 # got it and we don't want that to affect our config. 108 playlist = copy.deepcopy(playlists[self._playlist_name]) 109 else: 110 if self.use_teams: 111 playlist = _playlist.get_default_teams_playlist() 112 else: 113 playlist = _playlist.get_default_free_for_all_playlist() 114 115 # Resolve types and whatnot to get our final playlist. 116 playlist_resolved = _playlist.filter_playlist( 117 playlist, 118 sessiontype=type(self), 119 add_resolved_type=True, 120 name='default teams' if self.use_teams else 'default ffa', 121 ) 122 123 if not playlist_resolved: 124 raise RuntimeError('Playlist contains no valid games.') 125 126 self._playlist = ShuffleList( 127 playlist_resolved, shuffle=self._playlist_randomize 128 ) 129 130 # Get a game on deck ready to go. 131 self._current_game_spec: dict[str, Any] | None = None 132 self._next_game_spec: dict[str, Any] = self._playlist.pull_next() 133 self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[ 134 'resolved_type' 135 ] 136 137 # Go ahead and instantiate the next game we'll 138 # use so it has lots of time to load. 139 self._instantiate_next_game() 140 141 # Start in our custom join screen. 142 self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity))
Set up playlists & launch a bascenev1.Activity to accept joiners.
144 def get_ffa_series_length(self) -> int: 145 """Return free-for-all series length.""" 146 return self._ffa_series_length
Return free-for-all series length.
148 def get_series_length(self) -> int: 149 """Return teams series length.""" 150 return self._series_length
Return teams series length.
152 def get_next_game_description(self) -> babase.Lstr: 153 """Returns a description of the next game on deck.""" 154 # pylint: disable=cyclic-import 155 from bascenev1._gameactivity import GameActivity 156 157 gametype: type[GameActivity] = self._next_game_spec['resolved_type'] 158 assert issubclass(gametype, GameActivity) 159 return gametype.get_settings_display_string(self._next_game_spec)
Returns a description of the next game on deck.
161 def get_game_number(self) -> int: 162 """Returns which game in the series is currently being played.""" 163 return self._game_number
Returns which game in the series is currently being played.
165 @override 166 def on_team_join(self, team: bascenev1.SessionTeam) -> None: 167 team.customdata['previous_score'] = team.customdata['score'] = 0
Called when a new bascenev1.Team joins the session.
169 def get_max_players(self) -> int: 170 """Return max number of Players allowed to join the game at once.""" 171 if self.use_teams: 172 val = babase.app.config.get('Team Game Max Players', 8) 173 else: 174 val = babase.app.config.get('Free-for-All Max Players', 8) 175 assert isinstance(val, int) 176 return val
Return max number of Players allowed to join the game at once.
184 @override 185 def on_activity_end( 186 self, activity: bascenev1.Activity, results: Any 187 ) -> None: 188 # pylint: disable=cyclic-import 189 from bascenev1lib.tutorial import TutorialActivity 190 from bascenev1lib.activity.multiteamvictory import ( 191 TeamSeriesVictoryScoreScreenActivity, 192 ) 193 from bascenev1._activitytypes import ( 194 TransitionActivity, 195 JoinActivity, 196 ScoreScreenActivity, 197 ) 198 199 # If we have a tutorial to show, that's the first thing we do no 200 # matter what. 201 if self._tutorial_activity_instance is not None: 202 self.setactivity(self._tutorial_activity_instance) 203 self._tutorial_activity_instance = None 204 205 # If we're leaving the tutorial activity, pop a transition activity 206 # to transition us into a round gracefully (otherwise we'd snap from 207 # one terrain to another instantly). 208 elif isinstance(activity, TutorialActivity): 209 self.setactivity(_bascenev1.newactivity(TransitionActivity)) 210 211 # If we're in a between-round activity or a restart-activity, hop 212 # into a round. 213 elif isinstance( 214 activity, (JoinActivity, TransitionActivity, ScoreScreenActivity) 215 ): 216 # If we're coming from a series-end activity, reset scores. 217 if isinstance(activity, TeamSeriesVictoryScoreScreenActivity): 218 self.stats.reset() 219 self._game_number = 0 220 for team in self.sessionteams: 221 team.customdata['score'] = 0 222 223 # Otherwise just set accum (per-game) scores. 224 else: 225 self.stats.reset_accum() 226 227 next_game = self._next_game_instance 228 229 self._current_game_spec = self._next_game_spec 230 self._next_game_spec = self._playlist.pull_next() 231 self._game_number += 1 232 233 # Instantiate the next now so they have plenty of time to load. 234 self._instantiate_next_game() 235 236 # (Re)register all players and wire stats to our next activity. 237 for player in self.sessionplayers: 238 # ..but only ones who have been placed on a team 239 # (ie: no longer sitting in the lobby). 240 try: 241 has_team = player.sessionteam is not None 242 except babase.NotFoundError: 243 has_team = False 244 if has_team: 245 self.stats.register_sessionplayer(player) 246 self.stats.setactivity(next_game) 247 248 # Now flip the current activity. 249 self.setactivity(next_game) 250 251 # If we're leaving a round, go to the score screen. 252 else: 253 self._switch_to_score_screen(results)
Called when the current bascenev1.Activity has ended.
The bascenev1.Session should look at the results and start another bascenev1.Activity.
260 def announce_game_results( 261 self, 262 activity: bascenev1.GameActivity, 263 results: bascenev1.GameResults, 264 delay: float, 265 announce_winning_team: bool = True, 266 ) -> None: 267 """Show basic game result at the end of a game. 268 269 (before transitioning to a score screen). 270 This will include a zoom-text of 'BLUE WINS' 271 or whatnot, along with a possible audio 272 announcement of the same. 273 """ 274 # pylint: disable=cyclic-import 275 from bascenev1._gameutils import cameraflash 276 from bascenev1._freeforallsession import FreeForAllSession 277 from bascenev1._messages import CelebrateMessage 278 279 _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play) 280 281 if announce_winning_team: 282 winning_sessionteam = results.winning_sessionteam 283 if winning_sessionteam is not None: 284 # Have all players celebrate. 285 celebrate_msg = CelebrateMessage(duration=10.0) 286 assert winning_sessionteam.activityteam is not None 287 for player in winning_sessionteam.activityteam.players: 288 if player.actor: 289 player.actor.handlemessage(celebrate_msg) 290 cameraflash() 291 292 # Some languages say "FOO WINS" different for teams vs players. 293 if isinstance(self, FreeForAllSession): 294 wins_resource = 'winsPlayerText' 295 else: 296 wins_resource = 'winsTeamText' 297 wins_text = babase.Lstr( 298 resource=wins_resource, 299 subs=[('${NAME}', winning_sessionteam.name)], 300 ) 301 activity.show_zoom_message( 302 wins_text, 303 scale=0.85, 304 color=babase.normalized_color(winning_sessionteam.color), 305 )
Show basic game result at the end of a game.
(before transitioning to a score screen). This will include a zoom-text of 'BLUE WINS' or whatnot, along with a possible audio announcement of the same.
17class MusicType(Enum): 18 """Types of music available to play in-game. 19 20 Category: **Enums** 21 22 These do not correspond to specific pieces of music, but rather to 23 'situations'. The actual music played for each type can be overridden 24 by the game or by the user. 25 """ 26 27 MENU = 'Menu' 28 VICTORY = 'Victory' 29 CHAR_SELECT = 'CharSelect' 30 RUN_AWAY = 'RunAway' 31 ONSLAUGHT = 'Onslaught' 32 KEEP_AWAY = 'Keep Away' 33 RACE = 'Race' 34 EPIC_RACE = 'Epic Race' 35 SCORES = 'Scores' 36 GRAND_ROMP = 'GrandRomp' 37 TO_THE_DEATH = 'ToTheDeath' 38 CHOSEN_ONE = 'Chosen One' 39 FORWARD_MARCH = 'ForwardMarch' 40 FLAG_CATCHER = 'FlagCatcher' 41 SURVIVAL = 'Survival' 42 EPIC = 'Epic' 43 SPORTS = 'Sports' 44 HOCKEY = 'Hockey' 45 FOOTBALL = 'Football' 46 FLYING = 'Flying' 47 SCARY = 'Scary' 48 MARCHING = 'Marching'
Types of music available to play in-game.
Category: Enums
These do not correspond to specific pieces of music, but rather to 'situations'. The actual music played for each type can be overridden by the game or by the user.
1515def newactivity( 1516 activity_type: type[bascenev1.Activity], settings: dict | None = None 1517) -> bascenev1.Activity: 1518 """Instantiates a bascenev1.Activity given a type object. 1519 1520 Category: **General Utility Functions** 1521 1522 Activities require special setup and thus cannot be directly 1523 instantiated; you must go through this function. 1524 """ 1525 import bascenev1 # pylint: disable=cyclic-import 1526 1527 return bascenev1.Activity(settings={})
Instantiates a bascenev1.Activity given a type object.
Category: General Utility Functions
Activities require special setup and thus cannot be directly instantiated; you must go through this function.
1531def newnode( 1532 type: str, 1533 owner: bascenev1.Node | None = None, 1534 attrs: dict | None = None, 1535 name: str | None = None, 1536 delegate: Any = None, 1537) -> bascenev1.Node: 1538 """Add a node of the given type to the game. 1539 1540 Category: **Gameplay Functions** 1541 1542 If a dict is provided for 'attributes', the node's initial attributes 1543 will be set based on them. 1544 1545 'name', if provided, will be stored with the node purely for debugging 1546 purposes. If no name is provided, an automatic one will be generated 1547 such as 'terrain@foo.py:30'. 1548 1549 If 'delegate' is provided, Python messages sent to the node will go to 1550 that object's handlemessage() method. Note that the delegate is stored 1551 as a weak-ref, so the node itself will not keep the object alive. 1552 1553 if 'owner' is provided, the node will be automatically killed when that 1554 object dies. 'owner' can be another node or a bascenev1.Actor 1555 """ 1556 import bascenev1 # pylint: disable=cyclic-import 1557 1558 return bascenev1.Node()
Add a node of the given type to the game.
Category: Gameplay Functions
If a dict is provided for 'attributes', the node's initial attributes will be set based on them.
'name', if provided, will be stored with the node purely for debugging purposes. If no name is provided, an automatic one will be generated such as 'terrain@foo.py:30'.
If 'delegate' is provided, Python messages sent to the node will go to that object's handlemessage() method. Note that the delegate is stored as a weak-ref, so the node itself will not keep the object alive.
if 'owner' is provided, the node will be automatically killed when that object dies. 'owner' can be another node or a bascenev1.Actor
464class Node: 465 """Reference to a Node; the low level building block of a game. 466 467 Category: **Gameplay Classes** 468 469 At its core, a game is nothing more than a scene of Nodes 470 with attributes getting interconnected or set over time. 471 472 A bascenev1.Node instance should be thought of as a weak-reference 473 to a game node; *not* the node itself. This means a Node's 474 lifecycle is completely independent of how many Python references 475 to it exist. To explicitly add a new node to the game, use 476 bascenev1.newnode(), and to explicitly delete one, 477 use bascenev1.Node.delete(). 478 babase.Node.exists() can be used to determine if a Node still points 479 to a live node in the game. 480 481 You can use `ba.Node(None)` to instantiate an invalid 482 Node reference (sometimes used as attr values/etc). 483 """ 484 485 # Note attributes: 486 # NOTE: I'm just adding *all* possible node attrs here 487 # now now since we have a single bascenev1.Node type; in the 488 # future I hope to create proper individual classes 489 # corresponding to different node types with correct 490 # attributes per node-type. 491 color: Sequence[float] = (0.0, 0.0, 0.0) 492 size: Sequence[float] = (0.0, 0.0, 0.0) 493 position: Sequence[float] = (0.0, 0.0, 0.0) 494 position_center: Sequence[float] = (0.0, 0.0, 0.0) 495 position_forward: Sequence[float] = (0.0, 0.0, 0.0) 496 punch_position: Sequence[float] = (0.0, 0.0, 0.0) 497 punch_velocity: Sequence[float] = (0.0, 0.0, 0.0) 498 velocity: Sequence[float] = (0.0, 0.0, 0.0) 499 name_color: Sequence[float] = (0.0, 0.0, 0.0) 500 tint_color: Sequence[float] = (0.0, 0.0, 0.0) 501 tint2_color: Sequence[float] = (0.0, 0.0, 0.0) 502 text: babase.Lstr | str = '' 503 texture: bascenev1.Texture | None = None 504 tint_texture: bascenev1.Texture | None = None 505 times: Sequence[int] = (1, 2, 3, 4, 5) 506 values: Sequence[float] = (1.0, 2.0, 3.0, 4.0) 507 offset: float = 0.0 508 input0: float = 0.0 509 input1: float = 0.0 510 input2: float = 0.0 511 input3: float = 0.0 512 flashing: bool = False 513 scale: float | Sequence[float] = 0.0 514 opacity: float = 0.0 515 loop: bool = False 516 time1: int = 0 517 time2: int = 0 518 timemax: int = 0 519 client_only: bool = False 520 materials: Sequence[bascenev1.Material] = () 521 roller_materials: Sequence[bascenev1.Material] = () 522 name: str = '' 523 punch_materials: Sequence[bascenev1.Material] = () 524 pickup_materials: Sequence[bascenev1.Material] = () 525 extras_material: Sequence[bascenev1.Material] = () 526 rotate: float = 0.0 527 hold_node: bascenev1.Node | None = None 528 hold_body: int = 0 529 host_only: bool = False 530 premultiplied: bool = False 531 source_player: bascenev1.Player | None = None 532 mesh_opaque: bascenev1.Mesh | None = None 533 mesh_transparent: bascenev1.Mesh | None = None 534 damage_smoothed: float = 0.0 535 gravity_scale: float = 1.0 536 punch_power: float = 0.0 537 punch_momentum_linear: Sequence[float] = (0.0, 0.0, 0.0) 538 punch_momentum_angular: float = 0.0 539 rate: int = 0 540 vr_depth: float = 0.0 541 is_area_of_interest: bool = False 542 jump_pressed: bool = False 543 pickup_pressed: bool = False 544 punch_pressed: bool = False 545 bomb_pressed: bool = False 546 fly_pressed: bool = False 547 hold_position_pressed: bool = False 548 knockout: float = 0.0 549 invincible: bool = False 550 stick_to_owner: bool = False 551 damage: int = 0 552 run: float = 0.0 553 move_up_down: float = 0.0 554 move_left_right: float = 0.0 555 curse_death_time: int = 0 556 boxing_gloves: bool = False 557 hockey: bool = False 558 use_fixed_vr_overlay: bool = False 559 allow_kick_idle_players: bool = False 560 music_continuous: bool = False 561 music_count: int = 0 562 hurt: float = 0.0 563 always_show_health_bar: bool = False 564 mini_billboard_1_texture: bascenev1.Texture | None = None 565 mini_billboard_1_start_time: int = 0 566 mini_billboard_1_end_time: int = 0 567 mini_billboard_2_texture: bascenev1.Texture | None = None 568 mini_billboard_2_start_time: int = 0 569 mini_billboard_2_end_time: int = 0 570 mini_billboard_3_texture: bascenev1.Texture | None = None 571 mini_billboard_3_start_time: int = 0 572 mini_billboard_3_end_time: int = 0 573 boxing_gloves_flashing: bool = False 574 dead: bool = False 575 floor_reflection: bool = False 576 debris_friction: float = 0.0 577 debris_kill_height: float = 0.0 578 vr_near_clip: float = 0.0 579 shadow_ortho: bool = False 580 happy_thoughts_mode: bool = False 581 shadow_offset: Sequence[float] = (0.0, 0.0) 582 paused: bool = False 583 time: int = 0 584 ambient_color: Sequence[float] = (1.0, 1.0, 1.0) 585 camera_mode: str = 'rotate' 586 frozen: bool = False 587 area_of_interest_bounds: Sequence[float] = (-1, -1, -1, 1, 1, 1) 588 shadow_range: Sequence[float] = (0, 0, 0, 0) 589 counter_text: str = '' 590 counter_texture: bascenev1.Texture | None = None 591 shattered: int = 0 592 billboard_texture: bascenev1.Texture | None = None 593 billboard_cross_out: bool = False 594 billboard_opacity: float = 0.0 595 slow_motion: bool = False 596 music: str = '' 597 vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0) 598 vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0) 599 vr_overlay_center_enabled: bool = False 600 vignette_outer: Sequence[float] = (0.0, 0.0) 601 vignette_inner: Sequence[float] = (0.0, 0.0) 602 tint: Sequence[float] = (1.0, 1.0, 1.0) 603 604 def __bool__(self) -> bool: 605 """Support for bool evaluation.""" 606 return bool(True) # Slight obfuscation. 607 608 def add_death_action(self, action: Callable[[], None]) -> None: 609 """Add a callable object to be called upon this node's death. 610 Note that these actions are run just after the node dies, not before. 611 """ 612 return None 613 614 def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None: 615 """Connect one of this node's attributes to an attribute on another 616 node. This will immediately set the target attribute's value to that 617 of the source attribute, and will continue to do so once per step 618 as long as the two nodes exist. The connection can be severed by 619 setting the target attribute to any value or connecting another 620 node attribute to it. 621 622 ##### Example 623 Create a locator and attach a light to it: 624 >>> light = bascenev1.newnode('light') 625 ... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)}) 626 ... loc.connectattr('position', light, 'position') 627 """ 628 return None 629 630 def delete(self, ignore_missing: bool = True) -> None: 631 """Delete the node. Ignores already-deleted nodes if `ignore_missing` 632 is True; otherwise a bascenev1.NodeNotFoundError is thrown. 633 """ 634 return None 635 636 def exists(self) -> bool: 637 """Returns whether the Node still exists. 638 Most functionality will fail on a nonexistent Node, so it's never a bad 639 idea to check this. 640 641 Note that you can also use the boolean operator for this same 642 functionality, so a statement such as "if mynode" will do 643 the right thing both for Node objects and values of None. 644 """ 645 return bool() 646 647 # Show that ur return type varies based on "doraise" value: 648 @overload 649 def getdelegate( 650 self, type: type[_T], doraise: Literal[False] = False 651 ) -> _T | None: ... 652 653 @overload 654 def getdelegate(self, type: type[_T], doraise: Literal[True]) -> _T: ... 655 656 def getdelegate(self, type: Any, doraise: bool = False) -> Any: 657 """Return the node's current delegate object if it matches 658 a certain type. 659 660 If the node has no delegate or it is not an instance of the passed 661 type, then None will be returned. If 'doraise' is True, then an 662 babase.DelegateNotFoundError will be raised instead. 663 """ 664 return None 665 666 def getname(self) -> str: 667 """Return the name assigned to a Node; used mainly for debugging""" 668 return str() 669 670 def getnodetype(self) -> str: 671 """Return the type of Node referenced by this object as a string. 672 (Note this is different from the Python type which is always 673 bascenev1.Node) 674 """ 675 return str() 676 677 def handlemessage(self, *args: Any) -> None: 678 """General message handling; can be passed any message object. 679 680 All standard message objects are forwarded along to the 681 bascenev1.Node's delegate for handling (generally the bascenev1.Actor 682 that made the node). 683 684 bascenev1.Node-s are unique, however, in that they can be passed a 685 second form of message; 'node-messages'. These consist of a string 686 type-name as a first argument along with the args specific to that type 687 name as additional arguments. 688 Node-messages communicate directly with the low-level node layer 689 and are delivered simultaneously on all game clients, 690 acting as an alternative to setting node attributes. 691 """ 692 return None
Reference to a Node; the low level building block of a game.
Category: Gameplay Classes
At its core, a game is nothing more than a scene of Nodes with attributes getting interconnected or set over time.
A bascenev1.Node instance should be thought of as a weak-reference to a game node; not the node itself. This means a Node's lifecycle is completely independent of how many Python references to it exist. To explicitly add a new node to the game, use bascenev1.newnode(), and to explicitly delete one, use bascenev1.Node.delete(). babase.Node.exists() can be used to determine if a Node still points to a live node in the game.
You can use ba.Node(None)
to instantiate an invalid
Node reference (sometimes used as attr values/etc).
608 def add_death_action(self, action: Callable[[], None]) -> None: 609 """Add a callable object to be called upon this node's death. 610 Note that these actions are run just after the node dies, not before. 611 """ 612 return None
Add a callable object to be called upon this node's death. Note that these actions are run just after the node dies, not before.
614 def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None: 615 """Connect one of this node's attributes to an attribute on another 616 node. This will immediately set the target attribute's value to that 617 of the source attribute, and will continue to do so once per step 618 as long as the two nodes exist. The connection can be severed by 619 setting the target attribute to any value or connecting another 620 node attribute to it. 621 622 ##### Example 623 Create a locator and attach a light to it: 624 >>> light = bascenev1.newnode('light') 625 ... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)}) 626 ... loc.connectattr('position', light, 'position') 627 """ 628 return None
Connect one of this node's attributes to an attribute on another node. This will immediately set the target attribute's value to that of the source attribute, and will continue to do so once per step as long as the two nodes exist. The connection can be severed by setting the target attribute to any value or connecting another node attribute to it.
Example
Create a locator and attach a light to it:
>>> light = bascenev1.newnode('light')
... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
... loc.connectattr('position', light, 'position')
630 def delete(self, ignore_missing: bool = True) -> None: 631 """Delete the node. Ignores already-deleted nodes if `ignore_missing` 632 is True; otherwise a bascenev1.NodeNotFoundError is thrown. 633 """ 634 return None
Delete the node. Ignores already-deleted nodes if ignore_missing
is True; otherwise a bascenev1.NodeNotFoundError is thrown.
636 def exists(self) -> bool: 637 """Returns whether the Node still exists. 638 Most functionality will fail on a nonexistent Node, so it's never a bad 639 idea to check this. 640 641 Note that you can also use the boolean operator for this same 642 functionality, so a statement such as "if mynode" will do 643 the right thing both for Node objects and values of None. 644 """ 645 return bool()
Returns whether the Node still exists. Most functionality will fail on a nonexistent Node, so it's never a bad idea to check this.
Note that you can also use the boolean operator for this same functionality, so a statement such as "if mynode" will do the right thing both for Node objects and values of None.
656 def getdelegate(self, type: Any, doraise: bool = False) -> Any: 657 """Return the node's current delegate object if it matches 658 a certain type. 659 660 If the node has no delegate or it is not an instance of the passed 661 type, then None will be returned. If 'doraise' is True, then an 662 babase.DelegateNotFoundError will be raised instead. 663 """ 664 return None
Return the node's current delegate object if it matches a certain type.
If the node has no delegate or it is not an instance of the passed type, then None will be returned. If 'doraise' is True, then an babase.DelegateNotFoundError will be raised instead.
666 def getname(self) -> str: 667 """Return the name assigned to a Node; used mainly for debugging""" 668 return str()
Return the name assigned to a Node; used mainly for debugging
670 def getnodetype(self) -> str: 671 """Return the type of Node referenced by this object as a string. 672 (Note this is different from the Python type which is always 673 bascenev1.Node) 674 """ 675 return str()
Return the type of Node referenced by this object as a string. (Note this is different from the Python type which is always bascenev1.Node)
677 def handlemessage(self, *args: Any) -> None: 678 """General message handling; can be passed any message object. 679 680 All standard message objects are forwarded along to the 681 bascenev1.Node's delegate for handling (generally the bascenev1.Actor 682 that made the node). 683 684 bascenev1.Node-s are unique, however, in that they can be passed a 685 second form of message; 'node-messages'. These consist of a string 686 type-name as a first argument along with the args specific to that type 687 name as additional arguments. 688 Node-messages communicate directly with the low-level node layer 689 and are delivered simultaneously on all game clients, 690 acting as an alternative to setting node attributes. 691 """ 692 return None
General message handling; can be passed any message object.
All standard message objects are forwarded along to the bascenev1.Node's delegate for handling (generally the bascenev1.Actor that made the node).
bascenev1.Node-s are unique, however, in that they can be passed a second form of message; 'node-messages'. These consist of a string type-name as a first argument along with the args specific to that type name as additional arguments. Node-messages communicate directly with the low-level node layer and are delivered simultaneously on all game clients, acting as an alternative to setting node attributes.
19class NodeActor(Actor): 20 """A simple bascenev1.Actor type that wraps a single bascenev1.Node. 21 22 Category: **Gameplay Classes** 23 24 This Actor will delete its Node when told to die, and it's 25 exists() call will return whether the Node still exists or not. 26 """ 27 28 def __init__(self, node: bascenev1.Node): 29 super().__init__() 30 self.node = node 31 32 @override 33 def handlemessage(self, msg: Any) -> Any: 34 if isinstance(msg, DieMessage): 35 if self.node: 36 self.node.delete() 37 return None 38 return super().handlemessage(msg) 39 40 @override 41 def exists(self) -> bool: 42 return bool(self.node)
A simple bascenev1.Actor type that wraps a single bascenev1.Node.
Category: Gameplay Classes
This Actor will delete its Node when told to die, and it's exists() call will return whether the Node still exists or not.
32 @override 33 def handlemessage(self, msg: Any) -> Any: 34 if isinstance(msg, DieMessage): 35 if self.node: 36 self.node.delete() 37 return None 38 return super().handlemessage(msg)
General message handling; can be passed any message object.
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.
75class NodeNotFoundError(NotFoundError): 76 """Exception raised when an expected Node does not exist. 77 78 Category: **Exception Classes** 79 """
Exception raised when an expected Node does not exist.
Category: Exception Classes
52def normalized_color(color: Sequence[float]) -> tuple[float, ...]: 53 """Scale a color so its largest value is 1; useful for coloring lights. 54 55 category: General Utility Functions 56 """ 57 color_biased = tuple(max(c, 0.01) for c in color) # account for black 58 mult = 1.0 / max(color_biased) 59 return tuple(c * mult for c in color_biased)
Scale a color so its largest value is 1; useful for coloring lights.
category: General Utility Functions
26class NotFoundError(Exception): 27 """Exception raised when a referenced object does not exist. 28 29 Category: **Exception Classes** 30 """
Exception raised when a referenced object does not exist.
Category: Exception Classes
30@dataclass 31class OutOfBoundsMessage: 32 """A message telling an object that it is out of bounds. 33 34 Category: Message Classes 35 """
A message telling an object that it is out of bounds.
Category: Message Classes
164@dataclass 165class PickedUpMessage: 166 """Tells an object that it has been picked up by something. 167 168 Category: **Message Classes** 169 """ 170 171 node: bascenev1.Node 172 """The bascenev1.Node doing the picking up."""
Tells an object that it has been picked up by something.
Category: Message Classes
145@dataclass 146class PickUpMessage: 147 """Tells an object that it has picked something up. 148 149 Category: **Message Classes** 150 """ 151 152 node: bascenev1.Node 153 """The bascenev1.Node that is getting picked up."""
Tells an object that it has picked something up.
Category: Message Classes
48class Player(Generic[TeamT]): 49 """A player in a specific bascenev1.Activity. 50 51 Category: Gameplay Classes 52 53 These correspond to bascenev1.SessionPlayer objects, but are associated 54 with a single bascenev1.Activity instance. This allows activities to 55 specify their own custom bascenev1.Player types. 56 """ 57 58 # These are instance attrs but we define them at the type level so 59 # their type annotations are introspectable (for docs generation). 60 character: str 61 62 actor: bascenev1.Actor | None 63 """The bascenev1.Actor associated with the player.""" 64 65 color: Sequence[float] 66 highlight: Sequence[float] 67 68 _team: TeamT 69 _sessionplayer: bascenev1.SessionPlayer 70 _nodeactor: bascenev1.NodeActor | None 71 _expired: bool 72 _postinited: bool 73 _customdata: dict 74 75 # NOTE: avoiding having any __init__() here since it seems to not 76 # get called by default if a dataclass inherits from us. 77 # This also lets us keep trivial player classes cleaner by skipping 78 # the super().__init__() line. 79 80 def postinit(self, sessionplayer: bascenev1.SessionPlayer) -> None: 81 """Wire up a newly created player. 82 83 (internal) 84 """ 85 from bascenev1._nodeactor import NodeActor 86 87 # Sanity check; if a dataclass is created that inherits from us, 88 # it will define an equality operator by default which will break 89 # internal game logic. So complain loudly if we find one. 90 if type(self).__eq__ is not object.__eq__: 91 raise RuntimeError( 92 f'Player class {type(self)} defines an equality' 93 f' operator (__eq__) which will break internal' 94 f' logic. Please remove it.\n' 95 f'For dataclasses you can do "dataclass(eq=False)"' 96 f' in the class decorator.' 97 ) 98 99 self.actor = None 100 self.character = '' 101 self._nodeactor: bascenev1.NodeActor | None = None 102 self._sessionplayer = sessionplayer 103 self.character = sessionplayer.character 104 self.color = sessionplayer.color 105 self.highlight = sessionplayer.highlight 106 self._team = cast(TeamT, sessionplayer.sessionteam.activityteam) 107 assert self._team is not None 108 self._customdata = {} 109 self._expired = False 110 self._postinited = True 111 node = _bascenev1.newnode( 112 'player', attrs={'playerID': sessionplayer.id} 113 ) 114 self._nodeactor = NodeActor(node) 115 sessionplayer.setnode(node) 116 117 def leave(self) -> None: 118 """Called when the Player leaves a running game. 119 120 (internal) 121 """ 122 assert self._postinited 123 assert not self._expired 124 try: 125 # If they still have an actor, kill it. 126 if self.actor: 127 self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME)) 128 self.actor = None 129 except Exception: 130 logging.exception('Error killing actor on leave for %s.', self) 131 self._nodeactor = None 132 del self._team 133 del self._customdata 134 135 def expire(self) -> None: 136 """Called when the Player is expiring (when its Activity does so). 137 138 (internal) 139 """ 140 assert self._postinited 141 assert not self._expired 142 self._expired = True 143 144 try: 145 self.on_expire() 146 except Exception: 147 logging.exception('Error in on_expire for %s.', self) 148 149 self._nodeactor = None 150 self.actor = None 151 del self._team 152 del self._customdata 153 154 def on_expire(self) -> None: 155 """Can be overridden to handle player expiration. 156 157 The player expires when the Activity it is a part of expires. 158 Expired players should no longer run any game logic (which will 159 likely error). They should, however, remove any references to 160 players/teams/games/etc. which could prevent them from being freed. 161 """ 162 163 @property 164 def team(self) -> TeamT: 165 """The bascenev1.Team for this player.""" 166 assert self._postinited 167 assert not self._expired 168 return self._team 169 170 @property 171 def customdata(self) -> dict: 172 """Arbitrary values associated with the player. 173 Though it is encouraged that most player values be properly defined 174 on the bascenev1.Player subclass, it may be useful for player-agnostic 175 objects to store values here. This dict is cleared when the player 176 leaves or expires so objects stored here will be disposed of at 177 the expected time, unlike the Player instance itself which may 178 continue to be referenced after it is no longer part of the game. 179 """ 180 assert self._postinited 181 assert not self._expired 182 return self._customdata 183 184 @property 185 def sessionplayer(self) -> bascenev1.SessionPlayer: 186 """Return the bascenev1.SessionPlayer corresponding to this Player. 187 188 Throws a bascenev1.SessionPlayerNotFoundError if it does not exist. 189 """ 190 assert self._postinited 191 if bool(self._sessionplayer): 192 return self._sessionplayer 193 raise babase.SessionPlayerNotFoundError() 194 195 @property 196 def node(self) -> bascenev1.Node: 197 """A bascenev1.Node of type 'player' associated with this Player. 198 199 This node can be used to get a generic player position/etc. 200 """ 201 assert self._postinited 202 assert not self._expired 203 assert self._nodeactor 204 return self._nodeactor.node 205 206 @property 207 def position(self) -> babase.Vec3: 208 """The position of the player, as defined by its bascenev1.Actor. 209 210 If the player currently has no actor, raises a 211 babase.ActorNotFoundError. 212 """ 213 assert self._postinited 214 assert not self._expired 215 if self.actor is None: 216 raise babase.ActorNotFoundError 217 return babase.Vec3(self.node.position) 218 219 def exists(self) -> bool: 220 """Whether the underlying player still exists. 221 222 This will return False if the underlying bascenev1.SessionPlayer has 223 left the game or if the bascenev1.Activity this player was 224 associated with has ended. 225 Most functionality will fail on a nonexistent player. 226 Note that you can also use the boolean operator for this same 227 functionality, so a statement such as "if player" will do 228 the right thing both for Player objects and values of None. 229 """ 230 assert self._postinited 231 return self._sessionplayer.exists() and not self._expired 232 233 def getname(self, full: bool = False, icon: bool = True) -> str: 234 """ 235 Returns the player's name. If icon is True, the long version of the 236 name may include an icon. 237 """ 238 assert self._postinited 239 assert not self._expired 240 return self._sessionplayer.getname(full=full, icon=icon) 241 242 def is_alive(self) -> bool: 243 """ 244 Returns True if the player has a bascenev1.Actor assigned and its 245 is_alive() method return True. False is returned otherwise. 246 """ 247 assert self._postinited 248 assert not self._expired 249 return self.actor is not None and self.actor.is_alive() 250 251 def get_icon(self) -> dict[str, Any]: 252 """ 253 Returns the character's icon (images, colors, etc contained in a dict) 254 """ 255 assert self._postinited 256 assert not self._expired 257 return self._sessionplayer.get_icon() 258 259 def assigninput( 260 self, 261 inputtype: babase.InputType | tuple[babase.InputType, ...], 262 call: Callable, 263 ) -> None: 264 """ 265 Set the python callable to be run for one or more types of input. 266 """ 267 assert self._postinited 268 assert not self._expired 269 return self._sessionplayer.assigninput(type=inputtype, call=call) 270 271 def resetinput(self) -> None: 272 """ 273 Clears out the player's assigned input actions. 274 """ 275 assert self._postinited 276 assert not self._expired 277 self._sessionplayer.resetinput() 278 279 def __bool__(self) -> bool: 280 return self.exists()
A player in a specific bascenev1.Activity.
Category: Gameplay Classes
These correspond to bascenev1.SessionPlayer objects, but are associated with a single bascenev1.Activity instance. This allows activities to specify their own custom bascenev1.Player types.
154 def on_expire(self) -> None: 155 """Can be overridden to handle player expiration. 156 157 The player expires when the Activity it is a part of expires. 158 Expired players should no longer run any game logic (which will 159 likely error). They should, however, remove any references to 160 players/teams/games/etc. which could prevent them from being freed. 161 """
Can be overridden to handle player expiration.
The player expires when the Activity it is a part of expires. Expired players should no longer run any game logic (which will likely error). They should, however, remove any references to players/teams/games/etc. which could prevent them from being freed.
163 @property 164 def team(self) -> TeamT: 165 """The bascenev1.Team for this player.""" 166 assert self._postinited 167 assert not self._expired 168 return self._team
The bascenev1.Team for this player.
170 @property 171 def customdata(self) -> dict: 172 """Arbitrary values associated with the player. 173 Though it is encouraged that most player values be properly defined 174 on the bascenev1.Player subclass, it may be useful for player-agnostic 175 objects to store values here. This dict is cleared when the player 176 leaves or expires so objects stored here will be disposed of at 177 the expected time, unlike the Player instance itself which may 178 continue to be referenced after it is no longer part of the game. 179 """ 180 assert self._postinited 181 assert not self._expired 182 return self._customdata
Arbitrary values associated with the player. Though it is encouraged that most player values be properly defined on the bascenev1.Player subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the player leaves or expires so objects stored here will be disposed of at the expected time, unlike the Player instance itself which may continue to be referenced after it is no longer part of the game.
184 @property 185 def sessionplayer(self) -> bascenev1.SessionPlayer: 186 """Return the bascenev1.SessionPlayer corresponding to this Player. 187 188 Throws a bascenev1.SessionPlayerNotFoundError if it does not exist. 189 """ 190 assert self._postinited 191 if bool(self._sessionplayer): 192 return self._sessionplayer 193 raise babase.SessionPlayerNotFoundError()
Return the bascenev1.SessionPlayer corresponding to this Player.
Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.
195 @property 196 def node(self) -> bascenev1.Node: 197 """A bascenev1.Node of type 'player' associated with this Player. 198 199 This node can be used to get a generic player position/etc. 200 """ 201 assert self._postinited 202 assert not self._expired 203 assert self._nodeactor 204 return self._nodeactor.node
A bascenev1.Node of type 'player' associated with this Player.
This node can be used to get a generic player position/etc.
206 @property 207 def position(self) -> babase.Vec3: 208 """The position of the player, as defined by its bascenev1.Actor. 209 210 If the player currently has no actor, raises a 211 babase.ActorNotFoundError. 212 """ 213 assert self._postinited 214 assert not self._expired 215 if self.actor is None: 216 raise babase.ActorNotFoundError 217 return babase.Vec3(self.node.position)
The position of the player, as defined by its bascenev1.Actor.
If the player currently has no actor, raises a babase.ActorNotFoundError.
219 def exists(self) -> bool: 220 """Whether the underlying player still exists. 221 222 This will return False if the underlying bascenev1.SessionPlayer has 223 left the game or if the bascenev1.Activity this player was 224 associated with has ended. 225 Most functionality will fail on a nonexistent player. 226 Note that you can also use the boolean operator for this same 227 functionality, so a statement such as "if player" will do 228 the right thing both for Player objects and values of None. 229 """ 230 assert self._postinited 231 return self._sessionplayer.exists() and not self._expired
Whether the underlying player still exists.
This will return False if the underlying bascenev1.SessionPlayer has left the game or if the bascenev1.Activity this player was associated with has ended. Most functionality will fail on a nonexistent player. Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do the right thing both for Player objects and values of None.
233 def getname(self, full: bool = False, icon: bool = True) -> str: 234 """ 235 Returns the player's name. If icon is True, the long version of the 236 name may include an icon. 237 """ 238 assert self._postinited 239 assert not self._expired 240 return self._sessionplayer.getname(full=full, icon=icon)
Returns the player's name. If icon is True, the long version of the name may include an icon.
242 def is_alive(self) -> bool: 243 """ 244 Returns True if the player has a bascenev1.Actor assigned and its 245 is_alive() method return True. False is returned otherwise. 246 """ 247 assert self._postinited 248 assert not self._expired 249 return self.actor is not None and self.actor.is_alive()
Returns True if the player has a bascenev1.Actor assigned and its is_alive() method return True. False is returned otherwise.
251 def get_icon(self) -> dict[str, Any]: 252 """ 253 Returns the character's icon (images, colors, etc contained in a dict) 254 """ 255 assert self._postinited 256 assert not self._expired 257 return self._sessionplayer.get_icon()
Returns the character's icon (images, colors, etc contained in a dict)
259 def assigninput( 260 self, 261 inputtype: babase.InputType | tuple[babase.InputType, ...], 262 call: Callable, 263 ) -> None: 264 """ 265 Set the python callable to be run for one or more types of input. 266 """ 267 assert self._postinited 268 assert not self._expired 269 return self._sessionplayer.assigninput(type=inputtype, call=call)
Set the python callable to be run for one or more types of input.
74class PlayerDiedMessage: 75 """A message saying a bascenev1.Player has died. 76 77 Category: **Message Classes** 78 """ 79 80 killed: bool 81 """If True, the player was killed; 82 If False, they left the game or the round ended.""" 83 84 how: DeathType 85 """The particular type of death.""" 86 87 def __init__( 88 self, 89 player: bascenev1.Player, 90 was_killed: bool, 91 killerplayer: bascenev1.Player | None, 92 how: DeathType, 93 ): 94 """Instantiate a message with the given values.""" 95 96 # Invalid refs should never be passed as args. 97 assert player.exists() 98 self._player = player 99 100 # Invalid refs should never be passed as args. 101 assert killerplayer is None or killerplayer.exists() 102 self._killerplayer = killerplayer 103 self.killed = was_killed 104 self.how = how 105 106 def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None: 107 """Return the bascenev1.Player responsible for the killing, if any. 108 109 Pass the Player type being used by the current game. 110 """ 111 assert isinstance(self._killerplayer, (playertype, type(None))) 112 return self._killerplayer 113 114 def getplayer(self, playertype: type[PlayerT]) -> PlayerT: 115 """Return the bascenev1.Player that died. 116 117 The type of player for the current activity should be passed so that 118 the type-checker properly identifies the returned value as one. 119 """ 120 player: Any = self._player 121 assert isinstance(player, playertype) 122 123 # We should never be delivering invalid refs. 124 # (could theoretically happen if someone holds on to us) 125 assert player.exists() 126 return player
A message saying a bascenev1.Player has died.
Category: Message Classes
87 def __init__( 88 self, 89 player: bascenev1.Player, 90 was_killed: bool, 91 killerplayer: bascenev1.Player | None, 92 how: DeathType, 93 ): 94 """Instantiate a message with the given values.""" 95 96 # Invalid refs should never be passed as args. 97 assert player.exists() 98 self._player = player 99 100 # Invalid refs should never be passed as args. 101 assert killerplayer is None or killerplayer.exists() 102 self._killerplayer = killerplayer 103 self.killed = was_killed 104 self.how = how
Instantiate a message with the given values.
106 def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None: 107 """Return the bascenev1.Player responsible for the killing, if any. 108 109 Pass the Player type being used by the current game. 110 """ 111 assert isinstance(self._killerplayer, (playertype, type(None))) 112 return self._killerplayer
Return the bascenev1.Player responsible for the killing, if any.
Pass the Player type being used by the current game.
114 def getplayer(self, playertype: type[PlayerT]) -> PlayerT: 115 """Return the bascenev1.Player that died. 116 117 The type of player for the current activity should be passed so that 118 the type-checker properly identifies the returned value as one. 119 """ 120 player: Any = self._player 121 assert isinstance(player, playertype) 122 123 # We should never be delivering invalid refs. 124 # (could theoretically happen if someone holds on to us) 125 assert player.exists() 126 return player
Return the bascenev1.Player that died.
The type of player for the current activity should be passed so that the type-checker properly identifies the returned value as one.
292@dataclass 293class PlayerProfilesChangedMessage: 294 """Signals player profiles may have changed and should be reloaded."""
Signals player profiles may have changed and should be reloaded.
26@dataclass 27class PlayerInfo: 28 """Holds basic info about a player. 29 30 Category: Gameplay Classes 31 """ 32 33 name: str 34 character: str
Holds basic info about a player.
Category: Gameplay Classes
33class PlayerNotFoundError(NotFoundError): 34 """Exception raised when an expected player does not exist. 35 36 Category: **Exception Classes** 37 """
Exception raised when an expected player does not exist.
Category: Exception Classes
35class PlayerRecord: 36 """Stats for an individual player in a bascenev1.Stats object. 37 38 Category: **Gameplay Classes** 39 40 This does not necessarily correspond to a bascenev1.Player that is 41 still present (stats may be retained for players that leave 42 mid-game) 43 """ 44 45 character: str 46 47 def __init__( 48 self, 49 name: str, 50 name_full: str, 51 sessionplayer: bascenev1.SessionPlayer, 52 stats: bascenev1.Stats, 53 ): 54 self.name = name 55 self.name_full = name_full 56 self.score = 0 57 self.accumscore = 0 58 self.kill_count = 0 59 self.accum_kill_count = 0 60 self.killed_count = 0 61 self.accum_killed_count = 0 62 self._multi_kill_timer: bascenev1.Timer | None = None 63 self._multi_kill_count = 0 64 self._stats = weakref.ref(stats) 65 self._last_sessionplayer: bascenev1.SessionPlayer | None = None 66 self._sessionplayer: bascenev1.SessionPlayer | None = None 67 self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None 68 self.streak = 0 69 self.associate_with_sessionplayer(sessionplayer) 70 71 @property 72 def team(self) -> bascenev1.SessionTeam: 73 """The bascenev1.SessionTeam the last associated player was last on. 74 75 This can still return a valid result even if the player is gone. 76 Raises a bascenev1.SessionTeamNotFoundError if the team no longer 77 exists. 78 """ 79 assert self._sessionteam is not None 80 team = self._sessionteam() 81 if team is None: 82 raise babase.SessionTeamNotFoundError() 83 return team 84 85 @property 86 def player(self) -> bascenev1.SessionPlayer: 87 """Return the instance's associated bascenev1.SessionPlayer. 88 89 Raises a bascenev1.SessionPlayerNotFoundError if the player 90 no longer exists. 91 """ 92 if not self._sessionplayer: 93 raise babase.SessionPlayerNotFoundError() 94 return self._sessionplayer 95 96 def getname(self, full: bool = False) -> str: 97 """Return the player entry's name.""" 98 return self.name_full if full else self.name 99 100 def get_icon(self) -> dict[str, Any]: 101 """Get the icon for this instance's player.""" 102 player = self._last_sessionplayer 103 assert player is not None 104 return player.get_icon() 105 106 def cancel_multi_kill_timer(self) -> None: 107 """Cancel any multi-kill timer for this player entry.""" 108 self._multi_kill_timer = None 109 110 def getactivity(self) -> bascenev1.Activity | None: 111 """Return the bascenev1.Activity this instance is associated with. 112 113 Returns None if the activity no longer exists.""" 114 stats = self._stats() 115 if stats is not None: 116 return stats.getactivity() 117 return None 118 119 def associate_with_sessionplayer( 120 self, sessionplayer: bascenev1.SessionPlayer 121 ) -> None: 122 """Associate this entry with a bascenev1.SessionPlayer.""" 123 self._sessionteam = weakref.ref(sessionplayer.sessionteam) 124 self.character = sessionplayer.character 125 self._last_sessionplayer = sessionplayer 126 self._sessionplayer = sessionplayer 127 self.streak = 0 128 129 def _end_multi_kill(self) -> None: 130 self._multi_kill_timer = None 131 self._multi_kill_count = 0 132 133 def get_last_sessionplayer(self) -> bascenev1.SessionPlayer: 134 """Return the last bascenev1.Player we were associated with.""" 135 assert self._last_sessionplayer is not None 136 return self._last_sessionplayer 137 138 def submit_kill(self, showpoints: bool = True) -> None: 139 """Submit a kill for this player entry.""" 140 # FIXME Clean this up. 141 # pylint: disable=too-many-statements 142 143 self._multi_kill_count += 1 144 stats = self._stats() 145 assert stats 146 if self._multi_kill_count == 1: 147 score = 0 148 name = None 149 delay = 0.0 150 color = (0.0, 0.0, 0.0, 1.0) 151 scale = 1.0 152 sound = None 153 elif self._multi_kill_count == 2: 154 score = 20 155 name = babase.Lstr(resource='twoKillText') 156 color = (0.1, 1.0, 0.0, 1) 157 scale = 1.0 158 delay = 0.0 159 sound = stats.orchestrahitsound1 160 elif self._multi_kill_count == 3: 161 score = 40 162 name = babase.Lstr(resource='threeKillText') 163 color = (1.0, 0.7, 0.0, 1) 164 scale = 1.1 165 delay = 0.3 166 sound = stats.orchestrahitsound2 167 elif self._multi_kill_count == 4: 168 score = 60 169 name = babase.Lstr(resource='fourKillText') 170 color = (1.0, 1.0, 0.0, 1) 171 scale = 1.2 172 delay = 0.6 173 sound = stats.orchestrahitsound3 174 elif self._multi_kill_count == 5: 175 score = 80 176 name = babase.Lstr(resource='fiveKillText') 177 color = (1.0, 0.5, 0.0, 1) 178 scale = 1.3 179 delay = 0.9 180 sound = stats.orchestrahitsound4 181 else: 182 score = 100 183 name = babase.Lstr( 184 resource='multiKillText', 185 subs=[('${COUNT}', str(self._multi_kill_count))], 186 ) 187 color = (1.0, 0.5, 0.0, 1) 188 scale = 1.3 189 delay = 1.0 190 sound = stats.orchestrahitsound4 191 192 def _apply( 193 name2: babase.Lstr, 194 score2: int, 195 showpoints2: bool, 196 color2: tuple[float, float, float, float], 197 scale2: float, 198 sound2: bascenev1.Sound | None, 199 ) -> None: 200 # pylint: disable=too-many-positional-arguments 201 from bascenev1lib.actor.popuptext import PopupText 202 203 # Only award this if they're still alive and we can get 204 # a current position for them. 205 our_pos: babase.Vec3 | None = None 206 if self._sessionplayer: 207 if self._sessionplayer.activityplayer is not None: 208 try: 209 our_pos = self._sessionplayer.activityplayer.position 210 except babase.NotFoundError: 211 pass 212 if our_pos is None: 213 return 214 215 # Jitter position a bit since these often come in clusters. 216 our_pos = babase.Vec3( 217 our_pos[0] + (random.random() - 0.5) * 2.0, 218 our_pos[1] + (random.random() - 0.5) * 2.0, 219 our_pos[2] + (random.random() - 0.5) * 2.0, 220 ) 221 activity = self.getactivity() 222 if activity is not None: 223 PopupText( 224 babase.Lstr( 225 value=(('+' + str(score2) + ' ') if showpoints2 else '') 226 + '${N}', 227 subs=[('${N}', name2)], 228 ), 229 color=color2, 230 scale=scale2, 231 position=our_pos, 232 ).autoretain() 233 if sound2: 234 sound2.play() 235 236 self.score += score2 237 self.accumscore += score2 238 239 # Inform a running game of the score. 240 if score2 != 0 and activity is not None: 241 activity.handlemessage(PlayerScoredMessage(score=score2)) 242 243 if name is not None: 244 _bascenev1.timer( 245 0.3 + delay, 246 babase.Call( 247 _apply, name, score, showpoints, color, scale, sound 248 ), 249 ) 250 251 # Keep the tally rollin'... 252 # set a timer for a bit in the future. 253 self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)
Stats for an individual player in a bascenev1.Stats object.
Category: Gameplay Classes
This does not necessarily correspond to a bascenev1.Player that is still present (stats may be retained for players that leave mid-game)
47 def __init__( 48 self, 49 name: str, 50 name_full: str, 51 sessionplayer: bascenev1.SessionPlayer, 52 stats: bascenev1.Stats, 53 ): 54 self.name = name 55 self.name_full = name_full 56 self.score = 0 57 self.accumscore = 0 58 self.kill_count = 0 59 self.accum_kill_count = 0 60 self.killed_count = 0 61 self.accum_killed_count = 0 62 self._multi_kill_timer: bascenev1.Timer | None = None 63 self._multi_kill_count = 0 64 self._stats = weakref.ref(stats) 65 self._last_sessionplayer: bascenev1.SessionPlayer | None = None 66 self._sessionplayer: bascenev1.SessionPlayer | None = None 67 self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None 68 self.streak = 0 69 self.associate_with_sessionplayer(sessionplayer)
71 @property 72 def team(self) -> bascenev1.SessionTeam: 73 """The bascenev1.SessionTeam the last associated player was last on. 74 75 This can still return a valid result even if the player is gone. 76 Raises a bascenev1.SessionTeamNotFoundError if the team no longer 77 exists. 78 """ 79 assert self._sessionteam is not None 80 team = self._sessionteam() 81 if team is None: 82 raise babase.SessionTeamNotFoundError() 83 return team
The bascenev1.SessionTeam the last associated player was last on.
This can still return a valid result even if the player is gone. Raises a bascenev1.SessionTeamNotFoundError if the team no longer exists.
85 @property 86 def player(self) -> bascenev1.SessionPlayer: 87 """Return the instance's associated bascenev1.SessionPlayer. 88 89 Raises a bascenev1.SessionPlayerNotFoundError if the player 90 no longer exists. 91 """ 92 if not self._sessionplayer: 93 raise babase.SessionPlayerNotFoundError() 94 return self._sessionplayer
Return the instance's associated bascenev1.SessionPlayer.
Raises a bascenev1.SessionPlayerNotFoundError if the player no longer exists.
96 def getname(self, full: bool = False) -> str: 97 """Return the player entry's name.""" 98 return self.name_full if full else self.name
Return the player entry's name.
100 def get_icon(self) -> dict[str, Any]: 101 """Get the icon for this instance's player.""" 102 player = self._last_sessionplayer 103 assert player is not None 104 return player.get_icon()
Get the icon for this instance's player.
106 def cancel_multi_kill_timer(self) -> None: 107 """Cancel any multi-kill timer for this player entry.""" 108 self._multi_kill_timer = None
Cancel any multi-kill timer for this player entry.
110 def getactivity(self) -> bascenev1.Activity | None: 111 """Return the bascenev1.Activity this instance is associated with. 112 113 Returns None if the activity no longer exists.""" 114 stats = self._stats() 115 if stats is not None: 116 return stats.getactivity() 117 return None
Return the bascenev1.Activity this instance is associated with.
Returns None if the activity no longer exists.
119 def associate_with_sessionplayer( 120 self, sessionplayer: bascenev1.SessionPlayer 121 ) -> None: 122 """Associate this entry with a bascenev1.SessionPlayer.""" 123 self._sessionteam = weakref.ref(sessionplayer.sessionteam) 124 self.character = sessionplayer.character 125 self._last_sessionplayer = sessionplayer 126 self._sessionplayer = sessionplayer 127 self.streak = 0
Associate this entry with a bascenev1.SessionPlayer.
133 def get_last_sessionplayer(self) -> bascenev1.SessionPlayer: 134 """Return the last bascenev1.Player we were associated with.""" 135 assert self._last_sessionplayer is not None 136 return self._last_sessionplayer
Return the last bascenev1.Player we were associated with.
138 def submit_kill(self, showpoints: bool = True) -> None: 139 """Submit a kill for this player entry.""" 140 # FIXME Clean this up. 141 # pylint: disable=too-many-statements 142 143 self._multi_kill_count += 1 144 stats = self._stats() 145 assert stats 146 if self._multi_kill_count == 1: 147 score = 0 148 name = None 149 delay = 0.0 150 color = (0.0, 0.0, 0.0, 1.0) 151 scale = 1.0 152 sound = None 153 elif self._multi_kill_count == 2: 154 score = 20 155 name = babase.Lstr(resource='twoKillText') 156 color = (0.1, 1.0, 0.0, 1) 157 scale = 1.0 158 delay = 0.0 159 sound = stats.orchestrahitsound1 160 elif self._multi_kill_count == 3: 161 score = 40 162 name = babase.Lstr(resource='threeKillText') 163 color = (1.0, 0.7, 0.0, 1) 164 scale = 1.1 165 delay = 0.3 166 sound = stats.orchestrahitsound2 167 elif self._multi_kill_count == 4: 168 score = 60 169 name = babase.Lstr(resource='fourKillText') 170 color = (1.0, 1.0, 0.0, 1) 171 scale = 1.2 172 delay = 0.6 173 sound = stats.orchestrahitsound3 174 elif self._multi_kill_count == 5: 175 score = 80 176 name = babase.Lstr(resource='fiveKillText') 177 color = (1.0, 0.5, 0.0, 1) 178 scale = 1.3 179 delay = 0.9 180 sound = stats.orchestrahitsound4 181 else: 182 score = 100 183 name = babase.Lstr( 184 resource='multiKillText', 185 subs=[('${COUNT}', str(self._multi_kill_count))], 186 ) 187 color = (1.0, 0.5, 0.0, 1) 188 scale = 1.3 189 delay = 1.0 190 sound = stats.orchestrahitsound4 191 192 def _apply( 193 name2: babase.Lstr, 194 score2: int, 195 showpoints2: bool, 196 color2: tuple[float, float, float, float], 197 scale2: float, 198 sound2: bascenev1.Sound | None, 199 ) -> None: 200 # pylint: disable=too-many-positional-arguments 201 from bascenev1lib.actor.popuptext import PopupText 202 203 # Only award this if they're still alive and we can get 204 # a current position for them. 205 our_pos: babase.Vec3 | None = None 206 if self._sessionplayer: 207 if self._sessionplayer.activityplayer is not None: 208 try: 209 our_pos = self._sessionplayer.activityplayer.position 210 except babase.NotFoundError: 211 pass 212 if our_pos is None: 213 return 214 215 # Jitter position a bit since these often come in clusters. 216 our_pos = babase.Vec3( 217 our_pos[0] + (random.random() - 0.5) * 2.0, 218 our_pos[1] + (random.random() - 0.5) * 2.0, 219 our_pos[2] + (random.random() - 0.5) * 2.0, 220 ) 221 activity = self.getactivity() 222 if activity is not None: 223 PopupText( 224 babase.Lstr( 225 value=(('+' + str(score2) + ' ') if showpoints2 else '') 226 + '${N}', 227 subs=[('${N}', name2)], 228 ), 229 color=color2, 230 scale=scale2, 231 position=our_pos, 232 ).autoretain() 233 if sound2: 234 sound2.play() 235 236 self.score += score2 237 self.accumscore += score2 238 239 # Inform a running game of the score. 240 if score2 != 0 and activity is not None: 241 activity.handlemessage(PlayerScoredMessage(score=score2)) 242 243 if name is not None: 244 _bascenev1.timer( 245 0.3 + delay, 246 babase.Call( 247 _apply, name, score, showpoints, color, scale, sound 248 ), 249 ) 250 251 # Keep the tally rollin'... 252 # set a timer for a bit in the future. 253 self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)
Submit a kill for this player entry.
24@dataclass 25class PlayerScoredMessage: 26 """Informs something that a bascenev1.Player scored. 27 28 Category: **Message Classes** 29 """ 30 31 score: int 32 """The score value."""
Informs something that a bascenev1.Player scored.
Category: Message Classes
322class Plugin: 323 """A plugin to alter app behavior in some way. 324 325 Category: **App Classes** 326 327 Plugins are discoverable by the meta-tag system 328 and the user can select which ones they want to enable. 329 Enabled plugins are then called at specific times as the 330 app is running in order to modify its behavior in some way. 331 """ 332 333 def on_app_running(self) -> None: 334 """Called when the app reaches the running state.""" 335 336 def on_app_suspend(self) -> None: 337 """Called when the app enters the suspended state.""" 338 339 def on_app_unsuspend(self) -> None: 340 """Called when the app exits the suspended state.""" 341 342 def on_app_shutdown(self) -> None: 343 """Called when the app is beginning the shutdown process.""" 344 345 def on_app_shutdown_complete(self) -> None: 346 """Called when the app has completed the shutdown process.""" 347 348 def has_settings_ui(self) -> bool: 349 """Called to ask if we have settings UI we can show.""" 350 return False 351 352 def show_settings_ui(self, source_widget: Any | None) -> None: 353 """Called to show our settings UI."""
A plugin to alter app behavior in some way.
Category: App Classes
Plugins are discoverable by the meta-tag system and the user can select which ones they want to enable. Enabled plugins are then called at specific times as the app is running in order to modify its behavior in some way.
342 def on_app_shutdown(self) -> None: 343 """Called when the app is beginning the shutdown process."""
Called when the app is beginning the shutdown process.
345 def on_app_shutdown_complete(self) -> None: 346 """Called when the app has completed the shutdown process."""
Called when the app has completed the shutdown process.
38@dataclass 39class PowerupAcceptMessage: 40 """A message informing a bascenev1.Powerup that it was accepted. 41 42 Category: **Message Classes** 43 44 This is generally sent in response to a bascenev1.PowerupMessage 45 to inform the box (or whoever granted it) that it can go away. 46 """
A message informing a bascenev1.Powerup that it was accepted.
Category: Message Classes
This is generally sent in response to a bascenev1.PowerupMessage to inform the box (or whoever granted it) that it can go away.
17@dataclass 18class PowerupMessage: 19 """A message telling an object to accept a powerup. 20 21 Category: **Message Classes** 22 23 This message is normally received by touching a bascenev1.PowerupBox. 24 """ 25 26 poweruptype: str 27 """The type of powerup to be granted (a string). 28 See bascenev1.Powerup.poweruptype for available type values.""" 29 30 sourcenode: bascenev1.Node | None = None 31 """The node the powerup game from, or None otherwise. 32 If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be 33 sent back to the sourcenode to inform it of the fact. This will 34 generally cause the powerup box to make a sound and disappear or 35 whatnot."""
A message telling an object to accept a powerup.
Category: Message Classes
This message is normally received by touching a bascenev1.PowerupBox.
The type of powerup to be granted (a string). See bascenev1.Powerup.poweruptype for available type values.
The node the powerup game from, or None otherwise. If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be sent back to the sourcenode to inform it of the fact. This will generally cause the powerup box to make a sound and disappear or whatnot.
17def print_live_object_warnings( 18 when: Any, 19 ignore_session: bascenev1.Session | None = None, 20 ignore_activity: bascenev1.Activity | None = None, 21) -> None: 22 """Print warnings for remaining objects in the current context. 23 24 IMPORTANT - don't call this in production; usage of gc.get_objects() 25 can bork Python. See notes at top of efro.debug module. 26 """ 27 # pylint: disable=cyclic-import 28 import gc 29 30 from bascenev1._session import Session 31 from bascenev1._actor import Actor 32 from bascenev1._activity import Activity 33 34 assert babase.app.classic is not None 35 36 sessions: list[bascenev1.Session] = [] 37 activities: list[bascenev1.Activity] = [] 38 actors: list[bascenev1.Actor] = [] 39 40 # Once we come across leaked stuff, printing again is probably 41 # redundant. 42 if babase.app.classic.printed_live_object_warning: 43 return 44 for obj in gc.get_objects(): 45 if isinstance(obj, Actor): 46 actors.append(obj) 47 elif isinstance(obj, Session): 48 sessions.append(obj) 49 elif isinstance(obj, Activity): 50 activities.append(obj) 51 52 # Complain about any remaining sessions. 53 for session in sessions: 54 if session is ignore_session: 55 continue 56 babase.app.classic.printed_live_object_warning = True 57 print(f'ERROR: Session found {when}: {session}') 58 59 # Complain about any remaining activities. 60 for activity in activities: 61 if activity is ignore_activity: 62 continue 63 babase.app.classic.printed_live_object_warning = True 64 print(f'ERROR: Activity found {when}: {activity}') 65 66 # Complain about any remaining actors. 67 for actor in actors: 68 babase.app.classic.printed_live_object_warning = True 69 print(f'ERROR: Actor found {when}: {actor}')
Print warnings for remaining objects in the current context.
IMPORTANT - don't call this in production; usage of gc.get_objects() can bork Python. See notes at top of efro.debug module.
1569def printnodes() -> None: 1570 """Print various info about existing nodes; useful for debugging. 1571 1572 Category: **Gameplay Functions** 1573 """ 1574 return None
Print various info about existing nodes; useful for debugging.
Category: Gameplay Functions
1413def pushcall( 1414 call: Callable, 1415 from_other_thread: bool = False, 1416 suppress_other_thread_warning: bool = False, 1417 other_thread_use_fg_context: bool = False, 1418 raw: bool = False, 1419) -> None: 1420 """Push a call to the logic event-loop. 1421 Category: **General Utility Functions** 1422 1423 This call expects to be used in the logic thread, and will automatically 1424 save and restore the babase.Context to behave seamlessly. 1425 1426 If you want to push a call from outside of the logic thread, 1427 however, you can pass 'from_other_thread' as True. In this case 1428 the call will always run in the UI context_ref on the logic thread 1429 or whichever context_ref is in the foreground if 1430 other_thread_use_fg_context is True. 1431 Passing raw=True will disable thread checks and context_ref sets/restores. 1432 """ 1433 return None
Push a call to the logic event-loop. Category: General Utility Functions
This call expects to be used in the logic thread, and will automatically save and restore the babase.Context to behave seamlessly.
If you want to push a call from outside of the logic thread, however, you can pass 'from_other_thread' as True. In this case the call will always run in the UI context_ref on the logic thread or whichever context_ref is in the foreground if other_thread_use_fg_context is True. Passing raw=True will disable thread checks and context_ref sets/restores.
370def register_map(maptype: type[Map]) -> None: 371 """Register a map class with the game.""" 372 assert babase.app.classic is not None 373 if maptype.name in babase.app.classic.maps: 374 raise RuntimeError(f'Map "{maptype.name}" is already registered.') 375 babase.app.classic.maps[maptype.name] = maptype
Register a map class with the game.
1487def safecolor( 1488 color: Sequence[float], target_intensity: float = 0.6 1489) -> tuple[float, ...]: 1490 """Given a color tuple, return a color safe to display as text. 1491 1492 Category: **General Utility Functions** 1493 1494 Accepts tuples of length 3 or 4. This will slightly brighten very 1495 dark colors, etc. 1496 """ 1497 return (0.0, 0.0, 0.0)
Given a color tuple, return a color safe to display as text.
Category: General Utility Functions
Accepts tuples of length 3 or 4. This will slightly brighten very dark colors, etc.
1500def screenmessage( 1501 message: str | babase.Lstr, 1502 color: Sequence[float] | None = None, 1503 log: bool = False, 1504) -> None: 1505 """Print a message to the local client's screen, in a given color. 1506 1507 Category: **General Utility Functions** 1508 1509 Note that this version of the function is purely for local display. 1510 To broadcast screen messages in network play, look for methods such as 1511 broadcastmessage() provided by the scene-version packages. 1512 """ 1513 return None
Print a message to the local client's screen, in a given color.
Category: General Utility Functions
Note that this version of the function is purely for local display. To broadcast screen messages in network play, look for methods such as broadcastmessage() provided by the scene-version packages.
28@dataclass 29class ScoreConfig: 30 """Settings for how a game handles scores. 31 32 Category: **Gameplay Classes** 33 """ 34 35 label: str = 'Score' 36 """A label show to the user for scores; 'Score', 'Time Survived', etc.""" 37 38 scoretype: bascenev1.ScoreType = ScoreType.POINTS 39 """How the score value should be displayed.""" 40 41 lower_is_better: bool = False 42 """Whether lower scores are preferable. Higher scores are by default.""" 43 44 none_is_winner: bool = False 45 """Whether a value of None is considered better than other scores. 46 By default it is not.""" 47 48 version: str = '' 49 """To change high-score lists used by a game without renaming the game, 50 change this. Defaults to an empty string."""
Settings for how a game handles scores.
Category: Gameplay Classes
134class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]): 135 """A standard score screen that fades in and shows stuff for a while. 136 137 After a specified delay, player input is assigned to end the activity. 138 """ 139 140 transition_time = 0.5 141 inherits_tint = True 142 inherits_vr_camera_offset = True 143 use_fixed_vr_overlay = True 144 145 default_music: MusicType | None = MusicType.SCORES 146 147 def __init__(self, settings: dict): 148 super().__init__(settings) 149 self._birth_time = babase.apptime() 150 self._min_view_time = 5.0 151 self._allow_server_transition = False 152 self._background: bascenev1.Actor | None = None 153 self._tips_text: bascenev1.Actor | None = None 154 self._kicked_off_server_shutdown = False 155 self._kicked_off_server_restart = False 156 self._default_show_tips = True 157 self._custom_continue_message: babase.Lstr | None = None 158 self._server_transitioning: bool | None = None 159 160 @override 161 def on_player_join(self, player: EmptyPlayer) -> None: 162 super().on_player_join(player) 163 time_till_assign = max( 164 0, self._birth_time + self._min_view_time - babase.apptime() 165 ) 166 167 # If we're still kicking at the end of our assign-delay, assign this 168 # guy's input to trigger us. 169 _bascenev1.timer( 170 time_till_assign, babase.WeakCall(self._safe_assign, player) 171 ) 172 173 @override 174 def on_transition_in(self) -> None: 175 from bascenev1lib.actor.tipstext import TipsText 176 from bascenev1lib.actor.background import Background 177 178 super().on_transition_in() 179 self._background = Background( 180 fade_time=0.5, start_faded=False, show_logo=True 181 ) 182 if self._default_show_tips: 183 self._tips_text = TipsText() 184 setmusic(self.default_music) 185 186 @override 187 def on_begin(self) -> None: 188 # pylint: disable=cyclic-import 189 from bascenev1lib.actor.text import Text 190 191 super().on_begin() 192 193 # Pop up a 'press any button to continue' statement after our 194 # min-view-time show a 'press any button to continue..' 195 # thing after a bit. 196 assert babase.app.classic is not None 197 if babase.app.ui_v1.uiscale is babase.UIScale.LARGE: 198 # FIXME: Need a better way to determine whether we've probably 199 # got a keyboard. 200 sval = babase.Lstr(resource='pressAnyKeyButtonText') 201 else: 202 sval = babase.Lstr(resource='pressAnyButtonText') 203 204 Text( 205 ( 206 self._custom_continue_message 207 if self._custom_continue_message is not None 208 else sval 209 ), 210 v_attach=Text.VAttach.BOTTOM, 211 h_align=Text.HAlign.CENTER, 212 flash=True, 213 vr_depth=50, 214 position=(0, 10), 215 scale=0.8, 216 color=(0.5, 0.7, 0.5, 0.5), 217 transition=Text.Transition.IN_BOTTOM_SLOW, 218 transition_delay=self._min_view_time, 219 ).autoretain() 220 221 def _player_press(self) -> None: 222 # If this activity is a good 'end point', ask server-mode just once if 223 # it wants to do anything special like switch sessions or kill the app. 224 if ( 225 self._allow_server_transition 226 and babase.app.classic is not None 227 and babase.app.classic.server is not None 228 and self._server_transitioning is None 229 ): 230 self._server_transitioning = ( 231 babase.app.classic.server.handle_transition() 232 ) 233 assert isinstance(self._server_transitioning, bool) 234 235 # If server-mode is handling this, don't do anything ourself. 236 if self._server_transitioning is True: 237 return 238 239 # Otherwise end the activity normally. 240 self.end() 241 242 def _safe_assign(self, player: EmptyPlayer) -> None: 243 # Just to be extra careful, don't assign if we're transitioning out. 244 # (though theoretically that should be ok). 245 if not self.is_transitioning_out() and player: 246 player.assigninput( 247 ( 248 babase.InputType.JUMP_PRESS, 249 babase.InputType.PUNCH_PRESS, 250 babase.InputType.BOMB_PRESS, 251 babase.InputType.PICK_UP_PRESS, 252 ), 253 self._player_press, 254 )
A standard score screen that fades in and shows stuff for a while.
After a specified delay, player input is assigned to end the activity.
147 def __init__(self, settings: dict): 148 super().__init__(settings) 149 self._birth_time = babase.apptime() 150 self._min_view_time = 5.0 151 self._allow_server_transition = False 152 self._background: bascenev1.Actor | None = None 153 self._tips_text: bascenev1.Actor | None = None 154 self._kicked_off_server_shutdown = False 155 self._kicked_off_server_restart = False 156 self._default_show_tips = True 157 self._custom_continue_message: babase.Lstr | None = None 158 self._server_transitioning: bool | None = None
Creates an Activity in the current bascenev1.Session.
The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.
Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.
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.
Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).
Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).
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.
160 @override 161 def on_player_join(self, player: EmptyPlayer) -> None: 162 super().on_player_join(player) 163 time_till_assign = max( 164 0, self._birth_time + self._min_view_time - babase.apptime() 165 ) 166 167 # If we're still kicking at the end of our assign-delay, assign this 168 # guy's input to trigger us. 169 _bascenev1.timer( 170 time_till_assign, babase.WeakCall(self._safe_assign, player) 171 )
Called when a new bascenev1.Player has joined the Activity.
(including the initial set of Players)
173 @override 174 def on_transition_in(self) -> None: 175 from bascenev1lib.actor.tipstext import TipsText 176 from bascenev1lib.actor.background import Background 177 178 super().on_transition_in() 179 self._background = Background( 180 fade_time=0.5, start_faded=False, show_logo=True 181 ) 182 if self._default_show_tips: 183 self._tips_text = TipsText() 184 setmusic(self.default_music)
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.
186 @override 187 def on_begin(self) -> None: 188 # pylint: disable=cyclic-import 189 from bascenev1lib.actor.text import Text 190 191 super().on_begin() 192 193 # Pop up a 'press any button to continue' statement after our 194 # min-view-time show a 'press any button to continue..' 195 # thing after a bit. 196 assert babase.app.classic is not None 197 if babase.app.ui_v1.uiscale is babase.UIScale.LARGE: 198 # FIXME: Need a better way to determine whether we've probably 199 # got a keyboard. 200 sval = babase.Lstr(resource='pressAnyKeyButtonText') 201 else: 202 sval = babase.Lstr(resource='pressAnyButtonText') 203 204 Text( 205 ( 206 self._custom_continue_message 207 if self._custom_continue_message is not None 208 else sval 209 ), 210 v_attach=Text.VAttach.BOTTOM, 211 h_align=Text.HAlign.CENTER, 212 flash=True, 213 vr_depth=50, 214 position=(0, 10), 215 scale=0.8, 216 color=(0.5, 0.7, 0.5, 0.5), 217 transition=Text.Transition.IN_BOTTOM_SLOW, 218 transition_delay=self._min_view_time, 219 ).autoretain()
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
16@unique 17class ScoreType(Enum): 18 """Type of scores. 19 20 Category: **Enums** 21 """ 22 23 SECONDS = 's' 24 MILLISECONDS = 'ms' 25 POINTS = 'p'
Type of scores.
Category: Enums
1001def broadcastmessage( 1002 message: str | babase.Lstr, 1003 color: Sequence[float] | None = None, 1004 top: bool = False, 1005 image: dict[str, Any] | None = None, 1006 log: bool = False, 1007 clients: Sequence[int] | None = None, 1008 transient: bool = False, 1009) -> None: 1010 """Broadcast a screen-message to clients in the current session. 1011 1012 Category: **General Utility Functions** 1013 1014 If 'top' is True, the message will go to the top message area. 1015 For 'top' messages, 'image' must be a dict containing 'texture' 1016 and 'tint_texture' textures and 'tint_color' and 'tint2_color' 1017 colors. This defines an icon to display alongside the message. 1018 If 'log' is True, the message will also be submitted to the log. 1019 'clients' can be a list of client-ids the message should be sent 1020 to, or None to specify that everyone should receive it. 1021 If 'transient' is True, the message will not be included in the 1022 game-stream and thus will not show up when viewing replays. 1023 Currently the 'clients' option only works for transient messages. 1024 """ 1025 return None
Broadcast a screen-message to clients in the current session.
Category: General Utility Functions
If 'top' is True, the message will go to the top message area. For 'top' messages, 'image' must be a dict containing 'texture' and 'tint_texture' textures and 'tint_color' and 'tint2_color' colors. This defines an icon to display alongside the message. If 'log' is True, the message will also be submitted to the log. 'clients' can be a list of client-ids the message should be sent to, or None to specify that everyone should receive it. If 'transient' is True, the message will not be included in the game-stream and thus will not show up when viewing replays. Currently the 'clients' option only works for transient messages.
43class Session: 44 """Defines a high level series of bascenev1.Activity-es. 45 46 Category: **Gameplay Classes** 47 48 Examples of sessions are bascenev1.FreeForAllSession, 49 bascenev1.DualTeamSession, and bascenev1.CoopSession. 50 51 A Session is responsible for wrangling and transitioning between various 52 bascenev1.Activity instances such as mini-games and score-screens, and for 53 maintaining state between them (players, teams, score tallies, etc). 54 """ 55 56 use_teams: bool = False 57 """Whether this session groups players into an explicit set of 58 teams. If this is off, a unique team is generated for each 59 player that joins.""" 60 61 use_team_colors: bool = True 62 """Whether players on a team should all adopt the colors of that 63 team instead of their own profile colors. This only applies if 64 use_teams is enabled.""" 65 66 # Note: even though these are instance vars, we annotate and document them 67 # at the class level so that looks better and nobody get lost while 68 # reading large __init__ 69 70 lobby: bascenev1.Lobby 71 """The baclassic.Lobby instance where new bascenev1.Player-s go to select 72 a Profile/Team/etc. before being added to games. 73 Be aware this value may be None if a Session does not allow 74 any such selection.""" 75 76 max_players: int 77 """The maximum number of players allowed in the Session.""" 78 79 min_players: int 80 """The minimum number of players who must be present for the Session 81 to proceed past the initial joining screen""" 82 83 sessionplayers: list[bascenev1.SessionPlayer] 84 """All bascenev1.SessionPlayers in the Session. Most things should use 85 the list of bascenev1.Player-s in bascenev1.Activity; not this. Some 86 players, such as those who have not yet selected a character, will 87 only be found on this list.""" 88 89 customdata: dict 90 """A shared dictionary for objects to use as storage on this session. 91 Ensure that keys here are unique to avoid collisions.""" 92 93 sessionteams: list[bascenev1.SessionTeam] 94 """All the bascenev1.SessionTeams in the Session. Most things should 95 use the list of bascenev1.Team-s in bascenev1.Activity; not this.""" 96 97 def __init__( 98 self, 99 depsets: Sequence[bascenev1.DependencySet], 100 *, 101 team_names: Sequence[str] | None = None, 102 team_colors: Sequence[Sequence[float]] | None = None, 103 min_players: int = 1, 104 max_players: int = 8, 105 submit_score: bool = True, 106 ): 107 """Instantiate a session. 108 109 depsets should be a sequence of successfully resolved 110 bascenev1.DependencySet instances; one for each bascenev1.Activity 111 the session may potentially run. 112 """ 113 # pylint: disable=too-many-statements 114 # pylint: disable=too-many-locals 115 # pylint: disable=cyclic-import 116 # pylint: disable=too-many-branches 117 from efro.util import empty_weakref 118 from bascenev1._dependency import ( 119 Dependency, 120 AssetPackage, 121 DependencyError, 122 ) 123 from bascenev1._lobby import Lobby 124 from bascenev1._stats import Stats 125 from bascenev1._gameactivity import GameActivity 126 from bascenev1._activity import Activity 127 from bascenev1._team import SessionTeam 128 129 # First off, resolve all dependency-sets we were passed. 130 # If things are missing, we'll try to gather them into a single 131 # missing-deps exception if possible to give the caller a clean 132 # path to download missing stuff and try again. 133 missing_asset_packages: set[str] = set() 134 for depset in depsets: 135 try: 136 depset.resolve() 137 except DependencyError as exc: 138 # Gather/report missing assets only; barf on anything else. 139 if all(issubclass(d.cls, AssetPackage) for d in exc.deps): 140 for dep in exc.deps: 141 assert isinstance(dep.config, str) 142 missing_asset_packages.add(dep.config) 143 else: 144 missing_info = [(d.cls, d.config) for d in exc.deps] 145 raise RuntimeError( 146 f'Missing non-asset dependencies: {missing_info}' 147 ) from exc 148 149 # Throw a combined exception if we found anything missing. 150 if missing_asset_packages: 151 raise DependencyError( 152 [ 153 Dependency(AssetPackage, set_id) 154 for set_id in missing_asset_packages 155 ] 156 ) 157 158 # Ok; looks like our dependencies check out. 159 # Now give the engine a list of asset-set-ids to pass along to clients. 160 required_asset_packages: set[str] = set() 161 for depset in depsets: 162 required_asset_packages.update(depset.get_asset_package_ids()) 163 164 # print('Would set host-session asset-reqs to:', 165 # required_asset_packages) 166 167 # Init our C++ layer data. 168 self._sessiondata = _bascenev1.register_session(self) 169 170 # Should remove this if possible. 171 self.tournament_id: str | None = None 172 173 self.sessionteams = [] 174 self.sessionplayers = [] 175 self.min_players = min_players 176 self.max_players = ( 177 max_players 178 if _max_players_override is None 179 else _max_players_override 180 ) 181 self.submit_score = submit_score 182 183 self.customdata = {} 184 self._in_set_activity = False 185 self._next_team_id = 0 186 self._activity_retained: bascenev1.Activity | None = None 187 self._launch_end_session_activity_time: float | None = None 188 self._activity_end_timer: bascenev1.BaseTimer | None = None 189 self._activity_weak = empty_weakref(Activity) 190 self._next_activity: bascenev1.Activity | None = None 191 self._wants_to_end = False 192 self._ending = False 193 self._activity_should_end_immediately = False 194 self._activity_should_end_immediately_results: ( 195 bascenev1.GameResults | None 196 ) = None 197 self._activity_should_end_immediately_delay = 0.0 198 199 # Create static teams if we're using them. 200 if self.use_teams: 201 if team_names is None: 202 raise RuntimeError( 203 'use_teams is True but team_names not provided.' 204 ) 205 if team_colors is None: 206 raise RuntimeError( 207 'use_teams is True but team_colors not provided.' 208 ) 209 if len(team_colors) != len(team_names): 210 raise RuntimeError( 211 f'Got {len(team_names)} team_names' 212 f' and {len(team_colors)} team_colors;' 213 f' these numbers must match.' 214 ) 215 for i, color in enumerate(team_colors): 216 team = SessionTeam( 217 team_id=self._next_team_id, 218 name=GameActivity.get_team_display_string(team_names[i]), 219 color=color, 220 ) 221 self.sessionteams.append(team) 222 self._next_team_id += 1 223 try: 224 with self.context: 225 self.on_team_join(team) 226 except Exception: 227 logging.exception('Error in on_team_join for %s.', self) 228 229 self.lobby = Lobby() 230 self.stats = Stats() 231 232 # Instantiate our session globals node which will apply its settings. 233 self._sessionglobalsnode = _bascenev1.newnode('sessionglobals') 234 235 # Rejoin cooldown stuff. 236 self._players_on_wait: dict = {} 237 self._player_requested_identifiers: dict = {} 238 self._waitlist_timers: dict = {} 239 240 @property 241 def context(self) -> bascenev1.ContextRef: 242 """A context-ref pointing at this activity.""" 243 return self._sessiondata.context() 244 245 @property 246 def sessionglobalsnode(self) -> bascenev1.Node: 247 """The sessionglobals bascenev1.Node for the session.""" 248 node = self._sessionglobalsnode 249 if not node: 250 raise babase.NodeNotFoundError() 251 return node 252 253 def should_allow_mid_activity_joins( 254 self, activity: bascenev1.Activity 255 ) -> bool: 256 """Ask ourself if we should allow joins during an Activity. 257 258 Note that for a join to be allowed, both the Session and Activity 259 have to be ok with it (via this function and the 260 Activity.allow_mid_activity_joins property. 261 """ 262 del activity # Unused. 263 return True 264 265 def on_player_request(self, player: bascenev1.SessionPlayer) -> bool: 266 """Called when a new bascenev1.Player wants to join the Session. 267 268 This should return True or False to accept/reject. 269 """ 270 # Limit player counts *unless* we're in a stress test. 271 if ( 272 babase.app.classic is not None 273 and babase.app.classic.stress_test_update_timer is None 274 ): 275 if len(self.sessionplayers) >= self.max_players >= 0: 276 # Print a rejection message *only* to the client trying to 277 # join (prevents spamming everyone else in the game). 278 _bascenev1.getsound('error').play() 279 _bascenev1.broadcastmessage( 280 babase.Lstr( 281 resource='playerLimitReachedText', 282 subs=[('${COUNT}', str(self.max_players))], 283 ), 284 color=(0.8, 0.0, 0.0), 285 clients=[player.inputdevice.client_id], 286 transient=True, 287 ) 288 return False 289 290 # Rejoin cooldown. 291 identifier = player.get_v1_account_id() 292 if identifier: 293 leave_time = self._players_on_wait.get(identifier) 294 if leave_time: 295 diff = str( 296 math.ceil( 297 _g_player_rejoin_cooldown 298 - babase.apptime() 299 + leave_time 300 ) 301 ) 302 _bascenev1.broadcastmessage( 303 babase.Lstr( 304 translate=( 305 'serverResponses', 306 'You can join in ${COUNT} seconds.', 307 ), 308 subs=[('${COUNT}', diff)], 309 ), 310 color=(1, 1, 0), 311 clients=[player.inputdevice.client_id], 312 transient=True, 313 ) 314 return False 315 self._player_requested_identifiers[player.id] = identifier 316 317 _bascenev1.getsound('dripity').play() 318 return True 319 320 def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: 321 """Called when a previously-accepted bascenev1.SessionPlayer leaves.""" 322 323 if sessionplayer not in self.sessionplayers: 324 print( 325 'ERROR: Session.on_player_leave called' 326 ' for player not in our list.' 327 ) 328 return 329 330 _bascenev1.getsound('playerLeft').play() 331 332 activity = self._activity_weak() 333 334 # Rejoin cooldown. 335 identifier = self._player_requested_identifiers.get(sessionplayer.id) 336 if identifier: 337 self._players_on_wait[identifier] = babase.apptime() 338 with babase.ContextRef.empty(): 339 self._waitlist_timers[identifier] = babase.AppTimer( 340 _g_player_rejoin_cooldown, 341 babase.Call(self._remove_player_from_waitlist, identifier), 342 ) 343 344 if not sessionplayer.in_game: 345 # Ok, the player is still in the lobby; simply remove them. 346 with self.context: 347 try: 348 self.lobby.remove_chooser(sessionplayer) 349 except Exception: 350 logging.exception('Error in Lobby.remove_chooser().') 351 else: 352 # Ok, they've already entered the game. Remove them from 353 # teams/activities/etc. 354 sessionteam = sessionplayer.sessionteam 355 assert sessionteam is not None 356 357 _bascenev1.broadcastmessage( 358 babase.Lstr( 359 resource='playerLeftText', 360 subs=[('${PLAYER}', sessionplayer.getname(full=True))], 361 ) 362 ) 363 364 # Remove them from their SessionTeam. 365 if sessionplayer in sessionteam.players: 366 sessionteam.players.remove(sessionplayer) 367 else: 368 print( 369 'SessionPlayer not found in SessionTeam' 370 ' in on_player_leave.' 371 ) 372 373 # Grab their activity-specific player instance. 374 player = sessionplayer.activityplayer 375 assert isinstance(player, (Player, type(None))) 376 377 # Remove them from any current Activity. 378 if player is not None and activity is not None: 379 if player in activity.players: 380 activity.remove_player(sessionplayer) 381 else: 382 print('Player not found in Activity in on_player_leave.') 383 384 # If we're a non-team session, remove their team too. 385 if not self.use_teams: 386 self._remove_player_team(sessionteam, activity) 387 388 # Now remove them from the session list. 389 self.sessionplayers.remove(sessionplayer) 390 391 def _remove_player_team( 392 self, 393 sessionteam: bascenev1.SessionTeam, 394 activity: bascenev1.Activity | None, 395 ) -> None: 396 """Remove the player-specific team in non-teams mode.""" 397 398 # They should have been the only one on their team. 399 assert not sessionteam.players 400 401 # Remove their Team from the Activity. 402 if activity is not None: 403 if sessionteam.activityteam in activity.teams: 404 activity.remove_team(sessionteam) 405 else: 406 print('Team not found in Activity in on_player_leave.') 407 408 # And then from the Session. 409 with self.context: 410 if sessionteam in self.sessionteams: 411 try: 412 self.sessionteams.remove(sessionteam) 413 self.on_team_leave(sessionteam) 414 except Exception: 415 logging.exception( 416 'Error in on_team_leave for Session %s.', self 417 ) 418 else: 419 print('Team no in Session teams in on_player_leave.') 420 try: 421 sessionteam.leave() 422 except Exception: 423 logging.exception( 424 'Error clearing sessiondata for team %s in session %s.', 425 sessionteam, 426 self, 427 ) 428 429 def end(self) -> None: 430 """Initiates an end to the session and a return to the main menu. 431 432 Note that this happens asynchronously, allowing the 433 session and its activities to shut down gracefully. 434 """ 435 self._wants_to_end = True 436 if self._next_activity is None: 437 self._launch_end_session_activity() 438 439 def _launch_end_session_activity(self) -> None: 440 """(internal)""" 441 from bascenev1._activitytypes import EndSessionActivity 442 443 with self.context: 444 curtime = babase.apptime() 445 if self._ending: 446 # Ignore repeats unless its been a while. 447 assert self._launch_end_session_activity_time is not None 448 since_last = curtime - self._launch_end_session_activity_time 449 if since_last < 30.0: 450 return 451 logging.error( 452 '_launch_end_session_activity called twice (since_last=%s)', 453 since_last, 454 ) 455 self._launch_end_session_activity_time = curtime 456 self.setactivity(_bascenev1.newactivity(EndSessionActivity)) 457 self._wants_to_end = False 458 self._ending = True # Prevent further actions. 459 460 def on_team_join(self, team: bascenev1.SessionTeam) -> None: 461 """Called when a new bascenev1.Team joins the session.""" 462 463 def on_team_leave(self, team: bascenev1.SessionTeam) -> None: 464 """Called when a bascenev1.Team is leaving the session.""" 465 466 def end_activity( 467 self, 468 activity: bascenev1.Activity, 469 results: Any, 470 delay: float, 471 force: bool, 472 ) -> None: 473 """Commence shutdown of a bascenev1.Activity (if not already occurring). 474 475 'delay' is the time delay before the Activity actually ends 476 (in seconds). Further calls to end() will be ignored up until 477 this time, unless 'force' is True, in which case the new results 478 will replace the old. 479 """ 480 # Only pay attention if this is coming from our current activity. 481 if activity is not self._activity_retained: 482 return 483 484 # If this activity hasn't begun yet, just set it up to end immediately 485 # once it does. 486 if not activity.has_begun(): 487 # activity.set_immediate_end(results, delay, force) 488 if not self._activity_should_end_immediately or force: 489 self._activity_should_end_immediately = True 490 self._activity_should_end_immediately_results = results 491 self._activity_should_end_immediately_delay = delay 492 493 # The activity has already begun; get ready to end it. 494 else: 495 if (not activity.has_ended()) or force: 496 activity.set_has_ended(True) 497 498 # Set a timer to set in motion this activity's demise. 499 self._activity_end_timer = _bascenev1.BaseTimer( 500 delay, 501 babase.Call(self._complete_end_activity, activity, results), 502 ) 503 504 def handlemessage(self, msg: Any) -> Any: 505 """General message handling; can be passed any message object.""" 506 from bascenev1._lobby import PlayerReadyMessage 507 from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED 508 509 if isinstance(msg, PlayerReadyMessage): 510 self._on_player_ready(msg.chooser) 511 512 elif isinstance(msg, PlayerProfilesChangedMessage): 513 # If we have a current activity with a lobby, ask it to reload 514 # profiles. 515 with self.context: 516 self.lobby.reload_profiles() 517 return None 518 519 else: 520 return UNHANDLED 521 return None 522 523 class _SetActivityScopedLock: 524 def __init__(self, session: Session) -> None: 525 self._session = session 526 if session._in_set_activity: 527 raise RuntimeError('Session.setactivity() called recursively.') 528 self._session._in_set_activity = True 529 530 def __del__(self) -> None: 531 self._session._in_set_activity = False 532 533 def setactivity(self, activity: bascenev1.Activity) -> None: 534 """Assign a new current bascenev1.Activity for the session. 535 536 Note that this will not change the current context to the new 537 Activity's. Code must be run in the new activity's methods 538 (on_transition_in, etc) to get it. (so you can't do 539 session.setactivity(foo) and then bascenev1.newnode() to add a node 540 to foo) 541 """ 542 543 # Make sure we don't get called recursively. 544 _rlock = self._SetActivityScopedLock(self) 545 546 if activity.session is not _bascenev1.getsession(): 547 raise RuntimeError("Provided Activity's Session is not current.") 548 549 # Quietly ignore this if the whole session is going down. 550 if self._ending: 551 return 552 553 if activity is self._activity_retained: 554 logging.error('Activity set to already-current activity.') 555 return 556 557 if self._next_activity is not None: 558 raise RuntimeError( 559 'Activity switch already in progress (to ' 560 + str(self._next_activity) 561 + ')' 562 ) 563 564 prev_activity = self._activity_retained 565 prev_globals = ( 566 prev_activity.globalsnode if prev_activity is not None else None 567 ) 568 569 # Let the activity do its thing. 570 activity.transition_in(prev_globals) 571 572 self._next_activity = activity 573 574 # If we have a current activity, tell it it's transitioning out; 575 # the next one will become current once this one dies. 576 if prev_activity is not None: 577 prev_activity.transition_out() 578 579 # Setting this to None should free up the old activity to die, 580 # which will call begin_next_activity. 581 # We can still access our old activity through 582 # self._activity_weak() to keep it up to date on player 583 # joins/departures/etc until it dies. 584 self._activity_retained = None 585 586 # There's no existing activity; lets just go ahead with the begin call. 587 else: 588 self.begin_next_activity() 589 590 # We want to call destroy() for the previous activity once it should 591 # tear itself down, clear out any self-refs, etc. After this call 592 # the activity should have no refs left to it and should die (which 593 # will trigger the next activity to run). 594 if prev_activity is not None: 595 with babase.ContextRef.empty(): 596 babase.apptimer( 597 max(0.0, activity.transition_time), prev_activity.expire 598 ) 599 self._in_set_activity = False 600 601 def getactivity(self) -> bascenev1.Activity | None: 602 """Return the current foreground activity for this session.""" 603 return self._activity_weak() 604 605 def get_custom_menu_entries(self) -> list[dict[str, Any]]: 606 """Subclasses can override this to provide custom menu entries. 607 608 The returned value should be a list of dicts, each containing 609 a 'label' and 'call' entry, with 'label' being the text for 610 the entry and 'call' being the callable to trigger if the entry 611 is pressed. 612 """ 613 return [] 614 615 def _complete_end_activity( 616 self, activity: bascenev1.Activity, results: Any 617 ) -> None: 618 # Run the subclass callback in the session context. 619 try: 620 with self.context: 621 self.on_activity_end(activity, results) 622 except Exception: 623 logging.error( 624 'Error in on_activity_end() for session %s' 625 ' activity %s with results %s', 626 self, 627 activity, 628 results, 629 ) 630 631 def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool: 632 """Called by the native layer when a player wants to join.""" 633 634 # If we're ending, allow no new players. 635 if self._ending: 636 return False 637 638 # Ask the bascenev1.Session subclass to approve/deny this request. 639 try: 640 with self.context: 641 result = self.on_player_request(sessionplayer) 642 except Exception: 643 logging.exception('Error in on_player_request for %s.', self) 644 result = False 645 646 # If they said yes, add the player to the lobby. 647 if result: 648 self.sessionplayers.append(sessionplayer) 649 with self.context: 650 try: 651 self.lobby.add_chooser(sessionplayer) 652 except Exception: 653 logging.exception('Error in lobby.add_chooser().') 654 655 return result 656 657 def on_activity_end( 658 self, activity: bascenev1.Activity, results: Any 659 ) -> None: 660 """Called when the current bascenev1.Activity has ended. 661 662 The bascenev1.Session should look at the results and start 663 another bascenev1.Activity. 664 """ 665 666 def begin_next_activity(self) -> None: 667 """Called once the previous activity has been totally torn down. 668 669 This means we're ready to begin the next one 670 """ 671 if self._next_activity is None: 672 # Should this ever happen? 673 logging.error('begin_next_activity() called with no _next_activity') 674 return 675 676 # We store both a weak and a strong ref to the new activity; 677 # the strong is to keep it alive and the weak is so we can access 678 # it even after we've released the strong-ref to allow it to die. 679 self._activity_retained = self._next_activity 680 self._activity_weak = weakref.ref(self._next_activity) 681 self._next_activity = None 682 self._activity_should_end_immediately = False 683 684 # Kick out anyone loitering in the lobby. 685 self.lobby.remove_all_choosers_and_kick_players() 686 687 # Kick off the activity. 688 self._activity_retained.begin(self) 689 690 # If we want to completely end the session, we can now kick that off. 691 if self._wants_to_end: 692 self._launch_end_session_activity() 693 else: 694 # Otherwise, if the activity has already been told to end, 695 # do so now. 696 if self._activity_should_end_immediately: 697 self._activity_retained.end( 698 self._activity_should_end_immediately_results, 699 self._activity_should_end_immediately_delay, 700 ) 701 702 def _on_player_ready(self, chooser: bascenev1.Chooser) -> None: 703 """Called when a bascenev1.Player has checked themself ready.""" 704 lobby = chooser.lobby 705 activity = self._activity_weak() 706 707 # This happens sometimes. That seems like it shouldn't be happening; 708 # when would we have a session and a chooser with players but no 709 # active activity? 710 if activity is None: 711 print('_on_player_ready called with no activity.') 712 return 713 714 # In joining-activities, we wait till all choosers are ready 715 # and then create all players at once. 716 if activity.is_joining_activity: 717 if not lobby.check_all_ready(): 718 return 719 choosers = lobby.get_choosers() 720 min_players = self.min_players 721 if len(choosers) >= min_players: 722 for lch in lobby.get_choosers(): 723 self._add_chosen_player(lch) 724 lobby.remove_all_choosers() 725 726 # Get our next activity going. 727 self._complete_end_activity(activity, {}) 728 else: 729 _bascenev1.broadcastmessage( 730 babase.Lstr( 731 resource='notEnoughPlayersText', 732 subs=[('${COUNT}', str(min_players))], 733 ), 734 color=(1, 1, 0), 735 ) 736 _bascenev1.getsound('error').play() 737 738 # Otherwise just add players on the fly. 739 else: 740 self._add_chosen_player(chooser) 741 lobby.remove_chooser(chooser.getplayer()) 742 743 def transitioning_out_activity_was_freed( 744 self, can_show_ad_on_death: bool 745 ) -> None: 746 """(internal)""" 747 # pylint: disable=cyclic-import 748 749 # Since things should be generally still right now, it's a good time 750 # to run garbage collection to clear out any circular dependency 751 # loops. We keep this disabled normally to avoid non-deterministic 752 # hitches. 753 babase.garbage_collect() 754 755 assert babase.app.classic is not None 756 with self.context: 757 if can_show_ad_on_death: 758 babase.app.classic.ads.call_after_ad(self.begin_next_activity) 759 else: 760 babase.pushcall(self.begin_next_activity) 761 762 def _add_chosen_player( 763 self, chooser: bascenev1.Chooser 764 ) -> bascenev1.SessionPlayer: 765 from bascenev1._team import SessionTeam 766 767 sessionplayer = chooser.getplayer() 768 assert sessionplayer in self.sessionplayers, ( 769 'SessionPlayer not found in session ' 770 'player-list after chooser selection.' 771 ) 772 773 activity = self._activity_weak() 774 assert activity is not None 775 776 # Reset the player's input here, as it is probably 777 # referencing the chooser which could inadvertently keep it alive. 778 sessionplayer.resetinput() 779 780 # We can pass it to the current activity if it has already begun 781 # (otherwise it'll get passed once begin is called). 782 pass_to_activity = ( 783 activity.has_begun() and not activity.is_joining_activity 784 ) 785 786 # However, if we're not allowing mid-game joins, don't actually pass; 787 # just announce the arrival and say they'll partake next round. 788 if pass_to_activity: 789 if not ( 790 activity.allow_mid_activity_joins 791 and self.should_allow_mid_activity_joins(activity) 792 ): 793 pass_to_activity = False 794 with self.context: 795 _bascenev1.broadcastmessage( 796 babase.Lstr( 797 resource='playerDelayedJoinText', 798 subs=[ 799 ('${PLAYER}', sessionplayer.getname(full=True)) 800 ], 801 ), 802 color=(0, 1, 0), 803 ) 804 805 # If we're a non-team session, each player gets their own team. 806 # (keeps mini-game coding simpler if we can always deal with teams). 807 if self.use_teams: 808 sessionteam = chooser.sessionteam 809 else: 810 our_team_id = self._next_team_id 811 self._next_team_id += 1 812 sessionteam = SessionTeam( 813 team_id=our_team_id, 814 color=chooser.get_color(), 815 name=chooser.getplayer().getname(full=True, icon=False), 816 ) 817 818 # Add player's team to the Session. 819 self.sessionteams.append(sessionteam) 820 821 with self.context: 822 try: 823 self.on_team_join(sessionteam) 824 except Exception: 825 logging.exception('Error in on_team_join for %s.', self) 826 827 # Add player's team to the Activity. 828 if pass_to_activity: 829 activity.add_team(sessionteam) 830 831 assert sessionplayer not in sessionteam.players 832 sessionteam.players.append(sessionplayer) 833 sessionplayer.setdata( 834 team=sessionteam, 835 character=chooser.get_character_name(), 836 color=chooser.get_color(), 837 highlight=chooser.get_highlight(), 838 ) 839 840 self.stats.register_sessionplayer(sessionplayer) 841 if pass_to_activity: 842 activity.add_player(sessionplayer) 843 return sessionplayer 844 845 def _remove_player_from_waitlist(self, identifier: str) -> None: 846 try: 847 self._players_on_wait.pop(identifier) 848 except KeyError: 849 pass
Defines a high level series of bascenev1.Activity-es.
Category: Gameplay Classes
Examples of sessions are bascenev1.FreeForAllSession, bascenev1.DualTeamSession, and bascenev1.CoopSession.
A Session is responsible for wrangling and transitioning between various bascenev1.Activity instances such as mini-games and score-screens, and for maintaining state between them (players, teams, score tallies, etc).
97 def __init__( 98 self, 99 depsets: Sequence[bascenev1.DependencySet], 100 *, 101 team_names: Sequence[str] | None = None, 102 team_colors: Sequence[Sequence[float]] | None = None, 103 min_players: int = 1, 104 max_players: int = 8, 105 submit_score: bool = True, 106 ): 107 """Instantiate a session. 108 109 depsets should be a sequence of successfully resolved 110 bascenev1.DependencySet instances; one for each bascenev1.Activity 111 the session may potentially run. 112 """ 113 # pylint: disable=too-many-statements 114 # pylint: disable=too-many-locals 115 # pylint: disable=cyclic-import 116 # pylint: disable=too-many-branches 117 from efro.util import empty_weakref 118 from bascenev1._dependency import ( 119 Dependency, 120 AssetPackage, 121 DependencyError, 122 ) 123 from bascenev1._lobby import Lobby 124 from bascenev1._stats import Stats 125 from bascenev1._gameactivity import GameActivity 126 from bascenev1._activity import Activity 127 from bascenev1._team import SessionTeam 128 129 # First off, resolve all dependency-sets we were passed. 130 # If things are missing, we'll try to gather them into a single 131 # missing-deps exception if possible to give the caller a clean 132 # path to download missing stuff and try again. 133 missing_asset_packages: set[str] = set() 134 for depset in depsets: 135 try: 136 depset.resolve() 137 except DependencyError as exc: 138 # Gather/report missing assets only; barf on anything else. 139 if all(issubclass(d.cls, AssetPackage) for d in exc.deps): 140 for dep in exc.deps: 141 assert isinstance(dep.config, str) 142 missing_asset_packages.add(dep.config) 143 else: 144 missing_info = [(d.cls, d.config) for d in exc.deps] 145 raise RuntimeError( 146 f'Missing non-asset dependencies: {missing_info}' 147 ) from exc 148 149 # Throw a combined exception if we found anything missing. 150 if missing_asset_packages: 151 raise DependencyError( 152 [ 153 Dependency(AssetPackage, set_id) 154 for set_id in missing_asset_packages 155 ] 156 ) 157 158 # Ok; looks like our dependencies check out. 159 # Now give the engine a list of asset-set-ids to pass along to clients. 160 required_asset_packages: set[str] = set() 161 for depset in depsets: 162 required_asset_packages.update(depset.get_asset_package_ids()) 163 164 # print('Would set host-session asset-reqs to:', 165 # required_asset_packages) 166 167 # Init our C++ layer data. 168 self._sessiondata = _bascenev1.register_session(self) 169 170 # Should remove this if possible. 171 self.tournament_id: str | None = None 172 173 self.sessionteams = [] 174 self.sessionplayers = [] 175 self.min_players = min_players 176 self.max_players = ( 177 max_players 178 if _max_players_override is None 179 else _max_players_override 180 ) 181 self.submit_score = submit_score 182 183 self.customdata = {} 184 self._in_set_activity = False 185 self._next_team_id = 0 186 self._activity_retained: bascenev1.Activity | None = None 187 self._launch_end_session_activity_time: float | None = None 188 self._activity_end_timer: bascenev1.BaseTimer | None = None 189 self._activity_weak = empty_weakref(Activity) 190 self._next_activity: bascenev1.Activity | None = None 191 self._wants_to_end = False 192 self._ending = False 193 self._activity_should_end_immediately = False 194 self._activity_should_end_immediately_results: ( 195 bascenev1.GameResults | None 196 ) = None 197 self._activity_should_end_immediately_delay = 0.0 198 199 # Create static teams if we're using them. 200 if self.use_teams: 201 if team_names is None: 202 raise RuntimeError( 203 'use_teams is True but team_names not provided.' 204 ) 205 if team_colors is None: 206 raise RuntimeError( 207 'use_teams is True but team_colors not provided.' 208 ) 209 if len(team_colors) != len(team_names): 210 raise RuntimeError( 211 f'Got {len(team_names)} team_names' 212 f' and {len(team_colors)} team_colors;' 213 f' these numbers must match.' 214 ) 215 for i, color in enumerate(team_colors): 216 team = SessionTeam( 217 team_id=self._next_team_id, 218 name=GameActivity.get_team_display_string(team_names[i]), 219 color=color, 220 ) 221 self.sessionteams.append(team) 222 self._next_team_id += 1 223 try: 224 with self.context: 225 self.on_team_join(team) 226 except Exception: 227 logging.exception('Error in on_team_join for %s.', self) 228 229 self.lobby = Lobby() 230 self.stats = Stats() 231 232 # Instantiate our session globals node which will apply its settings. 233 self._sessionglobalsnode = _bascenev1.newnode('sessionglobals') 234 235 # Rejoin cooldown stuff. 236 self._players_on_wait: dict = {} 237 self._player_requested_identifiers: dict = {} 238 self._waitlist_timers: dict = {}
Instantiate a session.
depsets should be a sequence of successfully resolved bascenev1.DependencySet instances; one for each bascenev1.Activity the session may potentially run.
Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each player that joins.
Whether players on a team should all adopt the colors of that team instead of their own profile colors. This only applies if use_teams is enabled.
The baclassic.Lobby instance where new bascenev1.Player-s go to select a Profile/Team/etc. before being added to games. Be aware this value may be None if a Session does not allow any such selection.
The minimum number of players who must be present for the Session to proceed past the initial joining screen
All bascenev1.SessionPlayers in the Session. Most things should use the list of bascenev1.Player-s in bascenev1.Activity; not this. Some players, such as those who have not yet selected a character, will only be found on this list.
A shared dictionary for objects to use as storage on this session. Ensure that keys here are unique to avoid collisions.
All the bascenev1.SessionTeams in the Session. Most things should use the list of bascenev1.Team-s in bascenev1.Activity; not this.
240 @property 241 def context(self) -> bascenev1.ContextRef: 242 """A context-ref pointing at this activity.""" 243 return self._sessiondata.context()
A context-ref pointing at this activity.
245 @property 246 def sessionglobalsnode(self) -> bascenev1.Node: 247 """The sessionglobals bascenev1.Node for the session.""" 248 node = self._sessionglobalsnode 249 if not node: 250 raise babase.NodeNotFoundError() 251 return node
The sessionglobals bascenev1.Node for the session.
253 def should_allow_mid_activity_joins( 254 self, activity: bascenev1.Activity 255 ) -> bool: 256 """Ask ourself if we should allow joins during an Activity. 257 258 Note that for a join to be allowed, both the Session and Activity 259 have to be ok with it (via this function and the 260 Activity.allow_mid_activity_joins property. 261 """ 262 del activity # Unused. 263 return True
Ask ourself if we should allow joins during an Activity.
Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.
265 def on_player_request(self, player: bascenev1.SessionPlayer) -> bool: 266 """Called when a new bascenev1.Player wants to join the Session. 267 268 This should return True or False to accept/reject. 269 """ 270 # Limit player counts *unless* we're in a stress test. 271 if ( 272 babase.app.classic is not None 273 and babase.app.classic.stress_test_update_timer is None 274 ): 275 if len(self.sessionplayers) >= self.max_players >= 0: 276 # Print a rejection message *only* to the client trying to 277 # join (prevents spamming everyone else in the game). 278 _bascenev1.getsound('error').play() 279 _bascenev1.broadcastmessage( 280 babase.Lstr( 281 resource='playerLimitReachedText', 282 subs=[('${COUNT}', str(self.max_players))], 283 ), 284 color=(0.8, 0.0, 0.0), 285 clients=[player.inputdevice.client_id], 286 transient=True, 287 ) 288 return False 289 290 # Rejoin cooldown. 291 identifier = player.get_v1_account_id() 292 if identifier: 293 leave_time = self._players_on_wait.get(identifier) 294 if leave_time: 295 diff = str( 296 math.ceil( 297 _g_player_rejoin_cooldown 298 - babase.apptime() 299 + leave_time 300 ) 301 ) 302 _bascenev1.broadcastmessage( 303 babase.Lstr( 304 translate=( 305 'serverResponses', 306 'You can join in ${COUNT} seconds.', 307 ), 308 subs=[('${COUNT}', diff)], 309 ), 310 color=(1, 1, 0), 311 clients=[player.inputdevice.client_id], 312 transient=True, 313 ) 314 return False 315 self._player_requested_identifiers[player.id] = identifier 316 317 _bascenev1.getsound('dripity').play() 318 return True
Called when a new bascenev1.Player wants to join the Session.
This should return True or False to accept/reject.
320 def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: 321 """Called when a previously-accepted bascenev1.SessionPlayer leaves.""" 322 323 if sessionplayer not in self.sessionplayers: 324 print( 325 'ERROR: Session.on_player_leave called' 326 ' for player not in our list.' 327 ) 328 return 329 330 _bascenev1.getsound('playerLeft').play() 331 332 activity = self._activity_weak() 333 334 # Rejoin cooldown. 335 identifier = self._player_requested_identifiers.get(sessionplayer.id) 336 if identifier: 337 self._players_on_wait[identifier] = babase.apptime() 338 with babase.ContextRef.empty(): 339 self._waitlist_timers[identifier] = babase.AppTimer( 340 _g_player_rejoin_cooldown, 341 babase.Call(self._remove_player_from_waitlist, identifier), 342 ) 343 344 if not sessionplayer.in_game: 345 # Ok, the player is still in the lobby; simply remove them. 346 with self.context: 347 try: 348 self.lobby.remove_chooser(sessionplayer) 349 except Exception: 350 logging.exception('Error in Lobby.remove_chooser().') 351 else: 352 # Ok, they've already entered the game. Remove them from 353 # teams/activities/etc. 354 sessionteam = sessionplayer.sessionteam 355 assert sessionteam is not None 356 357 _bascenev1.broadcastmessage( 358 babase.Lstr( 359 resource='playerLeftText', 360 subs=[('${PLAYER}', sessionplayer.getname(full=True))], 361 ) 362 ) 363 364 # Remove them from their SessionTeam. 365 if sessionplayer in sessionteam.players: 366 sessionteam.players.remove(sessionplayer) 367 else: 368 print( 369 'SessionPlayer not found in SessionTeam' 370 ' in on_player_leave.' 371 ) 372 373 # Grab their activity-specific player instance. 374 player = sessionplayer.activityplayer 375 assert isinstance(player, (Player, type(None))) 376 377 # Remove them from any current Activity. 378 if player is not None and activity is not None: 379 if player in activity.players: 380 activity.remove_player(sessionplayer) 381 else: 382 print('Player not found in Activity in on_player_leave.') 383 384 # If we're a non-team session, remove their team too. 385 if not self.use_teams: 386 self._remove_player_team(sessionteam, activity) 387 388 # Now remove them from the session list. 389 self.sessionplayers.remove(sessionplayer)
Called when a previously-accepted bascenev1.SessionPlayer leaves.
429 def end(self) -> None: 430 """Initiates an end to the session and a return to the main menu. 431 432 Note that this happens asynchronously, allowing the 433 session and its activities to shut down gracefully. 434 """ 435 self._wants_to_end = True 436 if self._next_activity is None: 437 self._launch_end_session_activity()
Initiates an end to the session and a return to the main menu.
Note that this happens asynchronously, allowing the session and its activities to shut down gracefully.
460 def on_team_join(self, team: bascenev1.SessionTeam) -> None: 461 """Called when a new bascenev1.Team joins the session."""
Called when a new bascenev1.Team joins the session.
463 def on_team_leave(self, team: bascenev1.SessionTeam) -> None: 464 """Called when a bascenev1.Team is leaving the session."""
Called when a bascenev1.Team is leaving the session.
466 def end_activity( 467 self, 468 activity: bascenev1.Activity, 469 results: Any, 470 delay: float, 471 force: bool, 472 ) -> None: 473 """Commence shutdown of a bascenev1.Activity (if not already occurring). 474 475 'delay' is the time delay before the Activity actually ends 476 (in seconds). Further calls to end() will be ignored up until 477 this time, unless 'force' is True, in which case the new results 478 will replace the old. 479 """ 480 # Only pay attention if this is coming from our current activity. 481 if activity is not self._activity_retained: 482 return 483 484 # If this activity hasn't begun yet, just set it up to end immediately 485 # once it does. 486 if not activity.has_begun(): 487 # activity.set_immediate_end(results, delay, force) 488 if not self._activity_should_end_immediately or force: 489 self._activity_should_end_immediately = True 490 self._activity_should_end_immediately_results = results 491 self._activity_should_end_immediately_delay = delay 492 493 # The activity has already begun; get ready to end it. 494 else: 495 if (not activity.has_ended()) or force: 496 activity.set_has_ended(True) 497 498 # Set a timer to set in motion this activity's demise. 499 self._activity_end_timer = _bascenev1.BaseTimer( 500 delay, 501 babase.Call(self._complete_end_activity, activity, results), 502 )
Commence shutdown of a bascenev1.Activity (if not already occurring).
'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old.
504 def handlemessage(self, msg: Any) -> Any: 505 """General message handling; can be passed any message object.""" 506 from bascenev1._lobby import PlayerReadyMessage 507 from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED 508 509 if isinstance(msg, PlayerReadyMessage): 510 self._on_player_ready(msg.chooser) 511 512 elif isinstance(msg, PlayerProfilesChangedMessage): 513 # If we have a current activity with a lobby, ask it to reload 514 # profiles. 515 with self.context: 516 self.lobby.reload_profiles() 517 return None 518 519 else: 520 return UNHANDLED 521 return None
General message handling; can be passed any message object.
533 def setactivity(self, activity: bascenev1.Activity) -> None: 534 """Assign a new current bascenev1.Activity for the session. 535 536 Note that this will not change the current context to the new 537 Activity's. Code must be run in the new activity's methods 538 (on_transition_in, etc) to get it. (so you can't do 539 session.setactivity(foo) and then bascenev1.newnode() to add a node 540 to foo) 541 """ 542 543 # Make sure we don't get called recursively. 544 _rlock = self._SetActivityScopedLock(self) 545 546 if activity.session is not _bascenev1.getsession(): 547 raise RuntimeError("Provided Activity's Session is not current.") 548 549 # Quietly ignore this if the whole session is going down. 550 if self._ending: 551 return 552 553 if activity is self._activity_retained: 554 logging.error('Activity set to already-current activity.') 555 return 556 557 if self._next_activity is not None: 558 raise RuntimeError( 559 'Activity switch already in progress (to ' 560 + str(self._next_activity) 561 + ')' 562 ) 563 564 prev_activity = self._activity_retained 565 prev_globals = ( 566 prev_activity.globalsnode if prev_activity is not None else None 567 ) 568 569 # Let the activity do its thing. 570 activity.transition_in(prev_globals) 571 572 self._next_activity = activity 573 574 # If we have a current activity, tell it it's transitioning out; 575 # the next one will become current once this one dies. 576 if prev_activity is not None: 577 prev_activity.transition_out() 578 579 # Setting this to None should free up the old activity to die, 580 # which will call begin_next_activity. 581 # We can still access our old activity through 582 # self._activity_weak() to keep it up to date on player 583 # joins/departures/etc until it dies. 584 self._activity_retained = None 585 586 # There's no existing activity; lets just go ahead with the begin call. 587 else: 588 self.begin_next_activity() 589 590 # We want to call destroy() for the previous activity once it should 591 # tear itself down, clear out any self-refs, etc. After this call 592 # the activity should have no refs left to it and should die (which 593 # will trigger the next activity to run). 594 if prev_activity is not None: 595 with babase.ContextRef.empty(): 596 babase.apptimer( 597 max(0.0, activity.transition_time), prev_activity.expire 598 ) 599 self._in_set_activity = False
Assign a new current bascenev1.Activity for the session.
Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do session.setactivity(foo) and then bascenev1.newnode() to add a node to foo)
601 def getactivity(self) -> bascenev1.Activity | None: 602 """Return the current foreground activity for this session.""" 603 return self._activity_weak()
Return the current foreground activity for this session.
657 def on_activity_end( 658 self, activity: bascenev1.Activity, results: Any 659 ) -> None: 660 """Called when the current bascenev1.Activity has ended. 661 662 The bascenev1.Session should look at the results and start 663 another bascenev1.Activity. 664 """
Called when the current bascenev1.Activity has ended.
The bascenev1.Session should look at the results and start another bascenev1.Activity.
666 def begin_next_activity(self) -> None: 667 """Called once the previous activity has been totally torn down. 668 669 This means we're ready to begin the next one 670 """ 671 if self._next_activity is None: 672 # Should this ever happen? 673 logging.error('begin_next_activity() called with no _next_activity') 674 return 675 676 # We store both a weak and a strong ref to the new activity; 677 # the strong is to keep it alive and the weak is so we can access 678 # it even after we've released the strong-ref to allow it to die. 679 self._activity_retained = self._next_activity 680 self._activity_weak = weakref.ref(self._next_activity) 681 self._next_activity = None 682 self._activity_should_end_immediately = False 683 684 # Kick out anyone loitering in the lobby. 685 self.lobby.remove_all_choosers_and_kick_players() 686 687 # Kick off the activity. 688 self._activity_retained.begin(self) 689 690 # If we want to completely end the session, we can now kick that off. 691 if self._wants_to_end: 692 self._launch_end_session_activity() 693 else: 694 # Otherwise, if the activity has already been told to end, 695 # do so now. 696 if self._activity_should_end_immediately: 697 self._activity_retained.end( 698 self._activity_should_end_immediately_results, 699 self._activity_should_end_immediately_delay, 700 )
Called once the previous activity has been totally torn down.
This means we're ready to begin the next one
712class SessionPlayer: 713 """A reference to a player in the bascenev1.Session. 714 715 Category: **Gameplay Classes** 716 717 These are created and managed internally and 718 provided to your bascenev1.Session/bascenev1.Activity instances. 719 Be aware that, like `ba.Node`s, bascenev1.SessionPlayer objects are 720 'weak' references under-the-hood; a player can leave the game at 721 any point. For this reason, you should make judicious use of the 722 babase.SessionPlayer.exists() method (or boolean operator) to ensure 723 that a SessionPlayer is still present if retaining references to one 724 for any length of time. 725 """ 726 727 id: int 728 """The unique numeric ID of the Player. 729 730 Note that you can also use the boolean operator for this same 731 functionality, so a statement such as "if player" will do 732 the right thing both for Player objects and values of None.""" 733 734 in_game: bool 735 """This bool value will be True once the Player has completed 736 any lobby character/team selection.""" 737 738 sessionteam: bascenev1.SessionTeam 739 """The bascenev1.SessionTeam this Player is on. If the 740 SessionPlayer is still in its lobby selecting a team/etc. 741 then a bascenev1.SessionTeamNotFoundError will be raised.""" 742 743 inputdevice: bascenev1.InputDevice 744 """The input device associated with the player.""" 745 746 color: Sequence[float] 747 """The base color for this Player. 748 In team games this will match the bascenev1.SessionTeam's 749 color.""" 750 751 highlight: Sequence[float] 752 """A secondary color for this player. 753 This is used for minor highlights and accents 754 to allow a player to stand apart from his teammates 755 who may all share the same team (primary) color.""" 756 757 character: str 758 """The character this player has selected in their profile.""" 759 760 activityplayer: bascenev1.Player | None 761 """The current game-specific instance for this player.""" 762 763 def __bool__(self) -> bool: 764 """Support for bool evaluation.""" 765 return bool(True) # Slight obfuscation. 766 767 def assigninput( 768 self, 769 type: bascenev1.InputType | tuple[bascenev1.InputType, ...], 770 call: Callable, 771 ) -> None: 772 """Set the python callable to be run for one or more types of input.""" 773 return None 774 775 def exists(self) -> bool: 776 """Return whether the underlying player is still in the game.""" 777 return bool() 778 779 def get_icon(self) -> dict[str, Any]: 780 """Returns the character's icon (images, colors, etc contained 781 in a dict. 782 """ 783 return {'foo': 'bar'} 784 785 def get_icon_info(self) -> dict[str, Any]: 786 """(internal)""" 787 return {'foo': 'bar'} 788 789 def get_v1_account_id(self) -> str: 790 """Return the V1 Account ID this player is signed in under, if 791 there is one and it can be determined with relative certainty. 792 Returns None otherwise. Note that this may require an active 793 internet connection (especially for network-connected players) 794 and may return None for a short while after a player initially 795 joins (while verification occurs). 796 """ 797 return str() 798 799 def getname(self, full: bool = False, icon: bool = True) -> str: 800 """Returns the player's name. If icon is True, the long version of the 801 name may include an icon. 802 """ 803 return str() 804 805 def remove_from_game(self) -> None: 806 """Removes the player from the game.""" 807 return None 808 809 def resetinput(self) -> None: 810 """Clears out the player's assigned input actions.""" 811 return None 812 813 def set_icon_info( 814 self, 815 texture: str, 816 tint_texture: str, 817 tint_color: Sequence[float], 818 tint2_color: Sequence[float], 819 ) -> None: 820 """(internal)""" 821 return None 822 823 def setactivity(self, activity: bascenev1.Activity | None) -> None: 824 """(internal)""" 825 return None 826 827 def setdata( 828 self, 829 team: bascenev1.SessionTeam, 830 character: str, 831 color: Sequence[float], 832 highlight: Sequence[float], 833 ) -> None: 834 """(internal)""" 835 return None 836 837 def setname( 838 self, name: str, full_name: str | None = None, real: bool = True 839 ) -> None: 840 """Set the player's name to the provided string. 841 A number will automatically be appended if the name is not unique from 842 other players. 843 """ 844 return None 845 846 def setnode(self, node: bascenev1.Node | None) -> None: 847 """(internal)""" 848 return None
A reference to a player in the bascenev1.Session.
Category: Gameplay Classes
These are created and managed internally and
provided to your bascenev1.Session/bascenev1.Activity instances.
Be aware that, like ba.Node
s, bascenev1.SessionPlayer objects are
'weak' references under-the-hood; a player can leave the game at
any point. For this reason, you should make judicious use of the
babase.SessionPlayer.exists() method (or boolean operator) to ensure
that a SessionPlayer is still present if retaining references to one
for any length of time.
The unique numeric ID of the Player.
Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do the right thing both for Player objects and values of None.
This bool value will be True once the Player has completed any lobby character/team selection.
The bascenev1.SessionTeam this Player is on. If the SessionPlayer is still in its lobby selecting a team/etc. then a bascenev1.SessionTeamNotFoundError will be raised.
The base color for this Player. In team games this will match the bascenev1.SessionTeam's color.
A secondary color for this player. This is used for minor highlights and accents to allow a player to stand apart from his teammates who may all share the same team (primary) color.
767 def assigninput( 768 self, 769 type: bascenev1.InputType | tuple[bascenev1.InputType, ...], 770 call: Callable, 771 ) -> None: 772 """Set the python callable to be run for one or more types of input.""" 773 return None
Set the python callable to be run for one or more types of input.
775 def exists(self) -> bool: 776 """Return whether the underlying player is still in the game.""" 777 return bool()
Return whether the underlying player is still in the game.
779 def get_icon(self) -> dict[str, Any]: 780 """Returns the character's icon (images, colors, etc contained 781 in a dict. 782 """ 783 return {'foo': 'bar'}
Returns the character's icon (images, colors, etc contained in a dict.
789 def get_v1_account_id(self) -> str: 790 """Return the V1 Account ID this player is signed in under, if 791 there is one and it can be determined with relative certainty. 792 Returns None otherwise. Note that this may require an active 793 internet connection (especially for network-connected players) 794 and may return None for a short while after a player initially 795 joins (while verification occurs). 796 """ 797 return str()
Return the V1 Account ID this player is signed in under, if there is one and it can be determined with relative certainty. Returns None otherwise. Note that this may require an active internet connection (especially for network-connected players) and may return None for a short while after a player initially joins (while verification occurs).
799 def getname(self, full: bool = False, icon: bool = True) -> str: 800 """Returns the player's name. If icon is True, the long version of the 801 name may include an icon. 802 """ 803 return str()
Returns the player's name. If icon is True, the long version of the name may include an icon.
809 def resetinput(self) -> None: 810 """Clears out the player's assigned input actions.""" 811 return None
Clears out the player's assigned input actions.
837 def setname( 838 self, name: str, full_name: str | None = None, real: bool = True 839 ) -> None: 840 """Set the player's name to the provided string. 841 A number will automatically be appended if the name is not unique from 842 other players. 843 """ 844 return None
Set the player's name to the provided string. A number will automatically be appended if the name is not unique from other players.
20class SessionTeam: 21 """A team of one or more bascenev1.SessionPlayers. 22 23 Category: **Gameplay Classes** 24 25 Note that a SessionPlayer *always* has a SessionTeam; 26 in some cases, such as free-for-all bascenev1.Sessions, 27 each SessionTeam consists of just one SessionPlayer. 28 """ 29 30 # Annotate our attr types at the class level so they're introspectable. 31 32 name: babase.Lstr | str 33 """The team's name.""" 34 35 color: tuple[float, ...] # FIXME: can't we make this fixed len? 36 """The team's color.""" 37 38 players: list[bascenev1.SessionPlayer] 39 """The list of bascenev1.SessionPlayer-s on the team.""" 40 41 customdata: dict 42 """A dict for use by the current bascenev1.Session for 43 storing data associated with this team. 44 Unlike customdata, this persists for the duration 45 of the session.""" 46 47 id: int 48 """The unique numeric id of the team.""" 49 50 def __init__( 51 self, 52 team_id: int = 0, 53 name: babase.Lstr | str = '', 54 color: Sequence[float] = (1.0, 1.0, 1.0), 55 ): 56 """Instantiate a bascenev1.SessionTeam. 57 58 In most cases, all teams are provided to you by the bascenev1.Session, 59 bascenev1.Session, so calling this shouldn't be necessary. 60 """ 61 62 self.id = team_id 63 self.name = name 64 self.color = tuple(color) 65 self.players = [] 66 self.customdata = {} 67 self.activityteam: Team | None = None 68 69 def leave(self) -> None: 70 """(internal)""" 71 self.customdata = {}
A team of one or more bascenev1.SessionPlayers.
Category: Gameplay Classes
Note that a SessionPlayer always has a SessionTeam; in some cases, such as free-for-all bascenev1.Sessions, each SessionTeam consists of just one SessionPlayer.
50 def __init__( 51 self, 52 team_id: int = 0, 53 name: babase.Lstr | str = '', 54 color: Sequence[float] = (1.0, 1.0, 1.0), 55 ): 56 """Instantiate a bascenev1.SessionTeam. 57 58 In most cases, all teams are provided to you by the bascenev1.Session, 59 bascenev1.Session, so calling this shouldn't be necessary. 60 """ 61 62 self.id = team_id 63 self.name = name 64 self.color = tuple(color) 65 self.players = [] 66 self.customdata = {} 67 self.activityteam: Team | None = None
Instantiate a bascenev1.SessionTeam.
In most cases, all teams are provided to you by the bascenev1.Session, bascenev1.Session, so calling this shouldn't be necessary.
A dict for use by the current bascenev1.Session for storing data associated with this team. Unlike customdata, this persists for the duration of the session.
1516def set_analytics_screen(screen: str) -> None: 1517 """Used for analytics to see where in the app players spend their time. 1518 1519 Category: **General Utility Functions** 1520 1521 Generally called when opening a new window or entering some UI. 1522 'screen' should be a string description of an app location 1523 ('Main Menu', etc.) 1524 """ 1525 return None
Used for analytics to see where in the app players spend their time.
Category: General Utility Functions
Generally called when opening a new window or entering some UI. 'screen' should be a string description of an app location ('Main Menu', etc.)
31def set_player_rejoin_cooldown(cooldown: float) -> None: 32 """Set the cooldown for individual players rejoining after leaving.""" 33 global _g_player_rejoin_cooldown # pylint: disable=global-statement 34 _g_player_rejoin_cooldown = max(0.0, cooldown)
Set the cooldown for individual players rejoining after leaving.
37def set_max_players_override(max_players: int | None) -> None: 38 """Set the override for how many players can join a session""" 39 global _max_players_override # pylint: disable=global-statement 40 _max_players_override = max_players
Set the override for how many players can join a session
51def setmusic(musictype: MusicType | None, continuous: bool = False) -> None: 52 """Set the app to play (or stop playing) a certain type of music. 53 54 category: **Gameplay Functions** 55 56 This function will handle loading and playing sound assets as necessary, 57 and also supports custom user soundtracks on specific platforms so the 58 user can override particular game music with their own. 59 60 Pass None to stop music. 61 62 if 'continuous' is True and musictype is the same as what is already 63 playing, the playing track will not be restarted. 64 """ 65 66 # All we do here now is set a few music attrs on the current globals 67 # node. The foreground globals' current playing music then gets fed to 68 # the do_play_music call in our music controller. This way we can 69 # seamlessly support custom soundtracks in replays/etc since we're being 70 # driven purely by node data. 71 gnode = _bascenev1.getactivity().globalsnode 72 gnode.music_continuous = continuous 73 gnode.music = '' if musictype is None else musictype.value 74 gnode.music_count += 1
Set the app to play (or stop playing) a certain type of music.
category: Gameplay Functions
This function will handle loading and playing sound assets as necessary, and also supports custom user soundtracks on specific platforms so the user can override particular game music with their own.
Pass None to stop music.
if 'continuous' is True and musictype is the same as what is already playing, the playing track will not be restarted.
15@dataclass 16class Setting: 17 """Defines a user-controllable setting for a game or other entity. 18 19 Category: Gameplay Classes 20 """ 21 22 name: str 23 default: Any
Defines a user-controllable setting for a game or other entity.
Category: Gameplay Classes
186@dataclass 187class ShouldShatterMessage: 188 """Tells an object that it should shatter. 189 190 Category: **Message Classes** 191 """
Tells an object that it should shatter.
Category: Message Classes
177def show_damage_count( 178 damage: str, position: Sequence[float], direction: Sequence[float] 179) -> None: 180 """Pop up a damage count at a position in space. 181 182 Category: **Gameplay Functions** 183 """ 184 lifespan = 1.0 185 app = babase.app 186 187 # FIXME: Should never vary game elements based on local config. 188 # (connected clients may have differing configs so they won't 189 # get the intended results). 190 assert app.classic is not None 191 do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr 192 txtnode = _bascenev1.newnode( 193 'text', 194 attrs={ 195 'text': damage, 196 'in_world': True, 197 'h_align': 'center', 198 'flatness': 1.0, 199 'shadow': 1.0 if do_big else 0.7, 200 'color': (1, 0.25, 0.25, 1), 201 'scale': 0.015 if do_big else 0.01, 202 }, 203 ) 204 # Translate upward. 205 tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3}) 206 tcombine.connectattr('output', txtnode, 'position') 207 v_vals = [] 208 pval = 0.0 209 vval = 0.07 210 count = 6 211 for i in range(count): 212 v_vals.append((float(i) / count, pval)) 213 pval += vval 214 vval *= 0.5 215 p_start = position[0] 216 p_dir = direction[0] 217 animate( 218 tcombine, 219 'input0', 220 {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, 221 ) 222 p_start = position[1] 223 p_dir = direction[1] 224 animate( 225 tcombine, 226 'input1', 227 {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, 228 ) 229 p_start = position[2] 230 p_dir = direction[2] 231 animate( 232 tcombine, 233 'input2', 234 {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, 235 ) 236 animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) 237 _bascenev1.timer(lifespan, txtnode.delete)
Pop up a damage count at a position in space.
Category: Gameplay Functions
851class Sound: 852 """A reference to a sound. 853 854 Category: **Asset Classes** 855 856 Use bascenev1.getsound() to instantiate one. 857 """ 858 859 def play( 860 self, 861 volume: float = 1.0, 862 position: Sequence[float] | None = None, 863 host_only: bool = False, 864 ) -> None: 865 """Play the sound a single time. 866 867 Category: **Gameplay Functions** 868 869 If position is not provided, the sound will be at a constant volume 870 everywhere. Position should be a float tuple of size 3. 871 """ 872 return None
859 def play( 860 self, 861 volume: float = 1.0, 862 position: Sequence[float] | None = None, 863 host_only: bool = False, 864 ) -> None: 865 """Play the sound a single time. 866 867 Category: **Gameplay Functions** 868 869 If position is not provided, the sound will be at a constant volume 870 everywhere. Position should be a float tuple of size 3. 871 """ 872 return None
Play the sound a single time.
Category: Gameplay Functions
If position is not provided, the sound will be at a constant volume everywhere. Position should be a float tuple of size 3.
37@dataclass 38class StandLocation: 39 """Describes a point in space and an angle to face. 40 41 Category: Gameplay Classes 42 """ 43 44 position: babase.Vec3 45 angle: float | None = None
Describes a point in space and an angle to face.
Category: Gameplay Classes
129@dataclass 130class StandMessage: 131 """A message telling an object to move to a position in space. 132 133 Category: **Message Classes** 134 135 Used when teleporting players to home base, etc. 136 """ 137 138 position: Sequence[float] = (0.0, 0.0, 0.0) 139 """Where to move to.""" 140 141 angle: float = 0.0 142 """The angle to face (in degrees)"""
A message telling an object to move to a position in space.
Category: Message Classes
Used when teleporting players to home base, etc.
256class Stats: 257 """Manages scores and statistics for a bascenev1.Session. 258 259 Category: **Gameplay Classes** 260 """ 261 262 def __init__(self) -> None: 263 self._activity: weakref.ref[bascenev1.Activity] | None = None 264 self._player_records: dict[str, PlayerRecord] = {} 265 self.orchestrahitsound1: bascenev1.Sound | None = None 266 self.orchestrahitsound2: bascenev1.Sound | None = None 267 self.orchestrahitsound3: bascenev1.Sound | None = None 268 self.orchestrahitsound4: bascenev1.Sound | None = None 269 270 def setactivity(self, activity: bascenev1.Activity | None) -> None: 271 """Set the current activity for this instance.""" 272 273 self._activity = None if activity is None else weakref.ref(activity) 274 275 # Load our media into this activity's context. 276 if activity is not None: 277 if activity.expired: 278 logging.exception('Unexpected finalized activity.') 279 else: 280 with activity.context: 281 self._load_activity_media() 282 283 def getactivity(self) -> bascenev1.Activity | None: 284 """Get the activity associated with this instance. 285 286 May return None. 287 """ 288 if self._activity is None: 289 return None 290 return self._activity() 291 292 def _load_activity_media(self) -> None: 293 self.orchestrahitsound1 = _bascenev1.getsound('orchestraHit') 294 self.orchestrahitsound2 = _bascenev1.getsound('orchestraHit2') 295 self.orchestrahitsound3 = _bascenev1.getsound('orchestraHit3') 296 self.orchestrahitsound4 = _bascenev1.getsound('orchestraHit4') 297 298 def reset(self) -> None: 299 """Reset the stats instance completely.""" 300 301 # Just to be safe, lets make sure no multi-kill timers are gonna go off 302 # for no-longer-on-the-list players. 303 for p_entry in list(self._player_records.values()): 304 p_entry.cancel_multi_kill_timer() 305 self._player_records = {} 306 307 def reset_accum(self) -> None: 308 """Reset per-sound sub-scores.""" 309 for s_player in list(self._player_records.values()): 310 s_player.cancel_multi_kill_timer() 311 s_player.accumscore = 0 312 s_player.accum_kill_count = 0 313 s_player.accum_killed_count = 0 314 s_player.streak = 0 315 316 def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None: 317 """Register a bascenev1.SessionPlayer with this score-set.""" 318 assert player.exists() # Invalid refs should never be passed to funcs. 319 name = player.getname() 320 if name in self._player_records: 321 # If the player already exists, update his character and such as 322 # it may have changed. 323 self._player_records[name].associate_with_sessionplayer(player) 324 else: 325 name_full = player.getname(full=True) 326 self._player_records[name] = PlayerRecord( 327 name, name_full, player, self 328 ) 329 330 def get_records(self) -> dict[str, bascenev1.PlayerRecord]: 331 """Get PlayerRecord corresponding to still-existing players.""" 332 records = {} 333 334 # Go through our player records and return ones whose player id still 335 # corresponds to a player with that name. 336 for record_id, record in self._player_records.items(): 337 lastplayer = record.get_last_sessionplayer() 338 if lastplayer and lastplayer.getname() == record_id: 339 records[record_id] = record 340 return records 341 342 def player_scored( 343 self, 344 player: bascenev1.Player, 345 base_points: int = 1, 346 *, 347 target: Sequence[float] | None = None, 348 kill: bool = False, 349 victim_player: bascenev1.Player | None = None, 350 scale: float = 1.0, 351 color: Sequence[float] | None = None, 352 title: str | babase.Lstr | None = None, 353 screenmessage: bool = True, 354 display: bool = True, 355 importance: int = 1, 356 showpoints: bool = True, 357 big_message: bool = False, 358 ) -> int: 359 """Register a score for the player. 360 361 Return value is actual score with multipliers and such factored in. 362 """ 363 # FIXME: Tidy this up. 364 # pylint: disable=cyclic-import 365 # pylint: disable=too-many-branches 366 # pylint: disable=too-many-locals 367 from bascenev1lib.actor.popuptext import PopupText 368 369 from bascenev1._gameactivity import GameActivity 370 371 del victim_player # Currently unused. 372 name = player.getname() 373 s_player = self._player_records[name] 374 375 if kill: 376 s_player.submit_kill(showpoints=showpoints) 377 378 display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) 379 380 if color is not None: 381 display_color = color 382 elif importance != 1: 383 display_color = (1.0, 1.0, 0.4, 1.0) 384 points = base_points 385 386 # If they want a big announcement, throw a zoom-text up there. 387 if display and big_message: 388 try: 389 assert self._activity is not None 390 activity = self._activity() 391 if isinstance(activity, GameActivity): 392 name_full = player.getname(full=True, icon=False) 393 activity.show_zoom_message( 394 babase.Lstr( 395 resource='nameScoresText', 396 subs=[('${NAME}', name_full)], 397 ), 398 color=babase.normalized_color(player.team.color), 399 ) 400 except Exception: 401 logging.exception('Error showing big_message.') 402 403 # If we currently have a actor, pop up a score over it. 404 if display and showpoints: 405 our_pos = player.node.position if player.node else None 406 if our_pos is not None: 407 if target is None: 408 target = our_pos 409 410 # If display-pos is *way* lower than us, raise it up 411 # (so we can still see scores from dudes that fell off cliffs). 412 display_pos = ( 413 target[0], 414 max(target[1], our_pos[1] - 2.0), 415 min(target[2], our_pos[2] + 2.0), 416 ) 417 activity = self.getactivity() 418 if activity is not None: 419 if title is not None: 420 sval = babase.Lstr( 421 value='+${A} ${B}', 422 subs=[('${A}', str(points)), ('${B}', title)], 423 ) 424 else: 425 sval = babase.Lstr( 426 value='+${A}', subs=[('${A}', str(points))] 427 ) 428 PopupText( 429 sval, 430 color=display_color, 431 scale=1.2 * scale, 432 position=display_pos, 433 ).autoretain() 434 435 # Tally kills. 436 if kill: 437 s_player.accum_kill_count += 1 438 s_player.kill_count += 1 439 440 # Report non-kill scorings. 441 try: 442 if screenmessage and not kill: 443 _bascenev1.broadcastmessage( 444 babase.Lstr( 445 resource='nameScoresText', subs=[('${NAME}', name)] 446 ), 447 top=True, 448 color=player.color, 449 image=player.get_icon(), 450 ) 451 except Exception: 452 logging.exception('Error announcing score.') 453 454 s_player.score += points 455 s_player.accumscore += points 456 457 # Inform a running game of the score. 458 if points != 0: 459 activity = self._activity() if self._activity is not None else None 460 if activity is not None: 461 activity.handlemessage(PlayerScoredMessage(score=points)) 462 463 return points 464 465 def player_was_killed( 466 self, 467 player: bascenev1.Player, 468 killed: bool = False, 469 killer: bascenev1.Player | None = None, 470 ) -> None: 471 """Should be called when a player is killed.""" 472 name = player.getname() 473 prec = self._player_records[name] 474 prec.streak = 0 475 if killed: 476 prec.accum_killed_count += 1 477 prec.killed_count += 1 478 try: 479 if killed and _bascenev1.getactivity().announce_player_deaths: 480 if killer is player: 481 _bascenev1.broadcastmessage( 482 babase.Lstr( 483 resource='nameSuicideText', subs=[('${NAME}', name)] 484 ), 485 top=True, 486 color=player.color, 487 image=player.get_icon(), 488 ) 489 elif killer is not None: 490 if killer.team is player.team: 491 _bascenev1.broadcastmessage( 492 babase.Lstr( 493 resource='nameBetrayedText', 494 subs=[ 495 ('${NAME}', killer.getname()), 496 ('${VICTIM}', name), 497 ], 498 ), 499 top=True, 500 color=killer.color, 501 image=killer.get_icon(), 502 ) 503 else: 504 _bascenev1.broadcastmessage( 505 babase.Lstr( 506 resource='nameKilledText', 507 subs=[ 508 ('${NAME}', killer.getname()), 509 ('${VICTIM}', name), 510 ], 511 ), 512 top=True, 513 color=killer.color, 514 image=killer.get_icon(), 515 ) 516 else: 517 _bascenev1.broadcastmessage( 518 babase.Lstr( 519 resource='nameDiedText', subs=[('${NAME}', name)] 520 ), 521 top=True, 522 color=player.color, 523 image=player.get_icon(), 524 ) 525 except Exception: 526 logging.exception('Error announcing kill.')
Manages scores and statistics for a bascenev1.Session.
Category: Gameplay Classes
270 def setactivity(self, activity: bascenev1.Activity | None) -> None: 271 """Set the current activity for this instance.""" 272 273 self._activity = None if activity is None else weakref.ref(activity) 274 275 # Load our media into this activity's context. 276 if activity is not None: 277 if activity.expired: 278 logging.exception('Unexpected finalized activity.') 279 else: 280 with activity.context: 281 self._load_activity_media()
Set the current activity for this instance.
283 def getactivity(self) -> bascenev1.Activity | None: 284 """Get the activity associated with this instance. 285 286 May return None. 287 """ 288 if self._activity is None: 289 return None 290 return self._activity()
Get the activity associated with this instance.
May return None.
298 def reset(self) -> None: 299 """Reset the stats instance completely.""" 300 301 # Just to be safe, lets make sure no multi-kill timers are gonna go off 302 # for no-longer-on-the-list players. 303 for p_entry in list(self._player_records.values()): 304 p_entry.cancel_multi_kill_timer() 305 self._player_records = {}
Reset the stats instance completely.
307 def reset_accum(self) -> None: 308 """Reset per-sound sub-scores.""" 309 for s_player in list(self._player_records.values()): 310 s_player.cancel_multi_kill_timer() 311 s_player.accumscore = 0 312 s_player.accum_kill_count = 0 313 s_player.accum_killed_count = 0 314 s_player.streak = 0
Reset per-sound sub-scores.
316 def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None: 317 """Register a bascenev1.SessionPlayer with this score-set.""" 318 assert player.exists() # Invalid refs should never be passed to funcs. 319 name = player.getname() 320 if name in self._player_records: 321 # If the player already exists, update his character and such as 322 # it may have changed. 323 self._player_records[name].associate_with_sessionplayer(player) 324 else: 325 name_full = player.getname(full=True) 326 self._player_records[name] = PlayerRecord( 327 name, name_full, player, self 328 )
Register a bascenev1.SessionPlayer with this score-set.
330 def get_records(self) -> dict[str, bascenev1.PlayerRecord]: 331 """Get PlayerRecord corresponding to still-existing players.""" 332 records = {} 333 334 # Go through our player records and return ones whose player id still 335 # corresponds to a player with that name. 336 for record_id, record in self._player_records.items(): 337 lastplayer = record.get_last_sessionplayer() 338 if lastplayer and lastplayer.getname() == record_id: 339 records[record_id] = record 340 return records
Get PlayerRecord corresponding to still-existing players.
342 def player_scored( 343 self, 344 player: bascenev1.Player, 345 base_points: int = 1, 346 *, 347 target: Sequence[float] | None = None, 348 kill: bool = False, 349 victim_player: bascenev1.Player | None = None, 350 scale: float = 1.0, 351 color: Sequence[float] | None = None, 352 title: str | babase.Lstr | None = None, 353 screenmessage: bool = True, 354 display: bool = True, 355 importance: int = 1, 356 showpoints: bool = True, 357 big_message: bool = False, 358 ) -> int: 359 """Register a score for the player. 360 361 Return value is actual score with multipliers and such factored in. 362 """ 363 # FIXME: Tidy this up. 364 # pylint: disable=cyclic-import 365 # pylint: disable=too-many-branches 366 # pylint: disable=too-many-locals 367 from bascenev1lib.actor.popuptext import PopupText 368 369 from bascenev1._gameactivity import GameActivity 370 371 del victim_player # Currently unused. 372 name = player.getname() 373 s_player = self._player_records[name] 374 375 if kill: 376 s_player.submit_kill(showpoints=showpoints) 377 378 display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) 379 380 if color is not None: 381 display_color = color 382 elif importance != 1: 383 display_color = (1.0, 1.0, 0.4, 1.0) 384 points = base_points 385 386 # If they want a big announcement, throw a zoom-text up there. 387 if display and big_message: 388 try: 389 assert self._activity is not None 390 activity = self._activity() 391 if isinstance(activity, GameActivity): 392 name_full = player.getname(full=True, icon=False) 393 activity.show_zoom_message( 394 babase.Lstr( 395 resource='nameScoresText', 396 subs=[('${NAME}', name_full)], 397 ), 398 color=babase.normalized_color(player.team.color), 399 ) 400 except Exception: 401 logging.exception('Error showing big_message.') 402 403 # If we currently have a actor, pop up a score over it. 404 if display and showpoints: 405 our_pos = player.node.position if player.node else None 406 if our_pos is not None: 407 if target is None: 408 target = our_pos 409 410 # If display-pos is *way* lower than us, raise it up 411 # (so we can still see scores from dudes that fell off cliffs). 412 display_pos = ( 413 target[0], 414 max(target[1], our_pos[1] - 2.0), 415 min(target[2], our_pos[2] + 2.0), 416 ) 417 activity = self.getactivity() 418 if activity is not None: 419 if title is not None: 420 sval = babase.Lstr( 421 value='+${A} ${B}', 422 subs=[('${A}', str(points)), ('${B}', title)], 423 ) 424 else: 425 sval = babase.Lstr( 426 value='+${A}', subs=[('${A}', str(points))] 427 ) 428 PopupText( 429 sval, 430 color=display_color, 431 scale=1.2 * scale, 432 position=display_pos, 433 ).autoretain() 434 435 # Tally kills. 436 if kill: 437 s_player.accum_kill_count += 1 438 s_player.kill_count += 1 439 440 # Report non-kill scorings. 441 try: 442 if screenmessage and not kill: 443 _bascenev1.broadcastmessage( 444 babase.Lstr( 445 resource='nameScoresText', subs=[('${NAME}', name)] 446 ), 447 top=True, 448 color=player.color, 449 image=player.get_icon(), 450 ) 451 except Exception: 452 logging.exception('Error announcing score.') 453 454 s_player.score += points 455 s_player.accumscore += points 456 457 # Inform a running game of the score. 458 if points != 0: 459 activity = self._activity() if self._activity is not None else None 460 if activity is not None: 461 activity.handlemessage(PlayerScoredMessage(score=points)) 462 463 return points
Register a score for the player.
Return value is actual score with multipliers and such factored in.
465 def player_was_killed( 466 self, 467 player: bascenev1.Player, 468 killed: bool = False, 469 killer: bascenev1.Player | None = None, 470 ) -> None: 471 """Should be called when a player is killed.""" 472 name = player.getname() 473 prec = self._player_records[name] 474 prec.streak = 0 475 if killed: 476 prec.accum_killed_count += 1 477 prec.killed_count += 1 478 try: 479 if killed and _bascenev1.getactivity().announce_player_deaths: 480 if killer is player: 481 _bascenev1.broadcastmessage( 482 babase.Lstr( 483 resource='nameSuicideText', subs=[('${NAME}', name)] 484 ), 485 top=True, 486 color=player.color, 487 image=player.get_icon(), 488 ) 489 elif killer is not None: 490 if killer.team is player.team: 491 _bascenev1.broadcastmessage( 492 babase.Lstr( 493 resource='nameBetrayedText', 494 subs=[ 495 ('${NAME}', killer.getname()), 496 ('${VICTIM}', name), 497 ], 498 ), 499 top=True, 500 color=killer.color, 501 image=killer.get_icon(), 502 ) 503 else: 504 _bascenev1.broadcastmessage( 505 babase.Lstr( 506 resource='nameKilledText', 507 subs=[ 508 ('${NAME}', killer.getname()), 509 ('${VICTIM}', name), 510 ], 511 ), 512 top=True, 513 color=killer.color, 514 image=killer.get_icon(), 515 ) 516 else: 517 _bascenev1.broadcastmessage( 518 babase.Lstr( 519 resource='nameDiedText', subs=[('${NAME}', name)] 520 ), 521 top=True, 522 color=player.color, 523 image=player.get_icon(), 524 ) 525 except Exception: 526 logging.exception('Error announcing kill.')
Should be called when a player is killed.
334def storagename(suffix: str | None = None) -> str: 335 """Generate a unique name for storing class data in shared places. 336 337 Category: **General Utility Functions** 338 339 This consists of a leading underscore, the module path at the 340 call site with dots replaced by underscores, the containing class's 341 qualified name, and the provided suffix. When storing data in public 342 places such as 'customdata' dicts, this minimizes the chance of 343 collisions with other similarly named classes. 344 345 Note that this will function even if called in the class definition. 346 347 ##### Examples 348 Generate a unique name for storage purposes: 349 >>> class MyThingie: 350 ... # This will give something like 351 ... # '_mymodule_submodule_mythingie_data'. 352 ... _STORENAME = babase.storagename('data') 353 ... 354 ... # Use that name to store some data in the Activity we were 355 ... # passed. 356 ... def __init__(self, activity): 357 ... activity.customdata[self._STORENAME] = {} 358 """ 359 frame = inspect.currentframe() 360 if frame is None: 361 raise RuntimeError('Cannot get current stack frame.') 362 fback = frame.f_back 363 364 # Note: We need to explicitly clear frame here to avoid a ref-loop 365 # that keeps all function-dicts in the stack alive until the next 366 # full GC cycle (the stack frame refers to this function's dict, 367 # which refers to the stack frame). 368 del frame 369 370 if fback is None: 371 raise RuntimeError('Cannot get parent stack frame.') 372 modulepath = fback.f_globals.get('__name__') 373 if modulepath is None: 374 raise RuntimeError('Cannot get parent stack module path.') 375 assert isinstance(modulepath, str) 376 qualname = fback.f_locals.get('__qualname__') 377 if qualname is not None: 378 assert isinstance(qualname, str) 379 fullpath = f'_{modulepath}_{qualname.lower()}' 380 else: 381 fullpath = f'_{modulepath}' 382 if suffix is not None: 383 fullpath = f'{fullpath}_{suffix}' 384 return fullpath.replace('.', '_')
Generate a unique name for storing class data in shared places.
Category: General Utility Functions
This consists of a leading underscore, the module path at the call site with dots replaced by underscores, the containing class's qualified name, and the provided suffix. When storing data in public places such as 'customdata' dicts, this minimizes the chance of collisions with other similarly named classes.
Note that this will function even if called in the class definition.
Examples
Generate a unique name for storage purposes:
>>> class MyThingie:
... # This will give something like
... # '_mymodule_submodule_mythingie_data'.
... _STORENAME = babase.storagename('data')
...
... # Use that name to store some data in the Activity we were
... # passed.
... def __init__(self, activity):
... activity.customdata[self._STORENAME] = {}
77class Team(Generic[PlayerT]): 78 """A team in a specific bascenev1.Activity. 79 80 Category: **Gameplay Classes** 81 82 These correspond to bascenev1.SessionTeam objects, but are created 83 per activity so that the activity can use its own custom team subclass. 84 """ 85 86 # Defining these types at the class level instead of in __init__ so 87 # that types are introspectable (these are still instance attrs). 88 players: list[PlayerT] 89 id: int 90 name: babase.Lstr | str 91 color: tuple[float, ...] # FIXME: can't we make this fixed length? 92 _sessionteam: weakref.ref[SessionTeam] 93 _expired: bool 94 _postinited: bool 95 _customdata: dict 96 97 # NOTE: avoiding having any __init__() here since it seems to not 98 # get called by default if a dataclass inherits from us. 99 100 def postinit(self, sessionteam: SessionTeam) -> None: 101 """Wire up a newly created SessionTeam. 102 103 (internal) 104 """ 105 106 # Sanity check; if a dataclass is created that inherits from us, 107 # it will define an equality operator by default which will break 108 # internal game logic. So complain loudly if we find one. 109 if type(self).__eq__ is not object.__eq__: 110 raise RuntimeError( 111 f'Team class {type(self)} defines an equality' 112 f' operator (__eq__) which will break internal' 113 f' logic. Please remove it.\n' 114 f'For dataclasses you can do "dataclass(eq=False)"' 115 f' in the class decorator.' 116 ) 117 118 self.players = [] 119 self._sessionteam = weakref.ref(sessionteam) 120 self.id = sessionteam.id 121 self.name = sessionteam.name 122 self.color = sessionteam.color 123 self._customdata = {} 124 self._expired = False 125 self._postinited = True 126 127 def manual_init( 128 self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...] 129 ) -> None: 130 """Manually init a team for uses such as bots.""" 131 self.id = team_id 132 self.name = name 133 self.color = color 134 self._customdata = {} 135 self._expired = False 136 self._postinited = True 137 138 @property 139 def customdata(self) -> dict: 140 """Arbitrary values associated with the team. 141 Though it is encouraged that most player values be properly defined 142 on the bascenev1.Team subclass, it may be useful for player-agnostic 143 objects to store values here. This dict is cleared when the team 144 leaves or expires so objects stored here will be disposed of at 145 the expected time, unlike the Team instance itself which may 146 continue to be referenced after it is no longer part of the game. 147 """ 148 assert self._postinited 149 assert not self._expired 150 return self._customdata 151 152 def leave(self) -> None: 153 """Called when the Team leaves a running game. 154 155 (internal) 156 """ 157 assert self._postinited 158 assert not self._expired 159 del self._customdata 160 del self.players 161 162 def expire(self) -> None: 163 """Called when the Team is expiring (due to the Activity expiring). 164 165 (internal) 166 """ 167 assert self._postinited 168 assert not self._expired 169 self._expired = True 170 171 try: 172 self.on_expire() 173 except Exception: 174 logging.exception('Error in on_expire for %s.', self) 175 176 del self._customdata 177 del self.players 178 179 def on_expire(self) -> None: 180 """Can be overridden to handle team expiration.""" 181 182 @property 183 def sessionteam(self) -> SessionTeam: 184 """Return the bascenev1.SessionTeam corresponding to this Team. 185 186 Throws a babase.SessionTeamNotFoundError if there is none. 187 """ 188 assert self._postinited 189 if self._sessionteam is not None: 190 sessionteam = self._sessionteam() 191 if sessionteam is not None: 192 return sessionteam 193 194 raise babase.SessionTeamNotFoundError()
A team in a specific bascenev1.Activity.
Category: Gameplay Classes
These correspond to bascenev1.SessionTeam objects, but are created per activity so that the activity can use its own custom team subclass.
127 def manual_init( 128 self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...] 129 ) -> None: 130 """Manually init a team for uses such as bots.""" 131 self.id = team_id 132 self.name = name 133 self.color = color 134 self._customdata = {} 135 self._expired = False 136 self._postinited = True
Manually init a team for uses such as bots.
138 @property 139 def customdata(self) -> dict: 140 """Arbitrary values associated with the team. 141 Though it is encouraged that most player values be properly defined 142 on the bascenev1.Team subclass, it may be useful for player-agnostic 143 objects to store values here. This dict is cleared when the team 144 leaves or expires so objects stored here will be disposed of at 145 the expected time, unlike the Team instance itself which may 146 continue to be referenced after it is no longer part of the game. 147 """ 148 assert self._postinited 149 assert not self._expired 150 return self._customdata
Arbitrary values associated with the team. Though it is encouraged that most player values be properly defined on the bascenev1.Team subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the team leaves or expires so objects stored here will be disposed of at the expected time, unlike the Team instance itself which may continue to be referenced after it is no longer part of the game.
182 @property 183 def sessionteam(self) -> SessionTeam: 184 """Return the bascenev1.SessionTeam corresponding to this Team. 185 186 Throws a babase.SessionTeamNotFoundError if there is none. 187 """ 188 assert self._postinited 189 if self._sessionteam is not None: 190 sessionteam = self._sessionteam() 191 if sessionteam is not None: 192 return sessionteam 193 194 raise babase.SessionTeamNotFoundError()
Return the bascenev1.SessionTeam corresponding to this Team.
Throws a babase.SessionTeamNotFoundError if there is none.
30class TeamGameActivity(GameActivity[PlayerT, TeamT]): 31 """Base class for teams and free-for-all mode games. 32 33 Category: **Gameplay Classes** 34 35 (Free-for-all is essentially just a special case where every 36 bascenev1.Player has their own bascenev1.Team) 37 """ 38 39 @override 40 @classmethod 41 def supports_session_type( 42 cls, sessiontype: type[bascenev1.Session] 43 ) -> bool: 44 """ 45 Class method override; 46 returns True for ba.DualTeamSessions and ba.FreeForAllSessions; 47 False otherwise. 48 """ 49 return issubclass(sessiontype, DualTeamSession) or issubclass( 50 sessiontype, FreeForAllSession 51 ) 52 53 def __init__(self, settings: dict): 54 super().__init__(settings) 55 56 # By default we don't show kill-points in free-for-all sessions. 57 # (there's usually some activity-specific score and we don't 58 # wanna confuse things) 59 if isinstance(self.session, FreeForAllSession): 60 self.show_kill_points = False 61 62 @override 63 def on_transition_in(self) -> None: 64 # pylint: disable=cyclic-import 65 from bascenev1._coopsession import CoopSession 66 from bascenev1lib.actor.controlsguide import ControlsGuide 67 68 super().on_transition_in() 69 70 # On the first game, show the controls UI momentarily. 71 # (unless we're being run in co-op mode, in which case we leave 72 # it up to them) 73 if not isinstance(self.session, CoopSession) and getattr( 74 self, 'show_controls_guide', True 75 ): 76 attrname = '_have_shown_ctrl_help_overlay' 77 if not getattr(self.session, attrname, False): 78 delay = 4.0 79 lifespan = 10.0 80 if self.slow_motion: 81 lifespan *= 0.3 82 ControlsGuide( 83 delay=delay, 84 lifespan=lifespan, 85 scale=0.8, 86 position=(380, 200), 87 bright=True, 88 ).autoretain() 89 setattr(self.session, attrname, True) 90 91 @override 92 def on_begin(self) -> None: 93 super().on_begin() 94 try: 95 # Award a few (classic) achievements. 96 if isinstance(self.session, FreeForAllSession): 97 if len(self.players) >= 2: 98 if babase.app.classic is not None: 99 babase.app.classic.ach.award_local_achievement( 100 'Free Loader' 101 ) 102 elif isinstance(self.session, DualTeamSession): 103 if len(self.players) >= 4: 104 if babase.app.classic is not None: 105 babase.app.classic.ach.award_local_achievement( 106 'Team Player' 107 ) 108 except Exception: 109 logging.exception('Error in on_begin.') 110 111 @override 112 def spawn_player_spaz( 113 self, 114 player: PlayerT, 115 position: Sequence[float] | None = None, 116 angle: float | None = None, 117 ) -> PlayerSpaz: 118 """ 119 Method override; spawns and wires up a standard bascenev1.PlayerSpaz 120 for a bascenev1.Player. 121 122 If position or angle is not supplied, a default will be chosen based 123 on the bascenev1.Player and their bascenev1.Team. 124 """ 125 if position is None: 126 # In teams-mode get our team-start-location. 127 if isinstance(self.session, DualTeamSession): 128 position = self.map.get_start_position(player.team.id) 129 else: 130 # Otherwise do free-for-all spawn locations. 131 position = self.map.get_ffa_start_position(self.players) 132 133 return super().spawn_player_spaz(player, position, angle) 134 135 # FIXME: need to unify these arguments with GameActivity.end() 136 def end( # type: ignore 137 self, 138 results: Any = None, 139 announce_winning_team: bool = True, 140 announce_delay: float = 0.1, 141 force: bool = False, 142 ) -> None: 143 """ 144 End the game and announce the single winning team 145 unless 'announce_winning_team' is False. 146 (for results without a single most-important winner). 147 """ 148 # pylint: disable=arguments-renamed 149 from bascenev1._coopsession import CoopSession 150 from bascenev1._multiteamsession import MultiTeamSession 151 152 # Announce win (but only for the first finish() call) 153 # (also don't announce in co-op sessions; we leave that up to them). 154 session = self.session 155 if not isinstance(session, CoopSession): 156 do_announce = not self.has_ended() 157 super().end(results, delay=2.0 + announce_delay, force=force) 158 159 # Need to do this *after* end end call so that results is valid. 160 assert isinstance(results, GameResults) 161 if do_announce and isinstance(session, MultiTeamSession): 162 session.announce_game_results( 163 self, 164 results, 165 delay=announce_delay, 166 announce_winning_team=announce_winning_team, 167 ) 168 169 # For co-op we just pass this up the chain with a delay added 170 # (in most cases). Team games expect a delay for the announce 171 # portion in teams/ffa mode so this keeps it consistent. 172 else: 173 # don't want delay on restarts.. 174 if ( 175 isinstance(results, dict) 176 and 'outcome' in results 177 and results['outcome'] == 'restart' 178 ): 179 delay = 0.0 180 else: 181 delay = 2.0 182 _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play) 183 super().end(results, delay=delay, force=force)
Base class for teams and free-for-all mode games.
Category: Gameplay Classes
(Free-for-all is essentially just a special case where every bascenev1.Player has their own bascenev1.Team)
53 def __init__(self, settings: dict): 54 super().__init__(settings) 55 56 # By default we don't show kill-points in free-for-all sessions. 57 # (there's usually some activity-specific score and we don't 58 # wanna confuse things) 59 if isinstance(self.session, FreeForAllSession): 60 self.show_kill_points = False
Instantiate the Activity.
39 @override 40 @classmethod 41 def supports_session_type( 42 cls, sessiontype: type[bascenev1.Session] 43 ) -> bool: 44 """ 45 Class method override; 46 returns True for ba.DualTeamSessions and ba.FreeForAllSessions; 47 False otherwise. 48 """ 49 return issubclass(sessiontype, DualTeamSession) or issubclass( 50 sessiontype, FreeForAllSession 51 )
Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.
62 @override 63 def on_transition_in(self) -> None: 64 # pylint: disable=cyclic-import 65 from bascenev1._coopsession import CoopSession 66 from bascenev1lib.actor.controlsguide import ControlsGuide 67 68 super().on_transition_in() 69 70 # On the first game, show the controls UI momentarily. 71 # (unless we're being run in co-op mode, in which case we leave 72 # it up to them) 73 if not isinstance(self.session, CoopSession) and getattr( 74 self, 'show_controls_guide', True 75 ): 76 attrname = '_have_shown_ctrl_help_overlay' 77 if not getattr(self.session, attrname, False): 78 delay = 4.0 79 lifespan = 10.0 80 if self.slow_motion: 81 lifespan *= 0.3 82 ControlsGuide( 83 delay=delay, 84 lifespan=lifespan, 85 scale=0.8, 86 position=(380, 200), 87 bright=True, 88 ).autoretain() 89 setattr(self.session, attrname, True)
Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.
91 @override 92 def on_begin(self) -> None: 93 super().on_begin() 94 try: 95 # Award a few (classic) achievements. 96 if isinstance(self.session, FreeForAllSession): 97 if len(self.players) >= 2: 98 if babase.app.classic is not None: 99 babase.app.classic.ach.award_local_achievement( 100 'Free Loader' 101 ) 102 elif isinstance(self.session, DualTeamSession): 103 if len(self.players) >= 4: 104 if babase.app.classic is not None: 105 babase.app.classic.ach.award_local_achievement( 106 'Team Player' 107 ) 108 except Exception: 109 logging.exception('Error in on_begin.')
Called once the previous Activity has finished transitioning out.
At this point the activity's initial players and teams are filled in and it should begin its actual game logic.
111 @override 112 def spawn_player_spaz( 113 self, 114 player: PlayerT, 115 position: Sequence[float] | None = None, 116 angle: float | None = None, 117 ) -> PlayerSpaz: 118 """ 119 Method override; spawns and wires up a standard bascenev1.PlayerSpaz 120 for a bascenev1.Player. 121 122 If position or angle is not supplied, a default will be chosen based 123 on the bascenev1.Player and their bascenev1.Team. 124 """ 125 if position is None: 126 # In teams-mode get our team-start-location. 127 if isinstance(self.session, DualTeamSession): 128 position = self.map.get_start_position(player.team.id) 129 else: 130 # Otherwise do free-for-all spawn locations. 131 position = self.map.get_ffa_start_position(self.players) 132 133 return super().spawn_player_spaz(player, position, angle)
Method override; spawns and wires up a standard bascenev1.PlayerSpaz for a bascenev1.Player.
If position or angle is not supplied, a default will be chosen based on the bascenev1.Player and their bascenev1.Team.
136 def end( # type: ignore 137 self, 138 results: Any = None, 139 announce_winning_team: bool = True, 140 announce_delay: float = 0.1, 141 force: bool = False, 142 ) -> None: 143 """ 144 End the game and announce the single winning team 145 unless 'announce_winning_team' is False. 146 (for results without a single most-important winner). 147 """ 148 # pylint: disable=arguments-renamed 149 from bascenev1._coopsession import CoopSession 150 from bascenev1._multiteamsession import MultiTeamSession 151 152 # Announce win (but only for the first finish() call) 153 # (also don't announce in co-op sessions; we leave that up to them). 154 session = self.session 155 if not isinstance(session, CoopSession): 156 do_announce = not self.has_ended() 157 super().end(results, delay=2.0 + announce_delay, force=force) 158 159 # Need to do this *after* end end call so that results is valid. 160 assert isinstance(results, GameResults) 161 if do_announce and isinstance(session, MultiTeamSession): 162 session.announce_game_results( 163 self, 164 results, 165 delay=announce_delay, 166 announce_winning_team=announce_winning_team, 167 ) 168 169 # For co-op we just pass this up the chain with a delay added 170 # (in most cases). Team games expect a delay for the announce 171 # portion in teams/ffa mode so this keeps it consistent. 172 else: 173 # don't want delay on restarts.. 174 if ( 175 isinstance(results, dict) 176 and 'outcome' in results 177 and results['outcome'] == 'restart' 178 ): 179 delay = 0.0 180 else: 181 delay = 2.0 182 _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play) 183 super().end(results, delay=delay, force=force)
End the game and announce the single winning team unless 'announce_winning_team' is False. (for results without a single most-important winner).
215@dataclass 216class ThawMessage: 217 """Tells an object to stop being frozen. 218 219 Category: **Message Classes** 220 """
Tells an object to stop being frozen.
Category: Message Classes
1726def time() -> bascenev1.Time: 1727 """Return the current scene time in seconds. 1728 1729 Category: **General Utility Functions** 1730 1731 Scene time maps to local simulation time in bascenev1.Activity or 1732 bascenev1.Session Contexts. This means that it may progress slower 1733 in slow-motion play modes, stop when the game is paused, etc. 1734 1735 Note that the value returned here is simply a float; it just has a 1736 unique type in the type-checker's eyes to help prevent it from being 1737 accidentally used with time functionality expecting other time types. 1738 """ 1739 import bascenev1 # pylint: disable=cyclic-import 1740 1741 return bascenev1.Time(0.0)
Return the current scene time in seconds.
Category: General Utility Functions
Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.
Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.
1745def timer(time: float, call: Callable[[], Any], repeat: bool = False) -> None: 1746 """Schedule a call to run at a later point in time. 1747 1748 Category: **General Utility Functions** 1749 1750 This function adds a scene-time timer to the current babase.Context. 1751 This timer cannot be canceled or modified once created. If you 1752 require the ability to do so, use the babase.Timer class instead. 1753 1754 Scene time maps to local simulation time in bascenev1.Activity or 1755 bascenev1.Session Contexts. This means that it may progress slower 1756 in slow-motion play modes, stop when the game is paused, etc. 1757 1758 ##### Arguments 1759 ###### time (float) 1760 > Length of scene time in seconds that the timer will wait 1761 before firing. 1762 1763 ###### call (Callable[[], Any]) 1764 > A callable Python object. Note that the timer will retain a 1765 strong reference to the callable for as long as it exists, so you 1766 may want to look into concepts such as babase.WeakCall if that is not 1767 desired. 1768 1769 ###### repeat (bool) 1770 > If True, the timer will fire repeatedly, with each successive 1771 firing having the same delay as the first. 1772 1773 ##### Examples 1774 Print some stuff through time: 1775 >>> import bascenev1 as bs 1776 >>> bs.screenmessage('hello from now!') 1777 >>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!')) 1778 >>> bs.timer(2.0, bs.Call(bs.screenmessage, 1779 ... 'hello from the future 2!')) 1780 """ 1781 return None
Schedule a call to run at a later point in time.
Category: General Utility Functions
This function adds a scene-time timer to the current babase.Context. This timer cannot be canceled or modified once created. If you require the ability to do so, use the babase.Timer class instead.
Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.
Arguments
time (float)
Length of scene time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat (bool)
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Examples
Print some stuff through time:
>>> import bascenev1 as bs
>>> bs.screenmessage('hello from now!')
>>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
>>> bs.timer(2.0, bs.Call(bs.screenmessage,
... 'hello from the future 2!'))
887class Timer: 888 """Timers are used to run code at later points in time. 889 890 Category: **General Utility Classes** 891 892 This class encapsulates a scene-time timer in the current 893 bascenev1.Context. The underlying timer will be destroyed when either 894 this object is no longer referenced or when its Context (Activity, 895 etc.) dies. If you do not want to worry about keeping a reference to 896 your timer around, 897 you should use the bs.timer() function instead. 898 899 Scene time maps to local simulation time in bascenev1.Activity or 900 bascenev1.Session Contexts. This means that it may progress slower 901 in slow-motion play modes, stop when the game is paused, etc. 902 903 ###### time 904 > Length of time (in seconds by default) that the timer will wait 905 before firing. Note that the actual delay experienced may vary 906 depending on the timetype. (see below) 907 908 ###### call 909 > A callable Python object. Note that the timer will retain a 910 strong reference to the callable for as long as it exists, so you 911 may want to look into concepts such as babase.WeakCall if that is not 912 desired. 913 914 ###### repeat 915 > If True, the timer will fire repeatedly, with each successive 916 firing having the same delay as the first. 917 918 ##### Example 919 920 Use a Timer object to print repeatedly for a few seconds: 921 >>> import bascenev1 as bs 922 ... def say_it(): 923 ... bs.screenmessage('BADGER!') 924 ... def stop_saying_it(): 925 ... global g_timer 926 ... g_timer = None 927 ... bs.screenmessage('MUSHROOM MUSHROOM!') 928 ... # Create our timer; it will run as long as we have the self.t ref. 929 ... g_timer = bs.Timer(0.3, say_it, repeat=True) 930 ... # Now fire off a one-shot timer to kill it. 931 ... bs.timer(3.89, stop_saying_it) 932 """ 933 934 def __init__( 935 self, time: float, call: Callable[[], Any], repeat: bool = False 936 ) -> None: 937 pass
Timers are used to run code at later points in time.
Category: General Utility Classes
This class encapsulates a scene-time timer in the current bascenev1.Context. The underlying timer will be destroyed when either this object is no longer referenced or when its Context (Activity, etc.) dies. If you do not want to worry about keeping a reference to your timer around, you should use the bs.timer() function instead.
Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.
time
Length of time (in seconds by default) that the timer will wait before firing. Note that the actual delay experienced may vary depending on the timetype. (see below)
call
A callable Python object. Note that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds:
>>> import bascenev1 as bs
... def say_it():
... bs.screenmessage('BADGER!')
... def stop_saying_it():
... global g_timer
... g_timer = None
... bs.screenmessage('MUSHROOM MUSHROOM!')
... # Create our timer; it will run as long as we have the self.t ref.
... g_timer = bs.Timer(0.3, say_it, repeat=True)
... # Now fire off a one-shot timer to kill it.
... bs.timer(3.89, stop_saying_it)
15def timestring( 16 timeval: float | int, 17 centi: bool = True, 18) -> babase.Lstr: 19 """Generate a babase.Lstr for displaying a time value. 20 21 Category: **General Utility Functions** 22 23 Given a time value, returns a babase.Lstr with: 24 (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True). 25 26 WARNING: the underlying Lstr value is somewhat large so don't use this 27 to rapidly update Node text values for an onscreen timer or you may 28 consume significant network bandwidth. For that purpose you should 29 use a 'timedisplay' Node and attribute connections. 30 31 """ 32 from babase._language import Lstr 33 34 # We take float seconds but operate on int milliseconds internally. 35 timeval = int(1000 * timeval) 36 bits = [] 37 subs = [] 38 hval = (timeval // 1000) // (60 * 60) 39 if hval != 0: 40 bits.append('${H}') 41 subs.append( 42 ( 43 '${H}', 44 Lstr( 45 resource='timeSuffixHoursText', 46 subs=[('${COUNT}', str(hval))], 47 ), 48 ) 49 ) 50 mval = ((timeval // 1000) // 60) % 60 51 if mval != 0: 52 bits.append('${M}') 53 subs.append( 54 ( 55 '${M}', 56 Lstr( 57 resource='timeSuffixMinutesText', 58 subs=[('${COUNT}', str(mval))], 59 ), 60 ) 61 ) 62 63 # We add seconds if its non-zero *or* we haven't added anything else. 64 if centi: 65 # pylint: disable=consider-using-f-string 66 sval = timeval / 1000.0 % 60.0 67 if sval >= 0.005 or not bits: 68 bits.append('${S}') 69 subs.append( 70 ( 71 '${S}', 72 Lstr( 73 resource='timeSuffixSecondsText', 74 subs=[('${COUNT}', ('%.2f' % sval))], 75 ), 76 ) 77 ) 78 else: 79 sval = timeval // 1000 % 60 80 if sval != 0 or not bits: 81 bits.append('${S}') 82 subs.append( 83 ( 84 '${S}', 85 Lstr( 86 resource='timeSuffixSecondsText', 87 subs=[('${COUNT}', str(sval))], 88 ), 89 ) 90 ) 91 return Lstr(value=' '.join(bits), subs=subs)
Generate a babase.Lstr for displaying a time value.
Category: General Utility Functions
Given a time value, returns a babase.Lstr with: (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
WARNING: the underlying Lstr value is somewhat large so don't use this to rapidly update Node text values for an onscreen timer or you may consume significant network bandwidth. For that purpose you should use a 'timedisplay' Node and attribute connections.
63class UIScale(Enum): 64 """The overall scale the UI is being rendered for. Note that this is 65 independent of pixel resolution. For example, a phone and a desktop PC 66 might render the game at similar pixel resolutions but the size they 67 display content at will vary significantly. 68 69 Category: Enums 70 71 'large' is used for devices such as desktop PCs where fine details can 72 be clearly seen. UI elements are generally smaller on the screen 73 and more content can be seen at once. 74 75 'medium' is used for devices such as tablets, TVs, or VR headsets. 76 This mode strikes a balance between clean readability and amount of 77 content visible. 78 79 'small' is used primarily for phones or other small devices where 80 content needs to be presented as large and clear in order to remain 81 readable from an average distance. 82 """ 83 84 SMALL = 0 85 MEDIUM = 1 86 LARGE = 2
The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.
Category: Enums
'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.
'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.
'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.
394class Vec3(Sequence[float]): 395 """A vector of 3 floats. 396 397 Category: **General Utility Classes** 398 399 These can be created the following ways (checked in this order): 400 - with no args, all values are set to 0 401 - with a single numeric arg, all values are set to that value 402 - with a single three-member sequence arg, sequence values are copied 403 - otherwise assumes individual x/y/z args (positional or keywords) 404 """ 405 406 x: float 407 """The vector's X component.""" 408 409 y: float 410 """The vector's Y component.""" 411 412 z: float 413 """The vector's Z component.""" 414 415 # pylint: disable=function-redefined 416 417 @overload 418 def __init__(self) -> None: 419 pass 420 421 @overload 422 def __init__(self, value: float): 423 pass 424 425 @overload 426 def __init__(self, values: Sequence[float]): 427 pass 428 429 @overload 430 def __init__(self, x: float, y: float, z: float): 431 pass 432 433 def __init__(self, *args: Any, **kwds: Any): 434 pass 435 436 def __add__(self, other: Vec3) -> Vec3: 437 return self 438 439 def __sub__(self, other: Vec3) -> Vec3: 440 return self 441 442 @overload 443 def __mul__(self, other: float) -> Vec3: 444 return self 445 446 @overload 447 def __mul__(self, other: Sequence[float]) -> Vec3: 448 return self 449 450 def __mul__(self, other: Any) -> Any: 451 return self 452 453 @overload 454 def __rmul__(self, other: float) -> Vec3: 455 return self 456 457 @overload 458 def __rmul__(self, other: Sequence[float]) -> Vec3: 459 return self 460 461 def __rmul__(self, other: Any) -> Any: 462 return self 463 464 # (for index access) 465 @override 466 def __getitem__(self, typeargs: Any) -> Any: 467 return 0.0 468 469 @override 470 def __len__(self) -> int: 471 return 3 472 473 # (for iterator access) 474 @override 475 def __iter__(self) -> Any: 476 return self 477 478 def __next__(self) -> float: 479 return 0.0 480 481 def __neg__(self) -> Vec3: 482 return self 483 484 def __setitem__(self, index: int, val: float) -> None: 485 pass 486 487 def cross(self, other: Vec3) -> Vec3: 488 """Returns the cross product of this vector and another.""" 489 return Vec3() 490 491 def dot(self, other: Vec3) -> float: 492 """Returns the dot product of this vector and another.""" 493 return float() 494 495 def length(self) -> float: 496 """Returns the length of the vector.""" 497 return float() 498 499 def normalized(self) -> Vec3: 500 """Returns a normalized version of the vector.""" 501 return Vec3()
A vector of 3 floats.
Category: General Utility Classes
These can be created the following ways (checked in this order):
- with no args, all values are set to 0
- with a single numeric arg, all values are set to that value
- with a single three-member sequence arg, sequence values are copied
- otherwise assumes individual x/y/z args (positional or keywords)
487 def cross(self, other: Vec3) -> Vec3: 488 """Returns the cross product of this vector and another.""" 489 return Vec3()
Returns the cross product of this vector and another.
491 def dot(self, other: Vec3) -> float: 492 """Returns the dot product of this vector and another.""" 493 return float()
Returns the dot product of this vector and another.