# Released under the MIT License. See LICENSE for details.
#
# pylint: disable=too-many-lines
"""Provides chest related ui."""
from __future__ import annotations
import math
import random
from typing import override, TYPE_CHECKING
from efro.util import strict_partial
import bacommon.bs
import bauiv1 as bui
if TYPE_CHECKING:
import datetime
import baclassic
_g_open_voices: list[tuple[float, str, float]] = []
[docs]
class ChestWindow(bui.MainWindow):
"""Allows viewing and performing operations on a chest."""
def __init__(
self,
index: int,
transition: str | None = 'in_right',
origin_widget: bui.Widget | None = None,
):
self._index = index
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
self._width = 1050 if uiscale is bui.UIScale.SMALL else 650
self._height = 550 if uiscale is bui.UIScale.SMALL else 450
self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0
self._yoffs = -50 if uiscale is bui.UIScale.SMALL else -35
self._action_in_flight = False
self._open_now_button: bui.Widget | None = None
self._open_now_spinner: bui.Widget | None = None
self._open_now_texts: list[bui.Widget] = []
self._open_now_images: list[bui.Widget] = []
self._watch_ad_button: bui.Widget | None = None
self._time_string_timer: bui.AppTimer | None = None
self._time_string_text: bui.Widget | None = None
self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = []
self._prizeindex = -1
self._prizesettxts: dict[int, list[bui.Widget]] = {}
self._prizesetimgs: dict[int, list[bui.Widget]] = {}
self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = (
None
)
# The set of widgets we keep when doing a clear.
self._core_widgets: list[bui.Widget] = []
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility='menu_full',
scale=(
1.45
if uiscale is bui.UIScale.SMALL
else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9
),
stack_offset=(
(0, 0)
if uiscale is bui.UIScale.SMALL
else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
),
),
transition=transition,
origin_widget=origin_widget,
)
# Tell the root-ui to stop updating toolbar values immediately;
# this allows it to run animations based on the results of our
# chest opening.
bui.root_ui_pause_updates()
self._root_ui_updates_paused = True
self._title_text = bui.textwidget(
parent=self._root_widget,
position=(0, self._height - 50 + self._yoffs),
size=(self._width, 25),
text=bui.Lstr(
resource='chests.slotText',
subs=[('${NUM}', str(index + 1))],
),
color=bui.app.ui_v1.title_color,
maxwidth=150.0,
h_align='center',
v_align='center',
)
self._core_widgets.append(self._title_text)
if uiscale is bui.UIScale.SMALL:
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
else:
btn = bui.buttonwidget(
parent=self._root_widget,
position=(self._xoffs + 50, self._height - 55 + self._yoffs),
size=(60, 55),
scale=0.8,
label=bui.charstr(bui.SpecialChar.BACK),
button_type='backSmall',
extra_touch_border_scale=2.0,
autoselect=True,
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
self._core_widgets.append(btn)
# Note: Don't need to explicitly clean this up. Just not adding
# it to core_widgets so it will go away on next reset.
self._loadingspinner = bui.spinnerwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
size=48,
style='bomb',
)
self._infotext = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 200 + self._yoffs),
size=(0, 0),
text='',
maxwidth=700,
scale=0.8,
color=(0.6, 0.5, 0.6),
h_align='center',
v_align='center',
)
self._core_widgets.append(self._infotext)
plus = bui.app.plus
if plus is None:
self._error('Plus feature-set is not present.')
return
if plus.accounts.primary is None:
self._error(bui.Lstr(resource='notSignedInText'))
return
# Start by showing info/options for our target chest. Note that
# we always ask the server for these values even though we may
# have them through our appmode subscription which updates the
# chest UI. This is because the wait_for_connectivity()
# mechanism will often bring our window up a split second before
# the chest subscription receives its first values which would
# lead us to incorrectly think there is no chest there. If we
# want to optimize this in the future we could perhaps use local
# values only if there is a chest present in them.
assert not self._action_in_flight
self._action_in_flight = True
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.bs.ChestInfoMessage(chest_id=str(self._index)),
on_response=bui.WeakCall(self._on_chest_info_response),
)
def __del__(self) -> None:
# print('~ChestWindow()')
# Make sure UI updates are resumed if we haven't done so.
if self._root_ui_updates_paused:
bui.root_ui_resume_updates()
[docs]
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
# Pull anything we need from self out here; if we do it in the
# lambda we keep self alive which is bad.
index = self._index
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
index=index, transition=transition, origin_widget=origin_widget
)
)
def _update_time_display(self, unlock_time: datetime.datetime) -> None:
# Once text disappears, kill our timer.
if not self._time_string_text:
self._time_string_timer = None
return
now = bui.utc_now_cloud()
secs_till_open = max(0.0, (unlock_time - now).total_seconds())
tstr = (
bui.timestring(secs_till_open, centi=False)
if secs_till_open > 0
else ''
)
bui.textwidget(edit=self._time_string_text, text=tstr)
def _on_chest_info_response(
self, response: bacommon.bs.ChestInfoResponse | Exception
) -> None:
assert self._action_in_flight # Should be us.
self._action_in_flight = False
if isinstance(response, Exception):
self._error(
bui.Lstr(resource='internal.unableToCompleteTryAgainText'),
minor=True,
)
return
if response.chest is None:
self._show_about_chest_slots()
return
assert response.user_tokens is not None
self._show_chest_actions(response.user_tokens, response.chest)
def _on_chest_action_response(
self, response: bacommon.bs.ChestActionResponse | Exception
) -> None:
assert self._action_in_flight # Should be us.
self._action_in_flight = False
# Communication/local error:
if isinstance(response, Exception):
self._error(
bui.Lstr(resource='internal.unableToCompleteTryAgainText'),
minor=True,
)
return
# Server-side error:
if response.error is not None:
self._error(bui.Lstr(translate=('serverResponses', response.error)))
return
# Show any bundled success message.
if response.success_msg is not None:
bui.screenmessage(
bui.Lstr(translate=('serverResponses', response.success_msg)),
color=(0, 1.0, 0),
)
bui.getsound('cashRegister').play()
# Show any bundled warning.
if response.warning is not None:
bui.screenmessage(
bui.Lstr(translate=('serverResponses', response.warning)),
color=(1, 0.5, 0),
)
bui.getsound('error').play()
# If we just paid for something, make a sound accordingly.
if bool(False): # Hmm maybe this feels odd.
if response.tokens_charged > 0:
bui.getsound('cashRegister').play()
# If there's contents listed in the response, show them.
if response.contents is not None:
self._show_chest_contents(response)
else:
# Otherwise we're done here; just close out our UI.
self.main_window_back()
def _show_chest_actions(
self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest
) -> None:
"""Show state for our chest."""
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
from baclassic import (
ClassicAppMode,
CHEST_APPEARANCE_DISPLAY_INFOS,
CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
)
plus = bui.app.plus
assert plus is not None
# We expect to be run under classic app mode.
mode = bui.app.mode
if not isinstance(mode, ClassicAppMode):
self._error('Classic app mode not active.')
return
self._reset()
self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
)
bui.textwidget(
edit=self._title_text,
text=bui.Lstr(
translate=('displayItemNames', chest.appearance.pretty_name)
),
)
imgsize = 145
bui.imagewidget(
parent=self._root_widget,
position=(
self._width * 0.5 - imgsize * 0.5,
self._height - 223 + self._yoffs,
),
color=self._chestdisplayinfo.color,
size=(imgsize, imgsize),
texture=bui.gettexture(self._chestdisplayinfo.texclosed),
tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
tint_color=self._chestdisplayinfo.tint,
tint2_color=self._chestdisplayinfo.tint2,
)
# Store the prize-sets so we can display odds/etc. Sort them
# with largest weights first.
self._prizesets = sorted(
chest.prizesets, key=lambda s: s.weight, reverse=True
)
if chest.unlock_tokens > 0:
lsize = 30
bui.imagewidget(
parent=self._root_widget,
position=(
self._width * 0.5 - imgsize * 0.4 - lsize * 0.5,
self._height - 223 + 27.0 + self._yoffs,
),
size=(lsize, lsize),
texture=bui.gettexture('lock'),
)
# Time string.
if chest.unlock_tokens != 0:
self._time_string_text = bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 85 + self._yoffs),
size=(0, 0),
text='',
maxwidth=700,
scale=0.6,
color=(0.6, 1.0, 0.6),
h_align='center',
v_align='center',
)
self._update_time_display(chest.unlock_time)
self._time_string_timer = bui.AppTimer(
1.0,
repeat=True,
call=bui.WeakCall(self._update_time_display, chest.unlock_time),
)
# Allow watching an ad IF the server tells us we can AND we have
# an ad ready to show.
show_ad_button = (
chest.unlock_tokens > 0
and chest.ad_allow
and plus.have_incentivized_ad()
)
bwidth = 130
bheight = 90
bposy = -330 if chest.unlock_tokens == 0 else -340
hspace = 20
boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0
self._open_now_button = bui.buttonwidget(
parent=self._root_widget,
position=(
self._width * 0.5 - bwidth * 0.5 + boffsx,
self._height + bposy + self._yoffs,
),
size=(bwidth, bheight),
label='',
button_type='square',
autoselect=True,
on_activate_call=bui.WeakCall(
self._open_press, user_tokens, chest.unlock_tokens
),
enable_sound=False,
)
self._open_now_images = []
self._open_now_texts = []
iconsize = 50
if chest.unlock_tokens == 0:
self._open_now_texts.append(
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='openText'),
position=(
self._width * 0.5 + boffsx,
self._height + bposy + self._yoffs + bheight * 0.5,
),
color=(0, 1, 0),
draw_controller=self._open_now_button,
scale=0.7,
maxwidth=bwidth * 0.8,
size=(0, 0),
h_align='center',
v_align='center',
)
)
else:
self._open_now_texts.append(
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='openNowText'),
position=(
self._width * 0.5 + boffsx,
self._height + bposy + self._yoffs + bheight * 1.15,
),
maxwidth=bwidth * 0.8,
scale=0.7,
color=(0.7, 1, 0.7),
size=(0, 0),
h_align='center',
v_align='center',
)
)
self._open_now_images.append(
bui.imagewidget(
parent=self._root_widget,
size=(iconsize, iconsize),
position=(
self._width * 0.5 - iconsize * 0.5 + boffsx,
self._height + bposy + self._yoffs + bheight * 0.35,
),
draw_controller=self._open_now_button,
texture=bui.gettexture('coin'),
)
)
self._open_now_texts.append(
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(
resource='tokens.numTokensText',
subs=[('${COUNT}', str(chest.unlock_tokens))],
),
position=(
self._width * 0.5 + boffsx,
self._height + bposy + self._yoffs + bheight * 0.25,
),
scale=0.65,
color=(0, 1, 0),
draw_controller=self._open_now_button,
maxwidth=bwidth * 0.8,
size=(0, 0),
h_align='center',
v_align='center',
)
)
self._open_now_spinner = bui.spinnerwidget(
parent=self._root_widget,
position=(
self._width * 0.5 + boffsx,
self._height + bposy + self._yoffs + 0.5 * bheight,
),
visible=False,
)
if show_ad_button:
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='chests.reduceWaitText'),
position=(
self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
self._height + bposy + self._yoffs + bheight * 1.15,
),
maxwidth=bwidth * 0.8,
scale=0.7,
color=(0.7, 1, 0.7),
size=(0, 0),
h_align='center',
v_align='center',
)
self._watch_ad_button = bui.buttonwidget(
parent=self._root_widget,
position=(
self._width * 0.5 + hspace * 0.5,
self._height + bposy + self._yoffs,
),
size=(bwidth, bheight),
label='',
button_type='square',
autoselect=True,
on_activate_call=bui.WeakCall(self._watch_ad_press),
enable_sound=False,
)
bui.imagewidget(
parent=self._root_widget,
size=(iconsize, iconsize),
position=(
self._width * 0.5
+ hspace * 0.5
+ bwidth * 0.5
- iconsize * 0.5,
self._height + bposy + self._yoffs + bheight * 0.35,
),
draw_controller=self._watch_ad_button,
color=(1.5, 1.0, 2.0),
texture=bui.gettexture('tv'),
)
# Note to self: AdMob requires rewarded ad usage
# specifically says 'Ad' in it.
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='watchAnAdText'),
position=(
self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
self._height + bposy + self._yoffs + bheight * 0.25,
),
scale=0.65,
color=(0, 1, 0),
draw_controller=self._watch_ad_button,
maxwidth=bwidth * 0.8,
size=(0, 0),
h_align='center',
v_align='center',
)
self._show_odds(initial_highlighted_row=-1)
def _highlight_odds_row(self, row: int, extra: bool = False) -> None:
for rindex, imgs in self._prizesetimgs.items():
opacity = (
(0.9 if extra else 0.75)
if rindex == row
else (0.4 if extra else 0.5)
)
for img in imgs:
if img:
bui.imagewidget(edit=img, opacity=opacity)
for rindex, txts in self._prizesettxts.items():
opacity = (
(0.9 if extra else 0.75)
if rindex == row
else (0.4 if extra else 0.5)
)
for txt in txts:
if txt:
bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity))
def _show_odds(
self,
*,
initial_highlighted_row: int,
initial_highlighted_extra: bool = False,
) -> None:
# pylint: disable=too-many-locals
xoffs = 110
totalweight = max(0.001, sum(t.weight for t in self._prizesets))
rowheight = 25
totalheight = (len(self._prizesets) + 1) * rowheight
x = self._width * 0.5 + xoffs
y = self._height + self._yoffs - 150.0 + totalheight * 0.5
# Title.
bui.textwidget(
parent=self._root_widget,
text=bui.Lstr(resource='chests.prizeOddsText'),
color=(0.7, 0.65, 1, 0.5),
flatness=1.0,
shadow=1.0,
position=(x, y),
scale=0.55,
size=(0, 0),
h_align='left',
v_align='center',
)
y -= 5.0
prizesettxts: list[bui.Widget]
prizesetimgs: list[bui.Widget]
def _mkicon(img: str) -> None:
iconsize = 20.0
nonlocal x
nonlocal prizesetimgs
prizesetimgs.append(
bui.imagewidget(
parent=self._root_widget,
size=(iconsize, iconsize),
position=(x, y - iconsize * 0.5),
texture=bui.gettexture(img),
opacity=0.4,
)
)
x += iconsize
def _mktxt(txt: str, advance: bool = True) -> None:
tscale = 0.45
nonlocal x
nonlocal prizesettxts
prizesettxts.append(
bui.textwidget(
parent=self._root_widget,
text=txt,
flatness=1.0,
shadow=1.0,
position=(x, y),
scale=tscale,
size=(0, 0),
h_align='left',
v_align='center',
)
)
if advance:
x += (bui.get_string_width(txt, suppress_warning=True)) * tscale
self._prizesettxts = {}
self._prizesetimgs = {}
for i, p in enumerate(self._prizesets):
prizesettxts = self._prizesettxts.setdefault(i, [])
prizesetimgs = self._prizesetimgs.setdefault(i, [])
x = self._width * 0.5 + xoffs
y -= rowheight
percent = 100.0 * p.weight / totalweight
# Show decimals only if we get very small percentages (looks
# better than rounding as '0%').
percenttxt = (
f'{percent:.2f}%:'
if percent < 0.095
else (
f'{percent:.1f}%:'
if percent < 0.95
else f'{round(percent)}%:'
)
)
# We advance manually here to keep values lined up
# (otherwise single digit percent rows don't line up with
# double digit ones).
_mktxt(percenttxt, advance=False)
x += 35.0
for item in p.contents:
x += 5.0
if isinstance(item.item, bacommon.bs.TicketsDisplayItem):
_mktxt(str(item.item.count))
_mkicon('tickets')
elif isinstance(item.item, bacommon.bs.TokensDisplayItem):
_mktxt(str(item.item.count))
_mkicon('coin')
else:
# For other cases just fall back on text desc.
#
# Translate the wrapper description and apply any subs.
descfin = bui.Lstr(
translate=('serverResponses', item.description)
).evaluate()
subs = (
[]
if item.description_subs is None
else item.description_subs
)
assert len(subs) % 2 == 0 # Should always be even.
for j in range(0, len(subs) - 1, 2):
descfin = descfin.replace(subs[j], subs[j + 1])
_mktxt(descfin)
self._highlight_odds_row(
initial_highlighted_row, extra=initial_highlighted_extra
)
def _open_press(self, user_tokens: int, token_payment: int) -> None:
from bauiv1lib.gettokens import show_get_tokens_prompt
bui.getsound('click01').play()
# Allow only one in-flight action at once.
if self._action_in_flight:
bui.screenmessage(
bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
)
bui.getsound('error').play()
return
plus = bui.app.plus
assert plus is not None
if plus.accounts.primary is None:
self._error(bui.Lstr(resource='notSignedInText'))
return
# Offer to purchase tokens if they don't have enough.
if user_tokens < token_payment:
# Hack: We disable normal swish for the open button and it
# seems weird without a swish here, so explicitly do one.
bui.getsound('swish').play()
show_get_tokens_prompt()
return
self._action_in_flight = True
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.bs.ChestActionMessage(
chest_id=str(self._index),
action=bacommon.bs.ChestActionMessage.Action.UNLOCK,
token_payment=token_payment,
),
on_response=bui.WeakCall(self._on_chest_action_response),
)
# Convey that something is in progress.
if self._open_now_button:
bui.spinnerwidget(edit=self._open_now_spinner, visible=True)
for twidget in self._open_now_texts:
bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2))
for iwidget in self._open_now_images:
bui.imagewidget(edit=iwidget, opacity=0.2)
def _watch_ad_press(self) -> None:
bui.getsound('click01').play()
# Allow only one in-flight action at once.
if self._action_in_flight:
bui.screenmessage(
bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
)
bui.getsound('error').play()
return
assert bui.app.classic is not None
self._action_in_flight = True
bui.app.classic.ads.show_ad_2(
'reduce_chest_wait',
on_completion_call=bui.WeakCall(self._watch_ad_complete),
)
# Convey that something is in progress.
if self._watch_ad_button:
bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4))
def _watch_ad_complete(self, actually_showed: bool) -> None:
assert self._action_in_flight # Should be ad view.
self._action_in_flight = False
if not actually_showed:
return
# Allow only one in-flight action at once.
if self._action_in_flight:
bui.screenmessage(
bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
)
bui.getsound('error').play()
return
plus = bui.app.plus
assert plus is not None
if plus.accounts.primary is None:
self._error(bui.Lstr(resource='notSignedInText'))
return
self._action_in_flight = True
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.bs.ChestActionMessage(
chest_id=str(self._index),
action=bacommon.bs.ChestActionMessage.Action.AD,
token_payment=0,
),
on_response=bui.WeakCall(self._on_chest_action_response),
)
def _reset(self) -> None:
"""Clear all non-permanent widgets and clear infotext."""
for widget in self._root_widget.get_children():
if widget not in self._core_widgets:
widget.delete()
bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1))
def _error(self, msg: str | bui.Lstr, minor: bool = False) -> None:
"""Put ourself in an error state with a visible error message."""
self._reset()
bui.textwidget(
edit=self._infotext,
text=msg,
color=(1, 0.5, 0.5) if minor else (1, 0, 0),
)
def _show_about_chest_slots(self) -> None:
# No-op if our ui is dead.
if not self._root_widget:
return
self._reset()
bui.textwidget(
edit=self._infotext,
text=bui.Lstr(resource='chests.slotDescriptionText'),
color=(1, 1, 1),
)
def _show_chest_contents(
self, response: bacommon.bs.ChestActionResponse
) -> None:
# pylint: disable=too-many-locals
from baclassic import show_display_item
# No-op if our ui is dead.
if not self._root_widget:
return
assert response.contents is not None
# Insert test items for testing.
if bool(False):
response.contents += [
bacommon.bs.DisplayItemWrapper.for_display_item(
bacommon.bs.TestDisplayItem()
)
]
tincr = 0.4
tendoffs = tincr * 4.0
toffs = 0.0
bui.getsound('revUp').play(volume=2.0)
# Show nothing but the chest icon and animate it shaking.
self._reset()
imgsize = 145
assert self._chestdisplayinfo is not None
img = bui.imagewidget(
parent=self._root_widget,
color=self._chestdisplayinfo.color,
texture=bui.gettexture(self._chestdisplayinfo.texclosed),
tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
tint_color=self._chestdisplayinfo.tint,
tint2_color=self._chestdisplayinfo.tint2,
)
def _set_img(x: float, scale: float) -> None:
if not img:
return
bui.imagewidget(
edit=img,
position=(
self._width * 0.5 - imgsize * scale * 0.5 + x,
self._height
- 223
+ self._yoffs
+ imgsize * 0.5
- imgsize * scale * 0.5,
),
size=(imgsize * scale, imgsize * scale),
)
# Set initial place.
_set_img(0.0, 1.0)
sign = 1.0
while toffs < tendoffs:
toffs += 0.03 * random.uniform(0.5, 1.5)
sign = -sign
bui.apptimer(
toffs,
bui.Call(
_set_img,
x=(
20.0
* random.uniform(0.3, 1.0)
* math.pow(toffs / tendoffs, 2.0)
* sign
),
scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0),
),
)
xspacing = 100
xoffs = -0.5 * (len(response.contents) - 1) * xspacing
bui.apptimer(
toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0)
)
# Play a variety of voice sounds.
# We keep a global list of voice options which we randomly pull
# from and refill when empty. This ensures everything gets
# played somewhat frequently and minimizes annoying repeats.
global _g_open_voices # pylint: disable=global-statement
if not _g_open_voices:
_g_open_voices = [
(0.3, 'woo3', 2.5),
(0.1, 'gasp', 1.3),
(0.2, 'woo2', 2.0),
(0.2, 'wow', 2.0),
(0.2, 'kronk2', 2.0),
(0.2, 'mel03', 2.0),
(0.2, 'aww', 2.0),
(0.4, 'nice', 2.0),
(0.3, 'yeah', 1.5),
(0.2, 'woo', 1.0),
(0.5, 'ooh', 0.8),
]
voicetimeoffs, voicename, volume = _g_open_voices.pop(
random.randrange(len(_g_open_voices))
)
bui.apptimer(
toffs + voicetimeoffs,
lambda: bui.getsound(voicename).play(volume=volume),
)
toffsopen = toffs
bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening))
toffs += tincr * 1.0
width = xspacing * 0.95
for item in response.contents:
toffs += tincr
bui.apptimer(
toffs - 0.1, lambda: bui.getsound('cashRegister').play()
)
bui.apptimer(
toffs,
strict_partial(
show_display_item,
item,
self._root_widget,
pos=(
self._width * 0.5 + xoffs,
self._height - 250.0 + self._yoffs,
),
width=width,
),
)
xoffs += xspacing
toffs += tincr
bui.apptimer(toffs, bui.WeakCall(self._show_done_button))
self._show_odds(initial_highlighted_row=-1)
# Store this for later
self._prizeindex = response.prizeindex
# The final result was already randomly selected on the server,
# but we want to give the illusion of randomness here, so cycle
# through highlighting our options and stop on the winner when
# the chest opens. To do this, we start at the end at the prize
# and work backwards setting timers.
if self._prizesets:
toffs2 = toffsopen - 0.01
amt = 0.02
i = self._prizeindex
while toffs2 > 0.0:
bui.apptimer(
toffs2,
bui.WeakCall(self._highlight_odds_row, i),
)
toffs2 -= amt
amt *= 1.05 * random.uniform(0.9, 1.1)
i = (i - 1) % len(self._prizesets)
def _show_chest_opening(self) -> None:
# No-op if our ui is dead.
if not self._root_widget:
return
self._reset()
imgsize = 145
bui.getsound('hiss').play()
assert self._chestdisplayinfo is not None
img = bui.imagewidget(
parent=self._root_widget,
color=self._chestdisplayinfo.color,
texture=bui.gettexture(self._chestdisplayinfo.texopen),
tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint),
tint_color=self._chestdisplayinfo.tint,
tint2_color=self._chestdisplayinfo.tint2,
)
tincr = 0.8
tendoffs = tincr * 2.0
toffs = 0.0
def _set_img(x: float, scale: float) -> None:
if not img:
return
bui.imagewidget(
edit=img,
position=(
self._width * 0.5 - imgsize * scale * 0.5 + x,
self._height
- 223
+ self._yoffs
+ imgsize * 0.5
- imgsize * scale * 0.5,
),
size=(imgsize * scale, imgsize * scale),
)
# Set initial place.
_set_img(0.0, 1.0)
sign = 1.0
while toffs < tendoffs:
toffs += 0.03 * random.uniform(0.5, 1.5)
sign = -sign
# Note: we speed x along here (multing toffs) so position
# comes to rest before scale.
bui.apptimer(
toffs,
bui.Call(
_set_img,
x=(
1.0
* random.uniform(0.3, 1.0)
* (
1.0
- math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0)
)
* sign
),
scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5),
),
)
self._show_odds(
initial_highlighted_row=self._prizeindex,
initial_highlighted_extra=True,
)
def _show_done_button(self) -> None:
# No-op if our ui is dead.
if not self._root_widget:
return
bwidth = 200
bheight = 60
btn = bui.buttonwidget(
parent=self._root_widget,
position=(
self._width * 0.5 - bwidth * 0.5,
self._height - 350 + self._yoffs,
),
size=(bwidth, bheight),
label=bui.Lstr(resource='doneText'),
autoselect=True,
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, start_button=btn)
# Slight hack: we define window different classes for our different
# chest slots so that the default UI behavior is to replace each other
# when different ones are pressed. If they are all the same window class
# then the default behavior for such presses is to toggle the existing
# one back off.
[docs]
class ChestWindow0(ChestWindow):
"""Child class of ChestWindow for slighty hackish reasons."""
[docs]
class ChestWindow1(ChestWindow):
"""Child class of ChestWindow for slighty hackish reasons."""
[docs]
class ChestWindow2(ChestWindow):
"""Child class of ChestWindow for slighty hackish reasons."""
[docs]
class ChestWindow3(ChestWindow):
"""Child class of ChestWindow for slighty hackish reasons."""