Bombsquad-Ballistica-Modded.../dist/ba_data/python/bauiv1lib/specialoffer.py

556 lines
19 KiB
Python
Raw Normal View History

2021-03-29 03:24:13 +05:30
# Released under the MIT License. See LICENSE for details.
#
"""UI for presenting sales/etc."""
from __future__ import annotations
import copy
2023-08-13 17:21:49 +05:30
import logging
2021-03-29 03:24:13 +05:30
from typing import TYPE_CHECKING
2023-08-13 17:21:49 +05:30
import bauiv1 as bui
2021-03-29 03:24:13 +05:30
if TYPE_CHECKING:
2022-06-30 00:31:52 +05:30
from typing import Any
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
class SpecialOfferWindow(bui.Window):
2021-03-29 03:24:13 +05:30
"""Window for presenting sales/etc."""
def __init__(self, offer: dict[str, Any], transition: str = 'in_right'):
2021-03-29 03:24:13 +05:30
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
2023-08-13 17:21:49 +05:30
from babase import SpecialChar
from bauiv1lib.store import item as storeitemui
plus = bui.app.plus
assert plus is not None
assert bui.app.classic is not None
store = bui.app.classic.store
2021-03-29 03:24:13 +05:30
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).
2022-06-30 00:31:52 +05:30
real_price: str | None
2021-03-29 03:24:13 +05:30
# Misnomer: 'pro' actually means offer 'pro_sale'.
if offer['item'] in ['pro', 'pro_fullprice']:
2023-08-13 17:21:49 +05:30
real_price = plus.get_price(
'pro' if offer['item'] == 'pro_fullprice' else 'pro_sale'
)
2023-09-30 17:21:33 +05:30
if real_price is None and bui.app.env.debug:
2021-03-29 03:24:13 +05:30
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)
2023-08-13 17:21:49 +05:30
real_price = plus.get_price(offer['price'])
2023-09-30 17:21:33 +05:30
if real_price is None and bui.app.env.debug:
2021-03-29 03:24:13 +05:30
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.
2023-08-13 17:21:49 +05:30
bui.lock_all_input()
bui.apptimer(1.0, bui.unlock_all_input)
bui.getsound('ding').play()
bui.apptimer(0.3, bui.getsound('ooh').play)
2021-03-29 03:24:13 +05:30
self._offer = copy.deepcopy(offer)
self._width = 580
self._height = 590
2023-08-13 17:21:49 +05:30
uiscale = bui.app.ui_v1.uiscale
super().__init__(
2023-08-13 17:21:49 +05:30
root_widget=bui.containerwidget(
size=(self._width, self._height),
transition=transition,
scale=(
1.2
2023-08-13 17:21:49 +05:30
if uiscale is bui.UIScale.SMALL
else 1.15
2023-08-13 17:21:49 +05:30
if uiscale is bui.UIScale.MEDIUM
else 1.0
),
stack_offset=(0, -15)
2023-08-13 17:21:49 +05:30
if uiscale is bui.UIScale.SMALL
else (0, 0),
)
)
2021-03-29 03:24:13 +05:30
self._is_bundle_sale = False
try:
if offer['item'] in ['pro', 'pro_fullprice']:
2023-08-13 17:21:49 +05:30
original_price_str = plus.get_price('pro')
2021-03-29 03:24:13 +05:30
if original_price_str is None:
original_price_str = '?'
2023-08-13 17:21:49 +05:30
new_price_str = plus.get_price('pro_sale')
2021-03-29 03:24:13 +05:30
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
):
2021-03-29 03:24:13 +05:30
self._is_bundle_sale = True
2023-08-13 17:21:49 +05:30
original_price = plus.get_v1_account_misc_read_val(
'price.' + self._offer_item, 9999
)
2021-03-29 03:24:13 +05:30
# For pure ticket prices we can show a percent-off.
if isinstance(offer['price'], int):
new_price = offer['price']
2023-08-13 17:21:49 +05:30
tchar = bui.charstr(SpecialChar.TICKET)
2021-03-29 03:24:13 +05:30
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
)
)
2023-08-13 17:21:49 +05:30
percent_off_text = ' ' + bui.Lstr(
resource='store.salePercentText'
).evaluate().replace('${PERCENT}', str(percent_off))
2021-03-29 03:24:13 +05:30
else:
original_price_str = new_price_str = '?'
percent_off_text = ''
except Exception:
2023-08-13 17:21:49 +05:30
logging.exception('Error setting up special-offer: %s.', offer)
2021-03-29 03:24:13 +05:30
original_price_str = new_price_str = '?'
percent_off_text = ''
# If its a bundle sale, change the title.
if self._is_bundle_sale:
2023-08-13 17:21:49 +05:30
sale_text = bui.Lstr(
resource='store.saleBundleText',
fallback_resource='store.saleText',
).evaluate()
2021-03-29 03:24:13 +05:30
else:
# For full pro we say 'Upgrade?' since its not really a sale.
if offer['item'] == 'pro_fullprice':
2023-08-13 17:21:49 +05:30
sale_text = bui.Lstr(
2021-03-29 03:24:13 +05:30
resource='store.upgradeQuestionText',
fallback_resource='store.saleExclaimText',
).evaluate()
2021-03-29 03:24:13 +05:30
else:
2023-08-13 17:21:49 +05:30
sale_text = bui.Lstr(
2021-03-29 03:24:13 +05:30
resource='store.saleExclaimText',
fallback_resource='store.saleText',
).evaluate()
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
self._title_text = bui.textwidget(
2021-03-29 03:24:13 +05:30
parent=self._root_widget,
position=(self._width * 0.5, self._height - 40),
size=(0, 0),
text=sale_text
+ (
2023-08-13 17:21:49 +05:30
(' ' + bui.Lstr(resource='store.oneTimeOnlyText').evaluate())
if self._offer['oneTimeOnly']
else ''
)
+ percent_off_text,
2021-03-29 03:24:13 +05:30
h_align='center',
v_align='center',
maxwidth=self._width * 0.9 - 220,
scale=1.4,
color=(0.3, 1, 0.3),
)
2021-03-29 03:24:13 +05:30
self._flash_on = False
2023-08-13 17:21:49 +05:30
self._flashing_timer: bui.AppTimer | None = bui.AppTimer(
0.05, bui.WeakCall(self._flash_cycle), repeat=True
)
2023-08-13 17:21:49 +05:30
bui.apptimer(0.6, bui.WeakCall(self._stop_flashing))
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
size = store.get_store_item_display_size(self._offer_item)
display: dict[str, Any] = {}
2021-03-29 03:24:13 +05:30
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),
),
2021-03-29 03:24:13 +05:30
b_width=size[0],
b_height=size[1],
button=not self._is_bundle_sale,
)
2021-03-29 03:24:13 +05:30
# Wire up the parts we need.
if self._is_bundle_sale:
2023-08-13 17:21:49 +05:30
self._plus_text = bui.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),
)
2023-08-13 17:21:49 +05:30
self._plus_tickets = bui.textwidget(
2021-03-29 03:24:13 +05:30
parent=self._root_widget,
position=(self._width * 0.5 + 120, self._height * 0.5 + 50),
size=(0, 0),
2023-08-13 17:21:49 +05:30
text=bui.charstr(SpecialChar.TICKET_BACKING)
+ str(offer['bonusTickets']),
2021-03-29 03:24:13 +05:30
h_align='center',
v_align='center',
maxwidth=self._width * 0.9,
scale=2.5,
color=(0.2, 1, 0.2),
)
2023-08-13 17:21:49 +05:30
self._price_text = bui.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),
)
2021-03-29 03:24:13 +05:30
# Total-value if they supplied it.
total_worth_item = offer.get('valueItem', None)
if total_worth_item is not None:
2023-08-13 17:21:49 +05:30
price = plus.get_price(total_worth_item)
total_worth_price = (
2023-08-13 17:21:49 +05:30
store.get_clean_price(price) if price is not None else None
)
2021-03-29 03:24:13 +05:30
if total_worth_price is not None:
2023-08-13 17:21:49 +05:30
total_worth_text = bui.Lstr(
resource='store.totalWorthText',
subs=[('${TOTAL_WORTH}', total_worth_price)],
)
2023-08-13 17:21:49 +05:30
self._total_worth_text = bui.textwidget(
2021-03-29 03:24:13 +05:30
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),
)
2021-03-29 03:24:13 +05:30
elif offer['item'] == 'pro_fullprice':
# for full-price pro we simply show full price
2023-08-13 17:21:49 +05:30
bui.textwidget(edit=display['price_widget'], text=real_price)
bui.buttonwidget(
edit=display['button'], on_activate_call=self._purchase
)
2021-03-29 03:24:13 +05:30
else:
# Show old/new prices otherwise (for pro sale).
2023-08-13 17:21:49 +05:30
bui.buttonwidget(
edit=display['button'], on_activate_call=self._purchase
)
2023-08-13 17:21:49 +05:30
bui.imagewidget(edit=display['price_slash_widget'], opacity=1.0)
bui.textwidget(
edit=display['price_widget_left'], text=original_price_str
)
2023-08-13 17:21:49 +05:30
bui.textwidget(
edit=display['price_widget_right'], text=new_price_str
)
2021-03-29 03:24:13 +05:30
# Add ticket button only if this is ticket-purchasable.
if isinstance(offer.get('price'), int):
2023-08-13 17:21:49 +05:30
self._get_tickets_button = bui.buttonwidget(
2021-03-29 03:24:13 +05:30
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,
2023-08-13 17:21:49 +05:30
label=bui.Lstr(resource='getTicketsWindow.titleText'),
on_activate_call=self._on_get_more_tickets_press,
)
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
self._ticket_text_update_timer = bui.AppTimer(
1.0, bui.WeakCall(self._update_tickets_text), repeat=True
)
2021-03-29 03:24:13 +05:30
self._update_tickets_text()
2023-08-13 17:21:49 +05:30
self._update_timer = bui.AppTimer(
1.0, bui.WeakCall(self._update), repeat=True
)
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
self._cancel_button = bui.buttonwidget(
2021-03-29 03:24:13 +05:30
parent=self._root_widget,
position=(50, 40)
if self._is_bundle_sale
else (self._width * 0.5 - 75, 40),
2021-03-29 03:24:13 +05:30
size=(150, 60),
scale=1.0,
on_activate_call=self._cancel,
autoselect=True,
2023-08-13 17:21:49 +05:30
label=bui.Lstr(resource='noThanksText'),
)
2023-08-13 17:21:49 +05:30
self._cancel_countdown_text = bui.textwidget(
2021-03-29 03:24:13 +05:30
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),
2021-03-29 03:24:13 +05:30
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),
)
2021-03-29 03:24:13 +05:30
self._update_cancel_button_graphics()
if self._is_bundle_sale:
2023-08-13 17:21:49 +05:30
self._purchase_button = bui.buttonwidget(
2021-03-29 03:24:13 +05:30
parent=self._root_widget,
position=(self._width - 200, 40),
size=(150, 60),
scale=1.0,
on_activate_call=self._purchase,
autoselect=True,
2023-08-13 17:21:49 +05:30
label=bui.Lstr(resource='store.purchaseText'),
)
2023-08-13 17:21:49 +05:30
bui.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'],
)
2021-03-29 03:24:13 +05:30
def _stop_flashing(self) -> None:
self._flashing_timer = None
2023-08-13 17:21:49 +05:30
bui.textwidget(edit=self._title_text, color=(0.3, 1, 0.3))
2021-03-29 03:24:13 +05:30
def _flash_cycle(self) -> None:
if not self._root_widget:
return
self._flash_on = not self._flash_on
2023-08-13 17:21:49 +05:30
bui.textwidget(
edit=self._title_text,
color=(0.3, 1, 0.3) if self._flash_on else (1, 0.5, 0),
)
2021-03-29 03:24:13 +05:30
def _update_cancel_button_graphics(self) -> None:
2023-08-13 17:21:49 +05:30
bui.buttonwidget(
edit=self._cancel_button,
color=(0.5, 0.5, 0.5)
if self._cancel_delay > 0
else (0.7, 0.4, 0.34),
textcolor=(0.5, 0.5, 0.5)
if self._cancel_delay > 0
else (0.9, 0.9, 1.0),
)
2023-08-13 17:21:49 +05:30
bui.textwidget(
2021-03-29 03:24:13 +05:30
edit=self._cancel_countdown_text,
text=str(self._cancel_delay) if self._cancel_delay > 0 else '',
)
2021-03-29 03:24:13 +05:30
def _update(self) -> None:
2023-08-13 17:21:49 +05:30
plus = bui.app.plus
assert plus is not None
2021-03-29 03:24:13 +05:30
# If we've got seconds left on our countdown, update it.
if self._cancel_delay > 0:
self._cancel_delay = max(0, self._cancel_delay - 1)
self._update_cancel_button_graphics()
can_die = False
# We go away if we see that our target item is owned.
if self._offer_item == 'pro':
2023-08-13 17:21:49 +05:30
assert bui.app.classic is not None
if bui.app.classic.accounts.have_pro():
2021-03-29 03:24:13 +05:30
can_die = True
else:
2023-08-13 17:21:49 +05:30
if plus.get_purchased(self._offer_item):
2021-03-29 03:24:13 +05:30
can_die = True
if can_die:
self._transition_out('out_left')
def _transition_out(self, transition: str = 'out_left') -> None:
# Also clear any pending-special-offer we've stored at this point.
2023-08-13 17:21:49 +05:30
cfg = bui.app.config
2021-03-29 03:24:13 +05:30
if 'pendingSpecialOffer' in cfg:
del cfg['pendingSpecialOffer']
cfg.commit()
2023-08-13 17:21:49 +05:30
bui.containerwidget(edit=self._root_widget, transition=transition)
2021-03-29 03:24:13 +05:30
def _update_tickets_text(self) -> None:
2023-08-13 17:21:49 +05:30
from babase import SpecialChar
plus = bui.app.plus
assert plus is not None
2021-03-29 03:24:13 +05:30
if not self._root_widget:
return
2023-08-13 17:21:49 +05:30
sval: str | bui.Lstr
if plus.get_v1_account_state() == 'signed_in':
sval = bui.charstr(SpecialChar.TICKET) + str(
plus.get_v1_account_ticket_count()
)
2021-03-29 03:24:13 +05:30
else:
2023-08-13 17:21:49 +05:30
sval = bui.Lstr(resource='getTicketsWindow.titleText')
bui.buttonwidget(edit=self._get_tickets_button, label=sval)
2021-03-29 03:24:13 +05:30
def _on_get_more_tickets_press(self) -> None:
2023-08-13 17:21:49 +05:30
from bauiv1lib import account
from bauiv1lib import getcurrency
plus = bui.app.plus
assert plus is not None
2023-08-13 17:21:49 +05:30
if plus.get_v1_account_state() != 'signed_in':
2021-03-29 03:24:13 +05:30
account.show_sign_in_prompt()
return
getcurrency.GetCurrencyWindow(modal=True).get_root_widget()
def _purchase(self) -> None:
2023-08-13 17:21:49 +05:30
from bauiv1lib import getcurrency
from bauiv1lib import confirm
plus = bui.app.plus
assert plus is not None
assert bui.app.classic is not None
store = bui.app.classic.store
2021-03-29 03:24:13 +05:30
if self._offer['item'] == 'pro':
2023-08-13 17:21:49 +05:30
plus.purchase('pro_sale')
2021-03-29 03:24:13 +05:30
elif self._offer['item'] == 'pro_fullprice':
2023-08-13 17:21:49 +05:30
plus.purchase('pro')
2021-03-29 03:24:13 +05:30
elif self._is_bundle_sale:
# With bundle sales, the price is the name of the IAP.
2023-08-13 17:21:49 +05:30
plus.purchase(self._offer['price'])
2021-03-29 03:24:13 +05:30
else:
2022-06-30 00:31:52 +05:30
ticket_count: int | None
2021-03-29 03:24:13 +05:30
try:
2023-08-13 17:21:49 +05:30
ticket_count = plus.get_v1_account_ticket_count()
2021-03-29 03:24:13 +05:30
except Exception:
ticket_count = None
if ticket_count is not None and ticket_count < self._offer['price']:
2021-03-29 03:24:13 +05:30
getcurrency.show_get_tickets_prompt()
2023-08-13 17:21:49 +05:30
bui.getsound('error').play()
2021-03-29 03:24:13 +05:30
return
def do_it() -> None:
2023-08-13 17:21:49 +05:30
assert plus is not None
plus.in_game_purchase(
'offer:' + str(self._offer['id']), self._offer['price']
)
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
bui.getsound('swish').play()
confirm.ConfirmWindow(
2023-08-13 17:21:49 +05:30
bui.Lstr(
resource='store.purchaseConfirmText',
subs=[
(
'${ITEM}',
2023-08-13 17:21:49 +05:30
store.get_store_item_name_translated(
self._offer['item']
),
)
],
),
width=400,
height=120,
action=do_it,
2023-08-13 17:21:49 +05:30
ok_text=bui.Lstr(
resource='store.purchaseText', fallback_resource='okText'
),
)
2021-03-29 03:24:13 +05:30
def _cancel(self) -> None:
if self._cancel_delay > 0:
2023-08-13 17:21:49 +05:30
bui.getsound('error').play()
2021-03-29 03:24:13 +05:30
return
self._transition_out('out_right')
def show_offer() -> bool:
"""(internal)"""
try:
2023-08-13 17:21:49 +05:30
from bauiv1lib import feedback
2023-08-13 17:21:49 +05:30
plus = bui.app.plus
assert plus is not None
app = bui.app
assert app.classic is not None
2021-03-29 03:24:13 +05:30
# Space things out a bit so we don't hit the poor user with an ad and
# then an in-game offer.
has_been_long_enough_since_ad = True
2023-08-13 17:21:49 +05:30
if app.classic.ads.last_ad_completion_time is not None and (
bui.apptime() - app.classic.ads.last_ad_completion_time < 30.0
):
2021-03-29 03:24:13 +05:30
has_been_long_enough_since_ad = False
2023-08-13 17:21:49 +05:30
if (
app.classic.special_offer is not None
and has_been_long_enough_since_ad
):
2021-03-29 03:24:13 +05:30
# Special case: for pro offers, store this in our prefs so we
# can re-show it if the user kills us (set phasers to 'NAG'!!!).
2023-08-13 17:21:49 +05:30
if app.classic.special_offer.get('item') == 'pro_fullprice':
2021-03-29 03:24:13 +05:30
cfg = app.config
cfg['pendingSpecialOffer'] = {
2023-08-13 17:21:49 +05:30
'a': plus.get_v1_account_public_login_id(),
'o': app.classic.special_offer,
2021-03-29 03:24:13 +05:30
}
cfg.commit()
2023-08-13 17:21:49 +05:30
if app.classic.special_offer['item'] == 'rating':
feedback.ask_for_rating()
else:
SpecialOfferWindow(app.classic.special_offer)
2021-03-29 03:24:13 +05:30
2023-08-13 17:21:49 +05:30
app.classic.special_offer = None
2021-03-29 03:24:13 +05:30
return True
except Exception:
2023-08-13 17:21:49 +05:30
logging.exception('Error showing offer.')
2021-03-29 03:24:13 +05:30
return False