From d29cb35ff10401601b3ee363728892f4099c135e Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Tue, 19 May 2020 01:34:24 -0700 Subject: [PATCH] More minigame modernizing --- .efrocachemap | 24 +- assets/src/ba_data/python/_ba.py | 2 +- assets/src/ba_data/python/ba/_activity.py | 462 +++++++++--------- assets/src/ba_data/python/ba/_gameresults.py | 2 +- assets/src/ba_data/python/ba/_player.py | 12 + assets/src/ba_data/python/ba/_team.py | 21 + .../src/ba_data/python/bastd/actor/spazbot.py | 22 +- .../src/ba_data/python/bastd/game/assault.py | 4 +- .../python/bastd/game/capturetheflag.py | 4 +- .../ba_data/python/bastd/game/chosenone.py | 232 ++++----- .../ba_data/python/bastd/game/elimination.py | 274 ++++++----- .../src/ba_data/python/bastd/game/football.py | 48 +- .../src/ba_data/python/bastd/game/keepaway.py | 41 +- .../python/bastd/game/kingofthehill.py | 139 +++--- .../ba_data/python/bastd/game/meteorshower.py | 4 + .../ba_data/python/bastd/game/onslaught.py | 17 +- assets/src/ba_data/python/bastd/game/race.py | 40 +- .../python/bastd/game/targetpractice.py | 72 +-- .../ba_data/python/bastd/game/thelaststand.py | 15 +- docs/ba_module.md | 13 +- 20 files changed, 798 insertions(+), 650 deletions(-) diff --git a/.efrocachemap b/.efrocachemap index 82c262cf..c17d98c1 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/33/81/1bf0d898c26582776d0c2ef76b68", - "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7b/b6/9cf8cb137735545a5d9c5d2abc14", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4c/a0/b18786c5c4a3b8c8ec9417167412", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fe/41/1bf6ad4d57a589fb26cece393563", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/42/6d/39d0af901ac06b9ad655a9837a6d", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/98/0d/0d4594d20813a5b3dacb3aa2022f", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/8e/73/1e215e66ce6fd155b4af840465f2", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a2/21/aad47597886fe15f228386fdf0a5", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3c/e5/bdd60cba90f6955ba7a4a932bd45", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/4f/b7/86dec4a8ab32edaf850d3c0fe2c2", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/09/ab/48ca17f389375fd4db02295bab73", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/f4/12/6fb56bf6484b5b93fbfd3206bbdf" + "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/75/82/99d3b14b1c7ccab2de8c64bf9dea", + "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fe/78/0e5383f059887070323d08f6fa9a", + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/0d/23/a18e8d3f8a70f9865938d95d7184", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/27/57/8f95c8da763731b971488a18d9b4", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e9/c6/cd1d7d7568edf9f5e3d313e75eca", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7e/92/c4407e3e9017523745e381f681d8", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c9/57/5273080f7d383a3de8e3a519f96b", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/d3/01ce8f52b62fc308a358d9a5470e", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/c6/44/bd76f8dc1be8b2b862876d112420", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/68/86/6e1418947d35a647d563aff2aebe", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/9f/3d/f091bf3ed6b286a29f660a1e14cc", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a0/b3/ad0fa29417f197bed6f3fb184a3f" } \ No newline at end of file diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index e9207771..fe13f9ad 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=266649817838802754126771358652920545389 +# SOURCES_HASH=122350585846084418668853979161934598264 # I'm sorry Pylint. I know this file saddens you. Be strong. # pylint: disable=useless-suppression diff --git a/assets/src/ba_data/python/ba/_activity.py b/assets/src/ba_data/python/ba/_activity.py index 4098788f..87bbca24 100644 --- a/assets/src/ba_data/python/ba/_activity.py +++ b/assets/src/ba_data/python/ba/_activity.py @@ -331,106 +331,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): raise RuntimeError(f'destroy() called when' f' already expired for {self}') - @classmethod - def _check_activity_death(cls, activity_ref: ReferenceType[Activity], - counter: List[int]) -> None: - """Sanity check to make sure an Activity was destroyed properly. - - Receives a weakref to a ba.Activity which should have torn itself - down due to no longer being referenced anywhere. Will complain - and/or print debugging info if the Activity still exists. - """ - try: - import gc - import types - activity = activity_ref() - print('ERROR: Activity is not dying when expected:', activity, - '(warning ' + str(counter[0] + 1) + ')') - print('This means something is still strong-referencing it.') - counter[0] += 1 - - # FIXME: Running the code below shows us references but winds up - # keeping the object alive; need to figure out why. - # For now we just print refs if the count gets to 3, and then we - # kill the app at 4 so it doesn't matter anyway. - if counter[0] == 3: - print('Activity references for', activity, ':') - refs = list(gc.get_referrers(activity)) - i = 1 - for ref in refs: - if isinstance(ref, types.FrameType): - continue - print(' reference', i, ':', ref) - i += 1 - if counter[0] == 4: - print('Killing app due to stuck activity... :-(') - _ba.quit() - - except Exception: - print_exception('exception on _check_activity_death:') - - def _expire(self) -> None: - """Put the activity in a state where it can be garbage-collected. - - This involves clearing anything that might be holding a reference - to it, etc. - """ - assert not self._expired - self._expired = True - - try: - self.on_expire() - except Exception: - print_exception(f'Error in Activity on_expire() for {self}') - - # Send expire notices to all remaining actors. - for actor_ref in self._actor_weak_refs: - try: - actor = actor_ref() - if actor is not None: - actor.on_expire() - except Exception: - print_exception(f'Error expiring Actor {actor_ref()}') - - # Reset all Players. - # (releases any attached actors, clears game-data, etc) - for player in self.players: - if player: - try: - sessionplayer = player.sessionplayer - sessionplayer.set_node(None) - sessionplayer.set_activity(None) - sessionplayer.gameplayer = None - sessionplayer.reset() - except Exception: - print_exception(f'Error resetting Player {player}') - - # Ditto with Teams. - for team in self.teams: - try: - sessionteam = team.sessionteam - sessionteam.gameteam = None - sessionteam.reset_gamedata() - except SessionTeamNotFoundError: - pass - except Exception: - print_exception(f'Error resetting Team {team}') - - # Regardless of what happened here, we want to destroy our data, as - # our activity might not go down if we don't. This will kill all - # Timers, Nodes, etc, which should clear up any remaining refs to our - # Actors and Activity and allow us to die peacefully. - try: - self._activity_data.destroy() - except Exception: - print_exception( - 'Exception during ba.Activity._expire() destroying data:') - - def _prune_dead_actors(self) -> None: - self._actor_refs = [a for a in self._actor_refs if a] - self._actor_weak_refs = [a for a in self._actor_weak_refs if a()] - self._last_prune_dead_actors_time = _ba.time() - def retain_actor(self, actor: ba.Actor) -> None: """Add a strong-reference to a ba.Actor to this Activity. @@ -619,127 +519,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): except Exception: print_exception('Error in on_transition_out for', self) - def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType: - """Create the Player instance for this Activity. - - Subclasses can override this if the activity's player class - requires a custom constructor; otherwise it will be called with - no args. Note that the player object should not be used at this - point as it is not yet fully wired up; wait for on_player_join() - for that. - """ - del sessionplayer # Unused - player = self._playertype() - return player - - def create_team(self, sessionteam: ba.SessionTeam) -> TeamType: - """Create the Team instance for this Activity. - - Subclasses can override this if the activity's team class - requires a custom constructor; otherwise it will be called with - no args. Note that the team object should not be used at this - point as it is not yet fully wired up; wait for on_team_join() - for that. - """ - del sessionteam # Unused. - team = self._teamtype() - return team - - def add_player(self, sessionplayer: ba.SessionPlayer) -> None: - """(internal)""" - assert sessionplayer.team is not None - - sessionplayer.reset_input() - sessionteam = sessionplayer.team - assert sessionplayer in sessionteam.players - team = sessionteam.gameteam - assert team is not None - sessionplayer.set_activity(self) - with _ba.Context(self): - sessionplayer.gameplayer = player = self.create_player( - sessionplayer) - player.postinit(sessionplayer) - team.players.append(player) - self.players.append(player) - try: - self.on_player_join(player) - except Exception: - print_exception('Error in on_player_join for', self) - - def remove_player(self, sessionplayer: ba.SessionPlayer) -> None: - """(internal)""" - - # This should only be called on unexpired activities - # the player has been added to. - assert not self.expired - player = sessionplayer.gameplayer - assert isinstance(player, self._playertype) - assert player in self.players - - self.players.remove(player) - with _ba.Context(self): - # Make a decent attempt to persevere if user code breaks. - try: - self.on_player_leave(player) - except Exception: - print_exception(f'Error in on_player_leave for {self}') - try: - sessionplayer.reset() - sessionplayer.set_node(None) - sessionplayer.set_activity(None) - except Exception: - print_exception(f'Error resetting player for {self}') - - def add_team(self, sessionteam: ba.SessionTeam) -> None: - """(internal)""" - assert not self.expired - - with _ba.Context(self): - sessionteam.gameteam = team = self.create_team(sessionteam) - team.postinit(sessionteam) - self.teams.append(team) - try: - self.on_team_join(team) - except Exception: - print_exception(f'Error in on_team_join for {self}') - - def remove_team(self, sessionteam: ba.SessionTeam) -> None: - """(internal)""" - - # This should only be called on unexpired activities the team has - # been added to. - assert not self.expired - assert sessionteam.gameteam is not None - assert sessionteam.gameteam in self.teams - - team = sessionteam.gameteam - assert isinstance(team, self._teamtype) - self.teams.remove(team) - with _ba.Context(self): - # Make a decent attempt to persevere if user code breaks. - try: - self.on_team_leave(team) - except Exception: - print_exception(f'Error in on_team_leave for {self}') - try: - sessionteam.reset_gamedata() - except Exception: - print_exception(f'Error in reset_gamedata for {self}') - sessionteam.gameteam = None - - def _sanity_check_begin_call(self) -> None: - # Make sure ba.Activity.on_transition_in() got called at some point. - if not self._called_activity_on_transition_in: - print_error( - 'ba.Activity.on_transition_in() never got called for ' + - str(self) + '; did you forget to call it' - ' in your on_transition_in override?') - # Make sure that ba.Activity.on_begin() got called at some point. - if not self._called_activity_on_begin: - print_error( - 'ba.Activity.on_begin() never got called for ' + str(self) + - '; did you forget to call it in your on_begin override?') - def begin(self, session: ba.Session) -> None: """Begin the activity. @@ -779,6 +558,146 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self.end(self._should_end_immediately_results, self._should_end_immediately_delay) + def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType: + """Create the Player instance for this Activity. + + Subclasses can override this if the activity's player class + requires a custom constructor; otherwise it will be called with + no args. Note that the player object should not be used at this + point as it is not yet fully wired up; wait for on_player_join() + for that. + """ + del sessionplayer # Unused + player = self._playertype() + return player + + def create_team(self, sessionteam: ba.SessionTeam) -> TeamType: + """Create the Team instance for this Activity. + + Subclasses can override this if the activity's team class + requires a custom constructor; otherwise it will be called with + no args. Note that the team object should not be used at this + point as it is not yet fully wired up; wait for on_team_join() + for that. + """ + del sessionteam # Unused. + team = self._teamtype() + return team + + def add_player(self, sessionplayer: ba.SessionPlayer) -> None: + """(internal)""" + assert sessionplayer.team is not None + sessionplayer.reset_input() + sessionteam = sessionplayer.team + assert sessionplayer in sessionteam.players + team = sessionteam.gameteam + assert team is not None + sessionplayer.set_activity(self) + with _ba.Context(self): + sessionplayer.gameplayer = player = self.create_player( + sessionplayer) + player.postinit(sessionplayer) + + assert player not in team.players + team.players.append(player) + assert player in team.players + + assert player not in self.players + self.players.append(player) + assert player in self.players + + try: + self.on_player_join(player) + except Exception: + print_exception('Error in on_player_join for', self) + + def remove_player(self, sessionplayer: ba.SessionPlayer) -> None: + """(internal)""" + + # This should only be called on unexpired activities + # the player has been added to. + assert not self.expired + + player: Any = sessionplayer.gameplayer + assert isinstance(player, self._playertype) + team: Any = sessionplayer.team.gameteam + assert isinstance(team, self._teamtype) + + assert player in team.players + team.players.remove(player) + assert player not in team.players + + assert player in self.players + self.players.remove(player) + assert player not in self.players + + with _ba.Context(self): + # Make a decent attempt to persevere if user code breaks. + try: + self.on_player_leave(player) + except Exception: + print_exception(f'Error in on_player_leave for {self}') + try: + sessionplayer.reset() + sessionplayer.set_node(None) + sessionplayer.set_activity(None) + except Exception: + print_exception(f'Error resetting player for {self}') + + def add_team(self, sessionteam: ba.SessionTeam) -> None: + """(internal)""" + assert not self.expired + + with _ba.Context(self): + sessionteam.gameteam = team = self.create_team(sessionteam) + team.postinit(sessionteam) + self.teams.append(team) + try: + self.on_team_join(team) + except Exception: + print_exception(f'Error in on_team_join for {self}') + + def remove_team(self, sessionteam: ba.SessionTeam) -> None: + """(internal)""" + + # This should only be called on unexpired activities the team has + # been added to. + assert not self.expired + assert sessionteam.gameteam is not None + assert sessionteam.gameteam in self.teams + + team = sessionteam.gameteam + assert isinstance(team, self._teamtype) + + assert team in self.teams + self.teams.remove(team) + assert team not in self.teams + + with _ba.Context(self): + # Make a decent attempt to persevere if user code breaks. + try: + self.on_team_leave(team) + except Exception: + print_exception(f'Error in on_team_leave for {self}') + try: + sessionteam.reset_gamedata() + except Exception: + print_exception(f'Error in reset_gamedata for {self}') + sessionteam.gameteam = None + + def _sanity_check_begin_call(self) -> None: + # Make sure ba.Activity.on_transition_in() got called at some point. + if not self._called_activity_on_transition_in: + print_error( + 'ba.Activity.on_transition_in() never got called for ' + + str(self) + '; did you forget to call it' + ' in your on_transition_in override?') + # Make sure that ba.Activity.on_begin() got called at some point. + if not self._called_activity_on_begin: + print_error( + 'ba.Activity.on_begin() never got called for ' + str(self) + + '; did you forget to call it in your on_begin override?') + # noinspection PyUnresolvedReferences def _setup_player_and_team_types(self) -> None: """Pull player and team types from our typing.Generic params.""" @@ -805,3 +724,104 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): f' if you do not want to override it.') assert issubclass(self._playertype, Player) assert issubclass(self._teamtype, Team) + + @classmethod + def _check_activity_death(cls, activity_ref: ReferenceType[Activity], + counter: List[int]) -> None: + """Sanity check to make sure an Activity was destroyed properly. + + Receives a weakref to a ba.Activity which should have torn itself + down due to no longer being referenced anywhere. Will complain + and/or print debugging info if the Activity still exists. + """ + try: + import gc + import types + activity = activity_ref() + print('ERROR: Activity is not dying when expected:', activity, + '(warning ' + str(counter[0] + 1) + ')') + print('This means something is still strong-referencing it.') + counter[0] += 1 + + # FIXME: Running the code below shows us references but winds up + # keeping the object alive; need to figure out why. + # For now we just print refs if the count gets to 3, and then we + # kill the app at 4 so it doesn't matter anyway. + if counter[0] == 3: + print('Activity references for', activity, ':') + refs = list(gc.get_referrers(activity)) + i = 1 + for ref in refs: + if isinstance(ref, types.FrameType): + continue + print(' reference', i, ':', ref) + i += 1 + if counter[0] == 4: + print('Killing app due to stuck activity... :-(') + _ba.quit() + + except Exception: + print_exception('exception on _check_activity_death:') + + def _expire(self) -> None: + """Put the activity in a state where it can be garbage-collected. + + This involves clearing anything that might be holding a reference + to it, etc. + """ + assert not self._expired + self._expired = True + + try: + self.on_expire() + except Exception: + print_exception(f'Error in Activity on_expire() for {self}') + + # Send expire notices to all remaining actors. + for actor_ref in self._actor_weak_refs: + try: + actor = actor_ref() + if actor is not None: + actor.on_expire() + except Exception: + print_exception(f'Error expiring Actor {actor_ref()}') + + # Reset all Players. + # (releases any attached actors, clears game-data, etc) + for player in self.players: + if player: + try: + sessionplayer = player.sessionplayer + sessionplayer.set_node(None) + sessionplayer.set_activity(None) + sessionplayer.gameplayer = None + sessionplayer.reset() + except Exception: + print_exception(f'Error resetting Player {player}') + + # Ditto with Teams. + for team in self.teams: + try: + sessionteam = team.sessionteam + sessionteam.gameteam = None + sessionteam.reset_gamedata() + except SessionTeamNotFoundError: + pass + # print_exception(f'Error resetting Team {team}') + except Exception: + print_exception(f'Error resetting Team {team}') + + # Regardless of what happened here, we want to destroy our data, as + # our activity might not go down if we don't. This will kill all + # Timers, Nodes, etc, which should clear up any remaining refs to our + # Actors and Activity and allow us to die peacefully. + try: + self._activity_data.destroy() + except Exception: + print_exception( + 'Exception during ba.Activity._expire() destroying data:') + + def _prune_dead_actors(self) -> None: + self._actor_refs = [a for a in self._actor_refs if a] + self._actor_weak_refs = [a for a in self._actor_weak_refs if a()] + self._last_prune_dead_actors_time = _ba.time() diff --git a/assets/src/ba_data/python/ba/_gameresults.py b/assets/src/ba_data/python/ba/_gameresults.py index 9376cf69..a4997e37 100644 --- a/assets/src/ba_data/python/ba/_gameresults.py +++ b/assets/src/ba_data/python/ba/_gameresults.py @@ -77,7 +77,7 @@ class TeamGameResults: self._score_type = score_info.scoretype def set_team_score(self, team: Union[ba.SessionTeam, ba.Team], - score: int) -> None: + score: Optional[int]) -> None: """Set the score for a given ba.Team. This can be a number or None. diff --git a/assets/src/ba_data/python/ba/_player.py b/assets/src/ba_data/python/ba/_player.py index 2779717c..baf64908 100644 --- a/assets/src/ba_data/python/ba/_player.py +++ b/assets/src/ba_data/python/ba/_player.py @@ -59,6 +59,18 @@ class Player(Generic[TeamType]): """ from ba._nodeactor import NodeActor import _ba + + # Sanity check; if a dataclass is created that inherits from us, + # it will define an equality operator by default which will break + # internal game logic. So complain loudly if we find one. + if type(self).__eq__ is not object.__eq__: + raise RuntimeError( + f'Player class {type(self)} defines an equality' + f' operator (__eq__) which will break internal' + f' logic. Please remove it.\n' + f'For dataclasses you can do "dataclass(eq=False)"' + f' in the class decorator.') + self.actor = None self.character = '' self._nodeactor: Optional[ba.NodeActor] = None diff --git a/assets/src/ba_data/python/ba/_team.py b/assets/src/ba_data/python/ba/_team.py index 7c30d959..8a6beaae 100644 --- a/assets/src/ba_data/python/ba/_team.py +++ b/assets/src/ba_data/python/ba/_team.py @@ -126,6 +126,18 @@ class Team(Generic[PlayerType]): (internal) """ + + # Sanity check; if a dataclass is created that inherits from us, + # it will define an equality operator by default which will break + # internal game logic. So complain loudly if we find one. + if type(self).__eq__ is not object.__eq__: + raise RuntimeError( + f'Team class {type(self)} defines an equality' + f' operator (__eq__) which will break internal' + f' logic. Please remove it.\n' + f'For dataclasses you can do "dataclass(eq=False)"' + f' in the class decorator.') + self.players = [] self._sessionteam = weakref.ref(sessionteam) self.id = sessionteam.id @@ -134,6 +146,15 @@ class Team(Generic[PlayerType]): self.gamedata = sessionteam.gamedata self.sessiondata = sessionteam.sessiondata + def manual_init(self, team_id: int, name: Union[ba.Lstr, str], + color: Tuple[float, ...]) -> None: + """Manually init a team for uses such as bots.""" + self.id = team_id + self.name = name + self.color = color + self.gamedata = {} + self.sessiondata = {} + @property def sessionteam(self) -> SessionTeam: """Return the ba.SessionTeam corresponding to this Team. diff --git a/assets/src/ba_data/python/bastd/actor/spazbot.py b/assets/src/ba_data/python/bastd/actor/spazbot.py index e62d0b97..d0602951 100644 --- a/assets/src/ba_data/python/bastd/actor/spazbot.py +++ b/assets/src/ba_data/python/bastd/actor/spazbot.py @@ -28,7 +28,7 @@ import weakref from typing import TYPE_CHECKING import ba -from bastd.actor import spaz as basespaz +from bastd.actor.spaz import Spaz if TYPE_CHECKING: from typing import Any, Optional, List, Tuple, Sequence, Type, Callable @@ -87,7 +87,7 @@ class SpazBotDeathMessage: self.how = how -class SpazBot(basespaz.Spaz): +class SpazBot(Spaz): """A really dumb AI version of ba.Spaz. category: Bot Classes @@ -127,13 +127,12 @@ class SpazBot(basespaz.Spaz): def __init__(self) -> None: """Instantiate a spaz-bot.""" - basespaz.Spaz.__init__(self, - color=self.color, - highlight=self.highlight, - character=self.character, - source_player=None, - start_invincible=False, - can_accept_powerups=False) + super().__init__(color=self.color, + highlight=self.highlight, + character=self.character, + source_player=None, + start_invincible=False, + can_accept_powerups=False) # If you need to add custom behavior to a bot, set this to a callable # which takes one arg (the bot) and returns False if the bot's normal @@ -503,7 +502,7 @@ class SpazBot(basespaz.Spaz): ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage)) def on_expire(self) -> None: - basespaz.Spaz.on_expire(self) + super().on_expire() # We're being torn down; release our callback(s) so there's # no chance of them keeping activities or other things alive. @@ -980,9 +979,10 @@ class BotSet: # Update our list of player points for the bots to use. player_pts = [] for player in ba.getactivity().players: + assert isinstance(player, ba.Player) try: if player.is_alive(): - assert isinstance(player.actor, basespaz.Spaz) + assert isinstance(player.actor, Spaz) assert player.actor.node player_pts.append((ba.Vec3(player.actor.node.position), ba.Vec3(player.actor.node.velocity))) diff --git a/assets/src/ba_data/python/bastd/game/assault.py b/assets/src/ba_data/python/bastd/game/assault.py index efc46df3..c7542650 100644 --- a/assets/src/ba_data/python/bastd/game/assault.py +++ b/assets/src/ba_data/python/bastd/game/assault.py @@ -37,12 +37,12 @@ if TYPE_CHECKING: from typing import Any, Type, List, Dict, Tuple, Sequence, Union -@dataclass +@dataclass(eq=False) class Player(ba.Player['Team']): """Our player type for this game.""" -@dataclass +@dataclass(eq=False) class Team(ba.Team[Player]): """Our team type for this game.""" base_pos: Sequence[float] diff --git a/assets/src/ba_data/python/bastd/game/capturetheflag.py b/assets/src/ba_data/python/bastd/game/capturetheflag.py index 50c1e09a..c190da31 100644 --- a/assets/src/ba_data/python/bastd/game/capturetheflag.py +++ b/assets/src/ba_data/python/bastd/game/capturetheflag.py @@ -81,13 +81,13 @@ class CTFFlag(stdflag.Flag): return delegate if isinstance(delegate, CTFFlag) else None -@dataclass +@dataclass(eq=False) class Player(ba.Player['Team']): """Our player type for this game.""" touching_own_flag: int = 0 -@dataclass +@dataclass(eq=False) class Team(ba.Team[Player]): """Our team type for this game.""" base_pos: Sequence[float] diff --git a/assets/src/ba_data/python/bastd/game/chosenone.py b/assets/src/ba_data/python/bastd/game/chosenone.py index ce87a50d..92fd55b0 100644 --- a/assets/src/ba_data/python/bastd/game/chosenone.py +++ b/assets/src/ba_data/python/bastd/game/chosenone.py @@ -25,19 +25,31 @@ from __future__ import annotations from typing import TYPE_CHECKING +from dataclasses import dataclass import ba -from bastd.actor import flag -from bastd.actor import playerspaz -from bastd.actor import spaz +from bastd.actor.flag import Flag +from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage if TYPE_CHECKING: from typing import (Any, Type, List, Dict, Tuple, Optional, Sequence, Union) +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + chosen_light: Optional[ba.NodeActor] = None + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + time_remaining: int + + # ba_meta export game -class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class ChosenOneGame(ba.TeamGameActivity[Player, Team]): """ Game involving trying to remain the one 'chosen one' for a set length of time while everyone else tries to @@ -96,10 +108,8 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) - if self.settings_raw['Epic Mode']: - self.slow_motion = True self._scoreboard = Scoreboard() - self._chosen_one_player: Optional[ba.Player] = None + self._chosen_one_player: Optional[Player] = None self._swipsound = ba.getsound('swip') self._countdownsounds: Dict[int, ba.Sound] = { 10: ba.getsound('announceTen'), @@ -115,30 +125,36 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): } self._flag_spawn_pos: Optional[Sequence[float]] = None self._reset_region_material: Optional[ba.Material] = None - self._flag: Optional[flag.Flag] = None + self._flag: Optional[Flag] = None self._reset_region: Optional[ba.Node] = None + self._epic_mode = bool(settings['Epic Mode']) + self._chosen_one_time = int(settings['Chosen One Time']) + self._time_limit = float(settings['Time Limit']) + self._chosen_one_gets_shield = bool(settings['Chosen One Gets Shield']) + self._chosen_one_gets_gloves = bool(settings['Chosen One Gets Gloves']) + + # Base class overrides + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.CHOSEN_ONE) def get_instance_description(self) -> Union[str, Sequence]: return 'There can be only one.' - def on_transition_in(self) -> None: - self.default_music = (ba.MusicType.EPIC - if self.settings_raw['Epic Mode'] else - ba.MusicType.CHOSEN_ONE) - super().on_transition_in() + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team(time_remaining=self._chosen_one_time) - def on_team_join(self, team: ba.Team) -> None: - team.gamedata['time_remaining'] = self.settings_raw['Chosen One Time'] + def on_team_join(self, team: Team) -> None: self._update_scoreboard() - def on_player_leave(self, player: ba.Player) -> None: + def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) if self._get_chosen_one_player() is player: self._set_chosen_one_player(None) 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() self._flag_spawn_pos = self.map.get_flag_position(None) self.project_flag_stand(self._flag_spawn_pos) @@ -164,7 +180,7 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): 'materials': [mat] }) - def _get_chosen_one_player(self) -> Optional[ba.Player]: + def _get_chosen_one_player(self) -> Optional[Player]: if self._chosen_one_player: return self._chosen_one_player return None @@ -173,13 +189,11 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): # If we have a chosen one, ignore these. if self._get_chosen_one_player() is not None: return - try: - player = (ba.get_collision_info( - 'opposing_node').getdelegate().getplayer()) - except Exception: - return - if player is not None and player.is_alive(): - self._set_chosen_one_player(player) + delegate = ba.get_collision_info('opposing_node').getdelegate() + if isinstance(delegate, PlayerSpaz): + player = ba.playercast_o(Player, delegate.getplayer()) + if player is not None and player.is_alive(): + self._set_chosen_one_player(player) def _flash_flag_spawn(self) -> None: light = ba.newnode('light', @@ -210,29 +224,24 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): screenmessage=False, display=False) - scoring_team.gamedata['time_remaining'] = max( - 0, scoring_team.gamedata['time_remaining'] - 1) + scoring_team.time_remaining = max( + 0, scoring_team.time_remaining - 1) - # show the count over their head - try: - if scoring_team.gamedata['time_remaining'] > 0: - if isinstance(player.actor, spaz.Spaz): - player.actor.set_score_text( - str(scoring_team.gamedata['time_remaining'])) - except Exception: - pass + # Show the count over their head + if scoring_team.time_remaining > 0: + if isinstance(player.actor, PlayerSpaz) and player.actor: + player.actor.set_score_text( + str(scoring_team.time_remaining)) self._update_scoreboard() # announce numbers we have sounds for - try: - ba.playsound(self._countdownsounds[ - scoring_team.gamedata['time_remaining']]) - except Exception: - pass + if scoring_team.time_remaining in self._countdownsounds: + ba.playsound( + self._countdownsounds[scoring_team.time_remaining]) # Winner! - if scoring_team.gamedata['time_remaining'] <= 0: + if scoring_team.time_remaining <= 0: self.end_game() else: @@ -247,89 +256,81 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: - results.set_team_score( - team, self.settings_raw['Chosen One Time'] - - team.gamedata['time_remaining']) + results.set_team_score(team, + self._chosen_one_time - team.time_remaining) self.end(results=results, announce_delay=0) - def _set_chosen_one_player(self, player: Optional[ba.Player]) -> None: - try: - for p_other in self.players: - p_other.gamedata['chosen_light'] = None - ba.playsound(self._swipsound) - if not player: - assert self._flag_spawn_pos is not None - self._flag = flag.Flag(color=(1, 0.9, 0.2), - position=self._flag_spawn_pos, - touchable=False) - self._chosen_one_player = None + def _set_chosen_one_player(self, player: Optional[Player]) -> None: + for p_other in self.players: + p_other.chosen_light = None + ba.playsound(self._swipsound) + if not player: + assert self._flag_spawn_pos is not None + self._flag = Flag(color=(1, 0.9, 0.2), + position=self._flag_spawn_pos, + touchable=False) + self._chosen_one_player = None - # Create a light to highlight the flag; - # this will go away when the flag dies. - ba.newnode('light', - owner=self._flag.node, - attrs={ - 'position': self._flag_spawn_pos, - 'intensity': 0.6, - 'height_attenuated': False, - 'volume_intensity_scale': 0.1, - 'radius': 0.1, - 'color': (1.2, 1.2, 0.4) - }) + # Create a light to highlight the flag; + # this will go away when the flag dies. + ba.newnode('light', + owner=self._flag.node, + attrs={ + 'position': self._flag_spawn_pos, + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.1, + 'color': (1.2, 1.2, 0.4) + }) - # Also an extra momentary flash. - self._flash_flag_spawn() - else: - if player.actor is not None: - self._flag = None - self._chosen_one_player = player + # Also an extra momentary flash. + self._flash_flag_spawn() + else: + if player.actor: + self._flag = None + self._chosen_one_player = player - if player.actor: - if self.settings_raw['Chosen One Gets Shield']: - player.actor.handlemessage( - ba.PowerupMessage('shield')) - if self.settings_raw['Chosen One Gets Gloves']: - player.actor.handlemessage( - ba.PowerupMessage('punch')) + if self._chosen_one_gets_shield: + player.actor.handlemessage(ba.PowerupMessage('shield')) + if self._chosen_one_gets_gloves: + player.actor.handlemessage(ba.PowerupMessage('punch')) - # Use a color that's partway between their team color - # and white. - color = [ - 0.3 + c * 0.7 - for c in ba.normalized_color(player.team.color) - ] - light = player.gamedata['chosen_light'] = ba.NodeActor( - ba.newnode('light', - attrs={ - 'intensity': 0.6, - 'height_attenuated': False, - 'volume_intensity_scale': 0.1, - 'radius': 0.13, - 'color': color - })) + # Use a color that's partway between their team color + # and white. + color = [ + 0.3 + c * 0.7 + for c in ba.normalized_color(player.team.color) + ] + light = player.chosen_light = ba.NodeActor( + ba.newnode('light', + attrs={ + 'intensity': 0.6, + 'height_attenuated': False, + 'volume_intensity_scale': 0.1, + 'radius': 0.13, + 'color': color + })) - assert light.node - ba.animate(light.node, - 'intensity', { - 0: 1.0, - 0.2: 0.4, - 0.4: 1.0 - }, - loop=True) - assert isinstance(player.actor, playerspaz.PlayerSpaz) - player.actor.node.connectattr('position', light.node, - 'position') - - except Exception: - ba.print_exception('EXC in _set_chosen_one_player') + assert light.node + ba.animate(light.node, + 'intensity', { + 0: 1.0, + 0.2: 0.4, + 0.4: 1.0 + }, + loop=True) + assert isinstance(player.actor, PlayerSpaz) + player.actor.node.connectattr('position', light.node, + 'position') def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, playerspaz.PlayerSpazDeathMessage): + if isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) player = msg.playerspaz(self).player if player is self._get_chosen_one_player(): - killerplayer = msg.killerplayer + killerplayer = ba.playercast_o(Player, msg.killerplayer) self._set_chosen_one_player(None if ( killerplayer is None or killerplayer is player or not killerplayer.is_alive()) else killerplayer) @@ -339,8 +340,7 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]): def _update_scoreboard(self) -> None: for team in self.teams: - self._scoreboard.set_team_value( - team, - team.gamedata['time_remaining'], - self.settings_raw['Chosen One Time'], - countdown=True) + self._scoreboard.set_team_value(team, + team.time_remaining, + self._chosen_one_time, + countdown=True) diff --git a/assets/src/ba_data/python/bastd/game/elimination.py b/assets/src/ba_data/python/bastd/game/elimination.py index 96e9bb91..cb805901 100644 --- a/assets/src/ba_data/python/bastd/game/elimination.py +++ b/assets/src/ba_data/python/bastd/game/elimination.py @@ -25,6 +25,7 @@ from __future__ import annotations +from dataclasses import dataclass, field from typing import TYPE_CHECKING import ba @@ -40,7 +41,7 @@ class Icon(ba.Actor): """Creates in in-game icon on screen.""" def __init__(self, - player: ba.Player, + player: Player, position: Tuple[float, float], scale: float, show_lives: bool = True, @@ -117,7 +118,7 @@ class Icon(ba.Actor): def update_for_lives(self) -> None: """Update for the target player's current lives.""" if self._player: - lives = self._player.gamedata['lives'] + lives = self._player.lives else: lives = 0 if self._show_lives: @@ -158,13 +159,27 @@ class Icon(ba.Actor): 0.50: 1.0, 0.55: 0.2 }) - lives = self._player.gamedata['lives'] + lives = self._player.lives if lives == 0: ba.timer(0.6, self.update_for_lives) +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + lives: int = 0 + icons: List[Icon] = field(default_factory=list) + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + survival_seconds: Optional[int] = None + spawn_order: List[Player] = field(default_factory=list) + + # ba_meta export game -class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class EliminationGame(ba.TeamGameActivity[Player, Team]): """Game type where last player(s) left alive win.""" @classmethod @@ -196,13 +211,15 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: settings: List[Tuple[str, Dict[str, Any]]] = [ ('Lives Per Player', { - 'default': 1, 'min_value': 1, - 'max_value': 10, 'increment': 1 + 'default': 1, + 'min_value': 1, + 'max_value': 10, + 'increment': 1 }), ('Time Limit', { - 'choices': [('None', 0), ('1 Minute', 60), - ('2 Minutes', 120), ('5 Minutes', 300), - ('10 Minutes', 600), ('20 Minutes', 1200)], + 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), + ('5 Minutes', 300), ('10 Minutes', 600), + ('20 Minutes', 1200)], 'default': 0 }), ('Respawn Times', { @@ -210,7 +227,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): ('Long', 2.0), ('Longer', 4.0)], 'default': 1.0 }), - ('Epic Mode', {'default': False})] # yapf: disable + ('Epic Mode', { + 'default': False + }), + ] if issubclass(sessiontype, ba.DualTeamSession): settings.append(('Solo Mode', {'default': False})) @@ -221,17 +241,23 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard super().__init__(settings) - if self.settings_raw['Epic Mode']: - self.slow_motion = True - - # Show messages when players die since it's meaningful here. - self.announce_player_deaths = True - - self._solo_mode = settings.get('Solo Mode', False) self._scoreboard = Scoreboard() self._start_time: Optional[float] = None self._vs_text: Optional[ba.Actor] = None self._round_end_timer: Optional[ba.Timer] = None + self._epic_mode = bool(settings['Epic Mode']) + self._lives_per_player = int(settings['Lives Per Player']) + self._time_limit = float(settings['Time Limit']) + self._balance_total_lives = bool( + settings.get('Balance Total Lives', False)) + self._solo_mode = bool(settings.get('Solo Mode', False)) + + # Base class overrides: + # Show messages when players die since it's meaningful here. + self.announce_player_deaths = True + self.slow_motion = self._epic_mode + self.default_music = (ba.MusicType.EPIC + if self._epic_mode else ba.MusicType.SURVIVAL) def get_instance_description(self) -> Union[str, Sequence]: return 'Last team standing wins.' if isinstance( @@ -241,65 +267,93 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): return 'last team standing wins' if isinstance( self.session, ba.DualTeamSession) else 'last one standing wins' - def on_transition_in(self) -> None: - self.default_music = (ba.MusicType.EPIC - if self.settings_raw['Epic Mode'] else - ba.MusicType.SURVIVAL) - super().on_transition_in() - self._start_time = ba.time() - - def on_team_join(self, team: ba.Team) -> None: - team.gamedata['survival_seconds'] = None - team.gamedata['spawn_order'] = [] - - def on_player_join(self, player: ba.Player) -> None: + def on_player_join(self, player: Player) -> None: # No longer allowing mid-game joiners here; too easy to exploit. if self.has_begun(): - player.gamedata['lives'] = 0 - player.gamedata['icons'] = [] # Make sure our team has survival seconds set if they're all dead # (otherwise blocked new ffa players would be considered 'still # alive' in score tallying). - if self._get_total_team_lives( - player.team - ) == 0 and player.team.gamedata['survival_seconds'] is None: - player.team.gamedata['survival_seconds'] = 0 - ba.screenmessage(ba.Lstr(resource='playerDelayedJoinText', - subs=[('${PLAYER}', - player.get_name(full=True))]), - color=(0, 1, 0)) + if (self._get_total_team_lives(player.team) == 0 + and player.team.survival_seconds is None): + player.team.survival_seconds = 0 + ba.screenmessage( + ba.Lstr(resource='playerDelayedJoinText', + subs=[('${PLAYER}', player.get_name(full=True))]), + color=(0, 1, 0), + ) return - player.gamedata['lives'] = self.settings_raw['Lives Per Player'] + player.lives = self._lives_per_player if self._solo_mode: - player.gamedata['icons'] = [] - player.team.gamedata['spawn_order'].append(player) + player.team.spawn_order.append(player) self._update_solo_mode() else: # Create our icon and spawn. - player.gamedata['icons'] = [ - Icon(player, position=(0, 50), scale=0.8) - ] - if player.gamedata['lives'] > 0: + player.icons = [Icon(player, position=(0, 50), scale=0.8)] + if player.lives > 0: self.spawn_player(player) # Don't waste time doing this until begin. if self.has_begun(): self._update_icons() + def on_begin(self) -> None: + super().on_begin() + self._start_time = ba.time() + self.setup_standard_time_limit(self._time_limit) + self.setup_standard_powerup_drops() + if self._solo_mode: + self._vs_text = ba.NodeActor( + ba.newnode('text', + attrs={ + 'position': (0, 105), + 'h_attach': 'center', + 'h_align': 'center', + 'maxwidth': 200, + 'shadow': 0.5, + 'vr_depth': 390, + 'scale': 0.6, + 'v_attach': 'bottom', + 'color': (0.8, 0.8, 0.3, 1.0), + 'text': ba.Lstr(resource='vsText') + })) + + # If balance-team-lives is on, add lives to the smaller team until + # total lives match. + if (isinstance(self.session, ba.DualTeamSession) + and self._balance_total_lives and self.teams[0].players + and self.teams[1].players): + if self._get_total_team_lives( + self.teams[0]) < self._get_total_team_lives(self.teams[1]): + lesser_team = self.teams[0] + greater_team = self.teams[1] + else: + lesser_team = self.teams[1] + greater_team = self.teams[0] + add_index = 0 + while (self._get_total_team_lives(lesser_team) < + self._get_total_team_lives(greater_team)): + lesser_team.players[add_index].lives += 1 + add_index = (add_index + 1) % len(lesser_team.players) + + self._update_icons() + + # We could check game-over conditions at explicit trigger points, + # but lets just do the simple thing and poll it. + ba.timer(1.0, self._update, repeat=True) + def _update_solo_mode(self) -> None: # For both teams, find the first player on the spawn order list with # lives remaining and spawn them if they're not alive. for team in self.teams: # Prune dead players from the spawn order. - team.gamedata['spawn_order'] = [ - p for p in team.gamedata['spawn_order'] if p - ] - for player in team.gamedata['spawn_order']: - if player.gamedata['lives'] > 0: + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: if not player.is_alive(): self.spawn_player(player) break @@ -315,7 +369,7 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): for team in self.teams: if len(team.players) == 1: player = team.players[0] - for icon in player.gamedata['icons']: + for icon in player.icons: icon.set_position_and_scale((xval, 30), 0.7) icon.update_for_lives() xval += x_offs @@ -325,7 +379,7 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): if self._solo_mode: # First off, clear out all icons. for player in self.players: - player.gamedata['icons'] = [] + player.icons = [] # Now for each team, cycle through our available players # adding icons. @@ -340,13 +394,13 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): test_lives = 1 while True: players_with_lives = [ - p for p in team.gamedata['spawn_order'] - if p and p.gamedata['lives'] >= test_lives + p for p in team.spawn_order + if p and p.lives >= test_lives ] if not players_with_lives: break for player in players_with_lives: - player.gamedata['icons'].append( + player.icons.append( Icon(player, position=(xval, (40 if is_first else 25)), scale=1.0 if is_first else 0.5, @@ -369,12 +423,12 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): xval = 50 x_offs = 85 for player in team.players: - for icon in player.gamedata['icons']: + for icon in player.icons: icon.set_position_and_scale((xval, 30), 0.7) icon.update_for_lives() xval += x_offs - def _get_spawn_point(self, player: ba.Player) -> Optional[ba.Vec3]: + def _get_spawn_point(self, player: Player) -> Optional[ba.Vec3]: del player # Unused. # In solo-mode, if there's an existing live player on the map, spawn at @@ -403,119 +457,76 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): return points[-1][1] return None - def spawn_player(self, player: ba.Player) -> ba.Actor: + def spawn_player(self, player: Player) -> ba.Actor: actor = self.spawn_player_spaz(player, self._get_spawn_point(player)) if not self._solo_mode: ba.timer(0.3, ba.Call(self._print_lives, player)) # If we have any icons, update their state. - for icon in player.gamedata['icons']: + for icon in player.icons: icon.handle_player_spawned() return actor - def _print_lives(self, player: ba.Player) -> None: + def _print_lives(self, player: Player) -> None: from bastd.actor import popuptext - assert player # Shouldn't be passing invalid refs around. + + # We get called in a timer so it's possible our player has left/etc. if not player or not player.is_alive() or not player.node: return - popuptext.PopupText('x' + str(player.gamedata['lives'] - 1), + popuptext.PopupText('x' + str(player.lives - 1), color=(1, 1, 0, 1), offset=(0, -0.8, 0), random_offset=0.0, scale=1.8, position=player.node.position).autoretain() - def on_player_leave(self, player: ba.Player) -> None: + def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) - player.gamedata['icons'] = None + player.icons = [] # Remove us from spawn-order. if self._solo_mode: - if player in player.team.gamedata['spawn_order']: - player.team.gamedata['spawn_order'].remove(player) + if player in player.team.spawn_order: + player.team.spawn_order.remove(player) # Update icons in a moment since our team will be gone from the # list then. ba.timer(0, self._update_icons) - def on_begin(self) -> None: - super().on_begin() - self.setup_standard_time_limit(self.settings_raw['Time Limit']) - self.setup_standard_powerup_drops() - if self._solo_mode: - self._vs_text = ba.NodeActor( - ba.newnode('text', - attrs={ - 'position': (0, 105), - 'h_attach': 'center', - 'h_align': 'center', - 'maxwidth': 200, - 'shadow': 0.5, - 'vr_depth': 390, - 'scale': 0.6, - 'v_attach': 'bottom', - 'color': (0.8, 0.8, 0.3, 1.0), - 'text': ba.Lstr(resource='vsText') - })) - - # If balance-team-lives is on, add lives to the smaller team until - # total lives match. - if (isinstance(self.session, ba.DualTeamSession) - and self.settings_raw['Balance Total Lives'] - and self.teams[0].players and self.teams[1].players): - if self._get_total_team_lives( - self.teams[0]) < self._get_total_team_lives(self.teams[1]): - lesser_team = self.teams[0] - greater_team = self.teams[1] - else: - lesser_team = self.teams[1] - greater_team = self.teams[0] - add_index = 0 - while self._get_total_team_lives( - lesser_team) < self._get_total_team_lives(greater_team): - lesser_team.players[add_index].gamedata['lives'] += 1 - add_index = (add_index + 1) % len(lesser_team.players) - - self._update_icons() - - # We could check game-over conditions at explicit trigger points, - # but lets just do the simple thing and poll it. - ba.timer(1.0, self._update, repeat=True) - - def _get_total_team_lives(self, team: ba.Team) -> int: - return sum(player.gamedata['lives'] for player in team.players) + def _get_total_team_lives(self, team: Team) -> int: + return sum(player.lives for player in team.players) def handlemessage(self, msg: Any) -> Any: if isinstance(msg, playerspaz.PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) - player = msg.playerspaz(self).player + player: Player = msg.playerspaz(self).player - player.gamedata['lives'] -= 1 - if player.gamedata['lives'] < 0: + player.lives -= 1 + if player.lives < 0: ba.print_error( "Got lives < 0 in Elim; this shouldn't happen. solo:" + str(self._solo_mode)) - player.gamedata['lives'] = 0 + player.lives = 0 # If we have any icons, update their state. - for icon in player.gamedata['icons']: + for icon in player.icons: icon.handle_player_died() # Play big death sound on our last death # or for every one in solo mode. - if self._solo_mode or player.gamedata['lives'] == 0: + if self._solo_mode or player.lives == 0: ba.playsound(spaz.get_factory().single_player_death_sound) # If we hit zero lives, we're dead (and our team might be too). - if player.gamedata['lives'] == 0: + if player.lives == 0: # If the whole team is now dead, mark their survival time. if self._get_total_team_lives(player.team) == 0: assert self._start_time is not None - player.team.gamedata['survival_seconds'] = int( - ba.time() - self._start_time) + player.team.survival_seconds = int(ba.time() - + self._start_time) else: # Otherwise, in regular mode, respawn. if not self._solo_mode: @@ -523,8 +534,8 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): # In solo, put ourself at the back of the spawn order. if self._solo_mode: - player.team.gamedata['spawn_order'].remove(player) - player.team.gamedata['spawn_order'].append(player) + player.team.spawn_order.remove(player) + player.team.spawn_order.append(player) def _update(self) -> None: if self._solo_mode: @@ -532,11 +543,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): # list with lives remaining and spawn them if they're not alive. for team in self.teams: # Prune dead players from the spawn order. - team.gamedata['spawn_order'] = [ - p for p in team.gamedata['spawn_order'] if p - ] - for player in team.gamedata['spawn_order']: - if player.gamedata['lives'] > 0: + team.spawn_order = [p for p in team.spawn_order if p] + for player in team.spawn_order: + assert isinstance(player, Player) + if player.lives > 0: if not player.is_alive(): self.spawn_player(player) self._update_icons() @@ -548,10 +558,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): if len(self._get_living_teams()) < 2: self._round_end_timer = ba.Timer(0.5, self.end_game) - def _get_living_teams(self) -> List[ba.Team]: + def _get_living_teams(self) -> List[Team]: return [ team for team in self.teams - if len(team.players) > 0 and any(player.gamedata['lives'] > 0 + if len(team.players) > 0 and any(player.lives > 0 for player in team.players) ] @@ -561,5 +571,5 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]): results = ba.TeamGameResults() self._vs_text = None # Kill our 'vs' if its there. for team in self.teams: - results.set_team_score(team, team.gamedata['survival_seconds']) + results.set_team_score(team, team.survival_seconds) self.end(results=results) diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py index 1b98def2..be96698c 100644 --- a/assets/src/ba_data/python/bastd/game/football.py +++ b/assets/src/ba_data/python/bastd/game/football.py @@ -26,14 +26,15 @@ from __future__ import annotations import random +from dataclasses import dataclass from typing import TYPE_CHECKING import math import ba -from bastd.actor import bomb as stdbomb -from bastd.actor import flag as stdflag -from bastd.actor import playerspaz 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.scoreboard import Scoreboard if TYPE_CHECKING: @@ -66,8 +67,18 @@ class FootballFlag(stdflag.Flag): self.node.connectattr('position', self.light, 'position') +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + + # ba_meta export game -class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class FootballTeamGame(ba.TeamGameActivity[Player, Team]): """Football game for teams mode.""" @classmethod @@ -183,7 +194,7 @@ class FootballTeamGame(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() @@ -271,7 +282,7 @@ class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]): msg.flag.held_count -= 1 # Respawn dead players if they're still in the game. - elif isinstance(msg, playerspaz.PlayerSpazDeathMessage): + elif isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.playerspaz(self).player) @@ -320,7 +331,7 @@ class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]): self._flag = FootballFlag(position=self._flag_spawn_pos) -class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): +class FootballCoopGame(ba.CoopGameActivity[Player, Team]): """ Co-op variant of football """ @@ -385,15 +396,15 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): self._bot_types_initial: Optional[List[Type[spazbot.SpazBot]]] = None self._bot_types_7: Optional[List[Type[spazbot.SpazBot]]] = None self._bot_types_14: Optional[List[Type[spazbot.SpazBot]]] = None - self._bot_team: Optional[ba.Team] = None + self._bot_team: Optional[Team] = None self._starttime_ms: Optional[int] = None self._time_text: Optional[ba.NodeActor] = None self._time_text_input: Optional[ba.NodeActor] = None - self._tntspawner: Optional[stdbomb.TNTSpawner] = None + self._tntspawner: Optional[TNTSpawner] = None self._bots = spazbot.BotSet() self._bot_spawn_timer: Optional[ba.Timer] = None self._powerup_drop_timer: Optional[ba.Timer] = None - self.scoring_team: Optional[ba.Team] = None + self.scoring_team: Optional[Team] = None self._final_time_ms: Optional[int] = None self._time_text_timer: Optional[ba.Timer] = None self._flag_respawn_light: Optional[ba.Actor] = None @@ -508,11 +519,10 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): # Make a bogus team for our bots. bad_team_name = self.get_team_display_string('Bad Guys') - # self._bot_team = ba.Team(1, bad_team_name, (0.5, 0.4, 0.4)) - self._bot_team = ba.Team() - self._bot_team.id = 1 - self._bot_team.name = bad_team_name - self._bot_team.color = (0.5, 0.4, 0.4) + self._bot_team = Team() + self._bot_team.manual_init(team_id=1, + name=bad_team_name, + color=(0.5, 0.4, 0.4)) for team in [self.teams[0], self._bot_team]: team.gamedata['score'] = 0 @@ -547,7 +557,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): # Our TNT spawner (if applicable). if self._have_tnt: - self._tntspawner = stdbomb.TNTSpawner(position=(0, 1, -1)) + self._tntspawner = TNTSpawner(position=(0, 1, -1)) self._bots = spazbot.BotSet() self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) @@ -588,7 +598,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): if self._flag.node: for player in self.players: if player.actor: - assert isinstance(player.actor, playerspaz.PlayerSpaz) + assert isinstance(player.actor, PlayerSpaz) if (player.actor.is_alive() and player.actor.node.hold_node == self._flag.node): return @@ -808,7 +818,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): def handlemessage(self, msg: Any) -> Any: """ handle high-level game messages """ - if isinstance(msg, playerspaz.PlayerSpazDeathMessage): + if isinstance(msg, PlayerSpazDeathMessage): from bastd.actor import respawnicon # Respawn dead players. @@ -872,7 +882,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]): del player # Unused. self._player_has_punched = True - def spawn_player(self, player: ba.Player) -> ba.Actor: + def spawn_player(self, player: Player) -> ba.Actor: spaz = self.spawn_player_spaz(player, position=self.map.get_start_position( player.team.id)) diff --git a/assets/src/ba_data/python/bastd/game/keepaway.py b/assets/src/ba_data/python/bastd/game/keepaway.py index 41ea50a5..117cd5bb 100644 --- a/assets/src/ba_data/python/bastd/game/keepaway.py +++ b/assets/src/ba_data/python/bastd/game/keepaway.py @@ -25,19 +25,31 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING import ba -from bastd.actor import flag as stdflag -from bastd.actor 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, Tuple, Dict, Optional, Sequence, Union) +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + + # ba_meta export game -class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class KeepAwayGame(ba.TeamGameActivity[Player, Team]): """Game where you try to keep the flag away from your enemies.""" FLAG_NEW = 0 @@ -109,11 +121,11 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]): } self._flag_spawn_pos: Optional[Sequence[float]] = None self._update_timer: Optional[ba.Timer] = None - self._holding_players: List[ba.Player] = [] + self._holding_players: List[Player] = [] self._flag_state: Optional[int] = None self._flag_light: Optional[ba.Node] = None - self._scoring_team: Optional[ba.Team] = None - self._flag: Optional[stdflag.Flag] = None + self._scoring_team: Optional[Team] = None + self._flag: Optional[Flag] = None def get_instance_description(self) -> Union[str, Sequence]: return ('Carry the flag for ${ARG1} seconds.', @@ -127,7 +139,7 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]): self.default_music = ba.MusicType.KEEP_AWAY super().on_transition_in() - def on_team_join(self, team: ba.Team) -> None: + def on_team_join(self, team: Team) -> None: team.gamedata['time_remaining'] = self.settings_raw['Hold Time'] self._update_scoreboard() @@ -194,8 +206,8 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]): for player in self.players: holding_flag = False try: - assert isinstance(player.actor, playerspaz.PlayerSpaz) - if (player.actor.is_alive() and player.actor.node + assert isinstance(player.actor, (PlayerSpaz, type(None))) + if (player.actor and player.actor.node and player.actor.node.hold_node): holding_flag = ( player.actor.node.hold_node.getnodetype() == 'flag') @@ -235,8 +247,7 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]): ba.playsound(self._swipsound) self._flash_flag_spawn() assert self._flag_spawn_pos is not None - self._flag = stdflag.Flag(dropped_timeout=20, - position=self._flag_spawn_pos) + self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos) self._flag_state = self.FLAG_NEW self._flag_light = ba.newnode('light', owner=self._flag.node, @@ -268,15 +279,13 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]): countdown=True) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, playerspaz.PlayerSpazDeathMessage): + if isinstance(msg, PlayerSpazDeathMessage): # Augment standard behavior. super().handlemessage(msg) self.respawn_player(msg.playerspaz(self).player) - elif isinstance(msg, stdflag.FlagDeathMessage): + elif isinstance(msg, FlagDeathMessage): self._spawn_flag() - elif isinstance( - msg, - (stdflag.FlagDroppedMessage, stdflag.FlagPickedUpMessage)): + elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)): self._update_flag_state() else: super().handlemessage(msg) diff --git a/assets/src/ba_data/python/bastd/game/kingofthehill.py b/assets/src/ba_data/python/bastd/game/kingofthehill.py index c0fc84bf..d01f8e3b 100644 --- a/assets/src/ba_data/python/bastd/game/kingofthehill.py +++ b/assets/src/ba_data/python/bastd/game/kingofthehill.py @@ -26,11 +26,12 @@ from __future__ import annotations import weakref +from dataclasses import dataclass from typing import TYPE_CHECKING import ba -from bastd.actor import flag as stdflag -from bastd.actor import playerspaz +from bastd.actor.flag import Flag +from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage if TYPE_CHECKING: from weakref import ReferenceType @@ -38,8 +39,20 @@ if TYPE_CHECKING: Union) +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + time_at_flag: int = 0 + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + time_remaining: int + + # ba_meta export game -class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]): """Game where a team wins by holding a 'hill' for a set amount of time.""" FLAG_NEW = 0 @@ -71,23 +84,24 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]): def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: - return [('Hold Time', { - 'min_value': 10, - 'default': 30, - 'increment': 10 - }), - ('Time Limit', { - 'choices': [('None', 0), ('1 Minute', 60), - ('2 Minutes', 120), ('5 Minutes', 300), - ('10 Minutes', 600), ('20 Minutes', 1200)], - 'default': 0 - }), - ('Respawn Times', { - 'choices': [('Shorter', 0.25), ('Short', 0.5), - ('Normal', 1.0), ('Long', 2.0), - ('Longer', 4.0)], - 'default': 1.0 - })] + return [ + ('Hold Time', { + 'min_value': 10, + 'default': 30, + 'increment': 10 + }), + ('Time Limit', { + 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120), + ('5 Minutes', 300), ('10 Minutes', 600), + ('20 Minutes', 1200)], + 'default': 0 + }), + ('Respawn Times', { + 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0), + ('Long', 2.0), ('Longer', 4.0)], + 'default': 1.0 + }), + ] def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard @@ -109,9 +123,11 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]): } self._flag_pos: Optional[Sequence[float]] = None self._flag_state: Optional[int] = None - self._flag: Optional[stdflag.Flag] = None + self._flag: Optional[Flag] = None self._flag_light: Optional[ba.Node] = None - self._scoring_team: Optional[ReferenceType[ba.Team]] = None + self._scoring_team: Optional[ReferenceType[Team]] = None + self._hold_time = int(settings['Hold Time']) + self._time_limit = float(settings['Time Limit']) self._flag_region_material = ba.Material() self._flag_region_material.add_actions( @@ -124,38 +140,30 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]): ba.Call(self._handle_player_flag_region_collide, False)))) + # Base class overrides. + self.default_music = ba.MusicType.SCARY + def get_instance_description(self) -> Union[str, Sequence]: - return ('Secure the flag for ${ARG1} seconds.', - self.settings_raw['Hold Time']) + return 'Secure the flag for ${ARG1} seconds.', self._hold_time def get_instance_scoreboard_description(self) -> Union[str, Sequence]: - return ('secure the flag for ${ARG1} seconds', - self.settings_raw['Hold Time']) + return 'secure the flag for ${ARG1} seconds', self._hold_time - def on_transition_in(self) -> None: - self.default_music = ba.MusicType.SCARY - super().on_transition_in() - - def on_team_join(self, team: ba.Team) -> None: - team.gamedata['time_remaining'] = self.settings_raw['Hold Time'] - self._update_scoreboard() - - def on_player_join(self, player: ba.Player) -> None: - super().on_player_join(player) - player.gamedata['at_flag'] = 0 + def create_team(self, sessionteam: ba.SessionTeam) -> Team: + return Team(time_remaining=self._hold_time) 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() self._flag_pos = self.map.get_flag_position(None) ba.timer(1.0, self._tick, repeat=True) self._flag_state = self.FLAG_NEW self.project_flag_stand(self._flag_pos) - self._flag = stdflag.Flag(position=self._flag_pos, - touchable=False, - color=(1, 1, 1)) + self._flag = Flag(position=self._flag_pos, + touchable=False, + color=(1, 1, 1)) self._flag_light = ba.newnode('light', attrs={ 'position': self._flag_pos, @@ -184,51 +192,47 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]): # Give holding players points. for player in self.players: - if player.gamedata['at_flag'] > 0: + if player.time_at_flag > 0: self.stats.player_scored(player, 3, screenmessage=False, display=False) - if self._scoring_team is None: scoring_team = None else: scoring_team = self._scoring_team() if scoring_team: - if scoring_team.gamedata['time_remaining'] > 0: + if scoring_team.time_remaining > 0: ba.playsound(self._tick_sound) - scoring_team.gamedata['time_remaining'] = max( - 0, scoring_team.gamedata['time_remaining'] - 1) + scoring_team.time_remaining = max(0, + scoring_team.time_remaining - 1) self._update_scoreboard() - if scoring_team.gamedata['time_remaining'] > 0: + if scoring_team.time_remaining > 0: assert self._flag is not None - self._flag.set_score_text( - str(scoring_team.gamedata['time_remaining'])) + self._flag.set_score_text(str(scoring_team.time_remaining)) # Announce numbers we have sounds for. try: - ba.playsound(self._countdownsounds[ - scoring_team.gamedata['time_remaining']]) + ba.playsound( + self._countdownsounds[scoring_team.time_remaining]) except Exception: pass # winner - if scoring_team.gamedata['time_remaining'] <= 0: + if scoring_team.time_remaining <= 0: self.end_game() def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: - results.set_team_score( - team, self.settings_raw['Hold Time'] - - team.gamedata['time_remaining']) + results.set_team_score(team, self._hold_time - team.time_remaining) self.end(results=results, announce_delay=0) def _update_flag_state(self) -> None: holding_teams = set(player.team for player in self.players - if player.gamedata['at_flag']) + if player.time_at_flag) prev_state = self._flag_state assert self._flag_light assert self._flag is not None @@ -253,35 +257,36 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]): ba.playsound(self._swipsound) def _handle_player_flag_region_collide(self, colliding: bool) -> None: - playernode = ba.get_collision_info('opposing_node') - try: - player = playernode.getdelegate().getplayer() - except Exception: + delegate = ba.get_collision_info('opposing_node').getdelegate() + if not isinstance(delegate, PlayerSpaz): + return + player = ba.playercast_o(Player, delegate.getplayer()) + if not player: return # Different parts of us can collide so a single value isn't enough # also don't count it if we're dead (flying heads shouldn't be able to # win the game :-) if colliding and player.is_alive(): - player.gamedata['at_flag'] += 1 + player.time_at_flag += 1 else: - player.gamedata['at_flag'] = max(0, player.gamedata['at_flag'] - 1) + player.time_at_flag = max(0, player.time_at_flag - 1) self._update_flag_state() def _update_scoreboard(self) -> None: for team in self.teams: self._scoreboard.set_team_value(team, - team.gamedata['time_remaining'], - self.settings_raw['Hold Time'], + team.time_remaining, + self._hold_time, countdown=True) def handlemessage(self, msg: Any) -> Any: - if isinstance(msg, playerspaz.PlayerSpazDeathMessage): + if isinstance(msg, PlayerSpazDeathMessage): super().handlemessage(msg) # Augment default. - # No longer can count as at_flag once dead. + # No longer can count as time_at_flag once dead. player = msg.playerspaz(self).player - player.gamedata['at_flag'] = 0 + 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 462ea14a..8b5853ea 100644 --- a/assets/src/ba_data/python/bastd/game/meteorshower.py +++ b/assets/src/ba_data/python/bastd/game/meteorshower.py @@ -255,6 +255,10 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]): # (these per-player scores are only meaningful in team-games) for team in self.teams: for player in team.players: + + if not player: + print(f'GOT DEAD PLAYER {id(player)}') + survived = False # Throw an extra fudge factor in so teams that diff --git a/assets/src/ba_data/python/bastd/game/onslaught.py b/assets/src/ba_data/python/bastd/game/onslaught.py index 192da89f..c9df8d80 100644 --- a/assets/src/ba_data/python/bastd/game/onslaught.py +++ b/assets/src/ba_data/python/bastd/game/onslaught.py @@ -27,6 +27,7 @@ from __future__ import annotations import math import random +from dataclasses import dataclass from typing import TYPE_CHECKING import ba @@ -38,7 +39,17 @@ if TYPE_CHECKING: from bastd.actor.scoreboard import Scoreboard -class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]): +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +class OnslaughtGame(ba.CoopGameActivity[Player, Team]): """Co-op game where players try to survive attacking waves of enemies.""" tips: List[Union[str, Dict[str, Any]]] = [ @@ -622,7 +633,7 @@ class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]): return groups - def spawn_player(self, player: ba.Player) -> ba.Actor: + def spawn_player(self, player: Player) -> ba.Actor: # We keep track of who got hurt each wave for score purposes. player.gamedata['has_been_hurt'] = False @@ -816,7 +827,7 @@ class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]): self._score += self._time_bonus self._update_scores() - def _award_flawless_bonus(self, player: ba.Player) -> None: + def _award_flawless_bonus(self, player: Player) -> None: ba.playsound(self._cashregistersound) try: if player.is_alive(): diff --git a/assets/src/ba_data/python/bastd/game/race.py b/assets/src/ba_data/python/bastd/game/race.py index 0260de26..60b80066 100644 --- a/assets/src/ba_data/python/bastd/game/race.py +++ b/assets/src/ba_data/python/bastd/game/race.py @@ -66,8 +66,19 @@ class RaceRegion(ba.Actor): }) +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + distance_txt: Optional[ba.Node] = None + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + + # ba_meta export game -class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class RaceGame(ba.TeamGameActivity[Player, Team]): """Game of racing around a track.""" @classmethod @@ -186,7 +197,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): for rpt in pts: self._regions.append(RaceRegion(rpt, len(self._regions))) - def _flash_player(self, player: ba.Player, scale: float) -> None: + def _flash_player(self, player: Player, scale: float) -> None: assert isinstance(player.actor, PlayerSpaz) assert player.actor.node pos = player.actor.node.position @@ -214,7 +225,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): region = region_node.getdelegate() if not player or not region: return - assert isinstance(player, ba.Player) + assert isinstance(player, Player) assert isinstance(region, RaceRegion) last_region = player.gamedata['last_region'] @@ -342,13 +353,13 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): except Exception as exc: print('Exception printing lap:', exc) - def on_team_join(self, team: ba.Team) -> None: + def on_team_join(self, team: Team) -> None: team.gamedata['time'] = None team.gamedata['lap'] = 0 team.gamedata['finished'] = False self._update_scoreboard() - def on_player_join(self, player: ba.Player) -> None: + def on_player_join(self, player: Player) -> None: player.gamedata['last_region'] = 0 player.gamedata['lap'] = 0 player.gamedata['distance'] = 0.0 @@ -356,7 +367,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): player.gamedata['rank'] = None super().on_player_join(player) - def on_player_leave(self, player: ba.Player) -> None: + def on_player_leave(self, player: Player) -> None: super().on_player_leave(player) # A player leaving disqualifies the team if 'Entire Team Must Finish' @@ -550,16 +561,16 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): player.gamedata['distance'] = amt # Sort players by distance and update their ranks. - p_list = [[player.gamedata['distance'], player] + p_list = [(player.gamedata['distance'], player) for player in self.players] p_list.sort(reverse=True, key=lambda x: x[0]) for i, plr in enumerate(p_list): try: plr[1].gamedata['rank'] = i - if plr[1].actor is not None: + if plr[1].actor: # noinspection PyUnresolvedReferences - node = plr[1].actor.distance_txt + node = plr[1].distance_txt if node: node.text = str(i + 1) if plr[1].is_alive() else '' except Exception: @@ -620,10 +631,11 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): self._flash_mine(m_index) ba.timer(0.95, ba.Call(self._make_mine, m_index)) - def spawn_player(self, player: ba.Player) -> ba.Actor: + def spawn_player(self, player: Player) -> ba.Actor: if player.team.gamedata['finished']: - # FIXME: This is not type-safe - # (this call is expected to return an Actor). + # FIXME: This is not type-safe! + # This call is expected to always return an Actor! + # Perhaps we need something like can_spawn_player()... # noinspection PyTypeChecker return None # type: ignore pos = self._regions[player.gamedata['last_region']].pos @@ -661,9 +673,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): 'scale': 0.02, 'h_align': 'center' }) - # FIXME store this in a type-safe way - # noinspection PyTypeHints - spaz.distance_txt = distance_txt # type: ignore + player.distance_txt = distance_txt mathnode.connectattr('output', distance_txt, 'position') return spaz diff --git a/assets/src/ba_data/python/bastd/game/targetpractice.py b/assets/src/ba_data/python/bastd/game/targetpractice.py index 505dec52..3c18deb4 100644 --- a/assets/src/ba_data/python/bastd/game/targetpractice.py +++ b/assets/src/ba_data/python/bastd/game/targetpractice.py @@ -26,6 +26,7 @@ from __future__ import annotations import random +from dataclasses import dataclass from typing import TYPE_CHECKING import ba @@ -37,8 +38,20 @@ if TYPE_CHECKING: from bastd.actor.bomb import Bomb, Blast +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + streak: int = 0 + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + score: int = 0 + + # ba_meta export game -class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): +class TargetPracticeGame(ba.TeamGameActivity[Player, Team]): """Game where players try to hit targets with bombs.""" @classmethod @@ -63,14 +76,18 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): def get_settings( cls, sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: - return [('Target Count', { - 'min_value': 1, - 'default': 3 - }), ('Enable Impact Bombs', { - 'default': True - }), ('Enable Triple Bombs', { - 'default': True - })] + return [ + ('Target Count', { + 'min_value': 1, + 'default': 3 + }), + ('Enable Impact Bombs', { + 'default': True + }), + ('Enable Triple Bombs', { + 'default': True + }), + ] def __init__(self, settings: Dict[str, Any]): from bastd.actor.scoreboard import Scoreboard @@ -79,13 +96,14 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): self._targets: List[Target] = [] self._update_timer: Optional[ba.Timer] = None self._countdown: Optional[OnScreenCountdown] = None + self._target_count = int(settings['Target Count']) + self._enable_impact_bombs = bool(settings['Enable Impact Bombs']) + self._enable_triple_bombs = bool(settings['Enable Triple Bombs']) - def on_transition_in(self) -> None: + # Base class overrides self.default_music = ba.MusicType.FORWARD_MARCH - super().on_transition_in() - def on_team_join(self, team: ba.Team) -> None: - team.gamedata['score'] = 0 + def on_team_join(self, team: Team) -> None: if self.has_begun(): self.update_scoreboard() @@ -95,28 +113,27 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): self.update_scoreboard() # Number of targets is based on player count. - num_targets = self.settings_raw['Target Count'] - for i in range(num_targets): + for i in range(self._target_count): ba.timer(5.0 + i * 1.0, self._spawn_target) self._update_timer = ba.Timer(1.0, self._update, repeat=True) self._countdown = OnScreenCountdown(60, endcall=self.end_game) ba.timer(4.0, self._countdown.start) - def spawn_player(self, player: ba.Player) -> ba.Actor: + def spawn_player(self, player: Player) -> ba.Actor: spawn_center = (0, 3, -5) pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], spawn_center[2] + random.uniform(-1.5, 1.5)) # Reset their streak. - player.gamedata['streak'] = 0 + player.streak = 0 spaz = self.spawn_player_spaz(player, position=pos) # Give players permanent triple impact bombs and wire them up # to tell us when they drop a bomb. - if self.settings_raw['Enable Impact Bombs']: + if self._enable_impact_bombs: spaz.bomb_type = 'impact' - if self.settings_raw['Enable Triple Bombs']: + if self._enable_triple_bombs: spaz.set_bomb_count(3) spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) return spaz @@ -166,7 +183,7 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): # Feed the explosion point to all our targets and get points in return. # Note: we operate on a copy of self._targets since the list may change # under us if we hit stuff (don't wanna get points for new targets). - player = bomb.get_source_player() + player = ba.playercast_o(Player, bomb.get_source_player()) if not player: return # could happen if they leave after throwing a bomb.. @@ -174,9 +191,9 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): target.do_hit_at_position(pos, player) for target in list(self._targets)) if bullseye: - player.gamedata['streak'] += 1 + player.streak += 1 else: - player.gamedata['streak'] = 0 + player.streak = 0 def _update(self) -> None: """Misc. periodic updating.""" @@ -200,12 +217,12 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]): def update_scoreboard(self) -> None: """Update the game scoreboard with current team values.""" for team in self.teams: - self._scoreboard.set_team_value(team, team.gamedata['score']) + self._scoreboard.set_team_value(team, team.score) 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) @@ -278,8 +295,7 @@ class Target(ba.Actor): """Given a point, returns distance squared from it.""" return (ba.Vec3(pos) - self._position).length() - def do_hit_at_position(self, pos: Sequence[float], - player: ba.Player) -> bool: + def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool: """Handle a bomb hit at the given position.""" # pylint: disable=too-many-statements from bastd.actor import popuptext @@ -316,7 +332,7 @@ class Target(ba.Actor): ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) popupscale = 1.8 popupcolor = (1, 1, 0, 1) - streak = player.gamedata['streak'] + streak = player.streak points = 10 + min(20, streak * 2) ba.playsound(ba.getsound('bellHigh')) if streak > 0: @@ -357,7 +373,7 @@ class Target(ba.Actor): scale=popupscale).autoretain() # Give this player's team points and update the score-board. - player.team.gamedata['score'] += points + player.team.score += points assert isinstance(activity, TargetPracticeGame) activity.update_scoreboard() diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/assets/src/ba_data/python/bastd/game/thelaststand.py index 2cc04437..cff0e3c4 100644 --- a/assets/src/ba_data/python/bastd/game/thelaststand.py +++ b/assets/src/ba_data/python/bastd/game/thelaststand.py @@ -23,6 +23,7 @@ from __future__ import annotations import random +from dataclasses import dataclass from typing import TYPE_CHECKING import ba @@ -35,7 +36,17 @@ if TYPE_CHECKING: from bastd.actor.scoreboard import Scoreboard -class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]): +@dataclass(eq=False) +class Player(ba.Player['Team']): + """Our player type for this game.""" + + +@dataclass(eq=False) +class Team(ba.Team[Player]): + """Our team type for this game.""" + + +class TheLastStandGame(ba.CoopGameActivity[Player, Team]): """Slow motion how-long-can-you-last game.""" tips = [ @@ -118,7 +129,7 @@ class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]): self._tntspawner = TNTSpawner(position=self._tntspawnpos, respawn_time=10.0) - 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)) diff --git a/docs/ba_module.md b/docs/ba_module.md index 20ca4906..d041935b 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-05-18 for Ballistica version 1.5.0 build 20021

+

last updated on 2020-05-19 for Ballistica version 1.5.0 build 20021

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


@@ -4741,6 +4741,15 @@ of the session.

Throws a ba.SessionTeamNotFoundError if there is none.

+ + +

Methods:

+
+

manual_init()

+

manual_init(self, team_id: int, name: Union[ba.Lstr, str], color: Tuple[float, ...]) -> None

+ +

Manually init a team for uses such as bots.

+

@@ -4949,7 +4958,7 @@ Results for a completed ba.TeamGameActivity

set_team_score()

-

set_team_score(self, team: Union[ba.SessionTeam, ba.Team], score: int) -> None

+

set_team_score(self, team: Union[ba.SessionTeam, ba.Team], score: Optional[int]) -> None

Set the score for a given ba.Team.