diff --git a/.efrocachemap b/.efrocachemap
index d6c71862..e461c85c 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -4132,16 +4132,16 @@
"assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c",
"assets/build/windows/x64/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb",
"assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe",
- "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/fd/1edae1d48773436f72ceaf5c4588",
- "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/07/88/2e60127c99cd8018f4b7a77c455b",
- "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/01/ef/2c661b395a46f1abbd6c2042cb50",
- "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/98/ad/d73757e8902b9c5c36c73469773b",
- "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/94/e3/9ad5acc492f343a24e790be07ed0",
- "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/1f/5ae4c3d9b710b24ea2464c1f62ab",
- "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/83/f9/de5c6a7a1dd65f305565bd5a0897",
- "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c7/29/1f1d904b57ef0379654421737c6b",
- "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/bd/7b/308923feba6216c72c565cb9a942",
- "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/ce/44/4d2658f31d74fb342604e38bf12a",
- "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/53/d7/89dfbf816f8fe6824cf9f62f81f6",
- "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/1c/b8/d9def95f52348a50d01b4a9a3996"
+ "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/66/68/37e1f6d2afd5d6a4cbbcebba2f3e",
+ "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/dc/d4/f954892306c82ca4d9c74d335c15",
+ "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4d/b8/4cfc2035ec4cdeba78be2aee8aff",
+ "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3f/98/9edf61a1b38432213e93b9342a4e",
+ "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/43/f76e498f45bb42f2383986d3c15b",
+ "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a1/d6/a9dd60f83d58eb09b1b4c0771588",
+ "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/4f/9ded4658cf6e8d8d7fdf9477ae86",
+ "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a1/6b/56d9fa2709eb43be73c00aacb1b5",
+ "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a9/41/2e78f2f5dfa4273ce70fc5a59e0e",
+ "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/9e/3d/12c0ba5235b6750ec0f37726de6e",
+ "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/8f/9c/edea76ee92634ef9565988c9ef6e",
+ "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/35/86/2aeb9cfac9f7639676e149e1323b"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index 78083559..fe3fd86d 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -578,6 +578,8 @@
execlocals
executils
exhash
+ existable
+ existables
expatbuilder
expatreader
explodable
@@ -752,6 +754,7 @@
getcurrency
getcwd
getdata
+ getkillerplayer
getlevelname
getmaps
getmodel
@@ -1688,6 +1691,7 @@
spinoff
spinoffdata
spinoffs
+ splayer
splitlen
splitnumstr
squadcore
diff --git a/.idea/inspectionProfiles/Default.xml b/.idea/inspectionProfiles/Default.xml
index 869f7d0f..9f30bcb5 100644
--- a/.idea/inspectionProfiles/Default.xml
+++ b/.idea/inspectionProfiles/Default.xml
@@ -50,6 +50,7 @@
+
diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py
index 6b5325df..b65a4fff 100644
--- a/assets/src/ba_data/python/_ba.py
+++ b/assets/src/ba_data/python/_ba.py
@@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
"""
# (hash we can use to see if this file is out of date)
-# SOURCES_HASH=68384686054944380533078060197841658129
+# SOURCES_HASH=86859069497661939825754704003122742687
# I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression
@@ -253,9 +253,6 @@ class InputDevice:
Attributes:
- exists: bool
- Whether the underlying device for this object is still present.
-
allows_configuring: bool
Whether the input-device can be configured.
@@ -290,7 +287,6 @@ class InputDevice:
client.
"""
- exists: bool
allows_configuring: bool
player: Optional[ba.SessionPlayer]
client_id: int
@@ -301,6 +297,13 @@ class InputDevice:
is_controller_app: bool
is_remote_client: bool
+ def exists(self) -> bool:
+ """exists() -> bool
+
+ Return whether the underlying device for this object is still present.
+ """
+ return bool()
+
def get_account_name(self, full: bool) -> str:
"""get_account_name(full: bool) -> str
@@ -767,7 +770,7 @@ class SessionPlayer:
Be aware that, like ba.Nodes, ba.SessionPlayer objects are 'weak'
references under-the-hood; a player can leave the game at
any point. For this reason, you should make judicious use of the
- ba.SessionPlayer.exists attribute (or boolean operator) to ensure
+ ba.SessionPlayer.exists() method (or boolean operator) to ensure
that a SessionPlayer is still present if retaining references to one
for any length of time.
@@ -776,10 +779,6 @@ class SessionPlayer:
id: int
The unique numeric ID of the Player.
- exists: bool
- Whether the player still exists.
- Most functionality will fail on a nonexistent player.
-
Note that you can also use the boolean operator for this same
functionality, so a statement such as "if player" will do
the right thing both for Player objects and values of None.
@@ -820,7 +819,6 @@ class SessionPlayer:
The current game-specific instance for this player.
"""
id: int
- exists: bool
in_game: bool
team: ba.SessionTeam
sessiondata: Dict
@@ -845,6 +843,13 @@ class SessionPlayer:
"""
return None
+ def exists(self) -> bool:
+ """exists() -> bool
+
+ Return whether the underlying player is still in the game.
+ """
+ return bool()
+
def get_account_id(self) -> str:
"""get_account_id() -> str
diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py
index 1c5d157c..ef418c72 100644
--- a/assets/src/ba_data/python/ba/__init__.py
+++ b/assets/src/ba_data/python/ba/__init__.py
@@ -74,13 +74,13 @@ from ba._apputils import is_browser_likely_available
from ba._campaign import Campaign
from ba._gameutils import (animate, animate_array, show_damage_count,
sharedobj, timestring, cameraflash)
-from ba._general import WeakCall, Call
+from ba._general import WeakCall, Call, existing
from ba._level import Level
from ba._lobby import Lobby, Chooser
from ba._math import normalized_color, is_point_in_box, vec3validate
from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage,
- StandMessage, PickUpMessage, DropMessage,
- PickedUpMessage, DroppedMessage,
+ PlayerDiedMessage, StandMessage, PickUpMessage,
+ DropMessage, PickedUpMessage, DroppedMessage,
ShouldShatterMessage, ImpactDamageMessage,
FreezeMessage, ThawMessage, HitMessage,
CelebrateMessage)
diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py
index 8432620e..c2b6b03d 100644
--- a/assets/src/ba_data/python/ba/_gameactivity.py
+++ b/assets/src/ba_data/python/ba/_gameactivity.py
@@ -29,6 +29,7 @@ from typing import TYPE_CHECKING, TypeVar
from ba._activity import Activity
from ba._score import ScoreInfo
from ba._lang import Lstr
+from ba._messages import PlayerDiedMessage
import _ba
if TYPE_CHECKING:
@@ -645,21 +646,24 @@ class GameActivity(Activity[PlayerType, TeamType]):
player.set_actor(None)
def handlemessage(self, msg: Any) -> Any:
- from bastd.actor.playerspaz import PlayerSpazDeathMessage
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, PlayerDiedMessage):
+ # pylint: disable=cyclic-import
+ from bastd.actor.spaz import Spaz
- player = msg.playerspaz(self).player
- killer = msg.killerplayer
+ player = msg.getplayer(self.playertype)
+ killer = msg.getkillerplayer(self.playertype)
- # Inform our score-set of the demise.
+ # Inform our stats of the demise.
self.stats.player_was_killed(player,
killed=msg.killed,
killer=killer)
# Award the killer points if he's on a different team.
+ # FIXME: This should not be linked to Spaz actors.
+ # (should move get_death_points to Actor or make it a message)
if killer and killer.team is not player.team:
- pts, importance = msg.playerspaz(self).get_death_points(
- msg.how)
+ assert isinstance(killer.actor, Spaz)
+ pts, importance = killer.actor.get_death_points(msg.how)
if not self.has_ended():
self.stats.player_scored(killer,
pts,
diff --git a/assets/src/ba_data/python/ba/_general.py b/assets/src/ba_data/python/ba/_general.py
index ed61f77d..a535b00d 100644
--- a/assets/src/ba_data/python/ba/_general.py
+++ b/assets/src/ba_data/python/ba/_general.py
@@ -28,12 +28,40 @@ from typing import TYPE_CHECKING, TypeVar
import _ba
if TYPE_CHECKING:
- from typing import Any, Type
+ from typing import Any, Type, Optional
+ from typing_extensions import Protocol
from efro.call import Call as Call # 'as Call' so we re-export.
+ class Existable(Protocol):
+ """Protocol for objects supporting an exists() method."""
+
+ def exists(self) -> bool:
+ """Whether this object exists."""
+ ...
+
+ ExistableType = TypeVar('ExistableType', bound=Existable)
+
T = TypeVar('T')
+def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
+ """Convert invalid references to None.
+
+ Category: Gameplay Functions
+
+ To best support type checking, it is important that invalid references
+ not be passed around and instead get converted to values of None.
+ That way the type checker can properly flag attempts to pass dead
+ objects into functions expecting only live ones, etc.
+ This call can be used on any 'existable' object (one with an exists()
+ method) and will convert it to a None value if it does not exist.
+ For more info, see notes on 'existables' here:
+ https://github.com/efroemling/ballistica/wiki/Coding-Style-Guide
+ """
+ assert obj is None or hasattr(obj, 'exists'), f'No "exists" on {obj}'
+ return obj if obj is not None and obj.exists() else None
+
+
def getclass(name: str, subclassof: Type[T]) -> Type[T]:
"""Given a full class name such as foo.bar.MyClass, return the class.
diff --git a/assets/src/ba_data/python/ba/_messages.py b/assets/src/ba_data/python/ba/_messages.py
index eb0fda5a..ed309833 100644
--- a/assets/src/ba_data/python/ba/_messages.py
+++ b/assets/src/ba_data/python/ba/_messages.py
@@ -23,13 +23,13 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, TypeVar
from enum import Enum
import _ba
if TYPE_CHECKING:
- from typing import Sequence
+ from typing import Sequence, Optional, Type, Any
import ba
@@ -88,6 +88,64 @@ class DieMessage:
how: DeathType = DeathType.GENERIC
+PlayerType = TypeVar('PlayerType', bound='ba.Player')
+
+
+class PlayerDiedMessage:
+ """A message saying a ba.PlayerSpaz has died.
+
+ category: Message Classes
+
+ Attributes:
+
+ killed
+ If True, the spaz was killed;
+ If False, they left the game or the round ended.
+
+ how
+ The particular type of death.
+ """
+ killed: bool
+ how: ba.DeathType
+
+ def __init__(self, player: ba.Player, was_killed: bool,
+ killerplayer: Optional[ba.Player], how: ba.DeathType):
+ """Instantiate a message with the given values."""
+
+ # Invalid refs should never be passed as args.
+ assert player.exists()
+ self._player = player
+
+ # Invalid refs should never be passed as args.
+ assert killerplayer is None or killerplayer.exists()
+ self._killerplayer = killerplayer
+ self.killed = was_killed
+ self.how = how
+
+ def getkillerplayer(self,
+ playertype: Type[PlayerType]) -> Optional[PlayerType]:
+ """Return the ba.Player responsible for the killing, if any.
+
+ Pass the Player type being used by the current game.
+ """
+ assert isinstance(self._killerplayer, (playertype, type(None)))
+ return self._killerplayer
+
+ def getplayer(self, playertype: Type[PlayerType]) -> PlayerType:
+ """Return the spaz that died.
+
+ The current activity is required as an argument so the exact type of
+ PlayerSpaz can be determined by the type checker.
+ """
+ player: Any = self._player
+ assert isinstance(player, playertype)
+
+ # We should never be delivering invalid refs.
+ # (could theoretically happen if someone holds on to us)
+ assert player.exists()
+ return player
+
+
@dataclass
class StandMessage:
"""A message telling an object to move to a position in space.
@@ -212,7 +270,6 @@ class CelebrateMessage:
duration: float = 10.0
-@dataclass(init=False)
class HitMessage:
"""Tells an object it has been hit in some way.
@@ -243,7 +300,10 @@ class HitMessage:
self.magnitude = magnitude
self.velocity_magnitude = velocity_magnitude
self.radius = radius
- self.source_player = source_player
+
+ # Invalid refs should never be passed to things.
+ assert source_player is None or source_player.exists()
+ self._source_player = source_player
self.kick_back = kick_back
self.flat_damage = flat_damage
self.hit_type = hit_type
@@ -251,6 +311,21 @@ class HitMessage:
self.force_direction = (force_direction
if force_direction is not None else velocity)
+ def get_source_player(
+ self, playertype: Type[PlayerType]) -> Optional[PlayerType]:
+ """Return the spaz that died.
+
+ The current activity is required as an argument so the exact type of
+ PlayerSpaz can be determined by the type checker.
+ """
+ player: Any = self._source_player
+ assert isinstance(player, (playertype, type(None)))
+
+ # We should not be delivering invalid refs.
+ # (technically if someone holds on to this message this can happen)
+ assert player is None or player.exists()
+ return player
+
@dataclass
class PlayerProfilesChangedMessage:
diff --git a/assets/src/ba_data/python/ba/_player.py b/assets/src/ba_data/python/ba/_player.py
index 6622e609..56e490e9 100644
--- a/assets/src/ba_data/python/ba/_player.py
+++ b/assets/src/ba_data/python/ba/_player.py
@@ -29,6 +29,7 @@ if TYPE_CHECKING:
Callable)
import ba
+PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
@@ -117,7 +118,6 @@ class Player(Generic[TeamType]):
raise _error.NodeNotFoundError
return self._nodeactor.node
- @property
def exists(self) -> bool:
"""Whether the underlying player still exists.
@@ -126,7 +126,7 @@ class Player(Generic[TeamType]):
functionality, so a statement such as "if player" will do
the right thing both for Player objects and values of None.
"""
- return bool(self._sessionplayer)
+ return self._sessionplayer.exists()
def get_name(self, full: bool = False, icon: bool = True) -> str:
"""get_name(full: bool = False, icon: bool = True) -> str
@@ -181,10 +181,7 @@ class Player(Generic[TeamType]):
self._sessionplayer.reset_input()
def __bool__(self) -> bool:
- return bool(self._sessionplayer)
-
-
-PlayerType = TypeVar('PlayerType', bound='ba.Player')
+ return self._sessionplayer.exists()
def playercast(totype: Type[PlayerType], player: ba.Player) -> PlayerType:
diff --git a/assets/src/ba_data/python/bastd/actor/bomb.py b/assets/src/ba_data/python/bastd/actor/bomb.py
index bf4aca77..d88a5d69 100644
--- a/assets/src/ba_data/python/bastd/actor/bomb.py
+++ b/assets/src/ba_data/python/bastd/actor/bomb.py
@@ -623,7 +623,8 @@ class Blast(ba.Actor):
hit_type=self.hit_type,
hit_subtype=self.hit_subtype,
radius=self.radius,
- source_player=self.source_player))
+ source_player=ba.existing(
+ self.source_player)))
if self.blast_type == 'ice':
ba.playsound(get_factory().freeze_sound,
10,
@@ -987,8 +988,9 @@ class Bomb(ba.Actor):
# Also lets change the owner of the bomb to whoever is setting
# us off. (this way points for big chain reactions go to the
# person causing them).
- if msg.source_player not in [None]:
- self.source_player = msg.source_player
+ source_player = msg.get_source_player(ba.Player)
+ if source_player is not None:
+ self.source_player = source_player
# Also inherit the hit type (if a landmine sets off by a bomb,
# the credit should go to the mine)
diff --git a/assets/src/ba_data/python/bastd/actor/playerspaz.py b/assets/src/ba_data/python/bastd/actor/playerspaz.py
index 5c66414c..1c620736 100644
--- a/assets/src/ba_data/python/bastd/actor/playerspaz.py
+++ b/assets/src/ba_data/python/bastd/actor/playerspaz.py
@@ -34,44 +34,6 @@ PlayerType = TypeVar('PlayerType', bound=ba.Player)
TeamType = TypeVar('TeamType', bound=ba.Team)
-class PlayerSpazDeathMessage:
- """A message saying a ba.PlayerSpaz has died.
-
- category: Message Classes
-
- Attributes:
-
- killed
- If True, the spaz was killed;
- If False, they left the game or the round ended.
-
- killerplayer
- The ba.Player that did the killing, or None.
-
- how
- The particular type of death.
- """
-
- def __init__(self, spaz: PlayerSpaz, was_killed: bool,
- killerplayer: Optional[ba.Player], how: ba.DeathType):
- """Instantiate a message with the given values."""
- self._spaz = spaz
- self.killed = was_killed
- self.killerplayer = killerplayer
- self.how = how
-
- def playerspaz(
- self, activity: ba.Activity[PlayerType,
- TeamType]) -> PlayerSpaz[PlayerType]:
- """Return the spaz that died.
-
- The current activity is required as an argument so the exact type of
- PlayerSpaz can be determined by the type checker.
- """
- del activity # Unused
- return self._spaz
-
-
class PlayerSpazHurtMessage:
"""A message saying a ba.PlayerSpaz was hurt.
@@ -93,7 +55,7 @@ class PlayerSpaz(Spaz, Generic[PlayerType]):
category: Gameplay Classes
- When a PlayerSpaz dies, it delivers a ba.PlayerSpazDeathMessage
+ When a PlayerSpaz dies, it delivers a ba.PlayerDiedMessage
to the current ba.Activity. (unless the death was the result of the
player leaving the game, in which case no message is sent)
@@ -302,16 +264,16 @@ class PlayerSpaz(Spaz, Generic[PlayerType]):
# Only report if both the player and the activity still exist.
if killed and activity is not None and self.getplayer():
activity.handlemessage(
- PlayerSpazDeathMessage(self, killed, killerplayer,
- msg.how))
+ ba.PlayerDiedMessage(self.player, killed, killerplayer,
+ msg.how))
super().handlemessage(msg) # Augment standard behavior.
# Keep track of the player who last hit us for point rewarding.
elif isinstance(msg, ba.HitMessage):
- if msg.source_player:
- srcplayer = ba.playercast_o(self.playertype, msg.source_player)
- self.last_player_attacked_by = srcplayer
+ source_player = msg.get_source_player(self.playertype)
+ if source_player:
+ self.last_player_attacked_by = source_player
self.last_attacked_time = ba.time()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg) # Augment standard behavior.
diff --git a/assets/src/ba_data/python/bastd/actor/spaz.py b/assets/src/ba_data/python/bastd/actor/spaz.py
index 96c713c4..c61827d8 100644
--- a/assets/src/ba_data/python/bastd/actor/spaz.py
+++ b/assets/src/ba_data/python/bastd/actor/spaz.py
@@ -1089,8 +1089,9 @@ class Spaz(ba.Actor):
# If we're cursed, *any* damage blows us up.
if self._cursed and damage > 0:
ba.timer(
- 0.05, ba.WeakCall(self.curse_explode,
- msg.source_player))
+ 0.05,
+ ba.WeakCall(self.curse_explode,
+ msg.get_source_player(ba.Player)))
# if we're frozen, shatter.. otherwise die if we hit zero
if self.frozen and (damage > 200 or self.hitpoints <= 0):
self.shatter()
diff --git a/assets/src/ba_data/python/bastd/actor/spazbot.py b/assets/src/ba_data/python/bastd/actor/spazbot.py
index d0602951..5457cb20 100644
--- a/assets/src/ba_data/python/bastd/actor/spazbot.py
+++ b/assets/src/ba_data/python/bastd/actor/spazbot.py
@@ -574,8 +574,9 @@ class SpazBot(Spaz):
# Keep track of the player who last hit us for point rewarding.
elif isinstance(msg, ba.HitMessage):
- if msg.source_player:
- self.last_player_attacked_by = msg.source_player
+ source_player = msg.get_source_player(ba.Player)
+ if source_player:
+ self.last_player_attacked_by = source_player
self.last_attacked_time = ba.time()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg)
diff --git a/assets/src/ba_data/python/bastd/game/assault.py b/assets/src/ba_data/python/bastd/game/assault.py
index 334571df..cf4821a8 100644
--- a/assets/src/ba_data/python/bastd/game/assault.py
+++ b/assets/src/ba_data/python/bastd/game/assault.py
@@ -29,7 +29,7 @@ import random
from typing import TYPE_CHECKING
import ba
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
+from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.flag import Flag
if TYPE_CHECKING:
@@ -161,9 +161,9 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
self.setup_standard_powerup_drops()
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard.
- self.respawn_player(msg.playerspaz(self).player)
+ self.respawn_player(msg.getplayer(Player))
else:
super().handlemessage(msg)
diff --git a/assets/src/ba_data/python/bastd/game/capturetheflag.py b/assets/src/ba_data/python/bastd/game/capturetheflag.py
index 6b1e028c..f2f6e540 100644
--- a/assets/src/ba_data/python/bastd/game/capturetheflag.py
+++ b/assets/src/ba_data/python/bastd/game/capturetheflag.py
@@ -29,7 +29,7 @@ from typing import TYPE_CHECKING
import ba
from bastd.actor import flag as stdflag
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
+from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING:
@@ -548,10 +548,10 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
self._score_to_win)
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
- self.respawn_player(msg.playerspaz(self).player)
+ self.respawn_player(msg.getplayer(Player))
elif isinstance(msg, stdflag.FlagDeathMessage):
assert isinstance(msg.flag, CTFFlag)
ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))
diff --git a/assets/src/ba_data/python/bastd/game/chosenone.py b/assets/src/ba_data/python/bastd/game/chosenone.py
index e750c390..212f57d5 100644
--- a/assets/src/ba_data/python/bastd/game/chosenone.py
+++ b/assets/src/ba_data/python/bastd/game/chosenone.py
@@ -28,7 +28,7 @@ from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
+from bastd.actor.playerspaz import PlayerSpaz
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Optional, Sequence, Union
@@ -312,12 +312,13 @@ class ChosenOneGame(ba.TeamGameActivity[Player, Team]):
'position')
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
- player = msg.playerspaz(self).player
+ player = msg.getplayer(Player)
if player is self._get_chosen_one_player():
- killerplayer = ba.playercast_o(Player, msg.killerplayer)
+ killerplayer = ba.playercast_o(Player,
+ msg.getkillerplayer(Player))
self._set_chosen_one_player(None if (
killerplayer is None or killerplayer is player
or not killerplayer.is_alive()) else killerplayer)
diff --git a/assets/src/ba_data/python/bastd/game/conquest.py b/assets/src/ba_data/python/bastd/game/conquest.py
index b459e147..11430f35 100644
--- a/assets/src/ba_data/python/bastd/game/conquest.py
+++ b/assets/src/ba_data/python/bastd/game/conquest.py
@@ -30,7 +30,6 @@ from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
-from bastd.actor.playerspaz import PlayerSpazDeathMessage
if TYPE_CHECKING:
from typing import Any, Optional, Type, List, Dict, Sequence, Union
@@ -41,22 +40,30 @@ class ConquestFlag(Flag):
def __init__(self, *args: Any, **keywds: Any):
super().__init__(*args, **keywds)
- self._team: Optional[ba.Team] = None
+ self._team: Optional[Team] = None
self.light: Optional[ba.Node] = None
@property
- def team(self) -> Optional[ba.Team]:
+ def team(self) -> Optional[Team]:
"""The team that owns this flag."""
return self._team
@team.setter
- def team(self, team: ba.Team) -> None:
+ def team(self, team: Team) -> None:
"""Set the team that owns this flag."""
self._team = team
+class Player(ba.Player['Team']):
+ """Our player type for this game."""
+
+
+class Team(ba.Team[Player]):
+ """Our team type for this game."""
+
+
# ba_meta export game
-class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
+class ConquestGame(ba.TeamGameActivity[Player, Team]):
"""A game where teams try to claim all flags on the map."""
name = 'Conquest'
@@ -115,12 +122,12 @@ class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
ba.MusicType.GRAND_ROMP)
super().on_transition_in()
- def on_team_join(self, team: ba.Team) -> None:
+ def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scores()
team.gamedata['flags_held'] = 0
- def on_player_join(self, player: ba.Player) -> None:
+ def on_player_join(self, player: Player) -> None:
player.gamedata['respawn_timer'] = None
# Only spawn if this player's team has a flag currently.
@@ -213,7 +220,7 @@ class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
flag = flagnode.getdelegate()
except Exception:
return # Player may have left and his body hit the flag.
- assert isinstance(player, ba.Player)
+ assert isinstance(player, Player)
assert isinstance(flag, ConquestFlag)
assert flag.light
@@ -236,12 +243,12 @@ class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self.spawn_player(otherplayer)
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
# Respawn only if this team has a flag.
- player = msg.playerspaz(self).player
+ player = msg.getplayer(Player)
if player.team.gamedata['flags_held'] > 0:
self.respawn_player(player)
else:
@@ -250,12 +257,12 @@ class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
else:
super().handlemessage(msg)
- def spawn_player(self, player: ba.Player) -> ba.Actor:
+ def spawn_player(self, player: Player) -> ba.Actor:
# We spawn players at different places based on what flags are held.
return self.spawn_player_spaz(player,
self._get_player_spawn_position(player))
- def _get_player_spawn_position(self, player: ba.Player) -> Sequence[float]:
+ def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
# Iterate until we find a spawn owned by this team.
spawn_count = len(self.map.spawn_by_flag_points)
diff --git a/assets/src/ba_data/python/bastd/game/deathmatch.py b/assets/src/ba_data/python/bastd/game/deathmatch.py
index 71199095..03e48d08 100644
--- a/assets/src/ba_data/python/bastd/game/deathmatch.py
+++ b/assets/src/ba_data/python/bastd/game/deathmatch.py
@@ -27,11 +27,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import ba
-from bastd.actor import playerspaz
-from bastd.actor import spaz as stdspaz
+from bastd.actor.playerspaz import PlayerSpaz
if TYPE_CHECKING:
- from typing import Any, Type, List, Dict, Tuple, Union, Sequence
+ from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional
class Player(ba.Player['Team']):
@@ -42,7 +41,7 @@ class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
- pass
+ self.score = 0
# ba_meta export game
@@ -104,9 +103,14 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]):
from bastd.actor.scoreboard import Scoreboard
super().__init__(settings)
self._scoreboard = Scoreboard()
- self._score_to_win = None
+ self._score_to_win: Optional[int] = None
self._dingsound = ba.getsound('dingSmall')
self._epic_mode = bool(settings['Epic Mode'])
+ self._kills_to_win_per_player = int(
+ settings['Kills to Win Per Player'])
+ self._time_limit = float(settings['Time Limit'])
+ self._allow_negative_scores = bool(
+ settings.get('Allow Negative Scores', False))
# Base class overrides.
self.slow_motion = self._epic_mode
@@ -120,34 +124,30 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]):
return 'kill ${ARG1} enemies', self._score_to_win
def on_team_join(self, team: Team) -> None:
- team.gamedata['score'] = 0
if self.has_begun():
self._update_scoreboard()
def on_begin(self) -> None:
super().on_begin()
- self.setup_standard_time_limit(self.settings_raw['Time Limit'])
+ self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
- if self.teams:
- self._score_to_win = (
- self.settings_raw['Kills to Win Per Player'] *
- max(1, max(len(t.players) for t in self.teams)))
- else:
- self._score_to_win = self.settings_raw['Kills to Win Per Player']
+
+ # Base kills needed to win on the size of the largest team.
+ self._score_to_win = (self._kills_to_win_per_player *
+ max(1, max(len(t.players) for t in self.teams)))
self._update_scoreboard()
def handlemessage(self, msg: Any) -> Any:
- # pylint: disable=too-many-branches
- if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
- player = msg.playerspaz(self).player
+ player = msg.getplayer(Player)
self.respawn_player(player)
- killer = msg.killerplayer
+ killer = msg.getkillerplayer(Player)
if killer is None:
return
@@ -156,41 +156,37 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]):
# In free-for-all, killing yourself loses you a point.
if isinstance(self.session, ba.FreeForAllSession):
- new_score = player.team.gamedata['score'] - 1
- if not self.settings_raw['Allow Negative Scores']:
+ new_score = player.team.score - 1
+ if not self._allow_negative_scores:
new_score = max(0, new_score)
- player.team.gamedata['score'] = new_score
+ player.team.score = new_score
# In teams-mode it gives a point to the other team.
else:
ba.playsound(self._dingsound)
for team in self.teams:
if team is not killer.team:
- team.gamedata['score'] += 1
+ team.score += 1
# Killing someone on another team nets a kill.
else:
- killer.team.gamedata['score'] += 1
+ killer.team.score += 1
ba.playsound(self._dingsound)
# In FFA show scores since its hard to find on the scoreboard.
- try:
- if isinstance(killer.actor, stdspaz.Spaz):
- killer.actor.set_score_text(
- str(killer.team.gamedata['score']) + '/' +
- str(self._score_to_win),
- color=killer.team.color,
- flash=True)
- except Exception:
- pass
+ if isinstance(killer.actor, PlayerSpaz) and killer.actor:
+ killer.actor.set_score_text(str(killer.team.score) + '/' +
+ str(self._score_to_win),
+ color=killer.team.color,
+ flash=True)
self._update_scoreboard()
# If someone has won, set a timer to end shortly.
# (allows the dust to clear and draws to occur if deaths are
# close enough)
- if any(team.gamedata['score'] >= self._score_to_win
- for team in self.teams):
+ assert self._score_to_win is not None
+ if any(team.score >= self._score_to_win for team in self.teams):
ba.timer(0.5, self.end_game)
else:
@@ -198,11 +194,11 @@ class DeathMatchGame(ba.TeamGameActivity[Player, Team]):
def _update_scoreboard(self) -> None:
for team in self.teams:
- self._scoreboard.set_team_value(team, team.gamedata['score'],
+ self._scoreboard.set_team_value(team, team.score,
self._score_to_win)
def end_game(self) -> None:
results = ba.TeamGameResults()
for team in self.teams:
- results.set_team_score(team, team.gamedata['score'])
+ results.set_team_score(team, team.score)
self.end(results=results)
diff --git a/assets/src/ba_data/python/bastd/game/easteregghunt.py b/assets/src/ba_data/python/bastd/game/easteregghunt.py
index 79e5382b..96ea4ef9 100644
--- a/assets/src/ba_data/python/bastd/game/easteregghunt.py
+++ b/assets/src/ba_data/python/bastd/game/easteregghunt.py
@@ -29,9 +29,9 @@ import random
from typing import TYPE_CHECKING
import ba
-from bastd.actor import bomb
-from bastd.actor import playerspaz
-from bastd.actor import spazbot
+from bastd.actor.bomb import Bomb
+from bastd.actor.playerspaz import PlayerSpaz
+from bastd.actor.spazbot import BotSet, BouncyBot, SpazBotDeathMessage
from bastd.actor.onscreencountdown import OnScreenCountdown
from bastd.actor.scoreboard import Scoreboard
@@ -39,8 +39,16 @@ if TYPE_CHECKING:
from typing import Any, Type, Dict, List, Tuple, Optional
+class Player(ba.Player['Team']):
+ """Our player type for this game."""
+
+
+class Team(ba.Team[Player]):
+ """Our team type for this game."""
+
+
# ba_meta export game
-class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
+class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
"""A game where score is based on collecting eggs."""
name = 'Easter Egg Hunt'
@@ -78,7 +86,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self._eggs: List[Egg] = []
self._update_timer: Optional[ba.Timer] = None
self._countdown: Optional[OnScreenCountdown] = None
- self._bots: Optional[spazbot.BotSet] = None
+ self._bots: Optional[BotSet] = None
# Called when our game is transitioning in but not ready to start.
# ..we can go ahead and set our music and whatnot.
@@ -87,7 +95,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self.default_music = ba.MusicType.FORWARD_MARCH
super().on_transition_in()
- def on_team_join(self, team: ba.Team) -> None:
+ def on_team_join(self, team: Team) -> None:
team.gamedata['score'] = 0
if self.has_begun():
self._update_scoreboard()
@@ -106,23 +114,21 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self._update_timer = ba.Timer(0.25, self._update, repeat=True)
self._countdown = OnScreenCountdown(60, endcall=self.end_game)
ba.timer(4.0, self._countdown.start)
- self._bots = spazbot.BotSet()
+ self._bots = BotSet()
# Spawn evil bunny in co-op only.
if isinstance(self.session, ba.CoopSession) and self._pro_mode:
self._spawn_evil_bunny()
# Overriding the default character spawning.
- def spawn_player(self, player: ba.Player) -> ba.Actor:
+ def spawn_player(self, player: Player) -> ba.Actor:
spaz = self.spawn_player_spaz(player)
spaz.connect_controls_to_player()
return spaz
def _spawn_evil_bunny(self) -> None:
assert self._bots is not None
- self._bots.spawn_bot(spazbot.BouncyBot,
- pos=(6, 4, -7.8),
- spawn_time=10.0)
+ self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)
def _on_egg_player_collide(self) -> None:
if not self.has_ended():
@@ -132,7 +138,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
egg = egg_node.getdelegate()
assert isinstance(egg, Egg)
spaz = playernode.getdelegate()
- assert isinstance(spaz, playerspaz.PlayerSpaz)
+ assert isinstance(spaz, PlayerSpaz)
player = (spaz.getplayer()
if hasattr(spaz, 'getplayer') else None)
if player and egg:
@@ -184,8 +190,8 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
# Occasionally spawn a land-mine in addition.
if self._pro_mode and random.random() < 0.25:
- mine = bomb.Bomb(position=(xpos, ypos, zpos),
- bomb_type='land_mine').autoretain()
+ mine = Bomb(position=(xpos, ypos, zpos),
+ bomb_type='land_mine').autoretain()
mine.arm()
else:
self._eggs.append(Egg(position=(xpos, ypos, zpos)))
@@ -194,12 +200,12 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def handlemessage(self, msg: Any) -> Any:
# Respawn dead players.
- if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
from bastd.actor import respawnicon
# Augment standard behavior.
super().handlemessage(msg)
- player = msg.playerspaz(self).getplayer()
+ player = msg.getplayer(Player)
if not player:
return
self.stats.player_was_killed(player)
@@ -213,7 +219,7 @@ class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
player, respawn_time)
# Whenever our evil bunny dies, respawn him and spew some eggs.
- elif isinstance(msg, spazbot.SpazBotDeathMessage):
+ elif isinstance(msg, SpazBotDeathMessage):
self._spawn_evil_bunny()
assert msg.badguy.node
pos = msg.badguy.node.position
diff --git a/assets/src/ba_data/python/bastd/game/elimination.py b/assets/src/ba_data/python/bastd/game/elimination.py
index 97361733..b38eff49 100644
--- a/assets/src/ba_data/python/bastd/game/elimination.py
+++ b/assets/src/ba_data/python/bastd/game/elimination.py
@@ -28,8 +28,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import ba
-from bastd.actor import playerspaz
-from bastd.actor import spaz
+from bastd.actor.spaz import get_factory
if TYPE_CHECKING:
from typing import (Any, Tuple, Dict, Type, List, Sequence, Optional,
@@ -489,11 +488,11 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]):
return sum(player.lives for player in team.players)
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
- player: Player = msg.playerspaz(self).player
+ player: Player = msg.getplayer(Player)
player.lives -= 1
if player.lives < 0:
@@ -509,7 +508,7 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]):
# Play big death sound on our last death
# or for every one in solo mode.
if self._solo_mode or player.lives == 0:
- ba.playsound(spaz.get_factory().single_player_death_sound)
+ ba.playsound(get_factory().single_player_death_sound)
# If we hit zero lives, we're dead (and our team might be too).
if player.lives == 0:
diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py
index 2e281b84..bfb77ec1 100644
--- a/assets/src/ba_data/python/bastd/game/football.py
+++ b/assets/src/ba_data/python/bastd/game/football.py
@@ -33,8 +33,9 @@ import ba
from bastd.actor import spazbot
from bastd.actor import flag as stdflag
from bastd.actor.bomb import TNTSpawner
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
+from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
+from bastd.actor.respawnicon import RespawnIcon
if TYPE_CHECKING:
from typing import Any, List, Type, Dict, Sequence, Optional, Union
@@ -268,10 +269,10 @@ class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
msg.flag.held_count -= 1
# Respawn dead players if they're still in the game.
- elif isinstance(msg, PlayerSpazDeathMessage):
+ elif isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
- self.respawn_player(msg.playerspaz(self).player)
+ self.respawn_player(msg.getplayer(Player))
# Respawn dead flags.
elif isinstance(msg, stdflag.FlagDeathMessage):
@@ -798,11 +799,10 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
def handlemessage(self, msg: Any) -> Any:
""" handle high-level game messages """
- if isinstance(msg, PlayerSpazDeathMessage):
- from bastd.actor import respawnicon
+ if isinstance(msg, ba.PlayerDiedMessage):
# Respawn dead players.
- player = msg.playerspaz(self).player
+ player = msg.getplayer(Player)
self.stats.player_was_killed(player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
@@ -810,8 +810,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
# Respawn them shortly.
player.gamedata['respawn_timer'] = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player))
- player.gamedata['respawn_icon'] = respawnicon.RespawnIcon(
- player, respawn_time)
+ player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time)
# Augment standard behavior.
super().handlemessage(msg)
diff --git a/assets/src/ba_data/python/bastd/game/hockey.py b/assets/src/ba_data/python/bastd/game/hockey.py
index de4a813a..da8386fb 100644
--- a/assets/src/ba_data/python/bastd/game/hockey.py
+++ b/assets/src/ba_data/python/bastd/game/hockey.py
@@ -28,7 +28,6 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import ba
-from bastd.actor import playerspaz
if TYPE_CHECKING:
from typing import Any, Sequence, Dict, Type, List, Optional, Union
@@ -50,7 +49,7 @@ class Puck(ba.Actor):
# Spawn just above the provided point.
self._spawn_pos = (position[0], position[1] + 1.0, position[2])
- self.last_players_to_touch: Dict[int, ba.Player] = {}
+ self.last_players_to_touch: Dict[int, Player] = {}
self.scored = False
assert activity is not None
assert isinstance(activity, HockeyGame)
@@ -94,18 +93,26 @@ class Puck(ba.Actor):
msg.force_direction[2])
# If this hit came from a player, log them as the last to touch us.
- if msg.source_player is not None:
+ splayer = msg.get_source_player(Player)
+ if splayer is not None:
activity = self._activity()
if activity:
- if msg.source_player in activity.players:
- self.last_players_to_touch[
- msg.source_player.team.id] = msg.source_player
+ if splayer in activity.players:
+ self.last_players_to_touch[splayer.team.id] = splayer
else:
super().handlemessage(msg)
+class Player(ba.Player['Team']):
+ """Our player type for this game."""
+
+
+class Team(ba.Team[Player]):
+ """Our team type for this game."""
+
+
# ba_meta export game
-class HockeyGame(ba.TeamGameActivity[ba.Player, ba.Team]):
+class HockeyGame(ba.TeamGameActivity[Player, Team]):
"""Ice hockey game."""
name = 'Hockey'
@@ -234,7 +241,7 @@ class HockeyGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self._update_scoreboard()
ba.playsound(self._chant_sound)
- def on_team_join(self, team: ba.Team) -> None:
+ def on_team_join(self, team: Team) -> None:
team.gamedata['score'] = 0
self._update_scoreboard()
@@ -246,7 +253,7 @@ class HockeyGame(ba.TeamGameActivity[ba.Player, ba.Team]):
player = playernode.getdelegate().getplayer()
except Exception:
player = puck = None
- assert isinstance(player, ba.Player)
+ assert isinstance(player, Player)
assert isinstance(puck, Puck)
if player and puck:
puck.last_players_to_touch[player.team.id] = player
@@ -330,10 +337,10 @@ class HockeyGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def handlemessage(self, msg: Any) -> Any:
# Respawn dead players if they're still in the game.
- if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior...
super().handlemessage(msg)
- self.respawn_player(msg.playerspaz(self).player)
+ self.respawn_player(msg.getplayer(Player))
# Respawn dead pucks.
elif isinstance(msg, PuckDeathMessage):
diff --git a/assets/src/ba_data/python/bastd/game/keepaway.py b/assets/src/ba_data/python/bastd/game/keepaway.py
index b3ea1d54..9e924b20 100644
--- a/assets/src/ba_data/python/bastd/game/keepaway.py
+++ b/assets/src/ba_data/python/bastd/game/keepaway.py
@@ -29,9 +29,9 @@ from enum import Enum
from typing import TYPE_CHECKING
import ba
+from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.flag import (Flag, FlagDroppedMessage, FlagDeathMessage,
FlagPickedUpMessage)
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Optional, Sequence, Union
@@ -266,10 +266,10 @@ class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
countdown=True)
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
- self.respawn_player(msg.playerspaz(self).player)
+ self.respawn_player(msg.getplayer(Player))
elif isinstance(msg, FlagDeathMessage):
self._spawn_flag()
elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
diff --git a/assets/src/ba_data/python/bastd/game/kingofthehill.py b/assets/src/ba_data/python/bastd/game/kingofthehill.py
index b69be0f4..a20a721a 100644
--- a/assets/src/ba_data/python/bastd/game/kingofthehill.py
+++ b/assets/src/ba_data/python/bastd/game/kingofthehill.py
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
+from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING:
@@ -272,11 +272,11 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
countdown=True)
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment default.
# No longer can count as time_at_flag once dead.
- player = msg.playerspaz(self).player
+ player = msg.getplayer(Player)
player.time_at_flag = 0
self._update_flag_state()
self.respawn_player(player)
diff --git a/assets/src/ba_data/python/bastd/game/meteorshower.py b/assets/src/ba_data/python/bastd/game/meteorshower.py
index 53d404ec..fac0f292 100644
--- a/assets/src/ba_data/python/bastd/game/meteorshower.py
+++ b/assets/src/ba_data/python/bastd/game/meteorshower.py
@@ -30,7 +30,6 @@ from typing import TYPE_CHECKING
import ba
from bastd.actor.bomb import Bomb
-from bastd.actor.playerspaz import PlayerSpazDeathMessage
from bastd.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
@@ -151,7 +150,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
# Various high-level game events come through this method.
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
@@ -160,7 +159,7 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
# Record the player's moment of death.
# assert isinstance(msg.spaz.player
- msg.playerspaz(self).player.death_time = curtime
+ msg.getplayer(Player).death_time = curtime
# In co-op mode, end the game the instant everyone dies
# (more accurate looking).
diff --git a/assets/src/ba_data/python/bastd/game/ninjafight.py b/assets/src/ba_data/python/bastd/game/ninjafight.py
index 93c9e26a..52503bd9 100644
--- a/assets/src/ba_data/python/bastd/game/ninjafight.py
+++ b/assets/src/ba_data/python/bastd/game/ninjafight.py
@@ -29,9 +29,8 @@ import random
from typing import TYPE_CHECKING
import ba
-from bastd.actor import onscreentimer
-from bastd.actor import playerspaz
from bastd.actor import spazbot
+from bastd.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
from typing import Any, Type, Dict, List, Optional
@@ -76,7 +75,7 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
super().__init__(settings)
self._winsound = ba.getsound('score')
self._won = False
- self._timer: Optional[onscreentimer.OnScreenTimer] = None
+ self._timer: Optional[OnScreenTimer] = None
self._bots = spazbot.BotSet()
# Called when our game is transitioning in but not ready to begin;
@@ -95,7 +94,7 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
self.setup_standard_powerup_drops()
# Make our on-screen timer and start it roughly when our bots appear.
- self._timer = onscreentimer.OnScreenTimer()
+ self._timer = OnScreenTimer()
ba.timer(4.0, self._timer.start)
# Spawn some baddies.
@@ -146,10 +145,9 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
def handlemessage(self, msg: Any) -> Any:
# A player has died.
- if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
- super().handlemessage(msg) # do standard stuff
- self.respawn_player(
- msg.playerspaz(self).player) # kick off a respawn
+ if isinstance(msg, ba.PlayerDiedMessage):
+ super().handlemessage(msg) # Augment standard behavior.
+ self.respawn_player(msg.getplayer(Player))
# A spaz-bot has died.
elif isinstance(msg, spazbot.SpazBotDeathMessage):
diff --git a/assets/src/ba_data/python/bastd/game/onslaught.py b/assets/src/ba_data/python/bastd/game/onslaught.py
index 11856451..8f58c609 100644
--- a/assets/src/ba_data/python/bastd/game/onslaught.py
+++ b/assets/src/ba_data/python/bastd/game/onslaught.py
@@ -1164,10 +1164,9 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
self._score += msg.score
self._update_scores()
- elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
+ elif isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard behavior.
- player = msg.playerspaz(self).getplayer()
- assert player is not None
+ player = msg.getplayer(Player)
self._a_player_has_been_hurt = True
# Make note with the player when they can respawn:
diff --git a/assets/src/ba_data/python/bastd/game/race.py b/assets/src/ba_data/python/bastd/game/race.py
index 86950240..0bd2bac9 100644
--- a/assets/src/ba_data/python/bastd/game/race.py
+++ b/assets/src/ba_data/python/bastd/game/race.py
@@ -31,7 +31,7 @@ from dataclasses import dataclass
import ba
from bastd.actor.bomb import Bomb
-from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
+from bastd.actor.playerspaz import PlayerSpaz
if TYPE_CHECKING:
from typing import (Any, Type, Tuple, List, Sequence, Optional, Dict,
@@ -734,12 +734,12 @@ class RaceGame(ba.TeamGameActivity[Player, Team]):
ba.DualTeamSession))
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
# Augment default behavior.
super().handlemessage(msg)
- player = msg.playerspaz(self).getplayer()
+ player = msg.getplayer(Player)
if not player:
- ba.print_error('got no player in PlayerSpazDeathMessage')
+ ba.print_error('got no player in PlayerDiedMessage')
return
if not player.gamedata['finished']:
self.respawn_player(player, respawn_time=1)
diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/assets/src/ba_data/python/bastd/game/runaround.py
index 8e7d360f..6f985b9e 100644
--- a/assets/src/ba_data/python/bastd/game/runaround.py
+++ b/assets/src/ba_data/python/bastd/game/runaround.py
@@ -29,16 +29,24 @@ import random
from typing import TYPE_CHECKING
import ba
-from bastd.actor import playerspaz
from bastd.actor import spazbot
from bastd.actor.bomb import TNTSpawner
from bastd.actor.scoreboard import Scoreboard
+from bastd.actor.respawnicon import RespawnIcon
if TYPE_CHECKING:
from typing import Type, Any, List, Dict, Tuple, Sequence, Optional
-class RunaroundGame(ba.CoopGameActivity[ba.Player, ba.Team]):
+class Player(ba.Player['Team']):
+ """Our player type for this game."""
+
+
+class Team(ba.Team[Player]):
+ """Our team type for this game."""
+
+
+class RunaroundGame(ba.CoopGameActivity[Player, Team]):
"""Game involving trying to bomb bots as they walk through the map."""
name = 'Runaround'
@@ -457,7 +465,7 @@ class RunaroundGame(ba.CoopGameActivity[ba.Player, ba.Team]):
self._lives_text.node.text = str(self._lives)
self._bots.start_moving()
- def spawn_player(self, player: ba.Player) -> ba.Actor:
+ def spawn_player(self, player: Player) -> ba.Actor:
pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
self._spawn_center[1],
self._spawn_center[2] + random.uniform(-1.5, 1.5))
@@ -1118,16 +1126,9 @@ class RunaroundGame(ba.CoopGameActivity[ba.Player, ba.Team]):
self._update_scores()
# Respawn dead players.
- elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
- from bastd.actor import respawnicon
+ elif isinstance(msg, ba.PlayerDiedMessage):
self._a_player_has_been_killed = True
- player = msg.playerspaz(self).getplayer()
- if player is None:
- ba.print_error('FIXME: getplayer() should no'
- ' longer ever be returning None')
- return
- if not player:
- return
+ player = msg.getplayer(Player)
self.stats.player_was_killed(player)
# Respawn them shortly.
@@ -1135,8 +1136,7 @@ class RunaroundGame(ba.CoopGameActivity[ba.Player, ba.Team]):
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
player.gamedata['respawn_timer'] = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player))
- player.gamedata['respawn_icon'] = respawnicon.RespawnIcon(
- player, respawn_time)
+ player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time)
elif isinstance(msg, spazbot.SpazBotDeathMessage):
if msg.how is ba.DeathType.REACHED_GOAL:
diff --git a/assets/src/ba_data/python/bastd/game/targetpractice.py b/assets/src/ba_data/python/bastd/game/targetpractice.py
index d34ee538..33972629 100644
--- a/assets/src/ba_data/python/bastd/game/targetpractice.py
+++ b/assets/src/ba_data/python/bastd/game/targetpractice.py
@@ -29,7 +29,6 @@ import random
from typing import TYPE_CHECKING
import ba
-from bastd.actor.playerspaz import PlayerSpazDeathMessage
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Optional, Sequence
@@ -193,9 +192,9 @@ class TargetPracticeGame(ba.TeamGameActivity[Player, Team]):
def handlemessage(self, msg: Any) -> Any:
# When players die, respawn them.
- if isinstance(msg, PlayerSpazDeathMessage):
+ if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Do standard stuff.
- player = msg.playerspaz(self).getplayer()
+ player = msg.getplayer(Player)
assert player is not None
self.respawn_player(player) # Kick off a respawn.
elif isinstance(msg, Target.TargetHitMessage):
diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/assets/src/ba_data/python/bastd/game/thelaststand.py
index 3eb3eaad..147bd279 100644
--- a/assets/src/ba_data/python/bastd/game/thelaststand.py
+++ b/assets/src/ba_data/python/bastd/game/thelaststand.py
@@ -257,14 +257,8 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
self._scoreboard.set_team_value(self.teams[0], score, max_score=None)
def handlemessage(self, msg: Any) -> Any:
- if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
- player = msg.playerspaz(self).getplayer()
- if player is None:
- ba.print_error('FIXME: getplayer() should no longer '
- 'ever be returning None.')
- return
- if not player:
- return
+ if isinstance(msg, ba.PlayerDiedMessage):
+ player = msg.getplayer(Player)
self.stats.player_was_killed(player)
ba.timer(0.1, self._checkroundover)
diff --git a/assets/src/ba_data/python/bastd/ui/settings/gamepad.py b/assets/src/ba_data/python/bastd/ui/settings/gamepad.py
index 046f9b12..758b432f 100644
--- a/assets/src/ba_data/python/bastd/ui/settings/gamepad.py
+++ b/assets/src/ba_data/python/bastd/ui/settings/gamepad.py
@@ -836,7 +836,7 @@ class AwaitGamepadInputWindow(ba.Window):
assert isinstance(input_device, ba.InputDevice)
# Update - we now allow *any* input device of this type.
- if input_device.exists and input_device.name == self._input.name:
+ if input_device.exists() and input_device.name == self._input.name:
self._callback(self._capture_button, event, self)
def _decrement(self) -> None:
diff --git a/docs/ba_module.md b/docs/ba_module.md
index 8ec9e7f4..7a27b536 100644
--- a/docs/ba_module.md
+++ b/docs/ba_module.md
@@ -50,6 +50,7 @@
ba.cameraflash()
ba.camerashake()
ba.emitfx()
+ ba.existing()
ba.get_collision_info()
ba.getactivity()
ba.getnodes()
@@ -127,6 +128,7 @@
ba.OutOfBoundsMessage
ba.PickedUpMessage
ba.PickUpMessage
+ ba.PlayerDiedMessage
ba.PlayerScoredMessage
ba.PowerupAcceptMessage
ba.PowerupMessage
@@ -2453,12 +2455,22 @@ and short description of the game.
Methods:
+
-
ba.HitMessage(srcnode: 'ba.Node' = None, pos: 'Sequence[float]' = None, velocity: 'Sequence[float]' = None, magnitude: 'float' = 1.0, velocity_magnitude: 'float' = 0.0, radius: 'float' = 1.0, source_player: 'ba.Player' = None, kick_back: 'float' = 1.0, flat_damage: 'float' = None, hit_type: 'str' = 'generic', force_direction: 'Sequence[float]' = None, hit_subtype: 'str' = 'default')
Instantiate a message with given values.
+
+-
+
get_source_player(self, playertype: Type[PlayerType]) -> Optional[PlayerType]
+
+Return the spaz that died.
+
+The current activity is required as an argument so the exact type of
+PlayerSpaz can be determined by the type checker.
+
@@ -2493,7 +2505,7 @@ and short description of the game.
Category: Gameplay Classes
Attributes:
-
+
-
bool
@@ -2506,11 +2518,6 @@ and short description of the game.
This is only meaningful for remote client inputs; for
all local devices this will be -1.
-
--
-
bool
-Whether the underlying device for this object is still present.
-
-
int
@@ -2553,8 +2560,14 @@ prefs, etc.
Methods:
-
+
+-
+
exists() -> bool
+
+Return whether the underlying device for this object is still present.
+
+
-
get_account_name(full: bool) -> str
@@ -3717,18 +3730,8 @@ even if myactor is set to None.
Attributes:
-
+
--
-
bool
-Whether the underlying player still exists.
-
- Most functionality will fail on a nonexistent player.
- Note that you can also use the boolean operator for this same
- functionality, so a statement such as "if player" will do
- the right thing both for Player objects and values of None.
-
-
-
ba.Node
A ba.Node of type 'player' associated with this Player.
@@ -3745,7 +3748,7 @@ even if myactor is set to None.
Methods:
-
+
-
assign_input_call(self, inputtype: Union[str, Tuple[str, ...]], call: Callable) -> None
@@ -3761,6 +3764,17 @@ Valid type values are: 'jumpPress', 'jumpRelease', 'punchPress',
'rightRelease', 'run', 'flyPress', 'flyRelease', 'startPress',
'startRelease'
+
+-
+
exists(self) -> bool
+
+Whether the underlying player still exists.
+
+Most functionality will fail on a nonexistent player.
+Note that you can also use the boolean operator for this same
+functionality, so a statement such as "if player" will do
+the right thing both for Player objects and values of None.
+
-
get_icon(self) -> Dict[str, Any]
@@ -3803,6 +3817,56 @@ is_alive() method return True. False is returned otherwise.
Set the player's associated ba.Actor.
+
+
+
+<top level class>
+
+A message saying a ba.PlayerSpaz has died.
+
+Category: Message Classes
+
+Attributes:
+
+
+-
+
ba.DeathType
+The particular type of death.
+
+
+-
+
bool
+If True, the spaz was killed;
+If False, they left the game or the round ended.
+
+
+
+Methods:
+
+
+-
+
ba.PlayerDiedMessage(player: ba.Player, was_killed: bool, killerplayer: Optional[ba.Player], how: ba.DeathType)
+
+Instantiate a message with the given values.
+
+
+-
+
getkillerplayer(self, playertype: Type[PlayerType]) -> Optional[PlayerType]
+
+Return the ba.Player responsible for the killing, if any.
+
+Pass the Player type being used by the current game.
+
+
+-
+
getplayer(self, playertype: Type[PlayerType]) -> PlayerType
+
+Return the spaz that died.
+
+The current activity is required as an argument so the exact type of
+PlayerSpaz can be determined by the type checker.
+
@@ -4297,12 +4361,12 @@ provided to your Session/Activity instances.
Be aware that, like ba.Nodes, ba.SessionPlayer objects are 'weak'
references under-the-hood; a player can leave the game at
any point. For this reason, you should make judicious use of the
-ba.SessionPlayer.exists attribute (or boolean operator) to ensure
+ba.SessionPlayer.exists() method (or boolean operator) to ensure
that a SessionPlayer is still present if retaining references to one
for any length of time.
Attributes:
-
+
-
str
@@ -4314,16 +4378,6 @@ for any length of time.
The base color for this Player.
In team games this will match the ba.SessionTeam's color.
-
--
-
bool
-Whether the player still exists.
-Most functionality will fail on a nonexistent player.
-
-Note that you can also use the boolean operator for this same
-functionality, so a statement such as "if player" will do
-the right thing both for Player objects and values of None.
-
-
Dict
@@ -4349,6 +4403,10 @@ who may all share the same team (primary) color.
int
The unique numeric ID of the Player.
+Note that you can also use the boolean operator for this same
+functionality, so a statement such as "if player" will do
+the right thing both for Player objects and values of None.
+
-
bool
@@ -4372,7 +4430,7 @@ is still in its lobby selecting a team/etc. then a
Methods:
-
+
-
assign_input_call(type: Union[str, Tuple[str, ...]],
@@ -4386,6 +4444,12 @@ Valid type values are: 'jumpPress', 'jumpRelease', 'punchPress',
'rightRelease', 'run', 'flyPress', 'flyRelease', 'startPress',
'startRelease'
+
+-
+
exists() -> bool
+
+Return whether the underlying player is still in the game.
+
-
get_account_id() -> str
@@ -5622,6 +5686,23 @@ the background and just looks pretty; it does not affect gameplay.
Note that the actual amount emitted may vary depending on graphics
settings, exiting element counts, or other factors.
+
+existing(obj: Optional[ExistableType]) -> Optional[ExistableType]
+
+Convert invalid references to None.
+
+Category: Gameplay Functions
+
+To best support type checking, it is important that invalid references
+not be passed around and instead get converted to values of None.
+That way the type checker can properly flag attempts to pass dead
+objects into functions expecting only live ones, etc.
+This call can be used on any 'existable' object (one with an exists()
+method) and will convert it to a None value if it does not exist.
+For more info, see notes on 'existables' here:
+https://github.com/efroemling/ballistica/wiki/Coding-Style-Guide
+
get_collision_info(*args: Any) -> Any