mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 23:49:47 +08:00
More minigame modernizing
This commit is contained in:
parent
0742bce678
commit
d29cb35ff1
@ -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"
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)))
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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():
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2020-05-18 for Ballistica version 1.5.0 build 20021</em></h4>
|
||||
<h4><em>last updated on 2020-05-19 for Ballistica version 1.5.0 build 20021</em></h4>
|
||||
<p>This page documents the Python classes and functions in the 'ba' module,
|
||||
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||
<hr>
|
||||
@ -4741,6 +4741,15 @@ of the session.</p>
|
||||
|
||||
<p> Throws a <a href="#class_ba_SessionTeamNotFoundError">ba.SessionTeamNotFoundError</a> if there is none.</p>
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>Methods:</h3>
|
||||
<dl>
|
||||
<dt><h4><a name="method_ba_Team__manual_init">manual_init()</a></dt></h4><dd>
|
||||
<p><span>manual_init(self, team_id: int, name: Union[<a href="#class_ba_Lstr">ba.Lstr</a>, str], color: Tuple[float, ...]) -> None</span></p>
|
||||
|
||||
<p>Manually init a team for uses such as bots.</p>
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
<hr>
|
||||
@ -4949,7 +4958,7 @@ Results for a completed <a href="#class_ba_TeamGameActivity">ba.TeamGameActivity
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_TeamGameResults__set_team_score">set_team_score()</a></dt></h4><dd>
|
||||
<p><span>set_team_score(self, team: Union[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>, <a href="#class_ba_Team">ba.Team</a>], score: int) -> None</span></p>
|
||||
<p><span>set_team_score(self, team: Union[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>, <a href="#class_ba_Team">ba.Team</a>], score: Optional[int]) -> None</span></p>
|
||||
|
||||
<p>Set the score for a given <a href="#class_ba_Team">ba.Team</a>.</p>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user