# Released under the MIT License. See LICENSE for details. # """UI functionality for purchasing/acquiring currency.""" from __future__ import annotations from typing import TYPE_CHECKING import ba import ba.internal if TYPE_CHECKING: from typing import Any class GetCurrencyWindow(ba.Window): """Window for purchasing/acquiring currency.""" def __init__( self, transition: str = 'in_right', from_modal_store: bool = False, modal: bool = False, origin_widget: ba.Widget | None = None, store_back_location: str | None = None, ): # pylint: disable=too-many-statements # pylint: disable=too-many-locals ba.set_analytics_screen('Get Tickets Window') self._transitioning_out = False self._store_back_location = store_back_location # ew. self._ad_button_greyed = False self._smooth_update_timer: ba.Timer | None = None self._ad_button = None self._ad_label = None self._ad_image = None self._ad_time_text = None # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None uiscale = ba.app.ui.uiscale self._width = 1000.0 if uiscale is ba.UIScale.SMALL else 800.0 x_inset = 100.0 if uiscale is ba.UIScale.SMALL else 0.0 self._height = 480.0 self._modal = modal self._from_modal_store = from_modal_store self._r = 'getTicketsWindow' top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 super().__init__( root_widget=ba.containerwidget( size=(self._width, self._height + top_extra), transition=transition, scale_origin_stack_offset=scale_origin, color=(0.4, 0.37, 0.55), scale=( 1.63 if uiscale is ba.UIScale.SMALL else 1.2 if uiscale is ba.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -3) if uiscale is ba.UIScale.SMALL else (0, 0), ) ) btn = ba.buttonwidget( parent=self._root_widget, position=(55 + x_inset, self._height - 79), size=(140, 60), scale=1.0, autoselect=True, label=ba.Lstr(resource='doneText' if modal else 'backText'), button_type='regular' if modal else 'back', on_activate_call=self._back, ) ba.containerwidget(edit=self._root_widget, cancel_button=btn) ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 55), size=(0, 0), color=ba.app.ui.title_color, scale=1.2, h_align='center', v_align='center', text=ba.Lstr(resource=self._r + '.titleText'), maxwidth=290, ) if not modal: ba.buttonwidget( edit=btn, button_type='backSmall', size=(60, 60), label=ba.charstr(ba.SpecialChar.BACK), ) b_size = (220.0, 180.0) v = self._height - b_size[1] - 80 spacing = 1 self._ad_button = None def _add_button( item: str, position: tuple[float, float], size: tuple[float, float], label: ba.Lstr, price: str | None = None, tex_name: str | None = None, tex_opacity: float = 1.0, tex_scale: float = 1.0, enabled: bool = True, text_scale: float = 1.0, ) -> ba.Widget: btn2 = ba.buttonwidget( parent=self._root_widget, position=position, button_type='square', size=size, label='', autoselect=True, color=None if enabled else (0.5, 0.5, 0.5), on_activate_call=( ba.Call(self._purchase, item) if enabled else self._disabled_press ), ) txt = ba.textwidget( parent=self._root_widget, text=label, position=( position[0] + size[0] * 0.5, position[1] + size[1] * 0.3, ), scale=text_scale, maxwidth=size[0] * 0.75, size=(0, 0), h_align='center', v_align='center', draw_controller=btn2, color=(0.7, 0.9, 0.7, 1.0 if enabled else 0.2), ) if price is not None and enabled: ba.textwidget( parent=self._root_widget, text=price, position=( position[0] + size[0] * 0.5, position[1] + size[1] * 0.17, ), scale=0.7, maxwidth=size[0] * 0.75, size=(0, 0), h_align='center', v_align='center', draw_controller=btn2, color=(0.4, 0.9, 0.4, 1.0), ) i = None if tex_name is not None: tex_size = 90.0 * tex_scale i = ba.imagewidget( parent=self._root_widget, texture=ba.gettexture(tex_name), position=( position[0] + size[0] * 0.5 - tex_size * 0.5, position[1] + size[1] * 0.66 - tex_size * 0.5, ), size=(tex_size, tex_size), draw_controller=btn2, opacity=tex_opacity * (1.0 if enabled else 0.25), ) if item == 'ad': self._ad_button = btn2 self._ad_label = txt assert i is not None self._ad_image = i self._ad_time_text = ba.textwidget( parent=self._root_widget, text='1m 10s', position=( position[0] + size[0] * 0.5, position[1] + size[1] * 0.5, ), scale=text_scale * 1.2, maxwidth=size[0] * 0.85, size=(0, 0), h_align='center', v_align='center', draw_controller=btn2, color=(0.4, 0.9, 0.4, 1.0), ) return btn2 rsrc = self._r + '.ticketsText' c2txt = ba.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( ba.internal.get_v1_account_misc_read_val( 'tickets2Amount', 500 ) ), ) ], ) c3txt = ba.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( ba.internal.get_v1_account_misc_read_val( 'tickets3Amount', 1500 ) ), ) ], ) c4txt = ba.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( ba.internal.get_v1_account_misc_read_val( 'tickets4Amount', 5000 ) ), ) ], ) c5txt = ba.Lstr( resource=rsrc, subs=[ ( '${COUNT}', str( ba.internal.get_v1_account_misc_read_val( 'tickets5Amount', 15000 ) ), ) ], ) h = 110.0 # enable buttons if we have prices.. tickets2_price = ba.internal.get_price('tickets2') tickets3_price = ba.internal.get_price('tickets3') tickets4_price = ba.internal.get_price('tickets4') tickets5_price = ba.internal.get_price('tickets5') # TEMP # tickets1_price = '$0.99' # tickets2_price = '$4.99' # tickets3_price = '$9.99' # tickets4_price = '$19.99' # tickets5_price = '$49.99' _add_button( 'tickets2', enabled=(tickets2_price is not None), position=( self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h, v, ), size=b_size, label=c2txt, price=tickets2_price, tex_name='ticketsMore', ) # 0.99-ish _add_button( 'tickets3', enabled=(tickets3_price is not None), position=( self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h, v, ), size=b_size, label=c3txt, price=tickets3_price, tex_name='ticketRoll', ) # 4.99-ish v -= b_size[1] - 5 _add_button( 'tickets4', enabled=(tickets4_price is not None), position=( self._width * 0.5 - spacing * 1.5 - b_size[0] * 2.0 + h, v, ), size=b_size, label=c4txt, price=tickets4_price, tex_name='ticketRollBig', tex_scale=1.2, ) # 9.99-ish _add_button( 'tickets5', enabled=(tickets5_price is not None), position=( self._width * 0.5 - spacing * 0.5 - b_size[0] * 1.0 + h, v, ), size=b_size, label=c5txt, price=tickets5_price, tex_name='ticketRolls', tex_scale=1.2, ) # 19.99-ish self._enable_ad_button = ba.internal.has_video_ads() h = self._width * 0.5 + 110.0 v = self._height - b_size[1] - 115.0 if self._enable_ad_button: h_offs = 35 b_size_3 = (150, 120) cdb = _add_button( 'ad', position=(h + h_offs, v), size=b_size_3, label=ba.Lstr( resource=self._r + '.ticketsFromASponsorText', subs=[ ( '${COUNT}', str( ba.internal.get_v1_account_misc_read_val( 'sponsorTickets', 5 ) ), ) ], ), tex_name='ticketsMore', enabled=self._enable_ad_button, tex_opacity=0.6, tex_scale=0.7, text_scale=0.7, ) ba.buttonwidget( edit=cdb, color=(0.65, 0.5, 0.7) if self._enable_ad_button else (0.5, 0.5, 0.5), ) self._ad_free_text = ba.textwidget( parent=self._root_widget, text=ba.Lstr(resource=self._r + '.freeText'), position=( h + h_offs + b_size_3[0] * 0.5, v + b_size_3[1] * 0.5 + 25, ), size=(0, 0), color=(1, 1, 0, 1.0) if self._enable_ad_button else (1, 1, 1, 0.2), draw_controller=cdb, rotate=15, shadow=1.0, maxwidth=150, h_align='center', v_align='center', scale=1.0, ) v -= 125 else: v -= 20 if True: # pylint: disable=using-constant-test h_offs = 35 b_size_3 = (150, 120) cdb = _add_button( 'app_invite', position=(h + h_offs, v), size=b_size_3, label=ba.Lstr( resource='gatherWindow.earnTicketsForRecommendingText', subs=[ ( '${COUNT}', str( ba.internal.get_v1_account_misc_read_val( 'sponsorTickets', 5 ) ), ) ], ), tex_name='ticketsMore', enabled=True, tex_opacity=0.6, tex_scale=0.7, text_scale=0.7, ) ba.buttonwidget(edit=cdb, color=(0.65, 0.5, 0.7)) ba.textwidget( parent=self._root_widget, text=ba.Lstr(resource=self._r + '.freeText'), position=( h + h_offs + b_size_3[0] * 0.5, v + b_size_3[1] * 0.5 + 25, ), size=(0, 0), color=(1, 1, 0, 1.0), draw_controller=cdb, rotate=15, shadow=1.0, maxwidth=150, h_align='center', v_align='center', scale=1.0, ) tc_y_offs = 0 h = self._width - (185 + x_inset) v = self._height - 95 + tc_y_offs txt1 = ( ba.Lstr(resource=self._r + '.youHaveText') .evaluate() .partition('${COUNT}')[0] .strip() ) txt2 = ( ba.Lstr(resource=self._r + '.youHaveText') .evaluate() .rpartition('${COUNT}')[-1] .strip() ) ba.textwidget( parent=self._root_widget, text=txt1, position=(h, v), size=(0, 0), color=(0.5, 0.5, 0.6), maxwidth=200, h_align='center', v_align='center', scale=0.8, ) v -= 30 self._ticket_count_text = ba.textwidget( parent=self._root_widget, position=(h, v), size=(0, 0), color=(0.2, 1.0, 0.2), maxwidth=200, h_align='center', v_align='center', scale=1.6, ) v -= 30 ba.textwidget( parent=self._root_widget, text=txt2, position=(h, v), size=(0, 0), color=(0.5, 0.5, 0.6), maxwidth=200, h_align='center', v_align='center', scale=0.8, ) # update count now and once per second going forward.. self._ticking_node: ba.Node | None = None self._smooth_ticket_count: float | None = None self._ticket_count = 0 self._update() self._update_timer = ba.Timer( 1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, repeat=True, ) self._smooth_increase_speed = 1.0 def __del__(self) -> None: if self._ticking_node is not None: self._ticking_node.delete() self._ticking_node = None def _smooth_update(self) -> None: if not self._ticket_count_text: self._smooth_update_timer = None return finished = False # if we're going down, do it immediately assert self._smooth_ticket_count is not None if int(self._smooth_ticket_count) >= self._ticket_count: self._smooth_ticket_count = float(self._ticket_count) finished = True else: # we're going up; start a sound if need be self._smooth_ticket_count = min( self._smooth_ticket_count + 1.0 * self._smooth_increase_speed, self._ticket_count, ) if int(self._smooth_ticket_count) >= self._ticket_count: finished = True self._smooth_ticket_count = float(self._ticket_count) elif self._ticking_node is None: with ba.Context('ui'): self._ticking_node = ba.newnode( 'sound', attrs={ 'sound': ba.getsound('scoreIncrease'), 'positional': False, }, ) ba.textwidget( edit=self._ticket_count_text, text=str(int(self._smooth_ticket_count)), ) # if we've reached the target, kill the timer/sound/etc if finished: self._smooth_update_timer = None if self._ticking_node is not None: self._ticking_node.delete() self._ticking_node = None ba.playsound(ba.getsound('cashRegister2')) def _update(self) -> None: import datetime # if we somehow get signed out, just die.. if ba.internal.get_v1_account_state() != 'signed_in': self._back() return self._ticket_count = ba.internal.get_v1_account_ticket_count() # update our incentivized ad button depending on whether ads are # available if self._ad_button is not None: next_reward_ad_time = ba.internal.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None ) if next_reward_ad_time is not None: next_reward_ad_time = datetime.datetime.utcfromtimestamp( next_reward_ad_time ) now = datetime.datetime.utcnow() if ba.internal.have_incentivized_ad() and ( next_reward_ad_time is None or next_reward_ad_time <= now ): self._ad_button_greyed = False ba.buttonwidget(edit=self._ad_button, color=(0.65, 0.5, 0.7)) ba.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 1.0)) ba.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 1)) ba.imagewidget(edit=self._ad_image, opacity=0.6) ba.textwidget(edit=self._ad_time_text, text='') else: self._ad_button_greyed = True ba.buttonwidget(edit=self._ad_button, color=(0.5, 0.5, 0.5)) ba.textwidget(edit=self._ad_label, color=(0.7, 0.9, 0.7, 0.2)) ba.textwidget(edit=self._ad_free_text, color=(1, 1, 0, 0.2)) ba.imagewidget(edit=self._ad_image, opacity=0.6 * 0.25) sval: str | ba.Lstr if ( next_reward_ad_time is not None and next_reward_ad_time > now ): sval = ba.timestring( (next_reward_ad_time - now).total_seconds() * 1000.0, centi=False, timeformat=ba.TimeFormat.MILLISECONDS, ) else: sval = '' ba.textwidget(edit=self._ad_time_text, text=sval) # if this is our first update, assign immediately; otherwise kick # off a smooth transition if the value has changed if self._smooth_ticket_count is None: self._smooth_ticket_count = float(self._ticket_count) self._smooth_update() # will set the text widget elif ( self._ticket_count != int(self._smooth_ticket_count) and self._smooth_update_timer is None ): self._smooth_update_timer = ba.Timer( 0.05, ba.WeakCall(self._smooth_update), repeat=True, timetype=ba.TimeType.REAL, ) diff = abs(float(self._ticket_count) - self._smooth_ticket_count) self._smooth_increase_speed = ( diff / 100.0 if diff >= 5000 else diff / 50.0 if diff >= 1500 else diff / 30.0 if diff >= 500 else diff / 15.0 ) def _disabled_press(self) -> None: # if we're on a platform without purchases, inform the user they # can link their accounts and buy stuff elsewhere app = ba.app if ( app.test_build or ( app.platform == 'android' and app.subplatform in ['oculus', 'cardboard'] ) ) and ba.internal.get_v1_account_misc_read_val( 'allowAccountLinking2', False ): ba.screenmessage( ba.Lstr(resource=self._r + '.unavailableLinkAccountText'), color=(1, 0.5, 0), ) else: ba.screenmessage( ba.Lstr(resource=self._r + '.unavailableText'), color=(1, 0.5, 0), ) ba.playsound(ba.getsound('error')) def _purchase(self, item: str) -> None: from bastd.ui import account from bastd.ui import appinvite from ba.internal import master_server_get if item == 'app_invite': if ba.internal.get_v1_account_state() != 'signed_in': account.show_sign_in_prompt() return appinvite.handle_app_invites_press() return # here we ping the server to ask if it's valid for us to # purchase this.. (better to fail now than after we've paid locally) app = ba.app master_server_get( 'bsAccountPurchaseCheck', { 'item': item, 'platform': app.platform, 'subplatform': app.subplatform, 'version': app.version, 'buildNumber': app.build_number, }, callback=ba.WeakCall(self._purchase_check_result, item), ) def _purchase_check_result( self, item: str, result: dict[str, Any] | None ) -> None: if result is None: ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) else: if result['allow']: self._do_purchase(item) else: if result['reason'] == 'versionTooOld': ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource='getTicketsWindow.versionTooOldText'), color=(1, 0, 0), ) else: ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0), ) # actually start the purchase locally.. def _do_purchase(self, item: str) -> None: if item == 'ad': import datetime # if ads are disabled until some time, error.. next_reward_ad_time = ba.internal.get_v1_account_misc_read_val_2( 'nextRewardAdTime', None ) if next_reward_ad_time is not None: next_reward_ad_time = datetime.datetime.utcfromtimestamp( next_reward_ad_time ) now = datetime.datetime.utcnow() if ( next_reward_ad_time is not None and next_reward_ad_time > now ) or self._ad_button_greyed: ba.playsound(ba.getsound('error')) ba.screenmessage( ba.Lstr( resource='getTicketsWindow.unavailableTemporarilyText' ), color=(1, 0, 0), ) elif self._enable_ad_button: ba.app.ads.show_ad('tickets') else: ba.internal.purchase(item) def _back(self) -> None: from bastd.ui.store import browser if self._transitioning_out: return ba.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: window = browser.StoreBrowserWindow( transition='in_left', modal=self._from_modal_store, back_location=self._store_back_location, ).get_root_widget() if not self._from_modal_store: ba.app.ui.set_main_menu_window(window) self._transitioning_out = True def show_get_tickets_prompt() -> None: """Show a 'not enough tickets' prompt with an option to purchase more. Note that the purchase option may not always be available depending on the build of the game. """ from bastd.ui.confirm import ConfirmWindow if ba.app.allow_ticket_purchases: ConfirmWindow( ba.Lstr( translate=( 'serverResponses', 'You don\'t have enough tickets for this!', ) ), lambda: GetCurrencyWindow(modal=True), ok_text=ba.Lstr(resource='getTicketsWindow.titleText'), width=460, height=130, ) else: ConfirmWindow( ba.Lstr( translate=( 'serverResponses', 'You don\'t have enough tickets for this!', ) ), cancel_button=False, width=460, height=130, )