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:

    +
    <constructor>, get_source_player()

    <constructor>

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

    +

    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:

    -
    allows_configuring, client_id, exists, id, instance_number, is_controller_app, is_remote_client, name, player, unique_identifier
    +
    allows_configuring, client_id, id, instance_number, is_controller_app, is_remote_client, name, player, unique_identifier

    allows_configuring

    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.

    -
    -

    exists

    -

    bool

    -

    Whether the underlying device for this object is still present.

    -

    id

    int

    @@ -2553,8 +2560,14 @@ prefs, etc.

    Methods:

    -
    get_account_name(), get_axis_name(), get_button_name()
    +
    exists(), get_account_name(), get_axis_name(), get_button_name()
    +

    exists()

    +

    exists() -> bool

    + +

    Return whether the underlying device for this object is still present.

    + +

    get_account_name()

    get_account_name(full: bool) -> str

    @@ -3717,18 +3730,8 @@ even if myactor is set to None.

    Attributes:

    -
    exists, node, sessionplayer
    +
    node, sessionplayer
    -

    exists

    -

    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.

    - -

    node

    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(), get_icon(), get_name(), is_alive(), reset_input(), set_actor()
    +
    assign_input_call(), exists(), get_icon(), get_name(), is_alive(), reset_input(), set_actor()

    assign_input_call()

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

    +

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

    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.

    +
    +
    +
    +

    ba.PlayerDiedMessage

    +

    <top level class> +

    +

    A message saying a ba.PlayerSpaz has died.

    + +

    Category: Message Classes

    + +

    Attributes:

    +
    how, killed
    +
    +

    how

    +

    ba.DeathType

    +

    The particular type of death.

    + +
    +

    killed

    +

    bool

    +

    If True, the spaz was killed; +If False, they left the game or the round ended.

    + +
    +
    +

    Methods:

    +
    <constructor>, getkillerplayer(), getplayer()
    +
    +

    <constructor>

    +

    ba.PlayerDiedMessage(player: ba.Player, was_killed: bool, killerplayer: Optional[ba.Player], how: ba.DeathType)

    + +

    Instantiate a message with the given values.

    + +
    +

    getkillerplayer()

    +

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

    +

    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:

    -
    character, color, exists, gamedata, gameplayer, highlight, id, in_game, sessiondata, team
    +
    character, color, gamedata, gameplayer, highlight, id, in_game, sessiondata, team

    character

    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.

    -
    -

    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.

    -

    gamedata

    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.

    +

    in_game

    bool

    @@ -4372,7 +4430,7 @@ is still in its lobby selecting a team/etc. then a

    Methods:

    -
    assign_input_call(), get_account_id(), get_icon(), get_input_device(), get_name(), remove_from_game(), reset_input(), set_name()
    +
    assign_input_call(), exists(), get_account_id(), get_icon(), get_input_device(), get_name(), remove_from_game(), reset_input(), set_name()

    assign_input_call()

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

    +

    exists() -> bool

    + +

    Return whether the underlying player is still in the game.

    +

    get_account_id()

    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.

    +
    +

    ba.existing()

    +

    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

    +

    ba.get_collision_info()

    get_collision_info(*args: Any) -> Any