Initial commit

This commit is contained in:
vortex 2024-02-26 00:17:10 +05:30
parent bc49523c99
commit 44d606cce7
1929 changed files with 612166 additions and 0 deletions

5
dist/ba_data/python/bastd/__init__.py vendored Normal file
View file

@ -0,0 +1,5 @@
# Released under the MIT License. See LICENSE for details.
#
"""Ballistica standard library: games, UI, etc."""
# ba_meta require api 7

Binary file not shown.

View file

@ -0,0 +1 @@
# Released under the MIT License. See LICENSE for details.

View file

@ -0,0 +1,112 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the co-op join screen."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from ba.internal import JoinActivity
if TYPE_CHECKING:
pass
class CoopJoinActivity(JoinActivity):
"""Join-screen for co-op mode."""
# We can assume our session is a CoopSession.
session: ba.CoopSession
def __init__(self, settings: dict):
super().__init__(settings)
session = self.session
assert isinstance(session, ba.CoopSession)
def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide
from bastd.actor.text import Text
super().on_transition_in()
assert isinstance(self.session, ba.CoopSession)
assert self.session.campaign
Text(
self.session.campaign.getlevel(
self.session.campaign_level_name
).displayname,
scale=1.3,
h_attach=Text.HAttach.CENTER,
h_align=Text.HAlign.CENTER,
v_attach=Text.VAttach.TOP,
transition=Text.Transition.FADE_IN,
transition_delay=4.0,
color=(1, 1, 1, 0.6),
position=(0, -95),
).autoretain()
ControlsGuide(delay=1.0).autoretain()
ba.pushcall(self._show_remaining_achievements)
def _show_remaining_achievements(self) -> None:
from bastd.actor.text import Text
# We only show achievements and challenges for CoopGameActivities.
session = self.session
assert isinstance(session, ba.CoopSession)
gameinstance = session.get_current_game_instance()
if not isinstance(gameinstance, ba.CoopGameActivity):
return
delay = 1.0
vpos = -140.0
# Now list our remaining achievements for this level.
assert self.session.campaign is not None
assert isinstance(self.session, ba.CoopSession)
levelname = (
self.session.campaign.name + ':' + self.session.campaign_level_name
)
ts_h_offs = 60
if not (ba.app.demo_mode or ba.app.arcade_mode):
achievements = [
a
for a in ba.app.ach.achievements_for_coop_level(levelname)
if not a.complete
]
have_achievements = bool(achievements)
achievements = [a for a in achievements if not a.complete]
vrmode = ba.app.vr_mode
if have_achievements:
Text(
ba.Lstr(resource='achievementsRemainingText'),
host_only=True,
position=(ts_h_offs - 10, vpos),
transition=Text.Transition.FADE_IN,
scale=1.1 * 0.76,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1, 1),
shadow=1.0,
flatness=1.0 if vrmode else 0.6,
transition_delay=delay,
).autoretain()
hval = ts_h_offs + 50
vpos -= 35
for ach in achievements:
delay += 0.05
ach.create_display(hval, vpos, delay, style='in_game')
vpos -= 55
if not achievements:
Text(
ba.Lstr(resource='noAchievementsRemainingText'),
host_only=True,
position=(ts_h_offs + 15, vpos + 10),
transition=Text.Transition.FADE_IN,
scale=0.7,
h_attach=Text.HAttach.LEFT,
v_attach=Text.VAttach.TOP,
color=(1, 1, 1, 0.5),
transition_delay=delay + 0.5,
).autoretain()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the draw screen."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
from bastd.actor.zoomtext import ZoomText
if TYPE_CHECKING:
pass
class DrawScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Score screen shown after a draw."""
default_music = None # Awkward silence...
def on_begin(self) -> None:
ba.set_analytics_screen('Draw Score Screen')
super().on_begin()
ZoomText(
ba.Lstr(resource='drawText'),
position=(0, 0),
maxwidth=400,
shiftposition=(-220, 0),
shiftdelay=2.0,
flash=False,
trail=False,
jitter=1.0,
).autoretain()
ba.timer(0.35, ba.Call(ba.playsound, self._score_display_sound))
self.show_player_scores(results=self.settings_raw.get('results', None))

View file

@ -0,0 +1,168 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the end screen in dual-team mode."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
from bastd.actor.zoomtext import ZoomText
if TYPE_CHECKING:
pass
class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Scorescreen between rounds of a dual-team session."""
def __init__(self, settings: dict):
super().__init__(settings=settings)
self._winner: ba.SessionTeam = settings['winner']
assert isinstance(self._winner, ba.SessionTeam)
def on_begin(self) -> None:
ba.set_analytics_screen('Teams Score Screen')
super().on_begin()
height = 130
active_team_count = len(self.teams)
vval = (height * active_team_count) / 2 - height / 2
i = 0
shift_time = 2.5
# Usually we say 'Best of 7', but if the language prefers we can say
# 'First to 4'.
session = self.session
assert isinstance(session, ba.MultiTeamSession)
if ba.app.lang.get_resource('bestOfUseFirstToInstead'):
best_txt = ba.Lstr(
resource='firstToSeriesText',
subs=[('${COUNT}', str(session.get_series_length() / 2 + 1))],
)
else:
best_txt = ba.Lstr(
resource='bestOfSeriesText',
subs=[('${COUNT}', str(session.get_series_length()))],
)
ZoomText(
best_txt,
position=(0, 175),
shiftposition=(-250, 175),
shiftdelay=2.5,
flash=False,
trail=False,
h_align='center',
scale=0.25,
color=(0.5, 0.5, 0.5, 1.0),
jitter=3.0,
).autoretain()
for team in self.session.sessionteams:
ba.timer(
i * 0.15 + 0.15,
ba.WeakCall(
self._show_team_name,
vval - i * height,
team,
i * 0.2,
shift_time - (i * 0.150 + 0.150),
),
)
ba.timer(
i * 0.150 + 0.5,
ba.Call(ba.playsound, self._score_display_sound_small),
)
scored = team is self._winner
delay = 0.2
if scored:
delay = 1.2
ba.timer(
i * 0.150 + 0.2,
ba.WeakCall(
self._show_team_old_score,
vval - i * height,
team,
shift_time - (i * 0.15 + 0.2),
),
)
ba.timer(
i * 0.15 + 1.5,
ba.Call(ba.playsound, self._score_display_sound),
)
ba.timer(
i * 0.150 + delay,
ba.WeakCall(
self._show_team_score,
vval - i * height,
team,
scored,
i * 0.2 + 0.1,
shift_time - (i * 0.15 + delay),
),
)
i += 1
self.show_player_scores()
def _show_team_name(
self,
pos_v: float,
team: ba.SessionTeam,
kill_delay: float,
shiftdelay: float,
) -> None:
del kill_delay # Unused arg.
ZoomText(
ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
shiftposition=(-150, pos_v),
shiftdelay=shiftdelay,
flash=False,
trail=False,
h_align='right',
maxwidth=300,
color=team.color,
jitter=1.0,
).autoretain()
def _show_team_old_score(
self, pos_v: float, sessionteam: ba.SessionTeam, shiftdelay: float
) -> None:
ZoomText(
str(sessionteam.customdata['score'] - 1),
position=(150, pos_v),
maxwidth=100,
color=(0.6, 0.6, 0.7),
shiftposition=(-100, pos_v),
shiftdelay=shiftdelay,
flash=False,
trail=False,
lifespan=1.0,
h_align='left',
jitter=1.0,
).autoretain()
def _show_team_score(
self,
pos_v: float,
sessionteam: ba.SessionTeam,
scored: bool,
kill_delay: float,
shiftdelay: float,
) -> None:
del kill_delay # Unused arg.
ZoomText(
str(sessionteam.customdata['score']),
position=(150, pos_v),
maxwidth=100,
color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7),
shiftposition=(-100, pos_v),
shiftdelay=shiftdelay,
flash=scored,
trail=scored,
h_align='left',
jitter=1.0,
trailcolor=(1, 0.8, 0.0, 0),
).autoretain()

View file

@ -0,0 +1,368 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the final screen in free-for-all games."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
if TYPE_CHECKING:
from typing import Any
class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Score screen shown at after free-for-all rounds."""
def __init__(self, settings: dict):
super().__init__(settings=settings)
# Keep prev activity alive while we fade in.
self.transition_time = 0.5
self._cymbal_sound = ba.getsound('cymbal')
def on_begin(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
ba.set_analytics_screen('FreeForAll Score Screen')
super().on_begin()
y_base = 100.0
ts_h_offs = -305.0
tdelay = 1.0
scale = 1.2
spacing = 37.0
# We include name and previous score in the sort to reduce the amount
# of random jumping around the list we do in cases of ties.
player_order_prev = list(self.players)
player_order_prev.sort(
reverse=True,
key=lambda p: (
p.team.sessionteam.customdata['previous_score'],
p.getname(full=True),
),
)
player_order = list(self.players)
player_order.sort(
reverse=True,
key=lambda p: (
p.team.sessionteam.customdata['score'],
p.team.sessionteam.customdata['score'],
p.getname(full=True),
),
)
v_offs = -74.0 + spacing * len(player_order_prev) * 0.5
delay1 = 1.3 + 0.1
delay2 = 2.9 + 0.1
delay3 = 2.9 + 0.1
order_change = player_order != player_order_prev
if order_change:
delay3 += 1.5
ba.timer(0.3, ba.Call(ba.playsound, self._score_display_sound))
results = self.settings_raw['results']
assert isinstance(results, ba.GameResults)
self.show_player_scores(
delay=0.001, results=results, scale=1.2, x_offset=-110.0
)
sound_times: set[float] = set()
def _scoretxt(
text: str,
x_offs: float,
y_offs: float,
highlight: bool,
delay: float,
extrascale: float,
flash: bool = False,
) -> Text:
return Text(
text,
position=(
ts_h_offs + x_offs * scale,
y_base + (y_offs + v_offs + 2.0) * scale,
),
scale=scale * extrascale,
color=(
(1.0, 0.7, 0.3, 1.0) if highlight else (0.7, 0.7, 0.7, 0.7)
),
h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay + delay,
flash=flash,
).autoretain()
v_offs -= spacing
slide_amt = 0.0
transtime = 0.250
transtime2 = 0.250
session = self.session
assert isinstance(session, ba.FreeForAllSession)
title = Text(
ba.Lstr(
resource='firstToSeriesText',
subs=[('${COUNT}', str(session.get_ffa_series_length()))],
),
scale=1.05 * scale,
position=(
ts_h_offs - 0.0 * scale,
y_base + (v_offs + 50.0) * scale,
),
h_align=Text.HAlign.CENTER,
color=(0.5, 0.5, 0.5, 0.5),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
v_offs -= 25
v_offs_start = v_offs
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate,
title.position_combine,
'input0',
{
0.0: ts_h_offs - 0.0 * scale,
transtime2: ts_h_offs - (0.0 + slide_amt) * scale,
},
),
)
for i, player in enumerate(player_order_prev):
v_offs_2 = v_offs_start - spacing * (player_order.index(player))
ba.timer(
tdelay + 0.3,
ba.Call(ba.playsound, self._score_display_sound_small),
)
if order_change:
ba.timer(
tdelay + delay2 + 0.1,
ba.Call(ba.playsound, self._cymbal_sound),
)
img = Image(
player.get_icon(),
position=(
ts_h_offs - 72.0 * scale,
y_base + (v_offs + 15.0) * scale,
),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate,
img.position_combine,
'input1',
{
0: y_base + (v_offs + 15.0) * scale,
transtime: y_base + (v_offs_2 + 15.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate,
img.position_combine,
'input0',
{
0: ts_h_offs - 72.0 * scale,
transtime2: ts_h_offs - (72.0 + slide_amt) * scale,
},
),
)
txt = Text(
ba.Lstr(value=player.getname(full=True)),
maxwidth=130.0,
scale=0.75 * scale,
position=(
ts_h_offs - 50.0 * scale,
y_base + (v_offs + 15.0) * scale,
),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(player.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate,
txt.position_combine,
'input1',
{
0: y_base + (v_offs + 15.0) * scale,
transtime: y_base + (v_offs_2 + 15.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate,
txt.position_combine,
'input0',
{
0: ts_h_offs - 50.0 * scale,
transtime2: ts_h_offs - (50.0 + slide_amt) * scale,
},
),
)
txt_num = Text(
'#' + str(i + 1),
scale=0.55 * scale,
position=(
ts_h_offs - 95.0 * scale,
y_base + (v_offs + 8.0) * scale,
),
h_align=Text.HAlign.RIGHT,
color=(0.6, 0.6, 0.6, 0.6),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate,
txt_num.position_combine,
'input0',
{
0: ts_h_offs - 95.0 * scale,
transtime2: ts_h_offs - (95.0 + slide_amt) * scale,
},
),
)
s_txt = _scoretxt(
str(player.team.sessionteam.customdata['previous_score']),
80,
0,
False,
0,
1.0,
)
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate,
s_txt.position_combine,
'input1',
{
0: y_base + (v_offs + 2.0) * scale,
transtime: y_base + (v_offs_2 + 2.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate,
s_txt.position_combine,
'input0',
{
0: ts_h_offs + 80.0 * scale,
transtime2: ts_h_offs + (80.0 - slide_amt) * scale,
},
),
)
score_change = (
player.team.sessionteam.customdata['score']
- player.team.sessionteam.customdata['previous_score']
)
if score_change > 0:
xval = 113
yval = 3.0
s_txt_2 = _scoretxt(
'+' + str(score_change),
xval,
yval,
True,
0,
0.7,
flash=True,
)
ba.timer(
tdelay + delay2,
ba.WeakCall(
self._safe_animate,
s_txt_2.position_combine,
'input1',
{
0: y_base + (v_offs + yval + 2.0) * scale,
transtime: y_base + (v_offs_2 + yval + 2.0) * scale,
},
),
)
ba.timer(
tdelay + delay3,
ba.WeakCall(
self._safe_animate,
s_txt_2.position_combine,
'input0',
{
0: ts_h_offs + xval * scale,
transtime2: ts_h_offs + (xval - slide_amt) * scale,
},
),
)
def _safesetattr(
node: ba.Node | None, attr: str, value: Any
) -> None:
if node:
setattr(node, attr, value)
ba.timer(
tdelay + delay1,
ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)),
)
for j in range(score_change):
ba.timer(
(tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr,
s_txt.node,
'text',
str(
player.team.sessionteam.customdata[
'previous_score'
]
+ j
+ 1
),
),
)
tfin = tdelay + delay1 + 0.15 * j
if tfin not in sound_times:
sound_times.add(tfin)
ba.timer(
tfin,
ba.Call(
ba.playsound, self._score_display_sound_small
),
)
v_offs -= spacing
def _safe_animate(
self, node: ba.Node | None, attr: str, keys: dict[float, float]
) -> None:
"""Run an animation on a node if the node still exists."""
if node:
ba.animate(node, attr, keys)

View file

@ -0,0 +1,93 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the join screen for multi-team sessions."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from ba.internal import JoinActivity
from bastd.actor.text import Text
if TYPE_CHECKING:
pass
class MultiTeamJoinActivity(JoinActivity):
"""Join screen for teams sessions."""
def __init__(self, settings: dict):
super().__init__(settings)
self._next_up_text: Text | None = None
def on_transition_in(self) -> None:
from bastd.actor.controlsguide import ControlsGuide
from ba import DualTeamSession
super().on_transition_in()
ControlsGuide(delay=1.0).autoretain()
session = self.session
assert isinstance(session, ba.MultiTeamSession)
# Show info about the next up game.
self._next_up_text = Text(
ba.Lstr(
value='${1} ${2}',
subs=[
('${1}', ba.Lstr(resource='upFirstText')),
('${2}', session.get_next_game_description()),
],
),
h_attach=Text.HAttach.CENTER,
scale=0.7,
v_attach=Text.VAttach.TOP,
h_align=Text.HAlign.CENTER,
position=(0, -70),
flash=False,
color=(0.5, 0.5, 0.5, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=5.0,
)
# In teams mode, show our two team names.
# FIXME: Lobby should handle this.
if isinstance(ba.getsession(), DualTeamSession):
team_names = [team.name for team in ba.getsession().sessionteams]
team_colors = [
tuple(team.color) + (0.5,)
for team in ba.getsession().sessionteams
]
if len(team_names) == 2:
for i in range(2):
Text(
team_names[i],
scale=0.7,
h_attach=Text.HAttach.CENTER,
v_attach=Text.VAttach.TOP,
h_align=Text.HAlign.CENTER,
position=(-200 + 350 * i, -100),
color=team_colors[i],
transition=Text.Transition.FADE_IN,
).autoretain()
Text(
ba.Lstr(
resource='mustInviteFriendsText',
subs=[
('${GATHER}', ba.Lstr(resource='gatherWindow.titleText'))
],
),
h_attach=Text.HAttach.CENTER,
scale=0.8,
host_only=True,
v_attach=Text.VAttach.CENTER,
h_align=Text.HAlign.CENTER,
position=(0, 0),
flash=False,
color=(0, 1, 0, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=2.0,
transition_out_delay=7.0,
).autoretain()

View file

@ -0,0 +1,262 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to teams mode score screen."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from ba.internal import ScoreScreenActivity
from bastd.actor.text import Text
from bastd.actor.image import Image
if TYPE_CHECKING:
pass
class MultiTeamScoreScreenActivity(ScoreScreenActivity):
"""Base class for score screens."""
def __init__(self, settings: dict):
super().__init__(settings=settings)
self._score_display_sound = ba.getsound('scoreHit01')
self._score_display_sound_small = ba.getsound('scoreHit02')
self._show_up_next: bool = True
def on_begin(self) -> None:
super().on_begin()
session = self.session
if self._show_up_next and isinstance(session, ba.MultiTeamSession):
txt = ba.Lstr(
value='${A} ${B}',
subs=[
(
'${A}',
ba.Lstr(
resource='upNextText',
subs=[
('${COUNT}', str(session.get_game_number() + 1))
],
),
),
('${B}', session.get_next_game_description()),
],
)
Text(
txt,
maxwidth=900,
h_attach=Text.HAttach.CENTER,
v_attach=Text.VAttach.BOTTOM,
h_align=Text.HAlign.CENTER,
v_align=Text.VAlign.CENTER,
position=(0, 53),
flash=False,
color=(0.3, 0.3, 0.35, 1.0),
transition=Text.Transition.FADE_IN,
transition_delay=2.0,
).autoretain()
def show_player_scores(
self,
delay: float = 2.5,
results: ba.GameResults | None = None,
scale: float = 1.0,
x_offset: float = 0.0,
y_offset: float = 0.0,
) -> None:
"""Show scores for individual players."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
ts_v_offset = 150.0 + y_offset
ts_h_offs = 80.0 + x_offset
tdelay = delay
spacing = 40
is_free_for_all = isinstance(self.session, ba.FreeForAllSession)
def _get_prec_score(p_rec: ba.PlayerRecord) -> int | None:
if is_free_for_all and results is not None:
assert isinstance(results, ba.GameResults)
assert p_rec.team.activityteam is not None
val = results.get_sessionteam_score(p_rec.team)
return val
return p_rec.accumscore
def _get_prec_score_str(p_rec: ba.PlayerRecord) -> str | ba.Lstr:
if is_free_for_all and results is not None:
assert isinstance(results, ba.GameResults)
assert p_rec.team.activityteam is not None
val = results.get_sessionteam_score_str(p_rec.team)
assert val is not None
return val
return str(p_rec.accumscore)
# stats.get_records() can return players that are no longer in
# the game.. if we're using results we have to filter those out
# (since they're not in results and that's where we pull their
# scores from)
if results is not None:
assert isinstance(results, ba.GameResults)
player_records = []
assert self.stats
valid_players = list(self.stats.get_records().items())
# noinspection PyUnresolvedReferences
def _get_player_score_set_entry(
player: ba.SessionPlayer,
) -> ba.PlayerRecord | None:
for p_rec in valid_players:
if p_rec[1].player is player:
return p_rec[1]
return None
# Results is already sorted; just convert it into a list of
# score-set-entries.
for winnergroup in results.winnergroups:
for team in winnergroup.teams:
if len(team.players) == 1:
player_entry = _get_player_score_set_entry(
team.players[0]
)
if player_entry is not None:
player_records.append(player_entry)
else:
player_records = []
player_records_scores = [
(_get_prec_score(p), name, p)
for name, p in list(self.stats.get_records().items())
]
player_records_scores.sort(reverse=True)
# Just want living player entries.
player_records = [p[2] for p in player_records_scores if p[2]]
voffs = -140.0 + spacing * len(player_records) * 0.5
def _txt(
xoffs: float,
yoffs: float,
text: ba.Lstr,
h_align: Text.HAlign = Text.HAlign.RIGHT,
extrascale: float = 1.0,
maxwidth: float | None = 120.0,
) -> None:
Text(
text,
color=(0.5, 0.5, 0.6, 0.5),
position=(
ts_h_offs + xoffs * scale,
ts_v_offset + (voffs + yoffs + 4.0) * scale,
),
h_align=h_align,
v_align=Text.VAlign.CENTER,
scale=0.8 * scale * extrascale,
maxwidth=maxwidth,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
session = self.session
assert isinstance(session, ba.MultiTeamSession)
tval = ba.Lstr(
resource='gameLeadersText',
subs=[('${COUNT}', str(session.get_game_number()))],
)
_txt(
180,
43,
tval,
h_align=Text.HAlign.CENTER,
extrascale=1.4,
maxwidth=None,
)
_txt(-15, 4, ba.Lstr(resource='playerText'), h_align=Text.HAlign.LEFT)
_txt(180, 4, ba.Lstr(resource='killsText'))
_txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100)
score_label = 'Score' if results is None else results.score_label
translated = ba.Lstr(translate=('scoreNames', score_label))
_txt(390, 0, translated)
topkillcount = 0
topkilledcount = 99999
top_score = (
0 if not player_records else _get_prec_score(player_records[0])
)
for prec in player_records:
topkillcount = max(topkillcount, prec.accum_kill_count)
topkilledcount = min(topkilledcount, prec.accum_killed_count)
def _scoretxt(
text: str | ba.Lstr,
x_offs: float,
highlight: bool,
delay2: float,
maxwidth: float = 70.0,
) -> None:
Text(
text,
position=(
ts_h_offs + x_offs * scale,
ts_v_offset + (voffs + 15) * scale,
),
scale=scale,
color=(1.0, 0.9, 0.5, 1.0)
if highlight
else (0.5, 0.5, 0.6, 0.5),
h_align=Text.HAlign.RIGHT,
v_align=Text.VAlign.CENTER,
maxwidth=maxwidth,
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay + delay2,
).autoretain()
for playerrec in player_records:
tdelay += 0.05
voffs -= spacing
Image(
playerrec.get_icon(),
position=(
ts_h_offs - 12 * scale,
ts_v_offset + (voffs + 15.0) * scale,
),
scale=(30.0 * scale, 30.0 * scale),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
Text(
ba.Lstr(value=playerrec.getname(full=True)),
maxwidth=160,
scale=0.75 * scale,
position=(
ts_h_offs + 10.0 * scale,
ts_v_offset + (voffs + 15) * scale,
),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(playerrec.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
_scoretxt(
str(playerrec.accum_kill_count),
180,
playerrec.accum_kill_count == topkillcount,
0.1,
)
_scoretxt(
str(playerrec.accum_killed_count),
280,
playerrec.accum_killed_count == topkilledcount,
0.1,
)
_scoretxt(
_get_prec_score_str(playerrec),
390,
_get_prec_score(playerrec) == top_score,
0.2,
)

View file

@ -0,0 +1,481 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the final screen in multi-teams sessions."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
if TYPE_CHECKING:
pass
class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
"""Final score screen for a team series."""
# Dont' play music by default; (we do manually after a delay).
default_music = None
def __init__(self, settings: dict):
super().__init__(settings=settings)
self._min_view_time = 15.0
self._is_ffa = isinstance(self.session, ba.FreeForAllSession)
self._allow_server_transition = True
self._tips_text = None
self._default_show_tips = False
def on_begin(self) -> None:
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
ba.set_analytics_screen(
'FreeForAll Series Victory Screen'
if self._is_ffa
else 'Teams Series Victory Screen'
)
if ba.app.ui.uiscale is ba.UIScale.LARGE:
sval = ba.Lstr(resource='pressAnyKeyButtonPlayAgainText')
else:
sval = ba.Lstr(resource='pressAnyButtonPlayAgainText')
self._show_up_next = False
self._custom_continue_message = sval
super().on_begin()
winning_sessionteam = self.settings_raw['winner']
# Pause a moment before playing victory music.
ba.timer(0.6, ba.WeakCall(self._play_victory_music))
ba.timer(
4.4, ba.WeakCall(self._show_winner, self.settings_raw['winner'])
)
ba.timer(4.6, ba.Call(ba.playsound, self._score_display_sound))
# Score / Name / Player-record.
player_entries: list[tuple[int, str, ba.PlayerRecord]] = []
# Note: for ffa, exclude players who haven't entered the game yet.
if self._is_ffa:
for _pkey, prec in self.stats.get_records().items():
if prec.player.in_game:
player_entries.append(
(
prec.player.sessionteam.customdata['score'],
prec.getname(full=True),
prec,
)
)
player_entries.sort(reverse=True, key=lambda x: x[0])
else:
for _pkey, prec in self.stats.get_records().items():
player_entries.append((prec.score, prec.name_full, prec))
player_entries.sort(reverse=True, key=lambda x: x[0])
ts_height = 300.0
ts_h_offs = -390.0
tval = 6.4
t_incr = 0.12
always_use_first_to = ba.app.lang.get_resource(
'bestOfUseFirstToInstead'
)
session = self.session
if self._is_ffa:
assert isinstance(session, ba.FreeForAllSession)
txt = ba.Lstr(
value='${A}:',
subs=[
(
'${A}',
ba.Lstr(
resource='firstToFinalText',
subs=[
(
'${COUNT}',
str(session.get_ffa_series_length()),
)
],
),
)
],
)
else:
assert isinstance(session, ba.MultiTeamSession)
# Some languages may prefer to always show 'first to X' instead of
# 'best of X'.
# FIXME: This will affect all clients connected to us even if
# they're not using this language. Should try to come up
# with a wording that works everywhere.
if always_use_first_to:
txt = ba.Lstr(
value='${A}:',
subs=[
(
'${A}',
ba.Lstr(
resource='firstToFinalText',
subs=[
(
'${COUNT}',
str(
session.get_series_length() / 2 + 1
),
)
],
),
)
],
)
else:
txt = ba.Lstr(
value='${A}:',
subs=[
(
'${A}',
ba.Lstr(
resource='bestOfFinalText',
subs=[
(
'${COUNT}',
str(session.get_series_length()),
)
],
),
)
],
)
Text(
txt,
v_align=Text.VAlign.CENTER,
maxwidth=300,
color=(0.5, 0.5, 0.5, 1.0),
position=(0, 220),
scale=1.2,
transition=Text.Transition.IN_TOP_SLOW,
h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4,
).autoretain()
win_score = (session.get_series_length() - 1) // 2 + 1
lose_score = 0
for team in self.teams:
if team.sessionteam.customdata['score'] != win_score:
lose_score = team.sessionteam.customdata['score']
if not self._is_ffa:
Text(
ba.Lstr(
resource='gamesToText',
subs=[
('${WINCOUNT}', str(win_score)),
('${LOSECOUNT}', str(lose_score)),
],
),
color=(0.5, 0.5, 0.5, 1.0),
maxwidth=160,
v_align=Text.VAlign.CENTER,
position=(0, -215),
scale=1.8,
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.CENTER,
transition_delay=4.8 + t_incr * 4,
).autoretain()
if self._is_ffa:
v_extra = 120
else:
v_extra = 0
mvp: ba.PlayerRecord | None = None
mvp_name: str | None = None
# Show game MVP.
if not self._is_ffa:
mvp, mvp_name = None, None
for entry in player_entries:
if entry[2].team == winning_sessionteam:
mvp = entry[2]
mvp_name = entry[1]
break
if mvp is not None:
Text(
ba.Lstr(resource='mostValuablePlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
Image(
mvp.get_icon(),
position=(230, ts_height / 2 - 55 + 14 - 5),
scale=(70, 70),
transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mvp_name is not None
Text(
ba.Lstr(value=mvp_name),
position=(280, ts_height / 2 - 55 + 15 - 5),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=170,
scale=1.3,
color=ba.safecolor(mvp.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
# Most violent.
most_kills = 0
for entry in player_entries:
if entry[2].kill_count >= most_kills:
mvp = entry[2]
mvp_name = entry[1]
most_kills = entry[2].kill_count
if mvp is not None:
Text(
ba.Lstr(resource='mostViolentPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 - 150 + v_extra + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
Text(
ba.Lstr(
value='(${A})',
subs=[
(
'${A}',
ba.Lstr(
resource='killsTallyText',
subs=[('${COUNT}', str(most_kills))],
),
)
],
),
position=(260, ts_height / 2 - 150 - 15 + v_extra),
color=(0.3, 0.3, 0.3, 1.0),
scale=0.6,
h_align=Text.HAlign.LEFT,
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
Image(
mvp.get_icon(),
position=(233, ts_height / 2 - 150 - 30 - 46 + 25 + v_extra),
scale=(50, 50),
transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mvp_name is not None
Text(
ba.Lstr(value=mvp_name),
position=(270, ts_height / 2 - 150 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
color=ba.safecolor(mvp.team.color + (1,)),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
# Most killed.
most_killed = 0
mkp, mkp_name = None, None
for entry in player_entries:
if entry[2].killed_count >= most_killed:
mkp = entry[2]
mkp_name = entry[1]
most_killed = entry[2].killed_count
if mkp is not None:
Text(
ba.Lstr(resource='mostViolatedPlayerText'),
color=(0.5, 0.5, 0.5, 1.0),
v_align=Text.VAlign.CENTER,
maxwidth=300,
position=(180, ts_height / 2 - 300 + v_extra + 15),
transition=Text.Transition.IN_LEFT,
h_align=Text.HAlign.LEFT,
transition_delay=tval,
).autoretain()
Text(
ba.Lstr(
value='(${A})',
subs=[
(
'${A}',
ba.Lstr(
resource='deathsTallyText',
subs=[('${COUNT}', str(most_killed))],
),
)
],
),
position=(260, ts_height / 2 - 300 - 15 + v_extra),
h_align=Text.HAlign.LEFT,
scale=0.6,
color=(0.3, 0.3, 0.3, 1.0),
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
Image(
mkp.get_icon(),
position=(233, ts_height / 2 - 300 - 30 - 46 + 25 + v_extra),
scale=(50, 50),
transition=Image.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
assert mkp_name is not None
Text(
ba.Lstr(value=mkp_name),
position=(270, ts_height / 2 - 300 - 30 - 36 + v_extra + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
color=ba.safecolor(mkp.team.color + (1,)),
maxwidth=180,
transition=Text.Transition.IN_LEFT,
transition_delay=tval,
).autoretain()
tval += 4 * t_incr
# Now show individual scores.
tdelay = tval
Text(
ba.Lstr(resource='finalScoresText'),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs, ts_height / 2),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
tdelay += 4 * t_incr
v_offs = 0.0
tdelay += len(player_entries) * 8 * t_incr
for _score, name, prec in player_entries:
tdelay -= 4 * t_incr
v_offs -= 40
Text(
str(prec.team.customdata['score'])
if self._is_ffa
else str(prec.score),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs + 230, ts_height / 2 + v_offs),
h_align=Text.HAlign.RIGHT,
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
tdelay -= 4 * t_incr
Image(
prec.get_icon(),
position=(ts_h_offs - 72, ts_height / 2 + v_offs + 15),
scale=(30, 30),
transition=Image.Transition.IN_LEFT,
transition_delay=tdelay,
).autoretain()
Text(
ba.Lstr(value=name),
position=(ts_h_offs - 50, ts_height / 2 + v_offs + 15),
h_align=Text.HAlign.LEFT,
v_align=Text.VAlign.CENTER,
maxwidth=180,
color=ba.safecolor(prec.team.color + (1,)),
transition=Text.Transition.IN_RIGHT,
transition_delay=tdelay,
).autoretain()
ba.timer(15.0, ba.WeakCall(self._show_tips))
def _show_tips(self) -> None:
from bastd.actor.tipstext import TipsText
self._tips_text = TipsText(offs_y=70)
def _play_victory_music(self) -> None:
# Make sure we don't stomp on the next activity's music choice.
if not self.is_transitioning_out():
ba.setmusic(ba.MusicType.VICTORY)
def _show_winner(self, team: ba.SessionTeam) -> None:
from bastd.actor.image import Image
from bastd.actor.zoomtext import ZoomText
if not self._is_ffa:
offs_v = 0.0
ZoomText(
team.name,
position=(0, 97),
color=team.color,
scale=1.15,
jitter=1.0,
maxwidth=250,
).autoretain()
else:
offs_v = -80.0
if len(team.players) == 1:
i = Image(
team.players[0].get_icon(),
position=(0, 143),
scale=(100, 100),
).autoretain()
assert i.node
ba.animate(i.node, 'opacity', {0.0: 0.0, 0.25: 1.0})
ZoomText(
ba.Lstr(
value=team.players[0].getname(full=True, icon=False)
),
position=(0, 97 + offs_v),
color=team.color,
scale=1.15,
jitter=1.0,
maxwidth=250,
).autoretain()
s_extra = 1.0 if self._is_ffa else 1.0
# Some languages say "FOO WINS" differently for teams vs players.
if isinstance(self.session, ba.FreeForAllSession):
wins_resource = 'seriesWinLine1PlayerText'
else:
wins_resource = 'seriesWinLine1TeamText'
wins_text = ba.Lstr(resource=wins_resource)
# Temp - if these come up as the english default, fall-back to the
# unified old form which is more likely to be translated.
ZoomText(
wins_text,
position=(0, -10 + offs_v),
color=team.color,
scale=0.65 * s_extra,
jitter=1.0,
maxwidth=250,
).autoretain()
ZoomText(
ba.Lstr(resource='seriesWinLine2Text'),
position=(0, -110 + offs_v),
scale=1.0 * s_extra,
color=team.color,
jitter=1.0,
maxwidth=250,
).autoretain()

View file

@ -0,0 +1 @@
# Released under the MIT License. See LICENSE for details.

View file

@ -0,0 +1,145 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
import random
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any
class Background(ba.Actor):
"""Simple Fading Background Actor."""
def __init__(
self,
fade_time: float = 0.5,
start_faded: bool = False,
show_logo: bool = False,
):
super().__init__()
self._dying = False
self.fade_time = fade_time
# We're special in that we create our node in the session
# scene instead of the activity scene.
# This way we can overlap multiple activities for fades
# and whatnot.
session = ba.getsession()
self._session = weakref.ref(session)
with ba.Context(session):
self.node = ba.newnode(
'image',
delegate=self,
attrs={
'fill_screen': True,
'texture': ba.gettexture('bg'),
'tilt_translate': -0.3,
'has_alpha_channel': False,
'color': (1, 1, 1),
},
)
if not start_faded:
ba.animate(
self.node,
'opacity',
{0.0: 0.0, self.fade_time: 1.0},
loop=False,
)
if show_logo:
logo_texture = ba.gettexture('logo')
logo_model = ba.getmodel('logo')
logo_model_transparent = ba.getmodel('logoTransparent')
self.logo = ba.newnode(
'image',
owner=self.node,
attrs={
'texture': logo_texture,
'model_opaque': logo_model,
'model_transparent': logo_model_transparent,
'scale': (0.7, 0.7),
'vr_depth': -250,
'color': (0.15, 0.15, 0.15),
'position': (0, 0),
'tilt_translate': -0.05,
'absolute_scale': False,
},
)
self.node.connectattr('opacity', self.logo, 'opacity')
# add jitter/pulse for a stop-motion-y look unless we're in VR
# in which case stillness is better
if not ba.app.vr_mode:
self.cmb = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
for attr in ['input0', 'input1']:
ba.animate(
self.cmb,
attr,
{0.0: 0.693, 0.05: 0.7, 0.5: 0.693},
loop=True,
)
self.cmb.connectattr('output', self.logo, 'scale')
cmb = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
cmb.connectattr('output', self.logo, 'position')
# Gen some random keys for that stop-motion-y look.
keys = {}
timeval = 0.0
for _i in range(10):
keys[timeval] = (random.random() - 0.5) * 0.0015
timeval += random.random() * 0.1
ba.animate(cmb, 'input0', keys, loop=True)
keys = {}
timeval = 0.0
for _i in range(10):
keys[timeval] = (random.random() - 0.5) * 0.0015 + 0.05
timeval += random.random() * 0.1
ba.animate(cmb, 'input1', keys, loop=True)
def __del__(self) -> None:
# Normal actors don't get sent DieMessages when their
# activity is shutting down, but we still need to do so
# since our node lives in the session and it wouldn't die
# otherwise.
self._die()
super().__del__()
def _die(self, immediate: bool = False) -> None:
session = self._session()
if session is None and self.node:
# If session is gone, our node should be too,
# since it was part of the session's scene.
# Let's make sure that's the case.
# (since otherwise we have no way to kill it)
ba.print_error(
'got None session on Background _die'
' (and node still exists!)'
)
elif session is not None:
with ba.Context(session):
if not self._dying and self.node:
self._dying = True
if immediate:
self.node.delete()
else:
ba.animate(
self.node,
'opacity',
{0.0: 1.0, self.fade_time: 0.0},
loop=False,
)
ba.timer(self.fade_time + 0.1, self.node.delete)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
self._die(msg.immediate)
else:
super().handlemessage(msg)

1212
dist/ba_data/python/bastd/actor/bomb.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,639 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actors related to controls guides."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any, Sequence
class ControlsGuide(ba.Actor):
"""A screen overlay of game controls.
category: Gameplay Classes
Shows button mappings based on what controllers are connected.
Handy to show at the start of a series or whenever there might
be newbies watching.
"""
def __init__(
self,
position: tuple[float, float] = (390.0, 120.0),
scale: float = 1.0,
delay: float = 0.0,
lifespan: float | None = None,
bright: bool = False,
):
"""Instantiate an overlay.
delay: is the time in seconds before the overlay fades in.
lifespan: if not None, the overlay will fade back out and die after
that long (in milliseconds).
bright: if True, brighter colors will be used; handy when showing
over gameplay but may be too bright for join-screens, etc.
"""
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
super().__init__()
show_title = True
scale *= 0.75
image_size = 90.0 * scale
offs = 74.0 * scale
offs5 = 43.0 * scale
ouya = False
maxw = 50
self._lifespan = lifespan
self._dead = False
self._bright = bright
self._cancel_timer: ba.Timer | None = None
self._fade_in_timer: ba.Timer | None = None
self._update_timer: ba.Timer | None = None
self._title_text: ba.Node | None
clr: Sequence[float]
extra_pos_1: tuple[float, float] | None
extra_pos_2: tuple[float, float] | None
if ba.app.iircade_mode:
xtweak = 0.2
ytweak = 0.2
jump_pos = (
position[0] + offs * (-1.2 + xtweak),
position[1] + offs * (0.1 + ytweak),
)
bomb_pos = (
position[0] + offs * (0.0 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
punch_pos = (
position[0] + offs * (1.2 + xtweak),
position[1] + offs * (0.5 + ytweak),
)
pickup_pos = (
position[0] + offs * (-1.4 + xtweak),
position[1] + offs * (-1.2 + ytweak),
)
extra_pos_1 = (
position[0] + offs * (-0.2 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
extra_pos_2 = (
position[0] + offs * (1.0 + xtweak),
position[1] + offs * (-0.8 + ytweak),
)
self._force_hide_button_names = True
else:
punch_pos = (position[0] - offs * 1.1, position[1])
jump_pos = (position[0], position[1] - offs)
bomb_pos = (position[0] + offs * 1.1, position[1])
pickup_pos = (position[0], position[1] + offs)
extra_pos_1 = None
extra_pos_2 = None
self._force_hide_button_names = False
if show_title:
self._title_text_pos_top = (
position[0],
position[1] + 139.0 * scale,
)
self._title_text_pos_bottom = (
position[0],
position[1] + 139.0 * scale,
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
tval = ba.Lstr(
value='${A}:', subs=[('${A}', ba.Lstr(resource='controlsText'))]
)
self._title_text = ba.newnode(
'text',
attrs={
'text': tval,
'host_only': True,
'scale': 1.1 * scale,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 480,
'v_align': 'center',
'h_align': 'center',
'color': clr,
},
)
else:
self._title_text = None
pos = jump_pos
clr = (0.4, 1, 0.4)
self._jump_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonJump'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._jump_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.2, 0.6, 1) if ouya else (1, 0.7, 0.3)
pos = punch_pos
self._punch_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonPunch'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._punch_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = bomb_pos
clr = (1, 0.3, 0.3)
self._bomb_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonBomb'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._bomb_text = ba.newnode(
'text',
attrs={
'h_align': 'center',
'v_align': 'top',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
pos = pickup_pos
clr = (1, 0.8, 0.3) if ouya else (0.8, 0.5, 1)
self._pickup_image = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('buttonPickUp'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': pos,
'scale': (image_size, image_size),
'color': clr,
},
)
self._pick_up_text = ba.newnode(
'text',
attrs={
'v_align': 'top',
'h_align': 'center',
'scale': 1.5 * scale,
'flatness': 1.0,
'host_only': True,
'shadow': 1.0,
'maxwidth': maxw,
'position': (pos[0], pos[1] - offs5),
'color': clr,
},
)
clr = (0.9, 0.9, 2.0, 1.0) if bright else (0.8, 0.8, 2.0, 1.0)
self._run_text_pos_top = (position[0], position[1] - 135.0 * scale)
self._run_text_pos_bottom = (position[0], position[1] - 172.0 * scale)
sval = 1.0 * scale if ba.app.vr_mode else 0.8 * scale
self._run_text = ba.newnode(
'text',
attrs={
'scale': sval,
'host_only': True,
'shadow': 1.0 if ba.app.vr_mode else 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr,
},
)
clr = (1, 1, 1) if bright else (0.7, 0.7, 0.7)
self._extra_text = ba.newnode(
'text',
attrs={
'scale': 0.8 * scale,
'host_only': True,
'shadow': 0.5,
'flatness': 1.0,
'maxwidth': 380,
'v_align': 'top',
'h_align': 'center',
'color': clr,
},
)
if extra_pos_1 is not None:
self._extra_image_1: ba.Node | None = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('nub'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': extra_pos_1,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_1 = None
if extra_pos_2 is not None:
self._extra_image_2: ba.Node | None = ba.newnode(
'image',
attrs={
'texture': ba.gettexture('nub'),
'absolute_scale': True,
'host_only': True,
'vr_depth': 10,
'position': extra_pos_2,
'scale': (image_size, image_size),
'color': (0.5, 0.5, 0.5),
},
)
else:
self._extra_image_2 = None
self._nodes = [
self._bomb_image,
self._bomb_text,
self._punch_image,
self._punch_text,
self._jump_image,
self._jump_text,
self._pickup_image,
self._pick_up_text,
self._run_text,
self._extra_text,
]
if show_title:
assert self._title_text
self._nodes.append(self._title_text)
if self._extra_image_1 is not None:
self._nodes.append(self._extra_image_1)
if self._extra_image_2 is not None:
self._nodes.append(self._extra_image_2)
# Start everything invisible.
for node in self._nodes:
node.opacity = 0.0
# Don't do anything until our delay has passed.
ba.timer(delay, ba.WeakCall(self._start_updating))
@staticmethod
def _meaningful_button_name(device: ba.InputDevice, button: int) -> str:
"""Return a flattened string button name; empty for non-meaningful."""
if not device.has_meaningful_button_names:
return ''
return device.get_button_name(button).evaluate()
def _start_updating(self) -> None:
# Ok, our delay has passed. Now lets periodically see if we can fade
# in (if a touch-screen is present we only want to show up if gamepads
# are connected, etc).
# Also set up a timer so if we haven't faded in by the end of our
# duration, abort.
if self._lifespan is not None:
self._cancel_timer = ba.Timer(
self._lifespan,
ba.WeakCall(self.handlemessage, ba.DieMessage(immediate=True)),
)
self._fade_in_timer = ba.Timer(
1.0, ba.WeakCall(self._check_fade_in), repeat=True
)
self._check_fade_in() # Do one check immediately.
def _check_fade_in(self) -> None:
from ba.internal import get_device_value
# If we have a touchscreen, we only fade in if we have a player with
# an input device that is *not* the touchscreen.
# (otherwise it is confusing to see the touchscreen buttons right
# next to our display buttons)
touchscreen: ba.InputDevice | None = ba.internal.getinputdevice(
'TouchScreen', '#1', doraise=False
)
if touchscreen is not None:
# We look at the session's players; not the activity's.
# We want to get ones who are still in the process of
# selecting a character, etc.
input_devices = [
p.inputdevice for p in ba.getsession().sessionplayers
]
input_devices = [
i for i in input_devices if i and i is not touchscreen
]
fade_in = False
if input_devices:
# Only count this one if it has non-empty button names
# (filters out wiimotes, the remote-app, etc).
for device in input_devices:
for name in (
'buttonPunch',
'buttonJump',
'buttonBomb',
'buttonPickUp',
):
if (
self._meaningful_button_name(
device, get_device_value(device, name)
)
!= ''
):
fade_in = True
break
if fade_in:
break # No need to keep looking.
else:
# No touch-screen; fade in immediately.
fade_in = True
if fade_in:
self._cancel_timer = None # Didn't need this.
self._fade_in_timer = None # Done with this.
self._fade_in()
def _fade_in(self) -> None:
for node in self._nodes:
ba.animate(node, 'opacity', {0: 0.0, 2.0: 1.0})
# If we were given a lifespan, transition out after it.
if self._lifespan is not None:
ba.timer(
self._lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
self._update()
self._update_timer = ba.Timer(
1.0, ba.WeakCall(self._update), repeat=True
)
def _update(self) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
from ba.internal import get_device_value, get_remote_app_name
if self._dead:
return
punch_button_names = set()
jump_button_names = set()
pickup_button_names = set()
bomb_button_names = set()
# We look at the session's players; not the activity's - we want to
# get ones who are still in the process of selecting a character, etc.
input_devices = [p.inputdevice for p in ba.getsession().sessionplayers]
input_devices = [i for i in input_devices if i]
# If there's no players with input devices yet, try to default to
# showing keyboard controls.
if not input_devices:
kbd = ba.internal.getinputdevice('Keyboard', '#1', doraise=False)
if kbd is not None:
input_devices.append(kbd)
# We word things specially if we have nothing but keyboards.
all_keyboards = input_devices and all(
i.name == 'Keyboard' for i in input_devices
)
only_remote = len(input_devices) == 1 and all(
i.name == 'Amazon Fire TV Remote' for i in input_devices
)
right_button_names = set()
left_button_names = set()
up_button_names = set()
down_button_names = set()
# For each player in the game with an input device,
# get the name of the button for each of these 4 actions.
# If any of them are uniform across all devices, display the name.
for device in input_devices:
# We only care about movement buttons in the case of keyboards.
if all_keyboards:
right_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonRight')
)
)
left_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonLeft')
)
)
down_button_names.add(
device.get_button_name(
get_device_value(device, 'buttonDown')
)
)
up_button_names.add(
device.get_button_name(get_device_value(device, 'buttonUp'))
)
# Ignore empty values; things like the remote app or
# wiimotes can return these.
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPunch')
)
if bname != '':
punch_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonJump')
)
if bname != '':
jump_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonBomb')
)
if bname != '':
bomb_button_names.add(bname)
bname = self._meaningful_button_name(
device, get_device_value(device, 'buttonPickUp')
)
if bname != '':
pickup_button_names.add(bname)
# If we have no values yet, we may want to throw out some sane
# defaults.
if all(
not lst
for lst in (
punch_button_names,
jump_button_names,
bomb_button_names,
pickup_button_names,
)
):
# Otherwise on android show standard buttons.
if ba.app.platform == 'android':
punch_button_names.add('X')
jump_button_names.add('A')
bomb_button_names.add('B')
pickup_button_names.add('Y')
run_text = ba.Lstr(
value='${R}: ${B}',
subs=[
('${R}', ba.Lstr(resource='runText')),
(
'${B}',
ba.Lstr(
resource='holdAnyKeyText'
if all_keyboards
else 'holdAnyButtonText'
),
),
],
)
# If we're all keyboards, lets show move keys too.
if (
all_keyboards
and len(up_button_names) == 1
and len(down_button_names) == 1
and len(left_button_names) == 1
and len(right_button_names) == 1
):
up_text = list(up_button_names)[0]
down_text = list(down_button_names)[0]
left_text = list(left_button_names)[0]
right_text = list(right_button_names)[0]
run_text = ba.Lstr(
value='${M}: ${U}, ${L}, ${D}, ${R}\n${RUN}',
subs=[
('${M}', ba.Lstr(resource='moveText')),
('${U}', up_text),
('${L}', left_text),
('${D}', down_text),
('${R}', right_text),
('${RUN}', run_text),
],
)
if self._force_hide_button_names:
jump_button_names.clear()
punch_button_names.clear()
bomb_button_names.clear()
pickup_button_names.clear()
self._run_text.text = run_text
w_text: ba.Lstr | str
if only_remote and self._lifespan is None:
w_text = ba.Lstr(
resource='fireTVRemoteWarningText',
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
else:
w_text = ''
self._extra_text.text = w_text
if len(punch_button_names) == 1:
self._punch_text.text = list(punch_button_names)[0]
else:
self._punch_text.text = ''
if len(jump_button_names) == 1:
tval = list(jump_button_names)[0]
else:
tval = ''
self._jump_text.text = tval
if tval == '':
self._run_text.position = self._run_text_pos_top
self._extra_text.position = (
self._run_text_pos_top[0],
self._run_text_pos_top[1] - 50,
)
else:
self._run_text.position = self._run_text_pos_bottom
self._extra_text.position = (
self._run_text_pos_bottom[0],
self._run_text_pos_bottom[1] - 50,
)
if len(bomb_button_names) == 1:
self._bomb_text.text = list(bomb_button_names)[0]
else:
self._bomb_text.text = ''
# Also move our title up/down depending on if this is shown.
if len(pickup_button_names) == 1:
self._pick_up_text.text = list(pickup_button_names)[0]
if self._title_text is not None:
self._title_text.position = self._title_text_pos_top
else:
self._pick_up_text.text = ''
if self._title_text is not None:
self._title_text.position = self._title_text_pos_bottom
def _die(self) -> None:
for node in self._nodes:
node.delete()
self._nodes = []
self._update_timer = None
self._dead = True
def exists(self) -> bool:
return not self._dead
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if msg.immediate:
self._die()
else:
# If they don't need immediate,
# fade out our nodes and die later.
for node in self._nodes:
ba.animate(node, 'opacity', {0: node.opacity, 3.0: 0.0})
ba.timer(3.1, ba.WeakCall(self._die))
return None
return super().handlemessage(msg)

380
dist/ba_data/python/bastd/actor/flag.py vendored Normal file
View file

@ -0,0 +1,380 @@
# Released under the MIT License. See LICENSE for details.
#
"""Implements a flag used for marking bases, capture-the-flag games, etc."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
import ba
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class FlagFactory:
"""Wraps up media and other resources used by `Flag`s.
Category: **Gameplay Classes**
A single instance of this is shared between all flags
and can be retrieved via FlagFactory.get().
"""
flagmaterial: ba.Material
"""The ba.Material applied to all `Flag`s."""
impact_sound: ba.Sound
"""The ba.Sound used when a `Flag` hits the ground."""
skid_sound: ba.Sound
"""The ba.Sound used when a `Flag` skids along the ground."""
no_hit_material: ba.Material
"""A ba.Material that prevents contact with most objects;
applied to 'non-touchable' flags."""
flag_texture: ba.Texture
"""The ba.Texture for flags."""
_STORENAME = ba.storagename()
def __init__(self) -> None:
"""Instantiate a `FlagFactory`.
You shouldn't need to do this; call FlagFactory.get() to
get a shared instance.
"""
shared = SharedObjects.get()
self.flagmaterial = ba.Material()
self.flagmaterial.add_actions(
conditions=(
('we_are_younger_than', 100),
'and',
('they_have_material', shared.object_material),
),
actions=('modify_node_collision', 'collide', False),
)
self.flagmaterial.add_actions(
conditions=(
'they_have_material',
shared.footing_material,
),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
self.impact_sound = ba.getsound('metalHit')
self.skid_sound = ba.getsound('metalSkid')
self.flagmaterial.add_actions(
conditions=(
'they_have_material',
shared.footing_material,
),
actions=(
('impact_sound', self.impact_sound, 2, 5),
('skid_sound', self.skid_sound, 2, 5),
),
)
self.no_hit_material = ba.Material()
self.no_hit_material.add_actions(
conditions=(
('they_have_material', shared.pickup_material),
'or',
('they_have_material', shared.attack_material),
),
actions=('modify_part_collision', 'collide', False),
)
# We also don't want anything moving it.
self.no_hit_material.add_actions(
conditions=(
('they_have_material', shared.object_material),
'or',
('they_dont_have_material', shared.footing_material),
),
actions=(
('modify_part_collision', 'collide', False),
('modify_part_collision', 'physical', False),
),
)
self.flag_texture = ba.gettexture("pixieIcon")
@classmethod
def get(cls) -> FlagFactory:
"""Get/create a shared `FlagFactory` instance."""
activity = ba.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = FlagFactory()
activity.customdata[cls._STORENAME] = factory
assert isinstance(factory, FlagFactory)
return factory
@dataclass
class FlagPickedUpMessage:
"""A message saying a `Flag` has been picked up.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that has been picked up."""
node: ba.Node
"""The ba.Node doing the picking up."""
@dataclass
class FlagDiedMessage:
"""A message saying a `Flag` has died.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that died."""
@dataclass
class FlagDroppedMessage:
"""A message saying a `Flag` has been dropped.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that was dropped."""
node: ba.Node
"""The ba.Node that was holding it."""
class Flag(ba.Actor):
"""A flag; used in games such as capture-the-flag or king-of-the-hill.
Category: **Gameplay Classes**
Can be stationary or carry-able by players.
"""
def __init__(
self,
position: Sequence[float] = (0.0, 1.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0),
materials: Sequence[ba.Material] | None = None,
touchable: bool = True,
dropped_timeout: int | None = None,
):
"""Instantiate a flag.
If 'touchable' is False, the flag will only touch terrain;
useful for things like king-of-the-hill where players should
not be moving the flag around.
'materials can be a list of extra `ba.Material`s to apply to the flag.
If 'dropped_timeout' is provided (in seconds), the flag will die
after remaining untouched for that long once it has been moved
from its initial position.
"""
super().__init__()
self._initial_position: Sequence[float] | None = None
self._has_moved = False
shared = SharedObjects.get()
factory = FlagFactory.get()
if materials is None:
materials = []
elif not isinstance(materials, list):
# In case they passed a tuple or whatnot.
materials = list(materials)
if not touchable:
materials = [factory.no_hit_material] + materials
finalmaterials = [
shared.object_material,
factory.flagmaterial,
] + materials
self.node = ba.newnode(
'flag',
attrs={
'position': (position[0], position[1] + 0.75, position[2]),
'color_texture': factory.flag_texture,
'color': color,
'materials': finalmaterials,
},
delegate=self,
)
if dropped_timeout is not None:
dropped_timeout = int(dropped_timeout)
self._dropped_timeout = dropped_timeout
self._counter: ba.Node | None
if self._dropped_timeout is not None:
self._count = self._dropped_timeout
self._tick_timer = ba.Timer(
1.0, call=ba.WeakCall(self._tick), repeat=True
)
self._counter = ba.newnode(
'text',
owner=self.node,
attrs={
'in_world': True,
'color': (1, 1, 1, 0.7),
'scale': 0.015,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center',
},
)
else:
self._counter = None
self._held_count = 0
self._score_text: ba.Node | None = None
self._score_text_hide_timer: ba.Timer | None = None
def _tick(self) -> None:
if self.node:
# Grab our initial position after one tick (in case we fall).
if self._initial_position is None:
self._initial_position = self.node.position
# Keep track of when we first move; we don't count down
# until then.
if not self._has_moved:
nodepos = self.node.position
if (
max(
abs(nodepos[i] - self._initial_position[i])
for i in list(range(3))
)
> 1.0
):
self._has_moved = True
if self._held_count > 0 or not self._has_moved:
assert self._dropped_timeout is not None
assert self._counter
self._count = self._dropped_timeout
self._counter.text = ''
else:
self._count -= 1
if self._count <= 10:
nodepos = self.node.position
assert self._counter
self._counter.position = (
nodepos[0],
nodepos[1] + 1.3,
nodepos[2],
)
self._counter.text = str(self._count)
if self._count < 1:
self.handlemessage(ba.DieMessage())
else:
assert self._counter
self._counter.text = ''
def _hide_score_text(self) -> None:
assert self._score_text is not None
assert isinstance(self._score_text.scale, float)
ba.animate(
self._score_text, 'scale', {0: self._score_text.scale, 0.2: 0}
)
def set_score_text(self, text: str) -> None:
"""Show a message over the flag; handy for scores."""
if not self.node:
return
if not self._score_text:
start_scale = 0.0
math = ba.newnode(
'math',
owner=self.node,
attrs={'input1': (0, 1.4, 0), 'operation': 'add'},
)
self.node.connectattr('position', math, 'input2')
self._score_text = ba.newnode(
'text',
owner=self.node,
attrs={
'text': text,
'in_world': True,
'scale': 0.02,
'shadow': 0.5,
'flatness': 1.0,
'h_align': 'center',
},
)
math.connectattr('output', self._score_text, 'position')
else:
assert isinstance(self._score_text.scale, float)
start_scale = self._score_text.scale
self._score_text.text = text
self._score_text.color = ba.safecolor(self.node.color)
ba.animate(self._score_text, 'scale', {0: start_scale, 0.2: 0.02})
self._score_text_hide_timer = ba.Timer(
1.0, ba.WeakCall(self._hide_score_text)
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
if not msg.immediate:
self.activity.handlemessage(FlagDiedMessage(self))
elif isinstance(msg, ba.HitMessage):
assert self.node
assert msg.force_direction is not None
self.node.handlemessage(
'impulse',
msg.pos[0],
msg.pos[1],
msg.pos[2],
msg.velocity[0],
msg.velocity[1],
msg.velocity[2],
msg.magnitude,
msg.velocity_magnitude,
msg.radius,
0,
msg.force_direction[0],
msg.force_direction[1],
msg.force_direction[2],
)
elif isinstance(msg, ba.PickedUpMessage):
self._held_count += 1
if self._held_count == 1 and self._counter is not None:
self._counter.text = ''
self.activity.handlemessage(FlagPickedUpMessage(self, msg.node))
elif isinstance(msg, ba.DroppedMessage):
self._held_count -= 1
if self._held_count < 0:
print('Flag held count < 0.')
self._held_count = 0
self.activity.handlemessage(FlagDroppedMessage(self, msg.node))
else:
super().handlemessage(msg)
@staticmethod
def project_stand(pos: Sequence[float]) -> None:
"""Project a flag-stand onto the ground at the given position.
Useful for games such as capture-the-flag to show where a
movable flag originated from.
"""
assert len(pos) == 3
ba.emitfx(position=pos, emit_type='flag_stand')

174
dist/ba_data/python/bastd/actor/image.py vendored Normal file
View file

@ -0,0 +1,174 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class Image(ba.Actor):
"""Just a wrapped up image node with a few tricks up its sleeve."""
class Transition(Enum):
"""Transition types we support."""
FADE_IN = 'fade_in'
IN_RIGHT = 'in_right'
IN_LEFT = 'in_left'
IN_BOTTOM = 'in_bottom'
IN_BOTTOM_SLOW = 'in_bottom_slow'
IN_TOP_SLOW = 'in_top_slow'
class Attach(Enum):
"""Attach types we support."""
CENTER = 'center'
TOP_CENTER = 'topCenter'
TOP_LEFT = 'topLeft'
BOTTOM_CENTER = 'bottomCenter'
def __init__(
self,
texture: ba.Texture | dict[str, Any],
position: tuple[float, float] = (0, 0),
transition: Transition | None = None,
transition_delay: float = 0.0,
attach: Attach = Attach.CENTER,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
scale: tuple[float, float] = (100.0, 100.0),
transition_out_delay: float | None = None,
model_opaque: ba.Model | None = None,
model_transparent: ba.Model | None = None,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
super().__init__()
# If they provided a dict as texture, assume its an icon.
# otherwise its just a texture value itself.
mask_texture: ba.Texture | None
if isinstance(texture, dict):
tint_color = texture['tint_color']
tint2_color = texture['tint2_color']
tint_texture = texture['tint_texture']
texture = texture['texture']
mask_texture = ba.gettexture('characterIconMask')
else:
tint_color = (1, 1, 1)
tint2_color = None
tint_texture = None
mask_texture = None
self.node = ba.newnode(
'image',
attrs={
'texture': texture,
'tint_color': tint_color,
'tint_texture': tint_texture,
'position': position,
'vr_depth': vr_depth,
'scale': scale,
'mask_texture': mask_texture,
'color': color,
'absolute_scale': True,
'host_only': host_only,
'front': front,
'attach': attach.value,
},
delegate=self,
)
if model_opaque is not None:
self.node.model_opaque = model_opaque
if model_transparent is not None:
self.node.model_transparent = model_transparent
if tint2_color is not None:
self.node.tint2_color = tint2_color
if transition is self.Transition.FADE_IN:
keys = {transition_delay: 0, transition_delay + 0.5: color[3]}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = color[3]
keys[transition_delay + transition_out_delay + 0.5] = 0
ba.animate(self.node, 'opacity', keys)
cmb = self.position_combine = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
if transition is self.Transition.IN_RIGHT:
keys = {
transition_delay: position[0] + 1200,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_LEFT:
keys = {
transition_delay: position[0] - 1200,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[0]
keys[transition_delay + transition_out_delay + 200] = (
-position[0] - 1200
)
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM_SLOW:
keys = {transition_delay: -400, transition_delay + 3.5: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 2.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM:
keys = {transition_delay: -400, transition_delay + 0.2: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[1]
keys[transition_delay + transition_out_delay + 0.2] = -400
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_TOP_SLOW:
keys = {transition_delay: 400, transition_delay + 3.5: position[1]}
o_keys = {transition_delay: 0.0, transition_delay + 1.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
else:
assert transition is self.Transition.FADE_IN or transition is None
cmb.input0 = position[0]
cmb.input1 = position[1]
cmb.connectattr('output', self.node, 'position')
# If we're transitioning out, die at the end of it.
if transition_out_delay is not None:
ba.timer(
transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
return None
return super().handlemessage(msg)

View file

@ -0,0 +1,107 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor Type(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Callable
class OnScreenCountdown(ba.Actor):
"""A Handy On-Screen Timer.
category: Gameplay Classes
Useful for time-based games that count down to zero.
"""
def __init__(self, duration: int, endcall: Callable[[], Any] | None = None):
"""Duration is provided in seconds."""
super().__init__()
self._timeremaining = duration
self._ended = False
self._endcall = endcall
self.node = ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': '',
},
)
self.inputnode = ba.newnode(
'timedisplay',
attrs={
'time2': duration * 1000,
'timemax': duration * 1000,
'timemin': 0,
},
)
self.inputnode.connectattr('output', self.node, 'text')
self._countdownsounds = {
10: ba.getsound('announceTen'),
9: ba.getsound('announceNine'),
8: ba.getsound('announceEight'),
7: ba.getsound('announceSeven'),
6: ba.getsound('announceSix'),
5: ba.getsound('announceFive'),
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne'),
}
self._timer: ba.Timer | None = None
def start(self) -> None:
"""Start the timer."""
globalsnode = ba.getactivity().globalsnode
globalsnode.connectattr('time', self.inputnode, 'time1')
self.inputnode.time2 = (
globalsnode.time + (self._timeremaining + 1) * 1000
)
self._timer = ba.Timer(1.0, self._update, repeat=True)
def on_expire(self) -> None:
super().on_expire()
# Release callbacks/refs.
self._endcall = None
def _update(self, forcevalue: int | None = None) -> None:
if forcevalue is not None:
tval = forcevalue
else:
self._timeremaining = max(0, self._timeremaining - 1)
tval = self._timeremaining
# if there's a countdown sound for this time that we
# haven't played yet, play it
if tval == 10:
assert self.node
assert isinstance(self.node.scale, float)
self.node.scale *= 1.2
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4})
cmb.connectattr('output', self.node, 'color')
ba.animate(cmb, 'input0', {0: 1.0, 0.15: 1.0}, loop=True)
ba.animate(cmb, 'input1', {0: 1.0, 0.15: 0.5}, loop=True)
ba.animate(cmb, 'input2', {0: 0.1, 0.15: 0.0}, loop=True)
cmb.input3 = 1.0
if tval <= 10 and not self._ended:
ba.playsound(ba.getsound('tick'))
if tval in self._countdownsounds:
ba.playsound(self._countdownsounds[tval])
if tval <= 0 and not self._ended:
self._ended = True
if self._endcall is not None:
self._endcall()

View file

@ -0,0 +1,131 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING, overload
import ba
if TYPE_CHECKING:
from typing import Any, Literal
class OnScreenTimer(ba.Actor):
"""A handy on-screen timer.
category: Gameplay Classes
Useful for time-based games where time increases.
"""
def __init__(self) -> None:
super().__init__()
self._starttime_ms: int | None = None
self.node = ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -70),
'scale': 1.4,
'text': '',
},
)
self.inputnode = ba.newnode(
'timedisplay', attrs={'timemin': 0, 'showsubseconds': True}
)
self.inputnode.connectattr('output', self.node, 'text')
def start(self) -> None:
"""Start the timer."""
tval = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(tval, int)
self._starttime_ms = tval
self.inputnode.time1 = self._starttime_ms
ba.getactivity().globalsnode.connectattr(
'time', self.inputnode, 'time2'
)
def has_started(self) -> bool:
"""Return whether this timer has started yet."""
return self._starttime_ms is not None
def stop(
self,
endtime: int | float | None = None,
timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS,
) -> None:
"""End the timer.
If 'endtime' is not None, it is used when calculating
the final display time; otherwise the current time is used.
'timeformat' applies to endtime and can be SECONDS or MILLISECONDS
"""
if endtime is None:
endtime = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
timeformat = ba.TimeFormat.MILLISECONDS
if self._starttime_ms is None:
print('Warning: OnScreenTimer.stop() called without start() first')
else:
endtime_ms: int
if timeformat is ba.TimeFormat.SECONDS:
endtime_ms = int(endtime * 1000)
elif timeformat is ba.TimeFormat.MILLISECONDS:
assert isinstance(endtime, int)
endtime_ms = endtime
else:
raise ValueError(f'invalid timeformat: {timeformat}')
self.inputnode.timemax = endtime_ms - self._starttime_ms
# Overloads so type checker knows our exact return type based in args.
@overload
def getstarttime(
self, timeformat: Literal[ba.TimeFormat.SECONDS] = ba.TimeFormat.SECONDS
) -> float:
...
@overload
def getstarttime(
self, timeformat: Literal[ba.TimeFormat.MILLISECONDS]
) -> int:
...
def getstarttime(
self, timeformat: ba.TimeFormat = ba.TimeFormat.SECONDS
) -> int | float:
"""Return the sim-time when start() was called.
Time will be returned in seconds if timeformat is SECONDS or
milliseconds if it is MILLISECONDS.
"""
val_ms: Any
if self._starttime_ms is None:
print('WARNING: getstarttime() called on un-started timer')
val_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
else:
val_ms = self._starttime_ms
assert isinstance(val_ms, int)
if timeformat is ba.TimeFormat.SECONDS:
return 0.001 * val_ms
if timeformat is ba.TimeFormat.MILLISECONDS:
return val_ms
raise ValueError(f'invalid timeformat: {timeformat}')
@property
def starttime(self) -> float:
"""Shortcut for start time in seconds."""
return self.getstarttime()
def handlemessage(self, msg: Any) -> Any:
# if we're asked to die, just kill our node/timer
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()

View file

@ -0,0 +1,309 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to player-controlled Spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar, overload
import ba
from bastd.actor.spaz import Spaz
from spazmod import modifyspaz
if TYPE_CHECKING:
from typing import Any, Sequence, Literal
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound=ba.Player)
TeamType = TypeVar('TeamType', bound=ba.Team)
# pylint: enable=invalid-name
class PlayerSpazHurtMessage:
"""A message saying a PlayerSpaz was hurt.
Category: **Message Classes**
"""
spaz: PlayerSpaz
"""The PlayerSpaz that was hurt"""
def __init__(self, spaz: PlayerSpaz):
"""Instantiate with the given ba.Spaz value."""
self.spaz = spaz
class PlayerSpaz(Spaz):
"""A Spaz subclass meant to be controlled by a ba.Player.
Category: **Gameplay Classes**
When a PlayerSpaz dies, it delivers a ba.PlayerDiedMessage
to the current ba.Activity. (unless the death was the result of the
player leaving the game, in which case no message is sent)
When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage
to the current ba.Activity.
"""
def __init__(
self,
player: ba.Player,
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
character: str = 'Spaz',
powerups_expire: bool = True,
):
"""Create a spaz for the provided ba.Player.
Note: this does not wire up any controls;
you must call connect_controls_to_player() to do so.
"""
character=modifyspaz.getCharacter(player,character)
super().__init__(
color=color,
highlight=highlight,
character=character,
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire,
)
self.last_player_attacked_by: ba.Player | None = None
self.last_attacked_time = 0.0
self.last_attacked_type: tuple[str, str] | None = None
self.held_count = 0
self.last_player_held_by: ba.Player | None = None
self._player = player
self._drive_player_position()
import custom_hooks
custom_hooks.playerspaz_init(self, self.node, self._player)
# Overloads to tell the type system our return type based on doraise val.
@overload
def getplayer(
self, playertype: type[PlayerType], doraise: Literal[False] = False
) -> PlayerType | None:
...
@overload
def getplayer(
self, playertype: type[PlayerType], doraise: Literal[True]
) -> PlayerType:
...
def getplayer(
self, playertype: type[PlayerType], doraise: bool = False
) -> PlayerType | None:
"""Get the ba.Player associated with this Spaz.
By default this will return None if the Player no longer exists.
If you are logically certain that the Player still exists, pass
doraise=False to get a non-optional return type.
"""
player: Any = self._player
assert isinstance(player, playertype)
if not player.exists() and doraise:
raise ba.PlayerNotFoundError()
return player if player.exists() else None
def connect_controls_to_player(
self,
enable_jump: bool = True,
enable_punch: bool = True,
enable_pickup: bool = True,
enable_bomb: bool = True,
enable_run: bool = True,
enable_fly: bool = True,
) -> None:
"""Wire this spaz up to the provided ba.Player.
Full control of the character is given by default
but can be selectively limited by passing False
to specific arguments.
"""
player = self.getplayer(ba.Player)
assert player
# Reset any currently connected player and/or the player we're
# wiring up.
if self._connected_to_player:
if player != self._connected_to_player:
player.resetinput()
self.disconnect_controls_from_player()
else:
player.resetinput()
player.assigninput(ba.InputType.UP_DOWN, self.on_move_up_down)
player.assigninput(ba.InputType.LEFT_RIGHT, self.on_move_left_right)
player.assigninput(
ba.InputType.HOLD_POSITION_PRESS, self.on_hold_position_press
)
player.assigninput(
ba.InputType.HOLD_POSITION_RELEASE, self.on_hold_position_release
)
intp = ba.InputType
if enable_jump:
player.assigninput(intp.JUMP_PRESS, self.on_jump_press)
player.assigninput(intp.JUMP_RELEASE, self.on_jump_release)
if enable_pickup:
player.assigninput(intp.PICK_UP_PRESS, self.on_pickup_press)
player.assigninput(intp.PICK_UP_RELEASE, self.on_pickup_release)
if enable_punch:
player.assigninput(intp.PUNCH_PRESS, self.on_punch_press)
player.assigninput(intp.PUNCH_RELEASE, self.on_punch_release)
if enable_bomb:
player.assigninput(intp.BOMB_PRESS, self.on_bomb_press)
player.assigninput(intp.BOMB_RELEASE, self.on_bomb_release)
if enable_run:
player.assigninput(intp.RUN, self.on_run)
if enable_fly:
player.assigninput(intp.FLY_PRESS, self.on_fly_press)
player.assigninput(intp.FLY_RELEASE, self.on_fly_release)
self._connected_to_player = player
def disconnect_controls_from_player(self) -> None:
"""
Completely sever any previously connected
ba.Player from control of this spaz.
"""
if self._connected_to_player:
self._connected_to_player.resetinput()
self._connected_to_player = None
# Send releases for anything in case its held.
self.on_move_up_down(0)
self.on_move_left_right(0)
self.on_hold_position_release()
self.on_jump_release()
self.on_pickup_release()
self.on_punch_release()
self.on_bomb_release()
self.on_run(0.0)
self.on_fly_release()
else:
print(
'WARNING: disconnect_controls_from_player() called for'
' non-connected player'
)
def handlemessage(self, msg: Any) -> Any:
# FIXME: Tidy this up.
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-nested-blocks
assert not self.expired
# Keep track of if we're being held and by who most recently.
if isinstance(msg, ba.PickedUpMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.held_count += 1
picked_up_by = msg.node.source_player
if picked_up_by:
self.last_player_held_by = picked_up_by
elif isinstance(msg, ba.DroppedMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.held_count -= 1
if self.held_count < 0:
print('ERROR: spaz held_count < 0')
# Let's count someone dropping us as an attack.
picked_up_by = msg.node.source_player
if picked_up_by:
self.last_player_attacked_by = picked_up_by
self.last_attacked_time = ba.time()
self.last_attacked_type = ('picked_up', 'default')
elif isinstance(msg, ba.StandMessage):
super().handlemessage(msg) # Augment standard behavior.
# Our Spaz was just moved somewhere. Explicitly update
# our associated player's position in case it is being used
# for logic (otherwise it will be out of date until next step)
self._drive_player_position()
elif isinstance(msg, ba.DieMessage):
# Report player deaths to the game.
if not self._dead:
# Immediate-mode or left-game deaths don't count as 'kills'.
killed = (
not msg.immediate and msg.how is not ba.DeathType.LEFT_GAME
)
activity = self._activity()
player = self.getplayer(ba.Player, False)
if not killed:
killerplayer = None
else:
# If this player was being held at the time of death,
# the holder is the killer.
if self.held_count > 0 and self.last_player_held_by:
killerplayer = self.last_player_held_by
else:
# Otherwise, if they were attacked by someone in the
# last few seconds, that person is the killer.
# Otherwise it was a suicide.
# FIXME: Currently disabling suicides in Co-Op since
# all bot kills would register as suicides; need to
# change this from last_player_attacked_by to
# something like last_actor_attacked_by to fix that.
if (
self.last_player_attacked_by
and ba.time() - self.last_attacked_time < 4.0
):
killerplayer = self.last_player_attacked_by
else:
# ok, call it a suicide unless we're in co-op
if activity is not None and not isinstance(
activity.session, ba.CoopSession
):
killerplayer = player
else:
killerplayer = None
# We should never wind up with a dead-reference here;
# we want to use None in that case.
assert killerplayer is None or killerplayer
# Only report if both the player and the activity still exist.
if killed and activity is not None and player:
activity.handlemessage(
ba.PlayerDiedMessage(
player, killed, killerplayer, msg.how
)
)
super().handlemessage(msg) # Augment standard behavior.
# Keep track of the player who last hit us for point rewarding.
elif isinstance(msg, ba.HitMessage):
source_player = msg.get_source_player(type(self._player))
if source_player:
self.last_player_attacked_by = source_player
self.last_attacked_time = ba.time()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg) # Augment standard behavior.
activity = self._activity()
if activity is not None and self._player.exists():
activity.handlemessage(PlayerSpazHurtMessage(self))
else:
return super().handlemessage(msg)
return None
def _drive_player_position(self) -> None:
"""Drive our ba.Player's official position
If our position is changed explicitly, this should be called again
to instantly update the player position (otherwise it would be out
of date until the next sim step)
"""
player = self._player
if player:
assert self.node
assert player.node
self.node.connectattr('torso_position', player.node, 'position')

View file

@ -0,0 +1,127 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class PopupText(ba.Actor):
"""Text that pops up above a position to denote something special.
category: Gameplay Classes
"""
def __init__(
self,
text: str | ba.Lstr,
position: Sequence[float] = (0.0, 0.0, 0.0),
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
random_offset: float = 0.5,
offset: Sequence[float] = (0.0, 0.0, 0.0),
scale: float = 1.0,
):
"""Instantiate with given values.
random_offset is the amount of random offset from the provided position
that will be applied. This can help multiple achievements from
overlapping too much.
"""
super().__init__()
if len(color) == 3:
color = (color[0], color[1], color[2], 1.0)
pos = (
position[0] + offset[0] + random_offset * (0.5 - random.random()),
position[1] + offset[1] + random_offset * (0.5 - random.random()),
position[2] + offset[2] + random_offset * (0.5 - random.random()),
)
self.node = ba.newnode(
'text',
attrs={
'text': text,
'in_world': True,
'shadow': 1.0,
'flatness': 1.0,
'h_align': 'center',
},
delegate=self,
)
lifespan = 1.5
# scale up
ba.animate(
self.node,
'scale',
{
0: 0.0,
lifespan * 0.11: 0.020 * 0.7 * scale,
lifespan * 0.16: 0.013 * 0.7 * scale,
lifespan * 0.25: 0.014 * 0.7 * scale,
},
)
# translate upward
self._tcombine = ba.newnode(
'combine',
owner=self.node,
attrs={'input0': pos[0], 'input2': pos[2], 'size': 3},
)
ba.animate(
self._tcombine, 'input1', {0: pos[1] + 1.5, lifespan: pos[1] + 2.0}
)
self._tcombine.connectattr('output', self.node, 'position')
# fade our opacity in/out
self._combine = ba.newnode(
'combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4,
},
)
for i in range(4):
ba.animate(
self._combine,
'input' + str(i),
{
0.13 * lifespan: color[i],
0.18 * lifespan: 4.0 * color[i],
0.22 * lifespan: color[i],
},
)
ba.animate(
self._combine,
'input3',
{
0: 0,
0.1 * lifespan: color[3],
0.7 * lifespan: color[3],
lifespan: 0,
},
)
self._combine.connectattr('output', self.node, 'color')
# kill ourself
self._die_timer = ba.Timer(
lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage())
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
else:
super().handlemessage(msg)

View file

@ -0,0 +1,319 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
DEFAULT_POWERUP_INTERVAL = 8.0
class _TouchedMessage:
pass
class PowerupBoxFactory:
"""A collection of media and other resources used by ba.Powerups.
Category: **Gameplay Classes**
A single instance of this is shared between all powerups
and can be retrieved via ba.Powerup.get_factory().
"""
model: ba.Model
"""The ba.Model of the powerup box."""
model_simple: ba.Model
"""A simpler ba.Model of the powerup box, for use in shadows, etc."""
tex_bomb: ba.Texture
"""Triple-bomb powerup ba.Texture."""
tex_punch: ba.Texture
"""Punch powerup ba.Texture."""
tex_ice_bombs: ba.Texture
"""Ice bomb powerup ba.Texture."""
tex_sticky_bombs: ba.Texture
"""Sticky bomb powerup ba.Texture."""
tex_shield: ba.Texture
"""Shield powerup ba.Texture."""
tex_impact_bombs: ba.Texture
"""Impact-bomb powerup ba.Texture."""
tex_health: ba.Texture
"""Health powerup ba.Texture."""
tex_land_mines: ba.Texture
"""Land-mine powerup ba.Texture."""
tex_curse: ba.Texture
"""Curse powerup ba.Texture."""
health_powerup_sound: ba.Sound
"""ba.Sound played when a health powerup is accepted."""
powerup_sound: ba.Sound
"""ba.Sound played when a powerup is accepted."""
powerdown_sound: ba.Sound
"""ba.Sound that can be used when powerups wear off."""
powerup_material: ba.Material
"""ba.Material applied to powerup boxes."""
powerup_accept_material: ba.Material
"""Powerups will send a ba.PowerupMessage to anything they touch
that has this ba.Material applied."""
_STORENAME = ba.storagename()
def __init__(self) -> None:
"""Instantiate a PowerupBoxFactory.
You shouldn't need to do this; call Powerup.get_factory()
to get a shared instance.
"""
from ba.internal import get_default_powerup_distribution
shared = SharedObjects.get()
self._lastpoweruptype: str | None = None
self.model = ba.getmodel('powerup')
self.model_simple = ba.getmodel('powerupSimple')
self.tex_bomb = ba.gettexture('powerupBomb')
self.tex_punch = ba.gettexture('powerupPunch')
self.tex_ice_bombs = ba.gettexture('powerupIceBombs')
self.tex_sticky_bombs = ba.gettexture('powerupStickyBombs')
self.tex_shield = ba.gettexture('powerupShield')
self.tex_impact_bombs = ba.gettexture('powerupImpactBombs')
self.tex_health = ba.gettexture('powerupHealth')
self.tex_land_mines = ba.gettexture('powerupLandMines')
self.tex_curse = ba.gettexture('powerupCurse')
self.health_powerup_sound = ba.getsound('healthPowerup')
self.powerup_sound = ba.getsound('powerup01')
self.powerdown_sound = ba.getsound('powerdown01')
self.drop_sound = ba.getsound('boxDrop')
# Material for powerups.
self.powerup_material = ba.Material()
# Material for anyone wanting to accept powerups.
self.powerup_accept_material = ba.Material()
# Pass a powerup-touched message to applicable stuff.
self.powerup_material.add_actions(
conditions=('they_have_material', self.powerup_accept_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', _TouchedMessage()),
),
)
# We don't wanna be picked up.
self.powerup_material.add_actions(
conditions=('they_have_material', shared.pickup_material),
actions=('modify_part_collision', 'collide', False),
)
self.powerup_material.add_actions(
conditions=('they_have_material', shared.footing_material),
actions=('impact_sound', self.drop_sound, 0.5, 0.1),
)
self._powerupdist: list[str] = []
for powerup, freq in get_default_powerup_distribution():
for _i in range(int(freq)):
self._powerupdist.append(powerup)
def get_random_powerup_type(
self,
forcetype: str | None = None,
excludetypes: list[str] | None = None,
) -> str:
"""Returns a random powerup type (string).
See ba.Powerup.poweruptype for available type values.
There are certain non-random aspects to this; a 'curse' powerup,
for instance, is always followed by a 'health' powerup (to keep things
interesting). Passing 'forcetype' forces a given returned type while
still properly interacting with the non-random aspects of the system
(ie: forcing a 'curse' powerup will result
in the next powerup being health).
"""
if excludetypes is None:
excludetypes = []
if forcetype:
ptype = forcetype
else:
# If the last one was a curse, make this one a health to
# provide some hope.
if self._lastpoweruptype == 'curse':
ptype = 'health'
else:
while True:
ptype = self._powerupdist[
random.randint(0, len(self._powerupdist) - 1)
]
if ptype not in excludetypes:
break
self._lastpoweruptype = ptype
return ptype
@classmethod
def get(cls) -> PowerupBoxFactory:
"""Return a shared ba.PowerupBoxFactory object, creating if needed."""
activity = ba.getactivity()
if activity is None:
raise ba.ContextError('No current activity.')
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = activity.customdata[cls._STORENAME] = PowerupBoxFactory()
assert isinstance(factory, PowerupBoxFactory)
return factory
class PowerupBox(ba.Actor):
"""A box that grants a powerup.
category: Gameplay Classes
This will deliver a ba.PowerupMessage to anything that touches it
which has the ba.PowerupBoxFactory.powerup_accept_material applied.
"""
poweruptype: str
"""The string powerup type. This can be 'triple_bombs', 'punch',
'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield',
'health', or 'curse'."""
node: ba.Node
"""The 'prop' ba.Node representing this box."""
def __init__(
self,
position: Sequence[float] = (0.0, 1.0, 0.0),
poweruptype: str = 'triple_bombs',
expire: bool = True,
):
"""Create a powerup-box of the requested type at the given position.
see ba.Powerup.poweruptype for valid type strings.
"""
super().__init__()
shared = SharedObjects.get()
factory = PowerupBoxFactory.get()
self.poweruptype = poweruptype
self._powersgiven = False
if poweruptype == 'triple_bombs':
tex = factory.tex_bomb
elif poweruptype == 'punch':
tex = factory.tex_punch
elif poweruptype == 'ice_bombs':
tex = factory.tex_ice_bombs
elif poweruptype == 'impact_bombs':
tex = factory.tex_impact_bombs
elif poweruptype == 'land_mines':
tex = factory.tex_land_mines
elif poweruptype == 'sticky_bombs':
tex = factory.tex_sticky_bombs
elif poweruptype == 'shield':
tex = factory.tex_shield
elif poweruptype == 'health':
tex = factory.tex_health
elif poweruptype == 'curse':
tex = factory.tex_curse
else:
raise ValueError('invalid poweruptype: ' + str(poweruptype))
if len(position) != 3:
raise ValueError('expected 3 floats for position')
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'body': 'box',
'position': position,
'model': factory.model,
'light_model': factory.model_simple,
'shadow_size': 0.5,
'color_texture': tex,
'reflection': 'powerup',
'reflection_scale': [1.0],
'materials': (factory.powerup_material, shared.object_material),
},
) # yapf: disable
# Animate in.
curve = ba.animate(self.node, 'model_scale', {0: 0, 0.14: 1.6, 0.2: 1})
ba.timer(0.2, curve.delete)
if expire:
ba.timer(
DEFAULT_POWERUP_INTERVAL - 2.5,
ba.WeakCall(self._start_flashing),
)
ba.timer(
DEFAULT_POWERUP_INTERVAL - 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def _start_flashing(self) -> None:
if self.node:
self.node.flashing = True
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.PowerupAcceptMessage):
factory = PowerupBoxFactory.get()
assert self.node
if self.poweruptype == 'health':
ba.playsound(
factory.health_powerup_sound, 3, position=self.node.position
)
ba.playsound(factory.powerup_sound, 3, position=self.node.position)
self._powersgiven = True
self.handlemessage(ba.DieMessage())
elif isinstance(msg, _TouchedMessage):
if not self._powersgiven:
node = ba.getcollision().opposingnode
node.handlemessage(
ba.PowerupMessage(self.poweruptype, sourcenode=self.node)
)
elif isinstance(msg, ba.DieMessage):
if self.node:
if msg.immediate:
self.node.delete()
else:
ba.animate(self.node, 'model_scale', {0: 1, 0.1: 0})
ba.timer(0.1, self.node.delete)
elif isinstance(msg, ba.OutOfBoundsMessage):
self.handlemessage(ba.DieMessage())
elif isinstance(msg, ba.HitMessage):
# Don't die on punches (that's annoying).
if msg.hit_type != 'punch':
self.handlemessage(ba.DieMessage())
else:
return super().handlemessage(msg)
return None

View file

@ -0,0 +1,168 @@
# Released under the MIT License. See LICENSE for details.
#
"""Implements respawn icon actor."""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
pass
class RespawnIcon:
"""An icon with a countdown that appears alongside the screen.
category: Gameplay Classes
This is used to indicate that a ba.Player is waiting to respawn.
"""
_MASKTEXSTORENAME = ba.storagename('masktex')
_ICONSSTORENAME = ba.storagename('icons')
def __init__(self, player: ba.Player, respawn_time: float):
"""Instantiate with a ba.Player and respawn_time (in seconds)."""
self._visible = True
on_right, offs_extra, respawn_icons = self._get_context(player)
# Cache our mask tex on the team for easy access.
mask_tex = player.team.customdata.get(self._MASKTEXSTORENAME)
if mask_tex is None:
mask_tex = ba.gettexture('characterIconMask')
player.team.customdata[self._MASKTEXSTORENAME] = mask_tex
assert isinstance(mask_tex, ba.Texture)
# Now find the first unused slot and use that.
index = 0
while (
index in respawn_icons
and respawn_icons[index]() is not None
and respawn_icons[index]().visible
):
index += 1
respawn_icons[index] = weakref.ref(self)
offs = offs_extra + index * -53
icon = player.get_icon()
texture = icon['texture']
h_offs = -10
ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs)
self._image: ba.NodeActor | None = ba.NodeActor(
ba.newnode(
'image',
attrs={
'texture': texture,
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'tint2_color': icon['tint2_color'],
'mask_texture': mask_tex,
'position': ipos,
'scale': (32, 32),
'opacity': 1.0,
'absolute_scale': True,
'attach': 'topRight' if on_right else 'topLeft',
},
)
)
assert self._image.node
ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7})
npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs)
self._name: ba.NodeActor | None = ba.NodeActor(
ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'right' if on_right else 'left',
'text': ba.Lstr(value=player.getname()),
'maxwidth': 100,
'h_align': 'center',
'v_align': 'center',
'shadow': 1.0,
'flatness': 1.0,
'color': ba.safecolor(icon['tint_color']),
'scale': 0.5,
'position': npos,
},
)
)
assert self._name.node
ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5})
tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs)
self._text: ba.NodeActor | None = ba.NodeActor(
ba.newnode(
'text',
attrs={
'position': tpos,
'h_attach': 'right' if on_right else 'left',
'h_align': 'right' if on_right else 'left',
'scale': 0.9,
'shadow': 0.5,
'flatness': 0.5,
'v_attach': 'top',
'color': ba.safecolor(icon['tint_color']),
'text': '',
},
)
)
assert self._text.node
ba.animate(self._text.node, 'scale', {0: 0, 0.1: 0.9})
self._respawn_time = ba.time() + respawn_time
self._update()
self._timer: ba.Timer | None = ba.Timer(
1.0, ba.WeakCall(self._update), repeat=True
)
@property
def visible(self) -> bool:
"""Is this icon still visible?"""
return self._visible
def _get_context(self, player: ba.Player) -> tuple[bool, float, dict]:
"""Return info on where we should be shown and stored."""
activity = ba.getactivity()
if isinstance(ba.getsession(), ba.DualTeamSession):
on_right = player.team.id % 2 == 1
# Store a list of icons in the team.
icons = player.team.customdata.get(self._ICONSSTORENAME)
if icons is None:
player.team.customdata[self._ICONSSTORENAME] = icons = {}
assert isinstance(icons, dict)
offs_extra = -20
else:
on_right = False
# Store a list of icons in the activity.
icons = activity.customdata.get(self._ICONSSTORENAME)
if icons is None:
activity.customdata[self._ICONSSTORENAME] = icons = {}
assert isinstance(icons, dict)
if isinstance(activity.session, ba.FreeForAllSession):
offs_extra = -150
else:
offs_extra = -20
return on_right, offs_extra, icons
def _update(self) -> None:
remaining = int(round(self._respawn_time - ba.time()))
if remaining > 0:
assert self._text is not None
if self._text.node:
self._text.node.text = str(remaining)
else:
self._visible = False
self._image = self._text = self._timer = self._name = None

View file

@ -0,0 +1,447 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines ScoreBoard Actor and related functionality."""
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class _Entry:
def __init__(
self,
scoreboard: Scoreboard,
team: ba.Team,
do_cover: bool,
scale: float,
label: ba.Lstr | None,
flash_length: float,
):
# pylint: disable=too-many-statements
self._scoreboard = weakref.ref(scoreboard)
self._do_cover = do_cover
self._scale = scale
self._flash_length = flash_length
self._width = 140.0 * self._scale
self._height = 32.0 * self._scale
self._bar_width = 2.0 * self._scale
self._bar_height = 32.0 * self._scale
self._bar_tex = self._backing_tex = ba.gettexture('bar')
self._cover_tex = ba.gettexture('uiAtlas')
self._model = ba.getmodel('meterTransparent')
self._pos: Sequence[float] | None = None
self._flash_timer: ba.Timer | None = None
self._flash_counter: int | None = None
self._flash_colors: bool | None = None
self._score: float | None = None
safe_team_color = ba.safecolor(team.color, target_intensity=1.0)
# FIXME: Should not do things conditionally for vr-mode, as there may
# be non-vr clients connected which will also get these value.
vrmode = ba.app.vr_mode
if self._do_cover:
if vrmode:
self._backing_color = [0.1 + c * 0.1 for c in safe_team_color]
else:
self._backing_color = [0.05 + c * 0.17 for c in safe_team_color]
else:
self._backing_color = [0.05 + c * 0.1 for c in safe_team_color]
opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5
self._backing = ba.NodeActor(
ba.newnode(
'image',
attrs={
'scale': (self._width, self._height),
'opacity': opacity,
'color': self._backing_color,
'vr_depth': -3,
'attach': 'topLeft',
'texture': self._backing_tex,
},
)
)
self._barcolor = safe_team_color
self._bar = ba.NodeActor(
ba.newnode(
'image',
attrs={
'opacity': 0.7,
'color': self._barcolor,
'attach': 'topLeft',
'texture': self._bar_tex,
},
)
)
self._bar_scale = ba.newnode(
'combine',
owner=self._bar.node,
attrs={
'size': 2,
'input0': self._bar_width,
'input1': self._bar_height,
},
)
assert self._bar.node
self._bar_scale.connectattr('output', self._bar.node, 'scale')
self._bar_position = ba.newnode(
'combine',
owner=self._bar.node,
attrs={'size': 2, 'input0': 0, 'input1': 0},
)
self._bar_position.connectattr('output', self._bar.node, 'position')
self._cover_color = safe_team_color
if self._do_cover:
self._cover = ba.NodeActor(
ba.newnode(
'image',
attrs={
'scale': (self._width * 1.15, self._height * 1.6),
'opacity': 1.0,
'color': self._cover_color,
'vr_depth': 2,
'attach': 'topLeft',
'texture': self._cover_tex,
'model_transparent': self._model,
},
)
)
clr = safe_team_color
maxwidth = 130.0 * (1.0 - scoreboard.score_split)
flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0
self._score_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'right',
'v_align': 'center',
'maxwidth': maxwidth,
'vr_depth': 2,
'scale': self._scale * 0.9,
'text': '',
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'color': clr,
},
)
)
clr = safe_team_color
team_name_label: str | ba.Lstr
if label is not None:
team_name_label = label
else:
team_name_label = team.name
# We do our own clipping here; should probably try to tap into some
# existing functionality.
if isinstance(team_name_label, ba.Lstr):
# Hmmm; if the team-name is a non-translatable value lets go
# ahead and clip it otherwise we leave it as-is so
# translation can occur..
if team_name_label.is_flat_value():
val = team_name_label.evaluate()
if len(val) > 10:
team_name_label = ba.Lstr(value=val[:10] + '...')
else:
if len(team_name_label) > 10:
team_name_label = team_name_label[:10] + '...'
team_name_label = ba.Lstr(value=team_name_label)
flatness = (1.0 if vrmode else 0.5) if self._do_cover else 1.0
self._name_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
'h_attach': 'left',
'v_attach': 'top',
'h_align': 'left',
'v_align': 'center',
'vr_depth': 2,
'scale': self._scale * 0.9,
'shadow': 1.0 if vrmode else 0.5,
'flatness': flatness,
'maxwidth': 130 * scoreboard.score_split,
'text': team_name_label,
'color': clr + (1.0,),
},
)
)
def flash(self, countdown: bool, extra_flash: bool) -> None:
"""Flash momentarily."""
self._flash_timer = ba.Timer(
0.1, ba.WeakCall(self._do_flash), repeat=True
)
if countdown:
self._flash_counter = 10
else:
self._flash_counter = int(20.0 * self._flash_length)
if extra_flash:
self._flash_counter *= 4
self._set_flash_colors(True)
def set_position(self, position: Sequence[float]) -> None:
"""Set the entry's position."""
# Abort if we've been killed
if not self._backing.node:
return
self._pos = tuple(position)
self._backing.node.position = (
position[0] + self._width / 2,
position[1] - self._height / 2,
)
if self._do_cover:
assert self._cover.node
self._cover.node.position = (
position[0] + self._width / 2,
position[1] - self._height / 2,
)
self._bar_position.input0 = self._pos[0] + self._bar_width / 2
self._bar_position.input1 = self._pos[1] - self._bar_height / 2
assert self._score_text.node
self._score_text.node.position = (
self._pos[0] + self._width - 7.0 * self._scale,
self._pos[1] - self._bar_height + 16.0 * self._scale,
)
assert self._name_text.node
self._name_text.node.position = (
self._pos[0] + 7.0 * self._scale,
self._pos[1] - self._bar_height + 16.0 * self._scale,
)
def _set_flash_colors(self, flash: bool) -> None:
self._flash_colors = flash
def _safesetcolor(node: ba.Node | None, val: Any) -> None:
if node:
node.color = val
if flash:
scale = 2.0
_safesetcolor(
self._backing.node,
(
self._backing_color[0] * scale,
self._backing_color[1] * scale,
self._backing_color[2] * scale,
),
)
_safesetcolor(
self._bar.node,
(
self._barcolor[0] * scale,
self._barcolor[1] * scale,
self._barcolor[2] * scale,
),
)
if self._do_cover:
_safesetcolor(
self._cover.node,
(
self._cover_color[0] * scale,
self._cover_color[1] * scale,
self._cover_color[2] * scale,
),
)
else:
_safesetcolor(self._backing.node, self._backing_color)
_safesetcolor(self._bar.node, self._barcolor)
if self._do_cover:
_safesetcolor(self._cover.node, self._cover_color)
def _do_flash(self) -> None:
assert self._flash_counter is not None
if self._flash_counter <= 0:
self._set_flash_colors(False)
else:
self._flash_counter -= 1
self._set_flash_colors(not self._flash_colors)
def set_value(
self,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True,
) -> None:
"""Set the value for the scoreboard entry."""
# If we have no score yet, just set it.. otherwise compare
# and see if we should flash.
if self._score is None:
self._score = score
else:
if score > self._score or (countdown and score < self._score):
extra_flash = (
max_score is not None
and score >= max_score
and not countdown
) or (countdown and score == 0)
if flash:
self.flash(countdown, extra_flash)
self._score = score
if max_score is None:
self._bar_width = 0.0
else:
if countdown:
self._bar_width = max(
2.0 * self._scale,
self._width * (1.0 - (float(score) / max_score)),
)
else:
self._bar_width = max(
2.0 * self._scale,
self._width * (min(1.0, float(score) / max_score)),
)
cur_width = self._bar_scale.input0
ba.animate(
self._bar_scale, 'input0', {0.0: cur_width, 0.25: self._bar_width}
)
self._bar_scale.input1 = self._bar_height
cur_x = self._bar_position.input0
assert self._pos is not None
ba.animate(
self._bar_position,
'input0',
{0.0: cur_x, 0.25: self._pos[0] + self._bar_width / 2},
)
self._bar_position.input1 = self._pos[1] - self._bar_height / 2
assert self._score_text.node
if show_value:
self._score_text.node.text = str(score)
else:
self._score_text.node.text = ''
class _EntryProxy:
"""Encapsulates adding/removing of a scoreboard Entry."""
def __init__(self, scoreboard: Scoreboard, team: ba.Team):
self._scoreboard = weakref.ref(scoreboard)
# Have to store ID here instead of a weak-ref since the team will be
# dead when we die and need to remove it.
self._team_id = team.id
def __del__(self) -> None:
scoreboard = self._scoreboard()
# Remove our team from the scoreboard if its still around.
# (but deferred, in case we die in a sim step or something where
# its illegal to modify nodes)
if scoreboard is None:
return
try:
ba.pushcall(ba.Call(scoreboard.remove_team, self._team_id))
except ba.ContextError:
# This happens if we fire after the activity expires.
# In that case we don't need to do anything.
pass
class Scoreboard:
"""A display for player or team scores during a game.
category: Gameplay Classes
"""
_ENTRYSTORENAME = ba.storagename('entry')
def __init__(self, label: ba.Lstr | None = None, score_split: float = 0.7):
"""Instantiate a scoreboard.
Label can be something like 'points' and will
show up on boards if provided.
"""
self._flat_tex = ba.gettexture('null')
self._entries: dict[int, _Entry] = {}
self._label = label
self.score_split = score_split
# For free-for-all we go simpler since we have one per player.
self._pos: Sequence[float]
if isinstance(ba.getsession(), ba.FreeForAllSession):
self._do_cover = False
self._spacing = 35.0
self._pos = (17.0, -65.0)
self._scale = 0.8
self._flash_length = 0.5
else:
self._do_cover = True
self._spacing = 50.0
self._pos = (20.0, -70.0)
self._scale = 1.0
self._flash_length = 1.0
def set_team_value(
self,
team: ba.Team,
score: float,
max_score: float | None = None,
countdown: bool = False,
flash: bool = True,
show_value: bool = True,
) -> None:
"""Update the score-board display for the given ba.Team."""
if team.id not in self._entries:
self._add_team(team)
# Create a proxy in the team which will kill
# our entry when it dies (for convenience)
assert self._ENTRYSTORENAME not in team.customdata
team.customdata[self._ENTRYSTORENAME] = _EntryProxy(self, team)
# Now set the entry.
self._entries[team.id].set_value(
score=score,
max_score=max_score,
countdown=countdown,
flash=flash,
show_value=show_value,
)
def _add_team(self, team: ba.Team) -> None:
if team.id in self._entries:
raise RuntimeError('Duplicate team add')
self._entries[team.id] = _Entry(
self,
team,
do_cover=self._do_cover,
scale=self._scale,
label=self._label,
flash_length=self._flash_length,
)
self._update_teams()
def remove_team(self, team_id: int) -> None:
"""Remove the team with the given id from the scoreboard."""
del self._entries[team_id]
self._update_teams()
def _update_teams(self) -> None:
pos = list(self._pos)
for entry in list(self._entries.values()):
entry.set_position(pos)
pos[1] -= self._spacing * self._scale

View file

@ -0,0 +1,118 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines some lovely Actor(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence, Callable
# FIXME: Should make this an Actor.
class Spawner:
"""Utility for delayed spawning of objects.
Category: **Gameplay Classes**
Creates a light flash and sends a Spawner.SpawnMessage
to the current activity after a delay.
"""
class SpawnMessage:
"""Spawn message sent by a Spawner after its delay has passed.
Category: **Message Classes**
"""
spawner: Spawner
"""The ba.Spawner we came from."""
data: Any
"""The data object passed by the user."""
pt: Sequence[float]
"""The spawn position."""
def __init__(
self,
spawner: Spawner,
data: Any,
pt: Sequence[float], # pylint: disable=invalid-name
):
"""Instantiate with the given values."""
self.spawner = spawner
self.data = data
self.pt = pt # pylint: disable=invalid-name
def __init__(
self,
data: Any = None,
pt: Sequence[float] = (0, 0, 0), # pylint: disable=invalid-name
spawn_time: float = 1.0,
send_spawn_message: bool = True,
spawn_callback: Callable[[], Any] | None = None,
):
"""Instantiate a Spawner.
Requires some custom data, a position,
and a spawn-time in seconds.
"""
self._spawn_callback = spawn_callback
self._send_spawn_message = send_spawn_message
self._spawner_sound = ba.getsound('swip2')
self._data = data
self._pt = pt
# create a light where the spawn will happen
self._light = ba.newnode(
'light',
attrs={
'position': tuple(pt),
'radius': 0.1,
'color': (1.0, 0.1, 0.1),
'lights_volumes': False,
},
)
scl = float(spawn_time) / 3.75
min_val = 0.4
max_val = 0.7
ba.playsound(self._spawner_sound, position=self._light.position)
ba.animate(
self._light,
'intensity',
{
0.0: 0.0,
0.25 * scl: max_val,
0.500 * scl: min_val,
0.750 * scl: max_val,
1.000 * scl: min_val,
1.250 * scl: 1.1 * max_val,
1.500 * scl: min_val,
1.750 * scl: 1.2 * max_val,
2.000 * scl: min_val,
2.250 * scl: 1.3 * max_val,
2.500 * scl: min_val,
2.750 * scl: 1.4 * max_val,
3.000 * scl: min_val,
3.250 * scl: 1.5 * max_val,
3.500 * scl: min_val,
3.750 * scl: 2.0,
4.000 * scl: 0.0,
},
)
ba.timer(spawn_time, self._spawn)
def _spawn(self) -> None:
ba.timer(1.0, self._light.delete)
if self._spawn_callback is not None:
self._spawn_callback()
if self._send_spawn_message:
# only run if our activity still exists
activity = ba.getactivity()
if activity is not None:
activity.handlemessage(
self.SpawnMessage(self, self._data, self._pt)
)

1633
dist/ba_data/python/bastd/actor/spaz.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,988 @@
# Released under the MIT License. See LICENSE for details.
#
"""Appearance functionality for spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
pass
def get_appearances(include_locked: bool = False) -> list[str]:
"""Get the list of available spaz appearances."""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
get_purchased = ba.internal.get_purchased
disallowed = []
if not include_locked:
# hmm yeah this'll be tough to hack...
if not get_purchased('characters.santa'):
disallowed.append('Santa Claus')
if not get_purchased('characters.frosty'):
disallowed.append('Frosty')
if not get_purchased('characters.bones'):
disallowed.append('Bones')
if not get_purchased('characters.bernard'):
disallowed.append('Bernard')
if not get_purchased('characters.pixie'):
disallowed.append('Pixel')
if not get_purchased('characters.pascal'):
disallowed.append('Pascal')
if not get_purchased('characters.actionhero'):
disallowed.append('Todd McBurton')
if not get_purchased('characters.taobaomascot'):
disallowed.append('Taobao Mascot')
if not get_purchased('characters.agent'):
disallowed.append('Agent Johnson')
if not get_purchased('characters.jumpsuit'):
disallowed.append('Lee')
if not get_purchased('characters.assassin'):
disallowed.append('Zola')
if not get_purchased('characters.wizard'):
disallowed.append('Grumbledorf')
if not get_purchased('characters.cowboy'):
disallowed.append('Butch')
if not get_purchased('characters.witch'):
disallowed.append('Witch')
if not get_purchased('characters.warrior'):
disallowed.append('Warrior')
if not get_purchased('characters.superhero'):
disallowed.append('Middle-Man')
if not get_purchased('characters.alien'):
disallowed.append('Alien')
if not get_purchased('characters.oldlady'):
disallowed.append('OldLady')
if not get_purchased('characters.gladiator'):
disallowed.append('Gladiator')
if not get_purchased('characters.wrestler'):
disallowed.append('Wrestler')
if not get_purchased('characters.operasinger'):
disallowed.append('Gretel')
if not get_purchased('characters.robot'):
disallowed.append('Robot')
if not get_purchased('characters.cyborg'):
disallowed.append('B-9000')
if not get_purchased('characters.bunny'):
disallowed.append('Easter Bunny')
if not get_purchased('characters.kronk'):
disallowed.append('Kronk')
if not get_purchased('characters.zoe'):
disallowed.append('Zoe')
if not get_purchased('characters.jackmorgan'):
disallowed.append('Jack Morgan')
if not get_purchased('characters.mel'):
disallowed.append('Mel')
if not get_purchased('characters.snakeshadow'):
disallowed.append('Snake Shadow')
return [
s for s in list(ba.app.spaz_appearances.keys()) if s not in disallowed
]
class Appearance:
"""Create and fill out one of these suckers to define a spaz appearance"""
def __init__(self, name: str):
self.name = name
if self.name in ba.app.spaz_appearances:
raise Exception(
'spaz appearance name "' + self.name + '" already exists.'
)
ba.app.spaz_appearances[self.name] = self
self.color_texture = ''
self.color_mask_texture = ''
self.icon_texture = ''
self.icon_mask_texture = ''
self.head_model = ''
self.torso_model = ''
self.pelvis_model = ''
self.upper_arm_model = ''
self.forearm_model = ''
self.hand_model = ''
self.upper_leg_model = ''
self.lower_leg_model = ''
self.toes_model = ''
self.jump_sounds: list[str] = []
self.attack_sounds: list[str] = []
self.impact_sounds: list[str] = []
self.death_sounds: list[str] = []
self.pickup_sounds: list[str] = []
self.fall_sounds: list[str] = []
self.style = 'spaz'
self.default_color: tuple[float, float, float] | None = None
self.default_highlight: tuple[float, float, float] | None = None
def register_appearances() -> None:
"""Register our builtin spaz appearances."""
# this is quite ugly but will be going away so not worth cleaning up
# pylint: disable=invalid-name
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
# Spaz #######################################
t = Appearance('Spaz')
t.color_texture = 'neoSpazColor'
t.color_mask_texture = 'neoSpazColorMask'
t.icon_texture = 'neoSpazIcon'
t.icon_mask_texture = 'neoSpazIconColorMask'
t.head_model = 'neoSpazHead'
t.torso_model = 'neoSpazTorso'
t.pelvis_model = 'neoSpazPelvis'
t.upper_arm_model = 'neoSpazUpperArm'
t.forearm_model = 'neoSpazForeArm'
t.hand_model = 'neoSpazHand'
t.upper_leg_model = 'neoSpazUpperLeg'
t.lower_leg_model = 'neoSpazLowerLeg'
t.toes_model = 'neoSpazToes'
t.jump_sounds = ['spazJump01', 'spazJump02', 'spazJump03', 'spazJump04']
t.attack_sounds = [
'spazAttack01',
'spazAttack02',
'spazAttack03',
'spazAttack04',
]
t.impact_sounds = [
'spazImpact01',
'spazImpact02',
'spazImpact03',
'spazImpact04',
]
t.death_sounds = ['spazDeath01']
t.pickup_sounds = ['spazPickup01']
t.fall_sounds = ['spazFall01']
t.style = 'spaz'
# Zoe #####################################
t = Appearance('Zoe')
t.color_texture = 'zoeColor'
t.color_mask_texture = 'zoeColorMask'
t.default_color = (0.6, 0.6, 0.6)
t.default_highlight = (0, 1, 0)
t.icon_texture = 'zoeIcon'
t.icon_mask_texture = 'zoeIconColorMask'
t.head_model = 'zoeHead'
t.torso_model = 'zoeTorso'
t.pelvis_model = 'zoePelvis'
t.upper_arm_model = 'zoeUpperArm'
t.forearm_model = 'zoeForeArm'
t.hand_model = 'zoeHand'
t.upper_leg_model = 'zoeUpperLeg'
t.lower_leg_model = 'zoeLowerLeg'
t.toes_model = 'zoeToes'
t.jump_sounds = ['zoeJump01', 'zoeJump02', 'zoeJump03']
t.attack_sounds = [
'zoeAttack01',
'zoeAttack02',
'zoeAttack03',
'zoeAttack04',
]
t.impact_sounds = [
'zoeImpact01',
'zoeImpact02',
'zoeImpact03',
'zoeImpact04',
]
t.death_sounds = ['zoeDeath01']
t.pickup_sounds = ['zoePickup01']
t.fall_sounds = ['zoeFall01']
t.style = 'female'
# Ninja ##########################################
t = Appearance('Snake Shadow')
t.color_texture = 'ninjaColor'
t.color_mask_texture = 'ninjaColorMask'
t.default_color = (1, 1, 1)
t.default_highlight = (0.55, 0.8, 0.55)
t.icon_texture = 'ninjaIcon'
t.icon_mask_texture = 'ninjaIconColorMask'
t.head_model = 'ninjaHead'
t.torso_model = 'ninjaTorso'
t.pelvis_model = 'ninjaPelvis'
t.upper_arm_model = 'ninjaUpperArm'
t.forearm_model = 'ninjaForeArm'
t.hand_model = 'ninjaHand'
t.upper_leg_model = 'ninjaUpperLeg'
t.lower_leg_model = 'ninjaLowerLeg'
t.toes_model = 'ninjaToes'
ninja_attacks = ['ninjaAttack' + str(i + 1) + '' for i in range(7)]
ninja_hits = ['ninjaHit' + str(i + 1) + '' for i in range(8)]
ninja_jumps = ['ninjaAttack' + str(i + 1) + '' for i in range(7)]
t.jump_sounds = ninja_jumps
t.attack_sounds = ninja_attacks
t.impact_sounds = ninja_hits
t.death_sounds = ['ninjaDeath1']
t.pickup_sounds = ninja_attacks
t.fall_sounds = ['ninjaFall1']
t.style = 'ninja'
# Barbarian #####################################
t = Appearance('Kronk')
t.color_texture = 'kronk'
t.color_mask_texture = 'kronkColorMask'
t.default_color = (0.4, 0.5, 0.4)
t.default_highlight = (1, 0.5, 0.3)
t.icon_texture = 'kronkIcon'
t.icon_mask_texture = 'kronkIconColorMask'
t.head_model = 'kronkHead'
t.torso_model = 'kronkTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'kronkUpperArm'
t.forearm_model = 'kronkForeArm'
t.hand_model = 'kronkHand'
t.upper_leg_model = 'kronkUpperLeg'
t.lower_leg_model = 'kronkLowerLeg'
t.toes_model = 'kronkToes'
kronk_sounds = [
'kronk1',
'kronk2',
'kronk3',
'kronk4',
'kronk5',
'kronk6',
'kronk7',
'kronk8',
'kronk9',
'kronk10',
]
t.jump_sounds = kronk_sounds
t.attack_sounds = kronk_sounds
t.impact_sounds = kronk_sounds
t.death_sounds = ['kronkDeath']
t.pickup_sounds = kronk_sounds
t.fall_sounds = ['kronkFall']
t.style = 'kronk'
# Chef ###########################################
t = Appearance('Mel')
t.color_texture = 'melColor'
t.color_mask_texture = 'melColorMask'
t.default_color = (1, 1, 1)
t.default_highlight = (0.1, 0.6, 0.1)
t.icon_texture = 'melIcon'
t.icon_mask_texture = 'melIconColorMask'
t.head_model = 'melHead'
t.torso_model = 'melTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'melUpperArm'
t.forearm_model = 'melForeArm'
t.hand_model = 'melHand'
t.upper_leg_model = 'melUpperLeg'
t.lower_leg_model = 'melLowerLeg'
t.toes_model = 'melToes'
mel_sounds = [
'mel01',
'mel02',
'mel03',
'mel04',
'mel05',
'mel06',
'mel07',
'mel08',
'mel09',
'mel10',
]
t.attack_sounds = mel_sounds
t.jump_sounds = mel_sounds
t.impact_sounds = mel_sounds
t.death_sounds = ['melDeath01']
t.pickup_sounds = mel_sounds
t.fall_sounds = ['melFall01']
t.style = 'mel'
# Pirate #######################################
t = Appearance('Jack Morgan')
t.color_texture = 'jackColor'
t.color_mask_texture = 'jackColorMask'
t.default_color = (1, 0.2, 0.1)
t.default_highlight = (1, 1, 0)
t.icon_texture = 'jackIcon'
t.icon_mask_texture = 'jackIconColorMask'
t.head_model = 'jackHead'
t.torso_model = 'jackTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'jackUpperArm'
t.forearm_model = 'jackForeArm'
t.hand_model = 'jackHand'
t.upper_leg_model = 'jackUpperLeg'
t.lower_leg_model = 'jackLowerLeg'
t.toes_model = 'jackToes'
hit_sounds = [
'jackHit01',
'jackHit02',
'jackHit03',
'jackHit04',
'jackHit05',
'jackHit06',
'jackHit07',
]
sounds = ['jack01', 'jack02', 'jack03', 'jack04', 'jack05', 'jack06']
t.attack_sounds = sounds
t.jump_sounds = sounds
t.impact_sounds = hit_sounds
t.death_sounds = ['jackDeath01']
t.pickup_sounds = sounds
t.fall_sounds = ['jackFall01']
t.style = 'pirate'
# Santa ######################################
t = Appearance('Santa Claus')
t.color_texture = 'santaColor'
t.color_mask_texture = 'santaColorMask'
t.default_color = (1, 0, 0)
t.default_highlight = (1, 1, 1)
t.icon_texture = 'santaIcon'
t.icon_mask_texture = 'santaIconColorMask'
t.head_model = 'santaHead'
t.torso_model = 'santaTorso'
t.pelvis_model = 'kronkPelvis'
t.upper_arm_model = 'santaUpperArm'
t.forearm_model = 'santaForeArm'
t.hand_model = 'santaHand'
t.upper_leg_model = 'santaUpperLeg'
t.lower_leg_model = 'santaLowerLeg'
t.toes_model = 'santaToes'
hit_sounds = ['santaHit01', 'santaHit02', 'santaHit03', 'santaHit04']
sounds = ['santa01', 'santa02', 'santa03', 'santa04', 'santa05']
t.attack_sounds = sounds
t.jump_sounds = sounds
t.impact_sounds = hit_sounds
t.death_sounds = ['santaDeath']
t.pickup_sounds = sounds
t.fall_sounds = ['santaFall']
t.style = 'santa'
# Snowman ###################################
t = Appearance('Frosty')
t.color_texture = 'frostyColor'
t.color_mask_texture = 'frostyColorMask'
t.default_color = (0.5, 0.5, 1)
t.default_highlight = (1, 0.5, 0)
t.icon_texture = 'frostyIcon'
t.icon_mask_texture = 'frostyIconColorMask'
t.head_model = 'frostyHead'
t.torso_model = 'frostyTorso'
t.pelvis_model = 'frostyPelvis'
t.upper_arm_model = 'frostyUpperArm'
t.forearm_model = 'frostyForeArm'
t.hand_model = 'frostyHand'
t.upper_leg_model = 'frostyUpperLeg'
t.lower_leg_model = 'frostyLowerLeg'
t.toes_model = 'frostyToes'
frosty_sounds = ['frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05']
frosty_hit_sounds = ['frostyHit01', 'frostyHit02', 'frostyHit03']
t.attack_sounds = frosty_sounds
t.jump_sounds = frosty_sounds
t.impact_sounds = frosty_hit_sounds
t.death_sounds = ['frostyDeath']
t.pickup_sounds = frosty_sounds
t.fall_sounds = ['frostyFall']
t.style = 'frosty'
# Skeleton ################################
t = Appearance('Bones')
t.color_texture = 'bonesColor'
t.color_mask_texture = 'bonesColorMask'
t.default_color = (0.6, 0.9, 1)
t.default_highlight = (0.6, 0.9, 1)
t.icon_texture = 'bonesIcon'
t.icon_mask_texture = 'bonesIconColorMask'
t.head_model = 'bonesHead'
t.torso_model = 'bonesTorso'
t.pelvis_model = 'bonesPelvis'
t.upper_arm_model = 'bonesUpperArm'
t.forearm_model = 'bonesForeArm'
t.hand_model = 'bonesHand'
t.upper_leg_model = 'bonesUpperLeg'
t.lower_leg_model = 'bonesLowerLeg'
t.toes_model = 'bonesToes'
bones_sounds = ['bones1', 'bones2', 'bones3']
bones_hit_sounds = ['bones1', 'bones2', 'bones3']
t.attack_sounds = bones_sounds
t.jump_sounds = bones_sounds
t.impact_sounds = bones_hit_sounds
t.death_sounds = ['bonesDeath']
t.pickup_sounds = bones_sounds
t.fall_sounds = ['bonesFall']
t.style = 'bones'
# Bear ###################################
t = Appearance('Bernard')
t.color_texture = 'bearColor'
t.color_mask_texture = 'bearColorMask'
t.default_color = (0.7, 0.5, 0.0)
t.icon_texture = 'bearIcon'
t.icon_mask_texture = 'bearIconColorMask'
t.head_model = 'bearHead'
t.torso_model = 'bearTorso'
t.pelvis_model = 'bearPelvis'
t.upper_arm_model = 'bearUpperArm'
t.forearm_model = 'bearForeArm'
t.hand_model = 'bearHand'
t.upper_leg_model = 'bearUpperLeg'
t.lower_leg_model = 'bearLowerLeg'
t.toes_model = 'bearToes'
bear_sounds = ['bear1', 'bear2', 'bear3', 'bear4']
bear_hit_sounds = ['bearHit1', 'bearHit2']
t.attack_sounds = bear_sounds
t.jump_sounds = bear_sounds
t.impact_sounds = bear_hit_sounds
t.death_sounds = ['bearDeath']
t.pickup_sounds = bear_sounds
t.fall_sounds = ['bearFall']
t.style = 'bear'
# Penguin ###################################
t = Appearance('Pascal')
t.color_texture = 'penguinColor'
t.color_mask_texture = 'penguinColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'penguinIcon'
t.icon_mask_texture = 'penguinIconColorMask'
t.head_model = 'penguinHead'
t.torso_model = 'penguinTorso'
t.pelvis_model = 'penguinPelvis'
t.upper_arm_model = 'penguinUpperArm'
t.forearm_model = 'penguinForeArm'
t.hand_model = 'penguinHand'
t.upper_leg_model = 'penguinUpperLeg'
t.lower_leg_model = 'penguinLowerLeg'
t.toes_model = 'penguinToes'
penguin_sounds = ['penguin1', 'penguin2', 'penguin3', 'penguin4']
penguin_hit_sounds = ['penguinHit1', 'penguinHit2']
t.attack_sounds = penguin_sounds
t.jump_sounds = penguin_sounds
t.impact_sounds = penguin_hit_sounds
t.death_sounds = ['penguinDeath']
t.pickup_sounds = penguin_sounds
t.fall_sounds = ['penguinFall']
t.style = 'penguin'
# Ali ###################################
t = Appearance('Taobao Mascot')
t.color_texture = 'aliColor'
t.color_mask_texture = 'aliColorMask'
t.default_color = (1, 0.5, 0)
t.default_highlight = (1, 1, 1)
t.icon_texture = 'aliIcon'
t.icon_mask_texture = 'aliIconColorMask'
t.head_model = 'aliHead'
t.torso_model = 'aliTorso'
t.pelvis_model = 'aliPelvis'
t.upper_arm_model = 'aliUpperArm'
t.forearm_model = 'aliForeArm'
t.hand_model = 'aliHand'
t.upper_leg_model = 'aliUpperLeg'
t.lower_leg_model = 'aliLowerLeg'
t.toes_model = 'aliToes'
ali_sounds = ['ali1', 'ali2', 'ali3', 'ali4']
ali_hit_sounds = ['aliHit1', 'aliHit2']
t.attack_sounds = ali_sounds
t.jump_sounds = ali_sounds
t.impact_sounds = ali_hit_sounds
t.death_sounds = ['aliDeath']
t.pickup_sounds = ali_sounds
t.fall_sounds = ['aliFall']
t.style = 'ali'
# cyborg ###################################
t = Appearance('B-9000')
t.color_texture = 'cyborgColor'
t.color_mask_texture = 'cyborgColorMask'
t.default_color = (0.5, 0.5, 0.5)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'cyborgIcon'
t.icon_mask_texture = 'cyborgIconColorMask'
t.head_model = 'cyborgHead'
t.torso_model = 'cyborgTorso'
t.pelvis_model = 'cyborgPelvis'
t.upper_arm_model = 'cyborgUpperArm'
t.forearm_model = 'cyborgForeArm'
t.hand_model = 'cyborgHand'
t.upper_leg_model = 'cyborgUpperLeg'
t.lower_leg_model = 'cyborgLowerLeg'
t.toes_model = 'cyborgToes'
cyborg_sounds = ['cyborg1', 'cyborg2', 'cyborg3', 'cyborg4']
cyborg_hit_sounds = ['cyborgHit1', 'cyborgHit2']
t.attack_sounds = cyborg_sounds
t.jump_sounds = cyborg_sounds
t.impact_sounds = cyborg_hit_sounds
t.death_sounds = ['cyborgDeath']
t.pickup_sounds = cyborg_sounds
t.fall_sounds = ['cyborgFall']
t.style = 'cyborg'
# Agent ###################################
t = Appearance('Agent Johnson')
t.color_texture = 'agentColor'
t.color_mask_texture = 'agentColorMask'
t.default_color = (0.3, 0.3, 0.33)
t.default_highlight = (1, 0.5, 0.3)
t.icon_texture = 'agentIcon'
t.icon_mask_texture = 'agentIconColorMask'
t.head_model = 'agentHead'
t.torso_model = 'agentTorso'
t.pelvis_model = 'agentPelvis'
t.upper_arm_model = 'agentUpperArm'
t.forearm_model = 'agentForeArm'
t.hand_model = 'agentHand'
t.upper_leg_model = 'agentUpperLeg'
t.lower_leg_model = 'agentLowerLeg'
t.toes_model = 'agentToes'
agent_sounds = ['agent1', 'agent2', 'agent3', 'agent4']
agent_hit_sounds = ['agentHit1', 'agentHit2']
t.attack_sounds = agent_sounds
t.jump_sounds = agent_sounds
t.impact_sounds = agent_hit_sounds
t.death_sounds = ['agentDeath']
t.pickup_sounds = agent_sounds
t.fall_sounds = ['agentFall']
t.style = 'agent'
# Jumpsuit ###################################
t = Appearance('Lee')
t.color_texture = 'jumpsuitColor'
t.color_mask_texture = 'jumpsuitColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'jumpsuitIcon'
t.icon_mask_texture = 'jumpsuitIconColorMask'
t.head_model = 'jumpsuitHead'
t.torso_model = 'jumpsuitTorso'
t.pelvis_model = 'jumpsuitPelvis'
t.upper_arm_model = 'jumpsuitUpperArm'
t.forearm_model = 'jumpsuitForeArm'
t.hand_model = 'jumpsuitHand'
t.upper_leg_model = 'jumpsuitUpperLeg'
t.lower_leg_model = 'jumpsuitLowerLeg'
t.toes_model = 'jumpsuitToes'
jumpsuit_sounds = ['jumpsuit1', 'jumpsuit2', 'jumpsuit3', 'jumpsuit4']
jumpsuit_hit_sounds = ['jumpsuitHit1', 'jumpsuitHit2']
t.attack_sounds = jumpsuit_sounds
t.jump_sounds = jumpsuit_sounds
t.impact_sounds = jumpsuit_hit_sounds
t.death_sounds = ['jumpsuitDeath']
t.pickup_sounds = jumpsuit_sounds
t.fall_sounds = ['jumpsuitFall']
t.style = 'spaz'
# ActionHero ###################################
t = Appearance('Todd McBurton')
t.color_texture = 'actionHeroColor'
t.color_mask_texture = 'actionHeroColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'actionHeroIcon'
t.icon_mask_texture = 'actionHeroIconColorMask'
t.head_model = 'actionHeroHead'
t.torso_model = 'actionHeroTorso'
t.pelvis_model = 'actionHeroPelvis'
t.upper_arm_model = 'actionHeroUpperArm'
t.forearm_model = 'actionHeroForeArm'
t.hand_model = 'actionHeroHand'
t.upper_leg_model = 'actionHeroUpperLeg'
t.lower_leg_model = 'actionHeroLowerLeg'
t.toes_model = 'actionHeroToes'
action_hero_sounds = [
'actionHero1',
'actionHero2',
'actionHero3',
'actionHero4',
]
action_hero_hit_sounds = ['actionHeroHit1', 'actionHeroHit2']
t.attack_sounds = action_hero_sounds
t.jump_sounds = action_hero_sounds
t.impact_sounds = action_hero_hit_sounds
t.death_sounds = ['actionHeroDeath']
t.pickup_sounds = action_hero_sounds
t.fall_sounds = ['actionHeroFall']
t.style = 'spaz'
# Assassin ###################################
t = Appearance('Zola')
t.color_texture = 'assassinColor'
t.color_mask_texture = 'assassinColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'assassinIcon'
t.icon_mask_texture = 'assassinIconColorMask'
t.head_model = 'assassinHead'
t.torso_model = 'assassinTorso'
t.pelvis_model = 'assassinPelvis'
t.upper_arm_model = 'assassinUpperArm'
t.forearm_model = 'assassinForeArm'
t.hand_model = 'assassinHand'
t.upper_leg_model = 'assassinUpperLeg'
t.lower_leg_model = 'assassinLowerLeg'
t.toes_model = 'assassinToes'
assassin_sounds = ['assassin1', 'assassin2', 'assassin3', 'assassin4']
assassin_hit_sounds = ['assassinHit1', 'assassinHit2']
t.attack_sounds = assassin_sounds
t.jump_sounds = assassin_sounds
t.impact_sounds = assassin_hit_sounds
t.death_sounds = ['assassinDeath']
t.pickup_sounds = assassin_sounds
t.fall_sounds = ['assassinFall']
t.style = 'spaz'
# Wizard ###################################
t = Appearance('Grumbledorf')
t.color_texture = 'wizardColor'
t.color_mask_texture = 'wizardColorMask'
t.default_color = (0.2, 0.4, 1.0)
t.default_highlight = (0.06, 0.15, 0.4)
t.icon_texture = 'wizardIcon'
t.icon_mask_texture = 'wizardIconColorMask'
t.head_model = 'wizardHead'
t.torso_model = 'wizardTorso'
t.pelvis_model = 'wizardPelvis'
t.upper_arm_model = 'wizardUpperArm'
t.forearm_model = 'wizardForeArm'
t.hand_model = 'wizardHand'
t.upper_leg_model = 'wizardUpperLeg'
t.lower_leg_model = 'wizardLowerLeg'
t.toes_model = 'wizardToes'
wizard_sounds = ['wizard1', 'wizard2', 'wizard3', 'wizard4']
wizard_hit_sounds = ['wizardHit1', 'wizardHit2']
t.attack_sounds = wizard_sounds
t.jump_sounds = wizard_sounds
t.impact_sounds = wizard_hit_sounds
t.death_sounds = ['wizardDeath']
t.pickup_sounds = wizard_sounds
t.fall_sounds = ['wizardFall']
t.style = 'spaz'
# Cowboy ###################################
t = Appearance('Butch')
t.color_texture = 'cowboyColor'
t.color_mask_texture = 'cowboyColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'cowboyIcon'
t.icon_mask_texture = 'cowboyIconColorMask'
t.head_model = 'cowboyHead'
t.torso_model = 'cowboyTorso'
t.pelvis_model = 'cowboyPelvis'
t.upper_arm_model = 'cowboyUpperArm'
t.forearm_model = 'cowboyForeArm'
t.hand_model = 'cowboyHand'
t.upper_leg_model = 'cowboyUpperLeg'
t.lower_leg_model = 'cowboyLowerLeg'
t.toes_model = 'cowboyToes'
cowboy_sounds = ['cowboy1', 'cowboy2', 'cowboy3', 'cowboy4']
cowboy_hit_sounds = ['cowboyHit1', 'cowboyHit2']
t.attack_sounds = cowboy_sounds
t.jump_sounds = cowboy_sounds
t.impact_sounds = cowboy_hit_sounds
t.death_sounds = ['cowboyDeath']
t.pickup_sounds = cowboy_sounds
t.fall_sounds = ['cowboyFall']
t.style = 'spaz'
# Witch ###################################
t = Appearance('Witch')
t.color_texture = 'witchColor'
t.color_mask_texture = 'witchColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'witchIcon'
t.icon_mask_texture = 'witchIconColorMask'
t.head_model = 'witchHead'
t.torso_model = 'witchTorso'
t.pelvis_model = 'witchPelvis'
t.upper_arm_model = 'witchUpperArm'
t.forearm_model = 'witchForeArm'
t.hand_model = 'witchHand'
t.upper_leg_model = 'witchUpperLeg'
t.lower_leg_model = 'witchLowerLeg'
t.toes_model = 'witchToes'
witch_sounds = ['witch1', 'witch2', 'witch3', 'witch4']
witch_hit_sounds = ['witchHit1', 'witchHit2']
t.attack_sounds = witch_sounds
t.jump_sounds = witch_sounds
t.impact_sounds = witch_hit_sounds
t.death_sounds = ['witchDeath']
t.pickup_sounds = witch_sounds
t.fall_sounds = ['witchFall']
t.style = 'spaz'
# Warrior ###################################
t = Appearance('Warrior')
t.color_texture = 'warriorColor'
t.color_mask_texture = 'warriorColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'warriorIcon'
t.icon_mask_texture = 'warriorIconColorMask'
t.head_model = 'warriorHead'
t.torso_model = 'warriorTorso'
t.pelvis_model = 'warriorPelvis'
t.upper_arm_model = 'warriorUpperArm'
t.forearm_model = 'warriorForeArm'
t.hand_model = 'warriorHand'
t.upper_leg_model = 'warriorUpperLeg'
t.lower_leg_model = 'warriorLowerLeg'
t.toes_model = 'warriorToes'
warrior_sounds = ['warrior1', 'warrior2', 'warrior3', 'warrior4']
warrior_hit_sounds = ['warriorHit1', 'warriorHit2']
t.attack_sounds = warrior_sounds
t.jump_sounds = warrior_sounds
t.impact_sounds = warrior_hit_sounds
t.death_sounds = ['warriorDeath']
t.pickup_sounds = warrior_sounds
t.fall_sounds = ['warriorFall']
t.style = 'spaz'
# Superhero ###################################
t = Appearance('Middle-Man')
t.color_texture = 'superheroColor'
t.color_mask_texture = 'superheroColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'superheroIcon'
t.icon_mask_texture = 'superheroIconColorMask'
t.head_model = 'superheroHead'
t.torso_model = 'superheroTorso'
t.pelvis_model = 'superheroPelvis'
t.upper_arm_model = 'superheroUpperArm'
t.forearm_model = 'superheroForeArm'
t.hand_model = 'superheroHand'
t.upper_leg_model = 'superheroUpperLeg'
t.lower_leg_model = 'superheroLowerLeg'
t.toes_model = 'superheroToes'
superhero_sounds = ['superhero1', 'superhero2', 'superhero3', 'superhero4']
superhero_hit_sounds = ['superheroHit1', 'superheroHit2']
t.attack_sounds = superhero_sounds
t.jump_sounds = superhero_sounds
t.impact_sounds = superhero_hit_sounds
t.death_sounds = ['superheroDeath']
t.pickup_sounds = superhero_sounds
t.fall_sounds = ['superheroFall']
t.style = 'spaz'
# Alien ###################################
t = Appearance('Alien')
t.color_texture = 'alienColor'
t.color_mask_texture = 'alienColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'alienIcon'
t.icon_mask_texture = 'alienIconColorMask'
t.head_model = 'alienHead'
t.torso_model = 'alienTorso'
t.pelvis_model = 'alienPelvis'
t.upper_arm_model = 'alienUpperArm'
t.forearm_model = 'alienForeArm'
t.hand_model = 'alienHand'
t.upper_leg_model = 'alienUpperLeg'
t.lower_leg_model = 'alienLowerLeg'
t.toes_model = 'alienToes'
alien_sounds = ['alien1', 'alien2', 'alien3', 'alien4']
alien_hit_sounds = ['alienHit1', 'alienHit2']
t.attack_sounds = alien_sounds
t.jump_sounds = alien_sounds
t.impact_sounds = alien_hit_sounds
t.death_sounds = ['alienDeath']
t.pickup_sounds = alien_sounds
t.fall_sounds = ['alienFall']
t.style = 'spaz'
# OldLady ###################################
t = Appearance('OldLady')
t.color_texture = 'oldLadyColor'
t.color_mask_texture = 'oldLadyColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'oldLadyIcon'
t.icon_mask_texture = 'oldLadyIconColorMask'
t.head_model = 'oldLadyHead'
t.torso_model = 'oldLadyTorso'
t.pelvis_model = 'oldLadyPelvis'
t.upper_arm_model = 'oldLadyUpperArm'
t.forearm_model = 'oldLadyForeArm'
t.hand_model = 'oldLadyHand'
t.upper_leg_model = 'oldLadyUpperLeg'
t.lower_leg_model = 'oldLadyLowerLeg'
t.toes_model = 'oldLadyToes'
old_lady_sounds = ['oldLady1', 'oldLady2', 'oldLady3', 'oldLady4']
old_lady_hit_sounds = ['oldLadyHit1', 'oldLadyHit2']
t.attack_sounds = old_lady_sounds
t.jump_sounds = old_lady_sounds
t.impact_sounds = old_lady_hit_sounds
t.death_sounds = ['oldLadyDeath']
t.pickup_sounds = old_lady_sounds
t.fall_sounds = ['oldLadyFall']
t.style = 'spaz'
# Gladiator ###################################
t = Appearance('Gladiator')
t.color_texture = 'gladiatorColor'
t.color_mask_texture = 'gladiatorColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'gladiatorIcon'
t.icon_mask_texture = 'gladiatorIconColorMask'
t.head_model = 'gladiatorHead'
t.torso_model = 'gladiatorTorso'
t.pelvis_model = 'gladiatorPelvis'
t.upper_arm_model = 'gladiatorUpperArm'
t.forearm_model = 'gladiatorForeArm'
t.hand_model = 'gladiatorHand'
t.upper_leg_model = 'gladiatorUpperLeg'
t.lower_leg_model = 'gladiatorLowerLeg'
t.toes_model = 'gladiatorToes'
gladiator_sounds = ['gladiator1', 'gladiator2', 'gladiator3', 'gladiator4']
gladiator_hit_sounds = ['gladiatorHit1', 'gladiatorHit2']
t.attack_sounds = gladiator_sounds
t.jump_sounds = gladiator_sounds
t.impact_sounds = gladiator_hit_sounds
t.death_sounds = ['gladiatorDeath']
t.pickup_sounds = gladiator_sounds
t.fall_sounds = ['gladiatorFall']
t.style = 'spaz'
# Wrestler ###################################
t = Appearance('Wrestler')
t.color_texture = 'wrestlerColor'
t.color_mask_texture = 'wrestlerColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'wrestlerIcon'
t.icon_mask_texture = 'wrestlerIconColorMask'
t.head_model = 'wrestlerHead'
t.torso_model = 'wrestlerTorso'
t.pelvis_model = 'wrestlerPelvis'
t.upper_arm_model = 'wrestlerUpperArm'
t.forearm_model = 'wrestlerForeArm'
t.hand_model = 'wrestlerHand'
t.upper_leg_model = 'wrestlerUpperLeg'
t.lower_leg_model = 'wrestlerLowerLeg'
t.toes_model = 'wrestlerToes'
wrestler_sounds = ['wrestler1', 'wrestler2', 'wrestler3', 'wrestler4']
wrestler_hit_sounds = ['wrestlerHit1', 'wrestlerHit2']
t.attack_sounds = wrestler_sounds
t.jump_sounds = wrestler_sounds
t.impact_sounds = wrestler_hit_sounds
t.death_sounds = ['wrestlerDeath']
t.pickup_sounds = wrestler_sounds
t.fall_sounds = ['wrestlerFall']
t.style = 'spaz'
# OperaSinger ###################################
t = Appearance('Gretel')
t.color_texture = 'operaSingerColor'
t.color_mask_texture = 'operaSingerColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'operaSingerIcon'
t.icon_mask_texture = 'operaSingerIconColorMask'
t.head_model = 'operaSingerHead'
t.torso_model = 'operaSingerTorso'
t.pelvis_model = 'operaSingerPelvis'
t.upper_arm_model = 'operaSingerUpperArm'
t.forearm_model = 'operaSingerForeArm'
t.hand_model = 'operaSingerHand'
t.upper_leg_model = 'operaSingerUpperLeg'
t.lower_leg_model = 'operaSingerLowerLeg'
t.toes_model = 'operaSingerToes'
opera_singer_sounds = [
'operaSinger1',
'operaSinger2',
'operaSinger3',
'operaSinger4',
]
opera_singer_hit_sounds = ['operaSingerHit1', 'operaSingerHit2']
t.attack_sounds = opera_singer_sounds
t.jump_sounds = opera_singer_sounds
t.impact_sounds = opera_singer_hit_sounds
t.death_sounds = ['operaSingerDeath']
t.pickup_sounds = opera_singer_sounds
t.fall_sounds = ['operaSingerFall']
t.style = 'spaz'
# Pixie ###################################
t = Appearance('Pixel')
t.color_texture = 'pixieColor'
t.color_mask_texture = 'pixieColorMask'
t.default_color = (0, 1, 0.7)
t.default_highlight = (0.65, 0.35, 0.75)
t.icon_texture = 'pixieIcon'
t.icon_mask_texture = 'pixieIconColorMask'
t.head_model = 'pixieHead'
t.torso_model = 'pixieTorso'
t.pelvis_model = 'pixiePelvis'
t.upper_arm_model = 'pixieUpperArm'
t.forearm_model = 'pixieForeArm'
t.hand_model = 'pixieHand'
t.upper_leg_model = 'pixieUpperLeg'
t.lower_leg_model = 'pixieLowerLeg'
t.toes_model = 'pixieToes'
pixie_sounds = ['pixie1', 'pixie2', 'pixie3', 'pixie4']
pixie_hit_sounds = ['pixieHit1', 'pixieHit2']
t.attack_sounds = pixie_sounds
t.jump_sounds = pixie_sounds
t.impact_sounds = pixie_hit_sounds
t.death_sounds = ['pixieDeath']
t.pickup_sounds = pixie_sounds
t.fall_sounds = ['pixieFall']
t.style = 'pixie'
# Robot ###################################
t = Appearance('Robot')
t.color_texture = 'robotColor'
t.color_mask_texture = 'robotColorMask'
t.default_color = (0.3, 0.5, 0.8)
t.default_highlight = (1, 0, 0)
t.icon_texture = 'robotIcon'
t.icon_mask_texture = 'robotIconColorMask'
t.head_model = 'robotHead'
t.torso_model = 'robotTorso'
t.pelvis_model = 'robotPelvis'
t.upper_arm_model = 'robotUpperArm'
t.forearm_model = 'robotForeArm'
t.hand_model = 'robotHand'
t.upper_leg_model = 'robotUpperLeg'
t.lower_leg_model = 'robotLowerLeg'
t.toes_model = 'robotToes'
robot_sounds = ['robot1', 'robot2', 'robot3', 'robot4']
robot_hit_sounds = ['robotHit1', 'robotHit2']
t.attack_sounds = robot_sounds
t.jump_sounds = robot_sounds
t.impact_sounds = robot_hit_sounds
t.death_sounds = ['robotDeath']
t.pickup_sounds = robot_sounds
t.fall_sounds = ['robotFall']
t.style = 'spaz'
# Bunny ###################################
t = Appearance('Easter Bunny')
t.color_texture = 'bunnyColor'
t.color_mask_texture = 'bunnyColorMask'
t.default_color = (1, 1, 1)
t.default_highlight = (1, 0.5, 0.5)
t.icon_texture = 'bunnyIcon'
t.icon_mask_texture = 'bunnyIconColorMask'
t.head_model = 'bunnyHead'
t.torso_model = 'bunnyTorso'
t.pelvis_model = 'bunnyPelvis'
t.upper_arm_model = 'bunnyUpperArm'
t.forearm_model = 'bunnyForeArm'
t.hand_model = 'bunnyHand'
t.upper_leg_model = 'bunnyUpperLeg'
t.lower_leg_model = 'bunnyLowerLeg'
t.toes_model = 'bunnyToes'
bunny_sounds = ['bunny1', 'bunny2', 'bunny3', 'bunny4']
bunny_hit_sounds = ['bunnyHit1', 'bunnyHit2']
t.attack_sounds = bunny_sounds
t.jump_sounds = ['bunnyJump']
t.impact_sounds = bunny_hit_sounds
t.death_sounds = ['bunnyDeath']
t.pickup_sounds = bunny_sounds
t.fall_sounds = ['bunnyFall']
t.style = 'bunny'

1120
dist/ba_data/python/bastd/actor/spazbot.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,307 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides a factory object from creating Spazzes."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class SpazFactory:
"""Wraps up media and other resources used by ba.Spaz instances.
Category: **Gameplay Classes**
Generally one of these is created per ba.Activity and shared
between all spaz instances. Use ba.Spaz.get_factory() to return
the shared factory for the current activity.
"""
impact_sounds_medium: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something kinda hard."""
impact_sounds_hard: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something really hard."""
impact_sounds_harder: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something really
really hard."""
single_player_death_sound: ba.Sound
"""The sound that plays for an 'important' spaz death such as in
co-op games."""
punch_sound_weak: ba.Sound
"""A weak punch ba.Sound."""
punch_sound: ba.Sound
"""A standard punch ba.Sound."""
punch_sound_strong: Sequence[ba.Sound]
"""A tuple of stronger sounding punch ba.Sounds."""
punch_sound_stronger: ba.Sound
"""A really really strong sounding punch ba.Sound."""
swish_sound: ba.Sound
"""A punch swish ba.Sound."""
block_sound: ba.Sound
"""A ba.Sound for when an attack is blocked by invincibility."""
shatter_sound: ba.Sound
"""A ba.Sound for when a frozen ba.Spaz shatters."""
splatter_sound: ba.Sound
"""A ba.Sound for when a ba.Spaz blows up via curse."""
spaz_material: ba.Material
"""A ba.Material applied to all of parts of a ba.Spaz."""
roller_material: ba.Material
"""A ba.Material applied to the invisible roller ball body that
a ba.Spaz uses for locomotion."""
punch_material: ba.Material
"""A ba.Material applied to the 'fist' of a ba.Spaz."""
pickup_material: ba.Material
"""A ba.Material applied to the 'grabber' body of a ba.Spaz."""
curse_material: ba.Material
"""A ba.Material applied to a cursed ba.Spaz that triggers an explosion."""
_STORENAME = ba.storagename()
def _preload(self, character: str) -> None:
"""Preload media needed for a given character."""
self.get_media(character)
def __init__(self) -> None:
"""Instantiate a factory object."""
# pylint: disable=cyclic-import
# FIXME: should probably put these somewhere common so we don't
# have to import them from a module that imports us.
from bastd.actor.spaz import (
PickupMessage,
PunchHitMessage,
CurseExplodeMessage,
)
shared = SharedObjects.get()
self.impact_sounds_medium = (
ba.getsound('impactMedium'),
ba.getsound('impactMedium2'),
)
self.impact_sounds_hard = (
ba.getsound('impactHard'),
ba.getsound('impactHard2'),
ba.getsound('impactHard3'),
)
self.impact_sounds_harder = (
ba.getsound('bigImpact'),
ba.getsound('bigImpact2'),
)
self.single_player_death_sound = ba.getsound('playerDeath')
self.punch_sound_weak = ba.getsound('punchWeak01')
self.punch_sound = ba.getsound('punch01')
self.punch_sound_strong = (
ba.getsound('punchStrong01'),
ba.getsound('punchStrong02'),
)
self.punch_sound_stronger = ba.getsound('superPunch')
self.swish_sound = ba.getsound('punchSwish')
self.block_sound = ba.getsound('block')
self.shatter_sound = ba.getsound('shatter')
self.splatter_sound = ba.getsound('splatter')
self.spaz_material = ba.Material()
self.roller_material = ba.Material()
self.punch_material = ba.Material()
self.pickup_material = ba.Material()
self.curse_material = ba.Material()
footing_material = shared.footing_material
object_material = shared.object_material
player_material = shared.player_material
region_material = shared.region_material
# Send footing messages to spazzes so they know when they're on
# solid ground.
# Eww; this probably should just be built into the spaz node.
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('message', 'our_node', 'at_connect', 'footing', 1),
('message', 'our_node', 'at_disconnect', 'footing', -1),
),
)
# Punches.
self.punch_material.add_actions(
conditions=('they_are_different_node_than_us',),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PunchHitMessage()),
),
)
# Pickups.
self.pickup_material.add_actions(
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', object_material),
),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('message', 'our_node', 'at_connect', PickupMessage()),
),
)
# Curse.
self.curse_material.add_actions(
conditions=(
('they_are_different_node_than_us',),
'and',
('they_have_material', player_material),
),
actions=(
'message',
'our_node',
'at_connect',
CurseExplodeMessage(),
),
)
self.foot_impact_sounds = (
ba.getsound('footImpact01'),
ba.getsound('footImpact02'),
ba.getsound('footImpact03'),
)
self.foot_skid_sound = ba.getsound('skid01')
self.foot_roll_sound = ba.getsound('scamper01')
self.roller_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('impact_sound', self.foot_impact_sounds, 1, 0.2),
('skid_sound', self.foot_skid_sound, 20, 0.3),
('roll_sound', self.foot_roll_sound, 20, 3.0),
),
)
self.skid_sound = ba.getsound('gravelSkid')
self.spaz_material.add_actions(
conditions=('they_have_material', footing_material),
actions=(
('impact_sound', self.foot_impact_sounds, 20, 6),
('skid_sound', self.skid_sound, 2.0, 1),
('roll_sound', self.skid_sound, 2.0, 1),
),
)
self.shield_up_sound = ba.getsound('shieldUp')
self.shield_down_sound = ba.getsound('shieldDown')
self.shield_hit_sound = ba.getsound('shieldHit')
# We don't want to collide with stuff we're initially overlapping
# (unless its marked with a special region material).
self.spaz_material.add_actions(
conditions=(
(
('we_are_younger_than', 51),
'and',
('they_are_different_node_than_us',),
),
'and',
('they_dont_have_material', region_material),
),
actions=('modify_node_collision', 'collide', False),
)
self.spaz_media: dict[str, Any] = {}
# Lets load some basic rules.
# (allows them to be tweaked from the master server)
self.shield_decay_rate = ba.internal.get_v1_account_misc_read_val(
'rsdr', 10.0
)
self.punch_cooldown = ba.internal.get_v1_account_misc_read_val(
'rpc', 400
)
self.punch_cooldown_gloves = ba.internal.get_v1_account_misc_read_val(
'rpcg', 300
)
self.punch_power_scale = ba.internal.get_v1_account_misc_read_val(
'rpp', 1.2
)
self.punch_power_scale_gloves = (
ba.internal.get_v1_account_misc_read_val('rppg', 1.4)
)
self.max_shield_spillover_damage = (
ba.internal.get_v1_account_misc_read_val('rsms', 500)
)
def get_style(self, character: str) -> str:
"""Return the named style for this character.
(this influences subtle aspects of their appearance, etc)
"""
return ba.app.spaz_appearances[character].style
def get_media(self, character: str) -> dict[str, Any]:
"""Return the set of media used by this variant of spaz."""
char = ba.app.spaz_appearances[character]
if character not in self.spaz_media:
media = self.spaz_media[character] = {
'jump_sounds': [ba.getsound(s) for s in char.jump_sounds],
'attack_sounds': [ba.getsound(s) for s in char.attack_sounds],
'impact_sounds': [ba.getsound(s) for s in char.impact_sounds],
'death_sounds': [ba.getsound(s) for s in char.death_sounds],
'pickup_sounds': [ba.getsound(s) for s in char.pickup_sounds],
'fall_sounds': [ba.getsound(s) for s in char.fall_sounds],
'color_texture': ba.gettexture(char.color_texture),
'color_mask_texture': ba.gettexture(char.color_mask_texture),
'head_model': ba.getmodel(char.head_model),
'torso_model': ba.getmodel(char.torso_model),
'pelvis_model': ba.getmodel(char.pelvis_model),
'upper_arm_model': ba.getmodel(char.upper_arm_model),
'forearm_model': ba.getmodel(char.forearm_model),
'hand_model': ba.getmodel(char.hand_model),
'upper_leg_model': ba.getmodel(char.upper_leg_model),
'lower_leg_model': ba.getmodel(char.lower_leg_model),
'toes_model': ba.getmodel(char.toes_model),
}
else:
media = self.spaz_media[character]
return media
@classmethod
def get(cls) -> SpazFactory:
"""Return the shared ba.SpazFactory, creating it if necessary."""
# pylint: disable=cyclic-import
activity = ba.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
factory = activity.customdata[cls._STORENAME] = SpazFactory()
assert isinstance(factory, SpazFactory)
return factory

230
dist/ba_data/python/bastd/actor/text.py vendored Normal file
View file

@ -0,0 +1,230 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines Actor(s)."""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class Text(ba.Actor):
"""Text with some tricks."""
class Transition(Enum):
"""Transition types for text."""
FADE_IN = 'fade_in'
IN_RIGHT = 'in_right'
IN_LEFT = 'in_left'
IN_BOTTOM = 'in_bottom'
IN_BOTTOM_SLOW = 'in_bottom_slow'
IN_TOP_SLOW = 'in_top_slow'
class HAlign(Enum):
"""Horizontal alignment type."""
LEFT = 'left'
CENTER = 'center'
RIGHT = 'right'
class VAlign(Enum):
"""Vertical alignment type."""
NONE = 'none'
CENTER = 'center'
class HAttach(Enum):
"""Horizontal attach type."""
LEFT = 'left'
CENTER = 'center'
RIGHT = 'right'
class VAttach(Enum):
"""Vertical attach type."""
BOTTOM = 'bottom'
CENTER = 'center'
TOP = 'top'
def __init__(
self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
h_align: HAlign = HAlign.LEFT,
v_align: VAlign = VAlign.NONE,
color: Sequence[float] = (1.0, 1.0, 1.0, 1.0),
transition: Transition | None = None,
transition_delay: float = 0.0,
flash: bool = False,
v_attach: VAttach = VAttach.CENTER,
h_attach: HAttach = HAttach.CENTER,
scale: float = 1.0,
transition_out_delay: float | None = None,
maxwidth: float | None = None,
shadow: float = 0.5,
flatness: float = 0.0,
vr_depth: float = 0.0,
host_only: bool = False,
front: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
super().__init__()
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'text': text,
'color': color,
'position': position,
'h_align': h_align.value,
'vr_depth': vr_depth,
'v_align': v_align.value,
'h_attach': h_attach.value,
'v_attach': v_attach.value,
'shadow': shadow,
'flatness': flatness,
'maxwidth': 0.0 if maxwidth is None else maxwidth,
'host_only': host_only,
'front': front,
'scale': scale,
},
)
if transition is self.Transition.FADE_IN:
if flash:
raise RuntimeError(
'fixme: flash and fade-in currently cant both be on'
)
cmb = ba.newnode(
'combine',
owner=self.node,
attrs={
'input0': color[0],
'input1': color[1],
'input2': color[2],
'size': 4,
},
)
keys = {transition_delay: 0.0, transition_delay + 0.5: color[3]}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = color[3]
keys[transition_delay + transition_out_delay + 0.5] = 0.0
ba.animate(cmb, 'input3', keys)
cmb.connectattr('output', self.node, 'color')
if flash:
mult = 2.0
tm1 = 0.15
tm2 = 0.3
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 4})
ba.animate(
cmb,
'input0',
{0.0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
loop=True,
)
ba.animate(
cmb,
'input1',
{0.0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
loop=True,
)
ba.animate(
cmb,
'input2',
{0.0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
loop=True,
)
cmb.input3 = color[3]
cmb.connectattr('output', self.node, 'color')
cmb = self.position_combine = ba.newnode(
'combine', owner=self.node, attrs={'size': 2}
)
if transition is self.Transition.IN_RIGHT:
keys = {
transition_delay: position[0] + 1300,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_LEFT:
keys = {
transition_delay: position[0] - 1300,
transition_delay + 0.2: position[0],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[0]
keys[transition_delay + transition_out_delay + 0.2] = (
position[0] - 1300.0
)
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
ba.animate(cmb, 'input0', keys)
cmb.input1 = position[1]
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM_SLOW:
keys = {
transition_delay: -100.0,
transition_delay + 1.0: position[1],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.2: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_BOTTOM:
keys = {
transition_delay: -100.0,
transition_delay + 0.2: position[1],
}
o_keys = {transition_delay: 0.0, transition_delay + 0.05: 1.0}
if transition_out_delay is not None:
keys[transition_delay + transition_out_delay] = position[1]
keys[transition_delay + transition_out_delay + 0.2] = -100.0
o_keys[transition_delay + transition_out_delay + 0.15] = 1.0
o_keys[transition_delay + transition_out_delay + 0.2] = 0.0
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
elif transition is self.Transition.IN_TOP_SLOW:
keys = {
transition_delay: 400.0,
transition_delay + 3.5: position[1],
}
o_keys = {transition_delay: 0, transition_delay + 1.0: 1.0}
cmb.input0 = position[0]
ba.animate(cmb, 'input1', keys)
ba.animate(self.node, 'opacity', o_keys)
else:
assert transition is self.Transition.FADE_IN or transition is None
cmb.input0 = position[0]
cmb.input1 = position[1]
cmb.connectattr('output', self.node, 'position')
# If we're transitioning out, die at the end of it.
if transition_out_delay is not None:
ba.timer(
transition_delay + transition_out_delay + 1.0,
ba.WeakCall(self.handlemessage, ba.DieMessage()),
)
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
return None
return super().handlemessage(msg)

View file

@ -0,0 +1,101 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides tip related Actor(s)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any
class TipsText(ba.Actor):
"""A bit of text showing various helpful game tips."""
def __init__(self, offs_y: float = 100.0):
super().__init__()
self._tip_scale = 0.8
self._tip_title_scale = 1.1
self._offs_y = offs_y
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'text': '',
'scale': self._tip_scale,
'h_align': 'left',
'maxwidth': 800,
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
tval = ba.Lstr(
value='${A}:', subs=[('${A}', ba.Lstr(resource='tipText'))]
)
self.title_node = ba.newnode(
'text',
delegate=self,
attrs={
'text': tval,
'scale': self._tip_title_scale,
'maxwidth': 122,
'h_align': 'right',
'vr_depth': -20,
'v_align': 'center',
'v_attach': 'bottom',
},
)
self._message_duration = 10000
self._message_spacing = 3000
self._change_timer = ba.Timer(
0.001 * (self._message_duration + self._message_spacing),
ba.WeakCall(self.change_phrase),
repeat=True,
)
self._combine = ba.newnode(
'combine',
owner=self.node,
attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4},
)
self._combine.connectattr('output', self.node, 'color')
self._combine.connectattr('output', self.title_node, 'color')
self.change_phrase()
def change_phrase(self) -> None:
"""Switch the visible tip phrase."""
from ba.internal import get_remote_app_name, get_next_tip
next_tip = ba.Lstr(
translate=('tips', get_next_tip()),
subs=[('${REMOTE_APP_NAME}', get_remote_app_name())],
)
spc = self._message_spacing
assert self.node
self.node.position = (-200, self._offs_y)
self.title_node.position = (-220, self._offs_y + 3)
keys = {
spc: 0,
spc + 1000: 1.0,
spc + self._message_duration - 1000: 1.0,
spc + self._message_duration: 0.0,
}
ba.animate(
self._combine,
'input3',
{k: v * 0.5 for k, v in list(keys.items())},
timeformat=ba.TimeFormat.MILLISECONDS,
)
self.node.text = next_tip
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
self.title_node.delete()
return None
return super().handlemessage(msg)

View file

@ -0,0 +1,209 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defined Actor(s)."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Sequence
class ZoomText(ba.Actor):
"""Big Zooming Text.
Category: Gameplay Classes
Used for things such as the 'BOB WINS' victory messages.
"""
def __init__(
self,
text: str | ba.Lstr,
position: tuple[float, float] = (0.0, 0.0),
shiftposition: tuple[float, float] | None = None,
shiftdelay: float | None = None,
lifespan: float | None = None,
flash: bool = True,
trail: bool = True,
h_align: str = 'center',
color: Sequence[float] = (0.9, 0.4, 0.0),
jitter: float = 0.0,
trailcolor: Sequence[float] = (1.0, 0.35, 0.1, 0.0),
scale: float = 1.0,
project_scale: float = 1.0,
tilt_translate: float = 0.0,
maxwidth: float | None = None,
):
# pylint: disable=too-many-locals
super().__init__()
self._dying = False
positionadjusted = (position[0], position[1] - 100)
if shiftdelay is None:
shiftdelay = 2.500
if shiftdelay < 0.0:
ba.print_error('got shiftdelay < 0')
shiftdelay = 0.0
self._project_scale = project_scale
self.node = ba.newnode(
'text',
delegate=self,
attrs={
'position': positionadjusted,
'big': True,
'text': text,
'trail': trail,
'vr_depth': 0,
'shadow': 0.0 if trail else 0.3,
'scale': scale,
'maxwidth': maxwidth if maxwidth is not None else 0.0,
'tilt_translate': tilt_translate,
'h_align': h_align,
'v_align': 'center',
},
)
# we never jitter in vr mode..
if ba.app.vr_mode:
jitter = 0.0
# if they want jitter, animate its position slightly...
if jitter > 0.0:
self._jitter(positionadjusted, jitter * scale)
# if they want shifting, move to the shift position and
# then resume jittering
if shiftposition is not None:
positionadjusted2 = (shiftposition[0], shiftposition[1] - 100)
ba.timer(
shiftdelay,
ba.WeakCall(self._shift, positionadjusted, positionadjusted2),
)
if jitter > 0.0:
ba.timer(
shiftdelay + 0.25,
ba.WeakCall(
self._jitter, positionadjusted2, jitter * scale
),
)
color_combine = ba.newnode(
'combine',
owner=self.node,
attrs={'input2': color[2], 'input3': 1.0, 'size': 4},
)
if trail:
trailcolor_n = ba.newnode(
'combine',
owner=self.node,
attrs={
'size': 3,
'input0': trailcolor[0],
'input1': trailcolor[1],
'input2': trailcolor[2],
},
)
trailcolor_n.connectattr('output', self.node, 'trailcolor')
basemult = 0.85
ba.animate(
self.node,
'trail_project_scale',
{
0: 0 * project_scale,
basemult * 0.201: 0.6 * project_scale,
basemult * 0.347: 0.8 * project_scale,
basemult * 0.478: 0.9 * project_scale,
basemult * 0.595: 0.93 * project_scale,
basemult * 0.748: 0.95 * project_scale,
basemult * 0.941: 0.95 * project_scale,
},
)
if flash:
mult = 2.0
tm1 = 0.15
tm2 = 0.3
ba.animate(
color_combine,
'input0',
{0: color[0] * mult, tm1: color[0], tm2: color[0] * mult},
loop=True,
)
ba.animate(
color_combine,
'input1',
{0: color[1] * mult, tm1: color[1], tm2: color[1] * mult},
loop=True,
)
ba.animate(
color_combine,
'input2',
{0: color[2] * mult, tm1: color[2], tm2: color[2] * mult},
loop=True,
)
else:
color_combine.input0 = color[0]
color_combine.input1 = color[1]
color_combine.connectattr('output', self.node, 'color')
ba.animate(
self.node,
'project_scale',
{0: 0, 0.27: 1.05 * project_scale, 0.3: 1 * project_scale},
)
# if they give us a lifespan, kill ourself down the line
if lifespan is not None:
ba.timer(lifespan, ba.WeakCall(self.handlemessage, ba.DieMessage()))
def handlemessage(self, msg: Any) -> Any:
assert not self.expired
if isinstance(msg, ba.DieMessage):
if not self._dying and self.node:
self._dying = True
if msg.immediate:
self.node.delete()
else:
ba.animate(
self.node,
'project_scale',
{
0.0: 1 * self._project_scale,
0.6: 1.2 * self._project_scale,
},
)
ba.animate(self.node, 'opacity', {0.0: 1, 0.3: 0})
ba.animate(self.node, 'trail_opacity', {0.0: 1, 0.6: 0})
ba.timer(0.7, self.node.delete)
return None
return super().handlemessage(msg)
def _jitter(
self, position: tuple[float, float], jitter_amount: float
) -> None:
if not self.node:
return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})
for index, attr in enumerate(['input0', 'input1']):
keys = {}
timeval = 0.0
# gen some random keys for that stop-motion-y look
for _i in range(10):
keys[timeval] = (
position[index]
+ (random.random() - 0.5) * jitter_amount * 1.6
)
timeval += random.random() * 0.1
ba.animate(cmb, attr, keys, loop=True)
cmb.connectattr('output', self.node, 'position')
def _shift(
self, position1: tuple[float, float], position2: tuple[float, float]
) -> None:
if not self.node:
return
cmb = ba.newnode('combine', owner=self.node, attrs={'size': 2})
ba.animate(cmb, 'input0', {0.0: position1[0], 0.25: position2[0]})
ba.animate(cmb, 'input1', {0.0: position1[1], 0.25: position2[1]})
cmb.connectattr('output', self.node, 'position')

View file

@ -0,0 +1,37 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provide our delegate for high level app functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
if TYPE_CHECKING:
from typing import Any, Callable
class AppDelegate(ba.AppDelegate):
"""Defines handlers for high level app functionality."""
def create_default_game_settings_ui(
self,
gameclass: type[ba.GameActivity],
sessiontype: type[ba.Session],
settings: dict | None,
completion_call: Callable[[dict | None], Any],
) -> None:
"""(internal)"""
# Replace the main window once we come up successfully.
from bastd.ui.playlist.editgame import PlaylistEditGameWindow
ba.app.ui.clear_main_menu_window(transition='out_left')
ba.app.ui.set_main_menu_window(
PlaylistEditGameWindow(
gameclass,
sessiontype,
settings,
completion_call=completion_call,
).get_root_widget()
)

View file

@ -0,0 +1 @@
# Released under the MIT License. See LICENSE for details.

View file

@ -0,0 +1,262 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines assault minigame."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.flag import Flag
from bastd.actor.scoreboard import Scoreboard
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, base_pos: Sequence[float], flag: Flag) -> None:
self.base_pos = base_pos
self.flag = flag
self.score = 0
# ba_meta export game
class AssaultGame(ba.TeamGameActivity[Player, Team]):
"""Game where you score by touching the other team's flag."""
name = 'Assault'
description = 'Reach the enemy flag to score.'
available_settings = [
ba.IntSetting(
'Score to Win',
min_value=1,
default=3,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('team_flag')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._last_score_time = 0.0
self._score_sound = ba.getsound('score')
self._base_region_materials: dict[int, ba.Material] = {}
self._epic_mode = bool(settings['Epic Mode'])
self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
# Base class overrides
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH
)
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
return 'Touch the enemy flag.'
return 'Touch the enemy flag ${ARG1} times.', self._score_to_win
def get_instance_description_short(self) -> str | Sequence:
if self._score_to_win == 1:
return 'touch 1 flag'
return 'touch ${ARG1} flags', self._score_to_win
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
shared = SharedObjects.get()
base_pos = self.map.get_flag_position(sessionteam.id)
ba.newnode(
'light',
attrs={
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': sessionteam.color,
},
)
Flag.project_stand(base_pos)
flag = Flag(touchable=False, position=base_pos, color=sessionteam.color)
team = Team(base_pos=base_pos, flag=flag)
mat = self._base_region_materials[sessionteam.id] = ba.Material()
mat.add_actions(
conditions=('they_have_material', shared.player_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
(
'call',
'at_connect',
ba.Call(self._handle_base_collide, team),
),
),
)
ba.newnode(
'region',
owner=flag.node,
attrs={
'position': (base_pos[0], base_pos[1] + 0.75, base_pos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [self._base_region_materials[sessionteam.id]],
},
)
return team
def on_team_join(self, team: Team) -> None:
# Can't do this in create_team because the team's color/etc. have
# not been wired up yet at that point.
self._update_scoreboard()
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard.
self.respawn_player(msg.getplayer(Player))
else:
super().handlemessage(msg)
def _flash_base(self, team: Team, length: float = 2.0) -> None:
light = ba.newnode(
'light',
attrs={
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color,
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
ba.timer(length, light.delete)
def _handle_base_collide(self, team: Team) -> None:
try:
spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
return
if not spaz.is_alive():
return
try:
player = spaz.getplayer(Player, True)
except ba.NotFoundError:
return
# If its another team's player, they scored.
player_team = player.team
if player_team is not team:
# Prevent multiple simultaneous scores.
if ba.time() != self._last_score_time:
self._last_score_time = ba.time()
self.stats.player_scored(player, 50, big_message=True)
ba.playsound(self._score_sound)
self._flash_base(team)
# Move all players on the scoring team back to their start
# and add flashes of light so its noticeable.
for player in player_team.players:
if player.is_alive():
pos = player.node.position
light = ba.newnode(
'light',
attrs={
'position': pos,
'color': player_team.color,
'height_attenuated': False,
'radius': 0.4,
},
)
ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
new_pos = self.map.get_start_position(player_team.id)
light = ba.newnode(
'light',
attrs={
'position': new_pos,
'color': player_team.color,
'radius': 0.4,
'height_attenuated': False,
},
)
ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.5: 0})
if player.actor:
player.actor.handlemessage(
ba.StandMessage(new_pos, random.uniform(0, 360))
)
# Have teammates celebrate.
for player in player_team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
player_team.score += 1
self._update_scoreboard()
if player_team.score >= self._score_to_win:
self.end_game()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)

View file

@ -0,0 +1,613 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines a capture-the-flag game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.flag import (
FlagFactory,
Flag,
FlagPickedUpMessage,
FlagDroppedMessage,
FlagDiedMessage,
)
if TYPE_CHECKING:
from typing import Any, Sequence
class CTFFlag(Flag):
"""Special flag type for CTF games."""
activity: CaptureTheFlagGame
def __init__(self, team: Team):
assert team.flagmaterial is not None
super().__init__(
materials=[team.flagmaterial],
position=team.base_pos,
color=team.color,
)
self._team = team
self.held_count = 0
self.counter = ba.newnode(
'text',
owner=self.node,
attrs={'in_world': True, 'scale': 0.02, 'h_align': 'center'},
)
self.reset_return_times()
self.last_player_to_hold: Player | None = None
self.time_out_respawn_time: int | None = None
self.touch_return_time: float | None = None
def reset_return_times(self) -> None:
"""Clear flag related times in the activity."""
self.time_out_respawn_time = int(self.activity.flag_idle_return_time)
self.touch_return_time = float(self.activity.flag_touch_return_time)
@property
def team(self) -> Team:
"""The flag's team."""
return self._team
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.touching_own_flag = 0
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(
self,
base_pos: Sequence[float],
base_region_material: ba.Material,
base_region: ba.Node,
spaz_material_no_flag_physical: ba.Material,
spaz_material_no_flag_collide: ba.Material,
flagmaterial: ba.Material,
):
self.base_pos = base_pos
self.base_region_material = base_region_material
self.base_region = base_region
self.spaz_material_no_flag_physical = spaz_material_no_flag_physical
self.spaz_material_no_flag_collide = spaz_material_no_flag_collide
self.flagmaterial = flagmaterial
self.score = 0
self.flag_return_touches = 0
self.home_flag_at_base = True
self.touch_return_timer: ba.Timer | None = None
self.enemy_flag_at_base = False
self.flag: CTFFlag | None = None
self.last_flag_leave_time: float | None = None
self.touch_return_timer_ticking: ba.NodeActor | None = None
# ba_meta export game
class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
"""Game of stealing other team's flag and returning it to your base."""
name = 'Capture the Flag'
description = 'Return the enemy flag to score.'
available_settings = [
ba.IntSetting('Score to Win', min_value=1, default=3),
ba.IntSetting(
'Flag Touch Return Time',
min_value=0,
default=0,
increment=1,
),
ba.IntSetting(
'Flag Idle Return Time',
min_value=5,
default=30,
increment=5,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('team_flag')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._alarmsound = ba.getsound('alarm')
self._ticking_sound = ba.getsound('ticking')
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._last_score_time = 0
self._all_bases_material = ba.Material()
self._last_home_flag_notice_print_time = 0.0
self._score_to_win = int(settings['Score to Win'])
self._epic_mode = bool(settings['Epic Mode'])
self._time_limit = float(settings['Time Limit'])
self.flag_touch_return_time = float(settings['Flag Touch Return Time'])
self.flag_idle_return_time = float(settings['Flag Idle Return Time'])
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FLAG_CATCHER
)
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
return 'Steal the enemy flag.'
return 'Steal the enemy flag ${ARG1} times.', self._score_to_win
def get_instance_description_short(self) -> str | Sequence:
if self._score_to_win == 1:
return 'return 1 flag'
return 'return ${ARG1} flags', self._score_to_win
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
# Create our team instance and its initial values.
base_pos = self.map.get_flag_position(sessionteam.id)
Flag.project_stand(base_pos)
ba.newnode(
'light',
attrs={
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': sessionteam.color,
},
)
base_region_mat = ba.Material()
pos = base_pos
base_region = ba.newnode(
'region',
attrs={
'position': (pos[0], pos[1] + 0.75, pos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [base_region_mat, self._all_bases_material],
},
)
spaz_mat_no_flag_physical = ba.Material()
spaz_mat_no_flag_collide = ba.Material()
flagmat = ba.Material()
team = Team(
base_pos=base_pos,
base_region_material=base_region_mat,
base_region=base_region,
spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
flagmaterial=flagmat,
)
# Some parts of our spazzes don't collide physically with our
# flags but generate callbacks.
spaz_mat_no_flag_physical.add_actions(
conditions=('they_have_material', flagmat),
actions=(
('modify_part_collision', 'physical', False),
(
'call',
'at_connect',
lambda: self._handle_touching_own_flag(team, True),
),
(
'call',
'at_disconnect',
lambda: self._handle_touching_own_flag(team, False),
),
),
)
# Other parts of our spazzes don't collide with our flags at all.
spaz_mat_no_flag_collide.add_actions(
conditions=('they_have_material', flagmat),
actions=('modify_part_collision', 'collide', False),
)
# We wanna know when *any* flag enters/leaves our base.
base_region_mat.add_actions(
conditions=('they_have_material', FlagFactory.get().flagmaterial),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
(
'call',
'at_connect',
lambda: self._handle_flag_entered_base(team),
),
(
'call',
'at_disconnect',
lambda: self._handle_flag_left_base(team),
),
),
)
return team
def on_team_join(self, team: Team) -> None:
# Can't do this in create_team because the team's color/etc. have
# not been wired up yet at that point.
self._spawn_flag_for_team(team)
self._update_scoreboard()
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
ba.timer(1.0, call=self._tick, repeat=True)
def _spawn_flag_for_team(self, team: Team) -> None:
team.flag = CTFFlag(team)
team.flag_return_touches = 0
self._flash_base(team, length=1.0)
assert team.flag.node
ba.playsound(self._swipsound, position=team.flag.node.position)
def _handle_flag_entered_base(self, team: Team) -> None:
try:
flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True)
except ba.NotFoundError:
# Don't think this should logically ever happen.
print('Error getting CTFFlag in entering-base callback.')
return
if flag.team is team:
team.home_flag_at_base = True
# If the enemy flag is already here, score!
if team.enemy_flag_at_base:
# And show team name which scored (but actually we could
# show here player who returned enemy flag).
self.show_zoom_message(
ba.Lstr(
resource='nameScoresText', subs=[('${NAME}', team.name)]
),
color=team.color,
)
self._score(team)
else:
team.enemy_flag_at_base = True
if team.home_flag_at_base:
# Award points to whoever was carrying the enemy flag.
player = flag.last_player_to_hold
if player and player.team is team:
assert self.stats
self.stats.player_scored(player, 50, big_message=True)
# Update score and reset flags.
self._score(team)
# If the home-team flag isn't here, print a message to that effect.
else:
# Don't want slo-mo affecting this
curtime = ba.time(ba.TimeType.BASE)
if curtime - self._last_home_flag_notice_print_time > 5.0:
self._last_home_flag_notice_print_time = curtime
bpos = team.base_pos
tval = ba.Lstr(resource='ownFlagAtYourBaseWarning')
tnode = ba.newnode(
'text',
attrs={
'text': tval,
'in_world': True,
'scale': 0.013,
'color': (1, 1, 0, 1),
'h_align': 'center',
'position': (bpos[0], bpos[1] + 3.2, bpos[2]),
},
)
ba.timer(5.1, tnode.delete)
ba.animate(
tnode, 'scale', {0.0: 0, 0.2: 0.013, 4.8: 0.013, 5.0: 0}
)
def _tick(self) -> None:
# If either flag is away from base and not being held, tick down its
# respawn timer.
for team in self.teams:
flag = team.flag
assert flag is not None
if not team.home_flag_at_base and flag.held_count == 0:
time_out_counting_down = True
if flag.time_out_respawn_time is None:
flag.reset_return_times()
assert flag.time_out_respawn_time is not None
flag.time_out_respawn_time -= 1
if flag.time_out_respawn_time <= 0:
flag.handlemessage(ba.DieMessage())
else:
time_out_counting_down = False
if flag.node and flag.counter:
pos = flag.node.position
flag.counter.position = (pos[0], pos[1] + 1.3, pos[2])
# If there's no self-touches on this flag, set its text
# to show its auto-return counter. (if there's self-touches
# its showing that time).
if team.flag_return_touches == 0:
flag.counter.text = (
str(flag.time_out_respawn_time)
if (
time_out_counting_down
and flag.time_out_respawn_time is not None
and flag.time_out_respawn_time <= 10
)
else ''
)
flag.counter.color = (1, 1, 1, 0.5)
flag.counter.scale = 0.014
def _score(self, team: Team) -> None:
team.score += 1
ba.playsound(self._score_sound)
self._flash_base(team)
self._update_scoreboard()
# Have teammates celebrate.
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
# Reset all flags/state.
for reset_team in self.teams:
if not reset_team.home_flag_at_base:
assert reset_team.flag is not None
reset_team.flag.handlemessage(ba.DieMessage())
reset_team.enemy_flag_at_base = False
if team.score >= self._score_to_win:
self.end_game()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results, announce_delay=0.8)
def _handle_flag_left_base(self, team: Team) -> None:
cur_time = ba.time()
try:
flag = ba.getcollision().opposingnode.getdelegate(CTFFlag, True)
except ba.NotFoundError:
# This can happen if the flag stops touching us due to being
# deleted; that's ok.
return
if flag.team is team:
# Check times here to prevent too much flashing.
if (
team.last_flag_leave_time is None
or cur_time - team.last_flag_leave_time > 3.0
):
ba.playsound(self._alarmsound, position=team.base_pos)
self._flash_base(team)
team.last_flag_leave_time = cur_time
team.home_flag_at_base = False
else:
team.enemy_flag_at_base = False
def _touch_return_update(self, team: Team) -> None:
# Count down only while its away from base and not being held.
assert team.flag is not None
if team.home_flag_at_base or team.flag.held_count > 0:
team.touch_return_timer_ticking = None
return # No need to return when its at home.
if team.touch_return_timer_ticking is None:
team.touch_return_timer_ticking = ba.NodeActor(
ba.newnode(
'sound',
attrs={
'sound': self._ticking_sound,
'positional': False,
'loop': True,
},
)
)
flag = team.flag
if flag.touch_return_time is not None:
flag.touch_return_time -= 0.1
if flag.counter:
flag.counter.text = f'{flag.touch_return_time:.1f}'
flag.counter.color = (1, 1, 0, 1)
flag.counter.scale = 0.02
if flag.touch_return_time <= 0.0:
self._award_players_touching_own_flag(team)
flag.handlemessage(ba.DieMessage())
def _award_players_touching_own_flag(self, team: Team) -> None:
for player in team.players:
if player.touching_own_flag > 0:
return_score = 10 + 5 * int(self.flag_touch_return_time)
self.stats.player_scored(
player, return_score, screenmessage=False
)
def _handle_touching_own_flag(self, team: Team, connecting: bool) -> None:
"""Called when a player touches or stops touching their own team flag.
We keep track of when each player is touching their own flag so we
can award points when returned.
"""
player: Player | None
try:
spaz = ba.getcollision().sourcenode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
return
if not spaz.is_alive():
return
player = spaz.getplayer(Player, True)
if player:
player.touching_own_flag += 1 if connecting else -1
# If return-time is zero, just kill it immediately.. otherwise keep
# track of touches and count down.
if float(self.flag_touch_return_time) <= 0.0:
assert team.flag is not None
if (
connecting
and not team.home_flag_at_base
and team.flag.held_count == 0
):
self._award_players_touching_own_flag(team)
ba.getcollision().opposingnode.handlemessage(ba.DieMessage())
# Takes a non-zero amount of time to return.
else:
if connecting:
team.flag_return_touches += 1
if team.flag_return_touches == 1:
team.touch_return_timer = ba.Timer(
0.1,
call=ba.Call(self._touch_return_update, team),
repeat=True,
)
team.touch_return_timer_ticking = None
else:
team.flag_return_touches -= 1
if team.flag_return_touches == 0:
team.touch_return_timer = None
team.touch_return_timer_ticking = None
if team.flag_return_touches < 0:
ba.print_error('CTF flag_return_touches < 0')
def _flash_base(self, team: Team, length: float = 2.0) -> None:
light = ba.newnode(
'light',
attrs={
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color,
},
)
ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
ba.timer(length, light.delete)
def spawn_player_spaz(
self,
player: Player,
position: Sequence[float] | None = None,
angle: float | None = None,
) -> PlayerSpaz:
"""Intercept new spazzes and add our team material for them."""
spaz = super().spawn_player_spaz(player, position, angle)
player = spaz.getplayer(Player, True)
team: Team = player.team
player.touching_own_flag = 0
no_physical_mats: list[ba.Material] = [
team.spaz_material_no_flag_physical
]
no_collide_mats: list[ba.Material] = [
team.spaz_material_no_flag_collide
]
# Our normal parts should still collide; just not physically
# (so we can calc restores).
assert spaz.node
spaz.node.materials = list(spaz.node.materials) + no_physical_mats
spaz.node.roller_materials = (
list(spaz.node.roller_materials) + no_physical_mats
)
# Pickups and punches shouldn't hit at all though.
spaz.node.punch_materials = (
list(spaz.node.punch_materials) + no_collide_mats
)
spaz.node.pickup_materials = (
list(spaz.node.pickup_materials) + no_collide_mats
)
spaz.node.extras_material = (
list(spaz.node.extras_material) + no_collide_mats
)
return spaz
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard behavior.
self.respawn_player(msg.getplayer(Player))
elif isinstance(msg, FlagDiedMessage):
assert isinstance(msg.flag, CTFFlag)
ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))
elif isinstance(msg, FlagPickedUpMessage):
# Store the last player to hold the flag for scoring purposes.
assert isinstance(msg.flag, CTFFlag)
try:
msg.flag.last_player_to_hold = msg.node.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
pass
msg.flag.held_count += 1
msg.flag.reset_return_times()
elif isinstance(msg, FlagDroppedMessage):
# Store the last player to hold the flag for scoring purposes.
assert isinstance(msg.flag, CTFFlag)
msg.flag.held_count -= 1
else:
super().handlemessage(msg)

View file

@ -0,0 +1,365 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides the chosen-one mini-game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.chosen_light: ba.NodeActor | None = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, time_remaining: int) -> None:
self.time_remaining = time_remaining
# ba_meta export game
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
kill you and become the chosen one themselves.
"""
name = 'Chosen One'
description = (
'Be the chosen one for a length of time to win.\n'
'Kill the chosen one to become it.'
)
available_settings = [
ba.IntSetting(
'Chosen One Time',
min_value=10,
default=30,
increment=10,
),
ba.BoolSetting('Chosen One Gets Gloves', default=True),
ba.BoolSetting('Chosen One Gets Shield', default=False),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('keep_away')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._chosen_one_player: Player | None = None
self._swipsound = ba.getsound('swip')
self._countdownsounds: dict[int, ba.Sound] = {
10: ba.getsound('announceTen'),
9: ba.getsound('announceNine'),
8: ba.getsound('announceEight'),
7: ba.getsound('announceSeven'),
6: ba.getsound('announceSix'),
5: ba.getsound('announceFive'),
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne'),
}
self._flag_spawn_pos: Sequence[float] | None = None
self._reset_region_material: ba.Material | None = None
self._flag: Flag | None = None
self._reset_region: ba.Node | None = 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) -> str | Sequence:
return 'There can be only one.'
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
return Team(time_remaining=self._chosen_one_time)
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
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()
shared = SharedObjects.get()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._flag_spawn_pos = self.map.get_flag_position(None)
Flag.project_stand(self._flag_spawn_pos)
ba.timer(1.0, call=self._tick, repeat=True)
mat = self._reset_region_material = ba.Material()
mat.add_actions(
conditions=(
'they_have_material',
shared.player_material,
),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', ba.WeakCall(self._handle_reset_collide)),
),
)
self._set_chosen_one_player(None)
def _create_reset_region(self) -> None:
assert self._reset_region_material is not None
assert self._flag_spawn_pos is not None
pos = self._flag_spawn_pos
self._reset_region = ba.newnode(
'region',
attrs={
'position': (pos[0], pos[1] + 0.75, pos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [self._reset_region_material],
},
)
def _get_chosen_one_player(self) -> Player | None:
# Should never return invalid references; return None in that case.
if self._chosen_one_player:
return self._chosen_one_player
return None
def _handle_reset_collide(self) -> None:
# If we have a chosen one, ignore these.
if self._get_chosen_one_player() is not None:
return
# Attempt to get a Actor that we hit.
try:
spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
player = spaz.getplayer(Player, True)
except ba.NotFoundError:
return
if spaz.is_alive():
self._set_chosen_one_player(player)
def _flash_flag_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'color': (1, 1, 1),
'radius': 0.3,
'height_attenuated': False,
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _tick(self) -> None:
# Give the chosen one points.
player = self._get_chosen_one_player()
if player is not None:
# This shouldn't happen, but just in case.
if not player.is_alive():
ba.print_error('got dead player as chosen one in _tick')
self._set_chosen_one_player(None)
else:
scoring_team = player.team
assert self.stats
self.stats.player_scored(
player, 3, screenmessage=False, display=False
)
scoring_team.time_remaining = max(
0, scoring_team.time_remaining - 1
)
# 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
if scoring_team.time_remaining in self._countdownsounds:
ba.playsound(
self._countdownsounds[scoring_team.time_remaining]
)
# Winner!
if scoring_team.time_remaining <= 0:
self.end_game()
else:
# (player is None)
# This shouldn't happen, but just in case.
# (Chosen-one player ceasing to exist should
# trigger on_player_leave which resets chosen-one)
if self._chosen_one_player is not None:
ba.print_error('got nonexistent player as chosen one in _tick')
self._set_chosen_one_player(None)
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
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: Player | None) -> None:
existing = self._get_chosen_one_player()
if existing:
existing.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),
},
)
# Also an extra momentary flash.
self._flash_flag_spawn()
# Re-create our flag region in case if someone is waiting for
# flag right there:
self._create_reset_region()
else:
if player.actor:
self._flag = None
self._chosen_one_player = player
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.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)
player.actor.node.connectattr(
'position', light.node, 'position'
)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.getplayer(Player)
if player is self._get_chosen_one_player():
killerplayer = msg.getkillerplayer(Player)
self._set_chosen_one_player(
None
if (
killerplayer is None
or killerplayer is player
or not killerplayer.is_alive()
)
else killerplayer
)
self.respawn_player(player)
else:
super().handlemessage(msg)
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(
team, team.time_remaining, self._chosen_one_time, countdown=True
)

View file

@ -0,0 +1,330 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides the Conquest game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.playerspaz import PlayerSpaz
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
from bastd.actor.respawnicon import RespawnIcon
class ConquestFlag(Flag):
"""A custom flag for use with Conquest games."""
def __init__(self, *args: Any, **keywds: Any):
super().__init__(*args, **keywds)
self._team: Team | None = None
self.light: ba.Node | None = None
@property
def team(self) -> Team | None:
"""The team that owns this flag."""
return self._team
@team.setter
def team(self, team: Team) -> None:
"""Set the team that owns this flag."""
self._team = team
class Player(ba.Player['Team']):
"""Our player type for this game."""
# FIXME: We shouldn't be using customdata here
# (but need to update respawn funcs accordingly first).
@property
def respawn_timer(self) -> ba.Timer | None:
"""Type safe access to standard respawn timer."""
return self.customdata.get('respawn_timer', None)
@respawn_timer.setter
def respawn_timer(self, value: ba.Timer | None) -> None:
self.customdata['respawn_timer'] = value
@property
def respawn_icon(self) -> RespawnIcon | None:
"""Type safe access to standard respawn icon."""
return self.customdata.get('respawn_icon', None)
@respawn_icon.setter
def respawn_icon(self, value: RespawnIcon | None) -> None:
self.customdata['respawn_icon'] = value
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.flags_held = 0
# ba_meta export game
class ConquestGame(ba.TeamGameActivity[Player, Team]):
"""A game where teams try to claim all flags on the map."""
name = 'Conquest'
description = 'Secure all flags on the map to win.'
available_settings = [
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('conquest')
def __init__(self, settings: dict):
super().__init__(settings)
shared = SharedObjects.get()
self._scoreboard = Scoreboard()
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._extraflagmat = ba.Material()
self._flags: list[ConquestFlag] = []
self._epic_mode = bool(settings['Epic Mode'])
self._time_limit = float(settings['Time Limit'])
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.GRAND_ROMP
)
# We want flags to tell us they've been hit but not react physically.
self._extraflagmat.add_actions(
conditions=('they_have_material', shared.player_material),
actions=(
('modify_part_collision', 'collide', True),
('call', 'at_connect', self._handle_flag_player_collide),
),
)
def get_instance_description(self) -> str | Sequence:
return 'Secure all ${ARG1} flags.', len(self.map.flag_points)
def get_instance_description_short(self) -> str | Sequence:
return 'secure all ${ARG1} flags', len(self.map.flag_points)
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scores()
def on_player_join(self, player: Player) -> None:
player.respawn_timer = None
# Only spawn if this player's team has a flag currently.
if player.team.flags_held > 0:
self.spawn_player(player)
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
# Set up flags with marker lights.
for i, flag_point in enumerate(self.map.flag_points):
point = flag_point
flag = ConquestFlag(
position=point, touchable=False, materials=[self._extraflagmat]
)
self._flags.append(flag)
Flag.project_stand(point)
flag.light = ba.newnode(
'light',
owner=flag.node,
attrs={
'position': point,
'intensity': 0.25,
'height_attenuated': False,
'radius': 0.3,
'color': (1, 1, 1),
},
)
# Give teams a flag to start with.
for i, team in enumerate(self.teams):
self._flags[i].team = team
light = self._flags[i].light
assert light
node = self._flags[i].node
assert node
light.color = team.color
node.color = team.color
self._update_scores()
# Initial joiners didn't spawn due to no flags being owned yet;
# spawn them now.
for player in self.players:
self.spawn_player(player)
def _update_scores(self) -> None:
for team in self.teams:
team.flags_held = 0
for flag in self._flags:
if flag.team is not None:
flag.team.flags_held += 1
for team in self.teams:
# If a team finds themselves with no flags, cancel all
# outstanding spawn-timers.
if team.flags_held == 0:
for player in team.players:
player.respawn_timer = None
player.respawn_icon = None
if team.flags_held == len(self._flags):
self.end_game()
self._scoreboard.set_team_value(
team, team.flags_held, len(self._flags)
)
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.flags_held)
self.end(results=results)
def _flash_flag(self, flag: ConquestFlag, length: float = 1.0) -> None:
assert flag.node
assert flag.light
light = ba.newnode(
'light',
attrs={
'position': flag.node.position,
'height_attenuated': False,
'color': flag.light.color,
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0}, loop=True)
ba.timer(length, light.delete)
def _handle_flag_player_collide(self) -> None:
collision = ba.getcollision()
try:
flag = collision.sourcenode.getdelegate(ConquestFlag, True)
player = collision.opposingnode.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
return
assert flag.light
if flag.team is not player.team:
flag.team = player.team
flag.light.color = player.team.color
flag.node.color = player.team.color
self.stats.player_scored(player, 10, screenmessage=False)
ba.playsound(self._swipsound)
self._flash_flag(flag)
self._update_scores()
# Respawn any players on this team that were in limbo due to the
# lack of a flag for their team.
for otherplayer in self.players:
if (
otherplayer.team is flag.team
and otherplayer.actor is not None
and not otherplayer.is_alive()
and otherplayer.respawn_timer is None
):
self.spawn_player(otherplayer)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
# Respawn only if this team has a flag.
player = msg.getplayer(Player)
if player.team.flags_held > 0:
self.respawn_player(player)
else:
player.respawn_timer = None
else:
super().handlemessage(msg)
def spawn_player(self, player: Player) -> ba.Actor:
# We spawn players at different places based on what flags are held.
return self.spawn_player_spaz(
player, self._get_player_spawn_position(player)
)
def _get_player_spawn_position(self, player: Player) -> Sequence[float]:
# Iterate until we find a spawn owned by this team.
spawn_count = len(self.map.spawn_by_flag_points)
# Get all spawns owned by this team.
spawns = [
i for i in range(spawn_count) if self._flags[i].team is player.team
]
closest_spawn = 0
closest_distance = 9999.0
# Now find the spawn that's closest to a spawn not owned by us;
# we'll use that one.
for spawn in spawns:
spt = self.map.spawn_by_flag_points[spawn]
our_pt = ba.Vec3(spt[0], spt[1], spt[2])
for otherspawn in [
i
for i in range(spawn_count)
if self._flags[i].team is not player.team
]:
spt = self.map.spawn_by_flag_points[otherspawn]
their_pt = ba.Vec3(spt[0], spt[1], spt[2])
dist = (their_pt - our_pt).length()
if dist < closest_distance:
closest_distance = dist
closest_spawn = spawn
pos = self.map.spawn_by_flag_points[closest_spawn]
x_range = (-0.5, 0.5) if pos[3] == 0.0 else (-pos[3], pos[3])
z_range = (-0.5, 0.5) if pos[5] == 0.0 else (-pos[5], pos[5])
pos = (
pos[0] + random.uniform(*x_range),
pos[1],
pos[2] + random.uniform(*z_range),
)
return pos

View file

@ -0,0 +1,206 @@
# Released under the MIT License. See LICENSE for details.
#
"""DeathMatch game and support classes."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING:
from typing import Any, Sequence
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class DeathMatchGame(ba.TeamGameActivity[Player, Team]):
"""A game type based on acquiring kills."""
name = 'Death Match'
description = 'Kill a set number of enemies to win.'
# Print messages when players die since it matters here.
announce_player_deaths = True
@classmethod
def get_available_settings(
cls, sessiontype: type[ba.Session]
) -> list[ba.Setting]:
settings = [
ba.IntSetting(
'Kills to Win Per Player',
min_value=1,
default=5,
increment=1,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
# In teams mode, a suicide gives a point to the other team, but in
# free-for-all it subtracts from your own score. By default we clamp
# this at zero to benefit new players, but pro players might like to
# be able to go negative. (to avoid a strategy of just
# suiciding until you get a good drop)
if issubclass(sessiontype, ba.FreeForAllSession):
settings.append(
ba.BoolSetting('Allow Negative Scores', default=False)
)
return settings
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession) or issubclass(
sessiontype, ba.FreeForAllSession
)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('melee')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._score_to_win: int | None = None
self._dingsound = ba.getsound('dingSmall')
self._epic_mode = bool(settings['Epic Mode'])
self._kills_to_win_per_player = int(settings['Kills to Win Per Player'])
self._time_limit = float(settings['Time Limit'])
self._allow_negative_scores = bool(
settings.get('Allow Negative Scores', False)
)
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.TO_THE_DEATH
)
def get_instance_description(self) -> str | Sequence:
return 'Crush ${ARG1} of your enemies.', self._score_to_win
def get_instance_description_short(self) -> str | Sequence:
return 'kill ${ARG1} enemies', self._score_to_win
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scoreboard()
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
# Base kills needed to win on the size of the largest team.
self._score_to_win = self._kills_to_win_per_player * max(
1, max(len(t.players) for t in self.teams)
)
self._update_scoreboard()
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.getplayer(Player)
self.respawn_player(player)
killer = msg.getkillerplayer(Player)
if killer is None:
return None
# Handle team-kills.
if killer.team is player.team:
# In free-for-all, killing yourself loses you a point.
if isinstance(self.session, ba.FreeForAllSession):
new_score = player.team.score - 1
if not self._allow_negative_scores:
new_score = max(0, new_score)
player.team.score = new_score
# In teams-mode it gives a point to the other team.
else:
ba.playsound(self._dingsound)
for team in self.teams:
if team is not killer.team:
team.score += 1
# Killing someone on another team nets a kill.
else:
killer.team.score += 1
ba.playsound(self._dingsound)
# In FFA show scores since its hard to find on the scoreboard.
if isinstance(killer.actor, PlayerSpaz) and killer.actor:
killer.actor.set_score_text(
str(killer.team.score) + '/' + str(self._score_to_win),
color=killer.team.color,
flash=True,
)
self._update_scoreboard()
# If someone has won, set a timer to end shortly.
# (allows the dust to clear and draws to occur if deaths are
# close enough)
assert self._score_to_win is not None
if any(team.score >= self._score_to_win for team in self.teams):
ba.timer(0.5, self.end_game)
else:
return super().handlemessage(msg)
return None
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)

View file

@ -0,0 +1,303 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides an easter egg hunt game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.actor.bomb import Bomb
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.spazbot import SpazBotSet, BouncyBot, SpazBotDiedMessage
from bastd.actor.onscreencountdown import OnScreenCountdown
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.respawnicon import RespawnIcon
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.respawn_timer: ba.Timer | None = None
self.respawn_icon: RespawnIcon | None = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
"""A game where score is based on collecting eggs."""
name = 'Easter Egg Hunt'
description = 'Gather eggs!'
available_settings = [
ba.BoolSetting('Pro Mode', default=False),
ba.BoolSetting('Epic Mode', default=False),
]
scoreconfig = ba.ScoreConfig(label='Score', scoretype=ba.ScoreType.POINTS)
# We're currently hard-coded for one map.
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ['Tower D']
# We support teams, free-for-all, and co-op sessions.
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return (
issubclass(sessiontype, ba.CoopSession)
or issubclass(sessiontype, ba.DualTeamSession)
or issubclass(sessiontype, ba.FreeForAllSession)
)
def __init__(self, settings: dict):
super().__init__(settings)
shared = SharedObjects.get()
self._last_player_death_time = None
self._scoreboard = Scoreboard()
self.egg_model = ba.getmodel('egg')
self.egg_tex_1 = ba.gettexture('eggTex1')
self.egg_tex_2 = ba.gettexture('eggTex2')
self.egg_tex_3 = ba.gettexture('eggTex3')
self._collect_sound = ba.getsound('powerup01')
self._pro_mode = settings.get('Pro Mode', False)
self._epic_mode = settings.get('Epic Mode', False)
self._max_eggs = 1.0
self.egg_material = ba.Material()
self.egg_material.add_actions(
conditions=('they_have_material', shared.player_material),
actions=(('call', 'at_connect', self._on_egg_player_collide),),
)
self._eggs: list[Egg] = []
self._update_timer: ba.Timer | None = None
self._countdown: OnScreenCountdown | None = None
self._bots: SpazBotSet | None = None
# Base class overrides
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FORWARD_MARCH
)
def on_team_join(self, team: Team) -> None:
if self.has_begun():
self._update_scoreboard()
# Called when our game actually starts.
def on_begin(self) -> None:
from bastd.maps import TowerD
# There's a player-wall on the tower-d level to prevent
# players from getting up on the stairs.. we wanna kill that.
gamemap = self.map
assert isinstance(gamemap, TowerD)
gamemap.player_wall.delete()
super().on_begin()
self._update_scoreboard()
self._update_timer = ba.Timer(0.25, self._update, repeat=True)
self._countdown = OnScreenCountdown(60, endcall=self.end_game)
ba.timer(4.0, self._countdown.start)
self._bots = SpazBotSet()
# Spawn evil bunny in co-op only.
if isinstance(self.session, ba.CoopSession) and self._pro_mode:
self._spawn_evil_bunny()
# Overriding the default character spawning.
def spawn_player(self, player: Player) -> ba.Actor:
spaz = self.spawn_player_spaz(player)
spaz.connect_controls_to_player()
return spaz
def _spawn_evil_bunny(self) -> None:
assert self._bots is not None
self._bots.spawn_bot(BouncyBot, pos=(6, 4, -7.8), spawn_time=10.0)
def _on_egg_player_collide(self) -> None:
if self.has_ended():
return
collision = ba.getcollision()
# Be defensive here; we could be hitting the corpse of a player
# who just left/etc.
try:
egg = collision.sourcenode.getdelegate(Egg, True)
player = collision.opposingnode.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
return
player.team.score += 1
# Displays a +1 (and adds to individual player score in
# teams mode).
self.stats.player_scored(player, 1, screenmessage=False)
if self._max_eggs < 5:
self._max_eggs += 1.0
elif self._max_eggs < 10:
self._max_eggs += 0.5
elif self._max_eggs < 30:
self._max_eggs += 0.3
self._update_scoreboard()
ba.playsound(self._collect_sound, 0.5, position=egg.node.position)
# Create a flash.
light = ba.newnode(
'light',
attrs={
'position': egg.node.position,
'height_attenuated': False,
'radius': 0.1,
'color': (1, 1, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.1: 1.0, 0.2: 0}, loop=False)
ba.timer(0.200, light.delete)
egg.handlemessage(ba.DieMessage())
def _update(self) -> None:
# Misc. periodic updating.
xpos = random.uniform(-7.1, 6.0)
ypos = random.uniform(3.5, 3.5)
zpos = random.uniform(-8.2, 3.7)
# Prune dead eggs from our list.
self._eggs = [e for e in self._eggs if e]
# Spawn more eggs if we've got space.
if len(self._eggs) < int(self._max_eggs):
# Occasionally spawn a land-mine in addition.
if self._pro_mode and random.random() < 0.25:
mine = Bomb(
position=(xpos, ypos, zpos), bomb_type='land_mine'
).autoretain()
mine.arm()
else:
self._eggs.append(Egg(position=(xpos, ypos, zpos)))
# Various high-level game events come through this method.
def handlemessage(self, msg: Any) -> Any:
# Respawn dead players.
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
# Respawn them shortly.
player = msg.getplayer(Player)
assert self.initialplayerinfos is not None
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player)
)
player.respawn_icon = RespawnIcon(player, respawn_time)
# Whenever our evil bunny dies, respawn him and spew some eggs.
elif isinstance(msg, SpazBotDiedMessage):
self._spawn_evil_bunny()
assert msg.spazbot.node
pos = msg.spazbot.node.position
for _i in range(6):
spread = 0.4
self._eggs.append(
Egg(
position=(
pos[0] + random.uniform(-spread, spread),
pos[1] + random.uniform(-spread, spread),
pos[2] + random.uniform(-spread, spread),
)
)
)
else:
# Default handler.
return super().handlemessage(msg)
return None
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(team, team.score)
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results)
class Egg(ba.Actor):
"""A lovely egg that can be picked up for points."""
def __init__(self, position: tuple[float, float, float] = (0.0, 1.0, 0.0)):
super().__init__()
activity = self.activity
assert isinstance(activity, EasterEggHuntGame)
shared = SharedObjects.get()
# Spawn just above the provided point.
self._spawn_pos = (position[0], position[1] + 1.0, position[2])
ctex = (activity.egg_tex_1, activity.egg_tex_2, activity.egg_tex_3)[
random.randrange(3)
]
mats = [shared.object_material, activity.egg_material]
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'model': activity.egg_model,
'color_texture': ctex,
'body': 'capsule',
'reflection': 'soft',
'model_scale': 0.5,
'body_scale': 0.6,
'density': 4.0,
'reflection_scale': [0.15],
'shadow_size': 0.6,
'position': self._spawn_pos,
'materials': mats,
},
)
def exists(self) -> bool:
return bool(self.node)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
if self.node:
self.node.delete()
elif isinstance(msg, ba.HitMessage):
if self.node:
assert msg.force_direction is not None
self.node.handlemessage(
'impulse',
msg.pos[0],
msg.pos[1],
msg.pos[2],
msg.velocity[0],
msg.velocity[1],
msg.velocity[2],
1.0 * msg.magnitude,
1.0 * msg.velocity_magnitude,
msg.radius,
0,
msg.force_direction[0],
msg.force_direction[1],
msg.force_direction[2],
)
else:
super().handlemessage(msg)

View file

@ -0,0 +1,597 @@
# Released under the MIT License. See LICENSE for details.
#
"""Elimination mini-game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.spazfactory import SpazFactory
from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING:
from typing import Any, Sequence
class Icon(ba.Actor):
"""Creates in in-game icon on screen."""
def __init__(
self,
player: Player,
position: tuple[float, float],
scale: float,
show_lives: bool = True,
show_death: bool = True,
name_scale: float = 1.0,
name_maxwidth: float = 115.0,
flatness: float = 1.0,
shadow: float = 1.0,
):
super().__init__()
self._player = player
self._show_lives = show_lives
self._show_death = show_death
self._name_scale = name_scale
self._outline_tex = ba.gettexture('characterIconMask')
icon = player.get_icon()
self.node = ba.newnode(
'image',
delegate=self,
attrs={
'texture': icon['texture'],
'tint_texture': icon['tint_texture'],
'tint_color': icon['tint_color'],
'vr_depth': 400,
'tint2_color': icon['tint2_color'],
'mask_texture': self._outline_tex,
'opacity': 1.0,
'absolute_scale': True,
'attach': 'bottomCenter',
},
)
self._name_text = ba.newnode(
'text',
owner=self.node,
attrs={
'text': ba.Lstr(value=player.getname()),
'color': ba.safecolor(player.team.color),
'h_align': 'center',
'v_align': 'center',
'vr_depth': 410,
'maxwidth': name_maxwidth,
'shadow': shadow,
'flatness': flatness,
'h_attach': 'center',
'v_attach': 'bottom',
},
)
if self._show_lives:
self._lives_text = ba.newnode(
'text',
owner=self.node,
attrs={
'text': 'x0',
'color': (1, 1, 0.5),
'h_align': 'left',
'vr_depth': 430,
'shadow': 1.0,
'flatness': 1.0,
'h_attach': 'center',
'v_attach': 'bottom',
},
)
self.set_position_and_scale(position, scale)
def set_position_and_scale(
self, position: tuple[float, float], scale: float
) -> None:
"""(Re)position the icon."""
assert self.node
self.node.position = position
self.node.scale = [70.0 * scale]
self._name_text.position = (position[0], position[1] + scale * 52.0)
self._name_text.scale = 1.0 * scale * self._name_scale
if self._show_lives:
self._lives_text.position = (
position[0] + scale * 10.0,
position[1] - scale * 43.0,
)
self._lives_text.scale = 1.0 * scale
def update_for_lives(self) -> None:
"""Update for the target player's current lives."""
if self._player:
lives = self._player.lives
else:
lives = 0
if self._show_lives:
if lives > 0:
self._lives_text.text = 'x' + str(lives - 1)
else:
self._lives_text.text = ''
if lives == 0:
self._name_text.opacity = 0.2
assert self.node
self.node.color = (0.7, 0.3, 0.3)
self.node.opacity = 0.2
def handle_player_spawned(self) -> None:
"""Our player spawned; hooray!"""
if not self.node:
return
self.node.opacity = 1.0
self.update_for_lives()
def handle_player_died(self) -> None:
"""Well poo; our player died."""
if not self.node:
return
if self._show_death:
ba.animate(
self.node,
'opacity',
{
0.00: 1.0,
0.05: 0.0,
0.10: 1.0,
0.15: 0.0,
0.20: 1.0,
0.25: 0.0,
0.30: 1.0,
0.35: 0.0,
0.40: 1.0,
0.45: 0.0,
0.50: 1.0,
0.55: 0.2,
},
)
lives = self._player.lives
if lives == 0:
ba.timer(0.6, self.update_for_lives)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
self.node.delete()
return None
return super().handlemessage(msg)
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.lives = 0
self.icons: list[Icon] = []
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.survival_seconds: int | None = None
self.spawn_order: list[Player] = []
# ba_meta export game
class EliminationGame(ba.TeamGameActivity[Player, Team]):
"""Game type where last player(s) left alive win."""
name = 'Elimination'
description = 'Last remaining alive wins.'
scoreconfig = ba.ScoreConfig(
label='Survived', scoretype=ba.ScoreType.SECONDS, none_is_winner=True
)
# Show messages when players die since it's meaningful here.
announce_player_deaths = True
allow_mid_activity_joins = False
@classmethod
def get_available_settings(
cls, sessiontype: type[ba.Session]
) -> list[ba.Setting]:
settings = [
ba.IntSetting(
'Lives Per Player',
default=1,
min_value=1,
max_value=10,
increment=1,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
if issubclass(sessiontype, ba.DualTeamSession):
settings.append(ba.BoolSetting('Solo Mode', default=False))
settings.append(
ba.BoolSetting('Balance Total Lives', default=False)
)
return settings
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession) or issubclass(
sessiontype, ba.FreeForAllSession
)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('melee')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._start_time: float | None = None
self._vs_text: ba.Actor | None = None
self._round_end_timer: ba.Timer | None = 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:
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) -> str | Sequence:
return (
'Last team standing wins.'
if isinstance(self.session, ba.DualTeamSession)
else 'Last one standing wins.'
)
def get_instance_description_short(self) -> str | Sequence:
return (
'last team standing wins'
if isinstance(self.session, ba.DualTeamSession)
else 'last one standing wins'
)
def on_player_join(self, player: Player) -> None:
player.lives = self._lives_per_player
if self._solo_mode:
player.team.spawn_order.append(player)
self._update_solo_mode()
else:
# Create our icon and spawn.
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.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
def _update_icons(self) -> None:
# pylint: disable=too-many-branches
# In free-for-all mode, everyone is just lined up along the bottom.
if isinstance(self.session, ba.FreeForAllSession):
count = len(self.teams)
x_offs = 85
xval = x_offs * (count - 1) * -0.5
for team in self.teams:
if len(team.players) == 1:
player = team.players[0]
for icon in player.icons:
icon.set_position_and_scale((xval, 30), 0.7)
icon.update_for_lives()
xval += x_offs
# In teams mode we split up teams.
else:
if self._solo_mode:
# First off, clear out all icons.
for player in self.players:
player.icons = []
# Now for each team, cycle through our available players
# adding icons.
for team in self.teams:
if team.id == 0:
xval = -60
x_offs = -78
else:
xval = 60
x_offs = 78
is_first = True
test_lives = 1
while True:
players_with_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.icons.append(
Icon(
player,
position=(xval, (40 if is_first else 25)),
scale=1.0 if is_first else 0.5,
name_maxwidth=130 if is_first else 75,
name_scale=0.8 if is_first else 1.0,
flatness=0.0 if is_first else 1.0,
shadow=0.5 if is_first else 1.0,
show_death=is_first,
show_lives=False,
)
)
xval += x_offs * (0.8 if is_first else 0.56)
is_first = False
test_lives += 1
# Non-solo mode.
else:
for team in self.teams:
if team.id == 0:
xval = -50
x_offs = -85
else:
xval = 50
x_offs = 85
for player in team.players:
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: Player) -> ba.Vec3 | None:
del player # Unused.
# In solo-mode, if there's an existing live player on the map, spawn at
# whichever spot is farthest from them (keeps the action spread out).
if self._solo_mode:
living_player = None
living_player_pos = None
for team in self.teams:
for tplayer in team.players:
if tplayer.is_alive():
assert tplayer.node
ppos = tplayer.node.position
living_player = tplayer
living_player_pos = ppos
break
if living_player:
assert living_player_pos is not None
player_pos = ba.Vec3(living_player_pos)
points: list[tuple[float, ba.Vec3]] = []
for team in self.teams:
start_pos = ba.Vec3(self.map.get_start_position(team.id))
points.append(
((start_pos - player_pos).length(), start_pos)
)
# Hmm.. we need to sorting vectors too?
points.sort(key=lambda x: x[0])
return points[-1][1]
return None
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.icons:
icon.handle_player_spawned()
return actor
def _print_lives(self, player: Player) -> None:
from bastd.actor import popuptext
# 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.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: Player) -> None:
super().on_player_leave(player)
player.icons = []
# Remove us from spawn-order.
if self._solo_mode:
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)
# If the player to leave was the last in spawn order and had
# their final turn currently in-progress, mark the survival time
# for their team.
if self._get_total_team_lives(player.team) == 0:
assert self._start_time is not None
player.team.survival_seconds = int(ba.time() - self._start_time)
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, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
player: Player = msg.getplayer(Player)
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.lives = 0
# If we have any icons, update their state.
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.lives == 0:
ba.playsound(SpazFactory.get().single_player_death_sound)
# If we hit zero lives, we're dead (and our team might be too).
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.survival_seconds = int(
ba.time() - self._start_time
)
else:
# Otherwise, in regular mode, respawn.
if not self._solo_mode:
self.respawn_player(player)
# In solo, put ourself at the back of the spawn order.
if self._solo_mode:
player.team.spawn_order.remove(player)
player.team.spawn_order.append(player)
def _update(self) -> None:
if self._solo_mode:
# 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.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()
break
# If we're down to 1 or fewer living teams, start a timer to end
# the game (allows the dust to settle and draws to occur if deaths
# are close enough).
if len(self._get_living_teams()) < 2:
self._round_end_timer = ba.Timer(0.5, self.end_game)
def _get_living_teams(self) -> list[Team]:
return [
team
for team in self.teams
if len(team.players) > 0
and any(player.lives > 0 for player in team.players)
]
def end_game(self) -> None:
if self.has_ended():
return
results = ba.GameResults()
self._vs_text = None # Kill our 'vs' if its there.
for team in self.teams:
results.set_team_score(team, team.survival_seconds)
self.end(results=results)

View file

@ -0,0 +1,988 @@
# Released under the MIT License. See LICENSE for details.
#
"""Implements football games (both co-op and teams varieties)."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import math
import ba
from bastd.actor.bomb import TNTSpawner
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.respawnicon import RespawnIcon
from bastd.actor.powerupbox import PowerupBoxFactory, PowerupBox
from bastd.actor.flag import (
FlagFactory,
Flag,
FlagPickedUpMessage,
FlagDroppedMessage,
FlagDiedMessage,
)
from bastd.actor.spazbot import (
SpazBotDiedMessage,
SpazBotPunchedMessage,
SpazBotSet,
BrawlerBotLite,
BrawlerBot,
BomberBotLite,
BomberBot,
TriggerBot,
ChargerBot,
TriggerBotPro,
BrawlerBotPro,
StickyBot,
ExplodeyBot,
)
if TYPE_CHECKING:
from typing import Any, Sequence
from bastd.actor.spaz import Spaz
from bastd.actor.spazbot import SpazBot
class FootballFlag(Flag):
"""Custom flag class for football games."""
def __init__(self, position: Sequence[float]):
super().__init__(
position=position, dropped_timeout=20, color=(1.0, 1.0, 0.3)
)
assert self.node
self.last_holding_player: ba.Player | None = None
self.node.is_area_of_interest = True
self.respawn_timer: ba.Timer | None = None
self.scored = False
self.held_count = 0
self.light = ba.newnode(
'light',
owner=self.node,
attrs={
'intensity': 0.25,
'height_attenuated': False,
'radius': 0.2,
'color': (0.9, 0.7, 0.0),
},
)
self.node.connectattr('position', self.light, 'position')
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.respawn_timer: ba.Timer | None = None
self.respawn_icon: RespawnIcon | None = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class FootballTeamGame(ba.TeamGameActivity[Player, Team]):
"""Football game for teams mode."""
name = 'Football'
description = 'Get the flag to the enemy end zone.'
available_settings = [
ba.IntSetting(
'Score to Win',
min_value=7,
default=21,
increment=7,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
# We only support two-team play.
return issubclass(sessiontype, ba.DualTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('football')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard: Scoreboard | None = Scoreboard()
# Load some media we need.
self._cheer_sound = ba.getsound('cheer')
self._chant_sound = ba.getsound('crowdChant')
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._whistle_sound = ba.getsound('refWhistle')
self._score_region_material = ba.Material()
self._score_region_material.add_actions(
conditions=('they_have_material', FlagFactory.get().flagmaterial),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', self._handle_score),
),
)
self._flag_spawn_pos: Sequence[float] | None = None
self._score_regions: list[ba.NodeActor] = []
self._flag: FootballFlag | None = None
self._flag_respawn_timer: ba.Timer | None = None
self._flag_respawn_light: ba.NodeActor | None = None
self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
self._epic_mode = bool(settings['Epic Mode'])
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.FOOTBALL
)
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
# NOTE: if use just touchdowns = self._score_to_win // 7
# and we will need to score, for example, 27 points,
# we will be required to score 3 (not 4) goals ..
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'Score ${ARG1} touchdowns.', touchdowns
return 'Score a touchdown.'
def get_instance_description_short(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'score ${ARG1} touchdowns', touchdowns
return 'score a touchdown'
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._flag_spawn_pos = self.map.get_flag_position(None)
self._spawn_flag()
defs = self.map.defs
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal1'][0:3],
'scale': defs.boxes['goal1'][6:9],
'type': 'box',
'materials': (self._score_region_material,),
},
)
)
)
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal2'][0:3],
'scale': defs.boxes['goal2'][6:9],
'type': 'box',
'materials': (self._score_region_material,),
},
)
)
)
self._update_scoreboard()
ba.playsound(self._chant_sound)
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
def _kill_flag(self) -> None:
self._flag = None
def _handle_score(self) -> None:
"""A point has been scored."""
# Our flag might stick around for a second or two
# make sure it doesn't score again.
assert self._flag is not None
if self._flag.scored:
return
region = ba.getcollision().sourcenode
i = None
for i, score_region in enumerate(self._score_regions):
if region == score_region.node:
break
for team in self.teams:
if team.id == i:
team.score += 7
# Tell all players to celebrate.
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
# If someone on this team was last to touch it,
# give them points.
assert self._flag is not None
if (
self._flag.last_holding_player
and team == self._flag.last_holding_player.team
):
self.stats.player_scored(
self._flag.last_holding_player, 50, big_message=True
)
# End the game if we won.
if team.score >= self._score_to_win:
self.end_game()
ba.playsound(self._score_sound)
ba.playsound(self._cheer_sound)
assert self._flag
self._flag.scored = True
# Kill the flag (it'll respawn shortly).
ba.timer(1.0, self._kill_flag)
light = ba.newnode(
'light',
attrs={
'position': ba.getcollision().position,
'height_attenuated': False,
'color': (1, 0, 0),
},
)
ba.animate(light, 'intensity', {0.0: 0, 0.5: 1, 1.0: 0}, loop=True)
ba.timer(1.0, light.delete)
ba.cameraflash(duration=10.0)
self._update_scoreboard()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results, announce_delay=0.8)
def _update_scoreboard(self) -> None:
assert self._scoreboard is not None
for team in self.teams:
self._scoreboard.set_team_value(
team, team.score, self._score_to_win
)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, FlagPickedUpMessage):
assert isinstance(msg.flag, FootballFlag)
try:
msg.flag.last_holding_player = msg.node.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
pass
msg.flag.held_count += 1
elif isinstance(msg, FlagDroppedMessage):
assert isinstance(msg.flag, FootballFlag)
msg.flag.held_count -= 1
# Respawn dead players if they're still in the game.
elif isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.respawn_player(msg.getplayer(Player))
# Respawn dead flags.
elif isinstance(msg, FlagDiedMessage):
if not self.has_ended():
self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.NodeActor(
ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'radius': 0.15,
'color': (1.0, 1.0, 0.3),
},
)
)
assert self._flag_respawn_light.node
ba.animate(
self._flag_respawn_light.node,
'intensity',
{0.0: 0, 0.25: 0.15, 0.5: 0},
loop=True,
)
ba.timer(3.0, self._flag_respawn_light.node.delete)
else:
# Augment standard behavior.
super().handlemessage(msg)
def _flash_flag_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'color': (1, 1, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _spawn_flag(self) -> None:
ba.playsound(self._swipsound)
ba.playsound(self._whistle_sound)
self._flash_flag_spawn()
assert self._flag_spawn_pos is not None
self._flag = FootballFlag(position=self._flag_spawn_pos)
class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
"""Co-op variant of football."""
name = 'Football'
tips = ['Use the pick-up button to grab the flag < ${PICKUP} >']
scoreconfig = ba.ScoreConfig(
scoretype=ba.ScoreType.MILLISECONDS, version='B'
)
default_music = ba.MusicType.FOOTBALL
# FIXME: Need to update co-op games to use getscoreconfig.
def get_score_type(self) -> str:
return 'time'
def get_instance_description(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'Score ${ARG1} touchdowns.', touchdowns
return 'Score a touchdown.'
def get_instance_description_short(self) -> str | Sequence:
touchdowns = self._score_to_win / 7
touchdowns = math.ceil(touchdowns)
if touchdowns > 1:
return 'score ${ARG1} touchdowns', touchdowns
return 'score a touchdown'
def __init__(self, settings: dict):
settings['map'] = 'Football Stadium'
super().__init__(settings)
self._preset = settings.get('preset', 'rookie')
# Load some media we need.
self._cheer_sound = ba.getsound('cheer')
self._boo_sound = ba.getsound('boo')
self._chant_sound = ba.getsound('crowdChant')
self._score_sound = ba.getsound('score')
self._swipsound = ba.getsound('swip')
self._whistle_sound = ba.getsound('refWhistle')
self._score_to_win = 21
self._score_region_material = ba.Material()
self._score_region_material.add_actions(
conditions=('they_have_material', FlagFactory.get().flagmaterial),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', self._handle_score),
),
)
self._powerup_center = (0, 2, 0)
self._powerup_spread = (10, 5.5)
self._player_has_dropped_bomb = False
self._player_has_punched = False
self._scoreboard: Scoreboard | None = None
self._flag_spawn_pos: Sequence[float] | None = None
self._score_regions: list[ba.NodeActor] = []
self._exclude_powerups: list[str] = []
self._have_tnt = False
self._bot_types_initial: list[type[SpazBot]] | None = None
self._bot_types_7: list[type[SpazBot]] | None = None
self._bot_types_14: list[type[SpazBot]] | None = None
self._bot_team: Team | None = None
self._starttime_ms: int | None = None
self._time_text: ba.NodeActor | None = None
self._time_text_input: ba.NodeActor | None = None
self._tntspawner: TNTSpawner | None = None
self._bots = SpazBotSet()
self._bot_spawn_timer: ba.Timer | None = None
self._powerup_drop_timer: ba.Timer | None = None
self._scoring_team: Team | None = None
self._final_time_ms: int | None = None
self._time_text_timer: ba.Timer | None = None
self._flag_respawn_light: ba.Actor | None = None
self._flag: FootballFlag | None = None
def on_transition_in(self) -> None:
super().on_transition_in()
self._scoreboard = Scoreboard()
self._flag_spawn_pos = self.map.get_flag_position(None)
self._spawn_flag()
# Set up the two score regions.
defs = self.map.defs
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal1'][0:3],
'scale': defs.boxes['goal1'][6:9],
'type': 'box',
'materials': [self._score_region_material],
},
)
)
)
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal2'][0:3],
'scale': defs.boxes['goal2'][6:9],
'type': 'box',
'materials': [self._score_region_material],
},
)
)
)
ba.playsound(self._chant_sound)
def on_begin(self) -> None:
# FIXME: Split this up a bit.
# pylint: disable=too-many-statements
from bastd.actor import controlsguide
super().on_begin()
# Show controls help in kiosk mode.
if ba.app.demo_mode or ba.app.arcade_mode:
controlsguide.ControlsGuide(
delay=3.0, lifespan=10.0, bright=True
).autoretain()
assert self.initialplayerinfos is not None
abot: type[SpazBot]
bbot: type[SpazBot]
cbot: type[SpazBot]
if self._preset in ['rookie', 'rookie_easy']:
self._exclude_powerups = ['curse']
self._have_tnt = False
abot = (
BrawlerBotLite if self._preset == 'rookie_easy' else BrawlerBot
)
self._bot_types_initial = [abot] * len(self.initialplayerinfos)
bbot = BomberBotLite if self._preset == 'rookie_easy' else BomberBot
self._bot_types_7 = [bbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
cbot = BomberBot if self._preset == 'rookie_easy' else TriggerBot
self._bot_types_14 = [cbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
elif self._preset == 'tournament':
self._exclude_powerups = []
self._have_tnt = True
self._bot_types_initial = [BrawlerBot] * (
1 if len(self.initialplayerinfos) < 2 else 2
)
self._bot_types_7 = [TriggerBot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
self._bot_types_14 = [ChargerBot] * (
1 if len(self.initialplayerinfos) < 4 else 2
)
elif self._preset in ['pro', 'pro_easy', 'tournament_pro']:
self._exclude_powerups = ['curse']
self._have_tnt = True
self._bot_types_initial = [ChargerBot] * len(
self.initialplayerinfos
)
abot = BrawlerBot if self._preset == 'pro' else BrawlerBotLite
typed_bot_list: list[type[SpazBot]] = []
self._bot_types_7 = (
typed_bot_list
+ [abot]
+ [BomberBot] * (1 if len(self.initialplayerinfos) < 3 else 2)
)
bbot = TriggerBotPro if self._preset == 'pro' else TriggerBot
self._bot_types_14 = [bbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
elif self._preset in ['uber', 'uber_easy']:
self._exclude_powerups = []
self._have_tnt = True
abot = BrawlerBotPro if self._preset == 'uber' else BrawlerBot
bbot = TriggerBotPro if self._preset == 'uber' else TriggerBot
typed_bot_list_2: list[type[SpazBot]] = []
self._bot_types_initial = (
typed_bot_list_2
+ [StickyBot]
+ [abot] * len(self.initialplayerinfos)
)
self._bot_types_7 = [bbot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
self._bot_types_14 = [ExplodeyBot] * (
1 if len(self.initialplayerinfos) < 3 else 2
)
else:
raise Exception()
self.setup_low_life_warning_sound()
self._drop_powerups(standard_points=True)
ba.timer(4.0, self._start_powerup_drops)
# Make a bogus team for our bots.
bad_team_name = self.get_team_display_string('Bad Guys')
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.score = 0
self.update_scores()
# Time display.
starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(starttime_ms, int)
self._starttime_ms = starttime_ms
self._time_text = ba.NodeActor(
ba.newnode(
'text',
attrs={
'v_attach': 'top',
'h_attach': 'center',
'h_align': 'center',
'color': (1, 1, 0.5, 1),
'flatness': 0.5,
'shadow': 0.5,
'position': (0, -50),
'scale': 1.3,
'text': '',
},
)
)
self._time_text_input = ba.NodeActor(
ba.newnode('timedisplay', attrs={'showsubseconds': True})
)
self.globalsnode.connectattr(
'time', self._time_text_input.node, 'time2'
)
assert self._time_text_input.node
assert self._time_text.node
self._time_text_input.node.connectattr(
'output', self._time_text.node, 'text'
)
# Our TNT spawner (if applicable).
if self._have_tnt:
self._tntspawner = TNTSpawner(position=(0, 1, -1))
self._bots = SpazBotSet()
self._bot_spawn_timer = ba.Timer(1.0, self._update_bots, repeat=True)
for bottype in self._bot_types_initial:
self._spawn_bot(bottype)
def _on_bot_spawn(self, spaz: SpazBot) -> None:
# We want to move to the left by default.
spaz.target_point_default = ba.Vec3(0, 0, 0)
def _spawn_bot(
self, spaz_type: type[SpazBot], immediate: bool = False
) -> None:
assert self._bot_team is not None
pos = self.map.get_start_position(self._bot_team.id)
self._bots.spawn_bot(
spaz_type,
pos=pos,
spawn_time=0.001 if immediate else 3.0,
on_spawn_call=self._on_bot_spawn,
)
def _update_bots(self) -> None:
bots = self._bots.get_living_bots()
for bot in bots:
bot.target_flag = None
# If we're waiting on a continue, stop here so they don't keep scoring.
if self.is_waiting_for_continue():
self._bots.stop_moving()
return
# If we've got a flag and no player are holding it, find the closest
# bot to it, and make them the designated flag-bearer.
assert self._flag is not None
if self._flag.node:
for player in self.players:
if player.actor:
assert isinstance(player.actor, PlayerSpaz)
if (
player.actor.is_alive()
and player.actor.node.hold_node == self._flag.node
):
return
flagpos = ba.Vec3(self._flag.node.position)
closest_bot: SpazBot | None = None
closest_dist = 0.0 # Always gets assigned first time through.
for bot in bots:
# If a bot is picked up, he should forget about the flag.
if bot.held_count > 0:
continue
assert bot.node
botpos = ba.Vec3(bot.node.position)
botdist = (botpos - flagpos).length()
if closest_bot is None or botdist < closest_dist:
closest_bot = bot
closest_dist = botdist
if closest_bot is not None:
closest_bot.target_flag = self._flag
def _drop_powerup(self, index: int, poweruptype: str | None = None) -> None:
if poweruptype is None:
poweruptype = PowerupBoxFactory.get().get_random_powerup_type(
excludetypes=self._exclude_powerups
)
PowerupBox(
position=self.map.powerup_spawn_points[index],
poweruptype=poweruptype,
).autoretain()
def _start_powerup_drops(self) -> None:
self._powerup_drop_timer = ba.Timer(
3.0, self._drop_powerups, repeat=True
)
def _drop_powerups(
self, standard_points: bool = False, poweruptype: str | None = None
) -> None:
"""Generic powerup drop."""
if standard_points:
spawnpoints = self.map.powerup_spawn_points
for i, _point in enumerate(spawnpoints):
ba.timer(
1.0 + i * 0.5, ba.Call(self._drop_powerup, i, poweruptype)
)
else:
point = (
self._powerup_center[0]
+ random.uniform(
-1.0 * self._powerup_spread[0],
1.0 * self._powerup_spread[0],
),
self._powerup_center[1],
self._powerup_center[2]
+ random.uniform(
-self._powerup_spread[1], self._powerup_spread[1]
),
)
# Drop one random one somewhere.
PowerupBox(
position=point,
poweruptype=PowerupBoxFactory.get().get_random_powerup_type(
excludetypes=self._exclude_powerups
),
).autoretain()
def _kill_flag(self) -> None:
try:
assert self._flag is not None
self._flag.handlemessage(ba.DieMessage())
except Exception:
ba.print_exception('Error in _kill_flag.')
def _handle_score(self) -> None:
"""a point has been scored"""
# FIXME tidy this up
# pylint: disable=too-many-branches
# Our flag might stick around for a second or two;
# we don't want it to be able to score again.
assert self._flag is not None
if self._flag.scored:
return
# See which score region it was.
region = ba.getcollision().sourcenode
i = None
for i, score_region in enumerate(self._score_regions):
if region == score_region.node:
break
for team in [self.teams[0], self._bot_team]:
assert team is not None
if team.id == i:
team.score += 7
# Tell all players (or bots) to celebrate.
if i == 0:
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
else:
self._bots.celebrate(2.0)
# If the good guys scored, add more enemies.
if i == 0:
if self.teams[0].score == 7:
assert self._bot_types_7 is not None
for bottype in self._bot_types_7:
self._spawn_bot(bottype)
elif self.teams[0].score == 14:
assert self._bot_types_14 is not None
for bottype in self._bot_types_14:
self._spawn_bot(bottype)
ba.playsound(self._score_sound)
if i == 0:
ba.playsound(self._cheer_sound)
else:
ba.playsound(self._boo_sound)
# Kill the flag (it'll respawn shortly).
self._flag.scored = True
ba.timer(0.2, self._kill_flag)
self.update_scores()
light = ba.newnode(
'light',
attrs={
'position': ba.getcollision().position,
'height_attenuated': False,
'color': (1, 0, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True)
ba.timer(1.0, light.delete)
if i == 0:
ba.cameraflash(duration=10.0)
def end_game(self) -> None:
ba.setmusic(None)
self._bots.final_celebrate()
ba.timer(0.001, ba.Call(self.do_end, 'defeat'))
def on_continue(self) -> None:
# Subtract one touchdown from the bots and get them moving again.
assert self._bot_team is not None
self._bot_team.score -= 7
self._bots.start_moving()
self.update_scores()
def update_scores(self) -> None:
"""update scoreboard and check for winners"""
# FIXME: tidy this up
# pylint: disable=too-many-nested-blocks
have_scoring_team = False
win_score = self._score_to_win
for team in [self.teams[0], self._bot_team]:
assert team is not None
assert self._scoreboard is not None
self._scoreboard.set_team_value(team, team.score, win_score)
if team.score >= win_score:
if not have_scoring_team:
self._scoring_team = team
if team is self._bot_team:
self.continue_or_end_game()
else:
ba.setmusic(ba.MusicType.VICTORY)
# Completion achievements.
assert self._bot_team is not None
if self._preset in ['rookie', 'rookie_easy']:
self._award_achievement(
'Rookie Football Victory', sound=False
)
if self._bot_team.score == 0:
self._award_achievement(
'Rookie Football Shutout', sound=False
)
elif self._preset in ['pro', 'pro_easy']:
self._award_achievement(
'Pro Football Victory', sound=False
)
if self._bot_team.score == 0:
self._award_achievement(
'Pro Football Shutout', sound=False
)
elif self._preset in ['uber', 'uber_easy']:
self._award_achievement(
'Uber Football Victory', sound=False
)
if self._bot_team.score == 0:
self._award_achievement(
'Uber Football Shutout', sound=False
)
if (
not self._player_has_dropped_bomb
and not self._player_has_punched
):
self._award_achievement(
'Got the Moves', sound=False
)
self._bots.stop_moving()
self.show_zoom_message(
ba.Lstr(resource='victoryText'),
scale=1.0,
duration=4.0,
)
self.celebrate(10.0)
assert self._starttime_ms is not None
self._final_time_ms = int(
ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
- self._starttime_ms
)
self._time_text_timer = None
assert (
self._time_text_input is not None
and self._time_text_input.node
)
self._time_text_input.node.timemax = self._final_time_ms
# FIXME: Does this still need to be deferred?
ba.pushcall(ba.Call(self.do_end, 'victory'))
def do_end(self, outcome: str) -> None:
"""End the game with the specified outcome."""
if outcome == 'defeat':
self.fade_to_red()
assert self._final_time_ms is not None
scoreval = (
None if outcome == 'defeat' else int(self._final_time_ms // 10)
)
self.end(
delay=3.0,
results={
'outcome': outcome,
'score': scoreval,
'score_order': 'decreasing',
'playerinfos': self.initialplayerinfos,
},
)
def handlemessage(self, msg: Any) -> Any:
"""handle high-level game messages"""
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
# Respawn them shortly.
player = msg.getplayer(Player)
assert self.initialplayerinfos is not None
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player)
)
player.respawn_icon = RespawnIcon(player, respawn_time)
elif isinstance(msg, SpazBotDiedMessage):
# Every time a bad guy dies, spawn a new one.
ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot))))
elif isinstance(msg, SpazBotPunchedMessage):
if self._preset in ['rookie', 'rookie_easy']:
if msg.damage >= 500:
self._award_achievement('Super Punch')
elif self._preset in ['pro', 'pro_easy']:
if msg.damage >= 1000:
self._award_achievement('Super Mega Punch')
# Respawn dead flags.
elif isinstance(msg, FlagDiedMessage):
assert isinstance(msg.flag, FootballFlag)
msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.NodeActor(
ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'radius': 0.15,
'color': (1.0, 1.0, 0.3),
},
)
)
assert self._flag_respawn_light.node
ba.animate(
self._flag_respawn_light.node,
'intensity',
{0: 0, 0.25: 0.15, 0.5: 0},
loop=True,
)
ba.timer(3.0, self._flag_respawn_light.node.delete)
else:
return super().handlemessage(msg)
return None
def _handle_player_dropped_bomb(self, player: Spaz, bomb: ba.Actor) -> None:
del player, bomb # Unused.
self._player_has_dropped_bomb = True
def _handle_player_punched(self, player: Spaz) -> None:
del player # Unused.
self._player_has_punched = True
def spawn_player(self, player: Player) -> ba.Actor:
spaz = self.spawn_player_spaz(
player, position=self.map.get_start_position(player.team.id)
)
if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
spaz.impact_scale = 0.25
spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)
spaz.punch_callback = self._handle_player_punched
return spaz
def _flash_flag_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'height_attenuated': False,
'color': (1, 1, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.25: 0.25, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _spawn_flag(self) -> None:
ba.playsound(self._swipsound)
ba.playsound(self._whistle_sound)
self._flash_flag_spawn()
assert self._flag_spawn_pos is not None
self._flag = FootballFlag(position=self._flag_spawn_pos)

409
dist/ba_data/python/bastd/game/hockey.py vendored Normal file
View file

@ -0,0 +1,409 @@
# Released under the MIT License. See LICENSE for details.
#
"""Hockey game and support classes."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.powerupbox import PowerupBoxFactory
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class PuckDiedMessage:
"""Inform something that a puck has died."""
def __init__(self, puck: Puck):
self.puck = puck
class Puck(ba.Actor):
"""A lovely giant hockey puck."""
def __init__(self, position: Sequence[float] = (0.0, 1.0, 0.0)):
super().__init__()
shared = SharedObjects.get()
activity = self.getactivity()
# Spawn just above the provided point.
self._spawn_pos = (position[0], position[1] + 1.0, position[2])
self.last_players_to_touch: dict[int, Player] = {}
self.scored = False
assert activity is not None
assert isinstance(activity, HockeyGame)
pmats = [shared.object_material, activity.puck_material]
self.node = ba.newnode(
'prop',
delegate=self,
attrs={
'model': activity.puck_model,
'color_texture': activity.puck_tex,
'body': 'puck',
'reflection': 'soft',
'reflection_scale': [0.2],
'shadow_size': 1.0,
'is_area_of_interest': True,
'position': self._spawn_pos,
'materials': pmats,
},
)
ba.animate(self.node, 'model_scale', {0: 0, 0.2: 1.3, 0.26: 1})
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.DieMessage):
assert self.node
self.node.delete()
activity = self._activity()
if activity and not msg.immediate:
activity.handlemessage(PuckDiedMessage(self))
# If we go out of bounds, move back to where we started.
elif isinstance(msg, ba.OutOfBoundsMessage):
assert self.node
self.node.position = self._spawn_pos
elif isinstance(msg, ba.HitMessage):
assert self.node
assert msg.force_direction is not None
self.node.handlemessage(
'impulse',
msg.pos[0],
msg.pos[1],
msg.pos[2],
msg.velocity[0],
msg.velocity[1],
msg.velocity[2],
1.0 * msg.magnitude,
1.0 * msg.velocity_magnitude,
msg.radius,
0,
msg.force_direction[0],
msg.force_direction[1],
msg.force_direction[2],
)
# If this hit came from a player, log them as the last to touch us.
s_player = msg.get_source_player(Player)
if s_player is not None:
activity = self._activity()
if activity:
if s_player in activity.players:
self.last_players_to_touch[s_player.team.id] = s_player
else:
super().handlemessage(msg)
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self) -> None:
self.score = 0
# ba_meta export game
class HockeyGame(ba.TeamGameActivity[Player, Team]):
"""Ice hockey game."""
name = 'Hockey'
description = 'Score some goals.'
available_settings = [
ba.IntSetting(
'Score to Win',
min_value=1,
default=1,
increment=1,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('hockey')
def __init__(self, settings: dict):
super().__init__(settings)
shared = SharedObjects.get()
self._scoreboard = Scoreboard()
self._cheer_sound = ba.getsound('cheer')
self._chant_sound = ba.getsound('crowdChant')
self._foghorn_sound = ba.getsound('foghorn')
self._swipsound = ba.getsound('swip')
self._whistle_sound = ba.getsound('refWhistle')
self.puck_model = ba.getmodel('puck')
self.puck_tex = ba.gettexture('puckColor')
self._puck_sound = ba.getsound('metalHit')
self.puck_material = ba.Material()
self.puck_material.add_actions(
actions=('modify_part_collision', 'friction', 0.5)
)
self.puck_material.add_actions(
conditions=('they_have_material', shared.pickup_material),
actions=('modify_part_collision', 'collide', False),
)
self.puck_material.add_actions(
conditions=(
('we_are_younger_than', 100),
'and',
('they_have_material', shared.object_material),
),
actions=('modify_node_collision', 'collide', False),
)
self.puck_material.add_actions(
conditions=('they_have_material', shared.footing_material),
actions=('impact_sound', self._puck_sound, 0.2, 5),
)
# Keep track of which player last touched the puck
self.puck_material.add_actions(
conditions=('they_have_material', shared.player_material),
actions=(('call', 'at_connect', self._handle_puck_player_collide),),
)
# We want the puck to kill powerups; not get stopped by them
self.puck_material.add_actions(
conditions=(
'they_have_material',
PowerupBoxFactory.get().powerup_material,
),
actions=(
('modify_part_collision', 'physical', False),
('message', 'their_node', 'at_connect', ba.DieMessage()),
),
)
self._score_region_material = ba.Material()
self._score_region_material.add_actions(
conditions=('they_have_material', self.puck_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
('call', 'at_connect', self._handle_score),
),
)
self._puck_spawn_pos: Sequence[float] | None = None
self._score_regions: list[ba.NodeActor] | None = None
self._puck: Puck | None = None
self._score_to_win = int(settings['Score to Win'])
self._time_limit = float(settings['Time Limit'])
self._epic_mode = bool(settings['Epic Mode'])
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.HOCKEY
)
def get_instance_description(self) -> str | Sequence:
if self._score_to_win == 1:
return 'Score a goal.'
return 'Score ${ARG1} goals.', self._score_to_win
def get_instance_description_short(self) -> str | Sequence:
if self._score_to_win == 1:
return 'score a goal'
return 'score ${ARG1} goals', self._score_to_win
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._puck_spawn_pos = self.map.get_flag_position(None)
self._spawn_puck()
# Set up the two score regions.
defs = self.map.defs
self._score_regions = []
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal1'][0:3],
'scale': defs.boxes['goal1'][6:9],
'type': 'box',
'materials': [self._score_region_material],
},
)
)
)
self._score_regions.append(
ba.NodeActor(
ba.newnode(
'region',
attrs={
'position': defs.boxes['goal2'][0:3],
'scale': defs.boxes['goal2'][6:9],
'type': 'box',
'materials': [self._score_region_material],
},
)
)
)
self._update_scoreboard()
ba.playsound(self._chant_sound)
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
def _handle_puck_player_collide(self) -> None:
collision = ba.getcollision()
try:
puck = collision.sourcenode.getdelegate(Puck, True)
player = collision.opposingnode.getdelegate(
PlayerSpaz, True
).getplayer(Player, True)
except ba.NotFoundError:
return
puck.last_players_to_touch[player.team.id] = player
def _kill_puck(self) -> None:
self._puck = None
def _handle_score(self) -> None:
"""A point has been scored."""
assert self._puck is not None
assert self._score_regions is not None
# Our puck might stick around for a second or two
# we don't want it to be able to score again.
if self._puck.scored:
return
region = ba.getcollision().sourcenode
index = 0
for index, score_region in enumerate(self._score_regions):
if region == score_region.node:
break
for team in self.teams:
if team.id == index:
scoring_team = team
team.score += 1
# Tell all players to celebrate.
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage(2.0))
# If we've got the player from the scoring team that last
# touched us, give them points.
if (
scoring_team.id in self._puck.last_players_to_touch
and self._puck.last_players_to_touch[scoring_team.id]
):
self.stats.player_scored(
self._puck.last_players_to_touch[scoring_team.id],
100,
big_message=True,
)
# End game if we won.
if team.score >= self._score_to_win:
self.end_game()
ba.playsound(self._foghorn_sound)
ba.playsound(self._cheer_sound)
self._puck.scored = True
# Kill the puck (it'll respawn itself shortly).
ba.timer(1.0, self._kill_puck)
light = ba.newnode(
'light',
attrs={
'position': ba.getcollision().position,
'height_attenuated': False,
'color': (1, 0, 0),
},
)
ba.animate(light, 'intensity', {0: 0, 0.5: 1, 1.0: 0}, loop=True)
ba.timer(1.0, light.delete)
ba.cameraflash(duration=10.0)
self._update_scoreboard()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, team.score)
self.end(results=results)
def _update_scoreboard(self) -> None:
winscore = self._score_to_win
for team in self.teams:
self._scoreboard.set_team_value(team, team.score, winscore)
def handlemessage(self, msg: Any) -> Any:
# Respawn dead players if they're still in the game.
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior...
super().handlemessage(msg)
self.respawn_player(msg.getplayer(Player))
# Respawn dead pucks.
elif isinstance(msg, PuckDiedMessage):
if not self.has_ended():
ba.timer(3.0, self._spawn_puck)
else:
super().handlemessage(msg)
def _flash_puck_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._puck_spawn_pos,
'height_attenuated': False,
'color': (1, 0, 0),
},
)
ba.animate(light, 'intensity', {0.0: 0, 0.25: 1, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _spawn_puck(self) -> None:
ba.playsound(self._swipsound)
ba.playsound(self._whistle_sound)
self._flash_puck_spawn()
assert self._puck_spawn_pos is not None
self._puck = Puck(position=self._puck_spawn_pos)

View file

@ -0,0 +1,281 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines a keep-away game type."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import ba
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.actor.flag import (
Flag,
FlagDroppedMessage,
FlagDiedMessage,
FlagPickedUpMessage,
)
if TYPE_CHECKING:
from typing import Any, Sequence
class FlagState(Enum):
"""States our single flag can be in."""
NEW = 0
UNCONTESTED = 1
CONTESTED = 2
HELD = 3
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, timeremaining: int) -> None:
self.timeremaining = timeremaining
self.holdingflag = False
# ba_meta export game
class KeepAwayGame(ba.TeamGameActivity[Player, Team]):
"""Game where you try to keep the flag away from your enemies."""
name = 'Keep Away'
description = 'Carry the flag for a set length of time.'
available_settings = [
ba.IntSetting(
'Hold Time',
min_value=10,
default=30,
increment=10,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.DualTeamSession) or issubclass(
sessiontype, ba.FreeForAllSession
)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('keep_away')
def __init__(self, settings: dict):
super().__init__(settings)
self._scoreboard = Scoreboard()
self._swipsound = ba.getsound('swip')
self._tick_sound = ba.getsound('tick')
self._countdownsounds = {
10: ba.getsound('announceTen'),
9: ba.getsound('announceNine'),
8: ba.getsound('announceEight'),
7: ba.getsound('announceSeven'),
6: ba.getsound('announceSix'),
5: ba.getsound('announceFive'),
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne'),
}
self._flag_spawn_pos: Sequence[float] | None = None
self._update_timer: ba.Timer | None = None
self._holding_players: list[Player] = []
self._flag_state: FlagState | None = None
self._flag_light: ba.Node | None = None
self._scoring_team: Team | None = None
self._flag: Flag | None = None
self._hold_time = int(settings['Hold Time'])
self._time_limit = float(settings['Time Limit'])
self._epic_mode = bool(settings['Epic Mode'])
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.KEEP_AWAY
)
def get_instance_description(self) -> str | Sequence:
return 'Carry the flag for ${ARG1} seconds.', self._hold_time
def get_instance_description_short(self) -> str | Sequence:
return 'carry the flag for ${ARG1} seconds', self._hold_time
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
return Team(timeremaining=self._hold_time)
def on_team_join(self, team: Team) -> None:
self._update_scoreboard()
def on_begin(self) -> None:
super().on_begin()
self.setup_standard_time_limit(self._time_limit)
self.setup_standard_powerup_drops()
self._flag_spawn_pos = self.map.get_flag_position(None)
self._spawn_flag()
self._update_timer = ba.Timer(1.0, call=self._tick, repeat=True)
self._update_flag_state()
Flag.project_stand(self._flag_spawn_pos)
def _tick(self) -> None:
self._update_flag_state()
# Award points to all living players holding the flag.
for player in self._holding_players:
if player:
assert self.stats
self.stats.player_scored(
player, 3, screenmessage=False, display=False
)
scoreteam = self._scoring_team
if scoreteam is not None:
if scoreteam.timeremaining > 0:
ba.playsound(self._tick_sound)
scoreteam.timeremaining = max(0, scoreteam.timeremaining - 1)
self._update_scoreboard()
if scoreteam.timeremaining > 0:
assert self._flag is not None
self._flag.set_score_text(str(scoreteam.timeremaining))
# Announce numbers we have sounds for.
if scoreteam.timeremaining in self._countdownsounds:
ba.playsound(self._countdownsounds[scoreteam.timeremaining])
# Winner.
if scoreteam.timeremaining <= 0:
self.end_game()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
results.set_team_score(team, self._hold_time - team.timeremaining)
self.end(results=results, announce_delay=0)
def _update_flag_state(self) -> None:
for team in self.teams:
team.holdingflag = False
self._holding_players = []
for player in self.players:
holdingflag = False
try:
assert isinstance(player.actor, (PlayerSpaz, type(None)))
if (
player.actor
and player.actor.node
and player.actor.node.hold_node
):
holdingflag = (
player.actor.node.hold_node.getnodetype() == 'flag'
)
except Exception:
ba.print_exception('Error checking hold flag.')
if holdingflag:
self._holding_players.append(player)
player.team.holdingflag = True
holdingteams = set(t for t in self.teams if t.holdingflag)
prevstate = self._flag_state
assert self._flag is not None
assert self._flag_light
assert self._flag.node
if len(holdingteams) > 1:
self._flag_state = FlagState.CONTESTED
self._scoring_team = None
self._flag_light.color = (0.6, 0.6, 0.1)
self._flag.node.color = (1.0, 1.0, 0.4)
elif len(holdingteams) == 1:
holdingteam = list(holdingteams)[0]
self._flag_state = FlagState.HELD
self._scoring_team = holdingteam
self._flag_light.color = ba.normalized_color(holdingteam.color)
self._flag.node.color = holdingteam.color
else:
self._flag_state = FlagState.UNCONTESTED
self._scoring_team = None
self._flag_light.color = (0.2, 0.2, 0.2)
self._flag.node.color = (1, 1, 1)
if self._flag_state != prevstate:
ba.playsound(self._swipsound)
def _spawn_flag(self) -> None:
ba.playsound(self._swipsound)
self._flash_flag_spawn()
assert self._flag_spawn_pos is not None
self._flag = Flag(dropped_timeout=20, position=self._flag_spawn_pos)
self._flag_state = FlagState.NEW
self._flag_light = ba.newnode(
'light',
owner=self._flag.node,
attrs={'intensity': 0.2, 'radius': 0.3, 'color': (0.2, 0.2, 0.2)},
)
assert self._flag.node
self._flag.node.connectattr('position', self._flag_light, 'position')
self._update_flag_state()
def _flash_flag_spawn(self) -> None:
light = ba.newnode(
'light',
attrs={
'position': self._flag_spawn_pos,
'color': (1, 1, 1),
'radius': 0.3,
'height_attenuated': False,
},
)
ba.animate(light, 'intensity', {0.0: 0, 0.25: 0.5, 0.5: 0}, loop=True)
ba.timer(1.0, light.delete)
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(
team, team.timeremaining, self._hold_time, countdown=True
)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.respawn_player(msg.getplayer(Player))
elif isinstance(msg, FlagDiedMessage):
self._spawn_flag()
elif isinstance(msg, (FlagDroppedMessage, FlagPickedUpMessage)):
self._update_flag_state()
else:
super().handlemessage(msg)

View file

@ -0,0 +1,293 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines the King of the Hill game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import ba
from bastd.actor.flag import Flag
from bastd.actor.playerspaz import PlayerSpaz
from bastd.actor.scoreboard import Scoreboard
from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence
class FlagState(Enum):
"""States our single flag can be in."""
NEW = 0
UNCONTESTED = 1
CONTESTED = 2
HELD = 3
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
self.time_at_flag = 0
class Team(ba.Team[Player]):
"""Our team type for this game."""
def __init__(self, time_remaining: int) -> None:
self.time_remaining = time_remaining
# ba_meta export game
class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
"""Game where a team wins by holding a 'hill' for a set amount of time."""
name = 'King of the Hill'
description = 'Secure the flag for a set length of time.'
available_settings = [
ba.IntSetting(
'Hold Time',
min_value=10,
default=30,
increment=10,
),
ba.IntChoiceSetting(
'Time Limit',
choices=[
('None', 0),
('1 Minute', 60),
('2 Minutes', 120),
('5 Minutes', 300),
('10 Minutes', 600),
('20 Minutes', 1200),
],
default=0,
),
ba.FloatChoiceSetting(
'Respawn Times',
choices=[
('Shorter', 0.25),
('Short', 0.5),
('Normal', 1.0),
('Long', 2.0),
('Longer', 4.0),
],
default=1.0,
),
ba.BoolSetting('Epic Mode', default=False),
]
scoreconfig = ba.ScoreConfig(label='Time Held')
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return issubclass(sessiontype, ba.MultiTeamSession)
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ba.getmaps('king_of_the_hill')
def __init__(self, settings: dict):
super().__init__(settings)
shared = SharedObjects.get()
self._scoreboard = Scoreboard()
self._swipsound = ba.getsound('swip')
self._tick_sound = ba.getsound('tick')
self._countdownsounds = {
10: ba.getsound('announceTen'),
9: ba.getsound('announceNine'),
8: ba.getsound('announceEight'),
7: ba.getsound('announceSeven'),
6: ba.getsound('announceSix'),
5: ba.getsound('announceFive'),
4: ba.getsound('announceFour'),
3: ba.getsound('announceThree'),
2: ba.getsound('announceTwo'),
1: ba.getsound('announceOne'),
}
self._flag_pos: Sequence[float] | None = None
self._flag_state: FlagState | None = None
self._flag: Flag | None = None
self._flag_light: ba.Node | None = None
self._scoring_team: weakref.ref[Team] | None = None
self._hold_time = int(settings['Hold Time'])
self._time_limit = float(settings['Time Limit'])
self._epic_mode = bool(settings['Epic Mode'])
self._flag_region_material = ba.Material()
self._flag_region_material.add_actions(
conditions=('they_have_material', shared.player_material),
actions=(
('modify_part_collision', 'collide', True),
('modify_part_collision', 'physical', False),
(
'call',
'at_connect',
ba.Call(self._handle_player_flag_region_collide, True),
),
(
'call',
'at_disconnect',
ba.Call(self._handle_player_flag_region_collide, False),
),
),
)
# Base class overrides.
self.slow_motion = self._epic_mode
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SCARY
)
def get_instance_description(self) -> str | Sequence:
return 'Secure the flag for ${ARG1} seconds.', self._hold_time
def get_instance_description_short(self) -> str | Sequence:
return 'secure the flag for ${ARG1} seconds', self._hold_time
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
return Team(time_remaining=self._hold_time)
def on_begin(self) -> None:
super().on_begin()
shared = SharedObjects.get()
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 = FlagState.NEW
Flag.project_stand(self._flag_pos)
self._flag = Flag(
position=self._flag_pos, touchable=False, color=(1, 1, 1)
)
self._flag_light = ba.newnode(
'light',
attrs={
'position': self._flag_pos,
'intensity': 0.2,
'height_attenuated': False,
'radius': 0.4,
'color': (0.2, 0.2, 0.2),
},
)
# Flag region.
flagmats = [self._flag_region_material, shared.region_material]
ba.newnode(
'region',
attrs={
'position': self._flag_pos,
'scale': (1.8, 1.8, 1.8),
'type': 'sphere',
'materials': flagmats,
},
)
self._update_flag_state()
def _tick(self) -> None:
self._update_flag_state()
# Give holding players points.
for player in self.players:
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.time_remaining > 0:
ba.playsound(self._tick_sound)
scoring_team.time_remaining = max(
0, scoring_team.time_remaining - 1
)
self._update_scoreboard()
if scoring_team.time_remaining > 0:
assert self._flag is not None
self._flag.set_score_text(str(scoring_team.time_remaining))
# Announce numbers we have sounds for.
numsound = self._countdownsounds.get(scoring_team.time_remaining)
if numsound is not None:
ba.playsound(numsound)
# winner
if scoring_team.time_remaining <= 0:
self.end_game()
def end_game(self) -> None:
results = ba.GameResults()
for team in self.teams:
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.time_at_flag
)
prev_state = self._flag_state
assert self._flag_light
assert self._flag is not None
assert self._flag.node
if len(holding_teams) > 1:
self._flag_state = FlagState.CONTESTED
self._scoring_team = None
self._flag_light.color = (0.6, 0.6, 0.1)
self._flag.node.color = (1.0, 1.0, 0.4)
elif len(holding_teams) == 1:
holding_team = list(holding_teams)[0]
self._flag_state = FlagState.HELD
self._scoring_team = weakref.ref(holding_team)
self._flag_light.color = ba.normalized_color(holding_team.color)
self._flag.node.color = holding_team.color
else:
self._flag_state = FlagState.UNCONTESTED
self._scoring_team = None
self._flag_light.color = (0.2, 0.2, 0.2)
self._flag.node.color = (1, 1, 1)
if self._flag_state != prev_state:
ba.playsound(self._swipsound)
def _handle_player_flag_region_collide(self, colliding: bool) -> None:
try:
spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
return
if not spaz.is_alive():
return
player = spaz.getplayer(Player, True)
# 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.time_at_flag += 1
else:
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.time_remaining, self._hold_time, countdown=True
)
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment default.
# No longer can count as time_at_flag once dead.
player = msg.getplayer(Player)
player.time_at_flag = 0
self._update_flag_state()
self.respawn_player(player)

View file

@ -0,0 +1,267 @@
# Released under the MIT License. See LICENSE for details.
#
"""Defines a bomb-dodging mini-game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.actor.bomb import Bomb
from bastd.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
from typing import Any, Sequence
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
super().__init__()
self.death_time: float | None = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
# ba_meta export game
class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
"""Minigame involving dodging falling bombs."""
name = 'Meteor Shower'
description = 'Dodge the falling bombs.'
available_settings = [ba.BoolSetting('Epic Mode', default=False)]
scoreconfig = ba.ScoreConfig(
label='Survived', scoretype=ba.ScoreType.MILLISECONDS, version='B'
)
# Print messages when players die (since its meaningful in this game).
announce_player_deaths = True
# Don't allow joining after we start
# (would enable leave/rejoin tomfoolery).
allow_mid_activity_joins = False
# We're currently hard-coded for one map.
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
return ['Rampage']
# We support teams, free-for-all, and co-op sessions.
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
return (
issubclass(sessiontype, ba.DualTeamSession)
or issubclass(sessiontype, ba.FreeForAllSession)
or issubclass(sessiontype, ba.CoopSession)
)
def __init__(self, settings: dict):
super().__init__(settings)
self._epic_mode = settings.get('Epic Mode', False)
self._last_player_death_time: float | None = None
self._meteor_time = 2.0
self._timer: OnScreenTimer | None = None
# Some base class overrides:
self.default_music = (
ba.MusicType.EPIC if self._epic_mode else ba.MusicType.SURVIVAL
)
if self._epic_mode:
self.slow_motion = True
def on_begin(self) -> None:
super().on_begin()
# Drop a wave every few seconds.. and every so often drop the time
# between waves ..lets have things increase faster if we have fewer
# players.
delay = 5.0 if len(self.players) > 2 else 2.5
if self._epic_mode:
delay *= 0.25
ba.timer(delay, self._decrement_meteor_time, repeat=True)
# Kick off the first wave in a few seconds.
delay = 3.0
if self._epic_mode:
delay *= 0.25
ba.timer(delay, self._set_meteor_timer)
self._timer = OnScreenTimer()
self._timer.start()
# Check for immediate end (if we've only got 1 player, etc).
ba.timer(5.0, self._check_end_game)
def on_player_leave(self, player: Player) -> None:
# Augment default behavior.
super().on_player_leave(player)
# A departing player may trigger game-over.
self._check_end_game()
# overriding the default character spawning..
def spawn_player(self, player: Player) -> ba.Actor:
spaz = self.spawn_player_spaz(player)
# Let's reconnect this player's controls to this
# spaz but *without* the ability to attack or pick stuff up.
spaz.connect_controls_to_player(
enable_punch=False, enable_bomb=False, enable_pickup=False
)
# Also lets have them make some noise when they die.
spaz.play_big_death_sound = True
return spaz
# Various high-level game events come through this method.
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, ba.PlayerDiedMessage):
# Augment standard behavior.
super().handlemessage(msg)
curtime = ba.time()
# Record the player's moment of death.
# assert isinstance(msg.spaz.player
msg.getplayer(Player).death_time = curtime
# In co-op mode, end the game the instant everyone dies
# (more accurate looking).
# In teams/ffa, allow a one-second fudge-factor so we can
# get more draws if players die basically at the same time.
if isinstance(self.session, ba.CoopSession):
# Teams will still show up if we check now.. check in
# the next cycle.
ba.pushcall(self._check_end_game)
# Also record this for a final setting of the clock.
self._last_player_death_time = curtime
else:
ba.timer(1.0, self._check_end_game)
else:
# Default handler:
return super().handlemessage(msg)
return None
def _check_end_game(self) -> None:
living_team_count = 0
for team in self.teams:
for player in team.players:
if player.is_alive():
living_team_count += 1
break
# In co-op, we go till everyone is dead.. otherwise we go
# until one team remains.
if isinstance(self.session, ba.CoopSession):
if living_team_count <= 0:
self.end_game()
else:
if living_team_count <= 1:
self.end_game()
def _set_meteor_timer(self) -> None:
ba.timer(
(1.0 + 0.2 * random.random()) * self._meteor_time,
self._drop_bomb_cluster,
)
def _drop_bomb_cluster(self) -> None:
# Random note: code like this is a handy way to plot out extents
# and debug things.
loc_test = False
if loc_test:
ba.newnode('locator', attrs={'position': (8, 6, -5.5)})
ba.newnode('locator', attrs={'position': (8, 6, -2.3)})
ba.newnode('locator', attrs={'position': (-7.3, 6, -5.5)})
ba.newnode('locator', attrs={'position': (-7.3, 6, -2.3)})
# Drop several bombs in series.
delay = 0.0
for _i in range(random.randrange(1, 3)):
# Drop them somewhere within our bounds with velocity pointing
# toward the opposite side.
pos = (
-7.3 + 15.3 * random.random(),
11,
-5.57 + 2.1 * random.random(),
)
dropdir = -1.0 if pos[0] > 0 else 1.0
vel = (
(-5.0 + random.random() * 30.0) * dropdir,
random.uniform(-3.066, -4.12),
0,
)
ba.timer(delay, ba.Call(self._drop_bomb, pos, vel))
delay += 0.1
self._set_meteor_timer()
def _drop_bomb(
self, position: Sequence[float], velocity: Sequence[float]
) -> None:
Bomb(position=position, velocity=velocity).autoretain()
def _decrement_meteor_time(self) -> None:
self._meteor_time = max(0.01, self._meteor_time * 0.9)
def end_game(self) -> None:
cur_time = ba.time()
assert self._timer is not None
start_time = self._timer.getstarttime()
# Mark death-time as now for any still-living players
# and award players points for how long they lasted.
# (these per-player scores are only meaningful in team-games)
for team in self.teams:
for player in team.players:
survived = False
# Throw an extra fudge factor in so teams that
# didn't die come out ahead of teams that did.
if player.death_time is None:
survived = True
player.death_time = cur_time + 1
# Award a per-player score depending on how many seconds
# they lasted (per-player scores only affect teams mode;
# everywhere else just looks at the per-team score).
score = int(player.death_time - self._timer.getstarttime())
if survived:
score += 50 # A bit extra for survivors.
self.stats.player_scored(player, score, screenmessage=False)
# Stop updating our time text, and set the final time to match
# exactly when our last guy died.
self._timer.stop(endtime=self._last_player_death_time)
# Ok now calc game results: set a score for each team and then tell
# the game to end.
results = ba.GameResults()
# Remember that 'free-for-all' mode is simply a special form
# of 'teams' mode where each player gets their own team, so we can
# just always deal in teams and have all cases covered.
for team in self.teams:
# Set the team score to the max time survived by any player on
# that team.
longest_life = 0.0
for player in team.players:
assert player.death_time is not None
longest_life = max(longest_life, player.death_time - start_time)
# Submit the score value in milliseconds.
results.set_team_score(team, int(1000.0 * longest_life))
self.end(results=results)

View file

@ -0,0 +1,190 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides Ninja Fight mini-game."""
# ba_meta require api 7
# (see https://ballistica.net/wiki/meta-tag-system)
from __future__ import annotations
import random
from typing import TYPE_CHECKING
import ba
from bastd.actor.spazbot import SpazBotSet, ChargerBot, SpazBotDiedMessage
from bastd.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
from typing import Any
class Player(ba.Player['Team']):
"""Our player type for this game."""
class Team(ba.Team[Player]):
"""Our team type for this game."""
# ba_meta export game
class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
"""
A co-op game where you try to defeat a group
of Ninjas as fast as possible
"""
name = 'Ninja Fight'
description = 'How fast can you defeat the ninjas?'
scoreconfig = ba.ScoreConfig(
label='Time', scoretype=ba.ScoreType.MILLISECONDS, lower_is_better=True
)
default_music = ba.MusicType.TO_THE_DEATH
@classmethod
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
# For now we're hard-coding spawn positions and whatnot
# so we need to be sure to specify that we only support
# a specific map.
return ['Courtyard']
@classmethod
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
# We currently support Co-Op only.
return issubclass(sessiontype, ba.CoopSession)
# In the constructor we should load any media we need/etc.
# ...but not actually create anything yet.
def __init__(self, settings: dict):
super().__init__(settings)
self._winsound = ba.getsound('score')
self._won = False
self._timer: OnScreenTimer | None = None
self._bots = SpazBotSet()
self._preset = str(settings['preset'])
# Called when our game actually begins.
def on_begin(self) -> None:
super().on_begin()
is_pro = self._preset == 'pro'
# In pro mode there's no powerups.
if not is_pro:
self.setup_standard_powerup_drops()
# Make our on-screen timer and start it roughly when our bots appear.
self._timer = OnScreenTimer()
ba.timer(4.0, self._timer.start)
# Spawn some baddies.
ba.timer(
1.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(3, 3, -2), spawn_time=3.0
),
)
ba.timer(
2.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(-3, 3, -2), spawn_time=3.0
),
)
ba.timer(
3.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(5, 3, -2), spawn_time=3.0
),
)
ba.timer(
4.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(-5, 3, -2), spawn_time=3.0
),
)
# Add some extras for multiplayer or pro mode.
assert self.initialplayerinfos is not None
if len(self.initialplayerinfos) > 2 or is_pro:
ba.timer(
5.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(0, 3, -5), spawn_time=3.0
),
)
if len(self.initialplayerinfos) > 3 or is_pro:
ba.timer(
6.0,
lambda: self._bots.spawn_bot(
ChargerBot, pos=(0, 3, 1), spawn_time=3.0
),
)
# Called for each spawning player.
def spawn_player(self, player: Player) -> ba.Actor:
# Let's spawn close to the center.
spawn_center = (0, 3, -2)
pos = (
spawn_center[0] + random.uniform(-1.5, 1.5),
spawn_center[1],
spawn_center[2] + random.uniform(-1.5, 1.5),
)
return self.spawn_player_spaz(player, position=pos)
def _check_if_won(self) -> None:
# Simply end the game if there's no living bots.
# FIXME: Should also make sure all bots have been spawned;
# if spawning is spread out enough that we're able to kill
# all living bots before the next spawns, it would incorrectly
# count as a win.
if not self._bots.have_living_bots():
self._won = True
self.end_game()
# Called for miscellaneous messages.
def handlemessage(self, msg: Any) -> Any:
# A player has died.
if isinstance(msg, ba.PlayerDiedMessage):
super().handlemessage(msg) # Augment standard behavior.
self.respawn_player(msg.getplayer(Player))
# A spaz-bot has died.
elif isinstance(msg, SpazBotDiedMessage):
# Unfortunately the bot-set will always tell us there are living
# bots if we ask here (the currently-dying bot isn't officially
# marked dead yet) ..so lets push a call into the event loop to
# check once this guy has finished dying.
ba.pushcall(self._check_if_won)
# Let the base class handle anything we don't.
else:
return super().handlemessage(msg)
return None
# When this is called, we should fill out results and end the game
# *regardless* of whether is has been won. (this may be called due
# to a tournament ending or other external reason).
def end_game(self) -> None:
# Stop our on-screen timer so players can see what they got.
assert self._timer is not None
self._timer.stop()
results = ba.GameResults()
# If we won, set our score to the elapsed time in milliseconds.
# (there should just be 1 team here since this is co-op).
# ..if we didn't win, leave scores as default (None) which means
# we lost.
if self._won:
elapsed_time_ms = int((ba.time() - self._timer.starttime) * 1000.0)
ba.cameraflash()
ba.playsound(self._winsound)
for team in self.teams:
for player in team.players:
if player.actor:
player.actor.handlemessage(ba.CelebrateMessage())
results.set_team_score(team, elapsed_time_ms)
# Ends the activity.
self.end(results)

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more