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 8 6 7# The stuff we expose here at the top level is our 'public' api for use 8# from other modules/packages. Code *within* this package should import 9# things from this package's submodules directly to reduce the chance of 10# dependency loops. The exception is TYPE_CHECKING blocks and 11# annotations since those aren't evaluated at runtime. 12 13import logging 14 15# Aside from our own stuff, we also bundle a number of things from ba or 16# other modules; the goal is to let most simple mods rely solely on this 17# module to keep things simple. 18 19from efro.util import set_canonical_module_names 20from babase import ( 21 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 Session.
Category: Gameplay Classes
Examples of Activities include games, score-screens, cutscenes, etc. A 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 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 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' 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 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 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 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 Actor to this Activity.
The reference will be lazily released once Actor.exists() returns False for the Actor. The 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))
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 Session this 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 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 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 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 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 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 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 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 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 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 Activity.on_player_join() for that.
527 def create_team(self, sessionteam: bascenev1.SessionTeam) -> TeamT: 528 """Create the Team instance for this Activity. 529 530 Subclasses can override this if the activity's team class 531 requires a custom constructor; otherwise it will be called with 532 no args. Note that the team object should not be used at this 533 point as it is not yet fully wired up; wait for on_team_join() 534 for that. 535 """ 536 del sessionteam # Unused. 537 team = self._teamtype() 538 return team
Create the Team instance for this Activity.
Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.
Inherited Members
30class Actor: 31 """High level logical entities in a bascenev1.Activity. 32 33 Category: **Gameplay Classes** 34 35 Actors act as controllers, combining some number of Nodes, Textures, 36 Sounds, etc. into a high-level cohesive unit. 37 38 Some example actors include the Bomb, Flag, and Spaz classes that 39 live in the bascenev1lib.actor.* modules. 40 41 One key feature of Actors is that they generally 'die' 42 (killing off or transitioning out their nodes) when the last Python 43 reference to them disappears, so you can use logic such as: 44 45 ##### Example 46 >>> # Create a flag Actor in our game activity: 47 ... from bascenev1lib.actor.flag import Flag 48 ... self.flag = Flag(position=(0, 10, 0)) 49 ... 50 ... # Later, destroy the flag. 51 ... # (provided nothing else is holding a reference to it) 52 ... # We could also just assign a new flag to this value. 53 ... # Either way, the old flag disappears. 54 ... self.flag = None 55 56 This is in contrast to the behavior of the more low level 57 bascenev1.Node, which is always explicitly created and destroyed 58 and doesn't care how many Python references to it exist. 59 60 Note, however, that you can use the bascenev1.Actor.autoretain() method 61 if you want an Actor to stick around until explicitly killed 62 regardless of references. 63 64 Another key feature of bascenev1.Actor is its 65 bascenev1.Actor.handlemessage() method, which takes a single arbitrary 66 object as an argument. This provides a safe way to communicate between 67 bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other 68 class providing a handlemessage() method. The most universally handled 69 message type for Actors is the bascenev1.DieMessage. 70 71 Another way to kill the flag from the example above: 72 We can safely call this on any type with a 'handlemessage' method 73 (though its not guaranteed to always have a meaningful effect). 74 In this case the Actor instance will still be around, but its 75 bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will 76 both return False. 77 >>> self.flag.handlemessage(bascenev1.DieMessage()) 78 """ 79 80 def __init__(self) -> None: 81 """Instantiates an Actor in the current bascenev1.Activity.""" 82 83 if __debug__: 84 self._root_actor_init_called = True 85 activity = _bascenev1.getactivity() 86 self._activity = weakref.ref(activity) 87 activity.add_actor_weak_ref(self) 88 89 def __del__(self) -> None: 90 try: 91 # Unexpired Actors send themselves a DieMessage when going down. 92 # That way we can treat DieMessage handling as the single 93 # point-of-action for death. 94 if not self.expired: 95 self.handlemessage(DieMessage()) 96 except Exception: 97 logging.exception( 98 'Error in bascenev1.Actor.__del__() for %s.', self 99 ) 100 101 def handlemessage(self, msg: Any) -> Any: 102 """General message handling; can be passed any message object.""" 103 assert not self.expired 104 105 # By default, actors going out-of-bounds simply kill themselves. 106 if isinstance(msg, OutOfBoundsMessage): 107 return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS)) 108 109 return UNHANDLED 110 111 def autoretain(self: ActorT) -> ActorT: 112 """Keep this Actor alive without needing to hold a reference to it. 113 114 This keeps the bascenev1.Actor in existence by storing a reference 115 to it with the bascenev1.Activity it was created in. The reference 116 is lazily released once bascenev1.Actor.exists() returns False for 117 it or when the Activity is set as expired. This can be a convenient 118 alternative to storing references explicitly just to keep a 119 bascenev1.Actor from dying. 120 For convenience, this method returns the bascenev1.Actor it is called 121 with, enabling chained statements such as: 122 myflag = bascenev1.Flag().autoretain() 123 """ 124 activity = self._activity() 125 if activity is None: 126 raise babase.ActivityNotFoundError() 127 activity.retain_actor(self) 128 return self 129 130 def on_expire(self) -> None: 131 """Called for remaining `bascenev1.Actor`s when their activity dies. 132 133 Actors can use this opportunity to clear callbacks or other 134 references which have the potential of keeping the bascenev1.Activity 135 alive inadvertently (Activities can not exit cleanly while 136 any Python references to them remain.) 137 138 Once an actor is expired (see bascenev1.Actor.is_expired()) it should 139 no longer perform any game-affecting operations (creating, modifying, 140 or deleting nodes, media, timers, etc.) Attempts to do so will 141 likely result in errors. 142 """ 143 144 @property 145 def expired(self) -> bool: 146 """Whether the Actor is expired. 147 148 (see bascenev1.Actor.on_expire()) 149 """ 150 activity = self.getactivity(doraise=False) 151 return True if activity is None else activity.expired 152 153 def exists(self) -> bool: 154 """Returns whether the Actor is still present in a meaningful way. 155 156 Note that a dying character should still return True here as long as 157 their corpse is visible; this is about presence, not being 'alive' 158 (see bascenev1.Actor.is_alive() for that). 159 160 If this returns False, it is assumed the Actor can be completely 161 deleted without affecting the game; this call is often used 162 when pruning lists of Actors, such as with bascenev1.Actor.autoretain() 163 164 The default implementation of this method always return True. 165 166 Note that the boolean operator for the Actor class calls this method, 167 so a simple "if myactor" test will conveniently do the right thing 168 even if myactor is set to None. 169 """ 170 return True 171 172 def __bool__(self) -> bool: 173 # Cleaner way to test existence; friendlier to None values. 174 return self.exists() 175 176 def is_alive(self) -> bool: 177 """Returns whether the Actor is 'alive'. 178 179 What this means is up to the Actor. 180 It is not a requirement for Actors to be able to die; 181 just that they report whether they consider themselves 182 to be alive or not. In cases where dead/alive is 183 irrelevant, True should be returned. 184 """ 185 return True 186 187 @property 188 def activity(self) -> bascenev1.Activity: 189 """The Activity this Actor was created in. 190 191 Raises a bascenev1.ActivityNotFoundError if the Activity no longer 192 exists. 193 """ 194 activity = self._activity() 195 if activity is None: 196 raise babase.ActivityNotFoundError() 197 return activity 198 199 # Overloads to convey our exact return type depending on 'doraise' value. 200 201 @overload 202 def getactivity( 203 self, doraise: Literal[True] = True 204 ) -> bascenev1.Activity: ... 205 206 @overload 207 def getactivity( 208 self, doraise: Literal[False] 209 ) -> bascenev1.Activity | None: ... 210 211 def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None: 212 """Return the bascenev1.Activity this Actor is associated with. 213 214 If the Activity no longer exists, raises a 215 bascenev1.ActivityNotFoundError or returns None depending on whether 216 'doraise' is True. 217 """ 218 activity = self._activity() 219 if activity is None and doraise: 220 raise babase.ActivityNotFoundError() 221 return activity
High level logical entities in a 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 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 Actor.autoretain() method if you want an Actor to stick around until explicitly killed regardless of references.
Another key feature of Actor is its Actor.handlemessage() method, which takes a single arbitrary object as an argument. This provides a safe way to communicate between Actor, Activity, Session, and any other class providing a handlemessage() method. The most universally handled message type for Actors is the 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 Actor.exists() and Actor.is_alive() methods will both return False.
>>> self.flag.handlemessage(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 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 Actor in existence by storing a reference to it with the Activity it was created in. The reference is lazily released once 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 Actor from dying. For convenience, this method returns the 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 Actor
s when their activity dies.
Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the 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.
(see Actor.on_expire())
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 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 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 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.
51def animate( 52 node: bascenev1.Node, 53 attr: str, 54 keys: dict[float, float], 55 loop: bool = False, 56 offset: float = 0, 57) -> bascenev1.Node: 58 """Animate values on a target bascenev1.Node. 59 60 Category: **Gameplay Functions** 61 62 Creates an 'animcurve' node with the provided values and time as an input, 63 connect it to the provided attribute, and set it to die with the target. 64 Key values are provided as time:value dictionary pairs. Time values are 65 relative to the current time. By default, times are specified in seconds, 66 but timeformat can also be set to MILLISECONDS to recreate the old behavior 67 (prior to ba 1.5) of taking milliseconds. Returns the animcurve node. 68 """ 69 items = list(keys.items()) 70 items.sort() 71 72 curve = _bascenev1.newnode( 73 'animcurve', 74 owner=node, 75 name='Driving ' + str(node) + ' \'' + attr + '\'', 76 ) 77 78 # We take seconds but operate on milliseconds internally. 79 mult = 1000 80 81 curve.times = [int(mult * time) for time, val in items] 82 curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset) 83 curve.values = [val for time, val in items] 84 curve.loop = loop 85 86 # If we're not looping, set a timer to kill this curve 87 # after its done its job. 88 # FIXME: Even if we are looping we should have a way to die once we 89 # get disconnected. 90 if not loop: 91 # noinspection PyUnresolvedReferences 92 _bascenev1.timer( 93 (int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete 94 ) 95 96 # Do the connects last so all our attrs are in place when we push initial 97 # values through. 98 99 # We operate in either activities or sessions.. 100 try: 101 globalsnode = _bascenev1.getactivity().globalsnode 102 except babase.ActivityNotFoundError: 103 globalsnode = _bascenev1.getsession().sessionglobalsnode 104 105 globalsnode.connectattr('time', curve, 'in') 106 curve.connectattr('out', node, attr) 107 return curve
Animate values on a target 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.
110def animate_array( 111 node: bascenev1.Node, 112 attr: str, 113 size: int, 114 keys: dict[float, Sequence[float]], 115 loop: bool = False, 116 offset: float = 0, 117) -> None: 118 """Animate an array of values on a target bascenev1.Node. 119 120 Category: **Gameplay Functions** 121 122 Like bs.animate, but operates on array attributes. 123 """ 124 combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size}) 125 items = list(keys.items()) 126 items.sort() 127 128 # We take seconds but operate on milliseconds internally. 129 mult = 1000 130 131 # We operate in either activities or sessions.. 132 try: 133 globalsnode = _bascenev1.getactivity().globalsnode 134 except babase.ActivityNotFoundError: 135 globalsnode = _bascenev1.getsession().sessionglobalsnode 136 137 for i in range(size): 138 curve = _bascenev1.newnode( 139 'animcurve', 140 owner=node, 141 name=( 142 'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i) 143 ), 144 ) 145 globalsnode.connectattr('time', curve, 'in') 146 curve.times = [int(mult * time) for time, val in items] 147 curve.values = [val[i] for time, val in items] 148 curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset) 149 curve.loop = loop 150 curve.connectattr('out', combine, 'input' + str(i)) 151 152 # If we're not looping, set a timer to kill this 153 # curve after its done its job. 154 if not loop: 155 # (PyCharm seems to think item is a float, not a tuple) 156 # noinspection PyUnresolvedReferences 157 _bascenev1.timer( 158 (int(mult * items[-1][0]) + 1000) / 1000.0, 159 curve.delete, 160 ) 161 combine.connectattr('output', node, attr) 162 163 # If we're not looping, set a timer to kill the combine once 164 # the job is done. 165 # FIXME: Even if we are looping we should have a way to die 166 # once we get disconnected. 167 if not loop: 168 # (PyCharm seems to think item is a float, not a tuple) 169 # noinspection PyUnresolvedReferences 170 _bascenev1.timer( 171 (int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete 172 )
Animate an array of values on a target 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 babase.app.active changes. 59 60 The app-mode may want to take action such as pausing a running 61 game in such cases. 62 """
A high level mode for the app.
Category: App Classes
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 babase.app.active changes. 59 60 The app-mode may want to take action such as pausing a running 61 game in such cases. 62 """
Called when babase.app.active changes.
The app-mode may want to take action such as pausing a running game in such cases.
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 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 WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> screenmessage('hello from now!')
>>> apptimer(1.0, Call(screenmessage,
'hello from the future!'))
>>> apptimer(2.0, Call(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 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 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(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... 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)
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 Texture from the AssetPackage.
Behavior is similar to gettexture()
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 CollisionMesh from the AssetPackage.
Behavior is similar to 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 Sound from the AssetPackage.
Behavior is similar to getsound()
Inherited Members
940def basetime() -> bascenev1.BaseTime: 941 """Return the base-time in seconds for the current scene-v1 context. 942 943 Category: **General Utility Functions** 944 945 Base-time is a time value that progresses at a constant rate for a scene, 946 even when the scene is sped up, slowed down, or paused. It may, however, 947 speed up or slow down due to replay speed adjustments or may slow down 948 if the cpu is overloaded. 949 Note that the value returned here is simply a float; it just has a 950 unique type in the type-checker's eyes to help prevent it from being 951 accidentally used with time functionality expecting other time types. 952 """ 953 import bascenev1 # pylint: disable=cyclic-import 954 955 return bascenev1.BaseTime(0.0)
Return the base-time in seconds for the current scene-v1 context.
Category: General Utility Functions
Base-time is a time value that progresses at a constant rate for a scene, even when the scene is sped up, slowed down, or paused. It may, however, speed up or slow down due to replay speed adjustments or may slow down if the cpu is overloaded. Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.
960def basetimer( 961 time: float, call: Callable[[], Any], repeat: bool = False 962) -> None: 963 """Schedule a call to run at a later point in scene base-time. 964 Base-time is a value that progresses at a constant rate for a scene, 965 even when the scene is sped up, slowed down, or paused. It may, 966 however, speed up or slow down due to replay speed adjustments or may 967 slow down if the cpu is overloaded. 968 969 Category: **General Utility Functions** 970 971 This function adds a timer to the current scene context. 972 This timer cannot be canceled or modified once created. If you 973 require the ability to do so, use the bascenev1.BaseTimer class 974 instead. 975 976 ##### Arguments 977 ###### time (float) 978 > Length of time in seconds that the timer will wait before firing. 979 980 ###### call (Callable[[], Any]) 981 > A callable Python object. Remember that the timer will retain a 982 strong reference to the callable for the duration of the timer, so you 983 may want to look into concepts such as babase.WeakCall if that is not 984 desired. 985 986 ###### repeat (bool) 987 > If True, the timer will fire repeatedly, with each successive 988 firing having the same delay as the first. 989 990 ##### Examples 991 Print some stuff through time: 992 >>> import bascenev1 as bs 993 >>> bs.screenmessage('hello from now!') 994 >>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!')) 995 >>> bs.basetimer(2.0, bs.Call(bs.screenmessage, 996 ... 'hello from the future 2!')) 997 """ 998 return None
Schedule a call to run at a later point in scene base-time. Base-time is a value that progresses at a constant rate for a scene, even when the scene is sped up, slowed down, or paused. It may, however, speed up or slow down due to replay speed adjustments or may slow down if the cpu is overloaded.
Category: General Utility Functions
This function adds a timer to the current scene context. This timer cannot be canceled or modified once created. If you require the ability to do so, use the 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 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 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 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
238def cameraflash(duration: float = 999.0) -> None: 239 """Create a strobing camera flash effect. 240 241 Category: **Gameplay Functions** 242 243 (as seen when a team wins a game) 244 Duration is in seconds. 245 """ 246 # pylint: disable=too-many-locals 247 import random 248 from bascenev1._nodeactor import NodeActor 249 250 x_spread = 10 251 y_spread = 5 252 positions = [ 253 [-x_spread, -y_spread], 254 [0, -y_spread], 255 [0, y_spread], 256 [x_spread, -y_spread], 257 [x_spread, y_spread], 258 [-x_spread, y_spread], 259 ] 260 times = [0, 2700, 1000, 1800, 500, 1400] 261 262 # Store this on the current activity so we only have one at a time. 263 # FIXME: Need a type safe way to do this. 264 activity = _bascenev1.getactivity() 265 activity.camera_flash_data = [] # type: ignore 266 for i in range(6): 267 light = NodeActor( 268 _bascenev1.newnode( 269 'light', 270 attrs={ 271 'position': (positions[i][0], 0, positions[i][1]), 272 'radius': 1.0, 273 'lights_volumes': False, 274 'height_attenuated': False, 275 'color': (0.2, 0.2, 0.8), 276 }, 277 ) 278 ) 279 sval = 1.87 280 iscale = 1.3 281 tcombine = _bascenev1.newnode( 282 'combine', 283 owner=light.node, 284 attrs={ 285 'size': 3, 286 'input0': positions[i][0], 287 'input1': 0, 288 'input2': positions[i][1], 289 }, 290 ) 291 assert light.node 292 tcombine.connectattr('output', light.node, 'position') 293 xval = positions[i][0] 294 yval = positions[i][1] 295 spd = 0.5 + random.random() 296 spd2 = 0.5 + random.random() 297 animate( 298 tcombine, 299 'input0', 300 { 301 0.0: xval + 0, 302 0.069 * spd: xval + 10.0, 303 0.143 * spd: xval - 10.0, 304 0.201 * spd: xval + 0, 305 }, 306 loop=True, 307 ) 308 animate( 309 tcombine, 310 'input2', 311 { 312 0.0: yval + 0, 313 0.15 * spd2: yval + 10.0, 314 0.287 * spd2: yval - 10.0, 315 0.398 * spd2: yval + 0, 316 }, 317 loop=True, 318 ) 319 animate( 320 light.node, 321 'intensity', 322 { 323 0.0: 0, 324 0.02 * sval: 0, 325 0.05 * sval: 0.8 * iscale, 326 0.08 * sval: 0, 327 0.1 * sval: 0, 328 }, 329 loop=True, 330 offset=times[i], 331 ) 332 _bascenev1.timer( 333 (times[i] + random.randint(1, int(duration)) * 40 * sval) / 1000.0, 334 light.node.delete, 335 ) 336 activity.camera_flash_data.append(light) # type: ignore
Create a strobing camera flash effect.
Category: Gameplay Functions
(as seen when a team wins a game) Duration is in seconds.
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 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 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 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 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 classic.profile_browser_window(in_main_menu=False) 593 594 # Give their input-device UI ownership too 595 # (prevent someone else from snatching it in crowded games) 596 babase.set_ui_input_device(self._sessionplayer.inputdevice.id) 597 return 598 599 if not ready: 600 self._sessionplayer.assigninput( 601 babase.InputType.LEFT_PRESS, 602 babase.Call(self.handlemessage, ChangeMessage('team', -1)), 603 ) 604 self._sessionplayer.assigninput( 605 babase.InputType.RIGHT_PRESS, 606 babase.Call(self.handlemessage, ChangeMessage('team', 1)), 607 ) 608 self._sessionplayer.assigninput( 609 babase.InputType.BOMB_PRESS, 610 babase.Call(self.handlemessage, ChangeMessage('character', 1)), 611 ) 612 self._sessionplayer.assigninput( 613 babase.InputType.UP_PRESS, 614 babase.Call( 615 self.handlemessage, ChangeMessage('profileindex', -1) 616 ), 617 ) 618 self._sessionplayer.assigninput( 619 babase.InputType.DOWN_PRESS, 620 babase.Call( 621 self.handlemessage, ChangeMessage('profileindex', 1) 622 ), 623 ) 624 self._sessionplayer.assigninput( 625 ( 626 babase.InputType.JUMP_PRESS, 627 babase.InputType.PICK_UP_PRESS, 628 babase.InputType.PUNCH_PRESS, 629 ), 630 babase.Call(self.handlemessage, ChangeMessage('ready', 1)), 631 ) 632 self._ready = False 633 self._update_text() 634 self._sessionplayer.setname('untitled', real=False) 635 else: 636 self._sessionplayer.assigninput( 637 ( 638 babase.InputType.LEFT_PRESS, 639 babase.InputType.RIGHT_PRESS, 640 babase.InputType.UP_PRESS, 641 babase.InputType.DOWN_PRESS, 642 babase.InputType.JUMP_PRESS, 643 babase.InputType.BOMB_PRESS, 644 babase.InputType.PICK_UP_PRESS, 645 ), 646 self._do_nothing, 647 ) 648 self._sessionplayer.assigninput( 649 ( 650 babase.InputType.JUMP_PRESS, 651 babase.InputType.BOMB_PRESS, 652 babase.InputType.PICK_UP_PRESS, 653 babase.InputType.PUNCH_PRESS, 654 ), 655 babase.Call(self.handlemessage, ChangeMessage('ready', 0)), 656 ) 657 658 # Store the last profile picked by this input for reuse. 659 input_device = self._sessionplayer.inputdevice 660 name = input_device.name 661 unique_id = input_device.unique_identifier 662 device_profiles = babase.app.config.setdefault( 663 'Default Player Profiles', {} 664 ) 665 666 # Make an exception if we have no custom profiles and are set 667 # to random; in that case we'll want to start picking up custom 668 # profiles if/when one is made so keep our setting cleared. 669 special = ('_random', '_edit', '__account__') 670 have_custom_profiles = any(p not in special for p in self._profiles) 671 672 profilekey = name + ' ' + unique_id 673 if profilename == '_random' and not have_custom_profiles: 674 if profilekey in device_profiles: 675 del device_profiles[profilekey] 676 else: 677 device_profiles[profilekey] = profilename 678 babase.app.config.commit() 679 680 # Set this player's short and full name. 681 self._sessionplayer.setname( 682 self._getname(), self._getname(full=True), real=True 683 ) 684 self._ready = True 685 self._update_text() 686 687 # Inform the session that this player is ready. 688 _bascenev1.getsession().handlemessage(PlayerReadyMessage(self)) 689 690 def _handle_ready_msg(self, ready: bool) -> None: 691 force_team_switch = False 692 693 # Team auto-balance kicks us to another team if we try to 694 # join the team with the most players. 695 if not self._ready: 696 if babase.app.config.get('Auto Balance Teams', False): 697 lobby = self.lobby 698 sessionteams = lobby.sessionteams 699 if len(sessionteams) > 1: 700 # First, calc how many players are on each team 701 # ..we need to count both active players and 702 # choosers that have been marked as ready. 703 team_player_counts = {} 704 for sessionteam in sessionteams: 705 team_player_counts[sessionteam.id] = len( 706 sessionteam.players 707 ) 708 for chooser in lobby.choosers: 709 if chooser.ready: 710 team_player_counts[chooser.sessionteam.id] += 1 711 largest_team_size = max(team_player_counts.values()) 712 smallest_team_size = min(team_player_counts.values()) 713 714 # Force switch if we're on the biggest sessionteam 715 # and there's a smaller one available. 716 if ( 717 largest_team_size != smallest_team_size 718 and team_player_counts[self.sessionteam.id] 719 >= largest_team_size 720 ): 721 force_team_switch = True 722 723 # Either force switch teams, or actually for realsies do the set-ready. 724 if force_team_switch: 725 self._errorsound.play() 726 self.handlemessage(ChangeMessage('team', 1)) 727 else: 728 self._punchsound.play() 729 self._set_ready(ready) 730 731 # TODO: should handle this at the engine layer so this is unnecessary. 732 def _handle_repeat_message_attack(self) -> None: 733 now = babase.apptime() 734 count = self._last_change[1] 735 if now - self._last_change[0] < QUICK_CHANGE_INTERVAL: 736 count += 1 737 if count > MAX_QUICK_CHANGE_COUNT: 738 _bascenev1.disconnect_client( 739 self._sessionplayer.inputdevice.client_id 740 ) 741 elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL: 742 count = 0 743 self._last_change = (now, count) 744 745 def handlemessage(self, msg: Any) -> Any: 746 """Standard generic message handler.""" 747 748 if isinstance(msg, ChangeMessage): 749 self._handle_repeat_message_attack() 750 751 # If we've been removed from the lobby, ignore this stuff. 752 if self._dead: 753 logging.error('chooser got ChangeMessage after dying') 754 return 755 756 if not self._text_node: 757 logging.error('got ChangeMessage after nodes died') 758 return 759 760 if msg.what == 'team': 761 sessionteams = self.lobby.sessionteams 762 if len(sessionteams) > 1: 763 self._swish_sound.play() 764 self._selected_team_index = ( 765 self._selected_team_index + msg.value 766 ) % len(sessionteams) 767 self._update_text() 768 self.update_position() 769 self._update_icon() 770 771 elif msg.what == 'profileindex': 772 if len(self._profilenames) == 1: 773 # This should be pretty hard to hit now with 774 # automatic local accounts. 775 _bascenev1.getsound('error').play() 776 else: 777 # Pick the next player profile and assign our name 778 # and character based on that. 779 self._deek_sound.play() 780 self._profileindex = (self._profileindex + msg.value) % len( 781 self._profilenames 782 ) 783 self.update_from_profile() 784 785 elif msg.what == 'character': 786 self._click_sound.play() 787 # update our index in our local list of characters 788 self._character_index = ( 789 self._character_index + msg.value 790 ) % len(self._character_names) 791 self._update_text() 792 self._update_icon() 793 794 elif msg.what == 'ready': 795 self._handle_ready_msg(bool(msg.value)) 796 797 def _update_text(self) -> None: 798 assert self._text_node is not None 799 if self._ready: 800 # Once we're ready, we've saved the name, so lets ask the system 801 # for it so we get appended numbers and stuff. 802 text = babase.Lstr(value=self._sessionplayer.getname(full=True)) 803 text = babase.Lstr( 804 value='${A} (${B})', 805 subs=[ 806 ('${A}', text), 807 ('${B}', babase.Lstr(resource='readyText')), 808 ], 809 ) 810 else: 811 text = babase.Lstr(value=self._getname(full=True)) 812 813 can_switch_teams = len(self.lobby.sessionteams) > 1 814 815 # Flash as we're coming in. 816 fin_color = babase.safecolor(self.get_color()) + (1,) 817 if not self._inited: 818 animate_array( 819 self._text_node, 820 'color', 821 4, 822 {0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color}, 823 ) 824 else: 825 # Blend if we're in teams mode; switch instantly otherwise. 826 if can_switch_teams: 827 animate_array( 828 self._text_node, 829 'color', 830 4, 831 {0: self._text_node.color, 0.1: fin_color}, 832 ) 833 else: 834 self._text_node.color = fin_color 835 836 self._text_node.text = text 837 838 def get_color(self) -> Sequence[float]: 839 """Return the currently selected color.""" 840 val: Sequence[float] 841 if self.lobby.use_team_colors: 842 val = self.lobby.sessionteams[self._selected_team_index].color 843 else: 844 val = self._color 845 if len(val) != 3: 846 print('get_color: ignoring invalid color of len', len(val)) 847 val = (0, 1, 0) 848 return val 849 850 def get_highlight(self) -> Sequence[float]: 851 """Return the currently selected highlight.""" 852 if self._profilenames[self._profileindex] == '_edit': 853 return 0, 1, 0 854 855 # If we're using team colors we wanna make sure our highlight color 856 # isn't too close to any other team's color. 857 highlight = list(self._highlight) 858 if self.lobby.use_team_colors: 859 for i, sessionteam in enumerate(self.lobby.sessionteams): 860 if i != self._selected_team_index: 861 # Find the dominant component of this sessionteam's color 862 # and adjust ours so that the component is 863 # not super-dominant. 864 max_val = 0.0 865 max_index = 0 866 for j in range(3): 867 if sessionteam.color[j] > max_val: 868 max_val = sessionteam.color[j] 869 max_index = j 870 that_color_for_us = highlight[max_index] 871 our_second_biggest = max( 872 highlight[(max_index + 1) % 3], 873 highlight[(max_index + 2) % 3], 874 ) 875 diff = that_color_for_us - our_second_biggest 876 if diff > 0: 877 highlight[max_index] -= diff * 0.6 878 highlight[(max_index + 1) % 3] += diff * 0.3 879 highlight[(max_index + 2) % 3] += diff * 0.2 880 return highlight 881 882 def getplayer(self) -> bascenev1.SessionPlayer: 883 """Return the player associated with this chooser.""" 884 return self._sessionplayer 885 886 def _update_icon(self) -> None: 887 assert babase.app.classic is not None 888 if self._profilenames[self._profileindex] == '_edit': 889 tex = _bascenev1.gettexture('black') 890 tint_tex = _bascenev1.gettexture('black') 891 self.icon.color = (1, 1, 1) 892 self.icon.texture = tex 893 self.icon.tint_texture = tint_tex 894 self.icon.tint_color = (0, 1, 0) 895 return 896 897 try: 898 tex_name = babase.app.classic.spaz_appearances[ 899 self._character_names[self._character_index] 900 ].icon_texture 901 tint_tex_name = babase.app.classic.spaz_appearances[ 902 self._character_names[self._character_index] 903 ].icon_mask_texture 904 except Exception: 905 logging.exception('Error updating char icon list') 906 tex_name = 'neoSpazIcon' 907 tint_tex_name = 'neoSpazIconColorMask' 908 909 tex = _bascenev1.gettexture(tex_name) 910 tint_tex = _bascenev1.gettexture(tint_tex_name) 911 912 self.icon.color = (1, 1, 1) 913 self.icon.texture = tex 914 self.icon.tint_texture = tint_tex 915 clr = self.get_color() 916 clr2 = self.get_highlight() 917 918 can_switch_teams = len(self.lobby.sessionteams) > 1 919 920 # If we're initing, flash. 921 if not self._inited: 922 animate_array( 923 self.icon, 924 'color', 925 3, 926 {0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)}, 927 ) 928 929 # Blend in teams mode; switch instantly in ffa-mode. 930 if can_switch_teams: 931 animate_array( 932 self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr} 933 ) 934 else: 935 self.icon.tint_color = clr 936 self.icon.tint2_color = clr2 937 938 # Store the icon info the the player. 939 self._sessionplayer.set_icon_info(tex_name, tint_tex_name, clr, clr2)
A character/team selector for a 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 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 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 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.
745 def handlemessage(self, msg: Any) -> Any: 746 """Standard generic message handler.""" 747 748 if isinstance(msg, ChangeMessage): 749 self._handle_repeat_message_attack() 750 751 # If we've been removed from the lobby, ignore this stuff. 752 if self._dead: 753 logging.error('chooser got ChangeMessage after dying') 754 return 755 756 if not self._text_node: 757 logging.error('got ChangeMessage after nodes died') 758 return 759 760 if msg.what == 'team': 761 sessionteams = self.lobby.sessionteams 762 if len(sessionteams) > 1: 763 self._swish_sound.play() 764 self._selected_team_index = ( 765 self._selected_team_index + msg.value 766 ) % len(sessionteams) 767 self._update_text() 768 self.update_position() 769 self._update_icon() 770 771 elif msg.what == 'profileindex': 772 if len(self._profilenames) == 1: 773 # This should be pretty hard to hit now with 774 # automatic local accounts. 775 _bascenev1.getsound('error').play() 776 else: 777 # Pick the next player profile and assign our name 778 # and character based on that. 779 self._deek_sound.play() 780 self._profileindex = (self._profileindex + msg.value) % len( 781 self._profilenames 782 ) 783 self.update_from_profile() 784 785 elif msg.what == 'character': 786 self._click_sound.play() 787 # update our index in our local list of characters 788 self._character_index = ( 789 self._character_index + msg.value 790 ) % len(self._character_names) 791 self._update_text() 792 self._update_icon() 793 794 elif msg.what == 'ready': 795 self._handle_ready_msg(bool(msg.value))
Standard generic message handler.
838 def get_color(self) -> Sequence[float]: 839 """Return the currently selected color.""" 840 val: Sequence[float] 841 if self.lobby.use_team_colors: 842 val = self.lobby.sessionteams[self._selected_team_index].color 843 else: 844 val = self._color 845 if len(val) != 3: 846 print('get_color: ignoring invalid color of len', len(val)) 847 val = (0, 1, 0) 848 return val
Return the currently selected color.
850 def get_highlight(self) -> Sequence[float]: 851 """Return the currently selected highlight.""" 852 if self._profilenames[self._profileindex] == '_edit': 853 return 0, 1, 0 854 855 # If we're using team colors we wanna make sure our highlight color 856 # isn't too close to any other team's color. 857 highlight = list(self._highlight) 858 if self.lobby.use_team_colors: 859 for i, sessionteam in enumerate(self.lobby.sessionteams): 860 if i != self._selected_team_index: 861 # Find the dominant component of this sessionteam's color 862 # and adjust ours so that the component is 863 # not super-dominant. 864 max_val = 0.0 865 max_index = 0 866 for j in range(3): 867 if sessionteam.color[j] > max_val: 868 max_val = sessionteam.color[j] 869 max_index = j 870 that_color_for_us = highlight[max_index] 871 our_second_biggest = max( 872 highlight[(max_index + 1) % 3], 873 highlight[(max_index + 2) % 3], 874 ) 875 diff = that_color_for_us - our_second_biggest 876 if diff > 0: 877 highlight[max_index] -= diff * 0.6 878 highlight[(max_index + 1) % 3] += diff * 0.3 879 highlight[(max_index + 2) % 3] += diff * 0.2 880 return highlight
Return the currently selected highlight.
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 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 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.
16class ContextError(Exception): 17 """Exception raised when a call is made in an invalid context. 18 19 Category: **Exception Classes** 20 21 Examples of this include calling UI functions within an Activity context 22 or calling scene manipulation functions outside of a game context. 23 """
Exception raised when a call is made in an invalid context.
Category: Exception Classes
Examples of this include calling UI functions within an Activity context or calling scene manipulation functions outside of a game context.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- add_note
- args
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 newnode() or 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 ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a Activity instance has a 'context' attribute. You can also use 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 Session this 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.
Inherited Members
- GameActivity
- tips
- name
- description
- available_settings
- scoreconfig
- allow_pausing
- allow_kick_idle_players
- show_kill_points
- default_music
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_supported_maps
- get_settings_display_string
- initialplayerinfos
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- get_instance_description
- get_instance_description_short
- on_transition_in
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- handlemessage
- end
- end_game
- respawn_player
- spawn_player_if_exists
- spawn_player
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
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 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 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 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
Inherited Members
- enum.Enum
- name
- value
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 = 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 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."""
761def displaytime() -> babase.DisplayTime: 762 """Return the current display-time in seconds. 763 764 Category: **General Utility Functions** 765 766 Display-time is a time value intended to be used for animation and other 767 visual purposes. It will generally increment by a consistent amount each 768 frame. It will pass at an overall similar rate to AppTime, but trades 769 accuracy for smoothness. 770 771 Note that the value returned here is simply a float; it just has a 772 unique type in the type-checker's eyes to help prevent it from being 773 accidentally used with time functionality expecting other time types. 774 """ 775 import babase # pylint: disable=cyclic-import 776 777 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.
780def displaytimer(time: float, call: Callable[[], Any]) -> None: 781 """Schedule a callable object to run based on display-time. 782 783 Category: **General Utility Functions** 784 785 This function creates a one-off timer which cannot be canceled or 786 modified once created. If you require the ability to do so, or need 787 a repeating timer, use the babase.DisplayTimer class instead. 788 789 Display-time is a time value intended to be used for animation and other 790 visual purposes. It will generally increment by a consistent amount each 791 frame. It will pass at an overall similar rate to AppTime, but trades 792 accuracy for smoothness. 793 794 ##### Arguments 795 ###### time (float) 796 > Length of time in seconds that the timer will wait before firing. 797 798 ###### call (Callable[[], Any]) 799 > A callable Python object. Note that the timer will retain a 800 strong reference to the callable for as long as the timer exists, so you 801 may want to look into concepts such as babase.WeakCall if that is not 802 desired. 803 804 ##### Examples 805 Print some stuff through time: 806 >>> babase.screenmessage('hello from now!') 807 >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage, 808 ... 'hello from the future!')) 809 >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage, 810 ... 'hello from the future 2!')) 811 """ 812 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 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 WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> screenmessage('hello from now!')
>>> displaytimer(1.0, Call(screenmessage,
... 'hello from the future!'))
>>> displaytimer(2.0, Call(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 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 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(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... 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 )
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 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.
Inherited Members
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
Player and Team are 'Generic' types, and so passing those top level classes as type arguments when defining a 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 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.
Inherited Members
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
Player and Team are 'Generic' types, and so passing those top level classes as type arguments when defining a 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 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.
Inherited Members
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 add_resolved_type: bool = False, 26 remove_unowned: bool = True, 27 mark_unowned: bool = False, 28 name: str = '?', 29) -> PlaylistType: 30 """Return a filtered version of a playlist. 31 32 Strips out or replaces invalid or unowned game types, makes sure all 33 settings are present, and adds in a 'resolved_type' which is the actual 34 type. 35 """ 36 # pylint: disable=too-many-locals 37 # pylint: disable=too-many-branches 38 # pylint: disable=too-many-statements 39 from bascenev1._map import get_filtered_map_name 40 from bascenev1._gameactivity import GameActivity 41 42 assert babase.app.classic is not None 43 44 goodlist: list[dict] = [] 45 unowned_maps: Sequence[str] 46 available_maps: list[str] = list(babase.app.classic.maps.keys()) 47 if (remove_unowned or mark_unowned) and babase.app.classic is not None: 48 unowned_maps = babase.app.classic.store.get_unowned_maps() 49 unowned_game_types = babase.app.classic.store.get_unowned_game_types() 50 else: 51 unowned_maps = [] 52 unowned_game_types = set() 53 54 for entry in copy.deepcopy(playlist): 55 # 'map' used to be called 'level' here. 56 if 'level' in entry: 57 entry['map'] = entry['level'] 58 del entry['level'] 59 60 # We now stuff map into settings instead of it being its own thing. 61 if 'map' in entry: 62 entry['settings']['map'] = entry['map'] 63 del entry['map'] 64 65 # Update old map names to new ones. 66 entry['settings']['map'] = get_filtered_map_name( 67 entry['settings']['map'] 68 ) 69 if remove_unowned and entry['settings']['map'] in unowned_maps: 70 continue 71 72 # Ok, for each game in our list, try to import the module and grab 73 # the actual game class. add successful ones to our initial list 74 # to present to the user. 75 if not isinstance(entry['type'], str): 76 raise TypeError('invalid entry format') 77 try: 78 # Do some type filters for backwards compat. 79 if entry['type'] in ( 80 'Assault.AssaultGame', 81 'Happy_Thoughts.HappyThoughtsGame', 82 'bsAssault.AssaultGame', 83 'bs_assault.AssaultGame', 84 'bastd.game.assault.AssaultGame', 85 ): 86 entry['type'] = 'bascenev1lib.game.assault.AssaultGame' 87 if entry['type'] in ( 88 'King_of_the_Hill.KingOfTheHillGame', 89 'bsKingOfTheHill.KingOfTheHillGame', 90 'bs_king_of_the_hill.KingOfTheHillGame', 91 'bastd.game.kingofthehill.KingOfTheHillGame', 92 ): 93 entry['type'] = ( 94 'bascenev1lib.game.kingofthehill.KingOfTheHillGame' 95 ) 96 if entry['type'] in ( 97 'Capture_the_Flag.CTFGame', 98 'bsCaptureTheFlag.CTFGame', 99 'bs_capture_the_flag.CTFGame', 100 'bastd.game.capturetheflag.CaptureTheFlagGame', 101 ): 102 entry['type'] = ( 103 'bascenev1lib.game.capturetheflag.CaptureTheFlagGame' 104 ) 105 if entry['type'] in ( 106 'Death_Match.DeathMatchGame', 107 'bsDeathMatch.DeathMatchGame', 108 'bs_death_match.DeathMatchGame', 109 'bastd.game.deathmatch.DeathMatchGame', 110 ): 111 entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame' 112 if entry['type'] in ( 113 'ChosenOne.ChosenOneGame', 114 'bsChosenOne.ChosenOneGame', 115 'bs_chosen_one.ChosenOneGame', 116 'bastd.game.chosenone.ChosenOneGame', 117 ): 118 entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame' 119 if entry['type'] in ( 120 'Conquest.Conquest', 121 'Conquest.ConquestGame', 122 'bsConquest.ConquestGame', 123 'bs_conquest.ConquestGame', 124 'bastd.game.conquest.ConquestGame', 125 ): 126 entry['type'] = 'bascenev1lib.game.conquest.ConquestGame' 127 if entry['type'] in ( 128 'Elimination.EliminationGame', 129 'bsElimination.EliminationGame', 130 'bs_elimination.EliminationGame', 131 'bastd.game.elimination.EliminationGame', 132 ): 133 entry['type'] = 'bascenev1lib.game.elimination.EliminationGame' 134 if entry['type'] in ( 135 'Football.FootballGame', 136 'bsFootball.FootballTeamGame', 137 'bs_football.FootballTeamGame', 138 'bastd.game.football.FootballTeamGame', 139 ): 140 entry['type'] = 'bascenev1lib.game.football.FootballTeamGame' 141 if entry['type'] in ( 142 'Hockey.HockeyGame', 143 'bsHockey.HockeyGame', 144 'bs_hockey.HockeyGame', 145 'bastd.game.hockey.HockeyGame', 146 ): 147 entry['type'] = 'bascenev1lib.game.hockey.HockeyGame' 148 if entry['type'] in ( 149 'Keep_Away.KeepAwayGame', 150 'bsKeepAway.KeepAwayGame', 151 'bs_keep_away.KeepAwayGame', 152 'bastd.game.keepaway.KeepAwayGame', 153 ): 154 entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame' 155 if entry['type'] in ( 156 'Race.RaceGame', 157 'bsRace.RaceGame', 158 'bs_race.RaceGame', 159 'bastd.game.race.RaceGame', 160 ): 161 entry['type'] = 'bascenev1lib.game.race.RaceGame' 162 if entry['type'] in ( 163 'bsEasterEggHunt.EasterEggHuntGame', 164 'bs_easter_egg_hunt.EasterEggHuntGame', 165 'bastd.game.easteregghunt.EasterEggHuntGame', 166 ): 167 entry['type'] = ( 168 'bascenev1lib.game.easteregghunt.EasterEggHuntGame' 169 ) 170 if entry['type'] in ( 171 'bsMeteorShower.MeteorShowerGame', 172 'bs_meteor_shower.MeteorShowerGame', 173 'bastd.game.meteorshower.MeteorShowerGame', 174 ): 175 entry['type'] = ( 176 'bascenev1lib.game.meteorshower.MeteorShowerGame' 177 ) 178 if entry['type'] in ( 179 'bsTargetPractice.TargetPracticeGame', 180 'bs_target_practice.TargetPracticeGame', 181 'bastd.game.targetpractice.TargetPracticeGame', 182 ): 183 entry['type'] = ( 184 'bascenev1lib.game.targetpractice.TargetPracticeGame' 185 ) 186 187 gameclass = babase.getclass(entry['type'], GameActivity) 188 189 if entry['settings']['map'] not in available_maps: 190 raise babase.MapNotFoundError() 191 192 if remove_unowned and gameclass in unowned_game_types: 193 continue 194 if add_resolved_type: 195 entry['resolved_type'] = gameclass 196 if mark_unowned and entry['settings']['map'] in unowned_maps: 197 entry['is_unowned_map'] = True 198 if mark_unowned and gameclass in unowned_game_types: 199 entry['is_unowned_game'] = True 200 201 # Make sure all settings the game defines are present. 202 neededsettings = gameclass.get_available_settings(sessiontype) 203 for setting in neededsettings: 204 if setting.name not in entry['settings']: 205 entry['settings'][setting.name] = setting.default 206 207 goodlist.append(entry) 208 209 except babase.MapNotFoundError: 210 logging.warning( 211 'Map \'%s\' not found while scanning playlist \'%s\'.', 212 entry['settings']['map'], 213 name, 214 ) 215 except ImportError as exc: 216 logging.warning( 217 'Import failed while scanning playlist \'%s\': %s', name, exc 218 ) 219 except Exception: 220 logging.exception('Error in filter_playlist.') 221 222 return goodlist
Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all settings are present, and adds in a 'resolved_type' which is the actual type.
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 )
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 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.
Inherited Members
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 create_settings_ui( 70 cls, 71 sessiontype: type[bascenev1.Session], 72 settings: dict | None, 73 completion_call: Callable[[dict | None], None], 74 ) -> None: 75 """Launch an in-game UI to configure settings for a game type. 76 77 'sessiontype' should be the bascenev1.Session class the game will 78 be used in. 79 80 'settings' should be an existing settings dict (implies 'edit' 81 ui mode) or None (implies 'add' ui mode). 82 83 'completion_call' will be called with a filled-out settings dict on 84 success or None on cancel. 85 86 Generally subclasses don't need to override this; if they override 87 bascenev1.GameActivity.get_available_settings() and 88 bascenev1.GameActivity.get_supported_maps() they can just rely on 89 the default implementation here which calls those methods. 90 """ 91 # pylint: disable=cyclic-import 92 from bauiv1lib.playlist.editgame import PlaylistEditGameWindow 93 94 assert babase.app.classic is not None 95 babase.app.ui_v1.clear_main_window() 96 babase.app.ui_v1.set_main_window( 97 PlaylistEditGameWindow( 98 cls, 99 sessiontype, 100 settings, 101 completion_call=completion_call, 102 ), 103 from_window=False, # Disable check since we don't know. 104 ) 105 106 @classmethod 107 def getscoreconfig(cls) -> bascenev1.ScoreConfig: 108 """Return info about game scoring setup; can be overridden by games.""" 109 return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig() 110 111 @classmethod 112 def getname(cls) -> str: 113 """Return a str name for this game type. 114 115 This default implementation simply returns the 'name' class attr. 116 """ 117 return cls.name if cls.name is not None else 'Untitled Game' 118 119 @classmethod 120 def get_display_string(cls, settings: dict | None = None) -> babase.Lstr: 121 """Return a descriptive name for this game/settings combo. 122 123 Subclasses should override getname(); not this. 124 """ 125 name = babase.Lstr(translate=('gameNames', cls.getname())) 126 127 # A few substitutions for 'Epic', 'Solo' etc. modes. 128 # FIXME: Should provide a way for game types to define filters of 129 # their own and should not rely on hard-coded settings names. 130 if settings is not None: 131 if 'Solo Mode' in settings and settings['Solo Mode']: 132 name = babase.Lstr( 133 resource='soloNameFilterText', subs=[('${NAME}', name)] 134 ) 135 if 'Epic Mode' in settings and settings['Epic Mode']: 136 name = babase.Lstr( 137 resource='epicNameFilterText', subs=[('${NAME}', name)] 138 ) 139 140 return name 141 142 @classmethod 143 def get_team_display_string(cls, name: str) -> babase.Lstr: 144 """Given a team name, returns a localized version of it.""" 145 return babase.Lstr(translate=('teamNames', name)) 146 147 @classmethod 148 def get_description(cls, sessiontype: type[bascenev1.Session]) -> str: 149 """Get a str description of this game type. 150 151 The default implementation simply returns the 'description' class var. 152 Classes which want to change their description depending on the session 153 can override this method. 154 """ 155 del sessiontype # Unused arg. 156 return cls.description if cls.description is not None else '' 157 158 @classmethod 159 def get_description_display_string( 160 cls, sessiontype: type[bascenev1.Session] 161 ) -> babase.Lstr: 162 """Return a translated version of get_description(). 163 164 Sub-classes should override get_description(); not this. 165 """ 166 description = cls.get_description(sessiontype) 167 return babase.Lstr(translate=('gameDescriptions', description)) 168 169 @classmethod 170 def get_available_settings( 171 cls, sessiontype: type[bascenev1.Session] 172 ) -> list[bascenev1.Setting]: 173 """Return a list of settings relevant to this game type when 174 running under the provided session type. 175 """ 176 del sessiontype # Unused arg. 177 return [] if cls.available_settings is None else cls.available_settings 178 179 @classmethod 180 def get_supported_maps( 181 cls, sessiontype: type[bascenev1.Session] 182 ) -> list[str]: 183 """ 184 Called by the default bascenev1.GameActivity.create_settings_ui() 185 implementation; should return a list of map names valid 186 for this game-type for the given bascenev1.Session type. 187 """ 188 del sessiontype # Unused arg. 189 assert babase.app.classic is not None 190 return babase.app.classic.getmaps('melee') 191 192 @classmethod 193 def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr: 194 """Given a game config dict, return a short description for it. 195 196 This is used when viewing game-lists or showing what game 197 is up next in a series. 198 """ 199 name = cls.get_display_string(config['settings']) 200 201 # In newer configs, map is in settings; it used to be in the 202 # config root. 203 if 'map' in config['settings']: 204 sval = babase.Lstr( 205 value='${NAME} @ ${MAP}', 206 subs=[ 207 ('${NAME}', name), 208 ( 209 '${MAP}', 210 _map.get_map_display_string( 211 _map.get_filtered_map_name( 212 config['settings']['map'] 213 ) 214 ), 215 ), 216 ], 217 ) 218 elif 'map' in config: 219 sval = babase.Lstr( 220 value='${NAME} @ ${MAP}', 221 subs=[ 222 ('${NAME}', name), 223 ( 224 '${MAP}', 225 _map.get_map_display_string( 226 _map.get_filtered_map_name(config['map']) 227 ), 228 ), 229 ], 230 ) 231 else: 232 print('invalid game config - expected map entry under settings') 233 sval = babase.Lstr(value='???') 234 return sval 235 236 @classmethod 237 def supports_session_type( 238 cls, sessiontype: type[bascenev1.Session] 239 ) -> bool: 240 """Return whether this game supports the provided Session type.""" 241 from bascenev1._multiteamsession import MultiTeamSession 242 243 # By default, games support any versus mode 244 return issubclass(sessiontype, MultiTeamSession) 245 246 def __init__(self, settings: dict): 247 """Instantiate the Activity.""" 248 super().__init__(settings) 249 250 plus = babase.app.plus 251 252 # Holds some flattened info about the player set at the point 253 # when on_begin() is called. 254 self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None 255 256 # Go ahead and get our map loading. 257 self._map_type = _map.get_map_class(self._calc_map_name(settings)) 258 259 self._spawn_sound = _bascenev1.getsound('spawn') 260 self._map_type.preload() 261 self._map: bascenev1.Map | None = None 262 self._powerup_drop_timer: bascenev1.Timer | None = None 263 self._tnt_spawners: dict[int, TNTSpawner] | None = None 264 self._tnt_drop_timer: bascenev1.Timer | None = None 265 self._game_scoreboard_name_text: bascenev1.Actor | None = None 266 self._game_scoreboard_description_text: bascenev1.Actor | None = None 267 self._standard_time_limit_time: int | None = None 268 self._standard_time_limit_timer: bascenev1.Timer | None = None 269 self._standard_time_limit_text: bascenev1.NodeActor | None = None 270 self._standard_time_limit_text_input: bascenev1.NodeActor | None = None 271 self._tournament_time_limit: int | None = None 272 self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None 273 self._tournament_time_limit_title_text: bascenev1.NodeActor | None = ( 274 None 275 ) 276 self._tournament_time_limit_text: bascenev1.NodeActor | None = None 277 self._tournament_time_limit_text_input: bascenev1.NodeActor | None = ( 278 None 279 ) 280 self._zoom_message_times: dict[int, float] = {} 281 self._is_waiting_for_continue = False 282 283 self._continue_cost = ( 284 25 285 if plus is None 286 else plus.get_v1_account_misc_read_val('continueStartCost', 25) 287 ) 288 self._continue_cost_mult = ( 289 2 290 if plus is None 291 else plus.get_v1_account_misc_read_val('continuesMult', 2) 292 ) 293 self._continue_cost_offset = ( 294 0 295 if plus is None 296 else plus.get_v1_account_misc_read_val('continuesOffset', 0) 297 ) 298 299 @property 300 def map(self) -> _map.Map: 301 """The map being used for this game. 302 303 Raises a bascenev1.MapNotFoundError if the map does not currently 304 exist. 305 """ 306 if self._map is None: 307 raise babase.MapNotFoundError 308 return self._map 309 310 def get_instance_display_string(self) -> babase.Lstr: 311 """Return a name for this particular game instance.""" 312 return self.get_display_string(self.settings_raw) 313 314 # noinspection PyUnresolvedReferences 315 def get_instance_scoreboard_display_string(self) -> babase.Lstr: 316 """Return a name for this particular game instance. 317 318 This name is used above the game scoreboard in the corner 319 of the screen, so it should be as concise as possible. 320 """ 321 # If we're in a co-op session, use the level name. 322 # FIXME: Should clean this up. 323 try: 324 from bascenev1._coopsession import CoopSession 325 326 if isinstance(self.session, CoopSession): 327 campaign = self.session.campaign 328 assert campaign is not None 329 return campaign.getlevel( 330 self.session.campaign_level_name 331 ).displayname 332 except Exception: 333 logging.exception('Error getting campaign level name.') 334 return self.get_instance_display_string() 335 336 def get_instance_description(self) -> str | Sequence: 337 """Return a description for this game instance, in English. 338 339 This is shown in the center of the screen below the game name at the 340 start of a game. It should start with a capital letter and end with a 341 period, and can be a bit more verbose than the version returned by 342 get_instance_description_short(). 343 344 Note that translation is applied by looking up the specific returned 345 value as a key, so the number of returned variations should be limited; 346 ideally just one or two. To include arbitrary values in the 347 description, you can return a sequence of values in the following 348 form instead of just a string: 349 350 # This will give us something like 'Score 3 goals.' in English 351 # and can properly translate to 'Anota 3 goles.' in Spanish. 352 # If we just returned the string 'Score 3 Goals' here, there would 353 # have to be a translation entry for each specific number. ew. 354 return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']] 355 356 This way the first string can be consistently translated, with any arg 357 values then substituted into the result. ${ARG1} will be replaced with 358 the first value, ${ARG2} with the second, etc. 359 """ 360 return self.get_description(type(self.session)) 361 362 def get_instance_description_short(self) -> str | Sequence: 363 """Return a short description for this game instance in English. 364 365 This description is used above the game scoreboard in the 366 corner of the screen, so it should be as concise as possible. 367 It should be lowercase and should not contain periods or other 368 punctuation. 369 370 Note that translation is applied by looking up the specific returned 371 value as a key, so the number of returned variations should be limited; 372 ideally just one or two. To include arbitrary values in the 373 description, you can return a sequence of values in the following form 374 instead of just a string: 375 376 # This will give us something like 'score 3 goals' in English 377 # and can properly translate to 'anota 3 goles' in Spanish. 378 # If we just returned the string 'score 3 goals' here, there would 379 # have to be a translation entry for each specific number. ew. 380 return ['score ${ARG1} goals', self.settings_raw['Score to Win']] 381 382 This way the first string can be consistently translated, with any arg 383 values then substituted into the result. ${ARG1} will be replaced 384 with the first value, ${ARG2} with the second, etc. 385 386 """ 387 return '' 388 389 @override 390 def on_transition_in(self) -> None: 391 super().on_transition_in() 392 393 # Make our map. 394 self._map = self._map_type() 395 396 # Give our map a chance to override the music. 397 # (for happy-thoughts and other such themed maps) 398 map_music = self._map_type.get_music_type() 399 music = map_music if map_music is not None else self.default_music 400 401 if music is not None: 402 _music.setmusic(music) 403 404 def on_continue(self) -> None: 405 """ 406 This is called if a game supports and offers a continue and the player 407 accepts. In this case the player should be given an extra life or 408 whatever is relevant to keep the game going. 409 """ 410 411 def _continue_choice(self, do_continue: bool) -> None: 412 plus = babase.app.plus 413 assert plus is not None 414 self._is_waiting_for_continue = False 415 if self.has_ended(): 416 return 417 with self.context: 418 if do_continue: 419 _bascenev1.getsound('shieldUp').play() 420 _bascenev1.getsound('cashRegister').play() 421 plus.add_v1_account_transaction( 422 {'type': 'CONTINUE', 'cost': self._continue_cost} 423 ) 424 if plus is not None: 425 plus.run_v1_account_transactions() 426 self._continue_cost = ( 427 self._continue_cost * self._continue_cost_mult 428 + self._continue_cost_offset 429 ) 430 self.on_continue() 431 else: 432 self.end_game() 433 434 def is_waiting_for_continue(self) -> bool: 435 """Returns whether or not this activity is currently waiting for the 436 player to continue (or timeout)""" 437 return self._is_waiting_for_continue 438 439 def continue_or_end_game(self) -> None: 440 """If continues are allowed, prompts the player to purchase a continue 441 and calls either end_game or continue_game depending on the result 442 """ 443 # pylint: disable=too-many-nested-blocks 444 # pylint: disable=cyclic-import 445 from bascenev1._coopsession import CoopSession 446 447 classic = babase.app.classic 448 assert classic is not None 449 continues_window = classic.continues_window 450 451 # Turning these off. I want to migrate towards monetization that 452 # feels less pay-to-win-ish. 453 allow_continues = False 454 455 plus = babase.app.plus 456 try: 457 if ( 458 plus is not None 459 and plus.get_v1_account_misc_read_val('enableContinues', False) 460 and allow_continues 461 ): 462 session = self.session 463 464 # We only support continuing in non-tournament games. 465 tournament_id = session.tournament_id 466 if tournament_id is None: 467 # We currently only support continuing in sequential 468 # co-op campaigns. 469 if isinstance(session, CoopSession): 470 assert session.campaign is not None 471 if session.campaign.sequential: 472 gnode = self.globalsnode 473 474 # Only attempt this if we're not currently paused 475 # and there appears to be no UI. 476 assert babase.app.classic is not None 477 hmmw = babase.app.ui_v1.has_main_window() 478 if not gnode.paused and not hmmw: 479 self._is_waiting_for_continue = True 480 with babase.ContextRef.empty(): 481 babase.apptimer( 482 0.5, 483 lambda: continues_window( 484 self, 485 self._continue_cost, 486 continue_call=babase.WeakCall( 487 self._continue_choice, True 488 ), 489 cancel_call=babase.WeakCall( 490 self._continue_choice, False 491 ), 492 ), 493 ) 494 return 495 496 except Exception: 497 logging.exception('Error handling continues.') 498 499 self.end_game() 500 501 @override 502 def on_begin(self) -> None: 503 super().on_begin() 504 505 if babase.app.classic is not None: 506 babase.app.classic.game_begin_analytics() 507 508 # We don't do this in on_transition_in because it may depend on 509 # players/teams which aren't available until now. 510 _bascenev1.timer(0.001, self._show_scoreboard_info) 511 _bascenev1.timer(1.0, self._show_info) 512 _bascenev1.timer(2.5, self._show_tip) 513 514 # Store some basic info about players present at start time. 515 self.initialplayerinfos = [ 516 PlayerInfo(name=p.getname(full=True), character=p.character) 517 for p in self.players 518 ] 519 520 # Sort this by name so high score lists/etc will be consistent 521 # regardless of player join order. 522 self.initialplayerinfos.sort(key=lambda x: x.name) 523 524 # If this is a tournament, query info about it such as how much 525 # time is left. 526 tournament_id = self.session.tournament_id 527 if tournament_id is not None: 528 assert babase.app.plus is not None 529 babase.app.plus.tournament_query( 530 args={ 531 'tournamentIDs': [tournament_id], 532 'source': 'in-game time remaining query', 533 }, 534 callback=babase.WeakCall(self._on_tournament_query_response), 535 ) 536 537 def _on_tournament_query_response( 538 self, data: dict[str, Any] | None 539 ) -> None: 540 if data is not None: 541 data_t = data['t'] # This used to be the whole payload. 542 543 # Keep our cached tourney info up to date 544 assert babase.app.classic is not None 545 babase.app.classic.accounts.cache_tournament_info(data_t) 546 self._setup_tournament_time_limit( 547 max(5, data_t[0]['timeRemaining']) 548 ) 549 550 @override 551 def on_player_join(self, player: PlayerT) -> None: 552 super().on_player_join(player) 553 554 # By default, just spawn a dude. 555 self.spawn_player(player) 556 557 @override 558 def handlemessage(self, msg: Any) -> Any: 559 if isinstance(msg, PlayerDiedMessage): 560 # pylint: disable=cyclic-import 561 from bascenev1lib.actor.spaz import Spaz 562 563 player = msg.getplayer(self.playertype) 564 killer = msg.getkillerplayer(self.playertype) 565 566 # Inform our stats of the demise. 567 self.stats.player_was_killed( 568 player, killed=msg.killed, killer=killer 569 ) 570 571 # Award the killer points if he's on a different team. 572 # FIXME: This should not be linked to Spaz actors. 573 # (should move get_death_points to Actor or make it a message) 574 if killer and killer.team is not player.team: 575 assert isinstance(killer.actor, Spaz) 576 pts, importance = killer.actor.get_death_points(msg.how) 577 if not self.has_ended(): 578 self.stats.player_scored( 579 killer, 580 pts, 581 kill=True, 582 victim_player=player, 583 importance=importance, 584 showpoints=self.show_kill_points, 585 ) 586 else: 587 return super().handlemessage(msg) 588 return None 589 590 def _show_scoreboard_info(self) -> None: 591 """Create the game info display. 592 593 This is the thing in the top left corner showing the name 594 and short description of the game. 595 """ 596 # pylint: disable=too-many-locals 597 from bascenev1._freeforallsession import FreeForAllSession 598 from bascenev1._gameutils import animate 599 from bascenev1._nodeactor import NodeActor 600 601 sb_name = self.get_instance_scoreboard_display_string() 602 603 # The description can be either a string or a sequence with args 604 # to swap in post-translation. 605 sb_desc_in = self.get_instance_description_short() 606 sb_desc_l: Sequence 607 if isinstance(sb_desc_in, str): 608 sb_desc_l = [sb_desc_in] # handle simple string case 609 else: 610 sb_desc_l = sb_desc_in 611 if not isinstance(sb_desc_l[0], str): 612 raise TypeError('Invalid format for instance description.') 613 614 is_empty = sb_desc_l[0] == '' 615 subs = [] 616 for i in range(len(sb_desc_l) - 1): 617 subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1]))) 618 translation = babase.Lstr( 619 translate=('gameDescriptions', sb_desc_l[0]), subs=subs 620 ) 621 sb_desc = translation 622 vrmode = babase.app.env.vr 623 yval = -34 if is_empty else -20 624 yval -= 16 625 sbpos = ( 626 (15, yval) 627 if isinstance(self.session, FreeForAllSession) 628 else (15, yval) 629 ) 630 self._game_scoreboard_name_text = NodeActor( 631 _bascenev1.newnode( 632 'text', 633 attrs={ 634 'text': sb_name, 635 'maxwidth': 300, 636 'position': sbpos, 637 'h_attach': 'left', 638 'vr_depth': 10, 639 'v_attach': 'top', 640 'v_align': 'bottom', 641 'color': (1.0, 1.0, 1.0, 1.0), 642 'shadow': 1.0 if vrmode else 0.6, 643 'flatness': 1.0 if vrmode else 0.5, 644 'scale': 1.1, 645 }, 646 ) 647 ) 648 649 assert self._game_scoreboard_name_text.node 650 animate( 651 self._game_scoreboard_name_text.node, 'opacity', {0: 0.0, 1.0: 1.0} 652 ) 653 654 descpos = ( 655 (17, -44 + 10) 656 if isinstance(self.session, FreeForAllSession) 657 else (17, -44 + 10) 658 ) 659 self._game_scoreboard_description_text = NodeActor( 660 _bascenev1.newnode( 661 'text', 662 attrs={ 663 'text': sb_desc, 664 'maxwidth': 480, 665 'position': descpos, 666 'scale': 0.7, 667 'h_attach': 'left', 668 'v_attach': 'top', 669 'v_align': 'top', 670 'shadow': 1.0 if vrmode else 0.7, 671 'flatness': 1.0 if vrmode else 0.8, 672 'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0), 673 }, 674 ) 675 ) 676 677 assert self._game_scoreboard_description_text.node 678 animate( 679 self._game_scoreboard_description_text.node, 680 'opacity', 681 {0: 0.0, 1.0: 1.0}, 682 ) 683 684 def _show_info(self) -> None: 685 """Show the game description.""" 686 from bascenev1._gameutils import animate 687 from bascenev1lib.actor.zoomtext import ZoomText 688 689 name = self.get_instance_display_string() 690 ZoomText( 691 name, 692 maxwidth=800, 693 lifespan=2.5, 694 jitter=2.0, 695 position=(0, 180), 696 flash=False, 697 color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), 698 trailcolor=(0.15, 0.05, 1.0, 0.0), 699 ).autoretain() 700 _bascenev1.timer(0.2, _bascenev1.getsound('gong').play) 701 # _bascenev1.timer( 702 # 0.2, Call(_bascenev1.playsound, _bascenev1.getsound('gong')) 703 # ) 704 705 # The description can be either a string or a sequence with args 706 # to swap in post-translation. 707 desc_in = self.get_instance_description() 708 desc_l: Sequence 709 if isinstance(desc_in, str): 710 desc_l = [desc_in] # handle simple string case 711 else: 712 desc_l = desc_in 713 if not isinstance(desc_l[0], str): 714 raise TypeError('Invalid format for instance description') 715 subs = [] 716 for i in range(len(desc_l) - 1): 717 subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1]))) 718 translation = babase.Lstr( 719 translate=('gameDescriptions', desc_l[0]), subs=subs 720 ) 721 722 # Do some standard filters (epic mode, etc). 723 if self.settings_raw.get('Epic Mode', False): 724 translation = babase.Lstr( 725 resource='epicDescriptionFilterText', 726 subs=[('${DESCRIPTION}', translation)], 727 ) 728 vrmode = babase.app.env.vr 729 dnode = _bascenev1.newnode( 730 'text', 731 attrs={ 732 'v_attach': 'center', 733 'h_attach': 'center', 734 'h_align': 'center', 735 'color': (1, 1, 1, 1), 736 'shadow': 1.0 if vrmode else 0.5, 737 'flatness': 1.0 if vrmode else 0.5, 738 'vr_depth': -30, 739 'position': (0, 80), 740 'scale': 1.2, 741 'maxwidth': 700, 742 'text': translation, 743 }, 744 ) 745 cnode = _bascenev1.newnode( 746 'combine', 747 owner=dnode, 748 attrs={'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4}, 749 ) 750 cnode.connectattr('output', dnode, 'color') 751 keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0} 752 animate(cnode, 'input3', keys) 753 _bascenev1.timer(4.0, dnode.delete) 754 755 def _show_tip(self) -> None: 756 # pylint: disable=too-many-locals 757 from bascenev1._gameutils import animate, GameTip 758 759 # If there's any tips left on the list, display one. 760 if self.tips: 761 tip = self.tips.pop(random.randrange(len(self.tips))) 762 tip_title = babase.Lstr( 763 value='${A}:', subs=[('${A}', babase.Lstr(resource='tipText'))] 764 ) 765 icon: bascenev1.Texture | None = None 766 sound: bascenev1.Sound | None = None 767 if isinstance(tip, GameTip): 768 icon = tip.icon 769 sound = tip.sound 770 tip = tip.text 771 assert isinstance(tip, str) 772 773 # Do a few substitutions. 774 tip_lstr = babase.Lstr( 775 translate=('tips', tip), 776 subs=[ 777 ('${PICKUP}', babase.charstr(babase.SpecialChar.TOP_BUTTON)) 778 ], 779 ) 780 base_position = (75, 50) 781 tip_scale = 0.8 782 tip_title_scale = 1.2 783 vrmode = babase.app.env.vr 784 785 t_offs = -350.0 786 tnode = _bascenev1.newnode( 787 'text', 788 attrs={ 789 'text': tip_lstr, 790 'scale': tip_scale, 791 'maxwidth': 900, 792 'position': (base_position[0] + t_offs, base_position[1]), 793 'h_align': 'left', 794 'vr_depth': 300, 795 'shadow': 1.0 if vrmode else 0.5, 796 'flatness': 1.0 if vrmode else 0.5, 797 'v_align': 'center', 798 'v_attach': 'bottom', 799 }, 800 ) 801 t2pos = ( 802 base_position[0] + t_offs - (20 if icon is None else 82), 803 base_position[1] + 2, 804 ) 805 t2node = _bascenev1.newnode( 806 'text', 807 owner=tnode, 808 attrs={ 809 'text': tip_title, 810 'scale': tip_title_scale, 811 'position': t2pos, 812 'h_align': 'right', 813 'vr_depth': 300, 814 'shadow': 1.0 if vrmode else 0.5, 815 'flatness': 1.0 if vrmode else 0.5, 816 'maxwidth': 140, 817 'v_align': 'center', 818 'v_attach': 'bottom', 819 }, 820 ) 821 if icon is not None: 822 ipos = (base_position[0] + t_offs - 40, base_position[1] + 1) 823 img = _bascenev1.newnode( 824 'image', 825 attrs={ 826 'texture': icon, 827 'position': ipos, 828 'scale': (50, 50), 829 'opacity': 1.0, 830 'vr_depth': 315, 831 'color': (1, 1, 1), 832 'absolute_scale': True, 833 'attach': 'bottomCenter', 834 }, 835 ) 836 animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) 837 _bascenev1.timer(5.0, img.delete) 838 if sound is not None: 839 sound.play() 840 841 combine = _bascenev1.newnode( 842 'combine', 843 owner=tnode, 844 attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4}, 845 ) 846 combine.connectattr('output', tnode, 'color') 847 combine.connectattr('output', t2node, 'color') 848 animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0}) 849 _bascenev1.timer(5.0, tnode.delete) 850 851 @override 852 def end( 853 self, results: Any = None, delay: float = 0.0, force: bool = False 854 ) -> None: 855 from bascenev1._gameresults import GameResults 856 857 # If results is a standard team-game-results, associate it with us 858 # so it can grab our score prefs. 859 if isinstance(results, GameResults): 860 results.set_game(self) 861 862 # If we had a standard time-limit that had not expired, stop it so 863 # it doesnt tick annoyingly. 864 if ( 865 self._standard_time_limit_time is not None 866 and self._standard_time_limit_time > 0 867 ): 868 self._standard_time_limit_timer = None 869 self._standard_time_limit_text = None 870 871 # Ditto with tournament time limits. 872 if ( 873 self._tournament_time_limit is not None 874 and self._tournament_time_limit > 0 875 ): 876 self._tournament_time_limit_timer = None 877 self._tournament_time_limit_text = None 878 self._tournament_time_limit_title_text = None 879 880 super().end(results, delay, force) 881 882 def end_game(self) -> None: 883 """Tell the game to wrap up and call bascenev1.Activity.end(). 884 885 This method should be overridden by subclasses. A game should always 886 be prepared to end and deliver results, even if there is no 'winner' 887 yet; this way things like the standard time-limit 888 (bascenev1.GameActivity.setup_standard_time_limit()) will work with 889 the game. 890 """ 891 print( 892 'WARNING: default end_game() implementation called;' 893 ' your game should override this.' 894 ) 895 896 def respawn_player( 897 self, player: PlayerT, respawn_time: float | None = None 898 ) -> None: 899 """ 900 Given a bascenev1.Player, sets up a standard respawn timer, 901 along with the standard counter display, etc. 902 At the end of the respawn period spawn_player() will 903 be called if the Player still exists. 904 An explicit 'respawn_time' can optionally be provided 905 (in seconds). 906 """ 907 # pylint: disable=cyclic-import 908 909 assert player 910 if respawn_time is None: 911 teamsize = len(player.team.players) 912 if teamsize == 1: 913 respawn_time = 3.0 914 elif teamsize == 2: 915 respawn_time = 5.0 916 elif teamsize == 3: 917 respawn_time = 6.0 918 else: 919 respawn_time = 7.0 920 921 # If this standard setting is present, factor it in. 922 if 'Respawn Times' in self.settings_raw: 923 respawn_time *= self.settings_raw['Respawn Times'] 924 925 # We want whole seconds. 926 assert respawn_time is not None 927 respawn_time = round(max(1.0, respawn_time), 0) 928 929 if player.actor and not self.has_ended(): 930 from bascenev1lib.actor.respawnicon import RespawnIcon 931 932 player.customdata['respawn_timer'] = _bascenev1.Timer( 933 respawn_time, 934 babase.WeakCall(self.spawn_player_if_exists, player), 935 ) 936 player.customdata['respawn_icon'] = RespawnIcon( 937 player, respawn_time 938 ) 939 940 def spawn_player_if_exists(self, player: PlayerT) -> None: 941 """ 942 A utility method which calls self.spawn_player() *only* if the 943 bascenev1.Player provided still exists; handy for use in timers 944 and whatnot. 945 946 There is no need to override this; just override spawn_player(). 947 """ 948 if player: 949 self.spawn_player(player) 950 951 def spawn_player(self, player: PlayerT) -> bascenev1.Actor: 952 """Spawn *something* for the provided bascenev1.Player. 953 954 The default implementation simply calls spawn_player_spaz(). 955 """ 956 assert player # Dead references should never be passed as args. 957 958 return self.spawn_player_spaz(player) 959 960 def spawn_player_spaz( 961 self, 962 player: PlayerT, 963 position: Sequence[float] = (0, 0, 0), 964 angle: float | None = None, 965 ) -> PlayerSpaz: 966 """Create and wire up a bascenev1.PlayerSpaz for the provided Player.""" 967 # pylint: disable=too-many-locals 968 # pylint: disable=cyclic-import 969 from bascenev1._gameutils import animate 970 from bascenev1._coopsession import CoopSession 971 from bascenev1lib.actor.playerspaz import PlayerSpaz 972 973 name = player.getname() 974 color = player.color 975 highlight = player.highlight 976 977 playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz) 978 if not issubclass(playerspaztype, PlayerSpaz): 979 playerspaztype = PlayerSpaz 980 981 light_color = babase.normalized_color(color) 982 display_color = babase.safecolor(color, target_intensity=0.75) 983 spaz = playerspaztype( 984 color=color, 985 highlight=highlight, 986 character=player.character, 987 player=player, 988 ) 989 990 player.actor = spaz 991 assert spaz.node 992 993 # If this is co-op and we're on Courtyard or Runaround, add the 994 # material that allows us to collide with the player-walls. 995 # FIXME: Need to generalize this. 996 if isinstance(self.session, CoopSession) and self.map.getname() in [ 997 'Courtyard', 998 'Tower D', 999 ]: 1000 mat = self.map.preloaddata['collide_with_wall_material'] 1001 assert isinstance(spaz.node.materials, tuple) 1002 assert isinstance(spaz.node.roller_materials, tuple) 1003 spaz.node.materials += (mat,) 1004 spaz.node.roller_materials += (mat,) 1005 1006 spaz.node.name = name 1007 spaz.node.name_color = display_color 1008 spaz.connect_controls_to_player() 1009 1010 # Move to the stand position and add a flash of light. 1011 spaz.handlemessage( 1012 StandMessage( 1013 position, angle if angle is not None else random.uniform(0, 360) 1014 ) 1015 ) 1016 self._spawn_sound.play(1, position=spaz.node.position) 1017 light = _bascenev1.newnode('light', attrs={'color': light_color}) 1018 spaz.node.connectattr('position', light, 'position') 1019 animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) 1020 _bascenev1.timer(0.5, light.delete) 1021 return spaz 1022 1023 def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: 1024 """Create standard powerup drops for the current map.""" 1025 # pylint: disable=cyclic-import 1026 from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL 1027 1028 self._powerup_drop_timer = _bascenev1.Timer( 1029 DEFAULT_POWERUP_INTERVAL, 1030 babase.WeakCall(self._standard_drop_powerups), 1031 repeat=True, 1032 ) 1033 self._standard_drop_powerups() 1034 if enable_tnt: 1035 self._tnt_spawners = {} 1036 self._setup_standard_tnt_drops() 1037 1038 def _standard_drop_powerup(self, index: int, expire: bool = True) -> None: 1039 # pylint: disable=cyclic-import 1040 from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory 1041 1042 PowerupBox( 1043 position=self.map.powerup_spawn_points[index], 1044 poweruptype=PowerupBoxFactory.get().get_random_powerup_type(), 1045 expire=expire, 1046 ).autoretain() 1047 1048 def _standard_drop_powerups(self) -> None: 1049 """Standard powerup drop.""" 1050 1051 # Drop one powerup per point. 1052 points = self.map.powerup_spawn_points 1053 for i in range(len(points)): 1054 _bascenev1.timer( 1055 i * 0.4, babase.WeakCall(self._standard_drop_powerup, i) 1056 ) 1057 1058 def _setup_standard_tnt_drops(self) -> None: 1059 """Standard tnt drop.""" 1060 # pylint: disable=cyclic-import 1061 from bascenev1lib.actor.bomb import TNTSpawner 1062 1063 for i, point in enumerate(self.map.tnt_points): 1064 assert self._tnt_spawners is not None 1065 if self._tnt_spawners.get(i) is None: 1066 self._tnt_spawners[i] = TNTSpawner(point) 1067 1068 def setup_standard_time_limit(self, duration: float) -> None: 1069 """ 1070 Create a standard game time-limit given the provided 1071 duration in seconds. 1072 This will be displayed at the top of the screen. 1073 If the time-limit expires, end_game() will be called. 1074 """ 1075 from bascenev1._nodeactor import NodeActor 1076 1077 if duration <= 0.0: 1078 return 1079 self._standard_time_limit_time = int(duration) 1080 self._standard_time_limit_timer = _bascenev1.Timer( 1081 1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True 1082 ) 1083 self._standard_time_limit_text = NodeActor( 1084 _bascenev1.newnode( 1085 'text', 1086 attrs={ 1087 'v_attach': 'top', 1088 'h_attach': 'center', 1089 'h_align': 'left', 1090 'color': (1.0, 1.0, 1.0, 0.5), 1091 'position': (-25, -30), 1092 'flatness': 1.0, 1093 'scale': 0.9, 1094 }, 1095 ) 1096 ) 1097 self._standard_time_limit_text_input = NodeActor( 1098 _bascenev1.newnode( 1099 'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0} 1100 ) 1101 ) 1102 self.globalsnode.connectattr( 1103 'time', self._standard_time_limit_text_input.node, 'time1' 1104 ) 1105 assert self._standard_time_limit_text_input.node 1106 assert self._standard_time_limit_text.node 1107 self._standard_time_limit_text_input.node.connectattr( 1108 'output', self._standard_time_limit_text.node, 'text' 1109 ) 1110 1111 def _standard_time_limit_tick(self) -> None: 1112 from bascenev1._gameutils import animate 1113 1114 assert self._standard_time_limit_time is not None 1115 self._standard_time_limit_time -= 1 1116 if self._standard_time_limit_time <= 10: 1117 if self._standard_time_limit_time == 10: 1118 assert self._standard_time_limit_text is not None 1119 assert self._standard_time_limit_text.node 1120 self._standard_time_limit_text.node.scale = 1.3 1121 self._standard_time_limit_text.node.position = (-30, -45) 1122 cnode = _bascenev1.newnode( 1123 'combine', 1124 owner=self._standard_time_limit_text.node, 1125 attrs={'size': 4}, 1126 ) 1127 cnode.connectattr( 1128 'output', self._standard_time_limit_text.node, 'color' 1129 ) 1130 animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) 1131 animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) 1132 animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) 1133 cnode.input3 = 1.0 1134 _bascenev1.getsound('tick').play() 1135 if self._standard_time_limit_time <= 0: 1136 self._standard_time_limit_timer = None 1137 self.end_game() 1138 node = _bascenev1.newnode( 1139 'text', 1140 attrs={ 1141 'v_attach': 'top', 1142 'h_attach': 'center', 1143 'h_align': 'center', 1144 'color': (1, 0.7, 0, 1), 1145 'position': (0, -90), 1146 'scale': 1.2, 1147 'text': babase.Lstr(resource='timeExpiredText'), 1148 }, 1149 ) 1150 _bascenev1.getsound('refWhistle').play() 1151 animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2}) 1152 1153 def _setup_tournament_time_limit(self, duration: float) -> None: 1154 """ 1155 Create a tournament game time-limit given the provided 1156 duration in seconds. 1157 This will be displayed at the top of the screen. 1158 If the time-limit expires, end_game() will be called. 1159 """ 1160 from bascenev1._nodeactor import NodeActor 1161 1162 if duration <= 0.0: 1163 return 1164 self._tournament_time_limit = int(duration) 1165 1166 # We want this timer to match the server's time as close as possible, 1167 # so lets go with base-time. Theoretically we should do real-time but 1168 # then we have to mess with contexts and whatnot since its currently 1169 # not available in activity contexts. :-/ 1170 self._tournament_time_limit_timer = _bascenev1.BaseTimer( 1171 1.0, babase.WeakCall(self._tournament_time_limit_tick), repeat=True 1172 ) 1173 self._tournament_time_limit_title_text = NodeActor( 1174 _bascenev1.newnode( 1175 'text', 1176 attrs={ 1177 'v_attach': 'bottom', 1178 'h_attach': 'left', 1179 'h_align': 'center', 1180 'v_align': 'center', 1181 'vr_depth': 300, 1182 'maxwidth': 100, 1183 'color': (1.0, 1.0, 1.0, 0.5), 1184 'position': (60, 50), 1185 'flatness': 1.0, 1186 'scale': 0.5, 1187 'text': babase.Lstr(resource='tournamentText'), 1188 }, 1189 ) 1190 ) 1191 self._tournament_time_limit_text = NodeActor( 1192 _bascenev1.newnode( 1193 'text', 1194 attrs={ 1195 'v_attach': 'bottom', 1196 'h_attach': 'left', 1197 'h_align': 'center', 1198 'v_align': 'center', 1199 'vr_depth': 300, 1200 'maxwidth': 100, 1201 'color': (1.0, 1.0, 1.0, 0.5), 1202 'position': (60, 30), 1203 'flatness': 1.0, 1204 'scale': 0.9, 1205 }, 1206 ) 1207 ) 1208 self._tournament_time_limit_text_input = NodeActor( 1209 _bascenev1.newnode( 1210 'timedisplay', 1211 attrs={ 1212 'timemin': 0, 1213 'time2': self._tournament_time_limit * 1000, 1214 }, 1215 ) 1216 ) 1217 assert self._tournament_time_limit_text.node 1218 assert self._tournament_time_limit_text_input.node 1219 self._tournament_time_limit_text_input.node.connectattr( 1220 'output', self._tournament_time_limit_text.node, 'text' 1221 ) 1222 1223 def _tournament_time_limit_tick(self) -> None: 1224 from bascenev1._gameutils import animate 1225 1226 assert self._tournament_time_limit is not None 1227 self._tournament_time_limit -= 1 1228 if self._tournament_time_limit <= 10: 1229 if self._tournament_time_limit == 10: 1230 assert self._tournament_time_limit_title_text is not None 1231 assert self._tournament_time_limit_title_text.node 1232 assert self._tournament_time_limit_text is not None 1233 assert self._tournament_time_limit_text.node 1234 self._tournament_time_limit_title_text.node.scale = 1.0 1235 self._tournament_time_limit_text.node.scale = 1.3 1236 self._tournament_time_limit_title_text.node.position = (80, 85) 1237 self._tournament_time_limit_text.node.position = (80, 60) 1238 cnode = _bascenev1.newnode( 1239 'combine', 1240 owner=self._tournament_time_limit_text.node, 1241 attrs={'size': 4}, 1242 ) 1243 cnode.connectattr( 1244 'output', 1245 self._tournament_time_limit_title_text.node, 1246 'color', 1247 ) 1248 cnode.connectattr( 1249 'output', self._tournament_time_limit_text.node, 'color' 1250 ) 1251 animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True) 1252 animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True) 1253 animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True) 1254 cnode.input3 = 1.0 1255 _bascenev1.getsound('tick').play() 1256 if self._tournament_time_limit <= 0: 1257 self._tournament_time_limit_timer = None 1258 self.end_game() 1259 tval = babase.Lstr( 1260 resource='tournamentTimeExpiredText', 1261 fallback_resource='timeExpiredText', 1262 ) 1263 node = _bascenev1.newnode( 1264 'text', 1265 attrs={ 1266 'v_attach': 'top', 1267 'h_attach': 'center', 1268 'h_align': 'center', 1269 'color': (1, 0.7, 0, 1), 1270 'position': (0, -200), 1271 'scale': 1.6, 1272 'text': tval, 1273 }, 1274 ) 1275 _bascenev1.getsound('refWhistle').play() 1276 animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2}) 1277 1278 # Normally we just connect this to time, but since this is a bit of a 1279 # funky setup we just update it manually once per second. 1280 assert self._tournament_time_limit_text_input is not None 1281 assert self._tournament_time_limit_text_input.node 1282 self._tournament_time_limit_text_input.node.time2 = ( 1283 self._tournament_time_limit * 1000 1284 ) 1285 1286 def show_zoom_message( 1287 self, 1288 message: babase.Lstr, 1289 color: Sequence[float] = (0.9, 0.4, 0.0), 1290 scale: float = 0.8, 1291 duration: float = 2.0, 1292 trail: bool = False, 1293 ) -> None: 1294 """Zooming text used to announce game names and winners.""" 1295 # pylint: disable=cyclic-import 1296 from bascenev1lib.actor.zoomtext import ZoomText 1297 1298 # Reserve a spot on the screen (in case we get multiple of these so 1299 # they don't overlap). 1300 i = 0 1301 cur_time = babase.apptime() 1302 while True: 1303 if ( 1304 i not in self._zoom_message_times 1305 or self._zoom_message_times[i] < cur_time 1306 ): 1307 self._zoom_message_times[i] = cur_time + duration 1308 break 1309 i += 1 1310 ZoomText( 1311 message, 1312 lifespan=duration, 1313 jitter=2.0, 1314 position=(0, 200 - i * 100), 1315 scale=scale, 1316 maxwidth=800, 1317 trail=trail, 1318 color=color, 1319 ).autoretain() 1320 1321 def _calc_map_name(self, settings: dict) -> str: 1322 map_name: str 1323 if 'map' in settings: 1324 map_name = settings['map'] 1325 else: 1326 # If settings doesn't specify a map, pick a random one from the 1327 # list of supported ones. 1328 unowned_maps: list[str] = ( 1329 babase.app.classic.store.get_unowned_maps() 1330 if babase.app.classic is not None 1331 else [] 1332 ) 1333 valid_maps: list[str] = [ 1334 m 1335 for m in self.get_supported_maps(type(self.session)) 1336 if m not in unowned_maps 1337 ] 1338 if not valid_maps: 1339 _bascenev1.broadcastmessage( 1340 babase.Lstr(resource='noValidMapsErrorText') 1341 ) 1342 raise RuntimeError('No valid maps') 1343 map_name = valid_maps[random.randrange(len(valid_maps))] 1344 return map_name
Common base class for all game bascenev1.Activities.
Category: Gameplay Classes
246 def __init__(self, settings: dict): 247 """Instantiate the Activity.""" 248 super().__init__(settings) 249 250 plus = babase.app.plus 251 252 # Holds some flattened info about the player set at the point 253 # when on_begin() is called. 254 self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None 255 256 # Go ahead and get our map loading. 257 self._map_type = _map.get_map_class(self._calc_map_name(settings)) 258 259 self._spawn_sound = _bascenev1.getsound('spawn') 260 self._map_type.preload() 261 self._map: bascenev1.Map | None = None 262 self._powerup_drop_timer: bascenev1.Timer | None = None 263 self._tnt_spawners: dict[int, TNTSpawner] | None = None 264 self._tnt_drop_timer: bascenev1.Timer | None = None 265 self._game_scoreboard_name_text: bascenev1.Actor | None = None 266 self._game_scoreboard_description_text: bascenev1.Actor | None = None 267 self._standard_time_limit_time: int | None = None 268 self._standard_time_limit_timer: bascenev1.Timer | None = None 269 self._standard_time_limit_text: bascenev1.NodeActor | None = None 270 self._standard_time_limit_text_input: bascenev1.NodeActor | None = None 271 self._tournament_time_limit: int | None = None 272 self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None 273 self._tournament_time_limit_title_text: bascenev1.NodeActor | None = ( 274 None 275 ) 276 self._tournament_time_limit_text: bascenev1.NodeActor | None = None 277 self._tournament_time_limit_text_input: bascenev1.NodeActor | None = ( 278 None 279 ) 280 self._zoom_message_times: dict[int, float] = {} 281 self._is_waiting_for_continue = False 282 283 self._continue_cost = ( 284 25 285 if plus is None 286 else plus.get_v1_account_misc_read_val('continueStartCost', 25) 287 ) 288 self._continue_cost_mult = ( 289 2 290 if plus is None 291 else plus.get_v1_account_misc_read_val('continuesMult', 2) 292 ) 293 self._continue_cost_offset = ( 294 0 295 if plus is None 296 else plus.get_v1_account_misc_read_val('continuesOffset', 0) 297 )
Instantiate the Activity.
Whether idle players can potentially be kicked (should not happen in menus/etc).
68 @classmethod 69 def create_settings_ui( 70 cls, 71 sessiontype: type[bascenev1.Session], 72 settings: dict | None, 73 completion_call: Callable[[dict | None], None], 74 ) -> None: 75 """Launch an in-game UI to configure settings for a game type. 76 77 'sessiontype' should be the bascenev1.Session class the game will 78 be used in. 79 80 'settings' should be an existing settings dict (implies 'edit' 81 ui mode) or None (implies 'add' ui mode). 82 83 'completion_call' will be called with a filled-out settings dict on 84 success or None on cancel. 85 86 Generally subclasses don't need to override this; if they override 87 bascenev1.GameActivity.get_available_settings() and 88 bascenev1.GameActivity.get_supported_maps() they can just rely on 89 the default implementation here which calls those methods. 90 """ 91 # pylint: disable=cyclic-import 92 from bauiv1lib.playlist.editgame import PlaylistEditGameWindow 93 94 assert babase.app.classic is not None 95 babase.app.ui_v1.clear_main_window() 96 babase.app.ui_v1.set_main_window( 97 PlaylistEditGameWindow( 98 cls, 99 sessiontype, 100 settings, 101 completion_call=completion_call, 102 ), 103 from_window=False, # Disable check since we don't know. 104 )
Launch an in-game UI to configure settings for a game type.
'sessiontype' should be the Session class the game will be used in.
'settings' should be an existing settings dict (implies 'edit' ui mode) or None (implies 'add' ui mode).
'completion_call' will be called with a filled-out settings dict on success or None on cancel.
Generally subclasses don't need to override this; if they override GameActivity.get_available_settings() and GameActivity.get_supported_maps() they can just rely on the default implementation here which calls those methods.
106 @classmethod 107 def getscoreconfig(cls) -> bascenev1.ScoreConfig: 108 """Return info about game scoring setup; can be overridden by games.""" 109 return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig()
Return info about game scoring setup; can be overridden by games.
111 @classmethod 112 def getname(cls) -> str: 113 """Return a str name for this game type. 114 115 This default implementation simply returns the 'name' class attr. 116 """ 117 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.
119 @classmethod 120 def get_display_string(cls, settings: dict | None = None) -> babase.Lstr: 121 """Return a descriptive name for this game/settings combo. 122 123 Subclasses should override getname(); not this. 124 """ 125 name = babase.Lstr(translate=('gameNames', cls.getname())) 126 127 # A few substitutions for 'Epic', 'Solo' etc. modes. 128 # FIXME: Should provide a way for game types to define filters of 129 # their own and should not rely on hard-coded settings names. 130 if settings is not None: 131 if 'Solo Mode' in settings and settings['Solo Mode']: 132 name = babase.Lstr( 133 resource='soloNameFilterText', subs=[('${NAME}', name)] 134 ) 135 if 'Epic Mode' in settings and settings['Epic Mode']: 136 name = babase.Lstr( 137 resource='epicNameFilterText', subs=[('${NAME}', name)] 138 ) 139 140 return name
Return a descriptive name for this game/settings combo.
Subclasses should override getname(); not this.
142 @classmethod 143 def get_team_display_string(cls, name: str) -> babase.Lstr: 144 """Given a team name, returns a localized version of it.""" 145 return babase.Lstr(translate=('teamNames', name))
Given a team name, returns a localized version of it.
147 @classmethod 148 def get_description(cls, sessiontype: type[bascenev1.Session]) -> str: 149 """Get a str description of this game type. 150 151 The default implementation simply returns the 'description' class var. 152 Classes which want to change their description depending on the session 153 can override this method. 154 """ 155 del sessiontype # Unused arg. 156 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.
158 @classmethod 159 def get_description_display_string( 160 cls, sessiontype: type[bascenev1.Session] 161 ) -> babase.Lstr: 162 """Return a translated version of get_description(). 163 164 Sub-classes should override get_description(); not this. 165 """ 166 description = cls.get_description(sessiontype) 167 return babase.Lstr(translate=('gameDescriptions', description))
Return a translated version of get_description().
Sub-classes should override get_description(); not this.
169 @classmethod 170 def get_available_settings( 171 cls, sessiontype: type[bascenev1.Session] 172 ) -> list[bascenev1.Setting]: 173 """Return a list of settings relevant to this game type when 174 running under the provided session type. 175 """ 176 del sessiontype # Unused arg. 177 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.
179 @classmethod 180 def get_supported_maps( 181 cls, sessiontype: type[bascenev1.Session] 182 ) -> list[str]: 183 """ 184 Called by the default bascenev1.GameActivity.create_settings_ui() 185 implementation; should return a list of map names valid 186 for this game-type for the given bascenev1.Session type. 187 """ 188 del sessiontype # Unused arg. 189 assert babase.app.classic is not None 190 return babase.app.classic.getmaps('melee')
Called by the default GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given Session type.
192 @classmethod 193 def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr: 194 """Given a game config dict, return a short description for it. 195 196 This is used when viewing game-lists or showing what game 197 is up next in a series. 198 """ 199 name = cls.get_display_string(config['settings']) 200 201 # In newer configs, map is in settings; it used to be in the 202 # config root. 203 if 'map' in config['settings']: 204 sval = babase.Lstr( 205 value='${NAME} @ ${MAP}', 206 subs=[ 207 ('${NAME}', name), 208 ( 209 '${MAP}', 210 _map.get_map_display_string( 211 _map.get_filtered_map_name( 212 config['settings']['map'] 213 ) 214 ), 215 ), 216 ], 217 ) 218 elif 'map' in config: 219 sval = babase.Lstr( 220 value='${NAME} @ ${MAP}', 221 subs=[ 222 ('${NAME}', name), 223 ( 224 '${MAP}', 225 _map.get_map_display_string( 226 _map.get_filtered_map_name(config['map']) 227 ), 228 ), 229 ], 230 ) 231 else: 232 print('invalid game config - expected map entry under settings') 233 sval = babase.Lstr(value='???') 234 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.
236 @classmethod 237 def supports_session_type( 238 cls, sessiontype: type[bascenev1.Session] 239 ) -> bool: 240 """Return whether this game supports the provided Session type.""" 241 from bascenev1._multiteamsession import MultiTeamSession 242 243 # By default, games support any versus mode 244 return issubclass(sessiontype, MultiTeamSession)
Return whether this game supports the provided Session type.
299 @property 300 def map(self) -> _map.Map: 301 """The map being used for this game. 302 303 Raises a bascenev1.MapNotFoundError if the map does not currently 304 exist. 305 """ 306 if self._map is None: 307 raise babase.MapNotFoundError 308 return self._map
The map being used for this game.
Raises a bascenev1.MapNotFoundError if the map does not currently exist.
310 def get_instance_display_string(self) -> babase.Lstr: 311 """Return a name for this particular game instance.""" 312 return self.get_display_string(self.settings_raw)
Return a name for this particular game instance.
315 def get_instance_scoreboard_display_string(self) -> babase.Lstr: 316 """Return a name for this particular game instance. 317 318 This name is used above the game scoreboard in the corner 319 of the screen, so it should be as concise as possible. 320 """ 321 # If we're in a co-op session, use the level name. 322 # FIXME: Should clean this up. 323 try: 324 from bascenev1._coopsession import CoopSession 325 326 if isinstance(self.session, CoopSession): 327 campaign = self.session.campaign 328 assert campaign is not None 329 return campaign.getlevel( 330 self.session.campaign_level_name 331 ).displayname 332 except Exception: 333 logging.exception('Error getting campaign level name.') 334 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.
336 def get_instance_description(self) -> str | Sequence: 337 """Return a description for this game instance, in English. 338 339 This is shown in the center of the screen below the game name at the 340 start of a game. It should start with a capital letter and end with a 341 period, and can be a bit more verbose than the version returned by 342 get_instance_description_short(). 343 344 Note that translation is applied by looking up the specific returned 345 value as a key, so the number of returned variations should be limited; 346 ideally just one or two. To include arbitrary values in the 347 description, you can return a sequence of values in the following 348 form instead of just a string: 349 350 # This will give us something like 'Score 3 goals.' in English 351 # and can properly translate to 'Anota 3 goles.' in Spanish. 352 # If we just returned the string 'Score 3 Goals' here, there would 353 # have to be a translation entry for each specific number. ew. 354 return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']] 355 356 This way the first string can be consistently translated, with any arg 357 values then substituted into the result. ${ARG1} will be replaced with 358 the first value, ${ARG2} with the second, etc. 359 """ 360 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.
362 def get_instance_description_short(self) -> str | Sequence: 363 """Return a short description for this game instance in English. 364 365 This description is used above the game scoreboard in the 366 corner of the screen, so it should be as concise as possible. 367 It should be lowercase and should not contain periods or other 368 punctuation. 369 370 Note that translation is applied by looking up the specific returned 371 value as a key, so the number of returned variations should be limited; 372 ideally just one or two. To include arbitrary values in the 373 description, you can return a sequence of values in the following form 374 instead of just a string: 375 376 # This will give us something like 'score 3 goals' in English 377 # and can properly translate to 'anota 3 goles' in Spanish. 378 # If we just returned the string 'score 3 goals' here, there would 379 # have to be a translation entry for each specific number. ew. 380 return ['score ${ARG1} goals', self.settings_raw['Score to Win']] 381 382 This way the first string can be consistently translated, with any arg 383 values then substituted into the result. ${ARG1} will be replaced 384 with the first value, ${ARG2} with the second, etc. 385 386 """ 387 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.
389 @override 390 def on_transition_in(self) -> None: 391 super().on_transition_in() 392 393 # Make our map. 394 self._map = self._map_type() 395 396 # Give our map a chance to override the music. 397 # (for happy-thoughts and other such themed maps) 398 map_music = self._map_type.get_music_type() 399 music = map_music if map_music is not None else self.default_music 400 401 if music is not None: 402 _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 Activity.on_begin() is called.
404 def on_continue(self) -> None: 405 """ 406 This is called if a game supports and offers a continue and the player 407 accepts. In this case the player should be given an extra life or 408 whatever is relevant to keep the game going. 409 """
This is called if a game supports and offers a continue and the player accepts. In this case the player should be given an extra life or whatever is relevant to keep the game going.
434 def is_waiting_for_continue(self) -> bool: 435 """Returns whether or not this activity is currently waiting for the 436 player to continue (or timeout)""" 437 return self._is_waiting_for_continue
Returns whether or not this activity is currently waiting for the player to continue (or timeout)
439 def continue_or_end_game(self) -> None: 440 """If continues are allowed, prompts the player to purchase a continue 441 and calls either end_game or continue_game depending on the result 442 """ 443 # pylint: disable=too-many-nested-blocks 444 # pylint: disable=cyclic-import 445 from bascenev1._coopsession import CoopSession 446 447 classic = babase.app.classic 448 assert classic is not None 449 continues_window = classic.continues_window 450 451 # Turning these off. I want to migrate towards monetization that 452 # feels less pay-to-win-ish. 453 allow_continues = False 454 455 plus = babase.app.plus 456 try: 457 if ( 458 plus is not None 459 and plus.get_v1_account_misc_read_val('enableContinues', False) 460 and allow_continues 461 ): 462 session = self.session 463 464 # We only support continuing in non-tournament games. 465 tournament_id = session.tournament_id 466 if tournament_id is None: 467 # We currently only support continuing in sequential 468 # co-op campaigns. 469 if isinstance(session, CoopSession): 470 assert session.campaign is not None 471 if session.campaign.sequential: 472 gnode = self.globalsnode 473 474 # Only attempt this if we're not currently paused 475 # and there appears to be no UI. 476 assert babase.app.classic is not None 477 hmmw = babase.app.ui_v1.has_main_window() 478 if not gnode.paused and not hmmw: 479 self._is_waiting_for_continue = True 480 with babase.ContextRef.empty(): 481 babase.apptimer( 482 0.5, 483 lambda: continues_window( 484 self, 485 self._continue_cost, 486 continue_call=babase.WeakCall( 487 self._continue_choice, True 488 ), 489 cancel_call=babase.WeakCall( 490 self._continue_choice, False 491 ), 492 ), 493 ) 494 return 495 496 except Exception: 497 logging.exception('Error handling continues.') 498 499 self.end_game()
If continues are allowed, prompts the player to purchase a continue and calls either end_game or continue_game depending on the result
501 @override 502 def on_begin(self) -> None: 503 super().on_begin() 504 505 if babase.app.classic is not None: 506 babase.app.classic.game_begin_analytics() 507 508 # We don't do this in on_transition_in because it may depend on 509 # players/teams which aren't available until now. 510 _bascenev1.timer(0.001, self._show_scoreboard_info) 511 _bascenev1.timer(1.0, self._show_info) 512 _bascenev1.timer(2.5, self._show_tip) 513 514 # Store some basic info about players present at start time. 515 self.initialplayerinfos = [ 516 PlayerInfo(name=p.getname(full=True), character=p.character) 517 for p in self.players 518 ] 519 520 # Sort this by name so high score lists/etc will be consistent 521 # regardless of player join order. 522 self.initialplayerinfos.sort(key=lambda x: x.name) 523 524 # If this is a tournament, query info about it such as how much 525 # time is left. 526 tournament_id = self.session.tournament_id 527 if tournament_id is not None: 528 assert babase.app.plus is not None 529 babase.app.plus.tournament_query( 530 args={ 531 'tournamentIDs': [tournament_id], 532 'source': 'in-game time remaining query', 533 }, 534 callback=babase.WeakCall(self._on_tournament_query_response), 535 )
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.
550 @override 551 def on_player_join(self, player: PlayerT) -> None: 552 super().on_player_join(player) 553 554 # By default, just spawn a dude. 555 self.spawn_player(player)
Called when a new Player has joined the Activity.
(including the initial set of Players)
557 @override 558 def handlemessage(self, msg: Any) -> Any: 559 if isinstance(msg, PlayerDiedMessage): 560 # pylint: disable=cyclic-import 561 from bascenev1lib.actor.spaz import Spaz 562 563 player = msg.getplayer(self.playertype) 564 killer = msg.getkillerplayer(self.playertype) 565 566 # Inform our stats of the demise. 567 self.stats.player_was_killed( 568 player, killed=msg.killed, killer=killer 569 ) 570 571 # Award the killer points if he's on a different team. 572 # FIXME: This should not be linked to Spaz actors. 573 # (should move get_death_points to Actor or make it a message) 574 if killer and killer.team is not player.team: 575 assert isinstance(killer.actor, Spaz) 576 pts, importance = killer.actor.get_death_points(msg.how) 577 if not self.has_ended(): 578 self.stats.player_scored( 579 killer, 580 pts, 581 kill=True, 582 victim_player=player, 583 importance=importance, 584 showpoints=self.show_kill_points, 585 ) 586 else: 587 return super().handlemessage(msg) 588 return None
General message handling; can be passed any message object.
851 @override 852 def end( 853 self, results: Any = None, delay: float = 0.0, force: bool = False 854 ) -> None: 855 from bascenev1._gameresults import GameResults 856 857 # If results is a standard team-game-results, associate it with us 858 # so it can grab our score prefs. 859 if isinstance(results, GameResults): 860 results.set_game(self) 861 862 # If we had a standard time-limit that had not expired, stop it so 863 # it doesnt tick annoyingly. 864 if ( 865 self._standard_time_limit_time is not None 866 and self._standard_time_limit_time > 0 867 ): 868 self._standard_time_limit_timer = None 869 self._standard_time_limit_text = None 870 871 # Ditto with tournament time limits. 872 if ( 873 self._tournament_time_limit is not None 874 and self._tournament_time_limit > 0 875 ): 876 self._tournament_time_limit_timer = None 877 self._tournament_time_limit_text = None 878 self._tournament_time_limit_title_text = None 879 880 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.
882 def end_game(self) -> None: 883 """Tell the game to wrap up and call bascenev1.Activity.end(). 884 885 This method should be overridden by subclasses. A game should always 886 be prepared to end and deliver results, even if there is no 'winner' 887 yet; this way things like the standard time-limit 888 (bascenev1.GameActivity.setup_standard_time_limit()) will work with 889 the game. 890 """ 891 print( 892 'WARNING: default end_game() implementation called;' 893 ' your game should override this.' 894 )
Tell the game to wrap up and call 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 (GameActivity.setup_standard_time_limit()) will work with the game.
896 def respawn_player( 897 self, player: PlayerT, respawn_time: float | None = None 898 ) -> None: 899 """ 900 Given a bascenev1.Player, sets up a standard respawn timer, 901 along with the standard counter display, etc. 902 At the end of the respawn period spawn_player() will 903 be called if the Player still exists. 904 An explicit 'respawn_time' can optionally be provided 905 (in seconds). 906 """ 907 # pylint: disable=cyclic-import 908 909 assert player 910 if respawn_time is None: 911 teamsize = len(player.team.players) 912 if teamsize == 1: 913 respawn_time = 3.0 914 elif teamsize == 2: 915 respawn_time = 5.0 916 elif teamsize == 3: 917 respawn_time = 6.0 918 else: 919 respawn_time = 7.0 920 921 # If this standard setting is present, factor it in. 922 if 'Respawn Times' in self.settings_raw: 923 respawn_time *= self.settings_raw['Respawn Times'] 924 925 # We want whole seconds. 926 assert respawn_time is not None 927 respawn_time = round(max(1.0, respawn_time), 0) 928 929 if player.actor and not self.has_ended(): 930 from bascenev1lib.actor.respawnicon import RespawnIcon 931 932 player.customdata['respawn_timer'] = _bascenev1.Timer( 933 respawn_time, 934 babase.WeakCall(self.spawn_player_if_exists, player), 935 ) 936 player.customdata['respawn_icon'] = RespawnIcon( 937 player, respawn_time 938 )
Given a 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).
940 def spawn_player_if_exists(self, player: PlayerT) -> None: 941 """ 942 A utility method which calls self.spawn_player() *only* if the 943 bascenev1.Player provided still exists; handy for use in timers 944 and whatnot. 945 946 There is no need to override this; just override spawn_player(). 947 """ 948 if player: 949 self.spawn_player(player)
A utility method which calls self.spawn_player() only if the Player provided still exists; handy for use in timers and whatnot.
There is no need to override this; just override spawn_player().
951 def spawn_player(self, player: PlayerT) -> bascenev1.Actor: 952 """Spawn *something* for the provided bascenev1.Player. 953 954 The default implementation simply calls spawn_player_spaz(). 955 """ 956 assert player # Dead references should never be passed as args. 957 958 return self.spawn_player_spaz(player)
Spawn something for the provided Player.
The default implementation simply calls spawn_player_spaz().
960 def spawn_player_spaz( 961 self, 962 player: PlayerT, 963 position: Sequence[float] = (0, 0, 0), 964 angle: float | None = None, 965 ) -> PlayerSpaz: 966 """Create and wire up a bascenev1.PlayerSpaz for the provided Player.""" 967 # pylint: disable=too-many-locals 968 # pylint: disable=cyclic-import 969 from bascenev1._gameutils import animate 970 from bascenev1._coopsession import CoopSession 971 from bascenev1lib.actor.playerspaz import PlayerSpaz 972 973 name = player.getname() 974 color = player.color 975 highlight = player.highlight 976 977 playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz) 978 if not issubclass(playerspaztype, PlayerSpaz): 979 playerspaztype = PlayerSpaz 980 981 light_color = babase.normalized_color(color) 982 display_color = babase.safecolor(color, target_intensity=0.75) 983 spaz = playerspaztype( 984 color=color, 985 highlight=highlight, 986 character=player.character, 987 player=player, 988 ) 989 990 player.actor = spaz 991 assert spaz.node 992 993 # If this is co-op and we're on Courtyard or Runaround, add the 994 # material that allows us to collide with the player-walls. 995 # FIXME: Need to generalize this. 996 if isinstance(self.session, CoopSession) and self.map.getname() in [ 997 'Courtyard', 998 'Tower D', 999 ]: 1000 mat = self.map.preloaddata['collide_with_wall_material'] 1001 assert isinstance(spaz.node.materials, tuple) 1002 assert isinstance(spaz.node.roller_materials, tuple) 1003 spaz.node.materials += (mat,) 1004 spaz.node.roller_materials += (mat,) 1005 1006 spaz.node.name = name 1007 spaz.node.name_color = display_color 1008 spaz.connect_controls_to_player() 1009 1010 # Move to the stand position and add a flash of light. 1011 spaz.handlemessage( 1012 StandMessage( 1013 position, angle if angle is not None else random.uniform(0, 360) 1014 ) 1015 ) 1016 self._spawn_sound.play(1, position=spaz.node.position) 1017 light = _bascenev1.newnode('light', attrs={'color': light_color}) 1018 spaz.node.connectattr('position', light, 'position') 1019 animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}) 1020 _bascenev1.timer(0.5, light.delete) 1021 return spaz
Create and wire up a bascenev1.PlayerSpaz for the provided Player.
1023 def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None: 1024 """Create standard powerup drops for the current map.""" 1025 # pylint: disable=cyclic-import 1026 from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL 1027 1028 self._powerup_drop_timer = _bascenev1.Timer( 1029 DEFAULT_POWERUP_INTERVAL, 1030 babase.WeakCall(self._standard_drop_powerups), 1031 repeat=True, 1032 ) 1033 self._standard_drop_powerups() 1034 if enable_tnt: 1035 self._tnt_spawners = {} 1036 self._setup_standard_tnt_drops()
Create standard powerup drops for the current map.
1068 def setup_standard_time_limit(self, duration: float) -> None: 1069 """ 1070 Create a standard game time-limit given the provided 1071 duration in seconds. 1072 This will be displayed at the top of the screen. 1073 If the time-limit expires, end_game() will be called. 1074 """ 1075 from bascenev1._nodeactor import NodeActor 1076 1077 if duration <= 0.0: 1078 return 1079 self._standard_time_limit_time = int(duration) 1080 self._standard_time_limit_timer = _bascenev1.Timer( 1081 1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True 1082 ) 1083 self._standard_time_limit_text = NodeActor( 1084 _bascenev1.newnode( 1085 'text', 1086 attrs={ 1087 'v_attach': 'top', 1088 'h_attach': 'center', 1089 'h_align': 'left', 1090 'color': (1.0, 1.0, 1.0, 0.5), 1091 'position': (-25, -30), 1092 'flatness': 1.0, 1093 'scale': 0.9, 1094 }, 1095 ) 1096 ) 1097 self._standard_time_limit_text_input = NodeActor( 1098 _bascenev1.newnode( 1099 'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0} 1100 ) 1101 ) 1102 self.globalsnode.connectattr( 1103 'time', self._standard_time_limit_text_input.node, 'time1' 1104 ) 1105 assert self._standard_time_limit_text_input.node 1106 assert self._standard_time_limit_text.node 1107 self._standard_time_limit_text_input.node.connectattr( 1108 'output', self._standard_time_limit_text.node, 'text' 1109 )
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.
1286 def show_zoom_message( 1287 self, 1288 message: babase.Lstr, 1289 color: Sequence[float] = (0.9, 0.4, 0.0), 1290 scale: float = 0.8, 1291 duration: float = 2.0, 1292 trail: bool = False, 1293 ) -> None: 1294 """Zooming text used to announce game names and winners.""" 1295 # pylint: disable=cyclic-import 1296 from bascenev1lib.actor.zoomtext import ZoomText 1297 1298 # Reserve a spot on the screen (in case we get multiple of these so 1299 # they don't overlap). 1300 i = 0 1301 cur_time = babase.apptime() 1302 while True: 1303 if ( 1304 i not in self._zoom_message_times 1305 or self._zoom_message_times[i] < cur_time 1306 ): 1307 self._zoom_message_times[i] = cur_time + duration 1308 break 1309 i += 1 1310 ZoomText( 1311 message, 1312 lifespan=duration, 1313 jitter=2.0, 1314 position=(0, 200 - i * 100), 1315 scale=scale, 1316 maxwidth=800, 1317 trail=trail, 1318 color=color, 1319 ).autoretain()
Zooming text used to announce game names and winners.
Inherited Members
- Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
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 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 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.
32@dataclass 33class GameTip: 34 """Defines a tip presentable to the user at the start of a game. 35 36 Category: **Gameplay Classes** 37 """ 38 39 text: str 40 icon: bascenev1.Texture | None = None 41 sound: bascenev1.Sound | None = None
Defines a tip presentable to the user at the start of a game.
Category: Gameplay Classes
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.
225def get_default_free_for_all_playlist() -> PlaylistType: 226 """Return a default playlist for free-for-all mode.""" 227 228 # NOTE: these are currently using old type/map names, 229 # but filtering translates them properly to the new ones. 230 # (is kinda a handy way to ensure filtering is working). 231 # Eventually should update these though. 232 return [ 233 { 234 'settings': { 235 'Epic Mode': False, 236 'Kills to Win Per Player': 10, 237 'Respawn Times': 1.0, 238 'Time Limit': 300, 239 'map': 'Doom Shroom', 240 }, 241 'type': 'bs_death_match.DeathMatchGame', 242 }, 243 { 244 'settings': { 245 'Chosen One Gets Gloves': True, 246 'Chosen One Gets Shield': False, 247 'Chosen One Time': 30, 248 'Epic Mode': 0, 249 'Respawn Times': 1.0, 250 'Time Limit': 300, 251 'map': 'Monkey Face', 252 }, 253 'type': 'bs_chosen_one.ChosenOneGame', 254 }, 255 { 256 'settings': { 257 'Hold Time': 30, 258 'Respawn Times': 1.0, 259 'Time Limit': 300, 260 'map': 'Zigzag', 261 }, 262 'type': 'bs_king_of_the_hill.KingOfTheHillGame', 263 }, 264 { 265 'settings': {'Epic Mode': False, 'map': 'Rampage'}, 266 'type': 'bs_meteor_shower.MeteorShowerGame', 267 }, 268 { 269 'settings': { 270 'Epic Mode': 1, 271 'Lives Per Player': 1, 272 'Respawn Times': 1.0, 273 'Time Limit': 120, 274 'map': 'Tip Top', 275 }, 276 'type': 'bs_elimination.EliminationGame', 277 }, 278 { 279 'settings': { 280 'Hold Time': 30, 281 'Respawn Times': 1.0, 282 'Time Limit': 300, 283 'map': 'The Pad', 284 }, 285 'type': 'bs_keep_away.KeepAwayGame', 286 }, 287 { 288 'settings': { 289 'Epic Mode': True, 290 'Kills to Win Per Player': 10, 291 'Respawn Times': 0.25, 292 'Time Limit': 120, 293 'map': 'Rampage', 294 }, 295 'type': 'bs_death_match.DeathMatchGame', 296 }, 297 { 298 'settings': { 299 'Bomb Spawning': 1000, 300 'Epic Mode': False, 301 'Laps': 3, 302 'Mine Spawn Interval': 4000, 303 'Mine Spawning': 4000, 304 'Time Limit': 300, 305 'map': 'Big G', 306 }, 307 'type': 'bs_race.RaceGame', 308 }, 309 { 310 'settings': { 311 'Hold Time': 30, 312 'Respawn Times': 1.0, 313 'Time Limit': 300, 314 'map': 'Happy Thoughts', 315 }, 316 'type': 'bs_king_of_the_hill.KingOfTheHillGame', 317 }, 318 { 319 'settings': { 320 'Enable Impact Bombs': 1, 321 'Enable Triple Bombs': False, 322 'Target Count': 2, 323 'map': 'Doom Shroom', 324 }, 325 'type': 'bs_target_practice.TargetPracticeGame', 326 }, 327 { 328 'settings': { 329 'Epic Mode': False, 330 'Lives Per Player': 5, 331 'Respawn Times': 1.0, 332 'Time Limit': 300, 333 'map': 'Step Right Up', 334 }, 335 'type': 'bs_elimination.EliminationGame', 336 }, 337 { 338 'settings': { 339 'Epic Mode': False, 340 'Kills to Win Per Player': 10, 341 'Respawn Times': 1.0, 342 'Time Limit': 300, 343 'map': 'Crag Castle', 344 }, 345 'type': 'bs_death_match.DeathMatchGame', 346 }, 347 { 348 'map': 'Lake Frigid', 349 'settings': { 350 'Bomb Spawning': 0, 351 'Epic Mode': False, 352 'Laps': 6, 353 'Mine Spawning': 2000, 354 'Time Limit': 300, 355 'map': 'Lake Frigid', 356 }, 357 'type': 'bs_race.RaceGame', 358 }, 359 ]
Return a default playlist for free-for-all mode.
362def get_default_teams_playlist() -> PlaylistType: 363 """Return a default playlist for teams mode.""" 364 365 # NOTE: these are currently using old type/map names, 366 # but filtering translates them properly to the new ones. 367 # (is kinda a handy way to ensure filtering is working). 368 # Eventually should update these though. 369 return [ 370 { 371 'settings': { 372 'Epic Mode': False, 373 'Flag Idle Return Time': 30, 374 'Flag Touch Return Time': 0, 375 'Respawn Times': 1.0, 376 'Score to Win': 3, 377 'Time Limit': 600, 378 'map': 'Bridgit', 379 }, 380 'type': 'bs_capture_the_flag.CTFGame', 381 }, 382 { 383 'settings': { 384 'Epic Mode': False, 385 'Respawn Times': 1.0, 386 'Score to Win': 3, 387 'Time Limit': 600, 388 'map': 'Step Right Up', 389 }, 390 'type': 'bs_assault.AssaultGame', 391 }, 392 { 393 'settings': { 394 'Balance Total Lives': False, 395 'Epic Mode': False, 396 'Lives Per Player': 3, 397 'Respawn Times': 1.0, 398 'Solo Mode': True, 399 'Time Limit': 600, 400 'map': 'Rampage', 401 }, 402 'type': 'bs_elimination.EliminationGame', 403 }, 404 { 405 'settings': { 406 'Epic Mode': False, 407 'Kills to Win Per Player': 5, 408 'Respawn Times': 1.0, 409 'Time Limit': 300, 410 'map': 'Roundabout', 411 }, 412 'type': 'bs_death_match.DeathMatchGame', 413 }, 414 { 415 'settings': { 416 'Respawn Times': 1.0, 417 'Score to Win': 1, 418 'Time Limit': 600, 419 'map': 'Hockey Stadium', 420 }, 421 'type': 'bs_hockey.HockeyGame', 422 }, 423 { 424 'settings': { 425 'Hold Time': 30, 426 'Respawn Times': 1.0, 427 'Time Limit': 300, 428 'map': 'Monkey Face', 429 }, 430 'type': 'bs_keep_away.KeepAwayGame', 431 }, 432 { 433 'settings': { 434 'Balance Total Lives': False, 435 'Epic Mode': True, 436 'Lives Per Player': 1, 437 'Respawn Times': 1.0, 438 'Solo Mode': False, 439 'Time Limit': 120, 440 'map': 'Tip Top', 441 }, 442 'type': 'bs_elimination.EliminationGame', 443 }, 444 { 445 'settings': { 446 'Epic Mode': False, 447 'Respawn Times': 1.0, 448 'Score to Win': 3, 449 'Time Limit': 300, 450 'map': 'Crag Castle', 451 }, 452 'type': 'bs_assault.AssaultGame', 453 }, 454 { 455 'settings': { 456 'Epic Mode': False, 457 'Kills to Win Per Player': 5, 458 'Respawn Times': 1.0, 459 'Time Limit': 300, 460 'map': 'Doom Shroom', 461 }, 462 'type': 'bs_death_match.DeathMatchGame', 463 }, 464 { 465 'settings': {'Epic Mode': False, 'map': 'Rampage'}, 466 'type': 'bs_meteor_shower.MeteorShowerGame', 467 }, 468 { 469 'settings': { 470 'Epic Mode': False, 471 'Flag Idle Return Time': 30, 472 'Flag Touch Return Time': 0, 473 'Respawn Times': 1.0, 474 'Score to Win': 2, 475 'Time Limit': 600, 476 'map': 'Roundabout', 477 }, 478 'type': 'bs_capture_the_flag.CTFGame', 479 }, 480 { 481 'settings': { 482 'Respawn Times': 1.0, 483 'Score to Win': 21, 484 'Time Limit': 600, 485 'map': 'Football Stadium', 486 }, 487 'type': 'bs_football.FootballTeamGame', 488 }, 489 { 490 'settings': { 491 'Epic Mode': True, 492 'Respawn Times': 0.25, 493 'Score to Win': 3, 494 'Time Limit': 120, 495 'map': 'Bridgit', 496 }, 497 'type': 'bs_assault.AssaultGame', 498 }, 499 { 500 'map': 'Doom Shroom', 501 'settings': { 502 'Enable Impact Bombs': 1, 503 'Enable Triple Bombs': False, 504 'Target Count': 2, 505 'map': 'Doom Shroom', 506 }, 507 'type': 'bs_target_practice.TargetPracticeGame', 508 }, 509 { 510 'settings': { 511 'Hold Time': 30, 512 'Respawn Times': 1.0, 513 'Time Limit': 300, 514 'map': 'Tip Top', 515 }, 516 'type': 'bs_king_of_the_hill.KingOfTheHillGame', 517 }, 518 { 519 'settings': { 520 'Epic Mode': False, 521 'Respawn Times': 1.0, 522 'Score to Win': 2, 523 'Time Limit': 300, 524 'map': 'Zigzag', 525 }, 526 'type': 'bs_assault.AssaultGame', 527 }, 528 { 529 'settings': { 530 'Epic Mode': False, 531 'Flag Idle Return Time': 30, 532 'Flag Touch Return Time': 0, 533 'Respawn Times': 1.0, 534 'Score to Win': 3, 535 'Time Limit': 300, 536 'map': 'Happy Thoughts', 537 }, 538 'type': 'bs_capture_the_flag.CTFGame', 539 }, 540 { 541 'settings': { 542 'Bomb Spawning': 1000, 543 'Epic Mode': True, 544 'Laps': 1, 545 'Mine Spawning': 2000, 546 'Time Limit': 300, 547 'map': 'Big G', 548 }, 549 'type': 'bs_race.RaceGame', 550 }, 551 { 552 'settings': { 553 'Epic Mode': False, 554 'Kills to Win Per Player': 5, 555 'Respawn Times': 1.0, 556 'Time Limit': 300, 557 'map': 'Monkey Face', 558 }, 559 'type': 'bs_death_match.DeathMatchGame', 560 }, 561 { 562 'settings': { 563 'Hold Time': 30, 564 'Respawn Times': 1.0, 565 'Time Limit': 300, 566 'map': 'Lake Frigid', 567 }, 568 'type': 'bs_keep_away.KeepAwayGame', 569 }, 570 { 571 'settings': { 572 'Epic Mode': False, 573 'Flag Idle Return Time': 30, 574 'Flag Touch Return Time': 3, 575 'Respawn Times': 1.0, 576 'Score to Win': 2, 577 'Time Limit': 300, 578 'map': 'Tip Top', 579 }, 580 'type': 'bs_capture_the_flag.CTFGame', 581 }, 582 { 583 'settings': { 584 'Balance Total Lives': False, 585 'Epic Mode': False, 586 'Lives Per Player': 3, 587 'Respawn Times': 1.0, 588 'Solo Mode': False, 589 'Time Limit': 300, 590 'map': 'Crag Castle', 591 }, 592 'type': 'bs_elimination.EliminationGame', 593 }, 594 { 595 'settings': { 596 'Epic Mode': True, 597 'Respawn Times': 0.25, 598 'Time Limit': 120, 599 'map': 'Zigzag', 600 }, 601 'type': 'bs_conquest.ConquestGame', 602 }, 603 ]
Return a default playlist for teams mode.
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 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)
44def get_trophy_string(trophy_id: str) -> str: 45 """Given a trophy id, returns a string to visualize it.""" 46 if trophy_id in TROPHY_CHARS: 47 return babase.charstr(TROPHY_CHARS[trophy_id]) 48 return '?'
Given a trophy id, returns a string to visualize it.
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 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 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 srcnode: bascenev1.Node | None = None, 246 pos: Sequence[float] | None = None, 247 velocity: Sequence[float] | None = None, 248 magnitude: float = 1.0, 249 velocity_magnitude: float = 0.0, 250 radius: float = 1.0, 251 source_player: bascenev1.Player | None = None, 252 kick_back: float = 1.0, 253 flat_damage: float | None = None, 254 hit_type: str = 'generic', 255 force_direction: Sequence[float] | None = None, 256 hit_subtype: str = 'default', 257 ): 258 """Instantiate a message with given values.""" 259 260 self.srcnode = srcnode 261 self.pos = pos if pos is not None else babase.Vec3() 262 self.velocity = velocity if velocity is not None else babase.Vec3() 263 self.magnitude = magnitude 264 self.velocity_magnitude = velocity_magnitude 265 self.radius = radius 266 267 # We should not be getting passed an invalid ref. 268 assert source_player is None or source_player.exists() 269 self._source_player = source_player 270 self.kick_back = kick_back 271 self.flat_damage = flat_damage 272 self.hit_type = hit_type 273 self.hit_subtype = hit_subtype 274 self.force_direction = ( 275 force_direction if force_direction is not None else velocity 276 ) 277 278 def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: 279 """Return the source-player if one exists and is the provided type.""" 280 player: Any = self._source_player 281 282 # We should not be delivering invalid refs. 283 # (we could translate to None here but technically we are changing 284 # the message delivered which seems wrong) 285 assert player is None or player.exists() 286 287 # Return the player *only* if they're the type given. 288 return player if isinstance(player, playertype) else None
Tells an object it has been hit in some way.
Category: Message Classes
This is used by punches, explosions, etc to convey their effect to a target.
243 def __init__( 244 self, 245 srcnode: bascenev1.Node | None = None, 246 pos: Sequence[float] | None = None, 247 velocity: Sequence[float] | None = None, 248 magnitude: float = 1.0, 249 velocity_magnitude: float = 0.0, 250 radius: float = 1.0, 251 source_player: bascenev1.Player | None = None, 252 kick_back: float = 1.0, 253 flat_damage: float | None = None, 254 hit_type: str = 'generic', 255 force_direction: Sequence[float] | None = None, 256 hit_subtype: str = 'default', 257 ): 258 """Instantiate a message with given values.""" 259 260 self.srcnode = srcnode 261 self.pos = pos if pos is not None else babase.Vec3() 262 self.velocity = velocity if velocity is not None else babase.Vec3() 263 self.magnitude = magnitude 264 self.velocity_magnitude = velocity_magnitude 265 self.radius = radius 266 267 # We should not be getting passed an invalid ref. 268 assert source_player is None or source_player.exists() 269 self._source_player = source_player 270 self.kick_back = kick_back 271 self.flat_damage = flat_damage 272 self.hit_type = hit_type 273 self.hit_subtype = hit_subtype 274 self.force_direction = ( 275 force_direction if force_direction is not None else velocity 276 )
Instantiate a message with given values.
278 def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None: 279 """Return the source-player if one exists and is the provided type.""" 280 player: Any = self._source_player 281 282 # We should not be delivering invalid refs. 283 # (we could translate to None here but technically we are changing 284 # the message delivered which seems wrong) 285 assert player is None or player.exists() 286 287 # Return the player *only* if they're the type given. 288 return player if isinstance(player, playertype) else None
Return the source-player if one exists and is the provided type.
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
Inherited Members
- enum.Enum
- name
- value
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 Session.
The activity will not be actually run until 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 Activity.on_begin() is called.
Inherited Members
- Activity
- settings_raw
- teams
- players
- announce_player_deaths
- allow_pausing
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_join
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- on_begin
- handlemessage
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- end
- create_player
- create_team
19class Level: 20 """An entry in a bascenev1.Campaign. 21 22 Category: **Gameplay Classes** 23 """ 24 25 def __init__( 26 self, 27 name: str, 28 gametype: type[bascenev1.GameActivity], 29 settings: dict, 30 preview_texture_name: str, 31 displayname: str | None = None, 32 ): 33 self._name = name 34 self._gametype = gametype 35 self._settings = settings 36 self._preview_texture_name = preview_texture_name 37 self._displayname = displayname 38 self._campaign: weakref.ref[bascenev1.Campaign] | None = None 39 self._index: int | None = None 40 self._score_version_string: str | None = None 41 42 @override 43 def __repr__(self) -> str: 44 cls = type(self) 45 return f"<{cls.__module__}.{cls.__name__} '{self._name}'>" 46 47 @property 48 def name(self) -> str: 49 """The unique name for this Level.""" 50 return self._name 51 52 def get_settings(self) -> dict[str, Any]: 53 """Returns the settings for this Level.""" 54 settings = copy.deepcopy(self._settings) 55 56 # So the game knows what the level is called. 57 # Hmm; seems hacky; I think we should take this out. 58 settings['name'] = self._name 59 return settings 60 61 @property 62 def preview_texture_name(self) -> str: 63 """The preview texture name for this Level.""" 64 return self._preview_texture_name 65 66 # def get_preview_texture(self) -> bauiv1.Texture: 67 # """Load/return the preview Texture for this Level.""" 68 # return _bauiv1.gettexture(self._preview_texture_name) 69 70 @property 71 def displayname(self) -> bascenev1.Lstr: 72 """The localized name for this Level.""" 73 return babase.Lstr( 74 translate=( 75 'coopLevelNames', 76 ( 77 self._displayname 78 if self._displayname is not None 79 else self._name 80 ), 81 ), 82 subs=[ 83 ('${GAME}', self._gametype.get_display_string(self._settings)) 84 ], 85 ) 86 87 @property 88 def gametype(self) -> type[bascenev1.GameActivity]: 89 """The type of game used for this Level.""" 90 return self._gametype 91 92 @property 93 def campaign(self) -> bascenev1.Campaign | None: 94 """The baclassic.Campaign this Level is associated with, or None.""" 95 return None if self._campaign is None else self._campaign() 96 97 @property 98 def index(self) -> int: 99 """The zero-based index of this Level in its baclassic.Campaign. 100 101 Access results in a RuntimeError if the Level is not assigned to a 102 Campaign. 103 """ 104 if self._index is None: 105 raise RuntimeError('Level is not part of a Campaign') 106 return self._index 107 108 @property 109 def complete(self) -> bool: 110 """Whether this Level has been completed.""" 111 config = self._get_config_dict() 112 val = config.get('Complete', False) 113 assert isinstance(val, bool) 114 return val 115 116 def set_complete(self, val: bool) -> None: 117 """Set whether or not this level is complete.""" 118 old_val = self.complete 119 assert isinstance(old_val, bool) 120 assert isinstance(val, bool) 121 if val != old_val: 122 config = self._get_config_dict() 123 config['Complete'] = val 124 125 def get_high_scores(self) -> dict: 126 """Return the current high scores for this Level.""" 127 config = self._get_config_dict() 128 high_scores_key = 'High Scores' + self.get_score_version_string() 129 if high_scores_key not in config: 130 return {} 131 return copy.deepcopy(config[high_scores_key]) 132 133 def set_high_scores(self, high_scores: dict) -> None: 134 """Set high scores for this level.""" 135 config = self._get_config_dict() 136 high_scores_key = 'High Scores' + self.get_score_version_string() 137 config[high_scores_key] = high_scores 138 139 def get_score_version_string(self) -> str: 140 """Return the score version string for this Level. 141 142 If a Level's gameplay changes significantly, its version string 143 can be changed to separate its new high score lists/etc. from the old. 144 """ 145 if self._score_version_string is None: 146 scorever = self._gametype.getscoreconfig().version 147 if scorever != '': 148 scorever = ' ' + scorever 149 self._score_version_string = scorever 150 assert self._score_version_string is not None 151 return self._score_version_string 152 153 @property 154 def rating(self) -> float: 155 """The current rating for this Level.""" 156 val = self._get_config_dict().get('Rating', 0.0) 157 assert isinstance(val, float) 158 return val 159 160 def set_rating(self, rating: float) -> None: 161 """Set a rating for this Level, replacing the old ONLY IF higher.""" 162 old_rating = self.rating 163 config = self._get_config_dict() 164 config['Rating'] = max(old_rating, rating) 165 166 def _get_config_dict(self) -> dict[str, Any]: 167 """Return/create the persistent state dict for this level. 168 169 The referenced dict exists under the game's config dict and 170 can be modified in place.""" 171 campaign = self.campaign 172 if campaign is None: 173 raise RuntimeError('Level is not in a campaign.') 174 configdict = campaign.configdict 175 val: dict[str, Any] = configdict.setdefault( 176 self._name, {'Rating': 0.0, 'Complete': False} 177 ) 178 assert isinstance(val, dict) 179 return val 180 181 def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None: 182 """For use by baclassic.Campaign when adding levels to itself. 183 184 (internal)""" 185 self._campaign = weakref.ref(campaign) 186 self._index = index
An entry in a 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 displayname: str | None = None, 32 ): 33 self._name = name 34 self._gametype = gametype 35 self._settings = settings 36 self._preview_texture_name = preview_texture_name 37 self._displayname = displayname 38 self._campaign: weakref.ref[bascenev1.Campaign] | None = None 39 self._index: int | None = None 40 self._score_version_string: str | None = None
47 @property 48 def name(self) -> str: 49 """The unique name for this Level.""" 50 return self._name
The unique name for this Level.
52 def get_settings(self) -> dict[str, Any]: 53 """Returns the settings for this Level.""" 54 settings = copy.deepcopy(self._settings) 55 56 # So the game knows what the level is called. 57 # Hmm; seems hacky; I think we should take this out. 58 settings['name'] = self._name 59 return settings
Returns the settings for this Level.
61 @property 62 def preview_texture_name(self) -> str: 63 """The preview texture name for this Level.""" 64 return self._preview_texture_name
The preview texture name for this Level.
70 @property 71 def displayname(self) -> bascenev1.Lstr: 72 """The localized name for this Level.""" 73 return babase.Lstr( 74 translate=( 75 'coopLevelNames', 76 ( 77 self._displayname 78 if self._displayname is not None 79 else self._name 80 ), 81 ), 82 subs=[ 83 ('${GAME}', self._gametype.get_display_string(self._settings)) 84 ], 85 )
The localized name for this Level.
87 @property 88 def gametype(self) -> type[bascenev1.GameActivity]: 89 """The type of game used for this Level.""" 90 return self._gametype
The type of game used for this Level.
92 @property 93 def campaign(self) -> bascenev1.Campaign | None: 94 """The baclassic.Campaign this Level is associated with, or None.""" 95 return None if self._campaign is None else self._campaign()
The Campaign this Level is associated with, or None.
97 @property 98 def index(self) -> int: 99 """The zero-based index of this Level in its baclassic.Campaign. 100 101 Access results in a RuntimeError if the Level is not assigned to a 102 Campaign. 103 """ 104 if self._index is None: 105 raise RuntimeError('Level is not part of a Campaign') 106 return self._index
The zero-based index of this Level in its Campaign.
Access results in a RuntimeError if the Level is not assigned to a Campaign.
108 @property 109 def complete(self) -> bool: 110 """Whether this Level has been completed.""" 111 config = self._get_config_dict() 112 val = config.get('Complete', False) 113 assert isinstance(val, bool) 114 return val
Whether this Level has been completed.
116 def set_complete(self, val: bool) -> None: 117 """Set whether or not this level is complete.""" 118 old_val = self.complete 119 assert isinstance(old_val, bool) 120 assert isinstance(val, bool) 121 if val != old_val: 122 config = self._get_config_dict() 123 config['Complete'] = val
Set whether or not this level is complete.
125 def get_high_scores(self) -> dict: 126 """Return the current high scores for this Level.""" 127 config = self._get_config_dict() 128 high_scores_key = 'High Scores' + self.get_score_version_string() 129 if high_scores_key not in config: 130 return {} 131 return copy.deepcopy(config[high_scores_key])
Return the current high scores for this Level.
133 def set_high_scores(self, high_scores: dict) -> None: 134 """Set high scores for this level.""" 135 config = self._get_config_dict() 136 high_scores_key = 'High Scores' + self.get_score_version_string() 137 config[high_scores_key] = high_scores
Set high scores for this level.
139 def get_score_version_string(self) -> str: 140 """Return the score version string for this Level. 141 142 If a Level's gameplay changes significantly, its version string 143 can be changed to separate its new high score lists/etc. from the old. 144 """ 145 if self._score_version_string is None: 146 scorever = self._gametype.getscoreconfig().version 147 if scorever != '': 148 scorever = ' ' + scorever 149 self._score_version_string = scorever 150 assert self._score_version_string is not None 151 return self._score_version_string
Return the score version string for this Level.
If a Level's gameplay changes significantly, its version string can be changed to separate its new high score lists/etc. from the old.
153 @property 154 def rating(self) -> float: 155 """The current rating for this Level.""" 156 val = self._get_config_dict().get('Rating', 0.0) 157 assert isinstance(val, float) 158 return val
The current rating for this Level.
160 def set_rating(self, rating: float) -> None: 161 """Set a rating for this Level, replacing the old ONLY IF higher.""" 162 old_rating = self.rating 163 config = self._get_config_dict() 164 config['Rating'] = max(old_rating, rating)
Set a rating for this Level, replacing the old ONLY IF higher.
942class Lobby: 943 """Container for baclassic.Choosers. 944 945 Category: Gameplay Classes 946 """ 947 948 def __del__(self) -> None: 949 # Reset any players that still have a chooser in us. 950 # (should allow the choosers to die). 951 sessionplayers = [ 952 c.sessionplayer for c in self.choosers if c.sessionplayer 953 ] 954 for sessionplayer in sessionplayers: 955 sessionplayer.resetinput() 956 957 def __init__(self) -> None: 958 from bascenev1._team import SessionTeam 959 from bascenev1._coopsession import CoopSession 960 961 session = _bascenev1.getsession() 962 self._use_team_colors = session.use_team_colors 963 if session.use_teams: 964 self._sessionteams = [ 965 weakref.ref(team) for team in session.sessionteams 966 ] 967 else: 968 self._dummy_teams = SessionTeam() 969 self._sessionteams = [weakref.ref(self._dummy_teams)] 970 v_offset = -150 if isinstance(session, CoopSession) else -50 971 self.choosers: list[Chooser] = [] 972 self.base_v_offset = v_offset 973 self.update_positions() 974 self._next_add_team = 0 975 self.character_names_local_unlocked: list[str] = [] 976 self._vpos = 0 977 978 # Grab available profiles. 979 self.reload_profiles() 980 981 self._join_info_text = None 982 983 @property 984 def next_add_team(self) -> int: 985 """(internal)""" 986 return self._next_add_team 987 988 @property 989 def use_team_colors(self) -> bool: 990 """A bool for whether this lobby is using team colors. 991 992 If False, inidividual player colors are used instead. 993 """ 994 return self._use_team_colors 995 996 @property 997 def sessionteams(self) -> list[bascenev1.SessionTeam]: 998 """bascenev1.SessionTeams available in this lobby.""" 999 allteams = [] 1000 for tref in self._sessionteams: 1001 team = tref() 1002 assert team is not None 1003 allteams.append(team) 1004 return allteams 1005 1006 def get_choosers(self) -> list[Chooser]: 1007 """Return the lobby's current choosers.""" 1008 return self.choosers 1009 1010 def create_join_info(self) -> JoinInfo: 1011 """Create a display of on-screen information for joiners. 1012 1013 (how to switch teams, players, etc.) 1014 Intended for use in initial joining-screens. 1015 """ 1016 return JoinInfo(self) 1017 1018 def reload_profiles(self) -> None: 1019 """Reload available player profiles.""" 1020 # pylint: disable=cyclic-import 1021 from bascenev1lib.actor.spazappearance import get_appearances 1022 1023 assert babase.app.classic is not None 1024 1025 # We may have gained or lost character names if the user 1026 # bought something; reload these too. 1027 self.character_names_local_unlocked = get_appearances() 1028 self.character_names_local_unlocked.sort(key=lambda x: x.lower()) 1029 1030 # Do any overall prep we need to such as creating account profile. 1031 babase.app.classic.accounts.ensure_have_account_player_profile() 1032 for chooser in self.choosers: 1033 try: 1034 chooser.reload_profiles() 1035 chooser.update_from_profile() 1036 except Exception: 1037 logging.exception('Error reloading profiles.') 1038 1039 def update_positions(self) -> None: 1040 """Update positions for all choosers.""" 1041 self._vpos = -100 + self.base_v_offset 1042 for chooser in self.choosers: 1043 chooser.set_vpos(self._vpos) 1044 chooser.update_position() 1045 self._vpos -= 48 1046 1047 def check_all_ready(self) -> bool: 1048 """Return whether all choosers are marked ready.""" 1049 return all(chooser.ready for chooser in self.choosers) 1050 1051 def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None: 1052 """Add a chooser to the lobby for the provided player.""" 1053 self.choosers.append( 1054 Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self) 1055 ) 1056 self._next_add_team = (self._next_add_team + 1) % len( 1057 self._sessionteams 1058 ) 1059 self._vpos -= 48 1060 1061 def remove_chooser(self, player: bascenev1.SessionPlayer) -> None: 1062 """Remove a single player's chooser; does not kick them. 1063 1064 This is used when a player enters the game and no longer 1065 needs a chooser.""" 1066 found = False 1067 chooser = None 1068 for chooser in self.choosers: 1069 if chooser.getplayer() is player: 1070 found = True 1071 1072 # Mark it as dead since there could be more 1073 # change-commands/etc coming in still for it; 1074 # want to avoid duplicate player-adds/etc. 1075 chooser.set_dead(True) 1076 self.choosers.remove(chooser) 1077 break 1078 if not found: 1079 logging.exception('remove_chooser did not find player %s.', player) 1080 elif chooser in self.choosers: 1081 logging.exception('chooser remains after removal for %s.', player) 1082 self.update_positions() 1083 1084 def remove_all_choosers(self) -> None: 1085 """Remove all choosers without kicking players. 1086 1087 This is called after all players check in and enter a game. 1088 """ 1089 self.choosers = [] 1090 self.update_positions() 1091 1092 def remove_all_choosers_and_kick_players(self) -> None: 1093 """Remove all player choosers and kick attached players.""" 1094 1095 # Copy the list; it can change under us otherwise. 1096 for chooser in list(self.choosers): 1097 if chooser.sessionplayer: 1098 chooser.sessionplayer.remove_from_game() 1099 self.remove_all_choosers()
Container for baclassic.Choosers.
Category: Gameplay Classes
988 @property 989 def use_team_colors(self) -> bool: 990 """A bool for whether this lobby is using team colors. 991 992 If False, inidividual player colors are used instead. 993 """ 994 return self._use_team_colors
A bool for whether this lobby is using team colors.
If False, inidividual player colors are used instead.
996 @property 997 def sessionteams(self) -> list[bascenev1.SessionTeam]: 998 """bascenev1.SessionTeams available in this lobby.""" 999 allteams = [] 1000 for tref in self._sessionteams: 1001 team = tref() 1002 assert team is not None 1003 allteams.append(team) 1004 return allteams
bascenev1.SessionTeams available in this lobby.
1006 def get_choosers(self) -> list[Chooser]: 1007 """Return the lobby's current choosers.""" 1008 return self.choosers
Return the lobby's current choosers.
1010 def create_join_info(self) -> JoinInfo: 1011 """Create a display of on-screen information for joiners. 1012 1013 (how to switch teams, players, etc.) 1014 Intended for use in initial joining-screens. 1015 """ 1016 return JoinInfo(self)
Create a display of on-screen information for joiners.
(how to switch teams, players, etc.) Intended for use in initial joining-screens.
1018 def reload_profiles(self) -> None: 1019 """Reload available player profiles.""" 1020 # pylint: disable=cyclic-import 1021 from bascenev1lib.actor.spazappearance import get_appearances 1022 1023 assert babase.app.classic is not None 1024 1025 # We may have gained or lost character names if the user 1026 # bought something; reload these too. 1027 self.character_names_local_unlocked = get_appearances() 1028 self.character_names_local_unlocked.sort(key=lambda x: x.lower()) 1029 1030 # Do any overall prep we need to such as creating account profile. 1031 babase.app.classic.accounts.ensure_have_account_player_profile() 1032 for chooser in self.choosers: 1033 try: 1034 chooser.reload_profiles() 1035 chooser.update_from_profile() 1036 except Exception: 1037 logging.exception('Error reloading profiles.')
Reload available player profiles.
1039 def update_positions(self) -> None: 1040 """Update positions for all choosers.""" 1041 self._vpos = -100 + self.base_v_offset 1042 for chooser in self.choosers: 1043 chooser.set_vpos(self._vpos) 1044 chooser.update_position() 1045 self._vpos -= 48
Update positions for all choosers.
1047 def check_all_ready(self) -> bool: 1048 """Return whether all choosers are marked ready.""" 1049 return all(chooser.ready for chooser in self.choosers)
Return whether all choosers are marked ready.
1051 def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None: 1052 """Add a chooser to the lobby for the provided player.""" 1053 self.choosers.append( 1054 Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self) 1055 ) 1056 self._next_add_team = (self._next_add_team + 1) % len( 1057 self._sessionteams 1058 ) 1059 self._vpos -= 48
Add a chooser to the lobby for the provided player.
1061 def remove_chooser(self, player: bascenev1.SessionPlayer) -> None: 1062 """Remove a single player's chooser; does not kick them. 1063 1064 This is used when a player enters the game and no longer 1065 needs a chooser.""" 1066 found = False 1067 chooser = None 1068 for chooser in self.choosers: 1069 if chooser.getplayer() is player: 1070 found = True 1071 1072 # Mark it as dead since there could be more 1073 # change-commands/etc coming in still for it; 1074 # want to avoid duplicate player-adds/etc. 1075 chooser.set_dead(True) 1076 self.choosers.remove(chooser) 1077 break 1078 if not found: 1079 logging.exception('remove_chooser did not find player %s.', player) 1080 elif chooser in self.choosers: 1081 logging.exception('chooser remains after removal for %s.', player) 1082 self.update_positions()
Remove a single player's chooser; does not kick them.
This is used when a player enters the game and no longer needs a chooser.
1084 def remove_all_choosers(self) -> None: 1085 """Remove all choosers without kicking players. 1086 1087 This is called after all players check in and enter a game. 1088 """ 1089 self.choosers = [] 1090 self.update_positions()
Remove all choosers without kicking players.
This is called after all players check in and enter a game.
1092 def remove_all_choosers_and_kick_players(self) -> None: 1093 """Remove all player choosers and kick attached players.""" 1094 1095 # Copy the list; it can change under us otherwise. 1096 for chooser in list(self.choosers): 1097 if chooser.sessionplayer: 1098 chooser.sessionplayer.remove_from_game() 1099 self.remove_all_choosers()
Remove all player choosers and kick attached players.
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.
489class Lstr: 490 """Used to define strings in a language-independent way. 491 492 Category: **General Utility Classes** 493 494 These should be used whenever possible in place of hard-coded 495 strings so that in-game or UI elements show up correctly on all 496 clients in their currently-active language. 497 498 To see available resource keys, look at any of the bs_language_*.py 499 files in the game or the translations pages at 500 legacy.ballistica.net/translate. 501 502 ##### Examples 503 EXAMPLE 1: specify a string from a resource path 504 >>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText') 505 506 EXAMPLE 2: specify a translated string via a category and english 507 value; if a translated value is available, it will be used; otherwise 508 the english value will be. To see available translation categories, 509 look under the 'translations' resource section. 510 >>> mynode.text = babase.Lstr(translate=('gameDescriptions', 511 ... 'Defeat all enemies')) 512 513 EXAMPLE 3: specify a raw value and some substitutions. Substitutions 514 can be used with resource and translate modes as well. 515 >>> mynode.text = babase.Lstr(value='${A} / ${B}', 516 ... subs=[('${A}', str(score)), ('${B}', str(total))]) 517 518 EXAMPLE 4: babase.Lstr's can be nested. This example would display the 519 resource at res_a but replace ${NAME} with the value of the 520 resource at res_b 521 >>> mytextnode.text = babase.Lstr( 522 ... resource='res_a', 523 ... subs=[('${NAME}', babase.Lstr(resource='res_b'))]) 524 """ 525 526 # pylint: disable=dangerous-default-value 527 # noinspection PyDefaultArgument 528 @overload 529 def __init__( 530 self, 531 *, 532 resource: str, 533 fallback_resource: str = '', 534 fallback_value: str = '', 535 subs: Sequence[tuple[str, str | Lstr]] = [], 536 ) -> None: 537 """Create an Lstr from a string resource.""" 538 539 # noinspection PyShadowingNames,PyDefaultArgument 540 @overload 541 def __init__( 542 self, 543 *, 544 translate: tuple[str, str], 545 subs: Sequence[tuple[str, str | Lstr]] = [], 546 ) -> None: 547 """Create an Lstr by translating a string in a category.""" 548 549 # noinspection PyDefaultArgument 550 @overload 551 def __init__( 552 self, *, value: str, subs: Sequence[tuple[str, str | Lstr]] = [] 553 ) -> None: 554 """Create an Lstr from a raw string value.""" 555 556 # pylint: enable=redefined-outer-name, dangerous-default-value 557 558 def __init__(self, *args: Any, **keywds: Any) -> None: 559 """Instantiate a Lstr. 560 561 Pass a value for either 'resource', 'translate', 562 or 'value'. (see Lstr help for examples). 563 'subs' can be a sequence of 2-member sequences consisting of values 564 and replacements. 565 'fallback_resource' can be a resource key that will be used if the 566 main one is not present for 567 the current language in place of falling back to the english value 568 ('resource' mode only). 569 'fallback_value' can be a literal string that will be used if neither 570 the resource nor the fallback resource is found ('resource' mode only). 571 """ 572 # pylint: disable=too-many-branches 573 if args: 574 raise TypeError('Lstr accepts only keyword arguments') 575 576 # Basically just store the exact args they passed. 577 # However if they passed any Lstr values for subs, 578 # replace them with that Lstr's dict. 579 self.args = keywds 580 our_type = type(self) 581 582 if isinstance(self.args.get('value'), our_type): 583 raise TypeError("'value' must be a regular string; not an Lstr") 584 585 if 'subs' in self.args: 586 subs_new = [] 587 for key, value in keywds['subs']: 588 if isinstance(value, our_type): 589 subs_new.append((key, value.args)) 590 else: 591 subs_new.append((key, value)) 592 self.args['subs'] = subs_new 593 594 # As of protocol 31 we support compact key names 595 # ('t' instead of 'translate', etc). Convert as needed. 596 if 'translate' in keywds: 597 keywds['t'] = keywds['translate'] 598 del keywds['translate'] 599 if 'resource' in keywds: 600 keywds['r'] = keywds['resource'] 601 del keywds['resource'] 602 if 'value' in keywds: 603 keywds['v'] = keywds['value'] 604 del keywds['value'] 605 if 'fallback' in keywds: 606 from babase import _error 607 608 _error.print_error( 609 'deprecated "fallback" arg passed to Lstr(); use ' 610 'either "fallback_resource" or "fallback_value"', 611 once=True, 612 ) 613 keywds['f'] = keywds['fallback'] 614 del keywds['fallback'] 615 if 'fallback_resource' in keywds: 616 keywds['f'] = keywds['fallback_resource'] 617 del keywds['fallback_resource'] 618 if 'subs' in keywds: 619 keywds['s'] = keywds['subs'] 620 del keywds['subs'] 621 if 'fallback_value' in keywds: 622 keywds['fv'] = keywds['fallback_value'] 623 del keywds['fallback_value'] 624 625 def evaluate(self) -> str: 626 """Evaluate the Lstr and returns a flat string in the current language. 627 628 You should avoid doing this as much as possible and instead pass 629 and store Lstr values. 630 """ 631 return _babase.evaluate_lstr(self._get_json()) 632 633 def is_flat_value(self) -> bool: 634 """Return whether the Lstr is a 'flat' value. 635 636 This is defined as a simple string value incorporating no 637 translations, resources, or substitutions. In this case it may 638 be reasonable to replace it with a raw string value, perform 639 string manipulation on it, etc. 640 """ 641 return bool('v' in self.args and not self.args.get('s', [])) 642 643 def _get_json(self) -> str: 644 try: 645 return json.dumps(self.args, separators=(',', ':')) 646 except Exception: 647 from babase import _error 648 649 _error.print_exception('_get_json failed for', self.args) 650 return 'JSON_ERR' 651 652 @override 653 def __str__(self) -> str: 654 return '<ba.Lstr: ' + self._get_json() + '>' 655 656 @override 657 def __repr__(self) -> str: 658 return '<ba.Lstr: ' + self._get_json() + '>' 659 660 @staticmethod 661 def from_json(json_string: str) -> babase.Lstr: 662 """Given a json string, returns a babase.Lstr. Does no validation.""" 663 lstr = Lstr(value='') 664 lstr.args = json.loads(json_string) 665 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 = 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 = 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 = Lstr(value='${A} / ${B}',
... subs=[('${A}', str(score)), ('${B}', str(total))])
EXAMPLE 4: 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
558 def __init__(self, *args: Any, **keywds: Any) -> None: 559 """Instantiate a Lstr. 560 561 Pass a value for either 'resource', 'translate', 562 or 'value'. (see Lstr help for examples). 563 'subs' can be a sequence of 2-member sequences consisting of values 564 and replacements. 565 'fallback_resource' can be a resource key that will be used if the 566 main one is not present for 567 the current language in place of falling back to the english value 568 ('resource' mode only). 569 'fallback_value' can be a literal string that will be used if neither 570 the resource nor the fallback resource is found ('resource' mode only). 571 """ 572 # pylint: disable=too-many-branches 573 if args: 574 raise TypeError('Lstr accepts only keyword arguments') 575 576 # Basically just store the exact args they passed. 577 # However if they passed any Lstr values for subs, 578 # replace them with that Lstr's dict. 579 self.args = keywds 580 our_type = type(self) 581 582 if isinstance(self.args.get('value'), our_type): 583 raise TypeError("'value' must be a regular string; not an Lstr") 584 585 if 'subs' in self.args: 586 subs_new = [] 587 for key, value in keywds['subs']: 588 if isinstance(value, our_type): 589 subs_new.append((key, value.args)) 590 else: 591 subs_new.append((key, value)) 592 self.args['subs'] = subs_new 593 594 # As of protocol 31 we support compact key names 595 # ('t' instead of 'translate', etc). Convert as needed. 596 if 'translate' in keywds: 597 keywds['t'] = keywds['translate'] 598 del keywds['translate'] 599 if 'resource' in keywds: 600 keywds['r'] = keywds['resource'] 601 del keywds['resource'] 602 if 'value' in keywds: 603 keywds['v'] = keywds['value'] 604 del keywds['value'] 605 if 'fallback' in keywds: 606 from babase import _error 607 608 _error.print_error( 609 'deprecated "fallback" arg passed to Lstr(); use ' 610 'either "fallback_resource" or "fallback_value"', 611 once=True, 612 ) 613 keywds['f'] = keywds['fallback'] 614 del keywds['fallback'] 615 if 'fallback_resource' in keywds: 616 keywds['f'] = keywds['fallback_resource'] 617 del keywds['fallback_resource'] 618 if 'subs' in keywds: 619 keywds['s'] = keywds['subs'] 620 del keywds['subs'] 621 if 'fallback_value' in keywds: 622 keywds['fv'] = keywds['fallback_value'] 623 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).
625 def evaluate(self) -> str: 626 """Evaluate the Lstr and returns a flat string in the current language. 627 628 You should avoid doing this as much as possible and instead pass 629 and store Lstr values. 630 """ 631 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.
633 def is_flat_value(self) -> bool: 634 """Return whether the Lstr is a 'flat' value. 635 636 This is defined as a simple string value incorporating no 637 translations, resources, or substitutions. In this case it may 638 be reasonable to replace it with a raw string value, perform 639 string manipulation on it, etc. 640 """ 641 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 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 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 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 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.
Inherited Members
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 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 Material to instantiate a blank material, and then use its 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 Material?
('they_dont_have_material', material)
Does the part we're hitting not have a given 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 Node as us?
('they_are_different_node_than_us')
Does the part we're hitting belong to a different 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 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 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 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 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 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 = 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 DieMessage to anything we touch, but cause no physical response. This should cause any Actor to drop dead:
>>> m = Material()
... m.add_actions(
... actions=(('modify_part_collision', 'physical', False),
... ('message', 'their_node', 'at_connect',
... DieMessage())))
Example 3: play some sounds when we're contacting the ground:
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 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 Player having their own 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 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 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)
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.
Inherited Members
- Session
- use_teams
- use_team_colors
- lobby
- max_players
- min_players
- sessionplayers
- customdata
- sessionteams
- tournament_id
- submit_score
- stats
- context
- sessionglobalsnode
- should_allow_mid_activity_joins
- on_player_request
- on_player_leave
- end
- on_team_leave
- end_activity
- handlemessage
- setactivity
- getactivity
- begin_next_activity
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.
Inherited Members
- enum.Enum
- name
- value
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 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 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 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 newnode(), and to explicitly delete one, use Node.delete(). 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:
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 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 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 Node's delegate for handling (generally the Actor that made the node).
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 Actor type that wraps a single 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 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 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.
Inherited Members
75class NodeNotFoundError(NotFoundError): 76 """Exception raised when an expected Node does not exist. 77 78 Category: **Exception Classes** 79 """
Exception raised when an expected Node does not exist.
Category: Exception Classes
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- add_note
- args
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
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- add_note
- args
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 Activity.
Category: Gameplay Classes
These correspond to SessionPlayer objects, but are associated with a single Activity instance. This allows activities to specify their own custom 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 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 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 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 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 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 SessionPlayer has left the game or if the 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 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 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 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 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.
291@dataclass 292class PlayerProfilesChangedMessage: 293 """Signals player profiles may have changed and should be reloaded."""
Signals player profiles may have changed and should be reloaded.
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
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- add_note
- args
35class PlayerRecord: 36 """Stats for an individual player in a bascenev1.Stats object. 37 38 Category: **Gameplay Classes** 39 40 This does not necessarily correspond to a bascenev1.Player that is 41 still present (stats may be retained for players that leave 42 mid-game) 43 """ 44 45 character: str 46 47 def __init__( 48 self, 49 name: str, 50 name_full: str, 51 sessionplayer: bascenev1.SessionPlayer, 52 stats: bascenev1.Stats, 53 ): 54 self.name = name 55 self.name_full = name_full 56 self.score = 0 57 self.accumscore = 0 58 self.kill_count = 0 59 self.accum_kill_count = 0 60 self.killed_count = 0 61 self.accum_killed_count = 0 62 self._multi_kill_timer: bascenev1.Timer | None = None 63 self._multi_kill_count = 0 64 self._stats = weakref.ref(stats) 65 self._last_sessionplayer: bascenev1.SessionPlayer | None = None 66 self._sessionplayer: bascenev1.SessionPlayer | None = None 67 self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None 68 self.streak = 0 69 self.associate_with_sessionplayer(sessionplayer) 70 71 @property 72 def team(self) -> bascenev1.SessionTeam: 73 """The bascenev1.SessionTeam the last associated player was last on. 74 75 This can still return a valid result even if the player is gone. 76 Raises a bascenev1.SessionTeamNotFoundError if the team no longer 77 exists. 78 """ 79 assert self._sessionteam is not None 80 team = self._sessionteam() 81 if team is None: 82 raise babase.SessionTeamNotFoundError() 83 return team 84 85 @property 86 def player(self) -> bascenev1.SessionPlayer: 87 """Return the instance's associated bascenev1.SessionPlayer. 88 89 Raises a bascenev1.SessionPlayerNotFoundError if the player 90 no longer exists. 91 """ 92 if not self._sessionplayer: 93 raise babase.SessionPlayerNotFoundError() 94 return self._sessionplayer 95 96 def getname(self, full: bool = False) -> str: 97 """Return the player entry's name.""" 98 return self.name_full if full else self.name 99 100 def get_icon(self) -> dict[str, Any]: 101 """Get the icon for this instance's player.""" 102 player = self._last_sessionplayer 103 assert player is not None 104 return player.get_icon() 105 106 def cancel_multi_kill_timer(self) -> None: 107 """Cancel any multi-kill timer for this player entry.""" 108 self._multi_kill_timer = None 109 110 def getactivity(self) -> bascenev1.Activity | None: 111 """Return the bascenev1.Activity this instance is associated with. 112 113 Returns None if the activity no longer exists.""" 114 stats = self._stats() 115 if stats is not None: 116 return stats.getactivity() 117 return None 118 119 def associate_with_sessionplayer( 120 self, sessionplayer: bascenev1.SessionPlayer 121 ) -> None: 122 """Associate this entry with a bascenev1.SessionPlayer.""" 123 self._sessionteam = weakref.ref(sessionplayer.sessionteam) 124 self.character = sessionplayer.character 125 self._last_sessionplayer = sessionplayer 126 self._sessionplayer = sessionplayer 127 self.streak = 0 128 129 def _end_multi_kill(self) -> None: 130 self._multi_kill_timer = None 131 self._multi_kill_count = 0 132 133 def get_last_sessionplayer(self) -> bascenev1.SessionPlayer: 134 """Return the last bascenev1.Player we were associated with.""" 135 assert self._last_sessionplayer is not None 136 return self._last_sessionplayer 137 138 def submit_kill(self, showpoints: bool = True) -> None: 139 """Submit a kill for this player entry.""" 140 # FIXME Clean this up. 141 # pylint: disable=too-many-statements 142 143 self._multi_kill_count += 1 144 stats = self._stats() 145 assert stats 146 if self._multi_kill_count == 1: 147 score = 0 148 name = None 149 delay = 0.0 150 color = (0.0, 0.0, 0.0, 1.0) 151 scale = 1.0 152 sound = None 153 elif self._multi_kill_count == 2: 154 score = 20 155 name = babase.Lstr(resource='twoKillText') 156 color = (0.1, 1.0, 0.0, 1) 157 scale = 1.0 158 delay = 0.0 159 sound = stats.orchestrahitsound1 160 elif self._multi_kill_count == 3: 161 score = 40 162 name = babase.Lstr(resource='threeKillText') 163 color = (1.0, 0.7, 0.0, 1) 164 scale = 1.1 165 delay = 0.3 166 sound = stats.orchestrahitsound2 167 elif self._multi_kill_count == 4: 168 score = 60 169 name = babase.Lstr(resource='fourKillText') 170 color = (1.0, 1.0, 0.0, 1) 171 scale = 1.2 172 delay = 0.6 173 sound = stats.orchestrahitsound3 174 elif self._multi_kill_count == 5: 175 score = 80 176 name = babase.Lstr(resource='fiveKillText') 177 color = (1.0, 0.5, 0.0, 1) 178 scale = 1.3 179 delay = 0.9 180 sound = stats.orchestrahitsound4 181 else: 182 score = 100 183 name = babase.Lstr( 184 resource='multiKillText', 185 subs=[('${COUNT}', str(self._multi_kill_count))], 186 ) 187 color = (1.0, 0.5, 0.0, 1) 188 scale = 1.3 189 delay = 1.0 190 sound = stats.orchestrahitsound4 191 192 def _apply( 193 name2: babase.Lstr, 194 score2: int, 195 showpoints2: bool, 196 color2: tuple[float, float, float, float], 197 scale2: float, 198 sound2: bascenev1.Sound | None, 199 ) -> None: 200 from bascenev1lib.actor.popuptext import PopupText 201 202 # Only award this if they're still alive and we can get 203 # a current position for them. 204 our_pos: babase.Vec3 | None = None 205 if self._sessionplayer: 206 if self._sessionplayer.activityplayer is not None: 207 try: 208 our_pos = self._sessionplayer.activityplayer.position 209 except babase.NotFoundError: 210 pass 211 if our_pos is None: 212 return 213 214 # Jitter position a bit since these often come in clusters. 215 our_pos = babase.Vec3( 216 our_pos[0] + (random.random() - 0.5) * 2.0, 217 our_pos[1] + (random.random() - 0.5) * 2.0, 218 our_pos[2] + (random.random() - 0.5) * 2.0, 219 ) 220 activity = self.getactivity() 221 if activity is not None: 222 PopupText( 223 babase.Lstr( 224 value=(('+' + str(score2) + ' ') if showpoints2 else '') 225 + '${N}', 226 subs=[('${N}', name2)], 227 ), 228 color=color2, 229 scale=scale2, 230 position=our_pos, 231 ).autoretain() 232 if sound2: 233 sound2.play() 234 235 self.score += score2 236 self.accumscore += score2 237 238 # Inform a running game of the score. 239 if score2 != 0 and activity is not None: 240 activity.handlemessage(PlayerScoredMessage(score=score2)) 241 242 if name is not None: 243 _bascenev1.timer( 244 0.3 + delay, 245 babase.Call( 246 _apply, name, score, showpoints, color, scale, sound 247 ), 248 ) 249 250 # Keep the tally rollin'... 251 # set a timer for a bit in the future. 252 self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)
Stats for an individual player in a Stats object.
Category: Gameplay Classes
This does not necessarily correspond to a 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 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 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 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 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 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 from bascenev1lib.actor.popuptext import PopupText 201 202 # Only award this if they're still alive and we can get 203 # a current position for them. 204 our_pos: babase.Vec3 | None = None 205 if self._sessionplayer: 206 if self._sessionplayer.activityplayer is not None: 207 try: 208 our_pos = self._sessionplayer.activityplayer.position 209 except babase.NotFoundError: 210 pass 211 if our_pos is None: 212 return 213 214 # Jitter position a bit since these often come in clusters. 215 our_pos = babase.Vec3( 216 our_pos[0] + (random.random() - 0.5) * 2.0, 217 our_pos[1] + (random.random() - 0.5) * 2.0, 218 our_pos[2] + (random.random() - 0.5) * 2.0, 219 ) 220 activity = self.getactivity() 221 if activity is not None: 222 PopupText( 223 babase.Lstr( 224 value=(('+' + str(score2) + ' ') if showpoints2 else '') 225 + '${N}', 226 subs=[('${N}', name2)], 227 ), 228 color=color2, 229 scale=scale2, 230 position=our_pos, 231 ).autoretain() 232 if sound2: 233 sound2.play() 234 235 self.score += score2 236 self.accumscore += score2 237 238 # Inform a running game of the score. 239 if score2 != 0 and activity is not None: 240 activity.handlemessage(PlayerScoredMessage(score=score2)) 241 242 if name is not None: 243 _bascenev1.timer( 244 0.3 + delay, 245 babase.Call( 246 _apply, name, score, showpoints, color, scale, sound 247 ), 248 ) 249 250 # Keep the tally rollin'... 251 # set a timer for a bit in the future. 252 self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)
Submit a kill for this player entry.
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 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 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 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
1393def pushcall( 1394 call: Callable, 1395 from_other_thread: bool = False, 1396 suppress_other_thread_warning: bool = False, 1397 other_thread_use_fg_context: bool = False, 1398 raw: bool = False, 1399) -> None: 1400 """Push a call to the logic event-loop. 1401 Category: **General Utility Functions** 1402 1403 This call expects to be used in the logic thread, and will automatically 1404 save and restore the babase.Context to behave seamlessly. 1405 1406 If you want to push a call from outside of the logic thread, 1407 however, you can pass 'from_other_thread' as True. In this case 1408 the call will always run in the UI context_ref on the logic thread 1409 or whichever context_ref is in the foreground if 1410 other_thread_use_fg_context is True. 1411 Passing raw=True will disable thread checks and context_ref sets/restores. 1412 """ 1413 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.
1467def safecolor( 1468 color: Sequence[float], target_intensity: float = 0.6 1469) -> tuple[float, ...]: 1470 """Given a color tuple, return a color safe to display as text. 1471 1472 Category: **General Utility Functions** 1473 1474 Accepts tuples of length 3 or 4. This will slightly brighten very 1475 dark colors, etc. 1476 """ 1477 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.
1480def screenmessage( 1481 message: str | babase.Lstr, 1482 color: Sequence[float] | None = None, 1483 log: bool = False, 1484) -> None: 1485 """Print a message to the local client's screen, in a given color. 1486 1487 Category: **General Utility Functions** 1488 1489 Note that this version of the function is purely for local display. 1490 To broadcast screen messages in network play, look for methods such as 1491 broadcastmessage() provided by the scene-version packages. 1492 """ 1493 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 Session.
The activity will not be actually run until 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 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 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.
Inherited Members
- Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- allow_pausing
- allow_kick_idle_players
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_overlay_center
- allow_mid_activity_joins
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- handlemessage
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- end
- create_player
- create_team
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
Inherited Members
- enum.Enum
- name
- value
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 team_names: Sequence[str] | None = None, 101 team_colors: Sequence[Sequence[float]] | None = None, 102 min_players: int = 1, 103 max_players: int = 8, 104 submit_score: bool = True, 105 ): 106 """Instantiate a session. 107 108 depsets should be a sequence of successfully resolved 109 bascenev1.DependencySet instances; one for each bascenev1.Activity 110 the session may potentially run. 111 """ 112 # pylint: disable=too-many-statements 113 # pylint: disable=too-many-locals 114 # pylint: disable=cyclic-import 115 # pylint: disable=too-many-branches 116 from efro.util import empty_weakref 117 from bascenev1._dependency import ( 118 Dependency, 119 AssetPackage, 120 DependencyError, 121 ) 122 from bascenev1._lobby import Lobby 123 from bascenev1._stats import Stats 124 from bascenev1._gameactivity import GameActivity 125 from bascenev1._activity import Activity 126 from bascenev1._team import SessionTeam 127 128 # First off, resolve all dependency-sets we were passed. 129 # If things are missing, we'll try to gather them into a single 130 # missing-deps exception if possible to give the caller a clean 131 # path to download missing stuff and try again. 132 missing_asset_packages: set[str] = set() 133 for depset in depsets: 134 try: 135 depset.resolve() 136 except DependencyError as exc: 137 # Gather/report missing assets only; barf on anything else. 138 if all(issubclass(d.cls, AssetPackage) for d in exc.deps): 139 for dep in exc.deps: 140 assert isinstance(dep.config, str) 141 missing_asset_packages.add(dep.config) 142 else: 143 missing_info = [(d.cls, d.config) for d in exc.deps] 144 raise RuntimeError( 145 f'Missing non-asset dependencies: {missing_info}' 146 ) from exc 147 148 # Throw a combined exception if we found anything missing. 149 if missing_asset_packages: 150 raise DependencyError( 151 [ 152 Dependency(AssetPackage, set_id) 153 for set_id in missing_asset_packages 154 ] 155 ) 156 157 # Ok; looks like our dependencies check out. 158 # Now give the engine a list of asset-set-ids to pass along to clients. 159 required_asset_packages: set[str] = set() 160 for depset in depsets: 161 required_asset_packages.update(depset.get_asset_package_ids()) 162 163 # print('Would set host-session asset-reqs to:', 164 # required_asset_packages) 165 166 # Init our C++ layer data. 167 self._sessiondata = _bascenev1.register_session(self) 168 169 # Should remove this if possible. 170 self.tournament_id: str | None = None 171 172 self.sessionteams = [] 173 self.sessionplayers = [] 174 self.min_players = min_players 175 self.max_players = ( 176 max_players 177 if _max_players_override is None 178 else _max_players_override 179 ) 180 self.submit_score = submit_score 181 182 self.customdata = {} 183 self._in_set_activity = False 184 self._next_team_id = 0 185 self._activity_retained: bascenev1.Activity | None = None 186 self._launch_end_session_activity_time: float | None = None 187 self._activity_end_timer: bascenev1.BaseTimer | None = None 188 self._activity_weak = empty_weakref(Activity) 189 self._next_activity: bascenev1.Activity | None = None 190 self._wants_to_end = False 191 self._ending = False 192 self._activity_should_end_immediately = False 193 self._activity_should_end_immediately_results: ( 194 bascenev1.GameResults | None 195 ) = None 196 self._activity_should_end_immediately_delay = 0.0 197 198 # Create static teams if we're using them. 199 if self.use_teams: 200 if team_names is None: 201 raise RuntimeError( 202 'use_teams is True but team_names not provided.' 203 ) 204 if team_colors is None: 205 raise RuntimeError( 206 'use_teams is True but team_colors not provided.' 207 ) 208 if len(team_colors) != len(team_names): 209 raise RuntimeError( 210 f'Got {len(team_names)} team_names' 211 f' and {len(team_colors)} team_colors;' 212 f' these numbers must match.' 213 ) 214 for i, color in enumerate(team_colors): 215 team = SessionTeam( 216 team_id=self._next_team_id, 217 name=GameActivity.get_team_display_string(team_names[i]), 218 color=color, 219 ) 220 self.sessionteams.append(team) 221 self._next_team_id += 1 222 try: 223 with self.context: 224 self.on_team_join(team) 225 except Exception: 226 logging.exception('Error in on_team_join for %s.', self) 227 228 self.lobby = Lobby() 229 self.stats = Stats() 230 231 # Instantiate our session globals node which will apply its settings. 232 self._sessionglobalsnode = _bascenev1.newnode('sessionglobals') 233 234 # Rejoin cooldown stuff. 235 self._players_on_wait: dict = {} 236 self._player_requested_identifiers: dict = {} 237 self._waitlist_timers: dict = {} 238 239 @property 240 def context(self) -> bascenev1.ContextRef: 241 """A context-ref pointing at this activity.""" 242 return self._sessiondata.context() 243 244 @property 245 def sessionglobalsnode(self) -> bascenev1.Node: 246 """The sessionglobals bascenev1.Node for the session.""" 247 node = self._sessionglobalsnode 248 if not node: 249 raise babase.NodeNotFoundError() 250 return node 251 252 def should_allow_mid_activity_joins( 253 self, activity: bascenev1.Activity 254 ) -> bool: 255 """Ask ourself if we should allow joins during an Activity. 256 257 Note that for a join to be allowed, both the Session and Activity 258 have to be ok with it (via this function and the 259 Activity.allow_mid_activity_joins property. 260 """ 261 del activity # Unused. 262 return True 263 264 def on_player_request(self, player: bascenev1.SessionPlayer) -> bool: 265 """Called when a new bascenev1.Player wants to join the Session. 266 267 This should return True or False to accept/reject. 268 """ 269 # Limit player counts *unless* we're in a stress test. 270 if ( 271 babase.app.classic is not None 272 and babase.app.classic.stress_test_update_timer is None 273 ): 274 if len(self.sessionplayers) >= self.max_players >= 0: 275 # Print a rejection message *only* to the client trying to 276 # join (prevents spamming everyone else in the game). 277 _bascenev1.getsound('error').play() 278 _bascenev1.broadcastmessage( 279 babase.Lstr( 280 resource='playerLimitReachedText', 281 subs=[('${COUNT}', str(self.max_players))], 282 ), 283 color=(0.8, 0.0, 0.0), 284 clients=[player.inputdevice.client_id], 285 transient=True, 286 ) 287 return False 288 289 # Rejoin cooldown. 290 identifier = player.get_v1_account_id() 291 if identifier: 292 leave_time = self._players_on_wait.get(identifier) 293 if leave_time: 294 diff = str( 295 math.ceil( 296 _g_player_rejoin_cooldown 297 - babase.apptime() 298 + leave_time 299 ) 300 ) 301 _bascenev1.broadcastmessage( 302 babase.Lstr( 303 translate=( 304 'serverResponses', 305 'You can join in ${COUNT} seconds.', 306 ), 307 subs=[('${COUNT}', diff)], 308 ), 309 color=(1, 1, 0), 310 clients=[player.inputdevice.client_id], 311 transient=True, 312 ) 313 return False 314 self._player_requested_identifiers[player.id] = identifier 315 316 _bascenev1.getsound('dripity').play() 317 return True 318 319 def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: 320 """Called when a previously-accepted bascenev1.SessionPlayer leaves.""" 321 322 if sessionplayer not in self.sessionplayers: 323 print( 324 'ERROR: Session.on_player_leave called' 325 ' for player not in our list.' 326 ) 327 return 328 329 _bascenev1.getsound('playerLeft').play() 330 331 activity = self._activity_weak() 332 333 # Rejoin cooldown. 334 identifier = self._player_requested_identifiers.get(sessionplayer.id) 335 if identifier: 336 self._players_on_wait[identifier] = babase.apptime() 337 with babase.ContextRef.empty(): 338 self._waitlist_timers[identifier] = babase.AppTimer( 339 _g_player_rejoin_cooldown, 340 babase.Call(self._remove_player_from_waitlist, identifier), 341 ) 342 343 if not sessionplayer.in_game: 344 # Ok, the player is still in the lobby; simply remove them. 345 with self.context: 346 try: 347 self.lobby.remove_chooser(sessionplayer) 348 except Exception: 349 logging.exception('Error in Lobby.remove_chooser().') 350 else: 351 # Ok, they've already entered the game. Remove them from 352 # teams/activities/etc. 353 sessionteam = sessionplayer.sessionteam 354 assert sessionteam is not None 355 356 _bascenev1.broadcastmessage( 357 babase.Lstr( 358 resource='playerLeftText', 359 subs=[('${PLAYER}', sessionplayer.getname(full=True))], 360 ) 361 ) 362 363 # Remove them from their SessionTeam. 364 if sessionplayer in sessionteam.players: 365 sessionteam.players.remove(sessionplayer) 366 else: 367 print( 368 'SessionPlayer not found in SessionTeam' 369 ' in on_player_leave.' 370 ) 371 372 # Grab their activity-specific player instance. 373 player = sessionplayer.activityplayer 374 assert isinstance(player, (Player, type(None))) 375 376 # Remove them from any current Activity. 377 if player is not None and activity is not None: 378 if player in activity.players: 379 activity.remove_player(sessionplayer) 380 else: 381 print('Player not found in Activity in on_player_leave.') 382 383 # If we're a non-team session, remove their team too. 384 if not self.use_teams: 385 self._remove_player_team(sessionteam, activity) 386 387 # Now remove them from the session list. 388 self.sessionplayers.remove(sessionplayer) 389 390 def _remove_player_team( 391 self, 392 sessionteam: bascenev1.SessionTeam, 393 activity: bascenev1.Activity | None, 394 ) -> None: 395 """Remove the player-specific team in non-teams mode.""" 396 397 # They should have been the only one on their team. 398 assert not sessionteam.players 399 400 # Remove their Team from the Activity. 401 if activity is not None: 402 if sessionteam.activityteam in activity.teams: 403 activity.remove_team(sessionteam) 404 else: 405 print('Team not found in Activity in on_player_leave.') 406 407 # And then from the Session. 408 with self.context: 409 if sessionteam in self.sessionteams: 410 try: 411 self.sessionteams.remove(sessionteam) 412 self.on_team_leave(sessionteam) 413 except Exception: 414 logging.exception( 415 'Error in on_team_leave for Session %s.', self 416 ) 417 else: 418 print('Team no in Session teams in on_player_leave.') 419 try: 420 sessionteam.leave() 421 except Exception: 422 logging.exception( 423 'Error clearing sessiondata for team %s in session %s.', 424 sessionteam, 425 self, 426 ) 427 428 def end(self) -> None: 429 """Initiates an end to the session and a return to the main menu. 430 431 Note that this happens asynchronously, allowing the 432 session and its activities to shut down gracefully. 433 """ 434 self._wants_to_end = True 435 if self._next_activity is None: 436 self._launch_end_session_activity() 437 438 def _launch_end_session_activity(self) -> None: 439 """(internal)""" 440 from bascenev1._activitytypes import EndSessionActivity 441 442 with self.context: 443 curtime = babase.apptime() 444 if self._ending: 445 # Ignore repeats unless its been a while. 446 assert self._launch_end_session_activity_time is not None 447 since_last = curtime - self._launch_end_session_activity_time 448 if since_last < 30.0: 449 return 450 logging.error( 451 '_launch_end_session_activity called twice (since_last=%s)', 452 since_last, 453 ) 454 self._launch_end_session_activity_time = curtime 455 self.setactivity(_bascenev1.newactivity(EndSessionActivity)) 456 self._wants_to_end = False 457 self._ending = True # Prevent further actions. 458 459 def on_team_join(self, team: bascenev1.SessionTeam) -> None: 460 """Called when a new bascenev1.Team joins the session.""" 461 462 def on_team_leave(self, team: bascenev1.SessionTeam) -> None: 463 """Called when a bascenev1.Team is leaving the session.""" 464 465 def end_activity( 466 self, 467 activity: bascenev1.Activity, 468 results: Any, 469 delay: float, 470 force: bool, 471 ) -> None: 472 """Commence shutdown of a bascenev1.Activity (if not already occurring). 473 474 'delay' is the time delay before the Activity actually ends 475 (in seconds). Further calls to end() will be ignored up until 476 this time, unless 'force' is True, in which case the new results 477 will replace the old. 478 """ 479 # Only pay attention if this is coming from our current activity. 480 if activity is not self._activity_retained: 481 return 482 483 # If this activity hasn't begun yet, just set it up to end immediately 484 # once it does. 485 if not activity.has_begun(): 486 # activity.set_immediate_end(results, delay, force) 487 if not self._activity_should_end_immediately or force: 488 self._activity_should_end_immediately = True 489 self._activity_should_end_immediately_results = results 490 self._activity_should_end_immediately_delay = delay 491 492 # The activity has already begun; get ready to end it. 493 else: 494 if (not activity.has_ended()) or force: 495 activity.set_has_ended(True) 496 497 # Set a timer to set in motion this activity's demise. 498 self._activity_end_timer = _bascenev1.BaseTimer( 499 delay, 500 babase.Call(self._complete_end_activity, activity, results), 501 ) 502 503 def handlemessage(self, msg: Any) -> Any: 504 """General message handling; can be passed any message object.""" 505 from bascenev1._lobby import PlayerReadyMessage 506 from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED 507 508 if isinstance(msg, PlayerReadyMessage): 509 self._on_player_ready(msg.chooser) 510 511 elif isinstance(msg, PlayerProfilesChangedMessage): 512 # If we have a current activity with a lobby, ask it to reload 513 # profiles. 514 with self.context: 515 self.lobby.reload_profiles() 516 return None 517 518 else: 519 return UNHANDLED 520 return None 521 522 class _SetActivityScopedLock: 523 def __init__(self, session: Session) -> None: 524 self._session = session 525 if session._in_set_activity: 526 raise RuntimeError('Session.setactivity() called recursively.') 527 self._session._in_set_activity = True 528 529 def __del__(self) -> None: 530 self._session._in_set_activity = False 531 532 def setactivity(self, activity: bascenev1.Activity) -> None: 533 """Assign a new current bascenev1.Activity for the session. 534 535 Note that this will not change the current context to the new 536 Activity's. Code must be run in the new activity's methods 537 (on_transition_in, etc) to get it. (so you can't do 538 session.setactivity(foo) and then bascenev1.newnode() to add a node 539 to foo) 540 """ 541 542 # Make sure we don't get called recursively. 543 _rlock = self._SetActivityScopedLock(self) 544 545 if activity.session is not _bascenev1.getsession(): 546 raise RuntimeError("Provided Activity's Session is not current.") 547 548 # Quietly ignore this if the whole session is going down. 549 if self._ending: 550 return 551 552 if activity is self._activity_retained: 553 logging.error('Activity set to already-current activity.') 554 return 555 556 if self._next_activity is not None: 557 raise RuntimeError( 558 'Activity switch already in progress (to ' 559 + str(self._next_activity) 560 + ')' 561 ) 562 563 prev_activity = self._activity_retained 564 prev_globals = ( 565 prev_activity.globalsnode if prev_activity is not None else None 566 ) 567 568 # Let the activity do its thing. 569 activity.transition_in(prev_globals) 570 571 self._next_activity = activity 572 573 # If we have a current activity, tell it it's transitioning out; 574 # the next one will become current once this one dies. 575 if prev_activity is not None: 576 prev_activity.transition_out() 577 578 # Setting this to None should free up the old activity to die, 579 # which will call begin_next_activity. 580 # We can still access our old activity through 581 # self._activity_weak() to keep it up to date on player 582 # joins/departures/etc until it dies. 583 self._activity_retained = None 584 585 # There's no existing activity; lets just go ahead with the begin call. 586 else: 587 self.begin_next_activity() 588 589 # We want to call destroy() for the previous activity once it should 590 # tear itself down, clear out any self-refs, etc. After this call 591 # the activity should have no refs left to it and should die (which 592 # will trigger the next activity to run). 593 if prev_activity is not None: 594 with babase.ContextRef.empty(): 595 babase.apptimer( 596 max(0.0, activity.transition_time), prev_activity.expire 597 ) 598 self._in_set_activity = False 599 600 def getactivity(self) -> bascenev1.Activity | None: 601 """Return the current foreground activity for this session.""" 602 return self._activity_weak() 603 604 def get_custom_menu_entries(self) -> list[dict[str, Any]]: 605 """Subclasses can override this to provide custom menu entries. 606 607 The returned value should be a list of dicts, each containing 608 a 'label' and 'call' entry, with 'label' being the text for 609 the entry and 'call' being the callable to trigger if the entry 610 is pressed. 611 """ 612 return [] 613 614 def _complete_end_activity( 615 self, activity: bascenev1.Activity, results: Any 616 ) -> None: 617 # Run the subclass callback in the session context. 618 try: 619 with self.context: 620 self.on_activity_end(activity, results) 621 except Exception: 622 logging.error( 623 'Error in on_activity_end() for session %s' 624 ' activity %s with results %s', 625 self, 626 activity, 627 results, 628 ) 629 630 def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool: 631 """Called by the native layer when a player wants to join.""" 632 633 # If we're ending, allow no new players. 634 if self._ending: 635 return False 636 637 # Ask the bascenev1.Session subclass to approve/deny this request. 638 try: 639 with self.context: 640 result = self.on_player_request(sessionplayer) 641 except Exception: 642 logging.exception('Error in on_player_request for %s.', self) 643 result = False 644 645 # If they said yes, add the player to the lobby. 646 if result: 647 self.sessionplayers.append(sessionplayer) 648 with self.context: 649 try: 650 self.lobby.add_chooser(sessionplayer) 651 except Exception: 652 logging.exception('Error in lobby.add_chooser().') 653 654 return result 655 656 def on_activity_end( 657 self, activity: bascenev1.Activity, results: Any 658 ) -> None: 659 """Called when the current bascenev1.Activity has ended. 660 661 The bascenev1.Session should look at the results and start 662 another bascenev1.Activity. 663 """ 664 665 def begin_next_activity(self) -> None: 666 """Called once the previous activity has been totally torn down. 667 668 This means we're ready to begin the next one 669 """ 670 if self._next_activity is None: 671 # Should this ever happen? 672 logging.error('begin_next_activity() called with no _next_activity') 673 return 674 675 # We store both a weak and a strong ref to the new activity; 676 # the strong is to keep it alive and the weak is so we can access 677 # it even after we've released the strong-ref to allow it to die. 678 self._activity_retained = self._next_activity 679 self._activity_weak = weakref.ref(self._next_activity) 680 self._next_activity = None 681 self._activity_should_end_immediately = False 682 683 # Kick out anyone loitering in the lobby. 684 self.lobby.remove_all_choosers_and_kick_players() 685 686 # Kick off the activity. 687 self._activity_retained.begin(self) 688 689 # If we want to completely end the session, we can now kick that off. 690 if self._wants_to_end: 691 self._launch_end_session_activity() 692 else: 693 # Otherwise, if the activity has already been told to end, 694 # do so now. 695 if self._activity_should_end_immediately: 696 self._activity_retained.end( 697 self._activity_should_end_immediately_results, 698 self._activity_should_end_immediately_delay, 699 ) 700 701 def _on_player_ready(self, chooser: bascenev1.Chooser) -> None: 702 """Called when a bascenev1.Player has checked themself ready.""" 703 lobby = chooser.lobby 704 activity = self._activity_weak() 705 706 # This happens sometimes. That seems like it shouldn't be happening; 707 # when would we have a session and a chooser with players but no 708 # active activity? 709 if activity is None: 710 print('_on_player_ready called with no activity.') 711 return 712 713 # In joining-activities, we wait till all choosers are ready 714 # and then create all players at once. 715 if activity.is_joining_activity: 716 if not lobby.check_all_ready(): 717 return 718 choosers = lobby.get_choosers() 719 min_players = self.min_players 720 if len(choosers) >= min_players: 721 for lch in lobby.get_choosers(): 722 self._add_chosen_player(lch) 723 lobby.remove_all_choosers() 724 725 # Get our next activity going. 726 self._complete_end_activity(activity, {}) 727 else: 728 _bascenev1.broadcastmessage( 729 babase.Lstr( 730 resource='notEnoughPlayersText', 731 subs=[('${COUNT}', str(min_players))], 732 ), 733 color=(1, 1, 0), 734 ) 735 _bascenev1.getsound('error').play() 736 737 # Otherwise just add players on the fly. 738 else: 739 self._add_chosen_player(chooser) 740 lobby.remove_chooser(chooser.getplayer()) 741 742 def transitioning_out_activity_was_freed( 743 self, can_show_ad_on_death: bool 744 ) -> None: 745 """(internal)""" 746 # pylint: disable=cyclic-import 747 748 # Since things should be generally still right now, it's a good time 749 # to run garbage collection to clear out any circular dependency 750 # loops. We keep this disabled normally to avoid non-deterministic 751 # hitches. 752 babase.garbage_collect() 753 754 assert babase.app.classic is not None 755 with self.context: 756 if can_show_ad_on_death: 757 babase.app.classic.ads.call_after_ad(self.begin_next_activity) 758 else: 759 babase.pushcall(self.begin_next_activity) 760 761 def _add_chosen_player( 762 self, chooser: bascenev1.Chooser 763 ) -> bascenev1.SessionPlayer: 764 from bascenev1._team import SessionTeam 765 766 sessionplayer = chooser.getplayer() 767 assert sessionplayer in self.sessionplayers, ( 768 'SessionPlayer not found in session ' 769 'player-list after chooser selection.' 770 ) 771 772 activity = self._activity_weak() 773 assert activity is not None 774 775 # Reset the player's input here, as it is probably 776 # referencing the chooser which could inadvertently keep it alive. 777 sessionplayer.resetinput() 778 779 # We can pass it to the current activity if it has already begun 780 # (otherwise it'll get passed once begin is called). 781 pass_to_activity = ( 782 activity.has_begun() and not activity.is_joining_activity 783 ) 784 785 # However, if we're not allowing mid-game joins, don't actually pass; 786 # just announce the arrival and say they'll partake next round. 787 if pass_to_activity: 788 if not ( 789 activity.allow_mid_activity_joins 790 and self.should_allow_mid_activity_joins(activity) 791 ): 792 pass_to_activity = False 793 with self.context: 794 _bascenev1.broadcastmessage( 795 babase.Lstr( 796 resource='playerDelayedJoinText', 797 subs=[ 798 ('${PLAYER}', sessionplayer.getname(full=True)) 799 ], 800 ), 801 color=(0, 1, 0), 802 ) 803 804 # If we're a non-team session, each player gets their own team. 805 # (keeps mini-game coding simpler if we can always deal with teams). 806 if self.use_teams: 807 sessionteam = chooser.sessionteam 808 else: 809 our_team_id = self._next_team_id 810 self._next_team_id += 1 811 sessionteam = SessionTeam( 812 team_id=our_team_id, 813 color=chooser.get_color(), 814 name=chooser.getplayer().getname(full=True, icon=False), 815 ) 816 817 # Add player's team to the Session. 818 self.sessionteams.append(sessionteam) 819 820 with self.context: 821 try: 822 self.on_team_join(sessionteam) 823 except Exception: 824 logging.exception('Error in on_team_join for %s.', self) 825 826 # Add player's team to the Activity. 827 if pass_to_activity: 828 activity.add_team(sessionteam) 829 830 assert sessionplayer not in sessionteam.players 831 sessionteam.players.append(sessionplayer) 832 sessionplayer.setdata( 833 team=sessionteam, 834 character=chooser.get_character_name(), 835 color=chooser.get_color(), 836 highlight=chooser.get_highlight(), 837 ) 838 839 self.stats.register_sessionplayer(sessionplayer) 840 if pass_to_activity: 841 activity.add_player(sessionplayer) 842 return sessionplayer 843 844 def _remove_player_from_waitlist(self, identifier: str) -> None: 845 try: 846 self._players_on_wait.pop(identifier) 847 except KeyError: 848 pass
Defines a high level series of Activity-es.
Category: Gameplay Classes
Examples of sessions are FreeForAllSession, DualTeamSession, and CoopSession.
A Session is responsible for wrangling and transitioning between various 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 team_names: Sequence[str] | None = None, 101 team_colors: Sequence[Sequence[float]] | None = None, 102 min_players: int = 1, 103 max_players: int = 8, 104 submit_score: bool = True, 105 ): 106 """Instantiate a session. 107 108 depsets should be a sequence of successfully resolved 109 bascenev1.DependencySet instances; one for each bascenev1.Activity 110 the session may potentially run. 111 """ 112 # pylint: disable=too-many-statements 113 # pylint: disable=too-many-locals 114 # pylint: disable=cyclic-import 115 # pylint: disable=too-many-branches 116 from efro.util import empty_weakref 117 from bascenev1._dependency import ( 118 Dependency, 119 AssetPackage, 120 DependencyError, 121 ) 122 from bascenev1._lobby import Lobby 123 from bascenev1._stats import Stats 124 from bascenev1._gameactivity import GameActivity 125 from bascenev1._activity import Activity 126 from bascenev1._team import SessionTeam 127 128 # First off, resolve all dependency-sets we were passed. 129 # If things are missing, we'll try to gather them into a single 130 # missing-deps exception if possible to give the caller a clean 131 # path to download missing stuff and try again. 132 missing_asset_packages: set[str] = set() 133 for depset in depsets: 134 try: 135 depset.resolve() 136 except DependencyError as exc: 137 # Gather/report missing assets only; barf on anything else. 138 if all(issubclass(d.cls, AssetPackage) for d in exc.deps): 139 for dep in exc.deps: 140 assert isinstance(dep.config, str) 141 missing_asset_packages.add(dep.config) 142 else: 143 missing_info = [(d.cls, d.config) for d in exc.deps] 144 raise RuntimeError( 145 f'Missing non-asset dependencies: {missing_info}' 146 ) from exc 147 148 # Throw a combined exception if we found anything missing. 149 if missing_asset_packages: 150 raise DependencyError( 151 [ 152 Dependency(AssetPackage, set_id) 153 for set_id in missing_asset_packages 154 ] 155 ) 156 157 # Ok; looks like our dependencies check out. 158 # Now give the engine a list of asset-set-ids to pass along to clients. 159 required_asset_packages: set[str] = set() 160 for depset in depsets: 161 required_asset_packages.update(depset.get_asset_package_ids()) 162 163 # print('Would set host-session asset-reqs to:', 164 # required_asset_packages) 165 166 # Init our C++ layer data. 167 self._sessiondata = _bascenev1.register_session(self) 168 169 # Should remove this if possible. 170 self.tournament_id: str | None = None 171 172 self.sessionteams = [] 173 self.sessionplayers = [] 174 self.min_players = min_players 175 self.max_players = ( 176 max_players 177 if _max_players_override is None 178 else _max_players_override 179 ) 180 self.submit_score = submit_score 181 182 self.customdata = {} 183 self._in_set_activity = False 184 self._next_team_id = 0 185 self._activity_retained: bascenev1.Activity | None = None 186 self._launch_end_session_activity_time: float | None = None 187 self._activity_end_timer: bascenev1.BaseTimer | None = None 188 self._activity_weak = empty_weakref(Activity) 189 self._next_activity: bascenev1.Activity | None = None 190 self._wants_to_end = False 191 self._ending = False 192 self._activity_should_end_immediately = False 193 self._activity_should_end_immediately_results: ( 194 bascenev1.GameResults | None 195 ) = None 196 self._activity_should_end_immediately_delay = 0.0 197 198 # Create static teams if we're using them. 199 if self.use_teams: 200 if team_names is None: 201 raise RuntimeError( 202 'use_teams is True but team_names not provided.' 203 ) 204 if team_colors is None: 205 raise RuntimeError( 206 'use_teams is True but team_colors not provided.' 207 ) 208 if len(team_colors) != len(team_names): 209 raise RuntimeError( 210 f'Got {len(team_names)} team_names' 211 f' and {len(team_colors)} team_colors;' 212 f' these numbers must match.' 213 ) 214 for i, color in enumerate(team_colors): 215 team = SessionTeam( 216 team_id=self._next_team_id, 217 name=GameActivity.get_team_display_string(team_names[i]), 218 color=color, 219 ) 220 self.sessionteams.append(team) 221 self._next_team_id += 1 222 try: 223 with self.context: 224 self.on_team_join(team) 225 except Exception: 226 logging.exception('Error in on_team_join for %s.', self) 227 228 self.lobby = Lobby() 229 self.stats = Stats() 230 231 # Instantiate our session globals node which will apply its settings. 232 self._sessionglobalsnode = _bascenev1.newnode('sessionglobals') 233 234 # Rejoin cooldown stuff. 235 self._players_on_wait: dict = {} 236 self._player_requested_identifiers: dict = {} 237 self._waitlist_timers: dict = {}
Instantiate a session.
depsets should be a sequence of successfully resolved DependencySet instances; one for each 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 minimum number of players who must be present for the Session to proceed past the initial joining screen
A shared dictionary for objects to use as storage on this session. Ensure that keys here are unique to avoid collisions.
239 @property 240 def context(self) -> bascenev1.ContextRef: 241 """A context-ref pointing at this activity.""" 242 return self._sessiondata.context()
A context-ref pointing at this activity.
244 @property 245 def sessionglobalsnode(self) -> bascenev1.Node: 246 """The sessionglobals bascenev1.Node for the session.""" 247 node = self._sessionglobalsnode 248 if not node: 249 raise babase.NodeNotFoundError() 250 return node
The sessionglobals Node for the session.
252 def should_allow_mid_activity_joins( 253 self, activity: bascenev1.Activity 254 ) -> bool: 255 """Ask ourself if we should allow joins during an Activity. 256 257 Note that for a join to be allowed, both the Session and Activity 258 have to be ok with it (via this function and the 259 Activity.allow_mid_activity_joins property. 260 """ 261 del activity # Unused. 262 return True
Ask ourself if we should allow joins during an Activity.
Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.
264 def on_player_request(self, player: bascenev1.SessionPlayer) -> bool: 265 """Called when a new bascenev1.Player wants to join the Session. 266 267 This should return True or False to accept/reject. 268 """ 269 # Limit player counts *unless* we're in a stress test. 270 if ( 271 babase.app.classic is not None 272 and babase.app.classic.stress_test_update_timer is None 273 ): 274 if len(self.sessionplayers) >= self.max_players >= 0: 275 # Print a rejection message *only* to the client trying to 276 # join (prevents spamming everyone else in the game). 277 _bascenev1.getsound('error').play() 278 _bascenev1.broadcastmessage( 279 babase.Lstr( 280 resource='playerLimitReachedText', 281 subs=[('${COUNT}', str(self.max_players))], 282 ), 283 color=(0.8, 0.0, 0.0), 284 clients=[player.inputdevice.client_id], 285 transient=True, 286 ) 287 return False 288 289 # Rejoin cooldown. 290 identifier = player.get_v1_account_id() 291 if identifier: 292 leave_time = self._players_on_wait.get(identifier) 293 if leave_time: 294 diff = str( 295 math.ceil( 296 _g_player_rejoin_cooldown 297 - babase.apptime() 298 + leave_time 299 ) 300 ) 301 _bascenev1.broadcastmessage( 302 babase.Lstr( 303 translate=( 304 'serverResponses', 305 'You can join in ${COUNT} seconds.', 306 ), 307 subs=[('${COUNT}', diff)], 308 ), 309 color=(1, 1, 0), 310 clients=[player.inputdevice.client_id], 311 transient=True, 312 ) 313 return False 314 self._player_requested_identifiers[player.id] = identifier 315 316 _bascenev1.getsound('dripity').play() 317 return True
Called when a new Player wants to join the Session.
This should return True or False to accept/reject.
319 def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None: 320 """Called when a previously-accepted bascenev1.SessionPlayer leaves.""" 321 322 if sessionplayer not in self.sessionplayers: 323 print( 324 'ERROR: Session.on_player_leave called' 325 ' for player not in our list.' 326 ) 327 return 328 329 _bascenev1.getsound('playerLeft').play() 330 331 activity = self._activity_weak() 332 333 # Rejoin cooldown. 334 identifier = self._player_requested_identifiers.get(sessionplayer.id) 335 if identifier: 336 self._players_on_wait[identifier] = babase.apptime() 337 with babase.ContextRef.empty(): 338 self._waitlist_timers[identifier] = babase.AppTimer( 339 _g_player_rejoin_cooldown, 340 babase.Call(self._remove_player_from_waitlist, identifier), 341 ) 342 343 if not sessionplayer.in_game: 344 # Ok, the player is still in the lobby; simply remove them. 345 with self.context: 346 try: 347 self.lobby.remove_chooser(sessionplayer) 348 except Exception: 349 logging.exception('Error in Lobby.remove_chooser().') 350 else: 351 # Ok, they've already entered the game. Remove them from 352 # teams/activities/etc. 353 sessionteam = sessionplayer.sessionteam 354 assert sessionteam is not None 355 356 _bascenev1.broadcastmessage( 357 babase.Lstr( 358 resource='playerLeftText', 359 subs=[('${PLAYER}', sessionplayer.getname(full=True))], 360 ) 361 ) 362 363 # Remove them from their SessionTeam. 364 if sessionplayer in sessionteam.players: 365 sessionteam.players.remove(sessionplayer) 366 else: 367 print( 368 'SessionPlayer not found in SessionTeam' 369 ' in on_player_leave.' 370 ) 371 372 # Grab their activity-specific player instance. 373 player = sessionplayer.activityplayer 374 assert isinstance(player, (Player, type(None))) 375 376 # Remove them from any current Activity. 377 if player is not None and activity is not None: 378 if player in activity.players: 379 activity.remove_player(sessionplayer) 380 else: 381 print('Player not found in Activity in on_player_leave.') 382 383 # If we're a non-team session, remove their team too. 384 if not self.use_teams: 385 self._remove_player_team(sessionteam, activity) 386 387 # Now remove them from the session list. 388 self.sessionplayers.remove(sessionplayer)
Called when a previously-accepted SessionPlayer leaves.
428 def end(self) -> None: 429 """Initiates an end to the session and a return to the main menu. 430 431 Note that this happens asynchronously, allowing the 432 session and its activities to shut down gracefully. 433 """ 434 self._wants_to_end = True 435 if self._next_activity is None: 436 self._launch_end_session_activity()
Initiates an end to the session and a return to the main menu.
Note that this happens asynchronously, allowing the session and its activities to shut down gracefully.
459 def on_team_join(self, team: bascenev1.SessionTeam) -> None: 460 """Called when a new bascenev1.Team joins the session."""
Called when a new Team joins the session.
462 def on_team_leave(self, team: bascenev1.SessionTeam) -> None: 463 """Called when a bascenev1.Team is leaving the session."""
Called when a Team is leaving the session.
465 def end_activity( 466 self, 467 activity: bascenev1.Activity, 468 results: Any, 469 delay: float, 470 force: bool, 471 ) -> None: 472 """Commence shutdown of a bascenev1.Activity (if not already occurring). 473 474 'delay' is the time delay before the Activity actually ends 475 (in seconds). Further calls to end() will be ignored up until 476 this time, unless 'force' is True, in which case the new results 477 will replace the old. 478 """ 479 # Only pay attention if this is coming from our current activity. 480 if activity is not self._activity_retained: 481 return 482 483 # If this activity hasn't begun yet, just set it up to end immediately 484 # once it does. 485 if not activity.has_begun(): 486 # activity.set_immediate_end(results, delay, force) 487 if not self._activity_should_end_immediately or force: 488 self._activity_should_end_immediately = True 489 self._activity_should_end_immediately_results = results 490 self._activity_should_end_immediately_delay = delay 491 492 # The activity has already begun; get ready to end it. 493 else: 494 if (not activity.has_ended()) or force: 495 activity.set_has_ended(True) 496 497 # Set a timer to set in motion this activity's demise. 498 self._activity_end_timer = _bascenev1.BaseTimer( 499 delay, 500 babase.Call(self._complete_end_activity, activity, results), 501 )
Commence shutdown of a 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.
503 def handlemessage(self, msg: Any) -> Any: 504 """General message handling; can be passed any message object.""" 505 from bascenev1._lobby import PlayerReadyMessage 506 from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED 507 508 if isinstance(msg, PlayerReadyMessage): 509 self._on_player_ready(msg.chooser) 510 511 elif isinstance(msg, PlayerProfilesChangedMessage): 512 # If we have a current activity with a lobby, ask it to reload 513 # profiles. 514 with self.context: 515 self.lobby.reload_profiles() 516 return None 517 518 else: 519 return UNHANDLED 520 return None
General message handling; can be passed any message object.
532 def setactivity(self, activity: bascenev1.Activity) -> None: 533 """Assign a new current bascenev1.Activity for the session. 534 535 Note that this will not change the current context to the new 536 Activity's. Code must be run in the new activity's methods 537 (on_transition_in, etc) to get it. (so you can't do 538 session.setactivity(foo) and then bascenev1.newnode() to add a node 539 to foo) 540 """ 541 542 # Make sure we don't get called recursively. 543 _rlock = self._SetActivityScopedLock(self) 544 545 if activity.session is not _bascenev1.getsession(): 546 raise RuntimeError("Provided Activity's Session is not current.") 547 548 # Quietly ignore this if the whole session is going down. 549 if self._ending: 550 return 551 552 if activity is self._activity_retained: 553 logging.error('Activity set to already-current activity.') 554 return 555 556 if self._next_activity is not None: 557 raise RuntimeError( 558 'Activity switch already in progress (to ' 559 + str(self._next_activity) 560 + ')' 561 ) 562 563 prev_activity = self._activity_retained 564 prev_globals = ( 565 prev_activity.globalsnode if prev_activity is not None else None 566 ) 567 568 # Let the activity do its thing. 569 activity.transition_in(prev_globals) 570 571 self._next_activity = activity 572 573 # If we have a current activity, tell it it's transitioning out; 574 # the next one will become current once this one dies. 575 if prev_activity is not None: 576 prev_activity.transition_out() 577 578 # Setting this to None should free up the old activity to die, 579 # which will call begin_next_activity. 580 # We can still access our old activity through 581 # self._activity_weak() to keep it up to date on player 582 # joins/departures/etc until it dies. 583 self._activity_retained = None 584 585 # There's no existing activity; lets just go ahead with the begin call. 586 else: 587 self.begin_next_activity() 588 589 # We want to call destroy() for the previous activity once it should 590 # tear itself down, clear out any self-refs, etc. After this call 591 # the activity should have no refs left to it and should die (which 592 # will trigger the next activity to run). 593 if prev_activity is not None: 594 with babase.ContextRef.empty(): 595 babase.apptimer( 596 max(0.0, activity.transition_time), prev_activity.expire 597 ) 598 self._in_set_activity = False
600 def getactivity(self) -> bascenev1.Activity | None: 601 """Return the current foreground activity for this session.""" 602 return self._activity_weak()
Return the current foreground activity for this session.
665 def begin_next_activity(self) -> None: 666 """Called once the previous activity has been totally torn down. 667 668 This means we're ready to begin the next one 669 """ 670 if self._next_activity is None: 671 # Should this ever happen? 672 logging.error('begin_next_activity() called with no _next_activity') 673 return 674 675 # We store both a weak and a strong ref to the new activity; 676 # the strong is to keep it alive and the weak is so we can access 677 # it even after we've released the strong-ref to allow it to die. 678 self._activity_retained = self._next_activity 679 self._activity_weak = weakref.ref(self._next_activity) 680 self._next_activity = None 681 self._activity_should_end_immediately = False 682 683 # Kick out anyone loitering in the lobby. 684 self.lobby.remove_all_choosers_and_kick_players() 685 686 # Kick off the activity. 687 self._activity_retained.begin(self) 688 689 # If we want to completely end the session, we can now kick that off. 690 if self._wants_to_end: 691 self._launch_end_session_activity() 692 else: 693 # Otherwise, if the activity has already been told to end, 694 # do so now. 695 if self._activity_should_end_immediately: 696 self._activity_retained.end( 697 self._activity_should_end_immediately_results, 698 self._activity_should_end_immediately_delay, 699 )
Called once the previous activity has been totally torn down.
This means we're ready to begin the next one
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 Session.
Category: Gameplay Classes
These are created and managed internally and
provided to your bascenev1.Session/bascenev1Activity instances.
Be aware that, like ba.Node
s, 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
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 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 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 SessionTeam.
In most cases, all teams are provided to you by the Session, Session, so calling this shouldn't be necessary.
A dict for use by the current Session for storing data associated with this team. Unlike customdata, this persists for the duration of the session.
1496def set_analytics_screen(screen: str) -> None: 1497 """Used for analytics to see where in the app players spend their time. 1498 1499 Category: **General Utility Functions** 1500 1501 Generally called when opening a new window or entering some UI. 1502 'screen' should be a string description of an app location 1503 ('Main Menu', etc.) 1504 """ 1505 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
175def show_damage_count( 176 damage: str, position: Sequence[float], direction: Sequence[float] 177) -> None: 178 """Pop up a damage count at a position in space. 179 180 Category: **Gameplay Functions** 181 """ 182 lifespan = 1.0 183 app = babase.app 184 185 # FIXME: Should never vary game elements based on local config. 186 # (connected clients may have differing configs so they won't 187 # get the intended results). 188 assert app.classic is not None 189 do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr 190 txtnode = _bascenev1.newnode( 191 'text', 192 attrs={ 193 'text': damage, 194 'in_world': True, 195 'h_align': 'center', 196 'flatness': 1.0, 197 'shadow': 1.0 if do_big else 0.7, 198 'color': (1, 0.25, 0.25, 1), 199 'scale': 0.015 if do_big else 0.01, 200 }, 201 ) 202 # Translate upward. 203 tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3}) 204 tcombine.connectattr('output', txtnode, 'position') 205 v_vals = [] 206 pval = 0.0 207 vval = 0.07 208 count = 6 209 for i in range(count): 210 v_vals.append((float(i) / count, pval)) 211 pval += vval 212 vval *= 0.5 213 p_start = position[0] 214 p_dir = direction[0] 215 animate( 216 tcombine, 217 'input0', 218 {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, 219 ) 220 p_start = position[1] 221 p_dir = direction[1] 222 animate( 223 tcombine, 224 'input1', 225 {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, 226 ) 227 p_start = position[2] 228 p_dir = direction[2] 229 animate( 230 tcombine, 231 'input2', 232 {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals}, 233 ) 234 animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0}) 235 _bascenev1.timer(lifespan, txtnode.delete)
Pop up a damage count at a position in space.
Category: Gameplay Functions
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.
255class Stats: 256 """Manages scores and statistics for a bascenev1.Session. 257 258 Category: **Gameplay Classes** 259 """ 260 261 def __init__(self) -> None: 262 self._activity: weakref.ref[bascenev1.Activity] | None = None 263 self._player_records: dict[str, PlayerRecord] = {} 264 self.orchestrahitsound1: bascenev1.Sound | None = None 265 self.orchestrahitsound2: bascenev1.Sound | None = None 266 self.orchestrahitsound3: bascenev1.Sound | None = None 267 self.orchestrahitsound4: bascenev1.Sound | None = None 268 269 def setactivity(self, activity: bascenev1.Activity | None) -> None: 270 """Set the current activity for this instance.""" 271 272 self._activity = None if activity is None else weakref.ref(activity) 273 274 # Load our media into this activity's context. 275 if activity is not None: 276 if activity.expired: 277 logging.exception('Unexpected finalized activity.') 278 else: 279 with activity.context: 280 self._load_activity_media() 281 282 def getactivity(self) -> bascenev1.Activity | None: 283 """Get the activity associated with this instance. 284 285 May return None. 286 """ 287 if self._activity is None: 288 return None 289 return self._activity() 290 291 def _load_activity_media(self) -> None: 292 self.orchestrahitsound1 = _bascenev1.getsound('orchestraHit') 293 self.orchestrahitsound2 = _bascenev1.getsound('orchestraHit2') 294 self.orchestrahitsound3 = _bascenev1.getsound('orchestraHit3') 295 self.orchestrahitsound4 = _bascenev1.getsound('orchestraHit4') 296 297 def reset(self) -> None: 298 """Reset the stats instance completely.""" 299 300 # Just to be safe, lets make sure no multi-kill timers are gonna go off 301 # for no-longer-on-the-list players. 302 for p_entry in list(self._player_records.values()): 303 p_entry.cancel_multi_kill_timer() 304 self._player_records = {} 305 306 def reset_accum(self) -> None: 307 """Reset per-sound sub-scores.""" 308 for s_player in list(self._player_records.values()): 309 s_player.cancel_multi_kill_timer() 310 s_player.accumscore = 0 311 s_player.accum_kill_count = 0 312 s_player.accum_killed_count = 0 313 s_player.streak = 0 314 315 def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None: 316 """Register a bascenev1.SessionPlayer with this score-set.""" 317 assert player.exists() # Invalid refs should never be passed to funcs. 318 name = player.getname() 319 if name in self._player_records: 320 # If the player already exists, update his character and such as 321 # it may have changed. 322 self._player_records[name].associate_with_sessionplayer(player) 323 else: 324 name_full = player.getname(full=True) 325 self._player_records[name] = PlayerRecord( 326 name, name_full, player, self 327 ) 328 329 def get_records(self) -> dict[str, bascenev1.PlayerRecord]: 330 """Get PlayerRecord corresponding to still-existing players.""" 331 records = {} 332 333 # Go through our player records and return ones whose player id still 334 # corresponds to a player with that name. 335 for record_id, record in self._player_records.items(): 336 lastplayer = record.get_last_sessionplayer() 337 if lastplayer and lastplayer.getname() == record_id: 338 records[record_id] = record 339 return records 340 341 def player_scored( 342 self, 343 player: bascenev1.Player, 344 base_points: int = 1, 345 target: Sequence[float] | None = None, 346 kill: bool = False, 347 victim_player: bascenev1.Player | None = None, 348 scale: float = 1.0, 349 color: Sequence[float] | None = None, 350 title: str | babase.Lstr | None = None, 351 screenmessage: bool = True, 352 display: bool = True, 353 importance: int = 1, 354 showpoints: bool = True, 355 big_message: bool = False, 356 ) -> int: 357 """Register a score for the player. 358 359 Return value is actual score with multipliers and such factored in. 360 """ 361 # FIXME: Tidy this up. 362 # pylint: disable=cyclic-import 363 # pylint: disable=too-many-branches 364 # pylint: disable=too-many-locals 365 from bascenev1lib.actor.popuptext import PopupText 366 367 from bascenev1._gameactivity import GameActivity 368 369 del victim_player # Currently unused. 370 name = player.getname() 371 s_player = self._player_records[name] 372 373 if kill: 374 s_player.submit_kill(showpoints=showpoints) 375 376 display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) 377 378 if color is not None: 379 display_color = color 380 elif importance != 1: 381 display_color = (1.0, 1.0, 0.4, 1.0) 382 points = base_points 383 384 # If they want a big announcement, throw a zoom-text up there. 385 if display and big_message: 386 try: 387 assert self._activity is not None 388 activity = self._activity() 389 if isinstance(activity, GameActivity): 390 name_full = player.getname(full=True, icon=False) 391 activity.show_zoom_message( 392 babase.Lstr( 393 resource='nameScoresText', 394 subs=[('${NAME}', name_full)], 395 ), 396 color=babase.normalized_color(player.team.color), 397 ) 398 except Exception: 399 logging.exception('Error showing big_message.') 400 401 # If we currently have a actor, pop up a score over it. 402 if display and showpoints: 403 our_pos = player.node.position if player.node else None 404 if our_pos is not None: 405 if target is None: 406 target = our_pos 407 408 # If display-pos is *way* lower than us, raise it up 409 # (so we can still see scores from dudes that fell off cliffs). 410 display_pos = ( 411 target[0], 412 max(target[1], our_pos[1] - 2.0), 413 min(target[2], our_pos[2] + 2.0), 414 ) 415 activity = self.getactivity() 416 if activity is not None: 417 if title is not None: 418 sval = babase.Lstr( 419 value='+${A} ${B}', 420 subs=[('${A}', str(points)), ('${B}', title)], 421 ) 422 else: 423 sval = babase.Lstr( 424 value='+${A}', subs=[('${A}', str(points))] 425 ) 426 PopupText( 427 sval, 428 color=display_color, 429 scale=1.2 * scale, 430 position=display_pos, 431 ).autoretain() 432 433 # Tally kills. 434 if kill: 435 s_player.accum_kill_count += 1 436 s_player.kill_count += 1 437 438 # Report non-kill scorings. 439 try: 440 if screenmessage and not kill: 441 _bascenev1.broadcastmessage( 442 babase.Lstr( 443 resource='nameScoresText', subs=[('${NAME}', name)] 444 ), 445 top=True, 446 color=player.color, 447 image=player.get_icon(), 448 ) 449 except Exception: 450 logging.exception('Error announcing score.') 451 452 s_player.score += points 453 s_player.accumscore += points 454 455 # Inform a running game of the score. 456 if points != 0: 457 activity = self._activity() if self._activity is not None else None 458 if activity is not None: 459 activity.handlemessage(PlayerScoredMessage(score=points)) 460 461 return points 462 463 def player_was_killed( 464 self, 465 player: bascenev1.Player, 466 killed: bool = False, 467 killer: bascenev1.Player | None = None, 468 ) -> None: 469 """Should be called when a player is killed.""" 470 name = player.getname() 471 prec = self._player_records[name] 472 prec.streak = 0 473 if killed: 474 prec.accum_killed_count += 1 475 prec.killed_count += 1 476 try: 477 if killed and _bascenev1.getactivity().announce_player_deaths: 478 if killer is player: 479 _bascenev1.broadcastmessage( 480 babase.Lstr( 481 resource='nameSuicideText', subs=[('${NAME}', name)] 482 ), 483 top=True, 484 color=player.color, 485 image=player.get_icon(), 486 ) 487 elif killer is not None: 488 if killer.team is player.team: 489 _bascenev1.broadcastmessage( 490 babase.Lstr( 491 resource='nameBetrayedText', 492 subs=[ 493 ('${NAME}', killer.getname()), 494 ('${VICTIM}', name), 495 ], 496 ), 497 top=True, 498 color=killer.color, 499 image=killer.get_icon(), 500 ) 501 else: 502 _bascenev1.broadcastmessage( 503 babase.Lstr( 504 resource='nameKilledText', 505 subs=[ 506 ('${NAME}', killer.getname()), 507 ('${VICTIM}', name), 508 ], 509 ), 510 top=True, 511 color=killer.color, 512 image=killer.get_icon(), 513 ) 514 else: 515 _bascenev1.broadcastmessage( 516 babase.Lstr( 517 resource='nameDiedText', subs=[('${NAME}', name)] 518 ), 519 top=True, 520 color=player.color, 521 image=player.get_icon(), 522 ) 523 except Exception: 524 logging.exception('Error announcing kill.')
Manages scores and statistics for a Session.
Category: Gameplay Classes
269 def setactivity(self, activity: bascenev1.Activity | None) -> None: 270 """Set the current activity for this instance.""" 271 272 self._activity = None if activity is None else weakref.ref(activity) 273 274 # Load our media into this activity's context. 275 if activity is not None: 276 if activity.expired: 277 logging.exception('Unexpected finalized activity.') 278 else: 279 with activity.context: 280 self._load_activity_media()
Set the current activity for this instance.
282 def getactivity(self) -> bascenev1.Activity | None: 283 """Get the activity associated with this instance. 284 285 May return None. 286 """ 287 if self._activity is None: 288 return None 289 return self._activity()
Get the activity associated with this instance.
May return None.
297 def reset(self) -> None: 298 """Reset the stats instance completely.""" 299 300 # Just to be safe, lets make sure no multi-kill timers are gonna go off 301 # for no-longer-on-the-list players. 302 for p_entry in list(self._player_records.values()): 303 p_entry.cancel_multi_kill_timer() 304 self._player_records = {}
Reset the stats instance completely.
306 def reset_accum(self) -> None: 307 """Reset per-sound sub-scores.""" 308 for s_player in list(self._player_records.values()): 309 s_player.cancel_multi_kill_timer() 310 s_player.accumscore = 0 311 s_player.accum_kill_count = 0 312 s_player.accum_killed_count = 0 313 s_player.streak = 0
Reset per-sound sub-scores.
315 def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None: 316 """Register a bascenev1.SessionPlayer with this score-set.""" 317 assert player.exists() # Invalid refs should never be passed to funcs. 318 name = player.getname() 319 if name in self._player_records: 320 # If the player already exists, update his character and such as 321 # it may have changed. 322 self._player_records[name].associate_with_sessionplayer(player) 323 else: 324 name_full = player.getname(full=True) 325 self._player_records[name] = PlayerRecord( 326 name, name_full, player, self 327 )
Register a SessionPlayer with this score-set.
329 def get_records(self) -> dict[str, bascenev1.PlayerRecord]: 330 """Get PlayerRecord corresponding to still-existing players.""" 331 records = {} 332 333 # Go through our player records and return ones whose player id still 334 # corresponds to a player with that name. 335 for record_id, record in self._player_records.items(): 336 lastplayer = record.get_last_sessionplayer() 337 if lastplayer and lastplayer.getname() == record_id: 338 records[record_id] = record 339 return records
Get PlayerRecord corresponding to still-existing players.
341 def player_scored( 342 self, 343 player: bascenev1.Player, 344 base_points: int = 1, 345 target: Sequence[float] | None = None, 346 kill: bool = False, 347 victim_player: bascenev1.Player | None = None, 348 scale: float = 1.0, 349 color: Sequence[float] | None = None, 350 title: str | babase.Lstr | None = None, 351 screenmessage: bool = True, 352 display: bool = True, 353 importance: int = 1, 354 showpoints: bool = True, 355 big_message: bool = False, 356 ) -> int: 357 """Register a score for the player. 358 359 Return value is actual score with multipliers and such factored in. 360 """ 361 # FIXME: Tidy this up. 362 # pylint: disable=cyclic-import 363 # pylint: disable=too-many-branches 364 # pylint: disable=too-many-locals 365 from bascenev1lib.actor.popuptext import PopupText 366 367 from bascenev1._gameactivity import GameActivity 368 369 del victim_player # Currently unused. 370 name = player.getname() 371 s_player = self._player_records[name] 372 373 if kill: 374 s_player.submit_kill(showpoints=showpoints) 375 376 display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0) 377 378 if color is not None: 379 display_color = color 380 elif importance != 1: 381 display_color = (1.0, 1.0, 0.4, 1.0) 382 points = base_points 383 384 # If they want a big announcement, throw a zoom-text up there. 385 if display and big_message: 386 try: 387 assert self._activity is not None 388 activity = self._activity() 389 if isinstance(activity, GameActivity): 390 name_full = player.getname(full=True, icon=False) 391 activity.show_zoom_message( 392 babase.Lstr( 393 resource='nameScoresText', 394 subs=[('${NAME}', name_full)], 395 ), 396 color=babase.normalized_color(player.team.color), 397 ) 398 except Exception: 399 logging.exception('Error showing big_message.') 400 401 # If we currently have a actor, pop up a score over it. 402 if display and showpoints: 403 our_pos = player.node.position if player.node else None 404 if our_pos is not None: 405 if target is None: 406 target = our_pos 407 408 # If display-pos is *way* lower than us, raise it up 409 # (so we can still see scores from dudes that fell off cliffs). 410 display_pos = ( 411 target[0], 412 max(target[1], our_pos[1] - 2.0), 413 min(target[2], our_pos[2] + 2.0), 414 ) 415 activity = self.getactivity() 416 if activity is not None: 417 if title is not None: 418 sval = babase.Lstr( 419 value='+${A} ${B}', 420 subs=[('${A}', str(points)), ('${B}', title)], 421 ) 422 else: 423 sval = babase.Lstr( 424 value='+${A}', subs=[('${A}', str(points))] 425 ) 426 PopupText( 427 sval, 428 color=display_color, 429 scale=1.2 * scale, 430 position=display_pos, 431 ).autoretain() 432 433 # Tally kills. 434 if kill: 435 s_player.accum_kill_count += 1 436 s_player.kill_count += 1 437 438 # Report non-kill scorings. 439 try: 440 if screenmessage and not kill: 441 _bascenev1.broadcastmessage( 442 babase.Lstr( 443 resource='nameScoresText', subs=[('${NAME}', name)] 444 ), 445 top=True, 446 color=player.color, 447 image=player.get_icon(), 448 ) 449 except Exception: 450 logging.exception('Error announcing score.') 451 452 s_player.score += points 453 s_player.accumscore += points 454 455 # Inform a running game of the score. 456 if points != 0: 457 activity = self._activity() if self._activity is not None else None 458 if activity is not None: 459 activity.handlemessage(PlayerScoredMessage(score=points)) 460 461 return points
Register a score for the player.
Return value is actual score with multipliers and such factored in.
463 def player_was_killed( 464 self, 465 player: bascenev1.Player, 466 killed: bool = False, 467 killer: bascenev1.Player | None = None, 468 ) -> None: 469 """Should be called when a player is killed.""" 470 name = player.getname() 471 prec = self._player_records[name] 472 prec.streak = 0 473 if killed: 474 prec.accum_killed_count += 1 475 prec.killed_count += 1 476 try: 477 if killed and _bascenev1.getactivity().announce_player_deaths: 478 if killer is player: 479 _bascenev1.broadcastmessage( 480 babase.Lstr( 481 resource='nameSuicideText', subs=[('${NAME}', name)] 482 ), 483 top=True, 484 color=player.color, 485 image=player.get_icon(), 486 ) 487 elif killer is not None: 488 if killer.team is player.team: 489 _bascenev1.broadcastmessage( 490 babase.Lstr( 491 resource='nameBetrayedText', 492 subs=[ 493 ('${NAME}', killer.getname()), 494 ('${VICTIM}', name), 495 ], 496 ), 497 top=True, 498 color=killer.color, 499 image=killer.get_icon(), 500 ) 501 else: 502 _bascenev1.broadcastmessage( 503 babase.Lstr( 504 resource='nameKilledText', 505 subs=[ 506 ('${NAME}', killer.getname()), 507 ('${VICTIM}', name), 508 ], 509 ), 510 top=True, 511 color=killer.color, 512 image=killer.get_icon(), 513 ) 514 else: 515 _bascenev1.broadcastmessage( 516 babase.Lstr( 517 resource='nameDiedText', subs=[('${NAME}', name)] 518 ), 519 top=True, 520 color=player.color, 521 image=player.get_icon(), 522 ) 523 except Exception: 524 logging.exception('Error announcing kill.')
Should be called when a player is killed.
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 = 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 Activity.
Category: Gameplay Classes
These correspond to 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 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 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 Player has their own 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 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)
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).
Inherited Members
- GameActivity
- tips
- name
- description
- available_settings
- scoreconfig
- allow_pausing
- allow_kick_idle_players
- show_kill_points
- default_music
- create_settings_ui
- getscoreconfig
- getname
- get_display_string
- get_team_display_string
- get_description
- get_description_display_string
- get_available_settings
- get_supported_maps
- get_settings_display_string
- initialplayerinfos
- map
- get_instance_display_string
- get_instance_scoreboard_display_string
- get_instance_description
- get_instance_description_short
- on_continue
- is_waiting_for_continue
- continue_or_end_game
- on_player_join
- handlemessage
- end_game
- respawn_player
- spawn_player_if_exists
- spawn_player
- setup_standard_powerup_drops
- setup_standard_time_limit
- show_zoom_message
- Activity
- settings_raw
- teams
- players
- announce_player_deaths
- is_joining_activity
- use_fixed_vr_overlay
- slow_motion
- inherits_slow_motion
- inherits_music
- inherits_vr_camera_offset
- inherits_vr_overlay_center
- inherits_tint
- allow_mid_activity_joins
- transition_time
- can_show_ad_on_death
- paused_text
- preloads
- lobby
- context
- globalsnode
- stats
- on_expire
- customdata
- expired
- playertype
- teamtype
- retain_actor
- add_actor_weak_ref
- session
- on_player_leave
- on_team_join
- on_team_leave
- on_transition_out
- has_transitioned_in
- has_begun
- has_ended
- is_transitioning_out
- transition_out
- create_player
- create_team
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 Activity or 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 Timer class instead.
Scene time maps to local simulation time in Activity or 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 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 Activity or 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 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 Lstr for displaying a time value.
Category: General Utility Functions
Given a time value, returns a 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 LARGE = 0 85 MEDIUM = 1 86 SMALL = 2
The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.
Category: Enums
'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.
'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.
'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.
Inherited Members
- enum.Enum
- name
- value
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.
499 def normalized(self) -> Vec3: 500 """Returns a normalized version of the vector.""" 501 return Vec3()
Returns a normalized version of the vector.
Inherited Members
- collections.abc.Sequence
- index
- count