mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
Initial commit
This commit is contained in:
parent
bc49523c99
commit
44d606cce7
1929 changed files with 612166 additions and 0 deletions
5
dist/ba_data/python/bastd/__init__.py
vendored
Normal file
5
dist/ba_data/python/bastd/__init__.py
vendored
Normal 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
|
||||
BIN
dist/ba_data/python/bastd/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/__pycache__/appdelegate.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/__pycache__/gameutils.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/__pycache__/gameutils.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/__pycache__/mainmenu.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/__pycache__/maps.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/__pycache__/maps.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/__pycache__/tutorial.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/__pycache__/tutorial.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
1
dist/ba_data/python/bastd/activity/__init__.py
vendored
Normal file
1
dist/ba_data/python/bastd/activity/__init__.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
BIN
dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/coopscore.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/drawscore.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/dualteamscore.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/multiteamjoin.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/multiteamscore.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/activity/__pycache__/multiteamvictory.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
112
dist/ba_data/python/bastd/activity/coopjoin.py
vendored
Normal file
112
dist/ba_data/python/bastd/activity/coopjoin.py
vendored
Normal 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()
|
||||
1777
dist/ba_data/python/bastd/activity/coopscore.py
vendored
Normal file
1777
dist/ba_data/python/bastd/activity/coopscore.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
36
dist/ba_data/python/bastd/activity/drawscore.py
vendored
Normal file
36
dist/ba_data/python/bastd/activity/drawscore.py
vendored
Normal 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))
|
||||
168
dist/ba_data/python/bastd/activity/dualteamscore.py
vendored
Normal file
168
dist/ba_data/python/bastd/activity/dualteamscore.py
vendored
Normal 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()
|
||||
368
dist/ba_data/python/bastd/activity/freeforallvictory.py
vendored
Normal file
368
dist/ba_data/python/bastd/activity/freeforallvictory.py
vendored
Normal 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)
|
||||
93
dist/ba_data/python/bastd/activity/multiteamjoin.py
vendored
Normal file
93
dist/ba_data/python/bastd/activity/multiteamjoin.py
vendored
Normal 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()
|
||||
262
dist/ba_data/python/bastd/activity/multiteamscore.py
vendored
Normal file
262
dist/ba_data/python/bastd/activity/multiteamscore.py
vendored
Normal 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,
|
||||
)
|
||||
481
dist/ba_data/python/bastd/activity/multiteamvictory.py
vendored
Normal file
481
dist/ba_data/python/bastd/activity/multiteamvictory.py
vendored
Normal 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()
|
||||
1
dist/ba_data/python/bastd/actor/__init__.py
vendored
Normal file
1
dist/ba_data/python/bastd/actor/__init__.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
BIN
dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/background.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/background.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/bomb.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/controlsguide.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/flag.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/image.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/image.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/onscreencountdown.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/onscreentimer.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/playerspaz.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/popuptext.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/powerupbox.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/respawnicon.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/scoreboard.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/spawner.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/spawner.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/spaz.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/spazappearance.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/spazbot.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/spazfactory.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/text.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/text.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/tipstext.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/actor/__pycache__/zoomtext.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
145
dist/ba_data/python/bastd/actor/background.py
vendored
Normal file
145
dist/ba_data/python/bastd/actor/background.py
vendored
Normal 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
1212
dist/ba_data/python/bastd/actor/bomb.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
639
dist/ba_data/python/bastd/actor/controlsguide.py
vendored
Normal file
639
dist/ba_data/python/bastd/actor/controlsguide.py
vendored
Normal 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
380
dist/ba_data/python/bastd/actor/flag.py
vendored
Normal 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
174
dist/ba_data/python/bastd/actor/image.py
vendored
Normal 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)
|
||||
107
dist/ba_data/python/bastd/actor/onscreencountdown.py
vendored
Normal file
107
dist/ba_data/python/bastd/actor/onscreencountdown.py
vendored
Normal 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()
|
||||
131
dist/ba_data/python/bastd/actor/onscreentimer.py
vendored
Normal file
131
dist/ba_data/python/bastd/actor/onscreentimer.py
vendored
Normal 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()
|
||||
309
dist/ba_data/python/bastd/actor/playerspaz.py
vendored
Normal file
309
dist/ba_data/python/bastd/actor/playerspaz.py
vendored
Normal 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')
|
||||
127
dist/ba_data/python/bastd/actor/popuptext.py
vendored
Normal file
127
dist/ba_data/python/bastd/actor/popuptext.py
vendored
Normal 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)
|
||||
319
dist/ba_data/python/bastd/actor/powerupbox.py
vendored
Normal file
319
dist/ba_data/python/bastd/actor/powerupbox.py
vendored
Normal 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
|
||||
168
dist/ba_data/python/bastd/actor/respawnicon.py
vendored
Normal file
168
dist/ba_data/python/bastd/actor/respawnicon.py
vendored
Normal 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
|
||||
447
dist/ba_data/python/bastd/actor/scoreboard.py
vendored
Normal file
447
dist/ba_data/python/bastd/actor/scoreboard.py
vendored
Normal 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
|
||||
118
dist/ba_data/python/bastd/actor/spawner.py
vendored
Normal file
118
dist/ba_data/python/bastd/actor/spawner.py
vendored
Normal 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
1633
dist/ba_data/python/bastd/actor/spaz.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
988
dist/ba_data/python/bastd/actor/spazappearance.py
vendored
Normal file
988
dist/ba_data/python/bastd/actor/spazappearance.py
vendored
Normal 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
1120
dist/ba_data/python/bastd/actor/spazbot.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
307
dist/ba_data/python/bastd/actor/spazfactory.py
vendored
Normal file
307
dist/ba_data/python/bastd/actor/spazfactory.py
vendored
Normal 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
230
dist/ba_data/python/bastd/actor/text.py
vendored
Normal 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)
|
||||
101
dist/ba_data/python/bastd/actor/tipstext.py
vendored
Normal file
101
dist/ba_data/python/bastd/actor/tipstext.py
vendored
Normal 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)
|
||||
209
dist/ba_data/python/bastd/actor/zoomtext.py
vendored
Normal file
209
dist/ba_data/python/bastd/actor/zoomtext.py
vendored
Normal 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')
|
||||
37
dist/ba_data/python/bastd/appdelegate.py
vendored
Normal file
37
dist/ba_data/python/bastd/appdelegate.py
vendored
Normal 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()
|
||||
)
|
||||
1
dist/ba_data/python/bastd/game/__init__.py
vendored
Normal file
1
dist/ba_data/python/bastd/game/__init__.py
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
BIN
dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/__init__.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/assault.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/assault.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/capturetheflag.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/chosenone.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/conquest.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/conquest.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/deathmatch.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/easteregghunt.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/elimination.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/football.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/football.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/hockey.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/hockey.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/keepaway.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/kingofthehill.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/meteorshower.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/ninjafight.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/onslaught.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/race.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/race.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/runaround.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/targetpractice.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
BIN
dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-310.opt-1.pyc
vendored
Normal file
BIN
dist/ba_data/python/bastd/game/__pycache__/thelaststand.cpython-310.opt-1.pyc
vendored
Normal file
Binary file not shown.
262
dist/ba_data/python/bastd/game/assault.py
vendored
Normal file
262
dist/ba_data/python/bastd/game/assault.py
vendored
Normal 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
|
||||
)
|
||||
613
dist/ba_data/python/bastd/game/capturetheflag.py
vendored
Normal file
613
dist/ba_data/python/bastd/game/capturetheflag.py
vendored
Normal 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)
|
||||
365
dist/ba_data/python/bastd/game/chosenone.py
vendored
Normal file
365
dist/ba_data/python/bastd/game/chosenone.py
vendored
Normal 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
|
||||
)
|
||||
330
dist/ba_data/python/bastd/game/conquest.py
vendored
Normal file
330
dist/ba_data/python/bastd/game/conquest.py
vendored
Normal 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
|
||||
206
dist/ba_data/python/bastd/game/deathmatch.py
vendored
Normal file
206
dist/ba_data/python/bastd/game/deathmatch.py
vendored
Normal 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)
|
||||
303
dist/ba_data/python/bastd/game/easteregghunt.py
vendored
Normal file
303
dist/ba_data/python/bastd/game/easteregghunt.py
vendored
Normal 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)
|
||||
597
dist/ba_data/python/bastd/game/elimination.py
vendored
Normal file
597
dist/ba_data/python/bastd/game/elimination.py
vendored
Normal 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)
|
||||
988
dist/ba_data/python/bastd/game/football.py
vendored
Normal file
988
dist/ba_data/python/bastd/game/football.py
vendored
Normal 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
409
dist/ba_data/python/bastd/game/hockey.py
vendored
Normal 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)
|
||||
281
dist/ba_data/python/bastd/game/keepaway.py
vendored
Normal file
281
dist/ba_data/python/bastd/game/keepaway.py
vendored
Normal 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)
|
||||
293
dist/ba_data/python/bastd/game/kingofthehill.py
vendored
Normal file
293
dist/ba_data/python/bastd/game/kingofthehill.py
vendored
Normal 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)
|
||||
267
dist/ba_data/python/bastd/game/meteorshower.py
vendored
Normal file
267
dist/ba_data/python/bastd/game/meteorshower.py
vendored
Normal 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)
|
||||
190
dist/ba_data/python/bastd/game/ninjafight.py
vendored
Normal file
190
dist/ba_data/python/bastd/game/ninjafight.py
vendored
Normal 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)
|
||||
1559
dist/ba_data/python/bastd/game/onslaught.py
vendored
Normal file
1559
dist/ba_data/python/bastd/game/onslaught.py
vendored
Normal file
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
Loading…
Add table
Add a link
Reference in a new issue