def __init__(self, offer: Dict[str, Any], transition: str = 'in_right'): # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals from ba.internal import (get_store_item_display_size, get_clean_price) from ba import SpecialChar from bastd.ui.store import item as storeitemui self._cancel_delay = offer.get('cancelDelay', 0) # First thing: if we're offering pro or an IAP, see if we have a # price for it. # If not, abort and go into zombie mode (the user should never see # us that way). real_price: Optional[str] # Misnomer: 'pro' actually means offer 'pro_sale'. if offer['item'] in ['pro', 'pro_fullprice']: real_price = _ba.get_price('pro' if offer['item'] == 'pro_fullprice' else 'pro_sale') if real_price is None and ba.app.debug_build: print('NOTE: Faking prices for debug build.') real_price = '$1.23' zombie = real_price is None elif isinstance(offer['price'], str): # (a string price implies IAP id) real_price = _ba.get_price(offer['price']) if real_price is None and ba.app.debug_build: print('NOTE: Faking price for debug build.') real_price = '$1.23' zombie = real_price is None else: real_price = None zombie = False if real_price is None: real_price = '?' if offer['item'] in ['pro', 'pro_fullprice']: self._offer_item = 'pro' else: self._offer_item = offer['item'] # If we wanted a real price but didn't find one, go zombie. if zombie: return # This can pop up suddenly, so lets block input for 1 second. _ba.lock_all_input() ba.timer(1.0, _ba.unlock_all_input, timetype=ba.TimeType.REAL) ba.playsound(ba.getsound('ding')) ba.timer(0.3, lambda: ba.playsound(ba.getsound('ooh')), timetype=ba.TimeType.REAL) self._offer = copy.deepcopy(offer) self._width = 580 self._height = 590 uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height), transition=transition, scale=(1.2 if uiscale is ba.UIScale.SMALL else 1.15 if uiscale is ba.UIScale.MEDIUM else 1.0), stack_offset=(0, -15) if uiscale is ba.UIScale.SMALL else (0, 0))) self._is_bundle_sale = False try: if offer['item'] in ['pro', 'pro_fullprice']: original_price_str = _ba.get_price('pro') if original_price_str is None: original_price_str = '?' new_price_str = _ba.get_price('pro_sale') if new_price_str is None: new_price_str = '?' percent_off_text = '' else: # If the offer includes bonus tickets, it's a bundle-sale. if ('bonusTickets' in offer and offer['bonusTickets'] is not None): self._is_bundle_sale = True original_price = _ba.get_account_misc_read_val( 'price.' + self._offer_item, 9999) # For pure ticket prices we can show a percent-off. if isinstance(offer['price'], int): new_price = offer['price'] tchar = ba.charstr(SpecialChar.TICKET) original_price_str = tchar + str(original_price) new_price_str = tchar + str(new_price) percent_off = int( round(100.0 - (float(new_price) / original_price) * 100.0)) percent_off_text = ' ' + ba.Lstr( resource='store.salePercentText').evaluate().replace( '${PERCENT}', str(percent_off)) else: original_price_str = new_price_str = '?' percent_off_text = '' except Exception: print(f'Offer: {offer}') ba.print_exception('Error setting up special-offer') original_price_str = new_price_str = '?' percent_off_text = '' # If its a bundle sale, change the title. if self._is_bundle_sale: sale_text = ba.Lstr(resource='store.saleBundleText', fallback_resource='store.saleText').evaluate() else: # For full pro we say 'Upgrade?' since its not really a sale. if offer['item'] == 'pro_fullprice': sale_text = ba.Lstr( resource='store.upgradeQuestionText', fallback_resource='store.saleExclaimText').evaluate() else: sale_text = ba.Lstr( resource='store.saleExclaimText', fallback_resource='store.saleText').evaluate() self._title_text = ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 40), size=(0, 0), text=sale_text + ((' ' + ba.Lstr(resource='store.oneTimeOnlyText').evaluate()) if self._offer['oneTimeOnly'] else '') + percent_off_text, h_align='center', v_align='center', maxwidth=self._width * 0.9 - 220, scale=1.4, color=(0.3, 1, 0.3)) self._flash_on = False self._flashing_timer: Optional[ba.Timer] = ba.Timer( 0.05, ba.WeakCall(self._flash_cycle), repeat=True, timetype=ba.TimeType.REAL) ba.timer(0.6, ba.WeakCall(self._stop_flashing), timetype=ba.TimeType.REAL) size = get_store_item_display_size(self._offer_item) display: Dict[str, Any] = {} storeitemui.instantiate_store_item_display( self._offer_item, display, parent_widget=self._root_widget, b_pos=(self._width * 0.5 - size[0] * 0.5 + 10 - ((size[0] * 0.5 + 30) if self._is_bundle_sale else 0), self._height * 0.5 - size[1] * 0.5 + 20 + (20 if self._is_bundle_sale else 0)), b_width=size[0], b_height=size[1], button=not self._is_bundle_sale) # Wire up the parts we need. if self._is_bundle_sale: self._plus_text = ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, self._height * 0.5 + 50), size=(0, 0), text='+', h_align='center', v_align='center', maxwidth=self._width * 0.9, scale=1.4, color=(0.5, 0.5, 0.5)) self._plus_tickets = ba.textwidget( parent=self._root_widget, position=(self._width * 0.5 + 120, self._height * 0.5 + 50), size=(0, 0), text=ba.charstr(SpecialChar.TICKET_BACKING) + str(offer['bonusTickets']), h_align='center', v_align='center', maxwidth=self._width * 0.9, scale=2.5, color=(0.2, 1, 0.2)) self._price_text = ba.textwidget(parent=self._root_widget, position=(self._width * 0.5, 150), size=(0, 0), text=real_price, h_align='center', v_align='center', maxwidth=self._width * 0.9, scale=1.4, color=(0.2, 1, 0.2)) # Total-value if they supplied it. total_worth_item = offer.get('valueItem', None) if total_worth_item is not None: price = _ba.get_price(total_worth_item) total_worth_price = (get_clean_price(price) if price is not None else None) if total_worth_price is not None: total_worth_text = ba.Lstr(resource='store.totalWorthText', subs=[('${TOTAL_WORTH}', total_worth_price)]) self._total_worth_text = ba.textwidget( parent=self._root_widget, text=total_worth_text, position=(self._width * 0.5, 210), scale=0.9, maxwidth=self._width * 0.7, size=(0, 0), h_align='center', v_align='center', shadow=1.0, flatness=1.0, color=(0.3, 1, 1)) elif offer['item'] == 'pro_fullprice': # for full-price pro we simply show full price ba.textwidget(edit=display['price_widget'], text=real_price) ba.buttonwidget(edit=display['button'], on_activate_call=self._purchase) else: # Show old/new prices otherwise (for pro sale). ba.buttonwidget(edit=display['button'], on_activate_call=self._purchase) ba.imagewidget(edit=display['price_slash_widget'], opacity=1.0) ba.textwidget(edit=display['price_widget_left'], text=original_price_str) ba.textwidget(edit=display['price_widget_right'], text=new_price_str) # Add ticket button only if this is ticket-purchasable. if isinstance(offer.get('price'), int): self._get_tickets_button = ba.buttonwidget( parent=self._root_widget, position=(self._width - 125, self._height - 68), size=(90, 55), scale=1.0, button_type='square', color=(0.7, 0.5, 0.85), textcolor=(0.2, 1, 0.2), autoselect=True, label=ba.Lstr(resource='getTicketsWindow.titleText'), on_activate_call=self._on_get_more_tickets_press) self._ticket_text_update_timer = ba.Timer( 1.0, ba.WeakCall(self._update_tickets_text), timetype=ba.TimeType.REAL, repeat=True) self._update_tickets_text() self._update_timer = ba.Timer(1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, repeat=True) self._cancel_button = ba.buttonwidget( parent=self._root_widget, position=(50, 40) if self._is_bundle_sale else (self._width * 0.5 - 75, 40), size=(150, 60), scale=1.0, on_activate_call=self._cancel, autoselect=True, label=ba.Lstr(resource='noThanksText')) self._cancel_countdown_text = ba.textwidget( parent=self._root_widget, text='', position=(50 + 150 + 20, 40 + 27) if self._is_bundle_sale else (self._width * 0.5 - 75 + 150 + 20, 40 + 27), scale=1.1, size=(0, 0), h_align='left', v_align='center', shadow=1.0, flatness=1.0, color=(0.6, 0.5, 0.5)) self._update_cancel_button_graphics() if self._is_bundle_sale: self._purchase_button = ba.buttonwidget( parent=self._root_widget, position=(self._width - 200, 40), size=(150, 60), scale=1.0, on_activate_call=self._purchase, autoselect=True, label=ba.Lstr(resource='store.purchaseText')) ba.containerwidget(edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._purchase_button if self._is_bundle_sale else None, selected_child=self._purchase_button if self._is_bundle_sale else display['button'])
def instantiate(self, scrollwidget: ba.Widget, tab_button: ba.Widget) -> None: """Create the store.""" # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks from bastd.ui.store import item as storeitemui title_spacing = 40 button_border = 20 button_spacing = 4 boffs_h = 40 self._height = 80.0 # Calc total height. for i, section in enumerate(self._sections): if section['title'] != '': assert self._height is not None self._height += title_spacing b_width, b_height = section['button_size'] b_column_count = int( math.floor((self._width - boffs_h - 20) / (b_width + button_spacing))) b_row_count = int( math.ceil( float(len(section['items'])) / b_column_count)) b_height_total = ( 2 * button_border + b_row_count * b_height + (b_row_count - 1) * section['v_spacing']) self._height += b_height_total assert self._height is not None cnt2 = ba.containerwidget(parent=scrollwidget, scale=1.0, size=(self._width, self._height), background=False) ba.containerwidget(edit=cnt2, claims_left_right=True, claims_tab=True, selection_loop_to_parent=True) v = self._height - 20 if self._tab == 'characters': txt = ba.Lstr( resource='store.howToSwitchCharactersText', subs=[ ('${SETTINGS}', ba.Lstr( resource='accountSettingsWindow.titleText' )), ('${PLAYER_PROFILES}', ba.Lstr( resource='playerProfilesWindow.titleText') ) ]) ba.textwidget(parent=cnt2, text=txt, size=(0, 0), position=(self._width * 0.5, self._height - 28), h_align='center', v_align='center', color=(0.7, 1, 0.7, 0.4), scale=0.7, shadow=0, flatness=1.0, maxwidth=700, transition_delay=0.4) elif self._tab == 'icons': txt = ba.Lstr( resource='store.howToUseIconsText', subs=[ ('${SETTINGS}', ba.Lstr(resource='mainMenu.settingsText')), ('${PLAYER_PROFILES}', ba.Lstr( resource='playerProfilesWindow.titleText') ) ]) ba.textwidget(parent=cnt2, text=txt, size=(0, 0), position=(self._width * 0.5, self._height - 28), h_align='center', v_align='center', color=(0.7, 1, 0.7, 0.4), scale=0.7, shadow=0, flatness=1.0, maxwidth=700, transition_delay=0.4) elif self._tab == 'maps': assert self._width is not None assert self._height is not None txt = ba.Lstr(resource='store.howToUseMapsText') ba.textwidget(parent=cnt2, text=txt, size=(0, 0), position=(self._width * 0.5, self._height - 28), h_align='center', v_align='center', color=(0.7, 1, 0.7, 0.4), scale=0.7, shadow=0, flatness=1.0, maxwidth=700, transition_delay=0.4) prev_row_buttons: Optional[List] = None this_row_buttons = [] delay = 0.3 for section in self._sections: if section['title'] != '': ba.textwidget( parent=cnt2, position=(60, v - title_spacing * 0.8), size=(0, 0), scale=1.0, transition_delay=delay, color=(0.7, 0.9, 0.7, 1), h_align='left', v_align='center', text=ba.Lstr(resource=section['title']), maxwidth=self._width * 0.7) v -= title_spacing delay = max(0.100, delay - 0.100) v -= button_border b_width, b_height = section['button_size'] b_count = len(section['items']) b_column_count = int( math.floor((self._width - boffs_h - 20) / (b_width + button_spacing))) col = 0 item: Dict[str, Any] assert self._store_window.button_infos is not None for i, item_name in enumerate(section['items']): item = self._store_window.button_infos[ item_name] = {} item['call'] = ba.WeakCall(self._store_window.buy, item_name) if 'x_offs' in section: boffs_h2 = section['x_offs'] else: boffs_h2 = 0 if 'y_offs' in section: boffs_v2 = section['y_offs'] else: boffs_v2 = 0 b_pos = (boffs_h + boffs_h2 + (b_width + button_spacing) * col, v - b_height + boffs_v2) storeitemui.instantiate_store_item_display( item_name, item, parent_widget=cnt2, b_pos=b_pos, boffs_h=boffs_h, b_width=b_width, b_height=b_height, boffs_h2=boffs_h2, boffs_v2=boffs_v2, delay=delay) btn = item['button'] delay = max(0.1, delay - 0.1) this_row_buttons.append(btn) # Wire this button to the equivalent in the # previous row. if prev_row_buttons is not None: # pylint: disable=unsubscriptable-object if len(prev_row_buttons) > col: ba.widget(edit=btn, up_widget=prev_row_buttons[col]) ba.widget(edit=prev_row_buttons[col], down_widget=btn) # If we're the last button in our row, # wire any in the previous row past # our position to go to us if down is # pressed. if (col + 1 == b_column_count or i == b_count - 1): for b_prev in prev_row_buttons[col + 1:]: ba.widget(edit=b_prev, down_widget=btn) else: ba.widget(edit=btn, up_widget=prev_row_buttons[-1]) else: ba.widget(edit=btn, up_widget=tab_button) col += 1 if col == b_column_count or i == b_count - 1: prev_row_buttons = this_row_buttons this_row_buttons = [] col = 0 v -= b_height if i < b_count - 1: v -= section['v_spacing'] v -= button_border # Set a timer to update these buttons periodically as long # as we're alive (so if we buy one it will grey out, etc). self._store_window.update_buttons_timer = ba.Timer( 0.5, ba.WeakCall(self._store_window.update_buttons), repeat=True, timetype=ba.TimeType.REAL) # Also update them immediately. self._store_window.update_buttons()
def __init__(self, items: List[str], transition: str = 'in_right', header_text: ba.Lstr = None): from ba.internal import get_store_item_display_size from bastd.ui.store import item as storeitemui if header_text is None: header_text = ba.Lstr(resource='unlockThisText', fallback_resource='unlockThisInTheStoreText') if len(items) != 1: raise ValueError('expected exactly 1 item') self._items = list(items) self._width = 580 self._height = 520 uiscale = ba.app.ui.uiscale super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height), transition=transition, toolbar_visibility='menu_currency', scale=(1.2 if uiscale is ba.UIScale.SMALL else 1.1 if uiscale is ba.UIScale.MEDIUM else 1.0), stack_offset=(0, -15) if uiscale is ba.UIScale.SMALL else (0, 0))) self._is_double = False self._title_text = ba.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 = get_store_item_display_size(items[0]) display: Dict[str, Any] = {} storeitemui.instantiate_store_item_display( items[0], display, 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 = _ba.get_price(self._items[0]) pyoffs = -15 else: pyoffs = 0 price = self._price = _ba.get_account_misc_read_val( 'price.' + str(items[0]), -1) price_str = ba.charstr(ba.SpecialChar.TICKET) + str(price) self._price_text = ba.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 = ba.Timer(1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, repeat=True) self._cancel_button = ba.buttonwidget( parent=self._root_widget, position=(50, 40), size=(150, 60), scale=1.0, on_activate_call=self._cancel, autoselect=True, label=ba.Lstr(resource='cancelText')) self._purchase_button = ba.buttonwidget( parent=self._root_widget, position=(self._width - 200, 40), size=(150, 60), scale=1.0, on_activate_call=self._purchase, autoselect=True, label=ba.Lstr(resource='store.purchaseText')) ba.containerwidget(edit=self._root_widget, cancel_button=self._cancel_button, start_button=self._purchase_button, selected_child=self._purchase_button)