# Released under the MIT License. See LICENSE for details.
#
"""UI related to league rank."""
from __future__ import annotations # Docs-generation hack.
# pylint: disable=too-many-lines
import copy
import logging
from typing import TYPE_CHECKING, override
import bacommon.classic
import bauiv1 as bui
from bauiv1 import _commonassets, _classicassets
from bauiv1 import _builtinassets
from bauiv1 import _uiv1assets
from bauiv1lib.utils import (
TOOLBAR_SCROLL_FADE,
apply_toolbar_scroll_fades,
get_screen_margins,
)
from bauiv1lib.league import league_display_name
from bauiv1lib.popup import PopupMenu
if TYPE_CHECKING:
from typing import Any
[docs]
class LeagueRankWindow(bui.MainWindow):
"""Window for showing league rank."""
def __init__(
self,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
auxiliary_style: bool = True,
):
# pylint: disable=too-many-statements
plus = bui.app.plus
assert plus is not None
self._uiopenstate = bui.UIOpenState('classicleaguerank')
bui.set_analytics_screen('League Rank Window')
self._league_rank_data: dict[str, Any] | None = None
self._power_ranking_achievements_button: bui.Widget | None = None
self._up_to_date_bonus_button: bui.Widget | None = None
self._power_ranking_trophies_button: bui.Widget | None = None
self._league_title_text: bui.Widget | None = None
self._league_text: bui.Widget | None = None
self._league_number_text: bui.Widget | None = None
self._your_power_ranking_text: bui.Widget | None = None
self._loading_spinner: bui.Widget | None = None
self._season_ends_text: bui.Widget | None = None
self._power_ranking_rank_text: bui.Widget | None = None
self._to_ranked_text: bui.Widget | None = None
self._trophy_counts_reset_text: bui.Widget | None = None
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
# Note: small-ui width is sized so our backing covers the
# widest-case screen (21:9 aspect plus max screen margins) and
# our scroll clamp doesn't stop short of the screen edges.
self._width = 1630 if uiscale is bui.UIScale.SMALL else 1120
x_inset = 100 if uiscale is bui.UIScale.SMALL else 0
self._height = 1000 if uiscale is bui.UIScale.SMALL else 710
# Large ui-scale is medium's layout drawn smaller, by the same
# ratio doc-ui windows use between the two (root scale 0.9 vs
# 0.65); nothing here benefits from a bigger window. Popups we
# open shrink by it too, keeping their size relative to us.
self._large_shrink = 0.65 / 0.9 if uiscale is bui.UIScale.LARGE else 1.0
self._r = 'coopSelectWindow'
self._xoffs = 40
self._league_url_arg = ''
self._is_current_season = False
self._can_do_more_button = True
# Do some fancy math to fill all available screen area up to the
# size of our backing container. This lets us fit to the exact
# screen shape at small ui scale.
screensize = bui.get_virtual_screen_size()
scale = (
1.13 if uiscale is bui.UIScale.SMALL else 0.93 * self._large_shrink
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_width = min(self._width - 130, screensize[0] / scale)
target_height = min(self._height - 130, screensize[1] / scale)
# To get top/left coords, go to the center of our window and
# offset by half the width/height of our target area.
yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 50
scroll_bottom = yoffs - 80 - self._scroll_height
# Go with full-screen scrollable area in small ui.
if uiscale is bui.UIScale.SMALL:
self._scroll_height += 53
scroll_bottom -= 1
# In small ui we extend our scrollable area out into the side
# screen margins (space between the virtual bounds and the
# actual screen edges) while keeping content laid out within the
# virtual bounds. Our top and bottom stay at the virtual
# bounds', where our toolbar fades are calibrated.
self._margin_left, self._margin_right, _, _ = (
get_screen_margins(scale)
if uiscale is bui.UIScale.SMALL
else (0.0, 0.0, 0.0, 0.0)
)
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
scale=scale,
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
),
transition=transition,
origin_widget=origin_widget,
# We're affected by screen size only at small ui-scale.
refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
)
if uiscale is bui.UIScale.SMALL:
self._back_button = bui.get_special_widget('back_button')
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
else:
# Sized to match doc-ui windows' back/close button on screen
# (scale 0.88 under their 0.9 medium root scale; ours is
# 0.93), keeping the center it has always had.
back_size = (50.0, 50.0) if auxiliary_style else (60.0, 55.0)
back_scale = 0.88 * 0.9 / 0.93
back_center = (75.0 + x_inset + 36.0, yoffs - 27.0)
self._back_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(
back_center[0] - 0.5 * back_size[0] * back_scale,
back_center[1] - 0.5 * back_size[1] * back_scale,
),
size=back_size,
scale=back_scale,
autoselect=True,
label=(
bui.SpecialChar.CLOSE
if auxiliary_style
else bui.SpecialChar.BACK
).value,
button_type=None if auxiliary_style else 'backSmall',
on_activate_call=self.main_window_back,
)
bui.containerwidget(
edit=self._root_widget,
cancel_button=self._back_button,
selected_child=self._back_button,
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
size=(
self._scroll_width + self._margin_left + self._margin_right,
self._scroll_height,
),
position=(
self._width * 0.5
- self._scroll_width * 0.5
- self._margin_left,
scroll_bottom,
),
center_small_content=True,
center_small_content_horizontally=True,
# Outside small ui our content always fits; no outline.
border_opacity=0.4 if uiscale is bui.UIScale.SMALL else 0.0,
fading_scrollbar=True,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
# With full-screen scrolling, fade content as it approaches
# toolbars (only half as far along the top, as our old blotches
# did there).
if uiscale is bui.UIScale.SMALL:
apply_toolbar_scroll_fades(
self._scrollwidget, top_fade=0.5 * TOOLBAR_SCROLL_FADE
)
self._title_text = bui.textwidget(
parent=self._root_widget,
# (Outside small ui, centered on the back button and sized
# like doc-ui windows' titles on screen: 1.0 under their 0.9
# medium root scale; ours is 0.93.)
position=(
self._width * 0.5,
yoffs - (55 if uiscale is bui.UIScale.SMALL else 27),
),
size=(0, 0),
text=_classicassets.strings.league.league_rank,
h_align='center',
color=bui.app.ui_v1.title_color,
scale=1.2 if uiscale is bui.UIScale.SMALL else 0.9 / 0.93,
maxwidth=600,
v_align='center',
)
self._last_power_ranking_query_time: float | None = None
self._doing_power_ranking_query = False
self._subcontainer: bui.Widget | None = None
self._subcontainerwidth = 1024.0
self._subcontainerheight = 573.0
# For fullscreen scrollable, account for toolbar. Elsewhere, drop
# the empty space below our lowest content (the 'More...'
# button), which only small ui's full-screen layout wants; this
# way everything fits our scroll area without scrolling.
if uiscale is bui.UIScale.SMALL:
self._subcontainerheight += 53
else:
self._subcontainerheight -= 80
self._power_ranking_score_widgets: list[bui.Widget] = []
self._season_popup_menu: PopupMenu | None = None
self._requested_season: str | None = None
self._season: str | None = None
# Take note of our account state; we'll refresh later if this
# changes. (v1 sign-in state on purpose, here and below: league
# rank data comes from the v1 master server.)
self._account_state = plus.get_v1_account_state()
self._refresh()
# If we've got cached power-ranking data already, display it.
assert bui.app.classic is not None
info = bui.app.classic.accounts.get_cached_league_rank_data()
if info is not None:
self._update_for_league_rank_data(info)
self._update_timer = bui.AppTimer(
1.0, bui.WeakCallStrict(self._update), repeat=True
)
self._update(show=info is None)
def _on_p_button_info_response(
self,
response: (
bacommon.classic.GetClassicLeaguePresidentButtonInfoResponse
| Exception
),
) -> None:
# If our window has died, no-op.
if not self._root_widget:
return
if isinstance(response, Exception) or response.name is None:
bui.textwidget(
edit=self._president_name, color=(0.6, 0.6, 1, 0.2), text='-'
)
return
bui.textwidget(
edit=self._president_name,
color=(0.6, 0.6, 1, 0.9),
text=response.name,
literal=True,
)
[docs]
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget
)
)
[docs]
@override
def main_window_should_preserve_selection(self) -> bool:
return True
def _on_achievements_press(self) -> None:
from bauiv1lib.achievements import AchievementsWindow
# Only allow this for all-time or the current season (we
# currently don't keep specific achievement data for old
# seasons).
if self._season == 'a' or self._is_current_season:
prab = self._power_ranking_achievements_button
assert prab is not None
self.main_window_replace(
lambda: AchievementsWindow(origin_widget=prab)
)
else:
bui.screenmessage(
(
_classicassets.strings.league
).achievements_unavailable_old_seasons,
color=(1, 0, 0),
)
_builtinassets.audio.error.get().play()
def _on_activity_mult_press(self) -> None:
from bauiv1lib import confirm
plus = bui.app.plus
assert plus is not None
# (The legacy form passed a ${MAX} sub that neither string
# actually contained; dropped with the port.)
lstrs = _classicassets.strings.league
txt = (
lstrs.activeness_all_time_info
if self._season == 'a'
else lstrs.activeness_info
)
confirm.ConfirmWindow(
txt,
cancel_button=False,
width=460,
height=150,
origin_widget=self._activity_mult_button,
scale=self._popup_scale(1.5),
)
def _on_up_to_date_bonus_press(self) -> None:
from bauiv1lib import confirm
plus = bui.app.plus
assert plus is not None
txt = _classicassets.strings.league.up_to_date_bonus_description(
percent=str(
plus.get_v1_account_misc_read_val('proPowerRankingBoost', 10)
)
)
confirm.ConfirmWindow(
txt,
cancel_button=False,
width=460,
height=130,
origin_widget=self._up_to_date_bonus_button,
scale=self._popup_scale(1.5),
)
def _on_trophies_press(self) -> None:
from bauiv1lib.trophies import TrophiesWindow
info = self._league_rank_data
if info is not None:
prtb = self._power_ranking_trophies_button
assert prtb is not None
TrophiesWindow(
position=prtb.get_screen_space_center(),
data=info,
scale=self._popup_scale(1.65),
)
else:
_builtinassets.audio.error.get().play()
def _popup_scale(self, medium_scale: float) -> float | None:
"""Scale for a popup we open, given its medium ui-scale scale.
None (the popup's own default) at small ui-scale. At large it is
the medium scale shrunk as we are, so popups keep the same size
relative to us at medium and large.
"""
if bui.app.ui_v1.uiscale is bui.UIScale.SMALL:
return None
return medium_scale * self._large_shrink
def _on_power_ranking_query_response(
self, data: dict[str, Any] | None
) -> None:
self._doing_power_ranking_query = False
# Important: *only* cache this if we requested the current
# season.
if data is not None and data.get('s', None) is None:
assert bui.app.classic is not None
bui.app.classic.accounts.cache_league_rank_data(data)
# Always store a copy locally though (even for other seasons).
self._league_rank_data = copy.deepcopy(data)
self._update_for_league_rank_data(data)
def _update(self, show: bool = False) -> None:
plus = bui.app.plus
assert plus is not None
cur_time = bui.apptime()
# If our account state has changed, refresh our UI. (v1 sign-in
# state on purpose; see __init__.)
account_state = plus.get_v1_account_state()
if account_state != self._account_state:
self._account_state = account_state
self._refresh()
# And power ranking too.
if not self._doing_power_ranking_query:
self._last_power_ranking_query_time = None
# Send off a new power-ranking query if its been long enough or
# our requested season has changed or whatnot.
if not self._doing_power_ranking_query and (
self._last_power_ranking_query_time is None
or cur_time - self._last_power_ranking_query_time > 30.0
):
try:
if show:
bui.textwidget(edit=self._league_title_text, text='')
bui.textwidget(edit=self._league_text, text='')
bui.textwidget(edit=self._league_number_text, text='')
bui.textwidget(edit=self._your_power_ranking_text, text='')
bui.spinnerwidget(edit=self._loading_spinner, visible=True)
bui.textwidget(edit=self._to_ranked_text, text='')
bui.textwidget(edit=self._power_ranking_rank_text, text='')
bui.textwidget(edit=self._season_ends_text, text='')
bui.textwidget(edit=self._trophy_counts_reset_text, text='')
except Exception:
logging.exception('Error showing updated rank info.')
self._last_power_ranking_query_time = cur_time
self._doing_power_ranking_query = True
plus.power_ranking_query(
season=self._requested_season,
callback=bui.WeakCallPartial(
self._on_power_ranking_query_response
),
)
# Also kick off a query to v2 for latest league-president
# info.
if plus.accounts.primary is not None:
with plus.accounts.primary:
plus.cloud.send_message_cb(
(
bacommon.classic
).GetClassicLeaguePresidentButtonInfoMessage(
season=self._requested_season
),
on_response=bui.WeakCallPartial(
self._on_p_button_info_response
),
)
def _refresh(self) -> None:
plus = bui.app.plus
assert plus is not None
# (Re)create the sub-container if need be.
if self._subcontainer is not None:
self._subcontainer.delete()
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._subcontainerwidth, self._subcontainerheight),
background=False,
)
w_parent = self._subcontainer
v = self._subcontainerheight - 20
v -= 0
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
v -= 52
h2 = 80
v2 = v - 60
worth_color = (0.6, 0.6, 0.65)
tally_color = (0.5, 0.6, 0.8)
spc = 23
h_offs_tally = 150
tally_maxwidth = 120
v2 -= 70
bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 - 60, v2 + 106),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text=_classicassets.strings.ui.points,
h_align='left',
v_align='center',
scale=0.8,
color=(1, 1, 1, 0.3),
maxwidth=200,
)
self._power_ranking_achievements_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|ach',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 80),
icon=_classicassets.textures.achievements_icon.get(),
autoselect=True,
on_activate_call=bui.WeakCallStrict(self._on_achievements_press),
up_widget=self._back_button,
left_widget=self._back_button,
color=(0.5, 0.5, 0.6),
textcolor=(0.7, 0.7, 0.8),
label='',
)
self._power_ranking_achievement_total_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + h_offs_tally, v2 + 45),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text='-',
h_align='left',
v_align='center',
scale=0.8,
color=tally_color,
maxwidth=tally_maxwidth,
)
v2 -= 80
self._power_ranking_trophies_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|trophies',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 80),
icon=_classicassets.textures.medal_silver.get(),
autoselect=True,
on_activate_call=bui.WeakCallStrict(self._on_trophies_press),
left_widget=self._back_button,
color=(0.5, 0.5, 0.6),
textcolor=(0.7, 0.7, 0.8),
label='',
)
self._power_ranking_trophies_total_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + h_offs_tally, v2 + 45),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text='-',
h_align='left',
v_align='center',
scale=0.8,
color=tally_color,
maxwidth=tally_maxwidth,
)
v2 -= 100
bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 - 60, v2 + 86),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text=_classicassets.strings.league.multipliers,
h_align='left',
v_align='center',
scale=0.8,
color=(1, 1, 1, 0.3),
maxwidth=200,
)
self._activity_mult_button: bui.Widget | None
if plus.get_v1_account_misc_read_val('act', False):
self._activity_mult_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|amult',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 60),
icon=_classicassets.textures.heart.get(),
icon_color=(0.5, 0, 0.5),
label=_classicassets.strings.ui.activity,
autoselect=True,
on_activate_call=bui.WeakCallStrict(
self._on_activity_mult_press
),
left_widget=self._back_button,
color=(0.5, 0.5, 0.6),
textcolor=(0.7, 0.7, 0.8),
)
self._activity_mult_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + h_offs_tally, v2 + 40),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text='-',
h_align='left',
v_align='center',
scale=0.8,
color=tally_color,
maxwidth=tally_maxwidth,
)
v2 -= 65
else:
self._activity_mult_button = None
self._up_to_date_bonus_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|uptodatebonus',
position=(self._xoffs + h2 - 60, v2 + 10),
size=(200, 60),
icon=_classicassets.textures.logo.get(),
icon_color=(0.3, 0, 0.3),
label=_classicassets.strings.league.up_to_date_bonus,
autoselect=True,
on_activate_call=bui.WeakCallStrict(
self._on_up_to_date_bonus_press
),
left_widget=self._back_button,
color=(0.5, 0.5, 0.6),
textcolor=(0.7, 0.7, 0.8),
)
self._up_to_date_bonus_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + h_offs_tally, v2 + 40),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text='-',
h_align='left',
v_align='center',
scale=0.8,
color=tally_color,
maxwidth=tally_maxwidth,
)
v2 -= 30
v2 -= spc
bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + h_offs_tally - 10 - 40, v2 + 35),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text=_classicassets.strings.ui.final_score,
h_align='right',
v_align='center',
scale=0.9,
color=worth_color,
maxwidth=150,
)
self._power_ranking_total_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + h_offs_tally - 40, v2 + 35),
size=(0, 0),
flatness=1.0,
shadow=0.0,
text='-',
h_align='left',
v_align='center',
scale=0.9,
color=tally_color,
maxwidth=tally_maxwidth,
)
self._president_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|president',
label='',
position=(self._xoffs + h2 - 60, v2 - 100),
color=(0.7, 0.55, 0.9),
texture=_uiv1assets.textures.button_square_wide.get(),
opacity=0.3,
size=(200, 80),
autoselect=True,
on_activate_call=bui.WeakCallStrict(self._on_president_press),
)
self._president_label = bui.textwidget(
parent=w_parent,
text=_classicassets.strings.league.league_president,
flatness=1.0,
shadow=0.0,
color=(0.6, 0.6, 1, 0.7),
draw_controller=self._president_button,
scale=0.5,
h_align='center',
v_align='center',
maxwidth=140,
position=(self._xoffs + h2 - 60 + 100, v2 - 100 + 59),
size=(0, 0),
)
self._president_name = bui.textwidget(
parent=w_parent,
text='-',
draw_controller=self._president_button,
color=(0.6, 0.6, 1, 0.2),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center',
maxwidth=120,
position=(self._xoffs + h2 - 60 + 100, v2 - 100 + 34),
size=(0, 0),
)
self._president_star1 = bui.imagewidget(
parent=w_parent,
draw_controller=self._president_button,
texture=_classicassets.textures.star.get(),
color=(0.7, 0.55, 0.9),
opacity=0.2,
position=(self._xoffs + h2 - 60 + 5, v2 - 100 + 17),
size=(32, 32),
)
self._president_star1 = bui.imagewidget(
parent=w_parent,
draw_controller=self._president_button,
texture=_classicassets.textures.star.get(),
color=(0.7, 0.55, 0.9),
opacity=0.2,
position=(self._xoffs + h2 - 60 + 200 - 5 - 32, v2 - 100 + 17),
size=(32, 32),
)
self._season_show_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 390 - 15, v - 20),
size=(0, 0),
color=(0.6, 0.6, 0.7),
maxwidth=200,
text=_commonassets.strings.actions.show,
h_align='right',
v_align='center',
scale=0.8,
shadow=0,
flatness=1.0,
)
self._league_title_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 97),
size=(0, 0),
color=(0.6, 0.6, 0.7),
maxwidth=230,
text='',
h_align='center',
v_align='center',
scale=0.9,
shadow=0,
flatness=1.0,
)
self._league_text_scale = 1.8
self._league_text_maxwidth = 210
self._league_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 140),
size=(0, 0),
color=(1, 1, 1),
maxwidth=self._league_text_maxwidth,
text='-',
h_align='center',
v_align='center',
scale=self._league_text_scale,
shadow=1.0,
flatness=1.0,
)
self._league_number_base_pos = (470, v - 140)
self._league_number_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 140),
size=(0, 0),
color=(1, 1, 1),
maxwidth=100,
text='',
h_align='left',
v_align='center',
scale=0.8,
shadow=1.0,
flatness=1.0,
)
self._loading_spinner = bui.spinnerwidget(
parent=w_parent,
position=(
self._subcontainerwidth * 0.5,
self._subcontainerheight * 0.5,
),
style='bomb',
size=64,
)
self._your_power_ranking_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 142 - 70),
size=(0, 0),
color=(0.6, 0.6, 0.7),
maxwidth=230,
text='',
h_align='center',
v_align='center',
scale=0.9,
shadow=0,
flatness=1.0,
)
self._to_ranked_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 250 - 70),
size=(0, 0),
color=(0.6, 0.6, 0.7),
maxwidth=230,
text='',
h_align='center',
v_align='center',
scale=0.8,
shadow=0,
flatness=1.0,
)
self._power_ranking_rank_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 473, v - 210 - 70),
size=(0, 0),
big=False,
text='-',
h_align='center',
v_align='center',
scale=1.0,
)
self._season_ends_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 380),
size=(0, 0),
color=(0.6, 0.6, 0.6),
maxwidth=230,
text='',
h_align='center',
v_align='center',
scale=0.9,
shadow=0,
flatness=1.0,
)
self._trophy_counts_reset_text = bui.textwidget(
parent=w_parent,
position=(self._xoffs + 470, v - 410),
size=(0, 0),
color=(0.5, 0.5, 0.5),
maxwidth=230,
text='Trophy counts will reset next season.',
h_align='center',
v_align='center',
scale=0.8,
shadow=0,
flatness=1.0,
)
self._power_ranking_score_widgets = []
self._power_ranking_score_v = v - 56
h = 707
v -= 451
self._see_more_button = bui.buttonwidget(
parent=w_parent,
id=f'{self.main_window_id_prefix}|seemore',
label=bui.app.lang.get_resource(f'{self._r}.seeMoreText'),
position=(self._xoffs + h, v),
color=(0.5, 0.5, 0.6),
textcolor=(0.7, 0.7, 0.8),
size=(230, 60),
autoselect=True,
on_activate_call=bui.WeakCallStrict(self._on_more_press),
)
def _on_president_press(self) -> None:
import bacommon.docui.routes.classicleaguepresidency as lroutes
from bauiv1lib.league.presidency import LeaguePresidencyUIController
from bauiv1lib.connectivity import wait_for_connectivity
# No-op if we're not in control.
if not self.main_window_has_control():
return
plus = bui.app.plus
assert plus is not None
# We should be signed in at this point, but let's be sure.
if plus.accounts.primary is None:
bui.screenmessage(
_classicassets.strings.account.not_signed_in, color=(1, 0, 0)
)
_builtinassets.audio.error.get().play()
return
# Wait for connectivity if need be, then bring up a cloud based
# doc-ui window for showing/futzing-with league president stuff.
wait_for_connectivity(
on_connected=lambda: self.main_window_replace(
bui.CallStrict(
LeaguePresidencyUIController().create_window,
lroutes.Root(season=self._season),
origin_widget=self._president_button,
auxiliary_style=False,
),
extra_type_id=(
LeaguePresidencyUIController
).get_window_extra_type_id(),
)
)
def _on_more_press(self) -> None:
plus = bui.app.plus
assert plus is not None
our_login_id = plus.get_v1_account_public_login_id()
if not self._can_do_more_button or our_login_id is None:
_builtinassets.audio.error.get().play()
bui.screenmessage(
_commonassets.strings.status.unavailable_status, color=(1, 0, 0)
)
return
if self._season is None:
season_str = ''
else:
season_str = '&season=' + (
'all_time' if self._season == 'a' else self._season
)
if self._league_url_arg != '':
league_str = '&league=' + self._league_url_arg
else:
league_str = ''
bui.open_url(
plus.get_legacy_master_server_address()
+ '/highscores?list=powerRankings&v=2'
+ league_str
+ season_str
+ '&player='
+ our_login_id
)
def _update_for_league_rank_data(self, data: dict[str, Any] | None) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
if not self._root_widget:
return
plus = bui.app.plus
uiscale = bui.app.ui_v1.uiscale
assert plus is not None
assert bui.app.classic is not None
accounts = bui.app.classic.accounts
in_top = data is not None and data['rank'] is not None
eq_text = bui.app.lang.get_resource(
f'{self._r}.powerRankingPointsEqualsText'
)
pts_txt = bui.app.lang.get_resource(f'{self._r}.powerRankingPointsText')
def num_str(number: str) -> str:
# Layout code (flat widths/placement); evaluate locally.
return (
_classicassets.strings.league.number_badge(number=number)
).evaluate()
do_percent = False
finished_season_unranked = False
self._can_do_more_button = True
extra_text = ''
# (v1 sign-in state on purpose; see __init__.)
if plus.get_v1_account_state() != 'signed_in':
status_text = (
'('
+ _classicassets.strings.ui.not_signed_in_status.evaluate()
+ ')'
)
elif in_top:
assert data is not None
status_text = num_str(str(data['rank']))
elif data is not None:
try:
# Handle old seasons where we didn't wind up ranked at
# the end.
if not data['scores']:
status_text = bui.app.lang.get_resource(
f'{self._r}.powerRankingFinishedSeasonUnrankedText'
)
extra_text = ''
finished_season_unranked = True
self._can_do_more_button = False
else:
our_points = accounts.get_league_rank_points(data)
progress = float(our_points) / max(1, data['scores'][-1][1])
status_text = str(int(progress * 100.0)) + '%'
extra_text = '\n' + bui.app.lang.get_resource(
f'{self._r}.powerRankingPointsToRankedText'
).replace('${CURRENT}', str(our_points)).replace(
'${REMAINING}', str(data['scores'][-1][1])
)
do_percent = True
except Exception:
logging.exception('Error updating power ranking.')
status_text = bui.app.lang.get_resource(
f'{self._r}.powerRankingNotInTopText'
).replace('${NUMBER}', str(data['listSize']))
extra_text = ''
else:
status_text = '-'
self._season = data['s'] if data is not None else None
v = self._subcontainerheight - 20
# For fullscreen scrollable, account for toolbar.
uiscale = bui.app.ui_v1.uiscale
if uiscale is bui.UIScale.SMALL:
v -= 52
popup_was_selected = False
if self._season_popup_menu is not None:
btn = self._season_popup_menu.get_button()
assert self._subcontainer
if self._subcontainer.get_selected_child() == btn:
popup_was_selected = True
btn.delete()
season_choices = []
season_choices_display = []
did_first = False
self._is_current_season = False
if data is not None:
# Build our list of seasons we have available.
for ssn in data['sl']:
season_choices.append(ssn)
if ssn != 'a' and not did_first:
season_choices_display.append(
_classicassets.strings.league.current_season(number=ssn)
)
did_first = True
# If we either did not specify a season or specified
# the first, we're looking at the current.
if self._season in [ssn, None]:
self._is_current_season = True
elif ssn == 'a':
season_choices_display.append(
_classicassets.strings.league.all_time
)
else:
season_choices_display.append(
_classicassets.strings.league.season(number=ssn)
)
assert self._subcontainer
self._season_popup_menu = PopupMenu(
parent=self._subcontainer,
button_id=f'{self.main_window_id_prefix}|season',
position=(self._xoffs + 390, v - 45),
button_size=(200, 50),
choices=season_choices,
on_value_change_call=bui.WeakCallPartial(
self._on_season_change
),
choices_display=season_choices_display,
current_choice=self._season,
# (Medium's default popup-menu scale.)
scale=self._popup_scale(1.65),
)
if popup_was_selected:
bui.containerwidget(
edit=self._subcontainer,
selected_child=self._season_popup_menu.get_button(),
)
bui.widget(edit=self._see_more_button, show_buffer_bottom=100)
bui.widget(
edit=self._season_popup_menu.get_button(),
up_widget=self._back_button,
)
if uiscale is not bui.UIScale.SMALL:
bui.widget(
edit=self._back_button,
down_widget=self._power_ranking_achievements_button,
right_widget=self._season_popup_menu.get_button(),
)
bui.textwidget(
edit=self._league_title_text,
text=(
''
if self._season == 'a'
else _classicassets.strings.league.league
),
)
if data is None:
lname = ''
lnum = ''
lcolor = (1, 1, 1)
self._league_url_arg = ''
elif self._season == 'a':
lname = _classicassets.strings.league.all_time.evaluate()
lnum = ''
lcolor = (1, 1, 1)
self._league_url_arg = ''
else:
lnum = ('[' + str(data['l']['i']) + ']') if data['l']['i2'] else ''
lnameval = league_display_name(data['l']['n'])
lname = (
lnameval if isinstance(lnameval, str) else lnameval.evaluate()
)
lcolor = data['l']['c']
self._league_url_arg = (
data['l']['n'] + '_' + str(data['l']['i'])
).lower()
to_end_string: bui.Lstr | bui.LangStr | str
if data is None or self._season == 'a' or data['se'] is None:
to_end_string = ''
show_season_end = False
else:
show_season_end = True
days_to_end = data['se'][0]
minutes_to_end = data['se'][1]
if days_to_end > 0:
to_end_string = _classicassets.strings.league.season_ends_days(
days=days_to_end
)
elif days_to_end == 0 and minutes_to_end >= 60:
to_end_string = _classicassets.strings.league.season_ends_hours(
hours=minutes_to_end // 60
)
elif days_to_end == 0 and minutes_to_end >= 0:
to_end_string = (
_classicassets.strings.league.season_ends_minutes(
minutes=minutes_to_end
)
)
else:
to_end_string = (
_classicassets.strings.league.season_ended_days_ago(
days=-(days_to_end + 1)
)
)
bui.textwidget(edit=self._season_ends_text, text=to_end_string)
bui.textwidget(
edit=self._trophy_counts_reset_text,
text=(
_classicassets.strings.league.trophy_counts_reset
if self._is_current_season and show_season_end
else ''
),
)
bui.textwidget(edit=self._league_text, text=lname, color=lcolor)
l_text_width = min(
self._league_text_maxwidth,
bui.get_string_width(
lname, suppress_warning=True, suppress_logic_thread_warning=True
)
* self._league_text_scale,
)
bui.textwidget(
edit=self._league_number_text,
text=lnum,
color=lcolor,
position=(
self._xoffs
+ self._league_number_base_pos[0]
+ l_text_width * 0.5
+ 8,
self._league_number_base_pos[1] + 10,
),
)
bui.textwidget(
edit=self._to_ranked_text,
text=(
_classicassets.strings.league.to_ranked.evaluate()
+ ''
+ extra_text
if do_percent
else ''
),
)
bui.textwidget(
edit=self._your_power_ranking_text,
text=(_classicassets.strings.ui.rank if (not do_percent) else ''),
)
bui.spinnerwidget(edit=self._loading_spinner, visible=False)
bui.textwidget(
edit=self._power_ranking_rank_text,
position=(self._xoffs + 473, v - 70 - (170 if do_percent else 220)),
text=status_text,
big=(in_top or do_percent),
scale=(
3.0
if (in_top or do_percent)
else 0.7 if finished_season_unranked else 1.0
),
)
if self._activity_mult_button is not None:
if data is None or data['act'] is None:
bui.buttonwidget(
edit=self._activity_mult_button,
textcolor=(0.7, 0.7, 0.8, 0.5),
icon_color=(0.5, 0, 0.5, 0.3),
)
bui.textwidget(edit=self._activity_mult_text, text=' -')
else:
bui.buttonwidget(
edit=self._activity_mult_button,
textcolor=(0.7, 0.7, 0.8, 1.0),
icon_color=(0.5, 0, 0.5, 1.0),
)
bui.textwidget(
edit=self._activity_mult_text,
text=f'x {data['act']:.2f}',
)
# This used to be a bonus for 'BombSquad Pro' holders, but since
# we're transitioning away from that it is now a bonus for
# everyone running a recent-ish version of the game.
have_up_to_date_bonus = data is not None
pro_mult = (
1.0
+ float(
plus.get_v1_account_misc_read_val('proPowerRankingBoost', 0.0)
)
* 0.01
)
bui.textwidget(
edit=self._up_to_date_bonus_text,
text=(
' -'
if (data is None or not have_up_to_date_bonus)
else f'x {pro_mult:.2f}'
),
)
bui.buttonwidget(
edit=self._up_to_date_bonus_button,
textcolor=(0.7, 0.7, 0.8, (1.0 if have_up_to_date_bonus else 0.5)),
icon_color=(
(0.5, 0, 0.5) if have_up_to_date_bonus else (0.5, 0, 0.5, 0.2)
),
)
bui.buttonwidget(
edit=self._power_ranking_achievements_button,
label=('' if data is None else str(data['a']) + ' ')
+ _classicassets.strings.ui.achievements.evaluate(),
)
# For the achievement value, use the number they gave us for
# non-current seasons; otherwise calc our own.
total_ach_value = 0
for ach in bui.app.classic.ach.achievements:
if ach.complete:
total_ach_value += ach.power_ranking_value
if self._season != 'a' and not self._is_current_season:
if data is not None and 'at' in data:
total_ach_value = data['at']
bui.textwidget(
edit=self._power_ranking_achievement_total_text,
text=(
'-'
if data is None
else ('+ ' + pts_txt.replace('${NUMBER}', str(total_ach_value)))
),
)
total_trophies_count = accounts.get_league_rank_points(
data, 'trophyCount'
)
total_trophies_value = accounts.get_league_rank_points(data, 'trophies')
bui.buttonwidget(
edit=self._power_ranking_trophies_button,
label=('' if data is None else str(total_trophies_count) + ' ')
+ _classicassets.strings.ui.trophies.evaluate(),
)
bui.textwidget(
edit=self._power_ranking_trophies_total_text,
text=(
'-'
if data is None
else (
'+ '
+ pts_txt.replace('${NUMBER}', str(total_trophies_value))
)
),
)
bui.textwidget(
edit=self._power_ranking_total_text,
text=(
'-'
if data is None
else eq_text.replace(
'${NUMBER}', str(accounts.get_league_rank_points(data))
)
),
)
for widget in self._power_ranking_score_widgets:
widget.delete()
self._power_ranking_score_widgets = []
scores: list = data['scores'] if data is not None else []
tally_color = (0.5, 0.6, 0.8)
w_parent = self._subcontainer
v2 = self._power_ranking_score_v
for score in scores:
h2 = 680
is_us = score[3]
self._power_ranking_score_widgets.append(
bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 - 20, v2),
size=(0, 0),
color=(1, 1, 1) if is_us else (0.6, 0.6, 0.7),
maxwidth=40,
flatness=1.0,
shadow=0.0,
text=num_str(str(score[0])),
h_align='right',
v_align='center',
scale=0.5,
)
)
self._power_ranking_score_widgets.append(
bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + 20, v2),
size=(0, 0),
color=(1, 1, 1) if is_us else tally_color,
maxwidth=60,
text=str(score[1]),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center',
scale=0.7,
)
)
txt = bui.textwidget(
parent=w_parent,
position=(self._xoffs + h2 + 60, v2 - (28 * 0.5) / 0.9),
size=(210 / 0.9, 28),
color=(1, 1, 1) if is_us else (0.6, 0.6, 0.6),
maxwidth=210,
flatness=1.0,
shadow=0.0,
autoselect=True,
selectable=True,
click_activate=True,
text=score[2],
h_align='left',
v_align='center',
scale=0.9,
)
# Giving these ids doesn't work cleanly with sel
# save/restore due to refreshing, so let's just not for now.
bui.widget(edit=txt, allow_preserve_selection=False)
self._power_ranking_score_widgets.append(txt)
bui.textwidget(
edit=txt,
on_activate_call=bui.CallStrict(
self._show_account_info, score[4], txt
),
)
assert self._season_popup_menu is not None
bui.widget(
edit=txt, left_widget=self._season_popup_menu.get_button()
)
v2 -= 28
def _show_account_info(
self, account_id: str, textwidget: bui.Widget
) -> None:
from bauiv1lib.account.viewer import AccountViewerWindow
bui.play_swish()
AccountViewerWindow(
account_id=account_id, position=textwidget.get_screen_space_center()
)
def _on_season_change(self, value: str) -> None:
self._requested_season = value
self._last_power_ranking_query_time = None # Update asap.
self._update(show=True)
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self, TypeIs)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum