mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
1247 lines
44 KiB
Python
1247 lines
44 KiB
Python
# Released under the MIT License. See LICENSE for details.
|
|
#
|
|
"""UI for browsing available co-op levels/games/etc."""
|
|
# FIXME: Break this up.
|
|
# pylint: disable=too-many-lines
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import ba
|
|
import ba.internal
|
|
from bastd.ui.store.button import StoreButton
|
|
from bastd.ui.league.rankbutton import LeagueRankButton
|
|
from bastd.ui.store.browser import StoreBrowserWindow
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Any
|
|
|
|
from bastd.ui.coop.tournamentbutton import TournamentButton
|
|
|
|
|
|
class CoopBrowserWindow(ba.Window):
|
|
"""Window for browsing co-op levels/games/etc."""
|
|
|
|
def _update_corner_button_positions(self) -> None:
|
|
uiscale = ba.app.ui.uiscale
|
|
offs = (
|
|
-55
|
|
if uiscale is ba.UIScale.SMALL
|
|
and ba.internal.is_party_icon_visible()
|
|
else 0
|
|
)
|
|
if self._league_rank_button is not None:
|
|
self._league_rank_button.set_position(
|
|
(
|
|
self._width - 282 + offs - self._x_inset,
|
|
self._height
|
|
- 85
|
|
- (4 if uiscale is ba.UIScale.SMALL else 0),
|
|
)
|
|
)
|
|
if self._store_button is not None:
|
|
self._store_button.set_position(
|
|
(
|
|
self._width - 170 + offs - self._x_inset,
|
|
self._height
|
|
- 85
|
|
- (4 if uiscale is ba.UIScale.SMALL else 0),
|
|
)
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
transition: str | None = 'in_right',
|
|
origin_widget: ba.Widget | None = None,
|
|
):
|
|
# pylint: disable=too-many-statements
|
|
# pylint: disable=cyclic-import
|
|
import threading
|
|
|
|
# Preload some modules we use in a background thread so we won't
|
|
# have a visual hitch when the user taps them.
|
|
threading.Thread(target=self._preload_modules).start()
|
|
|
|
ba.set_analytics_screen('Coop Window')
|
|
|
|
app = ba.app
|
|
cfg = app.config
|
|
|
|
# Quick note to players that tourneys won't work in ballistica
|
|
# core builds. (need to split the word so it won't get subbed out)
|
|
if 'ballistica' + 'core' == ba.internal.appname():
|
|
ba.timer(
|
|
1.0,
|
|
lambda: ba.screenmessage(
|
|
ba.Lstr(resource='noTournamentsInTestBuildText'),
|
|
color=(1, 1, 0),
|
|
),
|
|
timetype=ba.TimeType.REAL,
|
|
)
|
|
|
|
# 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
|
|
|
|
# Try to recreate the same number of buttons we had last time so our
|
|
# re-selection code works.
|
|
self._tournament_button_count = app.config.get('Tournament Rows', 0)
|
|
assert isinstance(self._tournament_button_count, int)
|
|
|
|
self._easy_button: ba.Widget | None = None
|
|
self._hard_button: ba.Widget | None = None
|
|
self._hard_button_lock_image: ba.Widget | None = None
|
|
self._campaign_percent_text: ba.Widget | None = None
|
|
|
|
uiscale = ba.app.ui.uiscale
|
|
self._width = 1320 if uiscale is ba.UIScale.SMALL else 1120
|
|
self._x_inset = x_inset = 100 if uiscale is ba.UIScale.SMALL else 0
|
|
self._height = (
|
|
657
|
|
if uiscale is ba.UIScale.SMALL
|
|
else 730
|
|
if uiscale is ba.UIScale.MEDIUM
|
|
else 800
|
|
)
|
|
app.ui.set_main_menu_location('Coop Select')
|
|
self._r = 'coopSelectWindow'
|
|
top_extra = 20 if uiscale is ba.UIScale.SMALL else 0
|
|
|
|
self._tourney_data_up_to_date = False
|
|
|
|
self._campaign_difficulty = ba.internal.get_v1_account_misc_val(
|
|
'campaignDifficulty', 'easy'
|
|
)
|
|
|
|
super().__init__(
|
|
root_widget=ba.containerwidget(
|
|
size=(self._width, self._height + top_extra),
|
|
toolbar_visibility='menu_full',
|
|
scale_origin_stack_offset=scale_origin,
|
|
stack_offset=(
|
|
(0, -15)
|
|
if uiscale is ba.UIScale.SMALL
|
|
else (0, 0)
|
|
if uiscale is ba.UIScale.MEDIUM
|
|
else (0, 0)
|
|
),
|
|
transition=transition,
|
|
scale=(
|
|
1.2
|
|
if uiscale is ba.UIScale.SMALL
|
|
else 0.8
|
|
if uiscale is ba.UIScale.MEDIUM
|
|
else 0.75
|
|
),
|
|
)
|
|
)
|
|
|
|
if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
|
|
self._back_button = None
|
|
else:
|
|
self._back_button = ba.buttonwidget(
|
|
parent=self._root_widget,
|
|
position=(
|
|
75 + x_inset,
|
|
self._height
|
|
- 87
|
|
- (4 if uiscale is ba.UIScale.SMALL else 0),
|
|
),
|
|
size=(120, 60),
|
|
scale=1.2,
|
|
autoselect=True,
|
|
label=ba.Lstr(resource='backText'),
|
|
button_type='back',
|
|
)
|
|
|
|
self._league_rank_button: LeagueRankButton | None
|
|
self._store_button: StoreButton | None
|
|
self._store_button_widget: ba.Widget | None
|
|
self._league_rank_button_widget: ba.Widget | None
|
|
|
|
if not app.ui.use_toolbars:
|
|
prb = self._league_rank_button = LeagueRankButton(
|
|
parent=self._root_widget,
|
|
position=(
|
|
self._width - (282 + x_inset),
|
|
self._height
|
|
- 85
|
|
- (4 if uiscale is ba.UIScale.SMALL else 0),
|
|
),
|
|
size=(100, 60),
|
|
color=(0.4, 0.4, 0.9),
|
|
textcolor=(0.9, 0.9, 2.0),
|
|
scale=1.05,
|
|
on_activate_call=ba.WeakCall(self._switch_to_league_rankings),
|
|
)
|
|
self._league_rank_button_widget = prb.get_button()
|
|
|
|
sbtn = self._store_button = StoreButton(
|
|
parent=self._root_widget,
|
|
position=(
|
|
self._width - (170 + x_inset),
|
|
self._height
|
|
- 85
|
|
- (4 if uiscale is ba.UIScale.SMALL else 0),
|
|
),
|
|
size=(100, 60),
|
|
color=(0.6, 0.4, 0.7),
|
|
show_tickets=True,
|
|
button_type='square',
|
|
sale_scale=0.85,
|
|
textcolor=(0.9, 0.7, 1.0),
|
|
scale=1.05,
|
|
on_activate_call=ba.WeakCall(self._switch_to_score, None),
|
|
)
|
|
self._store_button_widget = sbtn.get_button()
|
|
ba.widget(
|
|
edit=self._back_button,
|
|
right_widget=self._league_rank_button_widget,
|
|
)
|
|
ba.widget(
|
|
edit=self._league_rank_button_widget,
|
|
left_widget=self._back_button,
|
|
)
|
|
else:
|
|
self._league_rank_button = None
|
|
self._store_button = None
|
|
self._store_button_widget = None
|
|
self._league_rank_button_widget = None
|
|
|
|
# Move our corner buttons dynamically to keep them out of the way of
|
|
# the party icon :-(
|
|
self._update_corner_button_positions()
|
|
self._update_corner_button_positions_timer = ba.Timer(
|
|
1.0,
|
|
ba.WeakCall(self._update_corner_button_positions),
|
|
repeat=True,
|
|
timetype=ba.TimeType.REAL,
|
|
)
|
|
|
|
self._last_tournament_query_time: float | None = None
|
|
self._last_tournament_query_response_time: float | None = None
|
|
self._doing_tournament_query = False
|
|
|
|
self._selected_campaign_level = cfg.get(
|
|
'Selected Coop Campaign Level', None
|
|
)
|
|
self._selected_custom_level = cfg.get(
|
|
'Selected Coop Custom Level', None
|
|
)
|
|
|
|
# Don't want initial construction affecting our last-selected.
|
|
self._do_selection_callbacks = False
|
|
v = self._height - 95
|
|
txt = ba.textwidget(
|
|
parent=self._root_widget,
|
|
position=(
|
|
self._width * 0.5,
|
|
v + 40 - (0 if uiscale is ba.UIScale.SMALL else 0),
|
|
),
|
|
size=(0, 0),
|
|
text=ba.Lstr(
|
|
resource='playModes.singlePlayerCoopText',
|
|
fallback_resource='playModes.coopText',
|
|
),
|
|
h_align='center',
|
|
color=app.ui.title_color,
|
|
scale=1.5,
|
|
maxwidth=500,
|
|
v_align='center',
|
|
)
|
|
|
|
if app.ui.use_toolbars and uiscale is ba.UIScale.SMALL:
|
|
ba.textwidget(edit=txt, text='')
|
|
|
|
if self._back_button is not None:
|
|
ba.buttonwidget(
|
|
edit=self._back_button,
|
|
button_type='backSmall',
|
|
size=(60, 50),
|
|
position=(
|
|
75 + x_inset,
|
|
self._height
|
|
- 87
|
|
- (4 if uiscale is ba.UIScale.SMALL else 0)
|
|
+ 6,
|
|
),
|
|
label=ba.charstr(ba.SpecialChar.BACK),
|
|
)
|
|
|
|
self._selected_row = cfg.get('Selected Coop Row', None)
|
|
|
|
self.star_tex = ba.gettexture('star')
|
|
self.lsbt = ba.getmodel('level_select_button_transparent')
|
|
self.lsbo = ba.getmodel('level_select_button_opaque')
|
|
self.a_outline_tex = ba.gettexture('achievementOutline')
|
|
self.a_outline_model = ba.getmodel('achievementOutline')
|
|
|
|
self._scroll_width = self._width - (130 + 2 * x_inset)
|
|
self._scroll_height = self._height - (
|
|
190 if uiscale is ba.UIScale.SMALL and app.ui.use_toolbars else 160
|
|
)
|
|
|
|
self._subcontainerwidth = 800.0
|
|
self._subcontainerheight = 1400.0
|
|
|
|
self._scrollwidget = ba.scrollwidget(
|
|
parent=self._root_widget,
|
|
highlight=False,
|
|
position=(65 + x_inset, 120)
|
|
if uiscale is ba.UIScale.SMALL and app.ui.use_toolbars
|
|
else (65 + x_inset, 70),
|
|
size=(self._scroll_width, self._scroll_height),
|
|
simple_culling_v=10.0,
|
|
claims_left_right=True,
|
|
claims_tab=True,
|
|
selection_loops_to_parent=True,
|
|
)
|
|
self._subcontainer: ba.Widget | None = None
|
|
|
|
# Take note of our account state; we'll refresh later if this changes.
|
|
self._account_state_num = ba.internal.get_v1_account_state_num()
|
|
|
|
# Same for fg/bg state.
|
|
self._fg_state = app.fg_state
|
|
|
|
self._refresh()
|
|
self._restore_state()
|
|
|
|
# Even though we might display cached tournament data immediately, we
|
|
# don't consider it valid until we've pinged.
|
|
# the server for an update
|
|
self._tourney_data_up_to_date = False
|
|
|
|
# If we've got a cached tournament list for our account and info for
|
|
# each one of those tournaments, go ahead and display it as a
|
|
# starting point.
|
|
if (
|
|
app.accounts_v1.account_tournament_list is not None
|
|
and app.accounts_v1.account_tournament_list[0]
|
|
== ba.internal.get_v1_account_state_num()
|
|
and all(
|
|
t_id in app.accounts_v1.tournament_info
|
|
for t_id in app.accounts_v1.account_tournament_list[1]
|
|
)
|
|
):
|
|
tourney_data = [
|
|
app.accounts_v1.tournament_info[t_id]
|
|
for t_id in app.accounts_v1.account_tournament_list[1]
|
|
]
|
|
self._update_for_data(tourney_data)
|
|
|
|
# This will pull new data periodically, update timers, etc.
|
|
self._update_timer = ba.Timer(
|
|
1.0,
|
|
ba.WeakCall(self._update),
|
|
timetype=ba.TimeType.REAL,
|
|
repeat=True,
|
|
)
|
|
self._update()
|
|
|
|
# noinspection PyUnresolvedReferences
|
|
@staticmethod
|
|
def _preload_modules() -> None:
|
|
"""Preload modules we use (called in bg thread)."""
|
|
import bastd.ui.purchase as _unused1
|
|
import bastd.ui.coop.gamebutton as _unused2
|
|
import bastd.ui.confirm as _unused3
|
|
import bastd.ui.account as _unused4
|
|
import bastd.ui.league.rankwindow as _unused5
|
|
import bastd.ui.store.browser as _unused6
|
|
import bastd.ui.account.viewer as _unused7
|
|
import bastd.ui.tournamentscores as _unused8
|
|
import bastd.ui.tournamententry as _unused9
|
|
import bastd.ui.play as _unused10
|
|
import bastd.ui.coop.tournamentbutton as _unused11
|
|
|
|
def _update(self) -> None:
|
|
# Do nothing if we've somehow outlived our actual UI.
|
|
if not self._root_widget:
|
|
return
|
|
|
|
cur_time = ba.time(ba.TimeType.REAL)
|
|
|
|
# If its been a while since we got a tournament update, consider the
|
|
# data invalid (prevents us from joining tournaments if our internet
|
|
# connection goes down for a while).
|
|
if (
|
|
self._last_tournament_query_response_time is None
|
|
or ba.time(ba.TimeType.REAL)
|
|
- self._last_tournament_query_response_time
|
|
> 60.0 * 2
|
|
):
|
|
self._tourney_data_up_to_date = False
|
|
|
|
# If our account state has changed, do a full request.
|
|
account_state_num = ba.internal.get_v1_account_state_num()
|
|
if account_state_num != self._account_state_num:
|
|
self._account_state_num = account_state_num
|
|
self._save_state()
|
|
self._refresh()
|
|
|
|
# Also encourage a new tournament query since this will clear out
|
|
# our current results.
|
|
if not self._doing_tournament_query:
|
|
self._last_tournament_query_time = None
|
|
|
|
# If we've been backgrounded/foregrounded, invalidate our
|
|
# tournament entries (they will be refreshed below asap).
|
|
if self._fg_state != ba.app.fg_state:
|
|
self._tourney_data_up_to_date = False
|
|
|
|
# Send off a new tournament query if its been long enough or whatnot.
|
|
if not self._doing_tournament_query and (
|
|
self._last_tournament_query_time is None
|
|
or cur_time - self._last_tournament_query_time > 30.0
|
|
or self._fg_state != ba.app.fg_state
|
|
):
|
|
self._fg_state = ba.app.fg_state
|
|
self._last_tournament_query_time = cur_time
|
|
self._doing_tournament_query = True
|
|
ba.internal.tournament_query(
|
|
args={'source': 'coop window refresh', 'numScores': 1},
|
|
callback=ba.WeakCall(self._on_tournament_query_response),
|
|
)
|
|
|
|
# Decrement time on our tournament buttons.
|
|
ads_enabled = ba.internal.have_incentivized_ad()
|
|
for tbtn in self._tournament_buttons:
|
|
tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
|
|
if tbtn.time_remaining_value_text is not None:
|
|
ba.textwidget(
|
|
edit=tbtn.time_remaining_value_text,
|
|
text=ba.timestring(
|
|
tbtn.time_remaining,
|
|
centi=False,
|
|
suppress_format_warning=True,
|
|
)
|
|
if (
|
|
tbtn.has_time_remaining
|
|
and self._tourney_data_up_to_date
|
|
)
|
|
else '-',
|
|
)
|
|
|
|
# Also adjust the ad icon visibility.
|
|
if tbtn.allow_ads and ba.internal.has_video_ads():
|
|
ba.imagewidget(
|
|
edit=tbtn.entry_fee_ad_image,
|
|
opacity=1.0 if ads_enabled else 0.25,
|
|
)
|
|
ba.textwidget(
|
|
edit=tbtn.entry_fee_text_remaining,
|
|
color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
|
|
)
|
|
|
|
self._update_hard_mode_lock_image()
|
|
|
|
def _update_hard_mode_lock_image(self) -> None:
|
|
try:
|
|
ba.imagewidget(
|
|
edit=self._hard_button_lock_image,
|
|
opacity=0.0 if ba.app.accounts_v1.have_pro_options() else 1.0,
|
|
)
|
|
except Exception:
|
|
ba.print_exception('Error updating campaign lock.')
|
|
|
|
def _update_for_data(self, data: list[dict[str, Any]] | None) -> None:
|
|
|
|
# If the number of tournaments or challenges in the data differs from
|
|
# our current arrangement, refresh with the new number.
|
|
if (data is None and self._tournament_button_count != 0) or (
|
|
data is not None and (len(data) != self._tournament_button_count)
|
|
):
|
|
self._tournament_button_count = len(data) if data is not None else 0
|
|
ba.app.config['Tournament Rows'] = self._tournament_button_count
|
|
self._refresh()
|
|
|
|
# Update all of our tourney buttons based on whats in data.
|
|
for i, tbtn in enumerate(self._tournament_buttons):
|
|
assert data is not None
|
|
tbtn.update_for_data(data[i])
|
|
|
|
def _on_tournament_query_response(
|
|
self, data: dict[str, Any] | None
|
|
) -> None:
|
|
accounts = ba.app.accounts_v1
|
|
if data is not None:
|
|
tournament_data = data['t'] # This used to be the whole payload.
|
|
self._last_tournament_query_response_time = ba.time(
|
|
ba.TimeType.REAL
|
|
)
|
|
else:
|
|
tournament_data = None
|
|
|
|
# Keep our cached tourney info up to date.
|
|
if data is not None:
|
|
self._tourney_data_up_to_date = True
|
|
accounts.cache_tournament_info(tournament_data)
|
|
|
|
# Also cache the current tourney list/order for this account.
|
|
accounts.account_tournament_list = (
|
|
ba.internal.get_v1_account_state_num(),
|
|
[e['tournamentID'] for e in tournament_data],
|
|
)
|
|
|
|
self._doing_tournament_query = False
|
|
self._update_for_data(tournament_data)
|
|
|
|
def _set_campaign_difficulty(self, difficulty: str) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.purchase import PurchaseWindow
|
|
|
|
if difficulty != self._campaign_difficulty:
|
|
if (
|
|
difficulty == 'hard'
|
|
and not ba.app.accounts_v1.have_pro_options()
|
|
):
|
|
PurchaseWindow(items=['pro'])
|
|
return
|
|
ba.playsound(ba.getsound('gunCocking'))
|
|
if difficulty not in ('easy', 'hard'):
|
|
print('ERROR: invalid campaign difficulty:', difficulty)
|
|
difficulty = 'easy'
|
|
self._campaign_difficulty = difficulty
|
|
ba.internal.add_transaction(
|
|
{
|
|
'type': 'SET_MISC_VAL',
|
|
'name': 'campaignDifficulty',
|
|
'value': difficulty,
|
|
}
|
|
)
|
|
self._refresh_campaign_row()
|
|
else:
|
|
ba.playsound(ba.getsound('click01'))
|
|
|
|
def _refresh_campaign_row(self) -> None:
|
|
# pylint: disable=too-many-locals
|
|
# pylint: disable=cyclic-import
|
|
from ba.internal import getcampaign
|
|
from bastd.ui.coop.gamebutton import GameButton
|
|
|
|
parent_widget = self._campaign_sub_container
|
|
|
|
# Clear out anything in the parent widget already.
|
|
for child in parent_widget.get_children():
|
|
child.delete()
|
|
|
|
next_widget_down = self._tournament_info_button
|
|
|
|
h = 0
|
|
v2 = -2
|
|
sel_color = (0.75, 0.85, 0.5)
|
|
sel_color_hard = (0.4, 0.7, 0.2)
|
|
un_sel_color = (0.5, 0.5, 0.5)
|
|
sel_textcolor = (2, 2, 0.8)
|
|
un_sel_textcolor = (0.6, 0.6, 0.6)
|
|
self._easy_button = ba.buttonwidget(
|
|
parent=parent_widget,
|
|
position=(h + 30, v2 + 105),
|
|
size=(120, 70),
|
|
label=ba.Lstr(resource='difficultyEasyText'),
|
|
button_type='square',
|
|
autoselect=True,
|
|
enable_sound=False,
|
|
on_activate_call=ba.Call(self._set_campaign_difficulty, 'easy'),
|
|
on_select_call=ba.Call(self.sel_change, 'campaign', 'easyButton'),
|
|
color=sel_color
|
|
if self._campaign_difficulty == 'easy'
|
|
else un_sel_color,
|
|
textcolor=sel_textcolor
|
|
if self._campaign_difficulty == 'easy'
|
|
else un_sel_textcolor,
|
|
)
|
|
ba.widget(edit=self._easy_button, show_buffer_left=100)
|
|
if self._selected_campaign_level == 'easyButton':
|
|
ba.containerwidget(
|
|
edit=parent_widget,
|
|
selected_child=self._easy_button,
|
|
visible_child=self._easy_button,
|
|
)
|
|
lock_tex = ba.gettexture('lock')
|
|
|
|
self._hard_button = ba.buttonwidget(
|
|
parent=parent_widget,
|
|
position=(h + 30, v2 + 32),
|
|
size=(120, 70),
|
|
label=ba.Lstr(resource='difficultyHardText'),
|
|
button_type='square',
|
|
autoselect=True,
|
|
enable_sound=False,
|
|
on_activate_call=ba.Call(self._set_campaign_difficulty, 'hard'),
|
|
on_select_call=ba.Call(self.sel_change, 'campaign', 'hardButton'),
|
|
color=sel_color_hard
|
|
if self._campaign_difficulty == 'hard'
|
|
else un_sel_color,
|
|
textcolor=sel_textcolor
|
|
if self._campaign_difficulty == 'hard'
|
|
else un_sel_textcolor,
|
|
)
|
|
self._hard_button_lock_image = ba.imagewidget(
|
|
parent=parent_widget,
|
|
size=(30, 30),
|
|
draw_controller=self._hard_button,
|
|
position=(h + 30 - 10, v2 + 32 + 70 - 35),
|
|
texture=lock_tex,
|
|
)
|
|
self._update_hard_mode_lock_image()
|
|
ba.widget(edit=self._hard_button, show_buffer_left=100)
|
|
if self._selected_campaign_level == 'hardButton':
|
|
ba.containerwidget(
|
|
edit=parent_widget,
|
|
selected_child=self._hard_button,
|
|
visible_child=self._hard_button,
|
|
)
|
|
|
|
ba.widget(edit=self._hard_button, down_widget=next_widget_down)
|
|
h_spacing = 200
|
|
campaign_buttons = []
|
|
if self._campaign_difficulty == 'easy':
|
|
campaignname = 'Easy'
|
|
else:
|
|
campaignname = 'Default'
|
|
items = [
|
|
campaignname + ':Onslaught Training',
|
|
campaignname + ':Rookie Onslaught',
|
|
campaignname + ':Rookie Football',
|
|
campaignname + ':Pro Onslaught',
|
|
campaignname + ':Pro Football',
|
|
campaignname + ':Pro Runaround',
|
|
campaignname + ':Uber Onslaught',
|
|
campaignname + ':Uber Football',
|
|
campaignname + ':Uber Runaround',
|
|
]
|
|
items += [campaignname + ':The Last Stand']
|
|
if self._selected_campaign_level is None:
|
|
self._selected_campaign_level = items[0]
|
|
h = 150
|
|
for i in items:
|
|
is_last_sel = i == self._selected_campaign_level
|
|
campaign_buttons.append(
|
|
GameButton(
|
|
self, parent_widget, i, h, v2, is_last_sel, 'campaign'
|
|
).get_button()
|
|
)
|
|
h += h_spacing
|
|
|
|
ba.widget(edit=campaign_buttons[0], left_widget=self._easy_button)
|
|
|
|
if self._back_button is not None:
|
|
ba.widget(edit=self._easy_button, up_widget=self._back_button)
|
|
for btn in campaign_buttons:
|
|
ba.widget(
|
|
edit=btn,
|
|
up_widget=self._back_button,
|
|
down_widget=next_widget_down,
|
|
)
|
|
|
|
# Update our existing percent-complete text.
|
|
campaign = getcampaign(campaignname)
|
|
levels = campaign.levels
|
|
levels_complete = sum((1 if l.complete else 0) for l in levels)
|
|
|
|
# Last level cant be completed; hence the -1.
|
|
progress = min(1.0, float(levels_complete) / (len(levels) - 1))
|
|
p_str = str(int(progress * 100.0)) + '%'
|
|
|
|
self._campaign_percent_text = ba.textwidget(
|
|
edit=self._campaign_percent_text,
|
|
text=ba.Lstr(
|
|
value='${C} (${P})',
|
|
subs=[
|
|
('${C}', ba.Lstr(resource=self._r + '.campaignText')),
|
|
('${P}', p_str),
|
|
],
|
|
),
|
|
)
|
|
|
|
def _on_tournament_info_press(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.confirm import ConfirmWindow
|
|
|
|
txt = ba.Lstr(resource=self._r + '.tournamentInfoText')
|
|
ConfirmWindow(
|
|
txt,
|
|
cancel_button=False,
|
|
width=550,
|
|
height=260,
|
|
origin_widget=self._tournament_info_button,
|
|
)
|
|
|
|
def _refresh(self) -> None:
|
|
# pylint: disable=too-many-statements
|
|
# pylint: disable=too-many-branches
|
|
# pylint: disable=too-many-locals
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.coop.gamebutton import GameButton
|
|
from bastd.ui.coop.tournamentbutton import TournamentButton
|
|
|
|
# (Re)create the sub-container if need be.
|
|
if self._subcontainer is not None:
|
|
self._subcontainer.delete()
|
|
|
|
tourney_row_height = 200
|
|
self._subcontainerheight = (
|
|
620 + self._tournament_button_count * tourney_row_height
|
|
)
|
|
|
|
self._subcontainer = ba.containerwidget(
|
|
parent=self._scrollwidget,
|
|
size=(self._subcontainerwidth, self._subcontainerheight),
|
|
background=False,
|
|
claims_left_right=True,
|
|
claims_tab=True,
|
|
selection_loops_to_parent=True,
|
|
)
|
|
|
|
ba.containerwidget(
|
|
edit=self._root_widget, selected_child=self._scrollwidget
|
|
)
|
|
if self._back_button is not None:
|
|
ba.containerwidget(
|
|
edit=self._root_widget, cancel_button=self._back_button
|
|
)
|
|
|
|
w_parent = self._subcontainer
|
|
h_base = 6
|
|
|
|
v = self._subcontainerheight - 73
|
|
|
|
self._campaign_percent_text = ba.textwidget(
|
|
parent=w_parent,
|
|
position=(h_base + 27, v + 30),
|
|
size=(0, 0),
|
|
text='',
|
|
h_align='left',
|
|
v_align='center',
|
|
color=ba.app.ui.title_color,
|
|
scale=1.1,
|
|
)
|
|
|
|
row_v_show_buffer = 100
|
|
v -= 198
|
|
|
|
h_scroll = ba.hscrollwidget(
|
|
parent=w_parent,
|
|
size=(self._scroll_width - 10, 205),
|
|
position=(-5, v),
|
|
simple_culling_h=70,
|
|
highlight=False,
|
|
border_opacity=0.0,
|
|
color=(0.45, 0.4, 0.5),
|
|
on_select_call=lambda: self._on_row_selected('campaign'),
|
|
)
|
|
self._campaign_h_scroll = h_scroll
|
|
ba.widget(
|
|
edit=h_scroll,
|
|
show_buffer_top=row_v_show_buffer,
|
|
show_buffer_bottom=row_v_show_buffer,
|
|
autoselect=True,
|
|
)
|
|
if self._selected_row == 'campaign':
|
|
ba.containerwidget(
|
|
edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
|
|
)
|
|
ba.containerwidget(edit=h_scroll, claims_left_right=True)
|
|
self._campaign_sub_container = ba.containerwidget(
|
|
parent=h_scroll, size=(180 + 200 * 10, 200), background=False
|
|
)
|
|
|
|
# Tournaments
|
|
|
|
self._tournament_buttons: list[TournamentButton] = []
|
|
|
|
v -= 53
|
|
# FIXME shouldn't use hard-coded strings here.
|
|
txt = ba.Lstr(
|
|
resource='tournamentsText', fallback_resource='tournamentText'
|
|
).evaluate()
|
|
t_width = ba.internal.get_string_width(txt, suppress_warning=True)
|
|
ba.textwidget(
|
|
parent=w_parent,
|
|
position=(h_base + 27, v + 30),
|
|
size=(0, 0),
|
|
text=txt,
|
|
h_align='left',
|
|
v_align='center',
|
|
color=ba.app.ui.title_color,
|
|
scale=1.1,
|
|
)
|
|
self._tournament_info_button = ba.buttonwidget(
|
|
parent=w_parent,
|
|
label='?',
|
|
size=(20, 20),
|
|
text_scale=0.6,
|
|
position=(h_base + 27 + t_width * 1.1 + 15, v + 18),
|
|
button_type='square',
|
|
color=(0.6, 0.5, 0.65),
|
|
textcolor=(0.7, 0.6, 0.75),
|
|
autoselect=True,
|
|
up_widget=self._campaign_h_scroll,
|
|
on_activate_call=self._on_tournament_info_press,
|
|
)
|
|
ba.widget(
|
|
edit=self._tournament_info_button,
|
|
left_widget=self._tournament_info_button,
|
|
right_widget=self._tournament_info_button,
|
|
)
|
|
|
|
# Say 'unavailable' if there are zero tournaments, and if we're not
|
|
# signed in add that as well (that's probably why we see
|
|
# no tournaments).
|
|
if self._tournament_button_count == 0:
|
|
unavailable_text = ba.Lstr(resource='unavailableText')
|
|
if ba.internal.get_v1_account_state() != 'signed_in':
|
|
unavailable_text = ba.Lstr(
|
|
value='${A} (${B})',
|
|
subs=[
|
|
('${A}', unavailable_text),
|
|
('${B}', ba.Lstr(resource='notSignedInText')),
|
|
],
|
|
)
|
|
ba.textwidget(
|
|
parent=w_parent,
|
|
position=(h_base + 47, v),
|
|
size=(0, 0),
|
|
text=unavailable_text,
|
|
h_align='left',
|
|
v_align='center',
|
|
color=ba.app.ui.title_color,
|
|
scale=0.9,
|
|
)
|
|
v -= 40
|
|
v -= 198
|
|
|
|
tournament_h_scroll = None
|
|
if self._tournament_button_count > 0:
|
|
for i in range(self._tournament_button_count):
|
|
tournament_h_scroll = h_scroll = ba.hscrollwidget(
|
|
parent=w_parent,
|
|
size=(self._scroll_width - 10, 205),
|
|
position=(-5, v),
|
|
highlight=False,
|
|
border_opacity=0.0,
|
|
color=(0.45, 0.4, 0.5),
|
|
on_select_call=ba.Call(
|
|
self._on_row_selected, 'tournament' + str(i + 1)
|
|
),
|
|
)
|
|
ba.widget(
|
|
edit=h_scroll,
|
|
show_buffer_top=row_v_show_buffer,
|
|
show_buffer_bottom=row_v_show_buffer,
|
|
autoselect=True,
|
|
)
|
|
if self._selected_row == 'tournament' + str(i + 1):
|
|
ba.containerwidget(
|
|
edit=w_parent,
|
|
selected_child=h_scroll,
|
|
visible_child=h_scroll,
|
|
)
|
|
ba.containerwidget(edit=h_scroll, claims_left_right=True)
|
|
sc2 = ba.containerwidget(
|
|
parent=h_scroll,
|
|
size=(self._scroll_width - 24, 200),
|
|
background=False,
|
|
)
|
|
h = 0
|
|
v2 = -2
|
|
is_last_sel = True
|
|
self._tournament_buttons.append(
|
|
TournamentButton(
|
|
sc2,
|
|
h,
|
|
v2,
|
|
is_last_sel,
|
|
on_pressed=ba.WeakCall(self.run_tournament),
|
|
)
|
|
)
|
|
v -= 200
|
|
|
|
# Custom Games. (called 'Practice' in UI these days).
|
|
v -= 50
|
|
ba.textwidget(
|
|
parent=w_parent,
|
|
position=(h_base + 27, v + 30 + 198),
|
|
size=(0, 0),
|
|
text=ba.Lstr(
|
|
resource='practiceText',
|
|
fallback_resource='coopSelectWindow.customText',
|
|
),
|
|
h_align='left',
|
|
v_align='center',
|
|
color=ba.app.ui.title_color,
|
|
scale=1.1,
|
|
)
|
|
|
|
items = [
|
|
'Challenges:Infinite Onslaught',
|
|
'Challenges:Infinite Runaround',
|
|
'Challenges:Ninja Fight',
|
|
'Challenges:Pro Ninja Fight',
|
|
'Challenges:Meteor Shower',
|
|
'Challenges:Target Practice B',
|
|
'Challenges:Target Practice',
|
|
]
|
|
|
|
# Show easter-egg-hunt either if its easter or we own it.
|
|
if ba.internal.get_v1_account_misc_read_val(
|
|
'easter', False
|
|
) or ba.internal.get_purchased('games.easter_egg_hunt'):
|
|
items = [
|
|
'Challenges:Easter Egg Hunt',
|
|
'Challenges:Pro Easter Egg Hunt',
|
|
] + items
|
|
|
|
# If we've defined custom games, put them at the beginning.
|
|
if ba.app.custom_coop_practice_games:
|
|
items = ba.app.custom_coop_practice_games + items
|
|
|
|
self._custom_h_scroll = custom_h_scroll = h_scroll = ba.hscrollwidget(
|
|
parent=w_parent,
|
|
size=(self._scroll_width - 10, 205),
|
|
position=(-5, v),
|
|
highlight=False,
|
|
border_opacity=0.0,
|
|
color=(0.45, 0.4, 0.5),
|
|
on_select_call=ba.Call(self._on_row_selected, 'custom'),
|
|
)
|
|
ba.widget(
|
|
edit=h_scroll,
|
|
show_buffer_top=row_v_show_buffer,
|
|
show_buffer_bottom=1.5 * row_v_show_buffer,
|
|
autoselect=True,
|
|
)
|
|
if self._selected_row == 'custom':
|
|
ba.containerwidget(
|
|
edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
|
|
)
|
|
ba.containerwidget(edit=h_scroll, claims_left_right=True)
|
|
sc2 = ba.containerwidget(
|
|
parent=h_scroll,
|
|
size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200),
|
|
background=False,
|
|
)
|
|
h_spacing = 200
|
|
self._custom_buttons: list[GameButton] = []
|
|
h = 0
|
|
v2 = -2
|
|
for item in items:
|
|
is_last_sel = item == self._selected_custom_level
|
|
self._custom_buttons.append(
|
|
GameButton(self, sc2, item, h, v2, is_last_sel, 'custom')
|
|
)
|
|
h += h_spacing
|
|
|
|
# We can't fill in our campaign row until tourney buttons are in place.
|
|
# (for wiring up)
|
|
self._refresh_campaign_row()
|
|
|
|
for i, tbutton in enumerate(self._tournament_buttons):
|
|
ba.widget(
|
|
edit=tbutton.button,
|
|
up_widget=self._tournament_info_button
|
|
if i == 0
|
|
else self._tournament_buttons[i - 1].button,
|
|
down_widget=self._tournament_buttons[(i + 1)].button
|
|
if i + 1 < len(self._tournament_buttons)
|
|
else custom_h_scroll,
|
|
)
|
|
ba.widget(
|
|
edit=tbutton.more_scores_button,
|
|
down_widget=self._tournament_buttons[
|
|
(i + 1)
|
|
].current_leader_name_text
|
|
if i + 1 < len(self._tournament_buttons)
|
|
else custom_h_scroll,
|
|
)
|
|
ba.widget(
|
|
edit=tbutton.current_leader_name_text,
|
|
up_widget=self._tournament_info_button
|
|
if i == 0
|
|
else self._tournament_buttons[i - 1].more_scores_button,
|
|
)
|
|
|
|
for btn in self._custom_buttons:
|
|
try:
|
|
ba.widget(
|
|
edit=btn.get_button(),
|
|
up_widget=tournament_h_scroll
|
|
if self._tournament_buttons
|
|
else self._tournament_info_button,
|
|
)
|
|
except Exception:
|
|
ba.print_exception('Error wiring up custom buttons.')
|
|
|
|
if self._back_button is not None:
|
|
ba.buttonwidget(edit=self._back_button, on_activate_call=self._back)
|
|
else:
|
|
ba.containerwidget(
|
|
edit=self._root_widget, on_cancel_call=self._back
|
|
)
|
|
|
|
# There's probably several 'onSelected' callbacks pushed onto the
|
|
# event queue.. we need to push ours too so we're enabled *after* them.
|
|
ba.pushcall(self._enable_selectable_callback)
|
|
|
|
def _on_row_selected(self, row: str) -> None:
|
|
if self._do_selection_callbacks:
|
|
if self._selected_row != row:
|
|
self._selected_row = row
|
|
|
|
def _enable_selectable_callback(self) -> None:
|
|
self._do_selection_callbacks = True
|
|
|
|
def _switch_to_league_rankings(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.account import show_sign_in_prompt
|
|
from bastd.ui.league.rankwindow import LeagueRankWindow
|
|
|
|
if ba.internal.get_v1_account_state() != 'signed_in':
|
|
show_sign_in_prompt()
|
|
return
|
|
self._save_state()
|
|
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
|
assert self._league_rank_button is not None
|
|
ba.app.ui.set_main_menu_window(
|
|
LeagueRankWindow(
|
|
origin_widget=self._league_rank_button.get_button()
|
|
).get_root_widget()
|
|
)
|
|
|
|
def _switch_to_score(
|
|
self,
|
|
show_tab: StoreBrowserWindow.TabID
|
|
| None = StoreBrowserWindow.TabID.EXTRAS,
|
|
) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.account import show_sign_in_prompt
|
|
|
|
if ba.internal.get_v1_account_state() != 'signed_in':
|
|
show_sign_in_prompt()
|
|
return
|
|
self._save_state()
|
|
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
|
assert self._store_button is not None
|
|
ba.app.ui.set_main_menu_window(
|
|
StoreBrowserWindow(
|
|
origin_widget=self._store_button.get_button(),
|
|
show_tab=show_tab,
|
|
back_location='CoopBrowserWindow',
|
|
).get_root_widget()
|
|
)
|
|
|
|
def is_tourney_data_up_to_date(self) -> bool:
|
|
"""Return whether our tourney data is up to date."""
|
|
return self._tourney_data_up_to_date
|
|
|
|
def run_game(self, game: str) -> None:
|
|
"""Run the provided game."""
|
|
# pylint: disable=too-many-branches
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.confirm import ConfirmWindow
|
|
from bastd.ui.purchase import PurchaseWindow
|
|
from bastd.ui.account import show_sign_in_prompt
|
|
|
|
args: dict[str, Any] = {}
|
|
|
|
if game == 'Easy:The Last Stand':
|
|
ConfirmWindow(
|
|
ba.Lstr(
|
|
resource='difficultyHardUnlockOnlyText',
|
|
fallback_resource='difficultyHardOnlyText',
|
|
),
|
|
cancel_button=False,
|
|
width=460,
|
|
height=130,
|
|
)
|
|
return
|
|
|
|
# Infinite onslaught/runaround require pro; bring up a store link
|
|
# if need be.
|
|
if (
|
|
game
|
|
in (
|
|
'Challenges:Infinite Runaround',
|
|
'Challenges:Infinite Onslaught',
|
|
)
|
|
and not ba.app.accounts_v1.have_pro()
|
|
):
|
|
if ba.internal.get_v1_account_state() != 'signed_in':
|
|
show_sign_in_prompt()
|
|
else:
|
|
PurchaseWindow(items=['pro'])
|
|
return
|
|
|
|
required_purchase: str | None
|
|
if game in ['Challenges:Meteor Shower']:
|
|
required_purchase = 'games.meteor_shower'
|
|
elif game in [
|
|
'Challenges:Target Practice',
|
|
'Challenges:Target Practice B',
|
|
]:
|
|
required_purchase = 'games.target_practice'
|
|
elif game in ['Challenges:Ninja Fight']:
|
|
required_purchase = 'games.ninja_fight'
|
|
elif game in ['Challenges:Pro Ninja Fight']:
|
|
required_purchase = 'games.ninja_fight'
|
|
elif game in [
|
|
'Challenges:Easter Egg Hunt',
|
|
'Challenges:Pro Easter Egg Hunt',
|
|
]:
|
|
required_purchase = 'games.easter_egg_hunt'
|
|
else:
|
|
required_purchase = None
|
|
|
|
if required_purchase is not None and not ba.internal.get_purchased(
|
|
required_purchase
|
|
):
|
|
if ba.internal.get_v1_account_state() != 'signed_in':
|
|
show_sign_in_prompt()
|
|
else:
|
|
PurchaseWindow(items=[required_purchase])
|
|
return
|
|
|
|
self._save_state()
|
|
|
|
if ba.app.launch_coop_game(game, args=args):
|
|
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
|
|
|
def run_tournament(self, tournament_button: TournamentButton) -> None:
|
|
"""Run the provided tournament game."""
|
|
from bastd.ui.account import show_sign_in_prompt
|
|
from bastd.ui.tournamententry import TournamentEntryWindow
|
|
|
|
if ba.internal.get_v1_account_state() != 'signed_in':
|
|
show_sign_in_prompt()
|
|
return
|
|
|
|
if ba.internal.workspaces_in_use():
|
|
ba.screenmessage(
|
|
ba.Lstr(resource='tournamentsDisabledWorkspaceText'),
|
|
color=(1, 0, 0),
|
|
)
|
|
ba.playsound(ba.getsound('error'))
|
|
return
|
|
|
|
if not self._tourney_data_up_to_date:
|
|
ba.screenmessage(
|
|
ba.Lstr(resource='tournamentCheckingStateText'), color=(1, 1, 0)
|
|
)
|
|
ba.playsound(ba.getsound('error'))
|
|
return
|
|
|
|
if tournament_button.tournament_id is None:
|
|
ba.screenmessage(
|
|
ba.Lstr(resource='internal.unavailableNoConnectionText'),
|
|
color=(1, 0, 0),
|
|
)
|
|
ba.playsound(ba.getsound('error'))
|
|
return
|
|
|
|
if tournament_button.required_league is not None:
|
|
ba.screenmessage(
|
|
ba.Lstr(
|
|
resource='league.tournamentLeagueText',
|
|
subs=[
|
|
(
|
|
'${NAME}',
|
|
ba.Lstr(
|
|
translate=(
|
|
'leagueNames',
|
|
tournament_button.required_league,
|
|
)
|
|
),
|
|
)
|
|
],
|
|
),
|
|
color=(1, 0, 0),
|
|
)
|
|
ba.playsound(ba.getsound('error'))
|
|
return
|
|
|
|
if tournament_button.time_remaining <= 0:
|
|
ba.screenmessage(
|
|
ba.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
|
|
)
|
|
ba.playsound(ba.getsound('error'))
|
|
return
|
|
|
|
self._save_state()
|
|
|
|
assert tournament_button.tournament_id is not None
|
|
TournamentEntryWindow(
|
|
tournament_id=tournament_button.tournament_id,
|
|
position=tournament_button.button.get_screen_space_center(),
|
|
)
|
|
|
|
def _back(self) -> None:
|
|
# pylint: disable=cyclic-import
|
|
from bastd.ui.play import PlayWindow
|
|
|
|
# If something is selected, store it.
|
|
self._save_state()
|
|
ba.containerwidget(
|
|
edit=self._root_widget, transition=self._transition_out
|
|
)
|
|
ba.app.ui.set_main_menu_window(
|
|
PlayWindow(transition='in_left').get_root_widget()
|
|
)
|
|
|
|
def _save_state(self) -> None:
|
|
cfg = ba.app.config
|
|
try:
|
|
sel = self._root_widget.get_selected_child()
|
|
if sel == self._back_button:
|
|
sel_name = 'Back'
|
|
elif sel == self._store_button_widget:
|
|
sel_name = 'Store'
|
|
elif sel == self._league_rank_button_widget:
|
|
sel_name = 'PowerRanking'
|
|
elif sel == self._scrollwidget:
|
|
sel_name = 'Scroll'
|
|
else:
|
|
raise ValueError('unrecognized selection')
|
|
ba.app.ui.window_states[type(self)] = {'sel_name': sel_name}
|
|
except Exception:
|
|
ba.print_exception(f'Error saving state for {self}.')
|
|
|
|
cfg['Selected Coop Row'] = self._selected_row
|
|
cfg['Selected Coop Custom Level'] = self._selected_custom_level
|
|
cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
|
|
cfg.commit()
|
|
|
|
def _restore_state(self) -> None:
|
|
try:
|
|
sel_name = ba.app.ui.window_states.get(type(self), {}).get(
|
|
'sel_name'
|
|
)
|
|
if sel_name == 'Back':
|
|
sel = self._back_button
|
|
elif sel_name == 'Scroll':
|
|
sel = self._scrollwidget
|
|
elif sel_name == 'PowerRanking':
|
|
sel = self._league_rank_button_widget
|
|
elif sel_name == 'Store':
|
|
sel = self._store_button_widget
|
|
else:
|
|
sel = self._scrollwidget
|
|
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
|
except Exception:
|
|
ba.print_exception(f'Error restoring state for {self}.')
|
|
|
|
def sel_change(self, row: str, game: str) -> None:
|
|
"""(internal)"""
|
|
if self._do_selection_callbacks:
|
|
if row == 'custom':
|
|
self._selected_custom_level = game
|
|
elif row == 'campaign':
|
|
self._selected_campaign_level = game
|