bauiv1lib.chest

Provides chest related ui.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3# pylint: disable=too-many-lines
   4"""Provides chest related ui."""
   5
   6from __future__ import annotations
   7
   8import math
   9import random
  10from typing import override, TYPE_CHECKING
  11
  12from efro.util import strict_partial
  13import bacommon.bs
  14import bauiv1 as bui
  15
  16if TYPE_CHECKING:
  17    import datetime
  18
  19    import baclassic
  20
  21_g_open_voices: list[tuple[float, str, float]] = []
  22
  23
  24class ChestWindow(bui.MainWindow):
  25    """Allows viewing and performing operations on a chest."""
  26
  27    def __init__(
  28        self,
  29        index: int,
  30        transition: str | None = 'in_right',
  31        origin_widget: bui.Widget | None = None,
  32    ):
  33        self._index = index
  34
  35        assert bui.app.classic is not None
  36        uiscale = bui.app.ui_v1.uiscale
  37        self._width = 1050 if uiscale is bui.UIScale.SMALL else 650
  38        self._height = 550 if uiscale is bui.UIScale.SMALL else 450
  39        self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0
  40        self._yoffs = -50 if uiscale is bui.UIScale.SMALL else -35
  41        self._action_in_flight = False
  42        self._open_now_button: bui.Widget | None = None
  43        self._open_now_spinner: bui.Widget | None = None
  44        self._open_now_texts: list[bui.Widget] = []
  45        self._open_now_images: list[bui.Widget] = []
  46        self._watch_ad_button: bui.Widget | None = None
  47        self._time_string_timer: bui.AppTimer | None = None
  48        self._time_string_text: bui.Widget | None = None
  49        self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = []
  50        self._prizeindex = -1
  51        self._prizesettxts: dict[int, list[bui.Widget]] = {}
  52        self._prizesetimgs: dict[int, list[bui.Widget]] = {}
  53        self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = (
  54            None
  55        )
  56
  57        # The set of widgets we keep when doing a clear.
  58        self._core_widgets: list[bui.Widget] = []
  59
  60        super().__init__(
  61            root_widget=bui.containerwidget(
  62                size=(self._width, self._height),
  63                toolbar_visibility='menu_full',
  64                scale=(
  65                    1.45
  66                    if uiscale is bui.UIScale.SMALL
  67                    else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9
  68                ),
  69                stack_offset=(
  70                    (0, 0)
  71                    if uiscale is bui.UIScale.SMALL
  72                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
  73                ),
  74            ),
  75            transition=transition,
  76            origin_widget=origin_widget,
  77        )
  78
  79        # Tell the root-ui to stop updating toolbar values immediately;
  80        # this allows it to run animations based on the results of our
  81        # chest opening.
  82        bui.root_ui_pause_updates()
  83        self._root_ui_updates_paused = True
  84
  85        self._title_text = bui.textwidget(
  86            parent=self._root_widget,
  87            position=(0, self._height - 50 + self._yoffs),
  88            size=(self._width, 25),
  89            text=f'Chest Slot {self._index + 1}',
  90            color=bui.app.ui_v1.title_color,
  91            maxwidth=150.0,
  92            h_align='center',
  93            v_align='center',
  94        )
  95        self._core_widgets.append(self._title_text)
  96
  97        if uiscale is bui.UIScale.SMALL:
  98            bui.containerwidget(
  99                edit=self._root_widget, on_cancel_call=self.main_window_back
 100            )
 101        else:
 102            btn = bui.buttonwidget(
 103                parent=self._root_widget,
 104                position=(self._xoffs + 50, self._height - 55 + self._yoffs),
 105                size=(60, 55),
 106                scale=0.8,
 107                label=bui.charstr(bui.SpecialChar.BACK),
 108                button_type='backSmall',
 109                extra_touch_border_scale=2.0,
 110                autoselect=True,
 111                on_activate_call=self.main_window_back,
 112            )
 113            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 114            self._core_widgets.append(btn)
 115
 116        # Note: Don't need to explicitly clean this up. Just not adding
 117        # it to core_widgets so it will go away on next reset.
 118        self._loadingspinner = bui.spinnerwidget(
 119            parent=self._root_widget,
 120            position=(self._width * 0.5, self._height * 0.5),
 121        )
 122
 123        self._infotext = bui.textwidget(
 124            parent=self._root_widget,
 125            position=(self._width * 0.5, self._height - 200 + self._yoffs),
 126            size=(0, 0),
 127            text='',
 128            maxwidth=700,
 129            scale=0.8,
 130            color=(0.6, 0.5, 0.6),
 131            h_align='center',
 132            v_align='center',
 133        )
 134        self._core_widgets.append(self._infotext)
 135
 136        plus = bui.app.plus
 137        if plus is None:
 138            self._error('Plus feature-set is not present.')
 139            return
 140
 141        if plus.accounts.primary is None:
 142            self._error(bui.Lstr(resource='notSignedInText'))
 143            return
 144
 145        # Start by showing info/options for our target chest. Note that
 146        # we always ask the server for these values even though we may
 147        # have them through our appmode subscription which updates the
 148        # chest UI. This is because the wait_for_connectivity()
 149        # mechanism will often bring our window up a split second before
 150        # the chest subscription receives its first values which would
 151        # lead us to incorrectly think there is no chest there. If we
 152        # want to optimize this in the future we could perhaps use local
 153        # values only if there is a chest present in them.
 154        assert not self._action_in_flight
 155        self._action_in_flight = True
 156        with plus.accounts.primary:
 157            plus.cloud.send_message_cb(
 158                bacommon.bs.ChestInfoMessage(chest_id=str(self._index)),
 159                on_response=bui.WeakCall(self._on_chest_info_response),
 160            )
 161
 162    def __del__(self) -> None:
 163        # print('~ChestWindow()')
 164
 165        # Make sure UI updates are resumed if we haven't done so.
 166        if self._root_ui_updates_paused:
 167            bui.root_ui_resume_updates()
 168
 169    @override
 170    def get_main_window_state(self) -> bui.MainWindowState:
 171        # Support recreating our window for back/refresh purposes.
 172        cls = type(self)
 173
 174        # Pull anything we need from self out here; if we do it in the
 175        # lambda we keep self alive which is bad.
 176        index = self._index
 177
 178        return bui.BasicMainWindowState(
 179            create_call=lambda transition, origin_widget: cls(
 180                index=index, transition=transition, origin_widget=origin_widget
 181            )
 182        )
 183
 184    def _update_time_display(self, unlock_time: datetime.datetime) -> None:
 185        # Once text disappears, kill our timer.
 186        if not self._time_string_text:
 187            self._time_string_timer = None
 188            return
 189        now = bui.utc_now_cloud()
 190        secs_till_open = max(0.0, (unlock_time - now).total_seconds())
 191        tstr = (
 192            bui.timestring(secs_till_open, centi=False)
 193            if secs_till_open > 0
 194            else ''
 195        )
 196        bui.textwidget(edit=self._time_string_text, text=tstr)
 197
 198    def _on_chest_info_response(
 199        self, response: bacommon.bs.ChestInfoResponse | Exception
 200    ) -> None:
 201        assert self._action_in_flight  # Should be us.
 202        self._action_in_flight = False
 203
 204        if isinstance(response, Exception):
 205            self._error(
 206                # bui.Lstr(resource='internal.unavailableNoConnectionText')
 207                'Unable to complete this right now.\nPlease try again.',
 208                minor=True,
 209            )
 210            return
 211
 212        if response.chest is None:
 213            self._show_about_chest_slots()
 214            return
 215
 216        assert response.user_tokens is not None
 217        self._show_chest_actions(response.user_tokens, response.chest)
 218
 219    def _on_chest_action_response(
 220        self, response: bacommon.bs.ChestActionResponse | Exception
 221    ) -> None:
 222        assert self._action_in_flight  # Should be us.
 223        self._action_in_flight = False
 224
 225        # Communication/local error:
 226        if isinstance(response, Exception):
 227            self._error(
 228                # bui.Lstr(resource='internal.unavailableNoConnectionText')
 229                'Unable to complete this right now.\nPlease try again.',
 230                minor=True,
 231            )
 232            return
 233
 234        # Server-side error:
 235        if response.error is not None:
 236            self._error(bui.Lstr(translate=('serverResponses', response.error)))
 237            return
 238
 239        # Show any bundled success message.
 240        if response.success_msg is not None:
 241            bui.screenmessage(
 242                bui.Lstr(translate=('serverResponses', response.success_msg)),
 243                color=(0, 1.0, 0),
 244            )
 245            bui.getsound('cashRegister').play()
 246
 247        # Show any bundled warning.
 248        if response.warning is not None:
 249            bui.screenmessage(
 250                bui.Lstr(translate=('serverResponses', response.warning)),
 251                color=(1, 0.5, 0),
 252            )
 253            bui.getsound('error').play()
 254
 255        # If we just paid for something, make a sound accordingly.
 256        if bool(False):  # Hmm maybe this feels odd.
 257            if response.tokens_charged > 0:
 258                bui.getsound('cashRegister').play()
 259
 260        # If there's contents listed in the response, show them.
 261        if response.contents is not None:
 262            self._show_chest_contents(response)
 263        else:
 264            # Otherwise we're done here; just close out our UI.
 265            self.main_window_back()
 266
 267    def _show_chest_actions(
 268        self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest
 269    ) -> None:
 270        """Show state for our chest."""
 271        # pylint: disable=too-many-locals
 272        # pylint: disable=cyclic-import
 273        from baclassic import (
 274            ClassicAppMode,
 275            CHEST_APPEARANCE_DISPLAY_INFOS,
 276            CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
 277        )
 278
 279        plus = bui.app.plus
 280        assert plus is not None
 281
 282        # We expect to be run under classic app mode.
 283        mode = bui.app.mode
 284        if not isinstance(mode, ClassicAppMode):
 285            self._error('Classic app mode not active.')
 286            return
 287
 288        self._reset()
 289
 290        self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
 291            chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
 292        )
 293
 294        bui.textwidget(
 295            edit=self._title_text, text=f'{chest.appearance.name} Chest'
 296        )
 297
 298        imgsize = 145
 299        bui.imagewidget(
 300            parent=self._root_widget,
 301            position=(
 302                self._width * 0.5 - imgsize * 0.5,
 303                self._height - 223 + self._yoffs,
 304            ),
 305            color=self._chestdisplayinfo.color,
 306            size=(imgsize, imgsize),
 307            texture=bui.gettexture(self._chestdisplayinfo.texclosed),
 308            tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
 309            tint_color=self._chestdisplayinfo.tint,
 310            tint2_color=self._chestdisplayinfo.tint2,
 311        )
 312
 313        # Store the prize-sets so we can display odds/etc. Sort them
 314        # with largest weights first.
 315        self._prizesets = sorted(
 316            chest.prizesets, key=lambda s: s.weight, reverse=True
 317        )
 318
 319        if chest.unlock_tokens > 0:
 320            lsize = 30
 321            bui.imagewidget(
 322                parent=self._root_widget,
 323                position=(
 324                    self._width * 0.5 - imgsize * 0.4 - lsize * 0.5,
 325                    self._height - 223 + 27.0 + self._yoffs,
 326                ),
 327                size=(lsize, lsize),
 328                texture=bui.gettexture('lock'),
 329            )
 330
 331        # Time string.
 332        if chest.unlock_tokens != 0:
 333            self._time_string_text = bui.textwidget(
 334                parent=self._root_widget,
 335                position=(self._width * 0.5, self._height - 85 + self._yoffs),
 336                size=(0, 0),
 337                text='',
 338                maxwidth=700,
 339                scale=0.6,
 340                color=(0.6, 1.0, 0.6),
 341                h_align='center',
 342                v_align='center',
 343            )
 344            self._update_time_display(chest.unlock_time)
 345            self._time_string_timer = bui.AppTimer(
 346                1.0,
 347                repeat=True,
 348                call=bui.WeakCall(self._update_time_display, chest.unlock_time),
 349            )
 350
 351        # Allow watching an ad IF the server tells us we can AND we have
 352        # an ad ready to show.
 353        show_ad_button = (
 354            chest.unlock_tokens > 0
 355            and chest.ad_allow
 356            and plus.have_incentivized_ad()
 357        )
 358
 359        bwidth = 130
 360        bheight = 90
 361        bposy = -330 if chest.unlock_tokens == 0 else -340
 362        hspace = 20
 363        boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0
 364
 365        self._open_now_button = bui.buttonwidget(
 366            parent=self._root_widget,
 367            position=(
 368                self._width * 0.5 - bwidth * 0.5 + boffsx,
 369                self._height + bposy + self._yoffs,
 370            ),
 371            size=(bwidth, bheight),
 372            label='',
 373            button_type='square',
 374            autoselect=True,
 375            on_activate_call=bui.WeakCall(
 376                self._open_press, user_tokens, chest.unlock_tokens
 377            ),
 378            enable_sound=False,
 379        )
 380        self._open_now_images = []
 381        self._open_now_texts = []
 382
 383        iconsize = 50
 384        if chest.unlock_tokens == 0:
 385            self._open_now_texts.append(
 386                bui.textwidget(
 387                    parent=self._root_widget,
 388                    text='Open',
 389                    position=(
 390                        self._width * 0.5 + boffsx,
 391                        self._height + bposy + self._yoffs + bheight * 0.5,
 392                    ),
 393                    color=(0, 1, 0),
 394                    draw_controller=self._open_now_button,
 395                    scale=0.7,
 396                    maxwidth=bwidth * 0.8,
 397                    size=(0, 0),
 398                    h_align='center',
 399                    v_align='center',
 400                )
 401            )
 402        else:
 403            self._open_now_texts.append(
 404                bui.textwidget(
 405                    parent=self._root_widget,
 406                    text='Open Now',
 407                    position=(
 408                        self._width * 0.5 + boffsx,
 409                        self._height + bposy + self._yoffs + bheight * 1.15,
 410                    ),
 411                    maxwidth=bwidth * 0.8,
 412                    scale=0.7,
 413                    color=(0.7, 1, 0.7),
 414                    size=(0, 0),
 415                    h_align='center',
 416                    v_align='center',
 417                )
 418            )
 419            self._open_now_images.append(
 420                bui.imagewidget(
 421                    parent=self._root_widget,
 422                    size=(iconsize, iconsize),
 423                    position=(
 424                        self._width * 0.5 - iconsize * 0.5 + boffsx,
 425                        self._height + bposy + self._yoffs + bheight * 0.35,
 426                    ),
 427                    draw_controller=self._open_now_button,
 428                    texture=bui.gettexture('coin'),
 429                )
 430            )
 431            self._open_now_texts.append(
 432                bui.textwidget(
 433                    parent=self._root_widget,
 434                    text=bui.Lstr(
 435                        resource='tokens.numTokensText',
 436                        subs=[('${COUNT}', str(chest.unlock_tokens))],
 437                    ),
 438                    position=(
 439                        self._width * 0.5 + boffsx,
 440                        self._height + bposy + self._yoffs + bheight * 0.25,
 441                    ),
 442                    scale=0.65,
 443                    color=(0, 1, 0),
 444                    draw_controller=self._open_now_button,
 445                    maxwidth=bwidth * 0.8,
 446                    size=(0, 0),
 447                    h_align='center',
 448                    v_align='center',
 449                )
 450            )
 451        self._open_now_spinner = bui.spinnerwidget(
 452            parent=self._root_widget,
 453            position=(
 454                self._width * 0.5 + boffsx,
 455                self._height + bposy + self._yoffs + 0.5 * bheight,
 456            ),
 457            visible=False,
 458        )
 459
 460        if show_ad_button:
 461            bui.textwidget(
 462                parent=self._root_widget,
 463                text='Reduce Wait',
 464                position=(
 465                    self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
 466                    self._height + bposy + self._yoffs + bheight * 1.15,
 467                ),
 468                maxwidth=bwidth * 0.8,
 469                scale=0.7,
 470                color=(0.7, 1, 0.7),
 471                size=(0, 0),
 472                h_align='center',
 473                v_align='center',
 474            )
 475            self._watch_ad_button = bui.buttonwidget(
 476                parent=self._root_widget,
 477                position=(
 478                    self._width * 0.5 + hspace * 0.5,
 479                    self._height + bposy + self._yoffs,
 480                ),
 481                size=(bwidth, bheight),
 482                label='',
 483                button_type='square',
 484                autoselect=True,
 485                on_activate_call=bui.WeakCall(self._watch_ad_press),
 486                enable_sound=False,
 487            )
 488            bui.imagewidget(
 489                parent=self._root_widget,
 490                size=(iconsize, iconsize),
 491                position=(
 492                    self._width * 0.5
 493                    + hspace * 0.5
 494                    + bwidth * 0.5
 495                    - iconsize * 0.5,
 496                    self._height + bposy + self._yoffs + bheight * 0.35,
 497                ),
 498                draw_controller=self._watch_ad_button,
 499                color=(1.5, 1.0, 2.0),
 500                texture=bui.gettexture('tv'),
 501            )
 502            # Note to self: AdMob requires rewarded ad usage
 503            # specifically says 'Ad' in it.
 504            bui.textwidget(
 505                parent=self._root_widget,
 506                text=bui.Lstr(resource='watchAnAdText'),
 507                position=(
 508                    self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
 509                    self._height + bposy + self._yoffs + bheight * 0.25,
 510                ),
 511                scale=0.65,
 512                color=(0, 1, 0),
 513                draw_controller=self._watch_ad_button,
 514                maxwidth=bwidth * 0.8,
 515                size=(0, 0),
 516                h_align='center',
 517                v_align='center',
 518            )
 519
 520        self._show_odds(initial_highlighted_row=-1)
 521
 522    def _highlight_odds_row(self, row: int, extra: bool = False) -> None:
 523
 524        for rindex, imgs in self._prizesetimgs.items():
 525            opacity = (
 526                (0.9 if extra else 0.75)
 527                if rindex == row
 528                else (0.4 if extra else 0.5)
 529            )
 530            for img in imgs:
 531                if img:
 532                    bui.imagewidget(edit=img, opacity=opacity)
 533
 534        for rindex, txts in self._prizesettxts.items():
 535            opacity = (
 536                (0.9 if extra else 0.75)
 537                if rindex == row
 538                else (0.4 if extra else 0.5)
 539            )
 540            for txt in txts:
 541                if txt:
 542                    bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity))
 543
 544    def _show_odds(
 545        self,
 546        *,
 547        initial_highlighted_row: int,
 548        initial_highlighted_extra: bool = False,
 549    ) -> None:
 550        # pylint: disable=too-many-locals
 551        xoffs = 110
 552
 553        totalweight = max(0.001, sum(t.weight for t in self._prizesets))
 554
 555        rowheight = 25
 556        totalheight = (len(self._prizesets) + 1) * rowheight
 557        x = self._width * 0.5 + xoffs
 558        y = self._height + self._yoffs - 150.0 + totalheight * 0.5
 559
 560        # Title.
 561        bui.textwidget(
 562            parent=self._root_widget,
 563            text='Prize Odds',
 564            color=(0.7, 0.65, 1, 0.5),
 565            flatness=1.0,
 566            shadow=1.0,
 567            position=(x, y),
 568            scale=0.55,
 569            size=(0, 0),
 570            h_align='left',
 571            v_align='center',
 572        )
 573        y -= 5.0
 574
 575        prizesettxts: list[bui.Widget]
 576        prizesetimgs: list[bui.Widget]
 577
 578        def _mkicon(img: str) -> None:
 579            iconsize = 20.0
 580            nonlocal x
 581            nonlocal prizesetimgs
 582            prizesetimgs.append(
 583                bui.imagewidget(
 584                    parent=self._root_widget,
 585                    size=(iconsize, iconsize),
 586                    position=(x, y - iconsize * 0.5),
 587                    texture=bui.gettexture(img),
 588                    opacity=0.4,
 589                )
 590            )
 591            x += iconsize
 592
 593        def _mktxt(txt: str, advance: bool = True) -> None:
 594            tscale = 0.45
 595            nonlocal x
 596            nonlocal prizesettxts
 597            prizesettxts.append(
 598                bui.textwidget(
 599                    parent=self._root_widget,
 600                    text=txt,
 601                    flatness=1.0,
 602                    shadow=1.0,
 603                    position=(x, y),
 604                    scale=tscale,
 605                    size=(0, 0),
 606                    h_align='left',
 607                    v_align='center',
 608                )
 609            )
 610            if advance:
 611                x += (bui.get_string_width(txt, suppress_warning=True)) * tscale
 612
 613        self._prizesettxts = {}
 614        self._prizesetimgs = {}
 615
 616        for i, p in enumerate(self._prizesets):
 617            prizesettxts = self._prizesettxts.setdefault(i, [])
 618            prizesetimgs = self._prizesetimgs.setdefault(i, [])
 619            x = self._width * 0.5 + xoffs
 620            y -= rowheight
 621            percent = 100.0 * p.weight / totalweight
 622
 623            # Show decimals only if we get very small percentages (looks
 624            # better than rounding as '0%').
 625            percenttxt = (
 626                f'{percent:.2f}'
 627                if percent < 0.1
 628                else (
 629                    f'{percent:.1f}' if percent < 1.0 else f'{round(percent)}%:'
 630                )
 631            )
 632
 633            # We advance manually here to keep values lined up
 634            # (otherwise single digit percent rows don't line up with
 635            # double digit ones).
 636            _mktxt(percenttxt, advance=False)
 637            x += 35.0
 638
 639            for item in p.contents:
 640                x += 5.0
 641                if isinstance(item.item, bacommon.bs.TicketsDisplayItem):
 642                    _mktxt(str(item.item.count))
 643                    _mkicon('tickets')
 644                elif isinstance(item.item, bacommon.bs.TokensDisplayItem):
 645                    _mktxt(str(item.item.count))
 646                    _mkicon('coin')
 647                else:
 648                    # For other cases just fall back on text desc.
 649                    #
 650                    # Translate the wrapper description and apply any subs.
 651                    descfin = bui.Lstr(
 652                        translate=('serverResponses', item.description)
 653                    ).evaluate()
 654                    subs = (
 655                        []
 656                        if item.description_subs is None
 657                        else item.description_subs
 658                    )
 659                    assert len(subs) % 2 == 0  # Should always be even.
 660                    for j in range(0, len(subs) - 1, 2):
 661                        descfin = descfin.replace(subs[j], subs[j + 1])
 662                    _mktxt(descfin)
 663        self._highlight_odds_row(
 664            initial_highlighted_row, extra=initial_highlighted_extra
 665        )
 666
 667    def _open_press(self, user_tokens: int, token_payment: int) -> None:
 668        from bauiv1lib.gettokens import show_get_tokens_prompt
 669
 670        bui.getsound('click01').play()
 671
 672        # Allow only one in-flight action at once.
 673        if self._action_in_flight:
 674            bui.screenmessage(
 675                bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
 676            )
 677            bui.getsound('error').play()
 678            return
 679
 680        plus = bui.app.plus
 681        assert plus is not None
 682
 683        if plus.accounts.primary is None:
 684            self._error(bui.Lstr(resource='notSignedInText'))
 685            return
 686
 687        # Offer to purchase tokens if they don't have enough.
 688        if user_tokens < token_payment:
 689            # Hack: We disable normal swish for the open button and it
 690            # seems weird without a swish here, so explicitly do one.
 691            bui.getsound('swish').play()
 692            show_get_tokens_prompt()
 693            return
 694
 695        self._action_in_flight = True
 696        with plus.accounts.primary:
 697            plus.cloud.send_message_cb(
 698                bacommon.bs.ChestActionMessage(
 699                    chest_id=str(self._index),
 700                    action=bacommon.bs.ChestActionMessage.Action.UNLOCK,
 701                    token_payment=token_payment,
 702                ),
 703                on_response=bui.WeakCall(self._on_chest_action_response),
 704            )
 705
 706        # Convey that something is in progress.
 707        if self._open_now_button:
 708            bui.spinnerwidget(edit=self._open_now_spinner, visible=True)
 709            for twidget in self._open_now_texts:
 710                bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2))
 711            for iwidget in self._open_now_images:
 712                bui.imagewidget(edit=iwidget, opacity=0.2)
 713
 714    def _watch_ad_press(self) -> None:
 715
 716        bui.getsound('click01').play()
 717
 718        # Allow only one in-flight action at once.
 719        if self._action_in_flight:
 720            bui.screenmessage(
 721                bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
 722            )
 723            bui.getsound('error').play()
 724            return
 725
 726        assert bui.app.classic is not None
 727
 728        self._action_in_flight = True
 729        bui.app.classic.ads.show_ad_2(
 730            'reduce_chest_wait',
 731            on_completion_call=bui.WeakCall(self._watch_ad_complete),
 732        )
 733
 734        # Convey that something is in progress.
 735        if self._watch_ad_button:
 736            bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4))
 737
 738    def _watch_ad_complete(self, actually_showed: bool) -> None:
 739
 740        assert self._action_in_flight  # Should be ad view.
 741        self._action_in_flight = False
 742
 743        if not actually_showed:
 744            return
 745
 746        # Allow only one in-flight action at once.
 747        if self._action_in_flight:
 748            bui.screenmessage(
 749                bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
 750            )
 751            bui.getsound('error').play()
 752            return
 753
 754        plus = bui.app.plus
 755        assert plus is not None
 756
 757        if plus.accounts.primary is None:
 758            self._error(bui.Lstr(resource='notSignedInText'))
 759            return
 760
 761        self._action_in_flight = True
 762        with plus.accounts.primary:
 763            plus.cloud.send_message_cb(
 764                bacommon.bs.ChestActionMessage(
 765                    chest_id=str(self._index),
 766                    action=bacommon.bs.ChestActionMessage.Action.AD,
 767                    token_payment=0,
 768                ),
 769                on_response=bui.WeakCall(self._on_chest_action_response),
 770            )
 771
 772    def _reset(self) -> None:
 773        """Clear all non-permanent widgets and clear infotext."""
 774        for widget in self._root_widget.get_children():
 775            if widget not in self._core_widgets:
 776                widget.delete()
 777        bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1))
 778
 779    def _error(self, msg: str | bui.Lstr, minor: bool = False) -> None:
 780        """Put ourself in an error state with a visible error message."""
 781        self._reset()
 782        bui.textwidget(
 783            edit=self._infotext,
 784            text=msg,
 785            color=(1, 0.5, 0.5) if minor else (1, 0, 0),
 786        )
 787
 788    def _show_about_chest_slots(self) -> None:
 789        # No-op if our ui is dead.
 790        if not self._root_widget:
 791            return
 792
 793        self._reset()
 794        msg = (
 795            'This slot can hold a treasure chest.\n\n'
 796            'Earn chests by playing campaign levels,\n'
 797            'placing in tournaments, and completing\n'
 798            'achievements.'
 799        )
 800        bui.textwidget(edit=self._infotext, text=msg, color=(1, 1, 1))
 801
 802    def _show_chest_contents(
 803        self, response: bacommon.bs.ChestActionResponse
 804    ) -> None:
 805        # pylint: disable=too-many-locals
 806
 807        from baclassic import show_display_item
 808
 809        # No-op if our ui is dead.
 810        if not self._root_widget:
 811            return
 812
 813        assert response.contents is not None
 814
 815        # Insert test items for testing.
 816        if bool(False):
 817            response.contents += [
 818                bacommon.bs.DisplayItemWrapper.for_display_item(
 819                    bacommon.bs.TestDisplayItem()
 820                )
 821            ]
 822
 823        tincr = 0.4
 824        tendoffs = tincr * 4.0
 825        toffs = 0.0
 826
 827        bui.getsound('revUp').play(volume=2.0)
 828
 829        # Show nothing but the chest icon and animate it shaking.
 830        self._reset()
 831        imgsize = 145
 832        assert self._chestdisplayinfo is not None
 833        img = bui.imagewidget(
 834            parent=self._root_widget,
 835            color=self._chestdisplayinfo.color,
 836            texture=bui.gettexture(self._chestdisplayinfo.texclosed),
 837            tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
 838            tint_color=self._chestdisplayinfo.tint,
 839            tint2_color=self._chestdisplayinfo.tint2,
 840        )
 841
 842        def _set_img(x: float, scale: float) -> None:
 843            if not img:
 844                return
 845            bui.imagewidget(
 846                edit=img,
 847                position=(
 848                    self._width * 0.5 - imgsize * scale * 0.5 + x,
 849                    self._height
 850                    - 223
 851                    + self._yoffs
 852                    + imgsize * 0.5
 853                    - imgsize * scale * 0.5,
 854                ),
 855                size=(imgsize * scale, imgsize * scale),
 856            )
 857
 858        # Set initial place.
 859        _set_img(0.0, 1.0)
 860
 861        sign = 1.0
 862        while toffs < tendoffs:
 863            toffs += 0.03 * random.uniform(0.5, 1.5)
 864            sign = -sign
 865            bui.apptimer(
 866                toffs,
 867                bui.Call(
 868                    _set_img,
 869                    x=(
 870                        20.0
 871                        * random.uniform(0.3, 1.0)
 872                        * math.pow(toffs / tendoffs, 2.0)
 873                        * sign
 874                    ),
 875                    scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0),
 876                ),
 877            )
 878
 879        xspacing = 100
 880        xoffs = -0.5 * (len(response.contents) - 1) * xspacing
 881        bui.apptimer(
 882            toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0)
 883        )
 884        # Play a variety of voice sounds.
 885
 886        # We keep a global list of voice options which we randomly pull
 887        # from and refill when empty. This ensures everything gets
 888        # played somewhat frequently and minimizes annoying repeats.
 889        global _g_open_voices  # pylint: disable=global-statement
 890        if not _g_open_voices:
 891            _g_open_voices = [
 892                (0.3, 'woo3', 2.5),
 893                (0.1, 'gasp', 1.3),
 894                (0.2, 'woo2', 2.0),
 895                (0.2, 'wow', 2.0),
 896                (0.2, 'kronk2', 2.0),
 897                (0.2, 'mel03', 2.0),
 898                (0.2, 'aww', 2.0),
 899                (0.4, 'nice', 2.0),
 900                (0.3, 'yeah', 1.5),
 901                (0.2, 'woo', 1.0),
 902                (0.5, 'ooh', 0.8),
 903            ]
 904
 905        voicetimeoffs, voicename, volume = _g_open_voices.pop(
 906            random.randrange(len(_g_open_voices))
 907        )
 908        bui.apptimer(
 909            toffs + voicetimeoffs,
 910            lambda: bui.getsound(voicename).play(volume=volume),
 911        )
 912
 913        toffsopen = toffs
 914        bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening))
 915        toffs += tincr * 1.0
 916        width = xspacing * 0.95
 917
 918        for item in response.contents:
 919            toffs += tincr
 920            bui.apptimer(
 921                toffs - 0.1, lambda: bui.getsound('cashRegister').play()
 922            )
 923            bui.apptimer(
 924                toffs,
 925                strict_partial(
 926                    show_display_item,
 927                    item,
 928                    self._root_widget,
 929                    pos=(
 930                        self._width * 0.5 + xoffs,
 931                        self._height - 250.0 + self._yoffs,
 932                    ),
 933                    width=width,
 934                ),
 935            )
 936            xoffs += xspacing
 937        toffs += tincr
 938        bui.apptimer(toffs, bui.WeakCall(self._show_done_button))
 939
 940        self._show_odds(initial_highlighted_row=-1)
 941
 942        # Store this for later
 943        self._prizeindex = response.prizeindex
 944
 945        # The final result was already randomly selected on the server,
 946        # but we want to give the illusion of randomness here, so cycle
 947        # through highlighting our options and stop on the winner when
 948        # the chest opens. To do this, we start at the end at the prize
 949        # and work backwards setting timers.
 950        if self._prizesets:
 951            toffs2 = toffsopen - 0.01
 952            amt = 0.02
 953            i = self._prizeindex
 954            while toffs2 > 0.0:
 955                bui.apptimer(
 956                    toffs2,
 957                    bui.WeakCall(self._highlight_odds_row, i),
 958                )
 959                toffs2 -= amt
 960                amt *= 1.05 * random.uniform(0.9, 1.1)
 961                i = (i - 1) % len(self._prizesets)
 962
 963    def _show_chest_opening(self) -> None:
 964
 965        # No-op if our ui is dead.
 966        if not self._root_widget:
 967            return
 968
 969        self._reset()
 970        imgsize = 145
 971        bui.getsound('hiss').play()
 972        assert self._chestdisplayinfo is not None
 973        img = bui.imagewidget(
 974            parent=self._root_widget,
 975            color=self._chestdisplayinfo.color,
 976            texture=bui.gettexture(self._chestdisplayinfo.texopen),
 977            tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint),
 978            tint_color=self._chestdisplayinfo.tint,
 979            tint2_color=self._chestdisplayinfo.tint2,
 980        )
 981        tincr = 0.8
 982        tendoffs = tincr * 2.0
 983        toffs = 0.0
 984
 985        def _set_img(x: float, scale: float) -> None:
 986            if not img:
 987                return
 988            bui.imagewidget(
 989                edit=img,
 990                position=(
 991                    self._width * 0.5 - imgsize * scale * 0.5 + x,
 992                    self._height
 993                    - 223
 994                    + self._yoffs
 995                    + imgsize * 0.5
 996                    - imgsize * scale * 0.5,
 997                ),
 998                size=(imgsize * scale, imgsize * scale),
 999            )
1000
1001        # Set initial place.
1002        _set_img(0.0, 1.0)
1003
1004        sign = 1.0
1005        while toffs < tendoffs:
1006            toffs += 0.03 * random.uniform(0.5, 1.5)
1007            sign = -sign
1008            # Note: we speed x along here (multing toffs) so position
1009            # comes to rest before scale.
1010            bui.apptimer(
1011                toffs,
1012                bui.Call(
1013                    _set_img,
1014                    x=(
1015                        1.0
1016                        * random.uniform(0.3, 1.0)
1017                        * (
1018                            1.0
1019                            - math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0)
1020                        )
1021                        * sign
1022                    ),
1023                    scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5),
1024                ),
1025            )
1026
1027        self._show_odds(
1028            initial_highlighted_row=self._prizeindex,
1029            initial_highlighted_extra=True,
1030        )
1031
1032    def _show_done_button(self) -> None:
1033        # No-op if our ui is dead.
1034        if not self._root_widget:
1035            return
1036
1037        bwidth = 200
1038        bheight = 60
1039
1040        btn = bui.buttonwidget(
1041            parent=self._root_widget,
1042            position=(
1043                self._width * 0.5 - bwidth * 0.5,
1044                self._height - 350 + self._yoffs,
1045            ),
1046            size=(bwidth, bheight),
1047            label=bui.Lstr(resource='doneText'),
1048            autoselect=True,
1049            on_activate_call=self.main_window_back,
1050        )
1051        bui.containerwidget(edit=self._root_widget, start_button=btn)
1052
1053
1054# Slight hack: we define window different classes for our different
1055# chest slots so that the default UI behavior is to replace each other
1056# when different ones are pressed. If they are all the same window class
1057# then the default behavior for such presses is to toggle the existing
1058# one back off.
1059
1060
1061class ChestWindow0(ChestWindow):
1062    """Child class of ChestWindow for slighty hackish reasons."""
1063
1064
1065class ChestWindow1(ChestWindow):
1066    """Child class of ChestWindow for slighty hackish reasons."""
1067
1068
1069class ChestWindow2(ChestWindow):
1070    """Child class of ChestWindow for slighty hackish reasons."""
1071
1072
1073class ChestWindow3(ChestWindow):
1074    """Child class of ChestWindow for slighty hackish reasons."""
class ChestWindow(bauiv1._uitypes.MainWindow):
  25class ChestWindow(bui.MainWindow):
  26    """Allows viewing and performing operations on a chest."""
  27
  28    def __init__(
  29        self,
  30        index: int,
  31        transition: str | None = 'in_right',
  32        origin_widget: bui.Widget | None = None,
  33    ):
  34        self._index = index
  35
  36        assert bui.app.classic is not None
  37        uiscale = bui.app.ui_v1.uiscale
  38        self._width = 1050 if uiscale is bui.UIScale.SMALL else 650
  39        self._height = 550 if uiscale is bui.UIScale.SMALL else 450
  40        self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0
  41        self._yoffs = -50 if uiscale is bui.UIScale.SMALL else -35
  42        self._action_in_flight = False
  43        self._open_now_button: bui.Widget | None = None
  44        self._open_now_spinner: bui.Widget | None = None
  45        self._open_now_texts: list[bui.Widget] = []
  46        self._open_now_images: list[bui.Widget] = []
  47        self._watch_ad_button: bui.Widget | None = None
  48        self._time_string_timer: bui.AppTimer | None = None
  49        self._time_string_text: bui.Widget | None = None
  50        self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = []
  51        self._prizeindex = -1
  52        self._prizesettxts: dict[int, list[bui.Widget]] = {}
  53        self._prizesetimgs: dict[int, list[bui.Widget]] = {}
  54        self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = (
  55            None
  56        )
  57
  58        # The set of widgets we keep when doing a clear.
  59        self._core_widgets: list[bui.Widget] = []
  60
  61        super().__init__(
  62            root_widget=bui.containerwidget(
  63                size=(self._width, self._height),
  64                toolbar_visibility='menu_full',
  65                scale=(
  66                    1.45
  67                    if uiscale is bui.UIScale.SMALL
  68                    else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9
  69                ),
  70                stack_offset=(
  71                    (0, 0)
  72                    if uiscale is bui.UIScale.SMALL
  73                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
  74                ),
  75            ),
  76            transition=transition,
  77            origin_widget=origin_widget,
  78        )
  79
  80        # Tell the root-ui to stop updating toolbar values immediately;
  81        # this allows it to run animations based on the results of our
  82        # chest opening.
  83        bui.root_ui_pause_updates()
  84        self._root_ui_updates_paused = True
  85
  86        self._title_text = bui.textwidget(
  87            parent=self._root_widget,
  88            position=(0, self._height - 50 + self._yoffs),
  89            size=(self._width, 25),
  90            text=f'Chest Slot {self._index + 1}',
  91            color=bui.app.ui_v1.title_color,
  92            maxwidth=150.0,
  93            h_align='center',
  94            v_align='center',
  95        )
  96        self._core_widgets.append(self._title_text)
  97
  98        if uiscale is bui.UIScale.SMALL:
  99            bui.containerwidget(
 100                edit=self._root_widget, on_cancel_call=self.main_window_back
 101            )
 102        else:
 103            btn = bui.buttonwidget(
 104                parent=self._root_widget,
 105                position=(self._xoffs + 50, self._height - 55 + self._yoffs),
 106                size=(60, 55),
 107                scale=0.8,
 108                label=bui.charstr(bui.SpecialChar.BACK),
 109                button_type='backSmall',
 110                extra_touch_border_scale=2.0,
 111                autoselect=True,
 112                on_activate_call=self.main_window_back,
 113            )
 114            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 115            self._core_widgets.append(btn)
 116
 117        # Note: Don't need to explicitly clean this up. Just not adding
 118        # it to core_widgets so it will go away on next reset.
 119        self._loadingspinner = bui.spinnerwidget(
 120            parent=self._root_widget,
 121            position=(self._width * 0.5, self._height * 0.5),
 122        )
 123
 124        self._infotext = bui.textwidget(
 125            parent=self._root_widget,
 126            position=(self._width * 0.5, self._height - 200 + self._yoffs),
 127            size=(0, 0),
 128            text='',
 129            maxwidth=700,
 130            scale=0.8,
 131            color=(0.6, 0.5, 0.6),
 132            h_align='center',
 133            v_align='center',
 134        )
 135        self._core_widgets.append(self._infotext)
 136
 137        plus = bui.app.plus
 138        if plus is None:
 139            self._error('Plus feature-set is not present.')
 140            return
 141
 142        if plus.accounts.primary is None:
 143            self._error(bui.Lstr(resource='notSignedInText'))
 144            return
 145
 146        # Start by showing info/options for our target chest. Note that
 147        # we always ask the server for these values even though we may
 148        # have them through our appmode subscription which updates the
 149        # chest UI. This is because the wait_for_connectivity()
 150        # mechanism will often bring our window up a split second before
 151        # the chest subscription receives its first values which would
 152        # lead us to incorrectly think there is no chest there. If we
 153        # want to optimize this in the future we could perhaps use local
 154        # values only if there is a chest present in them.
 155        assert not self._action_in_flight
 156        self._action_in_flight = True
 157        with plus.accounts.primary:
 158            plus.cloud.send_message_cb(
 159                bacommon.bs.ChestInfoMessage(chest_id=str(self._index)),
 160                on_response=bui.WeakCall(self._on_chest_info_response),
 161            )
 162
 163    def __del__(self) -> None:
 164        # print('~ChestWindow()')
 165
 166        # Make sure UI updates are resumed if we haven't done so.
 167        if self._root_ui_updates_paused:
 168            bui.root_ui_resume_updates()
 169
 170    @override
 171    def get_main_window_state(self) -> bui.MainWindowState:
 172        # Support recreating our window for back/refresh purposes.
 173        cls = type(self)
 174
 175        # Pull anything we need from self out here; if we do it in the
 176        # lambda we keep self alive which is bad.
 177        index = self._index
 178
 179        return bui.BasicMainWindowState(
 180            create_call=lambda transition, origin_widget: cls(
 181                index=index, transition=transition, origin_widget=origin_widget
 182            )
 183        )
 184
 185    def _update_time_display(self, unlock_time: datetime.datetime) -> None:
 186        # Once text disappears, kill our timer.
 187        if not self._time_string_text:
 188            self._time_string_timer = None
 189            return
 190        now = bui.utc_now_cloud()
 191        secs_till_open = max(0.0, (unlock_time - now).total_seconds())
 192        tstr = (
 193            bui.timestring(secs_till_open, centi=False)
 194            if secs_till_open > 0
 195            else ''
 196        )
 197        bui.textwidget(edit=self._time_string_text, text=tstr)
 198
 199    def _on_chest_info_response(
 200        self, response: bacommon.bs.ChestInfoResponse | Exception
 201    ) -> None:
 202        assert self._action_in_flight  # Should be us.
 203        self._action_in_flight = False
 204
 205        if isinstance(response, Exception):
 206            self._error(
 207                # bui.Lstr(resource='internal.unavailableNoConnectionText')
 208                'Unable to complete this right now.\nPlease try again.',
 209                minor=True,
 210            )
 211            return
 212
 213        if response.chest is None:
 214            self._show_about_chest_slots()
 215            return
 216
 217        assert response.user_tokens is not None
 218        self._show_chest_actions(response.user_tokens, response.chest)
 219
 220    def _on_chest_action_response(
 221        self, response: bacommon.bs.ChestActionResponse | Exception
 222    ) -> None:
 223        assert self._action_in_flight  # Should be us.
 224        self._action_in_flight = False
 225
 226        # Communication/local error:
 227        if isinstance(response, Exception):
 228            self._error(
 229                # bui.Lstr(resource='internal.unavailableNoConnectionText')
 230                'Unable to complete this right now.\nPlease try again.',
 231                minor=True,
 232            )
 233            return
 234
 235        # Server-side error:
 236        if response.error is not None:
 237            self._error(bui.Lstr(translate=('serverResponses', response.error)))
 238            return
 239
 240        # Show any bundled success message.
 241        if response.success_msg is not None:
 242            bui.screenmessage(
 243                bui.Lstr(translate=('serverResponses', response.success_msg)),
 244                color=(0, 1.0, 0),
 245            )
 246            bui.getsound('cashRegister').play()
 247
 248        # Show any bundled warning.
 249        if response.warning is not None:
 250            bui.screenmessage(
 251                bui.Lstr(translate=('serverResponses', response.warning)),
 252                color=(1, 0.5, 0),
 253            )
 254            bui.getsound('error').play()
 255
 256        # If we just paid for something, make a sound accordingly.
 257        if bool(False):  # Hmm maybe this feels odd.
 258            if response.tokens_charged > 0:
 259                bui.getsound('cashRegister').play()
 260
 261        # If there's contents listed in the response, show them.
 262        if response.contents is not None:
 263            self._show_chest_contents(response)
 264        else:
 265            # Otherwise we're done here; just close out our UI.
 266            self.main_window_back()
 267
 268    def _show_chest_actions(
 269        self, user_tokens: int, chest: bacommon.bs.ChestInfoResponse.Chest
 270    ) -> None:
 271        """Show state for our chest."""
 272        # pylint: disable=too-many-locals
 273        # pylint: disable=cyclic-import
 274        from baclassic import (
 275            ClassicAppMode,
 276            CHEST_APPEARANCE_DISPLAY_INFOS,
 277            CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT,
 278        )
 279
 280        plus = bui.app.plus
 281        assert plus is not None
 282
 283        # We expect to be run under classic app mode.
 284        mode = bui.app.mode
 285        if not isinstance(mode, ClassicAppMode):
 286            self._error('Classic app mode not active.')
 287            return
 288
 289        self._reset()
 290
 291        self._chestdisplayinfo = CHEST_APPEARANCE_DISPLAY_INFOS.get(
 292            chest.appearance, CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT
 293        )
 294
 295        bui.textwidget(
 296            edit=self._title_text, text=f'{chest.appearance.name} Chest'
 297        )
 298
 299        imgsize = 145
 300        bui.imagewidget(
 301            parent=self._root_widget,
 302            position=(
 303                self._width * 0.5 - imgsize * 0.5,
 304                self._height - 223 + self._yoffs,
 305            ),
 306            color=self._chestdisplayinfo.color,
 307            size=(imgsize, imgsize),
 308            texture=bui.gettexture(self._chestdisplayinfo.texclosed),
 309            tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
 310            tint_color=self._chestdisplayinfo.tint,
 311            tint2_color=self._chestdisplayinfo.tint2,
 312        )
 313
 314        # Store the prize-sets so we can display odds/etc. Sort them
 315        # with largest weights first.
 316        self._prizesets = sorted(
 317            chest.prizesets, key=lambda s: s.weight, reverse=True
 318        )
 319
 320        if chest.unlock_tokens > 0:
 321            lsize = 30
 322            bui.imagewidget(
 323                parent=self._root_widget,
 324                position=(
 325                    self._width * 0.5 - imgsize * 0.4 - lsize * 0.5,
 326                    self._height - 223 + 27.0 + self._yoffs,
 327                ),
 328                size=(lsize, lsize),
 329                texture=bui.gettexture('lock'),
 330            )
 331
 332        # Time string.
 333        if chest.unlock_tokens != 0:
 334            self._time_string_text = bui.textwidget(
 335                parent=self._root_widget,
 336                position=(self._width * 0.5, self._height - 85 + self._yoffs),
 337                size=(0, 0),
 338                text='',
 339                maxwidth=700,
 340                scale=0.6,
 341                color=(0.6, 1.0, 0.6),
 342                h_align='center',
 343                v_align='center',
 344            )
 345            self._update_time_display(chest.unlock_time)
 346            self._time_string_timer = bui.AppTimer(
 347                1.0,
 348                repeat=True,
 349                call=bui.WeakCall(self._update_time_display, chest.unlock_time),
 350            )
 351
 352        # Allow watching an ad IF the server tells us we can AND we have
 353        # an ad ready to show.
 354        show_ad_button = (
 355            chest.unlock_tokens > 0
 356            and chest.ad_allow
 357            and plus.have_incentivized_ad()
 358        )
 359
 360        bwidth = 130
 361        bheight = 90
 362        bposy = -330 if chest.unlock_tokens == 0 else -340
 363        hspace = 20
 364        boffsx = (hspace * -0.5 - bwidth * 0.5) if show_ad_button else 0.0
 365
 366        self._open_now_button = bui.buttonwidget(
 367            parent=self._root_widget,
 368            position=(
 369                self._width * 0.5 - bwidth * 0.5 + boffsx,
 370                self._height + bposy + self._yoffs,
 371            ),
 372            size=(bwidth, bheight),
 373            label='',
 374            button_type='square',
 375            autoselect=True,
 376            on_activate_call=bui.WeakCall(
 377                self._open_press, user_tokens, chest.unlock_tokens
 378            ),
 379            enable_sound=False,
 380        )
 381        self._open_now_images = []
 382        self._open_now_texts = []
 383
 384        iconsize = 50
 385        if chest.unlock_tokens == 0:
 386            self._open_now_texts.append(
 387                bui.textwidget(
 388                    parent=self._root_widget,
 389                    text='Open',
 390                    position=(
 391                        self._width * 0.5 + boffsx,
 392                        self._height + bposy + self._yoffs + bheight * 0.5,
 393                    ),
 394                    color=(0, 1, 0),
 395                    draw_controller=self._open_now_button,
 396                    scale=0.7,
 397                    maxwidth=bwidth * 0.8,
 398                    size=(0, 0),
 399                    h_align='center',
 400                    v_align='center',
 401                )
 402            )
 403        else:
 404            self._open_now_texts.append(
 405                bui.textwidget(
 406                    parent=self._root_widget,
 407                    text='Open Now',
 408                    position=(
 409                        self._width * 0.5 + boffsx,
 410                        self._height + bposy + self._yoffs + bheight * 1.15,
 411                    ),
 412                    maxwidth=bwidth * 0.8,
 413                    scale=0.7,
 414                    color=(0.7, 1, 0.7),
 415                    size=(0, 0),
 416                    h_align='center',
 417                    v_align='center',
 418                )
 419            )
 420            self._open_now_images.append(
 421                bui.imagewidget(
 422                    parent=self._root_widget,
 423                    size=(iconsize, iconsize),
 424                    position=(
 425                        self._width * 0.5 - iconsize * 0.5 + boffsx,
 426                        self._height + bposy + self._yoffs + bheight * 0.35,
 427                    ),
 428                    draw_controller=self._open_now_button,
 429                    texture=bui.gettexture('coin'),
 430                )
 431            )
 432            self._open_now_texts.append(
 433                bui.textwidget(
 434                    parent=self._root_widget,
 435                    text=bui.Lstr(
 436                        resource='tokens.numTokensText',
 437                        subs=[('${COUNT}', str(chest.unlock_tokens))],
 438                    ),
 439                    position=(
 440                        self._width * 0.5 + boffsx,
 441                        self._height + bposy + self._yoffs + bheight * 0.25,
 442                    ),
 443                    scale=0.65,
 444                    color=(0, 1, 0),
 445                    draw_controller=self._open_now_button,
 446                    maxwidth=bwidth * 0.8,
 447                    size=(0, 0),
 448                    h_align='center',
 449                    v_align='center',
 450                )
 451            )
 452        self._open_now_spinner = bui.spinnerwidget(
 453            parent=self._root_widget,
 454            position=(
 455                self._width * 0.5 + boffsx,
 456                self._height + bposy + self._yoffs + 0.5 * bheight,
 457            ),
 458            visible=False,
 459        )
 460
 461        if show_ad_button:
 462            bui.textwidget(
 463                parent=self._root_widget,
 464                text='Reduce Wait',
 465                position=(
 466                    self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
 467                    self._height + bposy + self._yoffs + bheight * 1.15,
 468                ),
 469                maxwidth=bwidth * 0.8,
 470                scale=0.7,
 471                color=(0.7, 1, 0.7),
 472                size=(0, 0),
 473                h_align='center',
 474                v_align='center',
 475            )
 476            self._watch_ad_button = bui.buttonwidget(
 477                parent=self._root_widget,
 478                position=(
 479                    self._width * 0.5 + hspace * 0.5,
 480                    self._height + bposy + self._yoffs,
 481                ),
 482                size=(bwidth, bheight),
 483                label='',
 484                button_type='square',
 485                autoselect=True,
 486                on_activate_call=bui.WeakCall(self._watch_ad_press),
 487                enable_sound=False,
 488            )
 489            bui.imagewidget(
 490                parent=self._root_widget,
 491                size=(iconsize, iconsize),
 492                position=(
 493                    self._width * 0.5
 494                    + hspace * 0.5
 495                    + bwidth * 0.5
 496                    - iconsize * 0.5,
 497                    self._height + bposy + self._yoffs + bheight * 0.35,
 498                ),
 499                draw_controller=self._watch_ad_button,
 500                color=(1.5, 1.0, 2.0),
 501                texture=bui.gettexture('tv'),
 502            )
 503            # Note to self: AdMob requires rewarded ad usage
 504            # specifically says 'Ad' in it.
 505            bui.textwidget(
 506                parent=self._root_widget,
 507                text=bui.Lstr(resource='watchAnAdText'),
 508                position=(
 509                    self._width * 0.5 + hspace * 0.5 + bwidth * 0.5,
 510                    self._height + bposy + self._yoffs + bheight * 0.25,
 511                ),
 512                scale=0.65,
 513                color=(0, 1, 0),
 514                draw_controller=self._watch_ad_button,
 515                maxwidth=bwidth * 0.8,
 516                size=(0, 0),
 517                h_align='center',
 518                v_align='center',
 519            )
 520
 521        self._show_odds(initial_highlighted_row=-1)
 522
 523    def _highlight_odds_row(self, row: int, extra: bool = False) -> None:
 524
 525        for rindex, imgs in self._prizesetimgs.items():
 526            opacity = (
 527                (0.9 if extra else 0.75)
 528                if rindex == row
 529                else (0.4 if extra else 0.5)
 530            )
 531            for img in imgs:
 532                if img:
 533                    bui.imagewidget(edit=img, opacity=opacity)
 534
 535        for rindex, txts in self._prizesettxts.items():
 536            opacity = (
 537                (0.9 if extra else 0.75)
 538                if rindex == row
 539                else (0.4 if extra else 0.5)
 540            )
 541            for txt in txts:
 542                if txt:
 543                    bui.textwidget(edit=txt, color=(0.7, 0.65, 1, opacity))
 544
 545    def _show_odds(
 546        self,
 547        *,
 548        initial_highlighted_row: int,
 549        initial_highlighted_extra: bool = False,
 550    ) -> None:
 551        # pylint: disable=too-many-locals
 552        xoffs = 110
 553
 554        totalweight = max(0.001, sum(t.weight for t in self._prizesets))
 555
 556        rowheight = 25
 557        totalheight = (len(self._prizesets) + 1) * rowheight
 558        x = self._width * 0.5 + xoffs
 559        y = self._height + self._yoffs - 150.0 + totalheight * 0.5
 560
 561        # Title.
 562        bui.textwidget(
 563            parent=self._root_widget,
 564            text='Prize Odds',
 565            color=(0.7, 0.65, 1, 0.5),
 566            flatness=1.0,
 567            shadow=1.0,
 568            position=(x, y),
 569            scale=0.55,
 570            size=(0, 0),
 571            h_align='left',
 572            v_align='center',
 573        )
 574        y -= 5.0
 575
 576        prizesettxts: list[bui.Widget]
 577        prizesetimgs: list[bui.Widget]
 578
 579        def _mkicon(img: str) -> None:
 580            iconsize = 20.0
 581            nonlocal x
 582            nonlocal prizesetimgs
 583            prizesetimgs.append(
 584                bui.imagewidget(
 585                    parent=self._root_widget,
 586                    size=(iconsize, iconsize),
 587                    position=(x, y - iconsize * 0.5),
 588                    texture=bui.gettexture(img),
 589                    opacity=0.4,
 590                )
 591            )
 592            x += iconsize
 593
 594        def _mktxt(txt: str, advance: bool = True) -> None:
 595            tscale = 0.45
 596            nonlocal x
 597            nonlocal prizesettxts
 598            prizesettxts.append(
 599                bui.textwidget(
 600                    parent=self._root_widget,
 601                    text=txt,
 602                    flatness=1.0,
 603                    shadow=1.0,
 604                    position=(x, y),
 605                    scale=tscale,
 606                    size=(0, 0),
 607                    h_align='left',
 608                    v_align='center',
 609                )
 610            )
 611            if advance:
 612                x += (bui.get_string_width(txt, suppress_warning=True)) * tscale
 613
 614        self._prizesettxts = {}
 615        self._prizesetimgs = {}
 616
 617        for i, p in enumerate(self._prizesets):
 618            prizesettxts = self._prizesettxts.setdefault(i, [])
 619            prizesetimgs = self._prizesetimgs.setdefault(i, [])
 620            x = self._width * 0.5 + xoffs
 621            y -= rowheight
 622            percent = 100.0 * p.weight / totalweight
 623
 624            # Show decimals only if we get very small percentages (looks
 625            # better than rounding as '0%').
 626            percenttxt = (
 627                f'{percent:.2f}'
 628                if percent < 0.1
 629                else (
 630                    f'{percent:.1f}' if percent < 1.0 else f'{round(percent)}%:'
 631                )
 632            )
 633
 634            # We advance manually here to keep values lined up
 635            # (otherwise single digit percent rows don't line up with
 636            # double digit ones).
 637            _mktxt(percenttxt, advance=False)
 638            x += 35.0
 639
 640            for item in p.contents:
 641                x += 5.0
 642                if isinstance(item.item, bacommon.bs.TicketsDisplayItem):
 643                    _mktxt(str(item.item.count))
 644                    _mkicon('tickets')
 645                elif isinstance(item.item, bacommon.bs.TokensDisplayItem):
 646                    _mktxt(str(item.item.count))
 647                    _mkicon('coin')
 648                else:
 649                    # For other cases just fall back on text desc.
 650                    #
 651                    # Translate the wrapper description and apply any subs.
 652                    descfin = bui.Lstr(
 653                        translate=('serverResponses', item.description)
 654                    ).evaluate()
 655                    subs = (
 656                        []
 657                        if item.description_subs is None
 658                        else item.description_subs
 659                    )
 660                    assert len(subs) % 2 == 0  # Should always be even.
 661                    for j in range(0, len(subs) - 1, 2):
 662                        descfin = descfin.replace(subs[j], subs[j + 1])
 663                    _mktxt(descfin)
 664        self._highlight_odds_row(
 665            initial_highlighted_row, extra=initial_highlighted_extra
 666        )
 667
 668    def _open_press(self, user_tokens: int, token_payment: int) -> None:
 669        from bauiv1lib.gettokens import show_get_tokens_prompt
 670
 671        bui.getsound('click01').play()
 672
 673        # Allow only one in-flight action at once.
 674        if self._action_in_flight:
 675            bui.screenmessage(
 676                bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
 677            )
 678            bui.getsound('error').play()
 679            return
 680
 681        plus = bui.app.plus
 682        assert plus is not None
 683
 684        if plus.accounts.primary is None:
 685            self._error(bui.Lstr(resource='notSignedInText'))
 686            return
 687
 688        # Offer to purchase tokens if they don't have enough.
 689        if user_tokens < token_payment:
 690            # Hack: We disable normal swish for the open button and it
 691            # seems weird without a swish here, so explicitly do one.
 692            bui.getsound('swish').play()
 693            show_get_tokens_prompt()
 694            return
 695
 696        self._action_in_flight = True
 697        with plus.accounts.primary:
 698            plus.cloud.send_message_cb(
 699                bacommon.bs.ChestActionMessage(
 700                    chest_id=str(self._index),
 701                    action=bacommon.bs.ChestActionMessage.Action.UNLOCK,
 702                    token_payment=token_payment,
 703                ),
 704                on_response=bui.WeakCall(self._on_chest_action_response),
 705            )
 706
 707        # Convey that something is in progress.
 708        if self._open_now_button:
 709            bui.spinnerwidget(edit=self._open_now_spinner, visible=True)
 710            for twidget in self._open_now_texts:
 711                bui.textwidget(edit=twidget, color=(1, 1, 1, 0.2))
 712            for iwidget in self._open_now_images:
 713                bui.imagewidget(edit=iwidget, opacity=0.2)
 714
 715    def _watch_ad_press(self) -> None:
 716
 717        bui.getsound('click01').play()
 718
 719        # Allow only one in-flight action at once.
 720        if self._action_in_flight:
 721            bui.screenmessage(
 722                bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
 723            )
 724            bui.getsound('error').play()
 725            return
 726
 727        assert bui.app.classic is not None
 728
 729        self._action_in_flight = True
 730        bui.app.classic.ads.show_ad_2(
 731            'reduce_chest_wait',
 732            on_completion_call=bui.WeakCall(self._watch_ad_complete),
 733        )
 734
 735        # Convey that something is in progress.
 736        if self._watch_ad_button:
 737            bui.buttonwidget(edit=self._watch_ad_button, color=(0.4, 0.4, 0.4))
 738
 739    def _watch_ad_complete(self, actually_showed: bool) -> None:
 740
 741        assert self._action_in_flight  # Should be ad view.
 742        self._action_in_flight = False
 743
 744        if not actually_showed:
 745            return
 746
 747        # Allow only one in-flight action at once.
 748        if self._action_in_flight:
 749            bui.screenmessage(
 750                bui.Lstr(resource='pleaseWaitText'), color=(1, 0, 0)
 751            )
 752            bui.getsound('error').play()
 753            return
 754
 755        plus = bui.app.plus
 756        assert plus is not None
 757
 758        if plus.accounts.primary is None:
 759            self._error(bui.Lstr(resource='notSignedInText'))
 760            return
 761
 762        self._action_in_flight = True
 763        with plus.accounts.primary:
 764            plus.cloud.send_message_cb(
 765                bacommon.bs.ChestActionMessage(
 766                    chest_id=str(self._index),
 767                    action=bacommon.bs.ChestActionMessage.Action.AD,
 768                    token_payment=0,
 769                ),
 770                on_response=bui.WeakCall(self._on_chest_action_response),
 771            )
 772
 773    def _reset(self) -> None:
 774        """Clear all non-permanent widgets and clear infotext."""
 775        for widget in self._root_widget.get_children():
 776            if widget not in self._core_widgets:
 777                widget.delete()
 778        bui.textwidget(edit=self._infotext, text='', color=(1, 1, 1))
 779
 780    def _error(self, msg: str | bui.Lstr, minor: bool = False) -> None:
 781        """Put ourself in an error state with a visible error message."""
 782        self._reset()
 783        bui.textwidget(
 784            edit=self._infotext,
 785            text=msg,
 786            color=(1, 0.5, 0.5) if minor else (1, 0, 0),
 787        )
 788
 789    def _show_about_chest_slots(self) -> None:
 790        # No-op if our ui is dead.
 791        if not self._root_widget:
 792            return
 793
 794        self._reset()
 795        msg = (
 796            'This slot can hold a treasure chest.\n\n'
 797            'Earn chests by playing campaign levels,\n'
 798            'placing in tournaments, and completing\n'
 799            'achievements.'
 800        )
 801        bui.textwidget(edit=self._infotext, text=msg, color=(1, 1, 1))
 802
 803    def _show_chest_contents(
 804        self, response: bacommon.bs.ChestActionResponse
 805    ) -> None:
 806        # pylint: disable=too-many-locals
 807
 808        from baclassic import show_display_item
 809
 810        # No-op if our ui is dead.
 811        if not self._root_widget:
 812            return
 813
 814        assert response.contents is not None
 815
 816        # Insert test items for testing.
 817        if bool(False):
 818            response.contents += [
 819                bacommon.bs.DisplayItemWrapper.for_display_item(
 820                    bacommon.bs.TestDisplayItem()
 821                )
 822            ]
 823
 824        tincr = 0.4
 825        tendoffs = tincr * 4.0
 826        toffs = 0.0
 827
 828        bui.getsound('revUp').play(volume=2.0)
 829
 830        # Show nothing but the chest icon and animate it shaking.
 831        self._reset()
 832        imgsize = 145
 833        assert self._chestdisplayinfo is not None
 834        img = bui.imagewidget(
 835            parent=self._root_widget,
 836            color=self._chestdisplayinfo.color,
 837            texture=bui.gettexture(self._chestdisplayinfo.texclosed),
 838            tint_texture=bui.gettexture(self._chestdisplayinfo.texclosedtint),
 839            tint_color=self._chestdisplayinfo.tint,
 840            tint2_color=self._chestdisplayinfo.tint2,
 841        )
 842
 843        def _set_img(x: float, scale: float) -> None:
 844            if not img:
 845                return
 846            bui.imagewidget(
 847                edit=img,
 848                position=(
 849                    self._width * 0.5 - imgsize * scale * 0.5 + x,
 850                    self._height
 851                    - 223
 852                    + self._yoffs
 853                    + imgsize * 0.5
 854                    - imgsize * scale * 0.5,
 855                ),
 856                size=(imgsize * scale, imgsize * scale),
 857            )
 858
 859        # Set initial place.
 860        _set_img(0.0, 1.0)
 861
 862        sign = 1.0
 863        while toffs < tendoffs:
 864            toffs += 0.03 * random.uniform(0.5, 1.5)
 865            sign = -sign
 866            bui.apptimer(
 867                toffs,
 868                bui.Call(
 869                    _set_img,
 870                    x=(
 871                        20.0
 872                        * random.uniform(0.3, 1.0)
 873                        * math.pow(toffs / tendoffs, 2.0)
 874                        * sign
 875                    ),
 876                    scale=1.0 - 0.2 * math.pow(toffs / tendoffs, 2.0),
 877                ),
 878            )
 879
 880        xspacing = 100
 881        xoffs = -0.5 * (len(response.contents) - 1) * xspacing
 882        bui.apptimer(
 883            toffs - 0.2, lambda: bui.getsound('corkPop2').play(volume=4.0)
 884        )
 885        # Play a variety of voice sounds.
 886
 887        # We keep a global list of voice options which we randomly pull
 888        # from and refill when empty. This ensures everything gets
 889        # played somewhat frequently and minimizes annoying repeats.
 890        global _g_open_voices  # pylint: disable=global-statement
 891        if not _g_open_voices:
 892            _g_open_voices = [
 893                (0.3, 'woo3', 2.5),
 894                (0.1, 'gasp', 1.3),
 895                (0.2, 'woo2', 2.0),
 896                (0.2, 'wow', 2.0),
 897                (0.2, 'kronk2', 2.0),
 898                (0.2, 'mel03', 2.0),
 899                (0.2, 'aww', 2.0),
 900                (0.4, 'nice', 2.0),
 901                (0.3, 'yeah', 1.5),
 902                (0.2, 'woo', 1.0),
 903                (0.5, 'ooh', 0.8),
 904            ]
 905
 906        voicetimeoffs, voicename, volume = _g_open_voices.pop(
 907            random.randrange(len(_g_open_voices))
 908        )
 909        bui.apptimer(
 910            toffs + voicetimeoffs,
 911            lambda: bui.getsound(voicename).play(volume=volume),
 912        )
 913
 914        toffsopen = toffs
 915        bui.apptimer(toffs, bui.WeakCall(self._show_chest_opening))
 916        toffs += tincr * 1.0
 917        width = xspacing * 0.95
 918
 919        for item in response.contents:
 920            toffs += tincr
 921            bui.apptimer(
 922                toffs - 0.1, lambda: bui.getsound('cashRegister').play()
 923            )
 924            bui.apptimer(
 925                toffs,
 926                strict_partial(
 927                    show_display_item,
 928                    item,
 929                    self._root_widget,
 930                    pos=(
 931                        self._width * 0.5 + xoffs,
 932                        self._height - 250.0 + self._yoffs,
 933                    ),
 934                    width=width,
 935                ),
 936            )
 937            xoffs += xspacing
 938        toffs += tincr
 939        bui.apptimer(toffs, bui.WeakCall(self._show_done_button))
 940
 941        self._show_odds(initial_highlighted_row=-1)
 942
 943        # Store this for later
 944        self._prizeindex = response.prizeindex
 945
 946        # The final result was already randomly selected on the server,
 947        # but we want to give the illusion of randomness here, so cycle
 948        # through highlighting our options and stop on the winner when
 949        # the chest opens. To do this, we start at the end at the prize
 950        # and work backwards setting timers.
 951        if self._prizesets:
 952            toffs2 = toffsopen - 0.01
 953            amt = 0.02
 954            i = self._prizeindex
 955            while toffs2 > 0.0:
 956                bui.apptimer(
 957                    toffs2,
 958                    bui.WeakCall(self._highlight_odds_row, i),
 959                )
 960                toffs2 -= amt
 961                amt *= 1.05 * random.uniform(0.9, 1.1)
 962                i = (i - 1) % len(self._prizesets)
 963
 964    def _show_chest_opening(self) -> None:
 965
 966        # No-op if our ui is dead.
 967        if not self._root_widget:
 968            return
 969
 970        self._reset()
 971        imgsize = 145
 972        bui.getsound('hiss').play()
 973        assert self._chestdisplayinfo is not None
 974        img = bui.imagewidget(
 975            parent=self._root_widget,
 976            color=self._chestdisplayinfo.color,
 977            texture=bui.gettexture(self._chestdisplayinfo.texopen),
 978            tint_texture=bui.gettexture(self._chestdisplayinfo.texopentint),
 979            tint_color=self._chestdisplayinfo.tint,
 980            tint2_color=self._chestdisplayinfo.tint2,
 981        )
 982        tincr = 0.8
 983        tendoffs = tincr * 2.0
 984        toffs = 0.0
 985
 986        def _set_img(x: float, scale: float) -> None:
 987            if not img:
 988                return
 989            bui.imagewidget(
 990                edit=img,
 991                position=(
 992                    self._width * 0.5 - imgsize * scale * 0.5 + x,
 993                    self._height
 994                    - 223
 995                    + self._yoffs
 996                    + imgsize * 0.5
 997                    - imgsize * scale * 0.5,
 998                ),
 999                size=(imgsize * scale, imgsize * scale),
1000            )
1001
1002        # Set initial place.
1003        _set_img(0.0, 1.0)
1004
1005        sign = 1.0
1006        while toffs < tendoffs:
1007            toffs += 0.03 * random.uniform(0.5, 1.5)
1008            sign = -sign
1009            # Note: we speed x along here (multing toffs) so position
1010            # comes to rest before scale.
1011            bui.apptimer(
1012                toffs,
1013                bui.Call(
1014                    _set_img,
1015                    x=(
1016                        1.0
1017                        * random.uniform(0.3, 1.0)
1018                        * (
1019                            1.0
1020                            - math.pow(min(1.0, 3.0 * toffs / tendoffs), 2.0)
1021                        )
1022                        * sign
1023                    ),
1024                    scale=1.0 - 0.1 * math.pow(toffs / tendoffs, 0.5),
1025                ),
1026            )
1027
1028        self._show_odds(
1029            initial_highlighted_row=self._prizeindex,
1030            initial_highlighted_extra=True,
1031        )
1032
1033    def _show_done_button(self) -> None:
1034        # No-op if our ui is dead.
1035        if not self._root_widget:
1036            return
1037
1038        bwidth = 200
1039        bheight = 60
1040
1041        btn = bui.buttonwidget(
1042            parent=self._root_widget,
1043            position=(
1044                self._width * 0.5 - bwidth * 0.5,
1045                self._height - 350 + self._yoffs,
1046            ),
1047            size=(bwidth, bheight),
1048            label=bui.Lstr(resource='doneText'),
1049            autoselect=True,
1050            on_activate_call=self.main_window_back,
1051        )
1052        bui.containerwidget(edit=self._root_widget, start_button=btn)

Allows viewing and performing operations on a chest.

ChestWindow( index: int, transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 28    def __init__(
 29        self,
 30        index: int,
 31        transition: str | None = 'in_right',
 32        origin_widget: bui.Widget | None = None,
 33    ):
 34        self._index = index
 35
 36        assert bui.app.classic is not None
 37        uiscale = bui.app.ui_v1.uiscale
 38        self._width = 1050 if uiscale is bui.UIScale.SMALL else 650
 39        self._height = 550 if uiscale is bui.UIScale.SMALL else 450
 40        self._xoffs = 70 if uiscale is bui.UIScale.SMALL else 0
 41        self._yoffs = -50 if uiscale is bui.UIScale.SMALL else -35
 42        self._action_in_flight = False
 43        self._open_now_button: bui.Widget | None = None
 44        self._open_now_spinner: bui.Widget | None = None
 45        self._open_now_texts: list[bui.Widget] = []
 46        self._open_now_images: list[bui.Widget] = []
 47        self._watch_ad_button: bui.Widget | None = None
 48        self._time_string_timer: bui.AppTimer | None = None
 49        self._time_string_text: bui.Widget | None = None
 50        self._prizesets: list[bacommon.bs.ChestInfoResponse.Chest.PrizeSet] = []
 51        self._prizeindex = -1
 52        self._prizesettxts: dict[int, list[bui.Widget]] = {}
 53        self._prizesetimgs: dict[int, list[bui.Widget]] = {}
 54        self._chestdisplayinfo: baclassic.ChestAppearanceDisplayInfo | None = (
 55            None
 56        )
 57
 58        # The set of widgets we keep when doing a clear.
 59        self._core_widgets: list[bui.Widget] = []
 60
 61        super().__init__(
 62            root_widget=bui.containerwidget(
 63                size=(self._width, self._height),
 64                toolbar_visibility='menu_full',
 65                scale=(
 66                    1.45
 67                    if uiscale is bui.UIScale.SMALL
 68                    else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.9
 69                ),
 70                stack_offset=(
 71                    (0, 0)
 72                    if uiscale is bui.UIScale.SMALL
 73                    else (0, 15) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 74                ),
 75            ),
 76            transition=transition,
 77            origin_widget=origin_widget,
 78        )
 79
 80        # Tell the root-ui to stop updating toolbar values immediately;
 81        # this allows it to run animations based on the results of our
 82        # chest opening.
 83        bui.root_ui_pause_updates()
 84        self._root_ui_updates_paused = True
 85
 86        self._title_text = bui.textwidget(
 87            parent=self._root_widget,
 88            position=(0, self._height - 50 + self._yoffs),
 89            size=(self._width, 25),
 90            text=f'Chest Slot {self._index + 1}',
 91            color=bui.app.ui_v1.title_color,
 92            maxwidth=150.0,
 93            h_align='center',
 94            v_align='center',
 95        )
 96        self._core_widgets.append(self._title_text)
 97
 98        if uiscale is bui.UIScale.SMALL:
 99            bui.containerwidget(
100                edit=self._root_widget, on_cancel_call=self.main_window_back
101            )
102        else:
103            btn = bui.buttonwidget(
104                parent=self._root_widget,
105                position=(self._xoffs + 50, self._height - 55 + self._yoffs),
106                size=(60, 55),
107                scale=0.8,
108                label=bui.charstr(bui.SpecialChar.BACK),
109                button_type='backSmall',
110                extra_touch_border_scale=2.0,
111                autoselect=True,
112                on_activate_call=self.main_window_back,
113            )
114            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
115            self._core_widgets.append(btn)
116
117        # Note: Don't need to explicitly clean this up. Just not adding
118        # it to core_widgets so it will go away on next reset.
119        self._loadingspinner = bui.spinnerwidget(
120            parent=self._root_widget,
121            position=(self._width * 0.5, self._height * 0.5),
122        )
123
124        self._infotext = bui.textwidget(
125            parent=self._root_widget,
126            position=(self._width * 0.5, self._height - 200 + self._yoffs),
127            size=(0, 0),
128            text='',
129            maxwidth=700,
130            scale=0.8,
131            color=(0.6, 0.5, 0.6),
132            h_align='center',
133            v_align='center',
134        )
135        self._core_widgets.append(self._infotext)
136
137        plus = bui.app.plus
138        if plus is None:
139            self._error('Plus feature-set is not present.')
140            return
141
142        if plus.accounts.primary is None:
143            self._error(bui.Lstr(resource='notSignedInText'))
144            return
145
146        # Start by showing info/options for our target chest. Note that
147        # we always ask the server for these values even though we may
148        # have them through our appmode subscription which updates the
149        # chest UI. This is because the wait_for_connectivity()
150        # mechanism will often bring our window up a split second before
151        # the chest subscription receives its first values which would
152        # lead us to incorrectly think there is no chest there. If we
153        # want to optimize this in the future we could perhaps use local
154        # values only if there is a chest present in them.
155        assert not self._action_in_flight
156        self._action_in_flight = True
157        with plus.accounts.primary:
158            plus.cloud.send_message_cb(
159                bacommon.bs.ChestInfoMessage(chest_id=str(self._index)),
160                on_response=bui.WeakCall(self._on_chest_info_response),
161            )

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
170    @override
171    def get_main_window_state(self) -> bui.MainWindowState:
172        # Support recreating our window for back/refresh purposes.
173        cls = type(self)
174
175        # Pull anything we need from self out here; if we do it in the
176        # lambda we keep self alive which is bad.
177        index = self._index
178
179        return bui.BasicMainWindowState(
180            create_call=lambda transition, origin_widget: cls(
181                index=index, transition=transition, origin_widget=origin_widget
182            )
183        )

Return a WindowState to recreate this window, if supported.

class ChestWindow0(ChestWindow):
1062class ChestWindow0(ChestWindow):
1063    """Child class of ChestWindow for slighty hackish reasons."""

Child class of ChestWindow for slighty hackish reasons.

class ChestWindow1(ChestWindow):
1066class ChestWindow1(ChestWindow):
1067    """Child class of ChestWindow for slighty hackish reasons."""

Child class of ChestWindow for slighty hackish reasons.

class ChestWindow2(ChestWindow):
1070class ChestWindow2(ChestWindow):
1071    """Child class of ChestWindow for slighty hackish reasons."""

Child class of ChestWindow for slighty hackish reasons.

class ChestWindow3(ChestWindow):
1074class ChestWindow3(ChestWindow):
1075    """Child class of ChestWindow for slighty hackish reasons."""

Child class of ChestWindow for slighty hackish reasons.