2019-10-05 05:01:46 -07:00

993 lines
39 KiB
Python

"""Implements lobby system for gathering before games, char select, etc."""
from __future__ import annotations
import random
import weakref
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Optional, List, Dict, Any, Sequence, Union
import ba
# Hmm should we move this to actors?..
class JoinInfo:
"""Display useful info for joiners."""
def __init__(self, lobby: ba.Lobby):
# pylint: disable=too-many-locals
from ba import _input
from ba._lang import Lstr
from ba import _actor
from ba import _general
from ba._enums import SpecialChar
can_switch_teams = (len(lobby.teams) > 1)
self._state = 0
press_to_punch: Union[str, ba.Lstr] = _ba.charstr(
SpecialChar.LEFT_BUTTON)
press_to_bomb: Union[str, ba.Lstr] = _ba.charstr(
SpecialChar.RIGHT_BUTTON)
# If we have a keyboard, grab keys for punch and pickup.
# FIXME: This of course is only correct on the local device;
# Should change this for net games.
keyboard = _ba.get_input_device('Keyboard', '#1', doraise=False)
if keyboard is not None:
punch_key = keyboard.get_button_name(
_input.get_device_value(keyboard, 'buttonPunch'))
press_to_punch = Lstr(resource='orText',
subs=[('${A}',
Lstr(value='\'${K}\'',
subs=[('${K}', punch_key)])),
('${B}', press_to_punch)])
bomb_key = keyboard.get_button_name(
_input.get_device_value(keyboard, 'buttonBomb'))
press_to_bomb = Lstr(resource='orText',
subs=[('${A}',
Lstr(value='\'${K}\'',
subs=[('${K}', bomb_key)])),
('${B}', press_to_bomb)])
join_str = Lstr(value='${A} < ${B} >',
subs=[('${A}',
Lstr(resource='pressPunchToJoinText')),
('${B}', press_to_punch)])
else:
join_str = Lstr(resource='pressAnyButtonToJoinText')
flatness = 1.0 if _ba.app.vr_mode else 0.0
self._text = _actor.Actor(
_ba.newnode('text',
attrs={
'position': (0, -40),
'h_attach': 'center',
'v_attach': 'top',
'h_align': 'center',
'color': (0.7, 0.7, 0.95, 1.0),
'flatness': flatness,
'text': join_str
}))
if _ba.app.kiosk_mode:
self._messages = [join_str]
else:
msg1 = Lstr(resource='pressToSelectProfileText',
subs=[
('${BUTTONS}', _ba.charstr(SpecialChar.UP_ARROW) +
' ' + _ba.charstr(SpecialChar.DOWN_ARROW))
])
msg2 = Lstr(resource='pressToOverrideCharacterText',
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))])
msg3 = Lstr(value='${A} < ${B} >',
subs=[('${A}', msg2), ('${B}', press_to_bomb)])
self._messages = (([
Lstr(resource='pressToSelectTeamText',
subs=[('${BUTTONS}', _ba.charstr(SpecialChar.LEFT_ARROW) +
' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))])
] if can_switch_teams else []) + [msg1] + [msg3] + [join_str])
self._timer = _ba.Timer(4.0,
_general.WeakCall(self._update),
repeat=True)
def _update(self) -> None:
assert self._text.node
self._text.node.text = self._messages[self._state]
self._state = (self._state + 1) % len(self._messages)
class PlayerReadyMessage:
"""Tells an object a player has been selected from the given chooser."""
def __init__(self, chooser: ba.Chooser):
self.chooser = chooser
class ChangeMessage:
"""Tells an object a selection is being changed."""
def __init__(self, what: str, value: int):
self.what = what
self.value = value
class Chooser:
"""A character/team selector for a single player."""
def __del__(self) -> None:
# Just kill off our base node; the rest should go down with it.
if self._text_node:
self._text_node.delete()
def __init__(self, vpos: float, player: _ba.Player,
lobby: 'Lobby') -> None:
# FIXME: Tidy up around here.
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
from ba import _gameutils
from ba import _profile
from ba import _lang
app = _ba.app
self._deek_sound = _ba.getsound('deek')
self._click_sound = _ba.getsound('click01')
self._punchsound = _ba.getsound('punch01')
self._swish_sound = _ba.getsound('punchSwish')
self._errorsound = _ba.getsound('error')
self._mask_texture = _ba.gettexture('characterIconMask')
self._vpos = vpos
self._lobby = weakref.ref(lobby)
self._player = player
self._inited = False
self._dead = False
self._text_node: Optional[ba.Node] = None
self._profilename = ''
self._profilenames: List[str] = []
self._ready: bool = False
self.character_names: List[str] = []
# Hmm does this need to be public?
self.profiles: Dict[str, Dict[str, Any]] = {}
# Load available profiles either from the local config or from the
# remote device.
self.reload_profiles()
# Note: this is just our local index out of available teams; *not*
# the team-id!
self._selected_team_index: int = self.lobby.next_add_team
# Store a persistent random character index; we'll use this for the
# '_random' profile. Let's use their input_device id to seed it. This
# will give a persistent character for them between games and will
# distribute characters nicely if everyone is random.
try:
input_device_id = self._player.get_input_device().id
except Exception:
from ba import _error
_error.print_exception('Error getting device-id on chooser create')
input_device_id = 0
if app.lobby_random_char_index_offset is None:
# We want the first device that asks for a chooser to always get
# spaz as a random character..
# scratch that.. we now kinda accomplish the same thing with
# account profiles so lets just be fully random here.
app.lobby_random_char_index_offset = (random.randrange(1000))
# To calc our random index we pick a random character out of our
# unlocked list and then locate that character's index in the full
# list.
char_index_offset = app.lobby_random_char_index_offset
assert char_index_offset is not None
self._random_character_index = ((input_device_id + char_index_offset) %
len(self.character_names))
self._random_color, self._random_highlight = (
_profile.get_player_profile_colors(None))
# Attempt to pick an initial profile based on what's been stored
# for this input device.
input_device = self._player.get_input_device()
try:
name = input_device.name
unique_id = input_device.unique_identifier
self._profilename = (
app.config['Default Player Profiles'][name + ' ' + unique_id])
self._profileindex = self._profilenames.index(self._profilename)
# If this one is __account__ and is local and we haven't marked
# anyone as the account-profile device yet, mark this guy as it.
# (prevents the next joiner from getting the account profile too).
if (self._profilename == '__account__'
and not input_device.is_remote_client
and app.lobby_account_profile_device_id is None):
app.lobby_account_profile_device_id = input_device_id
# Well hmm that didn't work.. pick __account__, _random, or some
# other random profile.
except Exception:
profilenames = self._profilenames
# We want the first local input-device in the game to latch on to
# the account profile.
if (not input_device.is_remote_client
and not input_device.is_controller_app):
if (app.lobby_account_profile_device_id is None
and '__account__' in profilenames):
app.lobby_account_profile_device_id = input_device_id
# If this is the designated account-profile-device, try to default
# to the account profile.
if (input_device_id == app.lobby_account_profile_device_id
and '__account__' in profilenames):
self._profileindex = profilenames.index('__account__')
else:
# If this is the controller app, it defaults to using a random
# profile (since we can pull the random name from the app).
if input_device.is_controller_app:
self._profileindex = profilenames.index('_random')
else:
# If its a client connection, for now just force
# the account profile if possible.. (need to provide a
# way for clients to specify/remember their default
# profile on remote servers that do not already know them).
if (input_device.is_remote_client
and '__account__' in profilenames):
self._profileindex = profilenames.index('__account__')
else:
# Cycle through our non-random profiles once; after
# that, everyone gets random.
while (app.lobby_random_profile_index <
len(profilenames) and
profilenames[app.lobby_random_profile_index] in
('_random', '__account__', '_edit')):
app.lobby_random_profile_index += 1
if (app.lobby_random_profile_index <
len(profilenames)):
self._profileindex = (
app.lobby_random_profile_index)
app.lobby_random_profile_index += 1
else:
self._profileindex = profilenames.index('_random')
self._profilename = profilenames[self._profileindex]
self.character_index = self._random_character_index
self._color = self._random_color
self._highlight = self._random_highlight
self._text_node = _ba.newnode('text',
delegate=self,
attrs={
'position': (-100, self._vpos),
'maxwidth': 160,
'shadow': 0.5,
'vr_depth': -20,
'h_align': 'left',
'v_align': 'center',
'v_attach': 'top'
})
_gameutils.animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
self.icon = _ba.newnode('image',
owner=self._text_node,
attrs={
'position': (-130, self._vpos + 20),
'mask_texture': self._mask_texture,
'vr_depth': -10,
'attach': 'topCenter'
})
_gameutils.animate_array(self.icon, 'scale', 2, {
0: (0, 0),
0.1: (45, 45)
})
self._set_ready(False)
# Set our initial name to '<choosing player>' in case anyone asks.
self._player.set_name(
_lang.Lstr(resource='choosingPlayerText').evaluate(), real=False)
self.update_from_player_profiles()
self.update_position()
self._inited = True
@property
def player(self) -> ba.Player:
"""The ba.Player associated with this chooser."""
return self._player
@property
def ready(self) -> bool:
"""Whether this chooser is checked in as ready."""
return self._ready
def set_vpos(self, vpos: float) -> None:
"""(internal)"""
self._vpos = vpos
def set_dead(self, val: bool) -> None:
"""(internal)"""
self._dead = val
def get_team(self) -> ba.Team:
"""Return this chooser's selected ba.Team."""
return self.lobby.teams[self._selected_team_index]
@property
def lobby(self) -> ba.Lobby:
"""The chooser's ba.Lobby."""
lobby = self._lobby()
if lobby is None:
raise Exception('Lobby does not exist.')
return lobby
def get_lobby(self) -> Optional[ba.Lobby]:
"""Return this chooser's lobby if it still exists; otherwise None."""
return self._lobby()
def update_from_player_profiles(self) -> None:
"""Set character based on profile; otherwise use pre-picked random."""
try:
from ba import _profile
# Store the name even though we usually use index (in case
# the profile list changes)
self._profilename = self._profilenames[self._profileindex]
character = self.profiles[self._profilename]['character']
# Hmmm; at the moment we're not properly pulling the list
# of available characters from clients, so profiles might use a
# character not in their list. for now, just go ahead and add
# the character name to their list as long as we're aware of it
# UPDATE: actually we now should be getting their character list,
# so this should no longer be happening; adding warning print
# for now and can delete later.
if (character not in self.character_names
and character in _ba.app.spaz_appearances):
print('got remote character not in their character list:',
character, self.character_names)
self.character_names.append(character)
self.character_index = self.character_names.index(character)
self._color, self._highlight = (_profile.get_player_profile_colors(
self._profilename, profiles=self.profiles))
except Exception:
# FIXME: Should never use top level Exception for logic; only
# error catching (and they should always be logged).
self.character_index = self._random_character_index
self._color = self._random_color
self._highlight = self._random_highlight
self._update_icon()
self._update_text()
def reload_profiles(self) -> None:
"""Reload all player profiles."""
from ba import _general
app = _ba.app
# Re-construct our profile index and other stuff since the profile
# list might have changed.
input_device = self._player.get_input_device()
is_remote = input_device.is_remote_client
is_test_input = (input_device is not None
and input_device.name.startswith('TestInput'))
# Pull this player's list of unlocked characters.
if is_remote:
# FIXME: Pull this from remote player (but make sure to
# filter it to ones we've got).
self.character_names = ['Spaz']
else:
self.character_names = self.lobby.character_names_local_unlocked
# If we're a local player, pull our local profiles from the config.
# Otherwise ask the remote-input-device for its profile list.
if is_remote:
self.profiles = input_device.get_player_profiles()
else:
self.profiles = app.config.get('Player Profiles', {})
# These may have come over the wire from an older
# (non-unicode/non-json) version.
# Make sure they conform to our standards
# (unicode strings, no tuples, etc)
self.profiles = _general.json_prep(self.profiles)
# Filter out any characters we're unaware of.
for profile in list(self.profiles.items()):
if profile[1].get('character', '') not in app.spaz_appearances:
profile[1]['character'] = 'Spaz'
# Add in a random one so we're ok even if there's no
# user-created profiles.
self.profiles['_random'] = {}
# In kiosk mode we disable account profiles to force random.
if app.kiosk_mode:
if '__account__' in self.profiles:
del self.profiles['__account__']
# For local devices, add it an 'edit' option which will pop up
# the profile window.
if not is_remote and not is_test_input and not app.kiosk_mode:
self.profiles['_edit'] = {}
# Build a sorted name list we can iterate through.
self._profilenames = list(self.profiles.keys())
self._profilenames.sort(key=lambda x: x.lower())
if self._profilename in self._profilenames:
self._profileindex = self._profilenames.index(self._profilename)
else:
self._profileindex = 0
self._profilename = self._profilenames[self._profileindex]
def update_position(self) -> None:
"""Update this chooser's position."""
from ba import _gameutils
# Hmmm this shouldn't be happening.
if not self._text_node:
print('Error: chooser text nonexistent..')
import traceback
traceback.print_stack()
return
spacing = 350
teams = self.lobby.teams
offs = (spacing * -0.5 * len(teams) +
spacing * self._selected_team_index + 250)
if len(teams) > 1:
offs -= 35
_gameutils.animate_array(self._text_node, 'position', 2, {
0: self._text_node.position,
0.1: (-100 + offs, self._vpos + 23)
})
_gameutils.animate_array(self.icon, 'position', 2, {
0: self.icon.position,
0.1: (-130 + offs, self._vpos + 22)
})
def get_character_name(self) -> str:
"""Return the selected character name."""
return self.character_names[self.character_index]
def _do_nothing(self) -> None:
"""Does nothing! (hacky way to disable callbacks)"""
def _get_name(self, full: bool = False) -> str:
# FIXME: Needs cleanup.
# pylint: disable=too-many-branches
from ba._lang import Lstr
from ba._enums import SpecialChar
name_raw = name = self._profilenames[self._profileindex]
clamp = False
if name == '_random':
input_device: Optional[ba.InputDevice]
try:
input_device = self._player.get_input_device()
except Exception:
input_device = None
if input_device is not None:
name = input_device.get_default_player_name()
else:
name = 'Invalid'
if not full:
clamp = True
elif name == '__account__':
try:
input_device = self._player.get_input_device()
except Exception:
input_device = None
if input_device is not None:
name = input_device.get_account_name(full)
else:
name = 'Invalid'
if not full:
clamp = True
elif name == '_edit':
# FIXME: This causes problems as an Lstr, but its ok to
# explicitly translate for now since this is only shown on the
# host. (also should elaborate; don't remember what problems this
# caused)
name = (Lstr(
resource='createEditPlayerText',
fallback_resource='editProfileWindow.titleNewText').evaluate())
else:
# If we have a regular profile marked as global with an icon,
# use it (for full only).
if full:
try:
if self.profiles[name_raw].get('global', False):
icon = (self.profiles[name_raw]['icon']
if 'icon' in self.profiles[name_raw] else
_ba.charstr(SpecialChar.LOGO))
name = icon + name
except Exception:
from ba import _error
_error.print_exception('Error applying global icon')
else:
# We now clamp non-full versions of names so there's at
# least some hope of reading them in-game.
clamp = True
if clamp:
if len(name) > 10:
name = name[:10] + '...'
return name
def _set_ready(self, ready: bool) -> None:
# pylint: disable=cyclic-import
from bastd.ui.profile import browser as pbrowser
from ba import _general
profilename = self._profilenames[self._profileindex]
# Handle '_edit' as a special case.
if profilename == '_edit' and ready:
with _ba.Context('ui'):
pbrowser.ProfileBrowserWindow(in_main_menu=False)
# give their input-device UI ownership too
# (prevent someone else from snatching it in crowded games)
_ba.set_ui_input_device(self._player.get_input_device())
return
if not ready:
self._player.assign_input_call(
'leftPress',
_general.Call(self.handlemessage, ChangeMessage('team', -1)))
self._player.assign_input_call(
'rightPress',
_general.Call(self.handlemessage, ChangeMessage('team', 1)))
self._player.assign_input_call(
'bombPress',
_general.Call(self.handlemessage,
ChangeMessage('character', 1)))
self._player.assign_input_call(
'upPress',
_general.Call(self.handlemessage,
ChangeMessage('profileindex', -1)))
self._player.assign_input_call(
'downPress',
_general.Call(self.handlemessage,
ChangeMessage('profileindex', 1)))
self._player.assign_input_call(
('jumpPress', 'pickUpPress', 'punchPress'),
_general.Call(self.handlemessage, ChangeMessage('ready', 1)))
self._ready = False
self._update_text()
self._player.set_name('untitled', real=False)
else:
self._player.assign_input_call(
('leftPress', 'rightPress', 'upPress', 'downPress',
'jumpPress', 'bombPress', 'pickUpPress'), self._do_nothing)
self._player.assign_input_call(
('jumpPress', 'bombPress', 'pickUpPress', 'punchPress'),
_general.Call(self.handlemessage, ChangeMessage('ready', 0)))
# Store the last profile picked by this input for reuse.
input_device = self._player.get_input_device()
name = input_device.name
unique_id = input_device.unique_identifier
device_profiles = _ba.app.config.setdefault(
'Default Player Profiles', {})
# Make an exception if we have no custom profiles and are set
# to random; in that case we'll want to start picking up custom
# profiles if/when one is made so keep our setting cleared.
special = ('_random', '_edit', '__account__')
have_custom_profiles = any(p not in special for p in self.profiles)
profilekey = name + ' ' + unique_id
if profilename == '_random' and not have_custom_profiles:
if profilekey in device_profiles:
del device_profiles[profilekey]
else:
device_profiles[profilekey] = profilename
_ba.app.config.commit()
# Set this player's short and full name.
self._player.set_name(self._get_name(),
self._get_name(full=True),
real=True)
self._ready = True
self._update_text()
# Inform the session that this player is ready.
_ba.getsession().handlemessage(PlayerReadyMessage(self))
def _handle_ready_msg(self, ready: bool) -> None:
force_team_switch = False
# Team auto-balance kicks us to another team if we try to
# join the team with the most players.
if not self._ready:
if _ba.app.config.get('Auto Balance Teams', False):
lobby = self.lobby
teams = lobby.teams
if len(teams) > 1:
# First, calc how many players are on each team
# ..we need to count both active players and
# choosers that have been marked as ready.
team_player_counts = {}
for team in teams:
team_player_counts[team.get_id()] = (len(team.players))
for chooser in lobby.choosers:
if chooser.ready:
team_player_counts[
chooser.get_team().get_id()] += 1
largest_team_size = max(team_player_counts.values())
smallest_team_size = (min(team_player_counts.values()))
# Force switch if we're on the biggest team
# and there's a smaller one available.
if (largest_team_size != smallest_team_size
and team_player_counts[self.get_team().get_id()] >=
largest_team_size):
force_team_switch = True
# Either force switch teams, or actually for realsies do the set-ready.
if force_team_switch:
_ba.playsound(self._errorsound)
self.handlemessage(ChangeMessage('team', 1))
else:
_ba.playsound(self._punchsound)
self._set_ready(ready)
def handlemessage(self, msg: Any) -> Any:
"""Standard generic message handler."""
if isinstance(msg, ChangeMessage):
# If we've been removed from the lobby, ignore this stuff.
if self._dead:
from ba import _error
_error.print_error("chooser got ChangeMessage after dying")
return
if not self._text_node:
from ba import _error
_error.print_error('got ChangeMessage after nodes died')
return
if msg.what == 'team':
teams = self.lobby.teams
if len(teams) > 1:
_ba.playsound(self._swish_sound)
self._selected_team_index = (
(self._selected_team_index + msg.value) % len(teams))
self._update_text()
self.update_position()
self._update_icon()
elif msg.what == 'profileindex':
if len(self._profilenames) == 1:
# This should be pretty hard to hit now with
# automatic local accounts.
_ba.playsound(_ba.getsound('error'))
else:
# Pick the next player profile and assign our name
# and character based on that.
_ba.playsound(self._deek_sound)
self._profileindex = ((self._profileindex + msg.value) %
len(self._profilenames))
self.update_from_player_profiles()
elif msg.what == 'character':
_ba.playsound(self._click_sound)
# update our index in our local list of characters
self.character_index = ((self.character_index + msg.value) %
len(self.character_names))
self._update_text()
self._update_icon()
elif msg.what == 'ready':
self._handle_ready_msg(bool(msg.value))
def _update_text(self) -> None:
from ba import _gameutils
from ba._lang import Lstr
assert self._text_node is not None
if self._ready:
# Once we're ready, we've saved the name, so lets ask the system
# for it so we get appended numbers and stuff.
text = Lstr(value=self._player.get_name(full=True))
text = Lstr(value='${A} (${B})',
subs=[('${A}', text),
('${B}', Lstr(resource='readyText'))])
else:
text = Lstr(value=self._get_name(full=True))
can_switch_teams = len(self.lobby.teams) > 1
# Flash as we're coming in.
fin_color = _ba.safecolor(self.get_color()) + (1, )
if not self._inited:
_gameutils.animate_array(self._text_node, 'color', 4, {
0.15: fin_color,
0.25: (2, 2, 2, 1),
0.35: fin_color
})
else:
# Blend if we're in teams mode; switch instantly otherwise.
if can_switch_teams:
_gameutils.animate_array(self._text_node, 'color', 4, {
0: self._text_node.color,
0.1: fin_color
})
else:
self._text_node.color = fin_color
self._text_node.text = text
def get_color(self) -> Sequence[float]:
"""Return the currently selected color."""
val: Sequence[float]
# if self._profilenames[self._profileindex] == '_edit':
# val = (0, 1, 0)
if self.lobby.use_team_colors:
val = self.lobby.teams[self._selected_team_index].color
else:
val = self._color
if len(val) != 3:
print('get_color: ignoring invalid color of len', len(val))
val = (0, 1, 0)
return val
def get_highlight(self) -> Sequence[float]:
"""Return the currently selected highlight."""
if self._profilenames[self._profileindex] == '_edit':
return 0, 1, 0
# If we're using team colors we wanna make sure our highlight color
# isn't too close to any other team's color.
highlight = list(self._highlight)
if self.lobby.use_team_colors:
for i, team in enumerate(self.lobby.teams):
if i != self._selected_team_index:
# Find the dominant component of this team's color
# and adjust ours so that the component is
# not super-dominant.
max_val = 0.0
max_index = 0
for j in range(3):
if team.color[j] > max_val:
max_val = team.color[j]
max_index = j
that_color_for_us = highlight[max_index]
our_second_biggest = max(highlight[(max_index + 1) % 3],
highlight[(max_index + 2) % 3])
diff = (that_color_for_us - our_second_biggest)
if diff > 0:
highlight[max_index] -= diff * 0.6
highlight[(max_index + 1) % 3] += diff * 0.3
highlight[(max_index + 2) % 3] += diff * 0.2
return highlight
def getplayer(self) -> ba.Player:
"""Return the player associated with this chooser."""
return self._player
def _update_icon(self) -> None:
from ba import _gameutils
if self._profilenames[self._profileindex] == '_edit':
tex = _ba.gettexture('black')
tint_tex = _ba.gettexture('black')
self.icon.color = (1, 1, 1)
self.icon.texture = tex
self.icon.tint_texture = tint_tex
self.icon.tint_color = (0, 1, 0)
return
try:
tex_name = (_ba.app.spaz_appearances[self.character_names[
self.character_index]].icon_texture)
tint_tex_name = (_ba.app.spaz_appearances[self.character_names[
self.character_index]].icon_mask_texture)
except Exception:
from ba import _error
_error.print_exception('Error updating char icon list')
tex_name = 'neoSpazIcon'
tint_tex_name = 'neoSpazIconColorMask'
tex = _ba.gettexture(tex_name)
tint_tex = _ba.gettexture(tint_tex_name)
self.icon.color = (1, 1, 1)
self.icon.texture = tex
self.icon.tint_texture = tint_tex
clr = self.get_color()
clr2 = self.get_highlight()
can_switch_teams = len(self.lobby.teams) > 1
# If we're initing, flash.
if not self._inited:
_gameutils.animate_array(self.icon, 'color', 3, {
0.15: (1, 1, 1),
0.25: (2, 2, 2),
0.35: (1, 1, 1)
})
# Blend in teams mode; switch instantly in ffa-mode.
if can_switch_teams:
_gameutils.animate_array(self.icon, 'tint_color', 3, {
0: self.icon.tint_color,
0.1: clr
})
else:
self.icon.tint_color = clr
self.icon.tint2_color = clr2
# Store the icon info the the player.
self._player.set_icon_info(tex_name, tint_tex_name, clr, clr2)
class Lobby:
"""Container for choosers."""
def __del__(self) -> None:
# Reset any players that still have a chooser in us
# (should allow the choosers to die).
players = [
chooser.player for chooser in self.choosers if chooser.player
]
for player in players:
player.reset()
def __init__(self) -> None:
from ba import _team as bs_team
from ba import _coopsession
session = _ba.getsession()
teams = session.teams if session.use_teams else None
self._use_team_colors = session.use_team_colors
if teams is not None:
self._teams = [weakref.ref(team) for team in teams]
else:
self._dummy_teams = bs_team.Team()
self._teams = [weakref.ref(self._dummy_teams)]
v_offset = (-150
if isinstance(session, _coopsession.CoopSession) else -50)
self.choosers: List[Chooser] = []
self.base_v_offset = v_offset
self.update_positions()
self._next_add_team = 0
self.character_names_local_unlocked: List[str] = []
self._vpos = 0
# Grab available profiles.
self.reload_profiles()
self._join_info_text = None
@property
def next_add_team(self) -> int:
"""(internal)"""
return self._next_add_team
@property
def use_team_colors(self) -> bool:
"""A bool for whether this lobby is using team colors.
If False, inidividual player colors are used instead.
"""
return self._use_team_colors
@property
def teams(self) -> List[ba.Team]:
"""Teams available in this lobby."""
allteams = []
for tref in self._teams:
team = tref()
assert team is not None
allteams.append(team)
return allteams
def get_choosers(self) -> List[Chooser]:
"""Return the lobby's current choosers."""
return self.choosers
def create_join_info(self) -> JoinInfo:
"""Create a display of on-screen information for joiners.
(how to switch teams, players, etc.)
Intended for use in initial joining-screens.
"""
return JoinInfo(self)
def reload_profiles(self) -> None:
"""Reload available player profiles."""
# pylint: disable=cyclic-import
from ba._account import ensure_have_account_player_profile
from bastd.actor.spazappearance import get_appearances
# We may have gained or lost character names if the user
# bought something; reload these too.
self.character_names_local_unlocked = get_appearances()
self.character_names_local_unlocked.sort(key=lambda x: x.lower())
# Do any overall prep we need to such as creating account profile.
ensure_have_account_player_profile()
for chooser in self.choosers:
try:
chooser.reload_profiles()
chooser.update_from_player_profiles()
except Exception:
from ba import _error
_error.print_exception('error reloading profiles')
def update_positions(self) -> None:
"""Update positions for all choosers."""
self._vpos = -100 + self.base_v_offset
for chooser in self.choosers:
chooser.set_vpos(self._vpos)
chooser.update_position()
self._vpos -= 48
def check_all_ready(self) -> bool:
"""Return whether all choosers are marked ready."""
return all(chooser.ready for chooser in self.choosers)
def add_chooser(self, player: ba.Player) -> None:
"""Add a chooser to the lobby for the provided player."""
self.choosers.append(
Chooser(vpos=self._vpos, player=player, lobby=self))
self._next_add_team = (self._next_add_team + 1) % len(self._teams)
self._vpos -= 48
def remove_chooser(self, player: ba.Player) -> None:
"""Remove a single player's chooser; does not kick him.
This is used when a player enters the game and no longer
needs a chooser."""
found = False
chooser = None
for chooser in self.choosers:
if chooser.getplayer() is player:
found = True
# Mark it as dead since there could be more
# change-commands/etc coming in still for it;
# want to avoid duplicate player-adds/etc.
chooser.set_dead(True)
self.choosers.remove(chooser)
break
if not found:
from ba import _error
_error.print_error(f'remove_chooser did not find player {player}')
elif chooser in self.choosers:
from ba import _error
_error.print_error(f'chooser remains after removal for {player}')
self.update_positions()
def remove_all_choosers(self) -> None:
"""Remove all choosers without kicking players.
This is called after all players check in and enter a game.
"""
self.choosers = []
self.update_positions()
def remove_all_choosers_and_kick_players(self) -> None:
"""Remove all player choosers and kick attached players."""
# Copy the list; it can change under us otherwise.
for chooser in list(self.choosers):
if chooser.player:
chooser.player.remove_from_game()
self.remove_all_choosers()