vh-bombsquad-modded-server-.../dist/ba_data/python/ba/_store.py

499 lines
19 KiB
Python
Raw Normal View History

2024-06-06 19:50:58 +05:30
# 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()