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        # restore_previous_call: Callable[[bui.Widget], None] | None = None,
 73    ):
 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._restore_previous_call = restore_previous_call
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        # If they provided an origin-widget, scale up from that.
315        # scale_origin: tuple[float, float] | None
316        # if origin_widget is not None:
317        #     self._transition_out = 'out_scale'
318        #     scale_origin = origin_widget.get_screen_space_center()
319        #     transition = 'in_scale'
320        # else:
321        #     self._transition_out = 'out_right'
322        #     scale_origin = None
323
324        uiscale = bui.app.ui_v1.uiscale
325        self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0
326        self._x_inset = 25.0 if uiscale is bui.UIScale.SMALL else 0.0
327        self._height = 550 if uiscale is bui.UIScale.SMALL else 480.0
328        self._y_offset = -60 if uiscale is bui.UIScale.SMALL else 0
329
330        self._r = 'getTokensWindow'
331
332        super().__init__(
333            root_widget=bui.containerwidget(
334                size=(self._width, self._height),
335                # transition=transition,
336                # scale_origin_stack_offset=scale_origin,
337                color=(0.3, 0.23, 0.36),
338                scale=(
339                    1.5
340                    if uiscale is bui.UIScale.SMALL
341                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
342                ),
343                stack_offset=(
344                    (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0)
345                ),
346                # toolbar_visibility='menu_minimal',
347                toolbar_visibility=(
348                    'get_tokens'
349                    if uiscale is bui.UIScale.SMALL
350                    else 'menu_full'
351                ),
352            ),
353            transition=transition,
354            origin_widget=origin_widget,
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=(
366                    55 + self._x_inset,
367                    self._height - 80 + self._y_offset,
368                ),
369                size=(
370                    # (140, 60)
371                    # if self._restore_previous_call is None
372                    # else
373                    (60, 60)
374                ),
375                scale=1.0,
376                autoselect=True,
377                label=(
378                    # bui.Lstr(resource='doneText')
379                    # if self._restore_previous_call is None
380                    # else
381                    bui.charstr(bui.SpecialChar.BACK)
382                ),
383                button_type=(
384                    # 'regular'
385                    # if self._restore_previous_call is None
386                    # else
387                    'backSmall'
388                ),
389                on_activate_call=self.main_window_back,
390            )
391            # if uiscale is bui.UIScale.SMALL:
392            #     bui.widget(
393            #         edit=self._back_button,
394            #         up_widget=bui.get_special_widget('tokens_meter'),
395            #     )
396            bui.containerwidget(
397                edit=self._root_widget, cancel_button=self._back_button
398            )
399
400        self._title_text = bui.textwidget(
401            parent=self._root_widget,
402            position=(self._width * 0.5, self._height - 42 + self._y_offset),
403            size=(0, 0),
404            color=self._textcolor,
405            flatness=0.0,
406            shadow=1.0,
407            scale=1.2,
408            h_align='center',
409            v_align='center',
410            text=bui.Lstr(resource='tokens.getTokensText'),
411            maxwidth=260,
412        )
413
414        self._status_text = bui.textwidget(
415            parent=self._root_widget,
416            size=(0, 0),
417            position=(self._width * 0.5, self._height * 0.5),
418            h_align='center',
419            v_align='center',
420            color=(0.6, 0.6, 0.6),
421            scale=0.75,
422            text=bui.Lstr(resource='store.loadingText'),
423        )
424
425        self._core_widgets = [
426            self._back_button,
427            self._title_text,
428            self._status_text,
429        ]
430
431        # self._token_count_widget: bui.Widget | None = None
432        # self._smooth_update_timer: bui.AppTimer | None = None
433        # self._smooth_token_count: float | None = None
434        # self._token_count: int = 0
435        # self._smooth_increase_speed = 1.0
436        # self._ticking_sound: bui.Sound | None = None
437
438        # Get all textures used by our buttons preloading so hopefully
439        # they'll be in place by the time we show them.
440        for bdef in self._buttondefs:
441            for bimg in bdef.imgdefs:
442                bui.gettexture(bimg.tex)
443
444        self._state = self.State.LOADING
445
446        self._update_timer = bui.AppTimer(
447            0.789, bui.WeakCall(self._update), repeat=True
448        )
449        self._update()
450
451    # def __del__(self) -> None:
452    #     if self._ticking_sound is not None:
453    #         self._ticking_sound.stop()
454    #         self._ticking_sound = None
455
456    @override
457    def get_main_window_state(self) -> bui.MainWindowState:
458        # Support recreating our window for back/refresh purposes.
459        cls = type(self)
460        return bui.BasicMainWindowState(
461            create_call=lambda transition, origin_widget: cls(
462                transition=transition, origin_widget=origin_widget
463            )
464        )
465
466    def _update(self) -> None:
467        # No-op if our underlying widget is dead or on its way out.
468        if not self._root_widget or self._root_widget.transitioning_out:
469            return
470
471        plus = bui.app.plus
472
473        if plus is None or plus.accounts.primary is None:
474            self._update_state(self.State.NOT_SIGNED_IN)
475            return
476
477        # Poll for relevant changes to the store or our account.
478        now = time.monotonic()
479        if not self._query_in_flight and now - self._last_query_time > 2.0:
480            self._last_query_time = now
481            self._query_in_flight = True
482            with plus.accounts.primary:
483                plus.cloud.send_message_cb(
484                    bacommon.cloud.StoreQueryMessage(),
485                    on_response=bui.WeakCall(self._on_store_query_response),
486                )
487
488        # Can't do much until we get a store state.
489        if self._last_query_response is None:
490            return
491
492        # If we've got a gold-pass, just show that. No need to offer any
493        # other purchases.
494        if self._last_query_response.gold_pass:
495            self._update_state(self.State.HAVE_GOLD_PASS)
496            return
497
498        # Ok we seem to be signed in and have store stuff we can show.
499        # Do that.
500        self._update_state(self.State.SHOWING_STORE)
501
502    def _update_state(self, state: State) -> None:
503
504        # We don't do much when state is unchanged.
505        if state is self._state:
506            # Update a few things in store mode though, such as token
507            # count.
508            if state is self.State.SHOWING_STORE:
509                self._update_store_state()
510            return
511
512        # Ok, state is changing. Start by resetting to a blank slate.
513        # self._token_count_widget = None
514        for widget in self._root_widget.get_children():
515            if widget not in self._core_widgets:
516                widget.delete()
517
518        # Build up new state.
519        if state is self.State.NOT_SIGNED_IN:
520            bui.textwidget(
521                edit=self._status_text,
522                color=(1, 0, 0),
523                text=bui.Lstr(resource='notSignedInErrorText'),
524            )
525        elif state is self.State.LOADING:
526            raise RuntimeError('Should never return to loading state.')
527        elif state is self.State.HAVE_GOLD_PASS:
528            bui.textwidget(
529                edit=self._status_text,
530                color=(0, 1, 0),
531                text=bui.Lstr(resource='tokens.youHaveGoldPassText'),
532            )
533        elif state is self.State.SHOWING_STORE:
534            assert self._last_query_response is not None
535            bui.textwidget(edit=self._status_text, text='')
536            self._build_store_for_response(self._last_query_response)
537        else:
538            # Make sure we handle all cases.
539            assert_never(state)
540
541        self._state = state
542
543    def _on_load_error(self) -> None:
544        bui.textwidget(
545            edit=self._status_text,
546            text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
547            color=(1, 0, 0),
548        )
549
550    def _on_store_query_response(
551        self, response: bacommon.cloud.StoreQueryResponse | Exception
552    ) -> None:
553        self._query_in_flight = False
554        if isinstance(response, bacommon.cloud.StoreQueryResponse):
555            self._last_query_response = response
556            # Hurry along any effects of this response.
557            self._update()
558
559    def _build_store_for_response(
560        self, response: bacommon.cloud.StoreQueryResponse
561    ) -> None:
562        # pylint: disable=too-many-locals
563        plus = bui.app.plus
564
565        uiscale = bui.app.ui_v1.uiscale
566
567        bui.textwidget(edit=self._status_text, text='')
568
569        xinset = 40
570
571        scrollwidth = self._width - 2 * (self._x_inset + xinset)
572        scrollheight = 280
573        buttonpadding = -5
574
575        yoffs = 5
576
577        # We currently don't handle the zero-button case.
578        assert self._buttondefs
579
580        sidepad = 10.0
581        total_button_width = (
582            sum(b.width + b.prepad for b in self._buttondefs)
583            + buttonpadding * (len(self._buttondefs) - 1)
584            + 2 * sidepad
585        )
586
587        h_scroll = bui.hscrollwidget(
588            parent=self._root_widget,
589            size=(scrollwidth, scrollheight),
590            position=(
591                self._x_inset + xinset,
592                self._height - 415 + self._y_offset,
593            ),
594            claims_left_right=True,
595            highlight=False,
596            border_opacity=0.3 if uiscale is bui.UIScale.SMALL else 1.0,
597        )
598        subcontainer = bui.containerwidget(
599            parent=h_scroll,
600            background=False,
601            size=(max(total_button_width, scrollwidth), scrollheight),
602        )
603        tinfobtn = bui.buttonwidget(
604            parent=self._root_widget,
605            autoselect=True,
606            label=bui.Lstr(resource='learnMoreText'),
607            position=(
608                self._width * 0.5 - 75,
609                self._height - 125 + self._y_offset,
610            ),
611            size=(180, 43),
612            scale=0.8,
613            color=(0.4, 0.25, 0.5),
614            textcolor=self._textcolor,
615            on_activate_call=partial(
616                self._on_learn_more_press, response.token_info_url
617            ),
618        )
619        if uiscale is bui.UIScale.SMALL:
620            bui.widget(
621                edit=tinfobtn,
622                left_widget=bui.get_special_widget('back_button'),
623                up_widget=bui.get_special_widget('back_button'),
624            )
625
626        bui.widget(
627            edit=tinfobtn,
628            right_widget=bui.get_special_widget('tokens_meter'),
629        )
630
631        x = sidepad
632        bwidgets: list[bui.Widget] = []
633        for i, buttondef in enumerate(self._buttondefs):
634
635            price = None if plus is None else plus.get_price(buttondef.itemid)
636
637            x += buttondef.prepad
638            tdelay = 0.3 - i / len(self._buttondefs) * 0.25
639            btn = bui.buttonwidget(
640                autoselect=True,
641                label='',
642                color=buttondef.color,
643                transition_delay=tdelay,
644                up_widget=tinfobtn,
645                parent=subcontainer,
646                size=(buttondef.width, 275),
647                position=(x, -10 + yoffs),
648                button_type='square',
649                on_activate_call=partial(
650                    self._purchase_press, buttondef.itemid
651                ),
652            )
653            bwidgets.append(btn)
654
655            if i == 0:
656                bui.widget(edit=btn, left_widget=self._back_button)
657
658            for imgdef in buttondef.imgdefs:
659                _img = bui.imagewidget(
660                    parent=subcontainer,
661                    size=imgdef.size,
662                    position=(x + imgdef.pos[0], imgdef.pos[1] + yoffs),
663                    draw_controller=btn,
664                    draw_controller_mult=imgdef.draw_controller_mult,
665                    color=imgdef.color,
666                    texture=bui.gettexture(imgdef.tex),
667                    transition_delay=tdelay,
668                    opacity=imgdef.opacity,
669                )
670            for txtdef in buttondef.txtdefs:
671                txt: bui.Lstr | str
672                if isinstance(txtdef.text, TextContents):
673                    if txtdef.text is TextContents.PRICE:
674                        tcolor = (
675                            (1, 1, 1, 0.5) if price is None else txtdef.color
676                        )
677                        txt = (
678                            bui.Lstr(resource='unavailableText')
679                            if price is None
680                            else price
681                        )
682                    else:
683                        # Make sure we cover all cases.
684                        assert_never(txtdef.text)
685                else:
686                    tcolor = txtdef.color
687                    txt = txtdef.text
688                _txt = bui.textwidget(
689                    parent=subcontainer,
690                    text=txt,
691                    position=(x + txtdef.pos[0], txtdef.pos[1] + yoffs),
692                    size=(0, 0),
693                    scale=txtdef.scale,
694                    h_align='center',
695                    v_align='center',
696                    draw_controller=btn,
697                    color=tcolor,
698                    transition_delay=tdelay,
699                    flatness=0.0,
700                    shadow=1.0,
701                    rotate=txtdef.rotate,
702                    maxwidth=txtdef.maxwidth,
703                )
704            x += buttondef.width + buttonpadding
705        bui.containerwidget(edit=subcontainer, visible_child=bwidgets[0])
706
707        _tinfotxt = bui.textwidget(
708            parent=self._root_widget,
709            position=(self._width * 0.5, self._height - 70 + self._y_offset),
710            color=self._textcolor,
711            shadow=1.0,
712            scale=0.7,
713            size=(0, 0),
714            h_align='center',
715            v_align='center',
716            text=bui.Lstr(resource='tokens.shinyNewCurrencyText'),
717        )
718        # self._token_count_widget = bui.textwidget(
719        #     parent=self._root_widget,
720        #     position=(
721        #         self._width - self._x_inset - 120.0,
722        #         self._height - 48 + self._y_offset,
723        #     ),
724        #     color=(2.0, 0.7, 0.0),
725        #     shadow=1.0,
726        #     flatness=0.0,
727        #     size=(0, 0),
728        #     h_align='left',
729        #     v_align='center',
730        #     text='',
731        # )
732        # self._token_count = response.tokens
733        # self._smooth_token_count = float(self._token_count)
734        # self._smooth_update()  # will set the text widget.
735
736        # _tlabeltxt = bui.textwidget(
737        #     parent=self._root_widget,
738        #     position=(
739        #         self._width - self._x_inset - 123.0,
740        #         self._height - 48 + self._y_offset,
741        #     ),
742        #     size=(0, 0),
743        #     h_align='right',
744        #     v_align='center',
745        #     text=bui.charstr(bui.SpecialChar.TOKEN),
746        # )
747
748    def _purchase_press(self, itemid: str) -> None:
749        plus = bui.app.plus
750
751        price = None if plus is None else plus.get_price(itemid)
752
753        if price is None:
754            if plus is not None and plus.supports_purchases():
755                # Looks like internet is down or something temporary.
756                errmsg = bui.Lstr(resource='purchaseNotAvailableText')
757            else:
758                # Looks like purchases will never work here.
759                errmsg = bui.Lstr(resource='purchaseNeverAvailableText')
760
761            bui.screenmessage(errmsg, color=(1, 0.5, 0))
762            bui.getsound('error').play()
763            return
764
765        assert plus is not None
766        plus.purchase(itemid)
767
768    def _update_store_state(self) -> None:
769        """Called to make minor updates to an already shown store."""
770        # assert self._token_count_widget is not None
771        assert self._last_query_response is not None
772
773        # self._token_count = self._last_query_response.tokens
774
775        # Kick off new smooth update if need be.
776        # assert self._smooth_token_count is not None
777        # if (
778        #     self._token_count != int(self._smooth_token_count)
779        #     and self._smooth_update_timer is None
780        # ):
781        #     self._smooth_update_timer = bui.AppTimer(
782        #         0.05, bui.WeakCall(self._smooth_update), repeat=True
783        #     )
784        #     diff = abs(float(self._token_count) - self._smooth_token_count)
785        #     self._smooth_increase_speed = (
786        #         diff / 100.0
787        #         if diff >= 5000
788        #         else (
789        #             diff / 50.0
790        #             if diff >= 1500
791        #             else diff / 30.0 if diff >= 500 else diff / 15.0
792        #         )
793        #     )
794
795    # def _smooth_update(self) -> None:
796
797    #     # Stop if the count widget disappears.
798    #     if not self._token_count_widget:
799    #         self._smooth_update_timer = None
800    #         return
801
802    #     finished = False
803
804    #     # If we're going down, do it immediately.
805    #     assert self._smooth_token_count is not None
806    #     if int(self._smooth_token_count) >= self._token_count:
807    #         self._smooth_token_count = float(self._token_count)
808    #         finished = True
809    #     else:
810    #         # We're going up; start a sound if need be.
811    #         self._smooth_token_count = min(
812    #             self._smooth_token_count + 1.0 * self._smooth_increase_speed,
813    #             self._token_count,
814    #         )
815    #         if int(self._smooth_token_count) >= self._token_count:
816    #             finished = True
817    #             self._smooth_token_count = float(self._token_count)
818    #         elif self._ticking_sound is None:
819    #             self._ticking_sound = bui.getsound('scoreIncrease')
820    #             self._ticking_sound.play()
821
822    #     bui.textwidget(
823    #         edit=self._token_count_widget,
824    #         text=str(int(self._smooth_token_count)),
825    #     )
826
827    #     # If we've reached the target, kill the timer/sound/etc.
828    #     if finished:
829    #         self._smooth_update_timer = None
830    #         if self._ticking_sound is not None:
831    #             self._ticking_sound.stop()
832    #             self._ticking_sound = None
833    #             bui.getsound('cashRegister2').play()
834
835    # def _back(self) -> None:
836
837    #     self.main_
838    # No-op if our underlying widget is dead or on its way out.
839    # if not self._root_widget or self._root_widget.transitioning_out:
840    #     return
841
842    # bui.containerwidget(
843    #     edit=self._root_widget, transition=self._transition_out
844    # )
845    # if self._restore_previous_call is not None:
846    #     self._restore_previous_call(self._root_widget)
847
848    def _on_learn_more_press(self, url: str) -> None:
849        bui.open_url(url)
850
851
852def show_get_tokens_prompt() -> None:
853    """Show a 'not enough tokens' prompt with an option to purchase more.
854
855    Note that the purchase option may not always be available
856    depending on the build of the game.
857    """
858    from bauiv1lib.confirm import ConfirmWindow
859
860    assert bui.app.classic is not None
861
862    # Currently always allowing token purchases.
863    if bool(True):
864        ConfirmWindow(
865            bui.Lstr(resource='tokens.notEnoughTokensText'),
866            GetTokensWindow,
867            ok_text=bui.Lstr(resource='tokens.getTokensText'),
868            width=460,
869            height=130,
870        )
871    else:
872        ConfirmWindow(
873            bui.Lstr(resource='tokens.notEnoughTokensText'),
874            cancel_button=False,
875            width=460,
876            height=130,
877        )
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        # restore_previous_call: Callable[[bui.Widget], None] | None = None,
 74    ):
 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._restore_previous_call = restore_previous_call
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        # If they provided an origin-widget, scale up from that.
316        # scale_origin: tuple[float, float] | None
317        # if origin_widget is not None:
318        #     self._transition_out = 'out_scale'
319        #     scale_origin = origin_widget.get_screen_space_center()
320        #     transition = 'in_scale'
321        # else:
322        #     self._transition_out = 'out_right'
323        #     scale_origin = None
324
325        uiscale = bui.app.ui_v1.uiscale
326        self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0
327        self._x_inset = 25.0 if uiscale is bui.UIScale.SMALL else 0.0
328        self._height = 550 if uiscale is bui.UIScale.SMALL else 480.0
329        self._y_offset = -60 if uiscale is bui.UIScale.SMALL else 0
330
331        self._r = 'getTokensWindow'
332
333        super().__init__(
334            root_widget=bui.containerwidget(
335                size=(self._width, self._height),
336                # transition=transition,
337                # scale_origin_stack_offset=scale_origin,
338                color=(0.3, 0.23, 0.36),
339                scale=(
340                    1.5
341                    if uiscale is bui.UIScale.SMALL
342                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
343                ),
344                stack_offset=(
345                    (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0)
346                ),
347                # toolbar_visibility='menu_minimal',
348                toolbar_visibility=(
349                    'get_tokens'
350                    if uiscale is bui.UIScale.SMALL
351                    else 'menu_full'
352                ),
353            ),
354            transition=transition,
355            origin_widget=origin_widget,
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=(
367                    55 + self._x_inset,
368                    self._height - 80 + self._y_offset,
369                ),
370                size=(
371                    # (140, 60)
372                    # if self._restore_previous_call is None
373                    # else
374                    (60, 60)
375                ),
376                scale=1.0,
377                autoselect=True,
378                label=(
379                    # bui.Lstr(resource='doneText')
380                    # if self._restore_previous_call is None
381                    # else
382                    bui.charstr(bui.SpecialChar.BACK)
383                ),
384                button_type=(
385                    # 'regular'
386                    # if self._restore_previous_call is None
387                    # else
388                    'backSmall'
389                ),
390                on_activate_call=self.main_window_back,
391            )
392            # if uiscale is bui.UIScale.SMALL:
393            #     bui.widget(
394            #         edit=self._back_button,
395            #         up_widget=bui.get_special_widget('tokens_meter'),
396            #     )
397            bui.containerwidget(
398                edit=self._root_widget, cancel_button=self._back_button
399            )
400
401        self._title_text = bui.textwidget(
402            parent=self._root_widget,
403            position=(self._width * 0.5, self._height - 42 + self._y_offset),
404            size=(0, 0),
405            color=self._textcolor,
406            flatness=0.0,
407            shadow=1.0,
408            scale=1.2,
409            h_align='center',
410            v_align='center',
411            text=bui.Lstr(resource='tokens.getTokensText'),
412            maxwidth=260,
413        )
414
415        self._status_text = bui.textwidget(
416            parent=self._root_widget,
417            size=(0, 0),
418            position=(self._width * 0.5, self._height * 0.5),
419            h_align='center',
420            v_align='center',
421            color=(0.6, 0.6, 0.6),
422            scale=0.75,
423            text=bui.Lstr(resource='store.loadingText'),
424        )
425
426        self._core_widgets = [
427            self._back_button,
428            self._title_text,
429            self._status_text,
430        ]
431
432        # self._token_count_widget: bui.Widget | None = None
433        # self._smooth_update_timer: bui.AppTimer | None = None
434        # self._smooth_token_count: float | None = None
435        # self._token_count: int = 0
436        # self._smooth_increase_speed = 1.0
437        # self._ticking_sound: bui.Sound | None = None
438
439        # Get all textures used by our buttons preloading so hopefully
440        # they'll be in place by the time we show them.
441        for bdef in self._buttondefs:
442            for bimg in bdef.imgdefs:
443                bui.gettexture(bimg.tex)
444
445        self._state = self.State.LOADING
446
447        self._update_timer = bui.AppTimer(
448            0.789, bui.WeakCall(self._update), repeat=True
449        )
450        self._update()
451
452    # def __del__(self) -> None:
453    #     if self._ticking_sound is not None:
454    #         self._ticking_sound.stop()
455    #         self._ticking_sound = None
456
457    @override
458    def get_main_window_state(self) -> bui.MainWindowState:
459        # Support recreating our window for back/refresh purposes.
460        cls = type(self)
461        return bui.BasicMainWindowState(
462            create_call=lambda transition, origin_widget: cls(
463                transition=transition, origin_widget=origin_widget
464            )
465        )
466
467    def _update(self) -> None:
468        # No-op if our underlying widget is dead or on its way out.
469        if not self._root_widget or self._root_widget.transitioning_out:
470            return
471
472        plus = bui.app.plus
473
474        if plus is None or plus.accounts.primary is None:
475            self._update_state(self.State.NOT_SIGNED_IN)
476            return
477
478        # Poll for relevant changes to the store or our account.
479        now = time.monotonic()
480        if not self._query_in_flight and now - self._last_query_time > 2.0:
481            self._last_query_time = now
482            self._query_in_flight = True
483            with plus.accounts.primary:
484                plus.cloud.send_message_cb(
485                    bacommon.cloud.StoreQueryMessage(),
486                    on_response=bui.WeakCall(self._on_store_query_response),
487                )
488
489        # Can't do much until we get a store state.
490        if self._last_query_response is None:
491            return
492
493        # If we've got a gold-pass, just show that. No need to offer any
494        # other purchases.
495        if self._last_query_response.gold_pass:
496            self._update_state(self.State.HAVE_GOLD_PASS)
497            return
498
499        # Ok we seem to be signed in and have store stuff we can show.
500        # Do that.
501        self._update_state(self.State.SHOWING_STORE)
502
503    def _update_state(self, state: State) -> None:
504
505        # We don't do much when state is unchanged.
506        if state is self._state:
507            # Update a few things in store mode though, such as token
508            # count.
509            if state is self.State.SHOWING_STORE:
510                self._update_store_state()
511            return
512
513        # Ok, state is changing. Start by resetting to a blank slate.
514        # self._token_count_widget = None
515        for widget in self._root_widget.get_children():
516            if widget not in self._core_widgets:
517                widget.delete()
518
519        # Build up new state.
520        if state is self.State.NOT_SIGNED_IN:
521            bui.textwidget(
522                edit=self._status_text,
523                color=(1, 0, 0),
524                text=bui.Lstr(resource='notSignedInErrorText'),
525            )
526        elif state is self.State.LOADING:
527            raise RuntimeError('Should never return to loading state.')
528        elif state is self.State.HAVE_GOLD_PASS:
529            bui.textwidget(
530                edit=self._status_text,
531                color=(0, 1, 0),
532                text=bui.Lstr(resource='tokens.youHaveGoldPassText'),
533            )
534        elif state is self.State.SHOWING_STORE:
535            assert self._last_query_response is not None
536            bui.textwidget(edit=self._status_text, text='')
537            self._build_store_for_response(self._last_query_response)
538        else:
539            # Make sure we handle all cases.
540            assert_never(state)
541
542        self._state = state
543
544    def _on_load_error(self) -> None:
545        bui.textwidget(
546            edit=self._status_text,
547            text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
548            color=(1, 0, 0),
549        )
550
551    def _on_store_query_response(
552        self, response: bacommon.cloud.StoreQueryResponse | Exception
553    ) -> None:
554        self._query_in_flight = False
555        if isinstance(response, bacommon.cloud.StoreQueryResponse):
556            self._last_query_response = response
557            # Hurry along any effects of this response.
558            self._update()
559
560    def _build_store_for_response(
561        self, response: bacommon.cloud.StoreQueryResponse
562    ) -> None:
563        # pylint: disable=too-many-locals
564        plus = bui.app.plus
565
566        uiscale = bui.app.ui_v1.uiscale
567
568        bui.textwidget(edit=self._status_text, text='')
569
570        xinset = 40
571
572        scrollwidth = self._width - 2 * (self._x_inset + xinset)
573        scrollheight = 280
574        buttonpadding = -5
575
576        yoffs = 5
577
578        # We currently don't handle the zero-button case.
579        assert self._buttondefs
580
581        sidepad = 10.0
582        total_button_width = (
583            sum(b.width + b.prepad for b in self._buttondefs)
584            + buttonpadding * (len(self._buttondefs) - 1)
585            + 2 * sidepad
586        )
587
588        h_scroll = bui.hscrollwidget(
589            parent=self._root_widget,
590            size=(scrollwidth, scrollheight),
591            position=(
592                self._x_inset + xinset,
593                self._height - 415 + self._y_offset,
594            ),
595            claims_left_right=True,
596            highlight=False,
597            border_opacity=0.3 if uiscale is bui.UIScale.SMALL else 1.0,
598        )
599        subcontainer = bui.containerwidget(
600            parent=h_scroll,
601            background=False,
602            size=(max(total_button_width, scrollwidth), scrollheight),
603        )
604        tinfobtn = bui.buttonwidget(
605            parent=self._root_widget,
606            autoselect=True,
607            label=bui.Lstr(resource='learnMoreText'),
608            position=(
609                self._width * 0.5 - 75,
610                self._height - 125 + self._y_offset,
611            ),
612            size=(180, 43),
613            scale=0.8,
614            color=(0.4, 0.25, 0.5),
615            textcolor=self._textcolor,
616            on_activate_call=partial(
617                self._on_learn_more_press, response.token_info_url
618            ),
619        )
620        if uiscale is bui.UIScale.SMALL:
621            bui.widget(
622                edit=tinfobtn,
623                left_widget=bui.get_special_widget('back_button'),
624                up_widget=bui.get_special_widget('back_button'),
625            )
626
627        bui.widget(
628            edit=tinfobtn,
629            right_widget=bui.get_special_widget('tokens_meter'),
630        )
631
632        x = sidepad
633        bwidgets: list[bui.Widget] = []
634        for i, buttondef in enumerate(self._buttondefs):
635
636            price = None if plus is None else plus.get_price(buttondef.itemid)
637
638            x += buttondef.prepad
639            tdelay = 0.3 - i / len(self._buttondefs) * 0.25
640            btn = bui.buttonwidget(
641                autoselect=True,
642                label='',
643                color=buttondef.color,
644                transition_delay=tdelay,
645                up_widget=tinfobtn,
646                parent=subcontainer,
647                size=(buttondef.width, 275),
648                position=(x, -10 + yoffs),
649                button_type='square',
650                on_activate_call=partial(
651                    self._purchase_press, buttondef.itemid
652                ),
653            )
654            bwidgets.append(btn)
655
656            if i == 0:
657                bui.widget(edit=btn, left_widget=self._back_button)
658
659            for imgdef in buttondef.imgdefs:
660                _img = bui.imagewidget(
661                    parent=subcontainer,
662                    size=imgdef.size,
663                    position=(x + imgdef.pos[0], imgdef.pos[1] + yoffs),
664                    draw_controller=btn,
665                    draw_controller_mult=imgdef.draw_controller_mult,
666                    color=imgdef.color,
667                    texture=bui.gettexture(imgdef.tex),
668                    transition_delay=tdelay,
669                    opacity=imgdef.opacity,
670                )
671            for txtdef in buttondef.txtdefs:
672                txt: bui.Lstr | str
673                if isinstance(txtdef.text, TextContents):
674                    if txtdef.text is TextContents.PRICE:
675                        tcolor = (
676                            (1, 1, 1, 0.5) if price is None else txtdef.color
677                        )
678                        txt = (
679                            bui.Lstr(resource='unavailableText')
680                            if price is None
681                            else price
682                        )
683                    else:
684                        # Make sure we cover all cases.
685                        assert_never(txtdef.text)
686                else:
687                    tcolor = txtdef.color
688                    txt = txtdef.text
689                _txt = bui.textwidget(
690                    parent=subcontainer,
691                    text=txt,
692                    position=(x + txtdef.pos[0], txtdef.pos[1] + yoffs),
693                    size=(0, 0),
694                    scale=txtdef.scale,
695                    h_align='center',
696                    v_align='center',
697                    draw_controller=btn,
698                    color=tcolor,
699                    transition_delay=tdelay,
700                    flatness=0.0,
701                    shadow=1.0,
702                    rotate=txtdef.rotate,
703                    maxwidth=txtdef.maxwidth,
704                )
705            x += buttondef.width + buttonpadding
706        bui.containerwidget(edit=subcontainer, visible_child=bwidgets[0])
707
708        _tinfotxt = bui.textwidget(
709            parent=self._root_widget,
710            position=(self._width * 0.5, self._height - 70 + self._y_offset),
711            color=self._textcolor,
712            shadow=1.0,
713            scale=0.7,
714            size=(0, 0),
715            h_align='center',
716            v_align='center',
717            text=bui.Lstr(resource='tokens.shinyNewCurrencyText'),
718        )
719        # self._token_count_widget = bui.textwidget(
720        #     parent=self._root_widget,
721        #     position=(
722        #         self._width - self._x_inset - 120.0,
723        #         self._height - 48 + self._y_offset,
724        #     ),
725        #     color=(2.0, 0.7, 0.0),
726        #     shadow=1.0,
727        #     flatness=0.0,
728        #     size=(0, 0),
729        #     h_align='left',
730        #     v_align='center',
731        #     text='',
732        # )
733        # self._token_count = response.tokens
734        # self._smooth_token_count = float(self._token_count)
735        # self._smooth_update()  # will set the text widget.
736
737        # _tlabeltxt = bui.textwidget(
738        #     parent=self._root_widget,
739        #     position=(
740        #         self._width - self._x_inset - 123.0,
741        #         self._height - 48 + self._y_offset,
742        #     ),
743        #     size=(0, 0),
744        #     h_align='right',
745        #     v_align='center',
746        #     text=bui.charstr(bui.SpecialChar.TOKEN),
747        # )
748
749    def _purchase_press(self, itemid: str) -> None:
750        plus = bui.app.plus
751
752        price = None if plus is None else plus.get_price(itemid)
753
754        if price is None:
755            if plus is not None and plus.supports_purchases():
756                # Looks like internet is down or something temporary.
757                errmsg = bui.Lstr(resource='purchaseNotAvailableText')
758            else:
759                # Looks like purchases will never work here.
760                errmsg = bui.Lstr(resource='purchaseNeverAvailableText')
761
762            bui.screenmessage(errmsg, color=(1, 0.5, 0))
763            bui.getsound('error').play()
764            return
765
766        assert plus is not None
767        plus.purchase(itemid)
768
769    def _update_store_state(self) -> None:
770        """Called to make minor updates to an already shown store."""
771        # assert self._token_count_widget is not None
772        assert self._last_query_response is not None
773
774        # self._token_count = self._last_query_response.tokens
775
776        # Kick off new smooth update if need be.
777        # assert self._smooth_token_count is not None
778        # if (
779        #     self._token_count != int(self._smooth_token_count)
780        #     and self._smooth_update_timer is None
781        # ):
782        #     self._smooth_update_timer = bui.AppTimer(
783        #         0.05, bui.WeakCall(self._smooth_update), repeat=True
784        #     )
785        #     diff = abs(float(self._token_count) - self._smooth_token_count)
786        #     self._smooth_increase_speed = (
787        #         diff / 100.0
788        #         if diff >= 5000
789        #         else (
790        #             diff / 50.0
791        #             if diff >= 1500
792        #             else diff / 30.0 if diff >= 500 else diff / 15.0
793        #         )
794        #     )
795
796    # def _smooth_update(self) -> None:
797
798    #     # Stop if the count widget disappears.
799    #     if not self._token_count_widget:
800    #         self._smooth_update_timer = None
801    #         return
802
803    #     finished = False
804
805    #     # If we're going down, do it immediately.
806    #     assert self._smooth_token_count is not None
807    #     if int(self._smooth_token_count) >= self._token_count:
808    #         self._smooth_token_count = float(self._token_count)
809    #         finished = True
810    #     else:
811    #         # We're going up; start a sound if need be.
812    #         self._smooth_token_count = min(
813    #             self._smooth_token_count + 1.0 * self._smooth_increase_speed,
814    #             self._token_count,
815    #         )
816    #         if int(self._smooth_token_count) >= self._token_count:
817    #             finished = True
818    #             self._smooth_token_count = float(self._token_count)
819    #         elif self._ticking_sound is None:
820    #             self._ticking_sound = bui.getsound('scoreIncrease')
821    #             self._ticking_sound.play()
822
823    #     bui.textwidget(
824    #         edit=self._token_count_widget,
825    #         text=str(int(self._smooth_token_count)),
826    #     )
827
828    #     # If we've reached the target, kill the timer/sound/etc.
829    #     if finished:
830    #         self._smooth_update_timer = None
831    #         if self._ticking_sound is not None:
832    #             self._ticking_sound.stop()
833    #             self._ticking_sound = None
834    #             bui.getsound('cashRegister2').play()
835
836    # def _back(self) -> None:
837
838    #     self.main_
839    # No-op if our underlying widget is dead or on its way out.
840    # if not self._root_widget or self._root_widget.transitioning_out:
841    #     return
842
843    # bui.containerwidget(
844    #     edit=self._root_widget, transition=self._transition_out
845    # )
846    # if self._restore_previous_call is not None:
847    #     self._restore_previous_call(self._root_widget)
848
849    def _on_learn_more_press(self, url: str) -> None:
850        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        # restore_previous_call: Callable[[bui.Widget], None] | None = None,
 74    ):
 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._restore_previous_call = restore_previous_call
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        # If they provided an origin-widget, scale up from that.
316        # scale_origin: tuple[float, float] | None
317        # if origin_widget is not None:
318        #     self._transition_out = 'out_scale'
319        #     scale_origin = origin_widget.get_screen_space_center()
320        #     transition = 'in_scale'
321        # else:
322        #     self._transition_out = 'out_right'
323        #     scale_origin = None
324
325        uiscale = bui.app.ui_v1.uiscale
326        self._width = 1000.0 if uiscale is bui.UIScale.SMALL else 800.0
327        self._x_inset = 25.0 if uiscale is bui.UIScale.SMALL else 0.0
328        self._height = 550 if uiscale is bui.UIScale.SMALL else 480.0
329        self._y_offset = -60 if uiscale is bui.UIScale.SMALL else 0
330
331        self._r = 'getTokensWindow'
332
333        super().__init__(
334            root_widget=bui.containerwidget(
335                size=(self._width, self._height),
336                # transition=transition,
337                # scale_origin_stack_offset=scale_origin,
338                color=(0.3, 0.23, 0.36),
339                scale=(
340                    1.5
341                    if uiscale is bui.UIScale.SMALL
342                    else 1.2 if uiscale is bui.UIScale.MEDIUM else 1.0
343                ),
344                stack_offset=(
345                    (0, -3) if uiscale is bui.UIScale.SMALL else (0, 0)
346                ),
347                # toolbar_visibility='menu_minimal',
348                toolbar_visibility=(
349                    'get_tokens'
350                    if uiscale is bui.UIScale.SMALL
351                    else 'menu_full'
352                ),
353            ),
354            transition=transition,
355            origin_widget=origin_widget,
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=(
367                    55 + self._x_inset,
368                    self._height - 80 + self._y_offset,
369                ),
370                size=(
371                    # (140, 60)
372                    # if self._restore_previous_call is None
373                    # else
374                    (60, 60)
375                ),
376                scale=1.0,
377                autoselect=True,
378                label=(
379                    # bui.Lstr(resource='doneText')
380                    # if self._restore_previous_call is None
381                    # else
382                    bui.charstr(bui.SpecialChar.BACK)
383                ),
384                button_type=(
385                    # 'regular'
386                    # if self._restore_previous_call is None
387                    # else
388                    'backSmall'
389                ),
390                on_activate_call=self.main_window_back,
391            )
392            # if uiscale is bui.UIScale.SMALL:
393            #     bui.widget(
394            #         edit=self._back_button,
395            #         up_widget=bui.get_special_widget('tokens_meter'),
396            #     )
397            bui.containerwidget(
398                edit=self._root_widget, cancel_button=self._back_button
399            )
400
401        self._title_text = bui.textwidget(
402            parent=self._root_widget,
403            position=(self._width * 0.5, self._height - 42 + self._y_offset),
404            size=(0, 0),
405            color=self._textcolor,
406            flatness=0.0,
407            shadow=1.0,
408            scale=1.2,
409            h_align='center',
410            v_align='center',
411            text=bui.Lstr(resource='tokens.getTokensText'),
412            maxwidth=260,
413        )
414
415        self._status_text = bui.textwidget(
416            parent=self._root_widget,
417            size=(0, 0),
418            position=(self._width * 0.5, self._height * 0.5),
419            h_align='center',
420            v_align='center',
421            color=(0.6, 0.6, 0.6),
422            scale=0.75,
423            text=bui.Lstr(resource='store.loadingText'),
424        )
425
426        self._core_widgets = [
427            self._back_button,
428            self._title_text,
429            self._status_text,
430        ]
431
432        # self._token_count_widget: bui.Widget | None = None
433        # self._smooth_update_timer: bui.AppTimer | None = None
434        # self._smooth_token_count: float | None = None
435        # self._token_count: int = 0
436        # self._smooth_increase_speed = 1.0
437        # self._ticking_sound: bui.Sound | None = None
438
439        # Get all textures used by our buttons preloading so hopefully
440        # they'll be in place by the time we show them.
441        for bdef in self._buttondefs:
442            for bimg in bdef.imgdefs:
443                bui.gettexture(bimg.tex)
444
445        self._state = self.State.LOADING
446
447        self._update_timer = bui.AppTimer(
448            0.789, bui.WeakCall(self._update), repeat=True
449        )
450        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:
457    @override
458    def get_main_window_state(self) -> bui.MainWindowState:
459        # Support recreating our window for back/refresh purposes.
460        cls = type(self)
461        return bui.BasicMainWindowState(
462            create_call=lambda transition, origin_widget: cls(
463                transition=transition, origin_widget=origin_widget
464            )
465        )

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:
853def show_get_tokens_prompt() -> None:
854    """Show a 'not enough tokens' prompt with an option to purchase more.
855
856    Note that the purchase option may not always be available
857    depending on the build of the game.
858    """
859    from bauiv1lib.confirm import ConfirmWindow
860
861    assert bui.app.classic is not None
862
863    # Currently always allowing token purchases.
864    if bool(True):
865        ConfirmWindow(
866            bui.Lstr(resource='tokens.notEnoughTokensText'),
867            GetTokensWindow,
868            ok_text=bui.Lstr(resource='tokens.getTokensText'),
869            width=460,
870            height=130,
871        )
872    else:
873        ConfirmWindow(
874            bui.Lstr(resource='tokens.notEnoughTokensText'),
875            cancel_button=False,
876            width=460,
877            height=130,
878        )

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.