Cleaned up Actor class to behave better in type-checking world

This commit is contained in:
Eric Froemling 2020-04-07 17:16:25 -07:00
parent f71404e7f7
commit ff043c0262
32 changed files with 263 additions and 202 deletions

View File

@ -78,7 +78,8 @@ from ba._messages import (OutOfBoundsMessage, DeathType, DieMessage,
StandMessage, PickUpMessage, DropMessage, StandMessage, PickUpMessage, DropMessage,
PickedUpMessage, DroppedMessage, PickedUpMessage, DroppedMessage,
ShouldShatterMessage, ImpactDamageMessage, ShouldShatterMessage, ImpactDamageMessage,
FreezeMessage, ThawMessage, HitMessage) FreezeMessage, ThawMessage, HitMessage,
CelebrateMessage)
from ba._music import setmusic, MusicPlayer, MusicType, MusicPlayMode from ba._music import setmusic, MusicPlayer, MusicType, MusicPlayMode
from ba._powerup import PowerupMessage, PowerupAcceptMessage from ba._powerup import PowerupMessage, PowerupAcceptMessage
from ba._teambasesession import TeamBaseSession from ba._teambasesession import TeamBaseSession

View File

@ -25,6 +25,9 @@ from __future__ import annotations
import weakref import weakref
from typing import TYPE_CHECKING, TypeVar from typing import TYPE_CHECKING, TypeVar
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage
from ba import _error
import _ba import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
@ -83,9 +86,11 @@ class Actor:
def __init__(self) -> None: def __init__(self) -> None:
"""Instantiates an Actor in the current ba.Activity.""" """Instantiates an Actor in the current ba.Activity."""
# FIXME: Actor should not be require to have a 'node' attr. # FIXME: Actor should not be assumed to have a 'node' attr.
self.node: Optional[ba.Node] = None # self.node: Optional[ba.Node] = None
if __debug__:
self._root_actor_init_called = True
activity = _ba.getactivity() activity = _ba.getactivity()
self._activity = weakref.ref(activity) self._activity = weakref.ref(activity)
activity.add_actor_weak_ref(self) activity.add_actor_weak_ref(self)
@ -96,27 +101,20 @@ class Actor:
# That way we can treat DieMessage handling as the single # That way we can treat DieMessage handling as the single
# point-of-action for death. # point-of-action for death.
if not self.is_expired(): if not self.is_expired():
from ba import _messages self.handlemessage(DieMessage())
self.handlemessage(_messages.DieMessage())
except Exception: except Exception:
from ba import _error
_error.print_exception('exception in ba.Actor.__del__() for', self) _error.print_exception('exception in ba.Actor.__del__() for', self)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object. """General message handling; can be passed any message object."""
if __debug__:
self._handlemessage_sanity_check()
The default implementation will handle ba.DieMessages by # By default, actors going out-of-bounds simply kill themselves.
calling self.node.delete() if self contains a 'node' attribute. if isinstance(msg, OutOfBoundsMessage):
""" return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
from ba._error import UNHANDLED
del msg # Unused.
return UNHANDLED
def _handlemessage_sanity_check(self) -> None: return _error.UNHANDLED
if self.is_expired():
from ba import _error
_error.print_error(
f'handlemessage called on expired actor: {self}')
def autoretain(self: T) -> T: def autoretain(self: T) -> T:
"""Keep this Actor alive without needing to hold a reference to it. """Keep this Actor alive without needing to hold a reference to it.
@ -131,8 +129,7 @@ class Actor:
""" """
activity = self._activity() activity = self._activity()
if activity is None: if activity is None:
from ba._error import ActivityNotFoundError raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
activity.retain_actor(self) activity.retain_actor(self)
return self return self
@ -192,6 +189,21 @@ class Actor:
""" """
return True return True
def _handlemessage_sanity_check(self) -> None:
"""Make sure things are kosher in handlemessage().
Place this in an 'if __debug__:' clause at the top of handlemessage()
overrides. This will will complain if anything is sending the Actor
messages after the activity has ended, which should be explicitly
avoided.
"""
assert __debug__, "This should only be called in __debug__ mode."
if not getattr(self, '_root_actor_init_called', False):
_error.print_error('Root Actor __init__() not called.')
if self.is_expired():
_error.print_error(
f'handlemessage() called on expired actor: {self}')
@property @property
def activity(self) -> ba.Activity: def activity(self) -> ba.Activity:
"""The Activity this Actor was created in. """The Activity this Actor was created in.
@ -200,8 +212,7 @@ class Actor:
""" """
activity = self._activity() activity = self._activity()
if activity is None: if activity is None:
from ba._error import ActivityNotFoundError raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
return activity return activity
def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]: def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]:
@ -212,6 +223,5 @@ class Actor:
""" """
activity = self._activity() activity = self._activity()
if activity is None and doraise: if activity is None and doraise:
from ba._error import ActivityNotFoundError raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
return activity return activity

View File

@ -188,7 +188,6 @@ def print_live_object_warnings(when: Any,
ignore_session: ba.Session = None, ignore_session: ba.Session = None,
ignore_activity: ba.Activity = None) -> None: ignore_activity: ba.Activity = None) -> None:
"""Print warnings for remaining objects in the current context.""" """Print warnings for remaining objects in the current context."""
# pylint: disable=too-many-branches
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
import gc import gc
from ba import _session as bs_session from ba import _session as bs_session
@ -252,13 +251,14 @@ def print_live_object_warnings(when: Any,
for actor in actors: for actor in actors:
_ba.app.printed_live_object_warning = True _ba.app.printed_live_object_warning = True
print('ERROR: Actor found', when, ':', actor) print('ERROR: Actor found', when, ':', actor)
if isinstance(actor, bs_actor.Actor): # if isinstance(actor, bs_actor.Actor):
try: # try:
if actor.node: # if actor.node:
print(' - contains node:', actor.node.getnodetype(), ';', # print(' - contains node:',
actor.node.get_name()) # actor.node.getnodetype(), ';',
except Exception as exc: # actor.node.get_name())
print(' - exception checking actor node:', exc) # except Exception as exc:
# print(' - exception checking actor node:', exc)
# refs = list(gc.get_referrers(actor)) # refs = list(gc.get_referrers(actor))
# i = 1 # i = 1
# for ref in refs: # for ref in refs:

View File

@ -135,10 +135,10 @@ class CoopGameActivity(GameActivity):
a wave. a wave.
duration is given in seconds. duration is given in seconds.
""" """
from ba._messages import CelebrateMessage
for player in self.players: for player in self.players:
if player.actor is not None and player.actor.node: if player.actor:
player.actor.node.handlemessage('celebrate', player.actor.handlemessage(CelebrateMessage(duration))
int(duration * 1000))
def _preload_achievements(self) -> None: def _preload_achievements(self) -> None:
from ba import _achievement from ba import _achievement

View File

@ -315,7 +315,6 @@ class AssetPackage(DependencyComponent):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
# pylint: disable=no-member
# This is used internally by the get_package_xxx calls. # This is used internally by the get_package_xxx calls.
self.context = _ba.Context('current') self.context = _ba.Context('current')

View File

@ -337,13 +337,13 @@ class GameActivity(Activity):
self._game_scoreboard_description_text: Optional[ba.Actor] = None self._game_scoreboard_description_text: Optional[ba.Actor] = None
self._standard_time_limit_time: Optional[int] = None self._standard_time_limit_time: Optional[int] = None
self._standard_time_limit_timer: Optional[ba.Timer] = None self._standard_time_limit_timer: Optional[ba.Timer] = None
self._standard_time_limit_text: Optional[ba.Actor] = None self._standard_time_limit_text: Optional[ba.NodeActor] = None
self._standard_time_limit_text_input: Optional[ba.Actor] = None self._standard_time_limit_text_input: Optional[ba.NodeActor] = None
self._tournament_time_limit: Optional[int] = None self._tournament_time_limit: Optional[int] = None
self._tournament_time_limit_timer: Optional[ba.Timer] = None self._tournament_time_limit_timer: Optional[ba.Timer] = None
self._tournament_time_limit_title_text: Optional[ba.Actor] = None self._tournament_time_limit_title_text: Optional[ba.NodeActor] = None
self._tournament_time_limit_text: Optional[ba.Actor] = None self._tournament_time_limit_text: Optional[ba.NodeActor] = None
self._tournament_time_limit_text_input: Optional[ba.Actor] = None self._tournament_time_limit_text_input: Optional[ba.NodeActor] = None
self._zoom_message_times: Dict[int, float] = {} self._zoom_message_times: Dict[int, float] = {}
self._is_waiting_for_continue = False self._is_waiting_for_continue = False
@ -1042,17 +1042,18 @@ class GameActivity(Activity):
from ba import _messages from ba import _messages
from ba._gameutils import animate from ba._gameutils import animate
from ba._coopsession import CoopSession from ba._coopsession import CoopSession
from bastd.actor import playerspaz from bastd.actor.playerspaz import PlayerSpaz
name = player.get_name() name = player.get_name()
color = player.color color = player.color
highlight = player.highlight highlight = player.highlight
light_color = _math.normalized_color(color) light_color = _math.normalized_color(color)
display_color = _ba.safecolor(color, target_intensity=0.75) display_color = _ba.safecolor(color, target_intensity=0.75)
spaz = playerspaz.PlayerSpaz(color=color, spaz = PlayerSpaz(color=color,
highlight=highlight, highlight=highlight,
character=player.character, character=player.character,
player=player) player=player)
player.set_actor(spaz) player.set_actor(spaz)
assert spaz.node assert spaz.node

View File

@ -211,7 +211,7 @@ class Map(Actor):
# This is expected to always be a ba.Node object (whether valid or not) # This is expected to always be a ba.Node object (whether valid or not)
# should be set to something meaningful by child classes. # should be set to something meaningful by child classes.
self.node = None self.node: Optional[_ba.Node] = None
# Make our class' preload-data available to us # Make our class' preload-data available to us
# (and instruct the user if we weren't preloaded properly). # (and instruct the user if we weren't preloaded properly).
@ -365,9 +365,8 @@ class Map(Actor):
player_pts = [] player_pts = []
for player in players: for player in players:
try: try:
if player.actor is not None and player.actor.is_alive(): if player.node:
assert player.actor.node pnt = _ba.Vec3(player.node.position)
pnt = _ba.Vec3(player.actor.node.position)
player_pts.append(pnt) player_pts.append(pnt)
except Exception as exc: except Exception as exc:
print('EXC in get_ffa_start_position:', exc) print('EXC in get_ffa_start_position:', exc)
@ -412,12 +411,17 @@ class Map(Actor):
return self.flag_points_default[:3] return self.flag_points_default[:3]
return self.flag_points[team_index % len(self.flag_points)][:3] return self.flag_points[team_index % len(self.flag_points)][:3]
def exists(self) -> bool:
return bool(self.node)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
from ba import _messages from ba import _messages
if isinstance(msg, _messages.DieMessage): if isinstance(msg, _messages.DieMessage):
if self.node: if self.node:
self.node.delete() self.node.delete()
super().handlemessage(msg) else:
return super().handlemessage(msg)
return None
def register_map(maptype: Type[Map]) -> None: def register_map(maptype: Type[Map]) -> None:

View File

@ -47,6 +47,7 @@ class DeathType(Enum):
Category: Enums Category: Enums
""" """
GENERIC = 'generic' GENERIC = 'generic'
OUT_OF_BOUNDS = 'out_of_bounds'
IMPACT = 'impact' IMPACT = 'impact'
FALL = 'fall' FALL = 'fall'
REACHED_GOAL = 'reached_goal' REACHED_GOAL = 'reached_goal'
@ -187,6 +188,20 @@ class ThawMessage:
""" """
@dataclass
class CelebrateMessage:
"""Tells an object to celebrate.
Category: Message Classes
Attributes:
duration
Amount of time to celebrate in seconds.
"""
duration: float = 10.0
@dataclass(init=False) @dataclass(init=False)
class HitMessage: class HitMessage:
"""Tells an object it has been hit in some way. """Tells an object it has been hit in some way.

View File

@ -51,3 +51,6 @@ class NodeActor(Actor):
self.node.delete() self.node.delete()
return None return None
return super().handlemessage(msg) return super().handlemessage(msg)
def exists(self) -> bool:
return bool(self.node)

View File

@ -98,21 +98,6 @@ class Team:
"""Returns the numeric team ID.""" """Returns the numeric team ID."""
return self._team_id return self._team_id
def celebrate(self, duration: float = 10.0) -> None:
"""Tells all players on the team to celebrate.
duration is given in seconds.
"""
for player in self.players:
try:
if player.actor is not None and player.actor.node:
# Internal node-message is in milliseconds.
player.actor.node.handlemessage('celebrate',
int(duration * 1000))
except Exception:
from ba import _error
_error.print_exception('Error on celebrate')
def reset(self) -> None: def reset(self) -> None:
"""(internal)""" """(internal)"""
self.reset_gamedata() self.reset_gamedata()

View File

@ -254,22 +254,23 @@ class TeamBaseSession(Session):
announcement of the same. announcement of the same.
""" """
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
# pylint: disable=too-many-locals
from ba import _math from ba import _math
from ba import _general from ba import _general
from ba._gameutils import cameraflash from ba._gameutils import cameraflash
from ba import _lang from ba import _lang
from ba._freeforallsession import FreeForAllSession from ba._freeforallsession import FreeForAllSession
from ba._messages import CelebrateMessage
_ba.timer(delay, _ba.timer(delay,
_general.Call(_ba.playsound, _ba.getsound("boxingBell"))) _general.Call(_ba.playsound, _ba.getsound("boxingBell")))
if announce_winning_team: if announce_winning_team:
winning_team = results.get_winning_team() winning_team = results.get_winning_team()
if winning_team is not None: if winning_team is not None:
# Have all players celebrate. # Have all players celebrate.
celebrate_msg = CelebrateMessage(duration=10.0)
for player in winning_team.players: for player in winning_team.players:
if player.actor is not None and player.actor.node: if player.actor:
# Note: celebrate message takes milliseconds player.actor.handlemessage(celebrate_msg)
# for historical reasons.
player.actor.node.handlemessage('celebrate', 10000)
cameraflash() cameraflash()
# Some languages say "FOO WINS" different for teams vs players. # Some languages say "FOO WINS" different for teams vs players.

View File

@ -25,7 +25,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
from bastd.actor import spaz as basespaz from bastd.actor.spaz import Spaz
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Optional, Sequence, Tuple from typing import Any, Optional, Sequence, Tuple
@ -77,7 +77,7 @@ class PlayerSpazHurtMessage:
self.spaz = spaz self.spaz = spaz
class PlayerSpaz(basespaz.Spaz): class PlayerSpaz(Spaz):
"""A ba.Spaz subclass meant to be controlled by a ba.Player. """A ba.Spaz subclass meant to be controlled by a ba.Player.
category: Gameplay Classes category: Gameplay Classes
@ -102,13 +102,12 @@ class PlayerSpaz(basespaz.Spaz):
you must call connect_controls_to_player() to do so. you must call connect_controls_to_player() to do so.
""" """
basespaz.Spaz.__init__(self, super().__init__(color=color,
color=color, highlight=highlight,
highlight=highlight, character=character,
character=character, source_player=player,
source_player=player, start_invincible=True,
start_invincible=True, powerups_expire=powerups_expire)
powerups_expire=powerups_expire)
self.last_player_attacked_by: Optional[ba.Player] = None self.last_player_attacked_by: Optional[ba.Player] = None
self.last_attacked_time = 0.0 self.last_attacked_time = 0.0
self.last_attacked_type: Optional[Tuple[str, str]] = None self.last_attacked_type: Optional[Tuple[str, str]] = None

View File

@ -155,7 +155,7 @@ class Spaz(ba.Actor):
media = factory.get_media(character) media = factory.get_media(character)
punchmats = (factory.punch_material, ba.sharedobj('attack_material')) punchmats = (factory.punch_material, ba.sharedobj('attack_material'))
pickupmats = (factory.pickup_material, ba.sharedobj('pickup_material')) pickupmats = (factory.pickup_material, ba.sharedobj('pickup_material'))
self.node = ba.newnode( self.node: ba.Node = ba.newnode(
type="spaz", type="spaz",
delegate=self, delegate=self,
attrs={ attrs={
@ -256,10 +256,13 @@ class Spaz(ba.Actor):
self.punch_callback: Optional[Callable[[Spaz], Any]] = None self.punch_callback: Optional[Callable[[Spaz], Any]] = None
self.pick_up_powerup_callback: Optional[Callable[[Spaz], Any]] = None self.pick_up_powerup_callback: Optional[Callable[[Spaz], Any]] = None
def exists(self) -> bool:
return bool(self.node)
def on_expire(self) -> None: def on_expire(self) -> None:
super().on_expire() super().on_expire()
# release callbacks/refs so we don't wind up with dependency loops.. # Release callbacks/refs so we don't wind up with dependency loops.
self._dropped_bomb_callbacks = [] self._dropped_bomb_callbacks = []
self.punch_callback = None self.punch_callback = None
self.pick_up_powerup_callback = None self.pick_up_powerup_callback = None
@ -296,8 +299,8 @@ class Spaz(ba.Actor):
assert isinstance(t_ms, int) assert isinstance(t_ms, int)
t_bucket = int(t_ms / 1000) t_bucket = int(t_ms / 1000)
if t_bucket == self._turbo_filter_time_bucket: if t_bucket == self._turbo_filter_time_bucket:
# add only once per timestep (filter out buttons triggering # Add only once per timestep (filter out buttons triggering
# multiple actions) # multiple actions).
if t_ms != self._turbo_filter_times.get(source, 0): if t_ms != self._turbo_filter_times.get(source, 0):
self._turbo_filter_counts[source] = ( self._turbo_filter_counts[source] = (
self._turbo_filter_counts.get(source, 0) + 1) self._turbo_filter_counts.get(source, 0) + 1)
@ -306,11 +309,11 @@ class Spaz(ba.Actor):
# ba.screenmessage( str(source) + " " # ba.screenmessage( str(source) + " "
# + str(self._turbo_filter_counts[source])) # + str(self._turbo_filter_counts[source]))
if self._turbo_filter_counts[source] == 15: if self._turbo_filter_counts[source] == 15:
# knock 'em out. That'll learn 'em. # Knock 'em out. That'll learn 'em.
assert self.node assert self.node
self.node.handlemessage("knockout", 500.0) self.node.handlemessage("knockout", 500.0)
# also issue periodic notices about who is turbo-ing # Also issue periodic notices about who is turbo-ing.
now = ba.time(ba.TimeType.REAL) now = ba.time(ba.TimeType.REAL)
if now > ba.app.last_spaz_turbo_warn_time + 30.0: if now > ba.app.last_spaz_turbo_warn_time + 30.0:
ba.app.last_spaz_turbo_warn_time = now ba.app.last_spaz_turbo_warn_time = now
@ -1226,6 +1229,10 @@ class Spaz(ba.Actor):
# hold_body needs to be set before hold_node. # hold_body needs to be set before hold_node.
self.node.hold_body = opposing_body self.node.hold_body = opposing_body
self.node.hold_node = opposing_node self.node.hold_node = opposing_node
elif isinstance(msg, ba.CelebrateMessage):
if self.node:
self.node.handlemessage('celebrate', int(msg.duration * 1000))
else: else:
return super().handlemessage(msg) return super().handlemessage(msg)
return None return None

View File

@ -190,7 +190,7 @@ class SpazBot(basespaz.Spaz):
closest_dist = None closest_dist = None
closest_vel = None closest_vel = None
closest = None closest = None
assert self._player_pts assert self._player_pts is not None
for plpt, plvel in self._player_pts: for plpt, plvel in self._player_pts:
dist = (plpt - botpt).length() dist = (plpt - botpt).length()
@ -983,6 +983,7 @@ class BotSet:
for player in ba.getactivity().players: for player in ba.getactivity().players:
try: try:
if player.is_alive(): if player.is_alive():
assert isinstance(player.actor, basespaz.Spaz)
assert player.actor is not None and player.actor.node assert player.actor is not None and player.actor.node
player_pts.append((ba.Vec3(player.actor.node.position), player_pts.append((ba.Vec3(player.actor.node.position),
ba.Vec3(player.actor.node.velocity))) ba.Vec3(player.actor.node.velocity)))
@ -1006,16 +1007,6 @@ class BotSet:
bot.handlemessage(ba.DieMessage(immediate=True)) bot.handlemessage(ba.DieMessage(immediate=True))
self._bot_lists[i] = [] self._bot_lists[i] = []
def celebrate(self, duration: float) -> None:
"""Tell all living bots in the set to celebrate momentarily.
Duration is given in seconds.
"""
for botlist in self._bot_lists:
for bot in botlist:
if bot.node:
bot.node.handlemessage('celebrate', int(duration * 1000))
def start_moving(self) -> None: def start_moving(self) -> None:
"""Start processing bot AI updates so they start doing their thing.""" """Start processing bot AI updates so they start doing their thing."""
self._bot_update_timer = ba.Timer(0.05, self._bot_update_timer = ba.Timer(0.05,
@ -1035,6 +1026,17 @@ class BotSet:
bot.node.move_left_right = 0 bot.node.move_left_right = 0
bot.node.move_up_down = 0 bot.node.move_up_down = 0
def celebrate(self, duration: float) -> None:
"""Tell all living bots in the set to celebrate momentarily.
Duration is given in seconds.
"""
msg = ba.CelebrateMessage(duration=duration)
for botlist in self._bot_lists:
for bot in botlist:
if bot:
bot.handlemessage(msg)
def final_celebrate(self) -> None: def final_celebrate(self) -> None:
"""Tell all bots in the set to stop what they were doing and celebrate. """Tell all bots in the set to stop what they were doing and celebrate.
@ -1045,12 +1047,12 @@ class BotSet:
# At this point stop doing anything but jumping and celebrating. # At this point stop doing anything but jumping and celebrating.
for botlist in self._bot_lists: for botlist in self._bot_lists:
for bot in botlist: for bot in botlist:
if bot.node: if bot:
assert bot.node # (should exist if 'if bot' was True)
bot.node.move_left_right = 0 bot.node.move_left_right = 0
bot.node.move_up_down = 0 bot.node.move_up_down = 0
ba.timer( ba.timer(0.5 * random.random(),
0.5 * random.random(), ba.Call(bot.handlemessage, ba.CelebrateMessage()))
ba.Call(bot.node.handlemessage, 'celebrate', 10.0))
jump_duration = random.randrange(400, 500) jump_duration = random.randrange(400, 500)
j = random.randrange(0, 200) j = random.randrange(0, 200)
for _i in range(10): for _i in range(10):

View File

@ -225,12 +225,8 @@ class AssaultGame(ba.TeamGameActivity):
# Have teammates celebrate. # Have teammates celebrate.
for player in player_team.players: for player in player_team.players:
try: if player.actor:
# Note: celebrate message is milliseconds player.actor.handlemessage(ba.CelebrateMessage(2.0))
# for historical reasons.
player.actor.node.handlemessage('celebrate', 2000)
except Exception:
pass
player_team.gamedata['score'] += 1 player_team.gamedata['score'] += 1
self._update_scoreboard() self._update_scoreboard()

View File

@ -306,10 +306,8 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
# Have teammates celebrate # Have teammates celebrate
for player in team.players: for player in team.players:
if player.actor is not None and player.actor.node: if player.actor:
# Note: celebrate message is milliseconds player.actor.handlemessage(ba.CelebrateMessage(2.0))
# for historical reasons.
player.actor.node.handlemessage('celebrate', 2000)
# Reset all flags/state. # Reset all flags/state.
for reset_team in self.teams: for reset_team in self.teams:

View File

@ -279,7 +279,7 @@ class ChosenOneGame(ba.TeamGameActivity):
self._flag = None self._flag = None
self._chosen_one_player = player self._chosen_one_player = player
if player.actor.node: if player.actor:
if self.settings['Chosen One Gets Shield']: if self.settings['Chosen One Gets Shield']:
player.actor.handlemessage( player.actor.handlemessage(
ba.PowerupMessage('shield')) ba.PowerupMessage('shield'))
@ -311,8 +311,10 @@ class ChosenOneGame(ba.TeamGameActivity):
0.4: 1.0 0.4: 1.0
}, },
loop=True) loop=True)
assert isinstance(player.actor, playerspaz.PlayerSpaz)
player.actor.node.connectattr('position', light.node, player.actor.node.connectattr('position', light.node,
'position') 'position')
except Exception: except Exception:
ba.print_exception('EXC in _set_chosen_one_player') ba.print_exception('EXC in _set_chosen_one_player')

View File

@ -143,7 +143,9 @@ class EasterEggHuntGame(ba.TeamGameActivity):
'source_node', 'opposing_node') 'source_node', 'opposing_node')
if egg_node is not None and playernode is not None: if egg_node is not None and playernode is not None:
egg = egg_node.getdelegate() egg = egg_node.getdelegate()
assert isinstance(egg, Egg)
spaz = playernode.getdelegate() spaz = playernode.getdelegate()
assert isinstance(spaz, playerspaz.PlayerSpaz)
player = (spaz.getplayer() player = (spaz.getplayer()
if hasattr(spaz, 'getplayer') else None) if hasattr(spaz, 'getplayer') else None)
if player and egg: if player and egg:
@ -187,13 +189,8 @@ class EasterEggHuntGame(ba.TeamGameActivity):
ypos = random.uniform(3.5, 3.5) ypos = random.uniform(3.5, 3.5)
zpos = random.uniform(-8.2, 3.7) zpos = random.uniform(-8.2, 3.7)
def _is_exists(egg: Egg) -> bool:
if egg.node is None:
return False
return egg.node.exists()
# Prune dead eggs from our list. # Prune dead eggs from our list.
self._eggs = [e for e in self._eggs if _is_exists(e)] self._eggs = [e for e in self._eggs if e]
# Spawn more eggs if we've got space. # Spawn more eggs if we've got space.
if len(self._eggs) < int(self._max_eggs): if len(self._eggs) < int(self._max_eggs):
@ -283,12 +280,13 @@ class Egg(ba.Actor):
'materials': mats 'materials': mats
}) })
def exists(self) -> bool:
return bool(self.node)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage): if isinstance(msg, ba.DieMessage):
if self.node: if self.node:
self.node.delete() self.node.delete()
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.HitMessage): elif isinstance(msg, ba.HitMessage):
if self.node: if self.node:
assert msg.force_direction is not None assert msg.force_direction is not None

View File

@ -386,8 +386,8 @@ class EliminationGame(ba.TeamGameActivity):
for team in self.teams: for team in self.teams:
for tplayer in team.players: for tplayer in team.players:
if tplayer.is_alive(): if tplayer.is_alive():
assert tplayer.actor is not None and tplayer.actor.node assert tplayer.node
ppos = tplayer.actor.node.position ppos = tplayer.node.position
living_player = tplayer living_player = tplayer
living_player_pos = ppos living_player_pos = ppos
break break
@ -416,20 +416,16 @@ class EliminationGame(ba.TeamGameActivity):
def _print_lives(self, player: ba.Player) -> None: def _print_lives(self, player: ba.Player) -> None:
from bastd.actor import popuptext from bastd.actor import popuptext
if not player or not player.is_alive(): assert player # Shouldn't be passing invalid refs around.
return if not player or not player.is_alive() or not player.node:
try:
assert player.actor is not None and player.actor.node
pos = player.actor.node.position
except Exception as exc:
print('EXC getting player pos in bs_elim', exc)
return return
popuptext.PopupText('x' + str(player.gamedata['lives'] - 1), popuptext.PopupText('x' + str(player.gamedata['lives'] - 1),
color=(1, 1, 0, 1), color=(1, 1, 0, 1),
offset=(0, -0.8, 0), offset=(0, -0.8, 0),
random_offset=0.0, random_offset=0.0,
scale=1.8, scale=1.8,
position=pos).autoretain() position=player.node.position).autoretain()
def on_player_leave(self, player: ba.Player) -> None: def on_player_leave(self, player: ba.Player) -> None:
ba.TeamGameActivity.on_player_leave(self, player) ba.TeamGameActivity.on_player_leave(self, player)

View File

@ -128,10 +128,10 @@ class FootballTeamGame(ba.TeamGameActivity):
True), ("modify_part_collision", "physical", False), True), ("modify_part_collision", "physical", False),
("call", "at_connect", self._handle_score))) ("call", "at_connect", self._handle_score)))
self._flag_spawn_pos: Optional[Sequence[float]] = None self._flag_spawn_pos: Optional[Sequence[float]] = None
self._score_regions: List[ba.Actor] = [] self._score_regions: List[ba.NodeActor] = []
self._flag: Optional[FootballFlag] = None self._flag: Optional[FootballFlag] = None
self._flag_respawn_timer: Optional[ba.Timer] = None self._flag_respawn_timer: Optional[ba.Timer] = None
self._flag_respawn_light: Optional[ba.Actor] = None self._flag_respawn_light: Optional[ba.NodeActor] = None
def get_instance_description(self) -> Union[str, Sequence]: def get_instance_description(self) -> Union[str, Sequence]:
touchdowns = self.settings['Score to Win'] / 7 touchdowns = self.settings['Score to Win'] / 7
@ -203,13 +203,8 @@ class FootballTeamGame(ba.TeamGameActivity):
# Tell all players to celebrate. # Tell all players to celebrate.
for player in team.players: for player in team.players:
if player.actor is not None and player.actor.node: if player.actor:
try: player.actor.handlemessage(ba.CelebrateMessage(2.0))
# Note: celebrate message is milliseconds
# (for historical reasons).
player.actor.node.handlemessage('celebrate', 2000)
except Exception:
ba.print_exception('Error on celebrate')
# If someone on this team was last to touch it, # If someone on this team was last to touch it,
# give them points. # give them points.
@ -376,7 +371,7 @@ class FootballCoopGame(ba.CoopGameActivity):
self._player_has_punched = False self._player_has_punched = False
self._scoreboard: Optional[Scoreboard] = None self._scoreboard: Optional[Scoreboard] = None
self._flag_spawn_pos: Optional[Sequence[float]] = None self._flag_spawn_pos: Optional[Sequence[float]] = None
self.score_regions: List[ba.Actor] = [] self.score_regions: List[ba.NodeActor] = []
self._exclude_powerups: List[str] = [] self._exclude_powerups: List[str] = []
self._have_tnt = False self._have_tnt = False
self._bot_types_initial: Optional[List[Type[spazbot.SpazBot]]] = None self._bot_types_initial: Optional[List[Type[spazbot.SpazBot]]] = None
@ -384,8 +379,8 @@ class FootballCoopGame(ba.CoopGameActivity):
self._bot_types_14: Optional[List[Type[spazbot.SpazBot]]] = None self._bot_types_14: Optional[List[Type[spazbot.SpazBot]]] = None
self._bot_team: Optional[ba.Team] = None self._bot_team: Optional[ba.Team] = None
self._starttime_ms: Optional[int] = None self._starttime_ms: Optional[int] = None
self._time_text: Optional[ba.Actor] = None self._time_text: Optional[ba.NodeActor] = None
self._time_text_input: Optional[ba.Actor] = None self._time_text_input: Optional[ba.NodeActor] = None
self._tntspawner: Optional[stdbomb.TNTSpawner] = None self._tntspawner: Optional[stdbomb.TNTSpawner] = None
self._bots = spazbot.BotSet() self._bots = spazbot.BotSet()
self._bot_spawn_timer: Optional[ba.Timer] = None self._bot_spawn_timer: Optional[ba.Timer] = None
@ -580,13 +575,11 @@ class FootballCoopGame(ba.CoopGameActivity):
assert self._flag is not None assert self._flag is not None
if self._flag.node: if self._flag.node:
for player in self.players: for player in self.players:
try: if player.actor:
assert player.actor is not None and player.actor.node assert isinstance(player.actor, playerspaz.PlayerSpaz)
if (player.actor.is_alive() and if (player.actor.is_alive() and
player.actor.node.hold_node == self._flag.node): player.actor.node.hold_node == self._flag.node):
return return
except Exception:
ba.print_exception("exception checking hold node")
flagpos = ba.Vec3(self._flag.node.position) flagpos = ba.Vec3(self._flag.node.position)
closest_bot = None closest_bot = None
@ -651,7 +644,6 @@ class FootballCoopGame(ba.CoopGameActivity):
""" a point has been scored """ """ a point has been scored """
# FIXME tidy this up # FIXME tidy this up
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-nested-blocks
# Our flag might stick around for a second or two; # Our flag might stick around for a second or two;
# we don't want it to be able to score again. # we don't want it to be able to score again.
@ -674,16 +666,11 @@ class FootballCoopGame(ba.CoopGameActivity):
# Tell all players (or bots) to celebrate. # Tell all players (or bots) to celebrate.
if i == 0: if i == 0:
for player in team.players: for player in team.players:
try: if player.actor:
# Note: celebrate message is milliseconds player.actor.handlemessage(
# (for historical reasons). ba.CelebrateMessage(2.0))
if player.actor is not None and player.actor.node:
player.actor.node.handlemessage(
'celebrate', 2000)
except Exception:
ba.print_exception()
else: else:
self._bots.celebrate(2000) self._bots.celebrate(2.0)
# If the good guys scored, add more enemies. # If the good guys scored, add more enemies.
if i == 0: if i == 0:

View File

@ -193,7 +193,7 @@ class HockeyGame(ba.TeamGameActivity):
True), ("modify_part_collision", "physical", False), True), ("modify_part_collision", "physical", False),
("call", "at_connect", self._handle_score))) ("call", "at_connect", self._handle_score)))
self._puck_spawn_pos: Optional[Sequence[float]] = None self._puck_spawn_pos: Optional[Sequence[float]] = None
self._score_regions: Optional[List[ba.Actor]] = None self._score_regions: Optional[List[ba.NodeActor]] = None
self._puck: Optional[Puck] = None self._puck: Optional[Puck] = None
def get_instance_description(self) -> Union[str, Sequence]: def get_instance_description(self) -> Union[str, Sequence]:
@ -284,10 +284,8 @@ class HockeyGame(ba.TeamGameActivity):
# Tell all players to celebrate. # Tell all players to celebrate.
for player in team.players: for player in team.players:
if player.actor is not None and player.actor.node: if player.actor:
# Note: celebrate message takes milliseconds player.actor.handlemessage(ba.CelebrateMessage(2.0))
# (for historical reasons).
player.actor.node.handlemessage('celebrate', 2000)
# If we've got the player from the scoring team that last # If we've got the player from the scoring team that last
# touched us, give them points. # touched us, give them points.

View File

@ -194,7 +194,7 @@ class KeepAwayGame(ba.TeamGameActivity):
for player in self.players: for player in self.players:
holding_flag = False holding_flag = False
try: try:
assert player.actor is not None assert isinstance(player.actor, playerspaz.PlayerSpaz)
if (player.actor.is_alive() and player.actor.node if (player.actor.is_alive() and player.actor.node
and player.actor.node.hold_node): and player.actor.node.hold_node):
holding_flag = ( holding_flag = (

View File

@ -188,7 +188,9 @@ class NinjaFightGame(ba.TeamGameActivity):
ba.cameraflash() ba.cameraflash()
ba.playsound(self._winsound) ba.playsound(self._winsound)
for team in self.teams: for team in self.teams:
team.celebrate() # Woooo! par-tay! for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage())
results.set_team_score(team, elapsed_time_ms) results.set_team_score(team, elapsed_time_ms)
# Ends the activity. # Ends the activity.

View File

@ -105,7 +105,7 @@ class OnslaughtGame(ba.CoopGameActivity):
self._can_end_wave = True self._can_end_wave = True
self._score = 0 self._score = 0
self._time_bonus = 0 self._time_bonus = 0
self._spawn_info_text: Optional[ba.Actor] = None self._spawn_info_text: Optional[ba.NodeActor] = None
self._dingsound = ba.getsound('dingSmall') self._dingsound = ba.getsound('dingSmall')
self._dingsoundhigh = ba.getsound('dingSmallHigh') self._dingsoundhigh = ba.getsound('dingSmallHigh')
self._have_tnt = False self._have_tnt = False
@ -115,9 +115,9 @@ class OnslaughtGame(ba.CoopGameActivity):
self._bots: Optional[spazbot.BotSet] = None self._bots: Optional[spazbot.BotSet] = None
self._powerup_drop_timer: Optional[ba.Timer] = None self._powerup_drop_timer: Optional[ba.Timer] = None
self._time_bonus_timer: Optional[ba.Timer] = None self._time_bonus_timer: Optional[ba.Timer] = None
self._time_bonus_text: Optional[ba.Actor] = None self._time_bonus_text: Optional[ba.NodeActor] = None
self._flawless_bonus: Optional[int] = None self._flawless_bonus: Optional[int] = None
self._wave_text: Optional[ba.Actor] = None self._wave_text: Optional[ba.NodeActor] = None
self._wave_update_timer: Optional[ba.Timer] = None self._wave_update_timer: Optional[ba.Timer] = None
self._throw_off_kills = 0 self._throw_off_kills = 0
self._land_mine_kills = 0 self._land_mine_kills = 0
@ -873,7 +873,7 @@ class OnslaughtGame(ba.CoopGameActivity):
if self._game_over: if self._game_over:
return return
# respawn applicable players # Respawn applicable players.
if self._wave > 1 and not self.is_waiting_for_continue(): if self._wave > 1 and not self.is_waiting_for_continue():
for player in self.players: for player in self.players:
if (not player.is_alive() if (not player.is_alive()

View File

@ -189,6 +189,7 @@ class RaceGame(ba.TeamGameActivity):
self._regions.append(RaceRegion(rpt, len(self._regions))) self._regions.append(RaceRegion(rpt, len(self._regions)))
def _flash_player(self, player: ba.Player, scale: float) -> None: def _flash_player(self, player: ba.Player, scale: float) -> None:
assert isinstance(player.actor, PlayerSpaz)
assert player.actor is not None and player.actor.node assert player.actor is not None and player.actor.node
pos = player.actor.node.position pos = player.actor.node.position
light = ba.newnode('light', light = ba.newnode('light',
@ -523,6 +524,7 @@ class RaceGame(ba.TeamGameActivity):
for player in self.players: for player in self.players:
pos: Optional[ba.Vec3] pos: Optional[ba.Vec3]
try: try:
assert isinstance(player.actor, PlayerSpaz)
assert player.actor is not None and player.actor.node assert player.actor is not None and player.actor.node
pos = ba.Vec3(player.actor.node.position) pos = ba.Vec3(player.actor.node.position)
except Exception: except Exception:

View File

@ -117,15 +117,15 @@ class RunaroundGame(ba.CoopGameActivity):
self._waves: Optional[List[Dict[str, Any]]] = None self._waves: Optional[List[Dict[str, Any]]] = None
self._bots = spazbot.BotSet() self._bots = spazbot.BotSet()
self._tntspawner: Optional[TNTSpawner] = None self._tntspawner: Optional[TNTSpawner] = None
self._lives_bg: Optional[ba.Actor] = None self._lives_bg: Optional[ba.NodeActor] = None
self._start_lives = 10 self._start_lives = 10
self._lives = self._start_lives self._lives = self._start_lives
self._lives_text: Optional[ba.Actor] = None self._lives_text: Optional[ba.NodeActor] = None
self._flawless = True self._flawless = True
self._time_bonus_timer: Optional[ba.Timer] = None self._time_bonus_timer: Optional[ba.Timer] = None
self._time_bonus_text: Optional[ba.Actor] = None self._time_bonus_text: Optional[ba.NodeActor] = None
self._time_bonus_mult: Optional[float] = None self._time_bonus_mult: Optional[float] = None
self._wave_text: Optional[ba.Actor] = None self._wave_text: Optional[ba.NodeActor] = None
self._flawless_bonus: Optional[int] = None self._flawless_bonus: Optional[int] = None
self._wave_update_timer: Optional[ba.Timer] = None self._wave_update_timer: Optional[ba.Timer] = None
@ -1050,10 +1050,12 @@ class RunaroundGame(ba.CoopGameActivity):
def _update_bot(self, bot: spazbot.SpazBot) -> bool: def _update_bot(self, bot: spazbot.SpazBot) -> bool:
# Yup; that's a lot of return statements right there. # Yup; that's a lot of return statements right there.
# pylint: disable=too-many-return-statements # pylint: disable=too-many-return-statements
assert bot.node
if not bot.is_alive() or not bot.node.exists(): if not bool(bot):
return True return True
assert bot.node
# FIXME: Do this in a type safe way. # FIXME: Do this in a type safe way.
r_walk_speed: float = bot.r_walk_speed # type: ignore r_walk_speed: float = bot.r_walk_speed # type: ignore
r_walk_row: int = bot.r_walk_row # type: ignore r_walk_row: int = bot.r_walk_row # type: ignore

View File

@ -197,6 +197,7 @@ class TheLastStandGame(ba.CoopGameActivity):
for player in self.players: for player in self.players:
try: try:
if player.is_alive(): if player.is_alive():
assert isinstance(player.actor, playerspaz.PlayerSpaz)
assert player.actor is not None and player.actor.node assert player.actor is not None and player.actor.node
playerpts.append(player.actor.node.position) playerpts.append(player.actor.node.position)
except Exception as exc: except Exception as exc:

View File

@ -262,7 +262,7 @@ class MainMenuActivity(ba.Activity):
self._valid = True self._valid = True
self._message_duration = 10.0 self._message_duration = 10.0
self._message_spacing = 2.0 self._message_spacing = 2.0
self._text: Optional[ba.Actor] = None self._text: Optional[ba.NodeActor] = None
self._activity = weakref.ref(activity) self._activity = weakref.ref(activity)
# If we're signed in, fetch news immediately. # If we're signed in, fetch news immediately.
@ -290,8 +290,8 @@ class MainMenuActivity(ba.Activity):
# If our news is way out of date, lets re-request it; # If our news is way out of date, lets re-request it;
# otherwise, rotate our phrase. # otherwise, rotate our phrase.
assert app.main_menu_last_news_fetch_time is not None assert ba.app.main_menu_last_news_fetch_time is not None
if time.time() - app.main_menu_last_news_fetch_time > 600.0: if time.time() - ba.app.main_menu_last_news_fetch_time > 600.0:
self._fetch_news() self._fetch_news()
self._text = None self._text = None
else: else:

View File

@ -1169,8 +1169,8 @@ class TowerD(ba.Map):
running: bool = False) -> bool: running: bool = False) -> bool:
# see if we're within edge_box # see if we're within edge_box
boxes = self.defs.boxes boxes = self.defs.boxes
box_position = boxes['edge_box1'][0:3] box_position = boxes['edge_box'][0:3]
box_scale = boxes['edge_box1'][6:9] box_scale = boxes['edge_box'][6:9]
box_position2 = boxes['edge_box2'][0:3] box_position2 = boxes['edge_box2'][0:3]
box_scale2 = boxes['edge_box2'][6:9] box_scale2 = boxes['edge_box2'][6:9]
xpos = (point.x - box_position[0]) / box_scale[0] xpos = (point.x - box_position[0]) / box_scale[0]

View File

@ -1,4 +1,2 @@
+/tools +/tools
+/src/ballistica
+/src/generated_src
+/assets/src/ba_data/python +/assets/src/ba_data/python

View File

@ -80,6 +80,12 @@ good-names=i,
U, U,
_ _
[TYPECHECK]
# Mypy does most all the type checking we need, and in some cases
# Pylint has conflicting views about what types it thinks things are,
# so let's just flip stuff off here.
disable=no-member
[MISCELLANEOUS] [MISCELLANEOUS]
# We've got various TODO and FIXME notes in scripts, but don't want # We've got various TODO and FIXME notes in scripts, but don't want
# lint to trigger over that fact. # lint to trigger over that fact.

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-04-06 for Ballistica version 1.5.0 build 20001</em></h4> <h4><em>last updated on 2020-04-07 for Ballistica version 1.5.0 build 20001</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module, <p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p> which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr> <hr>
@ -112,6 +112,7 @@
</ul> </ul>
<h4><a name="class_category_Message_Classes">Message Classes</a></h4> <h4><a name="class_category_Message_Classes">Message Classes</a></h4>
<ul> <ul>
<li><a href="#class_ba_CelebrateMessage">ba.CelebrateMessage</a></li>
<li><a href="#class_ba_DieMessage">ba.DieMessage</a></li> <li><a href="#class_ba_DieMessage">ba.DieMessage</a></li>
<li><a href="#class_ba_DropMessage">ba.DropMessage</a></li> <li><a href="#class_ba_DropMessage">ba.DropMessage</a></li>
<li><a href="#class_ba_DroppedMessage">ba.DroppedMessage</a></li> <li><a href="#class_ba_DroppedMessage">ba.DroppedMessage</a></li>
@ -646,9 +647,6 @@ or returns None depending on whether 'doraise' is set.</p>
<p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p> <p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p>
<p>The default implementation will handle <a href="#class_ba_DieMessage">ba.DieMessages</a> by
calling self.node.delete() if self contains a 'node' attribute.</p>
</dd> </dd>
<dt><h4><a name="method_ba_Actor__is_alive">is_alive()</a></dt></h4><dd> <dt><h4><a name="method_ba_Actor__is_alive">is_alive()</a></dt></h4><dd>
<p><span>is_alive(self) -&gt; bool</span></p> <p><span>is_alive(self) -&gt; bool</span></p>
@ -1183,6 +1181,29 @@ mycall()</pre>
<p>Set the Level currently selected in the UI (by name).</p> <p>Set the Level currently selected in the UI (by name).</p>
</dd>
</dl>
<hr>
<h2><strong><a name="class_ba_CelebrateMessage">ba.CelebrateMessage</a></strong></h3>
<p><em>&lt;top level class&gt;</em>
</p>
<p>Tells an object to celebrate.</p>
<p>Category: <a href="#class_category_Message_Classes">Message Classes</a></p>
<h3>Attributes:</h3>
<dl>
<dt><h4><a name="attr_ba_CelebrateMessage__duration">duration</a></h4></dt><dd>
<p><span>float</span></p>
<p>Amount of time to celebrate in seconds.</p>
</dd>
</dl>
<h3>Methods:</h3>
<dl>
<dt><h4><a name="method_ba_CelebrateMessage____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.CelebrateMessage(duration: float = 10.0)</span></p>
</dd> </dd>
</dl> </dl>
<hr> <hr>
@ -1586,6 +1607,7 @@ the data object is requested and when it's value is accessed.</p>
<h3>Values:</h3> <h3>Values:</h3>
<ul> <ul>
<li>GENERIC</li> <li>GENERIC</li>
<li>OUT_OF_BOUNDS</li>
<li>IMPACT</li> <li>IMPACT</li>
<li>FALL</li> <li>FALL</li>
<li>REACHED_GOAL</li> <li>REACHED_GOAL</li>
@ -2782,15 +2804,35 @@ etc.</p>
</dd> </dd>
</dl> </dl>
<h3>Methods Inherited:</h3> <h3>Methods Inherited:</h3>
<h5><a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5> <h5><a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5>
<h3>Methods Defined or Overridden:</h3> <h3>Methods Defined or Overridden:</h3>
<h5><a href="#method_ba_Map____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Map__get_def_bound_box">get_def_bound_box()</a>, <a href="#method_ba_Map__get_def_point">get_def_point()</a>, <a href="#method_ba_Map__get_def_points">get_def_points()</a>, <a href="#method_ba_Map__get_ffa_start_position">get_ffa_start_position()</a>, <a href="#method_ba_Map__get_flag_position">get_flag_position()</a>, <a href="#method_ba_Map__get_music_type">get_music_type()</a>, <a href="#method_ba_Map__get_name">get_name()</a>, <a href="#method_ba_Map__get_play_types">get_play_types()</a>, <a href="#method_ba_Map__get_preview_texture_name">get_preview_texture_name()</a>, <a href="#method_ba_Map__get_start_position">get_start_position()</a>, <a href="#method_ba_Map__handlemessage">handlemessage()</a>, <a href="#method_ba_Map__is_point_near_edge">is_point_near_edge()</a>, <a href="#method_ba_Map__on_preload">on_preload()</a>, <a href="#method_ba_Map__preload">preload()</a></h5> <h5><a href="#method_ba_Map____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Map__exists">exists()</a>, <a href="#method_ba_Map__get_def_bound_box">get_def_bound_box()</a>, <a href="#method_ba_Map__get_def_point">get_def_point()</a>, <a href="#method_ba_Map__get_def_points">get_def_points()</a>, <a href="#method_ba_Map__get_ffa_start_position">get_ffa_start_position()</a>, <a href="#method_ba_Map__get_flag_position">get_flag_position()</a>, <a href="#method_ba_Map__get_music_type">get_music_type()</a>, <a href="#method_ba_Map__get_name">get_name()</a>, <a href="#method_ba_Map__get_play_types">get_play_types()</a>, <a href="#method_ba_Map__get_preview_texture_name">get_preview_texture_name()</a>, <a href="#method_ba_Map__get_start_position">get_start_position()</a>, <a href="#method_ba_Map__handlemessage">handlemessage()</a>, <a href="#method_ba_Map__is_point_near_edge">is_point_near_edge()</a>, <a href="#method_ba_Map__on_preload">on_preload()</a>, <a href="#method_ba_Map__preload">preload()</a></h5>
<dl> <dl>
<dt><h4><a name="method_ba_Map____init__">&lt;constructor&gt;</a></dt></h4><dd> <dt><h4><a name="method_ba_Map____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Map(vr_overlay_offset: Optional[Sequence[float]] = None)</span></p> <p><span>ba.Map(vr_overlay_offset: Optional[Sequence[float]] = None)</span></p>
<p>Instantiate a map.</p> <p>Instantiate a map.</p>
</dd>
<dt><h4><a name="method_ba_Map__exists">exists()</a></dt></h4><dd>
<p><span>exists(self) -&gt; bool</span></p>
<p>Returns whether the Actor is still present in a meaningful way.</p>
<p>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 <a href="#method_ba_Actor__is_alive">ba.Actor.is_alive</a>() for that).</p>
<p>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 <a href="#method_ba_Actor__autoretain">ba.Actor.autoretain</a>()</p>
<p>The default implementation of this method always return True.</p>
<p>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.</p>
</dd> </dd>
<dt><h4><a name="method_ba_Map__get_def_bound_box">get_def_bound_box()</a></dt></h4><dd> <dt><h4><a name="method_ba_Map__get_def_bound_box">get_def_bound_box()</a></dt></h4><dd>
<p><span>get_def_bound_box(self, name: str) -&gt; Optional[Tuple[float, float, float, float, float, float]]</span></p> <p><span>get_def_bound_box(self, name: str) -&gt; Optional[Tuple[float, float, float, float, float, float]]</span></p>
@ -2872,9 +2914,6 @@ as far from these players as possible.</p>
<p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p> <p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p>
<p>The default implementation will handle <a href="#class_ba_DieMessage">ba.DieMessages</a> by
calling self.node.delete() if self contains a 'node' attribute.</p>
</dd> </dd>
<dt><h4><a name="method_ba_Map__is_point_near_edge">is_point_near_edge()</a></dt></h4><dd> <dt><h4><a name="method_ba_Map__is_point_near_edge">is_point_near_edge()</a></dt></h4><dd>
<p><span>is_point_near_edge(self, point: <a href="#class_ba_Vec3">ba.Vec3</a>, running: bool = False) -&gt; bool</span></p> <p><span>is_point_near_edge(self, point: <a href="#class_ba_Vec3">ba.Vec3</a>, running: bool = False) -&gt; bool</span></p>
@ -3347,24 +3386,41 @@ acting as an alternative to setting node attributes.</p>
</dd> </dd>
</dl> </dl>
<h3>Methods Inherited:</h3> <h3>Methods Inherited:</h3>
<h5><a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5> <h5><a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5>
<h3>Methods Defined or Overridden:</h3> <h3>Methods Defined or Overridden:</h3>
<h5><a href="#method_ba_NodeActor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_NodeActor__handlemessage">handlemessage()</a></h5> <h5><a href="#method_ba_NodeActor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_NodeActor__exists">exists()</a>, <a href="#method_ba_NodeActor__handlemessage">handlemessage()</a></h5>
<dl> <dl>
<dt><h4><a name="method_ba_NodeActor____init__">&lt;constructor&gt;</a></dt></h4><dd> <dt><h4><a name="method_ba_NodeActor____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.NodeActor(node: <a href="#class_ba_Node">ba.Node</a>)</span></p> <p><span>ba.NodeActor(node: <a href="#class_ba_Node">ba.Node</a>)</span></p>
<p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p> <p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p>
</dd>
<dt><h4><a name="method_ba_NodeActor__exists">exists()</a></dt></h4><dd>
<p><span>exists(self) -&gt; bool</span></p>
<p>Returns whether the Actor is still present in a meaningful way.</p>
<p>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 <a href="#method_ba_Actor__is_alive">ba.Actor.is_alive</a>() for that).</p>
<p>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 <a href="#method_ba_Actor__autoretain">ba.Actor.autoretain</a>()</p>
<p>The default implementation of this method always return True.</p>
<p>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.</p>
</dd> </dd>
<dt><h4><a name="method_ba_NodeActor__handlemessage">handlemessage()</a></dt></h4><dd> <dt><h4><a name="method_ba_NodeActor__handlemessage">handlemessage()</a></dt></h4><dd>
<p><span>handlemessage(self, msg: Any) -&gt; Any</span></p> <p><span>handlemessage(self, msg: Any) -&gt; Any</span></p>
<p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p> <p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p>
<p>The default implementation will handle <a href="#class_ba_DieMessage">ba.DieMessages</a> by
calling self.node.delete() if self contains a 'node' attribute.</p>
</dd> </dd>
</dl> </dl>
<hr> <hr>
@ -4267,7 +4323,7 @@ of the session.</p>
</dd> </dd>
</dl> </dl>
<h3>Methods:</h3> <h3>Methods:</h3>
<h5><a href="#method_ba_Team____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Team__celebrate">celebrate()</a>, <a href="#method_ba_Team__get_id">get_id()</a></h5> <h5><a href="#method_ba_Team____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Team__get_id">get_id()</a></h5>
<dl> <dl>
<dt><h4><a name="method_ba_Team____init__">&lt;constructor&gt;</a></dt></h4><dd> <dt><h4><a name="method_ba_Team____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Team(team_id: 'int' = 0, name: 'Union[<a href="#class_ba_Lstr">ba.Lstr</a>, str]' = '', color: 'Sequence[float]' = (1.0, 1.0, 1.0))</span></p> <p><span>ba.Team(team_id: 'int' = 0, name: 'Union[<a href="#class_ba_Lstr">ba.Lstr</a>, str]' = '', color: 'Sequence[float]' = (1.0, 1.0, 1.0))</span></p>
@ -4277,14 +4333,6 @@ of the session.</p>
<p>In most cases, all teams are provided to you by the <a href="#class_ba_Session">ba.Session</a>, <p>In most cases, all teams are provided to you by the <a href="#class_ba_Session">ba.Session</a>,
<a href="#class_ba_Session">ba.Session</a>, so calling this shouldn't be necessary.</p> <a href="#class_ba_Session">ba.Session</a>, so calling this shouldn't be necessary.</p>
</dd>
<dt><h4><a name="method_ba_Team__celebrate">celebrate()</a></dt></h4><dd>
<p><span>celebrate(self, duration: float = 10.0) -&gt; None</span></p>
<p>Tells all players on the team to celebrate.</p>
<p>duration is given in seconds.</p>
</dd> </dd>
<dt><h4><a name="method_ba_Team__get_id">get_id()</a></dt></h4><dd> <dt><h4><a name="method_ba_Team__get_id">get_id()</a></dt></h4><dd>
<p><span>get_id(self) -&gt; int</span></p> <p><span>get_id(self) -&gt; int</span></p>