More minigame modernizing

This commit is contained in:
Eric Froemling 2020-05-19 01:34:24 -07:00
parent 0742bce678
commit d29cb35ff1
20 changed files with 798 additions and 650 deletions

View File

@ -4132,16 +4132,16 @@
"assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c", "assets/build/windows/x64/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/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", "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/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/7b/b6/9cf8cb137735545a5d9c5d2abc14", "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/4c/a0/b18786c5c4a3b8c8ec9417167412", "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/fe/41/1bf6ad4d57a589fb26cece393563", "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/42/6d/39d0af901ac06b9ad655a9837a6d", "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/98/0d/0d4594d20813a5b3dacb3aa2022f", "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/8e/73/1e215e66ce6fd155b4af840465f2", "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/a2/21/aad47597886fe15f228386fdf0a5", "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/3c/e5/bdd60cba90f6955ba7a4a932bd45", "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/4f/b7/86dec4a8ab32edaf850d3c0fe2c2", "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/09/ab/48ca17f389375fd4db02295bab73", "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/f4/12/6fb56bf6484b5b93fbfd3206bbdf" "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a0/b3/ad0fa29417f197bed6f3fb184a3f"
} }

View File

@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
""" """
# (hash we can use to see if this file is out of date) # (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. # I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression # pylint: disable=useless-suppression

View File

@ -331,106 +331,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
raise RuntimeError(f'destroy() called when' raise RuntimeError(f'destroy() called when'
f' already expired for {self}') 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: def retain_actor(self, actor: ba.Actor) -> None:
"""Add a strong-reference to a ba.Actor to this Activity. """Add a strong-reference to a ba.Actor to this Activity.
@ -619,127 +519,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
except Exception: except Exception:
print_exception('Error in on_transition_out for', self) 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: def begin(self, session: ba.Session) -> None:
"""Begin the activity. """Begin the activity.
@ -779,6 +558,146 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self.end(self._should_end_immediately_results, self.end(self._should_end_immediately_results,
self._should_end_immediately_delay) 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 # noinspection PyUnresolvedReferences
def _setup_player_and_team_types(self) -> None: def _setup_player_and_team_types(self) -> None:
"""Pull player and team types from our typing.Generic params.""" """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.') f' if you do not want to override it.')
assert issubclass(self._playertype, Player) assert issubclass(self._playertype, Player)
assert issubclass(self._teamtype, Team) 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()

View File

@ -77,7 +77,7 @@ class TeamGameResults:
self._score_type = score_info.scoretype self._score_type = score_info.scoretype
def set_team_score(self, team: Union[ba.SessionTeam, ba.Team], 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. """Set the score for a given ba.Team.
This can be a number or None. This can be a number or None.

View File

@ -59,6 +59,18 @@ class Player(Generic[TeamType]):
""" """
from ba._nodeactor import NodeActor from ba._nodeactor import NodeActor
import _ba 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.actor = None
self.character = '' self.character = ''
self._nodeactor: Optional[ba.NodeActor] = None self._nodeactor: Optional[ba.NodeActor] = None

View File

@ -126,6 +126,18 @@ class Team(Generic[PlayerType]):
(internal) (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.players = []
self._sessionteam = weakref.ref(sessionteam) self._sessionteam = weakref.ref(sessionteam)
self.id = sessionteam.id self.id = sessionteam.id
@ -134,6 +146,15 @@ class Team(Generic[PlayerType]):
self.gamedata = sessionteam.gamedata self.gamedata = sessionteam.gamedata
self.sessiondata = sessionteam.sessiondata 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 @property
def sessionteam(self) -> SessionTeam: def sessionteam(self) -> SessionTeam:
"""Return the ba.SessionTeam corresponding to this Team. """Return the ba.SessionTeam corresponding to this Team.

View File

@ -28,7 +28,7 @@ import weakref
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
from bastd.actor import spaz as basespaz from bastd.actor.spaz import Spaz
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Optional, List, Tuple, Sequence, Type, Callable from typing import Any, Optional, List, Tuple, Sequence, Type, Callable
@ -87,7 +87,7 @@ class SpazBotDeathMessage:
self.how = how self.how = how
class SpazBot(basespaz.Spaz): class SpazBot(Spaz):
"""A really dumb AI version of ba.Spaz. """A really dumb AI version of ba.Spaz.
category: Bot Classes category: Bot Classes
@ -127,13 +127,12 @@ class SpazBot(basespaz.Spaz):
def __init__(self) -> None: def __init__(self) -> None:
"""Instantiate a spaz-bot.""" """Instantiate a spaz-bot."""
basespaz.Spaz.__init__(self, super().__init__(color=self.color,
color=self.color, highlight=self.highlight,
highlight=self.highlight, character=self.character,
character=self.character, source_player=None,
source_player=None, start_invincible=False,
start_invincible=False, can_accept_powerups=False)
can_accept_powerups=False)
# If you need to add custom behavior to a bot, set this to a callable # 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 # 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)) ba.getactivity().handlemessage(SpazBotPunchedMessage(self, damage))
def on_expire(self) -> None: 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 # We're being torn down; release our callback(s) so there's
# no chance of them keeping activities or other things alive. # 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. # Update our list of player points for the bots to use.
player_pts = [] player_pts = []
for player in ba.getactivity().players: for player in ba.getactivity().players:
assert isinstance(player, ba.Player)
try: try:
if player.is_alive(): if player.is_alive():
assert isinstance(player.actor, basespaz.Spaz) assert isinstance(player.actor, Spaz)
assert player.actor.node assert player.actor.node
player_pts.append((ba.Vec3(player.actor.node.position), player_pts.append((ba.Vec3(player.actor.node.position),
ba.Vec3(player.actor.node.velocity))) ba.Vec3(player.actor.node.velocity)))

View File

@ -37,12 +37,12 @@ if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Tuple, Sequence, Union from typing import Any, Type, List, Dict, Tuple, Sequence, Union
@dataclass @dataclass(eq=False)
class Player(ba.Player['Team']): class Player(ba.Player['Team']):
"""Our player type for this game.""" """Our player type for this game."""
@dataclass @dataclass(eq=False)
class Team(ba.Team[Player]): class Team(ba.Team[Player]):
"""Our team type for this game.""" """Our team type for this game."""
base_pos: Sequence[float] base_pos: Sequence[float]

View File

@ -81,13 +81,13 @@ class CTFFlag(stdflag.Flag):
return delegate if isinstance(delegate, CTFFlag) else None return delegate if isinstance(delegate, CTFFlag) else None
@dataclass @dataclass(eq=False)
class Player(ba.Player['Team']): class Player(ba.Player['Team']):
"""Our player type for this game.""" """Our player type for this game."""
touching_own_flag: int = 0 touching_own_flag: int = 0
@dataclass @dataclass(eq=False)
class Team(ba.Team[Player]): class Team(ba.Team[Player]):
"""Our team type for this game.""" """Our team type for this game."""
base_pos: Sequence[float] base_pos: Sequence[float]

View File

@ -25,19 +25,31 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dataclasses import dataclass
import ba import ba
from bastd.actor import flag from bastd.actor.flag import Flag
from bastd.actor import playerspaz from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
from bastd.actor import spaz
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import (Any, Type, List, Dict, Tuple, Optional, Sequence, from typing import (Any, Type, List, Dict, Tuple, Optional, Sequence,
Union) 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 # 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' Game involving trying to remain the one 'chosen one'
for a set length of time while everyone else tries to 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]): def __init__(self, settings: Dict[str, Any]):
from bastd.actor.scoreboard import Scoreboard from bastd.actor.scoreboard import Scoreboard
super().__init__(settings) super().__init__(settings)
if self.settings_raw['Epic Mode']:
self.slow_motion = True
self._scoreboard = Scoreboard() self._scoreboard = Scoreboard()
self._chosen_one_player: Optional[ba.Player] = None self._chosen_one_player: Optional[Player] = None
self._swipsound = ba.getsound('swip') self._swipsound = ba.getsound('swip')
self._countdownsounds: Dict[int, ba.Sound] = { self._countdownsounds: Dict[int, ba.Sound] = {
10: ba.getsound('announceTen'), 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._flag_spawn_pos: Optional[Sequence[float]] = None
self._reset_region_material: Optional[ba.Material] = 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._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]: def get_instance_description(self) -> Union[str, Sequence]:
return 'There can be only one.' return 'There can be only one.'
def on_transition_in(self) -> None: def create_team(self, sessionteam: ba.SessionTeam) -> Team:
self.default_music = (ba.MusicType.EPIC return Team(time_remaining=self._chosen_one_time)
if self.settings_raw['Epic Mode'] else
ba.MusicType.CHOSEN_ONE)
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['Chosen One Time']
self._update_scoreboard() 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) super().on_player_leave(player)
if self._get_chosen_one_player() is player: if self._get_chosen_one_player() is player:
self._set_chosen_one_player(None) self._set_chosen_one_player(None)
def on_begin(self) -> None: def on_begin(self) -> None:
super().on_begin() 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.setup_standard_powerup_drops()
self._flag_spawn_pos = self.map.get_flag_position(None) self._flag_spawn_pos = self.map.get_flag_position(None)
self.project_flag_stand(self._flag_spawn_pos) self.project_flag_stand(self._flag_spawn_pos)
@ -164,7 +180,7 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
'materials': [mat] 'materials': [mat]
}) })
def _get_chosen_one_player(self) -> Optional[ba.Player]: def _get_chosen_one_player(self) -> Optional[Player]:
if self._chosen_one_player: if self._chosen_one_player:
return self._chosen_one_player return self._chosen_one_player
return None return None
@ -173,13 +189,11 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
# If we have a chosen one, ignore these. # If we have a chosen one, ignore these.
if self._get_chosen_one_player() is not None: if self._get_chosen_one_player() is not None:
return return
try: delegate = ba.get_collision_info('opposing_node').getdelegate()
player = (ba.get_collision_info( if isinstance(delegate, PlayerSpaz):
'opposing_node').getdelegate().getplayer()) player = ba.playercast_o(Player, delegate.getplayer())
except Exception: if player is not None and player.is_alive():
return self._set_chosen_one_player(player)
if player is not None and player.is_alive():
self._set_chosen_one_player(player)
def _flash_flag_spawn(self) -> None: def _flash_flag_spawn(self) -> None:
light = ba.newnode('light', light = ba.newnode('light',
@ -210,29 +224,24 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
screenmessage=False, screenmessage=False,
display=False) display=False)
scoring_team.gamedata['time_remaining'] = max( scoring_team.time_remaining = max(
0, scoring_team.gamedata['time_remaining'] - 1) 0, scoring_team.time_remaining - 1)
# show the count over their head # Show the count over their head
try: if scoring_team.time_remaining > 0:
if scoring_team.gamedata['time_remaining'] > 0: if isinstance(player.actor, PlayerSpaz) and player.actor:
if isinstance(player.actor, spaz.Spaz): player.actor.set_score_text(
player.actor.set_score_text( str(scoring_team.time_remaining))
str(scoring_team.gamedata['time_remaining']))
except Exception:
pass
self._update_scoreboard() self._update_scoreboard()
# announce numbers we have sounds for # announce numbers we have sounds for
try: if scoring_team.time_remaining in self._countdownsounds:
ba.playsound(self._countdownsounds[ ba.playsound(
scoring_team.gamedata['time_remaining']]) self._countdownsounds[scoring_team.time_remaining])
except Exception:
pass
# Winner! # Winner!
if scoring_team.gamedata['time_remaining'] <= 0: if scoring_team.time_remaining <= 0:
self.end_game() self.end_game()
else: else:
@ -247,89 +256,81 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def end_game(self) -> None: def end_game(self) -> None:
results = ba.TeamGameResults() results = ba.TeamGameResults()
for team in self.teams: for team in self.teams:
results.set_team_score( results.set_team_score(team,
team, self.settings_raw['Chosen One Time'] - self._chosen_one_time - team.time_remaining)
team.gamedata['time_remaining'])
self.end(results=results, announce_delay=0) self.end(results=results, announce_delay=0)
def _set_chosen_one_player(self, player: Optional[ba.Player]) -> None: def _set_chosen_one_player(self, player: Optional[Player]) -> None:
try: for p_other in self.players:
for p_other in self.players: p_other.chosen_light = None
p_other.gamedata['chosen_light'] = None ba.playsound(self._swipsound)
ba.playsound(self._swipsound) if not player:
if not player: assert self._flag_spawn_pos is not None
assert self._flag_spawn_pos is not None self._flag = Flag(color=(1, 0.9, 0.2),
self._flag = flag.Flag(color=(1, 0.9, 0.2), position=self._flag_spawn_pos,
position=self._flag_spawn_pos, touchable=False)
touchable=False) self._chosen_one_player = None
self._chosen_one_player = None
# Create a light to highlight the flag; # Create a light to highlight the flag;
# this will go away when the flag dies. # this will go away when the flag dies.
ba.newnode('light', ba.newnode('light',
owner=self._flag.node, owner=self._flag.node,
attrs={ attrs={
'position': self._flag_spawn_pos, 'position': self._flag_spawn_pos,
'intensity': 0.6, 'intensity': 0.6,
'height_attenuated': False, 'height_attenuated': False,
'volume_intensity_scale': 0.1, 'volume_intensity_scale': 0.1,
'radius': 0.1, 'radius': 0.1,
'color': (1.2, 1.2, 0.4) 'color': (1.2, 1.2, 0.4)
}) })
# Also an extra momentary flash. # Also an extra momentary flash.
self._flash_flag_spawn() self._flash_flag_spawn()
else: else:
if player.actor is not None: if player.actor:
self._flag = None self._flag = None
self._chosen_one_player = player self._chosen_one_player = player
if player.actor: if self._chosen_one_gets_shield:
if self.settings_raw['Chosen One Gets Shield']: player.actor.handlemessage(ba.PowerupMessage('shield'))
player.actor.handlemessage( if self._chosen_one_gets_gloves:
ba.PowerupMessage('shield')) player.actor.handlemessage(ba.PowerupMessage('punch'))
if self.settings_raw['Chosen One Gets Gloves']:
player.actor.handlemessage(
ba.PowerupMessage('punch'))
# Use a color that's partway between their team color # Use a color that's partway between their team color
# and white. # and white.
color = [ color = [
0.3 + c * 0.7 0.3 + c * 0.7
for c in ba.normalized_color(player.team.color) for c in ba.normalized_color(player.team.color)
] ]
light = player.gamedata['chosen_light'] = ba.NodeActor( light = player.chosen_light = ba.NodeActor(
ba.newnode('light', ba.newnode('light',
attrs={ attrs={
'intensity': 0.6, 'intensity': 0.6,
'height_attenuated': False, 'height_attenuated': False,
'volume_intensity_scale': 0.1, 'volume_intensity_scale': 0.1,
'radius': 0.13, 'radius': 0.13,
'color': color 'color': color
})) }))
assert light.node assert light.node
ba.animate(light.node, ba.animate(light.node,
'intensity', { 'intensity', {
0: 1.0, 0: 1.0,
0.2: 0.4, 0.2: 0.4,
0.4: 1.0 0.4: 1.0
}, },
loop=True) loop=True)
assert isinstance(player.actor, playerspaz.PlayerSpaz) assert isinstance(player.actor, PlayerSpaz)
player.actor.node.connectattr('position', light.node, player.actor.node.connectattr('position', light.node,
'position') 'position')
except Exception:
ba.print_exception('EXC in _set_chosen_one_player')
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage): if isinstance(msg, PlayerSpazDeathMessage):
# Augment standard behavior. # Augment standard behavior.
super().handlemessage(msg) super().handlemessage(msg)
player = msg.playerspaz(self).player player = msg.playerspaz(self).player
if player is self._get_chosen_one_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 ( self._set_chosen_one_player(None if (
killerplayer is None or killerplayer is player killerplayer is None or killerplayer is player
or not killerplayer.is_alive()) else killerplayer) or not killerplayer.is_alive()) else killerplayer)
@ -339,8 +340,7 @@ class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def _update_scoreboard(self) -> None: def _update_scoreboard(self) -> None:
for team in self.teams: for team in self.teams:
self._scoreboard.set_team_value( self._scoreboard.set_team_value(team,
team, team.time_remaining,
team.gamedata['time_remaining'], self._chosen_one_time,
self.settings_raw['Chosen One Time'], countdown=True)
countdown=True)

View File

@ -25,6 +25,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
@ -40,7 +41,7 @@ class Icon(ba.Actor):
"""Creates in in-game icon on screen.""" """Creates in in-game icon on screen."""
def __init__(self, def __init__(self,
player: ba.Player, player: Player,
position: Tuple[float, float], position: Tuple[float, float],
scale: float, scale: float,
show_lives: bool = True, show_lives: bool = True,
@ -117,7 +118,7 @@ class Icon(ba.Actor):
def update_for_lives(self) -> None: def update_for_lives(self) -> None:
"""Update for the target player's current lives.""" """Update for the target player's current lives."""
if self._player: if self._player:
lives = self._player.gamedata['lives'] lives = self._player.lives
else: else:
lives = 0 lives = 0
if self._show_lives: if self._show_lives:
@ -158,13 +159,27 @@ class Icon(ba.Actor):
0.50: 1.0, 0.50: 1.0,
0.55: 0.2 0.55: 0.2
}) })
lives = self._player.gamedata['lives'] lives = self._player.lives
if lives == 0: if lives == 0:
ba.timer(0.6, self.update_for_lives) 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 # 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.""" """Game type where last player(s) left alive win."""
@classmethod @classmethod
@ -196,13 +211,15 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]:
settings: List[Tuple[str, Dict[str, Any]]] = [ settings: List[Tuple[str, Dict[str, Any]]] = [
('Lives Per Player', { ('Lives Per Player', {
'default': 1, 'min_value': 1, 'default': 1,
'max_value': 10, 'increment': 1 'min_value': 1,
'max_value': 10,
'increment': 1
}), }),
('Time Limit', { ('Time Limit', {
'choices': [('None', 0), ('1 Minute', 60), 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120),
('2 Minutes', 120), ('5 Minutes', 300), ('5 Minutes', 300), ('10 Minutes', 600),
('10 Minutes', 600), ('20 Minutes', 1200)], ('20 Minutes', 1200)],
'default': 0 'default': 0
}), }),
('Respawn Times', { ('Respawn Times', {
@ -210,7 +227,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
('Long', 2.0), ('Longer', 4.0)], ('Long', 2.0), ('Longer', 4.0)],
'default': 1.0 'default': 1.0
}), }),
('Epic Mode', {'default': False})] # yapf: disable ('Epic Mode', {
'default': False
}),
]
if issubclass(sessiontype, ba.DualTeamSession): if issubclass(sessiontype, ba.DualTeamSession):
settings.append(('Solo Mode', {'default': False})) 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]): def __init__(self, settings: Dict[str, Any]):
from bastd.actor.scoreboard import Scoreboard from bastd.actor.scoreboard import Scoreboard
super().__init__(settings) 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._scoreboard = Scoreboard()
self._start_time: Optional[float] = None self._start_time: Optional[float] = None
self._vs_text: Optional[ba.Actor] = None self._vs_text: Optional[ba.Actor] = None
self._round_end_timer: Optional[ba.Timer] = 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]: def get_instance_description(self) -> Union[str, Sequence]:
return 'Last team standing wins.' if isinstance( 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( return 'last team standing wins' if isinstance(
self.session, ba.DualTeamSession) else 'last one standing wins' self.session, ba.DualTeamSession) else 'last one standing wins'
def on_transition_in(self) -> None: def on_player_join(self, player: Player) -> 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:
# No longer allowing mid-game joiners here; too easy to exploit. # No longer allowing mid-game joiners here; too easy to exploit.
if self.has_begun(): if self.has_begun():
player.gamedata['lives'] = 0
player.gamedata['icons'] = []
# Make sure our team has survival seconds set if they're all dead # Make sure our team has survival seconds set if they're all dead
# (otherwise blocked new ffa players would be considered 'still # (otherwise blocked new ffa players would be considered 'still
# alive' in score tallying). # alive' in score tallying).
if self._get_total_team_lives( if (self._get_total_team_lives(player.team) == 0
player.team and player.team.survival_seconds is None):
) == 0 and player.team.gamedata['survival_seconds'] is None: player.team.survival_seconds = 0
player.team.gamedata['survival_seconds'] = 0 ba.screenmessage(
ba.screenmessage(ba.Lstr(resource='playerDelayedJoinText', ba.Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}', subs=[('${PLAYER}', player.get_name(full=True))]),
player.get_name(full=True))]), color=(0, 1, 0),
color=(0, 1, 0)) )
return return
player.gamedata['lives'] = self.settings_raw['Lives Per Player'] player.lives = self._lives_per_player
if self._solo_mode: if self._solo_mode:
player.gamedata['icons'] = [] player.team.spawn_order.append(player)
player.team.gamedata['spawn_order'].append(player)
self._update_solo_mode() self._update_solo_mode()
else: else:
# Create our icon and spawn. # Create our icon and spawn.
player.gamedata['icons'] = [ player.icons = [Icon(player, position=(0, 50), scale=0.8)]
Icon(player, position=(0, 50), scale=0.8) if player.lives > 0:
]
if player.gamedata['lives'] > 0:
self.spawn_player(player) self.spawn_player(player)
# Don't waste time doing this until begin. # Don't waste time doing this until begin.
if self.has_begun(): if self.has_begun():
self._update_icons() 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: def _update_solo_mode(self) -> None:
# For both teams, find the first player on the spawn order list with # For both teams, find the first player on the spawn order list with
# lives remaining and spawn them if they're not alive. # lives remaining and spawn them if they're not alive.
for team in self.teams: for team in self.teams:
# Prune dead players from the spawn order. # Prune dead players from the spawn order.
team.gamedata['spawn_order'] = [ team.spawn_order = [p for p in team.spawn_order if p]
p for p in team.gamedata['spawn_order'] if p for player in team.spawn_order:
] assert isinstance(player, Player)
for player in team.gamedata['spawn_order']: if player.lives > 0:
if player.gamedata['lives'] > 0:
if not player.is_alive(): if not player.is_alive():
self.spawn_player(player) self.spawn_player(player)
break break
@ -315,7 +369,7 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
for team in self.teams: for team in self.teams:
if len(team.players) == 1: if len(team.players) == 1:
player = team.players[0] 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.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives() icon.update_for_lives()
xval += x_offs xval += x_offs
@ -325,7 +379,7 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
if self._solo_mode: if self._solo_mode:
# First off, clear out all icons. # First off, clear out all icons.
for player in self.players: for player in self.players:
player.gamedata['icons'] = [] player.icons = []
# Now for each team, cycle through our available players # Now for each team, cycle through our available players
# adding icons. # adding icons.
@ -340,13 +394,13 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
test_lives = 1 test_lives = 1
while True: while True:
players_with_lives = [ players_with_lives = [
p for p in team.gamedata['spawn_order'] p for p in team.spawn_order
if p and p.gamedata['lives'] >= test_lives if p and p.lives >= test_lives
] ]
if not players_with_lives: if not players_with_lives:
break break
for player in players_with_lives: for player in players_with_lives:
player.gamedata['icons'].append( player.icons.append(
Icon(player, Icon(player,
position=(xval, (40 if is_first else 25)), position=(xval, (40 if is_first else 25)),
scale=1.0 if is_first else 0.5, scale=1.0 if is_first else 0.5,
@ -369,12 +423,12 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
xval = 50 xval = 50
x_offs = 85 x_offs = 85
for player in team.players: 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.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives() icon.update_for_lives()
xval += x_offs 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. del player # Unused.
# In solo-mode, if there's an existing live player on the map, spawn at # 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 points[-1][1]
return None 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)) actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
if not self._solo_mode: if not self._solo_mode:
ba.timer(0.3, ba.Call(self._print_lives, player)) ba.timer(0.3, ba.Call(self._print_lives, player))
# If we have any icons, update their state. # If we have any icons, update their state.
for icon in player.gamedata['icons']: for icon in player.icons:
icon.handle_player_spawned() icon.handle_player_spawned()
return actor return actor
def _print_lives(self, player: ba.Player) -> None: def _print_lives(self, player: Player) -> None:
from bastd.actor import popuptext 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: if not player or not player.is_alive() or not player.node:
return return
popuptext.PopupText('x' + str(player.gamedata['lives'] - 1), popuptext.PopupText('x' + str(player.lives - 1),
color=(1, 1, 0, 1), color=(1, 1, 0, 1),
offset=(0, -0.8, 0), offset=(0, -0.8, 0),
random_offset=0.0, random_offset=0.0,
scale=1.8, scale=1.8,
position=player.node.position).autoretain() 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) super().on_player_leave(player)
player.gamedata['icons'] = None player.icons = []
# Remove us from spawn-order. # Remove us from spawn-order.
if self._solo_mode: if self._solo_mode:
if player in player.team.gamedata['spawn_order']: if player in player.team.spawn_order:
player.team.gamedata['spawn_order'].remove(player) player.team.spawn_order.remove(player)
# Update icons in a moment since our team will be gone from the # Update icons in a moment since our team will be gone from the
# list then. # list then.
ba.timer(0, self._update_icons) ba.timer(0, self._update_icons)
def on_begin(self) -> None: def _get_total_team_lives(self, team: Team) -> int:
super().on_begin() return sum(player.lives for player in team.players)
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 handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage): if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
# Augment standard behavior. # Augment standard behavior.
super().handlemessage(msg) super().handlemessage(msg)
player = msg.playerspaz(self).player player: Player = msg.playerspaz(self).player
player.gamedata['lives'] -= 1 player.lives -= 1
if player.gamedata['lives'] < 0: if player.lives < 0:
ba.print_error( ba.print_error(
"Got lives < 0 in Elim; this shouldn't happen. solo:" + "Got lives < 0 in Elim; this shouldn't happen. solo:" +
str(self._solo_mode)) str(self._solo_mode))
player.gamedata['lives'] = 0 player.lives = 0
# If we have any icons, update their state. # If we have any icons, update their state.
for icon in player.gamedata['icons']: for icon in player.icons:
icon.handle_player_died() icon.handle_player_died()
# Play big death sound on our last death # Play big death sound on our last death
# or for every one in solo mode. # 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) ba.playsound(spaz.get_factory().single_player_death_sound)
# If we hit zero lives, we're dead (and our team might be too). # 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 the whole team is now dead, mark their survival time.
if self._get_total_team_lives(player.team) == 0: if self._get_total_team_lives(player.team) == 0:
assert self._start_time is not None assert self._start_time is not None
player.team.gamedata['survival_seconds'] = int( player.team.survival_seconds = int(ba.time() -
ba.time() - self._start_time) self._start_time)
else: else:
# Otherwise, in regular mode, respawn. # Otherwise, in regular mode, respawn.
if not self._solo_mode: 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. # In solo, put ourself at the back of the spawn order.
if self._solo_mode: if self._solo_mode:
player.team.gamedata['spawn_order'].remove(player) player.team.spawn_order.remove(player)
player.team.gamedata['spawn_order'].append(player) player.team.spawn_order.append(player)
def _update(self) -> None: def _update(self) -> None:
if self._solo_mode: 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. # list with lives remaining and spawn them if they're not alive.
for team in self.teams: for team in self.teams:
# Prune dead players from the spawn order. # Prune dead players from the spawn order.
team.gamedata['spawn_order'] = [ team.spawn_order = [p for p in team.spawn_order if p]
p for p in team.gamedata['spawn_order'] if p for player in team.spawn_order:
] assert isinstance(player, Player)
for player in team.gamedata['spawn_order']: if player.lives > 0:
if player.gamedata['lives'] > 0:
if not player.is_alive(): if not player.is_alive():
self.spawn_player(player) self.spawn_player(player)
self._update_icons() self._update_icons()
@ -548,10 +558,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
if len(self._get_living_teams()) < 2: if len(self._get_living_teams()) < 2:
self._round_end_timer = ba.Timer(0.5, self.end_game) 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 [ return [
team for team in self.teams 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) for player in team.players)
] ]
@ -561,5 +571,5 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
results = ba.TeamGameResults() results = ba.TeamGameResults()
self._vs_text = None # Kill our 'vs' if its there. self._vs_text = None # Kill our 'vs' if its there.
for team in self.teams: 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) self.end(results=results)

View File

@ -26,14 +26,15 @@
from __future__ import annotations from __future__ import annotations
import random import random
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import math import math
import ba 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 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 from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING: if TYPE_CHECKING:
@ -66,8 +67,18 @@ class FootballFlag(stdflag.Flag):
self.node.connectattr('position', self.light, 'position') 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 # ba_meta export game
class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]): class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
"""Football game for teams mode.""" """Football game for teams mode."""
@classmethod @classmethod
@ -183,7 +194,7 @@ class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self._update_scoreboard() self._update_scoreboard()
ba.playsound(self._chant_sound) 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 team.gamedata['score'] = 0
self._update_scoreboard() self._update_scoreboard()
@ -271,7 +282,7 @@ class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]):
msg.flag.held_count -= 1 msg.flag.held_count -= 1
# Respawn dead players if they're still in the game. # Respawn dead players if they're still in the game.
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage): elif isinstance(msg, PlayerSpazDeathMessage):
# Augment standard behavior. # Augment standard behavior.
super().handlemessage(msg) super().handlemessage(msg)
self.respawn_player(msg.playerspaz(self).player) 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) 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 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_initial: Optional[List[Type[spazbot.SpazBot]]] = None
self._bot_types_7: 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_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._starttime_ms: Optional[int] = None
self._time_text: Optional[ba.NodeActor] = None self._time_text: Optional[ba.NodeActor] = None
self._time_text_input: 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._bots = spazbot.BotSet()
self._bot_spawn_timer: Optional[ba.Timer] = None self._bot_spawn_timer: Optional[ba.Timer] = None
self._powerup_drop_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._final_time_ms: Optional[int] = None
self._time_text_timer: Optional[ba.Timer] = None self._time_text_timer: Optional[ba.Timer] = None
self._flag_respawn_light: Optional[ba.Actor] = 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. # Make a bogus team for our bots.
bad_team_name = self.get_team_display_string('Bad Guys') 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 = Team()
self._bot_team = ba.Team() self._bot_team.manual_init(team_id=1,
self._bot_team.id = 1 name=bad_team_name,
self._bot_team.name = bad_team_name color=(0.5, 0.4, 0.4))
self._bot_team.color = (0.5, 0.4, 0.4)
for team in [self.teams[0], self._bot_team]: for team in [self.teams[0], self._bot_team]:
team.gamedata['score'] = 0 team.gamedata['score'] = 0
@ -547,7 +557,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]):
# Our TNT spawner (if applicable). # Our TNT spawner (if applicable).
if self._have_tnt: if self._have_tnt:
self._tntspawner = stdbomb.TNTSpawner(position=(0, 1, -1)) self._tntspawner = TNTSpawner(position=(0, 1, -1))
self._bots = spazbot.BotSet() self._bots = spazbot.BotSet()
self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True) 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: if self._flag.node:
for player in self.players: for player in self.players:
if player.actor: 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 if (player.actor.is_alive() and player.actor.node.hold_node
== self._flag.node): == self._flag.node):
return return
@ -808,7 +818,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]):
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
""" handle high-level game messages """ """ handle high-level game messages """
if isinstance(msg, playerspaz.PlayerSpazDeathMessage): if isinstance(msg, PlayerSpazDeathMessage):
from bastd.actor import respawnicon from bastd.actor import respawnicon
# Respawn dead players. # Respawn dead players.
@ -872,7 +882,7 @@ class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]):
del player # Unused. del player # Unused.
self._player_has_punched = True 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, spaz = self.spawn_player_spaz(player,
position=self.map.get_start_position( position=self.map.get_start_position(
player.team.id)) player.team.id))

View File

@ -25,19 +25,31 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
from bastd.actor import flag as stdflag from bastd.actor.flag import (Flag, FlagDroppedMessage, FlagDeathMessage,
from bastd.actor import playerspaz FlagPickedUpMessage)
from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import (Any, Type, List, Tuple, Dict, Optional, Sequence, from typing import (Any, Type, List, Tuple, Dict, Optional, Sequence,
Union) 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 # 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.""" """Game where you try to keep the flag away from your enemies."""
FLAG_NEW = 0 FLAG_NEW = 0
@ -109,11 +121,11 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]):
} }
self._flag_spawn_pos: Optional[Sequence[float]] = None self._flag_spawn_pos: Optional[Sequence[float]] = None
self._update_timer: Optional[ba.Timer] = 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_state: Optional[int] = None
self._flag_light: Optional[ba.Node] = None self._flag_light: Optional[ba.Node] = None
self._scoring_team: Optional[ba.Team] = None self._scoring_team: Optional[Team] = None
self._flag: Optional[stdflag.Flag] = None self._flag: Optional[Flag] = None
def get_instance_description(self) -> Union[str, Sequence]: def get_instance_description(self) -> Union[str, Sequence]:
return ('Carry the flag for ${ARG1} seconds.', 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 self.default_music = ba.MusicType.KEEP_AWAY
super().on_transition_in() 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'] team.gamedata['time_remaining'] = self.settings_raw['Hold Time']
self._update_scoreboard() self._update_scoreboard()
@ -194,8 +206,8 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]):
for player in self.players: for player in self.players:
holding_flag = False holding_flag = False
try: try:
assert isinstance(player.actor, playerspaz.PlayerSpaz) assert isinstance(player.actor, (PlayerSpaz, type(None)))
if (player.actor.is_alive() and player.actor.node if (player.actor and player.actor.node
and player.actor.node.hold_node): and player.actor.node.hold_node):
holding_flag = ( holding_flag = (
player.actor.node.hold_node.getnodetype() == 'flag') player.actor.node.hold_node.getnodetype() == 'flag')
@ -235,8 +247,7 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]):
ba.playsound(self._swipsound) ba.playsound(self._swipsound)
self._flash_flag_spawn() self._flash_flag_spawn()
assert self._flag_spawn_pos is not None assert self._flag_spawn_pos is not None
self._flag = stdflag.Flag(dropped_timeout=20, self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos)
position=self._flag_spawn_pos)
self._flag_state = self.FLAG_NEW self._flag_state = self.FLAG_NEW
self._flag_light = ba.newnode('light', self._flag_light = ba.newnode('light',
owner=self._flag.node, owner=self._flag.node,
@ -268,15 +279,13 @@ class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]):
countdown=True) countdown=True)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage): if isinstance(msg, PlayerSpazDeathMessage):
# Augment standard behavior. # Augment standard behavior.
super().handlemessage(msg) super().handlemessage(msg)
self.respawn_player(msg.playerspaz(self).player) self.respawn_player(msg.playerspaz(self).player)
elif isinstance(msg, stdflag.FlagDeathMessage): elif isinstance(msg, FlagDeathMessage):
self._spawn_flag() self._spawn_flag()
elif isinstance( elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
msg,
(stdflag.FlagDroppedMessage, stdflag.FlagPickedUpMessage)):
self._update_flag_state() self._update_flag_state()
else: else:
super().handlemessage(msg) super().handlemessage(msg)

View File

@ -26,11 +26,12 @@
from __future__ import annotations from __future__ import annotations
import weakref import weakref
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
from bastd.actor import flag as stdflag from bastd.actor.flag import Flag
from bastd.actor import playerspaz from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
if TYPE_CHECKING: if TYPE_CHECKING:
from weakref import ReferenceType from weakref import ReferenceType
@ -38,8 +39,20 @@ if TYPE_CHECKING:
Union) 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 # 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.""" """Game where a team wins by holding a 'hill' for a set amount of time."""
FLAG_NEW = 0 FLAG_NEW = 0
@ -71,23 +84,24 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def get_settings( def get_settings(
cls, cls,
sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]:
return [('Hold Time', { return [
'min_value': 10, ('Hold Time', {
'default': 30, 'min_value': 10,
'increment': 10 'default': 30,
}), 'increment': 10
('Time Limit', { }),
'choices': [('None', 0), ('1 Minute', 60), ('Time Limit', {
('2 Minutes', 120), ('5 Minutes', 300), 'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120),
('10 Minutes', 600), ('20 Minutes', 1200)], ('5 Minutes', 300), ('10 Minutes', 600),
'default': 0 ('20 Minutes', 1200)],
}), 'default': 0
('Respawn Times', { }),
'choices': [('Shorter', 0.25), ('Short', 0.5), ('Respawn Times', {
('Normal', 1.0), ('Long', 2.0), 'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0),
('Longer', 4.0)], ('Long', 2.0), ('Longer', 4.0)],
'default': 1.0 'default': 1.0
})] }),
]
def __init__(self, settings: Dict[str, Any]): def __init__(self, settings: Dict[str, Any]):
from bastd.actor.scoreboard import Scoreboard 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_pos: Optional[Sequence[float]] = None
self._flag_state: Optional[int] = 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._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 = ba.Material()
self._flag_region_material.add_actions( 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, ba.Call(self._handle_player_flag_region_collide,
False)))) False))))
# Base class overrides.
self.default_music = ba.MusicType.SCARY
def get_instance_description(self) -> Union[str, Sequence]: def get_instance_description(self) -> Union[str, Sequence]:
return ('Secure the flag for ${ARG1} seconds.', return 'Secure the flag for ${ARG1} seconds.', self._hold_time
self.settings_raw['Hold Time'])
def get_instance_scoreboard_description(self) -> Union[str, Sequence]: def get_instance_scoreboard_description(self) -> Union[str, Sequence]:
return ('secure the flag for ${ARG1} seconds', return 'secure the flag for ${ARG1} seconds', self._hold_time
self.settings_raw['Hold Time'])
def on_transition_in(self) -> None: def create_team(self, sessionteam: ba.SessionTeam) -> Team:
self.default_music = ba.MusicType.SCARY return Team(time_remaining=self._hold_time)
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 on_begin(self) -> None: def on_begin(self) -> None:
super().on_begin() 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.setup_standard_powerup_drops()
self._flag_pos = self.map.get_flag_position(None) self._flag_pos = self.map.get_flag_position(None)
ba.timer(1.0, self._tick, repeat=True) ba.timer(1.0, self._tick, repeat=True)
self._flag_state = self.FLAG_NEW self._flag_state = self.FLAG_NEW
self.project_flag_stand(self._flag_pos) self.project_flag_stand(self._flag_pos)
self._flag = stdflag.Flag(position=self._flag_pos, self._flag = Flag(position=self._flag_pos,
touchable=False, touchable=False,
color=(1, 1, 1)) color=(1, 1, 1))
self._flag_light = ba.newnode('light', self._flag_light = ba.newnode('light',
attrs={ attrs={
'position': self._flag_pos, 'position': self._flag_pos,
@ -184,51 +192,47 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]):
# Give holding players points. # Give holding players points.
for player in self.players: for player in self.players:
if player.gamedata['at_flag'] > 0: if player.time_at_flag > 0:
self.stats.player_scored(player, self.stats.player_scored(player,
3, 3,
screenmessage=False, screenmessage=False,
display=False) display=False)
if self._scoring_team is None: if self._scoring_team is None:
scoring_team = None scoring_team = None
else: else:
scoring_team = self._scoring_team() scoring_team = self._scoring_team()
if scoring_team: if scoring_team:
if scoring_team.gamedata['time_remaining'] > 0: if scoring_team.time_remaining > 0:
ba.playsound(self._tick_sound) ba.playsound(self._tick_sound)
scoring_team.gamedata['time_remaining'] = max( scoring_team.time_remaining = max(0,
0, scoring_team.gamedata['time_remaining'] - 1) scoring_team.time_remaining - 1)
self._update_scoreboard() self._update_scoreboard()
if scoring_team.gamedata['time_remaining'] > 0: if scoring_team.time_remaining > 0:
assert self._flag is not None assert self._flag is not None
self._flag.set_score_text( self._flag.set_score_text(str(scoring_team.time_remaining))
str(scoring_team.gamedata['time_remaining']))
# Announce numbers we have sounds for. # Announce numbers we have sounds for.
try: try:
ba.playsound(self._countdownsounds[ ba.playsound(
scoring_team.gamedata['time_remaining']]) self._countdownsounds[scoring_team.time_remaining])
except Exception: except Exception:
pass pass
# winner # winner
if scoring_team.gamedata['time_remaining'] <= 0: if scoring_team.time_remaining <= 0:
self.end_game() self.end_game()
def end_game(self) -> None: def end_game(self) -> None:
results = ba.TeamGameResults() results = ba.TeamGameResults()
for team in self.teams: for team in self.teams:
results.set_team_score( results.set_team_score(team, self._hold_time - team.time_remaining)
team, self.settings_raw['Hold Time'] -
team.gamedata['time_remaining'])
self.end(results=results, announce_delay=0) self.end(results=results, announce_delay=0)
def _update_flag_state(self) -> None: def _update_flag_state(self) -> None:
holding_teams = set(player.team for player in self.players 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 prev_state = self._flag_state
assert self._flag_light assert self._flag_light
assert self._flag is not None assert self._flag is not None
@ -253,35 +257,36 @@ class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]):
ba.playsound(self._swipsound) ba.playsound(self._swipsound)
def _handle_player_flag_region_collide(self, colliding: bool) -> None: def _handle_player_flag_region_collide(self, colliding: bool) -> None:
playernode = ba.get_collision_info('opposing_node') delegate = ba.get_collision_info('opposing_node').getdelegate()
try: if not isinstance(delegate, PlayerSpaz):
player = playernode.getdelegate().getplayer() return
except Exception: player = ba.playercast_o(Player, delegate.getplayer())
if not player:
return return
# Different parts of us can collide so a single value isn't enough # 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 # also don't count it if we're dead (flying heads shouldn't be able to
# win the game :-) # win the game :-)
if colliding and player.is_alive(): if colliding and player.is_alive():
player.gamedata['at_flag'] += 1 player.time_at_flag += 1
else: 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() self._update_flag_state()
def _update_scoreboard(self) -> None: def _update_scoreboard(self) -> None:
for team in self.teams: for team in self.teams:
self._scoreboard.set_team_value(team, self._scoreboard.set_team_value(team,
team.gamedata['time_remaining'], team.time_remaining,
self.settings_raw['Hold Time'], self._hold_time,
countdown=True) countdown=True)
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage): if isinstance(msg, PlayerSpazDeathMessage):
super().handlemessage(msg) # Augment default. 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 = msg.playerspaz(self).player
player.gamedata['at_flag'] = 0 player.time_at_flag = 0
self._update_flag_state() self._update_flag_state()
self.respawn_player(player) self.respawn_player(player)

View File

@ -255,6 +255,10 @@ class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
# (these per-player scores are only meaningful in team-games) # (these per-player scores are only meaningful in team-games)
for team in self.teams: for team in self.teams:
for player in team.players: for player in team.players:
if not player:
print(f'GOT DEAD PLAYER {id(player)}')
survived = False survived = False
# Throw an extra fudge factor in so teams that # Throw an extra fudge factor in so teams that

View File

@ -27,6 +27,7 @@ from __future__ import annotations
import math import math
import random import random
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
@ -38,7 +39,17 @@ if TYPE_CHECKING:
from bastd.actor.scoreboard import Scoreboard 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.""" """Co-op game where players try to survive attacking waves of enemies."""
tips: List[Union[str, Dict[str, Any]]] = [ tips: List[Union[str, Dict[str, Any]]] = [
@ -622,7 +633,7 @@ class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]):
return groups 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. # We keep track of who got hurt each wave for score purposes.
player.gamedata['has_been_hurt'] = False player.gamedata['has_been_hurt'] = False
@ -816,7 +827,7 @@ class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]):
self._score += self._time_bonus self._score += self._time_bonus
self._update_scores() self._update_scores()
def _award_flawless_bonus(self, player: ba.Player) -> None: def _award_flawless_bonus(self, player: Player) -> None:
ba.playsound(self._cashregistersound) ba.playsound(self._cashregistersound)
try: try:
if player.is_alive(): if player.is_alive():

View File

@ -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 # ba_meta export game
class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]): class RaceGame(ba.TeamGameActivity[Player, Team]):
"""Game of racing around a track.""" """Game of racing around a track."""
@classmethod @classmethod
@ -186,7 +197,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
for rpt in pts: for rpt in pts:
self._regions.append(RaceRegion(rpt, len(self._regions))) 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 isinstance(player.actor, PlayerSpaz)
assert player.actor.node assert player.actor.node
pos = player.actor.node.position pos = player.actor.node.position
@ -214,7 +225,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
region = region_node.getdelegate() region = region_node.getdelegate()
if not player or not region: if not player or not region:
return return
assert isinstance(player, ba.Player) assert isinstance(player, Player)
assert isinstance(region, RaceRegion) assert isinstance(region, RaceRegion)
last_region = player.gamedata['last_region'] last_region = player.gamedata['last_region']
@ -342,13 +353,13 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
except Exception as exc: except Exception as exc:
print('Exception printing lap:', 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['time'] = None
team.gamedata['lap'] = 0 team.gamedata['lap'] = 0
team.gamedata['finished'] = False team.gamedata['finished'] = False
self._update_scoreboard() 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['last_region'] = 0
player.gamedata['lap'] = 0 player.gamedata['lap'] = 0
player.gamedata['distance'] = 0.0 player.gamedata['distance'] = 0.0
@ -356,7 +367,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
player.gamedata['rank'] = None player.gamedata['rank'] = None
super().on_player_join(player) 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) super().on_player_leave(player)
# A player leaving disqualifies the team if 'Entire Team Must Finish' # 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 player.gamedata['distance'] = amt
# Sort players by distance and update their ranks. # 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] for player in self.players]
p_list.sort(reverse=True, key=lambda x: x[0]) p_list.sort(reverse=True, key=lambda x: x[0])
for i, plr in enumerate(p_list): for i, plr in enumerate(p_list):
try: try:
plr[1].gamedata['rank'] = i plr[1].gamedata['rank'] = i
if plr[1].actor is not None: if plr[1].actor:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
node = plr[1].actor.distance_txt node = plr[1].distance_txt
if node: if node:
node.text = str(i + 1) if plr[1].is_alive() else '' node.text = str(i + 1) if plr[1].is_alive() else ''
except Exception: except Exception:
@ -620,10 +631,11 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self._flash_mine(m_index) self._flash_mine(m_index)
ba.timer(0.95, ba.Call(self._make_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']: if player.team.gamedata['finished']:
# FIXME: This is not type-safe # FIXME: This is not type-safe!
# (this call is expected to return an Actor). # This call is expected to always return an Actor!
# Perhaps we need something like can_spawn_player()...
# noinspection PyTypeChecker # noinspection PyTypeChecker
return None # type: ignore return None # type: ignore
pos = self._regions[player.gamedata['last_region']].pos pos = self._regions[player.gamedata['last_region']].pos
@ -661,9 +673,7 @@ class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
'scale': 0.02, 'scale': 0.02,
'h_align': 'center' 'h_align': 'center'
}) })
# FIXME store this in a type-safe way player.distance_txt = distance_txt
# noinspection PyTypeHints
spaz.distance_txt = distance_txt # type: ignore
mathnode.connectattr('output', distance_txt, 'position') mathnode.connectattr('output', distance_txt, 'position')
return spaz return spaz

View File

@ -26,6 +26,7 @@
from __future__ import annotations from __future__ import annotations
import random import random
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
@ -37,8 +38,20 @@ if TYPE_CHECKING:
from bastd.actor.bomb import Bomb, Blast 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 # 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.""" """Game where players try to hit targets with bombs."""
@classmethod @classmethod
@ -63,14 +76,18 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def get_settings( def get_settings(
cls, cls,
sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]: sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]:
return [('Target Count', { return [
'min_value': 1, ('Target Count', {
'default': 3 'min_value': 1,
}), ('Enable Impact Bombs', { 'default': 3
'default': True }),
}), ('Enable Triple Bombs', { ('Enable Impact Bombs', {
'default': True 'default': True
})] }),
('Enable Triple Bombs', {
'default': True
}),
]
def __init__(self, settings: Dict[str, Any]): def __init__(self, settings: Dict[str, Any]):
from bastd.actor.scoreboard import Scoreboard from bastd.actor.scoreboard import Scoreboard
@ -79,13 +96,14 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self._targets: List[Target] = [] self._targets: List[Target] = []
self._update_timer: Optional[ba.Timer] = None self._update_timer: Optional[ba.Timer] = None
self._countdown: Optional[OnScreenCountdown] = 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 self.default_music = ba.MusicType.FORWARD_MARCH
super().on_transition_in()
def on_team_join(self, team: ba.Team) -> None: def on_team_join(self, team: Team) -> None:
team.gamedata['score'] = 0
if self.has_begun(): if self.has_begun():
self.update_scoreboard() self.update_scoreboard()
@ -95,28 +113,27 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]):
self.update_scoreboard() self.update_scoreboard()
# Number of targets is based on player count. # Number of targets is based on player count.
num_targets = self.settings_raw['Target Count'] for i in range(self._target_count):
for i in range(num_targets):
ba.timer(5.0 + i * 1.0, self._spawn_target) ba.timer(5.0 + i * 1.0, self._spawn_target)
self._update_timer = ba.Timer(1.0, self._update, repeat=True) self._update_timer = ba.Timer(1.0, self._update, repeat=True)
self._countdown = OnScreenCountdown(60, endcall=self.end_game) self._countdown = OnScreenCountdown(60, endcall=self.end_game)
ba.timer(4.0, self._countdown.start) 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) spawn_center = (0, 3, -5)
pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1], pos = (spawn_center[0] + random.uniform(-1.5, 1.5), spawn_center[1],
spawn_center[2] + random.uniform(-1.5, 1.5)) spawn_center[2] + random.uniform(-1.5, 1.5))
# Reset their streak. # Reset their streak.
player.gamedata['streak'] = 0 player.streak = 0
spaz = self.spawn_player_spaz(player, position=pos) spaz = self.spawn_player_spaz(player, position=pos)
# Give players permanent triple impact bombs and wire them up # Give players permanent triple impact bombs and wire them up
# to tell us when they drop a bomb. # to tell us when they drop a bomb.
if self.settings_raw['Enable Impact Bombs']: if self._enable_impact_bombs:
spaz.bomb_type = 'impact' spaz.bomb_type = 'impact'
if self.settings_raw['Enable Triple Bombs']: if self._enable_triple_bombs:
spaz.set_bomb_count(3) spaz.set_bomb_count(3)
spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb) spaz.add_dropped_bomb_callback(self._on_spaz_dropped_bomb)
return spaz 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. # 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 # 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). # 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: if not player:
return # could happen if they leave after throwing a bomb.. 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) target.do_hit_at_position(pos, player)
for target in list(self._targets)) for target in list(self._targets))
if bullseye: if bullseye:
player.gamedata['streak'] += 1 player.streak += 1
else: else:
player.gamedata['streak'] = 0 player.streak = 0
def _update(self) -> None: def _update(self) -> None:
"""Misc. periodic updating.""" """Misc. periodic updating."""
@ -200,12 +217,12 @@ class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def update_scoreboard(self) -> None: def update_scoreboard(self) -> None:
"""Update the game scoreboard with current team values.""" """Update the game scoreboard with current team values."""
for team in self.teams: 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: def end_game(self) -> None:
results = ba.TeamGameResults() results = ba.TeamGameResults()
for team in self.teams: for team in self.teams:
results.set_team_score(team, team.gamedata['score']) results.set_team_score(team, team.score)
self.end(results) self.end(results)
@ -278,8 +295,7 @@ class Target(ba.Actor):
"""Given a point, returns distance squared from it.""" """Given a point, returns distance squared from it."""
return (ba.Vec3(pos) - self._position).length() return (ba.Vec3(pos) - self._position).length()
def do_hit_at_position(self, pos: Sequence[float], def do_hit_at_position(self, pos: Sequence[float], player: Player) -> bool:
player: ba.Player) -> bool:
"""Handle a bomb hit at the given position.""" """Handle a bomb hit at the given position."""
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
from bastd.actor import popuptext from bastd.actor import popuptext
@ -316,7 +332,7 @@ class Target(ba.Actor):
ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True) ba.animate_array(self._nodes[0], 'color', 3, keys, loop=True)
popupscale = 1.8 popupscale = 1.8
popupcolor = (1, 1, 0, 1) popupcolor = (1, 1, 0, 1)
streak = player.gamedata['streak'] streak = player.streak
points = 10 + min(20, streak * 2) points = 10 + min(20, streak * 2)
ba.playsound(ba.getsound('bellHigh')) ba.playsound(ba.getsound('bellHigh'))
if streak > 0: if streak > 0:
@ -357,7 +373,7 @@ class Target(ba.Actor):
scale=popupscale).autoretain() scale=popupscale).autoretain()
# Give this player's team points and update the score-board. # Give this player's team points and update the score-board.
player.team.gamedata['score'] += points player.team.score += points
assert isinstance(activity, TargetPracticeGame) assert isinstance(activity, TargetPracticeGame)
activity.update_scoreboard() activity.update_scoreboard()

View File

@ -23,6 +23,7 @@
from __future__ import annotations from __future__ import annotations
import random import random
from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import ba import ba
@ -35,7 +36,17 @@ if TYPE_CHECKING:
from bastd.actor.scoreboard import Scoreboard 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.""" """Slow motion how-long-can-you-last game."""
tips = [ tips = [
@ -118,7 +129,7 @@ class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]):
self._tntspawner = TNTSpawner(position=self._tntspawnpos, self._tntspawner = TNTSpawner(position=self._tntspawnpos,
respawn_time=10.0) 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), pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
self._spawn_center[1], self._spawn_center[1],
self._spawn_center[2] + random.uniform(-1.5, 1.5)) self._spawn_center[2] + random.uniform(-1.5, 1.5))

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND --> <!-- 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, <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> 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> <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> <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, ...]) -&gt; None</span></p>
<p>Manually init a team for uses such as bots.</p>
</dd> </dd>
</dl> </dl>
<hr> <hr>
@ -4949,7 +4958,7 @@ Results for a completed <a href="#class_ba_TeamGameActivity">ba.TeamGameActivity
</dd> </dd>
<dt><h4><a name="method_ba_TeamGameResults__set_team_score">set_team_score()</a></dt></h4><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) -&gt; 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]) -&gt; None</span></p>
<p>Set the score for a given <a href="#class_ba_Team">ba.Team</a>.</p> <p>Set the score for a given <a href="#class_ba_Team">ba.Team</a>.</p>