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,
PickedUpMessage, DroppedMessage,
ShouldShatterMessage, ImpactDamageMessage,
FreezeMessage, ThawMessage, HitMessage)
FreezeMessage, ThawMessage, HitMessage,
CelebrateMessage)
from ba._music import setmusic, MusicPlayer, MusicType, MusicPlayMode
from ba._powerup import PowerupMessage, PowerupAcceptMessage
from ba._teambasesession import TeamBaseSession

View File

@ -25,6 +25,9 @@ from __future__ import annotations
import weakref
from typing import TYPE_CHECKING, TypeVar
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage
from ba import _error
import _ba
if TYPE_CHECKING:
@ -83,9 +86,11 @@ class Actor:
def __init__(self) -> None:
"""Instantiates an Actor in the current ba.Activity."""
# FIXME: Actor should not be require to have a 'node' attr.
self.node: Optional[ba.Node] = None
# FIXME: Actor should not be assumed to have a 'node' attr.
# self.node: Optional[ba.Node] = None
if __debug__:
self._root_actor_init_called = True
activity = _ba.getactivity()
self._activity = weakref.ref(activity)
activity.add_actor_weak_ref(self)
@ -96,27 +101,20 @@ class Actor:
# That way we can treat DieMessage handling as the single
# point-of-action for death.
if not self.is_expired():
from ba import _messages
self.handlemessage(_messages.DieMessage())
self.handlemessage(DieMessage())
except Exception:
from ba import _error
_error.print_exception('exception in ba.Actor.__del__() for', self)
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
calling self.node.delete() if self contains a 'node' attribute.
"""
from ba._error import UNHANDLED
del msg # Unused.
return UNHANDLED
# By default, actors going out-of-bounds simply kill themselves.
if isinstance(msg, OutOfBoundsMessage):
return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
def _handlemessage_sanity_check(self) -> None:
if self.is_expired():
from ba import _error
_error.print_error(
f'handlemessage called on expired actor: {self}')
return _error.UNHANDLED
def autoretain(self: T) -> T:
"""Keep this Actor alive without needing to hold a reference to it.
@ -131,8 +129,7 @@ class Actor:
"""
activity = self._activity()
if activity is None:
from ba._error import ActivityNotFoundError
raise ActivityNotFoundError()
raise _error.ActivityNotFoundError()
activity.retain_actor(self)
return self
@ -192,6 +189,21 @@ class Actor:
"""
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
def activity(self) -> ba.Activity:
"""The Activity this Actor was created in.
@ -200,8 +212,7 @@ class Actor:
"""
activity = self._activity()
if activity is None:
from ba._error import ActivityNotFoundError
raise ActivityNotFoundError()
raise _error.ActivityNotFoundError()
return activity
def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]:
@ -212,6 +223,5 @@ class Actor:
"""
activity = self._activity()
if activity is None and doraise:
from ba._error import ActivityNotFoundError
raise ActivityNotFoundError()
raise _error.ActivityNotFoundError()
return activity

View File

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

View File

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

View File

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

View File

@ -337,13 +337,13 @@ class GameActivity(Activity):
self._game_scoreboard_description_text: Optional[ba.Actor] = None
self._standard_time_limit_time: Optional[int] = None
self._standard_time_limit_timer: Optional[ba.Timer] = None
self._standard_time_limit_text: Optional[ba.Actor] = None
self._standard_time_limit_text_input: Optional[ba.Actor] = None
self._standard_time_limit_text: Optional[ba.NodeActor] = None
self._standard_time_limit_text_input: Optional[ba.NodeActor] = None
self._tournament_time_limit: Optional[int] = None
self._tournament_time_limit_timer: Optional[ba.Timer] = None
self._tournament_time_limit_title_text: Optional[ba.Actor] = None
self._tournament_time_limit_text: Optional[ba.Actor] = None
self._tournament_time_limit_text_input: Optional[ba.Actor] = None
self._tournament_time_limit_title_text: Optional[ba.NodeActor] = None
self._tournament_time_limit_text: Optional[ba.NodeActor] = None
self._tournament_time_limit_text_input: Optional[ba.NodeActor] = None
self._zoom_message_times: Dict[int, float] = {}
self._is_waiting_for_continue = False
@ -1042,17 +1042,18 @@ class GameActivity(Activity):
from ba import _messages
from ba._gameutils import animate
from ba._coopsession import CoopSession
from bastd.actor import playerspaz
from bastd.actor.playerspaz import PlayerSpaz
name = player.get_name()
color = player.color
highlight = player.highlight
light_color = _math.normalized_color(color)
display_color = _ba.safecolor(color, target_intensity=0.75)
spaz = playerspaz.PlayerSpaz(color=color,
highlight=highlight,
character=player.character,
player=player)
spaz = PlayerSpaz(color=color,
highlight=highlight,
character=player.character,
player=player)
player.set_actor(spaz)
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)
# 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
# (and instruct the user if we weren't preloaded properly).
@ -365,9 +365,8 @@ class Map(Actor):
player_pts = []
for player in players:
try:
if player.actor is not None and player.actor.is_alive():
assert player.actor.node
pnt = _ba.Vec3(player.actor.node.position)
if player.node:
pnt = _ba.Vec3(player.node.position)
player_pts.append(pnt)
except Exception as 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[team_index % len(self.flag_points)][:3]
def exists(self) -> bool:
return bool(self.node)
def handlemessage(self, msg: Any) -> Any:
from ba import _messages
if isinstance(msg, _messages.DieMessage):
if self.node:
self.node.delete()
super().handlemessage(msg)
else:
return super().handlemessage(msg)
return None
def register_map(maptype: Type[Map]) -> None:

View File

@ -47,6 +47,7 @@ class DeathType(Enum):
Category: Enums
"""
GENERIC = 'generic'
OUT_OF_BOUNDS = 'out_of_bounds'
IMPACT = 'impact'
FALL = 'fall'
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)
class HitMessage:
"""Tells an object it has been hit in some way.

View File

@ -51,3 +51,6 @@ class NodeActor(Actor):
self.node.delete()
return None
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."""
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:
"""(internal)"""
self.reset_gamedata()

View File

@ -254,22 +254,23 @@ class TeamBaseSession(Session):
announcement of the same.
"""
# pylint: disable=cyclic-import
# pylint: disable=too-many-locals
from ba import _math
from ba import _general
from ba._gameutils import cameraflash
from ba import _lang
from ba._freeforallsession import FreeForAllSession
from ba._messages import CelebrateMessage
_ba.timer(delay,
_general.Call(_ba.playsound, _ba.getsound("boxingBell")))
if announce_winning_team:
winning_team = results.get_winning_team()
if winning_team is not None:
# Have all players celebrate.
celebrate_msg = CelebrateMessage(duration=10.0)
for player in winning_team.players:
if player.actor is not None and player.actor.node:
# Note: celebrate message takes milliseconds
# for historical reasons.
player.actor.node.handlemessage('celebrate', 10000)
if player.actor:
player.actor.handlemessage(celebrate_msg)
cameraflash()
# 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
import ba
from bastd.actor import spaz as basespaz
from bastd.actor.spaz import Spaz
if TYPE_CHECKING:
from typing import Any, Optional, Sequence, Tuple
@ -77,7 +77,7 @@ class PlayerSpazHurtMessage:
self.spaz = spaz
class PlayerSpaz(basespaz.Spaz):
class PlayerSpaz(Spaz):
"""A ba.Spaz subclass meant to be controlled by a ba.Player.
category: Gameplay Classes
@ -102,13 +102,12 @@ class PlayerSpaz(basespaz.Spaz):
you must call connect_controls_to_player() to do so.
"""
basespaz.Spaz.__init__(self,
color=color,
highlight=highlight,
character=character,
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire)
super().__init__(color=color,
highlight=highlight,
character=character,
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire)
self.last_player_attacked_by: Optional[ba.Player] = None
self.last_attacked_time = 0.0
self.last_attacked_type: Optional[Tuple[str, str]] = None

View File

@ -155,7 +155,7 @@ class Spaz(ba.Actor):
media = factory.get_media(character)
punchmats = (factory.punch_material, ba.sharedobj('attack_material'))
pickupmats = (factory.pickup_material, ba.sharedobj('pickup_material'))
self.node = ba.newnode(
self.node: ba.Node = ba.newnode(
type="spaz",
delegate=self,
attrs={
@ -256,10 +256,13 @@ class Spaz(ba.Actor):
self.punch_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:
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.punch_callback = None
self.pick_up_powerup_callback = None
@ -296,8 +299,8 @@ class Spaz(ba.Actor):
assert isinstance(t_ms, int)
t_bucket = int(t_ms / 1000)
if t_bucket == self._turbo_filter_time_bucket:
# add only once per timestep (filter out buttons triggering
# multiple actions)
# Add only once per timestep (filter out buttons triggering
# multiple actions).
if t_ms != self._turbo_filter_times.get(source, 0):
self._turbo_filter_counts[source] = (
self._turbo_filter_counts.get(source, 0) + 1)
@ -306,11 +309,11 @@ class Spaz(ba.Actor):
# ba.screenmessage( str(source) + " "
# + str(self._turbo_filter_counts[source]))
if self._turbo_filter_counts[source] == 15:
# knock 'em out. That'll learn 'em.
# Knock 'em out. That'll learn 'em.
assert self.node
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)
if now > ba.app.last_spaz_turbo_warn_time + 30.0:
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.
self.node.hold_body = opposing_body
self.node.hold_node = opposing_node
elif isinstance(msg, ba.CelebrateMessage):
if self.node:
self.node.handlemessage('celebrate', int(msg.duration * 1000))
else:
return super().handlemessage(msg)
return None

View File

@ -190,7 +190,7 @@ class SpazBot(basespaz.Spaz):
closest_dist = None
closest_vel = None
closest = None
assert self._player_pts
assert self._player_pts is not None
for plpt, plvel in self._player_pts:
dist = (plpt - botpt).length()
@ -983,6 +983,7 @@ class BotSet:
for player in ba.getactivity().players:
try:
if player.is_alive():
assert isinstance(player.actor, basespaz.Spaz)
assert player.actor is not None and player.actor.node
player_pts.append((ba.Vec3(player.actor.node.position),
ba.Vec3(player.actor.node.velocity)))
@ -1006,16 +1007,6 @@ class BotSet:
bot.handlemessage(ba.DieMessage(immediate=True))
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:
"""Start processing bot AI updates so they start doing their thing."""
self._bot_update_timer = ba.Timer(0.05,
@ -1035,6 +1026,17 @@ class BotSet:
bot.node.move_left_right = 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:
"""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.
for botlist in self._bot_lists:
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_up_down = 0
ba.timer(
0.5 * random.random(),
ba.Call(bot.node.handlemessage, 'celebrate', 10.0))
ba.timer(0.5 * random.random(),
ba.Call(bot.handlemessage, ba.CelebrateMessage()))
jump_duration = random.randrange(400, 500)
j = random.randrange(0, 200)
for _i in range(10):

View File

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

View File

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

View File

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

View File

@ -143,7 +143,9 @@ class EasterEggHuntGame(ba.TeamGameActivity):
'source_node', 'opposing_node')
if egg_node is not None and playernode is not None:
egg = egg_node.getdelegate()
assert isinstance(egg, Egg)
spaz = playernode.getdelegate()
assert isinstance(spaz, playerspaz.PlayerSpaz)
player = (spaz.getplayer()
if hasattr(spaz, 'getplayer') else None)
if player and egg:
@ -187,13 +189,8 @@ class EasterEggHuntGame(ba.TeamGameActivity):
ypos = random.uniform(3.5, 3.5)
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.
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.
if len(self._eggs) < int(self._max_eggs):
@ -283,12 +280,13 @@ class Egg(ba.Actor):
'materials': mats
})
def exists(self) -> bool:
return bool(self.node)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.HitMessage):
if self.node:
assert msg.force_direction is not None

View File

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

View File

@ -128,10 +128,10 @@ class FootballTeamGame(ba.TeamGameActivity):
True), ("modify_part_collision", "physical", False),
("call", "at_connect", self._handle_score)))
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_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]:
touchdowns = self.settings['Score to Win'] / 7
@ -203,13 +203,8 @@ class FootballTeamGame(ba.TeamGameActivity):
# Tell all players to celebrate.
for player in team.players:
if player.actor is not None and player.actor.node:
try:
# Note: celebrate message is milliseconds
# (for historical reasons).
player.actor.node.handlemessage('celebrate', 2000)
except Exception:
ba.print_exception('Error on celebrate')
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
# If someone on this team was last to touch it,
# give them points.
@ -376,7 +371,7 @@ class FootballCoopGame(ba.CoopGameActivity):
self._player_has_punched = False
self._scoreboard: Optional[Scoreboard] = 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._have_tnt = False
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_team: Optional[ba.Team] = None
self._starttime_ms: Optional[int] = None
self._time_text: Optional[ba.Actor] = None
self._time_text_input: Optional[ba.Actor] = None
self._time_text: Optional[ba.NodeActor] = None
self._time_text_input: Optional[ba.NodeActor] = None
self._tntspawner: Optional[stdbomb.TNTSpawner] = None
self._bots = spazbot.BotSet()
self._bot_spawn_timer: Optional[ba.Timer] = None
@ -580,13 +575,11 @@ class FootballCoopGame(ba.CoopGameActivity):
assert self._flag is not None
if self._flag.node:
for player in self.players:
try:
assert player.actor is not None and player.actor.node
if player.actor:
assert isinstance(player.actor, playerspaz.PlayerSpaz)
if (player.actor.is_alive() and
player.actor.node.hold_node == self._flag.node):
return
except Exception:
ba.print_exception("exception checking hold node")
flagpos = ba.Vec3(self._flag.node.position)
closest_bot = None
@ -651,7 +644,6 @@ class FootballCoopGame(ba.CoopGameActivity):
""" a point has been scored """
# FIXME tidy this up
# pylint: disable=too-many-branches
# pylint: disable=too-many-nested-blocks
# Our flag might stick around for a second or two;
# 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.
if i == 0:
for player in team.players:
try:
# Note: celebrate message is milliseconds
# (for historical reasons).
if player.actor is not None and player.actor.node:
player.actor.node.handlemessage(
'celebrate', 2000)
except Exception:
ba.print_exception()
if player.actor:
player.actor.handlemessage(
ba.CelebrateMessage(2.0))
else:
self._bots.celebrate(2000)
self._bots.celebrate(2.0)
# If the good guys scored, add more enemies.
if i == 0:

View File

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

View File

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

View File

@ -188,7 +188,9 @@ class NinjaFightGame(ba.TeamGameActivity):
ba.cameraflash()
ba.playsound(self._winsound)
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)
# Ends the activity.

View File

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

View File

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

View File

@ -117,15 +117,15 @@ class RunaroundGame(ba.CoopGameActivity):
self._waves: Optional[List[Dict[str, Any]]] = None
self._bots = spazbot.BotSet()
self._tntspawner: Optional[TNTSpawner] = None
self._lives_bg: Optional[ba.Actor] = None
self._lives_bg: Optional[ba.NodeActor] = None
self._start_lives = 10
self._lives = self._start_lives
self._lives_text: Optional[ba.Actor] = None
self._lives_text: Optional[ba.NodeActor] = None
self._flawless = True
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._wave_text: Optional[ba.Actor] = None
self._wave_text: Optional[ba.NodeActor] = None
self._flawless_bonus: Optional[int] = 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:
# Yup; that's a lot of return statements right there.
# 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
assert bot.node
# FIXME: Do this in a type safe way.
r_walk_speed: float = bot.r_walk_speed # 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:
try:
if player.is_alive():
assert isinstance(player.actor, playerspaz.PlayerSpaz)
assert player.actor is not None and player.actor.node
playerpts.append(player.actor.node.position)
except Exception as exc:

View File

@ -262,7 +262,7 @@ class MainMenuActivity(ba.Activity):
self._valid = True
self._message_duration = 10.0
self._message_spacing = 2.0
self._text: Optional[ba.Actor] = None
self._text: Optional[ba.NodeActor] = None
self._activity = weakref.ref(activity)
# 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;
# otherwise, rotate our phrase.
assert app.main_menu_last_news_fetch_time is not None
if time.time() - app.main_menu_last_news_fetch_time > 600.0:
assert ba.app.main_menu_last_news_fetch_time is not None
if time.time() - ba.app.main_menu_last_news_fetch_time > 600.0:
self._fetch_news()
self._text = None
else:

View File

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

View File

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

View File

@ -80,6 +80,12 @@ good-names=i,
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]
# We've got various TODO and FIXME notes in scripts, but don't want
# lint to trigger over that fact.

View File

@ -1,5 +1,5 @@
<!-- 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,
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>
@ -112,6 +112,7 @@
</ul>
<h4><a name="class_category_Message_Classes">Message Classes</a></h4>
<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_DropMessage">ba.DropMessage</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>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>
<dt><h4><a name="method_ba_Actor__is_alive">is_alive()</a></dt></h4><dd>
<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>
</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>
</dl>
<hr>
@ -1586,6 +1607,7 @@ the data object is requested and when it's value is accessed.</p>
<h3>Values:</h3>
<ul>
<li>GENERIC</li>
<li>OUT_OF_BOUNDS</li>
<li>IMPACT</li>
<li>FALL</li>
<li>REACHED_GOAL</li>
@ -2782,15 +2804,35 @@ etc.</p>
</dd>
</dl>
<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>
<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>
<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>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>
<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>
@ -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>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>
<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>
@ -3347,24 +3386,41 @@ acting as an alternative to setting node attributes.</p>
</dd>
</dl>
<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>
<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>
<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>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>
<dt><h4><a name="method_ba_NodeActor__handlemessage">handlemessage()</a></dt></h4><dd>
<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>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>
</dl>
<hr>
@ -4267,7 +4323,7 @@ of the session.</p>
</dd>
</dl>
<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>
<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>
@ -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>,
<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>
<dt><h4><a name="method_ba_Team__get_id">get_id()</a></dt></h4><dd>
<p><span>get_id(self) -&gt; int</span></p>