# Released under the MIT License. See LICENSE for details. # """Store related functionality for classic mode.""" from __future__ import annotations from typing import TYPE_CHECKING import _ba from ba import _internal if TYPE_CHECKING: from typing import Any import ba def get_store_item(item: str) -> dict[str, Any]: """(internal)""" return get_store_items()[item] def get_store_item_name_translated(item_name: str) -> ba.Lstr: """Return a ba.Lstr for a store item name.""" # pylint: disable=cyclic-import from ba import _language from ba import _map item_info = get_store_item(item_name) if item_name.startswith('characters.'): return _language.Lstr( translate=('characterNames', item_info['character']) ) if item_name in ['merch']: return _language.Lstr(resource='merchText') if item_name in ['upgrades.pro', 'pro']: return _language.Lstr( resource='store.bombSquadProNameText', subs=[('${APP_NAME}', _language.Lstr(resource='titleText'))], ) if item_name.startswith('maps.'): map_type: type[ba.Map] = item_info['map_type'] return _map.get_map_display_string(map_type.name) if item_name.startswith('games.'): gametype: type[ba.GameActivity] = item_info['gametype'] return gametype.get_display_string() if item_name.startswith('icons.'): return _language.Lstr(resource='editProfileWindow.iconText') raise ValueError('unrecognized item: ' + item_name) def get_store_item_display_size(item_name: str) -> tuple[float, float]: """(internal)""" if item_name.startswith('characters.'): return 340 * 0.6, 430 * 0.6 if item_name in ['pro', 'upgrades.pro', 'merch']: from ba._generated.enums import UIScale return 650 * 0.9, 500 * ( 0.72 if ( _ba.app.config.get('Merch Link') and _ba.app.ui.uiscale is UIScale.SMALL ) else 0.85 ) if item_name.startswith('maps.'): return 510 * 0.6, 450 * 0.6 if item_name.startswith('icons.'): return 265 * 0.6, 250 * 0.6 return 450 * 0.6, 450 * 0.6 def get_store_items() -> dict[str, dict]: """Returns info about purchasable items. (internal) """ # pylint: disable=cyclic-import from ba._generated.enums import SpecialChar from bastd import maps if _ba.app.store_items is None: from bastd.game import ninjafight from bastd.game import meteorshower from bastd.game import targetpractice from bastd.game import easteregghunt # IMPORTANT - need to keep this synced with the master server. # (doing so manually for now) _ba.app.store_items = { 'characters.kronk': {'character': 'Kronk'}, 'characters.zoe': {'character': 'Zoe'}, 'characters.jackmorgan': {'character': 'Jack Morgan'}, 'characters.mel': {'character': 'Mel'}, 'characters.snakeshadow': {'character': 'Snake Shadow'}, 'characters.bones': {'character': 'Bones'}, 'characters.bernard': { 'character': 'Bernard', 'highlight': (0.6, 0.5, 0.8), }, 'characters.pixie': {'character': 'Pixel'}, 'characters.wizard': {'character': 'Grumbledorf'}, 'characters.frosty': {'character': 'Frosty'}, 'characters.pascal': {'character': 'Pascal'}, 'characters.cyborg': {'character': 'B-9000'}, 'characters.agent': {'character': 'Agent Johnson'}, 'characters.taobaomascot': {'character': 'Taobao Mascot'}, 'characters.santa': {'character': 'Santa Claus'}, 'characters.bunny': {'character': 'Easter Bunny'}, 'merch': {}, 'pro': {}, 'maps.lake_frigid': {'map_type': maps.LakeFrigid}, 'games.ninja_fight': { 'gametype': ninjafight.NinjaFightGame, 'previewTex': 'courtyardPreview', }, 'games.meteor_shower': { 'gametype': meteorshower.MeteorShowerGame, 'previewTex': 'rampagePreview', }, 'games.target_practice': { 'gametype': targetpractice.TargetPracticeGame, 'previewTex': 'doomShroomPreview', }, 'games.easter_egg_hunt': { 'gametype': easteregghunt.EasterEggHuntGame, 'previewTex': 'towerDPreview', }, 'icons.flag_us': { 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES) }, 'icons.flag_mexico': {'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)}, 'icons.flag_germany': { 'icon': _ba.charstr(SpecialChar.FLAG_GERMANY) }, 'icons.flag_brazil': {'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)}, 'icons.flag_russia': {'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)}, 'icons.flag_china': {'icon': _ba.charstr(SpecialChar.FLAG_CHINA)}, 'icons.flag_uk': { 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM) }, 'icons.flag_canada': {'icon': _ba.charstr(SpecialChar.FLAG_CANADA)}, 'icons.flag_india': {'icon': _ba.charstr(SpecialChar.FLAG_INDIA)}, 'icons.flag_japan': {'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)}, 'icons.flag_france': {'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)}, 'icons.flag_indonesia': { 'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA) }, 'icons.flag_italy': {'icon': _ba.charstr(SpecialChar.FLAG_ITALY)}, 'icons.flag_south_korea': { 'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA) }, 'icons.flag_netherlands': { 'icon': _ba.charstr(SpecialChar.FLAG_NETHERLANDS) }, 'icons.flag_uae': { 'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES) }, 'icons.flag_qatar': {'icon': _ba.charstr(SpecialChar.FLAG_QATAR)}, 'icons.flag_egypt': {'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)}, 'icons.flag_kuwait': {'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)}, 'icons.flag_algeria': { 'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA) }, 'icons.flag_saudi_arabia': { 'icon': _ba.charstr(SpecialChar.FLAG_SAUDI_ARABIA) }, 'icons.flag_malaysia': { 'icon': _ba.charstr(SpecialChar.FLAG_MALAYSIA) }, 'icons.flag_czech_republic': { 'icon': _ba.charstr(SpecialChar.FLAG_CZECH_REPUBLIC) }, 'icons.flag_australia': { 'icon': _ba.charstr(SpecialChar.FLAG_AUSTRALIA) }, 'icons.flag_singapore': { 'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE) }, 'icons.flag_iran': {'icon': _ba.charstr(SpecialChar.FLAG_IRAN)}, 'icons.flag_poland': {'icon': _ba.charstr(SpecialChar.FLAG_POLAND)}, 'icons.flag_argentina': { 'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA) }, 'icons.flag_philippines': { 'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES) }, 'icons.flag_chile': {'icon': _ba.charstr(SpecialChar.FLAG_CHILE)}, 'icons.fedora': {'icon': _ba.charstr(SpecialChar.FEDORA)}, 'icons.hal': {'icon': _ba.charstr(SpecialChar.HAL)}, 'icons.crown': {'icon': _ba.charstr(SpecialChar.CROWN)}, 'icons.yinyang': {'icon': _ba.charstr(SpecialChar.YIN_YANG)}, 'icons.eyeball': {'icon': _ba.charstr(SpecialChar.EYE_BALL)}, 'icons.skull': {'icon': _ba.charstr(SpecialChar.SKULL)}, 'icons.heart': {'icon': _ba.charstr(SpecialChar.HEART)}, 'icons.dragon': {'icon': _ba.charstr(SpecialChar.DRAGON)}, 'icons.helmet': {'icon': _ba.charstr(SpecialChar.HELMET)}, 'icons.mushroom': {'icon': _ba.charstr(SpecialChar.MUSHROOM)}, 'icons.ninja_star': {'icon': _ba.charstr(SpecialChar.NINJA_STAR)}, 'icons.viking_helmet': { 'icon': _ba.charstr(SpecialChar.VIKING_HELMET) }, 'icons.moon': {'icon': _ba.charstr(SpecialChar.MOON)}, 'icons.spider': {'icon': _ba.charstr(SpecialChar.SPIDER)}, 'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)}, 'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)}, } return _ba.app.store_items def get_store_layout() -> dict[str, list[dict[str, Any]]]: """Return what's available in the store at a given time. Categorized by tab and by section.""" if _ba.app.store_layout is None: _ba.app.store_layout = { 'characters': [{'items': []}], 'extras': [{'items': ['pro']}], 'maps': [{'items': ['maps.lake_frigid']}], 'minigames': [], 'icons': [ { 'items': [ 'icons.mushroom', 'icons.heart', 'icons.eyeball', 'icons.yinyang', 'icons.hal', 'icons.flag_us', 'icons.flag_mexico', 'icons.flag_germany', 'icons.flag_brazil', 'icons.flag_russia', 'icons.flag_china', 'icons.flag_uk', 'icons.flag_canada', 'icons.flag_india', 'icons.flag_japan', 'icons.flag_france', 'icons.flag_indonesia', 'icons.flag_italy', 'icons.flag_south_korea', 'icons.flag_netherlands', 'icons.flag_uae', 'icons.flag_qatar', 'icons.flag_egypt', 'icons.flag_kuwait', 'icons.flag_algeria', 'icons.flag_saudi_arabia', 'icons.flag_malaysia', 'icons.flag_czech_republic', 'icons.flag_australia', 'icons.flag_singapore', 'icons.flag_iran', 'icons.flag_poland', 'icons.flag_argentina', 'icons.flag_philippines', 'icons.flag_chile', 'icons.moon', 'icons.fedora', 'icons.spider', 'icons.ninja_star', 'icons.skull', 'icons.dragon', 'icons.viking_helmet', 'icons.fireball', 'icons.helmet', 'icons.crown', ] } ], } store_layout = _ba.app.store_layout store_layout['characters'] = [ { 'items': [ 'characters.kronk', 'characters.zoe', 'characters.jackmorgan', 'characters.mel', 'characters.snakeshadow', 'characters.bones', 'characters.bernard', 'characters.agent', 'characters.frosty', 'characters.pascal', 'characters.pixie', ] } ] store_layout['minigames'] = [ { 'items': [ 'games.ninja_fight', 'games.meteor_shower', 'games.target_practice', ] } ] if _internal.get_v1_account_misc_read_val('xmas', False): store_layout['characters'][0]['items'].append('characters.santa') store_layout['characters'][0]['items'].append('characters.wizard') store_layout['characters'][0]['items'].append('characters.cyborg') if _internal.get_v1_account_misc_read_val('easter', False): store_layout['characters'].append( {'title': 'store.holidaySpecialText', 'items': ['characters.bunny']} ) store_layout['minigames'].append( { 'title': 'store.holidaySpecialText', 'items': ['games.easter_egg_hunt'], } ) # This will cause merch to show only if the master-server has # given us a link (which means merch is available in our region). store_layout['extras'] = [{'items': ['pro']}] if _ba.app.config.get('Merch Link'): store_layout['extras'][0]['items'].append('merch') return store_layout def get_clean_price(price_string: str) -> str: """(internal)""" # I'm not brave enough to try and do any numerical # manipulation on formatted price strings, but lets do a # few swap-outs to tidy things up a bit. psubs = { '$2.99': '$3.00', '$4.99': '$5.00', '$9.99': '$10.00', '$19.99': '$20.00', '$49.99': '$50.00', } return psubs.get(price_string, price_string) def get_available_purchase_count(tab: str | None = None) -> int: """(internal)""" try: if _internal.get_v1_account_state() != 'signed_in': return 0 count = 0 our_tickets = _internal.get_v1_account_ticket_count() store_data = get_store_layout() if tab is not None: tabs = [(tab, store_data[tab])] else: tabs = list(store_data.items()) for tab_name, tabval in tabs: if tab_name == 'icons': continue # too many of these; don't show.. count = _calc_count_for_tab(tabval, our_tickets, count) return count except Exception: from ba import _error _error.print_exception('error calcing available purchases') return 0 def _calc_count_for_tab( tabval: list[dict[str, Any]], our_tickets: int, count: int ) -> int: for section in tabval: for item in section['items']: ticket_cost = _internal.get_v1_account_misc_read_val( 'price.' + item, None ) if ticket_cost is not None: if our_tickets >= ticket_cost and not _internal.get_purchased( item ): count += 1 return count def get_available_sale_time(tab: str) -> int | None: """(internal)""" # pylint: disable=too-many-branches # pylint: disable=too-many-nested-blocks # pylint: disable=too-many-locals try: import datetime from ba._generated.enums import TimeType, TimeFormat app = _ba.app sale_times: list[int | None] = [] # Calc time for our pro sale (old special case). if tab == 'extras': config = app.config if app.accounts_v1.have_pro(): return None # If we haven't calced/loaded start times yet. if app.pro_sale_start_time is None: # If we've got a time-remaining in our config, start there. if 'PSTR' in config: app.pro_sale_start_time = int( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) ) app.pro_sale_start_val = config['PSTR'] else: # We start the timer once we get the duration from # the server. start_duration = _internal.get_v1_account_misc_read_val( 'proSaleDurationMinutes', None ) if start_duration is not None: app.pro_sale_start_time = int( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) ) app.pro_sale_start_val = 60000 * start_duration # If we haven't heard from the server yet, no sale.. else: return None assert app.pro_sale_start_val is not None val: int | None = max( 0, app.pro_sale_start_val - ( _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) - app.pro_sale_start_time ), ) # Keep the value in the config up to date. I suppose we should # write the config occasionally but it should happen often enough # for other reasons. config['PSTR'] = val if val == 0: val = None sale_times.append(val) # Now look for sales in this tab. sales_raw = _internal.get_v1_account_misc_read_val('sales', {}) store_layout = get_store_layout() for section in store_layout[tab]: for item in section['items']: if item in sales_raw: if not _internal.get_purchased(item): to_end = ( datetime.datetime.utcfromtimestamp( sales_raw[item]['e'] ) - datetime.datetime.utcnow() ).total_seconds() if to_end > 0: sale_times.append(int(to_end * 1000)) # Return the smallest time I guess? sale_times_int = [t for t in sale_times if isinstance(t, int)] return min(sale_times_int) if sale_times_int else None except Exception: from ba import _error _error.print_exception('error calcing sale time') return None def get_unowned_maps() -> list[str]: """Return the list of local maps not owned by the current account. Category: **Asset Functions** """ unowned_maps: set[str] = set() if not _ba.app.headless_mode: for map_section in get_store_layout()['maps']: for mapitem in map_section['items']: if not _internal.get_purchased(mapitem): m_info = get_store_item(mapitem) unowned_maps.add(m_info['map_type'].name) return sorted(unowned_maps) def get_unowned_game_types() -> set[type[ba.GameActivity]]: """Return present game types not owned by the current account.""" try: unowned_games: set[type[ba.GameActivity]] = set() if not _ba.app.headless_mode: for section in get_store_layout()['minigames']: for mname in section['items']: if not _internal.get_purchased(mname): m_info = get_store_item(mname) unowned_games.add(m_info['gametype']) return unowned_games except Exception: from ba import _error _error.print_exception('error calcing un-owned games') return set()