Tourneys now require their maps/game types to be unlocked

This commit is contained in:
Eric Froemling 2025-01-18 14:22:56 -08:00
parent 091b369942
commit 6c8611a02b
No known key found for this signature in database
9 changed files with 215 additions and 179 deletions

48
.efrocachemap generated
View File

@ -4174,22 +4174,22 @@
"build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1",
"build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718",
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "31209ca509f46fde3450eb8b7a39a520",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "03ec26492ef7dc28370cbb7f9902b0b9",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "9331d3a163409aa08b5b1e681d923231",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "7aeb21b72648fa81d27d18251fb60f68",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "cecba50ac68398a3c4f8ddbaa9211d48",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "683ff26a659c420a3738b5c58e17b37e",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "5e2f2dc71bd451900d7ece96e098dc00",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "4745da922028681b1b5cc63a584030b3",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "a6527c54deee04b51029b1023a0b27fd",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "c6ceb3702ea22a2e187145a86bd51a13",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "4ee039201fdaa4c9a5afbefd5a31c064",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "15f71fc2ee8e71c657c159edceaec719",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "1a56770399e5f6c91c25be77a68f38da",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "bcb3448df41f7a7e91f2823e55253a57",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "6805beb9c0d1f967fe6a4344b5735aad",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b15bc3743c8021d1069568071e734a13",
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "ce8f860eca1987d08245184f86996182",
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "50d39660fa219c8d6cf2efb61685a61a",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "6ba96618dd57e6937abe9d1919dfa572",
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "cecc7cee479a3d34f9b2fc9e9d9ded23",
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "e62e0586da50386f4f363ec018a5db5e",
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "60c86684724816e72fe123a4181e9b5e",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "652e6dad52ffa1c7166ffad2da52e5cd",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "7a54b3311d70edc58f6f2953df088e92",
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "2e43eceb3930606383de961285d539d5",
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "02967dc5ba24c1b85ff0626934c3e1ae",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "e93d6a9a363e7b769c1b4504bd24e34d",
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "f65ae7e22fba795c6368fd4d652eeff1",
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5c6b347ac1a4f2a9cd0e253520355e66",
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "5ee86d89f53f1da49e9f35f6bb510ec5",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "eb531ec851c84295fad13f48d420a673",
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "9d55662daa82013b697d0715002fd950",
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "cbacac5a846cf8a0f6db760aaddcd13a",
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "3d16bac10d8f15bac7fe20e3a927b275",
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "cbacac5a846cf8a0f6db760aaddcd13a",
@ -4202,14 +4202,14 @@
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "7dd182733a34da0ca5f5c97e5cb0b7f0",
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "6121591b94d920ee541194b65d93958a",
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "7dd182733a34da0ca5f5c97e5cb0b7f0",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "c512e7bacb6c8654b517a38604e0ccf2",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "21b911b0ee5e354d25e8acf740140188",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "6313af7210edecf7e9cf026550bbb161",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "dc7d359250c39d0e636da2bf7d859deb",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "4c4c93e32beb69743b63f963748e1136",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "70f5185b13ab6e799dbfc9bdd0a9e997",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "f6adec2b792bc36c2cea2b395e827471",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "e674baf0049c634e6342cae5a266b567",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "3aae8d5bb14498fa8c38a90f92e6eb61",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "8ca4eb60a623835aedded88a2c43d786",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "4d69eddf79b7e53a354b0148ba3af821",
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "2d283c2d49f668594e858c33831354e8",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "bd3a90924b3dd122143cc99b9d054eaf",
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "4a9d7d32618519f04c4cc7b1cbdfb028",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "4ff97a62a991a2af73bfcf23faeac8fe",
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "dbf2ed253e622c90939ce31cec3f04e8",
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
"src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad",
"src/ballistica/base/mgen/pyembed/binding_base.inc": "06042d31df0ff9af96b99477162e2a91",

View File

@ -1,4 +1,4 @@
### 1.7.37 (build 22202, api 9, 2025-01-18)
### 1.7.37 (build 22204, api 9, 2025-01-18)
- Bumping api version to 9. As you'll see below, there's some UI changes that
will require a bit of work for any UI mods to adapt to. If your mods don't
touch UI stuff at all you can simply bump your api version and call it a day.

View File

@ -906,3 +906,67 @@ class ClassicAppSubsystem(babase.AppSubsystem):
assert_never(label)
return babase.Lstr(resource=rsrc)
def required_purchase_for_game(self, game: str) -> str | None:
"""Return which purchase (if any) is required for a game."""
# pylint: disable=too-many-return-statements
if game in (
'Challenges:Infinite Runaround',
'Challenges:Tournament Infinite Runaround',
):
# Special case: Pro used to unlock this.
return (
None
if self.accounts.have_pro()
else 'upgrades.infinite_runaround'
)
if game in (
'Challenges:Infinite Onslaught',
'Challenges:Tournament Infinite Onslaught',
):
# Special case: Pro used to unlock this.
return (
None
if self.accounts.have_pro()
else 'upgrades.infinite_onslaught'
)
if game in (
'Challenges:Meteor Shower',
'Challenges:Epic Meteor Shower',
):
return 'games.meteor_shower'
if game in (
'Challenges:Target Practice',
'Challenges:Target Practice B',
):
return 'games.target_practice'
if game in (
'Challenges:Ninja Fight',
'Challenges:Pro Ninja Fight',
):
return 'games.ninja_fight'
if game in ('Challenges:Lake Frigid Race',):
return 'maps.lake_frigid'
if game in (
'Challenges:Easter Egg Hunt',
'Challenges:Pro Easter Egg Hunt',
):
return 'games.easter_egg_hunt'
return None
def is_game_unlocked(self, game: str) -> bool:
"""Is a particular game unlocked?"""
plus = babase.app.plus
assert plus is not None
purchase = self.required_purchase_for_game(game)
if purchase is None:
return True
return plus.get_v1_account_product_purchased(purchase)

View File

@ -53,7 +53,7 @@ if TYPE_CHECKING:
# Build number and version of the ballistica binary we expect to be
# using.
TARGET_BALLISTICA_BUILD = 22202
TARGET_BALLISTICA_BUILD = 22204
TARGET_BALLISTICA_VERSION = '1.7.37'

View File

@ -326,8 +326,10 @@ class CoopBrowserWindow(bui.MainWindow):
):
self._tourney_data_up_to_date = False
# If our account state has changed, do a full request.
# If our account login state has changed, do a
# full request.
account_state_num = plus.get_v1_account_state_num()
if account_state_num != self._account_state_num:
self._account_state_num = account_state_num
self._save_state()
@ -405,6 +407,7 @@ class CoopBrowserWindow(bui.MainWindow):
logging.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 (
@ -985,9 +988,10 @@ class CoopBrowserWindow(bui.MainWindow):
"""Return whether our tourney data is up to date."""
return self._tourney_data_up_to_date
def run_game(self, game: str) -> None:
def run_game(
self, game: str, origin_widget: bui.Widget | None = None
) -> None:
"""Run the provided game."""
# pylint: disable=too-many-branches
# pylint: disable=cyclic-import
from bauiv1lib.confirm import ConfirmWindow
from bauiv1lib.purchase import PurchaseWindow
@ -1012,38 +1016,7 @@ class CoopBrowserWindow(bui.MainWindow):
)
return
required_purchase: str | None
# Infinite onslaught requires pro or the newer standalone
# upgrade.
if (
game in ['Challenges:Infinite Runaround']
and not bui.app.classic.accounts.have_pro()
):
required_purchase = 'upgrades.infinite_runaround'
elif (
game in ['Challenges:Infinite Onslaught']
and not bui.app.classic.accounts.have_pro()
):
required_purchase = 'upgrades.infinite_onslaught'
elif 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
required_purchase = bui.app.classic.required_purchase_for_game(game)
if (
required_purchase is not None
@ -1052,7 +1025,9 @@ class CoopBrowserWindow(bui.MainWindow):
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
else:
PurchaseWindow(items=[required_purchase])
PurchaseWindow(
items=[required_purchase], origin_widget=origin_widget
)
return
self._save_state()
@ -1062,12 +1037,18 @@ class CoopBrowserWindow(bui.MainWindow):
def run_tournament(self, tournament_button: TournamentButton) -> None:
"""Run the provided tournament game."""
# pylint: disable=too-many-return-statements
from bauiv1lib.purchase import PurchaseWindow
from bauiv1lib.account.signin import show_sign_in_prompt
from bauiv1lib.tournamententry import TournamentEntryWindow
plus = bui.app.plus
assert plus is not None
classic = bui.app.classic
assert classic is not None
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
@ -1117,6 +1098,22 @@ class CoopBrowserWindow(bui.MainWindow):
bui.getsound('error').play()
return
if tournament_button.game is not None and not classic.is_game_unlocked(
tournament_button.game
):
required_purchase = classic.required_purchase_for_game(
tournament_button.game
)
assert required_purchase is not None
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
else:
PurchaseWindow(
items=[required_purchase],
origin_widget=tournament_button.button,
)
return
if tournament_button.time_remaining <= 0:
bui.screenmessage(
bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
@ -1138,10 +1135,6 @@ class CoopBrowserWindow(bui.MainWindow):
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:

View File

@ -5,6 +5,7 @@
from __future__ import annotations
import random
import weakref
from typing import TYPE_CHECKING
import bauiv1 as bui
@ -59,12 +60,15 @@ class GameButton:
else:
stars = 1
self._window = weakref.ref(window)
self._game = game
self._button = btn = bui.buttonwidget(
parent=parent,
position=(x + 23, y + 4),
size=(sclx, scly),
label='',
on_activate_call=bui.Call(window.run_game, game),
on_activate_call=bui.WeakCall(self._on_press),
button_type='square',
autoselect=True,
on_select_call=bui.Call(window.sel_change, row, game),
@ -187,12 +191,16 @@ class GameButton:
)
self._update()
def _on_press(self) -> None:
window = self._window()
if window is not None:
window.run_game(self._game, origin_widget=self._button)
def get_button(self) -> bui.Widget:
"""Return the underlying button bui.Widget."""
return self._button
def _update(self) -> None:
# pylint: disable=too-many-boolean-expressions
plus = bui.app.plus
assert plus is not None
@ -232,64 +240,7 @@ class GameButton:
# Hard-code games we haven't unlocked.
assert bui.app.classic is not None
if (
(
game in ('Challenges:Infinite Runaround',)
and not (
bui.app.classic.accounts.have_pro()
or plus.get_v1_account_product_purchased(
'upgrades.infinite_runaround'
)
)
)
or (
game in ('Challenges:Infinite Onslaught',)
and not (
bui.app.classic.accounts.have_pro()
or plus.get_v1_account_product_purchased(
'upgrades.infinite_onslaught'
)
)
)
or (
game in ('Challenges:Meteor Shower',)
and not plus.get_v1_account_product_purchased(
'games.meteor_shower'
)
)
or (
game
in (
'Challenges:Target Practice',
'Challenges:Target Practice B',
)
and not plus.get_v1_account_product_purchased(
'games.target_practice'
)
)
or (
game in ('Challenges:Ninja Fight',)
and not plus.get_v1_account_product_purchased(
'games.ninja_fight'
)
)
or (
game in ('Challenges:Pro Ninja Fight',)
and not plus.get_v1_account_product_purchased(
'games.ninja_fight'
)
)
or (
game
in (
'Challenges:Easter Egg Hunt',
'Challenges:Pro Easter Egg Hunt',
)
and not plus.get_v1_account_product_purchased(
'games.easter_egg_hunt'
)
)
):
if not bui.app.classic.is_game_unlocked(game):
unlocked = False
# Let's tint levels a slightly different color when easy mode

View File

@ -39,6 +39,7 @@ class TournamentButton:
self.lsbo = bui.getmesh('level_select_button_opaque')
self.allow_ads = False
self.tournament_id: str | None = None
self.game: str | None = None
self.time_remaining: int = 0
self.has_time_remaining: bool = False
self.leader: Any = None
@ -400,6 +401,9 @@ class TournamentButton:
color=(0.4, 0.4, 0.5),
flatness=1.0,
)
self._lock_update_timer = bui.AppTimer(
1.03, bui.WeakCall(self._update_lock_state), repeat=True
)
def _pressed(self) -> None:
self.on_pressed(self)
@ -440,6 +444,33 @@ class TournamentButton:
position=self.more_scores_button.get_screen_space_center(),
)
def _update_lock_state(self) -> None:
if self.game is None:
return
assert bui.app.classic is not None
campaignname, levelname = self.game.split(':')
campaign = bui.app.classic.getcampaign(campaignname)
enabled = (
self.required_league is None
and bui.app.classic.is_game_unlocked(self.game)
)
bui.buttonwidget(
edit=self.button,
color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5),
)
bui.imagewidget(edit=self.lock_image, opacity=0.0 if enabled else 1.0)
bui.imagewidget(
edit=self.image,
texture=bui.gettexture(
campaign.getlevel(levelname).preview_texture_name
),
opacity=1.0 if enabled else 0.5,
)
def update_for_data(self, entry: dict[str, Any]) -> None:
"""Update for new incoming data."""
# pylint: disable=too-many-statements
@ -470,12 +501,22 @@ class TournamentButton:
entry, include_tickets=False
)
enabled = 'requiredLeague' not in entry
bui.buttonwidget(
edit=self.button,
color=(0.5, 0.7, 0.2) if enabled else (0.5, 0.5, 0.5),
)
bui.imagewidget(edit=self.lock_image, opacity=0.0 if enabled else 1.0)
self.time_remaining = entry['timeRemaining']
self.has_time_remaining = entry is not None
self.tournament_id = entry['tournamentID']
self.required_league = entry.get('requiredLeague')
assert bui.app.classic is not None
self.game = bui.app.classic.accounts.tournament_info[
self.tournament_id
]['game']
assert isinstance(self.game, str)
campaignname, levelname = self.game.split(':')
campaign = bui.app.classic.getcampaign(campaignname)
self._update_lock_state()
bui.textwidget(
edit=self.prize_range_1_text,
text='-' if pr1 == '' else pr1,
@ -604,51 +645,30 @@ class TournamentButton:
edit=self.time_remaining_out_of_text, text=out_of_time_text
)
self.time_remaining = entry['timeRemaining']
self.has_time_remaining = entry is not None
self.tournament_id = entry['tournamentID']
self.required_league = (
None if 'requiredLeague' not in entry else entry['requiredLeague']
)
# if self.game is None:
# bui.textwidget(edit=self.button_text, text='-')
# bui.imagewidget(
# edit=self.image, texture=bui.gettexture('black'), opacity=0.2
# )
# else:
max_players = bui.app.classic.accounts.tournament_info[
self.tournament_id
]['maxPlayers']
assert bui.app.classic is not None
game = bui.app.classic.accounts.tournament_info[self.tournament_id][
'game'
]
if game is None:
bui.textwidget(edit=self.button_text, text='-')
bui.imagewidget(
edit=self.image, texture=bui.gettexture('black'), opacity=0.2
)
else:
campaignname, levelname = game.split(':')
campaign = bui.app.classic.getcampaign(campaignname)
max_players = bui.app.classic.accounts.tournament_info[
self.tournament_id
]['maxPlayers']
txt = bui.Lstr(
value='${A} ${B}',
subs=[
('${A}', campaign.getlevel(levelname).displayname),
(
'${B}',
bui.Lstr(
resource='playerCountAbbreviatedText',
subs=[('${COUNT}', str(max_players))],
),
txt = bui.Lstr(
value='${A} ${B}',
subs=[
('${A}', campaign.getlevel(levelname).displayname),
(
'${B}',
bui.Lstr(
resource='playerCountAbbreviatedText',
subs=[('${COUNT}', str(max_players))],
),
],
)
bui.textwidget(edit=self.button_text, text=txt)
bui.imagewidget(
edit=self.image,
texture=bui.gettexture(
campaign.getlevel(levelname).preview_texture_name
),
opacity=1.0 if enabled else 0.5,
)
],
)
bui.textwidget(edit=self.button_text, text=txt)
fee = entry['fee']
assert isinstance(fee, int | None)

View File

@ -18,7 +18,7 @@ class PurchaseWindow(bui.Window):
def __init__(
self,
items: list[str],
transition: str = 'in_right',
origin_widget: bui.Widget | None = None,
header_text: bui.Lstr | None = None,
):
from bauiv1lib.store.item import instantiate_store_item_display
@ -40,16 +40,24 @@ class PurchaseWindow(bui.Window):
self._width = 580
self._height = 520
uiscale = bui.app.ui_v1.uiscale
if origin_widget is not None:
scale_origin = origin_widget.get_screen_space_center()
else:
scale_origin = None
super().__init__(
root_widget=bui.containerwidget(
parent=bui.get_special_widget('overlay_stack'),
size=(self._width, self._height),
transition=transition,
transition='in_scale',
toolbar_visibility='menu_store',
scale=(
1.2
if uiscale is bui.UIScale.SMALL
else 1.1 if uiscale is bui.UIScale.MEDIUM else 1.0
),
scale_origin_stack_offset=scale_origin,
stack_offset=(
(0, -15) if uiscale is bui.UIScale.SMALL else (0, 0)
),
@ -159,7 +167,7 @@ class PurchaseWindow(bui.Window):
can_die = True
if can_die:
bui.containerwidget(edit=self._root_widget, transition='out_left')
bui.containerwidget(edit=self._root_widget, transition='out_scale')
def _purchase(self) -> None:
@ -192,4 +200,4 @@ class PurchaseWindow(bui.Window):
do_it()
def _cancel(self) -> None:
bui.containerwidget(edit=self._root_widget, transition='out_right')
bui.containerwidget(edit=self._root_widget, transition='out_scale')

View File

@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kEngineBuildNumber = 22202;
const int kEngineBuildNumber = 22204;
const char* kEngineVersion = "1.7.37";
const int kEngineApiVersion = 9;