bauiv1lib.gettokens

UI functionality for purchasing/acquiring currency.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""UI functionality for purchasing/acquiring currency."""
  4
  5from __future__ import annotations
  6
  7import time
  8from enum import Enum
  9from functools import partial
 10from dataclasses import dataclass
 11from typing import TYPE_CHECKING, assert_never, override
 12
 13import bacommon.cloud
 14import bauiv1 as bui
 15
 16
 17if TYPE_CHECKING:
 18    from typing import Any, Callable
 19
 20
 21@dataclass
 22class _ButtonDef:
 23    itemid: str
 24    width: float
 25    color: tuple[float, float, float]
 26    imgdefs: list[_ImgDef]
 27    txtdefs: list[_TxtDef]
 28    prepad: float = 0.0
 29
 30
 31@dataclass
 32class _ImgDef:
 33    tex: str
 34    pos: tuple[float, float]
 35    size: tuple[float, float]
 36    color: tuple[float, float, float] = (1, 1, 1)
 37    opacity: float = 1.0
 38    draw_controller_mult: float | None = None
 39
 40
 41class TextContents(Enum):
 42    """Some type of text to show."""
 43
 44    PRICE = 'price'
 45
 46
 47@dataclass
 48class _TxtDef:
 49    text: str | TextContents | bui.Lstr
 50    pos: tuple[float, float]
 51    maxwidth: float | None
 52    scale: float = 1.0
 53    color: tuple[float, float, float] = (1, 1, 1)
 54    rotate: float | None = None
 55
 56
 57class GetTokensWindow(bui.MainWindow):
 58    """Window for purchasing/acquiring classic tickets."""
 59
 60    class State(Enum):
 61        """What are we doing?"""
 62
 63        LOADING = 'loading'
 64        NOT_SIGNED_IN = 'not_signed_in'
 65        HAVE_GOLD_PASS = 'have_gold_pass'
 66        SHOWING_STORE = 'showing_store'
 67
 68    def __init__(
 69        self,
 70        transition: str | None = 'in_right',
 71        origin_widget: bui.Widget | None = None,
 72    ):
 73        # pylint: disable=too-many-locals
 74        bwidthstd = 170
 75        bwidthwide = 300
 76        ycolor = (0, 0, 0.3)
 77        pcolor = (0, 0, 0.3)
 78        pos1 = 65
 79        pos2 = 34
 80        titlescale = 0.9
 81        pricescale = 0.65
 82        bcapcol1 = (0.25, 0.13, 0.02)
 83        self._buttondefs: list[_ButtonDef] = [
 84            _ButtonDef(
 85                itemid='tokens1',
 86                width=bwidthstd,
 87                color=ycolor,
 88                imgdefs=[
 89                    _ImgDef(
 90                        'tokens1',
 91                        pos=(-3, 85),
 92                        size=(172, 172),
 93                        opacity=1.0,
 94                        draw_controller_mult=0.5,
 95                    ),
 96                    _ImgDef(
 97                        'windowBottomCap',
 98                        pos=(1.5, 4),
 99                        size=(bwidthstd * 0.960, 100),
100                        color=bcapcol1,
101                        opacity=1.0,
102                    ),
103                ],
104                txtdefs=[
105                    _TxtDef(
106                        bui.Lstr(
107                            resource='tokens.numTokensText',
108                            subs=[('${COUNT}', '50')],
109                        ),
110                        pos=(bwidthstd * 0.5, pos1),
111                        color=(1.1, 1.05, 1.0),
112                        scale=titlescale,
113                        maxwidth=bwidthstd * 0.9,
114                    ),
115                    _TxtDef(
116                        TextContents.PRICE,
117                        pos=(bwidthstd * 0.5, pos2),
118                        color=(1.1, 1.05, 1.0),
119                        scale=pricescale,
120                        maxwidth=bwidthstd * 0.9,
121                    ),
122                ],
123            ),
124            _ButtonDef(
125                itemid='tokens2',
126                width=bwidthstd,
127                color=ycolor,
128                imgdefs=[
129                    _ImgDef(
130                        'tokens2',
131                        pos=(-3, 85),
132                        size=(172, 172),
133                        opacity=1.0,
134                        draw_controller_mult=0.5,
135                    ),
136                    _ImgDef(
137                        'windowBottomCap',
138                        pos=(1.5, 4),
139                        size=(bwidthstd * 0.960, 100),
140                        color=bcapcol1,
141                        opacity=1.0,
142                    ),
143                ],
144                txtdefs=[
145                    _TxtDef(
146                        bui.Lstr(
147                            resource='tokens.numTokensText',
148                            subs=[('${COUNT}', '500')],
149                        ),
150                        pos=(bwidthstd * 0.5, pos1),
151                        color=(1.1, 1.05, 1.0),
152                        scale=titlescale,
153                        maxwidth=bwidthstd * 0.9,
154                    ),
155                    _TxtDef(
156                        TextContents.PRICE,
157                        pos=(bwidthstd * 0.5, pos2),
158                        color=(1.1, 1.05, 1.0),
159                        scale=pricescale,
160                        maxwidth=bwidthstd * 0.9,
161                    ),
162                ],
163            ),
164            _ButtonDef(
165                itemid='tokens3',
166                width=bwidthstd,
167                color=ycolor,
168                imgdefs=[
169                    _ImgDef(
170                        'tokens3',
171                        pos=(-3, 85),
172                        size=(172, 172),
173                        opacity=1.0,
174                        draw_controller_mult=0.5,
175                    ),
176                    _ImgDef(
177                        'windowBottomCap',
178                        pos=(1.5, 4),
179                        size=(bwidthstd * 0.960, 100),
180                        color=bcapcol1,
181                        opacity=1.0,
182                    ),
183                ],
184                txtdefs=[
185                    _TxtDef(
186                        bui.Lstr(
187                            resource='tokens.numTokensText',
188                            subs=[('${COUNT}', '1200')],
189                        ),
190                        pos=(bwidthstd * 0.5, pos1),
191                        color=(1.1, 1.05, 1.0),
192                        scale=titlescale,
193                        maxwidth=bwidthstd * 0.9,
194                    ),
195                    _TxtDef(
196                        TextContents.PRICE,
197                        pos=(bwidthstd * 0.5, pos2),
198                        color=(1.1, 1.05, 1.0),
199                        scale=pricescale,
200                        maxwidth=bwidthstd * 0.9,
201                    ),
202                ],
203            ),
204            _ButtonDef(
205                itemid='tokens4',
206                width=bwidthstd,
207                color=ycolor,
208                imgdefs=[
209                    _ImgDef(
210                        'tokens4',
211                        pos=(-3, 85),
212                        size=(172, 172),
213                        opacity=1.0,
214                        draw_controller_mult=0.5,
215                    ),
216                    _ImgDef(
217                        'windowBottomCap',
218                        pos=(1.5, 4),
219                        size=(bwidthstd * 0.960, 100),
220                        color=bcapcol1,
221                        opacity=1.0,
222                    ),
223                ],
224                txtdefs=[
225                    _TxtDef(
226                        bui.Lstr(
227                            resource='tokens.numTokensText',
228                            subs=[('${COUNT}', '2600')],
229                        ),
230                        pos=(bwidthstd * 0.5, pos1),
231                        color=(1.1, 1.05, 1.0),
232                        scale=titlescale,
233                        maxwidth=bwidthstd * 0.9,
234                    ),
235                    _TxtDef(
236                        TextContents.PRICE,
237                        pos=(bwidthstd * 0.5, pos2),
238                        color=(1.1, 1.05, 1.0),
239                        scale=pricescale,
240                        maxwidth=bwidthstd * 0.9,
241                    ),
242                ],
243            ),
244            _ButtonDef(
245                itemid='gold_pass',
246                width=bwidthwide,
247                color=pcolor,
248                imgdefs=[
249                    _ImgDef(
250                        'goldPass',
251                        pos=(-7, 102),
252                        size=(312, 156),
253                        draw_controller_mult=0.3,
254                    ),
255                    _ImgDef(
256                        'windowBottomCap',
257                        pos=(8, 4),
258                        size=(bwidthwide * 0.923, 116),
259                        color=(0.25, 0.12, 0.15),
260                        opacity=1.0,
261                    ),
262                ],
263                txtdefs=[
264                    _TxtDef(
265                        bui.Lstr(resource='goldPass.goldPassText'),
266                        pos=(bwidthwide * 0.5, pos1 + 27),
267                        color=(1.1, 1.05, 1.0),
268                        scale=titlescale,
269                        maxwidth=bwidthwide * 0.8,
270                    ),
271                    _TxtDef(
272                        bui.Lstr(resource='goldPass.desc1InfTokensText'),
273                        pos=(bwidthwide * 0.5, pos1 + 6),
274                        color=(1.1, 1.05, 1.0),
275                        scale=0.4,
276                        maxwidth=bwidthwide * 0.8,
277                    ),
278                    _TxtDef(
279                        bui.Lstr(resource='goldPass.desc2NoAdsText'),
280                        pos=(bwidthwide * 0.5, pos1 + 6 - 13 * 1),
281                        color=(1.1, 1.05, 1.0),
282                        scale=0.4,
283                        maxwidth=bwidthwide * 0.8,
284                    ),
285                    _TxtDef(
286                        bui.Lstr(resource='goldPass.desc3ForeverText'),
287                        pos=(bwidthwide * 0.5, pos1 + 6 - 13 * 2),
288                        color=(1.1, 1.05, 1.0),
289                        scale=0.4,
290                        maxwidth=bwidthwide * 0.8,
291                    ),
292                    _TxtDef(
293                        TextContents.PRICE,
294                        pos=(bwidthwide * 0.5, pos2 - 9),
295                        color=(1.1, 1.05, 1.0),
296                        scale=pricescale,
297                        maxwidth=bwidthwide * 0.8,
298                    ),
299                ],
300                prepad=-8,
301            ),
302        ]
303
304        self._transitioning_out = False
305        self._textcolor = (0.92, 0.92, 2.0)
306
307        self._query_in_flight = False
308        self._last_query_time = -1.0
309        self._last_query_response: bacommon.cloud.StoreQueryResponse | None = (
310            None
311        )
312
313        uiscale = bui.app.ui_v1.uiscale
314        self._width = 1200.0 if uiscale is bui.UIScale.SMALL else 1070.0
315        self._height = 800 if uiscale is bui.UIScale.SMALL else 520.0
316
317        self._r = 'getTokensWindow'
318
319        # Do some fancy math to fill all available screen area up to the
320        # size of our backing container. This lets us fit to the exact
321        # screen shape at small ui scale.
322        screensize = bui.get_virtual_screen_size()
323        scale = (
324            1.5
325            if uiscale is bui.UIScale.SMALL
326            else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.95
327        )
328        # Calc screen size in our local container space and clamp to a
329        # bit smaller than our container size.
330        target_width = min(self._width - 60, screensize[0] / scale)
331        target_height = min(self._height - 70, screensize[1] / scale)
332
333        # To get top/left coords, go to the center of our window and
334        # offset by half the width/height of our target area.
335        self._yoffs = 0.5 * self._height + 0.5 * target_height + 20.0
336
337        self._scroll_width = target_width
338
339        super().__init__(
340            root_widget=bui.containerwidget(
341                size=(self._width, self._height),
342                color=(0.3, 0.23, 0.36),
343                scale=scale,
344                toolbar_visibility=(
345                    'get_tokens'
346                    if uiscale is bui.UIScale.SMALL
347                    else 'menu_full'
348                ),
349            ),
350            transition=transition,
351            origin_widget=origin_widget,
352            # We're affected by screen size only at small ui-scale.
353            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
354        )
355
356        if uiscale is bui.UIScale.SMALL:
357            bui.containerwidget(
358                edit=self._root_widget, on_cancel_call=self.main_window_back
359            )
360            self._back_button = bui.get_special_widget('back_button')
361        else:
362            self._back_button = bui.buttonwidget(
363                parent=self._root_widget,
364                position=(60, self._yoffs - 90),
365                size=((60, 60)),
366                scale=1.0,
367                autoselect=True,
368                label=(bui.charstr(bui.SpecialChar.BACK)),
369                button_type=('backSmall'),
370                on_activate_call=self.main_window_back,
371            )
372            bui.containerwidget(
373                edit=self._root_widget, cancel_button=self._back_button
374            )
375
376        self._title_text = bui.textwidget(
377            parent=self._root_widget,
378            position=(self._width * 0.5, self._yoffs - 42),
379            size=(0, 0),
380            color=self._textcolor,
381            flatness=0.0,
382            shadow=1.0,
383            scale=1.2,
384            h_align='center',
385            v_align='center',
386            text=bui.Lstr(resource='tokens.getTokensText'),
387            maxwidth=260,
388        )
389
390        self._status_text = bui.textwidget(
391            parent=self._root_widget,
392            size=(0, 0),
393            position=(self._width * 0.5, self._height * 0.5),
394            h_align='center',
395            v_align='center',
396            color=(0.6, 0.6, 0.6),
397            scale=0.75,
398            text='',
399        )
400        # Create a spinner - it will get cleared when state changes from
401        # LOADING.
402        bui.spinnerwidget(
403            parent=self._root_widget,
404            size=60,
405            position=(self._width * 0.5, self._height * 0.5),
406            style='bomb',
407        )
408
409        self._core_widgets = [
410            self._back_button,
411            self._title_text,
412            self._status_text,
413        ]
414
415        # Get all textures used by our buttons preloading so hopefully
416        # they'll be in place by the time we show them.
417        for bdef in self._buttondefs:
418            for bimg in bdef.imgdefs:
419                bui.gettexture(bimg.tex)
420
421        self._state = self.State.LOADING
422
423        self._update_timer = bui.AppTimer(
424            0.789, bui.WeakCall(self._update), repeat=True
425        )
426        self._update()
427
428    @override
429    def get_main_window_state(self) -> bui.MainWindowState:
430        # Support recreating our window for back/refresh purposes.
431        cls = type(self)
432        return bui.BasicMainWindowState(
433            create_call=lambda transition, origin_widget: cls(
434                transition=transition, origin_widget=origin_widget
435            )
436        )
437
438    def _update(self) -> None:
439        # No-op if our underlying widget is dead or on its way out.
440        if not self._root_widget or self._root_widget.transitioning_out:
441            return
442
443        plus = bui.app.plus
444
445        if plus is None or plus.accounts.primary is None:
446            self._update_state(self.State.NOT_SIGNED_IN)
447            return
448
449        # Poll for relevant changes to the store or our account.
450        now = time.monotonic()
451        if not self._query_in_flight and now - self._last_query_time > 2.0:
452            self._last_query_time = now
453            self._query_in_flight = True
454            with plus.accounts.primary:
455                plus.cloud.send_message_cb(
456                    bacommon.cloud.StoreQueryMessage(),
457                    on_response=bui.WeakCall(self._on_store_query_response),
458                )
459
460        # Can't do much until we get a store state.
461        if self._last_query_response is None:
462            return
463
464        # If we've got a gold-pass, just show that. No need to offer any
465        # other purchases.
466        if self._last_query_response.gold_pass:
467            self._update_state(self.State.HAVE_GOLD_PASS)
468            return
469
470        # Ok we seem to be signed in and have store stuff we can show.
471        # Do that.
472        self._update_state(self.State.SHOWING_STORE)
473
474    def _update_state(self, state: State) -> None:
475
476        # We don't do much when state is unchanged.
477        if state is self._state:
478            # Update a few things in store mode though, such as token
479            # count.
480            if state is self.State.SHOWING_STORE:
481                self._update_store_state()
482            return
483
484        # Ok, state is changing. Start by resetting to a blank slate.
485        # self._token_count_widget = None
486        for widget in self._root_widget.get_children():
487            if widget not in self._core_widgets:
488                widget.delete()
489
490        # Build up new state.
491        if state is self.State.NOT_SIGNED_IN:
492            bui.textwidget(
493                edit=self._status_text,
494                color=(1, 0, 0),
495                text=bui.Lstr(resource='notSignedInErrorText'),
496            )
497        elif state is self.State.LOADING:
498            raise RuntimeError('Should never return to loading state.')
499        elif state is self.State.HAVE_GOLD_PASS:
500            bui.textwidget(
501                edit=self._status_text,
502                color=(0, 1, 0),
503                text=bui.Lstr(resource='tokens.youHaveGoldPassText'),
504            )
505        elif state is self.State.SHOWING_STORE:
506            assert self._last_query_response is not None
507            bui.textwidget(edit=self._status_text, text='')
508            self._build_store_for_response(self._last_query_response)
509        else:
510            # Make sure we handle all cases.
511            assert_never(state)
512
513        self._state = state
514
515    def _on_load_error(self) -> None:
516        bui.textwidget(
517            edit=self._status_text,
518            text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
519            color=(1, 0, 0),
520        )
521
522    def _on_store_query_response(
523        self, response: bacommon.cloud.StoreQueryResponse | Exception
524    ) -> None:
525        self._query_in_flight = False
526        if isinstance(response, bacommon.cloud.StoreQueryResponse):
527            self._last_query_response = response
528            # Hurry along any effects of this response.
529            self._update()
530
531    def _build_store_for_response(
532        self, response: bacommon.cloud.StoreQueryResponse
533    ) -> None:
534        # pylint: disable=too-many-locals
535        plus = bui.app.plus
536        classic = bui.app.classic
537
538        uiscale = bui.app.ui_v1.uiscale
539
540        bui.textwidget(edit=self._status_text, text='')
541
542        scrollheight = 280
543        buttonpadding = -5
544
545        yoffs = 5
546
547        available_purchases = {
548            p.purchaseid for p in response.available_purchases
549        }
550        buttondefs_shown = [
551            b for b in self._buttondefs if b.itemid in available_purchases
552        ]
553
554        # Fail if something errored server-side or they didn't send us
555        # anything we can show.
556        if (
557            response.result is not response.Result.SUCCESS
558            or not buttondefs_shown
559        ):
560            self._on_load_error()
561            return
562
563        sidepad = 10.0
564        xfudge = 6.0
565        total_button_width = (
566            sum(b.width + b.prepad for b in buttondefs_shown)
567            + buttonpadding * (len(buttondefs_shown) - 1)
568            + 2 * sidepad
569        )
570
571        h_scroll = bui.hscrollwidget(
572            parent=self._root_widget,
573            size=(self._scroll_width, scrollheight),
574            position=(
575                self._width * 0.5 - 0.5 * self._scroll_width,
576                self._height * 0.5 - 0.5 * scrollheight - 40,
577            ),
578            claims_left_right=True,
579            highlight=False,
580            border_opacity=0.0,
581            center_small_content=True,
582        )
583        subcontainer = bui.containerwidget(
584            parent=h_scroll,
585            background=False,
586            size=(total_button_width, scrollheight),
587        )
588        tinfobtn = bui.buttonwidget(
589            parent=self._root_widget,
590            autoselect=True,
591            label=bui.Lstr(resource='learnMoreText'),
592            text_scale=0.7,
593            position=(
594                self._width * 0.5 - 75,
595                self._yoffs - 100,
596            ),
597            size=(180, 40),
598            scale=0.8,
599            color=(0.4, 0.25, 0.5),
600            textcolor=self._textcolor,
601            on_activate_call=partial(
602                self._on_learn_more_press, response.token_info_url
603            ),
604        )
605        if uiscale is bui.UIScale.SMALL:
606            bui.widget(
607                edit=tinfobtn,
608                left_widget=bui.get_special_widget('back_button'),
609                up_widget=bui.get_special_widget('back_button'),
610            )
611
612        bui.widget(
613            edit=tinfobtn,
614            right_widget=bui.get_special_widget('tokens_meter'),
615        )
616
617        x = sidepad + xfudge
618        bwidgets: list[bui.Widget] = []
619        for i, buttondef in enumerate(buttondefs_shown):
620
621            price = None if plus is None else plus.get_price(buttondef.itemid)
622
623            x += buttondef.prepad
624            tdelay = 0.3 - i / len(buttondefs_shown) * 0.25
625            btn = bui.buttonwidget(
626                autoselect=True,
627                label='',
628                color=buttondef.color,
629                transition_delay=tdelay,
630                up_widget=tinfobtn,
631                parent=subcontainer,
632                size=(buttondef.width, 275),
633                position=(x, -10 + yoffs),
634                button_type='square',
635                on_activate_call=partial(
636                    self._purchase_press, buttondef.itemid
637                ),
638            )
639            bwidgets.append(btn)
640
641            if i == 0:
642                bui.widget(edit=btn, left_widget=self._back_button)
643
644            for imgdef in buttondef.imgdefs:
645                _img = bui.imagewidget(
646                    parent=subcontainer,
647                    size=imgdef.size,
648                    position=(x + imgdef.pos[0], imgdef.pos[1] + yoffs),
649                    draw_controller=btn,
650                    draw_controller_mult=imgdef.draw_controller_mult,
651                    color=imgdef.color,
652                    texture=bui.gettexture(imgdef.tex),
653                    transition_delay=tdelay,
654                    opacity=imgdef.opacity,
655                )
656            for txtdef in buttondef.txtdefs:
657                txt: bui.Lstr | str
658                if isinstance(txtdef.text, TextContents):
659                    if txtdef.text is TextContents.PRICE:
660                        tcolor = (
661                            (1, 1, 1, 0.5) if price is None else txtdef.color
662                        )
663                        txt = (
664                            bui.Lstr(resource='unavailableText')
665                            if price is None
666                            else price
667                        )
668                    else:
669                        # Make sure we cover all cases.
670                        assert_never(txtdef.text)
671                else:
672                    tcolor = txtdef.color
673                    txt = txtdef.text
674                _txt = bui.textwidget(
675                    parent=subcontainer,
676                    text=txt,
677                    position=(x + txtdef.pos[0], txtdef.pos[1] + yoffs),
678                    size=(0, 0),
679                    scale=txtdef.scale,
680                    h_align='center',
681                    v_align='center',
682                    draw_controller=btn,
683                    color=tcolor,
684                    transition_delay=tdelay,
685                    flatness=0.0,
686                    shadow=1.0,
687                    rotate=txtdef.rotate,
688                    maxwidth=txtdef.maxwidth,
689                )
690            x += buttondef.width + buttonpadding
691        bui.containerwidget(edit=subcontainer, visible_child=bwidgets[0])
692
693        if bool(False):
694            _tinfotxt = bui.textwidget(
695                parent=self._root_widget,
696                position=(
697                    self._width * 0.5,
698                    self._yoffs - 70,
699                ),
700                color=self._textcolor,
701                shadow=1.0,
702                scale=0.7,
703                size=(0, 0),
704                h_align='center',
705                v_align='center',
706                text=bui.Lstr(resource='tokens.shinyNewCurrencyText'),
707            )
708
709        has_removed_ads = classic is not None and (
710            classic.gold_pass
711            or classic.remove_ads
712            or classic.accounts.have_pro()
713        )
714        if plus is not None and plus.has_video_ads() and not has_removed_ads:
715            _tinfotxt = bui.textwidget(
716                parent=self._root_widget,
717                position=(
718                    self._width * 0.5,
719                    self._yoffs - 120,
720                ),
721                color=(0.4, 1.0, 0.4),
722                shadow=1.0,
723                scale=0.5,
724                size=(0, 0),
725                h_align='center',
726                v_align='center',
727                maxwidth=self._scroll_width * 0.9,
728                text=bui.Lstr(resource='removeInGameAdsTokenPurchaseText'),
729            )
730
731    def _purchase_press(self, itemid: str) -> None:
732        plus = bui.app.plus
733
734        price = None if plus is None else plus.get_price(itemid)
735
736        if price is None:
737            if plus is not None and plus.supports_purchases():
738                # Looks like internet is down or something temporary.
739                errmsg = bui.Lstr(resource='purchaseNotAvailableText')
740            else:
741                # Looks like purchases will never work here.
742                errmsg = bui.Lstr(resource='purchaseNeverAvailableText')
743
744            bui.screenmessage(errmsg, color=(1, 0.5, 0))
745            bui.getsound('error').play()
746            return
747
748        assert plus is not None
749        plus.purchase(itemid)
750
751    def _update_store_state(self) -> None:
752        """Called to make minor updates to an already shown store."""
753        assert self._last_query_response is not None
754
755    def _on_learn_more_press(self, url: str) -> None:
756        bui.open_url(url)
757
758
759def show_get_tokens_prompt() -> None:
760    """Show a 'not enough tokens' prompt with an option to purchase more.
761
762    Note that the purchase option may not always be available
763    depending on the build of the game.
764    """
765    from bauiv1lib.confirm import ConfirmWindow
766
767    assert bui.app.classic is not None
768
769    # Currently always allowing token purchases.
770    if bool(True):
771        ConfirmWindow(
772            bui.Lstr(resource='tokens.notEnoughTokensText'),
773            show_get_tokens_window,
774            ok_text=bui.Lstr(resource='tokens.getTokensText'),
775            width=460,
776            height=130,
777        )
778    else:
779        ConfirmWindow(
780            bui.Lstr(resource='tokens.notEnoughTokensText'),
781            cancel_button=False,
782            width=460,
783            height=130,
784        )
785
786
787def show_get_tokens_window(origin_widget: bui.Widget | None = None) -> None:
788    """Transition to the get-tokens main-window from anywhere."""
789
790    # NOTE TO USERS: The code below is not the proper way to do things;
791    # whenever possible one should use a MainWindow's
792    # main_window_replace() or main_window_back() methods. We just need
793    # to do things a bit more manually in this particular case.
794
795    prev_main_window = bui.app.ui_v1.get_main_window()
796
797    # Special-case: If it seems we're already in the window, do nothing.
798    if isinstance(prev_main_window, GetTokensWindow):
799        return
800
801    # Set our new main window.
802    bui.app.ui_v1.set_main_window(
803        GetTokensWindow(origin_widget=origin_widget),
804        from_window=False,
805        is_auxiliary=True,
806        suppress_warning=True,
807    )
808
809    # Transition out any previous main window.
810    if prev_main_window is not None:
811        prev_main_window.main_window_close()
class TextContents(enum.Enum):
42class TextContents(Enum):
43    """Some type of text to show."""
44
45    PRICE = 'price'

Some type of text to show.

PRICE = <TextContents.PRICE: 'price'>
class GetTokensWindow(bauiv1._uitypes.MainWindow):
 58class GetTokensWindow(bui.MainWindow):
 59    """Window for purchasing/acquiring classic tickets."""
 60
 61    class State(Enum):
 62        """What are we doing?"""
 63
 64        LOADING = 'loading'
 65        NOT_SIGNED_IN = 'not_signed_in'
 66        HAVE_GOLD_PASS = 'have_gold_pass'
 67        SHOWING_STORE = 'showing_store'
 68
 69    def __init__(
 70        self,
 71        transition: str | None = 'in_right',
 72        origin_widget: bui.Widget | None = None,
 73    ):
 74        # pylint: disable=too-many-locals
 75        bwidthstd = 170
 76        bwidthwide = 300
 77        ycolor = (0, 0, 0.3)
 78        pcolor = (0, 0, 0.3)
 79        pos1 = 65
 80        pos2 = 34
 81        titlescale = 0.9
 82        pricescale = 0.65
 83        bcapcol1 = (0.25, 0.13, 0.02)
 84        self._buttondefs: list[_ButtonDef] = [
 85            _ButtonDef(
 86                itemid='tokens1',
 87                width=bwidthstd,
 88                color=ycolor,
 89                imgdefs=[
 90                    _ImgDef(
 91                        'tokens1',
 92                        pos=(-3, 85),
 93                        size=(172, 172),
 94                        opacity=1.0,
 95                        draw_controller_mult=0.5,
 96                    ),
 97                    _ImgDef(
 98                        'windowBottomCap',
 99                        pos=(1.5, 4),
100                        size=(bwidthstd * 0.960, 100),
101                        color=bcapcol1,
102                        opacity=1.0,
103                    ),
104                ],
105                txtdefs=[
106                    _TxtDef(
107                        bui.Lstr(
108                            resource='tokens.numTokensText',
109                            subs=[('${COUNT}', '50')],
110                        ),
111                        pos=(bwidthstd * 0.5, pos1),
112                        color=(1.1, 1.05, 1.0),
113                        scale=titlescale,
114                        maxwidth=bwidthstd * 0.9,
115                    ),
116                    _TxtDef(
117                        TextContents.PRICE,
118                        pos=(bwidthstd * 0.5, pos2),
119                        color=(1.1, 1.05, 1.0),
120                        scale=pricescale,
121                        maxwidth=bwidthstd * 0.9,
122                    ),
123                ],
124            ),
125            _ButtonDef(
126                itemid='tokens2',
127                width=bwidthstd,
128                color=ycolor,
129                imgdefs=[
130                    _ImgDef(
131                        'tokens2',
132                        pos=(-3, 85),
133                        size=(172, 172),
134                        opacity=1.0,
135                        draw_controller_mult=0.5,
136                    ),
137                    _ImgDef(
138                        'windowBottomCap',
139                        pos=(1.5, 4),
140                        size=(bwidthstd * 0.960, 100),
141                        color=bcapcol1,
142                        opacity=1.0,
143                    ),
144                ],
145                txtdefs=[
146                    _TxtDef(
147                        bui.Lstr(
148                            resource='tokens.numTokensText',
149                            subs=[('${COUNT}', '500')],
150                        ),
151                        pos=(bwidthstd * 0.5, pos1),
152                        color=(1.1, 1.05, 1.0),
153                        scale=titlescale,
154                        maxwidth=bwidthstd * 0.9,
155                    ),
156                    _TxtDef(
157                        TextContents.PRICE,
158                        pos=(bwidthstd * 0.5, pos2),
159                        color=(1.1, 1.05, 1.0),
160                        scale=pricescale,
161                        maxwidth=bwidthstd * 0.9,
162                    ),
163                ],
164            ),
165            _ButtonDef(
166                itemid='tokens3',
167                width=bwidthstd,
168                color=ycolor,
169                imgdefs=[
170                    _ImgDef(
171                        'tokens3',
172                        pos=(-3, 85),
173                        size=(172, 172),
174                        opacity=1.0,
175                        draw_controller_mult=0.5,
176                    ),
177                    _ImgDef(
178                        'windowBottomCap',
179                        pos=(1.5, 4),
180                        size=(bwidthstd * 0.960, 100),
181                        color=bcapcol1,
182                        opacity=1.0,
183                    ),
184                ],
185                txtdefs=[
186                    _TxtDef(
187                        bui.Lstr(
188                            resource='tokens.numTokensText',
189                            subs=[('${COUNT}', '1200')],
190                        ),
191                        pos=(bwidthstd * 0.5, pos1),
192                        color=(1.1, 1.05, 1.0),
193                        scale=titlescale,
194                        maxwidth=bwidthstd * 0.9,
195                    ),
196                    _TxtDef(
197                        TextContents.PRICE,
198                        pos=(bwidthstd * 0.5, pos2),
199                        color=(1.1, 1.05, 1.0),
200                        scale=pricescale,
201                        maxwidth=bwidthstd * 0.9,
202                    ),
203                ],
204            ),
205            _ButtonDef(
206                itemid='tokens4',
207                width=bwidthstd,
208                color=ycolor,
209                imgdefs=[
210                    _ImgDef(
211                        'tokens4',
212                        pos=(-3, 85),
213                        size=(172, 172),
214                        opacity=1.0,
215                        draw_controller_mult=0.5,
216                    ),
217                    _ImgDef(
218                        'windowBottomCap',
219                        pos=(1.5, 4),
220                        size=(bwidthstd * 0.960, 100),
221                        color=bcapcol1,
222                        opacity=1.0,
223                    ),
224                ],
225                txtdefs=[
226                    _TxtDef(
227                        bui.Lstr(
228                            resource='tokens.numTokensText',
229                            subs=[('${COUNT}', '2600')],
230                        ),
231                        pos=(bwidthstd * 0.5, pos1),
232                        color=(1.1, 1.05, 1.0),
233                        scale=titlescale,
234                        maxwidth=bwidthstd * 0.9,
235                    ),
236                    _TxtDef(
237                        TextContents.PRICE,
238                        pos=(bwidthstd * 0.5, pos2),
239                        color=(1.1, 1.05, 1.0),
240                        scale=pricescale,
241                        maxwidth=bwidthstd * 0.9,
242                    ),
243                ],
244            ),
245            _ButtonDef(
246                itemid='gold_pass',
247                width=bwidthwide,
248                color=pcolor,
249                imgdefs=[
250                    _ImgDef(
251                        'goldPass',
252                        pos=(-7, 102),
253                        size=(312, 156),
254                        draw_controller_mult=0.3,
255                    ),
256                    _ImgDef(
257                        'windowBottomCap',
258                        pos=(8, 4),
259                        size=(bwidthwide * 0.923, 116),
260                        color=(0.25, 0.12, 0.15),
261                        opacity=1.0,
262                    ),
263                ],
264                txtdefs=[
265                    _TxtDef(
266                        bui.Lstr(resource='goldPass.goldPassText'),
267                        pos=(bwidthwide * 0.5, pos1 + 27),
268                        color=(1.1, 1.05, 1.0),
269                        scale=titlescale,
270                        maxwidth=bwidthwide * 0.8,
271                    ),
272                    _TxtDef(
273                        bui.Lstr(resource='goldPass.desc1InfTokensText'),
274                        pos=(bwidthwide * 0.5, pos1 + 6),
275                        color=(1.1, 1.05, 1.0),
276                        scale=0.4,
277                        maxwidth=bwidthwide * 0.8,
278                    ),
279                    _TxtDef(
280                        bui.Lstr(resource='goldPass.desc2NoAdsText'),
281                        pos=(bwidthwide * 0.5, pos1 + 6 - 13 * 1),
282                        color=(1.1, 1.05, 1.0),
283                        scale=0.4,
284                        maxwidth=bwidthwide * 0.8,
285                    ),
286                    _TxtDef(
287                        bui.Lstr(resource='goldPass.desc3ForeverText'),
288                        pos=(bwidthwide * 0.5, pos1 + 6 - 13 * 2),
289                        color=(1.1, 1.05, 1.0),
290                        scale=0.4,
291                        maxwidth=bwidthwide * 0.8,
292                    ),
293                    _TxtDef(
294                        TextContents.PRICE,
295                        pos=(bwidthwide * 0.5, pos2 - 9),
296                        color=(1.1, 1.05, 1.0),
297                        scale=pricescale,
298                        maxwidth=bwidthwide * 0.8,
299                    ),
300                ],
301                prepad=-8,
302            ),
303        ]
304
305        self._transitioning_out = False
306        self._textcolor = (0.92, 0.92, 2.0)
307
308        self._query_in_flight = False
309        self._last_query_time = -1.0
310        self._last_query_response: bacommon.cloud.StoreQueryResponse | None = (
311            None
312        )
313
314        uiscale = bui.app.ui_v1.uiscale
315        self._width = 1200.0 if uiscale is bui.UIScale.SMALL else 1070.0
316        self._height = 800 if uiscale is bui.UIScale.SMALL else 520.0
317
318        self._r = 'getTokensWindow'
319
320        # Do some fancy math to fill all available screen area up to the
321        # size of our backing container. This lets us fit to the exact
322        # screen shape at small ui scale.
323        screensize = bui.get_virtual_screen_size()
324        scale = (
325            1.5
326            if uiscale is bui.UIScale.SMALL
327            else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.95
328        )
329        # Calc screen size in our local container space and clamp to a
330        # bit smaller than our container size.
331        target_width = min(self._width - 60, screensize[0] / scale)
332        target_height = min(self._height - 70, screensize[1] / scale)
333
334        # To get top/left coords, go to the center of our window and
335        # offset by half the width/height of our target area.
336        self._yoffs = 0.5 * self._height + 0.5 * target_height + 20.0
337
338        self._scroll_width = target_width
339
340        super().__init__(
341            root_widget=bui.containerwidget(
342                size=(self._width, self._height),
343                color=(0.3, 0.23, 0.36),
344                scale=scale,
345                toolbar_visibility=(
346                    'get_tokens'
347                    if uiscale is bui.UIScale.SMALL
348                    else 'menu_full'
349                ),
350            ),
351            transition=transition,
352            origin_widget=origin_widget,
353            # We're affected by screen size only at small ui-scale.
354            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
355        )
356
357        if uiscale is bui.UIScale.SMALL:
358            bui.containerwidget(
359                edit=self._root_widget, on_cancel_call=self.main_window_back
360            )
361            self._back_button = bui.get_special_widget('back_button')
362        else:
363            self._back_button = bui.buttonwidget(
364                parent=self._root_widget,
365                position=(60, self._yoffs - 90),
366                size=((60, 60)),
367                scale=1.0,
368                autoselect=True,
369                label=(bui.charstr(bui.SpecialChar.BACK)),
370                button_type=('backSmall'),
371                on_activate_call=self.main_window_back,
372            )
373            bui.containerwidget(
374                edit=self._root_widget, cancel_button=self._back_button
375            )
376
377        self._title_text = bui.textwidget(
378            parent=self._root_widget,
379            position=(self._width * 0.5, self._yoffs - 42),
380            size=(0, 0),
381            color=self._textcolor,
382            flatness=0.0,
383            shadow=1.0,
384            scale=1.2,
385            h_align='center',
386            v_align='center',
387            text=bui.Lstr(resource='tokens.getTokensText'),
388            maxwidth=260,
389        )
390
391        self._status_text = bui.textwidget(
392            parent=self._root_widget,
393            size=(0, 0),
394            position=(self._width * 0.5, self._height * 0.5),
395            h_align='center',
396            v_align='center',
397            color=(0.6, 0.6, 0.6),
398            scale=0.75,
399            text='',
400        )
401        # Create a spinner - it will get cleared when state changes from
402        # LOADING.
403        bui.spinnerwidget(
404            parent=self._root_widget,
405            size=60,
406            position=(self._width * 0.5, self._height * 0.5),
407            style='bomb',
408        )
409
410        self._core_widgets = [
411            self._back_button,
412            self._title_text,
413            self._status_text,
414        ]
415
416        # Get all textures used by our buttons preloading so hopefully
417        # they'll be in place by the time we show them.
418        for bdef in self._buttondefs:
419            for bimg in bdef.imgdefs:
420                bui.gettexture(bimg.tex)
421
422        self._state = self.State.LOADING
423
424        self._update_timer = bui.AppTimer(
425            0.789, bui.WeakCall(self._update), repeat=True
426        )
427        self._update()
428
429    @override
430    def get_main_window_state(self) -> bui.MainWindowState:
431        # Support recreating our window for back/refresh purposes.
432        cls = type(self)
433        return bui.BasicMainWindowState(
434            create_call=lambda transition, origin_widget: cls(
435                transition=transition, origin_widget=origin_widget
436            )
437        )
438
439    def _update(self) -> None:
440        # No-op if our underlying widget is dead or on its way out.
441        if not self._root_widget or self._root_widget.transitioning_out:
442            return
443
444        plus = bui.app.plus
445
446        if plus is None or plus.accounts.primary is None:
447            self._update_state(self.State.NOT_SIGNED_IN)
448            return
449
450        # Poll for relevant changes to the store or our account.
451        now = time.monotonic()
452        if not self._query_in_flight and now - self._last_query_time > 2.0:
453            self._last_query_time = now
454            self._query_in_flight = True
455            with plus.accounts.primary:
456                plus.cloud.send_message_cb(
457                    bacommon.cloud.StoreQueryMessage(),
458                    on_response=bui.WeakCall(self._on_store_query_response),
459                )
460
461        # Can't do much until we get a store state.
462        if self._last_query_response is None:
463            return
464
465        # If we've got a gold-pass, just show that. No need to offer any
466        # other purchases.
467        if self._last_query_response.gold_pass:
468            self._update_state(self.State.HAVE_GOLD_PASS)
469            return
470
471        # Ok we seem to be signed in and have store stuff we can show.
472        # Do that.
473        self._update_state(self.State.SHOWING_STORE)
474
475    def _update_state(self, state: State) -> None:
476
477        # We don't do much when state is unchanged.
478        if state is self._state:
479            # Update a few things in store mode though, such as token
480            # count.
481            if state is self.State.SHOWING_STORE:
482                self._update_store_state()
483            return
484
485        # Ok, state is changing. Start by resetting to a blank slate.
486        # self._token_count_widget = None
487        for widget in self._root_widget.get_children():
488            if widget not in self._core_widgets:
489                widget.delete()
490
491        # Build up new state.
492        if state is self.State.NOT_SIGNED_IN:
493            bui.textwidget(
494                edit=self._status_text,
495                color=(1, 0, 0),
496                text=bui.Lstr(resource='notSignedInErrorText'),
497            )
498        elif state is self.State.LOADING:
499            raise RuntimeError('Should never return to loading state.')
500        elif state is self.State.HAVE_GOLD_PASS:
501            bui.textwidget(
502                edit=self._status_text,
503                color=(0, 1, 0),
504                text=bui.Lstr(resource='tokens.youHaveGoldPassText'),
505            )
506        elif state is self.State.SHOWING_STORE:
507            assert self._last_query_response is not None
508            bui.textwidget(edit=self._status_text, text='')
509            self._build_store_for_response(self._last_query_response)
510        else:
511            # Make sure we handle all cases.
512            assert_never(state)
513
514        self._state = state
515
516    def _on_load_error(self) -> None:
517        bui.textwidget(
518            edit=self._status_text,
519            text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
520            color=(1, 0, 0),
521        )
522
523    def _on_store_query_response(
524        self, response: bacommon.cloud.StoreQueryResponse | Exception
525    ) -> None:
526        self._query_in_flight = False
527        if isinstance(response, bacommon.cloud.StoreQueryResponse):
528            self._last_query_response = response
529            # Hurry along any effects of this response.
530            self._update()
531
532    def _build_store_for_response(
533        self, response: bacommon.cloud.StoreQueryResponse
534    ) -> None:
535        # pylint: disable=too-many-locals
536        plus = bui.app.plus
537        classic = bui.app.classic
538
539        uiscale = bui.app.ui_v1.uiscale
540
541        bui.textwidget(edit=self._status_text, text='')
542
543        scrollheight = 280
544        buttonpadding = -5
545
546        yoffs = 5
547
548        available_purchases = {
549            p.purchaseid for p in response.available_purchases
550        }
551        buttondefs_shown = [
552            b for b in self._buttondefs if b.itemid in available_purchases
553        ]
554
555        # Fail if something errored server-side or they didn't send us
556        # anything we can show.
557        if (
558            response.result is not response.Result.SUCCESS
559            or not buttondefs_shown
560        ):
561            self._on_load_error()
562            return
563
564        sidepad = 10.0
565        xfudge = 6.0
566        total_button_width = (
567            sum(b.width + b.prepad for b in buttondefs_shown)
568            + buttonpadding * (len(buttondefs_shown) - 1)
569            + 2 * sidepad
570        )
571
572        h_scroll = bui.hscrollwidget(
573            parent=self._root_widget,
574            size=(self._scroll_width, scrollheight),
575            position=(
576                self._width * 0.5 - 0.5 * self._scroll_width,
577                self._height * 0.5 - 0.5 * scrollheight - 40,
578            ),
579            claims_left_right=True,
580            highlight=False,
581            border_opacity=0.0,
582            center_small_content=True,
583        )
584        subcontainer = bui.containerwidget(
585            parent=h_scroll,
586            background=False,
587            size=(total_button_width, scrollheight),
588        )
589        tinfobtn = bui.buttonwidget(
590            parent=self._root_widget,
591            autoselect=True,
592            label=bui.Lstr(resource='learnMoreText'),
593            text_scale=0.7,
594            position=(
595                self._width * 0.5 - 75,
596                self._yoffs - 100,
597            ),
598            size=(180, 40),
599            scale=0.8,
600            color=(0.4, 0.25, 0.5),
601            textcolor=self._textcolor,
602            on_activate_call=partial(
603                self._on_learn_more_press, response.token_info_url
604            ),
605        )
606        if uiscale is bui.UIScale.SMALL:
607            bui.widget(
608                edit=tinfobtn,
609                left_widget=bui.get_special_widget('back_button'),
610                up_widget=bui.get_special_widget('back_button'),
611            )
612
613        bui.widget(
614            edit=tinfobtn,
615            right_widget=bui.get_special_widget('tokens_meter'),
616        )
617
618        x = sidepad + xfudge
619        bwidgets: list[bui.Widget] = []
620        for i, buttondef in enumerate(buttondefs_shown):
621
622            price = None if plus is None else plus.get_price(buttondef.itemid)
623
624            x += buttondef.prepad
625            tdelay = 0.3 - i / len(buttondefs_shown) * 0.25
626            btn = bui.buttonwidget(
627                autoselect=True,
628                label='',
629                color=buttondef.color,
630                transition_delay=tdelay,
631                up_widget=tinfobtn,
632                parent=subcontainer,
633                size=(buttondef.width, 275),
634                position=(x, -10 + yoffs),
635                button_type='square',
636                on_activate_call=partial(
637                    self._purchase_press, buttondef.itemid
638                ),
639            )
640            bwidgets.append(btn)
641
642            if i == 0:
643                bui.widget(edit=btn, left_widget=self._back_button)
644
645            for imgdef in buttondef.imgdefs:
646                _img = bui.imagewidget(
647                    parent=subcontainer,
648                    size=imgdef.size,
649                    position=(x + imgdef.pos[0], imgdef.pos[1] + yoffs),
650                    draw_controller=btn,
651                    draw_controller_mult=imgdef.draw_controller_mult,
652                    color=imgdef.color,
653                    texture=bui.gettexture(imgdef.tex),
654                    transition_delay=tdelay,
655                    opacity=imgdef.opacity,
656                )
657            for txtdef in buttondef.txtdefs:
658                txt: bui.Lstr | str
659                if isinstance(txtdef.text, TextContents):
660                    if txtdef.text is TextContents.PRICE:
661                        tcolor = (
662                            (1, 1, 1, 0.5) if price is None else txtdef.color
663                        )
664                        txt = (
665                            bui.Lstr(resource='unavailableText')
666                            if price is None
667                            else price
668                        )
669                    else:
670                        # Make sure we cover all cases.
671                        assert_never(txtdef.text)
672                else:
673                    tcolor = txtdef.color
674                    txt = txtdef.text
675                _txt = bui.textwidget(
676                    parent=subcontainer,
677                    text=txt,
678                    position=(x + txtdef.pos[0], txtdef.pos[1] + yoffs),
679                    size=(0, 0),
680                    scale=txtdef.scale,
681                    h_align='center',
682                    v_align='center',
683                    draw_controller=btn,
684                    color=tcolor,
685                    transition_delay=tdelay,
686                    flatness=0.0,
687                    shadow=1.0,
688                    rotate=txtdef.rotate,
689                    maxwidth=txtdef.maxwidth,
690                )
691            x += buttondef.width + buttonpadding
692        bui.containerwidget(edit=subcontainer, visible_child=bwidgets[0])
693
694        if bool(False):
695            _tinfotxt = bui.textwidget(
696                parent=self._root_widget,
697                position=(
698                    self._width * 0.5,
699                    self._yoffs - 70,
700                ),
701                color=self._textcolor,
702                shadow=1.0,
703                scale=0.7,
704                size=(0, 0),
705                h_align='center',
706                v_align='center',
707                text=bui.Lstr(resource='tokens.shinyNewCurrencyText'),
708            )
709
710        has_removed_ads = classic is not None and (
711            classic.gold_pass
712            or classic.remove_ads
713            or classic.accounts.have_pro()
714        )
715        if plus is not None and plus.has_video_ads() and not has_removed_ads:
716            _tinfotxt = bui.textwidget(
717                parent=self._root_widget,
718                position=(
719                    self._width * 0.5,
720                    self._yoffs - 120,
721                ),
722                color=(0.4, 1.0, 0.4),
723                shadow=1.0,
724                scale=0.5,
725                size=(0, 0),
726                h_align='center',
727                v_align='center',
728                maxwidth=self._scroll_width * 0.9,
729                text=bui.Lstr(resource='removeInGameAdsTokenPurchaseText'),
730            )
731
732    def _purchase_press(self, itemid: str) -> None:
733        plus = bui.app.plus
734
735        price = None if plus is None else plus.get_price(itemid)
736
737        if price is None:
738            if plus is not None and plus.supports_purchases():
739                # Looks like internet is down or something temporary.
740                errmsg = bui.Lstr(resource='purchaseNotAvailableText')
741            else:
742                # Looks like purchases will never work here.
743                errmsg = bui.Lstr(resource='purchaseNeverAvailableText')
744
745            bui.screenmessage(errmsg, color=(1, 0.5, 0))
746            bui.getsound('error').play()
747            return
748
749        assert plus is not None
750        plus.purchase(itemid)
751
752    def _update_store_state(self) -> None:
753        """Called to make minor updates to an already shown store."""
754        assert self._last_query_response is not None
755
756    def _on_learn_more_press(self, url: str) -> None:
757        bui.open_url(url)

Window for purchasing/acquiring classic tickets.

GetTokensWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 69    def __init__(
 70        self,
 71        transition: str | None = 'in_right',
 72        origin_widget: bui.Widget | None = None,
 73    ):
 74        # pylint: disable=too-many-locals
 75        bwidthstd = 170
 76        bwidthwide = 300
 77        ycolor = (0, 0, 0.3)
 78        pcolor = (0, 0, 0.3)
 79        pos1 = 65
 80        pos2 = 34
 81        titlescale = 0.9
 82        pricescale = 0.65
 83        bcapcol1 = (0.25, 0.13, 0.02)
 84        self._buttondefs: list[_ButtonDef] = [
 85            _ButtonDef(
 86                itemid='tokens1',
 87                width=bwidthstd,
 88                color=ycolor,
 89                imgdefs=[
 90                    _ImgDef(
 91                        'tokens1',
 92                        pos=(-3, 85),
 93                        size=(172, 172),
 94                        opacity=1.0,
 95                        draw_controller_mult=0.5,
 96                    ),
 97                    _ImgDef(
 98                        'windowBottomCap',
 99                        pos=(1.5, 4),
100                        size=(bwidthstd * 0.960, 100),
101                        color=bcapcol1,
102                        opacity=1.0,
103                    ),
104                ],
105                txtdefs=[
106                    _TxtDef(
107                        bui.Lstr(
108                            resource='tokens.numTokensText',
109                            subs=[('${COUNT}', '50')],
110                        ),
111                        pos=(bwidthstd * 0.5, pos1),
112                        color=(1.1, 1.05, 1.0),
113                        scale=titlescale,
114                        maxwidth=bwidthstd * 0.9,
115                    ),
116                    _TxtDef(
117                        TextContents.PRICE,
118                        pos=(bwidthstd * 0.5, pos2),
119                        color=(1.1, 1.05, 1.0),
120                        scale=pricescale,
121                        maxwidth=bwidthstd * 0.9,
122                    ),
123                ],
124            ),
125            _ButtonDef(
126                itemid='tokens2',
127                width=bwidthstd,
128                color=ycolor,
129                imgdefs=[
130                    _ImgDef(
131                        'tokens2',
132                        pos=(-3, 85),
133                        size=(172, 172),
134                        opacity=1.0,
135                        draw_controller_mult=0.5,
136                    ),
137                    _ImgDef(
138                        'windowBottomCap',
139                        pos=(1.5, 4),
140                        size=(bwidthstd * 0.960, 100),
141                        color=bcapcol1,
142                        opacity=1.0,
143                    ),
144                ],
145                txtdefs=[
146                    _TxtDef(
147                        bui.Lstr(
148                            resource='tokens.numTokensText',
149                            subs=[('${COUNT}', '500')],
150                        ),
151                        pos=(bwidthstd * 0.5, pos1),
152                        color=(1.1, 1.05, 1.0),
153                        scale=titlescale,
154                        maxwidth=bwidthstd * 0.9,
155                    ),
156                    _TxtDef(
157                        TextContents.PRICE,
158                        pos=(bwidthstd * 0.5, pos2),
159                        color=(1.1, 1.05, 1.0),
160                        scale=pricescale,
161                        maxwidth=bwidthstd * 0.9,
162                    ),
163                ],
164            ),
165            _ButtonDef(
166                itemid='tokens3',
167                width=bwidthstd,
168                color=ycolor,
169                imgdefs=[
170                    _ImgDef(
171                        'tokens3',
172                        pos=(-3, 85),
173                        size=(172, 172),
174                        opacity=1.0,
175                        draw_controller_mult=0.5,
176                    ),
177                    _ImgDef(
178                        'windowBottomCap',
179                        pos=(1.5, 4),
180                        size=(bwidthstd * 0.960, 100),
181                        color=bcapcol1,
182                        opacity=1.0,
183                    ),
184                ],
185                txtdefs=[
186                    _TxtDef(
187                        bui.Lstr(
188                            resource='tokens.numTokensText',
189                            subs=[('${COUNT}', '1200')],
190                        ),
191                        pos=(bwidthstd * 0.5, pos1),
192                        color=(1.1, 1.05, 1.0),
193                        scale=titlescale,
194                        maxwidth=bwidthstd * 0.9,
195                    ),
196                    _TxtDef(
197                        TextContents.PRICE,
198                        pos=(bwidthstd * 0.5, pos2),
199                        color=(1.1, 1.05, 1.0),
200                        scale=pricescale,
201                        maxwidth=bwidthstd * 0.9,
202                    ),
203                ],
204            ),
205            _ButtonDef(
206                itemid='tokens4',
207                width=bwidthstd,
208                color=ycolor,
209                imgdefs=[
210                    _ImgDef(
211                        'tokens4',
212                        pos=(-3, 85),
213                        size=(172, 172),
214                        opacity=1.0,
215                        draw_controller_mult=0.5,
216                    ),
217                    _ImgDef(
218                        'windowBottomCap',
219                        pos=(1.5, 4),
220                        size=(bwidthstd * 0.960, 100),
221                        color=bcapcol1,
222                        opacity=1.0,
223                    ),
224                ],
225                txtdefs=[
226                    _TxtDef(
227                        bui.Lstr(
228                            resource='tokens.numTokensText',
229                            subs=[('${COUNT}', '2600')],
230                        ),
231                        pos=(bwidthstd * 0.5, pos1),
232                        color=(1.1, 1.05, 1.0),
233                        scale=titlescale,
234                        maxwidth=bwidthstd * 0.9,
235                    ),
236                    _TxtDef(
237                        TextContents.PRICE,
238                        pos=(bwidthstd * 0.5, pos2),
239                        color=(1.1, 1.05, 1.0),
240                        scale=pricescale,
241                        maxwidth=bwidthstd * 0.9,
242                    ),
243                ],
244            ),
245            _ButtonDef(
246                itemid='gold_pass',
247                width=bwidthwide,
248                color=pcolor,
249                imgdefs=[
250                    _ImgDef(
251                        'goldPass',
252                        pos=(-7, 102),
253                        size=(312, 156),
254                        draw_controller_mult=0.3,
255                    ),
256                    _ImgDef(
257                        'windowBottomCap',
258                        pos=(8, 4),
259                        size=(bwidthwide * 0.923, 116),
260                        color=(0.25, 0.12, 0.15),
261                        opacity=1.0,
262                    ),
263                ],
264                txtdefs=[
265                    _TxtDef(
266                        bui.Lstr(resource='goldPass.goldPassText'),
267                        pos=(bwidthwide * 0.5, pos1 + 27),
268                        color=(1.1, 1.05, 1.0),
269                        scale=titlescale,
270                        maxwidth=bwidthwide * 0.8,
271                    ),
272                    _TxtDef(
273                        bui.Lstr(resource='goldPass.desc1InfTokensText'),
274                        pos=(bwidthwide * 0.5, pos1 + 6),
275                        color=(1.1, 1.05, 1.0),
276                        scale=0.4,
277                        maxwidth=bwidthwide * 0.8,
278                    ),
279                    _TxtDef(
280                        bui.Lstr(resource='goldPass.desc2NoAdsText'),
281                        pos=(bwidthwide * 0.5, pos1 + 6 - 13 * 1),
282                        color=(1.1, 1.05, 1.0),
283                        scale=0.4,
284                        maxwidth=bwidthwide * 0.8,
285                    ),
286                    _TxtDef(
287                        bui.Lstr(resource='goldPass.desc3ForeverText'),
288                        pos=(bwidthwide * 0.5, pos1 + 6 - 13 * 2),
289                        color=(1.1, 1.05, 1.0),
290                        scale=0.4,
291                        maxwidth=bwidthwide * 0.8,
292                    ),
293                    _TxtDef(
294                        TextContents.PRICE,
295                        pos=(bwidthwide * 0.5, pos2 - 9),
296                        color=(1.1, 1.05, 1.0),
297                        scale=pricescale,
298                        maxwidth=bwidthwide * 0.8,
299                    ),
300                ],
301                prepad=-8,
302            ),
303        ]
304
305        self._transitioning_out = False
306        self._textcolor = (0.92, 0.92, 2.0)
307
308        self._query_in_flight = False
309        self._last_query_time = -1.0
310        self._last_query_response: bacommon.cloud.StoreQueryResponse | None = (
311            None
312        )
313
314        uiscale = bui.app.ui_v1.uiscale
315        self._width = 1200.0 if uiscale is bui.UIScale.SMALL else 1070.0
316        self._height = 800 if uiscale is bui.UIScale.SMALL else 520.0
317
318        self._r = 'getTokensWindow'
319
320        # Do some fancy math to fill all available screen area up to the
321        # size of our backing container. This lets us fit to the exact
322        # screen shape at small ui scale.
323        screensize = bui.get_virtual_screen_size()
324        scale = (
325            1.5
326            if uiscale is bui.UIScale.SMALL
327            else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.95
328        )
329        # Calc screen size in our local container space and clamp to a
330        # bit smaller than our container size.
331        target_width = min(self._width - 60, screensize[0] / scale)
332        target_height = min(self._height - 70, screensize[1] / scale)
333
334        # To get top/left coords, go to the center of our window and
335        # offset by half the width/height of our target area.
336        self._yoffs = 0.5 * self._height + 0.5 * target_height + 20.0
337
338        self._scroll_width = target_width
339
340        super().__init__(
341            root_widget=bui.containerwidget(
342                size=(self._width, self._height),
343                color=(0.3, 0.23, 0.36),
344                scale=scale,
345                toolbar_visibility=(
346                    'get_tokens'
347                    if uiscale is bui.UIScale.SMALL
348                    else 'menu_full'
349                ),
350            ),
351            transition=transition,
352            origin_widget=origin_widget,
353            # We're affected by screen size only at small ui-scale.
354            refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
355        )
356
357        if uiscale is bui.UIScale.SMALL:
358            bui.containerwidget(
359                edit=self._root_widget, on_cancel_call=self.main_window_back
360            )
361            self._back_button = bui.get_special_widget('back_button')
362        else:
363            self._back_button = bui.buttonwidget(
364                parent=self._root_widget,
365                position=(60, self._yoffs - 90),
366                size=((60, 60)),
367                scale=1.0,
368                autoselect=True,
369                label=(bui.charstr(bui.SpecialChar.BACK)),
370                button_type=('backSmall'),
371                on_activate_call=self.main_window_back,
372            )
373            bui.containerwidget(
374                edit=self._root_widget, cancel_button=self._back_button
375            )
376
377        self._title_text = bui.textwidget(
378            parent=self._root_widget,
379            position=(self._width * 0.5, self._yoffs - 42),
380            size=(0, 0),
381            color=self._textcolor,
382            flatness=0.0,
383            shadow=1.0,
384            scale=1.2,
385            h_align='center',
386            v_align='center',
387            text=bui.Lstr(resource='tokens.getTokensText'),
388            maxwidth=260,
389        )
390
391        self._status_text = bui.textwidget(
392            parent=self._root_widget,
393            size=(0, 0),
394            position=(self._width * 0.5, self._height * 0.5),
395            h_align='center',
396            v_align='center',
397            color=(0.6, 0.6, 0.6),
398            scale=0.75,
399            text='',
400        )
401        # Create a spinner - it will get cleared when state changes from
402        # LOADING.
403        bui.spinnerwidget(
404            parent=self._root_widget,
405            size=60,
406            position=(self._width * 0.5, self._height * 0.5),
407            style='bomb',
408        )
409
410        self._core_widgets = [
411            self._back_button,
412            self._title_text,
413            self._status_text,
414        ]
415
416        # Get all textures used by our buttons preloading so hopefully
417        # they'll be in place by the time we show them.
418        for bdef in self._buttondefs:
419            for bimg in bdef.imgdefs:
420                bui.gettexture(bimg.tex)
421
422        self._state = self.State.LOADING
423
424        self._update_timer = bui.AppTimer(
425            0.789, bui.WeakCall(self._update), repeat=True
426        )
427        self._update()

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:
429    @override
430    def get_main_window_state(self) -> bui.MainWindowState:
431        # Support recreating our window for back/refresh purposes.
432        cls = type(self)
433        return bui.BasicMainWindowState(
434            create_call=lambda transition, origin_widget: cls(
435                transition=transition, origin_widget=origin_widget
436            )
437        )

Return a WindowState to recreate this window, if supported.

class GetTokensWindow.State(enum.Enum):
61    class State(Enum):
62        """What are we doing?"""
63
64        LOADING = 'loading'
65        NOT_SIGNED_IN = 'not_signed_in'
66        HAVE_GOLD_PASS = 'have_gold_pass'
67        SHOWING_STORE = 'showing_store'

What are we doing?

LOADING = <State.LOADING: 'loading'>
NOT_SIGNED_IN = <State.NOT_SIGNED_IN: 'not_signed_in'>
HAVE_GOLD_PASS = <State.HAVE_GOLD_PASS: 'have_gold_pass'>
SHOWING_STORE = <State.SHOWING_STORE: 'showing_store'>
def show_get_tokens_prompt() -> None:
760def show_get_tokens_prompt() -> None:
761    """Show a 'not enough tokens' prompt with an option to purchase more.
762
763    Note that the purchase option may not always be available
764    depending on the build of the game.
765    """
766    from bauiv1lib.confirm import ConfirmWindow
767
768    assert bui.app.classic is not None
769
770    # Currently always allowing token purchases.
771    if bool(True):
772        ConfirmWindow(
773            bui.Lstr(resource='tokens.notEnoughTokensText'),
774            show_get_tokens_window,
775            ok_text=bui.Lstr(resource='tokens.getTokensText'),
776            width=460,
777            height=130,
778        )
779    else:
780        ConfirmWindow(
781            bui.Lstr(resource='tokens.notEnoughTokensText'),
782            cancel_button=False,
783            width=460,
784            height=130,
785        )

Show a 'not enough tokens' prompt with an option to purchase more.

Note that the purchase option may not always be available depending on the build of the game.

def show_get_tokens_window(origin_widget: _bauiv1.Widget | None = None) -> None:
788def show_get_tokens_window(origin_widget: bui.Widget | None = None) -> None:
789    """Transition to the get-tokens main-window from anywhere."""
790
791    # NOTE TO USERS: The code below is not the proper way to do things;
792    # whenever possible one should use a MainWindow's
793    # main_window_replace() or main_window_back() methods. We just need
794    # to do things a bit more manually in this particular case.
795
796    prev_main_window = bui.app.ui_v1.get_main_window()
797
798    # Special-case: If it seems we're already in the window, do nothing.
799    if isinstance(prev_main_window, GetTokensWindow):
800        return
801
802    # Set our new main window.
803    bui.app.ui_v1.set_main_window(
804        GetTokensWindow(origin_widget=origin_widget),
805        from_window=False,
806        is_auxiliary=True,
807        suppress_warning=True,
808    )
809
810    # Transition out any previous main window.
811    if prev_main_window is not None:
812        prev_main_window.main_window_close()

Transition to the get-tokens main-window from anywhere.