mirror of
https://github.com/bombsquad-community/plugin-manager.git
synced 2025-10-08 14:54:36 +00:00
879 lines
33 KiB
Python
879 lines
33 KiB
Python
|
|
# ba_meta require api 7
|
||
|
|
# (see https://ballistica.net/wiki/meta-tag-system)
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from typing import TYPE_CHECKING
|
||
|
|
|
||
|
|
import ba
|
||
|
|
import _ba
|
||
|
|
import copy
|
||
|
|
import random
|
||
|
|
from ba import _math
|
||
|
|
from ba._coopsession import CoopSession
|
||
|
|
from ba._messages import PlayerDiedMessage, StandMessage
|
||
|
|
from bastd.actor.playerspaz import PlayerSpaz
|
||
|
|
from bastd.actor.scoreboard import Scoreboard
|
||
|
|
from bastd.game.elimination import Icon, Player
|
||
|
|
from bastd.actor.spaz import PickupMessage
|
||
|
|
from bastd.actor.spazbot import SpazBotSet, BrawlerBot, SpazBotDiedMessage
|
||
|
|
from bastd.actor.spazfactory import SpazFactory
|
||
|
|
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from typing import Any, Sequence
|
||
|
|
|
||
|
|
|
||
|
|
class PlayerSpaz_Zom(PlayerSpaz):
|
||
|
|
def handlemessage(self, m: Any) -> Any:
|
||
|
|
if isinstance(m, ba.HitMessage):
|
||
|
|
if not self.node:
|
||
|
|
return
|
||
|
|
if not m._source_player is None:
|
||
|
|
try:
|
||
|
|
playa = m._source_player.getname(True, False)
|
||
|
|
if not playa is None:
|
||
|
|
if m._source_player.lives < 1:
|
||
|
|
super().handlemessage(m)
|
||
|
|
except:
|
||
|
|
super().handlemessage(m)
|
||
|
|
else:
|
||
|
|
super().handlemessage(m)
|
||
|
|
|
||
|
|
elif isinstance(m, ba.FreezeMessage):
|
||
|
|
pass
|
||
|
|
|
||
|
|
elif isinstance(m, PickupMessage):
|
||
|
|
if not self.node:
|
||
|
|
return None
|
||
|
|
|
||
|
|
try:
|
||
|
|
collision = ba.getcollision()
|
||
|
|
opposingnode = collision.opposingnode
|
||
|
|
opposingbody = collision.opposingbody
|
||
|
|
except ba.NotFoundError:
|
||
|
|
return True
|
||
|
|
|
||
|
|
try:
|
||
|
|
if opposingnode.invincible:
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
try:
|
||
|
|
playa = opposingnode._source_player.getname(True, False)
|
||
|
|
if not playa is None:
|
||
|
|
if opposingnode._source_player.lives > 0:
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
if (opposingnode.getnodetype() == 'spaz'
|
||
|
|
and not opposingnode.shattered and opposingbody == 4):
|
||
|
|
opposingbody = 1
|
||
|
|
|
||
|
|
held = self.node.hold_node
|
||
|
|
if held and held.getnodetype() == 'flag':
|
||
|
|
return True
|
||
|
|
|
||
|
|
self.node.hold_body = opposingbody
|
||
|
|
self.node.hold_node = opposingnode
|
||
|
|
else:
|
||
|
|
return super().handlemessage(m)
|
||
|
|
return None
|
||
|
|
|
||
|
|
class PlayerZombie(PlayerSpaz):
|
||
|
|
def handlemessage(self, m: Any) -> Any:
|
||
|
|
if isinstance(m, ba.HitMessage):
|
||
|
|
if not self.node:
|
||
|
|
return None
|
||
|
|
if not m._source_player is None:
|
||
|
|
try:
|
||
|
|
playa = m._source_player.getname(True, False)
|
||
|
|
if playa is None:
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
super().handlemessage(m)
|
||
|
|
except:
|
||
|
|
super().handlemessage(m)
|
||
|
|
else:
|
||
|
|
super().handlemessage(m)
|
||
|
|
else:
|
||
|
|
super().handlemessage(m)
|
||
|
|
|
||
|
|
class zBotSet(SpazBotSet):
|
||
|
|
def start_moving(self) -> None:
|
||
|
|
"""Start processing bot AI updates so they start doing their thing."""
|
||
|
|
self._bot_update_timer = ba.Timer(0.05,
|
||
|
|
ba.WeakCall(self.zUpdate),
|
||
|
|
repeat=True)
|
||
|
|
|
||
|
|
def zUpdate(self) -> None:
|
||
|
|
|
||
|
|
try:
|
||
|
|
bot_list = self._bot_lists[self._bot_update_list] = ([
|
||
|
|
b for b in self._bot_lists[self._bot_update_list] if b
|
||
|
|
])
|
||
|
|
except Exception:
|
||
|
|
bot_list = []
|
||
|
|
ba.print_exception('Error updating bot list: ' +
|
||
|
|
str(self._bot_lists[self._bot_update_list]))
|
||
|
|
self._bot_update_list = (self._bot_update_list +
|
||
|
|
1) % self._bot_list_count
|
||
|
|
|
||
|
|
player_pts = []
|
||
|
|
for player in ba.getactivity().players:
|
||
|
|
assert isinstance(player, ba.Player)
|
||
|
|
try:
|
||
|
|
if player.is_alive():
|
||
|
|
assert isinstance(player.actor, Spaz)
|
||
|
|
assert player.actor.node
|
||
|
|
if player.lives > 0:
|
||
|
|
player_pts.append(
|
||
|
|
(ba.Vec3(player.actor.node.position),
|
||
|
|
ba.Vec3(player.actor.node.velocity)))
|
||
|
|
except Exception:
|
||
|
|
ba.print_exception('Error on bot-set _update.')
|
||
|
|
|
||
|
|
for bot in bot_list:
|
||
|
|
bot.set_player_points(player_pts)
|
||
|
|
bot.update_ai()
|
||
|
|
|
||
|
|
class Team(ba.Team[Player]):
|
||
|
|
"""Our team type for this game."""
|
||
|
|
|
||
|
|
def __init__(self) -> None:
|
||
|
|
self.score = 0
|
||
|
|
self.spawn_order: list[Player] = []
|
||
|
|
|
||
|
|
|
||
|
|
# ba_meta export game
|
||
|
|
class ZombieHorde(ba.TeamGameActivity[Player, Team]):
|
||
|
|
|
||
|
|
name = 'Zombie Horde'
|
||
|
|
description = 'Kill walkers for points!'
|
||
|
|
scoreconfig = ba.ScoreConfig(label='Score',
|
||
|
|
scoretype=ba.ScoreType.POINTS,
|
||
|
|
none_is_winner=False,
|
||
|
|
lower_is_better=False)
|
||
|
|
# Show messages when players die since it's meaningful here.
|
||
|
|
announce_player_deaths = True
|
||
|
|
|
||
|
|
@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.IntSetting(
|
||
|
|
'Max Zombies',
|
||
|
|
default=10,
|
||
|
|
min_value=5,
|
||
|
|
max_value=50,
|
||
|
|
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),
|
||
|
|
]
|
||
|
|
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._max_zombies = int(settings['Max Zombies'])
|
||
|
|
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)
|
||
|
|
|
||
|
|
self.spazList = []
|
||
|
|
self.zombieQ = 0
|
||
|
|
|
||
|
|
activity = ba.getactivity()
|
||
|
|
my_factory = SpazFactory.get()
|
||
|
|
|
||
|
|
appears = ['Kronk','Zoe','Pixel','Agent Johnson',
|
||
|
|
'Bones','Frosty','Kronk2']
|
||
|
|
myAppear = copy.copy(ba.app.spaz_appearances['Kronk'])
|
||
|
|
myAppear.name = 'Kronk2'
|
||
|
|
ba.app.spaz_appearances['Kronk2'] = myAppear
|
||
|
|
for appear in appears:
|
||
|
|
my_factory.get_media(appear)
|
||
|
|
med = my_factory.spaz_media
|
||
|
|
med['Kronk2']['head_model'] = med['Zoe']['head_model']
|
||
|
|
med['Kronk2']['color_texture'] = med['Agent Johnson']['color_texture']
|
||
|
|
med['Kronk2']['color_mask_texture']=med['Pixel']['color_mask_texture']
|
||
|
|
med['Kronk2']['torso_model'] = med['Bones']['torso_model']
|
||
|
|
med['Kronk2']['pelvis_model'] = med['Pixel']['pelvis_model']
|
||
|
|
med['Kronk2']['upper_arm_model'] = med['Frosty']['upper_arm_model']
|
||
|
|
med['Kronk2']['forearm_model'] = med['Frosty']['forearm_model']
|
||
|
|
med['Kronk2']['hand_model'] = med['Bones']['hand_model']
|
||
|
|
med['Kronk2']['upper_leg_model'] = med['Bones']['upper_leg_model']
|
||
|
|
med['Kronk2']['lower_leg_model'] = med['Pixel']['lower_leg_model']
|
||
|
|
med['Kronk2']['toes_model'] = med['Bones']['toes_model']
|
||
|
|
|
||
|
|
def get_instance_description(self) -> str | Sequence:
|
||
|
|
return ('Kill walkers for points! ',
|
||
|
|
'Dead player walker: 2 points!') if isinstance(
|
||
|
|
self.session, ba.DualTeamSession) else (
|
||
|
|
'Kill walkers for points! Dead player walker: 2 points!')
|
||
|
|
|
||
|
|
def get_instance_description_short(self) -> str | Sequence:
|
||
|
|
return ('Kill walkers for points! ',
|
||
|
|
'Dead player walker: 2 points!') if isinstance(
|
||
|
|
self.session, ba.DualTeamSession) else (
|
||
|
|
'Kill walkers for points! Dead player walker: 2 points!')
|
||
|
|
|
||
|
|
def on_player_join(self, player: Player) -> None:
|
||
|
|
if self.has_begun():
|
||
|
|
player.lives = 0
|
||
|
|
player.icons = []
|
||
|
|
ba.screenmessage(
|
||
|
|
ba.Lstr(resource='playerDelayedJoinText',
|
||
|
|
subs=[('${PLAYER}', player.getname(full=True))]),
|
||
|
|
color=(0, 1, 0),
|
||
|
|
)
|
||
|
|
return
|
||
|
|
|
||
|
|
player.lives = self._lives_per_player
|
||
|
|
|
||
|
|
if self._solo_mode:
|
||
|
|
player.icons = []
|
||
|
|
player.team.spawn_order.append(player)
|
||
|
|
self._update_solo_mode()
|
||
|
|
else:
|
||
|
|
player.icons = [Icon(player, position=(0, 50), scale=0.8)]
|
||
|
|
if player.lives > 0:
|
||
|
|
self.spawn_player(player)
|
||
|
|
|
||
|
|
if self.has_begun():
|
||
|
|
self._update_icons()
|
||
|
|
|
||
|
|
def _update_solo_mode(self) -> None:
|
||
|
|
for team in self.teams:
|
||
|
|
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 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()
|
||
|
|
self.zombieQ = 1
|
||
|
|
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._bots = zBotSet()
|
||
|
|
|
||
|
|
#Set colors and character for ToughGuyBot to be zombie
|
||
|
|
setattr(BrawlerBot, 'color', (0.4,0.1,0.05))
|
||
|
|
setattr(BrawlerBot, 'highlight', (0.2,0.4,0.3))
|
||
|
|
setattr(BrawlerBot, 'character', 'Kronk2')
|
||
|
|
# start some timers to spawn bots
|
||
|
|
thePt = self.map.get_ffa_start_position(self.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_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:
|
||
|
|
position = self.map.get_ffa_start_position(self.players)
|
||
|
|
angle = 20
|
||
|
|
name = player.getname()
|
||
|
|
|
||
|
|
light_color = _math.normalized_color(player.color)
|
||
|
|
display_color = _ba.safecolor(player.color, target_intensity=0.75)
|
||
|
|
spaz = PlayerSpaz_Zom(color=player.color,
|
||
|
|
highlight=player.highlight,
|
||
|
|
character=player.character,
|
||
|
|
player=player)
|
||
|
|
player.actor = spaz
|
||
|
|
assert spaz.node
|
||
|
|
self.spazList.append(spaz)
|
||
|
|
|
||
|
|
if isinstance(self.session, CoopSession) and self.map.getname() in [
|
||
|
|
'Courtyard', 'Tower D'
|
||
|
|
]:
|
||
|
|
mat = self.map.preloaddata['collide_with_wall_material']
|
||
|
|
assert isinstance(spaz.node.materials, tuple)
|
||
|
|
assert isinstance(spaz.node.roller_materials, tuple)
|
||
|
|
spaz.node.materials += (mat, )
|
||
|
|
spaz.node.roller_materials += (mat, )
|
||
|
|
|
||
|
|
spaz.node.name = name
|
||
|
|
spaz.node.name_color = display_color
|
||
|
|
spaz.connect_controls_to_player()
|
||
|
|
factory = SpazFactory()
|
||
|
|
|
||
|
|
# Move to the stand position and add a flash of light.
|
||
|
|
spaz.handlemessage(
|
||
|
|
StandMessage(
|
||
|
|
position,
|
||
|
|
angle if angle is not None else random.uniform(0, 360)))
|
||
|
|
_ba.playsound(self._spawn_sound, 1, position=spaz.node.position)
|
||
|
|
light = _ba.newnode('light', attrs={'color': light_color})
|
||
|
|
spaz.node.connectattr('position', light, 'position')
|
||
|
|
ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
|
||
|
|
_ba.timer(0.5, light.delete)
|
||
|
|
|
||
|
|
if not self._solo_mode:
|
||
|
|
ba.timer(0.3, ba.Call(self._print_lives, player))
|
||
|
|
|
||
|
|
for icon in player.icons:
|
||
|
|
icon.handle_player_spawned()
|
||
|
|
return spaz
|
||
|
|
|
||
|
|
def respawn_player_zombie(self,
|
||
|
|
player: Player,
|
||
|
|
respawn_time: float | None = None) -> None:
|
||
|
|
# pylint: disable=cyclic-import
|
||
|
|
|
||
|
|
assert player
|
||
|
|
if respawn_time is None:
|
||
|
|
teamsize = len(player.team.players)
|
||
|
|
if teamsize == 1:
|
||
|
|
respawn_time = 3.0
|
||
|
|
elif teamsize == 2:
|
||
|
|
respawn_time = 5.0
|
||
|
|
elif teamsize == 3:
|
||
|
|
respawn_time = 6.0
|
||
|
|
else:
|
||
|
|
respawn_time = 7.0
|
||
|
|
|
||
|
|
# If this standard setting is present, factor it in.
|
||
|
|
if 'Respawn Times' in self.settings_raw:
|
||
|
|
respawn_time *= self.settings_raw['Respawn Times']
|
||
|
|
|
||
|
|
# We want whole seconds.
|
||
|
|
assert respawn_time is not None
|
||
|
|
respawn_time = round(max(1.0, respawn_time), 0)
|
||
|
|
|
||
|
|
if player.actor and not self.has_ended():
|
||
|
|
from bastd.actor.respawnicon import RespawnIcon
|
||
|
|
player.customdata['respawn_timer'] = _ba.Timer(
|
||
|
|
respawn_time, ba.WeakCall(
|
||
|
|
self.spawn_player_if_exists_as_zombie, player))
|
||
|
|
player.customdata['respawn_icon'] = RespawnIcon(
|
||
|
|
player, respawn_time)
|
||
|
|
|
||
|
|
def spawn_player_if_exists_as_zombie(self, player: PlayerType) -> None:
|
||
|
|
"""
|
||
|
|
A utility method which calls self.spawn_player() *only* if the
|
||
|
|
ba.Player provided still exists; handy for use in timers and whatnot.
|
||
|
|
|
||
|
|
There is no need to override this; just override spawn_player().
|
||
|
|
"""
|
||
|
|
if player:
|
||
|
|
self.spawn_player_zombie(player)
|
||
|
|
|
||
|
|
def spawn_player_zombie(self, player: PlayerType) -> ba.Actor:
|
||
|
|
position = self.map.get_ffa_start_position(self.players)
|
||
|
|
angle = 20
|
||
|
|
name = player.getname()
|
||
|
|
|
||
|
|
light_color = _math.normalized_color(player.color)
|
||
|
|
display_color = _ba.safecolor(player.color, target_intensity=0.75)
|
||
|
|
spaz = PlayerSpaz_Zom(color=player.color,
|
||
|
|
highlight=player.highlight,
|
||
|
|
character='Kronk2',
|
||
|
|
player=player)
|
||
|
|
player.actor = spaz
|
||
|
|
assert spaz.node
|
||
|
|
self.spazList.append(spaz)
|
||
|
|
|
||
|
|
if isinstance(self.session, CoopSession) and self.map.getname() in [
|
||
|
|
'Courtyard', 'Tower D'
|
||
|
|
]:
|
||
|
|
mat = self.map.preloaddata['collide_with_wall_material']
|
||
|
|
assert isinstance(spaz.node.materials, tuple)
|
||
|
|
assert isinstance(spaz.node.roller_materials, tuple)
|
||
|
|
spaz.node.materials += (mat, )
|
||
|
|
spaz.node.roller_materials += (mat, )
|
||
|
|
|
||
|
|
spaz.node.name = name
|
||
|
|
spaz.node.name_color = display_color
|
||
|
|
spaz.connect_controls_to_player(enable_punch=True,
|
||
|
|
enable_bomb=False,
|
||
|
|
enable_pickup=False)
|
||
|
|
|
||
|
|
# Move to the stand position and add a flash of light.
|
||
|
|
spaz.handlemessage(
|
||
|
|
StandMessage(
|
||
|
|
position,
|
||
|
|
angle if angle is not None else random.uniform(0, 360)))
|
||
|
|
_ba.playsound(self._spawn_sound, 1, position=spaz.node.position)
|
||
|
|
light = _ba.newnode('light', attrs={'color': light_color})
|
||
|
|
spaz.node.connectattr('position', light, 'position')
|
||
|
|
ba.animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
|
||
|
|
_ba.timer(0.5, light.delete)
|
||
|
|
|
||
|
|
if not self._solo_mode:
|
||
|
|
ba.timer(0.3, ba.Call(self._print_lives, player))
|
||
|
|
|
||
|
|
for icon in player.icons:
|
||
|
|
icon.handle_player_spawned()
|
||
|
|
return spaz
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
try:
|
||
|
|
pos = player.actor.node.position
|
||
|
|
except Exception as e:
|
||
|
|
print('EXC getting player pos in bsElim',e)
|
||
|
|
return
|
||
|
|
if player.lives > 0:
|
||
|
|
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=pos).autoretain()
|
||
|
|
else:
|
||
|
|
popuptext.PopupText('Dead!',
|
||
|
|
color=(1, 1, 0, 1),
|
||
|
|
offset=(0, -0.8, 0),
|
||
|
|
random_offset=0.0,
|
||
|
|
scale=1.8,
|
||
|
|
position=pos).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)
|
||
|
|
|
||
|
|
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)
|
||
|
|
|
||
|
|
if player.lives > 0:
|
||
|
|
player.lives -= 1
|
||
|
|
else:
|
||
|
|
if msg._killerplayer:
|
||
|
|
if msg._killerplayer.lives > 0:
|
||
|
|
msg._killerplayer.team.score += 2
|
||
|
|
self._update_scoreboard()
|
||
|
|
|
||
|
|
if msg._player in self.spazList:
|
||
|
|
self.spazList.remove(msg._player)
|
||
|
|
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:
|
||
|
|
self.respawn_player_zombie(player)
|
||
|
|
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)
|
||
|
|
|
||
|
|
elif isinstance(msg, SpazBotDiedMessage):
|
||
|
|
self._onSpazBotDied(msg)
|
||
|
|
super().handlemessage(msg)#bs.PopupText("died",position=self._position,color=popupColor,scale=popupScale).autoRetain()
|
||
|
|
else:
|
||
|
|
super().handlemessage(msg)
|
||
|
|
|
||
|
|
def _update(self) -> None:
|
||
|
|
if self.zombieQ > 0:
|
||
|
|
self.zombieQ -= 1
|
||
|
|
self.spawn_zombie()
|
||
|
|
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).
|
||
|
|
teamsRemain = self._get_living_teams()
|
||
|
|
if len(teamsRemain) < 2:
|
||
|
|
if len(teamsRemain) == 1:
|
||
|
|
theScores = []
|
||
|
|
for team in self.teams:
|
||
|
|
theScores.append(team.score)
|
||
|
|
if teamsRemain[0].score < max(theScores):
|
||
|
|
pass
|
||
|
|
elif teamsRemain[0].score == max(
|
||
|
|
theScores) and theScores.count(max(theScores)) > 1:
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
self._round_end_timer = ba.Timer(0.5, self.end_game)
|
||
|
|
else:
|
||
|
|
self._round_end_timer = ba.Timer(0.5, self.end_game)
|
||
|
|
|
||
|
|
def spawn_zombie(self) -> None:
|
||
|
|
#We need a Z height...
|
||
|
|
thePt = list(self.get_random_point_in_play())
|
||
|
|
thePt2 = self.map.get_ffa_start_position(self.players)
|
||
|
|
thePt[1] = thePt2[1]
|
||
|
|
ba.timer(0.1, ba.Call(
|
||
|
|
self._bots.spawn_bot, BrawlerBot, pos=thePt, spawn_time=1.0))
|
||
|
|
|
||
|
|
def _onSpazBotDied(self,DeathMsg) -> None:
|
||
|
|
#Just in case we are over max...
|
||
|
|
if len(self._bots.get_living_bots()) < self._max_zombies:
|
||
|
|
self.zombieQ += 1
|
||
|
|
|
||
|
|
if DeathMsg.killerplayer is None:
|
||
|
|
pass
|
||
|
|
else:
|
||
|
|
player = DeathMsg.killerplayer
|
||
|
|
if not player:
|
||
|
|
return
|
||
|
|
if player.lives < 1:
|
||
|
|
return
|
||
|
|
player.team.score += 1
|
||
|
|
self.zombieQ += 1
|
||
|
|
self._update_scoreboard()
|
||
|
|
|
||
|
|
def get_random_point_in_play(self) -> None:
|
||
|
|
myMap = self.map.getname()
|
||
|
|
if myMap == 'Doom Shroom':
|
||
|
|
while True:
|
||
|
|
x = random.uniform(-1.0,1.0)
|
||
|
|
y = random.uniform(-1.0,1.0)
|
||
|
|
if x*x+y*y < 1.0: break
|
||
|
|
return ((8.0*x,8.0,-3.5+5.0*y))
|
||
|
|
elif myMap == 'Rampage':
|
||
|
|
x = random.uniform(-6.0,7.0)
|
||
|
|
y = random.uniform(-6.0,-2.5)
|
||
|
|
return ((x, 8.0, y))
|
||
|
|
elif myMap == 'Hockey Stadium':
|
||
|
|
x = random.uniform(-11.5,11.5)
|
||
|
|
y = random.uniform(-4.5,4.5)
|
||
|
|
return ((x, 5.0, y))
|
||
|
|
elif myMap == 'Courtyard':
|
||
|
|
x = random.uniform(-4.3,4.3)
|
||
|
|
y = random.uniform(-4.4,0.3)
|
||
|
|
return ((x, 8.0, y))
|
||
|
|
elif myMap == 'Crag Castle':
|
||
|
|
x = random.uniform(-6.7,8.0)
|
||
|
|
y = random.uniform(-6.0,0.0)
|
||
|
|
return ((x, 12.0, y))
|
||
|
|
elif myMap == 'Big G':
|
||
|
|
x = random.uniform(-8.7,8.0)
|
||
|
|
y = random.uniform(-7.5,6.5)
|
||
|
|
return ((x, 8.0, y))
|
||
|
|
elif myMap == 'Football Stadium':
|
||
|
|
x = random.uniform(-12.5,12.5)
|
||
|
|
y = random.uniform(-5.0,5.5)
|
||
|
|
return ((x, 8.0, y))
|
||
|
|
else:
|
||
|
|
x = random.uniform(-5.0,5.0)
|
||
|
|
y = random.uniform(-6.0,0.0)
|
||
|
|
return ((x, 8.0, y))
|
||
|
|
|
||
|
|
def _update_scoreboard(self) -> None:
|
||
|
|
for team in self.teams:
|
||
|
|
self._scoreboard.set_team_value(team, team.score)
|
||
|
|
|
||
|
|
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
|
||
|
|
setattr(BrawlerBot, 'color', (0.6, 0.6, 0.6))
|
||
|
|
setattr(BrawlerBot, 'highlight', (0.6, 0.6, 0.6))
|
||
|
|
setattr(BrawlerBot, 'character', 'Kronk')
|
||
|
|
results = ba.GameResults()
|
||
|
|
self._vs_text = None # Kill our 'vs' if its there.
|
||
|
|
for team in self.teams:
|
||
|
|
results.set_team_score(team, team.score)
|
||
|
|
self.end(results=results)
|
||
|
|
|
||
|
|
|
||
|
|
# ba_meta export game
|
||
|
|
class ZombieHordeCoop(ZombieHorde):
|
||
|
|
|
||
|
|
name = 'Zombie Horde'
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def get_supported_maps(cls, sessiontype: type[ba.Session]) -> list[str]:
|
||
|
|
return ['Football Stadium']
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
||
|
|
return (issubclass(sessiontype, ba.CoopSession))
|
||
|
|
|
||
|
|
def _update(self) -> None:
|
||
|
|
if self.zombieQ > 0:
|
||
|
|
self.zombieQ -= 1
|
||
|
|
self.spawn_zombie()
|
||
|
|
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 not any(player.is_alive() for player in self.teams[0].players):
|
||
|
|
self._round_end_timer = ba.Timer(0.5, self.end_game)
|
||
|
|
|
||
|
|
def handlemessage(self, msg: Any) -> Any:
|
||
|
|
if isinstance(msg, ba.PlayerDiedMessage):
|
||
|
|
# Augment standard behavior.
|
||
|
|
ba.TeamGameActivity.handlemessage(self, msg)
|
||
|
|
player: Player = msg.getplayer(Player)
|
||
|
|
# If we have any icons, update their state.
|
||
|
|
for icon in player.icons:
|
||
|
|
icon.handle_player_died()
|
||
|
|
else:
|
||
|
|
super().handlemessage(msg)
|
||
|
|
|
||
|
|
|
||
|
|
# ba_meta export plugin
|
||
|
|
class ZombieHordeLevel(ba.Plugin):
|
||
|
|
def on_app_running(self) -> None:
|
||
|
|
ba.app.add_coop_practice_level(
|
||
|
|
ba.Level(
|
||
|
|
'Zombie Horde',
|
||
|
|
gametype=ZombieHordeCoop,
|
||
|
|
settings={},
|
||
|
|
preview_texture_name='footballStadiumPreview',
|
||
|
|
)
|
||
|
|
)
|