More type checking cleanup

This commit is contained in:
Eric Froemling 2020-05-20 18:36:21 -07:00
parent e32aa01088
commit 783d1ccd19
33 changed files with 440 additions and 275 deletions

View File

@ -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"
}

View File

@ -578,6 +578,8 @@
<w>execlocals</w>
<w>executils</w>
<w>exhash</w>
<w>existable</w>
<w>existables</w>
<w>expatbuilder</w>
<w>expatreader</w>
<w>explodable</w>
@ -752,6 +754,7 @@
<w>getcurrency</w>
<w>getcwd</w>
<w>getdata</w>
<w>getkillerplayer</w>
<w>getlevelname</w>
<w>getmaps</w>
<w>getmodel</w>
@ -1688,6 +1691,7 @@
<w>spinoff</w>
<w>spinoffdata</w>
<w>spinoffs</w>
<w>splayer</w>
<w>splitlen</w>
<w>splitnumstr</w>
<w>squadcore</w>

View File

@ -50,6 +50,7 @@
<scope name="UncheckedPython" level="WEAK WARNING" enabled="false" />
</inspection_tool>
<inspection_tool class="PyTypeCheckerInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyTypeHintsInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnreachableCodeInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
<scope name="PyIgnoreUnresolved" level="WARNING" enabled="false">

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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.

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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):

View File

@ -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)):

View File

@ -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)

View File

@ -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).

View File

@ -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):

View File

@ -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:

View File

@ -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)

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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:

View File

@ -50,6 +50,7 @@
<li><a href="#function_ba_cameraflash">ba.cameraflash()</a></li>
<li><a href="#function_ba_camerashake">ba.camerashake()</a></li>
<li><a href="#function_ba_emitfx">ba.emitfx()</a></li>
<li><a href="#function_ba_existing">ba.existing()</a></li>
<li><a href="#function_ba_get_collision_info">ba.get_collision_info()</a></li>
<li><a href="#function_ba_getactivity">ba.getactivity()</a></li>
<li><a href="#function_ba_getnodes">ba.getnodes()</a></li>
@ -127,6 +128,7 @@
<li><a href="#class_ba_OutOfBoundsMessage">ba.OutOfBoundsMessage</a></li>
<li><a href="#class_ba_PickedUpMessage">ba.PickedUpMessage</a></li>
<li><a href="#class_ba_PickUpMessage">ba.PickUpMessage</a></li>
<li><a href="#class_ba_PlayerDiedMessage">ba.PlayerDiedMessage</a></li>
<li><a href="#class_ba_PlayerScoredMessage">ba.PlayerScoredMessage</a></li>
<li><a href="#class_ba_PowerupAcceptMessage">ba.PowerupAcceptMessage</a></li>
<li><a href="#class_ba_PowerupMessage">ba.PowerupMessage</a></li>
@ -2453,12 +2455,22 @@ and short description of the game.</p>
</p>
<h3>Methods:</h3>
<h5><a href="#method_ba_HitMessage____init__">&lt;constructor&gt;</a>, <a href="#method_ba_HitMessage__get_source_player">get_source_player()</a></h5>
<dl>
<dt><h4><a name="method_ba_HitMessage____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.HitMessage(srcnode: '<a href="#class_ba_Node">ba.Node</a>' = None, pos: 'Sequence[float]' = None, velocity: 'Sequence[float]' = None, magnitude: 'float' = 1.0, velocity_magnitude: 'float' = 0.0, radius: 'float' = 1.0, source_player: '<a href="#class_ba_Player">ba.Player</a>' = None, kick_back: 'float' = 1.0, flat_damage: 'float' = None, hit_type: 'str' = 'generic', force_direction: 'Sequence[float]' = None, hit_subtype: 'str' = 'default')</span></p>
<p>Instantiate a message with given values.</p>
</dd>
<dt><h4><a name="method_ba_HitMessage__get_source_player">get_source_player()</a></dt></h4><dd>
<p><span>get_source_player(self, playertype: Type[PlayerType]) -&gt; Optional[PlayerType]</span></p>
<p>Return the spaz that died.</p>
<p>The current activity is required as an argument so the exact type of
PlayerSpaz can be determined by the type checker.</p>
</dd>
</dl>
<hr>
@ -2493,7 +2505,7 @@ and short description of the game.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_InputDevice__allows_configuring">allows_configuring</a>, <a href="#attr_ba_InputDevice__client_id">client_id</a>, <a href="#attr_ba_InputDevice__exists">exists</a>, <a href="#attr_ba_InputDevice__id">id</a>, <a href="#attr_ba_InputDevice__instance_number">instance_number</a>, <a href="#attr_ba_InputDevice__is_controller_app">is_controller_app</a>, <a href="#attr_ba_InputDevice__is_remote_client">is_remote_client</a>, <a href="#attr_ba_InputDevice__name">name</a>, <a href="#attr_ba_InputDevice__player">player</a>, <a href="#attr_ba_InputDevice__unique_identifier">unique_identifier</a></h5>
<h5><a href="#attr_ba_InputDevice__allows_configuring">allows_configuring</a>, <a href="#attr_ba_InputDevice__client_id">client_id</a>, <a href="#attr_ba_InputDevice__id">id</a>, <a href="#attr_ba_InputDevice__instance_number">instance_number</a>, <a href="#attr_ba_InputDevice__is_controller_app">is_controller_app</a>, <a href="#attr_ba_InputDevice__is_remote_client">is_remote_client</a>, <a href="#attr_ba_InputDevice__name">name</a>, <a href="#attr_ba_InputDevice__player">player</a>, <a href="#attr_ba_InputDevice__unique_identifier">unique_identifier</a></h5>
<dl>
<dt><h4><a name="attr_ba_InputDevice__allows_configuring">allows_configuring</a></h4></dt><dd>
<p><span> bool</span></p>
@ -2506,11 +2518,6 @@ and short description of the game.</p>
This is only meaningful for remote client inputs; for
all local devices this will be -1.</p>
</dd>
<dt><h4><a name="attr_ba_InputDevice__exists">exists</a></h4></dt><dd>
<p><span> bool</span></p>
<p>Whether the underlying device for this object is still present.</p>
</dd>
<dt><h4><a name="attr_ba_InputDevice__id">id</a></h4></dt><dd>
<p><span> int</span></p>
@ -2553,8 +2560,14 @@ prefs, etc.</p>
</dd>
</dl>
<h3>Methods:</h3>
<h5><a href="#method_ba_InputDevice__get_account_name">get_account_name()</a>, <a href="#method_ba_InputDevice__get_axis_name">get_axis_name()</a>, <a href="#method_ba_InputDevice__get_button_name">get_button_name()</a></h5>
<h5><a href="#method_ba_InputDevice__exists">exists()</a>, <a href="#method_ba_InputDevice__get_account_name">get_account_name()</a>, <a href="#method_ba_InputDevice__get_axis_name">get_axis_name()</a>, <a href="#method_ba_InputDevice__get_button_name">get_button_name()</a></h5>
<dl>
<dt><h4><a name="method_ba_InputDevice__exists">exists()</a></dt></h4><dd>
<p><span>exists() -&gt; bool</span></p>
<p>Return whether the underlying device for this object is still present.</p>
</dd>
<dt><h4><a name="method_ba_InputDevice__get_account_name">get_account_name()</a></dt></h4><dd>
<p><span>get_account_name(full: bool) -&gt; str</span></p>
@ -3717,18 +3730,8 @@ even if myactor is set to None.</p>
</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_Player__exists">exists</a>, <a href="#attr_ba_Player__node">node</a>, <a href="#attr_ba_Player__sessionplayer">sessionplayer</a></h5>
<h5><a href="#attr_ba_Player__node">node</a>, <a href="#attr_ba_Player__sessionplayer">sessionplayer</a></h5>
<dl>
<dt><h4><a name="attr_ba_Player__exists">exists</a></h4></dt><dd>
<p><span>bool</span></p>
<p>Whether the underlying player still exists.</p>
<p> 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.</p>
</dd>
<dt><h4><a name="attr_ba_Player__node">node</a></h4></dt><dd>
<p><span><a href="#class_ba_Node">ba.Node</a></span></p>
<p>A <a href="#class_ba_Node">ba.Node</a> of type 'player' associated with this Player.</p>
@ -3745,7 +3748,7 @@ even if myactor is set to None.</p>
</dd>
</dl>
<h3>Methods:</h3>
<h5><a href="#method_ba_Player__assign_input_call">assign_input_call()</a>, <a href="#method_ba_Player__get_icon">get_icon()</a>, <a href="#method_ba_Player__get_name">get_name()</a>, <a href="#method_ba_Player__is_alive">is_alive()</a>, <a href="#method_ba_Player__reset_input">reset_input()</a>, <a href="#method_ba_Player__set_actor">set_actor()</a></h5>
<h5><a href="#method_ba_Player__assign_input_call">assign_input_call()</a>, <a href="#method_ba_Player__exists">exists()</a>, <a href="#method_ba_Player__get_icon">get_icon()</a>, <a href="#method_ba_Player__get_name">get_name()</a>, <a href="#method_ba_Player__is_alive">is_alive()</a>, <a href="#method_ba_Player__reset_input">reset_input()</a>, <a href="#method_ba_Player__set_actor">set_actor()</a></h5>
<dl>
<dt><h4><a name="method_ba_Player__assign_input_call">assign_input_call()</a></dt></h4><dd>
<p><span>assign_input_call(self, inputtype: Union[str, Tuple[str, ...]], call: Callable) -&gt; None</span></p>
@ -3761,6 +3764,17 @@ Valid type values are: 'jumpPress', 'jumpRelease', 'punchPress',
'rightRelease', 'run', 'flyPress', 'flyRelease', 'startPress',
'startRelease'</p>
</dd>
<dt><h4><a name="method_ba_Player__exists">exists()</a></dt></h4><dd>
<p><span>exists(self) -&gt; bool</span></p>
<p>Whether the underlying player still exists.</p>
<p>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.</p>
</dd>
<dt><h4><a name="method_ba_Player__get_icon">get_icon()</a></dt></h4><dd>
<p><span>get_icon(self) -&gt; Dict[str, Any]</span></p>
@ -3803,6 +3817,56 @@ is_alive() method return True. False is returned otherwise.</p>
<p>Set the player's associated <a href="#class_ba_Actor">ba.Actor</a>.</p>
</dd>
</dl>
<hr>
<h2><strong><a name="class_ba_PlayerDiedMessage">ba.PlayerDiedMessage</a></strong></h3>
<p><em>&lt;top level class&gt;</em>
</p>
<p>A message saying a ba.PlayerSpaz has died.</p>
<p>Category: <a href="#class_category_Message_Classes">Message Classes</a></p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_PlayerDiedMessage__how">how</a>, <a href="#attr_ba_PlayerDiedMessage__killed">killed</a></h5>
<dl>
<dt><h4><a name="attr_ba_PlayerDiedMessage__how">how</a></h4></dt><dd>
<p><span><a href="#class_ba_DeathType">ba.DeathType</a></span></p>
<p>The particular type of death.</p>
</dd>
<dt><h4><a name="attr_ba_PlayerDiedMessage__killed">killed</a></h4></dt><dd>
<p><span>bool</span></p>
<p>If True, the spaz was killed;
If False, they left the game or the round ended.</p>
</dd>
</dl>
<h3>Methods:</h3>
<h5><a href="#method_ba_PlayerDiedMessage____init__">&lt;constructor&gt;</a>, <a href="#method_ba_PlayerDiedMessage__getkillerplayer">getkillerplayer()</a>, <a href="#method_ba_PlayerDiedMessage__getplayer">getplayer()</a></h5>
<dl>
<dt><h4><a name="method_ba_PlayerDiedMessage____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.PlayerDiedMessage(player: <a href="#class_ba_Player">ba.Player</a>, was_killed: bool, killerplayer: Optional[<a href="#class_ba_Player">ba.Player</a>], how: <a href="#class_ba_DeathType">ba.DeathType</a>)</span></p>
<p>Instantiate a message with the given values.</p>
</dd>
<dt><h4><a name="method_ba_PlayerDiedMessage__getkillerplayer">getkillerplayer()</a></dt></h4><dd>
<p><span>getkillerplayer(self, playertype: Type[PlayerType]) -&gt; Optional[PlayerType]</span></p>
<p>Return the <a href="#class_ba_Player">ba.Player</a> responsible for the killing, if any.</p>
<p>Pass the Player type being used by the current game.</p>
</dd>
<dt><h4><a name="method_ba_PlayerDiedMessage__getplayer">getplayer()</a></dt></h4><dd>
<p><span>getplayer(self, playertype: Type[PlayerType]) -&gt; PlayerType</span></p>
<p>Return the spaz that died.</p>
<p>The current activity is required as an argument so the exact type of
PlayerSpaz can be determined by the type checker.</p>
</dd>
</dl>
<hr>
@ -4297,12 +4361,12 @@ provided to your Session/Activity instances.
Be aware that, like <a href="#class_ba_Node">ba.Nodes</a>, <a href="#class_ba_SessionPlayer">ba.SessionPlayer</a> 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
<a href="#attr_ba_SessionPlayer__exists">ba.SessionPlayer.exists</a> attribute (or boolean operator) to ensure
<a href="#method_ba_SessionPlayer__exists">ba.SessionPlayer.exists</a>() method (or boolean operator) to ensure
that a SessionPlayer is still present if retaining references to one
for any length of time.</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_SessionPlayer__character">character</a>, <a href="#attr_ba_SessionPlayer__color">color</a>, <a href="#attr_ba_SessionPlayer__exists">exists</a>, <a href="#attr_ba_SessionPlayer__gamedata">gamedata</a>, <a href="#attr_ba_SessionPlayer__gameplayer">gameplayer</a>, <a href="#attr_ba_SessionPlayer__highlight">highlight</a>, <a href="#attr_ba_SessionPlayer__id">id</a>, <a href="#attr_ba_SessionPlayer__in_game">in_game</a>, <a href="#attr_ba_SessionPlayer__sessiondata">sessiondata</a>, <a href="#attr_ba_SessionPlayer__team">team</a></h5>
<h5><a href="#attr_ba_SessionPlayer__character">character</a>, <a href="#attr_ba_SessionPlayer__color">color</a>, <a href="#attr_ba_SessionPlayer__gamedata">gamedata</a>, <a href="#attr_ba_SessionPlayer__gameplayer">gameplayer</a>, <a href="#attr_ba_SessionPlayer__highlight">highlight</a>, <a href="#attr_ba_SessionPlayer__id">id</a>, <a href="#attr_ba_SessionPlayer__in_game">in_game</a>, <a href="#attr_ba_SessionPlayer__sessiondata">sessiondata</a>, <a href="#attr_ba_SessionPlayer__team">team</a></h5>
<dl>
<dt><h4><a name="attr_ba_SessionPlayer__character">character</a></h4></dt><dd>
<p><span> str</span></p>
@ -4314,16 +4378,6 @@ for any length of time.</p>
<p>The base color for this Player.
In team games this will match the <a href="#class_ba_SessionTeam">ba.SessionTeam</a>'s color.</p>
</dd>
<dt><h4><a name="attr_ba_SessionPlayer__exists">exists</a></h4></dt><dd>
<p><span> bool</span></p>
<p>Whether the player still exists.
Most functionality will fail on a nonexistent player.</p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_SessionPlayer__gamedata">gamedata</a></h4></dt><dd>
<p><span> Dict</span></p>
@ -4349,6 +4403,10 @@ who may all share the same team (primary) color.</p>
<p><span> int</span></p>
<p>The unique numeric ID of the Player.</p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_SessionPlayer__in_game">in_game</a></h4></dt><dd>
<p><span> bool</span></p>
@ -4372,7 +4430,7 @@ is still in its lobby selecting a team/etc. then a
</dd>
</dl>
<h3>Methods:</h3>
<h5><a href="#method_ba_SessionPlayer__assign_input_call">assign_input_call()</a>, <a href="#method_ba_SessionPlayer__get_account_id">get_account_id()</a>, <a href="#method_ba_SessionPlayer__get_icon">get_icon()</a>, <a href="#method_ba_SessionPlayer__get_input_device">get_input_device()</a>, <a href="#method_ba_SessionPlayer__get_name">get_name()</a>, <a href="#method_ba_SessionPlayer__remove_from_game">remove_from_game()</a>, <a href="#method_ba_SessionPlayer__reset_input">reset_input()</a>, <a href="#method_ba_SessionPlayer__set_name">set_name()</a></h5>
<h5><a href="#method_ba_SessionPlayer__assign_input_call">assign_input_call()</a>, <a href="#method_ba_SessionPlayer__exists">exists()</a>, <a href="#method_ba_SessionPlayer__get_account_id">get_account_id()</a>, <a href="#method_ba_SessionPlayer__get_icon">get_icon()</a>, <a href="#method_ba_SessionPlayer__get_input_device">get_input_device()</a>, <a href="#method_ba_SessionPlayer__get_name">get_name()</a>, <a href="#method_ba_SessionPlayer__remove_from_game">remove_from_game()</a>, <a href="#method_ba_SessionPlayer__reset_input">reset_input()</a>, <a href="#method_ba_SessionPlayer__set_name">set_name()</a></h5>
<dl>
<dt><h4><a name="method_ba_SessionPlayer__assign_input_call">assign_input_call()</a></dt></h4><dd>
<p><span>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'</p>
</dd>
<dt><h4><a name="method_ba_SessionPlayer__exists">exists()</a></dt></h4><dd>
<p><span>exists() -&gt; bool</span></p>
<p>Return whether the underlying player is still in the game.</p>
</dd>
<dt><h4><a name="method_ba_SessionPlayer__get_account_id">get_account_id()</a></dt></h4><dd>
<p><span>get_account_id() -&gt; str</span></p>
@ -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.</p>
<hr>
<h2><strong><a name="function_ba_existing">ba.existing()</a></strong></h3>
<p><span>existing(obj: Optional[ExistableType]) -&gt; Optional[ExistableType]</span></p>
<p>Convert invalid references to None.</p>
<p>Category: <a href="#function_category_Gameplay_Functions">Gameplay Functions</a></p>
<p>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</p>
<hr>
<h2><strong><a name="function_ba_get_collision_info">ba.get_collision_info()</a></strong></h3>
<p><span>get_collision_info(*args: Any) -&gt; Any</span></p>