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/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb",
"assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe",
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/33/81/1bf0d898c26582776d0c2ef76b68",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7b/b6/9cf8cb137735545a5d9c5d2abc14",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/4c/a0/b18786c5c4a3b8c8ec9417167412",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fe/41/1bf6ad4d57a589fb26cece393563",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/42/6d/39d0af901ac06b9ad655a9837a6d",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/98/0d/0d4594d20813a5b3dacb3aa2022f",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/8e/73/1e215e66ce6fd155b4af840465f2",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/a2/21/aad47597886fe15f228386fdf0a5",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3c/e5/bdd60cba90f6955ba7a4a932bd45",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/4f/b7/86dec4a8ab32edaf850d3c0fe2c2",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/09/ab/48ca17f389375fd4db02295bab73",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/f4/12/6fb56bf6484b5b93fbfd3206bbdf"
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/75/82/99d3b14b1c7ccab2de8c64bf9dea",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fe/78/0e5383f059887070323d08f6fa9a",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/0d/23/a18e8d3f8a70f9865938d95d7184",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/27/57/8f95c8da763731b971488a18d9b4",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e9/c6/cd1d7d7568edf9f5e3d313e75eca",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7e/92/c4407e3e9017523745e381f681d8",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c9/57/5273080f7d383a3de8e3a519f96b",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/7d/d3/01ce8f52b62fc308a358d9a5470e",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/c6/44/bd76f8dc1be8b2b862876d112420",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/68/86/6e1418947d35a647d563aff2aebe",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/9f/3d/f091bf3ed6b286a29f660a1e14cc",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a0/b3/ad0fa29417f197bed6f3fb184a3f"
}

View File

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

View File

@ -331,106 +331,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
raise RuntimeError(f'destroy() called when'
f' already expired for {self}')
@classmethod
def _check_activity_death(cls, activity_ref: ReferenceType[Activity],
counter: List[int]) -> None:
"""Sanity check to make sure an Activity was destroyed properly.
Receives a weakref to a ba.Activity which should have torn itself
down due to no longer being referenced anywhere. Will complain
and/or print debugging info if the Activity still exists.
"""
try:
import gc
import types
activity = activity_ref()
print('ERROR: Activity is not dying when expected:', activity,
'(warning ' + str(counter[0] + 1) + ')')
print('This means something is still strong-referencing it.')
counter[0] += 1
# FIXME: Running the code below shows us references but winds up
# keeping the object alive; need to figure out why.
# For now we just print refs if the count gets to 3, and then we
# kill the app at 4 so it doesn't matter anyway.
if counter[0] == 3:
print('Activity references for', activity, ':')
refs = list(gc.get_referrers(activity))
i = 1
for ref in refs:
if isinstance(ref, types.FrameType):
continue
print(' reference', i, ':', ref)
i += 1
if counter[0] == 4:
print('Killing app due to stuck activity... :-(')
_ba.quit()
except Exception:
print_exception('exception on _check_activity_death:')
def _expire(self) -> None:
"""Put the activity in a state where it can be garbage-collected.
This involves clearing anything that might be holding a reference
to it, etc.
"""
assert not self._expired
self._expired = True
try:
self.on_expire()
except Exception:
print_exception(f'Error in Activity on_expire() for {self}')
# Send expire notices to all remaining actors.
for actor_ref in self._actor_weak_refs:
try:
actor = actor_ref()
if actor is not None:
actor.on_expire()
except Exception:
print_exception(f'Error expiring Actor {actor_ref()}')
# Reset all Players.
# (releases any attached actors, clears game-data, etc)
for player in self.players:
if player:
try:
sessionplayer = player.sessionplayer
sessionplayer.set_node(None)
sessionplayer.set_activity(None)
sessionplayer.gameplayer = None
sessionplayer.reset()
except Exception:
print_exception(f'Error resetting Player {player}')
# Ditto with Teams.
for team in self.teams:
try:
sessionteam = team.sessionteam
sessionteam.gameteam = None
sessionteam.reset_gamedata()
except SessionTeamNotFoundError:
pass
except Exception:
print_exception(f'Error resetting Team {team}')
# Regardless of what happened here, we want to destroy our data, as
# our activity might not go down if we don't. This will kill all
# Timers, Nodes, etc, which should clear up any remaining refs to our
# Actors and Activity and allow us to die peacefully.
try:
self._activity_data.destroy()
except Exception:
print_exception(
'Exception during ba.Activity._expire() destroying data:')
def _prune_dead_actors(self) -> None:
self._actor_refs = [a for a in self._actor_refs if a]
self._actor_weak_refs = [a for a in self._actor_weak_refs if a()]
self._last_prune_dead_actors_time = _ba.time()
def retain_actor(self, actor: ba.Actor) -> None:
"""Add a strong-reference to a ba.Actor to this Activity.
@ -619,127 +519,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
except Exception:
print_exception('Error in on_transition_out for', self)
def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
"""Create the Player instance for this Activity.
Subclasses can override this if the activity's player class
requires a custom constructor; otherwise it will be called with
no args. Note that the player object should not be used at this
point as it is not yet fully wired up; wait for on_player_join()
for that.
"""
del sessionplayer # Unused
player = self._playertype()
return player
def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
"""Create the Team instance for this Activity.
Subclasses can override this if the activity's team class
requires a custom constructor; otherwise it will be called with
no args. Note that the team object should not be used at this
point as it is not yet fully wired up; wait for on_team_join()
for that.
"""
del sessionteam # Unused.
team = self._teamtype()
return team
def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
assert sessionplayer.team is not None
sessionplayer.reset_input()
sessionteam = sessionplayer.team
assert sessionplayer in sessionteam.players
team = sessionteam.gameteam
assert team is not None
sessionplayer.set_activity(self)
with _ba.Context(self):
sessionplayer.gameplayer = player = self.create_player(
sessionplayer)
player.postinit(sessionplayer)
team.players.append(player)
self.players.append(player)
try:
self.on_player_join(player)
except Exception:
print_exception('Error in on_player_join for', self)
def remove_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
# This should only be called on unexpired activities
# the player has been added to.
assert not self.expired
player = sessionplayer.gameplayer
assert isinstance(player, self._playertype)
assert player in self.players
self.players.remove(player)
with _ba.Context(self):
# Make a decent attempt to persevere if user code breaks.
try:
self.on_player_leave(player)
except Exception:
print_exception(f'Error in on_player_leave for {self}')
try:
sessionplayer.reset()
sessionplayer.set_node(None)
sessionplayer.set_activity(None)
except Exception:
print_exception(f'Error resetting player for {self}')
def add_team(self, sessionteam: ba.SessionTeam) -> None:
"""(internal)"""
assert not self.expired
with _ba.Context(self):
sessionteam.gameteam = team = self.create_team(sessionteam)
team.postinit(sessionteam)
self.teams.append(team)
try:
self.on_team_join(team)
except Exception:
print_exception(f'Error in on_team_join for {self}')
def remove_team(self, sessionteam: ba.SessionTeam) -> None:
"""(internal)"""
# This should only be called on unexpired activities the team has
# been added to.
assert not self.expired
assert sessionteam.gameteam is not None
assert sessionteam.gameteam in self.teams
team = sessionteam.gameteam
assert isinstance(team, self._teamtype)
self.teams.remove(team)
with _ba.Context(self):
# Make a decent attempt to persevere if user code breaks.
try:
self.on_team_leave(team)
except Exception:
print_exception(f'Error in on_team_leave for {self}')
try:
sessionteam.reset_gamedata()
except Exception:
print_exception(f'Error in reset_gamedata for {self}')
sessionteam.gameteam = None
def _sanity_check_begin_call(self) -> None:
# Make sure ba.Activity.on_transition_in() got called at some point.
if not self._called_activity_on_transition_in:
print_error(
'ba.Activity.on_transition_in() never got called for ' +
str(self) + '; did you forget to call it'
' in your on_transition_in override?')
# Make sure that ba.Activity.on_begin() got called at some point.
if not self._called_activity_on_begin:
print_error(
'ba.Activity.on_begin() never got called for ' + str(self) +
'; did you forget to call it in your on_begin override?')
def begin(self, session: ba.Session) -> None:
"""Begin the activity.
@ -779,6 +558,146 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self.end(self._should_end_immediately_results,
self._should_end_immediately_delay)
def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
"""Create the Player instance for this Activity.
Subclasses can override this if the activity's player class
requires a custom constructor; otherwise it will be called with
no args. Note that the player object should not be used at this
point as it is not yet fully wired up; wait for on_player_join()
for that.
"""
del sessionplayer # Unused
player = self._playertype()
return player
def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
"""Create the Team instance for this Activity.
Subclasses can override this if the activity's team class
requires a custom constructor; otherwise it will be called with
no args. Note that the team object should not be used at this
point as it is not yet fully wired up; wait for on_team_join()
for that.
"""
del sessionteam # Unused.
team = self._teamtype()
return team
def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
assert sessionplayer.team is not None
sessionplayer.reset_input()
sessionteam = sessionplayer.team
assert sessionplayer in sessionteam.players
team = sessionteam.gameteam
assert team is not None
sessionplayer.set_activity(self)
with _ba.Context(self):
sessionplayer.gameplayer = player = self.create_player(
sessionplayer)
player.postinit(sessionplayer)
assert player not in team.players
team.players.append(player)
assert player in team.players
assert player not in self.players
self.players.append(player)
assert player in self.players
try:
self.on_player_join(player)
except Exception:
print_exception('Error in on_player_join for', self)
def remove_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
# This should only be called on unexpired activities
# the player has been added to.
assert not self.expired
player: Any = sessionplayer.gameplayer
assert isinstance(player, self._playertype)
team: Any = sessionplayer.team.gameteam
assert isinstance(team, self._teamtype)
assert player in team.players
team.players.remove(player)
assert player not in team.players
assert player in self.players
self.players.remove(player)
assert player not in self.players
with _ba.Context(self):
# Make a decent attempt to persevere if user code breaks.
try:
self.on_player_leave(player)
except Exception:
print_exception(f'Error in on_player_leave for {self}')
try:
sessionplayer.reset()
sessionplayer.set_node(None)
sessionplayer.set_activity(None)
except Exception:
print_exception(f'Error resetting player for {self}')
def add_team(self, sessionteam: ba.SessionTeam) -> None:
"""(internal)"""
assert not self.expired
with _ba.Context(self):
sessionteam.gameteam = team = self.create_team(sessionteam)
team.postinit(sessionteam)
self.teams.append(team)
try:
self.on_team_join(team)
except Exception:
print_exception(f'Error in on_team_join for {self}')
def remove_team(self, sessionteam: ba.SessionTeam) -> None:
"""(internal)"""
# This should only be called on unexpired activities the team has
# been added to.
assert not self.expired
assert sessionteam.gameteam is not None
assert sessionteam.gameteam in self.teams
team = sessionteam.gameteam
assert isinstance(team, self._teamtype)
assert team in self.teams
self.teams.remove(team)
assert team not in self.teams
with _ba.Context(self):
# Make a decent attempt to persevere if user code breaks.
try:
self.on_team_leave(team)
except Exception:
print_exception(f'Error in on_team_leave for {self}')
try:
sessionteam.reset_gamedata()
except Exception:
print_exception(f'Error in reset_gamedata for {self}')
sessionteam.gameteam = None
def _sanity_check_begin_call(self) -> None:
# Make sure ba.Activity.on_transition_in() got called at some point.
if not self._called_activity_on_transition_in:
print_error(
'ba.Activity.on_transition_in() never got called for ' +
str(self) + '; did you forget to call it'
' in your on_transition_in override?')
# Make sure that ba.Activity.on_begin() got called at some point.
if not self._called_activity_on_begin:
print_error(
'ba.Activity.on_begin() never got called for ' + str(self) +
'; did you forget to call it in your on_begin override?')
# noinspection PyUnresolvedReferences
def _setup_player_and_team_types(self) -> None:
"""Pull player and team types from our typing.Generic params."""
@ -805,3 +724,104 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
f' if you do not want to override it.')
assert issubclass(self._playertype, Player)
assert issubclass(self._teamtype, Team)
@classmethod
def _check_activity_death(cls, activity_ref: ReferenceType[Activity],
counter: List[int]) -> None:
"""Sanity check to make sure an Activity was destroyed properly.
Receives a weakref to a ba.Activity which should have torn itself
down due to no longer being referenced anywhere. Will complain
and/or print debugging info if the Activity still exists.
"""
try:
import gc
import types
activity = activity_ref()
print('ERROR: Activity is not dying when expected:', activity,
'(warning ' + str(counter[0] + 1) + ')')
print('This means something is still strong-referencing it.')
counter[0] += 1
# FIXME: Running the code below shows us references but winds up
# keeping the object alive; need to figure out why.
# For now we just print refs if the count gets to 3, and then we
# kill the app at 4 so it doesn't matter anyway.
if counter[0] == 3:
print('Activity references for', activity, ':')
refs = list(gc.get_referrers(activity))
i = 1
for ref in refs:
if isinstance(ref, types.FrameType):
continue
print(' reference', i, ':', ref)
i += 1
if counter[0] == 4:
print('Killing app due to stuck activity... :-(')
_ba.quit()
except Exception:
print_exception('exception on _check_activity_death:')
def _expire(self) -> None:
"""Put the activity in a state where it can be garbage-collected.
This involves clearing anything that might be holding a reference
to it, etc.
"""
assert not self._expired
self._expired = True
try:
self.on_expire()
except Exception:
print_exception(f'Error in Activity on_expire() for {self}')
# Send expire notices to all remaining actors.
for actor_ref in self._actor_weak_refs:
try:
actor = actor_ref()
if actor is not None:
actor.on_expire()
except Exception:
print_exception(f'Error expiring Actor {actor_ref()}')
# Reset all Players.
# (releases any attached actors, clears game-data, etc)
for player in self.players:
if player:
try:
sessionplayer = player.sessionplayer
sessionplayer.set_node(None)
sessionplayer.set_activity(None)
sessionplayer.gameplayer = None
sessionplayer.reset()
except Exception:
print_exception(f'Error resetting Player {player}')
# Ditto with Teams.
for team in self.teams:
try:
sessionteam = team.sessionteam
sessionteam.gameteam = None
sessionteam.reset_gamedata()
except SessionTeamNotFoundError:
pass
# print_exception(f'Error resetting Team {team}')
except Exception:
print_exception(f'Error resetting Team {team}')
# Regardless of what happened here, we want to destroy our data, as
# our activity might not go down if we don't. This will kill all
# Timers, Nodes, etc, which should clear up any remaining refs to our
# Actors and Activity and allow us to die peacefully.
try:
self._activity_data.destroy()
except Exception:
print_exception(
'Exception during ba.Activity._expire() destroying data:')
def _prune_dead_actors(self) -> None:
self._actor_refs = [a for a in self._actor_refs if a]
self._actor_weak_refs = [a for a in self._actor_weak_refs if a()]
self._last_prune_dead_actors_time = _ba.time()

View File

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

View File

@ -59,6 +59,18 @@ class Player(Generic[TeamType]):
"""
from ba._nodeactor import NodeActor
import _ba
# Sanity check; if a dataclass is created that inherits from us,
# it will define an equality operator by default which will break
# internal game logic. So complain loudly if we find one.
if type(self).__eq__ is not object.__eq__:
raise RuntimeError(
f'Player class {type(self)} defines an equality'
f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.')
self.actor = None
self.character = ''
self._nodeactor: Optional[ba.NodeActor] = None

View File

@ -126,6 +126,18 @@ class Team(Generic[PlayerType]):
(internal)
"""
# Sanity check; if a dataclass is created that inherits from us,
# it will define an equality operator by default which will break
# internal game logic. So complain loudly if we find one.
if type(self).__eq__ is not object.__eq__:
raise RuntimeError(
f'Team class {type(self)} defines an equality'
f' operator (__eq__) which will break internal'
f' logic. Please remove it.\n'
f'For dataclasses you can do "dataclass(eq=False)"'
f' in the class decorator.')
self.players = []
self._sessionteam = weakref.ref(sessionteam)
self.id = sessionteam.id
@ -134,6 +146,15 @@ class Team(Generic[PlayerType]):
self.gamedata = sessionteam.gamedata
self.sessiondata = sessionteam.sessiondata
def manual_init(self, team_id: int, name: Union[ba.Lstr, str],
color: Tuple[float, ...]) -> None:
"""Manually init a team for uses such as bots."""
self.id = team_id
self.name = name
self.color = color
self.gamedata = {}
self.sessiondata = {}
@property
def sessionteam(self) -> SessionTeam:
"""Return the ba.SessionTeam corresponding to this Team.

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
import ba
@ -40,7 +41,7 @@ class Icon(ba.Actor):
"""Creates in in-game icon on screen."""
def __init__(self,
player: ba.Player,
player: Player,
position: Tuple[float, float],
scale: float,
show_lives: bool = True,
@ -117,7 +118,7 @@ class Icon(ba.Actor):
def update_for_lives(self) -> None:
"""Update for the target player's current lives."""
if self._player:
lives = self._player.gamedata['lives']
lives = self._player.lives
else:
lives = 0
if self._show_lives:
@ -158,13 +159,27 @@ class Icon(ba.Actor):
0.50: 1.0,
0.55: 0.2
})
lives = self._player.gamedata['lives']
lives = self._player.lives
if lives == 0:
ba.timer(0.6, self.update_for_lives)
@dataclass(eq=False)
class Player(ba.Player['Team']):
"""Our player type for this game."""
lives: int = 0
icons: List[Icon] = field(default_factory=list)
@dataclass(eq=False)
class Team(ba.Team[Player]):
"""Our team type for this game."""
survival_seconds: Optional[int] = None
spawn_order: List[Player] = field(default_factory=list)
# ba_meta export game
class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
class EliminationGame(ba.TeamGameActivity[Player, Team]):
"""Game type where last player(s) left alive win."""
@classmethod
@ -196,13 +211,15 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]:
settings: List[Tuple[str, Dict[str, Any]]] = [
('Lives Per Player', {
'default': 1, 'min_value': 1,
'max_value': 10, 'increment': 1
'default': 1,
'min_value': 1,
'max_value': 10,
'increment': 1
}),
('Time Limit', {
'choices': [('None', 0), ('1 Minute', 60),
('2 Minutes', 120), ('5 Minutes', 300),
('10 Minutes', 600), ('20 Minutes', 1200)],
'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120),
('5 Minutes', 300), ('10 Minutes', 600),
('20 Minutes', 1200)],
'default': 0
}),
('Respawn Times', {
@ -210,7 +227,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
('Long', 2.0), ('Longer', 4.0)],
'default': 1.0
}),
('Epic Mode', {'default': False})] # yapf: disable
('Epic Mode', {
'default': False
}),
]
if issubclass(sessiontype, ba.DualTeamSession):
settings.append(('Solo Mode', {'default': False}))
@ -221,17 +241,23 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
def __init__(self, settings: Dict[str, Any]):
from bastd.actor.scoreboard import Scoreboard
super().__init__(settings)
if self.settings_raw['Epic Mode']:
self.slow_motion = True
# Show messages when players die since it's meaningful here.
self.announce_player_deaths = True
self._solo_mode = settings.get('Solo Mode', False)
self._scoreboard = Scoreboard()
self._start_time: Optional[float] = None
self._vs_text: Optional[ba.Actor] = None
self._round_end_timer: Optional[ba.Timer] = None
self._epic_mode = bool(settings['Epic Mode'])
self._lives_per_player = int(settings['Lives Per Player'])
self._time_limit = float(settings['Time Limit'])
self._balance_total_lives = bool(
settings.get('Balance Total Lives', False))
self._solo_mode = bool(settings.get('Solo Mode', False))
# Base class overrides:
# Show messages when players die since it's meaningful here.
self.announce_player_deaths = True
self.slow_motion = self._epic_mode
self.default_music = (ba.MusicType.EPIC
if self._epic_mode else ba.MusicType.SURVIVAL)
def get_instance_description(self) -> Union[str, Sequence]:
return 'Last team standing wins.' if isinstance(
@ -241,65 +267,93 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
return 'last team standing wins' if isinstance(
self.session, ba.DualTeamSession) else 'last one standing wins'
def on_transition_in(self) -> None:
self.default_music = (ba.MusicType.EPIC
if self.settings_raw['Epic Mode'] else
ba.MusicType.SURVIVAL)
super().on_transition_in()
self._start_time = ba.time()
def on_team_join(self, team: ba.Team) -> None:
team.gamedata['survival_seconds'] = None
team.gamedata['spawn_order'] = []
def on_player_join(self, player: ba.Player) -> None:
def on_player_join(self, player: Player) -> None:
# No longer allowing mid-game joiners here; too easy to exploit.
if self.has_begun():
player.gamedata['lives'] = 0
player.gamedata['icons'] = []
# Make sure our team has survival seconds set if they're all dead
# (otherwise blocked new ffa players would be considered 'still
# alive' in score tallying).
if self._get_total_team_lives(
player.team
) == 0 and player.team.gamedata['survival_seconds'] is None:
player.team.gamedata['survival_seconds'] = 0
ba.screenmessage(ba.Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}',
player.get_name(full=True))]),
color=(0, 1, 0))
if (self._get_total_team_lives(player.team) == 0
and player.team.survival_seconds is None):
player.team.survival_seconds = 0
ba.screenmessage(
ba.Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}', player.get_name(full=True))]),
color=(0, 1, 0),
)
return
player.gamedata['lives'] = self.settings_raw['Lives Per Player']
player.lives = self._lives_per_player
if self._solo_mode:
player.gamedata['icons'] = []
player.team.gamedata['spawn_order'].append(player)
player.team.spawn_order.append(player)
self._update_solo_mode()
else:
# Create our icon and spawn.
player.gamedata['icons'] = [
Icon(player, position=(0, 50), scale=0.8)
]
if player.gamedata['lives'] > 0:
player.icons = [Icon(player, position=(0, 50), scale=0.8)]
if player.lives > 0:
self.spawn_player(player)
# Don't waste time doing this until begin.
if self.has_begun():
self._update_icons()
def on_begin(self) -> None:
super().on_begin()
self._start_time = ba.time()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
if self._solo_mode:
self._vs_text = ba.NodeActor(
ba.newnode('text',
attrs={
'position': (0, 105),
'h_attach': 'center',
'h_align': 'center',
'maxwidth': 200,
'shadow': 0.5,
'vr_depth': 390,
'scale': 0.6,
'v_attach': 'bottom',
'color': (0.8, 0.8, 0.3, 1.0),
'text': ba.Lstr(resource='vsText')
}))
# If balance-team-lives is on, add lives to the smaller team until
# total lives match.
if (isinstance(self.session, ba.DualTeamSession)
and self._balance_total_lives and self.teams[0].players
and self.teams[1].players):
if self._get_total_team_lives(
self.teams[0]) < self._get_total_team_lives(self.teams[1]):
lesser_team = self.teams[0]
greater_team = self.teams[1]
else:
lesser_team = self.teams[1]
greater_team = self.teams[0]
add_index = 0
while (self._get_total_team_lives(lesser_team) <
self._get_total_team_lives(greater_team)):
lesser_team.players[add_index].lives += 1
add_index = (add_index + 1) % len(lesser_team.players)
self._update_icons()
# We could check game-over conditions at explicit trigger points,
# but lets just do the simple thing and poll it.
ba.timer(1.0, self._update, repeat=True)
def _update_solo_mode(self) -> None:
# For both teams, find the first player on the spawn order list with
# lives remaining and spawn them if they're not alive.
for team in self.teams:
# Prune dead players from the spawn order.
team.gamedata['spawn_order'] = [
p for p in team.gamedata['spawn_order'] if p
]
for player in team.gamedata['spawn_order']:
if player.gamedata['lives'] > 0:
team.spawn_order = [p for p in team.spawn_order if p]
for player in team.spawn_order:
assert isinstance(player, Player)
if player.lives > 0:
if not player.is_alive():
self.spawn_player(player)
break
@ -315,7 +369,7 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
for team in self.teams:
if len(team.players) == 1:
player = team.players[0]
for icon in player.gamedata['icons']:
for icon in player.icons:
icon.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives()
xval += x_offs
@ -325,7 +379,7 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
if self._solo_mode:
# First off, clear out all icons.
for player in self.players:
player.gamedata['icons'] = []
player.icons = []
# Now for each team, cycle through our available players
# adding icons.
@ -340,13 +394,13 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
test_lives = 1
while True:
players_with_lives = [
p for p in team.gamedata['spawn_order']
if p and p.gamedata['lives'] >= test_lives
p for p in team.spawn_order
if p and p.lives >= test_lives
]
if not players_with_lives:
break
for player in players_with_lives:
player.gamedata['icons'].append(
player.icons.append(
Icon(player,
position=(xval, (40 if is_first else 25)),
scale=1.0 if is_first else 0.5,
@ -369,12 +423,12 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
xval = 50
x_offs = 85
for player in team.players:
for icon in player.gamedata['icons']:
for icon in player.icons:
icon.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives()
xval += x_offs
def _get_spawn_point(self, player: ba.Player) -> Optional[ba.Vec3]:
def _get_spawn_point(self, player: Player) -> Optional[ba.Vec3]:
del player # Unused.
# In solo-mode, if there's an existing live player on the map, spawn at
@ -403,119 +457,76 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
return points[-1][1]
return None
def spawn_player(self, player: ba.Player) -> ba.Actor:
def spawn_player(self, player: Player) -> ba.Actor:
actor = self.spawn_player_spaz(player, self._get_spawn_point(player))
if not self._solo_mode:
ba.timer(0.3, ba.Call(self._print_lives, player))
# If we have any icons, update their state.
for icon in player.gamedata['icons']:
for icon in player.icons:
icon.handle_player_spawned()
return actor
def _print_lives(self, player: ba.Player) -> None:
def _print_lives(self, player: Player) -> None:
from bastd.actor import popuptext
assert player # Shouldn't be passing invalid refs around.
# We get called in a timer so it's possible our player has left/etc.
if not player or not player.is_alive() or not player.node:
return
popuptext.PopupText('x' + str(player.gamedata['lives'] - 1),
popuptext.PopupText('x' + str(player.lives - 1),
color=(1, 1, 0, 1),
offset=(0, -0.8, 0),
random_offset=0.0,
scale=1.8,
position=player.node.position).autoretain()
def on_player_leave(self, player: ba.Player) -> None:
def on_player_leave(self, player: Player) -> None:
super().on_player_leave(player)
player.gamedata['icons'] = None
player.icons = []
# Remove us from spawn-order.
if self._solo_mode:
if player in player.team.gamedata['spawn_order']:
player.team.gamedata['spawn_order'].remove(player)
if player in player.team.spawn_order:
player.team.spawn_order.remove(player)
# Update icons in a moment since our team will be gone from the
# list then.
ba.timer(0, self._update_icons)
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self.settings_raw['Time Limit'])
self.setup_standard_powerup_drops()
if self._solo_mode:
self._vs_text = ba.NodeActor(
ba.newnode('text',
attrs={
'position': (0, 105),
'h_attach': 'center',
'h_align': 'center',
'maxwidth': 200,
'shadow': 0.5,
'vr_depth': 390,
'scale': 0.6,
'v_attach': 'bottom',
'color': (0.8, 0.8, 0.3, 1.0),
'text': ba.Lstr(resource='vsText')
}))
# If balance-team-lives is on, add lives to the smaller team until
# total lives match.
if (isinstance(self.session, ba.DualTeamSession)
and self.settings_raw['Balance Total Lives']
and self.teams[0].players and self.teams[1].players):
if self._get_total_team_lives(
self.teams[0]) < self._get_total_team_lives(self.teams[1]):
lesser_team = self.teams[0]
greater_team = self.teams[1]
else:
lesser_team = self.teams[1]
greater_team = self.teams[0]
add_index = 0
while self._get_total_team_lives(
lesser_team) < self._get_total_team_lives(greater_team):
lesser_team.players[add_index].gamedata['lives'] += 1
add_index = (add_index + 1) % len(lesser_team.players)
self._update_icons()
# We could check game-over conditions at explicit trigger points,
# but lets just do the simple thing and poll it.
ba.timer(1.0, self._update, repeat=True)
def _get_total_team_lives(self, team: ba.Team) -> int:
return sum(player.gamedata['lives'] for player in team.players)
def _get_total_team_lives(self, team: Team) -> int:
return sum(player.lives for player in team.players)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.playerspaz(self).player
player: Player = msg.playerspaz(self).player
player.gamedata['lives'] -= 1
if player.gamedata['lives'] < 0:
player.lives -= 1
if player.lives < 0:
ba.print_error(
"Got lives < 0 in Elim; this shouldn't happen. solo:" +
str(self._solo_mode))
player.gamedata['lives'] = 0
player.lives = 0
# If we have any icons, update their state.
for icon in player.gamedata['icons']:
for icon in player.icons:
icon.handle_player_died()
# Play big death sound on our last death
# or for every one in solo mode.
if self._solo_mode or player.gamedata['lives'] == 0:
if self._solo_mode or player.lives == 0:
ba.playsound(spaz.get_factory().single_player_death_sound)
# If we hit zero lives, we're dead (and our team might be too).
if player.gamedata['lives'] == 0:
if player.lives == 0:
# If the whole team is now dead, mark their survival time.
if self._get_total_team_lives(player.team) == 0:
assert self._start_time is not None
player.team.gamedata['survival_seconds'] = int(
ba.time() - self._start_time)
player.team.survival_seconds = int(ba.time() -
self._start_time)
else:
# Otherwise, in regular mode, respawn.
if not self._solo_mode:
@ -523,8 +534,8 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
# In solo, put ourself at the back of the spawn order.
if self._solo_mode:
player.team.gamedata['spawn_order'].remove(player)
player.team.gamedata['spawn_order'].append(player)
player.team.spawn_order.remove(player)
player.team.spawn_order.append(player)
def _update(self) -> None:
if self._solo_mode:
@ -532,11 +543,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
# list with lives remaining and spawn them if they're not alive.
for team in self.teams:
# Prune dead players from the spawn order.
team.gamedata['spawn_order'] = [
p for p in team.gamedata['spawn_order'] if p
]
for player in team.gamedata['spawn_order']:
if player.gamedata['lives'] > 0:
team.spawn_order = [p for p in team.spawn_order if p]
for player in team.spawn_order:
assert isinstance(player, Player)
if player.lives > 0:
if not player.is_alive():
self.spawn_player(player)
self._update_icons()
@ -548,10 +558,10 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
if len(self._get_living_teams()) < 2:
self._round_end_timer = ba.Timer(0.5, self.end_game)
def _get_living_teams(self) -> List[ba.Team]:
def _get_living_teams(self) -> List[Team]:
return [
team for team in self.teams
if len(team.players) > 0 and any(player.gamedata['lives'] > 0
if len(team.players) > 0 and any(player.lives > 0
for player in team.players)
]
@ -561,5 +571,5 @@ class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
results = ba.TeamGameResults()
self._vs_text = None # Kill our 'vs' if its there.
for team in self.teams:
results.set_team_score(team, team.gamedata['survival_seconds'])
results.set_team_score(team, team.survival_seconds)
self.end(results=results)

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -23,6 +23,7 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING
import ba
@ -35,7 +36,17 @@ if TYPE_CHECKING:
from bastd.actor.scoreboard import Scoreboard
class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]):
@dataclass(eq=False)
class Player(ba.Player['Team']):
"""Our player type for this game."""
@dataclass(eq=False)
class Team(ba.Team[Player]):
"""Our team type for this game."""
class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
"""Slow motion how-long-can-you-last game."""
tips = [
@ -118,7 +129,7 @@ class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]):
self._tntspawner = TNTSpawner(position=self._tntspawnpos,
respawn_time=10.0)
def spawn_player(self, player: ba.Player) -> ba.Actor:
def spawn_player(self, player: Player) -> ba.Actor:
pos = (self._spawn_center[0] + random.uniform(-1.5, 1.5),
self._spawn_center[1],
self._spawn_center[2] + random.uniform(-1.5, 1.5))

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-05-18 for Ballistica version 1.5.0 build 20021</em></h4>
<h4><em>last updated on 2020-05-19 for Ballistica version 1.5.0 build 20021</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>
@ -4741,6 +4741,15 @@ of the session.</p>
<p> Throws a <a href="#class_ba_SessionTeamNotFoundError">ba.SessionTeamNotFoundError</a> if there is none.</p>
</dd>
</dl>
<h3>Methods:</h3>
<dl>
<dt><h4><a name="method_ba_Team__manual_init">manual_init()</a></dt></h4><dd>
<p><span>manual_init(self, team_id: int, name: Union[<a href="#class_ba_Lstr">ba.Lstr</a>, str], color: Tuple[float, ...]) -&gt; None</span></p>
<p>Manually init a team for uses such as bots.</p>
</dd>
</dl>
<hr>
@ -4949,7 +4958,7 @@ Results for a completed <a href="#class_ba_TeamGameActivity">ba.TeamGameActivity
</dd>
<dt><h4><a name="method_ba_TeamGameResults__set_team_score">set_team_score()</a></dt></h4><dd>
<p><span>set_team_score(self, team: Union[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>, <a href="#class_ba_Team">ba.Team</a>], score: int) -&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>