Initial commit

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

View file

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

View file

@ -0,0 +1,446 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to browsing player profiles."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any
class ProfileBrowserWindow(ba.Window):
"""Window for browsing player profiles."""
def __init__(
self,
transition: str = 'in_right',
in_main_menu: bool = True,
selected_profile: str | None = None,
origin_widget: ba.Widget | None = None,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
self._in_main_menu = in_main_menu
if self._in_main_menu:
back_label = ba.Lstr(resource='backText')
else:
back_label = ba.Lstr(resource='doneText')
uiscale = ba.app.ui.uiscale
self._width = 700.0 if uiscale is ba.UIScale.SMALL else 600.0
x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
self._height = (
360.0
if uiscale is ba.UIScale.SMALL
else 385.0
if uiscale is ba.UIScale.MEDIUM
else 410.0
)
# If we're being called up standalone, handle pause/resume ourself.
if not self._in_main_menu:
ba.app.pause()
# If they provided an origin-widget, scale up from that.
scale_origin: tuple[float, float] | None
if origin_widget is not None:
self._transition_out = 'out_scale'
scale_origin = origin_widget.get_screen_space_center()
transition = 'in_scale'
else:
self._transition_out = 'out_right'
scale_origin = None
self._r = 'playerProfilesWindow'
# Ensure we've got an account-profile in cases where we're signed in.
ba.app.accounts_v1.ensure_have_account_player_profile()
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
transition=transition,
scale_origin_stack_offset=scale_origin,
scale=(
2.2
if uiscale is ba.UIScale.SMALL
else 1.6
if uiscale is ba.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -14)
if uiscale is ba.UIScale.SMALL
else (0, 0),
)
)
self._back_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(40 + x_inset, self._height - 59),
size=(120, 60),
scale=0.8,
label=back_label,
button_type='back' if self._in_main_menu else None,
autoselect=True,
on_activate_call=self._back,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 36),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.titleText'),
maxwidth=300,
color=ba.app.ui.title_color,
scale=0.9,
h_align='center',
v_align='center',
)
if self._in_main_menu:
ba.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=ba.charstr(ba.SpecialChar.BACK),
)
scroll_height = self._height - 140.0
self._scroll_width = self._width - (188 + x_inset * 2)
v = self._height - 84.0
h = 50 + x_inset
b_color = (0.6, 0.53, 0.63)
scl = (
1.055
if uiscale is ba.UIScale.SMALL
else 1.18
if uiscale is ba.UIScale.MEDIUM
else 1.3
)
v -= 70.0 * scl
self._new_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._new_profile,
color=b_color,
button_type='square',
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.newButtonText'),
)
v -= 70.0 * scl
self._edit_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._edit_profile,
color=b_color,
button_type='square',
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.editButtonText'),
)
v -= 70.0 * scl
self._delete_button = ba.buttonwidget(
parent=self._root_widget,
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._delete_profile,
color=b_color,
button_type='square',
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
text_scale=0.7,
label=ba.Lstr(resource=self._r + '.deleteButtonText'),
)
v = self._height - 87
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 71),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.explanationText'),
color=ba.app.ui.infotextcolor,
maxwidth=self._width * 0.83,
scale=0.6,
h_align='center',
v_align='center',
)
self._scrollwidget = ba.scrollwidget(
parent=self._root_widget,
highlight=False,
position=(140 + x_inset, v - scroll_height),
size=(self._scroll_width, scroll_height),
)
ba.widget(
edit=self._scrollwidget,
autoselect=True,
left_widget=self._new_button,
)
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
self._columnwidget = ba.columnwidget(
parent=self._scrollwidget, border=2, margin=0
)
v -= 255
self._profiles: dict[str, dict[str, Any]] | None = None
self._selected_profile = selected_profile
self._profile_widgets: list[ba.Widget] = []
self._refresh()
self._restore_state()
def _new_profile(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.profile.edit import EditProfileWindow
from bastd.ui.purchase import PurchaseWindow
# Limit to a handful profiles if they don't have pro-options.
max_non_pro_profiles = ba.internal.get_v1_account_misc_read_val(
'mnpp', 5
)
assert self._profiles is not None
if (
not ba.app.accounts_v1.have_pro_options()
and len(self._profiles) >= max_non_pro_profiles
):
PurchaseWindow(
items=['pro'],
header_text=ba.Lstr(
resource='unlockThisProfilesText',
subs=[('${NUM}', str(max_non_pro_profiles))],
),
)
return
# Clamp at 100 profiles (otherwise the server will and that's less
# elegant looking).
if len(self._profiles) > 100:
ba.screenmessage(
ba.Lstr(
translate=(
'serverResponses',
'Max number of profiles reached.',
)
),
color=(1, 0, 0),
)
ba.playsound(ba.getsound('error'))
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
EditProfileWindow(
existing_profile=None, in_main_menu=self._in_main_menu
).get_root_widget()
)
def _delete_profile(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui import confirm
if self._selected_profile is None:
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0)
)
return
if self._selected_profile == '__account__':
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource=self._r + '.cantDeleteAccountProfileText'),
color=(1, 0, 0),
)
return
confirm.ConfirmWindow(
ba.Lstr(
resource=self._r + '.deleteConfirmText',
subs=[('${PROFILE}', self._selected_profile)],
),
self._do_delete_profile,
350,
)
def _do_delete_profile(self) -> None:
ba.internal.add_transaction(
{'type': 'REMOVE_PLAYER_PROFILE', 'name': self._selected_profile}
)
ba.internal.run_transactions()
ba.playsound(ba.getsound('shieldDown'))
self._refresh()
# Select profile list.
ba.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
def _edit_profile(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.profile.edit import EditProfileWindow
if self._selected_profile is None:
ba.playsound(ba.getsound('error'))
ba.screenmessage(
ba.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0)
)
return
self._save_state()
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
EditProfileWindow(
self._selected_profile, in_main_menu=self._in_main_menu
).get_root_widget()
)
def _select(self, name: str, index: int) -> None:
del index # Unused.
self._selected_profile = name
def _back(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.account.settings import AccountSettingsWindow
self._save_state()
ba.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
if self._in_main_menu:
ba.app.ui.set_main_menu_window(
AccountSettingsWindow(transition='in_left').get_root_widget()
)
# If we're being called up standalone, handle pause/resume ourself.
else:
ba.app.resume()
def _refresh(self) -> None:
# pylint: disable=too-many-locals
from efro.util import asserttype
from ba.internal import (
PlayerProfilesChangedMessage,
get_player_profile_colors,
get_player_profile_icon,
)
old_selection = self._selected_profile
# Delete old.
while self._profile_widgets:
self._profile_widgets.pop().delete()
self._profiles = ba.app.config.get('Player Profiles', {})
assert self._profiles is not None
items = list(self._profiles.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
index = 0
account_name: str | None
if ba.internal.get_v1_account_state() == 'signed_in':
account_name = ba.internal.get_v1_account_display_string()
else:
account_name = None
widget_to_select = None
for p_name, _ in items:
if p_name == '__account__' and account_name is None:
continue
color, _highlight = get_player_profile_colors(p_name)
scl = 1.1
tval = (
account_name
if p_name == '__account__'
else get_player_profile_icon(p_name) + p_name
)
assert isinstance(tval, str)
txtw = ba.textwidget(
parent=self._columnwidget,
position=(0, 32),
size=((self._width - 40) / scl, 28),
text=ba.Lstr(value=tval),
h_align='left',
v_align='center',
on_select_call=ba.WeakCall(self._select, p_name, index),
maxwidth=self._scroll_width * 0.92,
corner_scale=scl,
color=ba.safecolor(color, 0.4),
always_highlight=True,
on_activate_call=ba.Call(self._edit_button.activate),
selectable=True,
)
if index == 0:
ba.widget(edit=txtw, up_widget=self._back_button)
ba.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40)
self._profile_widgets.append(txtw)
# Select/show this one if it was previously selected
# (but defer till after this loop since our height is
# still changing).
if p_name == old_selection:
widget_to_select = txtw
index += 1
if widget_to_select is not None:
ba.columnwidget(
edit=self._columnwidget,
selected_child=widget_to_select,
visible_child=widget_to_select,
)
# If there's a team-chooser in existence, tell it the profile-list
# has probably changed.
session = ba.internal.get_foreground_host_session()
if session is not None:
session.handlemessage(PlayerProfilesChangedMessage())
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._new_button:
sel_name = 'New'
elif sel == self._edit_button:
sel_name = 'Edit'
elif sel == self._delete_button:
sel_name = 'Delete'
elif sel == self._scrollwidget:
sel_name = 'Scroll'
else:
sel_name = 'Back'
ba.app.ui.window_states[type(self)] = sel_name
except Exception:
ba.print_exception(f'Error saving state for {self}.')
def _restore_state(self) -> None:
try:
sel_name = ba.app.ui.window_states.get(type(self))
if sel_name == 'Scroll':
sel = self._scrollwidget
elif sel_name == 'New':
sel = self._new_button
elif sel_name == 'Delete':
sel = self._delete_button
elif sel_name == 'Edit':
sel = self._edit_button
elif sel_name == 'Back':
sel = self._back_button
else:
# By default we select our scroll widget if we have profiles;
# otherwise our new widget.
if not self._profile_widgets:
sel = self._new_button
else:
sel = self._scrollwidget
ba.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
ba.print_exception(f'Error restoring state for {self}.')

View file

@ -0,0 +1,796 @@
# Released under the MIT License. See LICENSE for details.
#
"""Provides UI to edit a player profile."""
from __future__ import annotations
import random
from typing import TYPE_CHECKING, cast
import ba
import ba.internal
if TYPE_CHECKING:
from bastd.ui.colorpicker import ColorPicker
class EditProfileWindow(ba.Window):
"""Window for editing a player profile."""
# FIXME: WILL NEED TO CHANGE THIS FOR UILOCATION.
def reload_window(self) -> None:
"""Transitions out and recreates ourself."""
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
EditProfileWindow(
self.getname(), self._in_main_menu
).get_root_widget()
)
def __init__(
self,
existing_profile: str | None,
in_main_menu: bool,
transition: str = 'in_right',
):
# FIXME: Tidy this up a bit.
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
from ba.internal import get_player_profile_colors
self._in_main_menu = in_main_menu
self._existing_profile = existing_profile
self._r = 'editProfileWindow'
self._spazzes: list[str] = []
self._icon_textures: list[ba.Texture] = []
self._icon_tint_textures: list[ba.Texture] = []
# Grab profile colors or pick random ones.
self._color, self._highlight = get_player_profile_colors(
existing_profile
)
uiscale = ba.app.ui.uiscale
self._width = width = 780.0 if uiscale is ba.UIScale.SMALL else 680.0
self._x_inset = x_inset = 50.0 if uiscale is ba.UIScale.SMALL else 0.0
self._height = height = (
350.0
if uiscale is ba.UIScale.SMALL
else 400.0
if uiscale is ba.UIScale.MEDIUM
else 450.0
)
spacing = 40
self._base_scale = (
2.05
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.0
)
top_extra = 15 if uiscale is ba.UIScale.SMALL else 15
super().__init__(
root_widget=ba.containerwidget(
size=(width, height + top_extra),
transition=transition,
scale=self._base_scale,
stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0),
)
)
cancel_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(52 + x_inset, height - 60),
size=(155, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='cancelText'),
on_activate_call=self._cancel,
)
ba.containerwidget(edit=self._root_widget, cancel_button=btn)
save_button = btn = ba.buttonwidget(
parent=self._root_widget,
position=(width - (177 + x_inset), height - 60),
size=(155, 60),
autoselect=True,
scale=0.8,
label=ba.Lstr(resource='saveText'),
)
ba.widget(edit=save_button, left_widget=cancel_button)
ba.widget(edit=cancel_button, right_widget=save_button)
ba.containerwidget(edit=self._root_widget, start_button=btn)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, height - 38),
size=(0, 0),
text=(
ba.Lstr(resource=self._r + '.titleNewText')
if existing_profile is None
else ba.Lstr(resource=self._r + '.titleEditText')
),
color=ba.app.ui.title_color,
maxwidth=290,
scale=1.0,
h_align='center',
v_align='center',
)
# Make a list of spaz icons.
self.refresh_characters()
profile = ba.app.config.get('Player Profiles', {}).get(
self._existing_profile, {}
)
if 'global' in profile:
self._global = profile['global']
else:
self._global = False
if 'icon' in profile:
self._icon = profile['icon']
else:
self._icon = ba.charstr(ba.SpecialChar.LOGO)
assigned_random_char = False
# Look for existing character choice or pick random one otherwise.
try:
icon_index = self._spazzes.index(profile['character'])
except Exception:
# Let's set the default icon to spaz for our first profile; after
# that we go random.
# (SCRATCH THAT.. we now hard-code account-profiles to start with
# spaz which has a similar effect)
# try: p_len = len(ba.app.config['Player Profiles'])
# except Exception: p_len = 0
# if p_len == 0: icon_index = self._spazzes.index('Spaz')
# else:
random.seed()
icon_index = random.randrange(len(self._spazzes))
assigned_random_char = True
self._icon_index = icon_index
ba.buttonwidget(edit=save_button, on_activate_call=self.save)
v = height - 115.0
self._name = (
'' if self._existing_profile is None else self._existing_profile
)
self._is_account_profile = self._name == '__account__'
# If we just picked a random character, see if it has specific
# colors/highlights associated with it and assign them if so.
if assigned_random_char:
clr = ba.app.spaz_appearances[
self._spazzes[icon_index]
].default_color
if clr is not None:
self._color = clr
highlight = ba.app.spaz_appearances[
self._spazzes[icon_index]
].default_highlight
if highlight is not None:
self._highlight = highlight
# Assign a random name if they had none.
if self._name == '':
names = ba.internal.get_random_names()
self._name = names[random.randrange(len(names))]
self._clipped_name_text = ba.textwidget(
parent=self._root_widget,
text='',
position=(540 + x_inset, v - 8),
flatness=1.0,
shadow=0.0,
scale=0.55,
size=(0, 0),
maxwidth=100,
h_align='center',
v_align='center',
color=(1, 1, 0, 0.5),
)
if not self._is_account_profile and not self._global:
ba.textwidget(
parent=self._root_widget,
text=ba.Lstr(resource=self._r + '.nameText'),
position=(200 + x_inset, v - 6),
size=(0, 0),
h_align='right',
v_align='center',
color=(1, 1, 1, 0.5),
scale=0.9,
)
self._upgrade_button = None
if self._is_account_profile:
if ba.internal.get_v1_account_state() == 'signed_in':
sval = ba.internal.get_v1_account_display_string()
else:
sval = '??'
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, v - 7),
size=(0, 0),
scale=1.2,
text=sval,
maxwidth=270,
h_align='center',
v_align='center',
)
txtl = ba.Lstr(
resource='editProfileWindow.accountProfileText'
).evaluate()
b_width = min(
270.0,
ba.internal.get_string_width(txtl, suppress_warning=True) * 0.6,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, v - 39),
size=(0, 0),
scale=0.6,
color=ba.app.ui.infotextcolor,
text=txtl,
maxwidth=270,
h_align='center',
v_align='center',
)
self._account_type_info_button = ba.buttonwidget(
parent=self._root_widget,
label='?',
size=(15, 15),
text_scale=0.6,
position=(self._width * 0.5 + b_width * 0.5 + 13, v - 47),
button_type='square',
color=(0.6, 0.5, 0.65),
autoselect=True,
on_activate_call=self.show_account_profile_info,
)
elif self._global:
b_size = 60
self._icon_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(self._width * 0.5 - 160 - b_size * 0.5, v - 38 - 15),
size=(b_size, b_size),
color=(0.6, 0.5, 0.6),
label='',
button_type='square',
text_scale=1.2,
on_activate_call=self._on_icon_press,
)
self._icon_button_label = ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5 - 160, v - 35),
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
color=(1, 1, 1),
text='',
scale=2.0,
)
ba.textwidget(
parent=self._root_widget,
h_align='center',
v_align='center',
position=(self._width * 0.5 - 160, v - 55 - 15),
size=(0, 0),
draw_controller=btn,
text=ba.Lstr(resource=self._r + '.iconText'),
scale=0.7,
color=ba.app.ui.title_color,
maxwidth=120,
)
self._update_icon()
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, v - 7),
size=(0, 0),
scale=1.2,
text=self._name,
maxwidth=240,
h_align='center',
v_align='center',
)
# FIXME hard coded strings are bad
txtl = ba.Lstr(
resource='editProfileWindow.globalProfileText'
).evaluate()
b_width = min(
240.0,
ba.internal.get_string_width(txtl, suppress_warning=True) * 0.6,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, v - 39),
size=(0, 0),
scale=0.6,
color=ba.app.ui.infotextcolor,
text=txtl,
maxwidth=240,
h_align='center',
v_align='center',
)
self._account_type_info_button = ba.buttonwidget(
parent=self._root_widget,
label='?',
size=(15, 15),
text_scale=0.6,
position=(self._width * 0.5 + b_width * 0.5 + 13, v - 47),
button_type='square',
color=(0.6, 0.5, 0.65),
autoselect=True,
on_activate_call=self.show_global_profile_info,
)
else:
self._text_field = ba.textwidget(
parent=self._root_widget,
position=(220 + x_inset, v - 30),
size=(265, 40),
text=self._name,
h_align='left',
v_align='center',
max_chars=16,
description=ba.Lstr(resource=self._r + '.nameDescriptionText'),
autoselect=True,
editable=True,
padding=4,
color=(0.9, 0.9, 0.9, 1.0),
on_return_press_call=ba.Call(save_button.activate),
)
# FIXME hard coded strings are bad
txtl = ba.Lstr(
resource='editProfileWindow.localProfileText'
).evaluate()
b_width = min(
270.0,
ba.internal.get_string_width(txtl, suppress_warning=True) * 0.6,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, v - 43),
size=(0, 0),
scale=0.6,
color=ba.app.ui.infotextcolor,
text=txtl,
maxwidth=270,
h_align='center',
v_align='center',
)
self._account_type_info_button = ba.buttonwidget(
parent=self._root_widget,
label='?',
size=(15, 15),
text_scale=0.6,
position=(self._width * 0.5 + b_width * 0.5 + 13, v - 50),
button_type='square',
color=(0.6, 0.5, 0.65),
autoselect=True,
on_activate_call=self.show_local_profile_info,
)
self._upgrade_button = ba.buttonwidget(
parent=self._root_widget,
label=ba.Lstr(resource='upgradeText'),
size=(40, 17),
text_scale=1.0,
button_type='square',
position=(self._width * 0.5 + b_width * 0.5 + 13 + 43, v - 51),
color=(0.6, 0.5, 0.65),
autoselect=True,
on_activate_call=self.upgrade_profile,
)
self._update_clipped_name()
self._clipped_name_timer = ba.Timer(
0.333,
ba.WeakCall(self._update_clipped_name),
timetype=ba.TimeType.REAL,
repeat=True,
)
v -= spacing * 3.0
b_size = 80
b_size_2 = 100
b_offs = 150
self._color_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(self._width * 0.5 - b_offs - b_size * 0.5, v - 50),
size=(b_size, b_size),
color=self._color,
label='',
button_type='square',
)
origin = self._color_button.get_screen_space_center()
ba.buttonwidget(
edit=self._color_button,
on_activate_call=ba.WeakCall(self._make_picker, 'color', origin),
)
ba.textwidget(
parent=self._root_widget,
h_align='center',
v_align='center',
position=(self._width * 0.5 - b_offs, v - 65),
size=(0, 0),
draw_controller=btn,
text=ba.Lstr(resource=self._r + '.colorText'),
scale=0.7,
color=ba.app.ui.title_color,
maxwidth=120,
)
self._character_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(self._width * 0.5 - b_size_2 * 0.5, v - 60),
up_widget=self._account_type_info_button,
on_activate_call=self._on_character_press,
size=(b_size_2, b_size_2),
label='',
color=(1, 1, 1),
mask_texture=ba.gettexture('characterIconMask'),
)
if not self._is_account_profile and not self._global:
ba.containerwidget(
edit=self._root_widget, selected_child=self._text_field
)
ba.textwidget(
parent=self._root_widget,
h_align='center',
v_align='center',
position=(self._width * 0.5, v - 80),
size=(0, 0),
draw_controller=btn,
text=ba.Lstr(resource=self._r + '.characterText'),
scale=0.7,
color=ba.app.ui.title_color,
maxwidth=130,
)
self._highlight_button = btn = ba.buttonwidget(
parent=self._root_widget,
autoselect=True,
position=(self._width * 0.5 + b_offs - b_size * 0.5, v - 50),
up_widget=self._upgrade_button
if self._upgrade_button is not None
else self._account_type_info_button,
size=(b_size, b_size),
color=self._highlight,
label='',
button_type='square',
)
if not self._is_account_profile and not self._global:
ba.widget(edit=cancel_button, down_widget=self._text_field)
ba.widget(edit=save_button, down_widget=self._text_field)
ba.widget(edit=self._color_button, up_widget=self._text_field)
ba.widget(
edit=self._account_type_info_button,
down_widget=self._character_button,
)
origin = self._highlight_button.get_screen_space_center()
ba.buttonwidget(
edit=self._highlight_button,
on_activate_call=ba.WeakCall(
self._make_picker, 'highlight', origin
),
)
ba.textwidget(
parent=self._root_widget,
h_align='center',
v_align='center',
position=(self._width * 0.5 + b_offs, v - 65),
size=(0, 0),
draw_controller=btn,
text=ba.Lstr(resource=self._r + '.highlightText'),
scale=0.7,
color=ba.app.ui.title_color,
maxwidth=120,
)
self._update_character()
def upgrade_profile(self) -> None:
"""Attempt to ugrade the profile to global."""
from bastd.ui import account
from bastd.ui.profile import upgrade as pupgrade
if ba.internal.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
pupgrade.ProfileUpgradeWindow(self)
def show_account_profile_info(self) -> None:
"""Show an explanation of account profiles."""
from bastd.ui.confirm import ConfirmWindow
icons_str = ' '.join(
[
ba.charstr(n)
for n in [
ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO,
ba.SpecialChar.GAME_CENTER_LOGO,
ba.SpecialChar.GAME_CIRCLE_LOGO,
ba.SpecialChar.OUYA_LOGO,
ba.SpecialChar.LOCAL_ACCOUNT,
ba.SpecialChar.ALIBABA_LOGO,
ba.SpecialChar.OCULUS_LOGO,
ba.SpecialChar.NVIDIA_LOGO,
]
]
)
txtl = ba.Lstr(
resource='editProfileWindow.accountProfileInfoText',
subs=[('${ICONS}', icons_str)],
)
ConfirmWindow(
txtl,
cancel_button=False,
width=500,
height=300,
origin_widget=self._account_type_info_button,
)
def show_local_profile_info(self) -> None:
"""Show an explanation of local profiles."""
from bastd.ui.confirm import ConfirmWindow
txtl = ba.Lstr(resource='editProfileWindow.localProfileInfoText')
ConfirmWindow(
txtl,
cancel_button=False,
width=600,
height=250,
origin_widget=self._account_type_info_button,
)
def show_global_profile_info(self) -> None:
"""Show an explanation of global profiles."""
from bastd.ui.confirm import ConfirmWindow
txtl = ba.Lstr(resource='editProfileWindow.globalProfileInfoText')
ConfirmWindow(
txtl,
cancel_button=False,
width=600,
height=250,
origin_widget=self._account_type_info_button,
)
def refresh_characters(self) -> None:
"""Refresh available characters/icons."""
from bastd.actor import spazappearance
self._spazzes = spazappearance.get_appearances()
self._spazzes.sort()
self._icon_textures = [
ba.gettexture(ba.app.spaz_appearances[s].icon_texture)
for s in self._spazzes
]
self._icon_tint_textures = [
ba.gettexture(ba.app.spaz_appearances[s].icon_mask_texture)
for s in self._spazzes
]
def on_icon_picker_pick(self, icon: str) -> None:
"""An icon has been selected by the picker."""
self._icon = icon
self._update_icon()
def on_character_picker_pick(self, character: str) -> None:
"""A character has been selected by the picker."""
if not self._root_widget:
return
# The player could have bought a new one while the picker was up.
self.refresh_characters()
self._icon_index = (
self._spazzes.index(character) if character in self._spazzes else 0
)
self._update_character()
def _on_character_press(self) -> None:
from bastd.ui import characterpicker
characterpicker.CharacterPicker(
parent=self._root_widget,
position=self._character_button.get_screen_space_center(),
selected_character=self._spazzes[self._icon_index],
delegate=self,
tint_color=self._color,
tint2_color=self._highlight,
)
def _on_icon_press(self) -> None:
from bastd.ui import iconpicker
iconpicker.IconPicker(
parent=self._root_widget,
position=self._icon_button.get_screen_space_center(),
selected_icon=self._icon,
delegate=self,
tint_color=self._color,
tint2_color=self._highlight,
)
def _make_picker(
self, picker_type: str, origin: tuple[float, float]
) -> None:
from bastd.ui import colorpicker
if picker_type == 'color':
initial_color = self._color
elif picker_type == 'highlight':
initial_color = self._highlight
else:
raise ValueError('invalid picker_type: ' + picker_type)
colorpicker.ColorPicker(
parent=self._root_widget,
position=origin,
offset=(
self._base_scale * (-100 if picker_type == 'color' else 100),
0,
),
initial_color=initial_color,
delegate=self,
tag=picker_type,
)
def _cancel(self) -> None:
from bastd.ui.profile.browser import ProfileBrowserWindow
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
ProfileBrowserWindow(
'in_left',
selected_profile=self._existing_profile,
in_main_menu=self._in_main_menu,
).get_root_widget()
)
def _set_color(self, color: tuple[float, float, float]) -> None:
self._color = color
if self._color_button:
ba.buttonwidget(edit=self._color_button, color=color)
def _set_highlight(self, color: tuple[float, float, float]) -> None:
self._highlight = color
if self._highlight_button:
ba.buttonwidget(edit=self._highlight_button, color=color)
def color_picker_closing(self, picker: ColorPicker) -> None:
"""Called when a color picker is closing."""
if not self._root_widget:
return
tag = picker.get_tag()
if tag == 'color':
ba.containerwidget(
edit=self._root_widget, selected_child=self._color_button
)
elif tag == 'highlight':
ba.containerwidget(
edit=self._root_widget, selected_child=self._highlight_button
)
else:
print('color_picker_closing got unknown tag ' + str(tag))
def color_picker_selected_color(
self, picker: ColorPicker, color: tuple[float, float, float]
) -> None:
"""Called when a color is selected in a color picker."""
if not self._root_widget:
return
tag = picker.get_tag()
if tag == 'color':
self._set_color(color)
elif tag == 'highlight':
self._set_highlight(color)
else:
print('color_picker_selected_color got unknown tag ' + str(tag))
self._update_character()
def _update_clipped_name(self) -> None:
if not self._clipped_name_text:
return
name = self.getname()
if name == '__account__':
name = (
ba.internal.get_v1_account_name()
if ba.internal.get_v1_account_state() == 'signed_in'
else '???'
)
if len(name) > 10 and not (self._global or self._is_account_profile):
ba.textwidget(
edit=self._clipped_name_text,
text=ba.Lstr(
resource='inGameClippedNameText',
subs=[('${NAME}', name[:10] + '...')],
),
)
else:
ba.textwidget(edit=self._clipped_name_text, text='')
def _update_character(self, change: int = 0) -> None:
self._icon_index = (self._icon_index + change) % len(self._spazzes)
if self._character_button:
ba.buttonwidget(
edit=self._character_button,
texture=self._icon_textures[self._icon_index],
tint_texture=self._icon_tint_textures[self._icon_index],
tint_color=self._color,
tint2_color=self._highlight,
)
def _update_icon(self) -> None:
if self._icon_button_label:
ba.textwidget(edit=self._icon_button_label, text=self._icon)
def getname(self) -> str:
"""Return the current profile name value."""
if self._is_account_profile:
new_name = '__account__'
elif self._global:
new_name = self._name
else:
new_name = cast(str, ba.textwidget(query=self._text_field))
return new_name
def save(self, transition_out: bool = True) -> bool:
"""Save has been selected."""
from bastd.ui.profile.browser import ProfileBrowserWindow
new_name = self.getname().strip()
if not new_name:
ba.screenmessage(ba.Lstr(resource='nameNotEmptyText'))
ba.playsound(ba.getsound('error'))
return False
if transition_out:
ba.playsound(ba.getsound('gunCocking'))
# Delete old in case we're renaming.
if self._existing_profile and self._existing_profile != new_name:
ba.internal.add_transaction(
{
'type': 'REMOVE_PLAYER_PROFILE',
'name': self._existing_profile,
}
)
# Also lets be aware we're no longer global if we're taking a
# new name (will need to re-request it).
self._global = False
ba.internal.add_transaction(
{
'type': 'ADD_PLAYER_PROFILE',
'name': new_name,
'profile': {
'character': self._spazzes[self._icon_index],
'color': list(self._color),
'global': self._global,
'icon': self._icon,
'highlight': list(self._highlight),
},
}
)
if transition_out:
ba.internal.run_transactions()
ba.containerwidget(edit=self._root_widget, transition='out_right')
ba.app.ui.set_main_menu_window(
ProfileBrowserWindow(
'in_left',
selected_profile=new_name,
in_main_menu=self._in_main_menu,
).get_root_widget()
)
return True

View file

@ -0,0 +1,288 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI for player profile upgrades."""
from __future__ import annotations
import time
import weakref
from typing import TYPE_CHECKING
import ba
import ba.internal
if TYPE_CHECKING:
from typing import Any
from bastd.ui.profile.edit import EditProfileWindow
class ProfileUpgradeWindow(ba.Window):
"""Window for player profile upgrades to global."""
def __init__(
self,
edit_profile_window: EditProfileWindow,
transition: str = 'in_right',
):
from ba.internal import master_server_get
self._r = 'editProfileWindow'
self._width = 680
self._height = 350
uiscale = ba.app.ui.uiscale
self._base_scale = (
2.05
if uiscale is ba.UIScale.SMALL
else 1.5
if uiscale is ba.UIScale.MEDIUM
else 1.2
)
self._upgrade_start_time: float | None = None
self._name = edit_profile_window.getname()
self._edit_profile_window = weakref.ref(edit_profile_window)
top_extra = 15 if uiscale is ba.UIScale.SMALL else 15
super().__init__(
root_widget=ba.containerwidget(
size=(self._width, self._height + top_extra),
toolbar_visibility='menu_currency',
transition=transition,
scale=self._base_scale,
stack_offset=(0, 15) if uiscale is ba.UIScale.SMALL else (0, 0),
)
)
cancel_button = ba.buttonwidget(
parent=self._root_widget,
position=(52, 30),
size=(155, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='cancelText'),
on_activate_call=self._cancel,
)
self._upgrade_button = ba.buttonwidget(
parent=self._root_widget,
position=(self._width - 190, 30),
size=(155, 60),
scale=0.8,
autoselect=True,
label=ba.Lstr(resource='upgradeText'),
on_activate_call=self._on_upgrade_press,
)
ba.containerwidget(
edit=self._root_widget,
cancel_button=cancel_button,
start_button=self._upgrade_button,
selected_child=self._upgrade_button,
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 38),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.upgradeToGlobalProfileText'),
color=ba.app.ui.title_color,
maxwidth=self._width * 0.45,
scale=1.0,
h_align='center',
v_align='center',
)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 100),
size=(0, 0),
text=ba.Lstr(resource=self._r + '.upgradeProfileInfoText'),
color=ba.app.ui.infotextcolor,
maxwidth=self._width * 0.8,
scale=0.7,
h_align='center',
v_align='center',
)
self._status_text = ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 160),
size=(0, 0),
text=ba.Lstr(
resource=self._r + '.checkingAvailabilityText',
subs=[('${NAME}', self._name)],
),
color=(0.8, 0.4, 0.0),
maxwidth=self._width * 0.8,
scale=0.65,
h_align='center',
v_align='center',
)
self._price_text = ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 230),
size=(0, 0),
text='',
color=(0.2, 1, 0.2),
maxwidth=self._width * 0.8,
scale=1.5,
h_align='center',
v_align='center',
)
self._tickets_text: ba.Widget | None
if not ba.app.ui.use_toolbars:
self._tickets_text = ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.9 - 5, self._height - 30),
size=(0, 0),
text=ba.charstr(ba.SpecialChar.TICKET) + '123',
color=(0.2, 1, 0.2),
maxwidth=100,
scale=0.5,
h_align='right',
v_align='center',
)
else:
self._tickets_text = None
master_server_get(
'bsGlobalProfileCheck',
{'name': self._name, 'b': ba.app.build_number},
callback=ba.WeakCall(self._profile_check_result),
)
self._cost = ba.internal.get_v1_account_misc_read_val(
'price.global_profile', 500
)
self._status: str | None = 'waiting'
self._update_timer = ba.Timer(
1.0,
ba.WeakCall(self._update),
timetype=ba.TimeType.REAL,
repeat=True,
)
self._update()
def _profile_check_result(self, result: dict[str, Any] | None) -> None:
if result is None:
ba.textwidget(
edit=self._status_text,
text=ba.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0),
)
self._status = 'error'
ba.buttonwidget(
edit=self._upgrade_button,
color=(0.4, 0.4, 0.4),
textcolor=(0.5, 0.5, 0.5),
)
else:
if result['available']:
ba.textwidget(
edit=self._status_text,
text=ba.Lstr(
resource=self._r + '.availableText',
subs=[('${NAME}', self._name)],
),
color=(0, 1, 0),
)
ba.textwidget(
edit=self._price_text,
text=ba.charstr(ba.SpecialChar.TICKET) + str(self._cost),
)
self._status = None
else:
ba.textwidget(
edit=self._status_text,
text=ba.Lstr(
resource=self._r + '.unavailableText',
subs=[('${NAME}', self._name)],
),
color=(1, 0, 0),
)
self._status = 'unavailable'
ba.buttonwidget(
edit=self._upgrade_button,
color=(0.4, 0.4, 0.4),
textcolor=(0.5, 0.5, 0.5),
)
def _on_upgrade_press(self) -> None:
from bastd.ui import getcurrency
if self._status is None:
# If it appears we don't have enough tickets, offer to buy more.
tickets = ba.internal.get_v1_account_ticket_count()
if tickets < self._cost:
ba.playsound(ba.getsound('error'))
getcurrency.show_get_tickets_prompt()
return
ba.screenmessage(
ba.Lstr(resource='purchasingText'), color=(0, 1, 0)
)
self._status = 'pre_upgrading'
# Now we tell the original editor to save the profile, add an
# upgrade transaction, and then sit and wait for everything to
# go through.
edit_profile_window = self._edit_profile_window()
if edit_profile_window is None:
print('profile upgrade: original edit window gone')
return
success = edit_profile_window.save(transition_out=False)
if not success:
print('profile upgrade: error occurred saving profile')
ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
return
ba.internal.add_transaction(
{'type': 'UPGRADE_PROFILE', 'name': self._name}
)
ba.internal.run_transactions()
self._status = 'upgrading'
self._upgrade_start_time = time.time()
else:
ba.playsound(ba.getsound('error'))
def _update(self) -> None:
try:
t_str = str(ba.internal.get_v1_account_ticket_count())
except Exception:
t_str = '?'
if self._tickets_text is not None:
ba.textwidget(
edit=self._tickets_text,
text=ba.Lstr(
resource='getTicketsWindow.youHaveShortText',
subs=[
('${COUNT}', ba.charstr(ba.SpecialChar.TICKET) + t_str)
],
),
)
# Once we've kicked off an upgrade attempt and all transactions go
# through, we're done.
if (
self._status == 'upgrading'
and not ba.internal.have_outstanding_transactions()
):
self._status = 'exiting'
ba.containerwidget(edit=self._root_widget, transition='out_right')
edit_profile_window = self._edit_profile_window()
if edit_profile_window is None:
print(
'profile upgrade transition out:'
' original edit window gone'
)
return
ba.playsound(ba.getsound('gunCocking'))
edit_profile_window.reload_window()
def _cancel(self) -> None:
# If we recently sent out an upgrade request, disallow canceling
# for a bit.
if (
self._upgrade_start_time is not None
and time.time() - self._upgrade_start_time < 10.0
):
ba.playsound(ba.getsound('error'))
return
ba.containerwidget(edit=self._root_widget, transition='out_right')