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 bacommon.bs
 15import bauiv1 as bui
 16
 17
 18if TYPE_CHECKING:
 19    from typing import Any, Callable
 20
 21
 22@dataclass
 23class _ButtonDef:
 24    itemid: str
 25    width: float
 26    color: tuple[float, float, float]
 27    imgdefs: list[_ImgDef]
 28    txtdefs: list[_TxtDef]
 29    prepad: float = 0.0
 30
 31
 32@dataclass
 33class _ImgDef:
 34    tex: str
 35    pos: tuple[float, float]
 36    size: tuple[float, float]
 37    color: tuple[float, float, float] = (1, 1, 1)
 38    opacity: float = 1.0
 39    draw_controller_mult: float | None = None
 40
 41
 42class TextContents(Enum):
 43    """Some type of text to show."""
 44
 45    PRICE = 'price'
 46
 47
 48@dataclass
 49class _TxtDef:
 50    text: str | TextContents | bui.Lstr
 51    pos: tuple[float, float]
 52    maxwidth: float | None
 53    scale: float = 1.0
 54    color: tuple[float, float, float] = (1, 1, 1)
 55    rotate: float | None = None
 56
 57
 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}', str(bacommon.bs.TOKENS1_COUNT))],
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}', str(bacommon.bs.TOKENS2_COUNT))],
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}', str(bacommon.bs.TOKENS3_COUNT))],
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}', str(bacommon.bs.TOKENS4_COUNT))],
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)
758
759
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        )
786
787
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()
class TextContents(enum.Enum):
43class TextContents(Enum):
44    """Some type of text to show."""
45
46    PRICE = 'price'

Some type of text to show.

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

Window for purchasing/acquiring classic tickets.

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

Return a WindowState to recreate this window, if supported.

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

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

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