remving all files

This commit is contained in:
Ayush Saini 2026-02-16 18:22:33 +05:30
parent e02f1dbe52
commit 7672d57823
20 changed files with 0 additions and 6151 deletions

View file

@ -1,227 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to ads."""
from __future__ import annotations
import time
import asyncio
import logging
from typing import TYPE_CHECKING
import babase
import bascenev1
if TYPE_CHECKING:
from typing import Callable, Any
class AdsSubsystem:
"""Subsystem for ads functionality in the app.
Access the single shared instance of this class at 'ba.app.ads'.
"""
def __init__(self) -> None:
self.last_ad_network = 'unknown'
self.last_ad_network_set_time = time.time()
self.ad_amt: float | None = None
self.last_ad_purpose = 'invalid'
self.attempted_first_ad = False
self.last_in_game_ad_remove_message_show_time: float | None = None
self.last_ad_completion_time: float | None = None
self.last_ad_was_short = False
self._fallback_task: asyncio.Task | None = None
def do_remove_in_game_ads_message(self) -> None:
"""(internal)"""
# Print this message once every 10 minutes at most.
tval = babase.apptime()
if self.last_in_game_ad_remove_message_show_time is None or (
tval - self.last_in_game_ad_remove_message_show_time > 60 * 10
):
self.last_in_game_ad_remove_message_show_time = tval
with babase.ContextRef.empty():
babase.apptimer(
1.0,
lambda: babase.screenmessage(
babase.Lstr(
resource='removeInGameAdsTokenPurchaseText'
),
color=(1, 1, 0),
),
)
def show_ad(
self, purpose: str, on_completion_call: Callable[[], Any] | None = None
) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
assert babase.app.plus is not None
babase.app.plus.show_ad(purpose, on_completion_call)
def show_ad_2(
self,
purpose: str,
on_completion_call: Callable[[bool], Any] | None = None,
) -> None:
"""(internal)"""
self.last_ad_purpose = purpose
assert babase.app.plus is not None
babase.app.plus.show_ad_2(purpose, on_completion_call)
def call_after_ad(self, call: Callable[[], Any]) -> None:
"""Run a call after potentially showing an ad."""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
app = babase.app
plus = app.plus
classic = app.classic
assert plus is not None
assert classic is not None
show = True
# No ads without net-connections, etc.
if not plus.can_show_ad():
show = False
# Pro or other upgrades disable interstitials.
if (
classic.accounts.have_pro()
or classic.gold_pass
or classic.remove_ads
):
show = False
try:
session = bascenev1.get_foreground_host_session()
assert session is not None
is_tournament = session.tournament_id is not None
except Exception:
is_tournament = False
if is_tournament:
show = False # Never show ads during tournaments.
if show:
interval: float | None
launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently.
interval_mult = (
plus.get_v1_account_misc_read_val('ads.shortIntervalMult', 1.0)
if self.last_ad_was_short
else 1.0
)
if self.ad_amt is None:
if launch_count <= 1:
self.ad_amt = plus.get_v1_account_misc_read_val(
'ads.startVal1', 0.99
)
else:
self.ad_amt = plus.get_v1_account_misc_read_val(
'ads.startVal2', 1.0
)
interval = None
else:
# So far we're cleared to show; now calc our
# ad-show-threshold and see if we should *actually* show
# (we reach our threshold faster the longer we've been
# playing).
base = 'ads' if plus.has_video_ads() else 'ads2'
min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0)
max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = plus.get_v1_account_misc_read_val(
base + '.minLCScale', 0.25
)
max_lc_scale = plus.get_v1_account_misc_read_val(
base + '.maxLCScale', 0.34
)
min_lc_interval = plus.get_v1_account_misc_read_val(
base + '.minLCInterval', 360
)
max_lc_interval = plus.get_v1_account_misc_read_val(
base + '.maxLCInterval', 300
)
if launch_count < min_lc:
lc_amt = 0.0
elif launch_count > max_lc:
lc_amt = 1.0
else:
lc_amt = (float(launch_count) - min_lc) / (max_lc - min_lc)
incr = (1.0 - lc_amt) * min_lc_scale + lc_amt * max_lc_scale
interval = (
1.0 - lc_amt
) * min_lc_interval + lc_amt * max_lc_interval
self.ad_amt += incr
assert self.ad_amt is not None
if self.ad_amt >= 1.0:
self.ad_amt = self.ad_amt % 1.0
self.attempted_first_ad = True
# After we've reached the traditional show-threshold once,
# try again whenever its been INTERVAL since our last successful
# show.
elif self.attempted_first_ad and (
self.last_ad_completion_time is None
or (
interval is not None
and babase.apptime() - self.last_ad_completion_time
> (interval * interval_mult)
)
):
# Reset our other counter too in this case.
self.ad_amt = 0.0
else:
show = False
# If we're *still* cleared to show, actually tell the system to show.
if show:
# As a safety-check, we set up an object that will run the
# completion callback if we've returned and sat for several
# seconds (in case some random ad network doesn't properly
# deliver its completion callback).
class _Payload:
def __init__(self, pcall: Callable[[], Any]):
self._call = pcall
self._ran = False
def run(self, fallback: bool = False) -> None:
"""Run the payload."""
assert app.classic is not None
if not self._ran:
if fallback:
lanst = app.classic.ads.last_ad_network_set_time
logging.error(
'Relying on fallback ad-callback! '
'last network: %s (set %s seconds ago);'
' purpose=%s.',
app.classic.ads.last_ad_network,
time.time() - lanst,
app.classic.ads.last_ad_purpose,
)
babase.pushcall(self._call)
self._ran = True
payload = _Payload(call)
# Set up our backup.
with babase.ContextRef.empty():
# Note to self: Previously this was a simple 5 second
# timer because the app got totally suspended while ads
# were showing (which delayed the timer), but these days
# the app may continue to run, so we need to be more
# careful and only fire the fallback after we see that
# the app has been front-and-center for several seconds.
async def add_fallback_task() -> None:
activesecs = 5
while activesecs > 0:
if babase.app.active:
activesecs -= 1
await asyncio.sleep(1.0)
payload.run(fallback=True)
babase.app.create_async_task(add_fallback_task())
self.show_ad('between_game', on_completion_call=payload.run)
else:
babase.pushcall(call) # Just run the callback without the ad.

File diff suppressed because it is too large Load diff

View file

@ -1,139 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to bombsquad classic."""
from bacommon.bs._account import (
ClassicAccountLiveData,
)
from bacommon.bs._bs import (
TOKENS1_COUNT,
TOKENS2_COUNT,
TOKENS3_COUNT,
TOKENS4_COUNT,
)
from bacommon.bs._chest import (
ClassicChestAppearance,
)
from bacommon.bs._clienteffect import (
ClientEffect,
ClientEffectChestWaitTimeAnimation,
ClientEffectDelay,
ClientEffectScreenMessage,
ClientEffectSound,
ClientEffectTicketsAnimation,
ClientEffectTokensAnimation,
ClientEffectTypeID,
ClientEffectUnknown,
)
from bacommon.bs._clouddialog import (
BasicCloudDialog,
BasicCloudDialogComponent,
BasicCloudDialogBsClassicTourneyResult,
BasicCloudDialogComponentLink,
BasicCloudDialogComponentText,
BasicCloudDialogComponentTypeID,
BasicCloudDialogComponentUnknown,
BasicCloudDialogDisplayItems,
BasicCloudDialogExpireTime,
CloudDialog,
CloudDialogAction,
CloudDialogTypeID,
CloudDialogWrapper,
UnknownCloudDialog,
)
from bacommon.bs._cloudui import CloudUITypeID, CloudUI
from bacommon.bs._displayitem import (
ChestDisplayItem,
DisplayItem,
DisplayItemTypeID,
DisplayItemWrapper,
TestDisplayItem,
TicketsDisplayItem,
TokensDisplayItem,
UnknownDisplayItem,
)
from bacommon.bs._msg import (
ChestActionMessage,
ChestActionResponse,
ChestInfoMessage,
ChestInfoResponse,
CloudDialogActionMessage,
CloudDialogActionResponse,
GetClassicPurchasesMessage,
GetClassicPurchasesResponse,
GlobalProfileCheckMessage,
GlobalProfileCheckResponse,
InboxRequestMessage,
InboxRequestResponse,
LegacyRequest,
LegacyResponse,
PrivatePartyMessage,
PrivatePartyResponse,
ScoreSubmitMessage,
ScoreSubmitResponse,
SendInfoMessage,
SendInfoResponse,
)
__all__ = [
'BasicCloudDialog',
'BasicCloudDialogComponent',
'BasicCloudDialogBsClassicTourneyResult',
'BasicCloudDialogComponentLink',
'BasicCloudDialogComponentText',
'BasicCloudDialogComponentTypeID',
'BasicCloudDialogComponentUnknown',
'BasicCloudDialogDisplayItems',
'BasicCloudDialogExpireTime',
'ChestActionMessage',
'ChestActionResponse',
'ChestDisplayItem',
'ChestInfoMessage',
'ChestInfoResponse',
'ClassicAccountLiveData',
'ClassicChestAppearance',
'ClientEffect',
'ClientEffectChestWaitTimeAnimation',
'ClientEffectDelay',
'ClientEffectScreenMessage',
'ClientEffectSound',
'ClientEffectTicketsAnimation',
'ClientEffectTokensAnimation',
'ClientEffectTypeID',
'ClientEffectUnknown',
'CloudDialog',
'CloudDialogAction',
'CloudDialogActionMessage',
'CloudDialogActionResponse',
'CloudDialogTypeID',
'CloudDialogWrapper',
'CloudUI',
'CloudUITypeID',
'DisplayItem',
'DisplayItemTypeID',
'DisplayItemWrapper',
'GetClassicPurchasesMessage',
'GetClassicPurchasesResponse',
'GlobalProfileCheckMessage',
'GlobalProfileCheckResponse',
'InboxRequestMessage',
'InboxRequestResponse',
'LegacyRequest',
'LegacyResponse',
'PrivatePartyMessage',
'PrivatePartyResponse',
'ScoreSubmitMessage',
'ScoreSubmitResponse',
'SendInfoMessage',
'SendInfoResponse',
'TestDisplayItem',
'TicketsDisplayItem',
'TOKENS1_COUNT',
'TOKENS2_COUNT',
'TOKENS3_COUNT',
'TOKENS4_COUNT',
'TokensDisplayItem',
'UnknownCloudDialog',
'UnknownDisplayItem',
]

View file

@ -1,73 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass
from typing import Annotated
from efro.dataclassio import ioprepped, IOAttrs
from bacommon.bs._chest import ClassicChestAppearance
@ioprepped
@dataclass
class ClassicAccountLiveData:
"""Live account data fed to the client in the bs classic app mode."""
@dataclass
class Chest:
"""A lovely chest."""
appearance: Annotated[
ClassicChestAppearance,
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
]
create_time: Annotated[datetime.datetime, IOAttrs('c')]
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
unlock_tokens: Annotated[int, IOAttrs('k')]
ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
class LeagueType(Enum):
"""Type of league we are in."""
BRONZE = 'b'
SILVER = 's'
GOLD = 'g'
DIAMOND = 'd'
class Flag(Enum):
"""Flags set for our account."""
ASK_FOR_REVIEW = 'r'
tickets: Annotated[int, IOAttrs('ti')]
tokens: Annotated[int, IOAttrs('to')]
gold_pass: Annotated[bool, IOAttrs('g')]
remove_ads: Annotated[bool, IOAttrs('r')]
achievements: Annotated[int, IOAttrs('a')]
achievements_total: Annotated[int, IOAttrs('at')]
league_type: Annotated[LeagueType | None, IOAttrs('lt')]
league_num: Annotated[int | None, IOAttrs('ln')]
league_rank: Annotated[int | None, IOAttrs('lr')]
level: Annotated[int, IOAttrs('lv')]
xp: Annotated[int, IOAttrs('xp')]
xpmax: Annotated[int, IOAttrs('xpm')]
inbox_count: Annotated[int, IOAttrs('ibc')]
inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
inbox_contains_prize: Annotated[bool, IOAttrs('icp')]
chests: Annotated[dict[str, Chest], IOAttrs('c')]
# State id of our purchases for builds 22459+.
purchases_state: Annotated[str | None, IOAttrs('p')]
flags: Annotated[set[Flag], IOAttrs('f', soft_default_factory=set)]

View file

@ -1,9 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
# Token counts for our various packs.
TOKENS1_COUNT = 50
TOKENS2_COUNT = 500
TOKENS3_COUNT = 1200
TOKENS4_COUNT = 2600

View file

@ -1,46 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
from __future__ import annotations
from enum import Enum
from typing import assert_never
class ClassicChestAppearance(Enum):
"""Appearances bombsquad classic chests can have."""
UNKNOWN = 'u'
DEFAULT = 'd'
L1 = 'l1'
L2 = 'l2'
L3 = 'l3'
L4 = 'l4'
L5 = 'l5'
L6 = 'l6'
@property
def pretty_name(self) -> str:
"""Pretty name for the chest in English."""
# pylint: disable=too-many-return-statements
cls = type(self)
if self is cls.UNKNOWN:
return 'Unknown Chest'
if self is cls.DEFAULT:
return 'Chest'
if self is cls.L1:
return 'L1 Chest'
if self is cls.L2:
return 'L2 Chest'
if self is cls.L3:
return 'L3 Chest'
if self is cls.L4:
return 'L4 Chest'
if self is cls.L5:
return 'L5 Chest'
if self is cls.L6:
return 'L6 Chest'
assert_never(self)

View file

@ -1,180 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""ClientEffect related functionality."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
class ClientEffectTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
SCREEN_MESSAGE = 'm'
SOUND = 's'
DELAY = 'd'
CHEST_WAIT_TIME_ANIMATION = 't'
TICKETS_ANIMATION = 'ta'
TOKENS_ANIMATION = 'toa'
class ClientEffect(IOMultiType[ClientEffectTypeID]):
"""Something that can happen on the client.
This can include screen messages, sounds, visual effects, etc.
"""
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
# pylint: disable=too-many-return-statements
t = ClientEffectTypeID
if type_id is t.UNKNOWN:
return ClientEffectUnknown
if type_id is t.SCREEN_MESSAGE:
return ClientEffectScreenMessage
if type_id is t.SOUND:
return ClientEffectSound
if type_id is t.DELAY:
return ClientEffectDelay
if type_id is t.CHEST_WAIT_TIME_ANIMATION:
return ClientEffectChestWaitTimeAnimation
if type_id is t.TICKETS_ANIMATION:
return ClientEffectTicketsAnimation
if type_id is t.TOKENS_ANIMATION:
return ClientEffectTokensAnimation
# Important to make sure we provide all types.
assert_never(type_id)
@override
@classmethod
def get_unknown_type_fallback(cls) -> ClientEffect:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return ClientEffectUnknown()
@ioprepped
@dataclass
class ClientEffectUnknown(ClientEffect):
"""Fallback substitute for types we don't recognize."""
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.UNKNOWN
@ioprepped
@dataclass
class ClientEffectScreenMessage(ClientEffect):
"""Display a screen-message."""
message: Annotated[str, IOAttrs('m')]
subs: Annotated[list[str], IOAttrs('s')] = field(default_factory=list)
color: Annotated[tuple[float, float, float], IOAttrs('c')] = (1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.SCREEN_MESSAGE
@ioprepped
@dataclass
class ClientEffectSound(ClientEffect):
"""Play a sound."""
class Sound(Enum):
"""Sounds that can be made alongside the message."""
UNKNOWN = 'u'
CASH_REGISTER = 'c'
ERROR = 'e'
POWER_DOWN = 'p'
GUN_COCKING = 'g'
sound: Annotated[Sound, IOAttrs('s', enum_fallback=Sound.UNKNOWN)]
volume: Annotated[float, IOAttrs('v')] = 1.0
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.SOUND
@ioprepped
@dataclass
class ClientEffectChestWaitTimeAnimation(ClientEffect):
"""Animate chest wait time changing."""
chestid: Annotated[str, IOAttrs('c')]
duration: Annotated[float, IOAttrs('u')]
startvalue: Annotated[datetime.datetime, IOAttrs('o')]
endvalue: Annotated[datetime.datetime, IOAttrs('n')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION
@ioprepped
@dataclass
class ClientEffectTicketsAnimation(ClientEffect):
"""Animate tickets count."""
duration: Annotated[float, IOAttrs('u')]
startvalue: Annotated[int, IOAttrs('s')]
endvalue: Annotated[int, IOAttrs('e')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.TICKETS_ANIMATION
@ioprepped
@dataclass
class ClientEffectTokensAnimation(ClientEffect):
"""Animate tokens count."""
duration: Annotated[float, IOAttrs('u')]
startvalue: Annotated[int, IOAttrs('s')]
endvalue: Annotated[int, IOAttrs('e')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.TOKENS_ANIMATION
@ioprepped
@dataclass
class ClientEffectDelay(ClientEffect):
"""Delay effect processing."""
seconds: Annotated[float, IOAttrs('s')]
@override
@classmethod
def get_type_id(cls) -> ClientEffectTypeID:
return ClientEffectTypeID.DELAY

View file

@ -1,308 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""Simple cloud-defined UIs for things like notifications."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.bs._displayitem import DisplayItemWrapper
class CloudDialogTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
BASIC = 'b'
class CloudDialog(IOMultiType[CloudDialogTypeID]):
"""Small self-contained ui bit provided by the cloud.
These take care of updating and/or dismissing themselves based on
user input. Useful for things such as inbox messages. For more
complex UI construction, look at :class:`CloudUI`.
"""
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: CloudDialogTypeID) -> type[CloudDialog]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
out: type[CloudDialog]
t = CloudDialogTypeID
if type_id is t.UNKNOWN:
out = UnknownCloudDialog
elif type_id is t.BASIC:
out = BasicCloudDialog
else:
# Important to make sure we provide all types.
assert_never(type_id)
return out
@override
@classmethod
def get_unknown_type_fallback(cls) -> CloudDialog:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return UnknownCloudDialog()
@ioprepped
@dataclass
class UnknownCloudDialog(CloudDialog):
"""Fallback type for unrecognized entries."""
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
return CloudDialogTypeID.UNKNOWN
class BasicCloudDialogComponentTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TEXT = 't'
LINK = 'l'
BS_CLASSIC_TOURNEY_RESULT = 'ct'
DISPLAY_ITEMS = 'di'
EXPIRE_TIME = 'd'
class BasicCloudDialogComponent(IOMultiType[BasicCloudDialogComponentTypeID]):
"""Top level class for our multitype."""
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(
cls, type_id: BasicCloudDialogComponentTypeID
) -> type[BasicCloudDialogComponent]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = BasicCloudDialogComponentTypeID
if type_id is t.UNKNOWN:
return BasicCloudDialogComponentUnknown
if type_id is t.TEXT:
return BasicCloudDialogComponentText
if type_id is t.LINK:
return BasicCloudDialogComponentLink
if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
return BasicCloudDialogBsClassicTourneyResult
if type_id is t.DISPLAY_ITEMS:
return BasicCloudDialogDisplayItems
if type_id is t.EXPIRE_TIME:
return BasicCloudDialogExpireTime
# Important to make sure we provide all types.
assert_never(type_id)
@override
@classmethod
def get_unknown_type_fallback(cls) -> BasicCloudDialogComponent:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return BasicCloudDialogComponentUnknown()
@ioprepped
@dataclass
class BasicCloudDialogComponentUnknown(BasicCloudDialogComponent):
"""An unknown basic client component type.
In practice these should never show up since the master-server
generates these on the fly for the client and so should not send
clients one they can't digest.
"""
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.UNKNOWN
@ioprepped
@dataclass
class BasicCloudDialogComponentText(BasicCloudDialogComponent):
"""Show some text in the inbox message."""
text: Annotated[str, IOAttrs('t')]
subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
default_factory=list
)
scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
color: Annotated[
tuple[float, float, float, float], IOAttrs('c', store_default=False)
] = (1.0, 1.0, 1.0, 1.0)
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.TEXT
@ioprepped
@dataclass
class BasicCloudDialogComponentLink(BasicCloudDialogComponent):
"""Show a link in the inbox message."""
url: Annotated[str, IOAttrs('u')]
label: Annotated[str, IOAttrs('l')]
subs: Annotated[list[str], IOAttrs('s', store_default=False)] = field(
default_factory=list
)
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.LINK
@ioprepped
@dataclass
class BasicCloudDialogBsClassicTourneyResult(BasicCloudDialogComponent):
"""Show info about a classic tourney."""
tournament_id: Annotated[str, IOAttrs('t')]
game: Annotated[str, IOAttrs('g')]
players: Annotated[int, IOAttrs('p')]
rank: Annotated[int, IOAttrs('r')]
trophy: Annotated[str | None, IOAttrs('tr')]
prizes: Annotated[list[DisplayItemWrapper], IOAttrs('pr')]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.BS_CLASSIC_TOURNEY_RESULT
@ioprepped
@dataclass
class BasicCloudDialogDisplayItems(BasicCloudDialogComponent):
"""Show some display-items."""
items: Annotated[list[DisplayItemWrapper], IOAttrs('d')]
width: Annotated[float, IOAttrs('w')] = 100.0
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.DISPLAY_ITEMS
@ioprepped
@dataclass
class BasicCloudDialogExpireTime(BasicCloudDialogComponent):
"""Show expire-time."""
time: Annotated[datetime.datetime, IOAttrs('d')]
spacing_top: Annotated[float, IOAttrs('st', store_default=False)] = 0.0
spacing_bottom: Annotated[float, IOAttrs('sb', store_default=False)] = 0.0
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.EXPIRE_TIME
@ioprepped
@dataclass
class BasicCloudDialog(CloudDialog):
"""A basic UI for the client."""
class ButtonLabel(Enum):
"""Distinct button labels we support."""
UNKNOWN = 'u'
OK = 'o'
APPLY = 'a'
CANCEL = 'c'
ACCEPT = 'ac'
DECLINE = 'dn'
IGNORE = 'ig'
CLAIM = 'cl'
DISCARD = 'd'
class InteractionStyle(Enum):
"""Overall interaction styles we support."""
UNKNOWN = 'u'
BUTTON_POSITIVE = 'p'
BUTTON_POSITIVE_NEGATIVE = 'pn'
components: Annotated[list[BasicCloudDialogComponent], IOAttrs('s')]
interaction_style: Annotated[
InteractionStyle, IOAttrs('i', enum_fallback=InteractionStyle.UNKNOWN)
] = InteractionStyle.BUTTON_POSITIVE
button_label_positive: Annotated[
ButtonLabel, IOAttrs('p', enum_fallback=ButtonLabel.UNKNOWN)
] = ButtonLabel.OK
button_label_negative: Annotated[
ButtonLabel, IOAttrs('n', enum_fallback=ButtonLabel.UNKNOWN)
] = ButtonLabel.CANCEL
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
return CloudDialogTypeID.BASIC
def contains_unknown_elements(self) -> bool:
"""Whether something within us is an unknown type or enum."""
return (
self.interaction_style is self.InteractionStyle.UNKNOWN
or self.button_label_positive is self.ButtonLabel.UNKNOWN
or self.button_label_negative is self.ButtonLabel.UNKNOWN
or any(
c.get_type_id() is BasicCloudDialogComponentTypeID.UNKNOWN
for c in self.components
)
)
@ioprepped
@dataclass
class CloudDialogWrapper:
"""Wrapper for a CloudDialog and its common data."""
id: Annotated[str, IOAttrs('i')]
createtime: Annotated[datetime.datetime, IOAttrs('c')]
ui: Annotated[CloudDialog, IOAttrs('e')]
class CloudDialogAction(Enum):
"""Types of actions we can run."""
BUTTON_PRESS_POSITIVE = 'p'
BUTTON_PRESS_NEGATIVE = 'n'

View file

@ -1,180 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""Full UIs defined in the cloud - similar to a basic form of html"""
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
class CloudUITypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
V1 = 'v1'
class CloudUI(IOMultiType[CloudUITypeID]):
"""UI defined by the cloud.
Conceptually similar to a basic html page, except using app UI.
"""
@override
@classmethod
def get_type_id(cls) -> CloudUITypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: CloudUITypeID) -> type[CloudUI]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
out: type[CloudUI]
t = CloudUITypeID
if type_id is t.UNKNOWN:
out = UnknownCloudUI
elif type_id is t.V1:
out = V1CloudUI
else:
# Important to make sure we provide all types.
assert_never(type_id)
return out
@override
@classmethod
def get_unknown_type_fallback(cls) -> CloudUI:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return UnknownCloudUI()
@ioprepped
@dataclass
class UnknownCloudUI(CloudUI):
"""Fallback type for unrecognized UI types.
Will show the client a 'cannot display this UI' placeholder page.
"""
@override
@classmethod
def get_type_id(cls) -> CloudUITypeID:
return CloudUITypeID.UNKNOWN
class V1CloudUIComponentTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TEXT = 't'
class V1CloudUIComponent(IOMultiType[V1CloudUIComponentTypeID]):
"""Top level class for our multitype."""
@override
@classmethod
def get_type_id(cls) -> V1CloudUIComponentTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(
cls, type_id: V1CloudUIComponentTypeID
) -> type[V1CloudUIComponent]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = V1CloudUIComponentTypeID
if type_id is t.UNKNOWN:
return V1CloudUIComponentUnknown
if type_id is t.TEXT:
return V1CloudUIComponentText
# Important to make sure we provide all types.
assert_never(type_id)
@override
@classmethod
def get_unknown_type_fallback(cls) -> V1CloudUIComponent:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return V1CloudUIComponentUnknown()
@ioprepped
@dataclass
class V1CloudUIComponentUnknown(V1CloudUIComponent):
"""An unknown basic client component type.
In practice these should never show up since the master-server
generates these on the fly for the client and so should not send
clients one they can't digest.
"""
@override
@classmethod
def get_type_id(cls) -> V1CloudUIComponentTypeID:
return V1CloudUIComponentTypeID.UNKNOWN
@ioprepped
@dataclass
class V1CloudUIComponentText(V1CloudUIComponent):
"""Show some text over a button."""
text: Annotated[str, IOAttrs('t')]
# position: Annotated[float, IOAttrs('p', store_default=False)] = 1.0
# scale: Annotated[float, IOAttrs('s', store_default=False)] = 1.0
# color: Annotated[
# tuple[float, float, float, float], IOAttrs('c', store_default=False)
# ] = (1.0, 1.0, 1.0, 1.0)
@override
@classmethod
def get_type_id(cls) -> V1CloudUIComponentTypeID:
return V1CloudUIComponentTypeID.TEXT
@ioprepped
@dataclass
class V1CloudUIButton:
"""A button in our cloud ui."""
color: Annotated[tuple[float, float, float], IOAttrs('cl')]
size: Annotated[tuple[float, float], IOAttrs('sz')]
components: Annotated[list[V1CloudUIComponent], IOAttrs('c')]
scale: Annotated[float, IOAttrs('sc', store_default=False)] = 1.0
@ioprepped
@dataclass
class V1CloudUIRow:
"""A row in our cloud ui."""
buttons: Annotated[list[V1CloudUIButton], IOAttrs('b')]
@ioprepped
@dataclass
class V1CloudUI(CloudUI):
"""Version 1 of our cloud-defined UI type."""
rows: Annotated[list[V1CloudUIRow], IOAttrs('r')]
@override
@classmethod
def get_type_id(cls) -> CloudUITypeID:
return CloudUITypeID.V1

View file

@ -1,182 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""DisplayItem related functionality."""
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass
from typing import Annotated, override, assert_never
from efro.util import pairs_to_flat
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.bs._chest import ClassicChestAppearance
class DisplayItemTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TICKETS = 't'
TOKENS = 'k'
TEST = 's'
CHEST = 'c'
class DisplayItem(IOMultiType[DisplayItemTypeID]):
"""Some amount of something that can be shown or described.
Used to depict chest contents, inventory, rewards, etc.
"""
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
@override
@classmethod
def get_type(cls, type_id: DisplayItemTypeID) -> type[DisplayItem]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = DisplayItemTypeID
if type_id is t.UNKNOWN:
return UnknownDisplayItem
if type_id is t.TICKETS:
return TicketsDisplayItem
if type_id is t.TOKENS:
return TokensDisplayItem
if type_id is t.TEST:
return TestDisplayItem
if type_id is t.CHEST:
return ChestDisplayItem
# Important to make sure we provide all types.
assert_never(type_id)
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
"""Return a string description and subs for the item.
These decriptions are baked into the DisplayItemWrapper and
should be accessed from there when available. This allows
clients to give descriptions even for newer display items they
don't recognize.
"""
raise NotImplementedError()
# Implement fallbacks so client can digest item lists even if they
# contain unrecognized stuff. DisplayItemWrapper contains basic
# baked down info that they can still use in such cases.
@override
@classmethod
def get_unknown_type_fallback(cls) -> DisplayItem:
return UnknownDisplayItem()
@ioprepped
@dataclass
class UnknownDisplayItem(DisplayItem):
"""Something we don't know how to display."""
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.UNKNOWN
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
import logging
# Make noise but don't break.
logging.exception(
'UnknownDisplayItem.get_description() should never be called.'
' Always access descriptions on the DisplayItemWrapper.'
)
return 'Unknown', []
@ioprepped
@dataclass
class TicketsDisplayItem(DisplayItem):
"""Some amount of tickets."""
count: Annotated[int, IOAttrs('c')]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.TICKETS
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return '${C} Tickets', [('${C}', str(self.count))]
@ioprepped
@dataclass
class TokensDisplayItem(DisplayItem):
"""Some amount of tokens."""
count: Annotated[int, IOAttrs('c')]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.TOKENS
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return '${C} Tokens', [('${C}', str(self.count))]
@ioprepped
@dataclass
class TestDisplayItem(DisplayItem):
"""Fills usable space for a display-item - good for calibration."""
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.TEST
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return 'Test Display Item Here', []
@ioprepped
@dataclass
class ChestDisplayItem(DisplayItem):
"""Display a chest."""
appearance: Annotated[ClassicChestAppearance, IOAttrs('a')]
@override
@classmethod
def get_type_id(cls) -> DisplayItemTypeID:
return DisplayItemTypeID.CHEST
@override
def get_description(self) -> tuple[str, list[tuple[str, str]]]:
return self.appearance.pretty_name, []
@ioprepped
@dataclass
class DisplayItemWrapper:
"""Wraps a DisplayItem and common info."""
item: Annotated[DisplayItem, IOAttrs('i')]
description: Annotated[str, IOAttrs('d')]
description_subs: Annotated[list[str] | None, IOAttrs('s')]
@classmethod
def for_display_item(cls, item: DisplayItem) -> DisplayItemWrapper:
"""Convenience method to wrap a DisplayItem."""
desc, subs = item.get_description()
return DisplayItemWrapper(item, desc, pairs_to_flat(subs))

View file

@ -1,336 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""BombSquad specific bits."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override
from efro.dataclassio import ioprepped, IOAttrs
from efro.message import Message, Response
from bacommon.bs._displayitem import DisplayItemWrapper
from bacommon.bs._clienteffect import ClientEffect
from bacommon.bs._clouddialog import CloudDialogAction, CloudDialogWrapper
from bacommon.bs._chest import ClassicChestAppearance
@ioprepped
@dataclass
class ChestActionMessage(Message):
"""Request action about a chest."""
class Action(Enum):
"""Types of actions we can request."""
# Unlocking (for free or with tokens).
UNLOCK = 'u'
# Watched an ad to reduce wait.
AD = 'ad'
action: Annotated[Action, IOAttrs('a')]
# Tokens we are paying (only applies to unlock).
token_payment: Annotated[int, IOAttrs('t')]
chest_id: Annotated[str, IOAttrs('i')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ChestActionResponse]
@ioprepped
@dataclass
class ChestActionResponse(Response):
"""Here's the results of that action you asked for, boss."""
# Tokens that were actually charged.
tokens_charged: Annotated[int, IOAttrs('t')] = 0
# If present, signifies the chest has been opened and we should show
# the user this stuff that was in it.
contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
# If contents are present, which of the chest's prize-sets they
# represent.
prizeindex: Annotated[int, IOAttrs('i')] = 0
# Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None
# Printable warning. Shown in orange with an error sound. Does not
# mean the action failed; only that there's something to tell the
# users such as 'It looks like you are faking ad views; stop it or
# you won't have ad options anymore.'
warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None
# Printable success message. Shown in green with a cash-register
# sound. Can be used for things like successful wait reductions via
# ad views. Used in builds earlier than 22311; can remove once
# 22311+ is ubiquitous.
success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None
# Effects to show on the client. Replaces warning and success_msg in
# build 22311 or newer.
effects: Annotated[
list[ClientEffect], IOAttrs('fx', store_default=False)
] = field(default_factory=list)
@ioprepped
@dataclass
class CloudDialogActionMessage(Message):
"""Do something to a client ui."""
id: Annotated[str, IOAttrs('i')]
action: Annotated[CloudDialogAction, IOAttrs('a')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [CloudDialogActionResponse]
@ioprepped
@dataclass
class CloudDialogActionResponse(Response):
"""Did something to that inbox entry, boss."""
class ErrorType(Enum):
"""Types of errors that may have occurred."""
# Probably a future error type we don't recognize.
UNKNOWN = 'u'
# Something went wrong on the server, but specifics are not
# relevant.
INTERNAL = 'i'
# The entry expired on the server. In various cases such as 'ok'
# buttons this can generally be ignored.
EXPIRED = 'e'
error_type: Annotated[
ErrorType | None, IOAttrs('et', enum_fallback=ErrorType.UNKNOWN)
]
# User facing error message in the case of errors.
error_message: Annotated[str | None, IOAttrs('em')]
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
@ioprepped
@dataclass
class GetClassicPurchasesMessage(Message):
"""Asking for current account's classic purchases."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [GetClassicPurchasesResponse]
@ioprepped
@dataclass
class GetClassicPurchasesResponse(Response):
"""Here's those classic purchases ya asked for boss."""
purchases: Annotated[set[str], IOAttrs('p')]
@ioprepped
@dataclass
class GlobalProfileCheckMessage(Message):
"""Is this global profile name available?"""
name: Annotated[str, IOAttrs('n')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [GlobalProfileCheckResponse]
@ioprepped
@dataclass
class GlobalProfileCheckResponse(Response):
"""Here's that profile check ya asked for boss."""
available: Annotated[bool, IOAttrs('a')]
ticket_cost: Annotated[int, IOAttrs('tc')]
@ioprepped
@dataclass
class InboxRequestMessage(Message):
"""Message requesting our inbox."""
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [InboxRequestResponse]
@ioprepped
@dataclass
class InboxRequestResponse(Response):
"""Here's that inbox contents you asked for, boss."""
wrappers: Annotated[list[CloudDialogWrapper], IOAttrs('w')]
# Printable error if something goes wrong.
error: Annotated[str | None, IOAttrs('e')] = None
@ioprepped
@dataclass
class LegacyRequest(Message):
"""A generic request for the legacy master server."""
request: Annotated[str, IOAttrs('r')]
request_type: Annotated[str, IOAttrs('t')]
user_agent_string: Annotated[str, IOAttrs('u')]
data: Annotated[str, IOAttrs('d')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [LegacyResponse]
@ioprepped
@dataclass
class LegacyResponse(Response):
"""Response for generic legacy request."""
data: Annotated[str | None, IOAttrs('d')]
zipped: Annotated[bool, IOAttrs('z')]
@ioprepped
@dataclass
class ChestInfoMessage(Message):
"""Request info about a chest."""
chest_id: Annotated[str, IOAttrs('i')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ChestInfoResponse]
@ioprepped
@dataclass
class ChestInfoResponse(Response):
"""Here's that chest info you asked for, boss."""
@dataclass
class Chest:
"""A lovely chest."""
@dataclass
class PrizeSet:
"""A possible set of prizes for this chest."""
weight: Annotated[float, IOAttrs('w')]
contents: Annotated[list[DisplayItemWrapper], IOAttrs('c')]
appearance: Annotated[
ClassicChestAppearance,
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
]
# How much it costs to unlock *now*.
unlock_tokens: Annotated[int, IOAttrs('tk')]
# When it unlocks on its own.
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
# Possible prizes we contain.
prizesets: Annotated[list[PrizeSet], IOAttrs('p')]
# Are ads allowed now?
ad_allow: Annotated[bool, IOAttrs('aa')]
chest: Annotated[Chest | None, IOAttrs('c')]
user_tokens: Annotated[int | None, IOAttrs('t')]
@ioprepped
@dataclass
class PrivatePartyMessage(Message):
"""Message asking about info we need for private-party UI."""
need_datacode: Annotated[bool, IOAttrs('d')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [PrivatePartyResponse]
@ioprepped
@dataclass
class PrivatePartyResponse(Response):
"""Here's that private party UI info you asked for, boss."""
success: Annotated[bool, IOAttrs('s')]
tokens: Annotated[int, IOAttrs('t')]
gold_pass: Annotated[bool, IOAttrs('g')]
datacode: Annotated[str | None, IOAttrs('d')]
@ioprepped
@dataclass
class ScoreSubmitMessage(Message):
"""Let the server know we got some score in something."""
score_token: Annotated[str, IOAttrs('t')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [ScoreSubmitResponse]
@ioprepped
@dataclass
class ScoreSubmitResponse(Response):
"""Did something to that inbox entry, boss."""
# Things we should show on our end.
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
@ioprepped
@dataclass
class SendInfoMessage(Message):
"""User is using the send-info function."""
description: Annotated[str, IOAttrs('c')]
@override
@classmethod
def get_response_types(cls) -> list[type[Response] | None]:
return [SendInfoResponse]
@ioprepped
@dataclass
class SendInfoResponse(Response):
"""Response to sending info to the server."""
handled: Annotated[bool, IOAttrs('v')]
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
effects: Annotated[
list[ClientEffect], IOAttrs('e', store_default=False)
] = field(default_factory=list)
legacy_code: Annotated[str | None, IOAttrs('l', store_default=False)] = None

View file

@ -1,304 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UIs provided by the cloud (similar-ish to html in concept)."""
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, override, Annotated
from efro.dataclassio import ioprepped, IOAttrs
import babase
from bauiv1._window import MainWindow, BasicMainWindowState
import _bauiv1
if TYPE_CHECKING:
from bauiv1._window import MainWindowState
def show_cloud_ui_window() -> None:
"""Bust out a cloud-ui window."""
# Pop up an auxiliary window wherever we are in the nav stack.
babase.app.ui_v1.auxiliary_window_activate(
win_type=CloudUIWindow,
win_create_call=lambda: CloudUIWindow(state=None),
)
@ioprepped
@dataclass
class CloudUIButton:
"""Represents a button in a cloud-ui."""
@ioprepped
@dataclass
class CloudUIRow:
"""Represents a row in a cloud-ui."""
buttons: Annotated[list[CloudUIButton], IOAttrs('b')]
@ioprepped
@dataclass
class CloudUIRoot:
"""Represents an entire cloud-ui."""
title: Annotated[str, IOAttrs('t')]
rows: Annotated[list[CloudUIRow], IOAttrs('r')]
class CloudUIWindow(MainWindow):
"""An example of a well-behaved main-window."""
@dataclass
class _State:
root: CloudUIRoot | None
def __init__(
self,
state: _State | None,
*,
transition: str | None = 'in_right',
origin_widget: _bauiv1.Widget | None = None,
auxiliary_style: bool = True,
):
ui = babase.app.ui_v1
self._state: CloudUIWindow._State | None = None
# We want to display differently whether we're an auxiliary
# window or not, but unfortunately that value is not yet
# available until we're added to the main-window-stack so it
# must be explicitly passed in.
self._auxiliary_style = auxiliary_style
# Calc scale and size for our backing window. For medium & large
# ui-scale we aim for a window small enough to always be fully
# visible on-screen and for small mode we aim for a window big
# enough that we never see the window edges; only the window
# texture covering the whole screen.
uiscale = ui.uiscale
self._width = 1400 if uiscale is babase.UIScale.SMALL else 750
self._height = 1200 if uiscale is babase.UIScale.SMALL else 500
scale = (
1.5
if uiscale is babase.UIScale.SMALL
else 1.2 if uiscale is babase.UIScale.MEDIUM else 1.0
)
# Do some fancy math to calculate our visible area; this will be
# limited by the screen size in small mode and our backing size
# otherwise.
screensize = babase.get_virtual_screen_size()
self._vis_width = min(self._width - 100, screensize[0] / scale)
self._vis_height = min(self._height - 100, screensize[1] / scale)
self._vis_top = 0.5 * self._height + 0.5 * self._vis_height
self._vis_left = 0.5 * self._width - 0.5 * self._vis_width
# Nudge our vis area up a bit when we can see the full backing
# (visual fudge factor).
if uiscale is not babase.UIScale.SMALL:
self._vis_top += 12.0
super().__init__(
root_widget=_bauiv1.containerwidget(
size=(self._width, self._height),
toolbar_visibility='menu_full',
toolbar_cancel_button_style=(
'close' if auxiliary_style else 'back'
),
scale=scale,
),
transition=transition,
origin_widget=origin_widget,
# We respond to screen size changes only at small ui-scale;
# in other cases we assume our window remains fully visible
# always (flip to windowed mode and resize the app window to
# confirm this).
refresh_on_screen_size_changes=uiscale is babase.UIScale.SMALL,
)
# Avoid complaints if nothing is selected under us.
_bauiv1.widget(edit=self._root_widget, allow_preserve_selection=False)
# Title.
self._title = _bauiv1.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._vis_top - 20),
size=(0, 0),
text='',
color=ui.title_color,
scale=0.9 if uiscale is babase.UIScale.SMALL else 1.0,
# Make sure we avoid overlapping meters in small mode.
maxwidth=(130 if uiscale is babase.UIScale.SMALL else 200),
h_align='center',
v_align='center',
)
# For small UI-scale we use the system back/close button;
# otherwise we make our own.
if uiscale is babase.UIScale.SMALL:
_bauiv1.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
else:
btn = _bauiv1.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|close',
scale=0.8,
position=(self._vis_left - 15, self._vis_top - 30),
size=(50, 50) if auxiliary_style else (60, 55),
extra_touch_border_scale=2.0,
button_type=None if auxiliary_style else 'backSmall',
on_activate_call=self.main_window_back,
autoselect=True,
label=babase.charstr(
babase.SpecialChar.CLOSE
if auxiliary_style
else babase.SpecialChar.BACK
),
)
_bauiv1.containerwidget(edit=self._root_widget, cancel_button=btn)
# Show our vis-area bounds (for debugging).
if bool(True):
# Skip top-left since its always overlapping back/close
# buttons.
if bool(False):
_bauiv1.textwidget(
parent=self._root_widget,
position=(self._vis_left, self._vis_top),
size=(0, 0),
color=(1, 1, 1, 0.5),
scale=0.5,
text='TL',
h_align='left',
v_align='top',
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(self._vis_left + self._vis_width, self._vis_top),
size=(0, 0),
color=(1, 1, 1, 0.5),
scale=0.5,
text='TR',
h_align='right',
v_align='top',
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(self._vis_left, self._vis_top - self._vis_height),
size=(0, 0),
color=(1, 1, 1, 0.5),
scale=0.5,
text='BL',
h_align='left',
v_align='bottom',
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(
self._vis_left + self._vis_width,
self._vis_top - self._vis_height,
),
size=(0, 0),
scale=0.5,
color=(1, 1, 1, 0.5),
text='BR',
h_align='right',
v_align='bottom',
)
self._spinner: _bauiv1.Widget | None = _bauiv1.spinnerwidget(
parent=self._root_widget,
position=(
self._vis_left + self._vis_width * 0.5,
self._vis_top - self._vis_height * 0.5,
),
size=48,
style='bomb',
)
if state is not None:
self._set_state(state)
else:
if random.random() < 0.3:
babase.apptimer(1.0, babase.WeakCall(self._on_error_response))
else:
babase.apptimer(1.0, babase.WeakCall(self._on_response))
def _on_error_response(self) -> None:
self._set_state(self._State(None))
def _on_response(self) -> None:
self._set_state(self._State(CloudUIRoot(title='Testing', rows=[])))
def _set_state(self, state: _State) -> None:
"""Set a final state (error or page contents).
This state may be instantly restored if the window is recreated
(depending on cache lifespan/etc.)
"""
assert self._state is None
self._state = state
if self._spinner:
self._spinner.delete()
self._spinner = None
if self._state.root is None:
_bauiv1.textwidget(
edit=self._title,
literal=False, # Allow Lstr.
text=babase.Lstr(resource='errorText'),
)
_bauiv1.textwidget(
parent=self._root_widget,
position=(
self._vis_left + 0.5 * self._vis_width,
self._vis_top - 0.5 * self._vis_height,
),
size=(0, 0),
scale=0.6,
text=babase.Lstr(resource='store.loadErrorText'),
h_align='center',
v_align='center',
)
else:
_bauiv1.textwidget(
edit=self._title,
literal=True, # Never interpret as Lstr.
text=self._state.root.title,
)
@override
def get_main_window_state(self) -> MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
# IMPORTANT - Pull values from self HERE; if we do it in the
# lambda below it'll keep self alive which will lead to
# 'ui-not-getting-cleaned-up' warnings and memory leaks.
auxiliary_style = self._auxiliary_style
state = self._state
return BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
state=state,
transition=transition,
origin_widget=origin_widget,
auxiliary_style=auxiliary_style,
),
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
@override
def get_main_window_shared_state_id(self) -> str | None:
return 'cloudui'

View file

@ -1,198 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for linking accounts."""
from __future__ import annotations
import copy
import time
from typing import TYPE_CHECKING
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
class AccountLinkWindow(bui.Window):
"""Window for linking accounts."""
def __init__(self, origin_widget: bui.Widget | None = None):
plus = bui.app.plus
assert plus is not None
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
transition = 'in_right'
bg_color = (0.4, 0.4, 0.5)
self._width = 560
self._height = 420
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
base_scale = (
1.65
if uiscale is bui.UIScale.SMALL
else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.1
)
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=base_scale,
scale_origin_stack_offset=scale_origin,
stack_offset=(
(0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
),
)
)
self._cancel_button = bui.buttonwidget(
parent=self._root_widget,
position=(40, self._height - 45),
size=(50, 50),
scale=0.7,
label='',
color=bg_color,
on_activate_call=self._cancel,
autoselect=True,
icon=bui.gettexture('crossOut'),
iconscale=1.2,
)
maxlinks = plus.get_v1_account_misc_read_val('maxLinkAccounts', 5)
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.56),
size=(0, 0),
text=bui.Lstr(
resource=(
'accountSettingsWindow.linkAccountsInstructionsNewText'
),
subs=[('${COUNT}', str(maxlinks))],
),
maxwidth=self._width * 0.9,
color=bui.app.ui_v1.infotextcolor,
max_height=self._height * 0.6,
h_align='center',
v_align='center',
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._cancel_button
)
bui.buttonwidget(
parent=self._root_widget,
position=(40, 30),
size=(200, 60),
label=bui.Lstr(
resource='accountSettingsWindow.linkAccountsGenerateCodeText'
),
autoselect=True,
on_activate_call=self._generate_press,
)
self._enter_code_button = bui.buttonwidget(
parent=self._root_widget,
position=(self._width - 240, 30),
size=(200, 60),
label=bui.Lstr(
resource='accountSettingsWindow.linkAccountsEnterCodeText'
),
autoselect=True,
on_activate_call=self._enter_code_press,
)
def _generate_press(self) -> None:
from bauiv1lib.account.signin import show_sign_in_prompt
plus = bui.app.plus
assert plus is not None
if plus.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
bui.screenmessage(
bui.Lstr(resource='gatherWindow.requestingAPromoCodeText'),
color=(0, 1, 0),
)
plus.add_v1_account_transaction(
{
'type': 'ACCOUNT_LINK_CODE_REQUEST',
'expire_time': time.time() + 5,
}
)
plus.run_v1_account_transactions()
def _enter_code_press(self) -> None:
from bauiv1lib.sendinfo import SendInfoWindow
SendInfoWindow(
modal=True,
legacy_code_mode=True,
origin_widget=self._enter_code_button,
)
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
def _cancel(self) -> None:
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
class AccountLinkCodeWindow(bui.Window):
"""Window showing code for account-linking."""
def __init__(self, data: dict[str, Any]):
self._width = 350
self._height = 200
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
color=(0.45, 0.63, 0.15),
transition='in_scale',
scale=(
1.8
if uiscale is bui.UIScale.SMALL
else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
),
)
)
self._data = copy.deepcopy(data)
bui.getsound('cashRegister').play()
bui.getsound('swish').play()
self._cancel_button = bui.buttonwidget(
parent=self._root_widget,
scale=0.5,
position=(40, self._height - 40),
size=(50, 50),
label='',
on_activate_call=self.close,
autoselect=True,
color=(0.45, 0.63, 0.15),
icon=bui.gettexture('crossOut'),
iconscale=1.2,
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._cancel_button
)
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
size=(0, 0),
color=(1.0, 3.0, 1.0),
scale=2.0,
h_align='center',
v_align='center',
text=data['code'],
maxwidth=self._width * 0.85,
)
def close(self) -> None:
"""close the window"""
bui.containerwidget(edit=self._root_widget, transition='out_scale')

View file

@ -1,152 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality for unlinking accounts."""
from __future__ import annotations
import time
from typing import TYPE_CHECKING
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
class AccountUnlinkWindow(bui.Window):
"""A window to kick off account unlinks."""
def __init__(self, origin_widget: bui.Widget | None = None):
plus = bui.app.plus
assert plus is not None
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
transition = 'in_right'
bg_color = (0.4, 0.4, 0.5)
self._width = 540
self._height = 350
self._scroll_width = 400
self._scroll_height = 200
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
base_scale = (
2.0
if uiscale is bui.UIScale.SMALL
else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.1
)
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=base_scale,
scale_origin_stack_offset=scale_origin,
stack_offset=(
(0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
),
)
)
self._cancel_button = bui.buttonwidget(
parent=self._root_widget,
position=(30, self._height - 50),
size=(50, 50),
scale=0.7,
label='',
color=bg_color,
on_activate_call=self._cancel,
autoselect=True,
icon=bui.gettexture('crossOut'),
iconscale=1.2,
)
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.88),
size=(0, 0),
text=bui.Lstr(
resource='accountSettingsWindow.unlinkAccountsInstructionsText'
),
maxwidth=self._width * 0.7,
color=bui.app.ui_v1.infotextcolor,
h_align='center',
v_align='center',
)
bui.containerwidget(
edit=self._root_widget, cancel_button=self._cancel_button
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
position=(
(self._width - self._scroll_width) * 0.5,
self._height - 85 - self._scroll_height,
),
size=(self._scroll_width, self._scroll_height),
)
bui.containerwidget(edit=self._scrollwidget, claims_left_right=True)
self._columnwidget = bui.columnwidget(
parent=self._scrollwidget, border=2, margin=0, left_border=10
)
our_login_id = plus.get_v1_account_public_login_id()
if our_login_id is None:
entries = []
else:
account_infos = plus.get_v1_account_misc_read_val_2(
'linkedAccounts2', []
)
entries = [
{'name': ai['d'], 'id': ai['id']}
for ai in account_infos
if ai['id'] != our_login_id
]
# (avoid getting our selection stuck on an empty column widget)
if not entries:
bui.containerwidget(edit=self._scrollwidget, selectable=False)
for i, entry in enumerate(entries):
txt = bui.textwidget(
parent=self._columnwidget,
selectable=True,
text=entry['name'],
size=(self._scroll_width - 30, 30),
autoselect=True,
click_activate=True,
on_activate_call=bui.Call(self._on_entry_selected, entry),
)
bui.widget(edit=txt, left_widget=self._cancel_button)
if i == 0:
bui.widget(edit=txt, up_widget=self._cancel_button)
def _on_entry_selected(self, entry: dict[str, Any]) -> None:
plus = bui.app.plus
assert plus is not None
bui.screenmessage(
bui.Lstr(
resource='pleaseWaitText', fallback_resource='requestingText'
),
color=(0, 1, 0),
)
plus.add_v1_account_transaction(
{
'type': 'ACCOUNT_UNLINK_REQUEST',
'accountID': entry['id'],
'expire_time': time.time() + 5,
}
)
plus.run_v1_account_transactions()
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)
def _cancel(self) -> None:
bui.containerwidget(
edit=self._root_widget, transition=self._transition_out
)

View file

@ -1,91 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to users rating the game."""
from __future__ import annotations
import bauiv1 as bui
def ask_for_rating() -> bui.Widget | None:
"""(internal)"""
app = bui.app
assert app.classic is not None
platform = app.classic.platform
subplatform = app.classic.subplatform
# FIXME: should whitelist platforms we *do* want this for.
if bui.app.env.test:
return None
if not (
platform == 'mac'
or (platform == 'android' and subplatform in ['google', 'cardboard'])
):
return None
width = 700
height = 400
spacing = 40
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
dlg = bui.containerwidget(
size=(width, height),
transition='in_right',
scale=(
1.6
if uiscale is bui.UIScale.SMALL
else 1.35 if uiscale is bui.UIScale.MEDIUM else 1.0
),
)
v = height - 50
v -= spacing
v -= 140
bui.imagewidget(
parent=dlg,
position=(width / 2 - 100, v + 10),
size=(200, 200),
texture=bui.gettexture('cuteSpaz'),
)
bui.textwidget(
parent=dlg,
position=(15, v - 55),
size=(width - 30, 30),
color=bui.app.ui_v1.infotextcolor,
text=bui.Lstr(
resource='pleaseRateText',
subs=[('${APP_NAME}', bui.Lstr(resource='titleText'))],
),
maxwidth=width * 0.95,
max_height=130,
scale=0.85,
h_align='center',
v_align='center',
)
def do_rating() -> None:
# This is not currently in use anywhere.
bui.screenmessage(bui.Lstr(resource='error'))
# bui.open_url(url)
bui.containerwidget(edit=dlg, transition='out_left')
bui.buttonwidget(
parent=dlg,
position=(60, 20),
size=(200, 60),
label=bui.Lstr(resource='wellSureText'),
autoselect=True,
on_activate_call=do_rating,
)
def close() -> None:
bui.containerwidget(edit=dlg, transition='out_left')
btn = bui.buttonwidget(
parent=dlg,
position=(width - 270, 20),
size=(200, 60),
label=bui.Lstr(resource='noThanksText'),
autoselect=True,
on_activate_call=close,
)
bui.containerwidget(edit=dlg, cancel_button=btn, selected_child=btn)
return dlg

View file

@ -1,476 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to browsing player profiles."""
from __future__ import annotations
from typing import TYPE_CHECKING, override
import bauiv1 as bui
import bascenev1 as bs
if TYPE_CHECKING:
from typing import Any, ClassVar
class ProfileBrowserWindow(bui.MainWindow):
"""Window for browsing player profiles."""
# Keep track of this at the class level to share between instances.
selected_profile: ClassVar[str | None] = None
def __init__(
self,
transition: str | None = 'in_right',
selected_profile: str | None = None,
origin_widget: bui.Widget | None = None,
minimal_toolbar: bool = False,
):
self._minimal_toolbar = minimal_toolbar
back_label = bui.Lstr(resource='backText')
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 800.0 if uiscale is bui.UIScale.SMALL else 600.0
x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
self._height = (
360.0
if uiscale is bui.UIScale.SMALL
else 385.0 if uiscale is bui.UIScale.MEDIUM else 410.0
)
# Need to handle out-transitions ourself for modal mode.
if origin_widget is not None:
self._transition_out = 'out_scale'
else:
self._transition_out = 'out_right'
self._r = 'playerProfilesWindow'
# Ensure we've got an account-profile in cases where we're signed in.
assert bui.app.classic is not None
bui.app.classic.accounts.ensure_have_account_player_profile()
top_extra = 20 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height + top_extra),
toolbar_visibility=(
'menu_minimal'
if (uiscale is bui.UIScale.SMALL or minimal_toolbar)
else 'menu_full'
),
scale=(
2.5
if uiscale is bui.UIScale.SMALL
else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
),
stack_offset=(
(0, -14) if uiscale is bui.UIScale.SMALL else (0, 0)
),
),
transition=transition,
origin_widget=origin_widget,
)
if bui.app.ui_v1.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:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|back',
position=(40 + x_inset, self._height - 59),
size=(120, 60),
scale=0.8,
label=back_label,
button_type='back',
autoselect=True,
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
bui.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 60),
label=bui.charstr(bui.SpecialChar.BACK),
)
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 36),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.titleText'),
maxwidth=300,
color=bui.app.ui_v1.title_color,
scale=0.9,
h_align='center',
v_align='center',
)
scroll_height = self._height - 140.0
self._scroll_width = self._width - (188 + x_inset * 2)
v = self._height - 84.0
h = 50 + x_inset
b_color = (0.6, 0.53, 0.63)
scl = (
1.055
if uiscale is bui.UIScale.SMALL
else 1.18 if uiscale is bui.UIScale.MEDIUM else 1.3
)
v -= 70.0 * scl
self._new_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|new',
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._new_profile,
color=b_color,
button_type='square',
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
text_scale=0.7,
label=bui.Lstr(resource=f'{self._r}.newButtonText'),
)
v -= 70.0 * scl
self._edit_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|edit',
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._edit_profile,
color=b_color,
button_type='square',
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
text_scale=0.7,
label=bui.Lstr(resource=f'{self._r}.editButtonText'),
)
v -= 70.0 * scl
self._delete_button = bui.buttonwidget(
parent=self._root_widget,
id=f'{self.main_window_id_prefix}|delete',
position=(h, v),
size=(80, 66.0 * scl),
on_activate_call=self._delete_profile,
color=b_color,
button_type='square',
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
text_scale=0.7,
label=bui.Lstr(resource=f'{self._r}.deleteButtonText'),
)
v = self._height - 87
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 71),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.explanationText'),
color=bui.app.ui_v1.infotextcolor,
maxwidth=self._width * 0.83,
scale=0.6,
h_align='center',
v_align='center',
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
position=(140 + x_inset, v - scroll_height),
size=(self._scroll_width, scroll_height),
)
bui.widget(
edit=self._scrollwidget,
autoselect=True,
left_widget=self._new_button,
)
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._scroll_width, 32),
background=False,
)
v -= 255
self._profiles: dict[str, dict[str, Any]] | None = None
if selected_profile is not None:
type(self).selected_profile = selected_profile
self._profile_widgets: list[bui.Widget] = []
self._refresh()
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
minimal_toolbar = self._minimal_toolbar
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition,
origin_widget=origin_widget,
minimal_toolbar=minimal_toolbar,
)
)
@override
def main_window_should_preserve_selection(self) -> bool:
return True
# @override
# def main_window_do_save_shared_state(self, state: dict) -> None:
# state['selected_profile'] = self._selected_profile
# @override
# def main_window_do_restore_shared_state(self, state: dict) -> None:
# pval = state.get('selected_profile')
# if isinstance(pval, str | None):
# print('RESTORING', pval)
# self._selected_profile = pval
def _new_profile(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
from bauiv1lib.purchase import PurchaseWindow
# No-op if we're not the in-control main window.
if not self.main_window_has_control():
return
plus = bui.app.plus
assert plus is not None
# Limit to a handful profiles if they don't have pro-options.
max_non_pro_profiles = plus.get_v1_account_misc_read_val('mnpp', 5)
assert self._profiles is not None
assert bui.app.classic is not None
if (
bool(False) # Phasing out pro.
and not bui.app.classic.accounts.have_pro_options()
and len(self._profiles) >= max_non_pro_profiles
):
PurchaseWindow(
items=['pro'],
header_text=bui.Lstr(
resource='unlockThisProfilesText',
subs=[('${NUM}', str(max_non_pro_profiles))],
),
)
return
# Clamp at 100 profiles (otherwise the server will and that's less
# elegant looking).
if len(self._profiles) > 100:
bui.screenmessage(
bui.Lstr(
translate=(
'serverResponses',
'Max number of profiles reached.',
)
),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
self.main_window_replace(
lambda: EditProfileWindow(existing_profile=None)
)
def _delete_profile(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib import confirm
if self.selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0)
)
return
if self.selected_profile == '__account__':
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource=f'{self._r}.cantDeleteAccountProfileText'),
color=(1, 0, 0),
)
return
confirm.ConfirmWindow(
bui.Lstr(
resource=f'{self._r}.deleteConfirmText',
subs=[('${PROFILE}', self.selected_profile)],
),
self._do_delete_profile,
width=350,
)
def _do_delete_profile(self) -> None:
plus = bui.app.plus
assert plus is not None
# Go back to default selection.
type(self).selected_profile = None
plus.add_v1_account_transaction(
{'type': 'REMOVE_PLAYER_PROFILE', 'name': self.selected_profile}
)
plus.run_v1_account_transactions()
bui.getsound('shieldDown').play()
self._refresh()
# Select profile list.
bui.containerwidget(
edit=self._root_widget, selected_child=self._scrollwidget
)
def _edit_profile(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.profile.edit import EditProfileWindow
# No-op if we're not in control.
if not self.main_window_has_control():
return
if self.selected_profile is None:
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource='nothingIsSelectedErrorText'), color=(1, 0, 0)
)
return
self.main_window_replace(
lambda: EditProfileWindow(self.selected_profile)
)
def _select(self, name: str, index: int) -> None:
del index # Unused.
type(self).selected_profile = name
def _refresh(self) -> None:
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from efro.util import asserttype
from bascenev1 import PlayerProfilesChangedMessage
from bascenev1lib.actor import spazappearance
assert bui.app.classic is not None
plus = bui.app.plus
assert plus is not None
old_selection = self.selected_profile
# Delete old.
while self._profile_widgets:
self._profile_widgets.pop().delete()
self._profiles = bui.app.config.get('Player Profiles', {})
assert self._profiles is not None
items = list(self._profiles.items())
items.sort(key=lambda x: asserttype(x[0], str).lower())
spazzes = spazappearance.get_appearances()
spazzes.sort()
icon_textures = [
bui.gettexture(bui.app.classic.spaz_appearances[s].icon_texture)
for s in spazzes
]
icon_tint_textures = [
bui.gettexture(
bui.app.classic.spaz_appearances[s].icon_mask_texture
)
for s in spazzes
]
index = 0
y_val = 35 * (len(self._profiles) - 1)
account_name: str | None
if plus.get_v1_account_state() == 'signed_in':
account_name = plus.get_v1_account_display_string()
else:
account_name = None
widget_to_select = None
for p_name, p_info in items:
if p_name == '__account__' and account_name is None:
continue
color, _highlight = bui.app.classic.get_player_profile_colors(
p_name
)
scl = 1.1
tval = (
account_name
if p_name == '__account__'
else bui.app.classic.get_player_profile_icon(p_name) + p_name
)
try:
char_index = spazzes.index(p_info['character'])
except Exception:
char_index = spazzes.index('Spaz')
assert isinstance(tval, str)
txtw = bui.textwidget(
parent=self._subcontainer,
id=f'{self.main_window_id_prefix}|profile{index}',
position=(5, y_val),
size=((self._width - 210) / scl, 28),
text=bui.Lstr(value=f' {tval}'),
h_align='left',
v_align='center',
on_select_call=bui.WeakCall(self._select, p_name, index),
maxwidth=self._scroll_width * 0.86,
corner_scale=scl,
color=bui.safecolor(color, 0.4),
always_highlight=True,
on_activate_call=bui.Call(self._edit_button.activate),
selectable=True,
)
# We handle reselection of these manually; no need for ids.
bui.widget(edit=txtw, allow_preserve_selection=False)
character = bui.imagewidget(
parent=self._subcontainer,
position=(0, y_val),
size=(30, 30),
color=(1, 1, 1),
mask_texture=bui.gettexture('characterIconMask'),
tint_color=color,
tint2_color=_highlight,
texture=icon_textures[char_index],
tint_texture=icon_tint_textures[char_index],
)
if index == 0:
bui.widget(edit=txtw, up_widget=self._back_button)
if self.selected_profile is None:
type(self).selected_profile = p_name
bui.widget(edit=txtw, show_buffer_top=40, show_buffer_bottom=40)
self._profile_widgets.append(txtw)
self._profile_widgets.append(character)
# Select/show this one if it was previously selected.
# (but defer till after this loop since our height is
# still changing).
if p_name == old_selection or widget_to_select is None:
widget_to_select = txtw
index += 1
y_val -= 35
bui.containerwidget(
edit=self._subcontainer,
size=(self._scroll_width, index * 35),
)
if widget_to_select is not None:
bui.containerwidget(
edit=self._subcontainer,
selected_child=widget_to_select,
visible_child=widget_to_select,
)
# If there's a team-chooser in existence, tell it the profile-list
# has probably changed.
session = bs.get_foreground_host_session()
if session is not None:
session.handlemessage(PlayerProfilesChangedMessage())

View file

@ -1,209 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI related to purchasing items."""
from __future__ import annotations
from typing import TYPE_CHECKING
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
class PurchaseWindow(bui.Window):
"""Window for purchasing one or more items."""
def __init__(
self,
items: list[str],
origin_widget: bui.Widget | None = None,
header_text: bui.Lstr | None = None,
):
from bauiv1lib.store.item import instantiate_store_item_display
plus = bui.app.plus
assert plus is not None
assert bui.app.classic is not None
store = bui.app.classic.store
if header_text is None:
header_text = bui.Lstr(
resource='unlockThisText',
fallback_resource='unlockThisInTheStoreText',
)
if len(items) != 1:
raise ValueError('expected exactly 1 item')
self._idprefix = bui.app.ui_v1.new_id_prefix('purchase')
self._items = list(items)
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='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)
),
darken_behind=True,
)
)
self._is_double = False
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 30),
size=(0, 0),
text=header_text,
h_align='center',
v_align='center',
maxwidth=self._width * 0.9 - 120,
scale=1.2,
color=(1, 0.8, 0.3, 1),
)
size = store.get_store_item_display_size(items[0])
display: dict[str, Any] = {}
instantiate_store_item_display(
items[0],
display,
idprefix=self._idprefix,
parent_widget=self._root_widget,
b_pos=(
self._width * 0.5
- size[0] * 0.5
+ 10
- ((size[0] * 0.5 + 30) if self._is_double else 0),
self._height * 0.5
- size[1] * 0.5
+ 30
+ (20 if self._is_double else 0),
),
b_width=size[0],
b_height=size[1],
button=False,
)
# Wire up the parts we need.
if self._is_double:
pass # not working
else:
if self._items == ['pro']:
price_str = plus.get_price(self._items[0])
pyoffs = -15
else:
pyoffs = 0
price = self._price = plus.get_v1_account_misc_read_val(
'price.' + str(items[0]), -1
)
price_str = bui.charstr(bui.SpecialChar.TICKET) + str(price)
self._price_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, 150 + pyoffs),
size=(0, 0),
text=price_str,
h_align='center',
v_align='center',
maxwidth=self._width * 0.9,
scale=1.4,
color=(0.2, 1, 0.2),
)
self._update_timer = bui.AppTimer(
1.0, bui.WeakCall(self._update), repeat=True
)
self._cancel_button = bui.buttonwidget(
parent=self._root_widget,
position=(50, 40),
size=(150, 60),
scale=1.0,
on_activate_call=self._cancel,
autoselect=True,
label=bui.Lstr(resource='cancelText'),
)
self._purchase_button = bui.buttonwidget(
parent=self._root_widget,
position=(self._width - 200, 40),
size=(150, 60),
scale=1.0,
on_activate_call=self._purchase,
autoselect=True,
label=bui.Lstr(resource='store.purchaseText'),
)
bui.containerwidget(
edit=self._root_widget,
cancel_button=self._cancel_button,
start_button=self._purchase_button,
selected_child=self._purchase_button,
)
def _update(self) -> None:
can_die = False
plus = bui.app.plus
assert plus is not None
# We go away if we see that our target item is owned.
if self._items == ['pro']:
assert bui.app.classic is not None
if bui.app.classic.accounts.have_pro():
can_die = True
else:
assert bui.app.classic is not None
if self._items[0] in bui.app.classic.purchases:
can_die = True
if can_die:
bui.containerwidget(edit=self._root_widget, transition='out_scale')
def _purchase(self) -> None:
plus = bui.app.plus
assert plus is not None
classic = bui.app.classic
assert classic is not None
if self._items == ['pro']:
plus.purchase('pro')
else:
ticket_count: int | None
try:
ticket_count = classic.tickets
except Exception:
ticket_count = None
if ticket_count is not None and ticket_count < self._price:
bui.getsound('error').play()
bui.screenmessage(
bui.Lstr(resource='notEnoughTicketsText'),
color=(1, 0, 0),
)
return
def do_it() -> None:
assert plus is not None
plus.in_game_purchase(self._items[0], self._price)
bui.getsound('swish').play()
do_it()
def _cancel(self) -> None:
bui.containerwidget(edit=self._root_widget, transition='out_scale')

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,729 +0,0 @@
# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to UI items."""
from __future__ import annotations
from typing import TYPE_CHECKING
import bascenev1 as bs
import bauiv1 as bui
if TYPE_CHECKING:
from typing import Any
def instantiate_store_item_display(
item_name: str,
item: dict[str, Any],
*,
parent_widget: bui.Widget,
b_pos: tuple[float, float],
b_width: float,
b_height: float,
idprefix: str,
boffs_h: float = 0.0,
boffs_h2: float = 0.0,
boffs_v2: float = 0,
delay: float = 0.0,
button: bool = True,
) -> None:
"""(internal)"""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
assert bui.app.classic is not None
store = bui.app.classic.store
plus = bui.app.plus
assert plus is not None
del boffs_h # unused arg
del boffs_h2 # unused arg
del boffs_v2 # unused arg
item_info = store.get_store_item(item_name)
title_v = 0.24
price_v = 0.145
base_text_scale = 1.0
item['name'] = title = store.get_store_item_name_translated(item_name)
btn: bui.Widget | None
# Hack; showbuffer stuff isn't working well when we're showing merch.
showbuffer = 10 if item_name in {'merch', 'pro', 'pro_sale'} else 76.0
if button:
item['button'] = btn = bui.buttonwidget(
parent=parent_widget,
id=f'{idprefix}|store_item.{item_name}',
position=b_pos,
transition_delay=delay,
show_buffer_top=showbuffer,
enable_sound=False,
button_type='square',
size=(b_width, b_height),
autoselect=True,
label='',
)
bui.widget(edit=btn, show_buffer_bottom=showbuffer)
else:
btn = None
b_offs_x = -0.015 * b_width
check_pos = 0.76
icon_tex = None
tint_tex = None
tint_color = None
tint2_color = None
tex_name: str | None = None
desc: bui.Lstr | None = None
modes: bui.Lstr | None = None
if item_name.startswith('characters.'):
assert bui.app.classic is not None
character = bui.app.classic.spaz_appearances[item_info['character']]
tint_color = (
item_info['color']
if 'color' in item_info
else (
character.default_color
if character.default_color is not None
else (1, 1, 1)
)
)
tint2_color = (
item_info['highlight']
if 'highlight' in item_info
else (
character.default_highlight
if character.default_highlight is not None
else (1, 1, 1)
)
)
icon_tex = character.icon_texture
tint_tex = character.icon_mask_texture
title_v = 0.255
price_v = 0.145
elif item_name == 'merch':
base_text_scale = 0.6
title_v = 0.85
price_v = 0.15
elif item_name in ['upgrades.pro', 'pro']:
base_text_scale = 0.6
title_v = 0.85
price_v = 0.15
elif item_name.startswith('maps.'):
map_type = item_info['map_type']
tex_name = map_type.get_preview_texture_name()
title_v = 0.312
price_v = 0.17
elif item_name.startswith('games.'):
gametype = item_info['gametype']
modes_l = []
if gametype.supports_session_type(bs.CoopSession):
modes_l.append(bui.Lstr(resource='playModes.coopText'))
if gametype.supports_session_type(bs.DualTeamSession):
modes_l.append(bui.Lstr(resource='playModes.teamsText'))
if gametype.supports_session_type(bs.FreeForAllSession):
modes_l.append(bui.Lstr(resource='playModes.freeForAllText'))
if len(modes_l) == 3:
modes = bui.Lstr(
value='${A}, ${B}, ${C}',
subs=[
('${A}', modes_l[0]),
('${B}', modes_l[1]),
('${C}', modes_l[2]),
],
)
elif len(modes_l) == 2:
modes = bui.Lstr(
value='${A}, ${B}',
subs=[('${A}', modes_l[0]), ('${B}', modes_l[1])],
)
elif len(modes_l) == 1:
modes = modes_l[0]
else:
raise RuntimeError()
desc = gametype.get_description_display_string(bs.CoopSession)
tex_name = item_info['previewTex']
base_text_scale = 0.8
title_v = 0.48
price_v = 0.17
elif item_name == 'upgrades.infinite_runaround':
base_text_scale = 0.8
desc = bui.Lstr(
translate=(
'gameDescriptions',
'Prevent enemies from reaching the exit.',
)
)
modes = bui.Lstr(resource='playModes.coopText')
tex_name = 'towerDPreview'
title_v = 0.48
price_v = 0.17
elif item_name == 'upgrades.infinite_onslaught':
base_text_scale = 0.8
desc = bui.Lstr(
translate=(
'gameDescriptions',
'Defeat all enemies.',
)
)
modes = bui.Lstr(resource='playModes.coopText')
tex_name = 'doomShroomPreview'
title_v = 0.48
price_v = 0.17
elif item_name.startswith('icons.'):
base_text_scale = 1.5
price_v = 0.2
check_pos = 0.6
if item_name.startswith('characters.'):
frame_size = b_width * 0.7
im_dim = frame_size * (100.0 / 113.0)
im_pos = (
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
b_pos[1] + b_height * 0.57 - im_dim * 0.5,
)
mask_texture = bui.gettexture('characterIconMask')
assert icon_tex is not None
assert tint_tex is not None
bui.imagewidget(
parent=parent_widget,
position=im_pos,
size=(im_dim, im_dim),
color=(1, 1, 1),
transition_delay=delay,
mask_texture=mask_texture,
draw_controller=btn,
texture=bui.gettexture(icon_tex),
tint_texture=bui.gettexture(tint_tex),
tint_color=tint_color,
tint2_color=tint2_color,
)
if item_name == 'merch':
frame_size = b_width * 0.65
im_dim = frame_size * (100.0 / 113.0)
im_pos = (
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
b_pos[1] + b_height * 0.47 - im_dim * 0.5,
)
bui.imagewidget(
parent=parent_widget,
position=im_pos,
size=(im_dim, im_dim),
transition_delay=delay,
draw_controller=btn,
opacity=1.0,
texture=bui.gettexture('merch'),
)
if item_name in ['pro', 'upgrades.pro']:
frame_size = b_width * 0.5
im_dim = frame_size * (100.0 / 113.0)
im_pos = (
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
b_pos[1] + b_height * 0.5 - im_dim * 0.5,
)
bui.imagewidget(
parent=parent_widget,
position=im_pos,
size=(im_dim, im_dim),
transition_delay=delay,
draw_controller=btn,
color=(0.3, 0.0, 0.3),
opacity=0.3,
texture=bui.gettexture('logo'),
)
txt = bui.Lstr(resource='store.bombSquadProNewDescriptionText')
item['descriptionText'] = bui.textwidget(
parent=parent_widget,
text=txt,
position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.69),
transition_delay=delay,
scale=b_width * (1.0 / 230.0) * base_text_scale * 0.75,
maxwidth=b_width * 0.75,
max_height=b_height * 0.2,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
color=(0.3, 1, 0.3),
)
extra_backings = item['extra_backings'] = []
extra_images = item['extra_images'] = []
extra_texts = item['extra_texts'] = []
extra_texts_2 = item['extra_texts_2'] = []
backing_color = (0.5, 0.8, 0.3) if button else (0.6, 0.5, 0.65)
b_square_texture = bui.gettexture('buttonSquare')
char_mask_texture = bui.gettexture('characterIconMask')
pos = (0.17, 0.43)
tile_size = (b_width * 0.16 * 1.2, b_width * 0.2 * 1.2)
tile_pos = (b_pos[0] + b_width * pos[0], b_pos[1] + b_height * pos[1])
extra_backings.append(
bui.imagewidget(
parent=parent_widget,
position=(
tile_pos[0] - tile_size[0] * 0.5,
tile_pos[1] - tile_size[1] * 0.5,
),
size=tile_size,
transition_delay=delay,
draw_controller=btn,
color=backing_color,
texture=b_square_texture,
)
)
im_size = tile_size[0] * 0.8
extra_images.append(
bui.imagewidget(
parent=parent_widget,
position=(
tile_pos[0] - im_size * 0.5,
tile_pos[1] - im_size * 0.4,
),
size=(im_size, im_size),
transition_delay=delay,
draw_controller=btn,
color=(1, 1, 1),
texture=bui.gettexture('ticketsMore'),
)
)
bonus_tickets = str(
plus.get_v1_account_misc_read_val('proBonusTickets', 100)
)
extra_texts.append(
bui.textwidget(
parent=parent_widget,
draw_controller=btn,
position=(
tile_pos[0] - tile_size[0] * 0.03,
tile_pos[1] - tile_size[1] * 0.25,
),
size=(0, 0),
color=(0.6, 1, 0.6),
transition_delay=delay,
h_align='center',
v_align='center',
maxwidth=tile_size[0] * 0.7,
scale=0.55,
text=bui.Lstr(
resource='getTicketsWindow.ticketsText',
subs=[('${COUNT}', bonus_tickets)],
),
flatness=1.0,
shadow=0.0,
)
)
for charname, pos in [
('Kronk', (0.32, 0.45)),
('Zoe', (0.425, 0.4)),
('Jack Morgan', (0.555, 0.45)),
('Mel', (0.645, 0.4)),
]:
tile_size = (b_width * 0.16 * 0.9, b_width * 0.2 * 0.9)
tile_pos = (
b_pos[0] + b_width * pos[0],
b_pos[1] + b_height * pos[1],
)
assert bui.app.classic is not None
character = bui.app.classic.spaz_appearances[charname]
extra_backings.append(
bui.imagewidget(
parent=parent_widget,
position=(
tile_pos[0] - tile_size[0] * 0.5,
tile_pos[1] - tile_size[1] * 0.5,
),
size=tile_size,
transition_delay=delay,
draw_controller=btn,
color=backing_color,
texture=b_square_texture,
)
)
im_size = tile_size[0] * 0.7
extra_images.append(
bui.imagewidget(
parent=parent_widget,
position=(
tile_pos[0] - im_size * 0.53,
tile_pos[1] - im_size * 0.35,
),
size=(im_size, im_size),
transition_delay=delay,
draw_controller=btn,
color=(1, 1, 1),
texture=bui.gettexture(character.icon_texture),
tint_texture=bui.gettexture(character.icon_mask_texture),
tint_color=character.default_color,
tint2_color=character.default_highlight,
mask_texture=char_mask_texture,
)
)
extra_texts.append(
bui.textwidget(
parent=parent_widget,
draw_controller=btn,
position=(
tile_pos[0] - im_size * 0.03,
tile_pos[1] - im_size * 0.51,
),
size=(0, 0),
color=(0.6, 1, 0.6),
transition_delay=delay,
h_align='center',
v_align='center',
maxwidth=tile_size[0] * 0.7,
scale=0.55,
text=bui.Lstr(translate=('characterNames', charname)),
flatness=1.0,
shadow=0.0,
)
)
# If we have a 'total-worth' item-id for this id, show that price so
# the user knows how much this is worth.
total_worth_item = plus.get_v1_account_misc_read_val('twrths', {}).get(
item_name
)
total_worth_price: str | None
if total_worth_item is not None:
price = plus.get_price(total_worth_item)
total_worth_price = (
store.get_clean_price(price) if price is not None else '??'
)
else:
total_worth_price = None
if total_worth_price is not None:
total_worth_text = bui.Lstr(
resource='store.totalWorthText',
subs=[('${TOTAL_WORTH}', total_worth_price)],
)
extra_texts_2.append(
bui.textwidget(
parent=parent_widget,
text=total_worth_text,
position=(
b_pos[0] + b_width * 0.5 + b_offs_x,
b_pos[1] + b_height * 0.25,
),
transition_delay=delay,
scale=b_width * (1.0 / 230.0) * base_text_scale * 0.45,
maxwidth=b_width * 0.5,
size=(0, 0),
h_align='center',
v_align='center',
shadow=1.0,
flatness=1.0,
draw_controller=btn,
color=(0.3, 1, 1),
)
)
mesh_opaque = bui.getmesh('level_select_button_opaque')
mesh_transparent = bui.getmesh('level_select_button_transparent')
mask_tex = bui.gettexture('mapPreviewMask')
for levelname, preview_tex_name, pos in [
('Infinite Onslaught', 'doomShroomPreview', (0.80, 0.48)),
('Infinite Runaround', 'towerDPreview', (0.80, 0.32)),
]:
tile_size = (b_width * 0.2, b_width * 0.13)
tile_pos = (
b_pos[0] + b_width * pos[0],
b_pos[1] + b_height * pos[1],
)
im_size = tile_size[0] * 0.8
extra_backings.append(
bui.imagewidget(
parent=parent_widget,
position=(
tile_pos[0] - tile_size[0] * 0.5,
tile_pos[1] - tile_size[1] * 0.5,
),
size=tile_size,
transition_delay=delay,
draw_controller=btn,
color=backing_color,
texture=b_square_texture,
)
)
# Hack - gotta draw two transparent versions to avoid z issues.
for mod in mesh_opaque, mesh_transparent:
extra_images.append(
bui.imagewidget(
parent=parent_widget,
position=(
tile_pos[0] - im_size * 0.52,
tile_pos[1] - im_size * 0.2,
),
size=(im_size, im_size * 0.5),
transition_delay=delay,
mesh_transparent=mod,
mask_texture=mask_tex,
draw_controller=btn,
texture=bui.gettexture(preview_tex_name),
)
)
extra_texts.append(
bui.textwidget(
parent=parent_widget,
draw_controller=btn,
position=(
tile_pos[0] - im_size * 0.03,
tile_pos[1] - im_size * 0.2,
),
size=(0, 0),
color=(0.6, 1, 0.6),
transition_delay=delay,
h_align='center',
v_align='center',
maxwidth=tile_size[0] * 0.7,
scale=0.55,
text=bui.Lstr(translate=('coopLevelNames', levelname)),
flatness=1.0,
shadow=0.0,
)
)
if item_name.startswith('icons.'):
item['icon_text'] = bui.textwidget(
parent=parent_widget,
text=item_info['icon'],
position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.5),
transition_delay=delay,
scale=b_width * (1.0 / 230.0) * base_text_scale * 2.0,
maxwidth=b_width * 0.9,
max_height=b_height * 0.9,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
)
if item_name.startswith('maps.'):
frame_size = b_width * 0.9
im_dim = frame_size * (100.0 / 113.0)
im_pos = (
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
b_pos[1] + b_height * 0.62 - im_dim * 0.25,
)
mesh_opaque = bui.getmesh('level_select_button_opaque')
mesh_transparent = bui.getmesh('level_select_button_transparent')
mask_tex = bui.gettexture('mapPreviewMask')
assert tex_name is not None
bui.imagewidget(
parent=parent_widget,
position=im_pos,
size=(im_dim, im_dim * 0.5),
transition_delay=delay,
mesh_opaque=mesh_opaque,
mesh_transparent=mesh_transparent,
mask_texture=mask_tex,
draw_controller=btn,
texture=bui.gettexture(tex_name),
)
if item_name.startswith('games.') or item_name in (
'upgrades.infinite_runaround',
'upgrades.infinite_onslaught',
):
frame_size = b_width * 0.8
im_dim = frame_size * (100.0 / 113.0)
im_pos = (
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
b_pos[1] + b_height * 0.72 - im_dim * 0.25,
)
mesh_opaque = bui.getmesh('level_select_button_opaque')
mesh_transparent = bui.getmesh('level_select_button_transparent')
mask_tex = bui.gettexture('mapPreviewMask')
assert tex_name is not None
bui.imagewidget(
parent=parent_widget,
position=im_pos,
size=(im_dim, im_dim * 0.5),
transition_delay=delay,
mesh_opaque=mesh_opaque,
mesh_transparent=mesh_transparent,
mask_texture=mask_tex,
draw_controller=btn,
texture=bui.gettexture(tex_name),
)
item['descriptionText'] = bui.textwidget(
parent=parent_widget,
text=desc,
position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.36),
transition_delay=delay,
scale=b_width * (1.0 / 230.0) * base_text_scale * 0.78,
maxwidth=b_width * 0.8,
max_height=b_height * 0.14,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
flatness=1.0,
shadow=0.0,
color=(0.6, 1, 0.6),
)
item['gameModesText'] = bui.textwidget(
parent=parent_widget,
text=modes,
position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.26),
transition_delay=delay,
scale=b_width * (1.0 / 230.0) * base_text_scale * 0.65,
maxwidth=b_width * 0.8,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
shadow=0,
flatness=1.0,
color=(0.6, 0.8, 0.6),
)
if not item_name.startswith('icons.'):
item['title_text'] = bui.textwidget(
parent=parent_widget,
text=title,
position=(
b_pos[0] + b_width * 0.5 + b_offs_x,
b_pos[1] + b_height * title_v,
),
transition_delay=delay,
scale=b_width * (1.0 / 230.0) * base_text_scale,
maxwidth=b_width * 0.8,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
color=(0.7, 0.9, 0.7, 1.0),
)
item['purchase_check'] = bui.imagewidget(
parent=parent_widget,
position=(b_pos[0] + b_width * check_pos, b_pos[1] + b_height * 0.05),
transition_delay=delay,
mesh_transparent=bui.getmesh('checkTransparent'),
opacity=0.0,
size=(60, 60),
color=(0.6, 0.5, 0.8),
draw_controller=btn,
texture=bui.gettexture('uiAtlas'),
)
item['price_widget'] = bui.textwidget(
parent=parent_widget,
text='',
position=(
b_pos[0] + b_width * 0.5 + b_offs_x,
b_pos[1] + b_height * price_v,
),
transition_delay=delay,
scale=b_width * (1.0 / 300.0) * base_text_scale,
maxwidth=b_width * 0.9,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
color=(0.2, 1, 0.2, 1.0),
)
item['price_widget_left'] = bui.textwidget(
parent=parent_widget,
text='',
position=(
b_pos[0] + b_width * 0.33 + b_offs_x,
b_pos[1] + b_height * price_v,
),
transition_delay=delay,
scale=b_width * (1.0 / 300.0) * base_text_scale,
maxwidth=b_width * 0.3,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
color=(0.2, 1, 0.2, 0.5),
)
item['price_widget_right'] = bui.textwidget(
parent=parent_widget,
text='',
position=(
b_pos[0] + b_width * 0.66 + b_offs_x,
b_pos[1] + b_height * price_v,
),
transition_delay=delay,
scale=1.1 * b_width * (1.0 / 300.0) * base_text_scale,
maxwidth=b_width * 0.3,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
color=(0.2, 1, 0.2, 1.0),
)
item['price_slash_widget'] = bui.imagewidget(
parent=parent_widget,
position=(
b_pos[0] + b_width * 0.33 + b_offs_x - 36,
b_pos[1] + b_height * price_v - 35,
),
transition_delay=delay,
texture=bui.gettexture('slash'),
opacity=0.0,
size=(70, 70),
draw_controller=btn,
color=(1, 0, 0),
)
badge_rad = 44
badge_center = (
b_pos[0] + b_width * 0.1 + b_offs_x,
b_pos[1] + b_height * 0.87,
)
item['sale_bg_widget'] = bui.imagewidget(
parent=parent_widget,
position=(badge_center[0] - badge_rad, badge_center[1] - badge_rad),
opacity=0.0,
transition_delay=delay,
texture=bui.gettexture('circleZigZag'),
draw_controller=btn,
size=(badge_rad * 2, badge_rad * 2),
color=(0.5, 0, 1),
)
item['sale_title_widget'] = bui.textwidget(
parent=parent_widget,
position=(badge_center[0], badge_center[1] + 12),
transition_delay=delay,
scale=1.0,
maxwidth=badge_rad * 1.6,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
shadow=0.0,
flatness=1.0,
color=(0, 1, 0),
)
item['sale_time_widget'] = bui.textwidget(
parent=parent_widget,
position=(badge_center[0], badge_center[1] - 12),
transition_delay=delay,
scale=0.7,
maxwidth=badge_rad * 1.6,
size=(0, 0),
h_align='center',
v_align='center',
draw_controller=btn,
shadow=0.0,
flatness=1.0,
color=(0.0, 1, 0.0, 1),
)